diff --git a/.buildkite/Dockerfile b/.buildkite/Dockerfile new file mode 100644 index 0000000000..2ace3c0f50 --- /dev/null +++ b/.buildkite/Dockerfile @@ -0,0 +1,170 @@ +ARG LLVM_VERSION="18" +ARG REPORTED_LLVM_VERSION="18.1.8" +ARG OLD_BUN_VERSION="1.1.38" +ARG DEFAULT_CFLAGS="-mno-omit-leaf-frame-pointer -fno-omit-frame-pointer -ffunction-sections -fdata-sections -faddrsig -fno-unwind-tables -fno-asynchronous-unwind-tables" +ARG DEFAULT_CXXFLAGS="-flto=full -fwhole-program-vtables -fforce-emit-vtables" +ARG BUILDKITE_AGENT_TAGS="queue=linux,os=linux,arch=${TARGETARCH}" + +FROM --platform=$BUILDPLATFORM ubuntu:20.04 as base-arm64 +FROM --platform=$BUILDPLATFORM ubuntu:18.04 as base-amd64 +FROM base-$TARGETARCH as base + +ARG LLVM_VERSION +ARG OLD_BUN_VERSION +ARG TARGETARCH +ARG DEFAULT_CXXFLAGS +ARG DEFAULT_CFLAGS +ARG REPORTED_LLVM_VERSION + +ENV DEBIAN_FRONTEND=noninteractive \ + CI=true \ + DOCKER=true + +RUN echo "Acquire::Queue-Mode \"host\";" > /etc/apt/apt.conf.d/99-apt-queue-mode.conf \ + && echo "Acquire::Timeout \"120\";" >> /etc/apt/apt.conf.d/99-apt-timeout.conf \ + && echo "Acquire::Retries \"3\";" >> /etc/apt/apt.conf.d/99-apt-retries.conf \ + && echo "APT::Install-Recommends \"false\";" >> /etc/apt/apt.conf.d/99-apt-install-recommends.conf \ + && echo "APT::Install-Suggests \"false\";" >> /etc/apt/apt.conf.d/99-apt-install-suggests.conf + +RUN apt-get update && apt-get install -y --no-install-recommends \ + wget curl git python3 python3-pip ninja-build \ + software-properties-common apt-transport-https \ + ca-certificates gnupg lsb-release unzip \ + libxml2-dev ruby ruby-dev bison gawk perl make golang \ + && add-apt-repository ppa:ubuntu-toolchain-r/test \ + && apt-get update \ + && apt-get install -y gcc-13 g++-13 libgcc-13-dev libstdc++-13-dev \ + libasan6 libubsan1 libatomic1 libtsan0 liblsan0 \ + libgfortran5 libc6-dev \ + && wget https://apt.llvm.org/llvm.sh \ + && chmod +x llvm.sh \ + && ./llvm.sh ${LLVM_VERSION} all \ + && rm llvm.sh + + +RUN --mount=type=tmpfs,target=/tmp \ + cmake_version="3.30.5" && \ + if [ "$TARGETARCH" = "arm64" ]; then \ + cmake_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-linux-aarch64.sh"; \ + else \ + cmake_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-linux-x86_64.sh"; \ + fi && \ + wget -O /tmp/cmake.sh "$cmake_url" && \ + sh /tmp/cmake.sh --skip-license --prefix=/usr + +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 130 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-13 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-13 \ + --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-13 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-13 + +RUN echo "ARCH_PATH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64-linux-gnu" || echo "x86_64-linux-gnu")" >> /etc/environment \ + && echo "BUN_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x64")" >> /etc/environment + +ENV LD_LIBRARY_PATH=/usr/lib/gcc/${ARCH_PATH}/13:/usr/lib/${ARCH_PATH} \ + LIBRARY_PATH=/usr/lib/gcc/${ARCH_PATH}/13:/usr/lib/${ARCH_PATH} \ + CPLUS_INCLUDE_PATH=/usr/include/c++/13:/usr/include/${ARCH_PATH}/c++/13 \ + C_INCLUDE_PATH=/usr/lib/gcc/${ARCH_PATH}/13/include \ + CFLAGS=${DEFAULT_CFLAGS} \ + CXXFLAGS="${DEFAULT_CFLAGS} ${DEFAULT_CXXFLAGS}" + +RUN if [ "$TARGETARCH" = "arm64" ]; then \ + export ARCH_PATH="aarch64-linux-gnu"; \ + else \ + export ARCH_PATH="x86_64-linux-gnu"; \ + fi \ + && mkdir -p /usr/lib/gcc/${ARCH_PATH}/13 \ + && ln -sf /usr/lib/${ARCH_PATH}/libstdc++.so.6 /usr/lib/gcc/${ARCH_PATH}/13/ \ + && echo "/usr/lib/gcc/${ARCH_PATH}/13" > /etc/ld.so.conf.d/gcc-13.conf \ + && echo "/usr/lib/${ARCH_PATH}" >> /etc/ld.so.conf.d/gcc-13.conf \ + && ldconfig + +RUN for f in /usr/lib/llvm-${LLVM_VERSION}/bin/*; do ln -sf "$f" /usr/bin; done \ + && ln -sf /usr/bin/clang-${LLVM_VERSION} /usr/bin/clang \ + && ln -sf /usr/bin/clang++-${LLVM_VERSION} /usr/bin/clang++ \ + && ln -sf /usr/bin/lld-${LLVM_VERSION} /usr/bin/lld \ + && ln -sf /usr/bin/lldb-${LLVM_VERSION} /usr/bin/lldb \ + && ln -sf /usr/bin/clangd-${LLVM_VERSION} /usr/bin/clangd \ + && ln -sf /usr/bin/llvm-ar-${LLVM_VERSION} /usr/bin/llvm-ar \ + && ln -sf /usr/bin/ld.lld /usr/bin/ld \ + && ln -sf /usr/bin/clang /usr/bin/cc \ + && ln -sf /usr/bin/clang++ /usr/bin/c++ + +ENV CC="clang" \ + CXX="clang++" \ + AR="llvm-ar-${LLVM_VERSION}" \ + RANLIB="llvm-ranlib-${LLVM_VERSION}" \ + LD="lld-${LLVM_VERSION}" + +RUN --mount=type=tmpfs,target=/tmp \ + bash -c '\ + set -euxo pipefail && \ + source /etc/environment && \ + echo "Downloading bun-v${OLD_BUN_VERSION}/bun-linux-$BUN_ARCH.zip from https://pub-5e11e972747a44bf9aaf9394f185a982.r2.dev/releases/bun-v${OLD_BUN_VERSION}/bun-linux-$BUN_ARCH.zip" && \ + curl -fsSL https://pub-5e11e972747a44bf9aaf9394f185a982.r2.dev/releases/bun-v${OLD_BUN_VERSION}/bun-linux-$BUN_ARCH.zip -o /tmp/bun.zip && \ + unzip /tmp/bun.zip -d /tmp/bun && \ + mv /tmp/bun/*/bun /usr/bin/bun && \ + chmod +x /usr/bin/bun' + +ENV LLVM_VERSION=${REPORTED_LLVM_VERSION} + +WORKDIR /workspace + + +FROM --platform=$BUILDPLATFORM base as buildkite +ARG BUILDKITE_AGENT_TAGS + + +# Install Rust nightly +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && export PATH=$HOME/.cargo/bin:$PATH \ + && rustup install nightly \ + && rustup default nightly + + +RUN ARCH=$(if [ "$TARGETARCH" = "arm64" ]; then echo "arm64"; else echo "amd64"; fi) && \ + echo "Downloading buildkite" && \ + curl -fsSL "https://github.com/buildkite/agent/releases/download/v3.87.0/buildkite-agent-linux-${ARCH}-3.87.0.tar.gz" -o /tmp/buildkite-agent.tar.gz && \ + mkdir -p /tmp/buildkite-agent && \ + tar -xzf /tmp/buildkite-agent.tar.gz -C /tmp/buildkite-agent && \ + mv /tmp/buildkite-agent/buildkite-agent /usr/bin/buildkite-agent + +RUN mkdir -p /var/cache/buildkite-agent /var/log/buildkite-agent /var/run/buildkite-agent /etc/buildkite-agent /var/lib/buildkite-agent/cache/bun + +COPY ../*/agent.mjs /var/bun/scripts/ + +ENV BUN_INSTALL_CACHE=/var/lib/buildkite-agent/cache/bun +ENV BUILDKITE_AGENT_TAGS=${BUILDKITE_AGENT_TAGS} + + +WORKDIR /var/bun/scripts + +ENV PATH=/root/.cargo/bin:$PATH + + +CMD ["bun", "/var/bun/scripts/agent.mjs", "start"] + +FROM --platform=$BUILDPLATFORM base as bun-build-linux-local + +ARG LLVM_VERSION +WORKDIR /workspace/bun + +COPY . /workspace/bun + + +# Install Rust nightly +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && export PATH=$HOME/.cargo/bin:$PATH \ + && rustup install nightly \ + && rustup default nightly + +ENV PATH=/root/.cargo/bin:$PATH + +ENV LLVM_VERSION=${REPORTED_LLVM_VERSION} + + +RUN --mount=type=tmpfs,target=/workspace/bun/build \ + ls -la \ + && bun run build:release \ + && mkdir -p /target \ + && cp -r /workspace/bun/build/release/bun /target/bun \ No newline at end of file diff --git a/.buildkite/Dockerfile-bootstrap.sh b/.buildkite/Dockerfile-bootstrap.sh new file mode 100644 index 0000000000..b8ccba7d17 --- /dev/null +++ b/.buildkite/Dockerfile-bootstrap.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "error: must run as root" + exit 1 +fi + +# Check OS compatibility +if ! command -v dnf &> /dev/null; then + echo "error: this script requires dnf (RHEL/Fedora/CentOS)" + exit 1 +fi + +# Ensure /tmp/agent.mjs, /tmp/Dockerfile are present +if [ ! -f /tmp/agent.mjs ] || [ ! -f /tmp/Dockerfile ]; then + # Print each missing file + if [ ! -f /tmp/agent.mjs ]; then + echo "error: /tmp/agent.mjs is missing" + fi + if [ ! -f /tmp/Dockerfile ]; then + echo "error: /tmp/Dockerfile is missing" + fi + exit 1 +fi + +# Install Docker +dnf update -y +dnf install -y docker + +systemctl enable docker +systemctl start docker || { + echo "error: failed to start Docker" + exit 1 +} + +# Create builder +docker buildx create --name builder --driver docker-container --bootstrap --use || { + echo "error: failed to create Docker buildx builder" + exit 1 +} + +# Set up Docker to start on boot +cat << 'EOF' > /etc/systemd/system/buildkite-agent.service +[Unit] +Description=Buildkite Docker Container +After=docker.service network-online.target +Requires=docker.service network-online.target + +[Service] +TimeoutStartSec=0 +Restart=always +RestartSec=5 +ExecStartPre=-/usr/bin/docker stop buildkite +ExecStartPre=-/usr/bin/docker rm buildkite +ExecStart=/usr/bin/docker run \ + --name buildkite \ + --restart=unless-stopped \ + --network host \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /tmp:/tmp \ + buildkite:latest + +[Install] +WantedBy=multi-user.target +EOF + +echo "Building Buildkite image" + +# Clean up any previous build artifacts +rm -rf /tmp/fakebun +mkdir -p /tmp/fakebun/scripts /tmp/fakebun/.buildkite + +# Copy required files +cp /tmp/agent.mjs /tmp/fakebun/scripts/ || { + echo "error: failed to copy agent.mjs" + exit 1 +} +cp /tmp/Dockerfile /tmp/fakebun/.buildkite/Dockerfile || { + echo "error: failed to copy Dockerfile" + exit 1 +} + +cd /tmp/fakebun || { + echo "error: failed to change directory" + exit 1 +} + +# Build the Buildkite image +docker buildx build \ + --platform $(uname -m | sed 's/aarch64/linux\/arm64/;s/x86_64/linux\/amd64/') \ + --tag buildkite:latest \ + --target buildkite \ + -f .buildkite/Dockerfile \ + --load \ + . || { + echo "error: Docker build failed" + exit 1 +} + +# Create container to ensure image is cached in AMI +docker container create \ + --name buildkite \ + --restart=unless-stopped \ + buildkite:latest || { + echo "error: failed to create buildkite container" + exit 1 +} + +# Reload systemd to pick up new service +systemctl daemon-reload + +# Enable the service, but don't start it yet +systemctl enable buildkite-agent || { + echo "error: failed to enable buildkite-agent service" + exit 1 +} + +echo "Bootstrap complete" +echo "To start the Buildkite agent, run: " +echo " systemctl start buildkite-agent" \ No newline at end of file diff --git a/.buildkite/bootstrap.yml b/.buildkite/bootstrap.yml index b0b84616b3..5a75106d5e 100644 --- a/.buildkite/bootstrap.yml +++ b/.buildkite/bootstrap.yml @@ -13,19 +13,4 @@ steps: agents: queue: "build-darwin" command: - - ".buildkite/scripts/prepare-build.sh" - - - if: "build.branch == 'main' && !build.pull_request.repository.fork" - label: ":github:" - agents: - queue: "test-darwin" - depends_on: - - "darwin-aarch64-build-bun" - - "darwin-x64-build-bun" - - "linux-aarch64-build-bun" - - "linux-x64-build-bun" - - "linux-x64-baseline-build-bun" - - "windows-x64-build-bun" - - "windows-x64-baseline-build-bun" - command: - - ".buildkite/scripts/upload-release.sh" + - "node .buildkite/ci.mjs" diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 70d4e44c1d..3fb1e643f3 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -5,838 +5,1151 @@ * @link https://buildkite.com/docs/pipelines/defining-steps */ -import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { getBootstrapVersion, + getBuildkiteEmoji, + getBuildMetadata, getBuildNumber, getCanaryRevision, - getChangedFiles, - getCommit, getCommitMessage, + getEmoji, getEnv, getLastSuccessfulBuild, - getMainBranch, - getTargetBranch, isBuildkite, + isBuildManual, isFork, isMainBranch, isMergeQueue, - printEnvironment, + parseBoolean, spawnSafe, + startGroup, toYaml, uploadArtifact, + writeFile, } from "../scripts/utils.mjs"; /** - * @typedef PipelineOptions - * @property {string} [buildId] - * @property {boolean} [buildImages] - * @property {boolean} [publishImages] - * @property {boolean} [skipTests] + * @typedef {"linux" | "darwin" | "windows"} Os + * @typedef {"aarch64" | "x64"} Arch + * @typedef {"musl"} Abi + * @typedef {"debian" | "ubuntu" | "alpine" | "amazonlinux"} Distro + * @typedef {"latest" | "previous" | "oldest" | "eol"} Tier + * @typedef {"release" | "assert" | "debug"} Profile */ /** - * @param {PipelineOptions} options + * @typedef Target + * @property {Os} os + * @property {Arch} arch + * @property {Abi} [abi] + * @property {boolean} [baseline] + * @property {Profile} [profile] */ -function getPipeline(options) { - const { buildId, buildImages, publishImages, skipTests } = options; - /** - * Helpers - */ +/** + * @param {Target} target + * @returns {string} + */ +function getTargetKey(target) { + const { os, arch, abi, baseline, profile } = target; + let key = `${os}-${arch}`; + if (abi) { + key += `-${abi}`; + } + if (baseline) { + key += "-baseline"; + } + if (profile && profile !== "release") { + key += `-${profile}`; + } + return key; +} - /** - * @param {string} text - * @returns {string} - * @link https://github.com/buildkite/emojis#emoji-reference - */ - const getEmoji = string => { - if (string === "amazonlinux") { - return ":aws:"; - } - return `:${string}:`; +/** + * @param {Target} target + * @returns {string} + */ +function getTargetLabel(target) { + const { os, arch, abi, baseline, profile } = target; + let label = `${getBuildkiteEmoji(os)} ${arch}`; + if (abi) { + label += `-${abi}`; + } + if (baseline) { + label += "-baseline"; + } + if (profile && profile !== "release") { + label += `-${profile}`; + } + return label; +} + +/** + * @typedef Platform + * @property {Os} os + * @property {Arch} arch + * @property {Abi} [abi] + * @property {boolean} [baseline] + * @property {Profile} [profile] + * @property {Distro} [distro] + * @property {string} release + * @property {Tier} [tier] + * @property {string[]} [features] + */ + +/** + * @type {Platform[]} + */ +const buildPlatforms = [ + { os: "darwin", arch: "aarch64", release: "14" }, + { os: "darwin", arch: "x64", release: "14" }, + { os: "linux", arch: "aarch64", distro: "amazonlinux", release: "2023", features: ["docker"] }, + { os: "linux", arch: "x64", distro: "amazonlinux", release: "2023", features: ["docker"] }, + { os: "linux", arch: "x64", baseline: true, distro: "amazonlinux", release: "2023", features: ["docker"] }, + { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.20" }, + { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.20" }, + { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.20" }, + { os: "windows", arch: "x64", release: "2019" }, + { os: "windows", arch: "x64", baseline: true, release: "2019" }, +]; + +/** + * @type {Platform[]} + */ +const testPlatforms = [ + { os: "darwin", arch: "aarch64", release: "14", tier: "latest" }, + { os: "darwin", arch: "aarch64", release: "13", tier: "previous" }, + { os: "darwin", arch: "x64", release: "14", tier: "latest" }, + { os: "darwin", arch: "x64", release: "13", tier: "previous" }, + { os: "linux", arch: "aarch64", distro: "debian", release: "12", tier: "latest" }, + { os: "linux", arch: "x64", distro: "debian", release: "12", tier: "latest" }, + { os: "linux", arch: "x64", baseline: true, distro: "debian", release: "12", tier: "latest" }, + { os: "linux", arch: "aarch64", distro: "ubuntu", release: "24.04", tier: "latest" }, + { os: "linux", arch: "aarch64", distro: "ubuntu", release: "22.04", tier: "previous" }, + { os: "linux", arch: "aarch64", distro: "ubuntu", release: "20.04", tier: "oldest" }, + { os: "linux", arch: "x64", distro: "ubuntu", release: "24.04", tier: "latest" }, + { os: "linux", arch: "x64", distro: "ubuntu", release: "22.04", tier: "previous" }, + { os: "linux", arch: "x64", distro: "ubuntu", release: "20.04", tier: "oldest" }, + { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "24.04", tier: "latest" }, + { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "22.04", tier: "previous" }, + { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "20.04", tier: "oldest" }, + { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.20", tier: "latest" }, + { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.20", tier: "latest" }, + { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.20", tier: "latest" }, + { os: "windows", arch: "x64", release: "2019", tier: "oldest" }, + { os: "windows", arch: "x64", release: "2019", baseline: true, tier: "oldest" }, +]; + +/** + * @param {Platform} platform + * @returns {string} + */ +function getPlatformKey(platform) { + const { distro, release } = platform; + const target = getTargetKey(platform); + const version = release.replace(/\./g, ""); + if (distro) { + return `${target}-${distro}-${version}`; + } + return `${target}-${version}`; +} + +/** + * @param {Platform} platform + * @returns {string} + */ +function getPlatformLabel(platform) { + const { os, arch, baseline, profile, distro, release } = platform; + let label = `${getBuildkiteEmoji(distro || os)} ${release} ${arch}`; + if (baseline) { + label += "-baseline"; + } + if (profile && profile !== "release") { + label += `-${profile}`; + } + return label; +} + +/** + * @param {Platform} platform + * @returns {string} + */ +function getImageKey(platform) { + const { os, arch, distro, release, features, abi } = platform; + const version = release.replace(/\./g, ""); + let key = `${os}-${arch}-${version}`; + if (distro) { + key += `-${distro}`; + } + if (features?.length) { + key += `-with-${features.join("-")}`; + } + + if (abi) { + key += `-${abi}`; + } + + return key; +} + +/** + * @param {Platform} platform + * @returns {string} + */ +function getImageLabel(platform) { + const { os, arch, distro, release } = platform; + return `${getBuildkiteEmoji(distro || os)} ${release} ${arch}`; +} + +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {string} + */ +function getImageName(platform, options) { + const { os } = platform; + const { buildImages, publishImages } = options; + + const name = getImageKey(platform); + + if (buildImages && !publishImages) { + return `${name}-build-${getBuildNumber()}`; + } + + return `${name}-v${getBootstrapVersion(os)}`; +} + +/** + * @param {number} [limit] + * @link https://buildkite.com/docs/pipelines/command-step#retry-attributes + */ +function getRetry(limit = 0) { + return { + manual: { + permit_on_passed: true, + }, + automatic: [ + { exit_status: 1, limit }, + { exit_status: -1, limit: 1 }, + { exit_status: 255, limit: 1 }, + { signal_reason: "cancel", limit: 1 }, + { signal_reason: "agent_stop", limit: 1 }, + ], }; +} - /** - * @typedef {"linux" | "darwin" | "windows"} Os - * @typedef {"aarch64" | "x64"} Arch - * @typedef {"musl"} Abi - */ +/** + * @returns {number} + * @link https://buildkite.com/docs/pipelines/managing-priorities + */ +function getPriority() { + if (isFork()) { + return -1; + } + if (isMainBranch()) { + return 2; + } + if (isMergeQueue()) { + return 1; + } + return 0; +} - /** - * @typedef Target - * @property {Os} os - * @property {Arch} arch - * @property {Abi} [abi] - * @property {boolean} [baseline] - */ +/** + * Agents + */ - /** - * @param {Target} target - * @returns {string} - */ - const getTargetKey = target => { - const { os, arch, abi, baseline } = target; - let key = `${os}-${arch}`; - if (abi) { - key += `-${abi}`; - } - if (baseline) { - key += "-baseline"; - } - return key; +/** + * @typedef {Object} Ec2Options + * @property {string} instanceType + * @property {number} cpuCount + * @property {number} threadsPerCore + * @property {boolean} dryRun + */ + +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @param {Ec2Options} ec2Options + * @returns {Agent} + */ +function getEc2Agent(platform, options, ec2Options) { + const { os, arch, abi, distro, release } = platform; + const { instanceType, cpuCount, threadsPerCore } = ec2Options; + return { + os, + arch, + abi, + distro, + release, + robobun: true, + robobun2: true, + "image-name": getImageName(platform, options), + "instance-type": instanceType, + "cpu-count": cpuCount, + "threads-per-core": threadsPerCore, + "preemptible": false, }; +} - /** - * @param {Target} target - * @returns {string} - */ - const getTargetLabel = target => { - const { os, arch, abi, baseline } = target; - let label = `${getEmoji(os)} ${arch}`; - if (abi) { - label += `-${abi}`; - } - if (baseline) { - label += "-baseline"; - } - return label; - }; +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {string} + */ +function getCppAgent(platform, options) { + const { os, arch, distro } = platform; - /** - * @typedef Platform - * @property {Os} os - * @property {Arch} arch - * @property {Abi} [abi] - * @property {boolean} [baseline] - * @property {string} [distro] - * @property {string} release - */ - - /** - * @param {Platform} platform - * @returns {string} - */ - const getPlatformKey = platform => { - const { os, arch, abi, baseline, distro, release } = platform; - const target = getTargetKey({ os, arch, abi, baseline }); - if (distro) { - return `${target}-${distro}-${release.replace(/\./g, "")}`; - } - return `${target}-${release.replace(/\./g, "")}`; - }; - - /** - * @param {Platform} platform - * @returns {string} - */ - const getPlatformLabel = platform => { - const { os, arch, baseline, distro, release } = platform; - let label = `${getEmoji(distro || os)} ${release} ${arch}`; - if (baseline) { - label += "-baseline"; - } - return label; - }; - - /** - * @param {Platform} platform - * @returns {string} - */ - const getImageKey = platform => { - const { os, arch, distro, release } = platform; - if (distro) { - return `${os}-${arch}-${distro}-${release.replace(/\./g, "")}`; - } - return `${os}-${arch}-${release.replace(/\./g, "")}`; - }; - - /** - * @param {Platform} platform - * @returns {string} - */ - const getImageLabel = platform => { - const { os, arch, distro, release } = platform; - return `${getEmoji(distro || os)} ${release} ${arch}`; - }; - - /** - * @param {number} [limit] - * @link https://buildkite.com/docs/pipelines/command-step#retry-attributes - */ - const getRetry = (limit = 0) => { - return { - automatic: [ - { exit_status: 1, limit }, - { exit_status: -1, limit: 3 }, - { exit_status: 255, limit: 3 }, - { signal_reason: "agent_stop", limit: 3 }, - ], - }; - }; - - /** - * @returns {number} - * @link https://buildkite.com/docs/pipelines/managing-priorities - */ - const getPriority = () => { - if (isFork()) { - return -1; - } - if (isMainBranch()) { - return 2; - } - if (isMergeQueue()) { - return 1; - } - return 0; - }; - - /** - * @param {Target} target - * @returns {Record} - */ - const getBuildEnv = target => { - const { baseline, abi } = target; - return { - ENABLE_BASELINE: baseline ? "ON" : "OFF", - ABI: abi === "musl" ? "musl" : undefined, - }; - }; - - /** - * @param {Target} target - * @returns {string} - */ - const getBuildToolchain = target => { - const { os, arch, abi, baseline } = target; - let key = `${os}-${arch}`; - if (abi) { - key += `-${abi}`; - } - if (baseline) { - key += "-baseline"; - } - return key; - }; - - /** - * Agents - */ - - /** - * @typedef {Record} Agent - */ - - /** - * @param {Platform} platform - * @returns {boolean} - */ - const isUsingNewAgent = platform => { - const { os } = platform; - if (os === "linux") { - return true; - } - return false; - }; - - /** - * @param {"v1" | "v2"} version - * @param {Platform} platform - * @param {string} [instanceType] - * @returns {Agent} - */ - const getEmphemeralAgent = (version, platform, instanceType) => { - const { os, arch, abi, distro, release } = platform; - if (version === "v1") { - return { - robobun: true, - os, - arch, - distro, - release, - }; - } - let image; - if (distro) { - image = `${os}-${arch}-${distro}-${release}`; - } else { - image = `${os}-${arch}-${release}`; - } - if (buildImages && !publishImages) { - image += `-build-${getBuildNumber()}`; - } else { - image += `-v${getBootstrapVersion()}`; - } - return { - robobun: true, - robobun2: true, - os, - arch, - abi, - distro, - release, - "image-name": image, - "instance-type": instanceType, - }; - }; - - /** - * @param {Target} target - * @returns {Agent} - */ - const getBuildAgent = target => { - const { os, arch, abi } = target; - if (isUsingNewAgent(target)) { - const instanceType = arch === "aarch64" ? "c8g.8xlarge" : "c7i.8xlarge"; - return getEmphemeralAgent("v2", target, instanceType); - } + if (os === "darwin") { return { queue: `build-${os}`, os, arch, - abi, }; - }; + } - /** - * @param {Target} target - * @returns {Agent} - */ - const getZigAgent = platform => { - const { arch } = platform; - const instanceType = arch === "aarch64" ? "c8g.2xlarge" : "c7i.2xlarge"; + return getEc2Agent(platform, options, { + instanceType: arch === "aarch64" ? "c8g.16xlarge" : "c7i.16xlarge", + cpuCount: 32, + threadsPerCore: 1, + }); +} + +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Agent} + */ +function getZigAgent(platform, options) { + const { arch } = platform; + return { + queue: "build-zig", + }; +} + +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Agent} + */ +function getTestAgent(platform, options) { + const { os, arch } = platform; + + if (os === "darwin") { return { - robobun: true, - robobun2: true, - os: "linux", + queue: `test-${os}`, + os, arch, - distro: "debian", - release: "11", - "image-name": `linux-${arch}-debian-11-v5`, // v5 is not on main yet - "instance-type": instanceType, }; - // TODO: Temporarily disable due to configuration - // return { - // queue: "build-zig", - // }; + } + + // TODO: `dev-server-ssr-110.test.ts` and `next-build.test.ts` run out of memory at 8GB of memory, so use 16GB instead. + if (os === "windows") { + return getEc2Agent(platform, options, { + instanceType: "c7i.2xlarge", + cpuCount: 2, + threadsPerCore: 1, + }); + } + + if (arch === "aarch64") { + return getEc2Agent(platform, options, { + instanceType: "c8g.xlarge", + cpuCount: 2, + threadsPerCore: 1, + }); + } + + return getEc2Agent(platform, options, { + instanceType: "c7i.xlarge", + cpuCount: 2, + threadsPerCore: 1, + }); +} + +/** + * Steps + */ + +/** + * @param {Target} target + * @param {PipelineOptions} options + * @returns {Record} + */ +function getBuildEnv(target, options) { + const { profile, baseline, abi } = target; + const release = !profile || profile === "release"; + const { canary } = options; + const revision = typeof canary === "number" ? canary : 1; + + return { + CMAKE_BUILD_TYPE: release ? "Release" : profile === "debug" ? "Debug" : "RelWithDebInfo", + ENABLE_BASELINE: baseline ? "ON" : "OFF", + ENABLE_CANARY: revision > 0 ? "ON" : "OFF", + CANARY_REVISION: revision, + ENABLE_ASSERTIONS: release ? "OFF" : "ON", + ENABLE_LOGS: release ? "OFF" : "ON", + ABI: abi === "musl" ? "musl" : undefined, }; +} - /** - * @param {Platform} platform - * @returns {Agent} - */ - const getTestAgent = platform => { - const { os, arch, release } = platform; - if (isUsingNewAgent(platform)) { - const instanceType = arch === "aarch64" ? "t4g.large" : "t3.large"; - return getEmphemeralAgent("v2", platform, instanceType); - } - if (os === "darwin") { - return { - os, - arch, - release, - queue: "test-darwin", - }; - } - return getEmphemeralAgent("v1", platform); +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getBuildVendorStep(platform, options) { + return { + key: `${getTargetKey(platform)}-build-vendor`, + label: `${getTargetLabel(platform)} - build-vendor`, + agents: getCppAgent(platform, options), + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + env: getBuildEnv(platform, options), + command: "bun run build:ci --target dependencies", }; +} - /** - * Steps - */ - - /** - * @typedef Step - * @property {string} key - * @property {string} [label] - * @property {Record} [agents] - * @property {Record} [env] - * @property {string} command - * @property {string[]} [depends_on] - * @property {Record} [retry] - * @property {boolean} [cancel_on_build_failing] - * @property {boolean} [soft_fail] - * @property {number} [parallelism] - * @property {number} [concurrency] - * @property {string} [concurrency_group] - * @property {number} [priority] - * @property {number} [timeout_in_minutes] - * @link https://buildkite.com/docs/pipelines/command-step - */ - - /** - * @param {Platform} platform - * @param {string} [step] - * @returns {string[]} - */ - const getDependsOn = (platform, step) => { - if (imagePlatforms.has(getImageKey(platform))) { - const key = `${getImageKey(platform)}-build-image`; - if (key !== step) { - return [key]; - } - } - return []; +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getBuildCppStep(platform, options) { + return { + key: `${getTargetKey(platform)}-build-cpp`, + label: `${getTargetLabel(platform)} - build-cpp`, + agents: getCppAgent(platform, options), + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + env: { + BUN_CPP_ONLY: "ON", + ...getBuildEnv(platform, options), + }, + command: "bun run build:ci --target bun", }; +} - /** - * @param {Platform} platform - * @returns {Step} - */ - const getBuildImageStep = platform => { - const { os, arch, distro, release } = platform; - const action = publishImages ? "publish-image" : "create-image"; - return { - key: `${getImageKey(platform)}-build-image`, - label: `${getImageLabel(platform)} - build-image`, - agents: { - queue: "build-image", - }, - env: { - DEBUG: "1", - }, - retry: getRetry(), - command: `node ./scripts/machine.mjs ${action} --ci --cloud=aws --os=${os} --arch=${arch} --distro=${distro} --distro-version=${release}`, - }; +/** + * @param {Target} target + * @returns {string} + */ +function getBuildToolchain(target) { + const { os, arch, abi, baseline } = target; + let key = `${os}-${arch}`; + if (abi) { + key += `-${abi}`; + } + if (baseline) { + key += "-baseline"; + } + return key; +} + +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getBuildZigStep(platform, options) { + const toolchain = getBuildToolchain(platform); + return { + key: `${getTargetKey(platform)}-build-zig`, + label: `${getTargetLabel(platform)} - build-zig`, + agents: getZigAgent(platform, options), + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + env: getBuildEnv(platform, options), + command: `bun run build:ci --target bun-zig --toolchain ${toolchain}`, }; +} - /** - * @param {Platform} platform - * @returns {Step} - */ - const getBuildVendorStep = platform => { - return { - key: `${getTargetKey(platform)}-build-vendor`, - label: `${getTargetLabel(platform)} - build-vendor`, - depends_on: getDependsOn(platform), - agents: getBuildAgent(platform), - retry: getRetry(), - cancel_on_build_failing: isMergeQueue(), - env: getBuildEnv(platform), - command: "bun run build:ci --target dependencies", - }; +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getLinkBunStep(platform, options) { + return { + key: `${getTargetKey(platform)}-build-bun`, + label: `${getTargetLabel(platform)} - build-bun`, + depends_on: [ + `${getTargetKey(platform)}-build-vendor`, + `${getTargetKey(platform)}-build-cpp`, + `${getTargetKey(platform)}-build-zig`, + ], + agents: getCppAgent(platform, options), + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + env: { + BUN_LINK_ONLY: "ON", + ...getBuildEnv(platform, options), + }, + command: "bun run build:ci --target bun", }; +} - /** - * @param {Platform} platform - * @returns {Step} - */ - const getBuildCppStep = platform => { - return { - key: `${getTargetKey(platform)}-build-cpp`, - label: `${getTargetLabel(platform)} - build-cpp`, - depends_on: getDependsOn(platform), - agents: getBuildAgent(platform), - retry: getRetry(), - cancel_on_build_failing: isMergeQueue(), - env: { - BUN_CPP_ONLY: "ON", - ...getBuildEnv(platform), - }, - command: "bun run build:ci --target bun", - }; +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getBuildBunStep(platform, options) { + return { + key: `${getTargetKey(platform)}-build-bun`, + label: `${getTargetLabel(platform)} - build-bun`, + agents: getCppAgent(platform, options), + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + env: getBuildEnv(platform, options), + command: "bun run build:ci", }; +} - /** - * @param {Platform} platform - * @returns {Step} - */ - const getBuildZigStep = platform => { - const toolchain = getBuildToolchain(platform); - return { - key: `${getTargetKey(platform)}-build-zig`, - label: `${getTargetLabel(platform)} - build-zig`, - depends_on: getDependsOn(platform), - agents: getZigAgent(platform), - retry: getRetry(), - cancel_on_build_failing: isMergeQueue(), - env: getBuildEnv(platform), - command: `bun run build:ci --target bun-zig --toolchain ${toolchain}`, - }; +/** + * @typedef {Object} TestOptions + * @property {string} [buildId] + * @property {boolean} [unifiedTests] + * @property {string[]} [testFiles] + * @property {boolean} [dryRun] + */ + +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @param {TestOptions} [testOptions] + * @returns {Step} + */ +function getTestBunStep(platform, options, testOptions = {}) { + const { os } = platform; + const { buildId, unifiedTests, testFiles } = testOptions; + + const args = [`--step=${getTargetKey(platform)}-build-bun`]; + if (buildId) { + args.push(`--build-id=${buildId}`); + } + if (testFiles) { + args.push(...testFiles.map(testFile => `--include=${testFile}`)); + } + + const depends = []; + if (!buildId) { + depends.push(`${getTargetKey(platform)}-build-bun`); + } + + return { + key: `${getPlatformKey(platform)}-test-bun`, + label: `${getPlatformLabel(platform)} - test-bun`, + depends_on: depends, + agents: getTestAgent(platform, options), + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + parallelism: unifiedTests ? undefined : os === "darwin" ? 2 : 10, + command: + os === "windows" + ? `node .\\scripts\\runner.node.mjs ${args.join(" ")}` + : `./scripts/runner.node.mjs ${args.join(" ")}`, }; +} - /** - * @param {Platform} platform - * @returns {Step} - */ - const getBuildBunStep = platform => { - return { - key: `${getTargetKey(platform)}-build-bun`, - label: `${getTargetLabel(platform)} - build-bun`, - depends_on: [ - `${getTargetKey(platform)}-build-vendor`, - `${getTargetKey(platform)}-build-cpp`, - `${getTargetKey(platform)}-build-zig`, - ], - agents: getBuildAgent(platform), - retry: getRetry(), - cancel_on_build_failing: isMergeQueue(), - env: { - BUN_LINK_ONLY: "ON", - ...getBuildEnv(platform), - }, - command: "bun run build:ci --target bun", - }; +/** + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getBuildImageStep(platform, options) { + const { os, arch, distro, release, features } = platform; + const { publishImages } = options; + const action = publishImages ? "publish-image" : "create-image"; + + const command = [ + "node", + "./scripts/machine.mjs", + action, + `--os=${os}`, + `--arch=${arch}`, + distro && `--distro=${distro}`, + `--release=${release}`, + "--cloud=aws", + "--ci", + "--authorized-org=oven-sh", + ]; + for (const feature of features || []) { + command.push(`--feature=${feature}`); + } + + return { + key: `${getImageKey(platform)}-build-image`, + label: `${getImageLabel(platform)} - build-image`, + agents: { + queue: "build-image", + }, + env: { + DEBUG: "1", + }, + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + command: command.filter(Boolean).join(" "), + timeout_in_minutes: 3 * 60, }; +} - /** - * @param {Platform} platform - * @returns {Step} - */ - const getTestBunStep = platform => { - const { os } = platform; - let command; - if (os === "windows") { - command = `node .\\scripts\\runner.node.mjs --step ${getTargetKey(platform)}-build-bun`; - } else { - command = `./scripts/runner.node.mjs --step ${getTargetKey(platform)}-build-bun`; - } - let parallelism; - if (os === "darwin") { - parallelism = 2; - } else { - parallelism = 10; - } - let env; - let depends = []; - if (buildId) { - env = { - BUILDKITE_ARTIFACT_BUILD_ID: buildId, - }; - } else { - depends = [`${getTargetKey(platform)}-build-bun`]; - } - let retry; - if (os !== "windows") { - // When the runner fails on Windows, Buildkite only detects an exit code of 1. - // Because of this, we don't know if the run was fatal, or soft-failed. - retry = getRetry(1); - } - let soft_fail; - if (isMainBranch()) { - soft_fail = true; - } else { - soft_fail = [{ exit_status: 2 }]; - } - return { - key: `${getPlatformKey(platform)}-test-bun`, - label: `${getPlatformLabel(platform)} - test-bun`, - depends_on: [...depends, ...getDependsOn(platform)], - agents: getTestAgent(platform), - retry, - cancel_on_build_failing: isMergeQueue(), - soft_fail, - parallelism, - command, - env, - }; +/** + * @param {Platform[]} buildPlatforms + * @param {PipelineOptions} options + * @returns {Step} + */ +function getReleaseStep(buildPlatforms, options) { + const { canary } = options; + const revision = typeof canary === "number" ? canary : 1; + + return { + key: "release", + label: getBuildkiteEmoji("rocket"), + agents: { + queue: "test-darwin", + }, + depends_on: buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`), + env: { + CANARY: revision, + }, + command: ".buildkite/scripts/upload-release.sh", }; +} - /** - * Config - */ +/** + * @typedef {Object} Pipeline + * @property {Step[]} [steps] + * @property {number} [priority] + */ - /** - * @type {Platform[]} - */ - const buildPlatforms = [ - { os: "darwin", arch: "aarch64", release: "14" }, - { os: "darwin", arch: "x64", release: "14" }, - { os: "linux", arch: "aarch64", distro: "debian", release: "11" }, - { os: "linux", arch: "x64", distro: "debian", release: "11" }, - { os: "linux", arch: "x64", baseline: true, distro: "debian", release: "11" }, - { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.20" }, - { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.20" }, - { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.20" }, - { os: "windows", arch: "x64", release: "2019" }, - { os: "windows", arch: "x64", baseline: true, release: "2019" }, +/** + * @typedef {Record} Agent + */ + +/** + * @typedef {GroupStep | CommandStep | BlockStep} Step + */ + +/** + * @typedef {Object} GroupStep + * @property {string} key + * @property {string} group + * @property {Step[]} steps + * @property {string[]} [depends_on] + */ + +/** + * @typedef {Object} CommandStep + * @property {string} key + * @property {string} [label] + * @property {Record} [agents] + * @property {Record} [env] + * @property {string} command + * @property {string[]} [depends_on] + * @property {Record} [retry] + * @property {boolean} [cancel_on_build_failing] + * @property {boolean} [soft_fail] + * @property {number} [parallelism] + * @property {number} [concurrency] + * @property {string} [concurrency_group] + * @property {number} [priority] + * @property {number} [timeout_in_minutes] + * @link https://buildkite.com/docs/pipelines/command-step + */ + +/** + * @typedef {Object} BlockStep + * @property {string} key + * @property {string} block + * @property {string} [prompt] + * @property {"passed" | "failed" | "running"} [blocked_state] + * @property {(SelectInput | TextInput)[]} [fields] + */ + +/** + * @typedef {Object} TextInput + * @property {string} key + * @property {string} text + * @property {string} [default] + * @property {boolean} [required] + * @property {string} [hint] + */ + +/** + * @typedef {Object} SelectInput + * @property {string} key + * @property {string} select + * @property {string | string[]} [default] + * @property {boolean} [required] + * @property {boolean} [multiple] + * @property {string} [hint] + * @property {SelectOption[]} [options] + */ + +/** + * @typedef {Object} SelectOption + * @property {string} label + * @property {string} value + */ + +/** + * @typedef {Object} PipelineOptions + * @property {string | boolean} [skipEverything] + * @property {string | boolean} [skipBuilds] + * @property {string | boolean} [skipTests] + * @property {string | boolean} [forceBuilds] + * @property {string | boolean} [forceTests] + * @property {string | boolean} [buildImages] + * @property {string | boolean} [publishImages] + * @property {number} [canary] + * @property {Profile[]} [buildProfiles] + * @property {Platform[]} [buildPlatforms] + * @property {Platform[]} [testPlatforms] + * @property {string[]} [testFiles] + * @property {boolean} [unifiedBuilds] + * @property {boolean} [unifiedTests] + */ + +/** + * @param {Step} step + * @param {(string | undefined)[]} dependsOn + * @returns {Step} + */ +function getStepWithDependsOn(step, ...dependsOn) { + const { depends_on: existingDependsOn = [] } = step; + return { + ...step, + depends_on: [...existingDependsOn, ...dependsOn.filter(Boolean)], + }; +} + +/** + * @returns {BlockStep} + */ +function getOptionsStep() { + const booleanOptions = [ + { + label: `${getEmoji("true")} Yes`, + value: "true", + }, + { + label: `${getEmoji("false")} No`, + value: "false", + }, ]; - /** - * @type {Platform[]} - */ - const testPlatforms = [ - { os: "darwin", arch: "aarch64", release: "14" }, - { os: "darwin", arch: "aarch64", release: "13" }, - { os: "darwin", arch: "x64", release: "14" }, - { os: "darwin", arch: "x64", release: "13" }, - { os: "linux", arch: "aarch64", distro: "debian", release: "12" }, - { os: "linux", arch: "aarch64", distro: "debian", release: "11" }, - { os: "linux", arch: "x64", distro: "debian", release: "12" }, - { os: "linux", arch: "x64", distro: "debian", release: "11" }, - { os: "linux", arch: "x64", baseline: true, distro: "debian", release: "12" }, - { os: "linux", arch: "x64", baseline: true, distro: "debian", release: "11" }, - { os: "linux", arch: "aarch64", distro: "ubuntu", release: "22.04" }, - { os: "linux", arch: "aarch64", distro: "ubuntu", release: "20.04" }, - { os: "linux", arch: "x64", distro: "ubuntu", release: "22.04" }, - { os: "linux", arch: "x64", distro: "ubuntu", release: "20.04" }, - { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "22.04" }, - { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "20.04" }, - { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.20" }, - { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.20" }, - { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.20" }, - { os: "windows", arch: "x64", release: "2019" }, - { os: "windows", arch: "x64", baseline: true, release: "2019" }, - ]; + return { + key: "options", + block: getBuildkiteEmoji("clipboard"), + blocked_state: "running", + fields: [ + { + key: "canary", + select: "If building, is this a canary build?", + hint: "If you are building for a release, this should be false", + required: false, + default: "true", + options: booleanOptions, + }, + { + key: "skip-builds", + select: "Do you want to skip the build?", + hint: "If true, artifacts will be downloaded from the last successful build", + required: false, + default: "false", + options: booleanOptions, + }, + { + key: "skip-tests", + select: "Do you want to skip the tests?", + required: false, + default: "false", + options: booleanOptions, + }, + { + key: "force-builds", + select: "Do you want to force run the build?", + hint: "If true, the build will run even if no source files have changed", + required: false, + default: "false", + options: booleanOptions, + }, + { + key: "force-tests", + select: "Do you want to force run the tests?", + hint: "If true, the tests will run even if no test files have changed", + required: false, + default: "false", + options: booleanOptions, + }, + { + key: "build-profiles", + select: "If building, which profiles do you want to build?", + required: false, + multiple: true, + default: ["release"], + options: [ + { + label: `${getEmoji("release")} Release`, + value: "release", + }, + { + label: `${getEmoji("assert")} Release with Assertions`, + value: "assert", + }, + { + label: `${getEmoji("debug")} Debug`, + value: "debug", + }, + ], + }, + { + key: "build-platforms", + select: "If building, which platforms do you want to build?", + hint: "If this is left blank, all platforms are built", + required: false, + multiple: true, + default: [], + options: buildPlatforms.map(platform => { + const { os, arch, abi, baseline } = platform; + let label = `${getEmoji(os)} ${arch}`; + if (abi) { + label += `-${abi}`; + } + if (baseline) { + label += `-baseline`; + } + return { + label, + value: getTargetKey(platform), + }; + }), + }, + { + key: "test-platforms", + select: "If testing, which platforms do you want to test?", + hint: "If this is left blank, all platforms are tested", + required: false, + multiple: true, + default: [], + options: [...new Map(testPlatforms.map(platform => [getImageKey(platform), platform])).entries()].map( + ([key, platform]) => { + const { os, arch, abi, distro, release } = platform; + let label = `${getEmoji(os)} ${arch}`; + if (abi) { + label += `-${abi}`; + } + if (distro) { + label += ` ${distro}`; + } + if (release) { + label += ` ${release}`; + } + return { + label, + value: key, + }; + }, + ), + }, + { + key: "test-files", + text: "If testing, which files do you want to test?", + hint: "If specified, only run test paths that include the list of strings (e.g. 'test/js', 'test/cli/hot/watch.ts')", + required: false, + }, + { + key: "build-images", + select: "Do you want to re-build the base images?", + hint: "This can take 2-3 hours to complete, only do so if you've tested locally", + required: false, + default: "false", + options: booleanOptions, + }, + { + key: "publish-images", + select: "Do you want to re-build and publish the base images?", + hint: "This can take 2-3 hours to complete, only do so if you've tested locally", + required: false, + default: "false", + options: booleanOptions, + }, + { + key: "unified-builds", + select: "Do you want to build each platform in a single step?", + hint: "If true, builds will not be split into seperate steps (this will likely slow down the build)", + required: false, + default: "false", + options: booleanOptions, + }, + { + key: "unified-tests", + select: "Do you want to run tests in a single step?", + hint: "If true, tests will not be split into seperate steps (this will be very slow)", + required: false, + default: "false", + options: booleanOptions, + }, + ], + }; +} +/** + * @returns {Step} + */ +function getOptionsApplyStep() { + const command = getEnv("BUILDKITE_COMMAND"); + return { + key: "options-apply", + label: getBuildkiteEmoji("gear"), + command: `${command} --apply`, + depends_on: ["options"], + agents: { + queue: getEnv("BUILDKITE_AGENT_META_DATA_QUEUE", false), + }, + }; +} + +/** + * @returns {Promise} + */ +async function getPipelineOptions() { + const isManual = isBuildManual(); + if (isManual && !process.argv.includes("--apply")) { + return; + } + + const canary = await getCanaryRevision(); + const buildPlatformsMap = new Map(buildPlatforms.map(platform => [getTargetKey(platform), platform])); + const testPlatformsMap = new Map(testPlatforms.map(platform => [getPlatformKey(platform), platform])); + + if (isManual) { + const { fields } = getOptionsStep(); + const keys = fields?.map(({ key }) => key) ?? []; + const values = await Promise.all(keys.map(getBuildMetadata)); + const options = Object.fromEntries(keys.map((key, index) => [key, values[index]])); + + /** + * @param {string} value + * @returns {string[] | undefined} + */ + const parseArray = value => + value + ?.split("\n") + ?.map(item => item.trim()) + ?.filter(Boolean); + + const buildPlatformKeys = parseArray(options["build-platforms"]); + const testPlatformKeys = parseArray(options["test-platforms"]); + return { + canary: parseBoolean(options["canary"]) ? canary : 0, + skipBuilds: parseBoolean(options["skip-builds"]), + forceBuilds: parseBoolean(options["force-builds"]), + skipTests: parseBoolean(options["skip-tests"]), + buildImages: parseBoolean(options["build-images"]), + publishImages: parseBoolean(options["publish-images"]), + testFiles: parseArray(options["test-files"]), + unifiedBuilds: parseBoolean(options["unified-builds"]), + unifiedTests: parseBoolean(options["unified-tests"]), + buildProfiles: parseArray(options["build-profiles"]), + buildPlatforms: buildPlatformKeys?.length + ? buildPlatformKeys.map(key => buildPlatformsMap.get(key)) + : Array.from(buildPlatformsMap.values()), + testPlatforms: testPlatformKeys?.length + ? testPlatformKeys.map(key => testPlatformsMap.get(key)) + : Array.from(testPlatformsMap.values()), + dryRun: parseBoolean(options["dry-run"]), + }; + } + + const commitMessage = getCommitMessage(); + + /** + * @param {RegExp} pattern + * @returns {string | boolean} + */ + const parseOption = pattern => { + const match = pattern.exec(commitMessage); + if (match) { + const [, value] = match; + return value; + } + return false; + }; + + const isCanary = + !parseBoolean(getEnv("RELEASE", false) || "false") && + !/\[(release|build release|release build)\]/i.test(commitMessage); + return { + canary: isCanary ? canary : 0, + skipEverything: parseOption(/\[(skip ci|no ci)\]/i), + skipBuilds: parseOption(/\[(skip builds?|no builds?|only tests?)\]/i), + forceBuilds: parseOption(/\[(force builds?)\]/i), + skipTests: parseOption(/\[(skip tests?|no tests?|only builds?)\]/i), + buildImages: parseOption(/\[(build images?)\]/i), + dryRun: parseOption(/\[(dry run)\]/i), + publishImages: parseOption(/\[(publish images?)\]/i), + buildPlatforms: Array.from(buildPlatformsMap.values()), + testPlatforms: Array.from(testPlatformsMap.values()), + buildProfiles: ["release"], + }; +} + +/** + * @param {PipelineOptions} [options] + * @returns {Promise} + */ +async function getPipeline(options = {}) { + const priority = getPriority(); + + if (isBuildManual() && !Object.keys(options).length) { + return { + priority, + steps: [getOptionsStep(), getOptionsApplyStep()], + }; + } + + const { skipEverything } = options; + if (skipEverything) { + return; + } + + const { buildProfiles = [], buildPlatforms = [], testPlatforms = [], buildImages, publishImages } = options; const imagePlatforms = new Map( - [...buildPlatforms, ...testPlatforms] - .filter(platform => buildImages && isUsingNewAgent(platform)) - .map(platform => [getImageKey(platform), platform]), + buildImages || publishImages + ? [...buildPlatforms, ...testPlatforms] + .filter(({ os }) => os === "linux" || os === "windows") + .map(platform => [getImageKey(platform), platform]) + : [], ); - /** - * @type {Step[]} - */ + /** @type {Step[]} */ const steps = []; if (imagePlatforms.size) { steps.push({ - group: ":docker:", - steps: [...imagePlatforms.values()].map(platform => getBuildImageStep(platform)), + key: "build-images", + group: getBuildkiteEmoji("aws"), + steps: [...imagePlatforms.values()].map(platform => getBuildImageStep(platform, options)), }); } - for (const platform of buildPlatforms) { - const { os, arch, abi, baseline } = platform; + let { skipBuilds, forceBuilds, unifiedBuilds, dryRun } = options; + dryRun = dryRun || !!buildImages; - /** @type {Step[]} */ - const platformSteps = []; - - if (buildImages || !buildId) { - platformSteps.push( - getBuildVendorStep(platform), - getBuildCppStep(platform), - getBuildZigStep(platform), - getBuildBunStep(platform), - ); + /** @type {string | undefined} */ + let buildId; + if (skipBuilds && !forceBuilds) { + const lastBuild = await getLastSuccessfulBuild(); + if (lastBuild) { + const { id } = lastBuild; + buildId = id; + } else { + console.warn("No last successful build found, must force builds..."); } + } - if (!skipTests) { - platformSteps.push( + if (!buildId) { + steps.push( + ...buildPlatforms + .flatMap(platform => buildProfiles.map(profile => ({ ...platform, profile }))) + .map(target => { + const imageKey = getImageKey(target); + + return getStepWithDependsOn( + { + key: getTargetKey(target), + group: getTargetLabel(target), + steps: unifiedBuilds + ? [getBuildBunStep(target, options)] + : [ + getBuildVendorStep(target, options), + getBuildCppStep(target, options), + getBuildZigStep(target, options), + getLinkBunStep(target, options), + ], + }, + imagePlatforms.has(imageKey) ? `${imageKey}-build-image` : undefined, + ); + }), + ); + } + + if (!isMainBranch()) { + const { skipTests, forceTests, unifiedTests, testFiles } = options; + if (!skipTests || forceTests) { + steps.push( ...testPlatforms - .filter( - testPlatform => - testPlatform.os === os && - testPlatform.arch === arch && - testPlatform.abi === abi && - testPlatform.baseline === baseline, - ) - .map(testPlatform => getTestBunStep(testPlatform)), + .flatMap(platform => buildProfiles.map(profile => ({ ...platform, profile }))) + .map(target => ({ + key: getTargetKey(target), + group: getTargetLabel(target), + steps: [getTestBunStep(target, options, { unifiedTests, testFiles, buildId })], + })), ); } + } - if (!platformSteps.length) { + if (isMainBranch()) { + steps.push(getReleaseStep(buildPlatforms, options)); + } + + /** @type {Map} */ + const stepsByGroup = new Map(); + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + if (!("group" in step)) { continue; } - steps.push({ - key: getTargetKey(platform), - group: getTargetLabel(platform), - steps: platformSteps, - }); - } + const { group, steps: groupSteps } = step; + if (stepsByGroup.has(group)) { + stepsByGroup.get(group).steps.push(...groupSteps); + } else { + stepsByGroup.set(group, step); + } - if (isMainBranch() && !isFork()) { - steps.push({ - label: ":github:", - agents: { - queue: "test-darwin", - }, - depends_on: buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`), - command: ".buildkite/scripts/upload-release.sh", - }); + steps[i] = undefined; } return { - priority: getPriority(), - steps, + priority, + steps: [...steps.filter(step => typeof step !== "undefined"), ...Array.from(stepsByGroup.values())], }; } async function main() { - printEnvironment(); - - console.log("Checking last successful build..."); - const lastBuild = await getLastSuccessfulBuild(); - if (lastBuild) { - const { id, path, commit_id: commit } = lastBuild; - console.log(" - Build ID:", id); - console.log(" - Build URL:", new URL(path, "https://buildkite.com/").toString()); - console.log(" - Commit:", commit); - } else { - console.log(" - No build found"); + startGroup("Generating options..."); + const options = await getPipelineOptions(); + if (options) { + console.log("Generated options:", options); } - let changedFiles; - let changedFilesBranch; - if (!isFork() && !isMainBranch()) { - console.log("Checking changed files..."); - const targetRef = getTargetBranch(); - console.log(" - Target Ref:", targetRef); - const baseRef = lastBuild?.commit_id || targetRef || getMainBranch(); - console.log(" - Base Ref:", baseRef); - const headRef = getCommit(); - console.log(" - Head Ref:", headRef); - - changedFiles = await getChangedFiles(undefined, baseRef, headRef); - changedFilesBranch = await getChangedFiles(undefined, targetRef, headRef); - if (changedFiles) { - if (changedFiles.length) { - changedFiles.forEach(filename => console.log(` - ${filename}`)); - } else { - console.log(" - No changed files"); - } - } + startGroup("Generating pipeline..."); + const pipeline = await getPipeline(options); + if (!pipeline) { + console.log("Generated pipeline is empty, skipping..."); + return; } - const isDocumentationFile = filename => /^(\.vscode|\.github|bench|docs|examples)|\.(md)$/i.test(filename); - const isTestFile = filename => /^test/i.test(filename) || /runner\.node\.mjs$/i.test(filename); - - console.log("Checking if CI should be forced..."); - let forceBuild; - let ciFileChanged; - { - const message = getCommitMessage(); - const match = /\[(force ci|ci force|ci force build)\]/i.exec(message); - if (match) { - const [, reason] = match; - console.log(" - Yes, because commit message contains:", reason); - forceBuild = true; - } - for (const coref of [".buildkite/ci.mjs", "scripts/utils.mjs", "scripts/bootstrap.sh", "scripts/machine.mjs"]) { - if (changedFilesBranch && changedFilesBranch.includes(coref)) { - console.log(" - Yes, because the list of changed files contains:", coref); - forceBuild = true; - ciFileChanged = true; - } - } - } - - console.log("Checking if CI should be skipped..."); - if (!forceBuild) { - const message = getCommitMessage(); - const match = /\[(skip ci|no ci|ci skip|ci no)\]/i.exec(message); - if (match) { - const [, reason] = match; - console.log(" - Yes, because commit message contains:", reason); - return; - } - if (changedFiles && changedFiles.every(filename => isDocumentationFile(filename))) { - console.log(" - Yes, because all changed files are documentation"); - return; - } - } - - console.log("Checking if CI should re-build images..."); - let buildImages; - { - const message = getCommitMessage(); - const match = /\[(build images?|images? build)\]/i.exec(message); - if (match) { - const [, reason] = match; - console.log(" - Yes, because commit message contains:", reason); - buildImages = true; - } - if (ciFileChanged) { - console.log(" - Yes, because a core CI file changed"); - buildImages = true; - } - } - - console.log("Checking if CI should publish images..."); - let publishImages; - { - const message = getCommitMessage(); - const match = /\[(publish images?|images? publish)\]/i.exec(message); - if (match) { - const [, reason] = match; - console.log(" - Yes, because commit message contains:", reason); - publishImages = true; - buildImages = true; - } - if (ciFileChanged && isMainBranch()) { - console.log(" - Yes, because a core CI file changed and this is main branch"); - publishImages = true; - buildImages = true; - } - } - - console.log("Checking if build should be skipped..."); - let skipBuild; - if (!forceBuild) { - const message = getCommitMessage(); - const match = /\[(only tests?|tests? only|skip build|no build|build skip|build no)\]/i.exec(message); - if (match) { - const [, reason] = match; - console.log(" - Yes, because commit message contains:", reason); - skipBuild = true; - } - if (changedFiles && changedFiles.every(filename => isTestFile(filename) || isDocumentationFile(filename))) { - console.log(" - Yes, because all changed files are tests or documentation"); - skipBuild = true; - } - } - - console.log("Checking if tests should be skipped..."); - let skipTests; - { - const message = getCommitMessage(); - const match = /\[(skip tests?|tests? skip|no tests?|tests? no)\]/i.exec(message); - if (match) { - console.log(" - Yes, because commit message contains:", match[1]); - skipTests = true; - } - if (isMainBranch()) { - console.log(" - Yes, because we're on main branch"); - skipTests = true; - } - } - - console.log("Checking if build is a named release..."); - let buildRelease; - if (/^(1|true|on|yes)$/i.test(getEnv("RELEASE", false))) { - console.log(" - Yes, because RELEASE environment variable is set"); - buildRelease = true; - } else { - const message = getCommitMessage(); - const match = /\[(release|release build|build release)\]/i.exec(message); - if (match) { - const [, reason] = match; - console.log(" - Yes, because commit message contains:", reason); - buildRelease = true; - } - } - - console.log("Generating pipeline..."); - const pipeline = getPipeline({ - buildId: lastBuild && skipBuild && !forceBuild ? lastBuild.id : undefined, - buildImages, - publishImages, - skipTests, - }); - const content = toYaml(pipeline); const contentPath = join(process.cwd(), ".buildkite", "ci.yml"); - writeFileSync(contentPath, content); + writeFile(contentPath, content); console.log("Generated pipeline:"); console.log(" - Path:", contentPath); console.log(" - Size:", (content.length / 1024).toFixed(), "KB"); - if (isBuildkite) { - await uploadArtifact(contentPath); - } if (isBuildkite) { - console.log("Setting canary revision..."); - const canaryRevision = buildRelease ? 0 : await getCanaryRevision(); - await spawnSafe(["buildkite-agent", "meta-data", "set", "canary", `${canaryRevision}`], { stdio: "inherit" }); - - console.log("Uploading pipeline..."); - await spawnSafe(["buildkite-agent", "pipeline", "upload", contentPath], { stdio: "inherit" }); + startGroup("Uploading pipeline..."); + try { + await spawnSafe(["buildkite-agent", "pipeline", "upload", contentPath], { stdio: "inherit" }); + } finally { + await uploadArtifact(contentPath); + } } } diff --git a/.buildkite/scripts/prepare-build.sh b/.buildkite/scripts/prepare-build.sh deleted file mode 100755 index a76370fd7c..0000000000 --- a/.buildkite/scripts/prepare-build.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -function run_command() { - set -x - "$@" - { set +x; } 2>/dev/null -} - -run_command node ".buildkite/ci.mjs" diff --git a/.buildkite/scripts/upload-release.sh b/.buildkite/scripts/upload-release.sh index b684dfb4a3..45fa269b63 100755 --- a/.buildkite/scripts/upload-release.sh +++ b/.buildkite/scripts/upload-release.sh @@ -3,10 +3,6 @@ set -eo pipefail function assert_main() { - if [ "$RELEASE" == "1" ]; then - echo "info: Skipping canary release because this is a release build" - exit 0 - fi if [ -z "$BUILDKITE_REPO" ]; then echo "error: Cannot find repository for this build" exit 1 @@ -194,8 +190,6 @@ function create_release() { local artifacts=( bun-darwin-aarch64.zip bun-darwin-aarch64-profile.zip - bun-darwin-x64.zip - bun-darwin-x64-profile.zip bun-linux-aarch64.zip bun-linux-aarch64-profile.zip bun-linux-x64.zip @@ -237,8 +231,7 @@ function create_release() { } function assert_canary() { - local canary="$(buildkite-agent meta-data get canary 2>/dev/null)" - if [ -z "$canary" ] || [ "$canary" == "0" ]; then + if [ -z "$CANARY" ] || [ "$CANARY" == "0" ]; then echo "warn: Skipping release because this is not a canary build" exit 0 fi diff --git a/.clangd b/.clangd index f736d521d0..2d2d52668c 100644 --- a/.clangd +++ b/.clangd @@ -3,3 +3,9 @@ Index: CompileFlags: CompilationDatabase: build/debug + +Diagnostics: + UnusedIncludes: None + +HeaderInsertion: + IncludeBlocks: Preserve # Do not auto-include headers. diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000000..e56b21e52f --- /dev/null +++ b/.cursorignore @@ -0,0 +1,7 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +bench +vendor +*-fixture.{js,ts} +zig-cache +packages/bun-uws/fuzzing +build \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 6a0ae98134..d767837682 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,3 +16,6 @@ zig-out build vendor node_modules +*.trace + +packages/bun-uws/fuzzing \ No newline at end of file diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index bb2cca1880..8eff85a429 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -10,7 +10,7 @@ on: merge_group: env: - BUN_VERSION: "1.1.27" + BUN_VERSION: "1.1.44" LLVM_VERSION: "18.1.8" LLVM_VERSION_MAJOR: "18" diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index a6f06ad620..2d0aab1021 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -10,7 +10,7 @@ on: merge_group: env: - BUN_VERSION: "1.1.27" + BUN_VERSION: "1.1.44" LLVM_VERSION: "18.1.8" LLVM_VERSION_MAJOR: "18" diff --git a/.github/workflows/labeled.yml b/.github/workflows/labeled.yml index 3529f724b4..7d521d55a3 100644 --- a/.github/workflows/labeled.yml +++ b/.github/workflows/labeled.yml @@ -1,6 +1,6 @@ name: Issue Labeled env: - BUN_VERSION: 1.1.13 + BUN_VERSION: 1.1.44 on: issues: @@ -23,7 +23,7 @@ jobs: # - name: Setup Bun # uses: ./.github/actions/setup-bun # with: - # bun-version: "1.1.24" + # bun-version: ${{ env.BUN_VERSION }} # - name: "categorize bug" # id: add-labels # env: @@ -59,7 +59,7 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun with: - bun-version: "1.1.13" + bun-version: ${{ env.BUN_VERSION }} - name: "add platform and command label" id: add-labels if: github.event.label.name == 'crash' @@ -72,7 +72,7 @@ jobs: echo "labels=$LABELS" >> $GITHUB_OUTPUT bun scripts/is-outdated.ts - if [[ -f "is-outdated.txt" ]]; then + if [[ -f "is-outdated.txt" ]]; then echo "is-outdated=true" >> $GITHUB_OUTPUT fi @@ -154,7 +154,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | - Thank you for reporting this crash. + Thank you for reporting this crash. For Bun's internal tracking, this issue is [${{ steps.generate-comment-text.outputs.sentry-id }}](${{ steps.generate-comment-text.outputs.sentry-link }}). diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..593a77b115 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint + +on: + pull_request: + workflow_dispatch: + +env: + BUN_VERSION: "1.1.44" + OXLINT_VERSION: "0.15.0" + +jobs: + lint-js: + name: "Lint JavaScript" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Lint + run: bunx oxlint --config oxlint.json --quiet --format github diff --git a/.github/workflows/prettier-format.yml b/.github/workflows/prettier-format.yml index 43a407443e..67a1c96afb 100644 --- a/.github/workflows/prettier-format.yml +++ b/.github/workflows/prettier-format.yml @@ -10,7 +10,7 @@ on: merge_group: env: - BUN_VERSION: "1.1.27" + BUN_VERSION: "1.1.44" jobs: prettier-format: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab0bf70103..904c6a7b27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,7 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun with: - bun-version: "1.1.20" + bun-version: "1.1.44" - name: Install Dependencies run: bun install - name: Sign Release @@ -94,7 +94,7 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun with: - bun-version: "1.1.20" + bun-version: "1.1.44" - name: Install Dependencies run: bun install - name: Release @@ -123,7 +123,7 @@ jobs: if: ${{ env.BUN_VERSION != 'canary' }} uses: ./.github/actions/setup-bun with: - bun-version: "1.1.20" + bun-version: "1.1.44" - name: Setup Bun if: ${{ env.BUN_VERSION == 'canary' }} uses: ./.github/actions/setup-bun @@ -265,7 +265,7 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun with: - bun-version: "1.1.20" + bun-version: "1.1.44" - name: Install Dependencies run: bun install - name: Release @@ -309,7 +309,7 @@ jobs: uses: ./.github/actions/setup-bun if: ${{ env.BUN_LATEST == 'true' }} with: - bun-version: "1.1.12" + bun-version: "1.1.44" - name: Bump version uses: ./.github/actions/bump if: ${{ env.BUN_LATEST == 'true' }} diff --git a/.github/workflows/run-lint.yml b/.github/workflows/run-lint.yml index 45aa25307d..5ce9a19050 100644 --- a/.github/workflows/run-lint.yml +++ b/.github/workflows/run-lint.yml @@ -4,6 +4,7 @@ permissions: contents: read env: LLVM_VERSION: 16 + BUN_VERSION: "1.1.44" on: workflow_call: @@ -22,7 +23,7 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun with: - bun-version: "1.1.3" + bun-version: ${{ env.BUN_VERSION }} - name: Install Dependencies run: | bun --cwd=packages/bun-internal-test install diff --git a/.github/workflows/test-bump.yml b/.github/workflows/test-bump.yml index 03012a1239..7c8d9e57ef 100644 --- a/.github/workflows/test-bump.yml +++ b/.github/workflows/test-bump.yml @@ -8,6 +8,9 @@ on: description: What is the release tag? (e.g. "1.0.2", "canary") required: true +env: + BUN_VERSION: "1.1.44" + jobs: bump: name: "Bump version" @@ -21,7 +24,7 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun with: - bun-version: "1.1.12" + bun-version: ${{ env.BUN_VERSION }} - name: Bump version uses: ./.github/actions/bump with: diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 0000000000..74c2aaec89 --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,19 @@ +name: Typos + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Spellcheck + uses: crate-ci/typos@v1.29.4 + with: + files: docs/**/* diff --git a/.github/workflows/zig-format.yml b/.github/workflows/zig-format.yml index 24d5577ad7..1649a67c60 100644 --- a/.github/workflows/zig-format.yml +++ b/.github/workflows/zig-format.yml @@ -10,7 +10,7 @@ on: merge_group: env: - BUN_VERSION: "1.1.27" + BUN_VERSION: "1.1.44" jobs: zig-format: diff --git a/.gitignore b/.gitignore index 3822491fcb..af3120ab43 100644 --- a/.gitignore +++ b/.gitignore @@ -116,8 +116,10 @@ scripts/env.local sign.*.json sign.json src/bake/generated.ts +src/generated_enum_extractor.zig src/bun.js/bindings-obj src/bun.js/bindings/GeneratedJS2Native.zig +src/bun.js/bindings/GeneratedBindings.zig src/bun.js/debug-bindings-obj src/deps/zig-clap/.gitattributes src/deps/zig-clap/.github diff --git a/.lldbinit b/.lldbinit index b54a4195c3..a2357365bb 100644 --- a/.lldbinit +++ b/.lldbinit @@ -1,4 +1,4 @@ -command script import vendor/zig/tools/lldb_pretty_printers.py +# command script import vendor/zig/tools/lldb_pretty_printers.py command script import vendor/WebKit/Tools/lldb/lldb_webkit.py # type summary add --summary-string "${var} | inner=${var[0-30]}, source=${var[33-64]}, tag=${var[31-32]}" "unsigned long" diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..5b36b34dde --- /dev/null +++ b/.mailmap @@ -0,0 +1,2 @@ +# To learn more about git's mailmap: https://ntietz.com/blog/git-mailmap-for-name-changes +chloe caruso diff --git a/.prettierignore b/.prettierignore index da765c9c28..42d0c454a9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,5 @@ test/js/deno test/node.js src/react-refresh.js *.min.js -test/js/node/test/fixtures -test/js/node/test/common test/snippets +test/js/node/test diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000000..7819bc056f --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[type.md] +extend-ignore-words-re = ["^ba"] diff --git a/.vscode/launch.json b/.vscode/launch.json index 00f72d4ddf..d8efde85d1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,14 +16,13 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "1", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -33,14 +32,13 @@ "args": ["test", "--only", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "1", "BUN_DEBUG_jest": "1", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -56,14 +54,13 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "0", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -73,14 +70,13 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "0", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -90,14 +86,13 @@ "args": ["test", "--watch", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -107,14 +102,13 @@ "args": ["test", "--hot", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -124,7 +118,6 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -132,7 +125,7 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -147,7 +140,6 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -155,7 +147,7 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -168,7 +160,7 @@ "request": "launch", "name": "bun run [file]", "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${fileBasename}"], + "args": ["${file}"], "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "0", @@ -177,17 +169,16 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", "request": "launch", "name": "bun run [file] (fast)", "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${fileBasename}"], + "args": ["run", "${file}"], "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "0", "BUN_DEBUG_IncrementalGraph": "1", @@ -197,33 +188,31 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", "request": "launch", "name": "bun run [file] (verbose)", "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${fileBasename}"], + "args": ["run", "${file}"], "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "0", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", "request": "launch", "name": "bun run [file] --watch", "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "--watch", "${fileBasename}"], + "args": ["run", "--watch", "${file}"], "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1", // "BUN_DEBUG_DEBUGGER": "1", // "BUN_DEBUG_INTERNAL_DEBUGGER": "1", "BUN_DEBUG_QUIET_LOGS": "1", @@ -232,30 +221,29 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", "request": "launch", "name": "bun run [file] --hot", "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "--hot", "${fileBasename}"], + "args": ["run", "--hot", "${file}"], "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", "request": "launch", "name": "bun run [file] --inspect", "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${fileBasename}"], + "args": ["run", "${file}"], "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "0", @@ -265,7 +253,7 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -277,7 +265,7 @@ "request": "launch", "name": "bun run [file] --inspect-brk", "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${fileBasename}"], + "args": ["run", "${file}"], "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "0", @@ -287,7 +275,7 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -303,14 +291,13 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -320,14 +307,13 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "0", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -337,14 +323,13 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -354,14 +339,13 @@ "args": ["test", "--watch", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -371,14 +355,13 @@ "args": ["test", "--hot", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -388,7 +371,6 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -396,7 +378,7 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -411,7 +393,6 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -419,7 +400,7 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -435,13 +416,12 @@ "args": ["exec", "${input:testName}"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, // bun test [*] { @@ -452,13 +432,12 @@ "args": ["test"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -468,13 +447,12 @@ "args": ["test"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "0", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -484,14 +462,13 @@ "args": ["test"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", "BUN_INSPECT": "ws://localhost:0/", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -506,13 +483,12 @@ "args": ["install"], "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -522,13 +498,12 @@ "args": ["test/runner.node.mjs"], "cwd": "${workspaceFolder}", "env": { - "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, // Windows: bun test [file] { @@ -542,10 +517,6 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -571,10 +542,6 @@ "args": ["test", "--only", "${file}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -600,10 +567,6 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -629,10 +592,6 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "0", @@ -658,10 +617,6 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -696,10 +651,6 @@ "args": ["test", "${file}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -735,10 +686,6 @@ "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -764,10 +711,6 @@ "args": ["install"], "cwd": "${fileDirname}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -789,10 +732,6 @@ "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -814,10 +753,6 @@ "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -848,10 +783,6 @@ "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -883,10 +814,6 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -912,10 +839,6 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -941,10 +864,6 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "0", @@ -970,10 +889,6 @@ "args": ["test", "--watch", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -999,10 +914,6 @@ "args": ["test", "--hot", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1028,10 +939,6 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1066,10 +973,6 @@ "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1105,10 +1008,6 @@ "args": ["exec", "${input:testName}"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1131,10 +1030,6 @@ "args": ["test"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1156,10 +1051,6 @@ "args": ["test"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1185,10 +1076,6 @@ "args": ["test"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1223,10 +1110,6 @@ "args": ["test/runner.node.mjs"], "cwd": "${workspaceFolder}", "environment": [ - { - "name": "FORCE_COLOR", - "value": "1", - }, { "name": "BUN_DEBUG_QUIET_LOGS", "value": "1", @@ -1242,7 +1125,7 @@ ], "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["process handle -p true -s false -n false SIGUSR1"], + "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, ], "inputs": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index e1cc89f0a9..eca14849b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,7 +63,6 @@ "editor.tabSize": 4, "editor.defaultFormatter": "xaver.clang-format", }, - "clangd.arguments": ["-header-insertion=never"], // JavaScript "prettier.enable": true, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a7f41ae5c..03a2b4d663 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,6 @@ Configuring a development environment for Bun can take 10-30 minutes depending on your internet connection and computer speed. You will need ~10GB of free disk space for the repository and build artifacts. -If you are using Windows, please refer to [this guide](/docs/project/building-windows.md) - -{% details summary="For Ubuntu users" %} -TL;DR: Ubuntu 22.04 is suggested. -Bun currently requires `glibc >=2.32` in development which means if you're on Ubuntu 20.04 (glibc == 2.31), you may likely meet `error: undefined symbol: __libc_single_threaded `. You need to take extra configurations. Also, according to this [issue](https://github.com/llvm/llvm-project/issues/97314), LLVM 16 is no longer maintained on Ubuntu 24.04 (noble). And instead, you might want `brew` to install LLVM 16 for your Ubuntu 24.04. -{% /details %} +If you are using Windows, please refer to [this guide](https://bun.sh/docs/project/building-windows) ## Install Dependencies @@ -58,7 +53,7 @@ $ brew install bun ## Install LLVM -Bun requires LLVM 16 (`clang` is part of LLVM). This version requirement is to match WebKit (precompiled), as mismatching versions will cause memory allocation failures at runtime. In most cases, you can install LLVM through your system package manager: +Bun requires LLVM 18 (`clang` is part of LLVM). This version requirement is to match WebKit (precompiled), as mismatching versions will cause memory allocation failures at runtime. In most cases, you can install LLVM through your system package manager: {% codetabs group="os" %} @@ -68,7 +63,7 @@ $ brew install llvm@18 ```bash#Ubuntu/Debian $ # LLVM has an automatic installation script that is compatible with all versions of Ubuntu -$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 16 all +$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 18 all ``` ```bash#Arch @@ -76,23 +71,21 @@ $ sudo pacman -S llvm clang lld ``` ```bash#Fedora -$ sudo dnf install 'dnf-command(copr)' -$ sudo dnf copr enable -y @fedora-llvm-team/llvm17 -$ sudo dnf install llvm16 clang16 lld16-devel +$ sudo dnf install llvm18 clang18 lld18-devel ``` ```bash#openSUSE Tumbleweed -$ sudo zypper install clang16 lld16 llvm16 +$ sudo zypper install clang18 lld18 llvm18 ``` {% /codetabs %} -If none of the above solutions apply, you will have to install it [manually](https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.6). +If none of the above solutions apply, you will have to install it [manually](https://github.com/llvm/llvm-project/releases/tag/llvmorg-18.1.8). -Make sure Clang/LLVM 16 is in your path: +Make sure Clang/LLVM 18 is in your path: ```bash -$ which clang-16 +$ which clang-18 ``` If not, run this to manually add it: @@ -101,13 +94,13 @@ If not, run this to manually add it: ```bash#macOS (Homebrew) # use fish_add_path if you're using fish -# use path+="$(brew --prefix llvm@16)/bin" if you are using zsh -$ export PATH="$(brew --prefix llvm@16)/bin:$PATH" +# use path+="$(brew --prefix llvm@18)/bin" if you are using zsh +$ export PATH="$(brew --prefix llvm@18)/bin:$PATH" ``` ```bash#Arch # use fish_add_path if you're using fish -$ export PATH="$PATH:/usr/lib/llvm16/bin" +$ export PATH="$PATH:/usr/lib/llvm18/bin" ``` {% /codetabs %} @@ -168,7 +161,7 @@ The binary will be located at `./build/release/bun` and `./build/release/bun-pro ### Download release build from pull requests -To save you time spent building a release build locally, we provide a way to run release builds from pull requests. This is useful for manully testing changes in a release build before they are merged. +To save you time spent building a release build locally, we provide a way to run release builds from pull requests. This is useful for manually testing changes in a release build before they are merged. To run a release build from a pull request, you can use the `bun-pr` npm package: @@ -245,7 +238,7 @@ The issue may manifest when initially running `bun setup` as Clang being unable ``` The C++ compiler - "/usr/bin/clang++-16" + "/usr/bin/clang++-18" is not able to compile a simple test program. ``` diff --git a/LATEST b/LATEST index c99926d330..4e16b4e1ae 100644 --- a/LATEST +++ b/LATEST @@ -1 +1 @@ -1.1.38 \ No newline at end of file +1.1.44 \ No newline at end of file diff --git a/README.md b/README.md index 4b748b865c..675dd5b261 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called `bun`. -At its core is the _Bun runtime_, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage. +At its core is the _Bun runtime_, a fast JavaScript runtime designed as **a drop-in replacement for Node.js**. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage. ```bash bun run index.tsx # TS and JSX supported out-of-the-box diff --git a/bench/bun.lock b/bench/bun.lock new file mode 100644 index 0000000000..cba2c6b04c --- /dev/null +++ b/bench/bun.lock @@ -0,0 +1,416 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "bench", + "dependencies": { + "@babel/core": "^7.16.10", + "@babel/preset-react": "^7.16.7", + "@babel/standalone": "^7.24.7", + "@swc/core": "^1.2.133", + "benchmark": "^2.1.4", + "braces": "^3.0.2", + "color": "^4.2.3", + "esbuild": "^0.14.12", + "eventemitter3": "^5.0.0", + "execa": "^8.0.1", + "fast-glob": "3.3.1", + "fdir": "^6.1.0", + "mitata": "^1.0.25", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "string-width": "7.1.0", + "tinycolor2": "^1.6.0", + "zx": "^7.2.3", + }, + "devDependencies": { + "fast-deep-equal": "^3.1.3", + }, + }, + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.2.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w=="], + + "@babel/code-frame": ["@babel/code-frame@7.18.6", "", { "dependencies": { "@babel/highlight": "^7.18.6" } }, "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q=="], + + "@babel/compat-data": ["@babel/compat-data@7.20.14", "", {}, "sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw=="], + + "@babel/core": ["@babel/core@7.20.12", "", { "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", "@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-module-transforms": "^7.20.11", "@babel/helpers": "^7.20.7", "@babel/parser": "^7.20.7", "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.12", "@babel/types": "^7.20.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.2", "semver": "^6.3.0" } }, "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg=="], + + "@babel/generator": ["@babel/generator@7.20.14", "", { "dependencies": { "@babel/types": "^7.20.7", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" } }, "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.20.7", "", { "dependencies": { "@babel/compat-data": "^7.20.5", "@babel/helper-validator-option": "^7.18.6", "browserslist": "^4.21.3", "lru-cache": "^5.1.1", "semver": "^6.3.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ=="], + + "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.18.9", "", {}, "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg=="], + + "@babel/helper-function-name": ["@babel/helper-function-name@7.19.0", "", { "dependencies": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" } }, "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w=="], + + "@babel/helper-hoist-variables": ["@babel/helper-hoist-variables@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.20.11", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", "@babel/helper-simple-access": "^7.20.2", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.10", "@babel/types": "^7.20.7" } }, "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.20.2", "", {}, "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ=="], + + "@babel/helper-simple-access": ["@babel/helper-simple-access@7.20.2", "", { "dependencies": { "@babel/types": "^7.20.2" } }, "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA=="], + + "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.19.4", "", {}, "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.19.1", "", {}, "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.18.6", "", {}, "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw=="], + + "@babel/helpers": ["@babel/helpers@7.20.13", "", { "dependencies": { "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.13", "@babel/types": "^7.20.7" } }, "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg=="], + + "@babel/highlight": ["@babel/highlight@7.18.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g=="], + + "@babel/parser": ["@babel/parser@7.20.15", "", { "bin": "./bin/babel-parser.js" }, "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q=="], + + "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA=="], + + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.20.13", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-module-imports": "^7.18.6", "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MmTZx/bkUrfJhhYAYt3Urjm+h8DQGrPrnKQ94jLo7NLuOU+T89a7IByhKmrb8SKhrIYIQ0FN0CHMbnFRen4qNw=="], + + "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.18.6", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA=="], + + "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.18.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ=="], + + "@babel/preset-react": ["@babel/preset-react@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/helper-validator-option": "^7.18.6", "@babel/plugin-transform-react-display-name": "^7.18.6", "@babel/plugin-transform-react-jsx": "^7.18.6", "@babel/plugin-transform-react-jsx-development": "^7.18.6", "@babel/plugin-transform-react-pure-annotations": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg=="], + + "@babel/standalone": ["@babel/standalone@7.24.7", "", {}, "sha512-QRIRMJ2KTeN+vt4l9OjYlxDVXEpcor1Z6V7OeYzeBOw6Q8ew9oMTHjzTx8s6ClsZO7wVf6JgTRutihatN6K0yA=="], + + "@babel/template": ["@babel/template@7.20.7", "", { "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7" } }, "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw=="], + + "@babel/traverse": ["@babel/traverse@7.20.13", "", { "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/parser": "^7.20.13", "@babel/types": "^7.20.7", "debug": "^4.1.0", "globals": "^11.1.0" } }, "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ=="], + + "@babel/types": ["@babel/types@7.20.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.14.54", "", { "os": "linux", "cpu": "none" }, "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.1.1", "", { "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.0", "", {}, "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.1.2", "", {}, "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.14", "", {}, "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.17", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" } }, "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@swc/core": ["@swc/core@1.3.35", "", { "dependencies": { "@swc/core-darwin-arm64": "1.3.35", "@swc/core-darwin-x64": "1.3.35", "@swc/core-linux-arm-gnueabihf": "1.3.35", "@swc/core-linux-arm64-gnu": "1.3.35", "@swc/core-linux-arm64-musl": "1.3.35", "@swc/core-linux-x64-gnu": "1.3.35", "@swc/core-linux-x64-musl": "1.3.35", "@swc/core-win32-arm64-msvc": "1.3.35", "@swc/core-win32-ia32-msvc": "1.3.35", "@swc/core-win32-x64-msvc": "1.3.35" } }, "sha512-KmiBin0XSVzJhzX19zTiCqmLslZ40Cl7zqskJcTDeIrRhfgKdiAsxzYUanJgMJIRjYtl9Kcg1V/Ip2o2wL8v3w=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.3.35", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zQUFkHx4gZpu0uo2IspvPnKsz8bsdXd5bC33xwjtoAI1cpLerDyqo4v2zIahEp+FdKZjyVsLHtfJiQiA1Qka3A=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.3.35", "", { "os": "darwin", "cpu": "x64" }, "sha512-oOSkSGWtALovaw22lNevKD434OQTPf8X+dVPvPMrJXJpJ34dWDlFWpLntoc+arvKLNZ7LQmTuk8rR1hkrAY7cw=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.3.35", "", { "os": "linux", "cpu": "arm" }, "sha512-Yie8k00O6O8BCATS/xeKStquV4OYSskUGRDXBQVDw1FrE23PHaSeHCgg4q6iNZjJzXCOJbaTCKnYoIDn9DMf7A=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.3.35", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zlv3WHa/4x2p51HSvjUWXHfSe1Gl2prqImUZJc8NZOlj75BFzVuR0auhQ+LbwvIQ3gaA1LODX9lyS9wXL3yjxA=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.3.35", "", { "os": "linux", "cpu": "arm64" }, "sha512-u6tCYsrSyZ8U+4jLMA/O82veBfLy2aUpn51WxQaeH7wqZGy9TGSJXoO8vWxARQ6b72vjsnKDJHP4MD8hFwcctg=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.3.35", "", { "os": "linux", "cpu": "x64" }, "sha512-Dtxf2IbeH7XlNhP1Qt2/MvUPkpEbn7hhGfpSRs4ot8D3Vf5QEX4S/QtC1OsFWuciiYgHAT1Ybjt4xZic9DSkmA=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.3.35", "", { "os": "linux", "cpu": "x64" }, "sha512-4XavNJ60GprjpTiESCu5daJUnmErixPAqDitJSMu4TV32LNIE8G00S9pDLXinDTW1rgcGtQdq1NLkNRmwwovtg=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.3.35", "", { "os": "win32", "cpu": "arm64" }, "sha512-dNGfKCUSX2M4qVyaS80Lyos0FkXyHRCvrdQ2Y4Hrg3FVokiuw3yY6fLohpUfQ5ws3n2A39dh7jGDeh34+l0sGA=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.3.35", "", { "os": "win32", "cpu": "ia32" }, "sha512-ChuPSrDR+JBf7S7dEKPicnG8A3bM0uWPsW2vG+V2wH4iNfNxKVemESHosmYVeEZXqMpomNMvLyeHep1rjRsc0Q=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.3.35", "", { "os": "win32", "cpu": "x64" }, "sha512-/RvphT4WfuGfIK84Ha0dovdPrKB1bW/mc+dtdmhv2E3EGkNc5FoueNwYmXWRimxnU7X0X7IkcRhyKB4G5DeAmg=="], + + "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], + + "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], + + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + + "@types/node": ["@types/node@18.19.8", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg=="], + + "@types/ps-tree": ["@types/ps-tree@1.1.6", "", {}, "sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ=="], + + "@types/which": ["@types/which@3.0.3", "", {}, "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g=="], + + "ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="], + + "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "benchmark": ["benchmark@2.1.4", "", { "dependencies": { "lodash": "^4.17.4", "platform": "^1.3.3" } }, "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ=="], + + "braces": ["braces@3.0.2", "", { "dependencies": { "fill-range": "^7.0.1" } }, "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A=="], + + "browserslist": ["browserslist@4.21.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001449", "electron-to-chromium": "^1.4.284", "node-releases": "^2.0.8", "update-browserslist-db": "^1.0.10" }, "bin": { "browserslist": "cli.js" } }, "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001456", "", {}, "sha512-XFHJY5dUgmpMV25UqaD4kVq2LsiaU5rS8fb0f17pCoXQiQslzmFgnfOxfvo1bTpTqf7dwG/N/05CnLCnOEKmzA=="], + + "chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], + + "electron-to-chromium": ["electron-to-chromium@1.4.302", "", {}, "sha512-Uk7C+7aPBryUR1Fwvk9VmipBcN9fVsqBO57jV2ZjTm+IZ6BMNqu7EDVEg2HxCNufk6QcWlFsBkhQyQroB2VWKw=="], + + "emoji-regex": ["emoji-regex@10.3.0", "", {}, "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="], + + "esbuild": ["esbuild@0.14.54", "", { "dependencies": { "@esbuild/linux-loong64": "0.14.54", "esbuild-android-64": "0.14.54", "esbuild-android-arm64": "0.14.54", "esbuild-darwin-64": "0.14.54", "esbuild-darwin-arm64": "0.14.54", "esbuild-freebsd-64": "0.14.54", "esbuild-freebsd-arm64": "0.14.54", "esbuild-linux-32": "0.14.54", "esbuild-linux-64": "0.14.54", "esbuild-linux-arm": "0.14.54", "esbuild-linux-arm64": "0.14.54", "esbuild-linux-mips64le": "0.14.54", "esbuild-linux-ppc64le": "0.14.54", "esbuild-linux-riscv64": "0.14.54", "esbuild-linux-s390x": "0.14.54", "esbuild-netbsd-64": "0.14.54", "esbuild-openbsd-64": "0.14.54", "esbuild-sunos-64": "0.14.54", "esbuild-windows-32": "0.14.54", "esbuild-windows-64": "0.14.54", "esbuild-windows-arm64": "0.14.54" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA=="], + + "esbuild-android-64": ["esbuild-android-64@0.14.54", "", { "os": "android", "cpu": "x64" }, "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ=="], + + "esbuild-android-arm64": ["esbuild-android-arm64@0.14.54", "", { "os": "android", "cpu": "arm64" }, "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg=="], + + "esbuild-darwin-64": ["esbuild-darwin-64@0.14.54", "", { "os": "darwin", "cpu": "x64" }, "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug=="], + + "esbuild-darwin-arm64": ["esbuild-darwin-arm64@0.14.54", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw=="], + + "esbuild-freebsd-64": ["esbuild-freebsd-64@0.14.54", "", { "os": "freebsd", "cpu": "x64" }, "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg=="], + + "esbuild-freebsd-arm64": ["esbuild-freebsd-arm64@0.14.54", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q=="], + + "esbuild-linux-32": ["esbuild-linux-32@0.14.54", "", { "os": "linux", "cpu": "ia32" }, "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw=="], + + "esbuild-linux-64": ["esbuild-linux-64@0.14.54", "", { "os": "linux", "cpu": "x64" }, "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg=="], + + "esbuild-linux-arm": ["esbuild-linux-arm@0.14.54", "", { "os": "linux", "cpu": "arm" }, "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw=="], + + "esbuild-linux-arm64": ["esbuild-linux-arm64@0.14.54", "", { "os": "linux", "cpu": "arm64" }, "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig=="], + + "esbuild-linux-mips64le": ["esbuild-linux-mips64le@0.14.54", "", { "os": "linux", "cpu": "none" }, "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw=="], + + "esbuild-linux-ppc64le": ["esbuild-linux-ppc64le@0.14.54", "", { "os": "linux", "cpu": "ppc64" }, "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ=="], + + "esbuild-linux-riscv64": ["esbuild-linux-riscv64@0.14.54", "", { "os": "linux", "cpu": "none" }, "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg=="], + + "esbuild-linux-s390x": ["esbuild-linux-s390x@0.14.54", "", { "os": "linux", "cpu": "s390x" }, "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA=="], + + "esbuild-netbsd-64": ["esbuild-netbsd-64@0.14.54", "", { "os": "none", "cpu": "x64" }, "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w=="], + + "esbuild-openbsd-64": ["esbuild-openbsd-64@0.14.54", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw=="], + + "esbuild-sunos-64": ["esbuild-sunos-64@0.14.54", "", { "os": "sunos", "cpu": "x64" }, "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw=="], + + "esbuild-windows-32": ["esbuild-windows-32@0.14.54", "", { "os": "win32", "cpu": "ia32" }, "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w=="], + + "esbuild-windows-64": ["esbuild-windows-64@0.14.54", "", { "os": "win32", "cpu": "x64" }, "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ=="], + + "esbuild-windows-arm64": ["esbuild-windows-arm64@0.14.54", "", { "os": "win32", "cpu": "arm64" }, "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg=="], + + "escalade": ["escalade@3.1.1", "", {}, "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="], + + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "event-stream": ["event-stream@3.3.4", "", { "dependencies": { "duplexer": "~0.1.1", "from": "~0", "map-stream": "~0.1.0", "pause-stream": "0.0.11", "split": "0.3", "stream-combiner": "~0.0.4", "through": "~2.3.1" } }, "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g=="], + + "eventemitter3": ["eventemitter3@5.0.0", "", {}, "sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], + + "fastq": ["fastq@1.15.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw=="], + + "fdir": ["fdir@6.1.0", "", { "peerDependencies": { "picomatch": "2.x" } }, "sha512-274qhz5PxNnA/fybOu6apTCUnM0GnO3QazB6VH+oag/7DQskdYq8lm07ZSm90kEQuWYH5GvjAxGruuHrEr0bcg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "fill-range": ["fill-range@7.0.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "from": ["from@0.1.7", "", {}, "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="], + + "fs-extra": ["fs-extra@11.2.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="], + + "fx": ["fx@31.0.0", "", { "bin": { "fx": "index.js" } }, "sha512-OoeYSPKqNKmfnH4s+rGYI0c8OZmqqOOXsUtqy0YyHqQQoQSDiDs3m3M9uXKx5OQR+jDx7/FhYqpO3kl/As/xgg=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-east-asian-width": ["get-east-asian-width@1.2.0", "", {}, "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "globby": ["globby@13.2.2", "", { "dependencies": { "dir-glob": "^3.0.1", "fast-glob": "^3.3.0", "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "ignore": ["ignore@5.3.0", "", {}, "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@2.5.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "map-stream": ["map-stream@0.1.0", "", {}, "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mitata": ["mitata@1.0.25", "", {}, "sha512-0v5qZtVW5vwj9FDvYfraR31BMDcRLkhSFWPTLaxx/Z3/EvScfVtAAWtMI2ArIbBcwh7P86dXh0lQWKiXQPlwYA=="], + + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.1", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow=="], + + "node-releases": ["node-releases@2.0.10", "", {}, "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w=="], + + "npm-run-path": ["npm-run-path@5.2.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pause-stream": ["pause-stream@0.0.11", "", { "dependencies": { "through": "~2.3" } }, "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A=="], + + "picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], + + "ps-tree": ["ps-tree@1.2.0", "", { "dependencies": { "event-stream": "=3.3.4" }, "bin": { "ps-tree": "./bin/ps-tree.js" } }, "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@6.3.0", "", { "bin": { "semver": "./bin/semver.js" } }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="], + + "split": ["split@0.3.3", "", { "dependencies": { "through": "2" } }, "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA=="], + + "stream-combiner": ["stream-combiner@0.0.4", "", { "dependencies": { "duplexer": "~0.1.1" } }, "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw=="], + + "string-width": ["string-width@7.1.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + + "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "update-browserslist-db": ["update-browserslist-db@1.0.10", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "browserslist-lint": "cli.js" } }, "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.2", "", {}, "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ=="], + + "webpod": ["webpod@0.0.2", "", { "bin": { "webpod": "dist/index.js" } }, "sha512-cSwwQIeg8v4i3p4ajHhwgR7N6VyxAf+KYSSsY6Pd3aETE+xEU4vbitz7qQkB0I321xnhDdgtxuiSfk5r/FVtjg=="], + + "which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.3.4", "", {}, "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA=="], + + "zx": ["zx@7.2.3", "", { "dependencies": { "@types/fs-extra": "^11.0.1", "@types/minimist": "^1.2.2", "@types/node": "^18.16.3", "@types/ps-tree": "^1.1.2", "@types/which": "^3.0.0", "chalk": "^5.2.0", "fs-extra": "^11.1.1", "fx": "*", "globby": "^13.1.4", "minimist": "^1.2.8", "node-fetch": "3.3.1", "ps-tree": "^1.2.0", "webpod": "^0", "which": "^3.0.0", "yaml": "^2.2.2" }, "bin": { "zx": "build/cli.js" } }, "sha512-QODu38nLlYXg/B/Gw7ZKiZrvPkEsjPN3LQ5JFXM7h0JvwhEdPNNl+4Ao1y4+o3CLNiDUNcwzQYZ4/Ko7kKzCMA=="], + + "@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.2", "", { "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A=="], + + "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + } +} diff --git a/bench/bun.lockb b/bench/bun.lockb deleted file mode 100755 index e77a3b406c..0000000000 Binary files a/bench/bun.lockb and /dev/null differ diff --git a/bench/expect-to-equal/bun.lock b/bench/expect-to-equal/bun.lock new file mode 100644 index 0000000000..19f96c777c --- /dev/null +++ b/bench/expect-to-equal/bun.lock @@ -0,0 +1,669 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "expect-to-equal", + "devDependencies": { + "jest": "^29.3.1", + "vitest": "^0.25.3", + }, + }, + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.2.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w=="], + + "@babel/code-frame": ["@babel/code-frame@7.18.6", "", { "dependencies": { "@babel/highlight": "^7.18.6" } }, "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q=="], + + "@babel/compat-data": ["@babel/compat-data@7.20.14", "", {}, "sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw=="], + + "@babel/core": ["@babel/core@7.20.12", "", { "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", "@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-module-transforms": "^7.20.11", "@babel/helpers": "^7.20.7", "@babel/parser": "^7.20.7", "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.12", "@babel/types": "^7.20.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.2", "semver": "^6.3.0" } }, "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg=="], + + "@babel/generator": ["@babel/generator@7.20.14", "", { "dependencies": { "@babel/types": "^7.20.7", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" } }, "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.20.7", "", { "dependencies": { "@babel/compat-data": "^7.20.5", "@babel/helper-validator-option": "^7.18.6", "browserslist": "^4.21.3", "lru-cache": "^5.1.1", "semver": "^6.3.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ=="], + + "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.18.9", "", {}, "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg=="], + + "@babel/helper-function-name": ["@babel/helper-function-name@7.19.0", "", { "dependencies": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" } }, "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w=="], + + "@babel/helper-hoist-variables": ["@babel/helper-hoist-variables@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.20.11", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", "@babel/helper-simple-access": "^7.20.2", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.10", "@babel/types": "^7.20.7" } }, "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.20.2", "", {}, "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ=="], + + "@babel/helper-simple-access": ["@babel/helper-simple-access@7.20.2", "", { "dependencies": { "@babel/types": "^7.20.2" } }, "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA=="], + + "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.19.4", "", {}, "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.19.1", "", {}, "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.18.6", "", {}, "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw=="], + + "@babel/helpers": ["@babel/helpers@7.20.13", "", { "dependencies": { "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.13", "@babel/types": "^7.20.7" } }, "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg=="], + + "@babel/highlight": ["@babel/highlight@7.18.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g=="], + + "@babel/parser": ["@babel/parser@7.20.15", "", { "bin": "./bin/babel-parser.js" }, "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg=="], + + "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], + + "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], + + "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q=="], + + "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], + + "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], + + "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], + + "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], + + "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], + + "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], + + "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.20.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.19.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ=="], + + "@babel/template": ["@babel/template@7.20.7", "", { "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7" } }, "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw=="], + + "@babel/traverse": ["@babel/traverse@7.20.13", "", { "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/parser": "^7.20.13", "@babel/types": "^7.20.7", "debug": "^4.1.0", "globals": "^11.1.0" } }, "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ=="], + + "@babel/types": ["@babel/types@7.20.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.16.17", "", { "os": "android", "cpu": "arm" }, "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.16.17", "", { "os": "android", "cpu": "arm64" }, "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.16.17", "", { "os": "android", "cpu": "x64" }, "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.16.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.16.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.16.17", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.16.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.16.17", "", { "os": "linux", "cpu": "arm" }, "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.16.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.16.17", "", { "os": "linux", "cpu": "ia32" }, "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.16.17", "", { "os": "linux", "cpu": "none" }, "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.16.17", "", { "os": "linux", "cpu": "none" }, "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.16.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.16.17", "", { "os": "linux", "cpu": "none" }, "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.16.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.16.17", "", { "os": "linux", "cpu": "x64" }, "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.16.17", "", { "os": "none", "cpu": "x64" }, "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.16.17", "", { "os": "openbsd", "cpu": "x64" }, "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.16.17", "", { "os": "sunos", "cpu": "x64" }, "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.16.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.16.17", "", { "os": "win32", "cpu": "ia32" }, "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.16.17", "", { "os": "win32", "cpu": "x64" }, "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q=="], + + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jest/console": ["@jest/console@29.4.3", "", { "dependencies": { "@jest/types": "^29.4.3", "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.4.3", "jest-util": "^29.4.3", "slash": "^3.0.0" } }, "sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A=="], + + "@jest/core": ["@jest/core@29.4.3", "", { "dependencies": { "@jest/console": "^29.4.3", "@jest/reporters": "^29.4.3", "@jest/test-result": "^29.4.3", "@jest/transform": "^29.4.3", "@jest/types": "^29.4.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.4.3", "jest-config": "^29.4.3", "jest-haste-map": "^29.4.3", "jest-message-util": "^29.4.3", "jest-regex-util": "^29.4.3", "jest-resolve": "^29.4.3", "jest-resolve-dependencies": "^29.4.3", "jest-runner": "^29.4.3", "jest-runtime": "^29.4.3", "jest-snapshot": "^29.4.3", "jest-util": "^29.4.3", "jest-validate": "^29.4.3", "jest-watcher": "^29.4.3", "micromatch": "^4.0.4", "pretty-format": "^29.4.3", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" } }, "sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ=="], + + "@jest/environment": ["@jest/environment@29.4.3", "", { "dependencies": { "@jest/fake-timers": "^29.4.3", "@jest/types": "^29.4.3", "@types/node": "*", "jest-mock": "^29.4.3" } }, "sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA=="], + + "@jest/expect": ["@jest/expect@29.4.3", "", { "dependencies": { "expect": "^29.4.3", "jest-snapshot": "^29.4.3" } }, "sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ=="], + + "@jest/expect-utils": ["@jest/expect-utils@29.4.3", "", { "dependencies": { "jest-get-type": "^29.4.3" } }, "sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ=="], + + "@jest/fake-timers": ["@jest/fake-timers@29.4.3", "", { "dependencies": { "@jest/types": "^29.4.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.4.3", "jest-mock": "^29.4.3", "jest-util": "^29.4.3" } }, "sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw=="], + + "@jest/globals": ["@jest/globals@29.4.3", "", { "dependencies": { "@jest/environment": "^29.4.3", "@jest/expect": "^29.4.3", "@jest/types": "^29.4.3", "jest-mock": "^29.4.3" } }, "sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA=="], + + "@jest/reporters": ["@jest/reporters@29.4.3", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.4.3", "@jest/test-result": "^29.4.3", "@jest/transform": "^29.4.3", "@jest/types": "^29.4.3", "@jridgewell/trace-mapping": "^0.3.15", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^5.1.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.4.3", "jest-util": "^29.4.3", "jest-worker": "^29.4.3", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" } }, "sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg=="], + + "@jest/schemas": ["@jest/schemas@29.4.3", "", { "dependencies": { "@sinclair/typebox": "^0.25.16" } }, "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg=="], + + "@jest/source-map": ["@jest/source-map@29.4.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.15", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w=="], + + "@jest/test-result": ["@jest/test-result@29.4.3", "", { "dependencies": { "@jest/console": "^29.4.3", "@jest/types": "^29.4.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA=="], + + "@jest/test-sequencer": ["@jest/test-sequencer@29.4.3", "", { "dependencies": { "@jest/test-result": "^29.4.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.4.3", "slash": "^3.0.0" } }, "sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw=="], + + "@jest/transform": ["@jest/transform@29.4.3", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.4.3", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.4.3", "jest-regex-util": "^29.4.3", "jest-util": "^29.4.3", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg=="], + + "@jest/types": ["@jest/types@29.4.3", "", { "dependencies": { "@jest/schemas": "^29.4.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.2", "", { "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.0", "", {}, "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.1.2", "", {}, "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.14", "", {}, "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.17", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" } }, "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.25.23", "", {}, "sha512-VEB8ygeP42CFLWyAJhN5OklpxUliqdNEUcXb4xZ/CINqtYGTjL5ukluKdKzQ0iWdUxyQ7B0539PAUhHKrCNWSQ=="], + + "@sinonjs/commons": ["@sinonjs/commons@2.0.0", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.0.2", "", { "dependencies": { "@sinonjs/commons": "^2.0.0" } }, "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw=="], + + "@types/babel__core": ["@types/babel__core@7.20.0", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ=="], + + "@types/babel__generator": ["@types/babel__generator@7.6.4", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg=="], + + "@types/babel__template": ["@types/babel__template@7.4.1", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.18.3", "", { "dependencies": { "@babel/types": "^7.3.0" } }, "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w=="], + + "@types/chai": ["@types/chai@4.3.4", "", {}, "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw=="], + + "@types/chai-subset": ["@types/chai-subset@1.3.3", "", { "dependencies": { "@types/chai": "*" } }, "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw=="], + + "@types/graceful-fs": ["@types/graceful-fs@4.1.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw=="], + + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.4", "", {}, "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.0", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.1", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw=="], + + "@types/node": ["@types/node@18.14.0", "", {}, "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A=="], + + "@types/prettier": ["@types/prettier@2.7.2", "", {}, "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg=="], + + "@types/stack-utils": ["@types/stack-utils@2.0.1", "", {}, "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="], + + "@types/yargs": ["@types/yargs@17.0.22", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g=="], + + "@types/yargs-parser": ["@types/yargs-parser@20.2.2", "", {}, "sha512-sUWMriymrSqTvxCmCkf+7k392TNDcMJBHI1/rysWJxKnWAan/Zk4gZ/GEieSRo4EqIEPpbGU3Sd/0KTRoIA3pA=="], + + "acorn": ["acorn@8.8.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw=="], + + "acorn-walk": ["acorn-walk@8.2.0", "", {}, "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], + + "babel-jest": ["babel-jest@29.4.3", "", { "dependencies": { "@jest/transform": "^29.4.3", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.4.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw=="], + + "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + + "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.4.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q=="], + + "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.0.1", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.8.3", "@babel/plugin-syntax-import-meta": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.8.3", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-top-level-await": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ=="], + + "babel-preset-jest": ["babel-preset-jest@29.4.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.4.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.2", "", { "dependencies": { "fill-range": "^7.0.1" } }, "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A=="], + + "browserslist": ["browserslist@4.21.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001449", "electron-to-chromium": "^1.4.284", "node-releases": "^2.0.8", "update-browserslist-db": "^1.0.10" }, "bin": { "browserslist": "cli.js" } }, "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w=="], + + "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001456", "", {}, "sha512-XFHJY5dUgmpMV25UqaD4kVq2LsiaU5rS8fb0f17pCoXQiQslzmFgnfOxfvo1bTpTqf7dwG/N/05CnLCnOEKmzA=="], + + "chai": ["chai@4.3.7", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", "deep-eql": "^4.1.2", "get-func-name": "^2.0.0", "loupe": "^2.3.1", "pathval": "^1.1.1", "type-detect": "^4.0.5" } }, "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + + "check-error": ["check-error@1.0.2", "", {}, "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA=="], + + "ci-info": ["ci-info@3.8.0", "", {}, "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.2.2", "", {}, "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="], + + "collect-v8-coverage": ["collect-v8-coverage@1.0.1", "", {}, "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="], + + "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], + + "deep-eql": ["deep-eql@4.1.3", "", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw=="], + + "deepmerge": ["deepmerge@4.3.0", "", {}, "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og=="], + + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], + + "diff-sequences": ["diff-sequences@29.4.3", "", {}, "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA=="], + + "electron-to-chromium": ["electron-to-chromium@1.4.302", "", {}, "sha512-Uk7C+7aPBryUR1Fwvk9VmipBcN9fVsqBO57jV2ZjTm+IZ6BMNqu7EDVEg2HxCNufk6QcWlFsBkhQyQroB2VWKw=="], + + "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + + "esbuild": ["esbuild@0.16.17", "", { "dependencies": { "@esbuild/android-arm": "0.16.17", "@esbuild/android-arm64": "0.16.17", "@esbuild/android-x64": "0.16.17", "@esbuild/darwin-arm64": "0.16.17", "@esbuild/darwin-x64": "0.16.17", "@esbuild/freebsd-arm64": "0.16.17", "@esbuild/freebsd-x64": "0.16.17", "@esbuild/linux-arm": "0.16.17", "@esbuild/linux-arm64": "0.16.17", "@esbuild/linux-ia32": "0.16.17", "@esbuild/linux-loong64": "0.16.17", "@esbuild/linux-mips64el": "0.16.17", "@esbuild/linux-ppc64": "0.16.17", "@esbuild/linux-riscv64": "0.16.17", "@esbuild/linux-s390x": "0.16.17", "@esbuild/linux-x64": "0.16.17", "@esbuild/netbsd-x64": "0.16.17", "@esbuild/openbsd-x64": "0.16.17", "@esbuild/sunos-x64": "0.16.17", "@esbuild/win32-arm64": "0.16.17", "@esbuild/win32-ia32": "0.16.17", "@esbuild/win32-x64": "0.16.17" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg=="], + + "escalade": ["escalade@3.1.1", "", {}, "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="], + + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="], + + "expect": ["expect@29.4.3", "", { "dependencies": { "@jest/expect-utils": "^29.4.3", "jest-get-type": "^29.4.3", "jest-matcher-utils": "^29.4.3", "jest-message-util": "^29.4.3", "jest-util": "^29.4.3" } }, "sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + + "fill-range": ["fill-range@7.0.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "function-bind": ["function-bind@1.1.1", "", {}, "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-func-name": ["get-func-name@2.0.0", "", {}, "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig=="], + + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], + + "has": ["has@1.0.3", "", { "dependencies": { "function-bind": "^1.1.1" } }, "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "import-local": ["import-local@3.1.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-core-module": ["is-core-module@2.11.0", "", { "dependencies": { "has": "^1.0.3" } }, "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.0", "", {}, "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.0", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^3.0.0", "supports-color": "^7.1.0" } }, "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="], + + "istanbul-reports": ["istanbul-reports@3.1.5", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w=="], + + "jest": ["jest@29.4.3", "", { "dependencies": { "@jest/core": "^29.4.3", "@jest/types": "^29.4.3", "import-local": "^3.0.2", "jest-cli": "^29.4.3" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "bin": { "jest": "bin/jest.js" } }, "sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA=="], + + "jest-changed-files": ["jest-changed-files@29.4.3", "", { "dependencies": { "execa": "^5.0.0", "p-limit": "^3.1.0" } }, "sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ=="], + + "jest-circus": ["jest-circus@29.4.3", "", { "dependencies": { "@jest/environment": "^29.4.3", "@jest/expect": "^29.4.3", "@jest/test-result": "^29.4.3", "@jest/types": "^29.4.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", "jest-each": "^29.4.3", "jest-matcher-utils": "^29.4.3", "jest-message-util": "^29.4.3", "jest-runtime": "^29.4.3", "jest-snapshot": "^29.4.3", "jest-util": "^29.4.3", "p-limit": "^3.1.0", "pretty-format": "^29.4.3", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw=="], + + "jest-cli": ["jest-cli@29.4.3", "", { "dependencies": { "@jest/core": "^29.4.3", "@jest/test-result": "^29.4.3", "@jest/types": "^29.4.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", "jest-config": "^29.4.3", "jest-util": "^29.4.3", "jest-validate": "^29.4.3", "prompts": "^2.0.1", "yargs": "^17.3.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "bin": { "jest": "bin/jest.js" } }, "sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg=="], + + "jest-config": ["jest-config@29.4.3", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.4.3", "@jest/types": "^29.4.3", "babel-jest": "^29.4.3", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.4.3", "jest-environment-node": "^29.4.3", "jest-get-type": "^29.4.3", "jest-regex-util": "^29.4.3", "jest-resolve": "^29.4.3", "jest-runner": "^29.4.3", "jest-util": "^29.4.3", "jest-validate": "^29.4.3", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.4.3", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "optionalPeers": ["ts-node"] }, "sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ=="], + + "jest-diff": ["jest-diff@29.4.3", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.4.3", "jest-get-type": "^29.4.3", "pretty-format": "^29.4.3" } }, "sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA=="], + + "jest-docblock": ["jest-docblock@29.4.3", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg=="], + + "jest-each": ["jest-each@29.4.3", "", { "dependencies": { "@jest/types": "^29.4.3", "chalk": "^4.0.0", "jest-get-type": "^29.4.3", "jest-util": "^29.4.3", "pretty-format": "^29.4.3" } }, "sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q=="], + + "jest-environment-node": ["jest-environment-node@29.4.3", "", { "dependencies": { "@jest/environment": "^29.4.3", "@jest/fake-timers": "^29.4.3", "@jest/types": "^29.4.3", "@types/node": "*", "jest-mock": "^29.4.3", "jest-util": "^29.4.3" } }, "sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg=="], + + "jest-get-type": ["jest-get-type@29.4.3", "", {}, "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg=="], + + "jest-haste-map": ["jest-haste-map@29.4.3", "", { "dependencies": { "@jest/types": "^29.4.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.4.3", "jest-util": "^29.4.3", "jest-worker": "^29.4.3", "micromatch": "^4.0.4", "walker": "^1.0.8" } }, "sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ=="], + + "jest-leak-detector": ["jest-leak-detector@29.4.3", "", { "dependencies": { "jest-get-type": "^29.4.3", "pretty-format": "^29.4.3" } }, "sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA=="], + + "jest-matcher-utils": ["jest-matcher-utils@29.4.3", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.4.3", "jest-get-type": "^29.4.3", "pretty-format": "^29.4.3" } }, "sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg=="], + + "jest-message-util": ["jest-message-util@29.4.3", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.4.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.4.3", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw=="], + + "jest-mock": ["jest-mock@29.4.3", "", { "dependencies": { "@jest/types": "^29.4.3", "@types/node": "*", "jest-util": "^29.4.3" } }, "sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg=="], + + "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" } }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], + + "jest-regex-util": ["jest-regex-util@29.4.3", "", {}, "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg=="], + + "jest-resolve": ["jest-resolve@29.4.3", "", { "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.4.3", "jest-pnp-resolver": "^1.2.2", "jest-util": "^29.4.3", "jest-validate": "^29.4.3", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" } }, "sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw=="], + + "jest-resolve-dependencies": ["jest-resolve-dependencies@29.4.3", "", { "dependencies": { "jest-regex-util": "^29.4.3", "jest-snapshot": "^29.4.3" } }, "sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw=="], + + "jest-runner": ["jest-runner@29.4.3", "", { "dependencies": { "@jest/console": "^29.4.3", "@jest/environment": "^29.4.3", "@jest/test-result": "^29.4.3", "@jest/transform": "^29.4.3", "@jest/types": "^29.4.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.4.3", "jest-environment-node": "^29.4.3", "jest-haste-map": "^29.4.3", "jest-leak-detector": "^29.4.3", "jest-message-util": "^29.4.3", "jest-resolve": "^29.4.3", "jest-runtime": "^29.4.3", "jest-util": "^29.4.3", "jest-watcher": "^29.4.3", "jest-worker": "^29.4.3", "p-limit": "^3.1.0", "source-map-support": "0.5.13" } }, "sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA=="], + + "jest-runtime": ["jest-runtime@29.4.3", "", { "dependencies": { "@jest/environment": "^29.4.3", "@jest/fake-timers": "^29.4.3", "@jest/globals": "^29.4.3", "@jest/source-map": "^29.4.3", "@jest/test-result": "^29.4.3", "@jest/transform": "^29.4.3", "@jest/types": "^29.4.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.4.3", "jest-message-util": "^29.4.3", "jest-mock": "^29.4.3", "jest-regex-util": "^29.4.3", "jest-resolve": "^29.4.3", "jest-snapshot": "^29.4.3", "jest-util": "^29.4.3", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q=="], + + "jest-snapshot": ["jest-snapshot@29.4.3", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.4.3", "@jest/transform": "^29.4.3", "@jest/types": "^29.4.3", "@types/babel__traverse": "^7.0.6", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.4.3", "graceful-fs": "^4.2.9", "jest-diff": "^29.4.3", "jest-get-type": "^29.4.3", "jest-haste-map": "^29.4.3", "jest-matcher-utils": "^29.4.3", "jest-message-util": "^29.4.3", "jest-util": "^29.4.3", "natural-compare": "^1.4.0", "pretty-format": "^29.4.3", "semver": "^7.3.5" } }, "sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw=="], + + "jest-util": ["jest-util@29.4.3", "", { "dependencies": { "@jest/types": "^29.4.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q=="], + + "jest-validate": ["jest-validate@29.4.3", "", { "dependencies": { "@jest/types": "^29.4.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.4.3", "leven": "^3.1.0", "pretty-format": "^29.4.3" } }, "sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw=="], + + "jest-watcher": ["jest-watcher@29.4.3", "", { "dependencies": { "@jest/test-result": "^29.4.3", "@jest/types": "^29.4.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", "jest-util": "^29.4.3", "string-length": "^4.0.1" } }, "sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA=="], + + "jest-worker": ["jest-worker@29.4.3", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.4.3", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "jsesc": ["jsesc@2.5.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "local-pkg": ["local-pkg@0.4.3", "", {}, "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "loupe": ["loupe@2.3.6", "", { "dependencies": { "get-func-name": "^2.0.0" } }, "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "nanoid": ["nanoid@3.3.4", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + + "node-releases": ["node-releases@2.0.10", "", {}, "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + + "picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pirates": ["pirates@4.0.5", "", {}, "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ=="], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + + "postcss": ["postcss@8.4.21", "", { "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg=="], + + "pretty-format": ["pretty-format@29.4.3", "", { "dependencies": { "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "react-is": ["react-is@18.2.0", "", {}, "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve": ["resolve@1.22.1", "", { "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw=="], + + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve.exports": ["resolve.exports@2.0.0", "", {}, "sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg=="], + + "rollup": ["rollup@3.17.0", "", { "dependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-0zZQ0J4p0ZtTla6l8sheDTUyNfGZQDpU5h0nPHf6xtUXIzKK70LmB2IRR0wLnzaL8a02fjmsJy+XCncbSwOpjg=="], + + "semver": ["semver@7.3.8", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.0.2", "", {}, "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="], + + "source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strip-literal": ["strip-literal@1.0.1", "", { "dependencies": { "acorn": "^8.8.2" } }, "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + + "tinybench": ["tinybench@2.3.1", "", {}, "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA=="], + + "tinypool": ["tinypool@0.3.1", "", {}, "sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ=="], + + "tinyspy": ["tinyspy@1.1.1", "", {}, "sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g=="], + + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], + + "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "update-browserslist-db": ["update-browserslist-db@1.0.10", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "browserslist-lint": "cli.js" } }, "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ=="], + + "v8-to-istanbul": ["v8-to-istanbul@9.1.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^1.6.0" } }, "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA=="], + + "vite": ["vite@4.1.2", "", { "dependencies": { "esbuild": "^0.16.14", "fsevents": "~2.3.2", "postcss": "^8.4.21", "resolve": "^1.22.1", "rollup": "^3.10.0" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["less", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-MWDb9Rfy3DI8omDQySbMK93nQqStwbsQWejXRY2EBzEWKmLAXWb1mkI9Yw2IJrc+oCvPCI1Os5xSSIBYY6DEAw=="], + + "vitest": ["vitest@0.25.8", "", { "dependencies": { "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", "acorn": "^8.8.1", "acorn-walk": "^8.2.0", "chai": "^4.3.7", "debug": "^4.3.4", "local-pkg": "^0.4.2", "source-map": "^0.6.1", "strip-literal": "^1.0.0", "tinybench": "^2.3.1", "tinypool": "^0.3.0", "tinyspy": "^1.0.2", "vite": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@vitest/browser": "*", "@vitest/ui": "*", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-X75TApG2wZTJn299E/TIYevr4E9/nBo1sUtZzn0Ci5oK8qnpZAZyhwg0qCeMSakGIWtc6oRwcQFyFfW14aOFWg=="], + + "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yargs": ["yargs@17.7.0", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@ampproject/remapping/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.1.1", "", { "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w=="], + + "@babel/core/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "@babel/core/semver": ["semver@6.3.0", "", { "bin": { "semver": "./bin/semver.js" } }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.0", "", { "bin": { "semver": "./bin/semver.js" } }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="], + + "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "@types/chai-subset/@types/chai": ["@types/chai@2.0.4", "", {}, "sha512-DNUVOufLw3znPcZaEvjAER3F1KScpGSrp3n8eIE5R7rdjTJE2w4orUiVY3KHBzvGCx0++ZlymkAzjroa76NPRA=="], + + "istanbul-lib-instrument/semver": ["semver@6.3.0", "", { "bin": { "semver": "./bin/semver.js" } }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "make-dir/semver": ["semver@6.3.0", "", { "bin": { "semver": "./bin/semver.js" } }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="], + + "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "test-exclude/minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="], + + "v8-to-istanbul/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + } +} diff --git a/bench/expect-to-equal/bun.lockb b/bench/expect-to-equal/bun.lockb deleted file mode 100755 index a56b820270..0000000000 Binary files a/bench/expect-to-equal/bun.lockb and /dev/null differ diff --git a/bench/ffi/src/bun.lock b/bench/ffi/src/bun.lock new file mode 100644 index 0000000000..8b75ff131f --- /dev/null +++ b/bench/ffi/src/bun.lock @@ -0,0 +1,19 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "bench", + "dependencies": { + "@napi-rs/cli": "^2.10.1", + "@node-rs/helper": "^1.3.3", + }, + }, + }, + "packages": { + "@napi-rs/cli": ["@napi-rs/cli@2.14.8", "", { "bin": { "napi": "scripts/index.js" } }, "sha512-IvA3s8BqohMdUbOkFn7+23u1dhIJZCkA8Xps7DD4SLdCMbcbUF6MUuKiqxuqmVHBFTaxU25sU56WdX3efqGgPw=="], + + "@napi-rs/triples": ["@napi-rs/triples@1.1.0", "", {}, "sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w=="], + + "@node-rs/helper": ["@node-rs/helper@1.3.3", "", { "dependencies": { "@napi-rs/triples": "^1.1.0" } }, "sha512-p4OdfQObGN9YFy5WZaGwlPYICQSe7xZYyXB0sxREmvj1HzGKp5bPg2PlfgfMZEfnjIA882B9ZrnagYzZihIwjA=="], + } +} diff --git a/bench/ffi/src/bun.lockb b/bench/ffi/src/bun.lockb deleted file mode 100755 index 0b1e2969be..0000000000 Binary files a/bench/ffi/src/bun.lockb and /dev/null differ diff --git a/bench/gzip/bun.lock b/bench/gzip/bun.lock new file mode 100644 index 0000000000..2ec377b357 --- /dev/null +++ b/bench/gzip/bun.lock @@ -0,0 +1,14 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "bench", + "dependencies": { + "@babel/standalone": "7.24.10", + }, + }, + }, + "packages": { + "@babel/standalone": ["@babel/standalone@7.24.10", "", {}, "sha512-nGC37EKfmelpyCXto1pw6SBkD5ZQRdMbL6WISi28xWit9dtiy9dChU1WgEfzturUTxrmOGkMDRrCydFMA7uOaQ=="], + } +} diff --git a/bench/gzip/bun.lockb b/bench/gzip/bun.lockb deleted file mode 100755 index 96feac4287..0000000000 Binary files a/bench/gzip/bun.lockb and /dev/null differ diff --git a/bench/hot-module-reloading/css-stress-test/bun.lock b/bench/hot-module-reloading/css-stress-test/bun.lock new file mode 100644 index 0000000000..224235c38d --- /dev/null +++ b/bench/hot-module-reloading/css-stress-test/bun.lock @@ -0,0 +1,2434 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "simple-react", + "dependencies": { + "@emotion/core": "latest", + "@emotion/css": "latest", + "@emotion/react": "latest", + "@vitejs/plugin-react-refresh": "^1.3.3", + "antd": "^4.16.1", + "bun-framework-next": "latest", + "left-pad": "^1.3.0", + "next": "^12", + "parcel": "2.0.0-beta.3", + "path-browserify": "^1.0.1", + "percentile": "^1.5.0", + "puppeteer": "^10.4.0", + "puppeteer-video-recorder": "^1.0.5", + "react": "^17.0.2", + "react-bootstrap": "^1.6.1", + "react-dom": "^17.0.2", + "react-form": "^4.0.1", + "react-hook-form": "^7.8.3", + "url": "^0.11.0", + "wipwipwipwip-next-donotuse": "4.0.0", + }, + "devDependencies": { + "@microsoft/fetch-event-source": "^2.0.1", + "@snowpack/plugin-react-refresh": "^2.5.0", + "typescript": "^4.3.4", + }, + }, + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.2.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w=="], + + "@ant-design/colors": ["@ant-design/colors@6.0.0", "", { "dependencies": { "@ctrl/tinycolor": "^3.4.0" } }, "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ=="], + + "@ant-design/icons": ["@ant-design/icons@4.8.0", "", { "dependencies": { "@ant-design/colors": "^6.0.0", "@ant-design/icons-svg": "^4.2.1", "@babel/runtime": "^7.11.2", "classnames": "^2.2.6", "rc-util": "^5.9.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-T89P2jG2vM7OJ0IfGx2+9FC5sQjtTzRSz+mCHTXkFn/ELZc2YpfStmYHmqzq2Jx55J0F7+O6i5/ZKFSVNWCKNg=="], + + "@ant-design/icons-svg": ["@ant-design/icons-svg@4.2.1", "", {}, "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw=="], + + "@ant-design/react-slick": ["@ant-design/react-slick@0.29.2", "", { "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", "json2mq": "^0.2.0", "lodash": "^4.17.21", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0" } }, "sha512-kgjtKmkGHa19FW21lHnAfyyH9AAoh35pBdcJ53rHmQ3O+cfFHGHnUbj/HFrRNJ5vIts09FKJVAD8RpaC+RaWfA=="], + + "@babel/code-frame": ["@babel/code-frame@7.18.6", "", { "dependencies": { "@babel/highlight": "^7.18.6" } }, "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q=="], + + "@babel/compat-data": ["@babel/compat-data@7.20.14", "", {}, "sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw=="], + + "@babel/core": ["@babel/core@7.20.12", "", { "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", "@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-module-transforms": "^7.20.11", "@babel/helpers": "^7.20.7", "@babel/parser": "^7.20.7", "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.12", "@babel/types": "^7.20.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.2", "semver": "^6.3.0" } }, "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg=="], + + "@babel/generator": ["@babel/generator@7.20.14", "", { "dependencies": { "@babel/types": "^7.20.7", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" } }, "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.20.7", "", { "dependencies": { "@babel/compat-data": "^7.20.5", "@babel/helper-validator-option": "^7.18.6", "browserslist": "^4.21.3", "lru-cache": "^5.1.1", "semver": "^6.3.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ=="], + + "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.18.9", "", {}, "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg=="], + + "@babel/helper-function-name": ["@babel/helper-function-name@7.19.0", "", { "dependencies": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" } }, "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w=="], + + "@babel/helper-hoist-variables": ["@babel/helper-hoist-variables@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.20.11", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", "@babel/helper-simple-access": "^7.20.2", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.10", "@babel/types": "^7.20.7" } }, "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.20.2", "", {}, "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ=="], + + "@babel/helper-simple-access": ["@babel/helper-simple-access@7.20.2", "", { "dependencies": { "@babel/types": "^7.20.2" } }, "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA=="], + + "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.19.4", "", {}, "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.19.1", "", {}, "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.18.6", "", {}, "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw=="], + + "@babel/helpers": ["@babel/helpers@7.20.13", "", { "dependencies": { "@babel/template": "^7.20.7", "@babel/traverse": "^7.20.13", "@babel/types": "^7.20.7" } }, "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg=="], + + "@babel/highlight": ["@babel/highlight@7.18.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g=="], + + "@babel/parser": ["@babel/parser@7.20.15", "", { "bin": "./bin/babel-parser.js" }, "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg=="], + + "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], + + "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A=="], + + "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.19.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.19.0", "@babel/plugin-syntax-flow": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.19.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.19.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ=="], + + "@babel/runtime": ["@babel/runtime@7.20.13", "", { "dependencies": { "regenerator-runtime": "^0.13.11" } }, "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA=="], + + "@babel/template": ["@babel/template@7.20.7", "", { "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7" } }, "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw=="], + + "@babel/traverse": ["@babel/traverse@7.20.13", "", { "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/parser": "^7.20.13", "@babel/types": "^7.20.7", "debug": "^4.1.0", "globals": "^11.1.0" } }, "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ=="], + + "@babel/types": ["@babel/types@7.20.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg=="], + + "@ctrl/tinycolor": ["@ctrl/tinycolor@3.6.0", "", {}, "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ=="], + + "@emotion/babel-plugin": ["@emotion/babel-plugin@11.10.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.0", "@emotion/memoize": "^0.8.0", "@emotion/serialize": "^1.1.1", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.1.3" } }, "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ=="], + + "@emotion/cache": ["@emotion/cache@11.10.5", "", { "dependencies": { "@emotion/memoize": "^0.8.0", "@emotion/sheet": "^1.2.1", "@emotion/utils": "^1.2.0", "@emotion/weak-memoize": "^0.3.0", "stylis": "4.1.3" } }, "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA=="], + + "@emotion/core": ["@emotion/core@11.0.0", "", {}, "sha512-w4sE3AmHmyG6RDKf6mIbtHpgJUSJ2uGvPQb8VXFL7hFjMPibE8IiehG8cMX3Ztm4svfCQV6KqusQbeIOkurBcA=="], + + "@emotion/css": ["@emotion/css@11.10.6", "", { "dependencies": { "@emotion/babel-plugin": "^11.10.6", "@emotion/cache": "^11.10.5", "@emotion/serialize": "^1.1.1", "@emotion/sheet": "^1.2.1", "@emotion/utils": "^1.2.0" } }, "sha512-88Sr+3heKAKpj9PCqq5A1hAmAkoSIvwEq1O2TwDij7fUtsJpdkV4jMTISSTouFeRvsGvXIpuSuDQ4C1YdfNGXw=="], + + "@emotion/hash": ["@emotion/hash@0.9.0", "", {}, "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="], + + "@emotion/memoize": ["@emotion/memoize@0.8.0", "", {}, "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="], + + "@emotion/react": ["@emotion/react@11.10.6", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.10.6", "@emotion/cache": "^11.10.5", "@emotion/serialize": "^1.1.1", "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", "@emotion/utils": "^1.2.0", "@emotion/weak-memoize": "^0.3.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw=="], + + "@emotion/serialize": ["@emotion/serialize@1.1.1", "", { "dependencies": { "@emotion/hash": "^0.9.0", "@emotion/memoize": "^0.8.0", "@emotion/unitless": "^0.8.0", "@emotion/utils": "^1.2.0", "csstype": "^3.0.2" } }, "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA=="], + + "@emotion/sheet": ["@emotion/sheet@1.2.1", "", {}, "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="], + + "@emotion/unitless": ["@emotion/unitless@0.8.0", "", {}, "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="], + + "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.0.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A=="], + + "@emotion/utils": ["@emotion/utils@1.2.0", "", {}, "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw=="], + + "@emotion/weak-memoize": ["@emotion/weak-memoize@0.3.0", "", {}, "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="], + + "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.1.1", "", { "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.0", "", {}, "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.1.2", "", {}, "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.2", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.14", "", {}, "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.17", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" } }, "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g=="], + + "@microsoft/fetch-event-source": ["@microsoft/fetch-event-source@2.0.1", "", {}, "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="], + + "@next/env": ["@next/env@12.3.4", "", {}, "sha512-H/69Lc5Q02dq3o+dxxy5O/oNxFsZpdL6WREtOOtOM1B/weonIwDXkekr1KV5DPVPr12IHFPrMrcJQ6bgPMfn7A=="], + + "@next/swc-android-arm-eabi": ["@next/swc-android-arm-eabi@12.3.4", "", { "os": "android", "cpu": "arm" }, "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA=="], + + "@next/swc-android-arm64": ["@next/swc-android-arm64@12.3.4", "", { "os": "android", "cpu": "arm64" }, "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@12.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@12.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ=="], + + "@next/swc-freebsd-x64": ["@next/swc-freebsd-x64@12.3.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ=="], + + "@next/swc-linux-arm-gnueabihf": ["@next/swc-linux-arm-gnueabihf@12.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@12.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@12.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@12.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@12.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@12.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ=="], + + "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@12.3.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@12.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@parcel/babel-ast-utils": ["@parcel/babel-ast-utils@2.0.0-beta.3", "", { "dependencies": { "@babel/parser": "^7.0.0", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/utils": "2.0.0-beta.3", "astring": "^1.6.2" } }, "sha512-C8hXpZsgYP2UjskNmx8a25EScPWKVo06MDKXGPa9a4DoFDNBAO+H5WXAd7IOkdYnAmac7j+Bii1MdcUoH4ipUw=="], + + "@parcel/bundler-default": ["@parcel/bundler-default@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1" } }, "sha512-uHD1es4RlKFZPc8fFbiO2g5Lnxt/ccIh9KlsCBjdtxPhioJuKMSdh+CFNx0JJXfQT6rgpNhbNMqqa+eDOVU8yQ=="], + + "@parcel/cache": ["@parcel/cache@2.0.0-beta.3", "", { "dependencies": { "@parcel/logger": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3" }, "peerDependencies": { "@parcel/core": "^2.0.0-alpha.3.1" } }, "sha512-FD5NWBRXeKsuP4jXsWlDOTQriFJzU1flRoYoKPeuUGLKfM9WZ+RV37NOXvIeR6mrAnytXDx1q+fsxlA7/0AlEQ=="], + + "@parcel/codeframe": ["@parcel/codeframe@2.0.0-beta.3", "", { "dependencies": { "chalk": "^4.1.0", "emphasize": "^4.2.0", "slice-ansi": "^4.0.0", "string-width": "^4.2.0" } }, "sha512-IpOnHqnWD9Fcn1suGLwPMvs5wsLaL3M0PHvNKScMZgUelPtgpUPalDyyA2ImgO5Vllon4tTeLLt7246Pvyy5OQ=="], + + "@parcel/config-default": ["@parcel/config-default@2.0.0-beta.3", "", { "dependencies": { "@parcel/bundler-default": "2.0.0-beta.3", "@parcel/namer-default": "2.0.0-beta.3", "@parcel/optimizer-cssnano": "2.0.0-beta.3", "@parcel/optimizer-htmlnano": "2.0.0-beta.3", "@parcel/optimizer-terser": "2.0.0-beta.3", "@parcel/packager-css": "2.0.0-beta.3", "@parcel/packager-html": "2.0.0-beta.3", "@parcel/packager-js": "2.0.0-beta.3", "@parcel/packager-raw": "2.0.0-beta.3", "@parcel/reporter-dev-server": "2.0.0-beta.3", "@parcel/resolver-default": "2.0.0-beta.3", "@parcel/runtime-browser-hmr": "2.0.0-beta.3", "@parcel/runtime-js": "2.0.0-beta.3", "@parcel/runtime-react-refresh": "2.0.0-beta.3", "@parcel/transformer-babel": "2.0.0-beta.3", "@parcel/transformer-css": "2.0.0-beta.3", "@parcel/transformer-html": "2.0.0-beta.3", "@parcel/transformer-js": "2.0.0-beta.3", "@parcel/transformer-json": "2.0.0-beta.3", "@parcel/transformer-postcss": "2.0.0-beta.3", "@parcel/transformer-posthtml": "2.0.0-beta.3", "@parcel/transformer-raw": "2.0.0-beta.3", "@parcel/transformer-react-refresh-wrap": "2.0.0-beta.3" }, "peerDependencies": { "@parcel/core": "^2.0.0-alpha.3.1" } }, "sha512-uVBhKsP2aEG7TX7TtykZ/8n1Fe1VnrPBygPnT6FQoU4to8kWeM3lm0MRNXotJ1WYJr5yLkiugVTzxXqim8lwuw=="], + + "@parcel/core": ["@parcel/core@2.0.0-beta.3", "", { "dependencies": { "@parcel/cache": "2.0.0-beta.3", "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/events": "2.0.0-beta.3", "@parcel/fs": "2.0.0-beta.3", "@parcel/logger": "2.0.0-beta.3", "@parcel/package-manager": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/types": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "@parcel/workers": "2.0.0-beta.3", "abortcontroller-polyfill": "^1.1.9", "base-x": "^3.0.8", "browserslist": "^4.6.6", "clone": "^2.1.1", "dotenv": "^7.0.0", "dotenv-expand": "^5.1.0", "json-source-map": "^0.6.1", "json5": "^1.0.1", "micromatch": "^4.0.2", "nullthrows": "^1.1.1", "querystring": "^0.2.0", "semver": "^5.4.1" } }, "sha512-yUtnowQ3YkwgeWngaD0wiTFsW+EGuIqat5Afujvq5Q/XJczxpQFfhqVkHRiz39V0OXvUINaCZieaHlU6HQI6Fw=="], + + "@parcel/diagnostic": ["@parcel/diagnostic@2.0.0-beta.3", "", { "dependencies": { "json-source-map": "^0.6.1", "nullthrows": "^1.1.1" } }, "sha512-g+KYJglJ5fmq/hiP0RKZCfrNzEnH24SqhvPPS9OnVizcyCnWsj8rBK++J5h6iEsHfFCXjspr7J2457y4X9o7aA=="], + + "@parcel/events": ["@parcel/events@2.0.0-beta.3", "", {}, "sha512-UTCjozKoRNa+gFYkjId6t9GoLcQrMkLtD+uS9gVsHYnEgAkWdWn0qdi2CN1Vt/Pl/+gdd4A/vfcyD8f7xIQx4g=="], + + "@parcel/fs": ["@parcel/fs@2.0.0-beta.3", "", { "dependencies": { "@parcel/fs-search": "2.0.0-beta.3", "@parcel/fs-write-stream-atomic": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "@parcel/watcher": "2.0.0-alpha.10", "@parcel/workers": "2.0.0-beta.3", "graceful-fs": "^4.2.4", "mkdirp": "^0.5.1", "ncp": "^2.0.0", "nullthrows": "^1.1.1", "rimraf": "^3.0.2" }, "peerDependencies": { "@parcel/core": "^2.0.0-alpha.3.1" } }, "sha512-76YdRmqkRldr6MdyETrID6Y+0hXraQ4BTFJixewfdTmrDwHN7RHN/IOw8GxtDJ7XDX9skHnvT/NLYnnbs45PKw=="], + + "@parcel/fs-search": ["@parcel/fs-search@2.0.0-beta.3", "", { "dependencies": { "detect-libc": "^1.0.3" } }, "sha512-DId5pEv+vMiMwIT9XhcXR2Cq7Y8nypZCo89vXK8gnqfUsKMKGPuQRbKneS00co8ulflMl4qrprlmjzOQhAPyqQ=="], + + "@parcel/fs-write-stream-atomic": ["@parcel/fs-write-stream-atomic@2.0.0-beta.3", "", { "dependencies": { "graceful-fs": "^4.1.2", "iferr": "^1.0.2", "imurmurhash": "^0.1.4", "readable-stream": "1 || 2" } }, "sha512-gU6N845XLvHtOd93FO9WwW0Ld2NArdaMrH+m1hLztnaxcsGkk7TUE2ObeJyJXPdG+ZvOwFh/viewPXXGDA+byA=="], + + "@parcel/logger": ["@parcel/logger@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/events": "2.0.0-beta.3" } }, "sha512-6JDsgYjKneXC8dlwgiZqRQ7yo3hnxOan1C3E0XEcpncM6keYLHTSxBYIFxs8xXN33gTq7kZgm7KMraHe91pbnw=="], + + "@parcel/markdown-ansi": ["@parcel/markdown-ansi@2.0.0-beta.3", "", { "dependencies": { "chalk": "^4.1.0" } }, "sha512-j7UsvR145jF+F+p7eVKXkhfwEKKMRMgdZr4HE+6obnHjQZu6J/UHNcSRU8xbLmXyV6qGv7LUdztHrHZGYg19eA=="], + + "@parcel/namer-default": ["@parcel/namer-default@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3", "nullthrows": "^1.1.1" } }, "sha512-TbldmO5M2kHvBFabVkJjl463qQrAPtszxm0xyZSQ5wtp+IguO4h1I1ms3OrsjZLSFEiZ4DqOMvc6qtW32Qyoxg=="], + + "@parcel/node-libs-browser": ["@parcel/node-libs-browser@2.0.0-beta.3", "", { "dependencies": { "assert": "^2.0.0", "browserify-zlib": "^0.2.0", "buffer": "^5.5.0", "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.12.0", "domain-browser": "^3.5.0", "events": "^3.1.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.0", "process": "^0.11.10", "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.0", "stream-http": "^3.1.0", "string_decoder": "^1.3.0", "timers-browserify": "^2.0.11", "tty-browserify": "^0.0.1", "url": "^0.11.0", "util": "^0.12.3", "vm-browserify": "^1.1.2" } }, "sha512-lyhIiZaZ5EPPaFz7WG4P/Sxj6VE0GQoucZV2vPyJXmGxbO7SUgpT2FLuIM7jHeqt89gJv6Hyfu5K1vdF69UHTw=="], + + "@parcel/node-resolver-core": ["@parcel/node-resolver-core@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/node-libs-browser": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "micromatch": "^3.0.4", "nullthrows": "^1.1.1", "querystring": "^0.2.0" } }, "sha512-iuI8GOfS7vJBLH1boqhcVjgLPmFqZ70a3WkgUSEGzCsVvAx9d907pKnF5CufKVrgi6U+2tjMgfci5dKlneleNw=="], + + "@parcel/optimizer-cssnano": ["@parcel/optimizer-cssnano@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "cssnano": "^4.1.10", "postcss": "^8.0.5" } }, "sha512-BcEqC+f430Ed3zeAFHY2k6ZZaMtqbOFuPcZ7DPRzdk0C3MDnt/csPuXLz6zx7UAKBizxUr5kuetCIdAE300kIw=="], + + "@parcel/optimizer-htmlnano": ["@parcel/optimizer-htmlnano@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "htmlnano": "^0.2.2", "nullthrows": "^1.1.1", "posthtml": "^0.15.1" } }, "sha512-RJv17A9CYOm9KiebRSokOX54W4d5d83gOE31Tbn1GzmIzFVIRha0a6jrXBK+kxkikk3/jdrzkSI0bBom12pGQw=="], + + "@parcel/optimizer-terser": ["@parcel/optimizer-terser@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1", "terser": "^5.2.0" } }, "sha512-vq21XlmxbRoa6vscGTyexU6IEYeBnQl8ZYf27fki3L+hRL98qtn1uI0GC0963B+DYpl3YngTAp0o6XSCQj4hrg=="], + + "@parcel/package-manager": ["@parcel/package-manager@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/fs": "2.0.0-beta.3", "@parcel/logger": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "@parcel/workers": "2.0.0-beta.3", "command-exists": "^1.2.6", "cross-spawn": "^6.0.4", "nullthrows": "^1.1.1", "semver": "^5.4.1", "split2": "^3.1.1" }, "peerDependencies": { "@parcel/core": "^2.0.0-alpha.3.1" } }, "sha512-PsA4kL0JnUXg2EY5C223wR6BhGwFpq8kioerKm33L+JmsbqQruzeMgB/OD5SPx7XVtslB0dWv6yeoOI/nv6l4w=="], + + "@parcel/packager-css": ["@parcel/packager-css@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1", "postcss": "^8.2.1" } }, "sha512-UDS0KtjnvenCoCDz/6B37nSO67E/1zOLmLSxVszdxjJWqpsNwIw4wHbBosPAeqc/m456jV+gB34vnorU7AD9vg=="], + + "@parcel/packager-html": ["@parcel/packager-html@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/types": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1", "posthtml": "^0.15.1" } }, "sha512-Edvn17Gq92V3KGVnPgnKoTP5IwVe0Y3fGMim9reUeXg2ysSYLTnlFZ/Jnbx9O/LxK5cwZuHPRLu7iFQNAUuuNg=="], + + "@parcel/packager-js": ["@parcel/packager-js@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/utils": "2.0.0-beta.3", "globals": "^13.2.0", "nullthrows": "^1.1.1" } }, "sha512-zq1rp4JLb31c5nJdbXTw0eXWEoQTqGku1mbeSTt5DImTBqRNWBD5sdR0sNbqbUbC1VH9akvjvVQXgx5nQGRYKw=="], + + "@parcel/packager-raw": ["@parcel/packager-raw@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3" } }, "sha512-oJ+25lW58oBOHctumosHj9jvSn16qOgv5GlOckgLrZuJ5S1xcGGM3qDdbFUGBd0bWGvP8JOlDLCLCf1hFJUA/Q=="], + + "@parcel/plugin": ["@parcel/plugin@2.0.0-beta.3", "", { "dependencies": { "@parcel/types": "2.0.0-beta.3" } }, "sha512-BeJBftoRTgkJP4TfEMIDyvGAT3fW4/D6R14b6rTdn6/M4feAXwLlxGdadTyR5z2JlsaY/JdVr3l0pUInYbFcZw=="], + + "@parcel/reporter-cli": ["@parcel/reporter-cli@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/types": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "chalk": "^4.1.0", "filesize": "^6.1.0", "nullthrows": "^1.1.1", "ora": "^5.2.0", "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "term-size": "^2.2.1" } }, "sha512-PxSkK6feTf1p5ec7hlD2kegCA4EoiAXfyyxf32XGo7HRbto+xKiYLK+e3C52n4znowO1pRdtI0ck+YCQeppS4Q=="], + + "@parcel/reporter-dev-server": ["@parcel/reporter-dev-server@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "connect": "^3.7.0", "ejs": "^2.6.1", "http-proxy-middleware": "^1.0.0", "nullthrows": "^1.1.1", "serve-handler": "^6.0.0", "ws": "^7.0.0" } }, "sha512-tYBYO6fcaqOlyEJV1iPiqBgWM1J8suA7PD8aSsQr0PjHHb81XMKELkDc802YlJMPtdmiwELVAiY2iS6qa8ooFg=="], + + "@parcel/resolver-default": ["@parcel/resolver-default@2.0.0-beta.3", "", { "dependencies": { "@parcel/node-resolver-core": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3" } }, "sha512-EvqcH/1qJZQoPU80upVYqqJ3U9sxiACz97wFKm1S7gqQRQH1acxOHstRGe/iqgTMFh6KHoydY+QXRiudqJr7nQ=="], + + "@parcel/runtime-browser-hmr": ["@parcel/runtime-browser-hmr@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3" } }, "sha512-T4ZGEd4bcz6zE+mIOxfXb/u/1wVbLOkHPs3ydXWAe7nOQXF+BC4yZXGC0vePJ4HX2X+QNg//cA0owMOshHE83Q=="], + + "@parcel/runtime-js": ["@parcel/runtime-js@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1" } }, "sha512-JkWlfkO7E76ayksFTdk9Ek633YvBqHoMJ8UwvEJMjUn42v8hLZQUHKAFoUZRYqzjZOub0BVdGwrvbuf1dV90ig=="], + + "@parcel/runtime-react-refresh": ["@parcel/runtime-react-refresh@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "react-refresh": "^0.9.0" } }, "sha512-99VdouRuDM1IekY0b3onCHd9zrkM93Gl9xZgBWwJpdC/jcrvdNxgSb0PwQg19YVfjCn5MM/ou7yybE8xL3aU3A=="], + + "@parcel/source-map": ["@parcel/source-map@2.0.0-rc.1.0", "", { "dependencies": { "detect-libc": "^1.0.3", "globby": "^11.0.3" } }, "sha512-X+1Eef2eVLqGbUSBjP6n2tNnqQv0HyLu6j324hPSqqA8JeHk3X1M5V6FzUe9W2RbCF1Y49VvlXRfC6BqMrZyEw=="], + + "@parcel/transformer-babel": ["@parcel/transformer-babel@2.0.0-beta.3", "", { "dependencies": { "@babel/core": "^7.12.0", "@babel/generator": "^7.9.0", "@babel/helper-compilation-targets": "^7.8.4", "@babel/plugin-transform-flow-strip-types": "^7.0.0", "@babel/traverse": "^7.0.0", "@parcel/babel-ast-utils": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/utils": "2.0.0-beta.3", "browserslist": "^4.6.6", "core-js": "^3.2.1", "nullthrows": "^1.1.1", "semver": "^5.7.0" } }, "sha512-yvpQ51ih1G1sgQjkgQuB+pAXnbaJmIbm1iKRGyTwc6Ucmz0PIGmrwRM4NBu3KccOl1/1BthRZTMsKvuuaLZL8w=="], + + "@parcel/transformer-css": ["@parcel/transformer-css@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1", "postcss": "^8.2.1", "postcss-value-parser": "^4.1.0", "semver": "^5.4.1" } }, "sha512-1v76u4VuWAQ51HqAuxq+5Tw4spzZAtrUIC0n/CNQt9i15tx9Q61zlhsB2YNYMqJG/shyMTZb9ioNl1t9KD6iaA=="], + + "@parcel/transformer-html": ["@parcel/transformer-html@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1", "posthtml": "^0.15.1", "posthtml-parser": "^0.6.0", "posthtml-render": "^1.4.0", "semver": "^5.4.1" } }, "sha512-QIH1eTXjG1qKuWak6xw3iU/GK7HOltTO84InJUSLaaUxYbgh1DeXhsKloJmWgdZZx4eZOopf58JUM7OoEcFwtg=="], + + "@parcel/transformer-js": ["@parcel/transformer-js@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/plugin": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "@parcel/utils": "2.0.0-beta.3", "@swc/helpers": "^0.2.11", "browserslist": "^4.6.6", "detect-libc": "^1.0.3", "micromatch": "^4.0.2", "nullthrows": "^1.1.1", "semver": "^5.4.1" } }, "sha512-lRDc9HqB7o/EpMTYMfymyfp3kS2o8EGQlpPOVQGeTGCnEChNxX5qwbMchXU/I03bKYGgpgn3rMx+CJja2UEZhw=="], + + "@parcel/transformer-json": ["@parcel/transformer-json@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "json5": "^2.1.0" } }, "sha512-kig1K1CaSVJG7TaULwyVQKnWqjqNzFEwFweVRVgs1sG+uGrySJkJTiVW1B4wquNTT9pJwQMXtdQs5alyyhEweA=="], + + "@parcel/transformer-postcss": ["@parcel/transformer-postcss@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "clone": "^2.1.1", "css-modules-loader-core": "^1.1.0", "nullthrows": "^1.1.1", "postcss-modules": "^3.2.2", "postcss-value-parser": "^4.1.0", "semver": "^5.4.1" } }, "sha512-zl5GlcdSBc3DyrX1StafT5qJNhpJiWHntMSKaIOta8SL7/1olvUV7AuQeRvC1OmNFfdVSHQO+T/vnByER3EklQ=="], + + "@parcel/transformer-posthtml": ["@parcel/transformer-posthtml@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "nullthrows": "^1.1.1", "posthtml": "^0.15.1", "posthtml-parser": "^0.6.0", "posthtml-render": "^1.4.0", "semver": "^5.4.1" } }, "sha512-xl5m0PQ5cTCf77Mys5e17gGqXcJ2YANwrplgtOhv12W/RVMHpZhQLHycar3OZ3gC9qkAuA2qFZj7ArO78vEMPg=="], + + "@parcel/transformer-raw": ["@parcel/transformer-raw@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3" } }, "sha512-LjeqMZIp363Cz2JqV2Z02sCX4dDZGwqTU5YnENQ+YyOmchJjvOJxgb/0XaDnQkjwoe5LVqoZkDXYvTAg/GScNg=="], + + "@parcel/transformer-react-refresh-wrap": ["@parcel/transformer-react-refresh-wrap@2.0.0-beta.3", "", { "dependencies": { "@parcel/plugin": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "react-refresh": "^0.9.0" } }, "sha512-K53j6YmIpP5K/0sONNlBUkCXoQK+6KfNzB8uViaCCkDwhSA+u3ukvpRur71ReEsiVaL0z8yqzD37Ekyl448QvA=="], + + "@parcel/types": ["@parcel/types@2.0.0-beta.3", "", {}, "sha512-5o/6KmYVU68+4IhauELMDz/kwkcoMGAB7veUX5hmH4nVNw6T05ZUHF0Te1OILASdAj67+XRAqeSA/+aWOhW/AA=="], + + "@parcel/utils": ["@parcel/utils@2.0.0-beta.3", "", { "dependencies": { "@iarna/toml": "^2.2.0", "@parcel/codeframe": "2.0.0-beta.3", "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/logger": "2.0.0-beta.3", "@parcel/markdown-ansi": "2.0.0-beta.3", "@parcel/source-map": "2.0.0-rc.1.0", "ansi-html": "^0.0.7", "chalk": "^4.1.0", "clone": "^2.1.1", "fast-glob": "3.1.1", "fastest-levenshtein": "^1.0.8", "is-glob": "^4.0.0", "is-url": "^1.2.2", "json5": "^1.0.1", "lru-cache": "^6.0.0", "micromatch": "^4.0.2", "node-forge": "^0.10.0", "nullthrows": "^1.1.1", "open": "^7.0.3" } }, "sha512-a5DKWcEkOj/BKrftXoJV+CvFQn5Axmmmx3kk1J9QM+4sTclD7pdyN3r2L7sLIFkGOtpk55E7IgCDMtj3LpGm6w=="], + + "@parcel/watcher": ["@parcel/watcher@2.0.0-alpha.10", "", { "dependencies": { "node-addon-api": "^3.0.2", "node-gyp-build": "^4.2.3" } }, "sha512-8uA7Tmx/1XvmUdGzksg0+oN7uj24pXFFnKJqZr3L3mgYjdrL7CMs3PRIHv1k3LUz/hNRsb/p3qxztSkWz1IGZA=="], + + "@parcel/workers": ["@parcel/workers@2.0.0-beta.3", "", { "dependencies": { "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/logger": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "chrome-trace-event": "^1.0.2", "nullthrows": "^1.1.1" }, "peerDependencies": { "@parcel/core": "^2.0.0-alpha.3.1" } }, "sha512-EEs0qmTj7FzcwhETFJ0wE/zGYK1xQH1sOACu1slFS9QVPpUGvGGHs9Kc1PGNiEkbTD1xMReRXETc6vf90IymCg=="], + + "@popperjs/core": ["@popperjs/core@2.11.6", "", {}, "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="], + + "@rc-component/portal": ["@rc-component/portal@1.0.0-10", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.8.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Y4JgfSZtUZaM8C5ZYFtbJVOkRrR4QVIThd/VbPMRPDI5Mv6xnOAkjg50UbB8uYH7pclCqIBoc17djbAzo12r3w=="], + + "@restart/context": ["@restart/context@2.1.4", "", { "peerDependencies": { "react": ">=16.3.2" } }, "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q=="], + + "@restart/hooks": ["@restart/hooks@0.4.9", "", { "dependencies": { "dequal": "^2.0.2" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], + + "@snowpack/plugin-react-refresh": ["@snowpack/plugin-react-refresh@2.5.0", "", { "dependencies": { "@babel/core": "^7.0.0", "@babel/plugin-syntax-class-properties": "^7.10.0", "react-refresh": "^0.9.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3rYkwayAA+65IIYLXMEFqQwtBGbII9IidMJo1yXuj35kTEg9TdZrofoqcHaSts2sv2Nz0TD6v7BWRPdvCU0uIw=="], + + "@swc/helpers": ["@swc/helpers@0.4.11", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw=="], + + "@types/http-proxy": ["@types/http-proxy@1.17.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw=="], + + "@types/invariant": ["@types/invariant@2.2.35", "", {}, "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="], + + "@types/node": ["@types/node@18.14.0", "", {}, "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A=="], + + "@types/parse-json": ["@types/parse-json@4.0.0", "", {}, "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="], + + "@types/prop-types": ["@types/prop-types@15.7.5", "", {}, "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="], + + "@types/q": ["@types/q@1.5.5", "", {}, "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ=="], + + "@types/react": ["@types/react@18.0.28", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew=="], + + "@types/react-transition-group": ["@types/react-transition-group@4.4.5", "", { "dependencies": { "@types/react": "*" } }, "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA=="], + + "@types/scheduler": ["@types/scheduler@0.16.2", "", {}, "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="], + + "@types/warning": ["@types/warning@3.0.0", "", {}, "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA=="], + + "@types/yauzl": ["@types/yauzl@2.10.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw=="], + + "@vitejs/plugin-react-refresh": ["@vitejs/plugin-react-refresh@1.3.6", "", { "dependencies": { "@babel/core": "^7.14.8", "@babel/plugin-transform-react-jsx-self": "^7.14.5", "@babel/plugin-transform-react-jsx-source": "^7.14.5", "@rollup/pluginutils": "^4.1.1", "react-refresh": "^0.10.0" } }, "sha512-iNR/UqhUOmFFxiezt0em9CgmiJBdWR+5jGxB2FihaoJfqGt76kiwaKoVOJVU5NYcDWMdN06LbyN2VIGIoYdsEA=="], + + "abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], + + "abortcontroller-polyfill": ["abortcontroller-polyfill@1.7.5", "", {}, "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ=="], + + "acorn": ["acorn@8.8.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw=="], + + "acorn-globals": ["acorn-globals@4.3.4", "", { "dependencies": { "acorn": "^6.0.1", "acorn-walk": "^6.0.1" } }, "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A=="], + + "acorn-walk": ["acorn-walk@6.2.0", "", {}, "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="], + + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "alphanum-sort": ["alphanum-sort@1.0.2", "", {}, "sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ=="], + + "ansi-html": ["ansi-html@0.0.7", "", { "bin": { "ansi-html": "./bin/ansi-html" } }, "sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "antd": ["antd@4.24.8", "", { "dependencies": { "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.7.0", "@ant-design/react-slick": "~0.29.1", "@babel/runtime": "^7.18.3", "@ctrl/tinycolor": "^3.4.0", "classnames": "^2.2.6", "copy-to-clipboard": "^3.2.0", "lodash": "^4.17.21", "moment": "^2.29.2", "rc-cascader": "~3.7.0", "rc-checkbox": "~2.3.0", "rc-collapse": "~3.4.2", "rc-dialog": "~9.0.2", "rc-drawer": "~6.1.0", "rc-dropdown": "~4.0.0", "rc-field-form": "~1.27.0", "rc-image": "~5.13.0", "rc-input": "~0.1.4", "rc-input-number": "~7.3.9", "rc-mentions": "~1.13.1", "rc-menu": "~9.8.0", "rc-motion": "^2.6.1", "rc-notification": "~4.6.0", "rc-pagination": "~3.2.0", "rc-picker": "~2.7.0", "rc-progress": "~3.4.1", "rc-rate": "~2.9.0", "rc-resize-observer": "^1.2.0", "rc-segmented": "~2.1.0", "rc-select": "~14.1.13", "rc-slider": "~10.0.0", "rc-steps": "~5.0.0-alpha.2", "rc-switch": "~3.2.0", "rc-table": "~7.26.0", "rc-tabs": "~12.5.6", "rc-textarea": "~0.4.5", "rc-tooltip": "~5.2.0", "rc-tree": "~5.7.0", "rc-tree-select": "~5.5.0", "rc-trigger": "^5.2.10", "rc-upload": "~4.3.0", "rc-util": "^5.22.5", "scroll-into-view-if-needed": "^2.2.25" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-wrNy2Hi27uM3948okG3n2GwzQKBFUn1Qn5mn2I/ALcR28rC6cTjHYOuA248Zl9ECzz3jo4TY2R0SIa+5GZ/zGA=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "arr-diff": ["arr-diff@4.0.0", "", {}, "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA=="], + + "arr-flatten": ["arr-flatten@1.1.0", "", {}, "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg=="], + + "arr-union": ["arr-union@3.1.0", "", {}, "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q=="], + + "array-equal": ["array-equal@1.0.0", "", {}, "sha512-H3LU5RLiSsGXPhN+Nipar0iR0IofH+8r89G2y1tBKxQ/agagKyAjhkAFDRBfodP2caPrNKHpAWNIM/c9yeL7uA=="], + + "array-tree-filter": ["array-tree-filter@2.1.0", "", {}, "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array-unique": ["array-unique@0.3.2", "", {}, "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ=="], + + "array.prototype.reduce": ["array.prototype.reduce@1.0.5", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4", "es-array-method-boxes-properly": "^1.0.0", "is-string": "^1.0.7" } }, "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="], + + "assert": ["assert@2.0.0", "", { "dependencies": { "es6-object-assign": "^1.1.0", "is-nan": "^1.2.1", "object-is": "^1.0.1", "util": "^0.12.0" } }, "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "astring": ["astring@1.8.4", "", { "bin": { "astring": "bin/astring" } }, "sha512-97a+l2LBU3Op3bBQEff79i/E4jMD2ZLFD8rHx9B6mXyB2uQwhJQYfiDqUwtfjF4QA1F2qs//N6Cw8LetMbQjcw=="], + + "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], + + "async-validator": ["async-validator@4.2.5", "", {}, "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "atob": ["atob@2.1.2", "", { "bin": { "atob": "bin/atob.js" } }, "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.5", "", {}, "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="], + + "aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="], + + "aws4": ["aws4@1.12.0", "", {}, "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg=="], + + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base": ["base@0.11.2", "", { "dependencies": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", "component-emitter": "^1.2.1", "define-property": "^1.0.0", "isobject": "^3.0.1", "mixin-deep": "^1.2.0", "pascalcase": "^0.1.1" } }, "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg=="], + + "base-x": ["base-x@3.0.9", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "bn.js": ["bn.js@5.2.1", "", {}, "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.2", "", { "dependencies": { "fill-range": "^7.0.1" } }, "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A=="], + + "brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="], + + "browser-process-hrtime": ["browser-process-hrtime@1.0.0", "", {}, "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="], + + "browserify-aes": ["browserify-aes@1.2.0", "", { "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA=="], + + "browserify-cipher": ["browserify-cipher@1.0.1", "", { "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w=="], + + "browserify-des": ["browserify-des@1.0.2", "", { "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A=="], + + "browserify-rsa": ["browserify-rsa@4.1.0", "", { "dependencies": { "bn.js": "^5.0.0", "randombytes": "^2.0.1" } }, "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog=="], + + "browserify-sign": ["browserify-sign@4.2.1", "", { "dependencies": { "bn.js": "^5.1.1", "browserify-rsa": "^4.0.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "elliptic": "^6.5.3", "inherits": "^2.0.4", "parse-asn1": "^5.1.5", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg=="], + + "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], + + "browserslist": ["browserslist@4.21.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001449", "electron-to-chromium": "^1.4.284", "node-releases": "^2.0.8", "update-browserslist-db": "^1.0.10" }, "bin": { "browserslist": "cli.js" } }, "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "buffer-xor": ["buffer-xor@1.0.3", "", {}, "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="], + + "builtin-status-codes": ["builtin-status-codes@3.0.0", "", {}, "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="], + + "bun-framework-next": ["bun-framework-next@12.2.5", "", { "dependencies": { "react-is": "*" }, "peerDependencies": { "next": "~12.2.3" } }, "sha512-2sbt+qLxdv6FKIDMwi6dWeTQj6GwF0j0IAKTGk977TIUZMEBqr93Nb2DSIj0FPBF0ZCCO/lGYD3j3ulmmD0GAQ=="], + + "bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], + + "cache-base": ["cache-base@1.0.1", "", { "dependencies": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", "get-value": "^2.0.6", "has-value": "^1.0.0", "isobject": "^3.0.1", "set-value": "^2.0.0", "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" } }, "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ=="], + + "call-bind": ["call-bind@1.0.2", "", { "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" } }, "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA=="], + + "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="], + + "caller-path": ["caller-path@2.0.0", "", { "dependencies": { "caller-callsite": "^2.0.0" } }, "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001456", "", {}, "sha512-XFHJY5dUgmpMV25UqaD4kVq2LsiaU5rS8fb0f17pCoXQiQslzmFgnfOxfvo1bTpTqf7dwG/N/05CnLCnOEKmzA=="], + + "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "child-process": ["child-process@1.0.2", "", {}, "sha512-e45+JmjvkV2XQsJ9rUJghiYJ7/9Uk8fyYi1UwfP071VmGKBu/oD1OIwuD0+jSwjMtQkV0a0FVpPTEW+XGlbSdw=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "chrome-trace-event": ["chrome-trace-event@1.0.3", "", {}, "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="], + + "cipher-base": ["cipher-base@1.0.4", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q=="], + + "class-utils": ["class-utils@0.3.6", "", { "dependencies": { "arr-union": "^3.1.0", "define-property": "^0.2.5", "isobject": "^3.0.0", "static-extend": "^0.1.1" } }, "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg=="], + + "classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.7.0", "", {}, "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "coa": ["coa@2.0.2", "", { "dependencies": { "@types/q": "^1.5.1", "chalk": "^2.4.1", "q": "^1.1.2" } }, "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA=="], + + "collection-visit": ["collection-visit@1.0.0", "", { "dependencies": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" } }, "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], + + "commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "component-emitter": ["component-emitter@1.3.0", "", {}, "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="], + + "compute-scroll-into-view": ["compute-scroll-into-view@1.0.20", "", {}, "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="], + + "console-browserify": ["console-browserify@1.2.0", "", {}, "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="], + + "constants-browserify": ["constants-browserify@1.0.0", "", {}, "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ=="], + + "content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="], + + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "copy-descriptor": ["copy-descriptor@0.1.1", "", {}, "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw=="], + + "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="], + + "core-js": ["core-js@3.28.0", "", {}, "sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + + "create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="], + + "create-hash": ["create-hash@1.2.0", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg=="], + + "create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="], + + "cross-spawn": ["cross-spawn@6.0.5", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ=="], + + "crypto-browserify": ["crypto-browserify@3.12.0", "", { "dependencies": { "browserify-cipher": "^1.0.0", "browserify-sign": "^4.0.0", "create-ecdh": "^4.0.0", "create-hash": "^1.1.0", "create-hmac": "^1.1.0", "diffie-hellman": "^5.0.0", "inherits": "^2.0.1", "pbkdf2": "^3.0.3", "public-encrypt": "^4.0.0", "randombytes": "^2.0.0", "randomfill": "^1.0.3" } }, "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg=="], + + "css-color-names": ["css-color-names@0.0.4", "", {}, "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q=="], + + "css-declaration-sorter": ["css-declaration-sorter@4.0.1", "", { "dependencies": { "postcss": "^7.0.1", "timsort": "^0.3.0" } }, "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA=="], + + "css-modules-loader-core": ["css-modules-loader-core@1.1.0", "", { "dependencies": { "icss-replace-symbols": "1.1.0", "postcss": "6.0.1", "postcss-modules-extract-imports": "1.1.0", "postcss-modules-local-by-default": "1.2.0", "postcss-modules-scope": "1.1.0", "postcss-modules-values": "1.3.0" } }, "sha512-XWOBwgy5nwBn76aA+6ybUGL/3JBnCtBX9Ay9/OWIpzKYWlVHMazvJ+WtHumfi+xxdPF440cWK7JCYtt8xDifew=="], + + "css-select": ["css-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^3.2.1", "domutils": "^1.7.0", "nth-check": "^1.0.2" } }, "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ=="], + + "css-select-base-adapter": ["css-select-base-adapter@0.1.1", "", {}, "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="], + + "css-selector-tokenizer": ["css-selector-tokenizer@0.7.3", "", { "dependencies": { "cssesc": "^3.0.0", "fastparse": "^1.1.2" } }, "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg=="], + + "css-tree": ["css-tree@1.0.0-alpha.37", "", { "dependencies": { "mdn-data": "2.0.4", "source-map": "^0.6.1" } }, "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg=="], + + "css-what": ["css-what@3.4.2", "", {}, "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssnano": ["cssnano@4.1.11", "", { "dependencies": { "cosmiconfig": "^5.0.0", "cssnano-preset-default": "^4.0.8", "is-resolvable": "^1.0.0", "postcss": "^7.0.0" } }, "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g=="], + + "cssnano-preset-default": ["cssnano-preset-default@4.0.8", "", { "dependencies": { "css-declaration-sorter": "^4.0.1", "cssnano-util-raw-cache": "^4.0.1", "postcss": "^7.0.0", "postcss-calc": "^7.0.1", "postcss-colormin": "^4.0.3", "postcss-convert-values": "^4.0.1", "postcss-discard-comments": "^4.0.2", "postcss-discard-duplicates": "^4.0.2", "postcss-discard-empty": "^4.0.1", "postcss-discard-overridden": "^4.0.1", "postcss-merge-longhand": "^4.0.11", "postcss-merge-rules": "^4.0.3", "postcss-minify-font-values": "^4.0.2", "postcss-minify-gradients": "^4.0.2", "postcss-minify-params": "^4.0.2", "postcss-minify-selectors": "^4.0.2", "postcss-normalize-charset": "^4.0.1", "postcss-normalize-display-values": "^4.0.2", "postcss-normalize-positions": "^4.0.2", "postcss-normalize-repeat-style": "^4.0.2", "postcss-normalize-string": "^4.0.2", "postcss-normalize-timing-functions": "^4.0.2", "postcss-normalize-unicode": "^4.0.1", "postcss-normalize-url": "^4.0.1", "postcss-normalize-whitespace": "^4.0.2", "postcss-ordered-values": "^4.1.2", "postcss-reduce-initial": "^4.0.3", "postcss-reduce-transforms": "^4.0.2", "postcss-svgo": "^4.0.3", "postcss-unique-selectors": "^4.0.1" } }, "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ=="], + + "cssnano-util-get-arguments": ["cssnano-util-get-arguments@4.0.0", "", {}, "sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw=="], + + "cssnano-util-get-match": ["cssnano-util-get-match@4.0.0", "", {}, "sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw=="], + + "cssnano-util-raw-cache": ["cssnano-util-raw-cache@4.0.1", "", { "dependencies": { "postcss": "^7.0.0" } }, "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA=="], + + "cssnano-util-same-parent": ["cssnano-util-same-parent@4.0.1", "", {}, "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q=="], + + "csso": ["csso@4.2.0", "", { "dependencies": { "css-tree": "^1.1.2" } }, "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA=="], + + "cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + + "cssstyle": ["cssstyle@1.4.0", "", { "dependencies": { "cssom": "0.3.x" } }, "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA=="], + + "csstype": ["csstype@3.1.1", "", {}, "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="], + + "dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="], + + "data-urls": ["data-urls@1.1.0", "", { "dependencies": { "abab": "^2.0.0", "whatwg-mimetype": "^2.2.0", "whatwg-url": "^7.0.0" } }, "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ=="], + + "date-fns": ["date-fns@2.29.3", "", {}, "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="], + + "dayjs": ["dayjs@1.11.7", "", {}, "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="], + + "debug": ["debug@4.3.1", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ=="], + + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "define-properties": ["define-properties@1.2.0", "", { "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA=="], + + "define-property": ["define-property@2.0.2", "", { "dependencies": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" } }, "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "des.js": ["des.js@1.0.1", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA=="], + + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "devtools-protocol": ["devtools-protocol@0.0.901419", "", {}, "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ=="], + + "diffie-hellman": ["diffie-hellman@5.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dom-align": ["dom-align@1.12.4", "", {}, "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw=="], + + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "domain-browser": ["domain-browser@3.5.0", "", {}, "sha512-zrzUu6auyZWRexjCEPJnfWc30Hupxh2lJZOJAF3qa2bCuD4O/55t0FvQt3ZMhEw++gjNkwdkOVZh8yA32w/Vfw=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domexception": ["domexception@1.0.1", "", { "dependencies": { "webidl-conversions": "^4.0.2" } }, "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug=="], + + "domhandler": ["domhandler@3.3.0", "", { "dependencies": { "domelementtype": "^2.0.1" } }, "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA=="], + + "domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + + "dotenv": ["dotenv@7.0.0", "", {}, "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g=="], + + "dotenv-expand": ["dotenv-expand@5.1.0", "", {}, "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="], + + "ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "ejs": ["ejs@2.7.4", "", {}, "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="], + + "electron-to-chromium": ["electron-to-chromium@1.4.302", "", {}, "sha512-Uk7C+7aPBryUR1Fwvk9VmipBcN9fVsqBO57jV2ZjTm+IZ6BMNqu7EDVEg2HxCNufk6QcWlFsBkhQyQroB2VWKw=="], + + "elliptic": ["elliptic@6.5.4", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="], + + "emphasize": ["emphasize@4.2.0", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "~10.4.0", "lowlight": "~1.17.0" } }, "sha512-yGKvcFUHlBsUPwlxTlzKLR8+zhpbitkFOMCUxN8fTJng9bdH3WNzUGkhdaGdjndSUgqmMPBN7umfwnUdLz5Axg=="], + + "encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + + "es-abstract": ["es-abstract@1.21.1", "", { "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "internal-slot": "^1.0.4", "is-array-buffer": "^3.0.1", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", "object-inspect": "^1.12.2", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "safe-regex-test": "^1.0.0", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.9" } }, "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg=="], + + "es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3", "has": "^1.0.3", "has-tostringtag": "^1.0.0" } }, "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg=="], + + "es-to-primitive": ["es-to-primitive@1.2.1", "", { "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", "is-symbol": "^1.0.2" } }, "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA=="], + + "es6-object-assign": ["es6-object-assign@1.1.0", "", {}, "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw=="], + + "escalade": ["escalade@3.1.1", "", {}, "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "escodegen": ["escodegen@1.14.3", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "evp_bytestokey": ["evp_bytestokey@1.0.3", "", { "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA=="], + + "expand-brackets": ["expand-brackets@2.1.4", "", { "dependencies": { "debug": "^2.3.3", "define-property": "^0.2.5", "extend-shallow": "^2.0.1", "posix-character-classes": "^0.1.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" } }, "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], + + "extglob": ["extglob@2.0.4", "", { "dependencies": { "array-unique": "^0.3.2", "define-property": "^1.0.0", "expand-brackets": "^2.1.4", "extend-shallow": "^2.0.1", "fragment-cache": "^0.2.1", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" } }, "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw=="], + + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "@types/yauzl": "^2.9.1", "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.1.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.0", "merge2": "^1.3.0", "micromatch": "^4.0.2" } }, "sha512-nTCREpBY8w8r+boyFYAx21iL6faSsQynliPHM4Uf56SbkyohCNxpVPEH9xrF5TXKy+IsjkPUHDKiUkzBVRXn9g=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-url-parser": ["fast-url-parser@1.1.3", "", { "dependencies": { "punycode": "^1.3.2" } }, "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ=="], + + "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], + + "fastparse": ["fastparse@1.1.2", "", {}, "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ=="], + + "fastq": ["fastq@1.15.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw=="], + + "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "filesize": ["filesize@6.4.0", "", {}, "sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ=="], + + "fill-range": ["fill-range@7.0.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ=="], + + "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "follow-redirects": ["follow-redirects@1.15.2", "", {}, "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="], + + "for-each": ["for-each@0.3.3", "", { "dependencies": { "is-callable": "^1.1.3" } }, "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw=="], + + "for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="], + + "forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="], + + "form-data": ["form-data@2.3.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ=="], + + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + + "fragment-cache": ["fragment-cache@0.2.1", "", { "dependencies": { "map-cache": "^0.2.2" } }, "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA=="], + + "fs": ["fs@0.0.1-security", "", {}, "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.1", "", {}, "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="], + + "function.prototype.name": ["function.prototype.name@1.1.5", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", "es-abstract": "^1.19.0", "functions-have-names": "^1.2.2" } }, "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generic-names": ["generic-names@2.0.1", "", { "dependencies": { "loader-utils": "^1.1.0" } }, "sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.2.0", "", { "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.3" } }, "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q=="], + + "get-port": ["get-port@4.2.0", "", {}, "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw=="], + + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "get-symbol-description": ["get-symbol-description@1.0.0", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" } }, "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw=="], + + "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], + + "getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "globalthis": ["globalthis@1.0.3", "", { "dependencies": { "define-properties": "^1.1.3" } }, "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="], + + "graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], + + "har-schema": ["har-schema@2.0.0", "", {}, "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="], + + "har-validator": ["har-validator@5.1.5", "", { "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w=="], + + "has": ["has@1.0.3", "", { "dependencies": { "function-bind": "^1.1.1" } }, "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw=="], + + "has-ansi": ["has-ansi@2.0.0", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg=="], + + "has-bigints": ["has-bigints@1.0.2", "", {}, "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.0", "", { "dependencies": { "get-intrinsic": "^1.1.1" } }, "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ=="], + + "has-proto": ["has-proto@1.0.1", "", {}, "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="], + + "has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="], + + "has-tostringtag": ["has-tostringtag@1.0.0", "", { "dependencies": { "has-symbols": "^1.0.2" } }, "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ=="], + + "has-value": ["has-value@1.0.0", "", { "dependencies": { "get-value": "^2.0.6", "has-values": "^1.0.0", "isobject": "^3.0.0" } }, "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw=="], + + "has-values": ["has-values@1.0.0", "", { "dependencies": { "is-number": "^3.0.0", "kind-of": "^4.0.0" } }, "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ=="], + + "hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="], + + "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], + + "hex-color-regex": ["hex-color-regex@1.1.0", "", {}, "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="], + + "highlight.js": ["highlight.js@10.4.1", "", {}, "sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg=="], + + "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "hsl-regex": ["hsl-regex@1.0.0", "", {}, "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A=="], + + "hsla-regex": ["hsla-regex@1.0.0", "", {}, "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@1.0.2", "", { "dependencies": { "whatwg-encoding": "^1.0.1" } }, "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw=="], + + "html-tags": ["html-tags@1.2.0", "", {}, "sha512-uVteDXUCs08M7QJx0eY6ue7qQztwIfknap81vAtNob2sdEPKa8PjPinx0vxbs2JONPamovZjMvKZWNW44/PBKg=="], + + "htmlnano": ["htmlnano@0.2.9", "", { "dependencies": { "cssnano": "^4.1.11", "posthtml": "^0.15.1", "purgecss": "^2.3.0", "relateurl": "^0.2.7", "srcset": "^3.0.0", "svgo": "^1.3.2", "terser": "^5.6.1", "timsort": "^0.3.0", "uncss": "^0.17.3" } }, "sha512-jWTtP3dCd7R8x/tt9DK3pvpcQd7HDMcRPUqPxr/i9989q2k5RHIhmlRDFeyQ/LSd8IKrteG8Ce5g0Ig4eGIipg=="], + + "htmlparser2": ["htmlparser2@5.0.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^3.3.0", "domutils": "^2.4.2", "entities": "^2.0.0" } }, "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ=="], + + "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], + + "http-proxy-middleware": ["http-proxy-middleware@1.3.1", "", { "dependencies": { "@types/http-proxy": "^1.17.5", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" } }, "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg=="], + + "http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="], + + "https-browserify": ["https-browserify@1.0.0", "", {}, "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="], + + "https-proxy-agent": ["https-proxy-agent@5.0.0", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "icss-replace-symbols": ["icss-replace-symbols@1.1.0", "", {}, "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg=="], + + "icss-utils": ["icss-utils@4.1.1", "", { "dependencies": { "postcss": "^7.0.14" } }, "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "iferr": ["iferr@1.0.2", "", {}, "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg=="], + + "ignore": ["ignore@5.2.4", "", {}, "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="], + + "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indexes-of": ["indexes-of@1.0.1", "", {}, "sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "internal-slot": ["internal-slot@1.0.5", "", { "dependencies": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" } }, "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ=="], + + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + + "is-absolute-url": ["is-absolute-url@3.0.3", "", {}, "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q=="], + + "is-accessor-descriptor": ["is-accessor-descriptor@1.0.0", "", { "dependencies": { "kind-of": "^6.0.0" } }, "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ=="], + + "is-arguments": ["is-arguments@1.1.1", "", { "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA=="], + + "is-array-buffer": ["is-array-buffer@3.0.1", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", "is-typed-array": "^1.1.10" } }, "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-bigint": ["is-bigint@1.0.4", "", { "dependencies": { "has-bigints": "^1.0.1" } }, "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg=="], + + "is-boolean-object": ["is-boolean-object@1.1.2", "", { "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-color-stop": ["is-color-stop@1.1.0", "", { "dependencies": { "css-color-names": "^0.0.4", "hex-color-regex": "^1.1.0", "hsl-regex": "^1.0.0", "hsla-regex": "^1.0.0", "rgb-regex": "^1.0.1", "rgba-regex": "^1.0.0" } }, "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA=="], + + "is-core-module": ["is-core-module@2.11.0", "", { "dependencies": { "has": "^1.0.3" } }, "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw=="], + + "is-data-descriptor": ["is-data-descriptor@1.0.0", "", { "dependencies": { "kind-of": "^6.0.0" } }, "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ=="], + + "is-date-object": ["is-date-object@1.0.5", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ=="], + + "is-descriptor": ["is-descriptor@1.0.2", "", { "dependencies": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } }, "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg=="], + + "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extendable": ["is-extendable@1.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-function": ["is-generator-function@1.0.10", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-html": ["is-html@1.1.0", "", { "dependencies": { "html-tags": "^1.0.0" } }, "sha512-eoGsQVAAyvLFRKnbt4jo7Il56agsH5I04pDymPoxRp/tnna5yiIpdNzvKPOy5G1Ff0zY/jfN2hClb7ju+sOrdA=="], + + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="], + + "is-negative-zero": ["is-negative-zero@2.0.2", "", {}, "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.0.7", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ=="], + + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "is-plain-obj": ["is-plain-obj@3.0.0", "", {}, "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + + "is-regex": ["is-regex@1.1.4", "", { "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg=="], + + "is-resolvable": ["is-resolvable@1.1.0", "", {}, "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.2", "", { "dependencies": { "call-bind": "^1.0.2" } }, "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA=="], + + "is-string": ["is-string@1.0.7", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg=="], + + "is-symbol": ["is-symbol@1.0.4", "", { "dependencies": { "has-symbols": "^1.0.2" } }, "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg=="], + + "is-typed-array": ["is-typed-array@1.1.10", "", { "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" } }, "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A=="], + + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], + + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "is-url": ["is-url@1.2.4", "", {}, "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="], + + "is-weakref": ["is-weakref@1.0.2", "", { "dependencies": { "call-bind": "^1.0.2" } }, "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + + "isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="], + + "jsdom": ["jsdom@14.1.0", "", { "dependencies": { "abab": "^2.0.0", "acorn": "^6.0.4", "acorn-globals": "^4.3.0", "array-equal": "^1.0.0", "cssom": "^0.3.4", "cssstyle": "^1.1.1", "data-urls": "^1.1.0", "domexception": "^1.0.1", "escodegen": "^1.11.0", "html-encoding-sniffer": "^1.0.2", "nwsapi": "^2.1.3", "parse5": "5.1.0", "pn": "^1.1.0", "request": "^2.88.0", "request-promise-native": "^1.0.5", "saxes": "^3.1.9", "symbol-tree": "^3.2.2", "tough-cookie": "^2.5.0", "w3c-hr-time": "^1.0.1", "w3c-xmlserializer": "^1.1.2", "webidl-conversions": "^4.0.2", "whatwg-encoding": "^1.0.5", "whatwg-mimetype": "^2.3.0", "whatwg-url": "^7.0.0", "ws": "^6.1.2", "xml-name-validator": "^3.0.0" } }, "sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng=="], + + "jsesc": ["jsesc@2.5.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="], + + "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-source-map": ["json-source-map@0.6.1", "", {}, "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "left-pad": ["left-pad@1.3.0", "", {}, "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA=="], + + "levn": ["levn@0.3.0", "", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "loader-utils": ["loader-utils@1.4.2", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lowlight": ["lowlight@1.17.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.4.0" } }, "sha512-vmtBgYKD+QVNy7tIa7ulz5d//Il9R4MooOVh4nkOf9R9Cb/Dk5TXMSTieg/vDulkBkIWj59/BIlyFQxT9X1oAQ=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "map-cache": ["map-cache@0.2.2", "", {}, "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg=="], + + "map-visit": ["map-visit@1.0.0", "", { "dependencies": { "object-visit": "^1.0.0" } }, "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w=="], + + "md5.js": ["md5.js@1.3.5", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg=="], + + "mdn-data": ["mdn-data@2.0.4", "", {}, "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], + + "miller-rabin": ["miller-rabin@4.0.1", "", { "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, "bin": { "miller-rabin": "bin/miller-rabin" } }, "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA=="], + + "mime-db": ["mime-db@1.33.0", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="], + + "mime-types": ["mime-types@2.1.18", "", { "dependencies": { "mime-db": "~1.33.0" } }, "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mixin-deep": ["mixin-deep@1.3.2", "", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "nanoid": ["nanoid@3.3.4", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="], + + "nanomatch": ["nanomatch@1.2.13", "", { "dependencies": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "fragment-cache": "^0.2.1", "is-windows": "^1.0.2", "kind-of": "^6.0.2", "object.pick": "^1.3.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" } }, "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA=="], + + "ncp": ["ncp@2.0.0", "", { "bin": { "ncp": "./bin/ncp" } }, "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA=="], + + "next": ["next@12.3.4", "", { "dependencies": { "@next/env": "12.3.4", "@next/swc-android-arm-eabi": "12.3.4", "@next/swc-android-arm64": "12.3.4", "@next/swc-darwin-arm64": "12.3.4", "@next/swc-darwin-x64": "12.3.4", "@next/swc-freebsd-x64": "12.3.4", "@next/swc-linux-arm-gnueabihf": "12.3.4", "@next/swc-linux-arm64-gnu": "12.3.4", "@next/swc-linux-arm64-musl": "12.3.4", "@next/swc-linux-x64-gnu": "12.3.4", "@next/swc-linux-x64-musl": "12.3.4", "@next/swc-win32-arm64-msvc": "12.3.4", "@next/swc-win32-ia32-msvc": "12.3.4", "@next/swc-win32-x64-msvc": "12.3.4", "@swc/helpers": "0.4.11", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", "styled-jsx": "5.0.7", "use-sync-external-store": "1.2.0" }, "peerDependencies": { "fibers": ">= 3.1.0", "node-sass": "^6.0.0 || ^7.0.0", "react": "^17.0.2 || ^18.0.0-0", "react-dom": "^17.0.2 || ^18.0.0-0", "sass": "^1.3.0" }, "optionalPeers": ["node-sass", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-VcyMJUtLZBGzLKo3oMxrEF0stxh8HwuW976pAzlHhI3t8qJ4SROjCrSh1T24bhrbjw55wfZXAbXPGwPt5FLRfQ=="], + + "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], + + "node-addon-api": ["node-addon-api@3.2.1", "", {}, "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="], + + "node-fetch": ["node-fetch@2.6.1", "", {}, "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="], + + "node-forge": ["node-forge@0.10.0", "", {}, "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="], + + "node-gyp-build": ["node-gyp-build@4.6.0", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ=="], + + "node-releases": ["node-releases@2.0.10", "", {}, "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w=="], + + "normalize-url": ["normalize-url@3.3.0", "", {}, "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg=="], + + "nth-check": ["nth-check@1.0.2", "", { "dependencies": { "boolbase": "~1.0.0" } }, "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg=="], + + "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + + "nwsapi": ["nwsapi@2.2.2", "", {}, "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw=="], + + "oauth-sign": ["oauth-sign@0.9.0", "", {}, "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-copy": ["object-copy@0.1.0", "", { "dependencies": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", "kind-of": "^3.0.3" } }, "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ=="], + + "object-inspect": ["object-inspect@1.12.3", "", {}, "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="], + + "object-is": ["object-is@1.1.5", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" } }, "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object-visit": ["object-visit@1.0.1", "", { "dependencies": { "isobject": "^3.0.0" } }, "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA=="], + + "object.assign": ["object.assign@4.1.4", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ=="], + + "object.getownpropertydescriptors": ["object.getownpropertydescriptors@2.1.5", "", { "dependencies": { "array.prototype.reduce": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" } }, "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw=="], + + "object.pick": ["object.pick@1.3.0", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ=="], + + "object.values": ["object.values@1.1.6", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" } }, "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw=="], + + "on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + + "optionator": ["optionator@0.8.3", "", { "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "word-wrap": "~1.2.3" } }, "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA=="], + + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "os-browserify": ["os-browserify@0.3.0", "", {}, "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parcel": ["parcel@2.0.0-beta.3", "", { "dependencies": { "@parcel/config-default": "2.0.0-beta.3", "@parcel/core": "2.0.0-beta.3", "@parcel/diagnostic": "2.0.0-beta.3", "@parcel/events": "2.0.0-beta.3", "@parcel/fs": "2.0.0-beta.3", "@parcel/logger": "2.0.0-beta.3", "@parcel/package-manager": "2.0.0-beta.3", "@parcel/reporter-cli": "2.0.0-beta.3", "@parcel/reporter-dev-server": "2.0.0-beta.3", "@parcel/utils": "2.0.0-beta.3", "chalk": "^4.1.0", "commander": "^7.0.0", "get-port": "^4.2.0", "v8-compile-cache": "^2.0.0" }, "bin": { "parcel": "lib/bin.js" } }, "sha512-85lYzs87O7jedNhuKj21fqA4Kq0dDXFHNOqxvKnIxltlPLXPXFiGwR2EcjTmF8Trv82KoeKWuWLtUVSzjZ79nQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-asn1": ["parse-asn1@5.1.6", "", { "dependencies": { "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" } }, "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse5": ["parse5@5.1.0", "", {}, "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "pascalcase": ["pascalcase@0.1.1", "", {}, "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw=="], + + "path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-is-inside": ["path-is-inside@1.0.2", "", {}, "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="], + + "path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-to-regexp": ["path-to-regexp@2.2.1", "", {}, "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pbkdf2": ["pbkdf2@3.1.2", "", { "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", "ripemd160": "^2.0.1", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "percentile": ["percentile@1.6.0", "", {}, "sha512-8vSyjdzwxGDHHwH+cSGch3A9Uj2On3UpgOWxWXMKwUvoAbnujx6DaqmV1duWXNiH/oEWpyVd6nSQccix6DM3Ng=="], + + "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], + + "picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + + "pn": ["pn@1.1.0", "", {}, "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA=="], + + "posix-character-classes": ["posix-character-classes@0.1.1", "", {}, "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg=="], + + "postcss": ["postcss@8.4.14", "", { "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig=="], + + "postcss-calc": ["postcss-calc@7.0.5", "", { "dependencies": { "postcss": "^7.0.27", "postcss-selector-parser": "^6.0.2", "postcss-value-parser": "^4.0.2" } }, "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg=="], + + "postcss-colormin": ["postcss-colormin@4.0.3", "", { "dependencies": { "browserslist": "^4.0.0", "color": "^3.0.0", "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw=="], + + "postcss-convert-values": ["postcss-convert-values@4.0.1", "", { "dependencies": { "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ=="], + + "postcss-discard-comments": ["postcss-discard-comments@4.0.2", "", { "dependencies": { "postcss": "^7.0.0" } }, "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg=="], + + "postcss-discard-duplicates": ["postcss-discard-duplicates@4.0.2", "", { "dependencies": { "postcss": "^7.0.0" } }, "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ=="], + + "postcss-discard-empty": ["postcss-discard-empty@4.0.1", "", { "dependencies": { "postcss": "^7.0.0" } }, "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w=="], + + "postcss-discard-overridden": ["postcss-discard-overridden@4.0.1", "", { "dependencies": { "postcss": "^7.0.0" } }, "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg=="], + + "postcss-merge-longhand": ["postcss-merge-longhand@4.0.11", "", { "dependencies": { "css-color-names": "0.0.4", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0", "stylehacks": "^4.0.0" } }, "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw=="], + + "postcss-merge-rules": ["postcss-merge-rules@4.0.3", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-api": "^3.0.0", "cssnano-util-same-parent": "^4.0.0", "postcss": "^7.0.0", "postcss-selector-parser": "^3.0.0", "vendors": "^1.0.0" } }, "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ=="], + + "postcss-minify-font-values": ["postcss-minify-font-values@4.0.2", "", { "dependencies": { "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg=="], + + "postcss-minify-gradients": ["postcss-minify-gradients@4.0.2", "", { "dependencies": { "cssnano-util-get-arguments": "^4.0.0", "is-color-stop": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q=="], + + "postcss-minify-params": ["postcss-minify-params@4.0.2", "", { "dependencies": { "alphanum-sort": "^1.0.0", "browserslist": "^4.0.0", "cssnano-util-get-arguments": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0", "uniqs": "^2.0.0" } }, "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg=="], + + "postcss-minify-selectors": ["postcss-minify-selectors@4.0.2", "", { "dependencies": { "alphanum-sort": "^1.0.0", "has": "^1.0.0", "postcss": "^7.0.0", "postcss-selector-parser": "^3.0.0" } }, "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g=="], + + "postcss-modules": ["postcss-modules@3.2.2", "", { "dependencies": { "generic-names": "^2.0.1", "icss-replace-symbols": "^1.1.0", "lodash.camelcase": "^4.3.0", "postcss": "^7.0.32", "postcss-modules-extract-imports": "^2.0.0", "postcss-modules-local-by-default": "^3.0.2", "postcss-modules-scope": "^2.2.0", "postcss-modules-values": "^3.0.0", "string-hash": "^1.1.1" } }, "sha512-JQ8IAqHELxC0N6tyCg2UF40pACY5oiL6UpiqqcIFRWqgDYO8B0jnxzoQ0EOpPrWXvcpu6BSbQU/3vSiq7w8Nhw=="], + + "postcss-modules-extract-imports": ["postcss-modules-extract-imports@1.1.0", "", { "dependencies": { "postcss": "^6.0.1" } }, "sha512-zF9+UIEvtpeqMGxhpeT9XaIevQSrBBCz9fi7SwfkmjVacsSj8DY5eFVgn+wY8I9vvdDDwK5xC8Myq4UkoLFIkA=="], + + "postcss-modules-local-by-default": ["postcss-modules-local-by-default@1.2.0", "", { "dependencies": { "css-selector-tokenizer": "^0.7.0", "postcss": "^6.0.1" } }, "sha512-X4cquUPIaAd86raVrBwO8fwRfkIdbwFu7CTfEOjiZQHVQwlHRSkTgH5NLDmMm5+1hQO8u6dZ+TOOJDbay1hYpA=="], + + "postcss-modules-scope": ["postcss-modules-scope@1.1.0", "", { "dependencies": { "css-selector-tokenizer": "^0.7.0", "postcss": "^6.0.1" } }, "sha512-LTYwnA4C1He1BKZXIx1CYiHixdSe9LWYVKadq9lK5aCCMkoOkFyZ7aigt+srfjlRplJY3gIol6KUNefdMQJdlw=="], + + "postcss-modules-values": ["postcss-modules-values@1.3.0", "", { "dependencies": { "icss-replace-symbols": "^1.1.0", "postcss": "^6.0.1" } }, "sha512-i7IFaR9hlQ6/0UgFuqM6YWaCfA1Ej8WMg8A5DggnH1UGKJvTV/ugqq/KaULixzzOi3T/tF6ClBXcHGCzdd5unA=="], + + "postcss-normalize-charset": ["postcss-normalize-charset@4.0.1", "", { "dependencies": { "postcss": "^7.0.0" } }, "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g=="], + + "postcss-normalize-display-values": ["postcss-normalize-display-values@4.0.2", "", { "dependencies": { "cssnano-util-get-match": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ=="], + + "postcss-normalize-positions": ["postcss-normalize-positions@4.0.2", "", { "dependencies": { "cssnano-util-get-arguments": "^4.0.0", "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA=="], + + "postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@4.0.2", "", { "dependencies": { "cssnano-util-get-arguments": "^4.0.0", "cssnano-util-get-match": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q=="], + + "postcss-normalize-string": ["postcss-normalize-string@4.0.2", "", { "dependencies": { "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA=="], + + "postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@4.0.2", "", { "dependencies": { "cssnano-util-get-match": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A=="], + + "postcss-normalize-unicode": ["postcss-normalize-unicode@4.0.1", "", { "dependencies": { "browserslist": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg=="], + + "postcss-normalize-url": ["postcss-normalize-url@4.0.1", "", { "dependencies": { "is-absolute-url": "^2.0.0", "normalize-url": "^3.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA=="], + + "postcss-normalize-whitespace": ["postcss-normalize-whitespace@4.0.2", "", { "dependencies": { "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA=="], + + "postcss-ordered-values": ["postcss-ordered-values@4.1.2", "", { "dependencies": { "cssnano-util-get-arguments": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw=="], + + "postcss-reduce-initial": ["postcss-reduce-initial@4.0.3", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-api": "^3.0.0", "has": "^1.0.0", "postcss": "^7.0.0" } }, "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA=="], + + "postcss-reduce-transforms": ["postcss-reduce-transforms@4.0.2", "", { "dependencies": { "cssnano-util-get-match": "^4.0.0", "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" } }, "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.0.11", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g=="], + + "postcss-svgo": ["postcss-svgo@4.0.3", "", { "dependencies": { "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0", "svgo": "^1.0.0" } }, "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw=="], + + "postcss-unique-selectors": ["postcss-unique-selectors@4.0.1", "", { "dependencies": { "alphanum-sort": "^1.0.0", "postcss": "^7.0.0", "uniqs": "^2.0.0" } }, "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "posthtml": ["posthtml@0.15.2", "", { "dependencies": { "posthtml-parser": "^0.7.2", "posthtml-render": "^1.3.1" } }, "sha512-YugEJ5ze/0DLRIVBjCpDwANWL4pPj1kHJ/2llY8xuInr0nbkon3qTiMPe5LQa+cCwNjxS7nAZZTp+1M+6mT4Zg=="], + + "posthtml-parser": ["posthtml-parser@0.6.0", "", { "dependencies": { "htmlparser2": "^5.0.1" } }, "sha512-5ffwKQNgtVHdhZniWxu+1ryvaZv5l25HPLUV6W5xy5nYVWMXtvjtwRnbSpfbKFvbyl7XI+d4AqkjmonkREqnXA=="], + + "posthtml-render": ["posthtml-render@1.4.0", "", {}, "sha512-W1779iVHGfq0Fvh2PROhCe2QhB8mEErgqzo1wpIt36tCgChafP+hbXIhLDOM8ePJrZcFs0vkNEtdibEWVqChqw=="], + + "prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "progress": ["progress@2.0.1", "", {}, "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "prop-types-extra": ["prop-types-extra@1.1.1", "", { "dependencies": { "react-is": "^16.3.2", "warning": "^4.0.0" }, "peerDependencies": { "react": ">=0.14.0" } }, "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "psl": ["psl@1.9.0", "", {}, "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="], + + "public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="], + + "pump": ["pump@3.0.0", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww=="], + + "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], + + "puppeteer": ["puppeteer@10.4.0", "", { "dependencies": { "debug": "4.3.1", "devtools-protocol": "0.0.901419", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.0", "node-fetch": "2.6.1", "pkg-dir": "4.2.0", "progress": "2.0.1", "proxy-from-env": "1.1.0", "rimraf": "3.0.2", "tar-fs": "2.0.0", "unbzip2-stream": "1.3.3", "ws": "7.4.6" } }, "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w=="], + + "puppeteer-mass-screenshots": ["puppeteer-mass-screenshots@1.0.15", "", { "dependencies": { "fs": "0.0.1-security", "path": "^0.12.7" } }, "sha512-QasQ6pxkXocZTyVkgIx2uXM5VOTUhXvHw44TJNgoQ2I/pj+DOKqXI0TD5VUOiKAWkjJhCtsJaL9ImQFBJrPWPg=="], + + "puppeteer-video-recorder": ["puppeteer-video-recorder@1.0.5", "", { "dependencies": { "child-process": "^1.0.2", "fs": "0.0.1-security", "path": "^0.12.7", "puppeteer-mass-screenshots": "^1.0.14" } }, "sha512-bulReIiRpf8j9davQwtMcUePW++znA1kpXfA1LP3P+nvpCHW7vuUToXuYXXXYyDQBkaAF7NWroEuoIjW8IENKA=="], + + "purgecss": ["purgecss@2.3.0", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.0.0", "postcss": "7.0.32", "postcss-selector-parser": "^6.0.2" }, "bin": { "purgecss": "bin/purgecss" } }, "sha512-BE5CROfVGsx2XIhxGuZAT7rTH9lLeQx/6M0P7DTXQH4IUc3BBzs9JUzt4yzGf3JrH9enkeq6YJBe9CTtkm1WmQ=="], + + "q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="], + + "qs": ["qs@6.5.3", "", {}, "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="], + + "querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="], + + "querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "randomfill": ["randomfill@1.0.4", "", { "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw=="], + + "range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="], + + "rc-align": ["rc-align@4.0.15", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "dom-align": "^1.7.0", "rc-util": "^5.26.0", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA=="], + + "rc-cascader": ["rc-cascader@3.7.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "array-tree-filter": "^2.1.0", "classnames": "^2.3.1", "rc-select": "~14.1.0", "rc-tree": "~5.7.0", "rc-util": "^5.6.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SFtGpwmYN7RaWEAGTS4Rkc62ZV/qmQGg/tajr/7mfIkleuu8ro9Hlk6J+aA0x1YS4zlaZBtTcSaXM01QMiEV/A=="], + + "rc-checkbox": ["rc-checkbox@2.3.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-afVi1FYiGv1U0JlpNH/UaEXdh6WUJjcWokj/nUN2TgG80bfG+MDdbfHKlLcNNba94mbjy2/SXJ1HDgrOkXGAjg=="], + + "rc-collapse": ["rc-collapse@3.4.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.2.1", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-jpTwLgJzkhAgp2Wpi3xmbTbbYExg6fkptL67Uu5LCRVEj6wqmy0DHTjjeynsjOLsppHGHu41t1ELntZ0lEvS/Q=="], + + "rc-dialog": ["rc-dialog@9.0.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-s3U+24xWUuB6Bn2Lk/Qt6rufy+uT+QvWkiFhNBcO9APLxcFFczWamaq7x9h8SCuhfc1nHcW4y8NbMsnAjNnWyg=="], + + "rc-drawer": ["rc-drawer@6.1.3", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-6", "classnames": "^2.2.6", "rc-motion": "^2.6.1", "rc-util": "^5.21.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AvHisO90A+xMLMKBw2zs89HxjWxusM2BUABlgK60RhweIHF8W/wk0hSOrxBlUXoA9r1F+10na3g6GZ97y1qDZA=="], + + "rc-dropdown": ["rc-dropdown@4.0.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.6", "rc-trigger": "^5.3.1", "rc-util": "^5.17.0" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-OdpXuOcme1rm45cR0Jzgfl1otzmU4vuBVb+etXM8vcaULGokAKVpKlw8p6xzspG7jGd/XxShvq+N3VNEfk/l5g=="], + + "rc-field-form": ["rc-field-form@1.27.4", "", { "dependencies": { "@babel/runtime": "^7.18.0", "async-validator": "^4.1.0", "rc-util": "^5.8.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-PQColQnZimGKArnOh8V2907+VzDCXcqtFvHgevDLtqWc/P7YASb/FqntSmdS8q3VND5SHX3Y1vgMIzY22/f/0Q=="], + + "rc-image": ["rc-image@5.13.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.0.0", "rc-motion": "^2.6.2", "rc-util": "^5.0.6" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-iZTOmw5eWo2+gcrJMMcnd7SsxVHl3w5xlyCgsULUdJhJbnuI8i/AL0tVOsE7aLn9VfOh1qgDT3mC2G75/c7mqg=="], + + "rc-input": ["rc-input@0.1.4", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-FqDdNz+fV2dKNgfXzcSLKvC+jEs1709t7nD+WdfjrdSaOcefpgc7BUJYadc3usaING+b7ediMTfKxuJBsEFbXA=="], + + "rc-input-number": ["rc-input-number@7.3.11", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.23.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aMWPEjFeles6PQnMqP5eWpxzsvHm9rh1jQOWXExUEIxhX62Fyl/ptifLHOn17+waDG1T/YUb6flfJbvwRhHrbA=="], + + "rc-mentions": ["rc-mentions@1.13.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", "rc-menu": "~9.8.0", "rc-textarea": "^0.4.0", "rc-trigger": "^5.0.4", "rc-util": "^5.22.5" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-FCkaWw6JQygtOz0+Vxz/M/NWqrWHB9LwqlY2RtcuFqWJNFK9njijOOzTSsBGANliGufVUzx/xuPHmZPBV0+Hgw=="], + + "rc-menu": ["rc-menu@9.8.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.2.8", "rc-trigger": "^5.1.2", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-EahOJVjLuEnJsThoPN+mGnVm431RzVzDLZWHRS/YnXTQULa7OsgdJa/Y7qXxc3Z5sz8mgT6xYtgpmBXLxrZFaQ=="], + + "rc-motion": ["rc-motion@2.6.3", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-xFLkes3/7VL/J+ah9jJruEW/Akbx5F6jVa2wG5o/ApGKQKSOd5FR3rseHLL9+xtJg4PmCwo6/1tqhDO/T+jFHA=="], + + "rc-notification": ["rc-notification@4.6.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.2.0", "rc-util": "^5.20.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-NSmFYwrrdY3+un1GvDAJQw62Xi9LNMSsoQyo95tuaYrcad5Bn9gJUL8AREufRxSQAQnr64u3LtP3EUyLYT6bhw=="], + + "rc-overflow": ["rc-overflow@1.2.8", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.19.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-QJ0UItckWPQ37ZL1dMEBAdY1dhfTXFL9k6oTTcyydVwoUNMnMqCGqnRNA98axSr/OeDKqR6DVFyi8eA5RQI/uQ=="], + + "rc-pagination": ["rc-pagination@3.2.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-5tIXjB670WwwcAJzAqp2J+cOBS9W3cH/WU1EiYwXljuZ4vtZXKlY2Idq8FZrnYBz8KhN3vwPo9CoV/SJS6SL1w=="], + + "rc-picker": ["rc-picker@2.7.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", "date-fns": "2.x", "dayjs": "1.x", "moment": "^2.24.0", "rc-trigger": "^5.0.4", "rc-util": "^5.4.0", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-oZH6FZ3j4iuBxHB4NvQ6ABRsS2If/Kpty1YFFsji7/aej6ruGmfM7WnJWQ88AoPfpJ++ya5z+nVEA8yCRYGKyw=="], + + "rc-progress": ["rc-progress@3.4.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-eAFDHXlk8aWpoXl0llrenPMt9qKHQXphxcVsnKs0FHC6eCSk1ebJtyaVjJUzKe0233ogiLDeEFK1Uihz3s67hw=="], + + "rc-rate": ["rc-rate@2.9.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.0.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SaiZFyN8pe0Fgphv8t3+kidlej+cq/EALkAJAc3A0w0XcPaH2L1aggM8bhe1u6GAGuQNAoFvTLjw4qLPGRKV5g=="], + + "rc-resize-observer": ["rc-resize-observer@1.3.1", "", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.27.0", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-iFUdt3NNhflbY3mwySv5CA1TC06zdJ+pfo0oc27xpf4PIOvfZwZGtD9Kz41wGYqC4SLio93RVAirSSpYlV/uYg=="], + + "rc-segmented": ["rc-segmented@2.1.2", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-motion": "^2.4.4", "rc-util": "^5.17.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-qGo1bCr83ESXpXVOCXjFe1QJlCAQXyi9KCiy8eX3rIMYlTeJr/ftySIaTnYsitL18SvWf5ZEHsfqIWoX0EMfFQ=="], + + "rc-select": ["rc-select@14.1.16", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-overflow": "^1.0.0", "rc-trigger": "^5.0.4", "rc-util": "^5.16.1", "rc-virtual-list": "^3.2.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-71XLHleuZmufpdV2vis5oituRkhg2WNvLpVMJBGWRar6WGAVOHXaY9DR5HvwWry3EGTn19BqnL6Xbybje6f8YA=="], + + "rc-slider": ["rc-slider@10.0.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.18.1", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-igTKF3zBet7oS/3yNiIlmU8KnZ45npmrmHlUUio8PNbIhzMcsh+oE/r2UD42Y6YD2D/s+kzCQkzQrPD6RY435Q=="], + + "rc-steps": ["rc-steps@5.0.0-alpha.2", "", { "dependencies": { "@babel/runtime": "^7.16.7", "classnames": "^2.2.3", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WPH5jgLnQ1OJHs5SnSp46Ep0wqK0afT1+6MVc4sU9uD+7W1v6Ccisrz0v1ZCsTmQJVwiD7mwVaZ+l75iMHcrvg=="], + + "rc-switch": ["rc-switch@3.2.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", "rc-util": "^5.0.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-+gUJClsZZzvAHGy1vZfnwySxj+MjLlGRyXKXScrtCTcmiYNPzxDFOxdQ/3pK1Kt/0POvwJ/6ALOR8gwdXGhs+A=="], + + "rc-table": ["rc-table@7.26.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", "rc-util": "^5.22.5", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-0cD8e6S+DTGAt5nBZQIPFYEaIukn17sfa5uFL98faHlH/whZzD8ii3dbFL4wmUDEL4BLybhYop+QUfZJ4CPvNQ=="], + + "rc-tabs": ["rc-tabs@12.5.6", "", { "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", "rc-dropdown": "~4.0.0", "rc-menu": "~9.8.0", "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.16.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aArXHzxK7YICxe+622CZ8FlO5coMi8P7E6tXpseCPKm1gdTjUt0LrQK1/AxcrRXZXG3K4QqhlKmET0+cX5DQaQ=="], + + "rc-textarea": ["rc-textarea@0.4.7", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.24.4", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-IQPd1CDI3mnMlkFyzt2O4gQ2lxUsnBAeJEoZGJnkkXgORNqyM9qovdrCj9NzcRfpHgLdzaEbU3AmobNFGUznwQ=="], + + "rc-tooltip": ["rc-tooltip@5.2.2", "", { "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "^2.3.1", "rc-trigger": "^5.0.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-jtQzU/18S6EI3lhSGoDYhPqNpWajMtS5VV/ld1LwyfrDByQpYmw/LW6U7oFXXLukjfDHQ7Ju705A82PRNFWYhg=="], + + "rc-tree": ["rc-tree@5.7.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-util": "^5.16.1", "rc-virtual-list": "^3.4.8" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-nmnL6qLnfwVckO5zoqKL2I9UhwDqzyCtjITQCkwhimyz1zfuFkG5ZPIXpzD/Guzso94qQA/QrMsvzic5W6QDjg=="], + + "rc-tree-select": ["rc-tree-select@5.5.5", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-select": "~14.1.0", "rc-tree": "~5.7.0", "rc-util": "^5.16.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-k2av7jF6tW9bIO4mQhaVdV4kJ1c54oxV3/hHVU+oD251Gb5JN+m1RbJFTMf1o0rAFqkvto33rxMdpafaGKQRJw=="], + + "rc-trigger": ["rc-trigger@5.3.4", "", { "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.6", "rc-align": "^4.0.0", "rc-motion": "^2.0.0", "rc-util": "^5.19.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw=="], + + "rc-upload": ["rc-upload@4.3.4", "", { "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.5", "rc-util": "^5.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uVbtHFGNjHG/RyAfm9fluXB6pvArAGyAx8z7XzXXyorEgVIWj6mOlriuDm0XowDHYz4ycNK0nE0oP3cbFnzxiQ=="], + + "rc-util": ["rc-util@5.27.2", "", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^16.12.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8XHRbeJOWlTR2Hk1K2xLaPOf7lZu+3taskAGuqOPccA676vB3ygrz3ZgdrA3wml40CzR9RlIEHDWwI7FZT3wBQ=="], + + "rc-virtual-list": ["rc-virtual-list@3.4.13", "", { "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", "rc-resize-observer": "^1.0.0", "rc-util": "^5.15.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-cPOVDmcNM7rH6ANotanMDilW/55XnFPw0Jh/GQYtrzZSy3AmWvCnqVNyNC/pgg3lfVmX2994dlzAhuUrd4jG7w=="], + + "react": ["react@17.0.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="], + + "react-bootstrap": ["react-bootstrap@1.6.6", "", { "dependencies": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", "@restart/hooks": "^0.4.7", "@types/invariant": "^2.2.33", "@types/prop-types": "^15.7.3", "@types/react": ">=16.14.8", "@types/react-transition-group": "^4.4.1", "@types/warning": "^3.0.0", "classnames": "^2.3.1", "dom-helpers": "^5.2.1", "invariant": "^2.2.4", "prop-types": "^15.7.2", "prop-types-extra": "^1.1.0", "react-overlays": "^5.1.2", "react-transition-group": "^4.4.1", "uncontrollable": "^7.2.1", "warning": "^4.0.3" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-pSzYyJT5u4rc8+5myM8Vid2JG52L8AmYSkpznReH/GM4+FhLqEnxUa0+6HRTaGwjdEixQNGchwY+b3xCdYWrDA=="], + + "react-dom": ["react-dom@17.0.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "scheduler": "^0.20.2" }, "peerDependencies": { "react": "17.0.2" } }, "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="], + + "react-form": ["react-form@4.0.1", "", { "peerDependencies": { "prop-types": "^15.5.4", "react": "^16.8.3" } }, "sha512-vhsCuBLZJYjm6vd8TBxIhIWeB/8Jg4mmmiR3Zj+1zIGBM39qJf1CLqekadLp0J9NeW0EsZxUnBtbmEnMeZ/7Pw=="], + + "react-hook-form": ["react-hook-form@7.43.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18" } }, "sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg=="], + + "react-is": ["react-is@18.2.0", "", {}, "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="], + + "react-lifecycles-compat": ["react-lifecycles-compat@3.0.4", "", {}, "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="], + + "react-overlays": ["react-overlays@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.13.8", "@popperjs/core": "^2.11.6", "@restart/hooks": "^0.4.7", "@types/warning": "^3.0.0", "dom-helpers": "^5.2.0", "prop-types": "^15.7.2", "uncontrollable": "^7.2.1", "warning": "^4.0.3" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA=="], + + "react-refresh": ["react-refresh@0.9.0", "", {}, "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "readable-stream": ["readable-stream@2.3.7", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw=="], + + "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], + + "regex-not": ["regex-not@1.0.2", "", { "dependencies": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" } }, "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.4.3", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", "functions-have-names": "^1.2.2" } }, "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA=="], + + "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], + + "repeat-element": ["repeat-element@1.1.4", "", {}, "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ=="], + + "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], + + "request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="], + + "request-promise-core": ["request-promise-core@1.1.4", "", { "dependencies": { "lodash": "^4.17.19" }, "peerDependencies": { "request": "^2.34" } }, "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw=="], + + "request-promise-native": ["request-promise-native@1.0.9", "", { "dependencies": { "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, "peerDependencies": { "request": "^2.34" } }, "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], + + "resolve": ["resolve@1.22.1", "", { "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-url": ["resolve-url@0.2.1", "", {}, "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], + + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + + "rgb-regex": ["rgb-regex@1.0.1", "", {}, "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w=="], + + "rgba-regex": ["rgba-regex@1.0.0", "", {}, "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-regex": ["safe-regex@1.1.0", "", { "dependencies": { "ret": "~0.1.10" } }, "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg=="], + + "safe-regex-test": ["safe-regex-test@1.0.0", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", "is-regex": "^1.1.4" } }, "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.2.4", "", {}, "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="], + + "saxes": ["saxes@3.1.11", "", { "dependencies": { "xmlchars": "^2.1.1" } }, "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g=="], + + "scheduler": ["scheduler@0.20.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ=="], + + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@2.2.31", "", { "dependencies": { "compute-scroll-into-view": "^1.0.20" } }, "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA=="], + + "semver": ["semver@6.3.0", "", { "bin": { "semver": "./bin/semver.js" } }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="], + + "serve-handler": ["serve-handler@6.1.5", "", { "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "fast-url-parser": "1.1.3", "mime-types": "2.1.18", "minimatch": "3.1.2", "path-is-inside": "1.0.2", "path-to-regexp": "2.2.1", "range-parser": "1.2.0" } }, "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg=="], + + "set-value": ["set-value@2.0.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" } }, "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "sha.js": ["sha.js@2.4.11", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "./bin.js" } }, "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ=="], + + "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], + + "shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "side-channel": ["side-channel@1.0.4", "", { "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", "object-inspect": "^1.9.0" } }, "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "snapdragon": ["snapdragon@0.8.2", "", { "dependencies": { "base": "^0.11.1", "debug": "^2.2.0", "define-property": "^0.2.5", "extend-shallow": "^2.0.1", "map-cache": "^0.2.2", "source-map": "^0.5.6", "source-map-resolve": "^0.5.0", "use": "^3.1.0" } }, "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg=="], + + "snapdragon-node": ["snapdragon-node@2.1.1", "", { "dependencies": { "define-property": "^1.0.0", "isobject": "^3.0.0", "snapdragon-util": "^3.0.1" } }, "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw=="], + + "snapdragon-util": ["snapdragon-util@3.0.1", "", { "dependencies": { "kind-of": "^3.2.0" } }, "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ=="], + + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + + "source-map-js": ["source-map-js@1.0.2", "", {}, "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="], + + "source-map-resolve": ["source-map-resolve@0.5.3", "", { "dependencies": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", "urix": "^0.1.0" } }, "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "source-map-url": ["source-map-url@0.4.1", "", {}, "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="], + + "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], + + "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "srcset": ["srcset@3.0.1", "", {}, "sha512-MM8wDGg5BQJEj94tDrZDrX9wrC439/Eoeg3sgmVLPMjHgrAFeXAKk3tmFlCbKw5k+yOEhPXRpPlRcisQmqWVSQ=="], + + "sshpk": ["sshpk@1.17.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ=="], + + "stable": ["stable@0.1.8", "", {}, "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="], + + "static-extend": ["static-extend@0.1.2", "", { "dependencies": { "define-property": "^0.2.5", "object-copy": "^0.1.0" } }, "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g=="], + + "statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + + "stealthy-require": ["stealthy-require@1.1.1", "", {}, "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g=="], + + "stream-http": ["stream-http@3.2.0", "", { "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.4", "readable-stream": "^3.6.0", "xtend": "^4.0.2" } }, "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A=="], + + "string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="], + + "string-hash": ["string-hash@1.1.3", "", {}, "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.6", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" } }, "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.6", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" } }, "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "styled-jsx": ["styled-jsx@5.0.7", "", { "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA=="], + + "stylehacks": ["stylehacks@4.0.3", "", { "dependencies": { "browserslist": "^4.0.0", "postcss": "^7.0.0", "postcss-selector-parser": "^3.0.0" } }, "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g=="], + + "stylis": ["stylis@4.1.3", "", {}, "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svgo": ["svgo@1.3.2", "", { "dependencies": { "chalk": "^2.4.1", "coa": "^2.0.2", "css-select": "^2.0.0", "css-select-base-adapter": "^0.1.1", "css-tree": "1.0.0-alpha.37", "csso": "^4.0.2", "js-yaml": "^3.13.1", "mkdirp": "~0.5.1", "object.values": "^1.1.0", "sax": "~1.2.4", "stable": "^0.1.8", "unquote": "~1.1.1", "util.promisify": "~1.0.0" }, "bin": { "svgo": "./bin/svgo" } }, "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tar-fs": ["tar-fs@2.0.0", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp": "^0.5.1", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "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" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + + "terser": ["terser@5.16.4", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-5yEGuZ3DZradbogeYQ1NaGz7rXVBDWujWlx1PT8efXO6Txn+eWbfKqB2bTDVmFXmePFkoLU6XI8UektMIEA0ug=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "timers-browserify": ["timers-browserify@2.0.12", "", { "dependencies": { "setimmediate": "^1.0.4" } }, "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ=="], + + "timsort": ["timsort@0.3.0", "", {}, "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="], + + "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], + + "to-object-path": ["to-object-path@0.3.0", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg=="], + + "to-regex": ["to-regex@3.0.2", "", { "dependencies": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "regex-not": "^1.0.2", "safe-regex": "^1.1.0" } }, "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toggle-selection": ["toggle-selection@1.0.6", "", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="], + + "tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="], + + "tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], + + "tslib": ["tslib@2.5.0", "", {}, "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="], + + "tty-browserify": ["tty-browserify@0.0.1", "", {}, "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-check": ["type-check@0.3.2", "", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="], + + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "typed-array-length": ["typed-array-length@1.0.4", "", { "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", "is-typed-array": "^1.1.9" } }, "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng=="], + + "typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], + + "unbox-primitive": ["unbox-primitive@1.0.2", "", { "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" } }, "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw=="], + + "unbzip2-stream": ["unbzip2-stream@1.3.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg=="], + + "uncontrollable": ["uncontrollable@7.2.1", "", { "dependencies": { "@babel/runtime": "^7.6.3", "@types/react": ">=16.9.11", "invariant": "^2.2.4", "react-lifecycles-compat": "^3.0.4" }, "peerDependencies": { "react": ">=15.0.0" } }, "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ=="], + + "uncss": ["uncss@0.17.3", "", { "dependencies": { "commander": "^2.20.0", "glob": "^7.1.4", "is-absolute-url": "^3.0.1", "is-html": "^1.1.0", "jsdom": "^14.1.0", "lodash": "^4.17.15", "postcss": "^7.0.17", "postcss-selector-parser": "6.0.2", "request": "^2.88.0" }, "bin": { "uncss": "bin/uncss" } }, "sha512-ksdDWl81YWvF/X14fOSw4iu8tESDHFIeyKIeDrK6GEVTQvqJc1WlOEXqostNwOCi3qAj++4EaLsdAgPmUbEyog=="], + + "union-value": ["union-value@1.0.1", "", { "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" } }, "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg=="], + + "uniq": ["uniq@1.0.1", "", {}, "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA=="], + + "uniqs": ["uniqs@2.0.0", "", {}, "sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "unquote": ["unquote@1.1.1", "", {}, "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg=="], + + "unset-value": ["unset-value@1.0.0", "", { "dependencies": { "has-value": "^0.3.1", "isobject": "^3.0.0" } }, "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.0.10", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "browserslist-lint": "cli.js" } }, "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "urix": ["urix@0.1.0", "", {}, "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg=="], + + "url": ["url@0.11.0", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ=="], + + "use": ["use@3.1.1", "", {}, "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], + + "util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "util.promisify": ["util.promisify@1.0.1", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.2", "has-symbols": "^1.0.1", "object.getownpropertydescriptors": "^2.1.0" } }, "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="], + + "v8-compile-cache": ["v8-compile-cache@2.3.0", "", {}, "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA=="], + + "vendors": ["vendors@1.0.4", "", {}, "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w=="], + + "verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], + + "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], + + "w3c-hr-time": ["w3c-hr-time@1.0.2", "", { "dependencies": { "browser-process-hrtime": "^1.0.0" } }, "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@1.1.2", "", { "dependencies": { "domexception": "^1.0.1", "webidl-conversions": "^4.0.2", "xml-name-validator": "^3.0.0" } }, "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg=="], + + "warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + + "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + + "whatwg-encoding": ["whatwg-encoding@1.0.5", "", { "dependencies": { "iconv-lite": "0.4.24" } }, "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw=="], + + "whatwg-mimetype": ["whatwg-mimetype@2.3.0", "", {}, "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g=="], + + "whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], + + "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.0.2", "", { "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" } }, "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg=="], + + "which-typed-array": ["which-typed-array@1.1.9", "", { "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0", "is-typed-array": "^1.1.10" } }, "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA=="], + + "wipwipwipwip-next-donotuse": ["wipwipwipwip-next-donotuse@4.0.0", "", {}, "sha512-VlhmxFbJ/wy7yMb4ekP6R46gUAE1vjpRA5qFrk7XqADckQHRkE0+3aDUGn2j3tidJPAOmMEVjU5dgLpeWT/C5g=="], + + "word-wrap": ["word-wrap@1.2.3", "", {}, "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@7.4.6", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["utf-8-validate"] }, "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="], + + "xml-name-validator": ["xml-name-validator@3.0.0", "", {}, "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "@babel/core/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.2", "", { "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "@babel/traverse/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "@jridgewell/source-map/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.2", "", { "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A=="], + + "@parcel/core/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "@parcel/core/querystring": ["querystring@0.2.1", "", {}, "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg=="], + + "@parcel/core/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/node-libs-browser/punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], + + "@parcel/node-libs-browser/readable-stream": ["readable-stream@3.6.0", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="], + + "@parcel/node-libs-browser/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "@parcel/node-libs-browser/util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + + "@parcel/node-resolver-core/micromatch": ["micromatch@3.1.10", "", { "dependencies": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", "braces": "^2.3.1", "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "extglob": "^2.0.4", "fragment-cache": "^0.2.1", "kind-of": "^6.0.2", "nanomatch": "^1.2.9", "object.pick": "^1.3.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.2" } }, "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg=="], + + "@parcel/node-resolver-core/querystring": ["querystring@0.2.1", "", {}, "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg=="], + + "@parcel/optimizer-cssnano/postcss": ["postcss@8.4.21", "", { "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg=="], + + "@parcel/package-manager/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/packager-css/postcss": ["postcss@8.4.21", "", { "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg=="], + + "@parcel/packager-js/globals": ["globals@13.20.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ=="], + + "@parcel/reporter-dev-server/ws": ["ws@7.5.9", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["utf-8-validate"] }, "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q=="], + + "@parcel/transformer-babel/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/transformer-css/postcss": ["postcss@8.4.21", "", { "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg=="], + + "@parcel/transformer-css/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/transformer-html/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/transformer-js/@swc/helpers": ["@swc/helpers@0.2.14", "", {}, "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA=="], + + "@parcel/transformer-js/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/transformer-postcss/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/transformer-posthtml/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "@parcel/utils/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "@vitejs/plugin-react-refresh/react-refresh": ["react-refresh@0.10.0", "", {}, "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ=="], + + "acorn-globals/acorn": ["acorn@6.4.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="], + + "agent-base/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "asn1.js/bn.js": ["bn.js@4.12.0", "", {}, "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="], + + "assert/util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + + "base/define-property": ["define-property@1.0.0", "", { "dependencies": { "is-descriptor": "^1.0.0" } }, "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA=="], + + "bl/readable-stream": ["readable-stream@3.6.0", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="], + + "browserify-sign/readable-stream": ["readable-stream@3.6.0", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="], + + "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], + + "class-utils/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="], + + "coa/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "create-ecdh/bn.js": ["bn.js@4.12.0", "", {}, "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="], + + "cross-spawn/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="], + + "css-declaration-sorter/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "css-modules-loader-core/postcss": ["postcss@6.0.1", "", { "dependencies": { "chalk": "^1.1.3", "source-map": "^0.5.6", "supports-color": "^3.2.3" } }, "sha512-VbGX1LQgQbf9l3cZ3qbUuC3hGqIEOGQFHAEHQ/Diaeo0yLgpgK5Rb8J+OcamIfQ9PbAU/fzBjVtQX3AhJHUvZw=="], + + "css-select/domutils": ["domutils@1.7.0", "", { "dependencies": { "dom-serializer": "0", "domelementtype": "1" } }, "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg=="], + + "css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "cssnano/cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="], + + "cssnano/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "cssnano-preset-default/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "cssnano-util-raw-cache/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "csso/css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], + + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "diffie-hellman/bn.js": ["bn.js@4.12.0", "", {}, "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="], + + "dom-serializer/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "domutils/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "elliptic/bn.js": ["bn.js@4.12.0", "", {}, "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="], + + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "expand-brackets/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "expand-brackets/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="], + + "expand-brackets/extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "extglob/define-property": ["define-property@1.0.0", "", { "dependencies": { "is-descriptor": "^1.0.0" } }, "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA=="], + + "extglob/extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "extract-zip/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "fast-url-parser/punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "globby/fast-glob": ["fast-glob@3.2.12", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w=="], + + "has-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "has-values/is-number": ["is-number@3.0.0", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg=="], + + "has-values/kind-of": ["kind-of@4.0.0", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw=="], + + "hash-base/readable-stream": ["readable-stream@3.6.0", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="], + + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "https-proxy-agent/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "icss-utils/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "jsdom/acorn": ["acorn@6.4.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="], + + "jsdom/ws": ["ws@6.2.2", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw=="], + + "loader-utils/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "miller-rabin/bn.js": ["bn.js@4.12.0", "", {}, "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="], + + "object-copy/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="], + + "object-copy/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "postcss-calc/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-colormin/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-colormin/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-convert-values/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-convert-values/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-discard-comments/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-discard-duplicates/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-discard-empty/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-discard-overridden/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-merge-longhand/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-merge-longhand/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-merge-rules/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-merge-rules/postcss-selector-parser": ["postcss-selector-parser@3.1.2", "", { "dependencies": { "dot-prop": "^5.2.0", "indexes-of": "^1.0.1", "uniq": "^1.0.1" } }, "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA=="], + + "postcss-minify-font-values/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-minify-font-values/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-minify-gradients/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-minify-gradients/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-minify-params/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-minify-params/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-minify-selectors/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-minify-selectors/postcss-selector-parser": ["postcss-selector-parser@3.1.2", "", { "dependencies": { "dot-prop": "^5.2.0", "indexes-of": "^1.0.1", "uniq": "^1.0.1" } }, "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA=="], + + "postcss-modules/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-modules/postcss-modules-extract-imports": ["postcss-modules-extract-imports@2.0.0", "", { "dependencies": { "postcss": "^7.0.5" } }, "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ=="], + + "postcss-modules/postcss-modules-local-by-default": ["postcss-modules-local-by-default@3.0.3", "", { "dependencies": { "icss-utils": "^4.1.1", "postcss": "^7.0.32", "postcss-selector-parser": "^6.0.2", "postcss-value-parser": "^4.1.0" } }, "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw=="], + + "postcss-modules/postcss-modules-scope": ["postcss-modules-scope@2.2.0", "", { "dependencies": { "postcss": "^7.0.6", "postcss-selector-parser": "^6.0.0" } }, "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ=="], + + "postcss-modules/postcss-modules-values": ["postcss-modules-values@3.0.0", "", { "dependencies": { "icss-utils": "^4.0.0", "postcss": "^7.0.6" } }, "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg=="], + + "postcss-modules-extract-imports/postcss": ["postcss@6.0.23", "", { "dependencies": { "chalk": "^2.4.1", "source-map": "^0.6.1", "supports-color": "^5.4.0" } }, "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag=="], + + "postcss-modules-local-by-default/postcss": ["postcss@6.0.23", "", { "dependencies": { "chalk": "^2.4.1", "source-map": "^0.6.1", "supports-color": "^5.4.0" } }, "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag=="], + + "postcss-modules-scope/postcss": ["postcss@6.0.23", "", { "dependencies": { "chalk": "^2.4.1", "source-map": "^0.6.1", "supports-color": "^5.4.0" } }, "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag=="], + + "postcss-modules-values/postcss": ["postcss@6.0.23", "", { "dependencies": { "chalk": "^2.4.1", "source-map": "^0.6.1", "supports-color": "^5.4.0" } }, "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag=="], + + "postcss-normalize-charset/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-display-values/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-display-values/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-normalize-positions/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-positions/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-normalize-repeat-style/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-repeat-style/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-normalize-string/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-string/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-normalize-timing-functions/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-timing-functions/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-normalize-unicode/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-unicode/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-normalize-url/is-absolute-url": ["is-absolute-url@2.1.0", "", {}, "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg=="], + + "postcss-normalize-url/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-url/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-normalize-whitespace/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-normalize-whitespace/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-ordered-values/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-ordered-values/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-reduce-initial/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-reduce-transforms/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-reduce-transforms/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-svgo/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "postcss-svgo/postcss-value-parser": ["postcss-value-parser@3.3.1", "", {}, "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="], + + "postcss-unique-selectors/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "posthtml/posthtml-parser": ["posthtml-parser@0.7.2", "", { "dependencies": { "htmlparser2": "^6.0.0" } }, "sha512-LjEEG/3fNcWZtBfsOE3Gbyg1Li4CmsZRkH1UmbMR7nKdMXVMYI3B4/ZMiCpaq8aI1Aym4FRMMW9SAOLSwOnNsQ=="], + + "posthtml/posthtml-render": ["posthtml-render@1.3.1", "", {}, "sha512-eSToKjNLu0FiF76SSGMHjOFXYzAc/CJqi677Sq6hYvcvFCBtD6de/W5l+0IYPf7ypscqAfjCttxvTdMJt5Gj8Q=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "prop-types-extra/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "public-encrypt/bn.js": ["bn.js@4.12.0", "", {}, "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="], + + "purgecss/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "purgecss/postcss": ["postcss@7.0.32", "", { "dependencies": { "chalk": "^2.4.2", "source-map": "^0.6.1", "supports-color": "^6.1.0" } }, "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw=="], + + "rc-image/@rc-component/portal": ["@rc-component/portal@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-tbXM9SB1r5FOuZjRCljERFByFiEUcMmCWMXLog/NmgCzlAzreXyf23Vei3ZpSMxSMavzPnhCovfZjZdmxS3d1w=="], + + "rc-util/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "request/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "set-value/extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "snapdragon/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "snapdragon/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="], + + "snapdragon/extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "snapdragon-node/define-property": ["define-property@1.0.0", "", { "dependencies": { "is-descriptor": "^1.0.0" } }, "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA=="], + + "snapdragon-util/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "split2/readable-stream": ["readable-stream@3.6.0", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="], + + "static-extend/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="], + + "stream-http/readable-stream": ["readable-stream@3.6.0", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="], + + "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "stylehacks/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "stylehacks/postcss-selector-parser": ["postcss-selector-parser@3.1.2", "", { "dependencies": { "dot-prop": "^5.2.0", "indexes-of": "^1.0.1", "uniq": "^1.0.1" } }, "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA=="], + + "svgo/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "tar-stream/readable-stream": ["readable-stream@3.6.0", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="], + + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "to-object-path/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "tough-cookie/punycode": ["punycode@2.3.0", "", {}, "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="], + + "tr46/punycode": ["punycode@2.3.0", "", {}, "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="], + + "uncss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "uncss/postcss": ["postcss@7.0.39", "", { "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" } }, "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA=="], + + "uncss/postcss-selector-parser": ["postcss-selector-parser@6.0.2", "", { "dependencies": { "cssesc": "^3.0.0", "indexes-of": "^1.0.1", "uniq": "^1.0.1" } }, "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg=="], + + "union-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "unset-value/has-value": ["has-value@0.3.1", "", { "dependencies": { "get-value": "^2.0.3", "has-values": "^0.1.4", "isobject": "^2.0.0" } }, "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q=="], + + "uri-js/punycode": ["punycode@2.3.0", "", {}, "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="], + + "util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], + + "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + + "verror/extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "@parcel/node-resolver-core/micromatch/braces": ["braces@2.3.2", "", { "dependencies": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", "extend-shallow": "^2.0.1", "fill-range": "^4.0.0", "isobject": "^3.0.1", "repeat-element": "^1.1.2", "snapdragon": "^0.8.1", "snapdragon-node": "^2.0.1", "split-string": "^3.0.2", "to-regex": "^3.0.1" } }, "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w=="], + + "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "browserify-sign/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "class-utils/define-property/is-descriptor": ["is-descriptor@0.1.6", "", { "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" } }, "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg=="], + + "coa/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "coa/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "coa/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "css-declaration-sorter/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "css-declaration-sorter/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "css-modules-loader-core/postcss/chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="], + + "css-modules-loader-core/postcss/supports-color": ["supports-color@3.2.3", "", { "dependencies": { "has-flag": "^1.0.0" } }, "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A=="], + + "css-select/domutils/dom-serializer": ["dom-serializer@0.2.2", "", { "dependencies": { "domelementtype": "^2.0.1", "entities": "^2.0.0" } }, "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g=="], + + "css-select/domutils/domelementtype": ["domelementtype@1.3.1", "", {}, "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="], + + "cssnano-preset-default/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "cssnano-preset-default/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "cssnano-util-raw-cache/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "cssnano-util-raw-cache/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "cssnano/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], + + "cssnano/cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + + "cssnano/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "cssnano/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "csso/css-tree/mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], + + "csso/css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "expand-brackets/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "expand-brackets/define-property/is-descriptor": ["is-descriptor@0.1.6", "", { "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" } }, "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg=="], + + "expand-brackets/extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "extglob/extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "has-values/is-number/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "hash-base/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "icss-utils/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "icss-utils/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "object-copy/define-property/is-descriptor": ["is-descriptor@0.1.6", "", { "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" } }, "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg=="], + + "postcss-calc/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-calc/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-colormin/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-colormin/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-convert-values/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-convert-values/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-discard-comments/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-discard-comments/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-discard-duplicates/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-discard-duplicates/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-discard-empty/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-discard-empty/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-discard-overridden/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-discard-overridden/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-merge-longhand/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-merge-longhand/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-merge-rules/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-merge-rules/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-minify-font-values/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-minify-font-values/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-minify-gradients/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-minify-gradients/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-minify-params/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-minify-params/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-minify-selectors/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-minify-selectors/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-modules-extract-imports/postcss/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "postcss-modules-extract-imports/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-modules-extract-imports/postcss/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "postcss-modules-local-by-default/postcss/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "postcss-modules-local-by-default/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-modules-local-by-default/postcss/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "postcss-modules-scope/postcss/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "postcss-modules-scope/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-modules-scope/postcss/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "postcss-modules-values/postcss/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "postcss-modules-values/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-modules-values/postcss/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "postcss-modules/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-modules/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-charset/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-charset/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-display-values/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-display-values/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-positions/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-positions/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-repeat-style/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-repeat-style/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-string/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-string/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-timing-functions/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-timing-functions/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-unicode/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-unicode/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-url/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-url/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-normalize-whitespace/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-normalize-whitespace/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-ordered-values/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-ordered-values/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-reduce-initial/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-reduce-initial/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-reduce-transforms/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-reduce-transforms/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-svgo/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-svgo/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "postcss-unique-selectors/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "postcss-unique-selectors/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "posthtml/posthtml-parser/htmlparser2": ["htmlparser2@6.1.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", "domutils": "^2.5.2", "entities": "^2.0.0" } }, "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A=="], + + "purgecss/postcss/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "purgecss/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "purgecss/postcss/supports-color": ["supports-color@6.1.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ=="], + + "request/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "snapdragon/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "snapdragon/define-property/is-descriptor": ["is-descriptor@0.1.6", "", { "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" } }, "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg=="], + + "snapdragon/extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "split2/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "static-extend/define-property/is-descriptor": ["is-descriptor@0.1.6", "", { "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" } }, "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg=="], + + "stream-http/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "stylehacks/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "stylehacks/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "svgo/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "svgo/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "svgo/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "uncss/postcss/picocolors": ["picocolors@0.2.1", "", {}, "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="], + + "uncss/postcss/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "unset-value/has-value/has-values": ["has-values@0.1.4", "", {}, "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ=="], + + "unset-value/has-value/isobject": ["isobject@2.1.0", "", { "dependencies": { "isarray": "1.0.0" } }, "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA=="], + + "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "@parcel/node-resolver-core/micromatch/braces/extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "@parcel/node-resolver-core/micromatch/braces/fill-range": ["fill-range@4.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", "repeat-string": "^1.6.1", "to-regex-range": "^2.1.0" } }, "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ=="], + + "class-utils/define-property/is-descriptor/is-accessor-descriptor": ["is-accessor-descriptor@0.1.6", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A=="], + + "class-utils/define-property/is-descriptor/is-data-descriptor": ["is-data-descriptor@0.1.4", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg=="], + + "class-utils/define-property/is-descriptor/kind-of": ["kind-of@5.1.0", "", {}, "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="], + + "coa/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "coa/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "css-modules-loader-core/postcss/chalk/ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="], + + "css-modules-loader-core/postcss/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "css-modules-loader-core/postcss/chalk/strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="], + + "css-modules-loader-core/postcss/chalk/supports-color": ["supports-color@2.0.0", "", {}, "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g=="], + + "css-modules-loader-core/postcss/supports-color/has-flag": ["has-flag@1.0.0", "", {}, "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA=="], + + "css-select/domutils/dom-serializer/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "cssnano/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], + + "expand-brackets/define-property/is-descriptor/is-accessor-descriptor": ["is-accessor-descriptor@0.1.6", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A=="], + + "expand-brackets/define-property/is-descriptor/is-data-descriptor": ["is-data-descriptor@0.1.4", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg=="], + + "expand-brackets/define-property/is-descriptor/kind-of": ["kind-of@5.1.0", "", {}, "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="], + + "object-copy/define-property/is-descriptor/is-accessor-descriptor": ["is-accessor-descriptor@0.1.6", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A=="], + + "object-copy/define-property/is-descriptor/is-data-descriptor": ["is-data-descriptor@0.1.4", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg=="], + + "object-copy/define-property/is-descriptor/kind-of": ["kind-of@5.1.0", "", {}, "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="], + + "postcss-modules-extract-imports/postcss/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "postcss-modules-extract-imports/postcss/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "postcss-modules-extract-imports/postcss/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "postcss-modules-local-by-default/postcss/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "postcss-modules-local-by-default/postcss/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "postcss-modules-local-by-default/postcss/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "postcss-modules-scope/postcss/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "postcss-modules-scope/postcss/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "postcss-modules-scope/postcss/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "postcss-modules-values/postcss/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "postcss-modules-values/postcss/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "postcss-modules-values/postcss/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "posthtml/posthtml-parser/htmlparser2/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "purgecss/postcss/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "purgecss/postcss/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "purgecss/postcss/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "purgecss/postcss/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "snapdragon/define-property/is-descriptor/is-accessor-descriptor": ["is-accessor-descriptor@0.1.6", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A=="], + + "snapdragon/define-property/is-descriptor/is-data-descriptor": ["is-data-descriptor@0.1.4", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg=="], + + "snapdragon/define-property/is-descriptor/kind-of": ["kind-of@5.1.0", "", {}, "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="], + + "static-extend/define-property/is-descriptor/is-accessor-descriptor": ["is-accessor-descriptor@0.1.6", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A=="], + + "static-extend/define-property/is-descriptor/is-data-descriptor": ["is-data-descriptor@0.1.4", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg=="], + + "static-extend/define-property/is-descriptor/kind-of": ["kind-of@5.1.0", "", {}, "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="], + + "svgo/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "svgo/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "@parcel/node-resolver-core/micromatch/braces/extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "@parcel/node-resolver-core/micromatch/braces/fill-range/is-number": ["is-number@3.0.0", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg=="], + + "@parcel/node-resolver-core/micromatch/braces/fill-range/to-regex-range": ["to-regex-range@2.1.1", "", { "dependencies": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" } }, "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg=="], + + "class-utils/define-property/is-descriptor/is-accessor-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "class-utils/define-property/is-descriptor/is-data-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "coa/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "css-modules-loader-core/postcss/chalk/strip-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "expand-brackets/define-property/is-descriptor/is-accessor-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "expand-brackets/define-property/is-descriptor/is-data-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "object-copy/define-property/is-descriptor/is-accessor-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "object-copy/define-property/is-descriptor/is-data-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "postcss-modules-extract-imports/postcss/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "postcss-modules-local-by-default/postcss/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "postcss-modules-scope/postcss/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "postcss-modules-values/postcss/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "purgecss/postcss/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "purgecss/postcss/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "snapdragon/define-property/is-descriptor/is-accessor-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "snapdragon/define-property/is-descriptor/is-data-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "static-extend/define-property/is-descriptor/is-accessor-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "static-extend/define-property/is-descriptor/is-data-descriptor/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "svgo/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "@parcel/node-resolver-core/micromatch/braces/fill-range/is-number/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "postcss-modules-extract-imports/postcss/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "postcss-modules-local-by-default/postcss/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "postcss-modules-scope/postcss/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "postcss-modules-values/postcss/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "purgecss/postcss/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + } +} diff --git a/bench/hot-module-reloading/css-stress-test/bun.lockb b/bench/hot-module-reloading/css-stress-test/bun.lockb deleted file mode 100755 index b2d009e0de..0000000000 Binary files a/bench/hot-module-reloading/css-stress-test/bun.lockb and /dev/null differ diff --git a/bench/hot-module-reloading/css-stress-test/src/index.tsx b/bench/hot-module-reloading/css-stress-test/src/index.tsx index 5eefb43040..c8b470ec7a 100644 --- a/bench/hot-module-reloading/css-stress-test/src/index.tsx +++ b/bench/hot-module-reloading/css-stress-test/src/index.tsx @@ -1,7 +1,7 @@ import ReactDOM from "react-dom"; import { Main } from "./main"; -const Base = ({}) => { +const Base = () => { const name = typeof location !== "undefined" ? decodeURIComponent(location.search.substring(1)) : null; return
; }; diff --git a/bench/package.json b/bench/package.json index a80d7566dc..d71efc00aa 100644 --- a/bench/package.json +++ b/bench/package.json @@ -13,7 +13,7 @@ "execa": "^8.0.1", "fast-glob": "3.3.1", "fdir": "^6.1.0", - "mitata": "^1.0.10", + "mitata": "^1.0.25", "react": "^18.3.1", "react-dom": "^18.3.1", "string-width": "7.1.0", diff --git a/bench/react-hello-world/bun.lock b/bench/react-hello-world/bun.lock new file mode 100644 index 0000000000..77ae3a3437 --- /dev/null +++ b/bench/react-hello-world/bun.lock @@ -0,0 +1,23 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "react-hello-world", + "dependencies": { + "react": "next", + "react-dom": "next", + }, + }, + }, + "packages": { + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "react": ["react@18.3.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-l6RbwXa9Peerh9pQEq62DDypxSQfavbybY0wV1vwZ63X0P5VaaEesZAz1KPpnVvXjTtQaOMQsIPvnQwmaVqzTQ=="], + + "react-dom": ["react-dom@18.3.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "0.24.0-next-b72ed698f-20230303" }, "peerDependencies": { "react": "18.3.0-next-b72ed698f-20230303" } }, "sha512-0Gh/gmTT6H8KxswIQB/8shdTTfs6QIu86nNqZf3Y0RBqIwgTVxRaQVz14/Fw4/Nt81nK/Jt6KT4bx3yvOxZDGQ=="], + + "scheduler": ["scheduler@0.24.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ct4DMMFbc2kFxCdvbG+i/Jn1S1oqrIFSn2VX/mam+Ya0iuNy+lb8rgT7A+YBUqrQNDaNEqABYI2sOQgqoRxp7w=="], + } +} diff --git a/bench/react-hello-world/bun.lockb b/bench/react-hello-world/bun.lockb deleted file mode 100755 index 6fd04cd03b..0000000000 Binary files a/bench/react-hello-world/bun.lockb and /dev/null differ diff --git a/bench/scanner/bun.lock b/bench/scanner/bun.lock new file mode 100644 index 0000000000..0640559c71 --- /dev/null +++ b/bench/scanner/bun.lock @@ -0,0 +1,56 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "scan", + "dependencies": { + "esbuild": "^0.14.11", + }, + }, + }, + "packages": { + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.14.54", "", { "os": "linux", "cpu": "none" }, "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw=="], + + "esbuild": ["esbuild@0.14.54", "", { "dependencies": { "@esbuild/linux-loong64": "0.14.54", "esbuild-android-64": "0.14.54", "esbuild-android-arm64": "0.14.54", "esbuild-darwin-64": "0.14.54", "esbuild-darwin-arm64": "0.14.54", "esbuild-freebsd-64": "0.14.54", "esbuild-freebsd-arm64": "0.14.54", "esbuild-linux-32": "0.14.54", "esbuild-linux-64": "0.14.54", "esbuild-linux-arm": "0.14.54", "esbuild-linux-arm64": "0.14.54", "esbuild-linux-mips64le": "0.14.54", "esbuild-linux-ppc64le": "0.14.54", "esbuild-linux-riscv64": "0.14.54", "esbuild-linux-s390x": "0.14.54", "esbuild-netbsd-64": "0.14.54", "esbuild-openbsd-64": "0.14.54", "esbuild-sunos-64": "0.14.54", "esbuild-windows-32": "0.14.54", "esbuild-windows-64": "0.14.54", "esbuild-windows-arm64": "0.14.54" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA=="], + + "esbuild-android-64": ["esbuild-android-64@0.14.54", "", { "os": "android", "cpu": "x64" }, "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ=="], + + "esbuild-android-arm64": ["esbuild-android-arm64@0.14.54", "", { "os": "android", "cpu": "arm64" }, "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg=="], + + "esbuild-darwin-64": ["esbuild-darwin-64@0.14.54", "", { "os": "darwin", "cpu": "x64" }, "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug=="], + + "esbuild-darwin-arm64": ["esbuild-darwin-arm64@0.14.54", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw=="], + + "esbuild-freebsd-64": ["esbuild-freebsd-64@0.14.54", "", { "os": "freebsd", "cpu": "x64" }, "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg=="], + + "esbuild-freebsd-arm64": ["esbuild-freebsd-arm64@0.14.54", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q=="], + + "esbuild-linux-32": ["esbuild-linux-32@0.14.54", "", { "os": "linux", "cpu": "ia32" }, "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw=="], + + "esbuild-linux-64": ["esbuild-linux-64@0.14.54", "", { "os": "linux", "cpu": "x64" }, "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg=="], + + "esbuild-linux-arm": ["esbuild-linux-arm@0.14.54", "", { "os": "linux", "cpu": "arm" }, "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw=="], + + "esbuild-linux-arm64": ["esbuild-linux-arm64@0.14.54", "", { "os": "linux", "cpu": "arm64" }, "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig=="], + + "esbuild-linux-mips64le": ["esbuild-linux-mips64le@0.14.54", "", { "os": "linux", "cpu": "none" }, "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw=="], + + "esbuild-linux-ppc64le": ["esbuild-linux-ppc64le@0.14.54", "", { "os": "linux", "cpu": "ppc64" }, "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ=="], + + "esbuild-linux-riscv64": ["esbuild-linux-riscv64@0.14.54", "", { "os": "linux", "cpu": "none" }, "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg=="], + + "esbuild-linux-s390x": ["esbuild-linux-s390x@0.14.54", "", { "os": "linux", "cpu": "s390x" }, "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA=="], + + "esbuild-netbsd-64": ["esbuild-netbsd-64@0.14.54", "", { "os": "none", "cpu": "x64" }, "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w=="], + + "esbuild-openbsd-64": ["esbuild-openbsd-64@0.14.54", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw=="], + + "esbuild-sunos-64": ["esbuild-sunos-64@0.14.54", "", { "os": "sunos", "cpu": "x64" }, "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw=="], + + "esbuild-windows-32": ["esbuild-windows-32@0.14.54", "", { "os": "win32", "cpu": "ia32" }, "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w=="], + + "esbuild-windows-64": ["esbuild-windows-64@0.14.54", "", { "os": "win32", "cpu": "x64" }, "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ=="], + + "esbuild-windows-arm64": ["esbuild-windows-arm64@0.14.54", "", { "os": "win32", "cpu": "arm64" }, "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg=="], + } +} diff --git a/bench/scanner/bun.lockb b/bench/scanner/bun.lockb deleted file mode 100755 index 4bfd38af12..0000000000 Binary files a/bench/scanner/bun.lockb and /dev/null differ diff --git a/bench/snippets/byteLength.mjs b/bench/snippets/byteLength.mjs new file mode 100644 index 0000000000..810bf487fd --- /dev/null +++ b/bench/snippets/byteLength.mjs @@ -0,0 +1,27 @@ +import { Buffer } from "node:buffer"; +import { bench, run } from "../runner.mjs"; + +const variations = [ + ["latin1", "hello world"], + ["utf16", "hello emoji 🤔"], +]; + +for (const [label, string] of variations) { + const big = Buffer.alloc(1000000, string).toString(); + const small = Buffer.from(string).toString(); + const substring = big.slice(0, big.length - 2); + + bench(`${substring.length}`, () => { + return Buffer.byteLength(substring, "utf8"); + }); + + bench(`${small.length}`, () => { + return Buffer.byteLength(small); + }); + + bench(`${big.length}`, () => { + return Buffer.byteLength(big); + }); +} + +await run(); diff --git a/bench/snippets/hash.mjs b/bench/snippets/hash.mjs new file mode 100644 index 0000000000..bcc619ff30 --- /dev/null +++ b/bench/snippets/hash.mjs @@ -0,0 +1,97 @@ +import { bench, run } from "../runner.mjs"; + +// Can be strings or buffers. +const shortStr = Buffer.from("abcd1234"); // 8 chars +const longStr = Buffer.alloc(128 * 1024, "xABcDpQrStUvWxYz=-1]23]12312312][3123][123]["); + +// Short string benchmarks + +bench("wyhash (short)", () => { + Bun.hash.wyhash(shortStr); +}); + +bench("adler32 (short)", () => { + Bun.hash.adler32(shortStr); +}); + +bench("crc32 (short)", () => { + Bun.hash.crc32(shortStr); +}); + +bench("cityHash32 (short)", () => { + Bun.hash.cityHash32(shortStr); +}); + +bench("cityHash64 (short)", () => { + Bun.hash.cityHash64(shortStr); +}); + +bench("xxHash32 (short)", () => { + Bun.hash.xxHash32(shortStr); +}); + +bench("xxHash64 (short)", () => { + Bun.hash.xxHash64(shortStr); +}); + +bench("xxHash3 (short)", () => { + Bun.hash.xxHash3(shortStr); +}); + +bench("murmur32v3 (short)", () => { + Bun.hash.murmur32v3(shortStr); +}); + +bench("murmur32v2 (short)", () => { + Bun.hash.murmur32v2(shortStr); +}); + +bench("murmur64v2 (short)", () => { + Bun.hash.murmur64v2(shortStr); +}); + +bench("wyhash (128 KB)", () => { + Bun.hash.wyhash(longStr); +}); + +bench("adler32 (128 KB)", () => { + Bun.hash.adler32(longStr); +}); + +bench("crc32 (128 KB)", () => { + Bun.hash.crc32(longStr); +}); + +bench("cityHash32 (128 KB)", () => { + Bun.hash.cityHash32(longStr); +}); + +bench("cityHash64 (128 KB)", () => { + Bun.hash.cityHash64(longStr); +}); + +bench("xxHash32 (128 KB)", () => { + Bun.hash.xxHash32(longStr); +}); + +bench("xxHash64 (128 KB)", () => { + Bun.hash.xxHash64(longStr); +}); + +bench("xxHash3 (128 KB)", () => { + Bun.hash.xxHash3(longStr); +}); + +bench("murmur32v3 (128 KB)", () => { + Bun.hash.murmur32v3(longStr); +}); + +bench("murmur32v2 (128 KB)", () => { + Bun.hash.murmur32v2(longStr); +}); + +bench("murmur64v2 (128 KB)", () => { + Bun.hash.murmur64v2(longStr); +}); + +run(); diff --git a/bench/snippets/native-overhead.mjs b/bench/snippets/native-overhead.mjs index 32d459247e..43576b21d4 100644 --- a/bench/snippets/native-overhead.mjs +++ b/bench/snippets/native-overhead.mjs @@ -1,20 +1,14 @@ +import { noOpForTesting as noop } from "bun:internal-for-testing"; import { bench, run } from "../runner.mjs"; // These are no-op C++ functions that are exported to JS. -const lazy = globalThis[Symbol.for("Bun.lazy")]; -const noop = lazy("noop"); const fn = noop.function; -const regular = noop.functionRegular; const callback = noop.callback; bench("C++ callback into JS", () => { callback(() => {}); }); -bench("C++ fn regular", () => { - regular(); -}); - bench("C++ fn", () => { fn(); }); diff --git a/bench/snippets/node-zlib-brotli.mjs b/bench/snippets/node-zlib-brotli.mjs new file mode 100644 index 0000000000..01208d3ec9 --- /dev/null +++ b/bench/snippets/node-zlib-brotli.mjs @@ -0,0 +1,37 @@ +import { bench, run } from "../runner.mjs"; +import { brotliCompress, brotliDecompress, createBrotliCompress, createBrotliDecompress } from "node:zlib"; +import { promisify } from "node:util"; +import { pipeline } from "node:stream/promises"; +import { Readable } from "node:stream"; +import { readFileSync } from "node:fs"; + +const brotliCompressAsync = promisify(brotliCompress); +const brotliDecompressAsync = promisify(brotliDecompress); + +const testData = + process.argv.length > 2 + ? readFileSync(process.argv[2]) + : Buffer.alloc(1024 * 1024 * 16, "abcdefghijklmnopqrstuvwxyz"); +let compressed; + +bench("brotli compress", async () => { + compressed = await brotliCompressAsync(testData); +}); + +bench("brotli decompress", async () => { + await brotliDecompressAsync(compressed); +}); + +bench("brotli compress stream", async () => { + const source = Readable.from([testData]); + const compress = createBrotliCompress(); + await pipeline(source, compress); +}); + +bench("brotli decompress stream", async () => { + const source = Readable.from([compressed]); + const decompress = createBrotliDecompress(); + await pipeline(source, decompress); +}); + +await run(); diff --git a/bench/snippets/urlsearchparams.mjs b/bench/snippets/urlsearchparams.mjs index 83a874dc5f..4663dbfedf 100644 --- a/bench/snippets/urlsearchparams.mjs +++ b/bench/snippets/urlsearchparams.mjs @@ -10,7 +10,6 @@ bench("new URLSearchParams(obj)", () => { "Content-Length": "123", "User-Agent": "node-fetch/1.0", "Accept-Encoding": "gzip,deflate", - "Content-Length": "0", "Content-Range": "bytes 0-9/10", }); }); diff --git a/bench/snippets/zlib.mjs b/bench/snippets/zlib.mjs new file mode 100644 index 0000000000..8bfc87e308 --- /dev/null +++ b/bench/snippets/zlib.mjs @@ -0,0 +1,62 @@ +import { bench, run } from "../runner.mjs"; +import zlib from "node:zlib"; +import { promisify } from "node:util"; + +const deflate = promisify(zlib.deflate); +const inflate = promisify(zlib.inflate); + +const short = "Hello World!"; +const long = "Hello World!".repeat(1024); +const veryLong = "Hello World!".repeat(10240); + +// Pre-compress some data for decompression tests +const shortBuf = Buffer.from(short); +const longBuf = Buffer.from(long); +const veryLongBuf = Buffer.from(veryLong); + +let [shortCompressed, longCompressed, veryLongCompressed] = await Promise.all([ + deflate(shortBuf, { level: 6 }), + deflate(longBuf, { level: 6 }), + deflate(veryLongBuf, { level: 6 }), +]); + +const format = new Intl.NumberFormat("en-US", { notation: "compact", unit: "byte" }); +// Compression tests at different levels +bench(`deflate ${format.format(short.length)}B (level 1)`, async () => { + await deflate(shortBuf, { level: 1 }); +}); + +bench(`deflate ${format.format(short.length)} (level 6)`, async () => { + await deflate(shortBuf, { level: 6 }); +}); + +bench(`deflate ${format.format(long.length)} (level 1)`, async () => { + await deflate(longBuf, { level: 1 }); +}); + +bench(`deflate ${format.format(long.length)} (level 6)`, async () => { + await deflate(longBuf, { level: 6 }); +}); + +bench(`deflate ${format.format(veryLong.length)} (level 1)`, async () => { + await deflate(veryLongBuf, { level: 1 }); +}); + +bench(`deflate ${format.format(veryLong.length)} (level 6)`, async () => { + await deflate(veryLongBuf, { level: 6 }); +}); + +// Decompression tests +bench(`inflate ${format.format(short.length)}`, async () => { + await inflate(shortCompressed); +}); + +bench(`inflate ${format.format(long.length)}`, async () => { + await inflate(longCompressed); +}); + +bench(`inflate ${format.format(veryLong.length)}`, async () => { + await inflate(veryLongCompressed); +}); + +await run(); diff --git a/bench/sqlite/bun.lock b/bench/sqlite/bun.lock new file mode 100644 index 0000000000..810e0c4b2e --- /dev/null +++ b/bench/sqlite/bun.lock @@ -0,0 +1,92 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "bench", + "dependencies": { + "better-sqlite3": "8.5.0", + }, + }, + }, + "packages": { + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-sqlite3": ["better-sqlite3@8.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.0" } }, "sha512-vbPcv/Hx5WYdyNg/NbcfyaBZyv9s/NVbxb7yCeC5Bq1pVocNxeL2tZmSu3Rlm4IEOTjYdGyzWQgyx0OSdORBzw=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "detect-libc": ["detect-libc@2.0.1", "", {}, "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "napi-build-utils": ["napi-build-utils@1.0.2", "", {}, "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="], + + "node-abi": ["node-abi@3.35.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-jAlSOFR1Bls963NmFwxeQkNTzqjUF0NThm8Le7eRIRGzFUVJuMOFZDLv5Y30W/Oaw+KEebEJLAigwO9gQHoEmw=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "prebuild-install": ["prebuild-install@7.1.1", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw=="], + + "pump": ["pump@3.0.0", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "semver": ["semver@7.4.0", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "tar-fs": ["tar-fs@2.1.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "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" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + } +} diff --git a/bench/sqlite/bun.lockb b/bench/sqlite/bun.lockb deleted file mode 100755 index 94f137da01..0000000000 Binary files a/bench/sqlite/bun.lockb and /dev/null differ diff --git a/bench/websocket-server/bun.lock b/bench/websocket-server/bun.lock new file mode 100644 index 0000000000..3587fe51bf --- /dev/null +++ b/bench/websocket-server/bun.lock @@ -0,0 +1,22 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "websocket-server", + "dependencies": { + "bufferutil": "4.0.7", + "utf-8-validate": "6.0.3", + "ws": "8.13.0", + }, + }, + }, + "packages": { + "bufferutil": ["bufferutil@4.0.7", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw=="], + + "node-gyp-build": ["node-gyp-build@4.6.0", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ=="], + + "utf-8-validate": ["utf-8-validate@6.0.3", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA=="], + + "ws": ["ws@8.13.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["utf-8-validate"] }, "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA=="], + } +} diff --git a/bench/websocket-server/bun.lockb b/bench/websocket-server/bun.lockb deleted file mode 100755 index 496d5c9c31..0000000000 Binary files a/bench/websocket-server/bun.lockb and /dev/null differ diff --git a/build.zig b/build.zig index fed6086672..abcefc752b 100644 --- a/build.zig +++ b/build.zig @@ -200,7 +200,10 @@ pub fn build(b: *Build) !void { b.option([]const u8, "codegen_path", "Set the generated code directory") orelse "build/debug/codegen", ); - const codegen_embed = b.option(bool, "codegen_embed", "If codegen files should be embedded in the binary") orelse false; + const codegen_embed = b.option(bool, "codegen_embed", "If codegen files should be embedded in the binary") orelse switch (b.release_mode) { + .off => false, + else => true, + }; const bun_version = b.option([]const u8, "version", "Value of `Bun.version`") orelse "0.0.0"; @@ -327,6 +330,25 @@ pub fn build(b: *Build) !void { .{ .os = .windows, .arch = .x86_64 }, }); } + + // zig build translate-c-headers + { + const step = b.step("translate-c", "Copy generated translated-c-headers.zig to zig-out"); + step.dependOn(&b.addInstallFile(getTranslateC(b, b.host, .Debug).getOutput(), "translated-c-headers.zig").step); + } + + // zig build enum-extractor + { + // const step = b.step("enum-extractor", "Extract enum definitions (invoked by a code generator)"); + // const exe = b.addExecutable(.{ + // .name = "enum_extractor", + // .root_source_file = b.path("./src/generated_enum_extractor.zig"), + // .target = b.graph.host, + // .optimize = .Debug, + // }); + // const run = b.addRunArtifact(exe); + // step.dependOn(&run.step); + } } pub fn addMultiCheck( @@ -367,6 +389,25 @@ pub fn addMultiCheck( } } +fn getTranslateC(b: *Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *Step.TranslateC { + const translate_c = b.addTranslateC(.{ + .root_source_file = b.path("src/c-headers-for-zig.h"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + inline for ([_](struct { []const u8, bool }){ + .{ "WINDOWS", translate_c.target.result.os.tag == .windows }, + .{ "POSIX", translate_c.target.result.os.tag != .windows }, + .{ "LINUX", translate_c.target.result.os.tag == .linux }, + .{ "DARWIN", translate_c.target.result.os.tag.isDarwin() }, + }) |entry| { + const str, const value = entry; + translate_c.defineCMacroRaw(b.fmt("{s}={d}", .{ str, @intFromBool(value) })); + } + return translate_c; +} + pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile { const obj = b.addObject(.{ .name = if (opts.optimize == .Debug) "bun-debug" else "bun", @@ -414,6 +455,10 @@ pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile { } addInternalPackages(b, obj, opts); obj.root_module.addImport("build_options", opts.buildOptionsModule(b)); + + const translate_c = getTranslateC(b, opts.target, opts.optimize); + obj.root_module.addImport("translated-c-headers", translate_c.createModule()); + return obj; } @@ -480,36 +525,36 @@ fn addInternalPackages(b: *Build, obj: *Compile, opts: *BunBuildOptions) void { .{ .file = "ZigGeneratedClasses.zig", .import = "ZigGeneratedClasses" }, .{ .file = "ResolvedSourceTag.zig", .import = "ResolvedSourceTag" }, .{ .file = "ErrorCode.zig", .import = "ErrorCode" }, - .{ .file = "runtime.out.js" }, + .{ .file = "runtime.out.js", .enable = opts.shouldEmbedCode() }, .{ .file = "bake.client.js", .import = "bake-codegen/bake.client.js", .enable = opts.shouldEmbedCode() }, .{ .file = "bake.error.js", .import = "bake-codegen/bake.error.js", .enable = opts.shouldEmbedCode() }, .{ .file = "bake.server.js", .import = "bake-codegen/bake.server.js", .enable = opts.shouldEmbedCode() }, .{ .file = "bun-error/index.js", .enable = opts.shouldEmbedCode() }, .{ .file = "bun-error/bun-error.css", .enable = opts.shouldEmbedCode() }, .{ .file = "fallback-decoder.js", .enable = opts.shouldEmbedCode() }, - .{ .file = "node-fallbacks/assert.js" }, - .{ .file = "node-fallbacks/buffer.js" }, - .{ .file = "node-fallbacks/console.js" }, - .{ .file = "node-fallbacks/constants.js" }, - .{ .file = "node-fallbacks/crypto.js" }, - .{ .file = "node-fallbacks/domain.js" }, - .{ .file = "node-fallbacks/events.js" }, - .{ .file = "node-fallbacks/http.js" }, - .{ .file = "node-fallbacks/https.js" }, - .{ .file = "node-fallbacks/net.js" }, - .{ .file = "node-fallbacks/os.js" }, - .{ .file = "node-fallbacks/path.js" }, - .{ .file = "node-fallbacks/process.js" }, - .{ .file = "node-fallbacks/punycode.js" }, - .{ .file = "node-fallbacks/querystring.js" }, - .{ .file = "node-fallbacks/stream.js" }, - .{ .file = "node-fallbacks/string_decoder.js" }, - .{ .file = "node-fallbacks/sys.js" }, - .{ .file = "node-fallbacks/timers.js" }, - .{ .file = "node-fallbacks/tty.js" }, - .{ .file = "node-fallbacks/url.js" }, - .{ .file = "node-fallbacks/util.js" }, - .{ .file = "node-fallbacks/zlib.js" }, + .{ .file = "node-fallbacks/assert.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/buffer.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/console.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/constants.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/crypto.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/domain.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/events.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/http.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/https.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/net.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/os.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/path.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/process.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/punycode.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/querystring.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/stream.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/string_decoder.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/sys.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/timers.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/tty.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/url.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/util.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "node-fallbacks/zlib.js", .enable = opts.shouldEmbedCode() }, }) |entry| { if (!@hasField(@TypeOf(entry), "enable") or entry.enable) { const path = b.pathJoin(&.{ opts.codegen_path, entry.file }); diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000000..955c0f5c38 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1068 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "bun", + "devDependencies": { + "@mdn/browser-compat-data": "~5.5.28", + "@types/bun": "^1.1.3", + "@types/react": "^18.3.3", + "@typescript-eslint/eslint-plugin": "^7.11.0", + "@typescript-eslint/parser": "^7.11.0", + "@vscode/debugadapter": "^1.65.0", + "autoprefixer": "^10.4.19", + "caniuse-lite": "^1.0.30001620", + "esbuild": "^0.21.4", + "eslint": "^9.4.0", + "eslint-config-prettier": "^9.1.0", + "mitata": "^0.1.11", + "peechy": "0.4.34", + "prettier": "^3.2.5", + "prettier-plugin-organize-imports": "^4.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "source-map-js": "^1.2.0", + "typescript": "^5.7.2", + }, + }, + "packages/bun-types": { + "name": "bun-types", + "dependencies": { + "@types/node": "~20.12.8", + "@types/ws": "~8.5.10", + }, + "devDependencies": { + "@biomejs/biome": "^1.5.3", + "@definitelytyped/dtslint": "^0.0.199", + "@definitelytyped/eslint-plugin": "^0.0.197", + "typescript": "^5.0.2", + }, + }, + }, + "overrides": { + "bun-types": "workspace:packages/bun-types", + }, + "packages": { + "@biomejs/biome": ["@biomejs/biome@1.8.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.8.3", "@biomejs/cli-darwin-x64": "1.8.3", "@biomejs/cli-linux-arm64": "1.8.3", "@biomejs/cli-linux-arm64-musl": "1.8.3", "@biomejs/cli-linux-x64": "1.8.3", "@biomejs/cli-linux-x64-musl": "1.8.3", "@biomejs/cli-win32-arm64": "1.8.3", "@biomejs/cli-win32-x64": "1.8.3" }, "bin": { "biome": "bin/biome" } }, "sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.8.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.8.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.8.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.8.3", "", { "os": "linux", "cpu": "x64" }, "sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.8.3", "", { "os": "linux", "cpu": "x64" }, "sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.8.3", "", { "os": "win32", "cpu": "x64" }, "sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg=="], + + "@definitelytyped/dts-critic": ["@definitelytyped/dts-critic@0.0.191", "", { "dependencies": { "@definitelytyped/header-parser": "0.0.190", "command-exists": "^1.2.9", "semver": "^7.5.4", "tmp": "^0.2.1", "typescript": "^5.2.2", "yargs": "^17.7.2" } }, "sha512-j5HK3pQYiQwSXRLJzyhXJ6KxdzLl4gXXhz3ysCtLnRQkj+zsEfloDkEZ3x2bZMWS0OsKLXmR91JeQ2/c9DFEjg=="], + + "@definitelytyped/dtslint": ["@definitelytyped/dtslint@0.0.199", "", { "dependencies": { "@definitelytyped/dts-critic": "0.0.191", "@definitelytyped/header-parser": "0.0.190", "@definitelytyped/typescript-versions": "0.0.182", "@definitelytyped/utils": "0.0.188", "@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/parser": "^6.11.0", "@typescript-eslint/types": "^6.11.0", "@typescript-eslint/typescript-estree": "^6.11.0", "@typescript-eslint/utils": "^6.11.0", "eslint": "^8.53.0", "eslint-plugin-import": "^2.29.0", "json-stable-stringify": "^1.0.2", "strip-json-comments": "^3.1.1", "tslint": "5.14.0", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.7.0-dev || >= 3.8.0-dev || >= 3.9.0-dev || >= 4.0.0-dev || >=5.0.0-dev" }, "bin": { "dtslint": "dist/index.js" } }, "sha512-GlPqLuFhylYSv0twBLaq5U+LyoDDysew99D5/AGxvhyy9vZZFcPyu6pLg4TCcefYE2DbC2qH5Ncw99W/aGMstA=="], + + "@definitelytyped/eslint-plugin": ["@definitelytyped/eslint-plugin@0.0.197", "", { "dependencies": { "@definitelytyped/utils": "0.0.188", "@typescript-eslint/types": "^6.11.0", "@typescript-eslint/utils": "^6.11.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/parser": "^6.11.0", "eslint": "^8.40.0", "eslint-plugin-jsdoc": "^44.0.0", "typescript": ">= 3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.7.0-dev || >= 3.8.0-dev || >= 3.9.0-dev || >= 4.0.0-dev || >=5.0.0-dev" } }, "sha512-jHR2tG8+JSLDIV2jhpbk+7bQXrPlwukGjRslHd6OYahGkynKZ2wa38Bd+ZXSDtVDaF4e6qnkD5GDDwb7TPrpAg=="], + + "@definitelytyped/header-parser": ["@definitelytyped/header-parser@0.0.190", "", { "dependencies": { "@definitelytyped/typescript-versions": "0.0.182", "@definitelytyped/utils": "0.0.188", "semver": "^7.5.4" } }, "sha512-awWRynVpFt6uAVDzgOa1Ry0ttjQywtt4nh9wa3/MbSTEx6PNohL1X6xDjifUElLSTIUMDSAJyWO9FuKBjnX7IQ=="], + + "@definitelytyped/typescript-versions": ["@definitelytyped/typescript-versions@0.0.182", "", {}, "sha512-ebGzGyZJW3ZSuE/nfAokKBo40HKnq/XvBbBnmCTR/3FCDX4aT7/6pQYEu2ihVI/2tf4+76GMoq0jRE69QWJ93g=="], + + "@definitelytyped/utils": ["@definitelytyped/utils@0.0.188", "", { "dependencies": { "@definitelytyped/typescript-versions": "0.0.182", "@qiwi/npm-registry-client": "^8.9.1", "@types/node": "^16.18.61", "charm": "^1.0.2", "minimatch": "^9.0.3", "tar": "^6.2.0", "tar-stream": "^3.1.6", "which": "^4.0.0" } }, "sha512-NPUP1FvRbpac09qETtr1dw3Ri7Q07hp9WGOBjqhzXeXOSxfKs7c3BY6I+XJ2yxexG05LKrCKwgKRKgZlj+Zjzw=="], + + "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.39.4", "", { "dependencies": { "comment-parser": "1.3.1", "esquery": "^1.5.0", "jsdoc-type-pratt-parser": "~4.0.0" } }, "sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.0", "", { "dependencies": { "eslint-visitor-keys": "^3.3.0" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.11.0", "", {}, "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A=="], + + "@eslint/config-array": ["@eslint/config-array@0.17.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.1.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ=="], + + "@eslint/js": ["@eslint/js@9.7.0", "", {}, "sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.4", "", {}, "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.11.14", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.0", "", {}, "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew=="], + + "@mdn/browser-compat-data": ["@mdn/browser-compat-data@5.5.49", "", {}, "sha512-FNYbYIA8WEff/+A8iMGstZhArpgy5ZxZ9uQRsBQ+qXsiKTYn3WjxpCmJRw3CFUOqFlQSZDkC3v1y3BijRnE1Pg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@qiwi/npm-registry-client": ["@qiwi/npm-registry-client@8.9.1", "", { "dependencies": { "concat-stream": "^2.0.0", "graceful-fs": "^4.2.4", "normalize-package-data": "~1.0.1 || ^2.0.0 || ^3.0.0", "npm-package-arg": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^8.0.0", "once": "^1.4.0", "request": "^2.88.2", "retry": "^0.12.0", "safe-buffer": "^5.2.1", "semver": "2 >=2.2.1 || 3.x || 4 || 5 || 7", "slide": "^1.1.6", "ssri": "^8.0.0" }, "optionalDependencies": { "npmlog": "2 || ^3.1.0 || ^4.0.0" } }, "sha512-rZF+mG+NfijR0SHphhTLHRr4aM4gtfdwoAMY6we2VGQam8vkN1cxGG1Lg/Llrj8Dd0Mu6VjdFQRyMMRZxtZR2A=="], + + "@types/bun": ["@types/bun@1.1.6", "", { "dependencies": { "bun-types": "1.1.17" } }, "sha512-uJgKjTdX0GkWEHZzQzFsJkWp5+43ZS7HC8sZPFnOwnSo1AsNl2q9o2bFeS23disNDqbggEgyFkKCHl/w8iZsMA=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], + + "@types/prop-types": ["@types/prop-types@15.7.12", "", {}, "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="], + + "@types/react": ["@types/react@18.3.3", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw=="], + + "@types/semver": ["@types/semver@7.5.8", "", {}, "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="], + + "@types/ws": ["@types/ws@8.5.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@7.16.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.16.1", "@typescript-eslint/type-utils": "7.16.1", "@typescript-eslint/utils": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@7.16.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.16.1", "@typescript-eslint/types": "7.16.1", "@typescript-eslint/typescript-estree": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.16.1", "", { "dependencies": { "@typescript-eslint/types": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1" } }, "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@7.16.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "7.16.1", "@typescript-eslint/utils": "7.16.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@7.16.1", "", {}, "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@7.16.1", "", { "dependencies": { "@typescript-eslint/types": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" } }, "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@7.16.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "7.16.1", "@typescript-eslint/types": "7.16.1", "@typescript-eslint/typescript-estree": "7.16.1" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@7.16.1", "", { "dependencies": { "@typescript-eslint/types": "7.16.1", "eslint-visitor-keys": "^3.4.3" } }, "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="], + + "@vscode/debugadapter": ["@vscode/debugadapter@1.66.0", "", { "dependencies": { "@vscode/debugprotocol": "1.66.0" } }, "sha512-U/m5l6igHtQ8rSMSKW9oWeco9ySPqGYjqW9NECGPGWZ/xnoYicpqUoXhGx3xUNsafrinzWvUWrSUL/Cdgj2V+w=="], + + "@vscode/debugprotocol": ["@vscode/debugprotocol@1.66.0", "", {}, "sha512-VGcRBLNL8QwHzwerSWOb60fy1FO7bdseZv6OkTS4opoP3xeyDX58i4/wAwakL2Y4P9NafN4VGrvlXSWIratmWA=="], + + "acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "aproba": ["aproba@1.2.0", "", {}, "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="], + + "are-docs-informative": ["are-docs-informative@0.0.2", "", {}, "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig=="], + + "are-we-there-yet": ["are-we-there-yet@1.1.7", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" } }, "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.1", "", { "dependencies": { "call-bind": "^1.0.5", "is-array-buffer": "^3.0.4" } }, "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg=="], + + "array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.2", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" } }, "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.2", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" } }, "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.5", "define-properties": "^1.2.1", "es-abstract": "^1.22.3", "es-errors": "^1.2.1", "get-intrinsic": "^1.2.3", "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" } }, "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="], + + "aws4": ["aws4@1.13.0", "", {}, "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g=="], + + "b4a": ["b4a@1.6.6", "", {}, "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg=="], + + "babel-code-frame": ["babel-code-frame@6.26.0", "", { "dependencies": { "chalk": "^1.1.3", "esutils": "^2.0.2", "js-tokens": "^3.0.2" } }, "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "bare-events": ["bare-events@2.4.2", "", {}, "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.23.3", "", { "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "builtin-modules": ["builtin-modules@1.1.1", "", {}, "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ=="], + + "builtins": ["builtins@1.0.3", "", {}, "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ=="], + + "bun-types": ["bun-types@workspace:packages/bun-types", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" }, "devDependencies": { "@biomejs/biome": "^1.5.3", "@definitelytyped/dtslint": "^0.0.199", "@definitelytyped/eslint-plugin": "^0.0.197", "typescript": "^5.0.2" } }], + + "call-bind": ["call-bind@1.0.7", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.1" } }, "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001653", "", {}, "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw=="], + + "capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="], + + "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "change-case": ["change-case@4.1.2", "", { "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", "constant-case": "^3.0.4", "dot-case": "^3.0.4", "header-case": "^2.0.4", "no-case": "^3.0.4", "param-case": "^3.0.4", "pascal-case": "^3.1.2", "path-case": "^3.0.4", "sentence-case": "^3.0.4", "snake-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A=="], + + "charm": ["charm@1.0.2", "", { "dependencies": { "inherits": "^2.0.1" } }, "sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw=="], + + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "code-point-at": ["code-point-at@1.1.0", "", {}, "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "comment-parser": ["comment-parser@1.3.1", "", {}, "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="], + + "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], + + "constant-case": ["constant-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case": "^2.0.2" } }, "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="], + + "data-view-buffer": ["data-view-buffer@1.0.1", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.0", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA=="], + + "debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="], + + "diff": ["diff@3.5.0", "", {}, "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.13", "", {}, "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "es-abstract": ["es-abstract@1.23.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "arraybuffer.prototype.slice": "^1.0.3", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", "data-view-buffer": "^1.0.1", "data-view-byte-length": "^1.0.1", "data-view-byte-offset": "^1.0.0", "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", "get-intrinsic": "^1.2.4", "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.3", "has-symbols": "^1.0.3", "hasown": "^2.0.2", "internal-slot": "^1.0.7", "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", "is-data-view": "^1.0.1", "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.5", "regexp.prototype.flags": "^1.5.2", "safe-array-concat": "^1.1.2", "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.9", "string.prototype.trimend": "^1.0.8", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.2", "typed-array-byte-length": "^1.0.1", "typed-array-byte-offset": "^1.0.2", "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.15" } }, "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A=="], + + "es-define-property": ["es-define-property@1.0.0", "", { "dependencies": { "get-intrinsic": "^1.2.4" } }, "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.0.3", "", { "dependencies": { "get-intrinsic": "^1.2.4", "has-tostringtag": "^1.0.2", "hasown": "^2.0.1" } }, "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="], + + "es-to-primitive": ["es-to-primitive@1.2.1", "", { "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", "is-symbol": "^1.0.2" } }, "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "escalade": ["escalade@3.1.2", "", {}, "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.7.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.17.0", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "9.7.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.2", "eslint-visitor-keys": "^4.0.0", "espree": "^10.1.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw=="], + + "eslint-config-prettier": ["eslint-config-prettier@9.1.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-module-utils": ["eslint-module-utils@2.8.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.29.1", "", { "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.8.0", "hasown": "^2.0.0", "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.7", "object.groupby": "^1.0.1", "object.values": "^1.1.7", "semver": "^6.3.1", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw=="], + + "eslint-plugin-jsdoc": ["eslint-plugin-jsdoc@44.2.7", "", { "dependencies": { "@es-joy/jsdoccomment": "~0.39.4", "are-docs-informative": "^0.0.2", "comment-parser": "1.3.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", "semver": "^7.5.1", "spdx-expression-parse": "^3.0.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-PcAJO7Wh4xIHPT+StBRpEbWgwCpIrYk75zL31RMbduVVHpgiy3Y8aXQ6pdbRJOq0fxHuepWSEAve8ZrPWTSKRg=="], + + "eslint-scope": ["eslint-scope@8.0.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.0.0", "", {}, "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw=="], + + "espree": ["espree@10.1.0", "", { "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" } }, "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + + "fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="], + + "for-each": ["for-each@0.3.3", "", { "dependencies": { "is-callable": "^1.1.3" } }, "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw=="], + + "forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="], + + "form-data": ["form-data@2.3.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ=="], + + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.6", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "functions-have-names": "^1.2.3" } }, "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "gauge": ["gauge@2.7.4", "", { "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.0", "object-assign": "^4.1.0", "signal-exit": "^3.0.0", "string-width": "^1.0.1", "strip-ansi": "^3.0.1", "wide-align": "^1.1.0" } }, "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="], + + "get-symbol-description": ["get-symbol-description@1.0.2", "", { "dependencies": { "call-bind": "^1.0.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4" } }, "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg=="], + + "getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "har-schema": ["har-schema@2.0.0", "", {}, "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="], + + "har-validator": ["har-validator@5.1.5", "", { "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w=="], + + "has-ansi": ["has-ansi@2.0.0", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg=="], + + "has-bigints": ["has-bigints@1.0.2", "", {}, "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.0.3", "", {}, "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="], + + "has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "header-case": ["header-case@2.0.4", "", { "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" } }, "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q=="], + + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="], + + "ignore": ["ignore@5.3.1", "", {}, "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw=="], + + "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "internal-slot": ["internal-slot@1.0.7", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" } }, "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g=="], + + "is-array-buffer": ["is-array-buffer@3.0.4", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1" } }, "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw=="], + + "is-bigint": ["is-bigint@1.0.4", "", { "dependencies": { "has-bigints": "^1.0.1" } }, "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg=="], + + "is-boolean-object": ["is-boolean-object@1.1.2", "", { "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.15.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA=="], + + "is-data-view": ["is-data-view@1.0.1", "", { "dependencies": { "is-typed-array": "^1.1.13" } }, "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w=="], + + "is-date-object": ["is-date-object@1.0.5", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.0.7", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-regex": ["is-regex@1.1.4", "", { "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7" } }, "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg=="], + + "is-string": ["is-string@1.0.7", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg=="], + + "is-symbol": ["is-symbol@1.0.4", "", { "dependencies": { "has-symbols": "^1.0.2" } }, "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg=="], + + "is-typed-array": ["is-typed-array@1.1.13", "", { "dependencies": { "which-typed-array": "^1.1.14" } }, "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw=="], + + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], + + "is-weakref": ["is-weakref@1.0.2", "", { "dependencies": { "call-bind": "^1.0.2" } }, "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="], + + "jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@4.0.0", "", {}, "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify": ["json-stable-stringify@1.1.1", "", { "dependencies": { "call-bind": "^1.0.5", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="], + + "jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.7", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mitata": ["mitata@0.1.11", "", {}, "sha512-cs6FiWcnRxn7atVumm8wA8R70XCDmMXgVgb/qWUSjr5dwuIBr7zC+22mbGYPlbyFixlIOjuP//A0e72Q1ZoGDw=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-releases": ["node-releases@2.0.18", "", {}, "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g=="], + + "normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "npm-package-arg": ["npm-package-arg@8.1.5", "", { "dependencies": { "hosted-git-info": "^4.0.1", "semver": "^7.3.4", "validate-npm-package-name": "^3.0.0" } }, "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q=="], + + "npmlog": ["npmlog@4.1.2", "", { "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", "gauge": "~2.7.3", "set-blocking": "~2.0.0" } }, "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg=="], + + "number-is-nan": ["number-is-nan@1.0.1", "", {}, "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ=="], + + "oauth-sign": ["oauth-sign@0.9.0", "", {}, "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.5", "", { "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.0", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + + "path-case": ["path-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "peechy": ["peechy@0.4.34", "", { "dependencies": { "change-case": "^4.1.2" }, "bin": { "peechy": "cli.js" } }, "sha512-Cpke/cCqqZHhkyxz7mdqS8ZAGJFUi5icu3ZGqxm9GC7g2VrhH0tmjPhZoWHAN5ghw1m1wq5+2YvfbDSqgC4+Zg=="], + + "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], + + "picocolors": ["picocolors@1.0.1", "", {}, "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], + + "postcss": ["postcss@8.4.41", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", "source-map-js": "^1.2.0" } }, "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.3.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew=="], + + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.1.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0" }, "optionalPeers": ["vue-tsc"] }, "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "psl": ["psl@1.9.0", "", {}, "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "qs": ["qs@6.5.3", "", {}, "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "queue-tick": ["queue-tick@1.0.1", "", {}, "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.2", "", { "dependencies": { "call-bind": "^1.0.6", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "set-function-name": "^2.0.1" } }, "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw=="], + + "request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.2", "", { "dependencies": { "call-bind": "^1.0.7", "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" } }, "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-regex-test": ["safe-regex-test@1.0.3", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-regex": "^1.1.4" } }, "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], + + "sentence-case": ["sentence-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" } }, "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slide": ["slide@1.1.6", "", {}, "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "source-map-js": ["source-map-js@1.2.0", "", {}, "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg=="], + + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.18", "", {}, "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], + + "ssri": ["ssri@8.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ=="], + + "streamx": ["streamx@2.18.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "queue-tick": "^1.0.1", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.9", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.0", "es-object-atoms": "^1.0.0" } }, "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + + "text-decoder": ["text-decoder@1.1.1", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="], + + "ts-api-utils": ["ts-api-utils@1.3.0", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], + + "tslint": ["tslint@5.14.0", "", { "dependencies": { "babel-code-frame": "^6.22.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", "commander": "^2.12.1", "diff": "^3.2.0", "glob": "^7.1.1", "js-yaml": "^3.7.0", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.29.0" }, "peerDependencies": { "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev" }, "bin": { "tslint": "./bin/tslint" } }, "sha512-IUla/ieHVnB8Le7LdQFRGlVJid2T/gaJe5VkjzRVSRR6pA2ODYrnfR1hmxi+5+au9l50jBwpbBL34txgv4NnTQ=="], + + "tsutils": ["tsutils@2.29.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" } }, "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.2", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "is-typed-array": "^1.1.13" } }, "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-proto": "^1.0.3", "is-typed-array": "^1.1.13" } }, "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.2", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-proto": "^1.0.3", "is-typed-array": "^1.1.13" } }, "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA=="], + + "typed-array-length": ["typed-array-length@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0" } }, "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g=="], + + "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], + + "typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], + + "unbox-primitive": ["unbox-primitive@1.0.2", "", { "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" } }, "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw=="], + + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.0", "", { "dependencies": { "escalade": "^3.1.2", "picocolors": "^1.0.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ=="], + + "upper-case": ["upper-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg=="], + + "upper-case-first": ["upper-case-first@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="], + + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "validate-npm-package-name": ["validate-npm-package-name@3.0.0", "", { "dependencies": { "builtins": "^1.0.3" } }, "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw=="], + + "verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.0.2", "", { "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" } }, "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg=="], + + "which-typed-array": ["which-typed-array@1.1.15", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.2" } }, "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA=="], + + "wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@definitelytyped/dts-critic/typescript": ["typescript@5.5.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ=="], + + "@definitelytyped/dtslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@6.21.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/type-utils": "6.21.0", "@typescript-eslint/utils": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA=="], + + "@definitelytyped/dtslint/@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="], + + "@definitelytyped/dtslint/@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "", {}, "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg=="], + + "@definitelytyped/dtslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ=="], + + "@definitelytyped/dtslint/@typescript-eslint/utils": ["@typescript-eslint/utils@6.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ=="], + + "@definitelytyped/dtslint/eslint": ["eslint@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.0", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "", {}, "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@6.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ=="], + + "@definitelytyped/utils/@types/node": ["@types/node@16.18.102", "", {}, "sha512-eSe2YwGCcRjqPidxfm20IAq02krERWcIIJW4FNPkU0zQLbc4L9pvhsmB0p6UJecjEf0j/E2ERHsKq7madvthKw=="], + + "@definitelytyped/utils/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@definitelytyped/utils/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "are-we-there-yet/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "babel-code-frame/chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="], + + "babel-code-frame/js-tokens": ["js-tokens@3.0.2", "", {}, "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg=="], + + "bun-types/typescript": ["typescript@5.5.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "gauge/string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="], + + "gauge/strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="], + + "has-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "tslint/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "tslint/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "tslint/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "tslint/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + + "@definitelytyped/dtslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="], + + "@definitelytyped/dtslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@6.21.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag=="], + + "@definitelytyped/dtslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="], + + "@definitelytyped/dtslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="], + + "@definitelytyped/dtslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="], + + "@definitelytyped/dtslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="], + + "@definitelytyped/dtslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], + + "@definitelytyped/dtslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="], + + "@definitelytyped/dtslint/eslint/@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@definitelytyped/dtslint/eslint/@eslint/js": ["@eslint/js@8.57.0", "", {}, "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g=="], + + "@definitelytyped/dtslint/eslint/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "@definitelytyped/dtslint/eslint/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@definitelytyped/dtslint/eslint/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "@definitelytyped/dtslint/eslint/file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "@definitelytyped/dtslint/eslint/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ=="], + + "@definitelytyped/utils/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@definitelytyped/utils/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "are-we-there-yet/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "are-we-there-yet/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "are-we-there-yet/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "babel-code-frame/chalk/ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="], + + "babel-code-frame/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "babel-code-frame/chalk/strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="], + + "babel-code-frame/chalk/supports-color": ["supports-color@2.0.0", "", {}, "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g=="], + + "gauge/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="], + + "gauge/strip-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "tslint/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "tslint/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "tslint/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "tslint/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "@definitelytyped/dtslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@definitelytyped/dtslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@definitelytyped/dtslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@definitelytyped/dtslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@definitelytyped/dtslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="], + + "@definitelytyped/dtslint/eslint/file-entry-cache/flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], + + "babel-code-frame/chalk/strip-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "tslint/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "tslint/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "@definitelytyped/dtslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@definitelytyped/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "tslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 4ccfae2715..0000000000 Binary files a/bun.lockb and /dev/null differ diff --git a/bunfig.node-test.toml b/bunfig.node-test.toml new file mode 100644 index 0000000000..284945e352 --- /dev/null +++ b/bunfig.node-test.toml @@ -0,0 +1,4 @@ +# FIXME: move this back to test/js/node +# https://github.com/oven-sh/bun/issues/16289 +[test] +preload = ["./test/js/node/harness.ts", "./test/preload.ts"] diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake index 31d738134a..847b365dda 100644 --- a/cmake/CompilerFlags.cmake +++ b/cmake/CompilerFlags.cmake @@ -176,6 +176,10 @@ if(LINUX) DESCRIPTION "Disable relocation read-only (RELRO)" -Wl,-z,norelro ) + register_compiler_flags( + DESCRIPTION "Disable semantic interposition" + -fno-semantic-interposition + ) endif() # --- Assertions --- diff --git a/cmake/Globals.cmake b/cmake/Globals.cmake index 3066bb2033..af66b00f08 100644 --- a/cmake/Globals.cmake +++ b/cmake/Globals.cmake @@ -291,7 +291,7 @@ function(find_command) set_property(GLOBAL PROPERTY ${FIND_NAME} "${exe}: ${reason}" APPEND) if(version) - satisfies_range(${version} ${${FIND_VERSION_VARIABLE}} ${variable}) + satisfies_range(${version} ${FIND_VERSION} ${variable}) set(${variable} ${${variable}} PARENT_SCOPE) endif() endfunction() diff --git a/cmake/Options.cmake b/cmake/Options.cmake index d6cc8582ea..fe3219c268 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -20,7 +20,7 @@ else() setx(RELEASE OFF) endif() -if(CMAKE_BUILD_TYPE MATCHES "Debug|RelWithDebInfo") +if(CMAKE_BUILD_TYPE MATCHES "Debug") setx(DEBUG ON) else() setx(DEBUG OFF) @@ -67,13 +67,7 @@ optionx(ENABLE_ASSERTIONS BOOL "If debug assertions should be enabled" DEFAULT $ optionx(ENABLE_CANARY BOOL "If canary features should be enabled" DEFAULT ON) -if(ENABLE_CANARY AND BUILDKITE) - execute_process( - COMMAND buildkite-agent meta-data get "canary" - OUTPUT_VARIABLE DEFAULT_CANARY_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) -elseif(ENABLE_CANARY) +if(ENABLE_CANARY) set(DEFAULT_CANARY_REVISION "1") else() set(DEFAULT_CANARY_REVISION "0") diff --git a/cmake/targets/BuildBoringSSL.cmake b/cmake/targets/BuildBoringSSL.cmake index 28575eb35f..8b709b3de2 100644 --- a/cmake/targets/BuildBoringSSL.cmake +++ b/cmake/targets/BuildBoringSSL.cmake @@ -4,7 +4,7 @@ register_repository( REPOSITORY oven-sh/boringssl COMMIT - 29a2cd359458c9384694b75456026e4b57e3e567 + 914b005ef3ece44159dca0ffad74eb42a9f6679f ) register_cmake_command( diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 20cbb8293e..bcbb8d8c5b 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -318,13 +318,13 @@ register_command( TARGET bun-bake-codegen COMMENT - "Bundling Kit Runtime" + "Bundling Bake Runtime" COMMAND ${BUN_EXECUTABLE} run ${BUN_BAKE_RUNTIME_CODEGEN_SCRIPT} --debug=${DEBUG} - --codegen_root=${CODEGEN_PATH} + --codegen-root=${CODEGEN_PATH} SOURCES ${BUN_BAKE_RUNTIME_SOURCES} ${BUN_BAKE_RUNTIME_CODEGEN_SOURCES} @@ -334,6 +334,39 @@ register_command( ${BUN_BAKE_RUNTIME_OUTPUTS} ) +set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts) + +file(GLOB_RECURSE BUN_BINDGEN_SOURCES ${CONFIGURE_DEPENDS} + ${CWD}/src/**/*.bind.ts +) + +set(BUN_BINDGEN_CPP_OUTPUTS + ${CODEGEN_PATH}/GeneratedBindings.cpp +) + +set(BUN_BINDGEN_ZIG_OUTPUTS + ${CWD}/src/bun.js/bindings/GeneratedBindings.zig +) + +register_command( + TARGET + bun-binding-generator + COMMENT + "Processing \".bind.ts\" files" + COMMAND + ${BUN_EXECUTABLE} + run + ${BUN_BINDGEN_SCRIPT} + --debug=${DEBUG} + --codegen-root=${CODEGEN_PATH} + SOURCES + ${BUN_BINDGEN_SOURCES} + ${BUN_BINDGEN_SCRIPT} + OUTPUTS + ${BUN_BINDGEN_CPP_OUTPUTS} + ${BUN_BINDGEN_ZIG_OUTPUTS} +) + set(BUN_JS_SINK_SCRIPT ${CWD}/src/codegen/generate-jssink.ts) set(BUN_JS_SINK_SOURCES @@ -385,7 +418,6 @@ set(BUN_OBJECT_LUT_OUTPUTS ${CODEGEN_PATH}/NodeModuleModule.lut.h ) - macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps) set(_tmp) get_source_file_property(_tmp ${_source} OBJECT_DEPENDS) @@ -461,6 +493,7 @@ list(APPEND BUN_ZIG_SOURCES ${CWD}/build.zig ${CWD}/root.zig ${CWD}/root_wasm.zig + ${BUN_BINDGEN_ZIG_OUTPUTS} ) set(BUN_ZIG_GENERATED_SOURCES @@ -482,7 +515,6 @@ endif() set(BUN_ZIG_OUTPUT ${BUILD_PATH}/bun-zig.o) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|ARM|arm64|ARM64|aarch64|AARCH64") if(APPLE) set(ZIG_CPU "apple_m1") @@ -544,6 +576,7 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "build.zig") set(BUN_USOCKETS_SOURCE ${CWD}/packages/bun-usockets) +# hand written cpp source files. Full list of "source" code (including codegen) is in BUN_CPP_SOURCES file(GLOB BUN_CXX_SOURCES ${CONFIGURE_DEPENDS} ${CWD}/src/io/*.cpp ${CWD}/src/bun.js/modules/*.cpp @@ -567,7 +600,8 @@ file(GLOB BUN_C_SOURCES ${CONFIGURE_DEPENDS} ) if(WIN32) - list(APPEND BUN_C_SOURCES ${CWD}/src/bun.js/bindings/windows/musl-memmem.c) + list(APPEND BUN_CXX_SOURCES ${CWD}/src/bun.js/bindings/windows/rescle.cpp) + list(APPEND BUN_CXX_SOURCES ${CWD}/src/bun.js/bindings/windows/rescle-binding.cpp) endif() register_repository( @@ -600,12 +634,14 @@ register_command( list(APPEND BUN_CPP_SOURCES ${BUN_C_SOURCES} ${BUN_CXX_SOURCES} + ${BUN_ERROR_CODE_OUTPUTS} ${VENDOR_PATH}/picohttpparser/picohttpparser.c ${NODEJS_HEADERS_PATH}/include/node/node_version.h ${BUN_ZIG_GENERATED_CLASSES_OUTPUTS} ${BUN_JS_SINK_OUTPUTS} ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_OBJECT_LUT_OUTPUTS} + ${BUN_BINDGEN_CPP_OUTPUTS} ) if(WIN32) @@ -615,11 +651,19 @@ if(WIN32) set(Bun_VERSION_WITH_TAG ${VERSION}) endif() set(BUN_ICO_PATH ${CWD}/src/bun.ico) + configure_file(${CWD}/src/bun.ico ${CODEGEN_PATH}/bun.ico COPYONLY) configure_file( ${CWD}/src/windows-app-info.rc ${CODEGEN_PATH}/windows-app-info.rc + @ONLY ) - list(APPEND BUN_CPP_SOURCES ${CODEGEN_PATH}/windows-app-info.rc) + add_custom_command( + OUTPUT ${CODEGEN_PATH}/windows-app-info.res + COMMAND rc.exe /fo ${CODEGEN_PATH}/windows-app-info.res ${CODEGEN_PATH}/windows-app-info.rc + DEPENDS ${CODEGEN_PATH}/windows-app-info.rc ${CODEGEN_PATH}/bun.ico + COMMENT "Adding Windows resource file ${CODEGEN_PATH}/windows-app-info.res with ico in ${CODEGEN_PATH}/bun.ico" + ) + set(WINDOWS_RESOURCES ${CODEGEN_PATH}/windows-app-info.res) endif() # --- Executable --- @@ -627,7 +671,7 @@ endif() set(BUN_CPP_OUTPUT ${BUILD_PATH}/${CMAKE_STATIC_LIBRARY_PREFIX}${bun}${CMAKE_STATIC_LIBRARY_SUFFIX}) if(BUN_LINK_ONLY) - add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT}) + add_executable(${bun} ${BUN_CPP_OUTPUT} ${BUN_ZIG_OUTPUT} ${WINDOWS_RESOURCES}) set_target_properties(${bun} PROPERTIES LINKER_LANGUAGE CXX) target_link_libraries(${bun} PRIVATE ${BUN_CPP_OUTPUT}) elseif(BUN_CPP_ONLY) @@ -645,7 +689,7 @@ elseif(BUN_CPP_ONLY) ${BUN_CPP_OUTPUT} ) else() - add_executable(${bun} ${BUN_CPP_SOURCES}) + add_executable(${bun} ${BUN_CPP_SOURCES} ${WINDOWS_RESOURCES}) target_link_libraries(${bun} PRIVATE ${BUN_ZIG_OUTPUT}) endif() @@ -815,7 +859,7 @@ endif() if(WIN32) target_link_options(${bun} PUBLIC - /STACK:0x1200000,0x100000 + /STACK:0x1200000,0x200000 /errorlimit:0 ) if(RELEASE) @@ -851,48 +895,28 @@ endif() if(LINUX) if(NOT ABI STREQUAL "musl") - if(ARCH STREQUAL "aarch64") - target_link_options(${bun} PUBLIC - -Wl,--wrap=fcntl64 - -Wl,--wrap=statx - ) - endif() - - if(ARCH STREQUAL "x64") - target_link_options(${bun} PUBLIC - -Wl,--wrap=fcntl - -Wl,--wrap=fcntl64 - -Wl,--wrap=fstat - -Wl,--wrap=fstat64 - -Wl,--wrap=fstatat - -Wl,--wrap=fstatat64 - -Wl,--wrap=lstat - -Wl,--wrap=lstat64 - -Wl,--wrap=mknod - -Wl,--wrap=mknodat - -Wl,--wrap=stat - -Wl,--wrap=stat64 - -Wl,--wrap=statx - ) - endif() - + # on arm64 + if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|ARM|arm64|ARM64|aarch64|AARCH64") target_link_options(${bun} PUBLIC - -Wl,--wrap=cosf -Wl,--wrap=exp -Wl,--wrap=expf - -Wl,--wrap=fmod - -Wl,--wrap=fmodf + -Wl,--wrap=fcntl64 -Wl,--wrap=log - -Wl,--wrap=log10f -Wl,--wrap=log2 -Wl,--wrap=log2f -Wl,--wrap=logf -Wl,--wrap=pow -Wl,--wrap=powf - -Wl,--wrap=sincosf - -Wl,--wrap=sinf - -Wl,--wrap=tanf ) + else() + target_link_options(${bun} PUBLIC + -Wl,--wrap=exp + -Wl,--wrap=expf + -Wl,--wrap=log2f + -Wl,--wrap=logf + -Wl,--wrap=powf + ) + endif() endif() if(NOT ABI STREQUAL "musl") @@ -921,7 +945,7 @@ if(LINUX) -Wl,-z,combreloc -Wl,--no-eh-frame-hdr -Wl,--sort-section=name - -Wl,--hash-style=gnu + -Wl,--hash-style=both -Wl,--build-id=sha1 # Better for debugging than default -Wl,-Map=${bun}.linker-map ) @@ -933,6 +957,7 @@ if(WIN32) set(BUN_SYMBOLS_PATH ${CWD}/src/symbols.def) target_link_options(${bun} PUBLIC /DEF:${BUN_SYMBOLS_PATH}) elseif(APPLE) + set(BUN_SYMBOLS_PATH ${CWD}/src/symbols.txt) target_link_options(${bun} PUBLIC -exported_symbols_list ${BUN_SYMBOLS_PATH}) else() diff --git a/cmake/targets/BuildCares.cmake b/cmake/targets/BuildCares.cmake index 373369b543..761f6d1998 100644 --- a/cmake/targets/BuildCares.cmake +++ b/cmake/targets/BuildCares.cmake @@ -4,7 +4,7 @@ register_repository( REPOSITORY c-ares/c-ares COMMIT - 41ee334af3e3d0027dca5e477855d0244936bd49 + 4f4912bce7374f787b10576851b687935f018e17 ) register_cmake_command( diff --git a/cmake/targets/BuildLibArchive.cmake b/cmake/targets/BuildLibArchive.cmake index e0cffd020b..da8bfcb7cd 100644 --- a/cmake/targets/BuildLibArchive.cmake +++ b/cmake/targets/BuildLibArchive.cmake @@ -18,7 +18,7 @@ register_cmake_command( -DENABLE_INSTALL=OFF -DENABLE_TEST=OFF -DENABLE_WERROR=OFF - -DENABLE_BZIP2=OFF + -DENABLE_BZip2=OFF -DENABLE_CAT=OFF -DENABLE_EXPAT=OFF -DENABLE_ICONV=OFF diff --git a/cmake/targets/BuildLibDeflate.cmake b/cmake/targets/BuildLibDeflate.cmake index f629d52fe5..f2820e3e79 100644 --- a/cmake/targets/BuildLibDeflate.cmake +++ b/cmake/targets/BuildLibDeflate.cmake @@ -4,7 +4,7 @@ register_repository( REPOSITORY ebiggers/libdeflate COMMIT - 9d624d1d8ba82c690d6d6be1d0a961fc5a983ea4 + 733848901289eca058804ca0737f8796875204c8 ) register_cmake_command( diff --git a/cmake/targets/BuildLolHtml.cmake b/cmake/targets/BuildLolHtml.cmake index 934f8d0be9..3b0d80a723 100644 --- a/cmake/targets/BuildLolHtml.cmake +++ b/cmake/targets/BuildLolHtml.cmake @@ -49,6 +49,8 @@ register_command( CARGO_TERM_VERBOSE=true CARGO_TERM_DIAGNOSTIC=true CARGO_ENCODED_RUSTFLAGS=${RUSTFLAGS} + CARGO_HOME=${CARGO_HOME} + RUSTUP_HOME=${RUSTUP_HOME} ) target_link_libraries(${bun} PRIVATE ${LOLHTML_LIBRARY}) diff --git a/cmake/tools/SetupCcache.cmake b/cmake/tools/SetupCcache.cmake index d2367205c8..a128fac98b 100644 --- a/cmake/tools/SetupCcache.cmake +++ b/cmake/tools/SetupCcache.cmake @@ -5,6 +5,11 @@ if(NOT ENABLE_CCACHE OR CACHE_STRATEGY STREQUAL "none") return() endif() +if (CI AND NOT APPLE) + setenv(CCACHE_DISABLE 1) + return() +endif() + find_command( VARIABLE CCACHE_PROGRAM @@ -38,7 +43,8 @@ setenv(CCACHE_FILECLONE 1) setenv(CCACHE_STATSLOG ${BUILD_PATH}/ccache.log) if(CI) - setenv(CCACHE_SLOPPINESS "pch_defines,time_macros,locale,clang_index_store,gcno_cwd,include_file_ctime,include_file_mtime") + # FIXME: Does not work on Ubuntu 18.04 + # setenv(CCACHE_SLOPPINESS "pch_defines,time_macros,locale,clang_index_store,gcno_cwd,include_file_ctime,include_file_mtime") else() setenv(CCACHE_SLOPPINESS "pch_defines,time_macros,locale,random_seed,clang_index_store,gcno_cwd") endif() diff --git a/cmake/tools/SetupLLVM.cmake b/cmake/tools/SetupLLVM.cmake index 9db637b60d..92423dc607 100644 --- a/cmake/tools/SetupLLVM.cmake +++ b/cmake/tools/SetupLLVM.cmake @@ -1,14 +1,18 @@ -optionx(ENABLE_LLVM BOOL "If LLVM should be used for compilation" DEFAULT ON) + +set(DEFAULT_ENABLE_LLVM ON) + +# if target is bun-zig, set ENABLE_LLVM to OFF +if(TARGET bun-zig) + set(DEFAULT_ENABLE_LLVM OFF) +endif() + +optionx(ENABLE_LLVM BOOL "If LLVM should be used for compilation" DEFAULT ${DEFAULT_ENABLE_LLVM}) if(NOT ENABLE_LLVM) return() endif() -if(CMAKE_HOST_WIN32 OR CMAKE_HOST_APPLE OR EXISTS "/etc/alpine-release") - set(DEFAULT_LLVM_VERSION "18.1.8") -else() - set(DEFAULT_LLVM_VERSION "16.0.6") -endif() +set(DEFAULT_LLVM_VERSION "18.1.8") optionx(LLVM_VERSION STRING "The version of LLVM to use" DEFAULT ${DEFAULT_LLVM_VERSION}) @@ -37,11 +41,11 @@ if(APPLE) endif() endif() - list(APPEND LLVM_PATHS ${HOMEBREW_PREFIX}/opt/llvm/bin) - if(USE_LLVM_VERSION) list(APPEND LLVM_PATHS ${HOMEBREW_PREFIX}/opt/llvm@${LLVM_VERSION_MAJOR}/bin) endif() + + list(APPEND LLVM_PATHS ${HOMEBREW_PREFIX}/opt/llvm/bin) endif() if(UNIX) @@ -73,7 +77,7 @@ macro(find_llvm_command variable command) VERSION_VARIABLE LLVM_VERSION COMMAND ${commands} PATHS ${LLVM_PATHS} - VERSION ${LLVM_VERSION} + VERSION >=${LLVM_VERSION_MAJOR}.1.0 ) list(APPEND CMAKE_ARGS -D${variable}=${${variable}}) endmacro() diff --git a/cmake/tools/SetupRust.cmake b/cmake/tools/SetupRust.cmake index a83b28bc5f..8a45d243eb 100644 --- a/cmake/tools/SetupRust.cmake +++ b/cmake/tools/SetupRust.cmake @@ -1,15 +1,42 @@ +if(DEFINED ENV{CARGO_HOME}) + set(CARGO_HOME $ENV{CARGO_HOME}) +elseif(CMAKE_HOST_WIN32) + set(CARGO_HOME $ENV{USERPROFILE}/.cargo) + if(NOT EXISTS ${CARGO_HOME}) + set(CARGO_HOME $ENV{PROGRAMFILES}/Rust/cargo) + endif() +else() + set(CARGO_HOME $ENV{HOME}/.cargo) +endif() + +if(DEFINED ENV{RUSTUP_HOME}) + set(RUSTUP_HOME $ENV{RUSTUP_HOME}) +elseif(CMAKE_HOST_WIN32) + set(RUSTUP_HOME $ENV{USERPROFILE}/.rustup) + if(NOT EXISTS ${RUSTUP_HOME}) + set(RUSTUP_HOME $ENV{PROGRAMFILES}/Rust/rustup) + endif() +else() + set(RUSTUP_HOME $ENV{HOME}/.rustup) +endif() + find_command( VARIABLE CARGO_EXECUTABLE COMMAND cargo PATHS - $ENV{HOME}/.cargo/bin + ${CARGO_HOME}/bin REQUIRED OFF ) if(EXISTS ${CARGO_EXECUTABLE}) + if(CARGO_EXECUTABLE MATCHES "^${CARGO_HOME}") + setx(CARGO_HOME ${CARGO_HOME}) + setx(RUSTUP_HOME ${RUSTUP_HOME}) + endif() + return() endif() diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index e7cb26be5e..d00d3a3e96 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,13 +2,15 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION 8f9ae4f01a047c666ef548864294e01df731d4ea) + set(WEBKIT_VERSION 9e3b60e4a6438d20ee6f8aa5bec6b71d2b7d213f) endif() +string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX) + if(WEBKIT_LOCAL) set(DEFAULT_WEBKIT_PATH ${VENDOR_PATH}/WebKit/WebKitBuild/${CMAKE_BUILD_TYPE}) else() - set(DEFAULT_WEBKIT_PATH ${CACHE_PATH}/webkit-${WEBKIT_VERSION}) + set(DEFAULT_WEBKIT_PATH ${CACHE_PATH}/webkit-${WEBKIT_VERSION_PREFIX}) endif() option(WEBKIT_PATH "The path to the WebKit directory") @@ -30,6 +32,8 @@ if(WEBKIT_LOCAL) ${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders ${WEBKIT_PATH}/bmalloc/Headers ${WEBKIT_PATH}/WTF/Headers + ${WEBKIT_PATH}/JavaScriptCore/DerivedSources/inspector + ${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders/JavaScriptCore ) endif() diff --git a/completions/bun.bash b/completions/bun.bash index ccabb1d73b..eabdc343fb 100644 --- a/completions/bun.bash +++ b/completions/bun.bash @@ -87,7 +87,7 @@ _bun_completions() { GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --env-file --extension-order --jsx-factory --jsx-fragment --extension-order --jsx-factory --jsx-fragment --jsx-import-source --jsx-production --jsx-runtime --main-fields --no-summary --version --platform --public-dir --tsconfig-override --define --external --help --inject --loader --origin --port --dump-environment-variables --dump-limits --disable-bun-js"; GLOBAL_OPTIONS[SHORT_OPTIONS]="-c -v -d -e -h -i -l -u -p"; - PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional"; + PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional --peer"; PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]="-d"; PACKAGE_OPTIONS[REMOVE_OPTIONS_LONG]=""; PACKAGE_OPTIONS[REMOVE_OPTIONS_SHORT]=""; diff --git a/completions/bun.zsh b/completions/bun.zsh index d75f2aa2f0..f885ac03ad 100644 --- a/completions/bun.zsh +++ b/completions/bun.zsh @@ -35,6 +35,7 @@ _bun_add_completion() { '-D[]' \ '--development[]' \ '--optional[Add dependency to "optionalDependencies]' \ + '--peer[Add dependency to "peerDependencies]' \ '--exact[Add the exact version instead of the ^range]' && ret=0 @@ -339,6 +340,7 @@ _bun_install_completion() { '--development[]' \ '-D[]' \ '--optional[Add dependency to "optionalDependencies]' \ + '--peer[Add dependency to "peerDependencies]' \ '--exact[Add the exact version instead of the ^range]' && ret=0 @@ -671,7 +673,7 @@ _bun() { cmd) local -a scripts_list IFS=$'\n' scripts_list=($(SHELL=zsh bun getcompletes i)) - scripts="scripts:scripts:(($scripts_list))" + scripts="scripts:scripts:((${scripts_list//:/\\\\:}))" IFS=$'\n' files_list=($(SHELL=zsh bun getcompletes j)) main_commands=( @@ -871,8 +873,8 @@ _bun_run_param_script_completion() { IFS=$'\n' scripts_list=($(SHELL=zsh bun getcompletes s)) IFS=$'\n' bins=($(SHELL=zsh bun getcompletes b)) - _alternative "scripts:scripts:(($scripts_list))" - _alternative "bin:bin:(($bins))" + _alternative "scripts:scripts:((${scripts_list//:/\\\\:}))" + _alternative "bin:bin:((${bins//:/\\\\:}))" _alternative "files:file:_files -g '*.(js|ts|jsx|tsx|wasm)'" } diff --git a/docs/api/binary-data.md b/docs/api/binary-data.md index e0820d44f3..3bd0a79cf6 100644 --- a/docs/api/binary-data.md +++ b/docs/api/binary-data.md @@ -235,7 +235,7 @@ The following classes are typed arrays, along with a description of how they int --- - [`BigInt64Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array) -- Every eight (8) bytes are interpreted as an unsigned `BigInt`. Range -9223372036854775808 to 9223372036854775807 (though `BigInt` is capable of representing larger numbers). +- Every eight (8) bytes are interpreted as a signed `BigInt`. Range -9223372036854775808 to 9223372036854775807 (though `BigInt` is capable of representing larger numbers). --- diff --git a/docs/api/cc.md b/docs/api/cc.md index 212b928df5..0cdf0b0a75 100644 --- a/docs/api/cc.md +++ b/docs/api/cc.md @@ -179,16 +179,16 @@ type Flags = string | string[]; These are flags like `-I` for include directories and `-D` for preprocessor definitions. -#### `defines: Record` +#### `define: Record` -The `defines` is an optional object that should be passed to the TinyCC compiler. +The `define` is an optional object that should be passed to the TinyCC compiler. ```ts type Defines = Record; cc({ source: "hello.c", - defines: { + define: { "NDEBUG": "1", }, }); diff --git a/docs/api/fetch.md b/docs/api/fetch.md index afbf53c5c1..c70aa2e764 100644 --- a/docs/api/fetch.md +++ b/docs/api/fetch.md @@ -195,7 +195,7 @@ This will print the request and response headers to your terminal: ```sh [fetch] > HTTP/1.1 GET http://example.com/ [fetch] > Connection: keep-alive -[fetch] > User-Agent: Bun/1.1.21 +[fetch] > User-Agent: Bun/$BUN_LATEST_VERSION [fetch] > Accept: */* [fetch] > Host: example.com [fetch] > Accept-Encoding: gzip, deflate, br @@ -234,7 +234,7 @@ To prefetch a DNS entry, you can use the `dns.prefetch` API. This API is useful ```ts import { dns } from "bun"; -dns.prefetch("bun.sh", 443); +dns.prefetch("bun.sh"); ``` #### DNS caching diff --git a/docs/api/ffi.md b/docs/api/ffi.md index 6284689cb2..18fe2bb439 100644 --- a/docs/api/ffi.md +++ b/docs/api/ffi.md @@ -297,6 +297,20 @@ setTimeout(() => { When you're done with a JSCallback, you should call `close()` to free the memory. +### Experimental thread-safe callbacks +`JSCallback` has experimental support for thread-safe callbacks. This will be needed if you pass a callback function into a different thread from it's instantiation context. You can enable it with the optional `threadsafe` option flag. +```ts +const searchIterator = new JSCallback( + (ptr, length) => /hello/.test(new CString(ptr, length)), + { + returns: "bool", + args: ["ptr", "usize"], + threadsafe: true, // Optional. Defaults to `false` + }, +); +``` +Be aware that there are still cases where this does not 100% work. + {% callout %} **⚡️ Performance tip** — For a slight performance boost, directly pass `JSCallback.prototype.ptr` instead of the `JSCallback` object: diff --git a/docs/api/file-io.md b/docs/api/file-io.md index f9fd2368f3..1f8e9468bf 100644 --- a/docs/api/file-io.md +++ b/docs/api/file-io.md @@ -62,6 +62,14 @@ Bun.stdout; Bun.stderr; ``` +### Deleting files (`file.delete()`) + +You can delete a file by calling the `.delete()` function. + +```ts +await Bun.file("logs.json").delete() +``` + ## Writing files (`Bun.write()`) `Bun.write(destination, data): Promise` diff --git a/docs/api/hashing.md b/docs/api/hashing.md index 5cc40e2a75..1b1b754a75 100644 --- a/docs/api/hashing.md +++ b/docs/api/hashing.md @@ -169,6 +169,9 @@ Bun.hash.crc32("data", 1234); Bun.hash.adler32("data", 1234); Bun.hash.cityHash32("data", 1234); Bun.hash.cityHash64("data", 1234); +Bun.hash.xxHash32("data", 1234); +Bun.hash.xxHash64("data", 1234); +Bun.hash.xxHash3("data", 1234); Bun.hash.murmur32v3("data", 1234); Bun.hash.murmur32v2("data", 1234); Bun.hash.murmur64v2("data", 1234); diff --git a/docs/api/html-rewriter.md b/docs/api/html-rewriter.md index f67577b135..0f0f8c2802 100644 --- a/docs/api/html-rewriter.md +++ b/docs/api/html-rewriter.md @@ -1,31 +1,334 @@ -Bun provides a fast native implementation of the `HTMLRewriter` pattern developed by Cloudflare. It provides a convenient, `EventListener`-like API for traversing and transforming HTML documents. +HTMLRewriter lets you use CSS selectors to transform HTML documents. It works with `Request`, `Response`, as well as `string`. Bun's implementation is based on Cloudflare's [lol-html](https://github.com/cloudflare/lol-html). + +## Usage + +A common usecase is rewriting URLs in HTML content. Here's an example that rewrites image sources and link URLs to use a CDN domain: ```ts -const rewriter = new HTMLRewriter(); +// Replace all images with a rickroll +const rewriter = new HTMLRewriter().on("img", { + element(img) { + // Famous rickroll video thumbnail + img.setAttribute( + "src", + "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg", + ); -rewriter.on("*", { - element(el) { - console.log(el.tagName); // "body" | "div" | ... + // Wrap the image in a link to the video + img.before( + '', + { html: true }, + ); + img.after("", { html: true }); + + // Add some fun alt text + img.setAttribute("alt", "Definitely not a rickroll"); + }, +}); + +// An example HTML document +const html = ` + + + + + + + +`; + +const result = rewriter.transform(html); +console.log(result); +``` + +This replaces all images with a thumbnail of Rick Astley and wraps each `` in a link, producing a diff like this: + +```html-diff + + +- +- +- ++ ++ Definitely not a rickroll ++ ++ ++ Definitely not a rickroll ++ ++ ++ Definitely not a rickroll ++ + + +``` + +Now every image on the page will be replaced with a thumbnail of Rick Astley, and clicking any image will lead to [a very famous video](https://www.youtube.com/watch?v=dQw4w9WgXcQ). + +### Input types + +HTMLRewriter can transform HTML from various sources. The input is automatically handled based on its type: + +```ts +// From Response +rewriter.transform(new Response("
content
")); + +// From string +rewriter.transform("
content
"); + +// From ArrayBuffer +rewriter.transform(new TextEncoder().encode("
content
").buffer); + +// From Blob +rewriter.transform(new Blob(["
content
"])); + +// From File +rewriter.transform(Bun.file("index.html")); +``` + +Note that Cloudflare Workers implementation of HTMLRewriter only supports `Response` objects. + +### Element Handlers + +The `on(selector, handlers)` method allows you to register handlers for HTML elements that match a CSS selector. The handlers are called for each matching element during parsing: + +```ts +rewriter.on("div.content", { + // Handle elements + element(element) { + element.setAttribute("class", "new-content"); + element.append("

New content

", { html: true }); + }, + // Handle text nodes + text(text) { + text.replace("new text"); + }, + // Handle comments + comments(comment) { + comment.remove(); }, }); ``` -To parse and/or transform the HTML: +The handlers can be asynchronous and return a Promise. Note that async operations will block the transformation until they complete: -```ts#rewriter.ts -rewriter.transform( - new Response(` - - - - - My First HTML Page - - -

My First Heading

-

My first paragraph.

- -`)); +```ts +rewriter.on("div", { + async element(element) { + await Bun.sleep(1000); + element.setInnerContent("replace", { html: true }); + }, +}); ``` -View the full documentation on the [Cloudflare website](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/). +### CSS Selector Support + +The `on()` method supports a wide range of CSS selectors: + +```ts +// Tag selectors +rewriter.on("p", handler); + +// Class selectors +rewriter.on("p.red", handler); + +// ID selectors +rewriter.on("h1#header", handler); + +// Attribute selectors +rewriter.on("p[data-test]", handler); // Has attribute +rewriter.on('p[data-test="one"]', handler); // Exact match +rewriter.on('p[data-test="one" i]', handler); // Case-insensitive +rewriter.on('p[data-test="one" s]', handler); // Case-sensitive +rewriter.on('p[data-test~="two"]', handler); // Word match +rewriter.on('p[data-test^="a"]', handler); // Starts with +rewriter.on('p[data-test$="1"]', handler); // Ends with +rewriter.on('p[data-test*="b"]', handler); // Contains +rewriter.on('p[data-test|="a"]', handler); // Dash-separated + +// Combinators +rewriter.on("div span", handler); // Descendant +rewriter.on("div > span", handler); // Direct child + +// Pseudo-classes +rewriter.on("p:nth-child(2)", handler); +rewriter.on("p:first-child", handler); +rewriter.on("p:nth-of-type(2)", handler); +rewriter.on("p:first-of-type", handler); +rewriter.on("p:not(:first-child)", handler); + +// Universal selector +rewriter.on("*", handler); +``` + +### Element Operations + +Elements provide various methods for manipulation. All modification methods return the element instance for chaining: + +```ts +rewriter.on("div", { + element(el) { + // Attributes + el.setAttribute("class", "new-class").setAttribute("data-id", "123"); + + const classAttr = el.getAttribute("class"); // "new-class" + const hasId = el.hasAttribute("id"); // boolean + el.removeAttribute("class"); + + // Content manipulation + el.setInnerContent("New content"); // Escapes HTML by default + el.setInnerContent("

HTML content

", { html: true }); // Parses HTML + el.setInnerContent(""); // Clear content + + // Position manipulation + el.before("Content before") + .after("Content after") + .prepend("First child") + .append("Last child"); + + // HTML content insertion + el.before("before", { html: true }) + .after("after", { html: true }) + .prepend("first", { html: true }) + .append("last", { html: true }); + + // Removal + el.remove(); // Remove element and contents + el.removeAndKeepContent(); // Remove only the element tags + + // Properties + console.log(el.tagName); // Lowercase tag name + console.log(el.namespaceURI); // Element's namespace URI + console.log(el.selfClosing); // Whether element is self-closing (e.g.
) + console.log(el.canHaveContent); // Whether element can contain content (false for void elements like
) + console.log(el.removed); // Whether element was removed + + // Attributes iteration + for (const [name, value] of el.attributes) { + console.log(name, value); + } + + // End tag handling + el.onEndTag(endTag => { + endTag.before("Before end tag"); + endTag.after("After end tag"); + endTag.remove(); // Remove the end tag + console.log(endTag.name); // Tag name in lowercase + }); + }, +}); +``` + +### Text Operations + +Text handlers provide methods for text manipulation. Text chunks represent portions of text content and provide information about their position in the text node: + +```ts +rewriter.on("p", { + text(text) { + // Content + console.log(text.text); // Text content + console.log(text.lastInTextNode); // Whether this is the last chunk + console.log(text.removed); // Whether text was removed + + // Manipulation + text.before("Before text").after("After text").replace("New text").remove(); + + // HTML content insertion + text + .before("before", { html: true }) + .after("after", { html: true }) + .replace("replace", { html: true }); + }, +}); +``` + +### Comment Operations + +Comment handlers allow comment manipulation with similar methods to text nodes: + +```ts +rewriter.on("*", { + comments(comment) { + // Content + console.log(comment.text); // Comment text + comment.text = "New comment text"; // Set comment text + console.log(comment.removed); // Whether comment was removed + + // Manipulation + comment + .before("Before comment") + .after("After comment") + .replace("New comment") + .remove(); + + // HTML content insertion + comment + .before("before", { html: true }) + .after("after", { html: true }) + .replace("replace", { html: true }); + }, +}); +``` + +### Document Handlers + +The `onDocument(handlers)` method allows you to handle document-level events. These handlers are called for events that occur at the document level rather than within specific elements: + +```ts +rewriter.onDocument({ + // Handle doctype + doctype(doctype) { + console.log(doctype.name); // "html" + console.log(doctype.publicId); // public identifier if present + console.log(doctype.systemId); // system identifier if present + }, + // Handle text nodes + text(text) { + console.log(text.text); + }, + // Handle comments + comments(comment) { + console.log(comment.text); + }, + // Handle document end + end(end) { + end.append("", { html: true }); + }, +}); +``` + +### Response Handling + +When transforming a Response: + +- The status code, headers, and other response properties are preserved +- The body is transformed while maintaining streaming capabilities +- Content-encoding (like gzip) is handled automatically +- The original response body is marked as used after transformation +- Headers are cloned to the new response + +## Error Handling + +HTMLRewriter operations can throw errors in several cases: + +- Invalid selector syntax in `on()` method +- Invalid HTML content in transformation methods +- Stream errors when processing Response bodies +- Memory allocation failures +- Invalid input types (e.g., passing Symbol) +- Body already used errors + +Errors should be caught and handled appropriately: + +```ts +try { + const result = rewriter.transform(input); + // Process result +} catch (error) { + console.error("HTMLRewriter error:", error); +} +``` + +## See also + +You can also read the [Cloudflare documentation](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/), which this API is intended to be compatible with. diff --git a/docs/api/s3.md b/docs/api/s3.md new file mode 100644 index 0000000000..c8f9d88f3c --- /dev/null +++ b/docs/api/s3.md @@ -0,0 +1,686 @@ +Production servers often read, upload, and write files to S3-compatible object storage services instead of the local filesystem. Historically, that means local filesystem APIs you use in development can't be used in production. When you use Bun, things are different. + +{% callout %} + +### Bun's S3 API is fast + +{% image src="https://bun.sh/bun-s3-node.gif" alt="Bun's S3 API is fast" caption="Left: Bun v1.1.44. Right: Node.js v23.6.0" /%} + +{% /callout %} + +Bun provides fast, native bindings for interacting with S3-compatible object storage services. Bun's S3 API is designed to be simple and feel similar to fetch's `Response` and `Blob` APIs (like Bun's local filesystem APIs). + +```ts +import { s3, write, S3Client } from "bun"; + +// Bun.s3 reads environment variables for credentials +// file() returns a lazy reference to a file on S3 +const metadata = s3.file("123.json"); + +// Download from S3 as JSON +const data = await metadata.json(); + +// Upload to S3 +await write(metadata, JSON.stringify({ name: "John", age: 30 })); + +// Presign a URL (synchronous - no network request needed) +const url = metadata.presign({ + acl: "public-read", + expiresIn: 60 * 60 * 24, // 1 day +}); + +// Delete the file +await metadata.delete(); +``` + +S3 is the [de facto standard](https://en.wikipedia.org/wiki/De_facto_standard) internet filesystem. Bun's S3 API works with S3-compatible storage services like: + +- AWS S3 +- Cloudflare R2 +- DigitalOcean Spaces +- MinIO +- Backblaze B2 +- ...and any other S3-compatible storage service + +## Basic Usage + +There are several ways to interact with Bun's S3 API. + +### `Bun.S3Client` & `Bun.s3` + +`Bun.s3` is equivalent to `new Bun.S3Client()`, relying on environment variables for credentials. + +To explicitly set credentials, pass them to the `Bun.S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +const client = new S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // sessionToken: "..." + // acl: "public-read", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 + // endpoint: "https://.digitaloceanspaces.com", // DigitalOcean Spaces + // endpoint: "http://localhost:9000", // MinIO +}); + +// Bun.s3 is a global singleton that is equivalent to `new Bun.S3Client()` +Bun.s3 = client; +``` + +### Working with S3 Files + +The **`file`** method in `S3Client` returns a **lazy reference to a file on S3**. + +```ts +// A lazy reference to a file on S3 +const s3file: S3File = client.file("123.json"); +``` + +Like `Bun.file(path)`, the `S3Client`'s `file` method is synchronous. It does zero network requests until you call a method that depends on a network request. + +### Reading files from S3 + +If you've used the `fetch` API, you're familiar with the `Response` and `Blob` APIs. `S3File` extends `Blob`. The same methods that work on `Blob` also work on `S3File`. + +```ts +// Read an S3File as text +const text = await s3file.text(); + +// Read an S3File as JSON +const json = await s3file.json(); + +// Read an S3File as an ArrayBuffer +const buffer = await s3file.arrayBuffer(); + +// Get only the first 1024 bytes +const partial = await s3file.slice(0, 1024).text(); + +// Stream the file +const stream = s3file.stream(); +for await (const chunk of stream) { + console.log(chunk); +} +``` + +#### Memory optimization + +Methods like `text()`, `json()`, `bytes()`, or `arrayBuffer()` avoid duplicating the string or bytes in memory when possible. + +If the text happens to be ASCII, Bun directly transfers the string to JavaScriptCore (the engine) without transcoding and without duplicating the string in memory. When you use `.bytes()` or `.arrayBuffer()`, it will also avoid duplicating the bytes in memory. + +These helper methods not only simplify the API, they also make it faster. + +### Writing & uploading files to S3 + +Writing to S3 is just as simple. + +```ts +// Write a string (replacing the file) +await s3file.write("Hello World!"); + +// Write a Buffer (replacing the file) +await s3file.write(Buffer.from("Hello World!")); + +// Write a Response (replacing the file) +await s3file.write(new Response("Hello World!")); + +// Write with content type +await s3file.write(JSON.stringify({ name: "John", age: 30 }), { + type: "application/json", +}); + +// Write using a writer (streaming) +const writer = s3file.writer({ type: "application/json" }); +writer.write("Hello"); +writer.write(" World!"); +await writer.end(); + +// Write using Bun.write +await Bun.write(s3file, "Hello World!"); +``` + +### Working with large files (streams) + +Bun automatically handles multipart uploads for large files and provides streaming capabilities. The same API that works for local files also works for S3 files. + +```ts +// Write a large file +const bigFile = Buffer.alloc(10 * 1024 * 1024); // 10MB +const writer = s3file.writer({ + // Automatically retry on network errors up to 3 times + retry: 3, + + // Queue up to 10 requests at a time + queueSize: 10, + + // Upload in 5 MB chunks + partSize: 5 * 1024 * 1024, +}); +for (let i = 0; i < 10; i++) { + await writer.write(bigFile); +} +await writer.end(); +``` + +## Presigning URLs + +When your production service needs to let users upload files to your server, it's often more reliable for the user to upload directly to S3 instead of your server acting as an intermediary. + +To facilitate this, you can presign URLs for S3 files. This generates a URL with a signature that allows a user to securely upload that specific file to S3, without exposing your credentials or granting them unnecessary access to your bucket. + +```ts +import { s3 } from "bun"; + +// Generate a presigned URL that expires in 24 hours (default) +const url = s3.presign("my-file.txt", { + expiresIn: 3600, // 1 hour +}); +``` + +### Setting ACLs + +To set an ACL (access control list) on a presigned URL, pass the `acl` option: + +```ts +const url = s3file.presign({ + acl: "public-read", + expiresIn: 3600, +}); +``` + +You can pass any of the following ACLs: + +| ACL | Explanation | +| ----------------------------- | ------------------------------------------------------------------- | +| `"public-read"` | The object is readable by the public. | +| `"private"` | The object is readable only by the bucket owner. | +| `"public-read-write"` | The object is readable and writable by the public. | +| `"authenticated-read"` | The object is readable by the bucket owner and authenticated users. | +| `"aws-exec-read"` | The object is readable by the AWS account that made the request. | +| `"bucket-owner-read"` | The object is readable by the bucket owner. | +| `"bucket-owner-full-control"` | The object is readable and writable by the bucket owner. | +| `"log-delivery-write"` | The object is writable by AWS services used for log delivery. | + +### Expiring URLs + +To set an expiration time for a presigned URL, pass the `expiresIn` option. + +```ts +const url = s3file.presign({ + // Seconds + expiresIn: 3600, // 1 hour + + // access control list + acl: "public-read", + + // HTTP method + method: "PUT", +}); +``` + +### `method` + +To set the HTTP method for a presigned URL, pass the `method` option. + +```ts +const url = s3file.presign({ + method: "PUT", + // method: "DELETE", + // method: "GET", + // method: "HEAD", + // method: "POST", + // method: "PUT", +}); +``` + +### `new Response(S3File)` + +To quickly redirect users to a presigned URL for an S3 file, pass an `S3File` instance to a `Response` object as the body. + +```ts +const response = new Response(s3file); +console.log(response); +``` + +This will automatically redirect the user to the presigned URL for the S3 file, saving you the memory, time, and bandwidth cost of downloading the file to your server and sending it back to the user. + +```ts +Response (0 KB) { + ok: false, + url: "", + status: 302, + statusText: "", + headers: Headers { + "location": "https://.r2.cloudflarestorage.com/...", + }, + redirected: true, + bodyUsed: false +} +``` + +## Support for S3-Compatible Services + +Bun's S3 implementation works with any S3-compatible storage service. Just specify the appropriate endpoint: + +### Using Bun's S3Client with AWS S3 + +AWS S3 is the default. You can also pass a `region` option instead of an `endpoint` option for AWS S3. + +```ts +import { S3Client } from "bun"; + +// AWS S3 +const s3 = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // region: "us-east-1", +}); +``` + +### Using Bun's S3Client with Google Cloud Storage + +To use Bun's S3 client with [Google Cloud Storage](https://cloud.google.com/storage), set `endpoint` to `"https://storage.googleapis.com"` in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +// Google Cloud Storage +const gcs = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "https://storage.googleapis.com", +}); +``` + +### Using Bun's S3Client with Cloudflare R2 + +To use Bun's S3 client with [Cloudflare R2](https://developers.cloudflare.com/r2/), set `endpoint` to the R2 endpoint in the `S3Client` constructor. The R2 endpoint includes your account ID. + +```ts +import { S3Client } from "bun"; + +// CloudFlare R2 +const r2 = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "https://.r2.cloudflarestorage.com", +}); +``` + +### Using Bun's S3Client with DigitalOcean Spaces + +To use Bun's S3 client with [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/), set `endpoint` to the DigitalOcean Spaces endpoint in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +const spaces = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + // region: "nyc3", + endpoint: "https://.digitaloceanspaces.com", +}); +``` + +### Using Bun's S3Client with MinIO + +To use Bun's S3 client with [MinIO](https://min.io/), set `endpoint` to the URL that MinIO is running on in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +const minio = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + + // Make sure to use the correct endpoint URL + // It might not be localhost in production! + endpoint: "http://localhost:9000", +}); +``` + +## Credentials + +Credentials are one of the hardest parts of using S3, and we've tried to make it as easy as possible. By default, Bun reads the following environment variables for credentials. + +| Option name | Environment variable | +| ----------------- | ---------------------- | +| `accessKeyId` | `S3_ACCESS_KEY_ID` | +| `secretAccessKey` | `S3_SECRET_ACCESS_KEY` | +| `region` | `S3_REGION` | +| `endpoint` | `S3_ENDPOINT` | +| `bucket` | `S3_BUCKET` | +| `sessionToken` | `S3_SESSION_TOKEN` | + +If the `S3_*` environment variable is not set, Bun will also check for the `AWS_*` environment variable, for each of the above options. + +| Option name | Fallback environment variable | +| ----------------- | ----------------------------- | +| `accessKeyId` | `AWS_ACCESS_KEY_ID` | +| `secretAccessKey` | `AWS_SECRET_ACCESS_KEY` | +| `region` | `AWS_REGION` | +| `endpoint` | `AWS_ENDPOINT` | +| `bucket` | `AWS_BUCKET` | +| `sessionToken` | `AWS_SESSION_TOKEN` | + +These environment variables are read from [`.env` files](/docs/runtime/env) or from the process environment at initialization time (`process.env` is not used for this). + +These defaults are overridden by the options you pass to `s3(credentials)`, `new Bun.S3Client(credentials)`, or any of the methods that accept credentials. So if, for example, you use the same credentials for different buckets, you can set the credentials once in your `.env` file and then pass `bucket: "my-bucket"` to the `s3()` helper function without having to specify all the credentials again. + +### `S3Client` objects + +When you're not using environment variables or using multiple buckets, you can create a `S3Client` object to explicitly set credentials. + +```ts +import { S3Client } from "bun"; + +const client = new S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // sessionToken: "..." + endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 + // endpoint: "http://localhost:9000", // MinIO +}); + +// Write using a Response +await file.write(new Response("Hello World!")); + +// Presign a URL +const url = file.presign({ + expiresIn: 60 * 60 * 24, // 1 day + acl: "public-read", +}); + +// Delete the file +await file.delete(); +``` + +### `S3Client.prototype.write` + +To upload or write a file to S3, call `write` on the `S3Client` instance. + +```ts +const client = new Bun.S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + endpoint: "https://s3.us-east-1.amazonaws.com", + bucket: "my-bucket", +}); +await client.write("my-file.txt", "Hello World!"); +await client.write("my-file.txt", new Response("Hello World!")); + +// equivalent to +// await client.file("my-file.txt").write("Hello World!"); +``` + +### `S3Client.prototype.delete` + +To delete a file from S3, call `delete` on the `S3Client` instance. + +```ts +const client = new Bun.S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", +}); + +await client.delete("my-file.txt"); +// equivalent to +// await client.file("my-file.txt").delete(); +``` + +### `S3Client.prototype.exists` + +To check if a file exists in S3, call `exists` on the `S3Client` instance. + +```ts +const client = new Bun.S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", +}); + +const exists = await client.exists("my-file.txt"); +// equivalent to +// const exists = await client.file("my-file.txt").exists(); +``` + +## `S3File` + +`S3File` instances are created by calling the `S3` instance method or the `s3()` helper function. Like `Bun.file()`, `S3File` instances are lazy. They don't refer to something that necessarily exists at the time of creation. That's why all the methods that don't involve network requests are fully synchronous. + +```ts +interface S3File extends Blob { + slice(start: number, end?: number): S3File; + exists(): Promise; + unlink(): Promise; + presign(options: S3Options): string; + text(): Promise; + json(): Promise; + bytes(): Promise; + arrayBuffer(): Promise; + stream(options: S3Options): ReadableStream; + write( + data: + | string + | Uint8Array + | ArrayBuffer + | Blob + | ReadableStream + | Response + | Request, + options?: BlobPropertyBag, + ): Promise; + + exists(options?: S3Options): Promise; + unlink(options?: S3Options): Promise; + delete(options?: S3Options): Promise; + presign(options?: S3Options): string; + + stat(options?: S3Options): Promise; + /** + * Size is not synchronously available because it requires a network request. + * + * @deprecated Use `stat()` instead. + */ + size: NaN; + + // ... more omitted for brevity +} +``` + +Like `Bun.file()`, `S3File` extends [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob), so all the methods that are available on `Blob` are also available on `S3File`. The same API for reading data from a local file is also available for reading data from S3. + +| Method | Output | +| ---------------------------- | ---------------- | +| `await s3File.text()` | `string` | +| `await s3File.bytes()` | `Uint8Array` | +| `await s3File.json()` | `JSON` | +| `await s3File.stream()` | `ReadableStream` | +| `await s3File.arrayBuffer()` | `ArrayBuffer` | + +That means using `S3File` instances with `fetch()`, `Response`, and other web APIs that accept `Blob` instances just works. + +### Partial reads with `slice` + +To read a partial range of a file, you can use the `slice` method. + +```ts +const partial = s3file.slice(0, 1024); + +// Read the partial range as a Uint8Array +const bytes = await partial.bytes(); + +// Read the partial range as a string +const text = await partial.text(); +``` + +Internally, this works by using the HTTP `Range` header to request only the bytes you want. This `slice` method is the same as [`Blob.prototype.slice`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice). + +### Deleting files from S3 + +To delete a file from S3, you can use the `delete` method. + +```ts +await s3file.delete(); +// await s3File.unlink(); +``` + +`delete` is the same as `unlink`. + +## Error codes + +When Bun's S3 API throws an error, it will have a `code` property that matches one of the following values: + +- `ERR_S3_MISSING_CREDENTIALS` +- `ERR_S3_INVALID_METHOD` +- `ERR_S3_INVALID_PATH` +- `ERR_S3_INVALID_ENDPOINT` +- `ERR_S3_INVALID_SIGNATURE` +- `ERR_S3_INVALID_SESSION_TOKEN` + +When the S3 Object Storage service returns an error (that is, not Bun), it will be an `S3Error` instance (an `Error` instance with the name `"S3Error"`). + +## `S3Client` static methods + +The `S3Client` class provides several static methods for interacting with S3. + +### `S3Client.presign` (static) + +To generate a presigned URL for an S3 file, you can use the `S3Client.presign` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 +}; + +const url = S3Client.presign("my-file.txt", { + ...credentials, + expiresIn: 3600, +}); +``` + +This is equivalent to calling `new S3Client(credentials).presign("my-file.txt", { expiresIn: 3600 })`. + +### `S3Client.exists` (static) + +To check if an S3 file exists, you can use the `S3Client.exists` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}; + +const exists = await S3Client.exists("my-file.txt", credentials); +``` + +The same method also works on `S3File` instances. + +```ts +const s3file = Bun.s3("my-file.txt", { + ...credentials, +}); +const exists = await s3file.exists(); +``` + +### `S3Client.stat` (static) + +To get the size, etag, and other metadata of an S3 file, you can use the `S3Client.stat` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}; + +const stat = await S3Client.stat("my-file.txt", credentials); +// { +// etag: "\"7a30b741503c0b461cc14157e2df4ad8\"", +// lastModified: 2025-01-07T00:19:10.000Z, +// size: 1024, +// type: "text/plain;charset=utf-8", +// } +``` + +### `S3Client.delete` (static) + +To delete an S3 file, you can use the `S3Client.delete` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}; + +await S3Client.delete("my-file.txt", credentials); +// equivalent to +// await new S3Client(credentials).delete("my-file.txt"); + +// S3Client.unlink is alias of S3Client.delete +await S3Client.unlink("my-file.txt", credentials); +``` + +## s3:// protocol + +To make it easier to use the same code for local files and S3 files, the `s3://` protocol is supported in `fetch` and `Bun.file()`. + +```ts +const response = await fetch("s3://my-bucket/my-file.txt"); +const file = Bun.file("s3://my-bucket/my-file.txt"); +``` + +You can additionally pass `s3` options to the `fetch` and `Bun.file` functions. + +```ts +const response = await fetch("s3://my-bucket/my-file.txt", { + s3: { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + endpoint: "https://s3.us-east-1.amazonaws.com", + }, + headers: { + "range": "bytes=0-1023", + }, +}); +``` + +### UTF-8, UTF-16, and BOM (byte order mark) + +Like `Response` and `Blob`, `S3File` assumes UTF-8 encoding by default. + +When calling one of the `text()` or `json()` methods on an `S3File`: + +- When a UTF-16 byte order mark (BOM) is detected, it will be treated as UTF-16. JavaScriptCore natively supports UTF-16, so it skips the UTF-8 transcoding process (and strips the BOM). This is mostly good, but it does mean if you have invalid surrogate pairs characters in your UTF-16 string, they will be passed through to JavaScriptCore (same as source code). +- When a UTF-8 BOM is detected, it gets stripped before the string is passed to JavaScriptCore and invalid UTF-8 codepoints are replaced with the Unicode replacement character (`\uFFFD`). +- UTF-32 is not supported. diff --git a/docs/api/spawn.md b/docs/api/spawn.md index 3097af8585..4219e016c6 100644 --- a/docs/api/spawn.md +++ b/docs/api/spawn.md @@ -110,7 +110,7 @@ You can read results from the subprocess via the `stdout` and `stderr` propertie ```ts const proc = Bun.spawn(["bun", "--version"]); const text = await new Response(proc.stdout).text(); -console.log(text); // => "1.1.7" +console.log(text); // => "$BUN_LATEST_VERSION" ``` Configure the output stream by passing one of the following values to `stdout/stderr`: diff --git a/docs/api/sqlite.md b/docs/api/sqlite.md index 49c84136bb..f9a707d27c 100644 --- a/docs/api/sqlite.md +++ b/docs/api/sqlite.md @@ -82,7 +82,7 @@ const strict = new Database( // throws error because of the typo: const query = strict .query("SELECT $message;") - .all({ messag: "Hello world" }); + .all({ message: "Hello world" }); const notStrict = new Database( ":memory:" @@ -90,7 +90,7 @@ const notStrict = new Database( // does not throw error: notStrict .query("SELECT $message;") - .all({ messag: "Hello world" }); + .all({ message: "Hello world" }); ``` ### Load via ES module import diff --git a/docs/api/transpiler.md b/docs/api/transpiler.md index 308a4e152d..d24eb4ac2c 100644 --- a/docs/api/transpiler.md +++ b/docs/api/transpiler.md @@ -137,7 +137,8 @@ Each import in the `imports` array has a `path` and `kind`. Bun categories impor - `import-rule`: `@import 'foo.css'` - `url-token`: `url('./foo.png')` +- `entry-point-build`: `import {foo} from 'bun:entry'` +- `entry-point-run`: `bun ./mymodule` --> ## `.scanImports()` @@ -267,7 +268,8 @@ type Import = { // The import was injected by Bun | "internal"  // Entry point (not common) - | "entry-point" + | "entry-point-build" + | "entry-point-run" } const transpiler = new Bun.Transpiler({ loader: "jsx" }); diff --git a/docs/api/utils.md b/docs/api/utils.md index 3b87922106..979e406851 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -121,7 +121,7 @@ const id = randomUUIDv7(); A UUID v7 is a 128-bit value that encodes the current timestamp, a random value, and a counter. The timestamp is encoded using the lowest 48 bits, and the random value and counter are encoded using the remaining bits. -The `timestamp` parameter defaults to the current time in milliseconds. When the timestamp changes, the counter is reset to a psuedo-random integer wrapped to 4096. This counter is atomic and threadsafe, meaning that using `Bun.randomUUIDv7()` in many Workers within the same process running at the same timestamp will not have colliding counter values. +The `timestamp` parameter defaults to the current time in milliseconds. When the timestamp changes, the counter is reset to a pseudo-random integer wrapped to 4096. This counter is atomic and threadsafe, meaning that using `Bun.randomUUIDv7()` in many Workers within the same process running at the same timestamp will not have colliding counter values. The final 8 bytes of the UUID are a cryptographically secure random value. It uses the same random number generator used by `crypto.randomUUID()` (which comes from BoringSSL, which in turn comes from the platform-specific system random number generator usually provided by the underlying hardware). @@ -771,3 +771,28 @@ console.log(obj); // => { foo: "bar" } ``` Internally, [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) serialize and deserialize the same way. This exposes the underlying [HTML Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) to JavaScript as an ArrayBuffer. + +## `estimateShallowMemoryUsageOf` in `bun:jsc` + +The `estimateShallowMemoryUsageOf` function returns a best-effort estimate of the memory usage of an object in bytes, excluding the memory usage of properties or other objects it references. For accurate per-object memory usage, use `Bun.generateHeapSnapshot`. + +```js +import { estimateShallowMemoryUsageOf } from "bun:jsc"; + +const obj = { foo: "bar" }; +const usage = estimateShallowMemoryUsageOf(obj); +console.log(usage); // => 16 + +const buffer = Buffer.alloc(1024 * 1024); +estimateShallowMemoryUsageOf(buffer); +// => 1048624 + +const req = new Request("https://bun.sh"); +estimateShallowMemoryUsageOf(req); +// => 167 + +const array = Array(1024).fill({ a: 1 }); +// Arrays are usually not stored contiguously in memory, so this will not return a useful value (which isn't a bug). +estimateShallowMemoryUsageOf(array); +// => 16 +``` diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index 6ae39a574c..c477e6a82c 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -279,6 +279,19 @@ $ bun build --compile --asset-naming="[name].[ext]" ./index.ts To trim down the size of the executable a little, pass `--minify` to `bun build --compile`. This uses Bun's minifier to reduce the code size. Overall though, Bun's binary is still way too big and we need to make it smaller. +## Windows-specific flags + +When compiling a standalone executable on Windows, there are two platform-specific options that can be used to customize metadata on the generated `.exe` file: + +- `--windows-icon=path/to/icon.ico` to customize the executable file icon. +- `--windows-hide-console` to disable the background terminal, which can be used for applications that do not need a TTY. + +{% callout %} + +These flags currently cannot be used when cross-compiling because they depend on Windows APIs. + +{% /callout %} + ## Unsupported CLI arguments Currently, the `--compile` flag can only accept a single entrypoint at a time and does not support the following flags: diff --git a/docs/bundler/fullstack.md b/docs/bundler/fullstack.md new file mode 100644 index 0000000000..877648b84e --- /dev/null +++ b/docs/bundler/fullstack.md @@ -0,0 +1,247 @@ +As of Bun v1.1.44, we've added initial support for bundling frontend apps directly in Bun's HTTP server: `Bun.serve()`. Run your frontend and backend in the same app with no extra steps. + +To get started, import your HTML files and pass them to the `static` option in `Bun.serve()`. + +```ts +import dashboard from "./dashboard.html"; +import homepage from "./index.html"; + +Bun.serve({ + // Add HTML imports to `static` + static: { + // Bundle & route index.html to "/" + "/": homepage, + // Bundle & route dashboard.html to "/dashboard" + "/dashboard": dashboard, + }, + + // Enable development mode for: + // - Detailed error messages + // - Rebuild on request + development: true, + + // Handle API requests + async fetch(req) { + // ...your API code + if (req.url.endsWith("/api/users")) { + const users = await Bun.sql`SELECT * FROM users`; + return Response.json(users); + } + + // Return 404 for unmatched routes + return new Response("Not Found", { status: 404 }); + }, +}); +``` + +You'll need to run your app with `bun --experimental-html` to enable this feature: + +```bash +$ bun --experimental-html run app.ts +``` + +## HTML imports are routes + +The web starts with HTML, and so does Bun's fullstack dev server. + +To specify entrypoints to your frontend, import HTML files into your JavaScript/TypeScript/TSX/JSX files. + +```ts +import dashboard from "./dashboard.html"; +import homepage from "./index.html"; +``` + +These HTML files are used as routes in Bun's dev server you can pass to `Bun.serve()`. + +```ts +Bun.serve({ + static: { + "/": homepage, + "/dashboard": dashboard, + } + + fetch(req) { + // ... api requests + }, +}); +``` + +When you make a request to `/dashboard` or `/`, Bun automatically bundles the ` + + + +``` + +Becomes something like this: + +```html#index.html + + + + Home + + + +
+ + + +``` + +### How to use with React + +To use React in your client-side code, import `react-dom/client` and render your app. + +{% codetabs %} + +```ts#src/backend.ts +import dashboard from "./public/dashboard.html"; +import { serve } from "bun"; + +serve({ + static: { + "/": dashboard, + }, + + async fetch(req) { + // ...api requests + return new Response("hello world"); + }, +}); +``` + +```ts#src/frontend.tsx +import "./styles.css"; +import { createRoot } from "react-dom/client"; +import { App } from "./app.tsx"; + +document.addEventListener("DOMContentLoaded", () => { + const root = createRoot(document.getElementById("root")); + root.render(); +}); +``` + +```html#public/dashboard.html + + + + Dashboard + + +
+ + + +``` + +```css#src/styles.css +body { + background-color: red; +} +``` + +```tsx#src/app.tsx +export function App() { + return
Hello World
; +} +``` + +{% /codetabs %} + +### Development mode + +When building locally, enable development mode by setting `development: true` in `Bun.serve()`. + +```js-diff +import homepage from "./index.html"; +import dashboard from "./dashboard.html"; + +Bun.serve({ + static: { + "/": homepage, + "/dashboard": dashboard, + } + ++ development: true, + + fetch(req) { + // ... api requests + }, +}); +``` + +When `development` is `true`, Bun will: + +- Include the `SourceMap` header in the response so that devtools can show the original source code +- Disable minification +- Re-bundle assets on each request to a .html file + +#### Production mode + +When serving your app in production, set `development: false` in `Bun.serve()`. + +- Enable in-memory caching of bundled assets. Bun will bundle assets lazily on the first request to an `.html` file, and cache the result in memory until the server restarts. +- Enables `Cache-Control` headers and `ETag` headers +- Minifies JavaScript/TypeScript/TSX/JSX files + +## How this works + +Bun uses [`HTMLRewriter`](/docs/api/html-rewriter) to scan for ` + ``` + +2. **`` processing** + + - Processes CSS imports and `` tags + - Concatenates CSS files + - Rewrites `url` and asset paths to include content-addressable hashes in URLs + + ```html + + ``` + +3. **`` & asset processing** + + - Links to assets are rewritten to include content-addressable hashes in URLs + - Small assets in CSS files are inlined into `data:` URLs, reducing the total number of HTTP requests sent over the wire + +4. **Rewrite HTML** + + - Combines all ` + + + + + +``` + +One command is all you need (won't be experimental after Bun v1.2): + +{% codetabs %} + +```bash#CLI +$ bun build --experimental-html --experimental-css ./index.html --outdir=dist +``` + +```ts#API +Bun.build({ + entrypoints: ["./index.html"], + outdir: "./dist", + + // On by default in Bun v1.2+ + html: true, + experimentalCss: true, +}); +``` + +{% /codetabs %} + +Bun automatically: + +- Bundles, tree-shakes, and optimizes your JavaScript, JSX and TypeScript +- Bundles and optimizes your CSS +- Copies & hashes images and other assets +- Updates all references to local files or packages in your HTML + +## Zero Config, Maximum Performance + +The HTML bundler is enabled by default after Bun v1.2+. Drop in your existing HTML files and Bun will handle: + +- **TypeScript & JSX** - Write modern JavaScript for browsers without the setup +- **CSS** - Bundle CSS stylesheets directly from `` or `@import` +- **Images & Assets** - Automatic copying & hashing & rewriting of assets in JavaScript, CSS, and HTML + +## Watch mode + +You can run `bun build --watch` to watch for changes and rebuild automatically. + +You've never seen a watch mode this fast. + +## Plugin API + +Need more control? Configure the bundler through the JavaScript API and use Bun's builtin `HTMLRewriter` to preprocess HTML. + +```ts +await Bun.build({ + entrypoints: ["./index.html"], + outdir: "./dist", + html: true, + experimentalCss: true, + minify: true, + + plugins: [ + { + // A plugin that makes every HTML tag lowercase + name: "lowercase-html-plugin", + setup({ onLoad }) { + const rewriter = new HTMLRewriter().on("*", { + element(element) { + element.tagName = element.tagName.toLowerCase(); + }, + text(element) { + element.replace(element.text.toLowerCase()); + }, + }); + + onLoad({ filter: /\.html$/ }, async args => { + const html = await Bun.file(args.path).text(); + + return { + // Bun's bundler will scan the HTML for + + +``` + +{% /codetabs %} + +It will output a new HTML file with the bundled assets: + +{% codetabs %} + +```html#dist/output.html + + + + Local image + External image + + + +``` + +{% /codetabs %} + +Under the hood, it uses [`lol-html`](https://github.com/cloudflare/lol-html) to extract script and link tags as entrypoints, and other assets as external. + +Currently, the list of selectors is: + +- `audio[src]` +- `iframe[src]` +- `img[src]` +- `img[srcset]` +- `link:not([rel~='stylesheet']):not([rel~='modulepreload']):not([rel~='manifest']):not([rel~='icon']):not([rel~='apple-touch-icon'])[href]` +- `link[as='font'][href], link[type^='font/'][href]` +- `link[as='image'][href]` +- `link[as='style'][href]` +- `link[as='video'][href], link[as='audio'][href]` +- `link[as='worker'][href]` +- `link[rel='icon'][href], link[rel='apple-touch-icon'][href]` +- `link[rel='manifest'][href]` +- `link[rel='stylesheet'][href]` +- `script[src]` +- `source[src]` +- `source[srcset]` +- `video[poster]` +- `video[src]` + ### `sh` loader **Bun Shell loader**. Default for `.sh` files diff --git a/docs/bundler/plugins.md b/docs/bundler/plugins.md index 6ac9654f0f..8ea89a4b42 100644 --- a/docs/bundler/plugins.md +++ b/docs/bundler/plugins.md @@ -2,11 +2,47 @@ Bun provides a universal plugin API that can be used to extend both the _runtime Plugins intercept imports and perform custom loading logic: reading files, transpiling code, etc. They can be used to add support for additional file types, like `.scss` or `.yaml`. In the context of Bun's bundler, plugins can be used to implement framework-level features like CSS extraction, macros, and client-server code co-location. -For more complete documentation of the Plugin API, see [Runtime > Plugins](https://bun.sh/docs/runtime/plugins). +## Lifecycle hooks + +Plugins can register callbacks to be run at various points in the lifecycle of a bundle: + +- [`onStart()`](#onstart): Run once the bundler has started a bundle +- [`onResolve()`](#onresolve): Run before a module is resolved +- [`onLoad()`](#onload): Run before a module is loaded. +- [`onBeforeParse()`](#onbeforeparse): Run zero-copy native addons in the parser thread before a file is parsed. + +### Reference + +A rough overview of the types (please refer to Bun's `bun.d.ts` for the full type definitions): + +```ts +type PluginBuilder = { + onStart(callback: () => void): void; + onResolve: ( + args: { filter: RegExp; namespace?: string }, + callback: (args: { path: string; importer: string }) => { + path: string; + namespace?: string; + } | void, + ) => void; + onLoad: ( + args: { filter: RegExp; namespace?: string }, + defer: () => Promise, + callback: (args: { path: string }) => { + loader?: Loader; + contents?: string; + exports?: Record; + }, + ) => void; + config: BuildConfig; +}; + +type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml"; +``` ## Usage -A plugin is defined as simple JavaScript object containing a `name` property and a `setup` function. Register a plugin with Bun using the `plugin` function. +A plugin is defined as simple JavaScript object containing a `name` property and a `setup` function. ```tsx#myPlugin.ts import type { BunPlugin } from "bun"; @@ -22,9 +58,343 @@ const myPlugin: BunPlugin = { This plugin can be passed into the `plugins` array when calling `Bun.build`. ```ts -Bun.build({ +await Bun.build({ entrypoints: ["./app.ts"], outdir: "./out", plugins: [myPlugin], }); ``` + +## Plugin lifecycle + +### Namespaces + +`onLoad` and `onResolve` accept an optional `namespace` string. What is a namespace? + +Every module has a namespace. Namespaces are used to prefix the import in transpiled code; for instance, a loader with a `filter: /\.yaml$/` and `namespace: "yaml:"` will transform an import from `./myfile.yaml` into `yaml:./myfile.yaml`. + +The default namespace is `"file"` and it is not necessary to specify it, for instance: `import myModule from "./my-module.ts"` is the same as `import myModule from "file:./my-module.ts"`. + +Other common namespaces are: + +- `"bun"`: for Bun-specific modules (e.g. `"bun:test"`, `"bun:sqlite"`) +- `"node"`: for Node.js modules (e.g. `"node:fs"`, `"node:path"`) + +### `onStart` + +```ts +onStart(callback: () => void): Promise | void; +``` + +Registers a callback to be run when the bundler starts a new bundle. + +```ts +import { plugin } from "bun"; + +plugin({ + name: "onStart example", + + setup(build) { + build.onStart(() => { + console.log("Bundle started!"); + }); + }, +}); +``` + +The callback can return a `Promise`. After the bundle process has initialized, the bundler waits until all `onStart()` callbacks have completed before continuing. + +For example: + +```ts +const result = await Bun.build({ + entrypoints: ["./app.ts"], + outdir: "./dist", + sourcemap: "external", + plugins: [ + { + name: "Sleep for 10 seconds", + setup(build) { + build.onStart(async () => { + await Bunlog.sleep(10_000); + }); + }, + }, + { + name: "Log bundle time to a file", + setup(build) { + build.onStart(async () => { + const now = Date.now(); + await Bun.$`echo ${now} > bundle-time.txt`; + }); + }, + }, + ], +}); +``` + +In the above example, Bun will wait until the first `onStart()` (sleeping for 10 seconds) has completed, _as well as_ the second `onStart()` (writing the bundle time to a file). + +Note that `onStart()` callbacks (like every other lifecycle callback) do not have the ability to modify the `build.config` object. If you want to mutate `build.config`, you must do so directly in the `setup()` function. + +### `onResolve` + +```ts +onResolve( + args: { filter: RegExp; namespace?: string }, + callback: (args: { path: string; importer: string }) => { + path: string; + namespace?: string; + } | void, +): void; +``` + +To bundle your project, Bun walks down the dependency tree of all modules in your project. For each imported module, Bun actually has to find and read that module. The "finding" part is known as "resolving" a module. + +The `onResolve()` plugin lifecycle callback allows you to configure how a module is resolved. + +The first argument to `onResolve()` is an object with a `filter` and [`namespace`](#what-is-a-namespace) property. The filter is a regular expression which is run on the import string. Effectively, these allow you to filter which modules your custom resolution logic will apply to. + +The second argument to `onResolve()` is a callback which is run for each module import Bun finds that matches the `filter` and `namespace` defined in the first argument. + +The callback receives as input the _path_ to the matching module. The callback can return a _new path_ for the module. Bun will read the contents of the _new path_ and parse it as a module. + +For example, redirecting all imports to `images/` to `./public/images/`: + +```ts +import { plugin } from "bun"; + +plugin({ + name: "onResolve example", + setup(build) { + build.onResolve({ filter: /.*/, namespace: "file" }, args => { + if (args.path.startsWith("images/")) { + return { + path: args.path.replace("images/", "./public/images/"), + }; + } + }); + }, +}); +``` + +### `onLoad` + +```ts +onLoad( + args: { filter: RegExp; namespace?: string }, + defer: () => Promise, + callback: (args: { path: string, importer: string, namespace: string, kind: ImportKind }) => { + loader?: Loader; + contents?: string; + exports?: Record; + }, +): void; +``` + +After Bun's bundler has resolved a module, it needs to read the contents of the module and parse it. + +The `onLoad()` plugin lifecycle callback allows you to modify the _contents_ of a module before it is read and parsed by Bun. + +Like `onResolve()`, the first argument to `onLoad()` allows you to filter which modules this invocation of `onLoad()` will apply to. + +The second argument to `onLoad()` is a callback which is run for each matching module _before_ Bun loads the contents of the module into memory. + +This callback receives as input the _path_ to the matching module, the _importer_ of the module (the module that imported the module), the _namespace_ of the module, and the _kind_ of the module. + +The callback can return a new `contents` string for the module as well as a new `loader`. + +For example: + +```ts +import { plugin } from "bun"; + +const envPlugin: BunPlugin = { + name: "env plugin", + setup(build) { + build.onLoad({ filter: /env/, namespace: "file" }, args => { + return { + contents: `export default ${JSON.stringify(process.env)}`, + loader: "js", + }; + }); + }, +}); + +Bun.build({ + entrypoints: ["./app.ts"], + outdir: "./dist", + plugins: [envPlugin], +}); + +// import env from "env" +// env.FOO === "bar" +``` + +This plugin will transform all imports of the form `import env from "env"` into a JavaScript module that exports the current environment variables. + +#### `.defer()` + +One of the arguments passed to the `onLoad` callback is a `defer` function. This function returns a `Promise` that is resolved when all _other_ modules have been loaded. + +This allows you to delay execution of the `onLoad` callback until all other modules have been loaded. + +This is useful for returning contents of a module that depends on other modules. + +##### Example: tracking and reporting unused exports + +```ts +import { plugin } from "bun"; + +plugin({ + name: "track imports", + setup(build) { + const transpiler = new Bun.Transpiler(); + + let trackedImports: Record = {}; + + // Each module that goes through this onLoad callback + // will record its imports in `trackedImports` + build.onLoad({ filter: /\.ts/ }, async ({ path }) => { + const contents = await Bun.file(path).arrayBuffer(); + + const imports = transpiler.scanImports(contents); + + for (const i of imports) { + trackedImports[i.path] = (trackedImports[i.path] || 0) + 1; + } + + return undefined; + }); + + build.onLoad({ filter: /stats\.json/ }, async ({ defer }) => { + // Wait for all files to be loaded, ensuring + // that every file goes through the above `onLoad()` function + // and their imports tracked + await defer(); + + // Emit JSON containing the stats of each import + return { + contents: `export default ${JSON.stringify(trackedImports)}`, + loader: "json", + }; + }); + }, +}); +``` + +Note that the `.defer()` function currently has the limitation that it can only be called once per `onLoad` callback. + +## Native plugins + +One of the reasons why Bun's bundler is so fast is that it is written in native code and leverages multi-threading to load and parse modules in parallel. + +However, one limitation of plugins written in JavaScript is that JavaScript itself is single-threaded. + +Native plugins are written as [NAPI](https://bun.sh/docs/api/node-api) modules and can be run on multiple threads. This allows native plugins to run much faster than JavaScript plugins. + +In addition, native plugins can skip unnecessary work such as the UTF-8 -> UTF-16 conversion needed to pass strings to JavaScript. + +These are the following lifecycle hooks which are available to native plugins: + +- [`onBeforeParse()`](#onbeforeparse): Called on any thread before a file is parsed by Bun's bundler. + +Native plugins are NAPI modules which expose lifecycle hooks as C ABI functions. + +To create a native plugin, you must export a C ABI function which matches the signature of the native lifecycle hook you want to implement. + +### Creating a native plugin in Rust + +Native plugins are NAPI modules which expose lifecycle hooks as C ABI functions. + +To create a native plugin, you must export a C ABI function which matches the signature of the native lifecycle hook you want to implement. + +```bash +bun add -g @napi-rs/cli +napi new +``` + +Then install this crate: + +```bash +cargo add bun-native-plugin +``` + +Now, inside the `lib.rs` file, we'll use the `bun_native_plugin::bun` proc macro to define a function which +will implement our native plugin. + +Here's an example implementing the `onBeforeParse` hook: + +```rs +use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader}; +use napi_derive::napi; + +/// Define the plugin and its name +define_bun_plugin!("replace-foo-with-bar"); + +/// Here we'll implement `onBeforeParse` with code that replaces all occurrences of +/// `foo` with `bar`. +/// +/// We use the #[bun] macro to generate some of the boilerplate code. +/// +/// The argument of the function (`handle: &mut OnBeforeParse`) tells +/// the macro that this function implements the `onBeforeParse` hook. +#[bun] +pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> { + // Fetch the input source code. + let input_source_code = handle.input_source_code()?; + + // Get the Loader for the file + let loader = handle.output_loader(); + + + let output_source_code = input_source_code.replace("foo", "bar"); + + handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX); + + Ok(()) +} +``` + +And to use it in Bun.build(): + +```typescript +import myNativeAddon from "./my-native-addon"; +Bun.build({ + entrypoints: ["./app.tsx"], + plugins: [ + { + name: "my-plugin", + + setup(build) { + build.onBeforeParse( + { + namespace: "file", + filter: "**/*.tsx", + }, + { + napiModule: myNativeAddon, + symbol: "replace_foo_with_bar", + // external: myNativeAddon.getSharedState() + }, + ); + }, + }, + ], +}); +``` + +### `onBeforeParse` + +```ts +onBeforeParse( + args: { filter: RegExp; namespace?: string }, + callback: { napiModule: NapiModule; symbol: string; external?: unknown }, +): void; +``` + +This lifecycle callback is run immediately before a file is parsed by Bun's bundler. + +As input, it receives the file's contents and can optionally return new source code. + +This callback can be called from any thread and so the napi module implementation must be thread-safe. diff --git a/docs/bundler/vs-esbuild.md b/docs/bundler/vs-esbuild.md index 1266914c05..35bb62f6f7 100644 --- a/docs/bundler/vs-esbuild.md +++ b/docs/bundler/vs-esbuild.md @@ -695,7 +695,7 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot - In Bun, `minify` can be a boolean or an object. ```ts - Bun.build({ + await Bun.build({ entrypoints: ['./index.tsx'], // enable all minification minify: true diff --git a/docs/cli/add.md b/docs/cli/add.md index ff90730d73..ca9a8af46e 100644 --- a/docs/cli/add.md +++ b/docs/cli/add.md @@ -33,6 +33,14 @@ To add a package as an optional dependency (`"optionalDependencies"`): $ bun add --optional lodash ``` +## `--peer` + +To add a package as a peer dependency (`"peerDependencies"`): + +```bash +$ bun add --peer @types/bun +``` + ## `--exact` {% callout %} diff --git a/docs/cli/bun-install.md b/docs/cli/bun-install.md index 20832cc53e..a2eb2ee196 100644 --- a/docs/cli/bun-install.md +++ b/docs/cli/bun-install.md @@ -47,6 +47,9 @@ registry = "https://registry.yarnpkg.com/" # Install for production? This is the equivalent to the "--production" CLI argument production = false +# Save a text-based lockfile? This is equivalent to the "--save-text-lockfile" CLI argument +saveTextLockfile = false + # Disallow changes to lockfile? This is the equivalent to the "--frozen-lockfile" CLI argument frozenLockfile = false @@ -54,12 +57,15 @@ frozenLockfile = false dryRun = true # Install optionalDependencies (default: true) +# Setting this to false is equivalent to the `--omit=optional` CLI argument optional = true # Install local devDependencies (default: true) +# Setting this to false is equivalent to the `--omit=dev` CLI argument dev = true # Install peerDependencies (default: true) +# Setting this to false is equivalent to the `--omit=peer` CLI argument peer = true # Max number of concurrent lifecycle scripts (default: (cpu count or GOMAXPROCS) x2) @@ -108,6 +114,7 @@ export interface Install { scopes: Scopes; registry: Registry; production: boolean; + saveTextLockfile: boolean; frozenLockfile: boolean; dryRun: boolean; optional: boolean; diff --git a/docs/cli/filter.md b/docs/cli/filter.md index b4ee0dcf3f..62c53658a3 100644 --- a/docs/cli/filter.md +++ b/docs/cli/filter.md @@ -1,4 +1,51 @@ -Use the `--filter` flag to execute lifecycle scripts in multiple packages at once: +The `--filter` (or `-F`) flag is used for selecting packages by pattern in a monorepo. Patterns can be used to match package names or package paths, with full glob syntax support. + +Currently `--filter` is supported by `bun install` and `bun outdated`, and can also be used to run scripts for multiple packages at once. + +## Matching + +### Package Name `--filter ` + +Name patterns select packages based on the package name, as specified in `package.json`. For example, if you have packages `pkg-a`, `pkg-b` and `other`, you can match all packages with `*`, only `pkg-a` and `pkg-b` with `pkg*`, and a specific package by providing the full name of the package. + +### Package Path `--filter ./` + +Path patterns are specified by starting the pattern with `./`, and will select all packages in directories that match the pattern. For example, to match all packages in subdirectories of `packages`, you can use `--filter './packages/**'`. To match a package located in `packages/foo`, use `--filter ./packages/foo`. + +## `bun install` and `bun outdated` + +Both `bun install` and `bun outdated` support the `--filter` flag. + +`bun install` by default will install dependencies for all packages in the monorepo. To install dependencies for specific packages, use `--filter`. + +Given a monorepo with workspaces `pkg-a`, `pkg-b`, and `pkg-c` under `./packages`: + +```bash +# Install dependencies for all workspaces except `pkg-c` +$ bun install --filter '!pkg-c' + +# Install dependencies for packages in `./packages` (`pkg-a`, `pkg-b`, `pkg-c`) +$ bun install --filter './packages/*' + +# Save as above, but exclude the root package.json +$ bun install --filter --filter '!./' --filter './packages/*' +``` + +Similarly, `bun outdated` will display outdated dependencies for all packages in the monorepo, and `--filter` can be used to restrict the command to a subset of the packages: + +```bash +# Display outdated dependencies for workspaces starting with `pkg-` +$ bun outdated --filter 'pkg-*' + +# Display outdated dependencies for only the root package.json +$ bun outdated --filter './' +``` + +For more information on both these commands, see [`bun install`](https://bun.sh/docs/cli/install) and [`bun outdated`](https://bun.sh/docs/cli/outdated). + +## Running scripts with `--filter` + +Use the `--filter` flag to execute scripts in multiple packages at once: ```bash bun --filter + + +``` + +--- + +Now you can `import` or `require` `*.svelte` files in your tests, and it will load the Svelte component as a JavaScript module. + +```ts#hello-svelte.test.ts +import { test, expect } from "bun:test"; +import { render, fireEvent } from "@testing-library/svelte"; +import Counter from "./Counter.svelte"; + +test("Counter increments when clicked", async () => { + const { getByText, component } = render(Counter); + const button = getByText("+1"); + + // Initial state + expect(component.$$.ctx[0]).toBe(0); // initialCount is the first prop + + // Click the increment button + await fireEvent.click(button); + + // Check the new state + expect(component.$$.ctx[0]).toBe(1); +}); +``` + +--- + +Use `bun test` to run your tests. + +```bash +$ bun test +``` + +--- diff --git a/docs/guides/test/testing-library.md b/docs/guides/test/testing-library.md index 6adc8ed00d..b78ccc8457 100644 --- a/docs/guides/test/testing-library.md +++ b/docs/guides/test/testing-library.md @@ -49,7 +49,7 @@ Next, add these preload scripts to your `bunfig.toml` (you can also have everyth ```toml#bunfig.toml [test] -preload = ["happydom.ts", "testing-library.ts"] +preload = ["./happydom.ts", "./testing-library.ts"] ``` --- @@ -84,4 +84,4 @@ test('Can use Testing Library', () => { --- -Refer to the [Testing Library docs](https://testing-library.com/), [Happy DOM repo](https://github.com/capricorn86/happy-dom) and [Docs > Test runner > DOM](https://bun.sh/docs/test/dom) for complete documentation on writing browser tests with Bun. \ No newline at end of file +Refer to the [Testing Library docs](https://testing-library.com/), [Happy DOM repo](https://github.com/capricorn86/happy-dom) and [Docs > Test runner > DOM](https://bun.sh/docs/test/dom) for complete documentation on writing browser tests with Bun. diff --git a/docs/guides/test/update-snapshots.md b/docs/guides/test/update-snapshots.md index 3d9ba078eb..9d0fb7c967 100644 --- a/docs/guides/test/update-snapshots.md +++ b/docs/guides/test/update-snapshots.md @@ -4,10 +4,6 @@ name: Update snapshots in `bun test` Bun's test runner supports Jest-style snapshot testing via `.toMatchSnapshot()`. -{% callout %} -The `.toMatchInlineSnapshot()` method is not yet supported. -{% /callout %} - ```ts#snap.test.ts import { test, expect } from "bun:test"; @@ -33,7 +29,7 @@ To regenerate snapshots, use the `--update-snapshots` flag. ```sh $ bun test --update-snapshots -bun test v1.x (9c68abdb) +bun test v$BUN_LATEST_VERSION (9c68abdb) test/snap.test.ts: ✓ snapshot [0.86ms] @@ -47,4 +43,4 @@ Ran 1 tests across 1 files. [102.00ms] --- -See [Docs > Test Runner > Snapshots](https://bun.sh/docs/test/mocks) for complete documentation on mocking with the Bun test runner. +See [Docs > Test Runner > Snapshots](https://bun.sh/docs/test/snapshots) for complete documentation on snapshots with the Bun test runner. diff --git a/docs/guides/util/version.md b/docs/guides/util/version.md index c24074bd06..0e358b8ce5 100644 --- a/docs/guides/util/version.md +++ b/docs/guides/util/version.md @@ -5,7 +5,7 @@ name: Get the current Bun version Get the current version of Bun in a semver format. ```ts#index.ts -Bun.version; // => "0.6.15" +Bun.version; // => "$BUN_LATEST_VERSION" ``` --- diff --git a/docs/guides/write-file/unlink.md b/docs/guides/write-file/unlink.md index ba0cbe8b1d..84cff374f3 100644 --- a/docs/guides/write-file/unlink.md +++ b/docs/guides/write-file/unlink.md @@ -2,22 +2,15 @@ name: Delete a file --- -To synchronously delete a file with Bun, use the `unlinkSync` function from the [`node:fs`](https://nodejs.org/api/fs.html#fs_fs_unlink_path_callback) module. (Currently, there is no `Bun` API for deleting files.) +The `Bun.file()` function accepts a path and returns a `BunFile` instance. Use the `.delete()` method to delete the file. ```ts -import { unlinkSync } from "node:fs"; - const path = "/path/to/file.txt"; -unlinkSync(path); +const file = Bun.file(path); + +await file.delete(); ``` --- -To remove a file asynchronously, use the `unlink` function from the [`node:fs/promises`](https://nodejs.org/api/fs.html#fs_fspromises_unlink_path) module. - -```ts -import { unlink } from "node:fs/promises"; - -const path = "/path/to/file.txt"; -await unlink(path); -``` +See [Docs > API > File I/O](https://bun.sh/docs/api/file-io#reading-files-bun-file) for complete documentation of `Bun.file()`. diff --git a/docs/index.md b/docs/index.md index 8a994cbcff..f7c3620f20 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called `bun`. -At its core is the _Bun runtime_, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage. +At its core is the _Bun runtime_, a fast JavaScript runtime designed as **a drop-in replacement for Node.js**. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage. ```bash $ bun run index.tsx # TS and JSX supported out of the box diff --git a/docs/install/cache.md b/docs/install/cache.md index 6543ec87a4..f1e84d4cea 100644 --- a/docs/install/cache.md +++ b/docs/install/cache.md @@ -1,4 +1,4 @@ -All packages downloaded from the registry are stored in a global cache at `~/.bun/install/cache`. They are stored in subdirectories named like `${name}@${version}`, so multiple versions of a package can be cached. +All packages downloaded from the registry are stored in a global cache at `~/.bun/install/cache`, or the path defined by the environment variable `BUN_INSTALL_CACHE_DIR`. They are stored in subdirectories named like `${name}@${version}`, so multiple versions of a package can be cached. {% details summary="Configuring cache behavior (bunfig.toml)" %} @@ -15,6 +15,8 @@ disable = false disableManifest = false ``` +{% /details %} + ## Minimizing re-downloads Bun strives to avoid re-downloading packages multiple times. When installing a package, if the cache already contains a version in the range specified by `package.json`, Bun will use the cached package instead of downloading it again. @@ -33,14 +35,14 @@ Once a package is downloaded into the cache, Bun still needs to copy those files ## Saving disk space -Since Bun uses hardlinks to "copy" a module into a project's `node_modules` directory on Linux, the contents of the package only exist in a single location on disk, greatly reducing the amount of disk space dedicated to `node_modules`. +Since Bun uses hardlinks to "copy" a module into a project's `node_modules` directory on Linux and Windows, the contents of the package only exist in a single location on disk, greatly reducing the amount of disk space dedicated to `node_modules`. This benefit also applies to macOS, but there are exceptions. It uses `clonefile` which is copy-on-write, meaning it will not occupy disk space, but it will count towards drive's limit. This behavior is useful if something attempts to patch `node_modules/*`, so it's impossible to affect other installations. {% details summary="Installation strategies" %} This behavior is configurable with the `--backend` flag, which is respected by all of Bun's package management commands. -- **`hardlink`**: Default on Linux. +- **`hardlink`**: Default on Linux and Windows. - **`clonefile`** Default on macOS. - **`clonefile_each_dir`**: Similar to `clonefile`, except it clones each file individually per directory. It is only available on macOS and tends to perform slower than `clonefile`. - **`copyfile`**: The fallback used when any of the above fail. It is the slowest option. On macOS, it uses `fcopyfile()`; on Linux it uses `copy_file_range()`. diff --git a/docs/install/index.md b/docs/install/index.md index cdf41b9311..0f04c92a12 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -55,12 +55,25 @@ To install dependencies without allowing changes to lockfile (useful on CI): $ bun install --frozen-lockfile ``` -To perform a dry run (i.e. don't actually install anything): +To exclude dependency types from installing, use `--omit` with `dev`, `optional`, or `peer`: + +```bash +# Disable devDependencies and optionalDependencies +$ bun install --omit=dev --omit=optional +``` + +To perform a dry run (i.e. don't actually install anything or update the lockfile): ```bash $ bun install --dry-run ``` +To generate a lockfile without install packages: + +```bash +$ bun install --lockfile-only +``` + To modify logging verbosity: ```bash @@ -86,6 +99,9 @@ peer = true # equivalent to `--production` flag production = false +# equivalent to `--save-text-lockfile` flag +saveTextLockfile = false + # equivalent to `--frozen-lockfile` flag frozenLockfile = false @@ -127,6 +143,12 @@ To add a package as an optional dependency (`"optionalDependencies"`): $ bun add --optional lodash ``` +To add a package as a peer dependency (`"peerDependencies"`): + +```bash +$ bun add --peer @types/bun +``` + To install a package globally: ```bash diff --git a/docs/install/lockfile.md b/docs/install/lockfile.md index 66fb28e2b2..533bc6489c 100644 --- a/docs/install/lockfile.md +++ b/docs/install/lockfile.md @@ -49,6 +49,18 @@ Packages, metadata for those packages, the hoisted install order, dependencies f It uses linear arrays for all data. [Packages](https://github.com/oven-sh/bun/blob/be03fc273a487ac402f19ad897778d74b6d72963/src/install/install.zig#L1825) are referenced by an auto-incrementing integer ID or a hash of the package name. Strings longer than 8 characters are de-duplicated. Prior to saving on disk, the lockfile is garbage-collected & made deterministic by walking the package tree and cloning the packages in dependency order. +#### Generate a lockfile without installing? + +To generate a lockfile without installing to `node_modules` you can use the `--lockfile-only` flag. The lockfile will always be saved to disk, even if it is up-to-date with the `package.json`(s) for your project. + +```bash +$ bun install --lockfile-only +``` + +{% callout %} +**Note** - using `--lockfile-only` will still populate the global install cache with registry metadata and git/tarball dependencies. +{% /callout %} + #### Can I opt out? To install without creating a lockfile: @@ -74,6 +86,24 @@ print = "yarn" {% /codetabs %} +### Text-based lockfile + +Bun v1.1.39 introduced `bun.lock`, a JSONC formatted lockfile. `bun.lock` is human-readable and git-diffable without configuration, at [no cost to performance](https://bun.sh/blog/bun-lock-text-lockfile#cached-bun-install-gets-30-faster). + +To generate the lockfile, use `--save-text-lockfile` with `bun install`. You can do this for new projects and existing projects already using `bun.lockb` (resolutions will be preserved). + +```bash +$ bun install --save-text-lockfile +$ head -n3 bun.lock +{ + "lockfileVersion": 0, + "workspaces": { +``` + +Once `bun.lock` is generated, Bun will use it for all subsequent installs and updates through commands that read and modify the lockfile. If both lockfiles exist, `bun.lock` will be chosen over `bun.lockb`. + +Bun v1.2.0 will switch the default lockfile format to `bun.lock`. + {% details summary="Configuring lockfile" %} ```toml diff --git a/docs/install/npmrc.md b/docs/install/npmrc.md index ae3c074892..cf7f8ac1f4 100644 --- a/docs/install/npmrc.md +++ b/docs/install/npmrc.md @@ -6,11 +6,11 @@ Bun supports loading configuration options from [`.npmrc`](https://docs.npmjs.co {% /callout %} -# Supported options +## Supported options ### `registry`: Set the default registry -The default registry is used to resolve packages, it's default value is `npm`'s official registry (`https://registry.npmjs.org/`). +The default registry is used to resolve packages, its default value is `npm`'s official registry (`https://registry.npmjs.org/`). To change it, you can set the `registry` option in `.npmrc`: diff --git a/docs/install/workspaces.md b/docs/install/workspaces.md index 64d2445132..fb25a0a7db 100644 --- a/docs/install/workspaces.md +++ b/docs/install/workspaces.md @@ -53,6 +53,16 @@ Each workspace has it's own `package.json`. When referencing other packages in t } ``` +`bun install` will install dependencies for all workspaces in the monorepo, de-duplicating packages if possible. If you only want to install dependencies for specific workspaces, you can use the `--filter` flag. + +```bash +# Install dependencies for all workspaces starting with `pkg-` except for `pkg-c` +$ bun install --filter "pkg-*" --filter "!pkg-c" + +# Paths can also be used. This is equivalent to the command above. +$ bun install --filter "./packages/pkg-*" --filter "!pkg-c" # or --filter "!./packages/pkg-c" +``` + Workspaces have a couple major benefits. - **Code can be split into logical parts.** If one package relies on another, you can simply add it as a dependency in `package.json`. If package `b` depends on `a`, `bun install` will install your local `packages/a` directory into `node_modules` instead of downloading it from the npm registry. diff --git a/docs/installation.md b/docs/installation.md index f98e3bbfa1..c1c6a09eab 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -14,7 +14,7 @@ Kernel version 5.6 or higher is strongly recommended, but the minimum is 5.1. Us ```bash#macOS/Linux_(curl) $ curl -fsSL https://bun.sh/install | bash # for macOS, Linux, and WSL # to install a specific version -$ curl -fsSL https://bun.sh/install | bash -s "bun-v1.0.0" +$ curl -fsSL https://bun.sh/install | bash -s "bun-v$BUN_LATEST_VERSION" ``` ```bash#npm @@ -166,10 +166,10 @@ Since Bun is a single binary, you can install older versions of Bun by re-runnin ### Installing a specific version of Bun on Linux/Mac -To install a specific version of Bun, you can pass the git tag of the version you want to install to the install script, such as `bun-v1.1.6` or `bun-v1.1.1`. +To install a specific version of Bun, you can pass the git tag of the version you want to install to the install script, such as `bun-v1.2.0` or `bun-v$BUN_LATEST_VERSION`. ```sh -$ curl -fsSL https://bun.sh/install | bash -s "bun-v1.1.6" +$ curl -fsSL https://bun.sh/install | bash -s "bun-v$BUN_LATEST_VERSION" ``` ### Installing a specific version of Bun on Windows @@ -178,7 +178,7 @@ On Windows, you can install a specific version of Bun by passing the version num ```sh # PowerShell: -$ iex "& {$(irm https://bun.sh/install.ps1)} -Version 1.1.6" +$ iex "& {$(irm https://bun.sh/install.ps1)} -Version $BUN_LATEST_VERSION" ``` ## Downloading Bun binaries directly diff --git a/docs/nav.ts b/docs/nav.ts index c4f04ca7c2..09ac029f51 100644 --- a/docs/nav.ts +++ b/docs/nav.ts @@ -214,9 +214,12 @@ export default { page("bundler", "`Bun.build`", { description: "Bundle code for consumption in the browser with Bun's native bundler.", }), - // page("bundler/intro", "How bundlers work", { - // description: "A visual introduction to bundling", - // }), + page("bundler/html", "HTML", { + description: `Bundle html files with Bun's native bundler.`, + }), + page("bundler/fullstack", "Fullstack Dev Server", { + description: "Serve your frontend and backend from the same app with Bun's dev server.", + }), page("bundler/loaders", "Loaders", { description: "Bun's built-in loaders for the bundler and runtime", }), @@ -226,6 +229,7 @@ export default { page("bundler/macros", "Macros", { description: `Run JavaScript functions at bundle-time and inline the results into your bundle`, }), + page("bundler/vs-esbuild", "vs esbuild", { description: `Guides for migrating from other bundlers to Bun.`, }), @@ -310,6 +314,9 @@ export default { page("api/streams", "Streams", { description: `Reading, writing, and manipulating streams of data in Bun.`, }), // "`Bun.serve`"), + page("api/s3", "S3 Object Storage", { + description: `Bun provides fast, native bindings for interacting with S3-compatible object storage services.`, + }), page("api/file-io", "File I/O", { description: `Read and write files fast with Bun's heavily optimized file system API.`, }), // "`Bun.write`"), @@ -402,6 +409,9 @@ export default { page("project/building-windows", "Building Windows", { description: "Learn how to setup a development environment for contributing to the Windows build of Bun.", }), + page("project/bindgen", "Bindgen", { + description: "About the bindgen code generator", + }), page("project/licensing", "License", { description: `Bun is a MIT-licensed project with a large number of statically-linked dependencies with various licenses.`, }), diff --git a/docs/project/bindgen.md b/docs/project/bindgen.md new file mode 100644 index 0000000000..83ee48d63a --- /dev/null +++ b/docs/project/bindgen.md @@ -0,0 +1,225 @@ +{% callout %} + +This document is for maintainers and contributors to Bun, and describes internal implementation details. + +{% /callout %} + +The new bindings generator, introduced to the codebase in Dec 2024, scans for +`*.bind.ts` to find function and class definition, and generates glue code to +interop between JavaScript and native code. + +There are currently other code generators and systems that achieve similar +purposes. The following will all eventually be completely phased out in favor of +this one: + +- "Classes generator", converting `*.classes.ts` for custom classes. +- "JS2Native", allowing ad-hoc calls from `src/js` to native code. + +## Creating JS Functions in Zig + +Given a file implementing a simple function, such as `add` + +```zig#src/bun.js/math.zig +pub fn add(global: *JSC.JSGlobalObject, a: i32, b: i32) !i32 { + return std.math.add(i32, a, b) catch { + // Binding functions can return `error.OutOfMemory` and `error.JSError`. + // Others like `error.Overflow` from `std.math.add` must be converted. + // Remember to be descriptive. + return global.throwPretty("Integer overflow while adding", .{}); + }; +} + +const gen = bun.gen.math; // "math" being this file's basename + +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +``` + +Then describe the API schema using a `.bind.ts` function. The binding file goes next to the Zig file. + +```ts#src/bun.js/math.bind.ts +import { t, fn } from 'bindgen'; + +export const add = fn({ + args: { + global: t.globalObject, + a: t.i32, + b: t.i32.default(1), + }, + ret: t.i32, +}); +``` + +This function declaration is equivalent to: + +```ts +/** + * Throws if zero arguments are provided. + * Wraps out of range numbers using modulo. + */ +declare function add(a: number, b: number = 1): number; +``` + +The code generator will provide `bun.gen.math.jsAdd`, which is the native +function implementation. To pass to JavaScript, use +`bun.gen.math.createAddCallback(global)`. JS files in `src/js/` may use +`$bindgenFn("math.bind.ts", "add")` to get a handle to the implementation. + +## Strings + +The type for receiving strings is one of [`t.DOMString`](https://webidl.spec.whatwg.org/#idl-DOMString), [`t.ByteString`](https://webidl.spec.whatwg.org/#idl-ByteString), and [`t.USVString`](https://webidl.spec.whatwg.org/#idl-USVString). These map directly to their WebIDL counterparts, and have slightly different conversion logic. Bindgen will pass BunString to native code in all cases. + +When in doubt, use DOMString. + +`t.UTF8String` can be used in place of `t.DOMString`, but will call `bun.String.toUTF8`. The native callback gets `[]const u8` (WTF-8 data) passed to native code, freeing it after the function returns. + +TLDRs from WebIDL spec: + +- ByteString can only contain valid latin1 characters. It is not safe to assume bun.String is already in 8-bit format, but it is extremely likely. +- USVString will not contain invalid surrogate pairs, aka text that can be represented correctly in UTF-8. +- DOMString is the loosest but also most recommended strategy. + +## Function Variants + +A `variants` can specify multiple variants (also known as overloads). + +```ts#src/bun.js/math.bind.ts +import { t, fn } from 'bindgen'; + +export const action = fn({ + variants: [ + { + args: { + a: t.i32, + }, + ret: t.i32, + }, + { + args: { + a: t.DOMString, + }, + ret: t.DOMString, + }, + ] +}); +``` + +In Zig, each variant gets a number, based on the order the schema defines. + +```zig +fn action1(a: i32) i32 { + return a; +} + +fn action2(a: bun.String) bun.String { + return a; +} +``` + +## `t.dictionary` + +A `dictionary` is a definition for a JavaScript object, typically as a function inputs. For function outputs, it is usually a smarter idea to declare a class type to add functions and destructuring. + +## Enumerations + +To use [WebIDL's enumeration](https://webidl.spec.whatwg.org/#idl-enums) type, use either: + +- `t.stringEnum`: Create and codegen a new enum type. +- `t.zigEnum`: Derive a bindgen type off of an existing enum in the codebase. + +An example of `stringEnum` as used in `fmt.zig` / `bun:internal-for-testing` + +```ts +export const Formatter = t.stringEnum( + "highlight-javascript", + "escape-powershell", +); + +export const fmtString = fn({ + args: { + global: t.globalObject, + code: t.UTF8String, + formatter: Formatter, + }, + ret: t.DOMString, +}); +``` + +WebIDL strongly encourages using kebab case for enumeration values, to be consistent with existing Web APIs. + +### Deriving enums from Zig code + +TODO: zigEnum + +## `t.oneOf` + +A `oneOf` is a union between two or more types. It is represented by `union(enum)` in Zig. + +TODO: + +## Attributes + +There are set of attributes that can be chained onto `t.*` types. On all types there are: + +- `.required`, in dictionary parameters only +- `.optional`, in function arguments only +- `.default(T)` + +When a value is optional, it is lowered to a Zig optional. + +Depending on the type, there are more attributes available. See the type definitions in auto-complete for more details. Note that one of the above three can only be applied, and they must be applied at the end. + +### Integer Attributes + +Integer types allow customizing the overflow behavior with `clamp` or `enforceRange` + +```ts +import { t, fn } from "bindgen"; + +export const add = fn({ + args: { + global: t.globalObject, + // enforce in i32 range + a: t.i32.enforceRange(), + // clamp to u16 range + b: t.u16, + // enforce in arbitrary range, with a default if not provided + c: t.i32.enforceRange(0, 1000).default(5), + // clamp to arbitrary range, or null + d: t.u16.clamp(0, 10).optional, + }, + ret: t.i32, +}); +``` + +Various Node.js validator functions such as `validateInteger`, `validateNumber`, and more are available. Use these when implementing Node.js APIs, so the error messages match 1:1 what Node would do. + +Unlike `enforceRange`, which is taken from WebIDL, `validate*` functions are much more strict on the input they accept. For example, Node's numerical validator check `typeof value === 'number'`, while WebIDL uses `ToNumber` for lossy conversion. + +```ts +import { t, fn } from "bindgen"; + +export const add = fn({ + args: { + global: t.globalObject, + // throw if not given a number + a: t.f64.validateNumber(), + // valid in i32 range + a: t.i32.validateInt32(), + // f64 within safe integer range + b: t.f64.validateInteger(), + // f64 in given range + c: t.f64.validateNumber(-10000, 10000), + }, + ret: t.i32, +}); +``` + +## Callbacks + +TODO + +## Classes + +TODO diff --git a/docs/quickstart.md b/docs/quickstart.md index 68ac012ed7..6e151b3fef 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -92,7 +92,7 @@ Bun can also execute `"scripts"` from your `package.json`. Add the following scr + "start": "bun run index.ts" + }, "devDependencies": { - "@types/bun": "^1.0.0" + "@types/bun": "latest" } } ``` diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 1bfcd540e5..8a19be1ce5 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -238,6 +238,17 @@ By default Bun uses caret ranges; if the `latest` version of a package is `2.4.1 exact = false ``` +### `install.saveTextLockfile` + +Generate `bun.lock`, a human-readable text-based lockfile. Once generated, Bun will use this file instead of `bun.lockb`, choosing it over the binary lockfile if both are present. + +Default `false`. In Bun v1.2.0 the default lockfile format will change to `bun.lock`. + +```toml +[install] +saveTextLockfile = true +``` + + "; + + int offset = (trimmedStr.length() + padding.length()) % 4; + // multiple X by the number in offset + pos = 0u; + for (int posCount = 0; posCount < offset; posCount = posCount + 1) { + if ((pos = padding.find(L"X", pos)) != std::string::npos) { + padding.replace(pos, 1, L"XX"); + pos += executionLevel_.length(); + } + } + + // convert the wchar back into char, so that it encodes correctly for Windows to read the XML. + std::wstring stringSectionW = trimmedStr + padding; + std::wstring_convert, wchar_t> converter; + std::string stringSection = converter.to_bytes(stringSectionW); + + if (!UpdateResourceW(ru.Get(), RT_MANIFEST, MAKEINTRESOURCEW(1), + kLangEnUs, // this is hardcoded at 1033, ie, en-us, as that is what RT_MANIFEST default uses + &stringSection.at(0), sizeof(char) * stringSection.size())) { + return false; + } + } + + // load file contents and replace the manifest + if (!applicationManifestPath_.empty()) { + std::wstring fileContents = ReadFileToString(applicationManifestPath_.c_str()); + + // clean old padding and add new padding, ensuring that the size is a multiple of 4 + std::wstring::size_type padPos = fileContents.find(L""); + // trim anything after the , 11 being the length of (ie, remove old padding) + std::wstring trimmedStr = fileContents.substr(0, padPos + 11); + std::wstring padding = L"\n"; + + int offset = (trimmedStr.length() + padding.length()) % 4; + // multiple X by the number in offset + std::wstring::size_type pos = 0u; + for (int posCount = 0; posCount < offset; posCount = posCount + 1) { + if ((pos = padding.find(L"X", pos)) != std::string::npos) { + padding.replace(pos, 1, L"XX"); + pos += executionLevel_.length(); + } + } + + // convert the wchar back into char, so that it encodes correctly for Windows to read the XML. + std::wstring stringSectionW = fileContents + padding; + std::wstring_convert, wchar_t> converter; + std::string stringSection = converter.to_bytes(stringSectionW); + + if (!UpdateResourceW(ru.Get(), RT_MANIFEST, MAKEINTRESOURCEW(1), + kLangEnUs, // this is hardcoded at 1033, ie, en-us, as that is what RT_MANIFEST default uses + &stringSection.at(0), sizeof(char) * stringSection.size())) { + return false; + } + } + + // update string table. + for (const auto& i : stringTableMap_) { + for (const auto& j : i.second) { + std::vector stringTableBuffer; + if (!SerializeStringTable(j.second, j.first, &stringTableBuffer)) { + return false; + } + + if (!UpdateResourceW(ru.Get(), RT_STRING, MAKEINTRESOURCEW(j.first + 1), i.first, + &stringTableBuffer[0], static_cast(stringTableBuffer.size()))) { + return false; + } + } + } + + for (const auto& rcDataLangPair : rcDataLngMap_) { + for (const auto& rcDataMap : rcDataLangPair.second) { + if (!UpdateResourceW(ru.Get(), RT_RCDATA, reinterpret_cast(rcDataMap.first), + rcDataLangPair.first, (LPVOID)rcDataMap.second.data(), rcDataMap.second.size())) { + return false; + } + } + } + + for (const auto& iLangIconInfoPair : iconBundleMap_) { + auto langId = iLangIconInfoPair.first; + auto maxIconId = iLangIconInfoPair.second.maxIconId; + for (const auto& iNameBundlePair : iLangIconInfoPair.second.iconBundles) { + UINT bundleId = iNameBundlePair.first; + const std::unique_ptr& pIcon = iNameBundlePair.second; + if (!pIcon) + continue; + + auto& icon = *pIcon; + // update icon. + if (icon.grpHeader.size() > 0) { + if (!UpdateResourceW(ru.Get(), RT_GROUP_ICON, MAKEINTRESOURCEW(bundleId), + langId, icon.grpHeader.data(), icon.grpHeader.size())) { + return false; + } + + for (size_t i = 0; i < icon.header.count; ++i) { + if (!UpdateResourceW(ru.Get(), RT_ICON, MAKEINTRESOURCEW(i + 1), + langId, icon.images[i].data(), icon.images[i].size())) { + + return false; + } + } + + for (size_t i = icon.header.count; i < maxIconId; ++i) { + if (!UpdateResourceW(ru.Get(), RT_ICON, MAKEINTRESOURCEW(i + 1), + langId, nullptr, 0)) { + return false; + } + } + } + } + } + + return ru.Commit(); +} + +bool ResourceUpdater::SerializeStringTable(const StringValues& values, UINT blockId, std::vector* out) +{ + // calc total size. + // string table is pascal string list. + size_t size = 0; + for (size_t i = 0; i < 16; i++) { + size += sizeof(WORD); + size += values[i].length() * sizeof(WCHAR); + } + + out->resize(size); + + // write. + char* pDst = &(*out)[0]; + for (size_t i = 0; i < 16; i++) { + WORD length = static_cast(values[i].length()); + memcpy(pDst, &length, sizeof(length)); + pDst += sizeof(WORD); + + if (length > 0) { + WORD bytes = length * sizeof(WCHAR); + memcpy(pDst, values[i].c_str(), bytes); + pDst += bytes; + } + } + + return true; +} + +// static +BOOL CALLBACK ResourceUpdater::OnEnumResourceLanguage(HANDLE hModule, LPCWSTR lpszType, LPCWSTR lpszName, WORD wIDLanguage, LONG_PTR lParam) +{ + ResourceUpdater* instance = reinterpret_cast(lParam); + if (IS_INTRESOURCE(lpszName) && IS_INTRESOURCE(lpszType)) { + // case reinterpret_cast(RT_VERSION): { + switch (reinterpret_cast(lpszType)) { + case 16: { + try { + instance->versionStampMap_[wIDLanguage] = VersionInfo(instance->module_, wIDLanguage); + } catch (const std::system_error& e) { + return false; + } + break; + } + case 6: { + // case reinterpret_cast(RT_STRING): { + UINT id = reinterpret_cast(lpszName) - 1; + auto& vector = instance->stringTableMap_[wIDLanguage][id]; + for (size_t k = 0; k < 16; k++) { + CStringW buf; + + buf.LoadStringW(instance->module_, id * 16 + k, wIDLanguage); + vector.push_back(buf.GetBuffer()); + } + break; + } + // case reinterpret_cast(RT_ICON): { + case 3: { + UINT iconId = reinterpret_cast(lpszName); + UINT maxIconId = instance->iconBundleMap_[wIDLanguage].maxIconId; + if (iconId > maxIconId) + maxIconId = iconId; + break; + } + // case reinterpret_cast(RT_GROUP_ICON): { + case 14: { + UINT iconId = reinterpret_cast(lpszName); + instance->iconBundleMap_[wIDLanguage].iconBundles[iconId] = nullptr; + break; + } + // case reinterpret_cast(RT_RCDATA): { + case 10: { + const auto moduleHandle = HMODULE(hModule); + HRSRC hResInfo = FindResource(moduleHandle, lpszName, lpszType); + DWORD cbResource = SizeofResource(moduleHandle, hResInfo); + HGLOBAL hResData = LoadResource(moduleHandle, hResInfo); + + const auto* pResource = (const BYTE*)LockResource(hResData); + const auto resId = reinterpret_cast(lpszName); + instance->rcDataLngMap_[wIDLanguage][resId] = std::vector(pResource, pResource + cbResource); + + UnlockResource(hResData); + FreeResource(hResData); + } + default: + break; + } + } + return TRUE; +} + +// static +BOOL CALLBACK ResourceUpdater::OnEnumResourceName(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam) +{ + EnumResourceLanguagesW(hModule, lpszType, lpszName, (ENUMRESLANGPROCW)OnEnumResourceLanguage, lParam); + return TRUE; +} + +// static +// courtesy of http://stackoverflow.com/questions/420852/reading-an-applications-manifest-file +BOOL CALLBACK ResourceUpdater::OnEnumResourceManifest(HMODULE hModule, LPCTSTR lpType, LPWSTR lpName, LONG_PTR lParam) +{ + ResourceUpdater* instance = reinterpret_cast(lParam); + HRSRC hResInfo = FindResource(hModule, lpName, lpType); + DWORD cbResource = SizeofResource(hModule, hResInfo); + + HGLOBAL hResData = LoadResource(hModule, hResInfo); + const BYTE* pResource = (const BYTE*)LockResource(hResData); + + // FIXME(zcbenz): Do a real UTF string convertion. + int len = strlen(reinterpret_cast(pResource)); + std::wstring manifestStringLocal(pResource, pResource + len); + + // FIXME(zcbenz): Strip the BOM instead of doing string search. + size_t start = manifestStringLocal.find(L" 0) { + manifestStringLocal = manifestStringLocal.substr(start); + } + + // Support alternative formatting, such as using " vs ' and level="..." on another line + size_t found = manifestStringLocal.find(L"requestedExecutionLevel"); + size_t level = manifestStringLocal.find(L"level=\"", found); + size_t end = manifestStringLocal.find(L"\"", level + 7); + if (level < 0) { + level = manifestStringLocal.find(L"level=\'", found); + end = manifestStringLocal.find(L"\'", level + 7); + } + + instance->originalExecutionLevel_ = manifestStringLocal.substr(level + 7, end - level - 7); + + // also store original manifestString + instance->manifestString_ = manifestStringLocal; + + UnlockResource(hResData); + FreeResource(hResData); + + return TRUE; // Keep going +} + +ScopedResourceUpdater::ScopedResourceUpdater(const WCHAR* filename, bool deleteOld) + : handle_(BeginUpdateResourceW(filename, deleteOld)) +{ +} + +ScopedResourceUpdater::~ScopedResourceUpdater() +{ + if (!commited_) { + EndUpdate(false); + } +} + +HANDLE ScopedResourceUpdater::Get() const +{ + return handle_; +} + +bool ScopedResourceUpdater::Commit() +{ + commited_ = true; + return EndUpdate(true); +} + +bool ScopedResourceUpdater::EndUpdate(bool doesCommit) +{ + BOOL fDiscard = doesCommit ? FALSE : TRUE; + BOOL bResult = EndUpdateResourceW(handle_, fDiscard); + DWORD e = GetLastError(); + return bResult ? true : false; +} + +} // namespace rescle diff --git a/src/bun.js/bindings/windows/rescle.h b/src/bun.js/bindings/windows/rescle.h new file mode 100644 index 0000000000..417de1dc0d --- /dev/null +++ b/src/bun.js/bindings/windows/rescle.h @@ -0,0 +1,211 @@ +// This file is from Electron's fork of rescle +// https://github.com/electron/rcedit/blob/e36b688b42df0e236922019ce14e0ea165dc176d/src/rescle.h +// 'bun build --compile' uses this on Windows to allow +// patching the icon of the generated executable. +// +// Copyright (c) 2013 GitHub Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// Use of this source code is governed by MIT license that can be found in the +// LICENSE file. +// +// This file is modified from Rescle written by yoshio.okumura@gmail.com: +// http://code.google.com/p/rescle/ + +#ifndef VERSION_INFO_UPDATER +#define VERSION_INFO_UPDATER + +#ifndef _UNICODE +#define _UNICODE +#endif + +#ifndef UNICODE +#define UNICODE +#endif + +#include +#include +#include + +#include +#include // unique_ptr + +#define RU_VS_COMMENTS L"Comments" +#define RU_VS_COMPANY_NAME L"CompanyName" +#define RU_VS_FILE_DESCRIPTION L"FileDescription" +#define RU_VS_FILE_VERSION L"FileVersion" +#define RU_VS_INTERNAL_NAME L"InternalName" +#define RU_VS_LEGAL_COPYRIGHT L"LegalCopyright" +#define RU_VS_LEGAL_TRADEMARKS L"LegalTrademarks" +#define RU_VS_ORIGINAL_FILENAME L"OriginalFilename" +#define RU_VS_PRIVATE_BUILD L"PrivateBuild" +#define RU_VS_PRODUCT_NAME L"ProductName" +#define RU_VS_PRODUCT_VERSION L"ProductVersion" +#define RU_VS_SPECIAL_BUILD L"SpecialBuild" + +namespace rescle { + +struct IconsValue { + typedef struct _ICONENTRY { + BYTE width; + BYTE height; + BYTE colorCount; + BYTE reserved; + WORD planes; + WORD bitCount; + DWORD bytesInRes; + DWORD imageOffset; + } ICONENTRY; + + typedef struct _ICONHEADER { + WORD reserved; + WORD type; + WORD count; + std::vector entries; + } ICONHEADER; + + ICONHEADER header; + std::vector> images; + std::vector grpHeader; +}; + +struct Translate { + LANGID wLanguage; + WORD wCodePage; +}; + +typedef std::pair VersionString; +typedef std::pair OffsetLengthPair; + +struct VersionStringTable { + Translate encoding; + std::vector strings; +}; + +class VersionInfo { +public: + VersionInfo(); + VersionInfo(HMODULE hModule, WORD languageId); + + std::vector Serialize() const; + + bool HasFixedFileInfo() const; + VS_FIXEDFILEINFO& GetFixedFileInfo(); + const VS_FIXEDFILEINFO& GetFixedFileInfo() const; + void SetFixedFileInfo(const VS_FIXEDFILEINFO& value); + + std::vector stringTables; + std::vector supportedTranslations; + +private: + VS_FIXEDFILEINFO fixedFileInfo_; + + void FillDefaultData(); + void DeserializeVersionInfo(const BYTE* pData, size_t size); + + VersionStringTable DeserializeVersionStringTable(const BYTE* tableData); + void DeserializeVersionStringFileInfo(const BYTE* offset, size_t length, std::vector& stringTables); + void DeserializeVarFileInfo(const unsigned char* offset, std::vector& translations); + OffsetLengthPair GetChildrenData(const BYTE* entryData); +}; + +class ResourceUpdater { +public: + typedef std::vector StringValues; + typedef std::map StringTable; + typedef std::map StringTableMap; + typedef std::map VersionStampMap; + typedef std::map> IconTable; + typedef std::vector RcDataValue; + typedef std::map RcDataMap; + typedef std::map RcDataLangMap; + + struct IconResInfo { + UINT maxIconId = 0; + IconTable iconBundles; + }; + + typedef std::map IconTableMap; + + ResourceUpdater(); + ~ResourceUpdater(); + + bool Load(const WCHAR* filename); + bool SetVersionString(WORD languageId, const WCHAR* name, const WCHAR* value); + bool SetVersionString(const WCHAR* name, const WCHAR* value); + const WCHAR* GetVersionString(WORD languageId, const WCHAR* name); + const WCHAR* GetVersionString(const WCHAR* name); + bool SetProductVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4); + bool SetProductVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4); + bool SetFileVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4); + bool SetFileVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4); + bool ChangeString(WORD languageId, UINT id, const WCHAR* value); + bool ChangeString(UINT id, const WCHAR* value); + bool ChangeRcData(UINT id, const WCHAR* pathToResource); + const WCHAR* GetString(WORD languageId, UINT id); + const WCHAR* GetString(UINT id); + bool SetIcon(const WCHAR* path, const LANGID& langId, UINT iconBundle); + bool SetIcon(const WCHAR* path, const LANGID& langId); + bool SetIcon(const WCHAR* path); + bool SetExecutionLevel(const WCHAR* value); + bool IsExecutionLevelSet(); + bool SetApplicationManifest(const WCHAR* value); + bool IsApplicationManifestSet(); + bool Commit(); + +private: + bool SerializeStringTable(const StringValues& values, UINT blockId, std::vector* out); + + static BOOL CALLBACK OnEnumResourceName(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam); + static BOOL CALLBACK OnEnumResourceManifest(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam); + static BOOL CALLBACK OnEnumResourceLanguage(HANDLE hModule, LPCWSTR lpszType, LPCWSTR lpszName, WORD wIDLanguage, LONG_PTR lParam); + + HMODULE module_; + std::wstring filename_; + std::wstring executionLevel_; + std::wstring originalExecutionLevel_; + std::wstring applicationManifestPath_; + std::wstring manifestString_; + VersionStampMap versionStampMap_; + StringTableMap stringTableMap_; + IconTableMap iconBundleMap_; + RcDataLangMap rcDataLngMap_; +}; + +class ScopedResourceUpdater { +public: + ScopedResourceUpdater(const WCHAR* filename, bool deleteOld); + ~ScopedResourceUpdater(); + + HANDLE Get() const; + bool Commit(); + +private: + bool EndUpdate(bool doesCommit); + + HANDLE handle_; + bool commited_ = false; +}; + +} // namespace rescle + +#endif // VERSION_INFO_UPDATER diff --git a/src/bun.js/bindings/workaround-missing-symbols.cpp b/src/bun.js/bindings/workaround-missing-symbols.cpp index b1fcc12a63..2ca4eaf351 100644 --- a/src/bun.js/bindings/workaround-missing-symbols.cpp +++ b/src/bun.js/bindings/workaround-missing-symbols.cpp @@ -1,5 +1,3 @@ - - #if defined(WIN32) #include @@ -66,6 +64,11 @@ extern "C" int kill(int pid, int sig) #include #include #include +#include +#include +#include +#include +#include #ifndef _STAT_VER #if defined(__aarch64__) @@ -78,38 +81,20 @@ extern "C" int kill(int pid, int sig) #endif #if defined(__x86_64__) -__asm__(".symver cosf,cosf@GLIBC_2.2.5"); __asm__(".symver exp,exp@GLIBC_2.2.5"); __asm__(".symver expf,expf@GLIBC_2.2.5"); -__asm__(".symver fcntl,fcntl@GLIBC_2.2.5"); -__asm__(".symver fmod,fmod@GLIBC_2.2.5"); -__asm__(".symver fmodf,fmodf@GLIBC_2.2.5"); -__asm__(".symver log,log@GLIBC_2.2.5"); -__asm__(".symver log10f,log10f@GLIBC_2.2.5"); -__asm__(".symver log2,log2@GLIBC_2.2.5"); __asm__(".symver log2f,log2f@GLIBC_2.2.5"); __asm__(".symver logf,logf@GLIBC_2.2.5"); -__asm__(".symver pow,pow@GLIBC_2.2.5"); __asm__(".symver powf,powf@GLIBC_2.2.5"); -__asm__(".symver sincosf,sincosf@GLIBC_2.2.5"); -__asm__(".symver sinf,sinf@GLIBC_2.2.5"); -__asm__(".symver tanf,tanf@GLIBC_2.2.5"); #elif defined(__aarch64__) -__asm__(".symver cosf,cosf@GLIBC_2.17"); -__asm__(".symver exp,exp@GLIBC_2.17"); __asm__(".symver expf,expf@GLIBC_2.17"); -__asm__(".symver fmod,fmod@GLIBC_2.17"); -__asm__(".symver fmodf,fmodf@GLIBC_2.17"); +__asm__(".symver exp,exp@GLIBC_2.17"); __asm__(".symver log,log@GLIBC_2.17"); -__asm__(".symver log10f,log10f@GLIBC_2.17"); __asm__(".symver log2,log2@GLIBC_2.17"); __asm__(".symver log2f,log2f@GLIBC_2.17"); __asm__(".symver logf,logf@GLIBC_2.17"); __asm__(".symver pow,pow@GLIBC_2.17"); __asm__(".symver powf,powf@GLIBC_2.17"); -__asm__(".symver sincosf,sincosf@GLIBC_2.17"); -__asm__(".symver sinf,sinf@GLIBC_2.17"); -__asm__(".symver tanf,tanf@GLIBC_2.17"); #endif #if defined(__x86_64__) || defined(__aarch64__) @@ -119,36 +104,43 @@ __asm__(".symver tanf,tanf@GLIBC_2.17"); #endif extern "C" { + double BUN_WRAP_GLIBC_SYMBOL(exp)(double); -double BUN_WRAP_GLIBC_SYMBOL(fmod)(double, double); -double BUN_WRAP_GLIBC_SYMBOL(log)(double); -double BUN_WRAP_GLIBC_SYMBOL(log2)(double); -double BUN_WRAP_GLIBC_SYMBOL(pow)(double, double); -float BUN_WRAP_GLIBC_SYMBOL(cosf)(float); float BUN_WRAP_GLIBC_SYMBOL(expf)(float); -float BUN_WRAP_GLIBC_SYMBOL(fmodf)(float, float); -float BUN_WRAP_GLIBC_SYMBOL(log10f)(float); float BUN_WRAP_GLIBC_SYMBOL(log2f)(float); float BUN_WRAP_GLIBC_SYMBOL(logf)(float); -float BUN_WRAP_GLIBC_SYMBOL(sinf)(float); -float BUN_WRAP_GLIBC_SYMBOL(tanf)(float); -int BUN_WRAP_GLIBC_SYMBOL(fcntl)(int, int, ...); -int BUN_WRAP_GLIBC_SYMBOL(fcntl64)(int, int, ...); -void BUN_WRAP_GLIBC_SYMBOL(sincosf)(float, float*, float*); -} +float BUN_WRAP_GLIBC_SYMBOL(powf)(float, float); -extern "C" { +#if defined(__aarch64__) + +double BUN_WRAP_GLIBC_SYMBOL(pow)(double, double); +double BUN_WRAP_GLIBC_SYMBOL(log)(double); +double BUN_WRAP_GLIBC_SYMBOL(log2)(double); +int BUN_WRAP_GLIBC_SYMBOL(fcntl64)(int, int, ...); + +#endif #if defined(__x86_64__) || defined(__aarch64__) -int __wrap_fcntl(int fd, int cmd, ...) -{ - va_list args; - va_start(args, cmd); - void* arg = va_arg(args, void*); - va_end(args); - return fcntl(fd, cmd, arg); -} +float __wrap_expf(float x) { return expf(x); } +float __wrap_powf(float x, float y) { return powf(x, y); } +float __wrap_logf(float x) { return logf(x); } +float __wrap_log2f(float x) { return log2f(x); } +double __wrap_exp(double x) { return exp(x); } + +#if defined(__aarch64__) + +double __wrap_pow(double x, double y) { return pow(x, y); } +double __wrap_log(double x) { return log(x); } +double __wrap_log2(double x) { return log2(x); } + +#endif + +#endif // x86_64 or aarch64 + +} // extern "C" + +#if defined(__aarch64__) typedef int (*fcntl64_func)(int fd, int cmd, ...); @@ -242,104 +234,8 @@ extern "C" int __wrap_fcntl64(int fd, int cmd, ...) #endif -#if defined(__x86_64__) - -#ifndef _MKNOD_VER -#define _MKNOD_VER 1 -#endif - -extern "C" int __lxstat(int ver, const char* filename, struct stat* stat); -extern "C" int __wrap_lstat(const char* filename, struct stat* stat) -{ - return __lxstat(_STAT_VER, filename, stat); -} - -extern "C" int __xstat(int ver, const char* filename, struct stat* stat); -extern "C" int __wrap_stat(const char* filename, struct stat* stat) -{ - return __xstat(_STAT_VER, filename, stat); -} - -extern "C" int __fxstat(int ver, int fd, struct stat* stat); -extern "C" int __wrap_fstat(int fd, struct stat* stat) -{ - return __fxstat(_STAT_VER, fd, stat); -} - -extern "C" int __fxstatat(int ver, int dirfd, const char* path, struct stat* stat, int flags); -extern "C" int __wrap_fstatat(int dirfd, const char* path, struct stat* stat, int flags) -{ - return __fxstatat(_STAT_VER, dirfd, path, stat, flags); -} - -extern "C" int __lxstat64(int ver, const char* filename, struct stat64* stat); -extern "C" int __wrap_lstat64(const char* filename, struct stat64* stat) -{ - return __lxstat64(_STAT_VER, filename, stat); -} - -extern "C" int __xstat64(int ver, const char* filename, struct stat64* stat); -extern "C" int __wrap_stat64(const char* filename, struct stat64* stat) -{ - return __xstat64(_STAT_VER, filename, stat); -} - -extern "C" int __fxstat64(int ver, int fd, struct stat64* stat); -extern "C" int __wrap_fstat64(int fd, struct stat64* stat) -{ - return __fxstat64(_STAT_VER, fd, stat); -} - -extern "C" int __fxstatat64(int ver, int dirfd, const char* path, struct stat64* stat, int flags); -extern "C" int __wrap_fstatat64(int dirfd, const char* path, struct stat64* stat, int flags) -{ - return __fxstatat64(_STAT_VER, dirfd, path, stat, flags); -} - -extern "C" int __xmknod(int ver, const char* path, mode_t mode, dev_t dev); -extern "C" int __wrap_mknod(const char* path, mode_t mode, dev_t dev) -{ - return __xmknod(_MKNOD_VER, path, mode, dev); -} - -extern "C" int __xmknodat(int ver, int dirfd, const char* path, mode_t mode, dev_t dev); -extern "C" int __wrap_mknodat(int dirfd, const char* path, mode_t mode, dev_t dev) -{ - return __xmknodat(_MKNOD_VER, dirfd, path, mode, dev); -} - -#endif - -double __wrap_exp(double x) -{ - return exp(x); -} -double __wrap_fmod(double x, double y) { return fmod(x, y); } -double __wrap_log(double x) { return log(x); } -double __wrap_log2(double x) { return log2(x); } -double __wrap_pow(double x, double y) { return pow(x, y); } -float __wrap_powf(float x, float y) { return powf(x, y); } -float __wrap_cosf(float x) { return cosf(x); } -float __wrap_expf(float x) { return expf(x); } -float __wrap_fmodf(float x, float y) { return fmodf(x, y); } -float __wrap_log10f(float x) { return log10f(x); } -float __wrap_log2f(float x) { return log2f(x); } -float __wrap_logf(float x) { return logf(x); } -float __wrap_sinf(float x) { return sinf(x); } -float __wrap_tanf(float x) { return tanf(x); } -void __wrap_sincosf(float x, float* sin_x, float* cos_x) { sincosf(x, sin_x, cos_x); } -} - -// ban statx, for now -extern "C" int __wrap_statx(int fd, const char* path, int flags, - unsigned int mask, struct statx* buf) -{ - errno = ENOSYS; -#ifdef BUN_DEBUG - abort(); -#endif - return -1; -} +extern "C" __attribute__((used)) char _libc_single_threaded = 0; +extern "C" __attribute__((used)) char __libc_single_threaded = 0; #endif // glibc diff --git a/src/bun.js/bindings/wtf-bindings.cpp b/src/bun.js/bindings/wtf-bindings.cpp index 848e7d143d..d33725bbae 100644 --- a/src/bun.js/bindings/wtf-bindings.cpp +++ b/src/bun.js/bindings/wtf-bindings.cpp @@ -1,6 +1,7 @@ #include "root.h" #include "wtf-bindings.h" - +#include +#include #include #include #include @@ -195,13 +196,10 @@ String base64URLEncodeToString(Vector data) if (!encodedLength) return String(); - LChar* ptr; + std::span ptr; auto result = String::createUninitialized(encodedLength, ptr); - if (UNLIKELY(!ptr)) { - RELEASE_ASSERT_NOT_REACHED(); - return String(); - } - encodedLength = WTF__base64URLEncode(reinterpret_cast(data.data()), data.size(), reinterpret_cast(ptr), encodedLength); + + encodedLength = WTF__base64URLEncode(reinterpret_cast(data.data()), data.size(), reinterpret_cast(ptr.data()), encodedLength); if (result.length() != encodedLength) { return result.substringSharingImpl(0, encodedLength); } @@ -240,4 +238,16 @@ size_t toISOString(JSC::VM& vm, double date, char in[64]) return charactersWritten; } +static thread_local WTF::StackBounds stackBoundsForCurrentThread = WTF::StackBounds::emptyBounds(); + +extern "C" void Bun__StackCheck__initialize() +{ + stackBoundsForCurrentThread = WTF::StackBounds::currentThreadStackBounds(); +} + +extern "C" void* Bun__StackCheck__getMaxStack() +{ + return stackBoundsForCurrentThread.end(); +} + } diff --git a/src/bun.js/config.zig b/src/bun.js/config.zig index d82c3467d2..c8fbeddbd4 100644 --- a/src/bun.js/config.zig +++ b/src/bun.js/config.zig @@ -16,7 +16,7 @@ const ast = @import("../import_record.zig"); const logger = bun.logger; const Api = @import("../api/schema.zig").Api; const options = @import("../options.zig"); -const Bundler = bun.bundler.ServeBundler; +const Transpiler = bun.transpiler.ServeBundler; const js_printer = bun.js_printer; pub const DefaultBunDefines = struct { diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index c80fe890ea..48e23a010b 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -3,7 +3,7 @@ const JSC = bun.JSC; const JSGlobalObject = JSC.JSGlobalObject; const VirtualMachine = JSC.VirtualMachine; const Allocator = std.mem.Allocator; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const bun = @import("root").bun; const Environment = bun.Environment; const Fetch = JSC.WebCore.Fetch; @@ -18,6 +18,10 @@ const ReadFileTask = WebCore.Blob.ReadFile.ReadFileTask; const WriteFileTask = WebCore.Blob.WriteFile.WriteFileTask; const napi_async_work = JSC.napi.napi_async_work; const FetchTasklet = Fetch.FetchTasklet; +const S3 = bun.S3; +const S3HttpSimpleTask = S3.S3HttpSimpleTask; +const S3HttpDownloadStreamingTask = S3.S3HttpDownloadStreamingTask; + const JSValue = JSC.JSValue; const js = JSC.C; const Waker = bun.Async.Waker; @@ -368,6 +372,7 @@ const Link = JSC.Node.Async.link; const Symlink = JSC.Node.Async.symlink; const Readlink = JSC.Node.Async.readlink; const Realpath = JSC.Node.Async.realpath; +const RealpathNonNative = JSC.Node.Async.realpathNonNative; const Mkdir = JSC.Node.Async.mkdir; const Fsync = JSC.Node.Async.fsync; const Rename = JSC.Node.Async.rename; @@ -407,6 +412,9 @@ const ServerAllConnectionsClosedTask = @import("./api/server.zig").ServerAllConn // Task.get(ReadFileTask) -> ?ReadFileTask pub const Task = TaggedPointerUnion(.{ FetchTasklet, + S3HttpSimpleTask, + S3HttpDownloadStreamingTask, + PosixSignalTask, AsyncGlobWalkTask, AsyncTransformTask, ReadFileTask, @@ -450,6 +458,7 @@ pub const Task = TaggedPointerUnion(.{ Symlink, Readlink, Realpath, + RealpathNonNative, Mkdir, Fsync, Fdatasync, @@ -555,7 +564,7 @@ pub const GarbageCollectionController = struct { } var gc_timer_interval: i32 = 1000; - if (vm.bundler.env.get("BUN_GC_TIMER_INTERVAL")) |timer| { + if (vm.transpiler.env.get("BUN_GC_TIMER_INTERVAL")) |timer| { if (std.fmt.parseInt(i32, timer, 10)) |parsed| { if (parsed > 0) { gc_timer_interval = parsed; @@ -564,7 +573,7 @@ pub const GarbageCollectionController = struct { } this.gc_timer_interval = gc_timer_interval; - this.disabled = vm.bundler.env.has("BUN_GC_TIMER_DISABLE"); + this.disabled = vm.transpiler.env.has("BUN_GC_TIMER_DISABLE"); if (!this.disabled) this.gc_repeating_timer.set(this, onGCRepeatingTimer, gc_timer_interval, gc_timer_interval); @@ -776,12 +785,26 @@ pub const EventLoop = struct { waker: ?Waker = null, forever_timer: ?*uws.Timer = null, deferred_tasks: DeferredTaskQueue = .{}, - uws_loop: if (Environment.isWindows) ?*uws.Loop else void = if (Environment.isWindows) null else {}, + uws_loop: if (Environment.isWindows) ?*uws.Loop else void = if (Environment.isWindows) null, debug: Debug = .{}, entered_event_loop_count: isize = 0, concurrent_ref: std.atomic.Value(i32) = std.atomic.Value(i32).init(0), + signal_handler: if (Environment.isPosix) ?*PosixSignalHandle else void = if (Environment.isPosix) null, + + pub export fn Bun__ensureSignalHandler() void { + if (Environment.isPosix) { + if (JSC.VirtualMachine.getMainThreadVM()) |vm| { + const this = vm.eventLoop(); + if (this.signal_handler == null) { + this.signal_handler = PosixSignalHandle.new(.{}); + @memset(&this.signal_handler.?.signals, 0); + } + } + } + } + pub const Debug = if (Environment.isDebug) struct { is_inside_tick_queue: bool = false, js_call_count_outside_tick_queue: usize = 0, @@ -991,6 +1014,15 @@ pub const EventLoop = struct { var fetch_task: *Fetch.FetchTasklet = task.get(Fetch.FetchTasklet).?; fetch_task.onProgressUpdate(); }, + .S3HttpSimpleTask => { + var s3_task: *S3HttpSimpleTask = task.get(S3HttpSimpleTask).?; + s3_task.onResponse(); + }, + .S3HttpDownloadStreamingTask => { + var s3_task: *S3HttpDownloadStreamingTask = task.get(S3HttpDownloadStreamingTask).?; + s3_task.onResponse(); + }, + @field(Task.Tag, @typeName(AsyncGlobWalkTask)) => { var globWalkTask: *AsyncGlobWalkTask = task.get(AsyncGlobWalkTask).?; globWalkTask.*.runFromJS(); @@ -1183,6 +1215,10 @@ pub const EventLoop = struct { var any: *Realpath = task.get(Realpath).?; any.runFromJSThread(); }, + @field(Task.Tag, typeBaseName(@typeName(RealpathNonNative))) => { + var any: *RealpathNonNative = task.get(RealpathNonNative).?; + any.runFromJSThread(); + }, @field(Task.Tag, typeBaseName(@typeName(Mkdir))) => { var any: *Mkdir = task.get(Mkdir).?; any.runFromJSThread(); @@ -1256,6 +1292,9 @@ pub const EventLoop = struct { var any: *bun.bundle_v2.DeferredBatchTask = task.get(bun.bundle_v2.DeferredBatchTask).?; any.runOnJSThread(); }, + @field(Task.Tag, typeBaseName(@typeName(PosixSignalTask))) => { + PosixSignalTask.runFromJSThread(@intCast(task.asUintptr()), global); + }, else => { bun.Output.panic("Unexpected tag: {s}", .{@tagName(task.tag())}); @@ -1310,6 +1349,12 @@ pub const EventLoop = struct { pub fn tickConcurrentWithCount(this: *EventLoop) usize { this.updateCounts(); + if (comptime Environment.isPosix) { + if (this.signal_handler) |signal_handler| { + signal_handler.drain(this); + } + } + var concurrent = this.concurrent_tasks.popBatch(); const count = concurrent.count; if (count == 0) @@ -1381,7 +1426,7 @@ pub const EventLoop = struct { if (loop.isActive()) { this.processGCTimer(); - var event_loop_sleep_timer = if (comptime Environment.isDebug) std.time.Timer.start() catch unreachable else {}; + var event_loop_sleep_timer = if (comptime Environment.isDebug) std.time.Timer.start() catch unreachable; // for the printer, this is defined: var timespec: bun.timespec = if (Environment.isDebug) .{ .sec = 0, .nsec = 0 } else undefined; loop.tickWithTimeout(if (ctx.timer.getTimeout(×pec)) ×pec else null); @@ -1438,6 +1483,7 @@ pub const EventLoop = struct { } } + this.processGCTimer(); this.processGCTimer(); loop.tick(); @@ -2026,6 +2072,13 @@ pub const AnyEventLoop = union(enum) { pub const Task = AnyTaskWithExtraContext; + pub fn iterationNumber(this: *const AnyEventLoop) u64 { + return switch (this.*) { + .js => this.js.usocketsLoop().iterationNumber(), + .mini => this.mini.loop.iterationNumber(), + }; + } + pub fn wakeup(this: *AnyEventLoop) void { this.loop().wakeup(); } @@ -2241,7 +2294,7 @@ pub const EventLoopHandle = union(enum) { pub inline fn createNullDelimitedEnvMap(this: @This(), alloc: Allocator) ![:null]?[*:0]u8 { return switch (this) { - .js => this.js.virtual_machine.bundler.env.map.createNullDelimitedEnvMap(alloc), + .js => this.js.virtual_machine.transpiler.env.map.createNullDelimitedEnvMap(alloc), .mini => this.mini.env.?.map.createNullDelimitedEnvMap(alloc), }; } @@ -2255,14 +2308,14 @@ pub const EventLoopHandle = union(enum) { pub inline fn topLevelDir(this: EventLoopHandle) []const u8 { return switch (this) { - .js => this.js.virtual_machine.bundler.fs.top_level_dir, + .js => this.js.virtual_machine.transpiler.fs.top_level_dir, .mini => this.mini.top_level_dir, }; } pub inline fn env(this: EventLoopHandle) *bun.DotEnv.Loader { return switch (this) { - .js => this.js.virtual_machine.bundler.env, + .js => this.js.virtual_machine.transpiler.env, .mini => this.mini.env.?, }; } @@ -2292,3 +2345,99 @@ pub const EventLoopTaskPtr = union { js: *ConcurrentTask, mini: *JSC.AnyTaskWithExtraContext, }; + +pub const PosixSignalHandle = struct { + const buffer_size = 8192; + + signals: [buffer_size]u8 = undefined, + + // Producer index (signal handler writes). + tail: std.atomic.Value(u16) = std.atomic.Value(u16).init(0), + // Consumer index (main thread reads). + head: std.atomic.Value(u16) = std.atomic.Value(u16).init(0), + + const log = bun.Output.scoped(.PosixSignalHandle, true); + + pub usingnamespace bun.New(@This()); + + /// Called by the signal handler (single producer). + /// Returns `true` if enqueued successfully, or `false` if the ring is full. + pub fn enqueue(this: *PosixSignalHandle, signal: u8) bool { + // Read the current tail and head (Acquire to ensure we have up‐to‐date values). + const old_tail = this.tail.load(.acquire); + const head_val = this.head.load(.acquire); + + // Compute the next tail (wrapping around buffer_size). + const next_tail = (old_tail +% 1) % buffer_size; + + // Check if the ring is full. + if (next_tail == (head_val % buffer_size)) { + // The ring buffer is full. + // We cannot block or wait here (since we're in a signal handler). + // So we just drop the signal or log if desired. + log("signal queue is full; dropping", .{}); + return false; + } + + // Store the signal into the ring buffer slot (Release to ensure data is visible). + @atomicStore(u8, &this.signals[old_tail % buffer_size], signal, .release); + + // Publish the new tail (Release so that the consumer sees the updated tail). + this.tail.store(old_tail +% 1, .release); + + JSC.VirtualMachine.getMainThreadVM().?.eventLoop().wakeup(); + + return true; + } + + /// This is the signal handler entry point. Calls enqueue on the ring buffer. + /// Note: Must be minimal logic here. Only do atomics & signal‐safe calls. + export fn Bun__onPosixSignal(number: i32) void { + const vm = JSC.VirtualMachine.getMainThreadVM().?; + _ = vm.eventLoop().signal_handler.?.enqueue(@intCast(number)); + } + + /// Called by the main thread (single consumer). + /// Returns `null` if the ring is empty, or the next signal otherwise. + pub fn dequeue(this: *PosixSignalHandle) ?u8 { + // Read the current head and tail. + const old_head = this.head.load(.acquire); + const tail_val = this.tail.load(.acquire); + + // If head == tail, the ring is empty. + if (old_head == tail_val) { + return null; // No available items + } + + const slot_index = old_head % buffer_size; + // Acquire load of the stored signal to get the item. + const signal = @atomicRmw(u8, &this.signals[slot_index], .Xchg, 0, .acq_rel); + + // Publish the updated head (Release). + this.head.store(old_head +% 1, .release); + + return signal; + } + + /// Drain as many signals as possible and enqueue them as tasks in the event loop. + /// Called by the main thread. + pub fn drain(this: *PosixSignalHandle, event_loop: *JSC.EventLoop) void { + while (this.dequeue()) |signal| { + // Example: wrap the signal into a Task structure + var posix_signal_task: PosixSignalTask = undefined; + var task = JSC.Task.init(&posix_signal_task); + task.setUintptr(signal); + event_loop.enqueueTask(task); + } + } +}; + +pub const PosixSignalTask = struct { + number: u8, + extern "C" fn Bun__onSignalForJS(number: i32, globalObject: *JSC.JSGlobalObject) void; + + pub usingnamespace bun.New(@This()); + pub fn runFromJSThread(number: u8, globalObject: *JSC.JSGlobalObject) void { + Bun__onSignalForJS(number, globalObject); + } +}; diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index 3717a9a90a..83e93bd133 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -194,21 +194,19 @@ const json = struct { // 1 is regular // 2 is internal - pub fn decodeIPCMessage( - data: []const u8, - globalThis: *JSC.JSGlobalObject, - ) IPCDecodeError!DecodeIPCMessageResult { + pub fn decodeIPCMessage(data: []const u8, globalThis: *JSC.JSGlobalObject) IPCDecodeError!DecodeIPCMessageResult { if (bun.strings.indexOfChar(data, '\n')) |idx| { var kind = data[0]; - var json_data = data[1..idx]; + var json_data = data[0..idx]; switch (kind) { - 1, 2 => {}, + 2 => { + json_data = data[1..idx]; + }, else => { - // if the message being recieved is from a node process then it wont have the leading marker byte - // assume full message will be json + // assume it's valid json with no header + // any error will be thrown by toJSByParseJSON below kind = 1; - json_data = data[0..idx]; }, } @@ -230,6 +228,7 @@ const json = struct { } const deserialized = str.toJSByParseJSON(globalThis); + if (deserialized == .zero) return error.InvalidFormat; return switch (kind) { 1 => .{ @@ -259,13 +258,12 @@ const json = struct { const slice = str.slice(); - try writer.ensureUnusedCapacity(1 + slice.len + 1); + try writer.ensureUnusedCapacity(slice.len + 1); - writer.writeAssumeCapacity(&.{1}); writer.writeAssumeCapacity(slice); writer.writeAssumeCapacity("\n"); - return 1 + slice.len + 1; + return slice.len + 1; } pub fn serializeInternal(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { @@ -341,7 +339,7 @@ const SocketIPCData = struct { const bytes = getVersionPacket(this.mode); if (bytes.len > 0) { const n = this.socket.write(bytes, false); - if (n != bytes.len) { + if (n >= 0 and n < @as(i32, @intCast(bytes.len))) { this.outgoing.write(bytes[@intCast(n)..]) catch bun.outOfMemory(); } } @@ -860,7 +858,7 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { // copy the remaining bytes to the start of the buffer bun.copy(u8, ipc.incoming.ptr[0..slice.len], slice); ipc.incoming.len = @truncate(slice.len); - log("hit NotEnoughBytes2", .{}); + log("hit NotEnoughBytes3", .{}); return; }, error.InvalidFormat => { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 7fc7ce4903..45f1034db6 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -12,22 +12,23 @@ const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const StoredFileDescriptorType = bun.StoredFileDescriptorType; const ErrorableString = bun.JSC.ErrorableString; -const Arena = @import("../mimalloc_arena.zig").Arena; +const Arena = @import("../allocators/mimalloc_arena.zig").Arena; const C = bun.C; +const Exception = bun.JSC.Exception; const Allocator = std.mem.Allocator; const IdentityContext = @import("../identity_context.zig").IdentityContext; const Fs = @import("../fs.zig"); const Resolver = @import("../resolver/resolver.zig"); const ast = @import("../import_record.zig"); -const MacroEntryPoint = bun.bundler.MacroEntryPoint; -const ParseResult = bun.bundler.ParseResult; +const MacroEntryPoint = bun.transpiler.MacroEntryPoint; +const ParseResult = bun.transpiler.ParseResult; const logger = bun.logger; const Api = @import("../api/schema.zig").Api; const options = @import("../options.zig"); -const Bundler = bun.Bundler; -const PluginRunner = bun.bundler.PluginRunner; -const ServerEntryPoint = bun.bundler.ServerEntryPoint; +const Transpiler = bun.Transpiler; +const PluginRunner = bun.transpiler.PluginRunner; +const ServerEntryPoint = bun.transpiler.ServerEntryPoint; const js_printer = bun.js_printer; const js_parser = bun.js_parser; const js_ast = bun.JSAst; @@ -70,7 +71,6 @@ const JSPromise = bun.JSC.JSPromise; const JSInternalPromise = bun.JSC.JSInternalPromise; const JSModuleLoader = bun.JSC.JSModuleLoader; const JSPromiseRejectionOperation = bun.JSC.JSPromiseRejectionOperation; -const Exception = bun.JSC.Exception; const ErrorableZigString = bun.JSC.ErrorableZigString; const ZigGlobalObject = bun.JSC.ZigGlobalObject; const VM = bun.JSC.VM; @@ -83,6 +83,7 @@ const PendingResolution = @import("../resolver/resolver.zig").PendingResolution; const ThreadSafeFunction = JSC.napi.ThreadSafeFunction; const PackageManager = @import("../install/install.zig").PackageManager; const IPC = @import("ipc.zig"); +const DNSResolver = @import("api/bun/dns_resolver.zig").DNSResolver; pub const GenericWatcher = @import("../watcher.zig"); const ModuleLoader = JSC.ModuleLoader; @@ -92,7 +93,7 @@ const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; const Task = JSC.Task; pub const Buffer = MarkedArrayBuffer; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const BuildMessage = JSC.BuildMessage; const ResolveMessage = JSC.ResolveMessage; const Async = bun.Async; @@ -123,7 +124,7 @@ const uv = bun.windows.libuv; pub const SavedSourceMap = struct { /// This is a pointer to the map located on the VirtualMachine struct map: *HashTable, - mutex: bun.Lock = .{}, + mutex: bun.Mutex = .{}, pub const vlq_offset = 24; @@ -567,7 +568,7 @@ pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSG .global_object = globalObject, .allocator = jsc_vm.allocator, }; - jsc_vm.bundler.linker.plugin_runner = &jsc_vm.plugin_runner.?; + jsc_vm.transpiler.linker.plugin_runner = &jsc_vm.plugin_runner.?; } const WindowsOnly = struct { @@ -772,7 +773,7 @@ pub const VirtualMachine = struct { global: *JSGlobalObject, allocator: std.mem.Allocator, has_loaded_constructors: bool = false, - bundler: Bundler, + transpiler: Transpiler, bun_watcher: ImportWatcher = .{ .none = {} }, console: *ConsoleObject, log: *logger.Log, @@ -790,6 +791,7 @@ pub const VirtualMachine = struct { unhandled_pending_rejection_to_capture: ?*JSC.JSValue = null, standalone_module_graph: ?*bun.StandaloneModuleGraph = null, smol: bool = false, + dns_result_order: DNSResolver.Order = .verbatim, hot_reload: bun.CLI.Command.HotReload = .none, jsc: *JSC.VM = undefined, @@ -885,6 +887,7 @@ pub const VirtualMachine = struct { onUnhandledRejectionExceptionList: ?*ExceptionList = null, unhandled_error_counter: usize = 0, is_handling_uncaught_exception: bool = false, + exit_on_uncaught_exception: bool = false, modules: ModuleLoader.AsyncModule.Queue = .{}, aggressive_garbage_collection: GCLevel = GCLevel.none, @@ -942,7 +945,7 @@ pub const VirtualMachine = struct { } pub fn getTLSRejectUnauthorized(this: *const VirtualMachine) bool { - return this.default_tls_reject_unauthorized orelse this.bundler.env.getTLSRejectUnauthorized(); + return this.default_tls_reject_unauthorized orelse this.transpiler.env.getTLSRejectUnauthorized(); } pub fn onSubprocessSpawn(this: *VirtualMachine, process: *bun.spawn.Process) void { @@ -955,7 +958,7 @@ pub const VirtualMachine = struct { pub fn getVerboseFetch(this: *VirtualMachine) bun.http.HTTPVerboseLevel { return this.default_verbose_fetch orelse { - if (this.bundler.env.get("BUN_CONFIG_VERBOSE_FETCH")) |verbose_fetch| { + if (this.transpiler.env.get("BUN_CONFIG_VERBOSE_FETCH")) |verbose_fetch| { if (strings.eqlComptime(verbose_fetch, "true") or strings.eqlComptime(verbose_fetch, "1")) { this.default_verbose_fetch = .headers; return .headers; @@ -972,9 +975,15 @@ pub const VirtualMachine = struct { pub const VMHolder = struct { pub threadlocal var vm: ?*VirtualMachine = null; pub threadlocal var cached_global_object: ?*JSGlobalObject = null; + pub var main_thread_vm: ?*VirtualMachine = null; pub export fn Bun__setDefaultGlobalObject(global: *JSGlobalObject) void { if (vm) |vm_instance| { vm_instance.global = global; + + // Ensure this is always set when it should be. + if (vm_instance.is_main_thread) { + VMHolder.main_thread_vm = vm_instance; + } } cached_global_object = global; @@ -994,6 +1003,10 @@ pub const VirtualMachine = struct { return VMHolder.vm.?; } + pub fn getMainThreadVM() ?*VirtualMachine { + return VMHolder.main_thread_vm; + } + pub fn mimeType(this: *VirtualMachine, str: []const u8) ?bun.http.MimeType { return this.rareData().mimeTypeFromString(this.allocator, str); } @@ -1026,7 +1039,7 @@ pub const VirtualMachine = struct { printer: *js_printer.BufferPrinter, pub fn get(this: *SourceMapHandlerGetter) js_printer.SourceMapHandler { - if (this.vm.debugger == null) { + if (this.vm.debugger == null or this.vm.debugger.?.mode == .connect) { return SavedSourceMap.SourceMapHandler.init(&this.vm.source_mappings); } @@ -1117,7 +1130,7 @@ pub const VirtualMachine = struct { } pub fn loadExtraEnvAndSourceCodePrinter(this: *VirtualMachine) void { - var map = this.bundler.env.map; + var map = this.transpiler.env.map; ensureSourceCodePrinter(this); @@ -1180,6 +1193,10 @@ pub const VirtualMachine = struct { extern fn Bun__handleUnhandledRejection(*JSC.JSGlobalObject, reason: JSC.JSValue, promise: JSC.JSValue) c_int; extern fn Bun__Process__exit(*JSC.JSGlobalObject, code: c_int) noreturn; + export fn Bun__VirtualMachine__exitDuringUncaughtException(this: *JSC.VirtualMachine) void { + this.exit_on_uncaught_exception = true; + } + pub fn unhandledRejection(this: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, reason: JSC.JSValue, promise: JSC.JSValue) bool { if (this.isShuttingDown()) { Output.debugWarn("unhandledRejection during shutdown.", .{}); @@ -1217,6 +1234,11 @@ pub const VirtualMachine = struct { Bun__Process__exit(globalObject, 7); @panic("Uncaught exception while handling uncaught exception"); } + if (this.exit_on_uncaught_exception) { + this.runErrorHandler(err, null); + Bun__Process__exit(globalObject, 1); + @panic("made it past Bun__Process__exit"); + } this.is_handling_uncaught_exception = true; defer this.is_handling_uncaught_exception = false; const handled = Bun__handleUncaughtException(globalObject, err.toError() orelse err, if (is_rejection) 1 else 0) > 0; @@ -1241,17 +1263,17 @@ pub const VirtualMachine = struct { } pub inline fn packageManager(this: *VirtualMachine) *PackageManager { - return this.bundler.getPackageManager(); + return this.transpiler.getPackageManager(); } - pub fn garbageCollect(this: *const VirtualMachine, sync: bool) JSValue { + pub fn garbageCollect(this: *const VirtualMachine, sync: bool) usize { @setCold(true); Global.mimalloc_cleanup(false); if (sync) return this.global.vm().runGC(true); this.global.vm().collectAsync(); - return JSValue.jsNumber(this.global.vm().heapSize()); + return this.global.vm().heapSize(); } pub inline fn autoGarbageCollect(this: *const VirtualMachine) void { @@ -1262,7 +1284,7 @@ pub const VirtualMachine = struct { pub fn reload(this: *VirtualMachine, _: *HotReloader.HotReloadTask) void { Output.debug("Reloading...", .{}); - const should_clear_terminal = !this.bundler.env.hasSetNoClearTerminalOnReload(!Output.enable_ansi_colors); + const should_clear_terminal = !this.transpiler.env.hasSetNoClearTerminalOnReload(!Output.enable_ansi_colors); if (this.hot_reload == .watch) { Output.flush(); bun.reloadProcess( @@ -1584,7 +1606,7 @@ pub const VirtualMachine = struct { Debugger.log("spin", .{}); while (futex_atomic.load(.monotonic) > 0) { - std.Thread.Futex.wait(&futex_atomic, 1); + bun.Futex.waitForever(&futex_atomic, 1); } if (comptime Environment.enable_logs) Debugger.log("waitForDebugger: {}", .{Output.ElapsedFormatter{ @@ -1700,7 +1722,7 @@ pub const VirtualMachine = struct { vm.allocator = arena.allocator(); vm.arena = &arena; - vm.bundler.configureDefines() catch @panic("Failed to configure defines"); + vm.transpiler.configureDefines() catch @panic("Failed to configure defines"); vm.is_main_thread = false; vm.eventLoop().ensureWaker(); @@ -1748,7 +1770,7 @@ pub const VirtualMachine = struct { log("wake", .{}); futex_atomic.store(0, .monotonic); - std.Thread.Futex.wake(&futex_atomic, 1); + bun.Futex.wake(&futex_atomic, 1); other_vm.eventLoop().wakeup(); @@ -1818,8 +1840,8 @@ pub const VirtualMachine = struct { ensureSourceCodePrinter(this); } - this.bundler.options.target = .bun_macro; - this.bundler.resolver.caches.fs.use_alternate_source_cache = true; + this.transpiler.options.target = .bun_macro; + this.transpiler.resolver.caches.fs.use_alternate_source_cache = true; this.macro_mode = true; this.event_loop = &this.macro_event_loop; Analytics.Features.macros += 1; @@ -1827,8 +1849,8 @@ pub const VirtualMachine = struct { } pub fn disableMacroMode(this: *VirtualMachine) void { - this.bundler.options.target = .bun; - this.bundler.resolver.caches.fs.use_alternate_source_cache = false; + this.transpiler.options.target = .bun; + this.transpiler.resolver.caches.fs.use_alternate_source_cache = false; this.macro_mode = false; this.event_loop = &this.regular_event_loop; this.transpiler_store.enabled = true; @@ -1868,7 +1890,7 @@ pub const VirtualMachine = struct { const console = try allocator.create(ConsoleObject); console.* = ConsoleObject.init(Output.errorWriter(), Output.writer()); const log = opts.log.?; - const bundler = try Bundler.init( + const transpiler = try Transpiler.init( allocator, log, opts.args, @@ -1881,10 +1903,10 @@ pub const VirtualMachine = struct { .transpiler_store = RuntimeTranspilerStore.init(), .allocator = allocator, .entry_point = ServerEntryPoint{}, - .bundler = bundler, + .transpiler = transpiler, .console = console, .log = log, - .origin = bundler.options.origin, + .origin = transpiler.options.origin, .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, .macros = MacroMap.init(allocator), @@ -1894,7 +1916,7 @@ pub const VirtualMachine = struct { .ref_strings = JSC.RefString.Map.init(allocator), .ref_strings_mutex = .{}, .standalone_module_graph = opts.graph.?, - .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId() else {}, + .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(), }; vm.source_mappings.init(&vm.saved_source_map_table); vm.regular_event_loop.tasks = EventLoop.Queue.init( @@ -1910,26 +1932,28 @@ pub const VirtualMachine = struct { vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; - vm.bundler.macro_context = null; - vm.bundler.resolver.store_fd = false; - vm.bundler.resolver.prefer_module_field = false; + vm.transpiler.macro_context = null; + vm.transpiler.resolver.store_fd = false; + vm.transpiler.resolver.prefer_module_field = false; - vm.bundler.resolver.onWakePackageManager = .{ + vm.transpiler.resolver.onWakePackageManager = .{ .context = &vm.modules, .handler = ModuleLoader.AsyncModule.Queue.onWakeHandler, .onDependencyError = JSC.ModuleLoader.AsyncModule.Queue.onDependencyError, }; - vm.bundler.resolver.standalone_module_graph = opts.graph.?; + vm.transpiler.resolver.standalone_module_graph = opts.graph.?; // Avoid reading from tsconfig.json & package.json when we're in standalone mode - vm.bundler.configureLinkerWithAutoJSX(false); - - vm.bundler.macro_context = js_ast.Macro.MacroContext.init(&vm.bundler); + vm.transpiler.configureLinkerWithAutoJSX(false); + vm.transpiler.macro_context = js_ast.Macro.MacroContext.init(&vm.transpiler); + if (opts.is_main_thread) { + VMHolder.main_thread_vm = vm; + } vm.global = ZigGlobalObject.create( vm.console, - -1, + if (opts.is_main_thread) -1 else std.math.maxInt(i32), false, false, null, @@ -1944,6 +1968,10 @@ pub const VirtualMachine = struct { return vm; } + export fn Bun__isMainThreadVM() callconv(.C) bool { + return get().is_main_thread; + } + pub const Options = struct { allocator: std.mem.Allocator, args: Api.TransformOptions, @@ -1951,12 +1979,14 @@ pub const VirtualMachine = struct { env_loader: ?*DotEnv.Loader = null, store_fd: bool = false, smol: bool = false, + dns_result_order: DNSResolver.Order = .verbatim, // --print needs the result from evaluating the main module eval: bool = false, graph: ?*bun.StandaloneModuleGraph = null, debugger: bun.CLI.Command.Debugger = .{ .unspecified = {} }, + is_main_thread: bool = false, }; pub var is_smol_mode = false; @@ -1975,23 +2005,25 @@ pub const VirtualMachine = struct { VMHolder.vm = try allocator.create(VirtualMachine); const console = try allocator.create(ConsoleObject); console.* = ConsoleObject.init(Output.errorWriter(), Output.writer()); - const bundler = try Bundler.init( + const transpiler = try Transpiler.init( allocator, log, try Config.configureTransformOptionsForBunVM(allocator, opts.args), opts.env_loader, ); var vm = VMHolder.vm.?; - + if (opts.is_main_thread) { + VMHolder.main_thread_vm = vm; + } vm.* = VirtualMachine{ .global = undefined, .transpiler_store = RuntimeTranspilerStore.init(), .allocator = allocator, .entry_point = ServerEntryPoint{}, - .bundler = bundler, + .transpiler = transpiler, .console = console, .log = log, - .origin = bundler.options.origin, + .origin = transpiler.options.origin, .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, .macros = MacroMap.init(allocator), @@ -2000,7 +2032,7 @@ pub const VirtualMachine = struct { .origin_timestamp = getOriginTimestamp(), .ref_strings = JSC.RefString.Map.init(allocator), .ref_strings_mutex = .{}, - .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId() else {}, + .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(), }; vm.source_mappings.init(&vm.saved_source_map_table); vm.regular_event_loop.tasks = EventLoop.Queue.init( @@ -2016,23 +2048,23 @@ pub const VirtualMachine = struct { vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; - vm.bundler.macro_context = null; - vm.bundler.resolver.store_fd = opts.store_fd; - vm.bundler.resolver.prefer_module_field = false; + vm.transpiler.macro_context = null; + vm.transpiler.resolver.store_fd = opts.store_fd; + vm.transpiler.resolver.prefer_module_field = false; - vm.bundler.resolver.onWakePackageManager = .{ + vm.transpiler.resolver.onWakePackageManager = .{ .context = &vm.modules, .handler = ModuleLoader.AsyncModule.Queue.onWakeHandler, .onDependencyError = JSC.ModuleLoader.AsyncModule.Queue.onDependencyError, }; - vm.bundler.configureLinker(); + vm.transpiler.configureLinker(); - vm.bundler.macro_context = js_ast.Macro.MacroContext.init(&vm.bundler); + vm.transpiler.macro_context = js_ast.Macro.MacroContext.init(&vm.transpiler); vm.global = ZigGlobalObject.create( vm.console, - -1, + if (opts.is_main_thread) -1 else std.math.maxInt(i32), opts.smol, opts.eval, null, @@ -2041,6 +2073,7 @@ pub const VirtualMachine = struct { vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); vm.smol = opts.smol; + vm.dns_result_order = opts.dns_result_order; if (opts.smol) is_smol_mode = opts.smol; @@ -2065,16 +2098,12 @@ pub const VirtualMachine = struct { } const unix = bun.getenvZ("BUN_INSPECT") orelse ""; - const notify = bun.getenvZ("BUN_INSPECT_NOTIFY") orelse ""; const connect_to = bun.getenvZ("BUN_INSPECT_CONNECT_TO") orelse ""; const set_breakpoint_on_first_line = unix.len > 0 and strings.endsWith(unix, "?break=1"); // If we should set a breakpoint on the first line const wait_for_debugger = unix.len > 0 and strings.endsWith(unix, "?wait=1"); // If we should wait for the debugger to connect before starting the event loop - const wait_for_connection: Debugger.Wait = switch (set_breakpoint_on_first_line or wait_for_debugger) { - true => if (notify.len > 0 or connect_to.len > 0) .shortly else .forever, - false => .off, - }; + const wait_for_connection: Debugger.Wait = if (set_breakpoint_on_first_line or wait_for_debugger) .forever else .off; switch (cli_flag) { .unspecified => { @@ -2085,22 +2114,14 @@ pub const VirtualMachine = struct { .wait_for_connection = wait_for_connection, .set_breakpoint_on_first_line = set_breakpoint_on_first_line, }; - } else if (notify.len > 0) { - this.debugger = Debugger{ - .path_or_port = null, - .from_environment_variable = notify, - .wait_for_connection = wait_for_connection, - .set_breakpoint_on_first_line = set_breakpoint_on_first_line, - .mode = .connect, - }; } else if (connect_to.len > 0) { // This works in the vscode debug terminal because that relies on unix or notify being set, which they // are in the debug terminal. This branch doesn't reach this.debugger = Debugger{ .path_or_port = null, .from_environment_variable = connect_to, - .wait_for_connection = wait_for_connection, - .set_breakpoint_on_first_line = set_breakpoint_on_first_line, + .wait_for_connection = .off, + .set_breakpoint_on_first_line = false, .mode = .connect, }; } @@ -2115,11 +2136,11 @@ pub const VirtualMachine = struct { }, } - if (this.debugger != null) { - this.bundler.options.minify_identifiers = false; - this.bundler.options.minify_syntax = false; - this.bundler.options.minify_whitespace = false; - this.bundler.options.debugger = true; + if (this.isInspectorEnabled() and this.debugger.?.mode != .connect) { + this.transpiler.options.minify_identifiers = false; + this.transpiler.options.minify_syntax = false; + this.transpiler.options.minify_whitespace = false; + this.transpiler.options.debugger = true; } } @@ -2140,7 +2161,7 @@ pub const VirtualMachine = struct { VMHolder.vm = try allocator.create(VirtualMachine); const console = try allocator.create(ConsoleObject); console.* = ConsoleObject.init(Output.errorWriter(), Output.writer()); - const bundler = try Bundler.init( + const transpiler = try Transpiler.init( allocator, log, try Config.configureTransformOptionsForBunVM(allocator, opts.args), @@ -2153,10 +2174,10 @@ pub const VirtualMachine = struct { .allocator = allocator, .transpiler_store = RuntimeTranspilerStore.init(), .entry_point = ServerEntryPoint{}, - .bundler = bundler, + .transpiler = transpiler, .console = console, .log = log, - .origin = bundler.options.origin, + .origin = transpiler.options.origin, .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, .macros = MacroMap.init(allocator), @@ -2167,7 +2188,7 @@ pub const VirtualMachine = struct { .ref_strings_mutex = .{}, .standalone_module_graph = worker.parent.standalone_module_graph, .worker = worker, - .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId() else {}, + .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(), }; vm.source_mappings.init(&vm.saved_source_map_table); vm.regular_event_loop.tasks = EventLoop.Queue.init( @@ -2183,24 +2204,24 @@ pub const VirtualMachine = struct { vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; vm.hot_reload = worker.parent.hot_reload; - vm.bundler.macro_context = null; - vm.bundler.resolver.store_fd = opts.store_fd; - vm.bundler.resolver.prefer_module_field = false; - vm.bundler.resolver.onWakePackageManager = .{ + vm.transpiler.macro_context = null; + vm.transpiler.resolver.store_fd = opts.store_fd; + vm.transpiler.resolver.prefer_module_field = false; + vm.transpiler.resolver.onWakePackageManager = .{ .context = &vm.modules, .handler = ModuleLoader.AsyncModule.Queue.onWakeHandler, .onDependencyError = JSC.ModuleLoader.AsyncModule.Queue.onDependencyError, }; - vm.bundler.resolver.standalone_module_graph = opts.graph; + vm.transpiler.resolver.standalone_module_graph = opts.graph; if (opts.graph == null) { - vm.bundler.configureLinker(); + vm.transpiler.configureLinker(); } else { - vm.bundler.configureLinkerWithAutoJSX(false); + vm.transpiler.configureLinkerWithAutoJSX(false); } vm.smol = opts.smol; - vm.bundler.macro_context = js_ast.Macro.MacroContext.init(&vm.bundler); + vm.transpiler.macro_context = js_ast.Macro.MacroContext.init(&vm.transpiler); vm.global = ZigGlobalObject.create( vm.console, @@ -2212,7 +2233,7 @@ pub const VirtualMachine = struct { vm.regular_event_loop.global = vm.global; vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); - vm.bundler.setAllocator(allocator); + vm.transpiler.setAllocator(allocator); vm.body_value_hive_allocator = BodyValueHiveAllocator.init(bun.typedAllocator(JSC.WebCore.Body.Value)); return vm; @@ -2232,7 +2253,7 @@ pub const VirtualMachine = struct { VMHolder.vm = try allocator.create(VirtualMachine); const console = try allocator.create(ConsoleObject); console.* = ConsoleObject.init(Output.errorWriter(), Output.writer()); - const bundler = try Bundler.init( + const transpiler = try Transpiler.init( allocator, log, try Config.configureTransformOptionsForBunVM(allocator, opts.args), @@ -2245,10 +2266,10 @@ pub const VirtualMachine = struct { .transpiler_store = RuntimeTranspilerStore.init(), .allocator = allocator, .entry_point = ServerEntryPoint{}, - .bundler = bundler, + .transpiler = transpiler, .console = console, .log = log, - .origin = bundler.options.origin, + .origin = transpiler.options.origin, .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, .macros = MacroMap.init(allocator), @@ -2257,7 +2278,7 @@ pub const VirtualMachine = struct { .origin_timestamp = getOriginTimestamp(), .ref_strings = JSC.RefString.Map.init(allocator), .ref_strings_mutex = .{}, - .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId() else {}, + .debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(), }; vm.source_mappings.init(&vm.saved_source_map_table); vm.regular_event_loop.tasks = EventLoop.Queue.init( @@ -2273,19 +2294,19 @@ pub const VirtualMachine = struct { vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; - vm.bundler.macro_context = null; - vm.bundler.resolver.store_fd = opts.store_fd; - vm.bundler.resolver.prefer_module_field = false; + vm.transpiler.macro_context = null; + vm.transpiler.resolver.store_fd = opts.store_fd; + vm.transpiler.resolver.prefer_module_field = false; - vm.bundler.resolver.onWakePackageManager = .{ + vm.transpiler.resolver.onWakePackageManager = .{ .context = &vm.modules, .handler = ModuleLoader.AsyncModule.Queue.onWakeHandler, .onDependencyError = JSC.ModuleLoader.AsyncModule.Queue.onDependencyError, }; - vm.bundler.configureLinker(); + vm.transpiler.configureLinker(); - vm.bundler.macro_context = js_ast.Macro.MacroContext.init(&vm.bundler); + vm.transpiler.macro_context = js_ast.Macro.MacroContext.init(&vm.transpiler); vm.regular_event_loop.virtual_machine = vm; vm.smol = opts.smol; @@ -2383,15 +2404,12 @@ pub const VirtualMachine = struct { return builtin; } - const display_specifier = _specifier.toUTF8(bun.default_allocator); - defer display_specifier.deinit(); const specifier_clone = _specifier.toUTF8(bun.default_allocator); defer specifier_clone.deinit(); - var display_slice = display_specifier.slice(); - const specifier = ModuleLoader.normalizeSpecifier(jsc_vm, specifier_clone.slice(), &display_slice); + const normalized_file_path_from_specifier, const specifier = ModuleLoader.normalizeSpecifier(jsc_vm, specifier_clone.slice()); const referrer_clone = referrer.toUTF8(bun.default_allocator); defer referrer_clone.deinit(); - var path = Fs.Path.init(specifier_clone.slice()); + var path = Fs.Path.init(normalized_file_path_from_specifier); // For blobs. var blob_source: ?JSC.WebCore.Blob = null; @@ -2466,7 +2484,7 @@ pub const VirtualMachine = struct { } break :brk .{ - jsc_vm.bundler.options.loaders.get(ext_for_loader) orelse brk2: { + jsc_vm.transpiler.options.loaders.get(ext_for_loader) orelse brk2: { if (strings.eqlLong(specifier, jsc_vm.main, true)) { break :brk2 options.Loader.js; } @@ -2484,8 +2502,7 @@ pub const VirtualMachine = struct { return try ModuleLoader.transpileSourceCode( jsc_vm, - specifier_clone.slice(), - display_slice, + specifier, referrer_clone.slice(), _specifier, path, @@ -2575,7 +2592,7 @@ pub const VirtualMachine = struct { else source else - jsc_vm.bundler.fs.top_level_dir; + jsc_vm.transpiler.fs.top_level_dir; const result: Resolver.Result = try brk: { // TODO: We only want to retry on not found only when the directories we searched for were cached. @@ -2585,7 +2602,7 @@ pub const VirtualMachine = struct { // This cache-bust is disabled when the filesystem is not being used to resolve. var retry_on_not_found = std.fs.path.isAbsolute(source_to_use); while (true) { - break :brk switch (jsc_vm.bundler.resolver.resolveAndAutoInstall( + break :brk switch (jsc_vm.transpiler.resolver.resolveAndAutoInstall( source_to_use, normalized_specifier, if (is_esm) .stmt else .require, @@ -2613,7 +2630,7 @@ pub const VirtualMachine = struct { }; break :name bun.path.joinAbsStringBufZ( - jsc_vm.bundler.fs.top_level_dir, + jsc_vm.transpiler.fs.top_level_dir, &specifier_cache_resolver_buf, &parts, .auto, @@ -2621,7 +2638,7 @@ pub const VirtualMachine = struct { }; // Only re-query if we previously had something cached. - if (jsc_vm.bundler.resolver.bustDirCache(bun.strings.withoutTrailingSlashWindowsPath(buster_name))) { + if (jsc_vm.transpiler.resolver.bustDirCache(bun.strings.withoutTrailingSlashWindowsPath(buster_name))) { continue; } @@ -2632,7 +2649,7 @@ pub const VirtualMachine = struct { }; if (!jsc_vm.macro_mode) { - jsc_vm.has_any_macro_remappings = jsc_vm.has_any_macro_remappings or jsc_vm.bundler.options.macro_remap.count() > 0; + jsc_vm.has_any_macro_remappings = jsc_vm.has_any_macro_remappings or jsc_vm.transpiler.options.macro_remap.count() > 0; } ret.result = result; ret.query_string = query_string; @@ -2731,7 +2748,7 @@ pub const VirtualMachine = struct { } } - if (JSC.HardcodedModule.Aliases.getWithEql(specifier, bun.String.eqlComptime, jsc_vm.bundler.options.target)) |hardcoded| { + if (JSC.HardcodedModule.Aliases.getWithEql(specifier, bun.String.eqlComptime, jsc_vm.transpiler.options.target)) |hardcoded| { // if (hardcoded.tag == .none) { // resolveMaybeNeedsTrailingSlash( // res, @@ -2754,12 +2771,12 @@ pub const VirtualMachine = struct { var log = logger.Log.init(bun.default_allocator); defer log.deinit(); jsc_vm.log = &log; - jsc_vm.bundler.resolver.log = &log; - jsc_vm.bundler.linker.log = &log; + jsc_vm.transpiler.resolver.log = &log; + jsc_vm.transpiler.linker.log = &log; defer { jsc_vm.log = old_log; - jsc_vm.bundler.linker.log = old_log; - jsc_vm.bundler.resolver.log = old_log; + jsc_vm.transpiler.linker.log = old_log; + jsc_vm.transpiler.resolver.log = old_log; } _resolve(&result, specifier_utf8.slice(), normalizeSource(source_utf8.slice()), is_esm, is_a_file_path) catch |err_| { var err = err_; @@ -2909,10 +2926,17 @@ pub const VirtualMachine = struct { writer: Writer, comptime allow_side_effects: bool, ) void { + var formatter = ConsoleObject.Formatter{ + .globalThis = this.global, + .quote_strings = false, + .single_line = false, + .stack_check = bun.StackCheck.init(), + }; + defer formatter.deinit(); if (Output.enable_ansi_colors) { - this.printErrorlikeObject(exception.value(), exception, exception_list, Writer, writer, true, allow_side_effects); + this.printErrorlikeObject(exception.value(), exception, exception_list, &formatter, Writer, writer, true, allow_side_effects); } else { - this.printErrorlikeObject(exception.value(), exception, exception_list, Writer, writer, false, allow_side_effects); + this.printErrorlikeObject(exception.value(), exception, exception_list, &formatter, Writer, writer, false, allow_side_effects); } } @@ -2942,7 +2966,6 @@ pub const VirtualMachine = struct { if (result.isException(this.global.vm())) { const exception = @as(*Exception, @ptrCast(result.asVoid())); - this.printException( exception, exception_list, @@ -2950,10 +2973,18 @@ pub const VirtualMachine = struct { writer, true, ); - } else if (Output.enable_ansi_colors) { - this.printErrorlikeObject(result, null, exception_list, @TypeOf(writer), writer, true, true); } else { - this.printErrorlikeObject(result, null, exception_list, @TypeOf(writer), writer, false, true); + var formatter = ConsoleObject.Formatter{ + .globalThis = this.global, + .quote_strings = false, + .single_line = false, + .stack_check = bun.StackCheck.init(), + .error_display_level = .full, + }; + defer formatter.deinit(); + switch (Output.enable_ansi_colors) { + inline else => |enable_colors| this.printErrorlikeObject(result, null, exception_list, &formatter, @TypeOf(writer), writer, enable_colors, true), + } } } @@ -2977,8 +3008,8 @@ pub const VirtualMachine = struct { defer this.is_in_preload = false; for (this.preload) |preload| { - var result = switch (this.bundler.resolver.resolveAndAutoInstall( - this.bundler.fs.top_level_dir, + var result = switch (this.transpiler.resolver.resolveAndAutoInstall( + this.transpiler.fs.top_level_dir, normalizeSource(preload), .stmt, if (this.standalone_module_graph == null) .read_only else .disable, @@ -2992,7 +3023,7 @@ pub const VirtualMachine = struct { "{s} resolving preload {}", .{ @errorName(e), - bun.fmt.formatJSONString(preload), + bun.fmt.formatJSONStringLatin1(preload), }, ) catch unreachable; return e; @@ -3004,7 +3035,7 @@ pub const VirtualMachine = struct { this.allocator, "preload not found {}", .{ - bun.fmt.formatJSONString(preload), + bun.fmt.formatJSONStringLatin1(preload), }, ) catch unreachable; return error.ModuleNotFound; @@ -3073,7 +3104,7 @@ pub const VirtualMachine = struct { ); this.eventLoop().ensureWaker(); - if (!this.bundler.options.disable_transpilation) { + if (!this.transpiler.options.disable_transpilation) { if (try this.loadPreloads()) |promise| { JSC.JSValue.fromCell(promise).ensureStillAlive(); JSC.JSValue.fromCell(promise).protect(); @@ -3103,7 +3134,7 @@ pub const VirtualMachine = struct { try this.ensureDebugger(true); - if (!this.bundler.options.disable_transpilation) { + if (!this.transpiler.options.disable_transpilation) { if (try this.loadPreloads()) |promise| { JSC.JSValue.fromCell(promise).ensureStillAlive(); this.pending_internal_promise = promise; @@ -3218,7 +3249,7 @@ pub const VirtualMachine = struct { if (!entry_point_entry.found_existing) { var macro_entry_pointer: *MacroEntryPoint = this.allocator.create(MacroEntryPoint) catch unreachable; entry_point_entry.value_ptr.* = macro_entry_pointer; - try macro_entry_pointer.generate(&this.bundler, Fs.PathName.init(entry_path), function_name, hash, specifier); + try macro_entry_pointer.generate(&this.transpiler, Fs.PathName.init(entry_path), function_name, hash, specifier); } const entry_point = entry_point_entry.value_ptr.*; @@ -3258,14 +3289,8 @@ pub const VirtualMachine = struct { return promise; } - pub fn printErrorLikeObjectSimple(this: *VirtualMachine, value: JSValue, writer: anytype, comptime escape_codes: bool) void { - this.printErrorlikeObject(value, null, null, @TypeOf(writer), writer, escape_codes, false); - } - pub fn printErrorLikeObjectToConsole(this: *VirtualMachine, value: JSValue) void { - switch (Output.enable_ansi_colors_stderr) { - inline else => |colors| this.printErrorLikeObjectSimple(value, Output.errorWriter(), colors), - } + this.runErrorHandler(value, null); } // When the Error-like object is one of our own, it's best to rely on the object directly instead of serializing it to a ZigException. @@ -3280,6 +3305,7 @@ pub const VirtualMachine = struct { value: JSValue, exception: ?*Exception, exception_list: ?*ExceptionList, + formatter: *ConsoleObject.Formatter, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, @@ -3297,7 +3323,7 @@ pub const VirtualMachine = struct { var holder = ZigException.Holder.init(); var zig_exception: *ZigException = holder.zigException(); holder.deinit(this); - exception_.getStackTrace(&zig_exception.stack); + exception_.getStackTrace(this.global, &zig_exception.stack); if (zig_exception.stack.frames_len > 0) { if (allow_ansi_color) { printStackTrace(Writer, writer, zig_exception.stack, true) catch {}; @@ -3307,7 +3333,7 @@ pub const VirtualMachine = struct { } if (exception_list) |list| { - zig_exception.addToErrorList(list, this.bundler.fs.top_level_dir, &this.origin) catch {}; + zig_exception.addToErrorList(list, this.transpiler.fs.top_level_dir, &this.origin) catch {}; } } } @@ -3317,6 +3343,7 @@ pub const VirtualMachine = struct { const AggregateErrorIterator = struct { writer: Writer, current_exception_list: ?*ExceptionList = null, + formatter: *ConsoleObject.Formatter, pub fn iteratorWithColor(_vm: [*c]VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { iterator(_vm, globalObject, nextValue, ctx.?, true); @@ -3326,10 +3353,10 @@ pub const VirtualMachine = struct { } inline fn iterator(_: [*c]VM, _: *JSGlobalObject, nextValue: JSValue, ctx: ?*anyopaque, comptime color: bool) void { const this_ = @as(*@This(), @ptrFromInt(@intFromPtr(ctx))); - VirtualMachine.get().printErrorlikeObject(nextValue, null, this_.current_exception_list, Writer, this_.writer, color, allow_side_effects); + VirtualMachine.get().printErrorlikeObject(nextValue, null, this_.current_exception_list, this_.formatter, Writer, this_.writer, color, allow_side_effects); } }; - var iter = AggregateErrorIterator{ .writer = writer, .current_exception_list = exception_list }; + var iter = AggregateErrorIterator{ .writer = writer, .current_exception_list = exception_list, .formatter = formatter }; if (comptime allow_ansi_color) { value.getErrorsProperty(this.global).forEach(this.global, &iter, AggregateErrorIterator.iteratorWithColor); } else { @@ -3341,6 +3368,7 @@ pub const VirtualMachine = struct { was_internal = this.printErrorFromMaybePrivateData( value, exception_list, + formatter, Writer, writer, allow_ansi_color, @@ -3348,10 +3376,11 @@ pub const VirtualMachine = struct { ); } - pub fn printErrorFromMaybePrivateData( + fn printErrorFromMaybePrivateData( this: *VirtualMachine, value: JSC.JSValue, exception_list: ?*ExceptionList, + formatter: *ConsoleObject.Formatter, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, @@ -3400,12 +3429,15 @@ pub const VirtualMachine = struct { this.printErrorInstance( value, exception_list, + formatter, Writer, writer, allow_ansi_color, allow_side_effects, ) catch |err| { - if (comptime Environment.isDebug) { + if (err == error.JSError) { + this.global.clearException(); + } else if (comptime Environment.isDebug) { // yo dawg Output.printErrorln("Error while printing Error-like object: {s}", .{@errorName(err)}); Output.flush(); @@ -3426,7 +3458,7 @@ pub const VirtualMachine = struct { if (stack.len > 0) { var vm = VirtualMachine.get(); const origin: ?*const URL = if (vm.is_from_devserver) &vm.origin else null; - const dir = vm.bundler.fs.top_level_dir; + const dir = vm.transpiler.fs.top_level_dir; for (stack) |frame| { const file_slice = frame.source_url.toUTF8(bun.default_allocator); @@ -3546,13 +3578,27 @@ pub const VirtualMachine = struct { exception_list: ?*ExceptionList, must_reset_parser_arena_later: *bool, source_code_slice: *?ZigString.Slice, + allow_source_code_preview: bool, ) void { error_instance.toZigException(this.global, exception); + const enable_source_code_preview = allow_source_code_preview and + !(bun.getRuntimeFeatureFlag("BUN_DISABLE_SOURCE_CODE_PREVIEW") or + bun.getRuntimeFeatureFlag("BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW")); + + defer { + if (Environment.isDebug) { + if (!enable_source_code_preview and source_code_slice.* != null) { + Output.panic("Do not collect source code when we don't need to", .{}); + } else if (!enable_source_code_preview and exception.stack.source_lines_numbers[0] != -1) { + Output.panic("Do not collect source code when we don't need to", .{}); + } + } + } // defer this so that it copies correctly defer { if (exception_list) |list| { - exception.addToErrorList(list, this.bundler.fs.top_level_dir, &this.origin) catch unreachable; + exception.addToErrorList(list, this.transpiler.fs.top_level_dir, &this.origin) catch unreachable; } } @@ -3613,7 +3659,8 @@ pub const VirtualMachine = struct { if (frame.source_url.hasPrefixComptime("bun:") or frame.source_url.hasPrefixComptime("node:") or frame.source_url.isEmpty() or - frame.source_url.eqlComptime("native")) + frame.source_url.eqlComptime("native") or + frame.source_url.eqlComptime("unknown")) { top_frame_is_builtin = true; continue; @@ -3662,7 +3709,10 @@ pub const VirtualMachine = struct { } const code = code: { - if (bun.getRuntimeFeatureFlag("BUN_DISABLE_SOURCE_CODE_PREVIEW") or bun.getRuntimeFeatureFlag("BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW")) break :code ZigString.Slice.empty; + if (!enable_source_code_preview) { + break :code ZigString.Slice.empty; + } + if (!top.remapped and lookup.source_map != null and lookup.source_map.?.isExternal()) { if (lookup.getSourceCode(top_source_url.slice())) |src| { break :code src; @@ -3681,7 +3731,13 @@ pub const VirtualMachine = struct { must_reset_parser_arena_later.* = true; break :code original_source.source_code.toUTF8(bun.default_allocator); }; - source_code_slice.* = code; + + if (enable_source_code_preview and code.len == 0) { + exception.collectSourceLines(error_instance, this.global); + } + + if (code.len > 0) + source_code_slice.* = code; top.position.line = Ordinal.fromZeroBased(mapping.original.lines); top.position.column = Ordinal.fromZeroBased(mapping.original.columns); @@ -3713,6 +3769,8 @@ pub const VirtualMachine = struct { exception.stack.source_lines_len = @as(u8, @truncate(lines.len)); } + } else if (enable_source_code_preview) { + exception.collectSourceLines(error_instance, this.global); } if (frames.len > 1) { @@ -3740,10 +3798,11 @@ pub const VirtualMachine = struct { } } - pub fn printErrorInstance(this: *VirtualMachine, error_instance: JSValue, exception_list: ?*ExceptionList, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, comptime allow_side_effects: bool) anyerror!void { + fn printErrorInstance(this: *VirtualMachine, error_instance: JSValue, exception_list: ?*ExceptionList, formatter: *ConsoleObject.Formatter, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, comptime allow_side_effects: bool) !void { var exception_holder = ZigException.Holder.init(); var exception = exception_holder.zigException(); defer exception_holder.deinit(this); + defer error_instance.ensureStillAlive(); // The ZigException structure stores substrings of the source code, in // which we need the lifetime of this data to outlive the inner call to @@ -3757,6 +3816,7 @@ pub const VirtualMachine = struct { exception_list, &exception_holder.need_to_clear_parser_arena_on_deinit, &source_code_slice, + formatter.error_display_level != .warn, ); const prev_had_errors = this.had_errors; this.had_errors = true; @@ -3816,9 +3876,28 @@ pub const VirtualMachine = struct { } const name = exception.name; - const message = exception.message; + const is_error_instance = error_instance != .zero and error_instance.jsType() == .ErrorInstance; + const code: ?[]const u8 = if (is_error_instance) code: { + if (error_instance.uncheckedPtrCast(JSC.JSObject).getCodePropertyVMInquiry(this.global)) |code_value| { + if (code_value.isString()) { + const code_string = code_value.toBunString2(this.global) catch { + // JSC::JSString to WTF::String can only fail on out of memory. + bun.outOfMemory(); + }; + defer code_string.deref(); + + if (code_string.is8Bit()) { + // We can count on this memory being valid until the end + // of this function because + break :code code_string.latin1(); + } + } + } + break :code null; + } else null; + var did_print_name = false; if (source_lines.next()) |source| brk: { if (source.text.len == 0) break :brk; @@ -3859,7 +3938,7 @@ pub const VirtualMachine = struct { ); } - try this.printErrorNameAndMessage(name, message, Writer, writer, allow_ansi_color); + try this.printErrorNameAndMessage(name, message, code, Writer, writer, allow_ansi_color, formatter.error_display_level); } else if (top_frame) |top| { defer did_print_name = true; const display_line = source.line + 1; @@ -3904,40 +3983,14 @@ pub const VirtualMachine = struct { } } - try this.printErrorNameAndMessage(name, message, Writer, writer, allow_ansi_color); + try this.printErrorNameAndMessage(name, message, code, Writer, writer, allow_ansi_color, formatter.error_display_level); } } if (!did_print_name) { - try this.printErrorNameAndMessage(name, message, Writer, writer, allow_ansi_color); + try this.printErrorNameAndMessage(name, message, code, Writer, writer, allow_ansi_color, formatter.error_display_level); } - var add_extra_line = false; - - const Show = struct { - system_code: bool = false, - syscall: bool = false, - errno: bool = false, - path: bool = false, - fd: bool = false, - }; - - const show = Show{ - .system_code = !exception.system_code.eql(name) and !exception.system_code.isEmpty(), - .syscall = !exception.syscall.isEmpty(), - .errno = exception.errno != 0, - .path = !exception.path.isEmpty(), - .fd = exception.fd != -1, - }; - - const extra_fields = .{ - "url", - "info", - "pkg", - "errors", - "cause", - }; - // This is usually unsafe to do, but we are protecting them each time first var errors_to_append = std.ArrayList(JSC.JSValue).init(this.allocator); defer { @@ -3947,103 +4000,197 @@ pub const VirtualMachine = struct { errors_to_append.deinit(); } - if (error_instance != .zero and error_instance.isCell() and error_instance.jsType().canGet()) { - inline for (extra_fields) |field| { - if (try error_instance.getTruthyComptime(this.global, field)) |value| { - const kind = value.jsType(); - if (kind.isStringLike()) { - if (value.toStringOrNull(this.global)) |str| { - var zig_str = str.toSlice(this.global, bun.default_allocator); - defer zig_str.deinit(); - try writer.print(comptime Output.prettyFmt(" {s}: \"{s}\"\n", allow_ansi_color), .{ field, zig_str.slice() }); - add_extra_line = true; + if (is_error_instance) { + var saw_cause = false; + const Iterator = JSC.JSPropertyIterator(.{ + .include_value = true, + .skip_empty_name = true, + .own_properties_only = true, + .observable = false, + .only_non_index_properties = true, + }); + var iterator = try Iterator.init(this.global, error_instance); + defer iterator.deinit(); + const longest_name = @min(iterator.getLongestPropertyName(), 10); + var is_first_property = true; + while (try iterator.next()) |field| { + const value = iterator.value; + if (field.eqlComptime("message") or field.eqlComptime("name") or field.eqlComptime("stack")) { + continue; + } + + // We special-case the code property. Let's avoid printing it twice. + if (field.eqlComptime("code") and code != null) { + continue; + } + + const kind = value.jsType(); + if (kind == .ErrorInstance and + // avoid infinite recursion + !prev_had_errors) + { + if (field.eqlComptime("cause")) { + saw_cause = true; + } + value.protect(); + try errors_to_append.append(value); + } else if (kind.isObject() or kind.isArray() or value.isPrimitive() or kind.isStringLike()) { + var bun_str = bun.String.empty; + defer bun_str.deref(); + const prev_disable_inspect_custom = formatter.disable_inspect_custom; + const prev_quote_strings = formatter.quote_strings; + const prev_max_depth = formatter.max_depth; + formatter.depth += 1; + defer { + formatter.depth -= 1; + formatter.max_depth = prev_max_depth; + formatter.quote_strings = prev_quote_strings; + formatter.disable_inspect_custom = prev_disable_inspect_custom; + } + formatter.max_depth = 1; + formatter.quote_strings = true; + formatter.disable_inspect_custom = true; + + const pad_left = longest_name -| field.length(); + is_first_property = false; + try writer.writeByteNTimes(' ', pad_left); + + try writer.print(comptime Output.prettyFmt(" {}: ", allow_ansi_color), .{field}); + + // When we're printing errors for a top-level uncaught exception / rejection, suppress further errors here. + if (allow_side_effects) { + if (this.global.hasException()) { + this.global.clearException(); } - } else if (kind == .ErrorInstance and - // avoid infinite recursion - !prev_had_errors) - { - value.protect(); - try errors_to_append.append(value); - } else if (kind.isObject() or kind.isArray()) { - var bun_str = bun.String.empty; - defer bun_str.deref(); - value.jsonStringify(this.global, 2, &bun_str); //2 - try writer.print(comptime Output.prettyFmt(" {s}: {}\n", allow_ansi_color), .{ field, bun_str }); - add_extra_line = true; + } + + formatter.format( + JSC.Formatter.Tag.getAdvanced( + value, + this.global, + .{ .disable_inspect_custom = true, .hide_global = true }, + ), + Writer, + writer, + value, + this.global, + allow_ansi_color, + ) catch {}; + + if (allow_side_effects) { + // When we're printing errors for a top-level uncaught exception / rejection, suppress further errors here. + if (this.global.hasException()) { + this.global.clearException(); + } + } else if (this.global.hasException() or formatter.failed) { + return; + } + + try writer.writeAll(comptime Output.prettyFmt(",\n", allow_ansi_color)); + } + } + + if (code) |code_str| { + const pad_left = longest_name -| "code".len; + is_first_property = false; + try writer.writeByteNTimes(' ', pad_left); + + try writer.print(comptime Output.prettyFmt(" code: {}\n", allow_ansi_color), .{ + bun.fmt.quote(code_str), + }); + } + + if (!is_first_property) { + try writer.writeAll("\n"); + } + + // "cause" is not enumerable, so the above loop won't see it. + if (!saw_cause) { + if (error_instance.getOwn(this.global, "cause")) |cause| { + if (cause.jsType() == .ErrorInstance) { + cause.protect(); + try errors_to_append.append(cause); } } } - } + } else if (error_instance != .zero) { + // If you do reportError([1,2,3]] we should still show something at least. + const tag = JSC.Formatter.Tag.getAdvanced( + error_instance, + this.global, + .{ .disable_inspect_custom = true, .hide_global = true }, + ); + if (tag.tag != .NativeCode) { + try formatter.format( + tag, + Writer, + writer, + error_instance, + this.global, + allow_ansi_color, + ); - if (show.errno) { - if (show.syscall) { - try writer.writeAll(" "); + // Always include a newline in this case + try writer.writeAll("\n"); } - try writer.print(comptime Output.prettyFmt(" errno: {d}\n", allow_ansi_color), .{exception.errno}); - add_extra_line = true; } - if (show.system_code) { - if (show.syscall) { - try writer.writeAll(" "); - } else if (show.errno) { - try writer.writeAll(" "); - } - try writer.print(comptime Output.prettyFmt(" code: \"{}\"\n", allow_ansi_color), .{exception.system_code}); - add_extra_line = true; - } - - if (show.syscall) { - try writer.print(comptime Output.prettyFmt(" syscall: \"{}\"\n", allow_ansi_color), .{exception.syscall}); - add_extra_line = true; - } - - if (show.path) { - if (show.syscall) { - try writer.writeAll(" "); - } else if (show.errno) { - try writer.writeAll(" "); - } - try writer.print(comptime Output.prettyFmt(" path: \"{}\"\n", allow_ansi_color), .{exception.path}); - } - - if (show.fd) { - if (show.syscall) { - try writer.writeAll(" "); - } else if (show.errno) { - try writer.writeAll(" "); - } - try writer.print(comptime Output.prettyFmt(" fd: {d}\n", allow_ansi_color), .{exception.fd}); - } - - if (add_extra_line) try writer.writeAll("\n"); - try printStackTrace(@TypeOf(writer), writer, exception.stack, allow_ansi_color); for (errors_to_append.items) |err| { try writer.writeAll("\n"); - try this.printErrorInstance(err, exception_list, Writer, writer, allow_ansi_color, allow_side_effects); + try this.printErrorInstance(err, exception_list, formatter, Writer, writer, allow_ansi_color, allow_side_effects); } } - fn printErrorNameAndMessage(_: *VirtualMachine, name: String, message: String, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { + fn printErrorNameAndMessage( + _: *VirtualMachine, + name: String, + message: String, + optional_code: ?[]const u8, + comptime Writer: type, + writer: Writer, + comptime allow_ansi_color: bool, + error_display_level: ConsoleObject.FormatOptions.ErrorDisplayLevel, + ) !void { if (!name.isEmpty() and !message.isEmpty()) { - const display_name: String = if (name.eqlComptime("Error")) String.init("error") else name; + const display_name, const display_message = if (name.eqlComptime("Error")) brk: { + // If `err.code` is set, and `err.message` is of form `{code}: {text}`, + // use the code as the name since `error: ENOENT: no such ...` is + // not as nice looking since it there are two error prefixes. + if (optional_code) |code| if (bun.strings.isAllASCII(code)) { + const has_prefix = switch (message.isUTF16()) { + inline else => |is_utf16| has_prefix: { + const msg_chars = if (is_utf16) message.utf16() else message.latin1(); + // + 1 to ensure the message is a non-empty string. + break :has_prefix msg_chars.len > code.len + ": ".len + 1 and + (if (is_utf16) + // there is no existing function to perform this slice comparison + // []const u16, []const u8 + for (code, msg_chars[0..code.len]) |a, b| { + if (a != b) break false; + } else true + else + bun.strings.eqlLong(msg_chars[0..code.len], code, false)) and + msg_chars[code.len] == ':' and + msg_chars[code.len + 1] == ' '; + }, + }; + if (has_prefix) break :brk .{ + String.init(code), + message.substring(code.len + ": ".len), + }; + }; - try writer.print(comptime Output.prettyFmt("{}: {s}\n", allow_ansi_color), .{ - display_name, - message, - }); + break :brk .{ String.empty, message }; + } else .{ name, message }; + try writer.print(comptime Output.prettyFmt("{}{}\n", allow_ansi_color), .{ error_display_level.formatter(display_name, allow_ansi_color, .include_colon), display_message }); } else if (!name.isEmpty()) { - if (!name.hasPrefixComptime("error")) { - try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{name}); - } else { - try writer.print(comptime Output.prettyFmt("{}\n", allow_ansi_color), .{name}); - } + try writer.print("{}\n", .{error_display_level.formatter(name, allow_ansi_color, .include_colon)}); } else if (!message.isEmpty()) { - try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{message}); + try writer.print(comptime Output.prettyFmt("{}{}\n", allow_ansi_color), .{ error_display_level.formatter(bun.String.empty, allow_ansi_color, .include_colon), message }); } else { - try writer.print(comptime Output.prettyFmt("error\n", allow_ansi_color), .{}); + try writer.print(comptime Output.prettyFmt("{}\n", allow_ansi_color), .{error_display_level.formatter(bun.String.empty, allow_ansi_color, .exclude_colon)}); } } @@ -4358,12 +4505,12 @@ pub const VirtualMachine = struct { /// To satisfy the interface from NewHotReloader() pub fn getLoaders(vm: *VirtualMachine) *bun.options.Loader.HashTable { - return &vm.bundler.options.loaders; + return &vm.transpiler.options.loaders; } /// To satisfy the interface from NewHotReloader() pub fn bustDirCache(vm: *VirtualMachine, path: []const u8) bool { - return vm.bundler.resolver.bustDirCache(path); + return vm.transpiler.resolver.bustDirCache(path); } comptime { @@ -4525,7 +4672,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime .{ .watch = Watcher.init( Reloader, reloader, - this.bundler.fs, + this.transpiler.fs, bun.default_allocator, ) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); @@ -4535,7 +4682,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime .{ .hot = Watcher.init( Reloader, reloader, - this.bundler.fs, + this.transpiler.fs, bun.default_allocator, ) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); @@ -4543,24 +4690,24 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime } }; if (reload_immediately) { - this.bundler.resolver.watcher = Resolver.ResolveWatcher(*Watcher, Watcher.onMaybeWatchDirectory).init(this.bun_watcher.watch); + this.transpiler.resolver.watcher = Resolver.ResolveWatcher(*Watcher, Watcher.onMaybeWatchDirectory).init(this.bun_watcher.watch); } else { - this.bundler.resolver.watcher = Resolver.ResolveWatcher(*Watcher, Watcher.onMaybeWatchDirectory).init(this.bun_watcher.hot); + this.transpiler.resolver.watcher = Resolver.ResolveWatcher(*Watcher, Watcher.onMaybeWatchDirectory).init(this.bun_watcher.hot); } } else { this.bun_watcher = Watcher.init( Reloader, reloader, - this.bundler.fs, + this.transpiler.fs, bun.default_allocator, ) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); Output.panic("Failed to enable File Watcher: {s}", .{@errorName(err)}); }; - this.bundler.resolver.watcher = Resolver.ResolveWatcher(*Watcher, Watcher.onMaybeWatchDirectory).init(this.bun_watcher.?); + this.transpiler.resolver.watcher = Resolver.ResolveWatcher(*Watcher, Watcher.onMaybeWatchDirectory).init(this.bun_watcher.?); } - clear_screen = !this.bundler.env.hasSetNoClearTerminalOnReload(!Output.enable_ansi_colors); + clear_screen = !this.transpiler.env.hasSetNoClearTerminalOnReload(!Output.enable_ansi_colors); reloader.getContext().start() catch @panic("Failed to start File Watcher"); } diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index df80522e28..a015716e91 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -11,7 +11,7 @@ const MutableString = bun.MutableString; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const StoredFileDescriptorType = bun.StoredFileDescriptorType; -const Arena = @import("../mimalloc_arena.zig").Arena; +const Arena = @import("../allocators/mimalloc_arena.zig").Arena; const C = bun.C; const Allocator = std.mem.Allocator; @@ -19,14 +19,14 @@ const IdentityContext = @import("../identity_context.zig").IdentityContext; const Fs = @import("../fs.zig"); const Resolver = @import("../resolver/resolver.zig"); const ast = @import("../import_record.zig"); -const MacroEntryPoint = bun.bundler.MacroEntryPoint; -const ParseResult = bun.bundler.ParseResult; +const MacroEntryPoint = bun.transpiler.MacroEntryPoint; +const ParseResult = bun.transpiler.ParseResult; const logger = bun.logger; const Api = @import("../api/schema.zig").Api; const options = @import("../options.zig"); -const Bundler = bun.Bundler; -const PluginRunner = bun.bundler.PluginRunner; -const ServerEntryPoint = bun.bundler.ServerEntryPoint; +const Transpiler = bun.Transpiler; +const PluginRunner = bun.transpiler.PluginRunner; +const ServerEntryPoint = bun.transpiler.ServerEntryPoint; const js_printer = bun.js_printer; const js_parser = bun.js_parser; const js_ast = bun.JSAst; @@ -68,7 +68,6 @@ const JSPromise = bun.JSC.JSPromise; const JSInternalPromise = bun.JSC.JSInternalPromise; const JSModuleLoader = bun.JSC.JSModuleLoader; const JSPromiseRejectionOperation = bun.JSC.JSPromiseRejectionOperation; -const Exception = bun.JSC.Exception; const ErrorableZigString = bun.JSC.ErrorableZigString; const ZigGlobalObject = bun.JSC.ZigGlobalObject; const VM = bun.JSC.VM; @@ -87,6 +86,7 @@ const Async = bun.Async; const String = bun.String; const debug = Output.scoped(.ModuleLoader, true); +const panic = std.debug.panic; inline fn jsSyntheticModule(comptime name: ResolvedSource.Tag, specifier: String) ResolvedSource { return ResolvedSource{ @@ -120,7 +120,7 @@ fn dumpSourceStringFailiable(vm: *VirtualMachine, specifier: string, written: [] const BunDebugHolder = struct { pub var dir: ?std.fs.Dir = null; - pub var lock: bun.Lock = .{}; + pub var lock: bun.Mutex = .{}; }; BunDebugHolder.lock.lock(); @@ -184,9 +184,9 @@ fn dumpSourceStringFailiable(vm: *VirtualMachine, specifier: string, written: [] \\ "mappings": "{}" \\}} , .{ - bun.fmt.formatJSONStringUTF8(std.fs.path.basename(specifier)), - bun.fmt.formatJSONStringUTF8(specifier), - bun.fmt.formatJSONStringUTF8(source_file), + bun.fmt.formatJSONStringUTF8(std.fs.path.basename(specifier), .{}), + bun.fmt.formatJSONStringUTF8(specifier, .{}), + bun.fmt.formatJSONStringUTF8(source_file, .{}), mappings.formatVLQs(), }); try bufw.flush(); @@ -249,19 +249,21 @@ pub const RuntimeTranspilerStore = struct { this: *RuntimeTranspilerStore, vm: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, + input_specifier: bun.String, path: Fs.Path, - referrer: []const u8, + referrer: bun.String, ) *anyopaque { var job: *TranspilerJob = this.store.get(); const owned_path = Fs.Path.init(bun.default_allocator.dupe(u8, path.text) catch unreachable); const promise = JSC.JSInternalPromise.create(globalObject); job.* = TranspilerJob{ + .non_threadsafe_input_specifier = input_specifier, .path = owned_path, .globalThis = globalObject, - .referrer = bun.default_allocator.dupe(u8, referrer) catch unreachable, + .non_threadsafe_referrer = referrer, .vm = vm, .log = logger.Log.init(bun.default_allocator), - .loader = vm.bundler.options.loader(owned_path.name.ext), + .loader = vm.transpiler.options.loader(owned_path.name.ext), .promise = JSC.Strong.create(JSC.JSValue.fromCell(promise), globalObject), .poll_ref = .{}, .fetcher = TranspilerJob.Fetcher{ @@ -276,7 +278,8 @@ pub const RuntimeTranspilerStore = struct { pub const TranspilerJob = struct { path: Fs.Path, - referrer: []const u8, + non_threadsafe_input_specifier: String, + non_threadsafe_referrer: String, loader: options.Loader, promise: JSC.Strong = .{}, vm: *JSC.VirtualMachine, @@ -305,11 +308,12 @@ pub const RuntimeTranspilerStore = struct { pub fn deinit(this: *TranspilerJob) void { bun.default_allocator.free(this.path.text); - bun.default_allocator.free(this.referrer); this.poll_ref.disable(); this.fetcher.deinit(); this.loader = options.Loader.file; + this.non_threadsafe_input_specifier.deref(); + this.non_threadsafe_referrer.deref(); this.path = Fs.Path.empty; this.log.deinit(); this.promise.deinit(); @@ -330,7 +334,8 @@ pub const RuntimeTranspilerStore = struct { const globalThis = this.globalThis; this.poll_ref.unref(vm); - const referrer = bun.String.createUTF8(this.referrer); + const referrer = this.non_threadsafe_referrer; + this.non_threadsafe_referrer = String.empty; var log = this.log; this.log = logger.Log.init(bun.default_allocator); var resolved_source = this.resolved_source; @@ -339,14 +344,21 @@ pub const RuntimeTranspilerStore = struct { break :brk bun.String.createUTF8(this.path.text); } - break :brk resolved_source.specifier; + const out = this.non_threadsafe_input_specifier; + this.non_threadsafe_input_specifier = String.empty; + + bun.debugAssert(resolved_source.source_url.isEmpty()); + bun.debugAssert(resolved_source.specifier.isEmpty()); + resolved_source.source_url = out.createIfDifferent(this.path.text); + resolved_source.specifier = out.dupeRef(); + break :brk out; }; resolved_source.tag = brk: { if (resolved_source.is_commonjs_module) { const actual_package_json: *PackageJSON = brk2: { // this should already be cached virtually always so it's fine to do this - const dir_info = (vm.bundler.resolver.readDirInfo(this.path.name.dir) catch null) orelse + const dir_info = (vm.transpiler.resolver.readDirInfo(this.path.name.dir) catch null) orelse break :brk .javascript; break :brk2 dir_info.package_json orelse dir_info.enclosing_package_json; @@ -412,13 +424,13 @@ pub const RuntimeTranspilerStore = struct { }; var vm = this.vm; - var bundler: bun.Bundler = undefined; - bundler = vm.bundler; - bundler.setAllocator(allocator); - bundler.setLog(&this.log); - bundler.resolver.opts = bundler.options; - bundler.macro_context = null; - bundler.linker.resolver = &bundler.resolver; + var transpiler: bun.Transpiler = undefined; + transpiler = vm.transpiler; + transpiler.setAllocator(allocator); + transpiler.setLog(&this.log); + transpiler.resolver.opts = transpiler.options; + transpiler.macro_context = null; + transpiler.linker.resolver = &transpiler.resolver; var fd: ?StoredFileDescriptorType = null; var package_json: ?*PackageJSON = null; @@ -441,7 +453,7 @@ pub const RuntimeTranspilerStore = struct { const macro_remappings = if (vm.macro_mode or !vm.has_any_macro_remappings or is_node_override) MacroRemap{} else - bundler.options.macro_remap; + transpiler.options.macro_remap; var fallback_source: logger.Source = undefined; @@ -458,7 +470,7 @@ pub const RuntimeTranspilerStore = struct { vm.main_hash == hash and strings.eqlLong(vm.main, path.text, false); - var parse_options = Bundler.ParseOptions{ + var parse_options = Transpiler.ParseOptions{ .allocator = allocator, .path = path, .loader = loader, @@ -467,12 +479,12 @@ pub const RuntimeTranspilerStore = struct { .file_fd_ptr = &input_file_fd, .file_hash = hash, .macro_remappings = macro_remappings, - .jsx = bundler.options.jsx, - .emit_decorator_metadata = bundler.options.emit_decorator_metadata, + .jsx = transpiler.options.jsx, + .emit_decorator_metadata = transpiler.options.emit_decorator_metadata, .virtual_source = null, .dont_bundle_twice = true, .allow_commonjs = true, - .inject_jest_globals = bundler.options.rewrite_jest_for_tests and is_main, + .inject_jest_globals = transpiler.options.rewrite_jest_for_tests and is_main, .set_breakpoint_on_first_line = vm.debugger != null and vm.debugger.?.set_breakpoint_on_first_line and is_main and @@ -497,7 +509,7 @@ pub const RuntimeTranspilerStore = struct { } } - var parse_result: bun.bundler.ParseResult = bundler.parseMaybeReturnFileOnlyAllowSharedBuffer( + var parse_result: bun.transpiler.ParseResult = transpiler.parseMaybeReturnFileOnlyAllowSharedBuffer( parse_options, null, false, @@ -544,7 +556,6 @@ pub const RuntimeTranspilerStore = struct { } if (cache.entry) |*entry| { - const duped = String.createUTF8(specifier); vm.source_mappings.putMappings(parse_result.source, .{ .list = .{ .items = @constCast(entry.sourcemap), .capacity = entry.sourcemap.len }, .allocator = bun.default_allocator, @@ -565,8 +576,6 @@ pub const RuntimeTranspilerStore = struct { break :brk result; }, }, - .specifier = duped, - .source_url = duped.createIfDifferent(path.text), .hash = 0, .is_commonjs_module = entry.metadata.module_type == .cjs, }; @@ -575,13 +584,10 @@ pub const RuntimeTranspilerStore = struct { } if (parse_result.already_bundled != .none) { - const duped = String.createUTF8(specifier); const bytecode_slice = parse_result.already_bundled.bytecodeSlice(); this.resolved_source = ResolvedSource{ .allocator = null, .source_code = bun.String.createLatin1(parse_result.source.contents), - .specifier = duped, - .source_url = duped.createIfDifferent(path.text), .already_bundled = true, .hash = 0, .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, @@ -595,14 +601,14 @@ pub const RuntimeTranspilerStore = struct { for (parse_result.ast.import_records.slice()) |*import_record_| { var import_record: *bun.ImportRecord = import_record_; - if (JSC.HardcodedModule.Aliases.get(import_record.path.text, bundler.options.target)) |replacement| { + if (JSC.HardcodedModule.Aliases.get(import_record.path.text, transpiler.options.target)) |replacement| { import_record.path.text = replacement.path; import_record.tag = replacement.tag; import_record.is_external_without_side_effects = true; continue; } - if (bundler.options.rewrite_jest_for_tests) { + if (transpiler.options.rewrite_jest_for_tests) { if (strings.eqlComptime( import_record.path.text, "@jest/globals", @@ -642,7 +648,7 @@ pub const RuntimeTranspilerStore = struct { { var mapper = vm.sourceMapHandler(&printer); defer source_code_printer.?.* = printer; - _ = bundler.printWithSourceMap( + _ = transpiler.printWithSourceMap( parse_result, @TypeOf(&printer), &printer, @@ -658,7 +664,6 @@ pub const RuntimeTranspilerStore = struct { dumpSource(this.vm, specifier, &printer); } - const duped = String.createUTF8(specifier); const source_code = brk: { const written = printer.ctx.getWritten(); @@ -684,8 +689,6 @@ pub const RuntimeTranspilerStore = struct { this.resolved_source = ResolvedSource{ .allocator = null, .source_code = source_code, - .specifier = duped, - .source_url = duped.createIfDifferent(path.text), .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, .hash = 0, }; @@ -1380,20 +1383,20 @@ pub const ModuleLoader = struct { const specifier = this.specifier; const old_log = jsc_vm.log; - jsc_vm.bundler.linker.log = log; - jsc_vm.bundler.log = log; - jsc_vm.bundler.resolver.log = log; + jsc_vm.transpiler.linker.log = log; + jsc_vm.transpiler.log = log; + jsc_vm.transpiler.resolver.log = log; jsc_vm.packageManager().log = log; defer { - jsc_vm.bundler.linker.log = old_log; - jsc_vm.bundler.log = old_log; - jsc_vm.bundler.resolver.log = old_log; + jsc_vm.transpiler.linker.log = old_log; + jsc_vm.transpiler.log = old_log; + jsc_vm.transpiler.resolver.log = old_log; jsc_vm.packageManager().log = old_log; } // We _must_ link because: // - node_modules bundle won't be properly - try jsc_vm.bundler.linker.link( + try jsc_vm.transpiler.linker.link( path, &parse_result, jsc_vm.origin, @@ -1409,7 +1412,7 @@ pub const ModuleLoader = struct { { var mapper = jsc_vm.sourceMapHandler(&printer); defer VirtualMachine.source_code_printer.?.* = printer; - _ = try jsc_vm.bundler.printWithSourceMap( + _ = try jsc_vm.transpiler.printWithSourceMap( parse_result, @TypeOf(&printer), &printer, @@ -1479,7 +1482,7 @@ pub const ModuleLoader = struct { var jsc_vm = global.bunVM(); const filename = str.toUTF8(jsc_vm.allocator); defer filename.deinit(); - const loader = jsc_vm.bundler.options.loader(Fs.PathName.init(filename.slice()).ext).toAPI(); + const loader = jsc_vm.transpiler.options.loader(Fs.PathName.init(filename.slice()).ext).toAPI(); if (loader == .file) { return Api.Loader.js; } @@ -1490,7 +1493,6 @@ pub const ModuleLoader = struct { pub fn transpileSourceCode( jsc_vm: *VirtualMachine, specifier: string, - display_specifier: string, referrer: string, input_specifier: String, path: Fs.Path, @@ -1504,10 +1506,23 @@ pub const ModuleLoader = struct { ) !ResolvedSource { const disable_transpilying = comptime flags.disableTranspiling(); + if (comptime disable_transpilying) { + if (!(loader.isJavaScriptLike() or loader == .toml or loader == .text or loader == .json)) { + // Don't print "export default " + return ResolvedSource{ + .allocator = null, + .source_code = bun.String.empty, + .specifier = input_specifier, + .source_url = input_specifier.createIfDifferent(path.text), + .hash = 0, + }; + } + } + switch (loader) { .js, .jsx, .ts, .tsx, .json, .toml, .text => { jsc_vm.transpiled_count += 1; - jsc_vm.bundler.resetStore(); + jsc_vm.transpiler.resetStore(); const hash = JSC.GenericWatcher.getHash(path.text); const is_main = jsc_vm.main.len == path.text.len and jsc_vm.main_hash == hash and @@ -1569,19 +1584,19 @@ pub const ModuleLoader = struct { .sourcemap_allocator = bun.default_allocator, }; - const old = jsc_vm.bundler.log; - jsc_vm.bundler.log = log; - jsc_vm.bundler.linker.log = log; - jsc_vm.bundler.resolver.log = log; - if (jsc_vm.bundler.resolver.package_manager) |pm| { + const old = jsc_vm.transpiler.log; + jsc_vm.transpiler.log = log; + jsc_vm.transpiler.linker.log = log; + jsc_vm.transpiler.resolver.log = log; + if (jsc_vm.transpiler.resolver.package_manager) |pm| { pm.log = log; } defer { - jsc_vm.bundler.log = old; - jsc_vm.bundler.linker.log = old; - jsc_vm.bundler.resolver.log = old; - if (jsc_vm.bundler.resolver.package_manager) |pm| { + jsc_vm.transpiler.log = old; + jsc_vm.transpiler.linker.log = old; + jsc_vm.transpiler.resolver.log = old; + if (jsc_vm.transpiler.resolver.package_manager) |pm| { pm.log = old; } } @@ -1592,7 +1607,7 @@ pub const ModuleLoader = struct { const macro_remappings = if (jsc_vm.macro_mode or !jsc_vm.has_any_macro_remappings or is_node_override) MacroRemap{} else - jsc_vm.bundler.options.macro_remap; + jsc_vm.transpiler.options.macro_remap; var fallback_source: logger.Source = undefined; @@ -1604,7 +1619,7 @@ pub const ModuleLoader = struct { var should_close_input_file_fd = fd == null; var input_file_fd: StoredFileDescriptorType = bun.invalid_fd; - var parse_options = Bundler.ParseOptions{ + var parse_options = Transpiler.ParseOptions{ .allocator = allocator, .path = path, .loader = loader, @@ -1613,12 +1628,12 @@ pub const ModuleLoader = struct { .file_fd_ptr = &input_file_fd, .file_hash = hash, .macro_remappings = macro_remappings, - .jsx = jsc_vm.bundler.options.jsx, - .emit_decorator_metadata = jsc_vm.bundler.options.emit_decorator_metadata, + .jsx = jsc_vm.transpiler.options.jsx, + .emit_decorator_metadata = jsc_vm.transpiler.options.emit_decorator_metadata, .virtual_source = virtual_source, .dont_bundle_twice = true, .allow_commonjs = true, - .inject_jest_globals = jsc_vm.bundler.options.rewrite_jest_for_tests and is_main, + .inject_jest_globals = jsc_vm.transpiler.options.rewrite_jest_for_tests and is_main, .keep_json_and_toml_as_one_statement = true, .allow_bytecode_cache = true, .set_breakpoint_on_first_line = is_main and @@ -1652,7 +1667,7 @@ pub const ModuleLoader = struct { JSC.VM.ReleaseHeapAccess{ .vm = jsc_vm.jsc, .needs_to_release = false }; defer heap_access.acquire(); - break :brk jsc_vm.bundler.parseMaybeReturnFileOnly( + break :brk jsc_vm.transpiler.parseMaybeReturnFileOnly( parse_options, null, return_file_only, @@ -1686,7 +1701,6 @@ pub const ModuleLoader = struct { return transpileSourceCode( jsc_vm, specifier, - display_specifier, referrer, input_specifier, path, @@ -1719,7 +1733,7 @@ pub const ModuleLoader = struct { } } - if (jsc_vm.bundler.log.errors > 0) { + if (jsc_vm.transpiler.log.errors > 0) { give_back_arena = false; return error.ParseError; } @@ -1767,7 +1781,7 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .hash = 0, - .jsvalue_for_export = parse_result.ast.parts.@"[0]"().stmts[0].data.s_expr.value.toJS(allocator, globalObject orelse jsc_vm.global) catch @panic("Unexpected JS error"), + .jsvalue_for_export = parse_result.ast.parts.@"[0]"().stmts[0].data.s_expr.value.toJS(allocator, globalObject orelse jsc_vm.global) catch |e| panic("Unexpected JS error: {s}", .{@errorName(e)}), .tag = .exports_object, }; } @@ -1816,7 +1830,7 @@ pub const ModuleLoader = struct { if (entry.metadata.module_type == .cjs and parse_result.source.path.isFile()) { const actual_package_json: *PackageJSON = package_json orelse brk2: { // this should already be cached virtually always so it's fine to do this - const dir_info = (jsc_vm.bundler.resolver.readDirInfo(parse_result.source.path.name.dir) catch null) orelse + const dir_info = (jsc_vm.transpiler.resolver.readDirInfo(parse_result.source.path.name.dir) catch null) orelse break :brk .javascript; break :brk2 dir_info.package_json orelse dir_info.enclosing_package_json; @@ -1832,11 +1846,11 @@ pub const ModuleLoader = struct { }; } - const start_count = jsc_vm.bundler.linker.import_counter; + const start_count = jsc_vm.transpiler.linker.import_counter; // We _must_ link because: // - node_modules bundle won't be properly - try jsc_vm.bundler.linker.link( + try jsc_vm.transpiler.linker.link( path, &parse_result, jsc_vm.origin, @@ -1852,8 +1866,8 @@ pub const ModuleLoader = struct { if (parse_result.source.contents_is_recycled) { // this shared buffer is about to become owned by the AsyncModule struct - jsc_vm.bundler.resolver.caches.fs.resetSharedBuffer( - jsc_vm.bundler.resolver.caches.fs.sharedBuffer(), + jsc_vm.transpiler.resolver.caches.fs.resetSharedBuffer( + jsc_vm.transpiler.resolver.caches.fs.sharedBuffer(), ); } @@ -1877,8 +1891,8 @@ pub const ModuleLoader = struct { } if (!jsc_vm.macro_mode) - jsc_vm.resolved_count += jsc_vm.bundler.linker.import_counter - start_count; - jsc_vm.bundler.linker.import_counter = 0; + jsc_vm.resolved_count += jsc_vm.transpiler.linker.import_counter - start_count; + jsc_vm.transpiler.linker.import_counter = 0; var printer = source_code_printer.*; printer.ctx.reset(); @@ -1886,7 +1900,7 @@ pub const ModuleLoader = struct { _ = brk: { var mapper = jsc_vm.sourceMapHandler(&printer); - break :brk try jsc_vm.bundler.printWithSourceMap( + break :brk try jsc_vm.transpiler.printWithSourceMap( parse_result, @TypeOf(&printer), &printer, @@ -1916,7 +1930,7 @@ pub const ModuleLoader = struct { if (parse_result.ast.exports_kind == .cjs and parse_result.source.path.isFile()) { const actual_package_json: *PackageJSON = package_json orelse brk2: { // this should already be cached virtually always so it's fine to do this - const dir_info = (jsc_vm.bundler.resolver.readDirInfo(parse_result.source.path.name.dirOrDot()) catch null) orelse + const dir_info = (jsc_vm.transpiler.resolver.readDirInfo(parse_result.source.path.name.dirOrDot()) catch null) orelse break :brk .javascript; break :brk2 dir_info.package_json orelse dir_info.enclosing_package_json; @@ -1965,7 +1979,7 @@ pub const ModuleLoader = struct { // } // } - // var parse_options = Bundler.ParseOptions{ + // var parse_options = Transpiler.ParseOptions{ // .allocator = allocator, // .path = path, // .loader = loader, @@ -1973,10 +1987,10 @@ pub const ModuleLoader = struct { // .file_descriptor = fd, // .file_hash = hash, // .macro_remappings = MacroRemap{}, - // .jsx = jsc_vm.bundler.options.jsx, + // .jsx = jsc_vm.transpiler.options.jsx, // }; - // var parse_result = jsc_vm.bundler.parse( + // var parse_result = jsc_vm.transpiler.parse( // parse_options, // null, // ) orelse { @@ -2021,7 +2035,6 @@ pub const ModuleLoader = struct { return transpileSourceCode( jsc_vm, specifier, - display_specifier, referrer, input_specifier, path, @@ -2078,6 +2091,33 @@ pub const ModuleLoader = struct { }; }, + .html => { + if (flags.disableTranspiling()) { + return ResolvedSource{ + .allocator = null, + .source_code = bun.String.empty, + .specifier = input_specifier, + .source_url = input_specifier.createIfDifferent(path.text), + .hash = 0, + .tag = .esm, + }; + } + + if (globalObject == null) { + return error.NotSupported; + } + + const html_bundle = try JSC.API.HTMLBundle.init(globalObject.?, path.text); + return ResolvedSource{ + .allocator = &jsc_vm.allocator, + .jsvalue_for_export = html_bundle.toJS(globalObject.?), + .specifier = input_specifier, + .source_url = input_specifier.createIfDifferent(path.text), + .hash = 0, + .tag = .export_default_object, + }; + }, + else => { if (virtual_source == null) { if (comptime !disable_transpilying) { @@ -2161,9 +2201,12 @@ pub const ModuleLoader = struct { } } - pub fn normalizeSpecifier(jsc_vm: *VirtualMachine, slice_: string, string_to_use_for_source: *[]const u8) string { + pub fn normalizeSpecifier( + jsc_vm: *VirtualMachine, + slice_: string, + ) struct { string, string } { var slice = slice_; - if (slice.len == 0) return slice; + if (slice.len == 0) return .{ slice, slice }; if (strings.hasPrefix(slice, jsc_vm.origin.host)) { slice = slice[jsc_vm.origin.host.len..]; @@ -2175,13 +2218,13 @@ pub const ModuleLoader = struct { } } - string_to_use_for_source.* = slice; + const specifier = slice; if (strings.indexOfChar(slice, '?')) |i| { slice = slice[0..i]; } - return slice; + return .{ slice, specifier }; } pub export fn Bun__fetchBuiltinModule( @@ -2192,7 +2235,7 @@ pub const ModuleLoader = struct { ret: *ErrorableResolvedSource, ) bool { JSC.markBinding(@src()); - var log = logger.Log.init(jsc_vm.bundler.allocator); + var log = logger.Log.init(jsc_vm.transpiler.allocator); defer log.deinit(); if (ModuleLoader.fetchBuiltinModule( @@ -2216,27 +2259,25 @@ pub const ModuleLoader = struct { pub export fn Bun__transpileFile( jsc_vm: *VirtualMachine, globalObject: *JSC.JSGlobalObject, - specifier_ptr: *const bun.String, - referrer: *const bun.String, + specifier_ptr: *bun.String, + referrer: *bun.String, type_attribute: ?*const bun.String, ret: *ErrorableResolvedSource, allow_promise: bool, ) ?*anyopaque { JSC.markBinding(@src()); - var log = logger.Log.init(jsc_vm.bundler.allocator); + var log = logger.Log.init(jsc_vm.transpiler.allocator); defer log.deinit(); var _specifier = specifier_ptr.toUTF8(jsc_vm.allocator); var referrer_slice = referrer.toUTF8(jsc_vm.allocator); defer _specifier.deinit(); defer referrer_slice.deinit(); - var display_specifier: []const u8 = ""; - const specifier = normalizeSpecifier( + const normalized_file_path_from_specifier, const specifier = normalizeSpecifier( jsc_vm, _specifier.slice(), - &display_specifier, ); - var path = Fs.Path.init(specifier); + var path = Fs.Path.init(normalized_file_path_from_specifier); var virtual_source: ?*logger.Source = null; var virtual_source_to_use: ?logger.Source = null; @@ -2249,7 +2290,7 @@ pub const ModuleLoader = struct { // Deliberately optional. // The concurrent one only handles javascript-like loaders right now. - var loader: ?options.Loader = jsc_vm.bundler.options.loaders.get(path.name.ext); + var loader: ?options.Loader = jsc_vm.transpiler.options.loaders.get(path.name.ext); if (jsc_vm.module_loader.eval_source) |eval_source| { if (strings.endsWithComptime(specifier, bun.pathLiteral("/[eval]"))) { @@ -2276,7 +2317,7 @@ pub const ModuleLoader = struct { path = current_path; } - loader = jsc_vm.bundler.options.loaders.get(current_path.name.ext) orelse .tsx; + loader = jsc_vm.transpiler.options.loaders.get(current_path.name.ext) orelse .tsx; } else { loader = .tsx; } @@ -2313,6 +2354,15 @@ pub const ModuleLoader = struct { loader = .ts; } else if (attribute.eqlComptime("tsx")) { loader = .tsx; + } else if (jsc_vm.transpiler.options.experimental.html and attribute.eqlComptime("html")) { + loader = .html; + } + } + + // If we were going to choose file loader, see if it's a bun.lock + if (loader == null) { + if (strings.eqlComptime(path.name.filename, "bun.lock")) { + loader = .json; } } @@ -2333,8 +2383,9 @@ pub const ModuleLoader = struct { return jsc_vm.transpiler_store.transpile( jsc_vm, globalObject, + specifier_ptr.dupeRef(), path, - referrer_slice.slice(), + referrer.dupeRef(), ); } } @@ -2366,7 +2417,6 @@ pub const ModuleLoader = struct { ModuleLoader.transpileSourceCode( jsc_vm, specifier, - display_specifier, referrer_slice.slice(), specifier_ptr.*, path, @@ -2420,7 +2470,7 @@ pub const ModuleLoader = struct { if (specifier.eqlComptime(Runtime.Runtime.Imports.Name)) { return ResolvedSource{ .allocator = null, - .source_code = String.init(Runtime.Runtime.source_code), + .source_code = String.init(Runtime.Runtime.sourceCode()), .specifier = specifier, .source_url = specifier, .hash = Runtime.Runtime.versionHash(), @@ -2508,6 +2558,7 @@ pub const ModuleLoader = struct { .@"node:stream/consumers" => return jsSyntheticModule(.@"node:stream/consumers", specifier), .@"node:stream/promises" => return jsSyntheticModule(.@"node:stream/promises", specifier), .@"node:stream/web" => return jsSyntheticModule(.@"node:stream/web", specifier), + .@"node:test" => return jsSyntheticModule(.@"node:test", specifier), .@"node:timers" => return jsSyntheticModule(.@"node:timers", specifier), .@"node:timers/promises" => return jsSyntheticModule(.@"node:timers/promises", specifier), .@"node:tls" => return jsSyntheticModule(.@"node:tls", specifier), @@ -2526,6 +2577,12 @@ pub const ModuleLoader = struct { .@"abort-controller" => return jsSyntheticModule(.@"abort-controller", specifier), .undici => return jsSyntheticModule(.undici, specifier), .ws => return jsSyntheticModule(.ws, specifier), + .@"node:_stream_duplex" => return jsSyntheticModule(.@"node:_stream_duplex", specifier), + .@"node:_stream_passthrough" => return jsSyntheticModule(.@"node:_stream_passthrough", specifier), + .@"node:_stream_readable" => return jsSyntheticModule(.@"node:_stream_readable", specifier), + .@"node:_stream_transform" => return jsSyntheticModule(.@"node:_stream_transform", specifier), + .@"node:_stream_wrap" => return jsSyntheticModule(.@"node:_stream_wrap", specifier), + .@"node:_stream_writable" => return jsSyntheticModule(.@"node:_stream_writable", specifier), } } else if (specifier.hasPrefixComptime(js_ast.Macro.namespaceWithColon)) { const spec = specifier.toUTF8(bun.default_allocator); @@ -2607,7 +2664,7 @@ pub const ModuleLoader = struct { const loader = if (loader_ != ._none) options.Loader.fromAPI(loader_) else - jsc_vm.bundler.options.loaders.get(path.name.ext) orelse brk: { + jsc_vm.transpiler.options.loaders.get(path.name.ext) orelse brk: { if (strings.eqlLong(specifier, jsc_vm.main, true)) { break :brk options.Loader.js; } @@ -2622,7 +2679,6 @@ pub const ModuleLoader = struct { ModuleLoader.transpileSourceCode( jsc_vm, specifier_slice.slice(), - specifier_slice.slice(), referrer_slice.slice(), specifier_ptr.*, path, @@ -2675,7 +2731,6 @@ pub const HardcodedModule = enum { @"bun:test", // usually replaced by the transpiler but `await import("bun:" + "test")` has to work @"bun:sql", @"bun:sqlite", - @"bun:internal-for-testing", @"detect-libc", @"node:assert", @"node:assert/strict", @@ -2709,6 +2764,7 @@ pub const HardcodedModule = enum { @"node:stream/promises", @"node:stream/web", @"node:string_decoder", + @"node:test", @"node:timers", @"node:timers/promises", @"node:tls", @@ -2736,6 +2792,15 @@ pub const HardcodedModule = enum { @"node:diagnostics_channel", @"node:dgram", @"node:cluster", + // these are gated behind '--expose-internals' + @"bun:internal-for-testing", + // + @"node:_stream_duplex", + @"node:_stream_passthrough", + @"node:_stream_readable", + @"node:_stream_transform", + @"node:_stream_wrap", + @"node:_stream_writable", /// Already resolved modules go in here. /// This does not remap the module name, it is just a hash table. @@ -2756,6 +2821,8 @@ pub const HardcodedModule = enum { .{ "node-fetch", HardcodedModule.@"node-fetch" }, .{ "isomorphic-fetch", HardcodedModule.@"isomorphic-fetch" }, + .{ "node:test", HardcodedModule.@"node:test" }, + .{ "assert", HardcodedModule.@"node:assert" }, .{ "assert/strict", HardcodedModule.@"node:assert/strict" }, .{ "async_hooks", HardcodedModule.@"node:async_hooks" }, @@ -2810,6 +2877,13 @@ pub const HardcodedModule = enum { .{ "worker_threads", HardcodedModule.@"node:worker_threads" }, .{ "zlib", HardcodedModule.@"node:zlib" }, + .{ "_stream_duplex", .@"node:_stream_duplex" }, + .{ "_stream_passthrough", .@"node:_stream_passthrough" }, + .{ "_stream_readable", .@"node:_stream_readable" }, + .{ "_stream_transform", .@"node:_stream_transform" }, + .{ "_stream_wrap", .@"node:_stream_wrap" }, + .{ "_stream_writable", .@"node:_stream_writable" }, + .{ "undici", HardcodedModule.undici }, .{ "ws", HardcodedModule.ws }, .{ "@vercel/fetch", HardcodedModule.@"@vercel/fetch" }, @@ -2825,7 +2899,7 @@ pub const HardcodedModule = enum { pub const Aliases = struct { // Used by both Bun and Node. - const common_alias_kvs = .{ + const common_alias_kvs = [_]struct { string, Alias }{ .{ "node:assert", .{ .path = "assert" } }, .{ "node:assert/strict", .{ .path = "assert/strict" } }, .{ "node:async_hooks", .{ .path = "async_hooks" } }, @@ -2865,6 +2939,7 @@ pub const HardcodedModule = enum { .{ "node:stream/promises", .{ .path = "stream/promises" } }, .{ "node:stream/web", .{ .path = "stream/web" } }, .{ "node:string_decoder", .{ .path = "string_decoder" } }, + .{ "node:test", .{ .path = "node:test" } }, .{ "node:timers", .{ .path = "timers" } }, .{ "node:timers/promises", .{ .path = "timers/promises" } }, .{ "node:tls", .{ .path = "tls" } }, @@ -2879,6 +2954,22 @@ pub const HardcodedModule = enum { .{ "node:worker_threads", .{ .path = "worker_threads" } }, .{ "node:zlib", .{ .path = "zlib" } }, + // These are returned in builtinModules, but probably not many packages use them so we will just alias them. + .{ "node:_http_agent", .{ .path = "http" } }, + .{ "node:_http_client", .{ .path = "http" } }, + .{ "node:_http_common", .{ .path = "http" } }, + .{ "node:_http_incoming", .{ .path = "http" } }, + .{ "node:_http_outgoing", .{ .path = "http" } }, + .{ "node:_http_server", .{ .path = "http" } }, + .{ "node:_stream_duplex", .{ .path = "_stream_duplex" } }, + .{ "node:_stream_passthrough", .{ .path = "_stream_passthrough" } }, + .{ "node:_stream_readable", .{ .path = "_stream_readable" } }, + .{ "node:_stream_transform", .{ .path = "_stream_transform" } }, + .{ "node:_stream_wrap", .{ .path = "_stream_wrap" } }, + .{ "node:_stream_writable", .{ .path = "_stream_writable" } }, + .{ "node:_tls_wrap", .{ .path = "tls" } }, + .{ "node:_tls_common", .{ .path = "tls" } }, + .{ "assert", .{ .path = "assert" } }, .{ "assert/strict", .{ .path = "assert/strict" } }, .{ "async_hooks", .{ .path = "async_hooks" } }, @@ -2918,6 +3009,7 @@ pub const HardcodedModule = enum { .{ "stream/promises", .{ .path = "stream/promises" } }, .{ "stream/web", .{ .path = "stream/web" } }, .{ "string_decoder", .{ .path = "string_decoder" } }, + // .{ "test", .{ .path = "test" } }, .{ "timers", .{ .path = "timers" } }, .{ "timers/promises", .{ .path = "timers/promises" } }, .{ "tls", .{ .path = "tls" } }, @@ -2944,12 +3036,12 @@ pub const HardcodedModule = enum { .{ "_http_incoming", .{ .path = "http" } }, .{ "_http_outgoing", .{ .path = "http" } }, .{ "_http_server", .{ .path = "http" } }, - .{ "_stream_duplex", .{ .path = "stream" } }, - .{ "_stream_passthrough", .{ .path = "stream" } }, - .{ "_stream_readable", .{ .path = "stream" } }, - .{ "_stream_transform", .{ .path = "stream" } }, - .{ "_stream_writable", .{ .path = "stream" } }, - .{ "_stream_wrap", .{ .path = "stream" } }, + .{ "_stream_duplex", .{ .path = "_stream_duplex" } }, + .{ "_stream_passthrough", .{ .path = "_stream_passthrough" } }, + .{ "_stream_readable", .{ .path = "_stream_readable" } }, + .{ "_stream_transform", .{ .path = "_stream_transform" } }, + .{ "_stream_wrap", .{ .path = "_stream_wrap" } }, + .{ "_stream_writable", .{ .path = "_stream_writable" } }, .{ "_tls_wrap", .{ .path = "tls" } }, .{ "_tls_common", .{ .path = "tls" } }, @@ -2958,7 +3050,7 @@ pub const HardcodedModule = enum { .{ "next/dist/compiled/undici", .{ .path = "undici" } }, }; - const bun_extra_alias_kvs = .{ + const bun_extra_alias_kvs = [_]struct { string, Alias }{ .{ "bun", .{ .path = "bun", .tag = .bun } }, .{ "bun:test", .{ .path = "bun:test", .tag = .bun_test } }, .{ "bun:ffi", .{ .path = "bun:ffi" } }, @@ -2988,10 +3080,9 @@ pub const HardcodedModule = enum { .{ "abort-controller/polyfill", .{ .path = "abort-controller" } }, }; - const node_alias_kvs = .{ + const node_alias_kvs = [_]struct { string, Alias }{ .{ "inspector/promises", .{ .path = "inspector/promises" } }, .{ "node:inspector/promises", .{ .path = "inspector/promises" } }, - .{ "node:test", .{ .path = "node:test" } }, }; const NodeAliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ node_alias_kvs); diff --git a/src/bun.js/modules/BunJSCModule.h b/src/bun.js/modules/BunJSCModule.h index 353e09fac9..562d795cfe 100644 --- a/src/bun.js/modules/BunJSCModule.h +++ b/src/bun.js/modules/BunJSCModule.h @@ -1,6 +1,7 @@ #include "_NativeModule.h" #include "ExceptionOr.h" +#include "JavaScriptCore/CallData.h" #include "JavaScriptCore/ArgList.h" #include "JavaScriptCore/ExceptionScope.h" #include "JavaScriptCore/JSCJSValue.h" @@ -693,7 +694,7 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca samplingProfiler.noticeCurrentThreadAsJSCExecutionThread(); samplingProfiler.start(); - JSValue returnValue = JSC::call(globalObject, function, callData, JSC::jsUndefined(), args); + JSValue returnValue = JSC::profiledCall(globalObject, ProfilingReason::API, function, callData, JSC::jsUndefined(), args); if (returnValue.isEmpty() || throwScope.exception()) { return JSValue::encode(reportFailure(vm)); @@ -889,6 +890,37 @@ JSC_DEFINE_HOST_FUNCTION(functionCodeCoverageForFile, basicBlocks.size(), functionStartOffset, ignoreSourceMap); } +JSC_DEFINE_HOST_FUNCTION(functionEstimateDirectMemoryUsageOf, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSValue value = callFrame->argument(0); + if (value.isCell()) { + auto& vm = globalObject->vm(); + EnsureStillAliveScope alive = value; + return JSValue::encode(jsDoubleNumber(alive.value().asCell()->estimatedSizeInBytes(vm))); + } + + return JSValue::encode(jsNumber(0)); +} + +#if USE(BMALLOC_MEMORY_FOOTPRINT_API) + +#include + +JSC_DEFINE_HOST_FUNCTION(functionPercentAvailableMemoryInUse, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return JSValue::encode(jsDoubleNumber(bmalloc::api::percentAvailableMemoryInUse())); +} + +#else + +JSC_DEFINE_HOST_FUNCTION(functionPercentAvailableMemoryInUse, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return JSValue::encode(jsNull()); +} + +#endif + // clang-format off /* Source for BunJSCModuleTable.lut.h @begin BunJSCModuleTable @@ -918,17 +950,19 @@ JSC_DEFINE_HOST_FUNCTION(functionCodeCoverageForFile, totalCompileTime functionTotalCompileTime Function 0 getProtectedObjects functionGetProtectedObjects Function 0 generateHeapSnapshotForDebugging functionGenerateHeapSnapshotForDebugging Function 0 - profile functionRunProfiler Function 0 + profile functionRunProfiler Function 0 setTimeZone functionSetTimeZone Function 0 serialize functionSerialize Function 0 - deserialize functionDeserialize Function 0 + deserialize functionDeserialize Function 0 + estimateShallowMemoryUsageOf functionEstimateDirectMemoryUsageOf Function 1 + percentAvailableMemoryInUse functionPercentAvailableMemoryInUse Function 0 @end */ namespace Zig { DEFINE_NATIVE_MODULE(BunJSC) { - INIT_NATIVE_MODULE(34); + INIT_NATIVE_MODULE(36); putNativeFn(Identifier::fromString(vm, "callerSourceOrigin"_s), functionCallerSourceOrigin); putNativeFn(Identifier::fromString(vm, "jscDescribe"_s), functionDescribe); @@ -957,11 +991,13 @@ DEFINE_NATIVE_MODULE(BunJSC) putNativeFn(Identifier::fromString(vm, "getProtectedObjects"_s), functionGetProtectedObjects); putNativeFn(Identifier::fromString(vm, "generateHeapSnapshotForDebugging"_s), functionGenerateHeapSnapshotForDebugging); putNativeFn(Identifier::fromString(vm, "profile"_s), functionRunProfiler); - putNativeFn(Identifier::fromString(vm, "codeCoverageForFile"_s), functionCodeCoverageForFile); + putNativeFn(Identifier::fromString(vm, "codeCoverageForFile"_s), functionCodeCoverageForFile); putNativeFn(Identifier::fromString(vm, "setTimeZone"_s), functionSetTimeZone); putNativeFn(Identifier::fromString(vm, "serialize"_s), functionSerialize); putNativeFn(Identifier::fromString(vm, "deserialize"_s), functionDeserialize); - + putNativeFn(Identifier::fromString(vm, "estimateShallowMemoryUsageOf"_s), functionEstimateDirectMemoryUsageOf); + putNativeFn(Identifier::fromString(vm, "percentAvailableMemoryInUse"_s), functionPercentAvailableMemoryInUse); + // Deprecated putNativeFn(Identifier::fromString(vm, "describe"_s), functionDescribe); putNativeFn(Identifier::fromString(vm, "describeArray"_s), functionDescribeArray); diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index e913e937b9..b3973e7eaf 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -284,13 +284,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionFindSourceMap, (JSGlobalObject * globalObject, CallFrame* callFrame)) { - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - throwException( - globalObject, scope, - createError(globalObject, - "module.SourceMap is not yet implemented in Bun"_s)); - return {}; + return JSValue::encode(jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsFunctionSyncBuiltinExports, diff --git a/src/bun.js/modules/NodeUtilTypesModule.h b/src/bun.js/modules/NodeUtilTypesModule.h index cc4117701e..219665c958 100644 --- a/src/bun.js/modules/NodeUtilTypesModule.h +++ b/src/bun.js/modules/NodeUtilTypesModule.h @@ -1,6 +1,8 @@ #pragma once #include "BunClientData.h" +#include "JSDOMWrapper.h" +#include "JSEventTarget.h" #include "JavaScriptCore/CatchScope.h" #include "_NativeModule.h" @@ -452,89 +454,67 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionIsCryptoKey, GET_FIRST_CELL return JSValue::encode(jsBoolean(cell->inherits())); } +JSC_DEFINE_HOST_FUNCTION(jsFunctionIsEventTarget, + (JSC::JSGlobalObject * globalObject, + JSC::CallFrame* callframe)) +{ + GET_FIRST_CELL + return JSValue::encode(jsBoolean(cell->inherits())); +} namespace Zig { + +// Hardcoded module "node:util/types" DEFINE_NATIVE_MODULE(NodeUtilTypes) { - INIT_NATIVE_MODULE(43); + INIT_NATIVE_MODULE(44); putNativeFn(Identifier::fromString(vm, "isExternal"_s), jsFunctionIsExternal); putNativeFn(Identifier::fromString(vm, "isDate"_s), jsFunctionIsDate); - putNativeFn(Identifier::fromString(vm, "isArgumentsObject"_s), - jsFunctionIsArgumentsObject); - putNativeFn(Identifier::fromString(vm, "isBigIntObject"_s), - jsFunctionIsBigIntObject); - putNativeFn(Identifier::fromString(vm, "isBooleanObject"_s), - jsFunctionIsBooleanObject); - putNativeFn(Identifier::fromString(vm, "isNumberObject"_s), - jsFunctionIsNumberObject); - putNativeFn(Identifier::fromString(vm, "isStringObject"_s), - jsFunctionIsStringObject); - putNativeFn(Identifier::fromString(vm, "isSymbolObject"_s), - jsFunctionIsSymbolObject); - putNativeFn(Identifier::fromString(vm, "isNativeError"_s), - jsFunctionIsNativeError); + putNativeFn(Identifier::fromString(vm, "isArgumentsObject"_s), jsFunctionIsArgumentsObject); + putNativeFn(Identifier::fromString(vm, "isBigIntObject"_s), jsFunctionIsBigIntObject); + putNativeFn(Identifier::fromString(vm, "isBooleanObject"_s), jsFunctionIsBooleanObject); + putNativeFn(Identifier::fromString(vm, "isNumberObject"_s), jsFunctionIsNumberObject); + putNativeFn(Identifier::fromString(vm, "isStringObject"_s), jsFunctionIsStringObject); + putNativeFn(Identifier::fromString(vm, "isSymbolObject"_s), jsFunctionIsSymbolObject); + putNativeFn(Identifier::fromString(vm, "isNativeError"_s), jsFunctionIsNativeError); putNativeFn(Identifier::fromString(vm, "isRegExp"_s), jsFunctionIsRegExp); - putNativeFn(Identifier::fromString(vm, "isAsyncFunction"_s), - jsFunctionIsAsyncFunction); - putNativeFn(Identifier::fromString(vm, "isGeneratorFunction"_s), - jsFunctionIsGeneratorFunction); - putNativeFn(Identifier::fromString(vm, "isGeneratorObject"_s), - jsFunctionIsGeneratorObject); + putNativeFn(Identifier::fromString(vm, "isAsyncFunction"_s), jsFunctionIsAsyncFunction); + putNativeFn(Identifier::fromString(vm, "isGeneratorFunction"_s), jsFunctionIsGeneratorFunction); + putNativeFn(Identifier::fromString(vm, "isGeneratorObject"_s), jsFunctionIsGeneratorObject); putNativeFn(Identifier::fromString(vm, "isPromise"_s), jsFunctionIsPromise); putNativeFn(Identifier::fromString(vm, "isMap"_s), jsFunctionIsMap); putNativeFn(Identifier::fromString(vm, "isSet"_s), jsFunctionIsSet); - putNativeFn(Identifier::fromString(vm, "isMapIterator"_s), - jsFunctionIsMapIterator); - putNativeFn(Identifier::fromString(vm, "isSetIterator"_s), - jsFunctionIsSetIterator); + putNativeFn(Identifier::fromString(vm, "isMapIterator"_s), jsFunctionIsMapIterator); + putNativeFn(Identifier::fromString(vm, "isSetIterator"_s), jsFunctionIsSetIterator); putNativeFn(Identifier::fromString(vm, "isWeakMap"_s), jsFunctionIsWeakMap); putNativeFn(Identifier::fromString(vm, "isWeakSet"_s), jsFunctionIsWeakSet); - putNativeFn(Identifier::fromString(vm, "isArrayBuffer"_s), - jsFunctionIsArrayBuffer); + putNativeFn(Identifier::fromString(vm, "isArrayBuffer"_s), jsFunctionIsArrayBuffer); putNativeFn(Identifier::fromString(vm, "isDataView"_s), jsFunctionIsDataView); - putNativeFn(Identifier::fromString(vm, "isSharedArrayBuffer"_s), - jsFunctionIsSharedArrayBuffer); + putNativeFn(Identifier::fromString(vm, "isSharedArrayBuffer"_s), jsFunctionIsSharedArrayBuffer); putNativeFn(Identifier::fromString(vm, "isProxy"_s), jsFunctionIsProxy); - putNativeFn(Identifier::fromString(vm, "isModuleNamespaceObject"_s), - jsFunctionIsModuleNamespaceObject); - putNativeFn(Identifier::fromString(vm, "isAnyArrayBuffer"_s), - jsFunctionIsAnyArrayBuffer); - putNativeFn(Identifier::fromString(vm, "isBoxedPrimitive"_s), - jsFunctionIsBoxedPrimitive); - putNativeFn(Identifier::fromString(vm, "isArrayBufferView"_s), - jsFunctionIsArrayBufferView); - putNativeFn(Identifier::fromString(vm, "isTypedArray"_s), - jsFunctionIsTypedArray); - putNativeFn(Identifier::fromString(vm, "isUint8Array"_s), - jsFunctionIsUint8Array); - putNativeFn(Identifier::fromString(vm, "isUint8ClampedArray"_s), - jsFunctionIsUint8ClampedArray); - putNativeFn(Identifier::fromString(vm, "isUint16Array"_s), - jsFunctionIsUint16Array); - putNativeFn(Identifier::fromString(vm, "isUint32Array"_s), - jsFunctionIsUint32Array); - putNativeFn(Identifier::fromString(vm, "isInt8Array"_s), - jsFunctionIsInt8Array); - putNativeFn(Identifier::fromString(vm, "isInt16Array"_s), - jsFunctionIsInt16Array); - putNativeFn(Identifier::fromString(vm, "isInt32Array"_s), - jsFunctionIsInt32Array); - putNativeFn(Identifier::fromString(vm, "isFloat16Array"_s), - jsFunctionIsFloat16Array); - putNativeFn(Identifier::fromString(vm, "isFloat32Array"_s), - jsFunctionIsFloat32Array); - putNativeFn(Identifier::fromString(vm, "isFloat64Array"_s), - jsFunctionIsFloat64Array); - putNativeFn(Identifier::fromString(vm, "isBigInt64Array"_s), - jsFunctionIsBigInt64Array); - putNativeFn(Identifier::fromString(vm, "isBigUint64Array"_s), - jsFunctionIsBigUint64Array); - putNativeFn(Identifier::fromString(vm, "isKeyObject"_s), - jsFunctionIsKeyObject); - putNativeFn(Identifier::fromString(vm, "isCryptoKey"_s), - jsFunctionIsCryptoKey); + putNativeFn(Identifier::fromString(vm, "isModuleNamespaceObject"_s), jsFunctionIsModuleNamespaceObject); + putNativeFn(Identifier::fromString(vm, "isAnyArrayBuffer"_s), jsFunctionIsAnyArrayBuffer); + putNativeFn(Identifier::fromString(vm, "isBoxedPrimitive"_s), jsFunctionIsBoxedPrimitive); + putNativeFn(Identifier::fromString(vm, "isArrayBufferView"_s), jsFunctionIsArrayBufferView); + putNativeFn(Identifier::fromString(vm, "isTypedArray"_s), jsFunctionIsTypedArray); + putNativeFn(Identifier::fromString(vm, "isUint8Array"_s), jsFunctionIsUint8Array); + putNativeFn(Identifier::fromString(vm, "isUint8ClampedArray"_s), jsFunctionIsUint8ClampedArray); + putNativeFn(Identifier::fromString(vm, "isUint16Array"_s), jsFunctionIsUint16Array); + putNativeFn(Identifier::fromString(vm, "isUint32Array"_s), jsFunctionIsUint32Array); + putNativeFn(Identifier::fromString(vm, "isInt8Array"_s), jsFunctionIsInt8Array); + putNativeFn(Identifier::fromString(vm, "isInt16Array"_s), jsFunctionIsInt16Array); + putNativeFn(Identifier::fromString(vm, "isInt32Array"_s), jsFunctionIsInt32Array); + putNativeFn(Identifier::fromString(vm, "isFloat16Array"_s), jsFunctionIsFloat16Array); + putNativeFn(Identifier::fromString(vm, "isFloat32Array"_s), jsFunctionIsFloat32Array); + putNativeFn(Identifier::fromString(vm, "isFloat64Array"_s), jsFunctionIsFloat64Array); + putNativeFn(Identifier::fromString(vm, "isBigInt64Array"_s), jsFunctionIsBigInt64Array); + putNativeFn(Identifier::fromString(vm, "isBigUint64Array"_s), jsFunctionIsBigUint64Array); + putNativeFn(Identifier::fromString(vm, "isKeyObject"_s), jsFunctionIsKeyObject); + putNativeFn(Identifier::fromString(vm, "isCryptoKey"_s), jsFunctionIsCryptoKey); + putNativeFn(Identifier::fromString(vm, "isEventTarget"_s), jsFunctionIsEventTarget); RETURN_NATIVE_MODULE(); } + } // namespace Zig diff --git a/src/bun.js/modules/ObjectModule.cpp b/src/bun.js/modules/ObjectModule.cpp index 9d3a5fe9e9..fddf20cf0a 100644 --- a/src/bun.js/modules/ObjectModule.cpp +++ b/src/bun.js/modules/ObjectModule.cpp @@ -84,6 +84,13 @@ generateJSValueModuleSourceCode(JSC::JSGlobalObject* globalObject, value.getObject()); } + return generateJSValueExportDefaultObjectSourceCode(globalObject, value); +} + +JSC::SyntheticSourceProvider::SyntheticSourceGenerator +generateJSValueExportDefaultObjectSourceCode(JSC::JSGlobalObject* globalObject, + JSC::JSValue value) +{ if (value.isCell()) gcProtectNullTolerant(value.asCell()); return [value](JSC::JSGlobalObject* lexicalGlobalObject, diff --git a/src/bun.js/modules/ObjectModule.h b/src/bun.js/modules/ObjectModule.h index 6988e9a94e..9e4807a8c4 100644 --- a/src/bun.js/modules/ObjectModule.h +++ b/src/bun.js/modules/ObjectModule.h @@ -16,4 +16,8 @@ JSC::SyntheticSourceProvider::SyntheticSourceGenerator generateJSValueModuleSourceCode(JSC::JSGlobalObject* globalObject, JSC::JSValue value); +JSC::SyntheticSourceProvider::SyntheticSourceGenerator +generateJSValueExportDefaultObjectSourceCode(JSC::JSGlobalObject* globalObject, + JSC::JSValue value); + } // namespace Zig diff --git a/src/bun.js/node/assert/myers_diff.zig b/src/bun.js/node/assert/myers_diff.zig new file mode 100644 index 0000000000..d6eb316c21 --- /dev/null +++ b/src/bun.js/node/assert/myers_diff.zig @@ -0,0 +1,631 @@ +//! ## IMPORTANT NOTE +//! +//! Do _NOT_ import from "root" in this file! Do _NOT_ use the Bun object in this file! +//! +//! This file has tests defined in it which _cannot_ be run if `@import("root")` is used! +//! +//! Run tests with `:zig test %` +const std = @import("std"); +const builtin = @import("builtin"); +const mem = std.mem; +const Allocator = mem.Allocator; +const stackFallback = std.heap.stackFallback; +const assert = std.debug.assert; +const print = std.debug.print; + +/// Comptime diff configuration. Defaults are usually sufficient. +pub const Options = struct { + /// Guesstimate for the number of bytes `expected` and `actual` will be. + /// Defaults to 256. + /// + /// Used to reserve space on the stack for the edit graph. + avg_input_size: comptime_int = 256, + /// How much stack space to reserve for edit trace frames. Defaults to 64. + initial_trace_capacity: comptime_int = 64, + /// When `true`, string lines that are only different by a trailing comma + /// are considered equal. Not used when comparing chars. Defaults to + /// `false`. + check_comma_disparity: bool = false, +}; + +// By limiting maximum string and buffer lengths, we can store u32s in the +// edit graph instead of usize's, halving our memory footprint. The +// downside is that `(2 * (actual.len + expected.len))` must be less than +// 4Gb. If this becomes a problem in real user scenarios, we can adjust this. +// +// Note that overflows are much more likely to occur in real user scenarios +// than in our own testing, so overflow checks _must_ be handled. Do _not_ +// use `assert` unless you also use `@setRuntimeSafety(true)`. +// +// TODO: make this configurable in `Options`? +const MAXLEN = std.math.maxInt(u32); +// Type aliasing to make future refactors easier +const uint = u32; +const int = i64; // must be large enough to hold all valid values of `uint` w/o overflow. + +/// diffs two sets of lines, returning the minimal number of edits needed to +/// make them equal. +/// +/// Lines may be string slices or chars. Derived from node's implementation of +/// the Myers' diff algorithm. +/// +/// ## Example +/// ```zig +/// const myers_diff = @import("inode/assert/myers_diff.zig"); +/// const StrDiffer = myers_diff.Differ([]const u8, .{}); +/// const actual = &[_][]const u8{ +/// "foo", +/// "bar", +/// "baz", +/// }; +/// const expected = &[_][]const u8{ +/// "foo", +/// "barrr", +/// "baz", +/// }; +/// const diff = try StrDiffer.diff(allocator, actual, expected); +/// ``` +/// +/// TODO: support non-ASCII UTF-8 characters. +/// +/// ## References +/// - [Node- `myers_diff.js`](https://github.com/nodejs/node/blob/main/lib/internal/assert/myers_diff.js) +/// - [An O(ND) Difference Algorithm and Its Variations](http://www.xmailserver.org/diff2.pdf) +pub fn Differ(comptime Line: type, comptime opts: Options) type { + const eql: LineCmp(Line) = switch (Line) { + // char-by-char comparison. u16 is for utf16 + u8, u16 => blk: { + const gen = struct { + pub fn eql(a: Line, b: Line) bool { + return a == b; + } + }; + break :blk gen.eql; + }, + []const u8, + []u8, + [:0]const u8, + [:0]u8, + []const u16, + []u16, + [:0]const u16, + [:0]u16, + => blk: { + const gen = struct { + pub fn eql(a: Line, b: Line) bool { + return areStrLinesEqual(Line, a, b, opts.check_comma_disparity); + } + }; + break :blk gen.eql; + }, + else => @compileError("Differ can only compare lines of chars or strings. Received: " ++ @typeName(Line)), + }; + + return DifferWithEql(Line, opts, eql); +} + +/// Like `Differ`, but allows the user to provide a custom equality function. +pub fn DifferWithEql(comptime Line: type, comptime opts: Options, comptime areLinesEql: LineCmp(Line)) type { + + // `V = [-MAX, MAX]`. + const graph_initial_size = comptime guess: { + const size_wanted = 2 * opts.avg_input_size + 1; + break :guess size_wanted + (size_wanted % 8); // 8-byte align + }; + if (graph_initial_size > MAXLEN) @compileError("Input guess size is too large. The edit graph must be 32-bit addressable."); + + return struct { + pub const eql = areLinesEql; + pub const LineType = Line; + + /// Compute the shortest edit path (diff) between two sets of lines. + /// + /// Returned `Diff` objects borrow from the input slices. Both `actual` + /// and `expected` must outlive them. + /// + /// ## References + /// - [Node- `myers_diff.js`](https://github.com/nodejs/node/blob/main/lib/internal/assert/myers_diff.js) + /// - [An O(ND) Difference Algorithm and Its Variations](http://www.xmailserver.org/diff2.pdf) + pub fn diff(bun_allocator: Allocator, actual: []const Line, expected: []const Line) Error!DiffList(Line) { + + // Edit graph's allocator + var graph_stack_alloc = stackFallback(graph_initial_size, bun_allocator); + const graph_alloc = graph_stack_alloc.get(); + + // Match point trace's allocator + var trace_stack_alloc = stackFallback(opts.initial_trace_capacity, bun_allocator); + const trace_alloc = trace_stack_alloc.get(); + + // const MAX \in [0, M+N] + // let V: int array = [-MAX..MAX]. V is a flattened representation of the edit graph. + const max: uint, const graph_size: uint = blk: { + // This is to preserve overflow protections even when runtime safety + // checks are disabled. We don't know what kind of stuff users are + // diffing in the wild. + const _max: usize = actual.len + expected.len; + const _graph_size = (2 * _max) + 1; + + if (_max > MAXLEN) return Error.InputsTooLarge; + if (_graph_size > MAXLEN) return Error.DiffTooLarge; + + // const m: + + break :blk .{ @intCast(_max), @intCast(_graph_size) }; + }; + + var graph = try graph_alloc.alloc(uint, graph_size); + defer graph_alloc.free(graph); + @memset(graph, 0); + graph.len = graph_size; + + var trace = std.ArrayList([]const uint).init(trace_alloc); + // reserve enough space for each frame to avoid realloc on ptr list. Lists may end up in the heap, but + // this list is at the very from (and ∴ on stack). + try trace.ensureTotalCapacityPrecise(max + 1); + defer { + for (trace.items) |frame| { + trace_alloc.free(frame); + } + trace.deinit(); + } + + // ================================================================ + // ==================== actual implementation ===================== + // ================================================================ + + for (0..max + 1) |_diff_level| { + const diff_level: int = @intCast(_diff_level); // why is this always usize? + // const new_trace = try TraceFrame.initCapacity(trace_alloc, graph.len); + const new_trace = try trace_alloc.dupe(uint, graph); + trace.appendAssumeCapacity(new_trace); + + const diag_start: int = -@as(int, @intCast(diff_level)); + const diag_end: int = @intCast(diff_level); + + // for k ← -D in steps of 2 do + var diag_idx = diag_start; + while (diag_idx <= diag_end) : (diag_idx += 2) { + // if k = -D or K ≠ D and V[k-1] < V[k+1] then + // x ← V[k+1] + // else + // x ← V[k-1] + 1 + assert(diag_idx + max >= 0); // sanity check. Fine to be stripped in release. + const k: uint = u(diag_idx + max); + + const uk = u(k); + var x = if (diag_idx == diag_start or + (diag_idx != diag_end and graph[uk - 1] < graph[uk + 1])) + graph[uk + 1] + else + graph[uk - 1] + 1; + + // y = x - diag_idx + var y: usize = blk: { + const x2: int = @intCast(x); + const y: int = x2 - diag_idx; + assert(y >= 0 and y <= MAXLEN); // sanity check. Fine to be stripped in release. + break :blk @intCast(y); + }; + + while (x < actual.len and y < expected.len and eql(actual[x], expected[y])) { + x += 1; + y += 1; + } + graph[k] = @intCast(x); + if (x >= actual.len and y >= expected.len) { + // todo: arena + return backtrack(bun_allocator, &trace, actual, expected); + } + } + } + + @panic("unreachable. Diffing should always reach the end of either `actual` or `expected` first."); + } + + fn backtrack( + allocator: Allocator, + trace: *const std.ArrayList([]const uint), + actual: []const Line, + expected: []const Line, + ) Error!DiffList(Line) { + const max = i(actual.len + expected.len); + var x = i(actual.len); + var y = i(expected.len); + + var result = DiffList(Line).init(allocator); + if (trace.items.len == 0) return result; + + //for (let diffLevel = trace.length - 1; diffLevel >= 0; diffLevel--) { + var diff_level: usize = trace.items.len; + while (diff_level > 0) { + diff_level -= 1; + const graph = trace.items[diff_level]; + const diagonal_index = x - y; + + const diag_offset = u(diagonal_index + max); + const prev_diagonal_index: int = if (diagonal_index == -i(diff_level) or + (diagonal_index != diff_level and graph[u(diag_offset - 1)] < graph[u(diag_offset + 1)])) + diagonal_index + 1 + else + diagonal_index - 1; + + const prev_x: int = i(graph[u(prev_diagonal_index + i(max))]); // v[prevDiagonalIndex + max] + const prev_y: int = i(prev_x) - prev_diagonal_index; + + try result.ensureUnusedCapacity(u(@max(x - prev_x, y - prev_y))); + while (x > prev_x and y > prev_y) { + const line: Line = blk: { + if (@typeInfo(Line) == .Pointer and comptime opts.check_comma_disparity) { + const actual_el = actual[u(x) - 1]; + // actual[x-1].endsWith(',') + break :blk if (actual_el[actual_el.len - 1] == ',') + actual[u(x) - 1] + else + expected[u(y) - 1]; + } else { + break :blk actual[u(x) - 1]; + } + }; + + result.appendAssumeCapacity(.{ .kind = .equal, .value = line }); + x -= 1; + y -= 1; + } + if (diff_level > 0) { + if (x > prev_x) { + try result.append(.{ .kind = .insert, .value = actual[u(x) - 1] }); + x -= 1; + } else { + try result.append(.{ .kind = .delete, .value = expected[u(y) - 1] }); + y -= 1; + } + } + } + + return result; + } + + // shorthands for int casting since I'm tired of writing `@as(int, @intCast(x))` everywhere + inline fn u(n: anytype) uint { + return @intCast(n); + } + inline fn us(n: anytype) usize { + return @intCast(n); + } + inline fn i(n: anytype) int { + return @intCast(n); + } + }; +} + +pub fn printDiff(T: type, diffs: std.ArrayList(Diff(T))) !void { + const stdout = if (builtin.is_test) + std.io.getStdErr().writer() + else + std.io.getStdOut().writer(); + + const specifier = switch (T) { + u8 => "c", + u32 => "u", + []const u8 => "s", + else => @compileError("printDiff can only print chars and strings. Received: " ++ @typeName(T)), + }; + + for (0..diffs.items.len) |idx| { + const d = diffs.items[diffs.items.len - (idx + 1)]; + const op: u8 = switch (d.kind) { + inline .equal => ' ', + inline .insert => '+', + inline .delete => '-', + }; + try stdout.writeByte(op); + try stdout.print(" {" ++ specifier ++ "}\n", .{d.value}); + } +} + +// ============================================================================= +// ============================ EQUALITY FUNCTIONS ============================ +// ============================================================================= + +fn areCharsEqual(comptime T: type, a: T, b: T) bool { + return a == b; +} + +fn areLinesEqual(comptime T: type, a: T, b: T, comptime check_comma_disparity: bool) bool { + return switch (T) { + u8, u32 => a == b, + []const u8, []u8, [:0]const u8, [:0]u8 => areStrLinesEqual(T, a, b, check_comma_disparity), + else => @compileError("areLinesEqual can only compare chars and strings. Received: " ++ @typeName(T)), + }; +} + +fn areStrLinesEqual(comptime T: type, a: T, b: T, comptime check_comma_disparity: bool) bool { + // Hypothesis: unlikely to be the same, since assert.equal, etc. is rarely + // used to compare the same object. May be true on shallow copies. + // TODO: check Godbolt + // if (a.ptr == b.ptr) return true; + + // []const u8 -> u8 + const info = @typeInfo(T); + const ChildType = info.Pointer.child; + + if (comptime !check_comma_disparity) { + return mem.eql(ChildType, a, b); + } + + const largest, const smallest = if (a.len > b.len) .{ a, b } else .{ b, a }; + return switch (largest.len - smallest.len) { + inline 0 => mem.eql(ChildType, a, b), + inline 1 => largest[largest.len - 1] == ',' and mem.eql(ChildType, largest[0..smallest.len], smallest), // 'foo,' == 'foo' + else => false, + }; +} + +// ============================================================================= +// =================================== TYPES =================================== +// ============================================================================= + +/// Generic equality function. Returns `true` if two lines are equal. +pub fn LineCmp(Line: type) type { + return fn (a: Line, b: Line) bool; +} + +pub const Error = error{ + DiffTooLarge, + InputsTooLarge, +} || Allocator.Error; + +const TraceFrame = std.ArrayListUnmanaged(u8); + +pub const DiffKind = enum { + insert, + delete, + equal, + + pub fn format(value: DiffKind, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + return switch (value) { + .insert => writer.writeByte('+'), + .delete => writer.writeByte('-'), + .equal => writer.writeByte(' '), + }; + } +}; + +pub fn Diff(comptime T: type) type { + return struct { + kind: DiffKind, + value: T, + + const Self = @This(); + pub fn eql(self: Self, other: Self) bool { + return self.kind == other.kind and mem.eql(T, self.value, other.value); + } + + /// pub fn format(value: ?, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void + pub fn format(value: anytype, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + const specifier = switch (T) { + u8 => "c", + u32 => "u", + []const u8, [:0]const u8, []u8, [:0]u8 => "s", + else => @compileError("printDiff can only print chars and strings. Received: " ++ @typeName(T)), + }; + return writer.print("{} {" ++ specifier ++ "}", .{ value.kind, value.value }); + } + }; +} + +pub fn DiffList(comptime T: type) type { + return std.ArrayList(Diff(T)); +} + +// ============================================================================= + +const t = std.testing; +test areLinesEqual { + // check_comma_disparity is never respected when comparing chars + try t.expect(areLinesEqual(u8, 'a', 'a', false)); + try t.expect(areLinesEqual(u8, 'a', 'a', true)); + try t.expect(!areLinesEqual(u8, ',', 'a', false)); + try t.expect(!areLinesEqual(u8, ',', 'a', true)); + + // strings w/o comma check + try t.expect(areLinesEqual([]const u8, "", "", false)); + try t.expect(areLinesEqual([]const u8, "a", "a", false)); + try t.expect(areLinesEqual([]const u8, "Bun", "Bun", false)); + try t.expect(areLinesEqual([]const u8, "😤", "😤", false)); + // not equal + try t.expect(!areLinesEqual([]const u8, "", "a", false)); + try t.expect(!areLinesEqual([]const u8, "", " ", false)); + try t.expect(!areLinesEqual([]const u8, "\n", "\t", false)); + try t.expect(!areLinesEqual([]const u8, "bun", "Bun", false)); + try t.expect(!areLinesEqual([]const u8, "😤", "😩", false)); + + // strings w/ comma check + try t.expect(areLinesEqual([]const u8, "", "", true)); + try t.expect(areLinesEqual([]const u8, "", ",", true)); + try t.expect(areLinesEqual([]const u8, " ", " ,", true)); + try t.expect(areLinesEqual([]const u8, "I am speed", "I am speed", true)); + try t.expect(areLinesEqual([]const u8, "I am speed,", "I am speed", true)); + try t.expect(areLinesEqual([]const u8, "I am speed", "I am speed,", true)); + try t.expect(areLinesEqual([]const u8, "😤", "😤", false)); + // try t.expect(areLinesEqual([]const u8, "😤", "😤,", false)); + // try t.expect(areLinesEqual([]const u8, "😤,", "😤", false)); + // not equal + try t.expect(!areLinesEqual([]const u8, "", "Bun", true)); + try t.expect(!areLinesEqual([]const u8, "bun", "Bun", true)); + try t.expect(!areLinesEqual([]const u8, ",Bun", "Bun", true)); + try t.expect(!areLinesEqual([]const u8, "Bun", ",Bun", true)); + try t.expect(!areLinesEqual([]const u8, "", " ,", true)); + try t.expect(!areLinesEqual([]const u8, " ", " , ", true)); + try t.expect(!areLinesEqual([]const u8, "I, am speed", "I am speed", true)); + try t.expect(!areLinesEqual([]const u8, ",😤", "😤", true)); +} + +// const CharList = DiffList(u8); +// const CDiff = Diff(u8); +// const CharDiffer = Differ(u8, .{}); + +// fn testCharDiff(actual: []const u8, expected: []const u8, expected_diff: []const Diff(u8)) !void { +// const allocator = t.allocator; +// const actual_diff = try CharDiffer.diff(allocator, actual, expected); +// defer actual_diff.deinit(); +// try t.expectEqualSlices(Diff(u8), expected_diff, actual_diff.items); +// } + +// test CharDiffer { +// const TestCase = std.meta.Tuple(&[_]type{ []const CDiff, []const u8, []const u8 }); +// const test_cases = &[_]TestCase{ +// .{ &[_]CDiff{}, "foo", "foo" }, +// }; +// for (test_cases) |test_case| { +// const expected_diff, const actual, const expected = test_case; +// try testCharDiff(actual, expected, expected_diff); +// } +// } + +const StrDiffer = Differ([]const u8, .{ .check_comma_disparity = true }); +test StrDiffer { + const a = t.allocator; + inline for (.{ + // .{ "foo", "foo" }, + // .{ "foo", "bar" }, + .{ + // actual + \\[ + \\ 1, + \\ 2, + \\ 3, + \\ 4, + \\ 5, + \\ 6, + \\ 7 + \\] + , + // expected + \\[ + \\ 1, + \\ 2, + \\ 3, + \\ 4, + \\ 5, + \\ 9, + \\ 7 + \\] + }, + // // remove line + // .{ + // \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + // \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + // \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + // \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + // \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + // \\culpa qui officia deserunt mollit anim id est laborum. + // , + // \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + // \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + // \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + // \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + // \\culpa qui officia deserunt mollit anim id est laborum. + // , + // }, + // // add some line + // .{ + // \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + // \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + // \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + // \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + // \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + // \\culpa qui officia deserunt mollit anim id est laborum. + // , + // \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + // \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + // \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + // \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + // \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + // \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + // \\culpa qui officia deserunt mollit anim id est laborum. + // \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + // , + // }, + // // modify lines + // .{ + // \\foo + // \\bar + // \\baz + // , + // \\foo + // \\barrr + // \\baz + // }, + // .{ + // \\foooo + // \\bar + // \\baz + // , + // \\foo + // \\bar + // \\baz + // }, + // .{ + // \\foo + // \\bar + // \\baz + // , + // \\foo + // \\bar + // \\baz + // }, + // .{ + // \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + // \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + // \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + // \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + // \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + // \\culpa qui officia deserunt mollit anim id est laborum. + // , + // \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor modified + // \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + // \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + // \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + // \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in also modified + // \\culpa qui officia deserunt mollit anim id est laborum. + // , + // }, + }) |thing| { + var actual = try split(u8, a, thing[0]); + var expected = try split(u8, a, thing[1]); + defer { + actual.deinit(a); + expected.deinit(a); + } + var d = try StrDiffer.diff(a, actual.items, expected.items); + defer d.deinit(); + for (d.items) |diff| { + std.debug.print("{}\n", .{diff}); + } + } +} + +pub fn split( + comptime T: type, + alloc: Allocator, + s: []const T, +) Allocator.Error!std.ArrayListUnmanaged([]const T) { + comptime { + if (T != u8 and T != u16) { + @compileError("Split only supports latin1, utf8, and utf16. Received: " ++ @typeName(T)); + } + } + const newline: T = if (comptime T == u8) '\n' else '\n'; + // + // thing + var it = std.mem.splitScalar(T, s, newline); + var lines = std.ArrayListUnmanaged([]const T){}; + try lines.ensureUnusedCapacity(alloc, s.len >> 4); + errdefer lines.deinit(alloc); + while (it.next()) |l| { + try lines.append(alloc, l); + } + + return lines; +} diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig index fd019882fb..9953527a6e 100644 --- a/src/bun.js/node/fs_events.zig +++ b/src/bun.js/node/fs_events.zig @@ -1,7 +1,7 @@ const std = @import("std"); const bun = @import("root").bun; const Environment = bun.Environment; -const Mutex = @import("../../lock.zig").Lock; +const Mutex = bun.Mutex; const sync = @import("../../sync.zig"); const Semaphore = sync.Semaphore; const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index c252f72954..0ebf557bf1 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -1,6 +1,80 @@ import { define } from "../../codegen/class-definitions"; export default [ + define({ + name: "DNSResolver", + construct: false, + noConstructor: true, + finalize: true, + configurable: false, + klass: {}, + proto: { + setServers: { + fn: "setServers", + length: 1, + }, + getServers: { + fn: "getServers", + length: 0, + }, + resolve: { + fn: "resolve", + length: 3, + }, + resolveSrv: { + fn: "resolveSrv", + length: 1, + }, + resolveTxt: { + fn: "resolveTxt", + length: 1, + }, + resolveSoa: { + fn: "resolveSoa", + length: 1, + }, + resolveNaptr: { + fn: "resolveNaptr", + length: 1, + }, + resolveMx: { + fn: "resolveMx", + length: 1, + }, + resolveCaa: { + fn: "resolveCaa", + length: 1, + }, + resolveNs: { + fn: "resolveNs", + length: 1, + }, + resolvePtr: { + fn: "resolvePtr", + length: 1, + }, + resolveCname: { + fn: "resolveCname", + length: 1, + }, + resolveAny: { + fn: "resolveAny", + length: 1, + }, + setLocalAddress: { + fn: "setLocalAddress", + length: 1, + }, + cancel: { + fn: "cancel", + length: 0, + }, + reverse: { + fn: "reverse", + length: 1, + }, + }, + }), define({ name: "FSWatcher", construct: false, @@ -497,8 +571,6 @@ export default [ mkdtempSync: { fn: "mkdtempSync", length: 2 }, open: { fn: "open", length: 4 }, openSync: { fn: "openSync", length: 3 }, - opendir: { fn: "opendir", length: 3 }, - opendirSync: { fn: "opendirSync", length: 2 }, readdir: { fn: "readdir", length: 3 }, readdirSync: { fn: "readdirSync", length: 2 }, read: { fn: "read", length: 6 }, @@ -536,6 +608,8 @@ export default [ writeSync: { fn: "writeSync", length: 5 }, writev: { fn: "writev", length: 4 }, writevSync: { fn: "writevSync", length: 3 }, + realpathNative: { fn: "realpathNative", length: 3 }, + realpathNativeSync: { fn: "realpathNativeSync", length: 3 }, // TODO: // Dir: { fn: 'Dir', length: 3 }, Dirent: { getter: "getDirent" }, diff --git a/src/bun.js/node/node_assert.zig b/src/bun.js/node/node_assert.zig new file mode 100644 index 0000000000..b0016b55c1 --- /dev/null +++ b/src/bun.js/node/node_assert.zig @@ -0,0 +1,130 @@ +const std = @import("std"); +const bun = @import("root").bun; +const MyersDiff = @import("./assert/myers_diff.zig"); + +const Allocator = std.mem.Allocator; +const BunString = bun.String; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; + +const StringDiffList = MyersDiff.DiffList([]const u8); + +/// Compare `actual` and `expected`, producing a diff that would turn `actual` +/// into `expected`. +/// +/// Lines in the returned diff have the same encoding as `actual` and +/// `expected`. Lines borrow from these inputs, but the diff list itself must +/// be deallocated. +/// +/// Use an arena allocator, otherwise this will leak memory. +/// +/// ## Invariants +/// If not met, this function will panic. +/// - `actual` and `expected` are alive and have the same encoding. +pub fn myersDiff( + allocator: Allocator, + global: *JSC.JSGlobalObject, + actual: *const BunString, + expected: *const BunString, + // If true, strings that have a trailing comma but are otherwise equal are + // considered equal. + check_comma_disparity: bool, + // split `actual` and `expected` into lines before diffing + lines: bool, +) bun.JSError!JSC.JSValue { + // Short circuit on empty strings. Note that, in release builds where + // assertions are disabled, if `actual` and `expected` are both dead, this + // branch will be hit since dead strings have a length of 0. This should be + // moot since BunStrings with non-zero reference counds should never be + // dead. + if (actual.length() == 0 and expected.length() == 0) { + return JSC.JSValue.createEmptyArray(global, 0); + } + + const actual_encoding = actual.encoding(); + const expected_encoding = expected.encoding(); + + if (lines) { + if (actual_encoding != expected_encoding) { + const actual_utf8 = actual.toUTF8WithoutRef(allocator); + defer actual_utf8.deinit(); + const expected_utf8 = expected.toUTF8WithoutRef(allocator); + defer expected_utf8.deinit(); + + return diffLines(u8, allocator, global, actual_utf8.byteSlice(), expected_utf8.byteSlice(), check_comma_disparity); + } + + return switch (actual_encoding) { + .latin1, .utf8 => diffLines(u8, allocator, global, actual.byteSlice(), expected.byteSlice(), check_comma_disparity), + .utf16 => diffLines(u16, allocator, global, actual.utf16(), expected.utf16(), check_comma_disparity), + }; + } + + if (actual_encoding != expected_encoding) { + const actual_utf8 = actual.toUTF8WithoutRef(allocator); + defer actual_utf8.deinit(); + const expected_utf8 = expected.toUTF8WithoutRef(allocator); + defer expected_utf8.deinit(); + + return diffChars(u8, allocator, global, actual.byteSlice(), expected.byteSlice()); + } + + return switch (actual_encoding) { + .latin1, .utf8 => diffChars(u8, allocator, global, actual.byteSlice(), expected.byteSlice()), + .utf16 => diffChars(u16, allocator, global, actual.utf16(), expected.utf16()), + }; +} + +fn diffChars( + comptime T: type, + allocator: Allocator, + global: *JSC.JSGlobalObject, + actual: []const T, + expected: []const T, +) bun.JSError!JSC.JSValue { + const Differ = MyersDiff.Differ(T, .{ .check_comma_disparity = false }); + const diff: MyersDiff.DiffList(T) = Differ.diff(allocator, actual, expected) catch |err| return mapDiffError(global, err); + return diffListToJS(T, global, diff); +} + +fn diffLines( + comptime T: type, + allocator: Allocator, + global: *JSC.JSGlobalObject, + actual: []const T, + expected: []const T, + check_comma_disparity: bool, +) bun.JSError!JSC.JSValue { + var a = try MyersDiff.split(T, allocator, actual); + defer a.deinit(allocator); + var e = try MyersDiff.split(T, allocator, expected); + defer e.deinit(allocator); + + const diff: MyersDiff.DiffList([]const T) = blk: { + if (check_comma_disparity) { + const Differ = MyersDiff.Differ([]const T, .{ .check_comma_disparity = true }); + break :blk Differ.diff(allocator, a.items, e.items) catch |err| return mapDiffError(global, err); + } else { + const Differ = MyersDiff.Differ([]const T, .{ .check_comma_disparity = false }); + break :blk Differ.diff(allocator, a.items, e.items) catch |err| return mapDiffError(global, err); + } + }; + return diffListToJS([]const T, global, diff); +} + +fn diffListToJS(comptime T: type, global: *JSC.JSGlobalObject, diff_list: MyersDiff.DiffList(T)) bun.JSError!JSC.JSValue { + var array = JSC.JSValue.createEmptyArray(global, diff_list.items.len); + for (diff_list.items, 0..) |*line, i| { + array.putIndex(global, @truncate(i), JSC.JSObject.createNullProto(line.*, global).toJS()); + } + return array; +} + +fn mapDiffError(global: *JSC.JSGlobalObject, err: MyersDiff.Error) bun.JSError { + return switch (err) { + error.OutOfMemory => error.OutOfMemory, + error.DiffTooLarge => global.throwInvalidArguments("Diffing these two values would create a string that is too large. If this was intentional, please open a bug report on GitHub.", .{}), + error.InputsTooLarge => global.throwInvalidArguments("Input strings are too large to diff. Please open a bug report on GitHub.", .{}), + }; +} diff --git a/src/bun.js/node/node_assert_binding.zig b/src/bun.js/node/node_assert_binding.zig new file mode 100644 index 0000000000..824e06bb7c --- /dev/null +++ b/src/bun.js/node/node_assert_binding.zig @@ -0,0 +1,86 @@ +const std = @import("std"); +const bun = @import("root").bun; +const assert = @import("./node_assert.zig"); +const DiffList = @import("./assert/myers_diff.zig").DiffList; +const Allocator = std.mem.Allocator; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; + +/// ```ts +/// const enum DiffType { +/// Insert = 0, +/// Delete = 1, +/// Equal = 2, +/// } +/// type Diff = { operation: DiffType, text: string }; +/// declare function myersDiff(actual: string, expected: string): Diff[]; +/// ``` +pub fn myersDiff(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var stack_fallback = std.heap.stackFallback(1024 * 2, bun.default_allocator); + var arena = std.heap.ArenaAllocator.init(stack_fallback.get()); + defer arena.deinit(); + const allocator = arena.allocator(); + + const nargs = callframe.argumentsCount(); + if (nargs < 2) { + return global.throwNotEnoughArguments("printMyersDiff", 2, callframe.argumentsCount()); + } + + const actual_arg: JSValue = callframe.argument(0); + const expected_arg: JSValue = callframe.argument(1); + const check_comma_disparity: bool, const lines: bool = switch (nargs) { + 0, 1 => unreachable, + 2 => .{ false, false }, + 3 => .{ callframe.argument(2).isTruthy(), false }, + else => .{ callframe.argument(2).isTruthy(), callframe.argument(3).isTruthy() }, + }; + + if (!actual_arg.isString()) return global.throwInvalidArgumentTypeValue("actual", "string", actual_arg); + if (!expected_arg.isString()) return global.throwInvalidArgumentTypeValue("expected", "string", expected_arg); + + const actual_str = try actual_arg.toBunString2(global); + defer actual_str.deref(); + const expected_str = try expected_arg.toBunString2(global); + defer expected_str.deref(); + + bun.assertWithLocation(actual_str.tag != .Dead, @src()); + bun.assertWithLocation(expected_str.tag != .Dead, @src()); + + return assert.myersDiff( + allocator, + global, + &actual_str, + &expected_str, + check_comma_disparity, + lines, + ); +} + +const StrDiffList = DiffList([]const u8); +fn diffListToJS(global: *JSC.JSGlobalObject, diff_list: StrDiffList) bun.JSError!JSC.JSValue { + // todo: replace with toJS + var array = JSC.JSValue.createEmptyArray(global, diff_list.items.len); + for (diff_list.items, 0..) |*line, i| { + var obj = JSC.JSValue.createEmptyObjectWithNullPrototype(global); + if (obj == .zero) return global.throwOutOfMemory(); + obj.put(global, bun.String.static("kind"), JSC.JSValue.jsNumber(@as(u32, @intFromEnum(line.kind)))); + obj.put(global, bun.String.static("value"), JSC.toJS(global, []const u8, line.value, .allocated)); + array.putIndex(global, @truncate(i), obj); + } + return array; +} + +// ============================================================================= + +pub fn generate(global: *JSC.JSGlobalObject) JSC.JSValue { + const exports = JSC.JSValue.createEmptyObject(global, 1); + + exports.put( + global, + bun.String.static("myersDiff"), + JSC.JSFunction.create(global, "myersDiff", myersDiff, 2, .{}), + ); + + return exports; +} diff --git a/src/bun.js/node/node_cluster_binding.zig b/src/bun.js/node/node_cluster_binding.zig index 8f6d09381e..bf930e8e84 100644 --- a/src/bun.js/node/node_cluster_binding.zig +++ b/src/bun.js/node/node_cluster_binding.zig @@ -1,3 +1,8 @@ +// Most of this code should be rewritten. +// - Usage of JSC.Strong here is likely to cause memory leaks. +// - These sequence numbers and ACKs shouldn't exist from JavaScript's perspective +// at all. It should happen in the protocol before it reaches JS. +// - We should not be creating JSFunction's in process.nextTick. const std = @import("std"); const bun = @import("root").bun; const Environment = bun.Environment; @@ -35,6 +40,8 @@ pub fn sendHelperChild(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFram return globalThis.throwInvalidArgumentTypeValue("message", "object", message); } if (callback.isFunction()) { + // TODO: remove this strong. This is expensive and would be an easy way to create a memory leak. + // These sequence numbers shouldn't exist from JavaScript's perspective at all. child_singleton.callbacks.put(bun.default_allocator, child_singleton.seq, JSC.Strong.create(callback, globalThis)) catch bun.outOfMemory(); } @@ -52,7 +59,7 @@ pub fn sendHelperChild(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFram fn impl(globalThis_: *JSC.JSGlobalObject, callframe_: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments_ = callframe_.arguments_old(1).slice(); const ex = arguments_[0]; - Process__emitErrorEvent(globalThis_, ex); + Process__emitErrorEvent(globalThis_, ex.toError() orelse ex); return .undefined; } }; @@ -73,6 +80,7 @@ pub fn sendHelperChild(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFram pub fn onInternalMessageChild(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { log("onInternalMessageChild", .{}); const arguments = callframe.arguments_old(2).ptr; + // TODO: we should not create two JSC.Strong here. If absolutely necessary, a single Array. should be all we use. child_singleton.worker = JSC.Strong.create(arguments[0], globalThis); child_singleton.cb = JSC.Strong.create(arguments[1], globalThis); try child_singleton.flush(globalThis); @@ -85,17 +93,16 @@ pub fn handleInternalMessageChild(globalThis: *JSC.JSGlobalObject, message: JSC. try child_singleton.dispatch(message, globalThis); } -// -// -// - +// TODO: rewrite this code. /// Queue for messages sent between parent and child processes in an IPC environment. node:cluster sends json serialized messages /// to describe different events it performs. It will send a message with an incrementing sequence number and then call a callback /// when a message is recieved with an 'ack' property of the same sequence number. pub const InternalMsgHolder = struct { seq: i32 = 0, - callbacks: std.AutoArrayHashMapUnmanaged(i32, JSC.Strong) = .{}, + // TODO: move this to an Array or a JS Object or something which doesn't + // individually create a Strong for every single IPC message... + callbacks: std.AutoArrayHashMapUnmanaged(i32, JSC.Strong) = .{}, worker: JSC.Strong = .{}, cb: JSC.Strong = .{}, messages: std.ArrayListUnmanaged(JSC.Strong) = .{}, @@ -211,6 +218,7 @@ pub fn onInternalMessagePrimary(globalThis: *JSC.JSGlobalObject, callframe: *JSC const arguments = callframe.arguments_old(3).ptr; const subprocess = arguments[0].as(bun.JSC.Subprocess).?; const ipc_data = subprocess.ipc() orelse return .undefined; + // TODO: remove these strongs. ipc_data.internal_msg_queue.worker = JSC.Strong.create(arguments[1], globalThis); ipc_data.internal_msg_queue.cb = JSC.Strong.create(arguments[2], globalThis); return .undefined; @@ -221,6 +229,7 @@ pub fn handleInternalMessagePrimary(globalThis: *JSC.JSGlobalObject, subprocess: const event_loop = globalThis.bunVM().eventLoop(); + // TODO: investigate if "ack" and "seq" are observable and if they're not, remove them entirely. if (try message.get(globalThis, "ack")) |p| { if (!p.isUndefined()) { const ack = p.toInt32(); diff --git a/src/bun.js/node/node_crypto_binding.zig b/src/bun.js/node/node_crypto_binding.zig index 2605f64fb8..ebdf65a76d 100644 --- a/src/bun.js/node/node_crypto_binding.zig +++ b/src/bun.js/node/node_crypto_binding.zig @@ -40,7 +40,7 @@ fn randomInt(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSE } fn pbkdf2(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(5); + const arguments = callframe.arguments_old(6); const data = try PBKDF2.fromJS(globalThis, arguments.slice(), true); diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 31c5d47fcb..50ab3b0802 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -10,7 +10,6 @@ const JSC = bun.JSC; const PathString = JSC.PathString; const Environment = bun.Environment; const C = bun.C; -const Flavor = JSC.Node.Flavor; const system = std.posix.system; const Maybe = JSC.Maybe; const Encoding = JSC.Node.Encoding; @@ -57,6 +56,11 @@ else // Windows does not have permissions 0; +// All async FS functions are run in a thread pool, but some implementations may +// decide to do something slightly different. For example, reading a file has +// an extra stack buffer in the async case. +pub const Flavor = enum { sync, @"async" }; + const ArrayBuffer = JSC.MarkedArrayBuffer; const Buffer = JSC.Buffer; const FileSystemFlags = JSC.Node.FileSystemFlags; @@ -89,6 +93,7 @@ pub const Async = struct { pub const readlink = NewAsyncFSTask(Return.Readlink, Arguments.Readlink, NodeFS.readlink); pub const readv = NewUVFSRequest(Return.Readv, Arguments.Readv, .readv); pub const realpath = NewAsyncFSTask(Return.Realpath, Arguments.Realpath, NodeFS.realpath); + pub const realpathNonNative = NewAsyncFSTask(Return.Realpath, Arguments.Realpath, NodeFS.realpathNonNative); pub const rename = NewAsyncFSTask(Return.Rename, Arguments.Rename, NodeFS.rename); pub const rm = NewAsyncFSTask(Return.Rm, Arguments.Rm, NodeFS.rm); pub const rmdir = NewAsyncFSTask(Return.Rmdir, Arguments.RmDir, NodeFS.rmdir); @@ -126,7 +131,6 @@ pub const Async = struct { .path = PathLike{ .string = PathString.init(this.path) }, .recursive = true, }, - .sync, ); switch (result) { .err => |err| { @@ -374,7 +378,7 @@ pub const Async = struct { var this: *Task = @alignCast(@fieldParentPtr("task", task)); var node_fs = NodeFS{}; - this.result = Function(&node_fs, this.args, .promise); + this.result = Function(&node_fs, this.args, .@"async"); if (this.result == .err) { this.result.err.path = bun.default_allocator.dupe(u8, this.result.err.path) catch ""; @@ -703,8 +707,8 @@ pub fn NewAsyncCpTask(comptime is_shell: bool) type { const args = this.args; var src_buf: bun.OSPathBuffer = undefined; var dest_buf: bun.OSPathBuffer = undefined; - const src = args.src.osPath(@ptrCast(&src_buf)); - const dest = args.dest.osPath(@ptrCast(&dest_buf)); + const src = args.src.osPath(&src_buf); + const dest = args.dest.osPath(&dest_buf); if (Environment.isWindows) { const attributes = windows.GetFileAttributesW(src); @@ -950,7 +954,7 @@ pub const AsyncReaddirRecursiveTask = struct { root_path: PathString = PathString.empty, pending_err: ?Syscall.Error = null, - pending_err_mutex: bun.Lock = .{}, + pending_err_mutex: bun.Mutex = .{}, pub usingnamespace bun.New(@This()); @@ -1297,7 +1301,7 @@ pub const Arguments = struct { pub const Truncate = struct { /// Passing a file descriptor is deprecated and may result in an error being thrown in the future. path: PathOrFileDescriptor, - len: JSC.WebCore.Blob.SizeType = 0, + len: JSC.WebCore.Blob.SizeType, flags: i32 = 0, pub fn deinit(this: @This()) void { @@ -1355,12 +1359,9 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Writev { - const fd_value = arguments.nextEat() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }; - + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + return throwInvalidFdError(ctx, fd_value); }; const buffers = try JSC.Node.VectorArrayBuffer.fromJS( @@ -1412,12 +1413,9 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Readv { - const fd_value = arguments.nextEat() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }; - + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + return throwInvalidFdError(ctx, fd_value); }; const buffers = try JSC.Node.VectorArrayBuffer.fromJS( @@ -1461,14 +1459,11 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FTruncate { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); - const len: JSC.WebCore.Blob.SizeType = brk: { const len_value = arguments.next() orelse break :brk 0; if (len_value.isNumber()) { @@ -1512,22 +1507,15 @@ pub const Arguments = struct { }; arguments.eat(); - if (!uid_value.isNumber()) { - return ctx.throwInvalidArgumentTypeValue("uid", "number", uid_value); - } - break :brk @as(uid_t, @intCast(uid_value.toInt32())); + break :brk wrapTo(uid_t, try JSC.Node.validators.validateInteger(ctx, uid_value, "uid", .{}, -1, std.math.maxInt(u32))); }; const gid: gid_t = brk: { const gid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("gid is required", .{}); }; - arguments.eat(); - if (!gid_value.isNumber()) { - return ctx.throwInvalidArgumentTypeValue("gid", "number", gid_value); - } - break :brk @as(gid_t, @intCast(gid_value.toInt32())); + break :brk wrapTo(gid_t, try JSC.Node.validators.validateInteger(ctx, gid_value, "gid", .{}, -1, std.math.maxInt(u32))); }; return Chown{ .path = path, .uid = uid, .gid = gid }; @@ -1544,10 +1532,9 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fchown { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; const uid: uid_t = brk: { @@ -1556,22 +1543,26 @@ pub const Arguments = struct { }; arguments.eat(); - break :brk @as(uid_t, @intCast(uid_value.toInt32())); + break :brk wrapTo(uid_t, try JSC.Node.validators.validateInteger(ctx, uid_value, "uid", .{}, -1, std.math.maxInt(u32))); }; const gid: gid_t = brk: { const gid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("gid is required", .{}); }; - arguments.eat(); - break :brk @as(gid_t, @intCast(gid_value.toInt32())); + break :brk wrapTo(gid_t, try JSC.Node.validators.validateInteger(ctx, gid_value, "gid", .{}, -1, std.math.maxInt(u32))); }; return Fchown{ .fd = fd, .uid = uid, .gid = gid }; } }; + fn wrapTo(T: type, in: i64) T { + comptime bun.assert(@typeInfo(T).Int.signedness == .unsigned); + return @intCast(@mod(in, std.math.maxInt(T))); + } + pub const LChown = Chown; pub const Lutimes = struct { @@ -1613,7 +1604,7 @@ pub const Arguments = struct { arguments.eat(); - return Lutimes{ .path = path, .atime = atime, .mtime = mtime }; + return .{ .path = path, .atime = atime, .mtime = mtime }; } }; @@ -1639,10 +1630,9 @@ pub const Arguments = struct { }; errdefer path.deinit(); - const mode: Mode = try JSC.Node.modeFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("mode is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("mode must be a string or integer", .{}); + const mode_arg = arguments.next() orelse .undefined; + const mode: Mode = try JSC.Node.modeFromJS(ctx, mode_arg) orelse { + return JSC.Node.validators.throwErrInvalidArgType(ctx, "mode", .{}, "number", mode_arg); }; arguments.eat(); @@ -1660,18 +1650,14 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FChmod { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); - - const mode: Mode = try JSC.Node.modeFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("mode is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("mode must be a string or integer", .{}); + const mode_arg = arguments.next() orelse .undefined; + const mode: Mode = try JSC.Node.modeFromJS(ctx, mode_arg) orelse { + return JSC.Node.validators.throwErrInvalidArgType(ctx, "mode", .{}, "number", mode_arg); }; arguments.eat(); @@ -1738,10 +1724,9 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *@This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fstat { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; const big_int = brk: { @@ -2095,6 +2080,9 @@ pub const Arguments = struct { mode = try JSC.Node.modeFromJS(ctx, mode_) orelse mode; } } + if (val.isNumber() or val.isString()) { + mode = try JSC.Node.modeFromJS(ctx, val) orelse mode; + } } return Mkdir{ @@ -2235,10 +2223,9 @@ pub const Arguments = struct { pub fn toThreadSafe(_: Close) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Close { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; return Close{ .fd = fd }; @@ -2323,12 +2310,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Futimes { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); const atime = JSC.Node.timeLikeFromJS(ctx, arguments.next() orelse { return ctx.throwInvalidArguments("atime is required", .{}); @@ -2375,7 +2360,6 @@ pub const Arguments = struct { /// The kernel ignores the position argument and always appends the data to /// the end of the file. /// @since v0.0.2 - /// pub const Write = struct { fd: FileDescriptor, buffer: StringOrBuffer, @@ -2398,12 +2382,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Write { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); const buffer_value = arguments.next(); const buffer = StringOrBuffer.fromJS(ctx, bun.default_allocator, buffer_value orelse { @@ -2427,45 +2409,59 @@ pub const Arguments = struct { arguments.eat(); - // TODO: make this faster by passing argument count at comptime - if (arguments.next()) |current_| { - parse: { - var current = current_; - switch (buffer) { - // fs.write(fd, string[, position[, encoding]], callback) - else => { - if (current.isNumber()) { - args.position = current.to(i52); - arguments.eat(); - current = arguments.next() orelse break :parse; - } - - if (current.isString()) { - args.encoding = try Encoding.assert(current, ctx, args.encoding); - arguments.eat(); - } - }, - // fs.write(fd, buffer[, offset[, length[, position]]], callback) - .buffer => { - if (!current.isNumber()) { - break :parse; - } - - if (!(current.isNumber() or current.isBigInt())) break :parse; - args.offset = current.to(u52); - arguments.eat(); - current = arguments.next() orelse break :parse; - - if (!(current.isNumber() or current.isBigInt())) break :parse; - args.length = current.to(u52); - arguments.eat(); - current = arguments.next() orelse break :parse; - - if (!(current.isNumber() or current.isBigInt())) break :parse; + parse: { + var current = arguments.next() orelse break :parse; + switch (buffer) { + // fs.write(fd, string[, position[, encoding]], callback) + else => { + if (current.isNumber()) { args.position = current.to(i52); arguments.eat(); - }, - } + current = arguments.next() orelse break :parse; + } + + if (current.isString()) { + args.encoding = try Encoding.assert(current, ctx, args.encoding); + arguments.eat(); + } + }, + // fs.write(fd, buffer[, offset[, length[, position]]], callback) + .buffer => { + args.offset = @intCast(try JSC.Node.validators.validateInteger(ctx, current, "offset", .{}, 0, 9007199254740991)); + arguments.eat(); + current = arguments.next() orelse break :parse; + + if (!(current.isNumber() or current.isBigInt())) break :parse; + const length = current.to(i64); + const buf_len = args.buffer.buffer.slice().len; + if (args.offset > buf_len) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(args.offset)), + .{ .field_name = "offset", .max = @intCast(@min(buf_len, std.math.maxInt(i64))) }, + ); + } + if (length > buf_len - args.offset) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(length)), + .{ .field_name = "length", .max = @intCast(@min(buf_len - args.offset, std.math.maxInt(i64))) }, + ); + } + if (length < 0) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(length)), + .{ .field_name = "length", .min = 0 }, + ); + } + args.length = @intCast(length); + + arguments.eat(); + current = arguments.next() orelse break :parse; + + if (!(current.isNumber() or current.isBigInt())) break :parse; + const position = current.to(i52); + if (position >= 0) args.position = position; + arguments.eat(); + }, } } @@ -2491,12 +2487,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Read { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); const buffer_value = arguments.next(); const buffer = Buffer.fromJS(ctx, buffer_value orelse { @@ -2532,7 +2526,9 @@ pub const Arguments = struct { if (arguments.next()) |arg_position| { arguments.eat(); if (arg_position.isNumber() or arg_position.isBigInt()) { - args.position = @as(ReadPosition, @intCast(arg_position.to(i52))); + const num = arg_position.to(i52); + if (num >= 0) + args.position = @as(ReadPosition, @intCast(num)); } } } else if (current.isObject()) { @@ -2551,15 +2547,26 @@ pub const Arguments = struct { if (try current.getTruthy(ctx, "position")) |num| { if (num.isNumber() or num.isBigInt()) { - args.position = num.to(i52); + const n = num.to(i52); + if (n >= 0) + args.position = num.to(i52); } } } } - if (defined_length and args.length > 0 and buffer.slice().len == 0) { - var formatter = bun.JSC.ConsoleObject.Formatter{ .globalThis = ctx }; - return ctx.ERR_INVALID_ARG_VALUE("The argument 'buffer' is empty and cannot be written. Received {}", .{buffer_value.?.toFmt(&formatter)}).throw(); + if (defined_length and args.length > 0) { + const buf_length = buffer.slice().len; + if (buf_length == 0) { + var formatter = bun.JSC.ConsoleObject.Formatter{ .globalThis = ctx }; + return ctx.ERR_INVALID_ARG_VALUE("The argument 'buffer' is empty and cannot be written. Received {}", .{buffer_value.?.toFmt(&formatter)}).throw(); + } + if (args.length > buf_length) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(args.length)), + .{ .field_name = "length", .max = @intCast(@min(buf_length, std.math.maxInt(i64))) }, + ); + } } return args; @@ -2686,15 +2693,13 @@ pub const Arguments = struct { } if (try arg.getTruthy(ctx, "mode")) |mode_| { - mode = try JSC.Node.modeFromJS(ctx, mode_) orelse { - return ctx.throwInvalidArguments("Invalid mode", .{}); - }; + mode = try JSC.Node.modeFromJS(ctx, mode_) orelse mode; } } } const data = try StringOrBuffer.fromJSWithEncodingMaybeAsync(ctx, bun.default_allocator, data_value, encoding, arguments.will_be_async) orelse { - return ctx.throwInvalidArguments("data must be a string or TypedArray", .{}); + return ctx.ERR_INVALID_ARG_TYPE("The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView", .{}).throw(); }; // Note: Signal is not implemented @@ -2811,11 +2816,7 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); - if (arg.isString()) { - mode = try FileSystemFlags.fromJS(ctx, arg) orelse { - return ctx.throwInvalidArguments("Invalid mode", .{}); - }; - } + mode = try FileSystemFlags.fromJSNumberOnly(ctx, arg, .access); } return Access{ @@ -2834,12 +2835,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FdataSync { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); return FdataSync{ .fd = fd }; } @@ -2876,12 +2875,10 @@ pub const Arguments = struct { }; errdefer dest.deinit(); - var mode: i32 = 0; + var mode: Mode = 0; if (arguments.next()) |arg| { arguments.eat(); - if (arg.isNumber()) { - mode = arg.coerce(i32, ctx); - } + mode = @intFromEnum(try FileSystemFlags.fromJSNumberOnly(ctx, arg, .copy_file)); } return CopyFile{ @@ -2988,12 +2985,10 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fsync { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); return Fsync{ .fd = fd }; } @@ -3031,8 +3026,15 @@ pub const StringOrUndefined = union(enum) { } }; +/// For use in `Return`'s definitions to act as `void` while returning `null` to JavaScript +const Null = struct { + pub fn toJS(_: @This(), _: *JSC.JSGlobalObject) JSC.JSValue { + return .null; + } +}; + const Return = struct { - pub const Access = void; + pub const Access = Null; pub const AppendFile = void; pub const Close = void; pub const CopyFile = void; @@ -3181,19 +3183,21 @@ pub const NodeFS = struct { /// /// We want to avoid allocating a new path buffer for every error message so that JSC can clone + GC it. /// That means a stack-allocated buffer won't suffice. Instead, we re-use - /// the heap allocated buffer on the NodefS struct + /// the heap allocated buffer on the NodeFS struct sync_error_buf: bun.PathBuffer = undefined, vm: ?*JSC.VirtualMachine = null, pub const ReturnType = Return; - pub fn access(this: *NodeFS, args: Arguments.Access, comptime _: Flavor) Maybe(Return.Access) { + pub fn access(this: *NodeFS, args: Arguments.Access, _: Flavor) Maybe(Return.Access) { const path = args.path.sliceZ(&this.sync_error_buf); - return Syscall.access(path, @intFromEnum(args.mode)); + return switch (Syscall.access(path, @intFromEnum(args.mode))) { + .err => |err| .{ .err = err }, + .result => .{ .result = .{} }, + }; } - pub fn appendFile(this: *NodeFS, args: Arguments.AppendFile, comptime flavor: Flavor) Maybe(Return.AppendFile) { - _ = flavor; + pub fn appendFile(this: *NodeFS, args: Arguments.AppendFile, _: Flavor) Maybe(Return.AppendFile) { var data = args.data.slice(); switch (args.file) { @@ -3233,8 +3237,7 @@ pub const NodeFS = struct { } } - pub fn close(_: *NodeFS, args: Arguments.Close, comptime flavor: Flavor) Maybe(Return.Close) { - _ = flavor; + pub fn close(_: *NodeFS, args: Arguments.Close, _: Flavor) Maybe(Return.Close) { return if (Syscall.close(args.fd)) |err| .{ .err = err } else Maybe(Return.Close).success; } @@ -3244,6 +3247,7 @@ pub const NodeFS = struct { .errno = @intCast(-rc), .syscall = .close, .fd = args.fd, + .from_libuv = true, } }; } return Maybe(Return.Close).success; @@ -3342,12 +3346,23 @@ pub const NodeFS = struct { return Maybe(Return.CopyFile).success; } + pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, _: Flavor) Maybe(Return.CopyFile) { + return switch (this.copyFileInner(args)) { + .result => .{ .result = {} }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .syscall = .copyfile, + .path = args.src.slice(), + .dest = args.dest.slice(), + } }, + }; + } + /// https://github.com/libuv/libuv/pull/2233 /// https://github.com/pnpm/pnpm/issues/2761 /// https://github.com/libuv/libuv/pull/2578 /// https://github.com/nodejs/node/issues/34624 - pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, comptime flavor: Flavor) Maybe(Return.CopyFile) { - _ = flavor; + fn copyFileInner(this: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) { const ret = Maybe(Return.CopyFile); // TODO: do we need to fchown? @@ -3360,7 +3375,7 @@ pub const NodeFS = struct { if (args.mode.isForceClone()) { // https://www.manpagez.com/man/2/clonefile/ - return ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) orelse ret.success; + return ret.errnoSysP(C.clonefile(src, dest, 0), .copyfile, src) orelse ret.success; } else { const stat_ = switch (Syscall.stat(src)) { .result => |result| result, @@ -3382,7 +3397,7 @@ pub const NodeFS = struct { _ = Syscall.unlink(dest); } - if (ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) == null) { + if (ret.errnoSysP(C.clonefile(src, dest, 0), .copyfile, src) == null) { _ = C.chmod(dest, stat_.mode); return ret.success; } @@ -3552,14 +3567,12 @@ pub const NodeFS = struct { } if (comptime Environment.isWindows) { - if (args.mode.isForceClone()) { - return Maybe(Return.CopyFile).todo(); - } - - var src_buf: bun.WPathBuffer = undefined; - var dest_buf: bun.WPathBuffer = undefined; - const src = strings.toWPathNormalizeAutoExtend(&src_buf, args.src.sliceZ(&this.sync_error_buf)); - const dest = strings.toWPathNormalizeAutoExtend(&dest_buf, args.dest.sliceZ(&this.sync_error_buf)); + const src_buf = bun.OSPathBufferPool.get(); + defer bun.OSPathBufferPool.put(src_buf); + const dest_buf = bun.OSPathBufferPool.get(); + defer bun.OSPathBufferPool.put(dest_buf); + const src = strings.toWPathNormalizeAutoExtend(src_buf, args.src.sliceZ(&this.sync_error_buf)); + const dest = strings.toWPathNormalizeAutoExtend(dest_buf, args.dest.sliceZ(&this.sync_error_buf)); if (windows.CopyFileW(src.ptr, dest.ptr, if (args.mode.shouldntOverwrite()) 1 else 0) == windows.FALSE) { if (ret.errnoSysP(0, .copyfile, args.src.slice())) |rest| { return shouldIgnoreEbusy(args.src, args.dest, rest); @@ -3572,10 +3585,8 @@ pub const NodeFS = struct { return Maybe(Return.CopyFile).todo(); } - pub fn exists(this: *NodeFS, args: Arguments.Exists, comptime flavor: Flavor) Maybe(Return.Exists) { - _ = flavor; - const Ret = Maybe(Return.Exists); - const path = args.path orelse return Ret{ .result = false }; + pub fn exists(this: *NodeFS, args: Arguments.Exists, _: Flavor) Maybe(Return.Exists) { + const path = args.path orelse return .{ .result = false }; const slice = path.sliceZ(&this.sync_error_buf); // Use libuv access on windows @@ -3588,11 +3599,10 @@ pub const NodeFS = struct { // hidden from the client, which checks permissions. Similar // problems can occur to FUSE mounts. const rc = (system.access(slice, std.posix.F_OK)); - return Ret{ .result = rc == 0 }; + return .{ .result = rc == 0 }; } - pub fn chown(this: *NodeFS, args: Arguments.Chown, comptime flavor: Flavor) Maybe(Return.Chown) { - _ = flavor; + pub fn chown(this: *NodeFS, args: Arguments.Chown, _: Flavor) Maybe(Return.Chown) { if (comptime Environment.isWindows) { return Syscall.chown(args.path.sliceZ(&this.sync_error_buf), args.uid, args.gid); } @@ -3602,27 +3612,22 @@ pub const NodeFS = struct { return Syscall.chown(path, args.uid, args.gid); } - /// This should almost never be async - pub fn chmod(this: *NodeFS, args: Arguments.Chmod, comptime flavor: Flavor) Maybe(Return.Chmod) { - _ = flavor; - if (comptime Environment.isWindows) { - return Syscall.chmod(args.path.sliceZ(&this.sync_error_buf), args.mode); - } - + pub fn chmod(this: *NodeFS, args: Arguments.Chmod, _: Flavor) Maybe(Return.Chmod) { const path = args.path.sliceZ(&this.sync_error_buf); + if (comptime Environment.isWindows) { + return Syscall.chmod(path, args.mode); + } + return Maybe(Return.Chmod).errnoSysP(C.chmod(path, args.mode), .chmod, path) orelse Maybe(Return.Chmod).success; } - /// This should almost never be async - pub fn fchmod(_: *NodeFS, args: Arguments.FChmod, comptime flavor: Flavor) Maybe(Return.Fchmod) { - _ = flavor; + pub fn fchmod(_: *NodeFS, args: Arguments.FChmod, _: Flavor) Maybe(Return.Fchmod) { return Syscall.fchmod(args.fd, args.mode); } - pub fn fchown(_: *NodeFS, args: Arguments.Fchown, comptime flavor: Flavor) Maybe(Return.Fchown) { - _ = flavor; + pub fn fchown(_: *NodeFS, args: Arguments.Fchown, _: Flavor) Maybe(Return.Fchown) { if (comptime Environment.isWindows) { return Syscall.fchown(args.fd, args.uid, args.gid); } @@ -3631,21 +3636,21 @@ pub const NodeFS = struct { Maybe(Return.Fchown).success; } - pub fn fdatasync(_: *NodeFS, args: Arguments.FdataSync, comptime _: Flavor) Maybe(Return.Fdatasync) { + pub fn fdatasync(_: *NodeFS, args: Arguments.FdataSync, _: Flavor) Maybe(Return.Fdatasync) { if (Environment.isWindows) { return Syscall.fdatasync(args.fd); } return Maybe(Return.Fdatasync).errnoSysFd(system.fdatasync(args.fd.int()), .fdatasync, args.fd) orelse Maybe(Return.Fdatasync).success; } - pub fn fstat(_: *NodeFS, args: Arguments.Fstat, comptime _: Flavor) Maybe(Return.Fstat) { + pub fn fstat(_: *NodeFS, args: Arguments.Fstat, _: Flavor) Maybe(Return.Fstat) { return switch (Syscall.fstat(args.fd)) { - .result => |result| Maybe(Return.Fstat){ .result = Stats.init(result, false) }, - .err => |err| Maybe(Return.Fstat){ .err = err }, + .result => |result| .{ .result = Stats.init(result, false) }, + .err => |err| .{ .err = err }, }; } - pub fn fsync(_: *NodeFS, args: Arguments.Fsync, comptime _: Flavor) Maybe(Return.Fsync) { + pub fn fsync(_: *NodeFS, args: Arguments.Fsync, _: Flavor) Maybe(Return.Fsync) { if (Environment.isWindows) { return Syscall.fsync(args.fd); } @@ -3653,22 +3658,21 @@ pub const NodeFS = struct { Maybe(Return.Fsync).success; } - pub fn ftruncateSync(args: Arguments.FTruncate) Maybe(Return.Ftruncate) { + pub fn ftruncate(_: *NodeFS, args: Arguments.FTruncate, _: Flavor) Maybe(Return.Ftruncate) { return Syscall.ftruncate(args.fd, args.len orelse 0); } - pub fn ftruncate(_: *NodeFS, args: Arguments.FTruncate, comptime flavor: Flavor) Maybe(Return.Ftruncate) { - _ = flavor; - return ftruncateSync(args); - } - - pub fn futimes(_: *NodeFS, args: Arguments.Futimes, comptime _: Flavor) Maybe(Return.Futimes) { + pub fn futimes(_: *NodeFS, args: Arguments.Futimes, _: Flavor) Maybe(Return.Futimes) { if (comptime Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_futime(uv.Loop.get(), &req, bun.uvfdcast(args.fd), args.mtime, args.atime, null); return if (rc.errno()) |e| - Maybe(Return.Futimes){ .err = .{ .errno = e, .syscall = .futime } } + .{ .err = .{ + .errno = e, + .syscall = .futime, + .fd = args.fd, + } } else Maybe(Return.Futimes).success; } @@ -3678,26 +3682,23 @@ pub const NodeFS = struct { args.atime, }; - return if (Maybe(Return.Futimes).errnoSys(system.futimens(args.fd.int(), ×), .futimens)) |err| + return if (Maybe(Return.Futimes).errnoSysFd(system.futimens(args.fd.int(), ×), .futime, args.fd)) |err| err else Maybe(Return.Futimes).success; } - pub fn lchmod(this: *NodeFS, args: Arguments.LCHmod, comptime flavor: Flavor) Maybe(Return.Lchmod) { - _ = flavor; + pub fn lchmod(this: *NodeFS, args: Arguments.LCHmod, _: Flavor) Maybe(Return.Lchmod) { if (comptime Environment.isWindows) { return Maybe(Return.Lchmod).todo(); } const path = args.path.sliceZ(&this.sync_error_buf); - return Maybe(Return.Lchmod).errnoSysP(C.lchmod(path, args.mode), .lchmod, path) orelse Maybe(Return.Lchmod).success; } - pub fn lchown(this: *NodeFS, args: Arguments.LChown, comptime flavor: Flavor) Maybe(Return.Lchown) { - _ = flavor; + pub fn lchown(this: *NodeFS, args: Arguments.LChown, _: Flavor) Maybe(Return.Lchown) { if (comptime Environment.isWindows) { return Maybe(Return.Lchown).todo(); } @@ -3708,25 +3709,21 @@ pub const NodeFS = struct { Maybe(Return.Lchown).success; } - pub fn link(this: *NodeFS, args: Arguments.Link, comptime _: Flavor) Maybe(Return.Link) { - var new_path_buf: bun.PathBuffer = undefined; + pub fn link(this: *NodeFS, args: Arguments.Link, _: Flavor) Maybe(Return.Link) { + var to_buf: bun.PathBuffer = undefined; const from = args.old_path.sliceZ(&this.sync_error_buf); - const to = args.new_path.sliceZ(&new_path_buf); + const to = args.new_path.sliceZ(&to_buf); if (Environment.isWindows) { return Syscall.link(from, to); } - return Maybe(Return.Link).errnoSysP(system.link(from, to, 0), .link, from) orelse + return Maybe(Return.Link).errnoSysPD(system.link(from, to, 0), .link, args.old_path.slice(), args.new_path.slice()) orelse Maybe(Return.Link).success; } - pub fn lstat(this: *NodeFS, args: Arguments.Lstat, comptime _: Flavor) Maybe(Return.Lstat) { - return switch (Syscall.lstat( - args.path.sliceZ( - &this.sync_error_buf, - ), - )) { + pub fn lstat(this: *NodeFS, args: Arguments.Lstat, _: Flavor) Maybe(Return.Lstat) { + return switch (Syscall.lstat(args.path.sliceZ(&this.sync_error_buf))) { .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = Stats.init(result, args.big_int) } }, .err => |err| brk: { if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { @@ -3737,39 +3734,31 @@ pub const NodeFS = struct { }; } - pub fn mkdir(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { - return if (args.recursive) mkdirRecursive(this, args, flavor) else mkdirNonRecursive(this, args, flavor); + pub fn mkdir(this: *NodeFS, args: Arguments.Mkdir, _: Flavor) Maybe(Return.Mkdir) { + return if (args.recursive) mkdirRecursive(this, args) else mkdirNonRecursive(this, args); } // Node doesn't absolute the path so we don't have to either - pub fn mkdirNonRecursive(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { - _ = flavor; - + pub fn mkdirNonRecursive(this: *NodeFS, args: Arguments.Mkdir) Maybe(Return.Mkdir) { const path = args.path.sliceZ(&this.sync_error_buf); return switch (Syscall.mkdir(path, args.mode)) { .result => Maybe(Return.Mkdir){ .result = .{ .none = {} } }, - .err => |err| Maybe(Return.Mkdir){ .err = err }, + .err => |err| Maybe(Return.Mkdir){ .err = err.withPath(path) }, }; } - pub const MkdirDummyVTable = struct { - pub fn onCreateDir(_: @This(), _: bun.OSPathSliceZ) void { - return; - } - }; - - pub fn mkdirRecursive(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { - return mkdirRecursiveImpl(this, args, flavor, MkdirDummyVTable, .{}); + pub fn mkdirRecursive(this: *NodeFS, args: Arguments.Mkdir) Maybe(Return.Mkdir) { + return mkdirRecursiveImpl(this, args, void, {}); } // TODO: verify this works correctly with unicode codepoints - pub fn mkdirRecursiveImpl(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor, comptime Ctx: type, ctx: Ctx) Maybe(Return.Mkdir) { - _ = flavor; - var buf: bun.OSPathBuffer = undefined; + pub fn mkdirRecursiveImpl(this: *NodeFS, args: Arguments.Mkdir, comptime Ctx: type, ctx: Ctx) Maybe(Return.Mkdir) { + const buf = bun.OSPathBufferPool.get(); + defer bun.OSPathBufferPool.put(buf); const path: bun.OSPathSliceZ = if (Environment.isWindows) - strings.toNTPath(&buf, args.path.slice()) + strings.toNTPath(buf, args.path.slice()) else - args.path.osPath(&buf); + args.path.osPath(buf); // TODO: remove and make it always a comptime argument return switch (args.always_return_none) { @@ -3785,7 +3774,7 @@ pub const NodeFS = struct { } pub fn mkdirRecursiveOSPath(this: *NodeFS, path: bun.OSPathSliceZ, mode: Mode, comptime return_path: bool) Maybe(Return.Mkdir) { - return mkdirRecursiveOSPathImpl(this, MkdirDummyVTable, .{}, path, mode, return_path); + return mkdirRecursiveOSPathImpl(this, void, {}, path, mode, return_path); } pub fn mkdirRecursiveOSPathImpl( @@ -3796,7 +3785,7 @@ pub const NodeFS = struct { mode: Mode, comptime return_path: bool, ) Maybe(Return.Mkdir) { - const VTable = struct { + const callbacks = struct { pub fn onCreateDir(c: Ctx, dirpath: bun.OSPathSliceZ) void { if (Ctx != void) { c.onCreateDir(dirpath); @@ -3816,8 +3805,27 @@ pub const NodeFS = struct { else => { return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(path[0..len])) }; }, - .EXIST => { - return .{ .result = .{ .none = {} } }; + // `mkpath_np` in macOS also checks for `EISDIR`. + // it is unclear if macOS lies about if the existing item is + // a directory or not, so it is checked. + .ISDIR, + // check if it was actually a directory or not. + .EXIST, + => return switch (bun.sys.directoryExistsAt(bun.invalid_fd, path)) { + .err => .{ .err = .{ + .errno = err.errno, + .syscall = .mkdir, + .path = this.osPathIntoSyncErrorBuf(path[0..len]), + } }, + // if is a directory, OK. otherwise failure + .result => |result| if (result) + .{ .result = .{ .none = {} } } + else + .{ .err = .{ + .errno = err.errno, + .syscall = .mkdir, + .path = this.osPathIntoSyncErrorBuf(path[0..len]), + } }, }, // continue .NOENT => { @@ -3829,7 +3837,7 @@ pub const NodeFS = struct { } }, .result => { - VTable.onCreateDir(ctx, path); + callbacks.onCreateDir(ctx, path); if (!return_path) { return .{ .result = .{ .none = {} } }; } @@ -3871,7 +3879,7 @@ pub const NodeFS = struct { } }, .result => { - VTable.onCreateDir(ctx, parent); + callbacks.onCreateDir(ctx, parent); // We found a parent that worked working_mem[i] = std.fs.path.sep; break; @@ -3902,7 +3910,7 @@ pub const NodeFS = struct { }, .result => { - VTable.onCreateDir(ctx, parent); + callbacks.onCreateDir(ctx, parent); working_mem[i] = std.fs.path.sep; }, } @@ -3928,7 +3936,7 @@ pub const NodeFS = struct { .result => {}, } - VTable.onCreateDir(ctx, working_mem[0..len :0]); + callbacks.onCreateDir(ctx, working_mem[0..len :0]); if (!return_path) { return .{ .result = .{ .none = {} } }; } @@ -3956,7 +3964,11 @@ pub const NodeFS = struct { defer req.deinit(); const rc = uv.uv_fs_mkdtemp(bun.Async.Loop.get(), &req, @ptrCast(prefix_buf.ptr), null); if (rc.errno()) |errno| { - return .{ .err = .{ .errno = errno, .syscall = .mkdtemp, .path = prefix_buf[0 .. len + 6] } }; + return .{ .err = .{ + .errno = errno, + .syscall = .mkdtemp, + .path = prefix_buf[0 .. len + 6], + } }; } return .{ .result = JSC.ZigString.dupeForJS(bun.sliceTo(req.path, 0), bun.default_allocator) catch bun.outOfMemory(), @@ -3976,6 +3988,7 @@ pub const NodeFS = struct { .err = Syscall.Error{ .errno = @as(Syscall.Error.Int, @truncate(@intFromEnum(errno))), .syscall = .mkdtemp, + .path = prefix_buf[0 .. len + 6], }, }; } @@ -4003,6 +4016,7 @@ pub const NodeFS = struct { .errno = @intCast(-rc), .syscall = .open, .path = args.path.slice(), + .from_libuv = true, } }; } return Maybe(Return.Open).initResult(FDImpl.decode(bun.toFD(@as(u32, @intCast(rc))))); @@ -4012,58 +4026,47 @@ pub const NodeFS = struct { return Maybe(Return.OpenDir).todo(); } - fn _read(_: *NodeFS, args: Arguments.Read, comptime _: Flavor) Maybe(Return.Read) { + fn readInner(_: *NodeFS, args: Arguments.Read) Maybe(Return.Read) { if (Environment.allow_assert) bun.assert(args.position == null); var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; return switch (Syscall.read(args.fd, buf)) { - .err => |err| .{ - .err = err, - }, - .result => |amt| .{ - .result = .{ - .bytes_read = @as(u52, @truncate(amt)), - }, - }, + .err => |err| .{ .err = err }, + .result => |amt| .{ .result = .{ + .bytes_read = @as(u52, @truncate(amt)), + } }, }; } - fn _pread(_: *NodeFS, args: Arguments.Read, comptime flavor: Flavor) Maybe(Return.Read) { - _ = flavor; + fn preadInner(_: *NodeFS, args: Arguments.Read) Maybe(Return.Read) { var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; return switch (Syscall.pread(args.fd, buf, args.position.?)) { - .err => |err| .{ - .err = err, - }, - .result => |amt| .{ - .result = .{ - .bytes_read = @as(u52, @truncate(amt)), - }, - }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .fd = args.fd, + .syscall = .read, + } }, + .result => |amt| .{ .result = .{ + .bytes_read = @as(u52, @truncate(amt)), + } }, }; } - pub fn read(this: *NodeFS, args: Arguments.Read, comptime flavor: Flavor) Maybe(Return.Read) { + pub fn read(this: *NodeFS, args: Arguments.Read, _: Flavor) Maybe(Return.Read) { const len1 = args.buffer.slice().len; const len2 = args.length; if (len1 == 0 or len2 == 0) { return Maybe(Return.Read).initResult(.{ .bytes_read = 0 }); } return if (args.position != null) - this._pread( - args, - comptime flavor, - ) + this.preadInner(args) else - this._read( - args, - comptime flavor, - ); + this.readInner(args); } pub fn uv_read(this: *NodeFS, args: Arguments.Read, rc: i64) Maybe(Return.Read) { @@ -4073,6 +4076,7 @@ pub const NodeFS = struct { .errno = @intCast(-rc), .syscall = .read, .fd = args.fd, + .from_libuv = true, } }; } return Maybe(Return.Read).initResult(.{ .bytes_read = @intCast(rc) }); @@ -4085,21 +4089,22 @@ pub const NodeFS = struct { .errno = @intCast(-rc), .syscall = .readv, .fd = args.fd, + .from_libuv = true, } }; } return Maybe(Return.Readv).initResult(.{ .bytes_read = @intCast(rc) }); } - pub fn readv(this: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { - return if (args.position != null) _preadv(this, args, flavor) else _readv(this, args, flavor); + pub fn readv(this: *NodeFS, args: Arguments.Readv, _: Flavor) Maybe(Return.Readv) { + return if (args.position != null) preadvInner(this, args) else readvInner(this, args); } - pub fn writev(this: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Writev) { - return if (args.position != null) _pwritev(this, args, flavor) else _writev(this, args, flavor); + pub fn writev(this: *NodeFS, args: Arguments.Writev, _: Flavor) Maybe(Return.Writev) { + return if (args.position != null) pwritevInner(this, args) else writevInner(this, args); } - pub fn write(this: *NodeFS, args: Arguments.Write, comptime flavor: Flavor) Maybe(Return.Write) { - return if (args.position != null) _pwrite(this, args, flavor) else _write(this, args, flavor); + pub fn write(this: *NodeFS, args: Arguments.Write, _: Flavor) Maybe(Return.Write) { + return if (args.position != null) pwriteInner(this, args) else writeInner(this, args); } pub fn uv_write(this: *NodeFS, args: Arguments.Write, rc: i64) Maybe(Return.Write) { @@ -4109,6 +4114,7 @@ pub const NodeFS = struct { .errno = @intCast(-rc), .syscall = .write, .fd = args.fd, + .from_libuv = true, } }; } return Maybe(Return.Write).initResult(.{ .bytes_written = @intCast(rc) }); @@ -4121,14 +4127,13 @@ pub const NodeFS = struct { .errno = @intCast(-rc), .syscall = .writev, .fd = args.fd, + .from_libuv = true, } }; } return Maybe(Return.Writev).initResult(.{ .bytes_written = @intCast(rc) }); } - fn _write(_: *NodeFS, args: Arguments.Write, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; - + fn writeInner(_: *NodeFS, args: Arguments.Write) Maybe(Return.Write) { var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; @@ -4145,8 +4150,7 @@ pub const NodeFS = struct { }; } - fn _pwrite(_: *NodeFS, args: Arguments.Write, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; + fn pwriteInner(_: *NodeFS, args: Arguments.Write) Maybe(Return.Write) { const position = args.position.?; var buf = args.buffer.slice(); @@ -4154,17 +4158,18 @@ pub const NodeFS = struct { buf = buf[0..@min(args.length, buf.len)]; return switch (Syscall.pwrite(args.fd, buf, position)) { - .err => |err| .{ - .err = err, - }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .fd = args.fd, + .syscall = .write, + } }, .result => |amt| .{ .result = .{ .bytes_written = @as(u52, @truncate(amt)), } }, }; } - fn _preadv(_: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { - _ = flavor; + fn preadvInner(_: *NodeFS, args: Arguments.Readv) Maybe(Return.Readv) { const position = args.position.?; return switch (Syscall.preadv(args.fd, args.buffers.buffers.items, position)) { @@ -4177,8 +4182,7 @@ pub const NodeFS = struct { }; } - fn _readv(_: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { - _ = flavor; + fn readvInner(_: *NodeFS, args: Arguments.Readv) Maybe(Return.Readv) { return switch (Syscall.readv(args.fd, args.buffers.buffers.items)) { .err => |err| .{ .err = err, @@ -4189,8 +4193,7 @@ pub const NodeFS = struct { }; } - fn _pwritev(_: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; + fn pwritevInner(_: *NodeFS, args: Arguments.Writev) Maybe(Return.Write) { const position = args.position.?; return switch (Syscall.pwritev(args.fd, @ptrCast(args.buffers.buffers.items), position)) { .err => |err| .{ @@ -4202,8 +4205,7 @@ pub const NodeFS = struct { }; } - fn _writev(_: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; + fn writevInner(_: *NodeFS, args: Arguments.Writev) Maybe(Return.Write) { return switch (Syscall.writev(args.fd, @ptrCast(args.buffers.buffers.items))) { .err => |err| .{ .err = err, @@ -4221,13 +4223,21 @@ pub const NodeFS = struct { } } - return switch (args.recursive) { + const maybe = switch (args.recursive) { inline else => |recursive| switch (args.tag()) { - .buffers => _readdir(&this.sync_error_buf, args, Buffer, recursive, flavor), - .with_file_types => _readdir(&this.sync_error_buf, args, Dirent, recursive, flavor), - .files => _readdir(&this.sync_error_buf, args, bun.String, recursive, flavor), + .buffers => readdirInner(&this.sync_error_buf, args, Buffer, recursive, flavor), + .with_file_types => readdirInner(&this.sync_error_buf, args, Dirent, recursive, flavor), + .files => readdirInner(&this.sync_error_buf, args, bun.String, recursive, flavor), }, }; + return switch (maybe) { + .err => |err| .{ .err = .{ + .syscall = .scandir, + .errno = err.errno, + .path = err.path, + } }, + .result => |result| .{ .result = result }, + }; } fn readdirWithEntries( @@ -4619,7 +4629,7 @@ pub const NodeFS = struct { return null; } - fn _readdir( + fn readdirInner( buf: *bun.PathBuffer, args: Arguments.Readdir, comptime ExpectedType: type, @@ -4771,8 +4781,8 @@ pub const NodeFS = struct { break :brk switch (bun.sys.open( path, - bun.O.RDONLY | bun.O.NOCTTY, - 0, + @intFromEnum(args.flag) | bun.O.NOCTTY, + default_permission, )) { .err => |err| return .{ .err = err.withPath(if (args.path == .path) args.path.path.slice() else ""), @@ -5153,7 +5163,7 @@ pub const NodeFS = struct { if (Environment.isWindows) { _ = std.os.windows.kernel32.SetEndOfFile(fd.cast()); } else { - _ = ftruncateSync(.{ .fd = fd, .len = @as(JSC.WebCore.Blob.SizeType, @truncate(written)) }); + _ = Syscall.ftruncate(fd, @intCast(@as(u63, @truncate(written)))); } } @@ -5193,16 +5203,39 @@ pub const NodeFS = struct { }; } + pub fn realpathNonNative(this: *NodeFS, args: Arguments.Realpath, comptime _: Flavor) Maybe(Return.Realpath) { + // For `fs.realpath`, Node.js uses `lstat`, exposing the native system call under + // `fs.realpath.native`. In Bun, the system call is the default, but the error + // code must be changed to make it seem like it is using lstat (tests expect this) + return switch (this.realpathInner(args)) { + .result => |res| .{ .result = res }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .syscall = .lstat, + .path = args.path.slice(), + } }, + }; + } + pub fn realpath(this: *NodeFS, args: Arguments.Realpath, comptime _: Flavor) Maybe(Return.Realpath) { + // Native realpath needs to force `realpath` as the name + return switch (this.realpathInner(args)) { + .result => |res| .{ .result = res }, + .err => |err| .{ + .err = .{ + .errno = err.errno, + .syscall = .realpath, + .path = args.path.slice(), + }, + }, + }; + } + + pub fn realpathInner(this: *NodeFS, args: Arguments.Realpath) Maybe(Return.Realpath) { if (Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); - const rc = uv.uv_fs_realpath( - bun.Async.Loop.get(), - &req, - args.path.sliceZ(&this.sync_error_buf).ptr, - null, - ); + const rc = uv.uv_fs_realpath(bun.Async.Loop.get(), &req, args.path.sliceZ(&this.sync_error_buf).ptr, null); if (rc.errno()) |errno| return .{ .err = Syscall.Error{ @@ -5283,21 +5316,17 @@ pub const NodeFS = struct { } pub const realpathNative = realpath; - // pub fn realpathNative(this: *NodeFS, args: Arguments.Realpath, comptime flavor: Flavor) Maybe(Return.Realpath) { - // _ = args; - // - // - // return error.NotImplementedYet; - // } - pub fn rename(this: *NodeFS, args: Arguments.Rename, comptime flavor: Flavor) Maybe(Return.Rename) { - _ = flavor; + pub fn rename(this: *NodeFS, args: Arguments.Rename, _: Flavor) Maybe(Return.Rename) { const from_buf = &this.sync_error_buf; var to_buf: bun.PathBuffer = undefined; const from = args.old_path.sliceZ(from_buf); const to = args.new_path.sliceZ(&to_buf); - return Syscall.rename(from, to); + return switch (Syscall.rename(from, to)) { + .result => |result| .{ .result = result }, + .err => |err| .{ .err = err.withPathDest(args.old_path.slice(), args.new_path.slice()) }, + }; } pub fn rmdir(this: *NodeFS, args: Arguments.RmDir, comptime _: Flavor) Maybe(Return.Rmdir) { @@ -5457,7 +5486,8 @@ pub const NodeFS = struct { } pub fn stat(this: *NodeFS, args: Arguments.Stat, comptime _: Flavor) Maybe(Return.Stat) { - return switch (Syscall.stat(args.path.sliceZ(&this.sync_error_buf))) { + const path = args.path.sliceZ(&this.sync_error_buf); + return switch (Syscall.stat(path)) { .result => |result| .{ .result = .{ .stats = Stats.init(result, args.big_int) }, }, @@ -5465,7 +5495,7 @@ pub const NodeFS = struct { if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { return .{ .result = .{ .not_found = {} } }; } - break :brk .{ .err = err }; + break :brk .{ .err = err.withPath(path) }; }, }; } @@ -5477,13 +5507,8 @@ pub const NodeFS = struct { const target: [:0]u8 = args.old_path.sliceZWithForceCopy(&this.sync_error_buf, true); // UV does not normalize slashes in symlink targets, but Node does // See https://github.com/oven-sh/bun/issues/8273 - // - // TODO: investigate if simd can be easily used here - for (target) |*c| { - if (c.* == '/') { - c.* = '\\'; - } - } + bun.path.dangerouslyConvertPathToWindowsInPlace(u8, target); + return Syscall.symlinkUV( target, args.new_path.sliceZ(&to_buf), @@ -5495,13 +5520,16 @@ pub const NodeFS = struct { ); } - return Syscall.symlink( + return switch (Syscall.symlink( args.old_path.sliceZ(&this.sync_error_buf), args.new_path.sliceZ(&to_buf), - ); + )) { + .result => |result| .{ .result = result }, + .err => |err| .{ .err = err.withPathDest(args.old_path.slice(), args.new_path.slice()) }, + }; } - fn _truncate(this: *NodeFS, path: PathLike, len: JSC.WebCore.Blob.SizeType, flags: i32, comptime _: Flavor) Maybe(Return.Truncate) { + fn truncateInner(this: *NodeFS, path: PathLike, len: JSC.WebCore.Blob.SizeType, flags: i32) Maybe(Return.Truncate) { if (comptime Environment.isWindows) { const file = bun.sys.open( path.sliceZ(&this.sync_error_buf), @@ -5518,17 +5546,13 @@ pub const NodeFS = struct { Maybe(Return.Truncate).success; } - pub fn truncate(this: *NodeFS, args: Arguments.Truncate, comptime flavor: Flavor) Maybe(Return.Truncate) { + pub fn truncate(this: *NodeFS, args: Arguments.Truncate, _: Flavor) Maybe(Return.Truncate) { return switch (args.path) { - .fd => |fd| this.ftruncate( - Arguments.FTruncate{ .fd = fd, .len = args.len }, - flavor, - ), - .path => this._truncate( + .fd => |fd| Syscall.ftruncate(fd, args.len), + .path => this.truncateInner( args.path.path, args.len, args.flags, - flavor, ), }; } @@ -5576,7 +5600,7 @@ pub const NodeFS = struct { return if (rc.errno()) |errno| .{ .err = Syscall.Error{ .errno = errno, - .syscall = .utimes, + .syscall = .utime, } } else Maybe(Return.Utimes).success; @@ -5595,7 +5619,7 @@ pub const NodeFS = struct { }, }; - return if (Maybe(Return.Utimes).errnoSysP(std.c.utimes(args.path.sliceZ(&this.sync_error_buf), ×), .utimes, args.path.slice())) |err| + return if (Maybe(Return.Utimes).errnoSysP(std.c.utimes(args.path.sliceZ(&this.sync_error_buf), ×), .utime, args.path.slice())) |err| err else Maybe(Return.Utimes).success; @@ -5616,7 +5640,7 @@ pub const NodeFS = struct { return if (rc.errno()) |errno| .{ .err = Syscall.Error{ .errno = errno, - .syscall = .utimes, + .syscall = .utime, } } else Maybe(Return.Utimes).success; @@ -5635,34 +5659,33 @@ pub const NodeFS = struct { }, }; - return if (Maybe(Return.Lutimes).errnoSysP(C.lutimes(args.path.sliceZ(&this.sync_error_buf), ×), .lutimes, args.path.slice())) |err| + return if (Maybe(Return.Lutimes).errnoSysP(C.lutimes(args.path.sliceZ(&this.sync_error_buf), ×), .lutime, args.path.slice())) |err| err else Maybe(Return.Lutimes).success; } pub fn watch(_: *NodeFS, args: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { - return args.createFSWatcher(); + return switch (args.createFSWatcher()) { + .result => |result| .{ .result = result.js_this }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .syscall = err.syscall, + .path = if (err.path.len > 0) args.path.slice() else "", + } }, + }; } /// This function is `cpSync`, but only if you pass `{ recursive: ..., force: ..., errorOnExist: ..., mode: ... }' /// The other options like `filter` use a JS fallback, see `src/js/internal/fs/cp.ts` - pub fn cp(this: *NodeFS, args: Arguments.Cp, comptime flavor: Flavor) Maybe(Return.Cp) { - comptime bun.assert(flavor == .sync); - - var src_buf: bun.PathBuffer = undefined; - var dest_buf: bun.PathBuffer = undefined; + pub fn cp(this: *NodeFS, args: Arguments.Cp, _: Flavor) Maybe(Return.Cp) { + var src_buf: bun.OSPathBuffer = undefined; + var dest_buf: bun.OSPathBuffer = undefined; const src = args.src.osPath(&src_buf); const dest = args.dest.osPath(&dest_buf); - return this._cpSync( - @as(*bun.OSPathBuffer, @alignCast(@ptrCast(&src_buf))), - @intCast(src.len), - @as(*bun.OSPathBuffer, @alignCast(@ptrCast(&dest_buf))), - @intCast(dest.len), - args, - ); + return this.cpSyncInner(&src_buf, @intCast(src.len), &dest_buf, @intCast(dest.len), args); } pub fn osPathIntoSyncErrorBuf(this: *NodeFS, slice: anytype) []const u8 { @@ -5676,13 +5699,14 @@ pub const NodeFS = struct { pub fn osPathIntoSyncErrorBufOverlap(this: *NodeFS, slice: anytype) []const u8 { if (Environment.isWindows) { - var tmp: bun.WPathBuffer = undefined; + const tmp = bun.OSPathBufferPool.get(); + defer bun.OSPathBufferPool.put(tmp); @memcpy(tmp[0..slice.len], slice); return bun.strings.fromWPath(&this.sync_error_buf, tmp[0..slice.len]); - } else {} + } } - fn _cpSync( + fn cpSyncInner( this: *NodeFS, src_buf: *bun.OSPathBuffer, src_dir_len: PathString.PathInt, @@ -5708,7 +5732,7 @@ pub const NodeFS = struct { const r = this._copySingleFileSync( src, dest, - @enumFromInt((if (cp_flags.errorOnExist or !cp_flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), + @enumFromInt(if (cp_flags.errorOnExist or !cp_flags.force) Constants.COPYFILE_EXCL else @as(u8, 0)), attributes, args, ); @@ -5742,13 +5766,11 @@ pub const NodeFS = struct { } if (!cp_flags.recursive) { - return .{ - .err = .{ - .errno = @intFromEnum(E.ISDIR), - .syscall = .copyfile, - .path = this.osPathIntoSyncErrorBuf(src), - }, - }; + return .{ .err = .{ + .errno = @intFromEnum(E.ISDIR), + .syscall = .copyfile, + .path = this.osPathIntoSyncErrorBuf(src), + } }; } if (comptime Environment.isMac) try_with_clonefile: { @@ -5788,7 +5810,7 @@ pub const NodeFS = struct { defer _ = Syscall.close(fd); switch (this.mkdirRecursiveOSPath(dest, Arguments.Mkdir.DefaultMode, false)) { - .err => |err| return Maybe(Return.Cp){ .err = err }, + .err => |err| return .{ .err = err }, .result => {}, } @@ -5815,7 +5837,7 @@ pub const NodeFS = struct { switch (current.kind) { .directory => { - const r = this._cpSync( + const r = this.cpSyncInner( src_buf, src_dir_len + @as(PathString.PathInt, @intCast(1 + name_slice.len)), dest_buf, @@ -5970,7 +5992,7 @@ pub const NodeFS = struct { const mkdirResult = this.mkdirRecursive(.{ .path = PathLike{ .string = PathString.init(dest[0..len]) }, .recursive = true, - }, .sync); + }); if (mkdirResult == .err) { return Maybe(Return.CopyFile){ .err = mkdirResult.err }; } @@ -6036,7 +6058,7 @@ pub const NodeFS = struct { const stat_: linux.Stat = switch (Syscall.fstat(src_fd)) { .result => |result| result, - .err => |err| return Maybe(Return.CopyFile){ .err = err }, + .err => |err| return Maybe(Return.CopyFile){ .err = err.withFd(src_fd) }, }; if (!posix.S.ISREG(stat_.mode)) { @@ -6065,7 +6087,7 @@ pub const NodeFS = struct { const mkdirResult = this.mkdirRecursive(.{ .path = PathLike{ .string = PathString.init(dest[0..len]) }, .recursive = true, - }, .sync); + }); if (mkdirResult == .err) { return Maybe(Return.CopyFile){ .err = mkdirResult.err }; } @@ -6187,8 +6209,9 @@ pub const NodeFS = struct { .err => |err| return .{ .err = err }, .result => |src_fd| src_fd, }; - var wbuf: bun.WPathBuffer = undefined; - const len = bun.windows.GetFinalPathNameByHandleW(handle.cast(), &wbuf, wbuf.len, 0); + const wbuf = bun.OSPathBufferPool.get(); + defer bun.OSPathBufferPool.put(wbuf); + const len = bun.windows.GetFinalPathNameByHandleW(handle.cast(), wbuf, wbuf.len, 0); if (len == 0) { return ret.errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(dest)) orelse dst_enoent_maybe; } @@ -6226,13 +6249,19 @@ pub const NodeFS = struct { } }; +fn throwInvalidFdError(global: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError { + if (value.isNumber()) { + return global.ERR_OUT_OF_RANGE("The value of \"fd\" is out of range. It must be an integer. Received {d}", .{bun.fmt.double(value.asNumber())}).throw(); + } + return JSC.Node.validators.throwErrInvalidArgType(global, "fd", .{}, "number", value); +} + pub export fn Bun__mkdirp(globalThis: *JSC.JSGlobalObject, path: [*:0]const u8) bool { return globalThis.bunVM().nodeFS().mkdirRecursive( Arguments.Mkdir{ .path = PathLike{ .string = PathString.init(bun.span(path)) }, .recursive = true, }, - .sync, ) != .err; } diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index cee9ac021f..89e4bea172 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -14,95 +14,69 @@ const NodeFSFunction = fn (this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobal const NodeFSFunctionEnum = std.meta.DeclEnum(JSC.Node.NodeFS); -fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { - const Function = @field(JSC.Node.NodeFS, @tagName(FunctionEnum)); - const FunctionType = @TypeOf(Function); +/// Returns bindings to call JSC.Node.NodeFS.. +/// Async calls use a thread pool. +fn Bindings(comptime function_name: NodeFSFunctionEnum) type { + const function = @field(JSC.Node.NodeFS, @tagName(function_name)); + const fn_info = @typeInfo(@TypeOf(function)).Fn; + if (fn_info.params.len != 3) { + @compileError("Expected fn(NodeFS, Arguments) Return for NodeFS." ++ @tagName(function_name)); + } + const Arguments = fn_info.params[1].type.?; - const function: std.builtin.Type.Fn = comptime @typeInfo(FunctionType).Fn; - comptime if (function.params.len != 3) @compileError("Expected 3 arguments"); - const Arguments = comptime function.params[1].type.?; - const FormattedName = comptime [1]u8{std.ascii.toUpper(@tagName(FunctionEnum)[0])} ++ @tagName(FunctionEnum)[1..]; - const Result = comptime JSC.Maybe(@field(JSC.Node.NodeFS.ReturnType, FormattedName)); - _ = Result; - - const NodeBindingClosure = struct { - pub fn bind(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var arguments = callframe.arguments_old(8); - - var slice = ArgumentsSlice.init(globalObject.bunVM(), arguments.slice()); + return struct { + pub fn runSync(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var slice = ArgumentsSlice.init(globalObject.bunVM(), callframe.arguments()); defer slice.deinit(); - const args = if (comptime Arguments != void) - (try Arguments.fromJS(globalObject, &slice)) - else - Arguments{}; - defer { - if (comptime Arguments != void and @hasDecl(Arguments, "deinit")) args.deinit(); - } + const args = if (Arguments != void) + try Arguments.fromJS(globalObject, &slice); + + defer if (comptime Arguments != void and @hasDecl(Arguments, "deinit")) + args.deinit(); if (globalObject.hasException()) { return .zero; } - var result = Function( - &this.node_fs, - args, - comptime Flavor.sync, - ); - switch (result) { - .err => |err| { - return globalObject.throwValue(JSC.JSValue.c(err.toJS(globalObject))); - }, - .result => |*res| { - return globalObject.toJS(res, .temporary); - }, - } + + var result = function(&this.node_fs, args, .sync); + return switch (result) { + .err => |err| globalObject.throwValue(JSC.JSValue.c(err.toJS(globalObject))), + .result => |*res| globalObject.toJS(res, .temporary), + }; } - }; - return NodeBindingClosure.bind; -} - -fn call(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { - const Function = @field(JSC.Node.NodeFS, @tagName(FunctionEnum)); - const FunctionType = @TypeOf(Function); - - const function: std.builtin.Type.Fn = comptime @typeInfo(FunctionType).Fn; - comptime if (function.params.len != 3) @compileError("Expected 3 arguments"); - const Arguments = comptime function.params[1].type.?; - const NodeBindingClosure = struct { - pub fn bind(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var arguments = callframe.arguments_old(8); - - var slice = ArgumentsSlice.init(globalObject.bunVM(), arguments.slice()); + pub fn runAsync(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var slice = ArgumentsSlice.init(globalObject.bunVM(), callframe.arguments()); slice.will_be_async = true; - const args = if (comptime Arguments != void) - (Arguments.fromJS(globalObject, &slice) catch { + + const args = if (Arguments != void) + Arguments.fromJS(globalObject, &slice) catch |err| { slice.deinit(); - return .zero; - }) - else - Arguments{}; + return err; + }; if (globalObject.hasException()) { slice.deinit(); return .zero; } - const Task = @field(JSC.Node.Async, @tagName(FunctionEnum)); - if (comptime FunctionEnum == .cp) { - return Task.create(globalObject, this, args, globalObject.bunVM(), slice.arena); - } else { - if (comptime FunctionEnum == .readdir) { - if (args.recursive) { - return JSC.Node.Async.readdir_recursive.create(globalObject, args, globalObject.bunVM()); - } - } - - return Task.create(globalObject, this, args, globalObject.bunVM()); + const Task = @field(JSC.Node.Async, @tagName(function_name)); + switch (comptime function_name) { + .cp => return Task.create(globalObject, this, args, globalObject.bunVM(), slice.arena), + .readdir => if (args.recursive) return JSC.Node.Async.readdir_recursive.create(globalObject, args, globalObject.bunVM()), + else => {}, } + return Task.create(globalObject, this, args, globalObject.bunVM()); } }; - return NodeBindingClosure.bind; +} + +fn callAsync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { + return Bindings(FunctionEnum).runAsync; +} +fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { + return Bindings(FunctionEnum).runSync; } pub const NodeJSFS = struct { @@ -121,43 +95,52 @@ pub const NodeJSFS = struct { this.destroy(); } - pub const access = call(.access); - pub const appendFile = call(.appendFile); - pub const close = call(.close); - pub const copyFile = call(.copyFile); - pub const cp = call(.cp); - pub const exists = call(.exists); - pub const chown = call(.chown); - pub const chmod = call(.chmod); - pub const fchmod = call(.fchmod); - pub const fchown = call(.fchown); - pub const fstat = call(.fstat); - pub const fsync = call(.fsync); - pub const ftruncate = call(.ftruncate); - pub const futimes = call(.futimes); - pub const lchmod = call(.lchmod); - pub const lchown = call(.lchown); - pub const link = call(.link); - pub const lstat = call(.lstat); - pub const mkdir = call(.mkdir); - pub const mkdtemp = call(.mkdtemp); - pub const open = call(.open); - pub const read = call(.read); - pub const write = call(.write); - pub const readdir = call(.readdir); - pub const readFile = call(.readFile); - pub const writeFile = call(.writeFile); - pub const readlink = call(.readlink); - pub const rm = call(.rm); - pub const rmdir = call(.rmdir); - pub const realpath = call(.realpath); - pub const rename = call(.rename); - pub const stat = call(.stat); - pub const symlink = call(.symlink); - pub const truncate = call(.truncate); - pub const unlink = call(.unlink); - pub const utimes = call(.utimes); - pub const lutimes = call(.lutimes); + pub fn getDirent(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.Node.Dirent.getConstructor(globalThis); + } + + pub fn getStats(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.Node.StatsSmall.getConstructor(globalThis); + } + + pub const access = callAsync(.access); + pub const appendFile = callAsync(.appendFile); + pub const close = callAsync(.close); + pub const copyFile = callAsync(.copyFile); + pub const cp = callAsync(.cp); + pub const exists = callAsync(.exists); + pub const chown = callAsync(.chown); + pub const chmod = callAsync(.chmod); + pub const fchmod = callAsync(.fchmod); + pub const fchown = callAsync(.fchown); + pub const fstat = callAsync(.fstat); + pub const fsync = callAsync(.fsync); + pub const ftruncate = callAsync(.ftruncate); + pub const futimes = callAsync(.futimes); + pub const lchmod = callAsync(.lchmod); + pub const lchown = callAsync(.lchown); + pub const link = callAsync(.link); + pub const lstat = callAsync(.lstat); + pub const mkdir = callAsync(.mkdir); + pub const mkdtemp = callAsync(.mkdtemp); + pub const open = callAsync(.open); + pub const read = callAsync(.read); + pub const write = callAsync(.write); + pub const readdir = callAsync(.readdir); + pub const readFile = callAsync(.readFile); + pub const writeFile = callAsync(.writeFile); + pub const readlink = callAsync(.readlink); + pub const rm = callAsync(.rm); + pub const rmdir = callAsync(.rmdir); + pub const realpath = callAsync(.realpathNonNative); + pub const realpathNative = callAsync(.realpath); + pub const rename = callAsync(.rename); + pub const stat = callAsync(.stat); + pub const symlink = callAsync(.symlink); + pub const truncate = callAsync(.truncate); + pub const unlink = callAsync(.unlink); + pub const utimes = callAsync(.utimes); + pub const lutimes = callAsync(.lutimes); pub const accessSync = callSync(.access); pub const appendFileSync = callSync(.appendFile); pub const closeSync = callSync(.close); @@ -185,7 +168,8 @@ pub const NodeJSFS = struct { pub const readFileSync = callSync(.readFile); pub const writeFileSync = callSync(.writeFile); pub const readlinkSync = callSync(.readlink); - pub const realpathSync = callSync(.realpath); + pub const realpathSync = callSync(.realpathNonNative); + pub const realpathNativeSync = callSync(.realpath); pub const renameSync = callSync(.rename); pub const statSync = callSync(.stat); pub const symlinkSync = callSync(.symlink); @@ -195,30 +179,15 @@ pub const NodeJSFS = struct { pub const lutimesSync = callSync(.lutimes); pub const rmSync = callSync(.rm); pub const rmdirSync = callSync(.rmdir); - pub const writev = call(.writev); + pub const writev = callAsync(.writev); pub const writevSync = callSync(.writev); - pub const readv = call(.readv); + pub const readv = callAsync(.readv); pub const readvSync = callSync(.readv); - pub const fdatasyncSync = callSync(.fdatasync); - pub const fdatasync = call(.fdatasync); - - pub fn getDirent(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.Node.Dirent.getConstructor(globalThis); - } - - pub fn getStats(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.Node.StatsSmall.getConstructor(globalThis); - } - + pub const fdatasync = callAsync(.fdatasync); pub const watch = callSync(.watch); pub const watchFile = callSync(.watchFile); pub const unwatchFile = callSync(.unwatchFile); - - // Not implemented yet: - const notimpl = fdatasync; - pub const opendir = notimpl; - pub const opendirSync = notimpl; }; pub fn createBinding(globalObject: *JSC.JSGlobalObject) JSC.JSValue { diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index 55418ffca8..f7bfb1eece 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -4,7 +4,7 @@ const bun = @import("root").bun; const Fs = @import("../../fs.zig"); const Path = @import("../../resolver/resolve_path.zig"); const Encoder = JSC.WebCore.Encoder; -const Mutex = @import("../../lock.zig").Lock; +const Mutex = bun.Mutex; const uws = @import("../../deps/uws.zig"); const PathWatcher = @import("./path_watcher.zig"); @@ -409,6 +409,10 @@ pub const StatWatcher = struct { }, ) catch |err| this.globalThis.reportActiveExceptionAsUnhandled(err); + if (this.closed) { + this.used_by_scheduler_thread.store(false, .release); + return; + } vm.rareData().nodeFSStatWatcherScheduler(vm).append(this); } diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index f2ccb8258c..c718225e15 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -4,7 +4,7 @@ const bun = @import("root").bun; const Fs = @import("../../fs.zig"); const Path = @import("../../resolver/resolve_path.zig"); const Encoder = JSC.WebCore.Encoder; -const Mutex = @import("../../lock.zig").Lock; +const Mutex = bun.Mutex; const VirtualMachine = JSC.VirtualMachine; const EventLoop = JSC.EventLoop; @@ -427,12 +427,7 @@ pub const FSWatcher = struct { }; } - pub fn createFSWatcher(this: Arguments) JSC.Maybe(JSC.JSValue) { - return switch (FSWatcher.init(this)) { - .result => |result| .{ .result = result.js_this }, - .err => |err| .{ .err = err }, - }; - } + pub const createFSWatcher = FSWatcher.init; }; pub fn initJS(this: *FSWatcher, listener: JSC.JSValue) void { diff --git a/src/bun.js/node/node_os.bind.ts b/src/bun.js/node/node_os.bind.ts new file mode 100644 index 0000000000..db3082c4b7 --- /dev/null +++ b/src/bun.js/node/node_os.bind.ts @@ -0,0 +1,94 @@ +// Internal bindings for node:os +// The entrypoint for node:os is `src/js/node/os.ts` +import { fn, t } from "bindgen"; + +export const cpus = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const freemem = fn({ + args: {}, + ret: t.u64, +}); +export const getPriority = fn({ + args: { + global: t.globalObject, + pid: t.i32.validateInt32().default(0).nonNull, + }, + ret: t.i32, +}); +export const homedir = fn({ + args: { + global: t.globalObject, + }, + ret: t.DOMString, +}); +export const hostname = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const loadavg = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const networkInterfaces = fn({ + args: { + global: t.globalObject, + }, + ret: t.any, +}); +export const release = fn({ + args: {}, + ret: t.DOMString, +}); +export const totalmem = fn({ + args: {}, + ret: t.u64, +}); +export const uptime = fn({ + args: { + global: t.globalObject, + }, + ret: t.f64, +}); +export const UserInfoOptions = t.dictionary({ + encoding: t.DOMString.default(""), +}); +export const userInfo = fn({ + args: { + global: t.globalObject, + options: UserInfoOptions.default({}), + }, + ret: t.any, +}); +export const version = fn({ + args: {}, + ret: t.DOMString, +}); +const PRI_MIN = -20; +const PRI_MAX = 19; +export const setPriority = fn({ + variants: [ + { + args: { + global: t.globalObject, + pid: t.i32.validateInt32(), + priority: t.i32.validateInt32(PRI_MIN, PRI_MAX), + }, + ret: t.undefined, + }, + { + args: { + global: t.globalObject, + priority: t.i32.validateInt32(PRI_MIN, PRI_MAX), + }, + ret: t.undefined, + }, + ], +}); diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index a7c4da6691..fc60648fee 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -7,856 +7,825 @@ const strings = bun.strings; const JSC = bun.JSC; const Environment = bun.Environment; const Global = bun.Global; -const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; - const libuv = bun.windows.libuv; -pub const OS = struct { - pub fn create(globalObject: *JSC.JSGlobalObject) JSC.JSValue { - const module = JSC.JSValue.createEmptyObject(globalObject, 16); +const gen = bun.gen.node_os; - module.put(globalObject, JSC.ZigString.static("cpus"), JSC.NewFunction(globalObject, JSC.ZigString.static("cpus"), 0, cpus, false)); - module.put(globalObject, JSC.ZigString.static("freemem"), JSC.NewFunction(globalObject, JSC.ZigString.static("freemem"), 0, freemem, false)); - module.put(globalObject, JSC.ZigString.static("getPriority"), JSC.NewFunction(globalObject, JSC.ZigString.static("getPriority"), 1, getPriority, false)); - module.put(globalObject, JSC.ZigString.static("homedir"), JSC.NewFunction(globalObject, JSC.ZigString.static("homedir"), 0, homedir, false)); - module.put(globalObject, JSC.ZigString.static("hostname"), JSC.NewFunction(globalObject, JSC.ZigString.static("hostname"), 0, hostname, false)); - module.put(globalObject, JSC.ZigString.static("loadavg"), JSC.NewFunction(globalObject, JSC.ZigString.static("loadavg"), 0, loadavg, false)); - module.put(globalObject, JSC.ZigString.static("machine"), JSC.NewFunction(globalObject, JSC.ZigString.static("machine"), 0, machine, false)); - module.put(globalObject, JSC.ZigString.static("networkInterfaces"), JSC.NewFunction(globalObject, JSC.ZigString.static("networkInterfaces"), 0, networkInterfaces, false)); - module.put(globalObject, JSC.ZigString.static("release"), JSC.NewFunction(globalObject, JSC.ZigString.static("release"), 0, release, false)); - module.put(globalObject, JSC.ZigString.static("setPriority"), JSC.NewFunction(globalObject, JSC.ZigString.static("setPriority"), 2, setPriority, false)); - module.put(globalObject, JSC.ZigString.static("totalmem"), JSC.NewFunction(globalObject, JSC.ZigString.static("totalmem"), 0, totalmem, false)); - module.put(globalObject, JSC.ZigString.static("type"), JSC.NewFunction(globalObject, JSC.ZigString.static("type"), 0, OS.type, false)); - module.put(globalObject, JSC.ZigString.static("uptime"), JSC.NewFunction(globalObject, JSC.ZigString.static("uptime"), 0, uptime, false)); - module.put(globalObject, JSC.ZigString.static("userInfo"), JSC.NewFunction(globalObject, JSC.ZigString.static("userInfo"), 0, userInfo, false)); - module.put(globalObject, JSC.ZigString.static("version"), JSC.NewFunction(globalObject, JSC.ZigString.static("version"), 0, version, false)); - module.put(globalObject, JSC.ZigString.static("machine"), JSC.NewFunction(globalObject, JSC.ZigString.static("machine"), 0, machine, false)); +pub fn createNodeOsBinding(global: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSObject.create(.{ + .cpus = gen.createCpusCallback(global), + .freemem = gen.createFreememCallback(global), + .getPriority = gen.createGetPriorityCallback(global), + .homedir = gen.createHomedirCallback(global), + .hostname = gen.createHostnameCallback(global), + .loadavg = gen.createLoadavgCallback(global), + .networkInterfaces = gen.createNetworkInterfacesCallback(global), + .release = gen.createReleaseCallback(global), + .totalmem = gen.createTotalmemCallback(global), + .uptime = gen.createUptimeCallback(global), + .userInfo = gen.createUserInfoCallback(global), + .version = gen.createVersionCallback(global), + .setPriority = gen.createSetPriorityCallback(global), + }, global).toJS(); +} - return module; +const CPUTimes = struct { + user: u64 = 0, + nice: u64 = 0, + sys: u64 = 0, + idle: u64 = 0, + irq: u64 = 0, + + pub fn toValue(self: CPUTimes, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + const fields = comptime std.meta.fieldNames(CPUTimes); + const ret = JSC.JSValue.createEmptyObject(globalThis, fields.len); + inline for (fields) |fieldName| { + ret.put(globalThis, JSC.ZigString.static(fieldName), JSC.JSValue.jsNumberFromUint64(@field(self, fieldName))); + } + return ret; + } +}; + +pub fn cpus(global: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + const cpusImpl = switch (Environment.os) { + .linux => cpusImplLinux, + .mac => cpusImplDarwin, + .windows => cpusImplWindows, + else => @compileError("Unsupported OS"), + }; + + return cpusImpl(global) catch { + const err = JSC.SystemError{ + .message = bun.String.static("Failed to get CPU information"), + .code = bun.String.static(@tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR)), + }; + return global.throwValue(err.toErrorInstance(global)); + }; +} + +fn cpusImplLinux(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { + // Create the return array + const values = JSC.JSValue.createEmptyArray(globalThis, 0); + var num_cpus: u32 = 0; + + var stack_fallback = std.heap.stackFallback(1024 * 8, bun.default_allocator); + var file_buf = std.ArrayList(u8).init(stack_fallback.get()); + defer file_buf.deinit(); + + // Read /proc/stat to get number of CPUs and times + { + const file = try std.fs.openFileAbsolute("/proc/stat", .{}); + defer file.close(); + + const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); + defer file_buf.clearRetainingCapacity(); + const contents = file_buf.items[0..read]; + + var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); + + // Skip the first line (aggregate of all CPUs) + _ = line_iter.next(); + + // Read each CPU line + while (line_iter.next()) |line| { + // CPU lines are formatted as `cpu0 user nice sys idle iowait irq softirq` + var toks = std.mem.tokenize(u8, line, " \t"); + const cpu_name = toks.next(); + if (cpu_name == null or !std.mem.startsWith(u8, cpu_name.?, "cpu")) break; // done with CPUs + + //NOTE: libuv assumes this is fixed on Linux, not sure that's actually the case + const scale = 10; + + var times = CPUTimes{}; + times.user = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + times.nice = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + times.sys = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + times.idle = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + _ = try (toks.next() orelse error.eol); // skip iowait + times.irq = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); + + // Actually create the JS object representing the CPU + const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); + cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); + values.putIndex(globalThis, num_cpus, cpu); + + num_cpus += 1; + } } - const CPUTimes = struct { - user: u64 = 0, - nice: u64 = 0, - sys: u64 = 0, - idle: u64 = 0, - irq: u64 = 0, + // Read /proc/cpuinfo to get model information (optional) + if (std.fs.openFileAbsolute("/proc/cpuinfo", .{})) |file| { + defer file.close(); - pub fn toValue(self: CPUTimes, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const fields = comptime std.meta.fieldNames(CPUTimes); - const ret = JSC.JSValue.createEmptyObject(globalThis, fields.len); - inline for (fields) |fieldName| { - ret.put(globalThis, JSC.ZigString.static(fieldName), JSC.JSValue.jsNumberFromUint64(@field(self, fieldName))); + const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); + defer file_buf.clearRetainingCapacity(); + const contents = file_buf.items[0..read]; + + var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); + + const key_processor = "processor\t: "; + const key_model_name = "model name\t: "; + + var cpu_index: u32 = 0; + var has_model_name = true; + while (line_iter.next()) |line| { + if (strings.hasPrefixComptime(line, key_processor)) { + if (!has_model_name) { + const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); + } + // If this line starts a new processor, parse the index from the line + const digits = std.mem.trim(u8, line[key_processor.len..], " \t\n"); + cpu_index = try std.fmt.parseInt(u32, digits, 10); + if (cpu_index >= num_cpus) return error.too_may_cpus; + has_model_name = false; + } else if (strings.hasPrefixComptime(line, key_model_name)) { + // If this is the model name, extract it and store on the current cpu + const model_name = line[key_model_name.len..]; + const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(model_name).withEncoding().toJS(globalThis)); + has_model_name = true; } - return ret; + } + if (!has_model_name) { + const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); + } + } else |_| { + // Initialize model name to "unknown" + var it = values.arrayIterator(globalThis); + while (it.next()) |cpu| { + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); + } + } + + // Read /sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq to get current frequency (optional) + for (0..num_cpus) |cpu_index| { + const cpu = JSC.JSObject.getIndex(values, globalThis, @truncate(cpu_index)); + + var path_buf: [128]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq", .{cpu_index}); + if (std.fs.openFileAbsolute(path, .{})) |file| { + defer file.close(); + + const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, true).unwrap(); + defer file_buf.clearRetainingCapacity(); + const contents = file_buf.items[0..read]; + + const digits = std.mem.trim(u8, contents, " \n"); + const speed = (std.fmt.parseInt(u64, digits, 10) catch 0) / 1000; + + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed)); + } else |_| { + // Initialize CPU speed to 0 + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(0)); + } + } + + return values; +} + +extern fn bun_sysconf__SC_CLK_TCK() isize; +fn cpusImplDarwin(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { + const local_bindings = @import("../../darwin_c.zig"); + const c = std.c; + + // Fetch the CPU info structure + var num_cpus: c.natural_t = 0; + var info: [*]local_bindings.processor_cpu_load_info = undefined; + var info_size: std.c.mach_msg_type_number_t = 0; + if (local_bindings.host_processor_info(std.c.mach_host_self(), local_bindings.PROCESSOR_CPU_LOAD_INFO, &num_cpus, @as(*local_bindings.processor_info_array_t, @ptrCast(&info)), &info_size) != .SUCCESS) { + return error.no_processor_info; + } + defer _ = std.c.vm_deallocate(std.c.mach_task_self(), @intFromPtr(info), info_size); + + // Ensure we got the amount of data we expected to guard against buffer overruns + if (info_size != C.PROCESSOR_CPU_LOAD_INFO_COUNT * num_cpus) { + return error.broken_process_info; + } + + // Get CPU model name + var model_name_buf: [512]u8 = undefined; + var len: usize = model_name_buf.len; + // Try brand_string first and if it fails try hw.model + if (!(std.c.sysctlbyname("machdep.cpu.brand_string", &model_name_buf, &len, null, 0) == 0 or + std.c.sysctlbyname("hw.model", &model_name_buf, &len, null, 0) == 0)) + { + return error.no_processor_info; + } + // NOTE: sysctlbyname doesn't update len if it was large enough, so we + // still have to find the null terminator. All cpus can share the same + // model name. + const model_name = JSC.ZigString.init(std.mem.sliceTo(&model_name_buf, 0)).withEncoding().toJS(globalThis); + + // Get CPU speed + var speed: u64 = 0; + len = @sizeOf(@TypeOf(speed)); + _ = std.c.sysctlbyname("hw.cpufrequency", &speed, &len, null, 0); + if (speed == 0) { + // Suggested by Node implementation: + // If sysctl hw.cputype == CPU_TYPE_ARM64, the correct value is unavailable + // from Apple, but we can hard-code it here to a plausible value. + speed = 2_400_000_000; + } + + // Get the multiplier; this is the number of ms/tick + const ticks: i64 = bun_sysconf__SC_CLK_TCK(); + const multiplier = 1000 / @as(u64, @intCast(ticks)); + + // Set up each CPU value in the return + const values = JSC.JSValue.createEmptyArray(globalThis, @as(u32, @intCast(num_cpus))); + var cpu_index: u32 = 0; + while (cpu_index < num_cpus) : (cpu_index += 1) { + const times = CPUTimes{ + .user = info[cpu_index].cpu_ticks[0] * multiplier, + .nice = info[cpu_index].cpu_ticks[3] * multiplier, + .sys = info[cpu_index].cpu_ticks[1] * multiplier, + .idle = info[cpu_index].cpu_ticks[2] * multiplier, + .irq = 0, // not available + }; + + const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed / 1_000_000)); + cpu.put(globalThis, JSC.ZigString.static("model"), model_name); + cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); + + values.putIndex(globalThis, cpu_index, cpu); + } + return values; +} + +pub fn cpusImplWindows(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { + var cpu_infos: [*]libuv.uv_cpu_info_t = undefined; + var count: c_int = undefined; + const err = libuv.uv_cpu_info(&cpu_infos, &count); + if (err != 0) { + return error.NoProcessorInfo; + } + defer libuv.uv_free_cpu_info(cpu_infos, count); + + const values = JSC.JSValue.createEmptyArray(globalThis, @intCast(count)); + + for (cpu_infos[0..@intCast(count)], 0..@intCast(count)) |cpu_info, i| { + const times = CPUTimes{ + .user = cpu_info.cpu_times.user, + .nice = cpu_info.cpu_times.nice, + .sys = cpu_info.cpu_times.sys, + .idle = cpu_info.cpu_times.idle, + .irq = cpu_info.cpu_times.irq, + }; + + const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); + cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(bun.span(cpu_info.model)).withEncoding().toJS(globalThis)); + cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(cpu_info.speed)); + cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); + + values.putIndex(globalThis, @intCast(i), cpu); + } + + return values; +} + +pub fn freemem() u64 { + return C.getFreeMemory(); +} + +pub fn getPriority(global: *JSC.JSGlobalObject, pid: i32) bun.JSError!i32 { + return C.getProcessPriority(pid) orelse { + const err = JSC.SystemError{ + .message = bun.String.static("no such process"), + .code = bun.String.static("ESRCH"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.SRCH)), + .windows => libuv.UV_ESRCH, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }; +} + +pub fn homedir(global: *JSC.JSGlobalObject) !bun.String { + // In Node.js, this is a wrapper around uv_os_homedir. + if (Environment.isWindows) { + var out: bun.PathBuffer = undefined; + var size: usize = out.len; + if (libuv.uv_os_homedir(&out, &size).toError(.uv_os_homedir)) |err| { + return global.throwValue(err.toJSC(global)); + } + return bun.String.createUTF8(out[0..size]); + } else { + + // The posix implementation of uv_os_homedir first checks the HOME + // environment variable, then falls back to reading the passwd entry. + if (bun.getenvZ("HOME")) |home| { + if (home.len > 0) + return bun.String.init(home); + } + + // From libuv: + // > Calling sysconf(_SC_GETPW_R_SIZE_MAX) would get the suggested size, but it + // > is frequently 1024 or 4096, so we can just use that directly. The pwent + // > will not usually be large. + // Instead of always using an allocation, first try a stack allocation + // of 4096, then fallback to heap. + var stack_string_bytes: [4096]u8 = undefined; + var string_bytes: []u8 = &stack_string_bytes; + defer if (string_bytes.ptr != &stack_string_bytes) + bun.default_allocator.free(string_bytes); + + var pw: bun.C.passwd = undefined; + var result: ?*bun.C.passwd = null; + + const ret = while (true) { + const ret = bun.C.getpwuid_r( + bun.C.geteuid(), + &pw, + string_bytes.ptr, + string_bytes.len, + &result, + ); + + if (ret == @intFromEnum(bun.C.E.INTR)) + continue; + + // If the system call wants more memory, double it. + if (ret == @intFromEnum(bun.C.E.RANGE)) { + const len = string_bytes.len; + bun.default_allocator.free(string_bytes); + string_bytes = ""; + string_bytes = try bun.default_allocator.alloc(u8, len * 2); + continue; + } + + break ret; + }; + + if (ret != 0) { + return global.throwValue(bun.sys.Error.fromCode( + @enumFromInt(ret), + .uv_os_homedir, + ).toJSC(global)); + } + + if (result == null) { + // in uv__getpwuid_r, null result throws UV_ENOENT. + return global.throwValue(bun.sys.Error.fromCode( + .NOENT, + .uv_os_homedir, + ).toJSC(global)); + } + + return if (pw.pw_dir) |dir| + bun.String.createUTF8(bun.span(dir)) + else + bun.String.empty; + } +} + +pub fn hostname(global: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + if (Environment.isWindows) { + var name_buffer: [129:0]u16 = undefined; + if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { + const str = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); + defer str.deref(); + return str.toJS(global); + } + + var result: std.os.windows.ws2_32.WSADATA = undefined; + if (std.os.windows.ws2_32.WSAStartup(0x202, &result) == 0) { + if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { + var y = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); + defer y.deref(); + return y.toJS(global); + } + } + + return JSC.ZigString.init("unknown").withEncoding().toJS(global); + } else { + var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; + return JSC.ZigString.init(std.posix.gethostname(&name_buffer) catch "unknown").withEncoding().toJS(global); + } +} + +pub fn loadavg(global: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + const result = C.getSystemLoadavg(); + return JSC.JSArray.create(global, &.{ + JSC.JSValue.jsNumber(result[0]), + JSC.JSValue.jsNumber(result[1]), + JSC.JSValue.jsNumber(result[2]), + }); +} + +pub const networkInterfaces = switch (Environment.os) { + .linux, .mac => networkInterfacesPosix, + .windows => networkInterfacesWindows, + else => @compileError("Unsupported OS"), +}; + +fn networkInterfacesPosix(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + // getifaddrs sets a pointer to a linked list + var interface_start: ?*C.ifaddrs = null; + const rc = C.getifaddrs(&interface_start); + if (rc != 0) { + const err = JSC.SystemError{ + .message = bun.String.static("A system error occurred: getifaddrs returned an error"), + .code = bun.String.static("ERR_SYSTEM_ERROR"), + .errno = @intFromEnum(std.posix.errno(rc)), + .syscall = bun.String.static("getifaddrs"), + }; + + return globalThis.throwValue(err.toErrorInstance(globalThis)); + } + defer C.freeifaddrs(interface_start); + + const helpers = struct { + // We'll skip interfaces that aren't actually available + pub fn skip(iface: *C.ifaddrs) bool { + // Skip interfaces that aren't actually available + if (iface.ifa_flags & C.IFF_RUNNING == 0) return true; + if (iface.ifa_flags & C.IFF_UP == 0) return true; + if (iface.ifa_addr == null) return true; + + return false; + } + + // We won't actually return link-layer interfaces but we need them for + // extracting the MAC address + pub fn isLinkLayer(iface: *C.ifaddrs) bool { + if (iface.ifa_addr == null) return false; + return if (comptime Environment.isLinux) + return iface.ifa_addr.*.sa_family == std.posix.AF.PACKET + else if (comptime Environment.isMac) + return iface.ifa_addr.?.*.family == std.posix.AF.LINK + else + unreachable; + } + + pub fn isLoopback(iface: *C.ifaddrs) bool { + return iface.ifa_flags & C.IFF_LOOPBACK == C.IFF_LOOPBACK; } }; - pub fn cpus(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return switch (Environment.os) { - .linux => cpusImplLinux(globalThis), - .mac => cpusImplDarwin(globalThis), - .windows => cpusImplWindows(globalThis), - else => @compileError("unsupported OS"), - } catch { - const err = JSC.SystemError{ - .message = bun.String.static("Failed to get cpu information"), - .code = bun.String.static(@tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR)), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - }; + // The list currently contains entries for link-layer interfaces + // and the IPv4, IPv6 interfaces. We only want to return the latter two + // but need the link-layer entries to determine MAC address. + // So, on our first pass through the linked list we'll count the number of + // INET interfaces only. + var num_inet_interfaces: usize = 0; + var it = interface_start; + while (it) |iface| : (it = iface.ifa_next) { + if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; + num_inet_interfaces += 1; } - fn cpusImplLinux(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { - // Create the return array - const values = JSC.JSValue.createEmptyArray(globalThis, 0); - var num_cpus: u32 = 0; + var ret = JSC.JSValue.createEmptyObject(globalThis, 8); - var stack_fallback = std.heap.stackFallback(1024 * 8, bun.default_allocator); - var file_buf = std.ArrayList(u8).init(stack_fallback.get()); - defer file_buf.deinit(); + // Second pass through, populate each interface object + it = interface_start; + while (it) |iface| : (it = iface.ifa_next) { + if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; - // Read /proc/stat to get number of CPUs and times - if (std.fs.openFileAbsolute("/proc/stat", .{})) |file| { - defer file.close(); + const interface_name = std.mem.sliceTo(iface.ifa_name, 0); + const addr = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_addr)))); + const netmask = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_netmask)))); - const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf).unwrap(); - defer file_buf.clearRetainingCapacity(); - const contents = file_buf.items[0..read]; + var interface = JSC.JSValue.createEmptyObject(globalThis, 7); - var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); - - // Skip the first line (aggregate of all CPUs) - _ = line_iter.next(); - - // Read each CPU line - while (line_iter.next()) |line| { - // CPU lines are formatted as `cpu0 user nice sys idle iowait irq softirq` - var toks = std.mem.tokenize(u8, line, " \t"); - const cpu_name = toks.next(); - if (cpu_name == null or !std.mem.startsWith(u8, cpu_name.?, "cpu")) break; // done with CPUs - - //NOTE: libuv assumes this is fixed on Linux, not sure that's actually the case - const scale = 10; - - var times = CPUTimes{}; - times.user = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - times.nice = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - times.sys = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - times.idle = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - _ = try (toks.next() orelse error.eol); // skip iowait - times.irq = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10); - - // Actually create the JS object representing the CPU - const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); - cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); - values.putIndex(globalThis, num_cpus, cpu); - - num_cpus += 1; - } - } else |_| { - return error.no_proc_stat; - } - - // Read /proc/cpuinfo to get model information (optional) - if (std.fs.openFileAbsolute("/proc/cpuinfo", .{})) |file| { - defer file.close(); - - const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf).unwrap(); - defer file_buf.clearRetainingCapacity(); - const contents = file_buf.items[0..read]; - - var line_iter = std.mem.tokenizeScalar(u8, contents, '\n'); - - const key_processor = "processor\t: "; - const key_model_name = "model name\t: "; - - var cpu_index: u32 = 0; - var has_model_name = true; - while (line_iter.next()) |line| { - if (strings.hasPrefixComptime(line, key_processor)) { - if (!has_model_name) { - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); - } - // If this line starts a new processor, parse the index from the line - const digits = std.mem.trim(u8, line[key_processor.len..], " \t\n"); - cpu_index = try std.fmt.parseInt(u32, digits, 10); - if (cpu_index >= num_cpus) return error.too_may_cpus; - has_model_name = false; - } else if (strings.hasPrefixComptime(line, key_model_name)) { - // If this is the model name, extract it and store on the current cpu - const model_name = line[key_model_name.len..]; - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(model_name).withEncoding().toJS(globalThis)); - has_model_name = true; - } - } - if (!has_model_name) { - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); - } - } else |_| { - // Initialize model name to "unknown" - var it = values.arrayIterator(globalThis); - while (it.next()) |cpu| { - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); - } - } - - // Read /sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq to get current frequency (optional) - for (0..num_cpus) |cpu_index| { - const cpu = JSC.JSObject.getIndex(values, globalThis, @truncate(cpu_index)); - - var path_buf: [128]u8 = undefined; - const path = try std.fmt.bufPrint(&path_buf, "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq", .{cpu_index}); - if (std.fs.openFileAbsolute(path, .{})) |file| { - defer file.close(); - - const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf).unwrap(); - defer file_buf.clearRetainingCapacity(); - const contents = file_buf.items[0..read]; - - const digits = std.mem.trim(u8, contents, " \n"); - const speed = (std.fmt.parseInt(u64, digits, 10) catch 0) / 1000; - - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed)); - } else |_| { - // Initialize CPU speed to 0 - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(0)); - } - } - - return values; - } - - extern fn bun_sysconf__SC_CLK_TCK() isize; - fn cpusImplDarwin(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { - const local_bindings = @import("../../darwin_c.zig"); - const c = std.c; - - // Fetch the CPU info structure - var num_cpus: c.natural_t = 0; - var info: [*]local_bindings.processor_cpu_load_info = undefined; - var info_size: std.c.mach_msg_type_number_t = 0; - if (local_bindings.host_processor_info(std.c.mach_host_self(), local_bindings.PROCESSOR_CPU_LOAD_INFO, &num_cpus, @as(*local_bindings.processor_info_array_t, @ptrCast(&info)), &info_size) != .SUCCESS) { - return error.no_processor_info; - } - defer _ = std.c.vm_deallocate(std.c.mach_task_self(), @intFromPtr(info), info_size); - - // Ensure we got the amount of data we expected to guard against buffer overruns - if (info_size != C.PROCESSOR_CPU_LOAD_INFO_COUNT * num_cpus) { - return error.broken_process_info; - } - - // Get CPU model name - var model_name_buf: [512]u8 = undefined; - var len: usize = model_name_buf.len; - // Try brand_string first and if it fails try hw.model - if (!(std.c.sysctlbyname("machdep.cpu.brand_string", &model_name_buf, &len, null, 0) == 0 or - std.c.sysctlbyname("hw.model", &model_name_buf, &len, null, 0) == 0)) + // address The assigned IPv4 or IPv6 address + // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. { - return error.no_processor_info; - } - //NOTE: sysctlbyname doesn't update len if it was large enough, so we - // still have to find the null terminator. All cpus can share the same - // model name. - const model_name = JSC.ZigString.init(std.mem.sliceTo(&model_name_buf, 0)).withEncoding().toJS(globalThis); - - // Get CPU speed - var speed: u64 = 0; - len = @sizeOf(@TypeOf(speed)); - _ = std.c.sysctlbyname("hw.cpufrequency", &speed, &len, null, 0); - if (speed == 0) { - // Suggested by Node implementation: - // If sysctl hw.cputype == CPU_TYPE_ARM64, the correct value is unavailable - // from Apple, but we can hard-code it here to a plausible value. - speed = 2_400_000_000; - } - - // Get the multiplier; this is the number of ms/tick - const ticks: i64 = bun_sysconf__SC_CLK_TCK(); - const multiplier = 1000 / @as(u64, @intCast(ticks)); - - // Set up each CPU value in the return - const values = JSC.JSValue.createEmptyArray(globalThis, @as(u32, @intCast(num_cpus))); - var cpu_index: u32 = 0; - while (cpu_index < num_cpus) : (cpu_index += 1) { - const times = CPUTimes{ - .user = info[cpu_index].cpu_ticks[0] * multiplier, - .nice = info[cpu_index].cpu_ticks[3] * multiplier, - .sys = info[cpu_index].cpu_ticks[1] * multiplier, - .idle = info[cpu_index].cpu_ticks[2] * multiplier, - .irq = 0, // not available + // Compute the CIDR suffix; returns null if the netmask cannot + // be converted to a CIDR suffix + const maybe_suffix: ?u8 = switch (addr.any.family) { + std.posix.AF.INET => netmaskToCIDRSuffix(netmask.in.sa.addr), + std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(netmask.in6.sa.addr))), + else => null, }; - const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed / 1_000_000)); - cpu.put(globalThis, JSC.ZigString.static("model"), model_name); - cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); - - values.putIndex(globalThis, cpu_index, cpu); - } - return values; - } - - pub fn cpusImplWindows(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { - var cpu_infos: [*]libuv.uv_cpu_info_t = undefined; - var count: c_int = undefined; - const err = libuv.uv_cpu_info(&cpu_infos, &count); - if (err != 0) { - return error.no_processor_info; - } - defer libuv.uv_free_cpu_info(cpu_infos, count); - - const values = JSC.JSValue.createEmptyArray(globalThis, 0); - - for (cpu_infos[0..@intCast(count)], 0..@intCast(count)) |cpu_info, i| { - const times = CPUTimes{ - .user = cpu_info.cpu_times.user, - .nice = cpu_info.cpu_times.nice, - .sys = cpu_info.cpu_times.sys, - .idle = cpu_info.cpu_times.idle, - .irq = cpu_info.cpu_times.irq, - }; - - const cpu = JSC.JSValue.createEmptyObject(globalThis, 3); - cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(bun.span(cpu_info.model)).withEncoding().toJS(globalThis)); - cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(cpu_info.speed)); - cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis)); - - values.putIndex(globalThis, @intCast(i), cpu); - } - - return values; - } - - pub fn endianness(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.ZigString.init("LE").withEncoding().toJS(globalThis); - } - - pub fn freemem(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.JSValue.jsNumberFromUint64(C.getFreeMemory()); - } - - pub fn getPriority(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - var args_ = callframe.arguments_old(1); - const arguments: []const JSC.JSValue = args_.ptr[0..args_.len]; - - if (arguments.len > 0 and !arguments[0].isNumber()) { - return globalThis.ERR_INVALID_ARG_TYPE("getPriority() expects a number", .{}).throw(); - } - - const pid = if (arguments.len > 0) arguments[0].asInt32() else 0; - - const priority = C.getProcessPriority(pid); - if (priority == -1) { - //const info = JSC.JSValue.createEmptyObject(globalThis, 4); - //info.put(globalThis, JSC.ZigString.static("errno"), JSC.JSValue.jsNumberFromInt32(-3)); - //info.put(globalThis, JSC.ZigString.static("code"), JSC.ZigString.init("ESRCH").withEncoding().toValueGC(globalThis)); - //info.put(globalThis, JSC.ZigString.static("message"), JSC.ZigString.init("no such process").withEncoding().toValueGC(globalThis)); - //info.put(globalThis, JSC.ZigString.static("syscall"), JSC.ZigString.init("uv_os_getpriority").withEncoding().toValueGC(globalThis)); - - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - //.info = info, - .errno = -3, - .syscall = bun.String.static("uv_os_getpriority"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - } - - return JSC.JSValue.jsNumberFromInt32(priority); - } - - pub fn homedir(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - const dir: []const u8 = brk: { - if (comptime Environment.isWindows) { - if (bun.getenvZ("USERPROFILE")) |env| - break :brk bun.asByteSlice(env); - } else { - if (bun.getenvZ("HOME")) |env| - break :brk bun.asByteSlice(env); + // Format the address and then, if valid, the CIDR suffix; both + // the address and cidr values can be slices into this same buffer + // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" + var buf: [64]u8 = undefined; + const addr_str = bun.fmt.formatIp(addr, &buf) catch unreachable; + var cidr = JSC.JSValue.null; + if (maybe_suffix) |suffix| { + //NOTE addr_str might not start at buf[0] due to slicing in formatIp + const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&buf[0]); + // Start writing the suffix immediately after the address + const suffix_str = std.fmt.bufPrint(buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; + // The full cidr value is the address + the suffix + const cidr_str = buf[start .. start + addr_str.len + suffix_str.len]; + cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); } - break :brk "unknown"; - }; - - return JSC.ZigString.init(dir).withEncoding().toJS(globalThis); - } - - pub fn hostname(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - if (comptime Environment.isWindows) { - var name_buffer: [129:0]u16 = undefined; - if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { - const str = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); - defer str.deref(); - return str.toJS(globalThis); - } - - var result: std.os.windows.ws2_32.WSADATA = undefined; - if (std.os.windows.ws2_32.WSAStartup(0x202, &result) == 0) { - if (bun.windows.GetHostNameW(&name_buffer, name_buffer.len) == 0) { - const str = bun.String.createUTF16(bun.sliceTo(&name_buffer, 0)); - defer str.deref(); - return str.toJS(globalThis); - } - } - - return JSC.ZigString.init("unknown").withEncoding().toJS(globalThis); + interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); + interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); } - var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; - - return JSC.ZigString.init(std.posix.gethostname(&name_buffer) catch "unknown").withEncoding().toJS(globalThis); - } - - pub fn loadavg(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - const result = C.getSystemLoadavg(); - return JSC.JSArray.create(globalThis, &.{ - JSC.JSValue.jsNumber(result[0]), - JSC.JSValue.jsNumber(result[1]), - JSC.JSValue.jsNumber(result[2]), - }); - } - - pub fn networkInterfaces(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - return switch (Environment.os) { - .windows => networkInterfacesWindows(globalThis), - else => networkInterfacesPosix(globalThis), - }; - } - - fn networkInterfacesPosix(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { - // getifaddrs sets a pointer to a linked list - var interface_start: ?*C.ifaddrs = null; - const rc = C.getifaddrs(&interface_start); - if (rc != 0) { - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: getifaddrs returned an error"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - .errno = @intFromEnum(std.posix.errno(rc)), - .syscall = bun.String.static("getifaddrs"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - } - defer C.freeifaddrs(interface_start); - - const helpers = struct { - // We'll skip interfaces that aren't actually available - pub fn skip(iface: *C.ifaddrs) bool { - // Skip interfaces that aren't actually available - if (iface.ifa_flags & C.IFF_RUNNING == 0) return true; - if (iface.ifa_flags & C.IFF_UP == 0) return true; - if (iface.ifa_addr == null) return true; - - return false; - } - - // We won't actually return link-layer interfaces but we need them for - // extracting the MAC address - pub fn isLinkLayer(iface: *C.ifaddrs) bool { - if (iface.ifa_addr == null) return false; - return if (comptime Environment.isLinux) - return iface.ifa_addr.*.sa_family == std.posix.AF.PACKET - else if (comptime Environment.isMac) - return iface.ifa_addr.?.*.family == std.posix.AF.LINK - else - unreachable; - } - - pub fn isLoopback(iface: *C.ifaddrs) bool { - return iface.ifa_flags & C.IFF_LOOPBACK == C.IFF_LOOPBACK; - } - }; - - // The list currently contains entries for link-layer interfaces - // and the IPv4, IPv6 interfaces. We only want to return the latter two - // but need the link-layer entries to determine MAC address. - // So, on our first pass through the linked list we'll count the number of - // INET interfaces only. - var num_inet_interfaces: usize = 0; - var it = interface_start; - while (it) |iface| : (it = iface.ifa_next) { - if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; - num_inet_interfaces += 1; + // netmask The IPv4 or IPv6 network mask + { + var buf: [64]u8 = undefined; + const str = bun.fmt.formatIp(netmask, &buf) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); } - var ret = JSC.JSValue.createEmptyObject(globalThis, 8); + // family Either IPv4 or IPv6 + interface.put(globalThis, JSC.ZigString.static("family"), (switch (addr.any.family) { + std.posix.AF.INET => JSC.ZigString.static("IPv4"), + std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), + else => JSC.ZigString.static("unknown"), + }).toJS(globalThis)); - // Second pass through, populate each interface object - it = interface_start; - while (it) |iface| : (it = iface.ifa_next) { - if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; + // mac The MAC address of the network interface + { + // We need to search for the link-layer interface whose name matches this one + var ll_it = interface_start; + const maybe_ll_addr = while (ll_it) |ll_iface| : (ll_it = ll_iface.ifa_next) { + if (helpers.skip(ll_iface) or !helpers.isLinkLayer(ll_iface)) continue; - const interface_name = std.mem.sliceTo(iface.ifa_name, 0); - const addr = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_addr)))); - const netmask = std.net.Address.initPosix(@alignCast(@as(*std.posix.sockaddr, @ptrCast(iface.ifa_netmask)))); + const ll_name = bun.sliceTo(ll_iface.ifa_name, 0); + if (!strings.hasPrefix(ll_name, interface_name)) continue; + if (ll_name.len > interface_name.len and ll_name[interface_name.len] != ':') continue; - var interface = JSC.JSValue.createEmptyObject(globalThis, 7); - - // address The assigned IPv4 or IPv6 address - // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. - { - // Compute the CIDR suffix; returns null if the netmask cannot - // be converted to a CIDR suffix - const maybe_suffix: ?u8 = switch (addr.any.family) { - std.posix.AF.INET => netmaskToCIDRSuffix(netmask.in.sa.addr), - std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(netmask.in6.sa.addr))), - else => null, - }; - - // Format the address and then, if valid, the CIDR suffix; both - // the address and cidr values can be slices into this same buffer - // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" - var buf: [64]u8 = undefined; - const addr_str = bun.fmt.formatIp(addr, &buf) catch unreachable; - var cidr = JSC.JSValue.null; - if (maybe_suffix) |suffix| { - //NOTE addr_str might not start at buf[0] due to slicing in formatIp - const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&buf[0]); - // Start writing the suffix immediately after the address - const suffix_str = std.fmt.bufPrint(buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; - // The full cidr value is the address + the suffix - const cidr_str = buf[start .. start + addr_str.len + suffix_str.len]; - cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); - } - - interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); - interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); - } - - // netmask The IPv4 or IPv6 network mask - { - var buf: [64]u8 = undefined; - const str = bun.fmt.formatIp(netmask, &buf) catch unreachable; - interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); - } - - // family Either IPv4 or IPv6 - interface.put(globalThis, JSC.ZigString.static("family"), (switch (addr.any.family) { - std.posix.AF.INET => JSC.ZigString.static("IPv4"), - std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), - else => JSC.ZigString.static("unknown"), - }).toJS(globalThis)); - - // mac The MAC address of the network interface - { - // We need to search for the link-layer interface whose name matches this one - var ll_it = interface_start; - const maybe_ll_addr = while (ll_it) |ll_iface| : (ll_it = ll_iface.ifa_next) { - if (helpers.skip(ll_iface) or !helpers.isLinkLayer(ll_iface)) continue; - - const ll_name = bun.sliceTo(ll_iface.ifa_name, 0); - if (!strings.hasPrefix(ll_name, interface_name)) continue; - if (ll_name.len > interface_name.len and ll_name[interface_name.len] != ':') continue; - - // This is the correct link-layer interface entry for the current interface, - // cast to a link-layer socket address - if (comptime Environment.isLinux) { - break @as(?*std.posix.sockaddr.ll, @ptrCast(@alignCast(ll_iface.ifa_addr))); - } else if (comptime Environment.isMac) { - break @as(?*C.sockaddr_dl, @ptrCast(@alignCast(ll_iface.ifa_addr))); - } else { - @compileError("unreachable"); - } - } else null; - - if (maybe_ll_addr) |ll_addr| { - // Encode its link-layer address. We need 2*6 bytes for the - // hex characters and 5 for the colon separators - var mac_buf: [17]u8 = undefined; - const addr_data = if (comptime Environment.isLinux) ll_addr.addr else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..] else @compileError("unreachable"); - if (addr_data.len < 6) { - const mac = "00:00:00:00:00:00"; - interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); - } else { - const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ - addr_data[0], addr_data[1], addr_data[2], - addr_data[3], addr_data[4], addr_data[5], - }) catch unreachable; - interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); - } + // This is the correct link-layer interface entry for the current interface, + // cast to a link-layer socket address + if (comptime Environment.isLinux) { + break @as(?*std.posix.sockaddr.ll, @ptrCast(@alignCast(ll_iface.ifa_addr))); + } else if (comptime Environment.isMac) { + break @as(?*C.sockaddr_dl, @ptrCast(@alignCast(ll_iface.ifa_addr))); } else { + @compileError("unreachable"); + } + } else null; + + if (maybe_ll_addr) |ll_addr| { + // Encode its link-layer address. We need 2*6 bytes for the + // hex characters and 5 for the colon separators + var mac_buf: [17]u8 = undefined; + const addr_data = if (comptime Environment.isLinux) ll_addr.addr else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..] else @compileError("unreachable"); + if (addr_data.len < 6) { const mac = "00:00:00:00:00:00"; interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); + } else { + const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ + addr_data[0], addr_data[1], addr_data[2], + addr_data[3], addr_data[4], addr_data[5], + }) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); } - } - - // internal true if the network interface is a loopback or similar interface that is not remotely accessible; otherwise false - interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(helpers.isLoopback(iface))); - - // scopeid The numeric IPv6 scope ID (only specified when family is IPv6) - if (addr.any.family == std.posix.AF.INET6) { - interface.put(globalThis, JSC.ZigString.static("scope_id"), JSC.JSValue.jsNumber(addr.in6.sa.scope_id)); - } - - // Does this entry already exist? - if (ret.get_unsafe(globalThis, interface_name)) |array| { - // Add this interface entry to the existing array - const next_index = @as(u32, @intCast(array.getLength(globalThis))); - array.putIndex(globalThis, next_index, interface); } else { - // Add it as an array with this interface as an element - const member_name = JSC.ZigString.init(interface_name); - var array = JSC.JSValue.createEmptyArray(globalThis, 1); - array.putIndex(globalThis, 0, interface); - ret.put(globalThis, &member_name, array); - } - } - - return ret; - } - - fn networkInterfacesWindows(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { - var ifaces: [*]libuv.uv_interface_address_t = undefined; - var count: c_int = undefined; - const err = libuv.uv_interface_addresses(&ifaces, &count); - if (err != 0) { - const sys_err = JSC.SystemError{ - .message = bun.String.static("uv_interface_addresses failed"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - //.info = info, - .errno = err, - .syscall = bun.String.static("uv_interface_addresses"), - }; - return globalThis.throwValue(sys_err.toErrorInstance(globalThis)); - } - defer libuv.uv_free_interface_addresses(ifaces, count); - - var ret = JSC.JSValue.createEmptyObject(globalThis, 8); - - // 65 comes from: https://stackoverflow.com/questions/39443413/why-is-inet6-addrstrlen-defined-as-46-in-c - var ip_buf: [65]u8 = undefined; - var mac_buf: [17]u8 = undefined; - - for (ifaces[0..@intCast(count)]) |iface| { - var interface = JSC.JSValue.createEmptyObject(globalThis, 7); - - // address The assigned IPv4 or IPv6 address - // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. - var cidr = JSC.JSValue.null; - { - // Compute the CIDR suffix; returns null if the netmask cannot - // be converted to a CIDR suffix - const maybe_suffix: ?u8 = switch (iface.address.address4.family) { - std.posix.AF.INET => netmaskToCIDRSuffix(iface.netmask.netmask4.addr), - std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(iface.netmask.netmask6.addr))), - else => null, - }; - - // Format the address and then, if valid, the CIDR suffix; both - // the address and cidr values can be slices into this same buffer - // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" - const addr_str = bun.fmt.formatIp( - // std.net.Address will do ptrCast depending on the family so this is ok - std.net.Address.initPosix(@ptrCast(&iface.address.address4)), - &ip_buf, - ) catch unreachable; - if (maybe_suffix) |suffix| { - //NOTE addr_str might not start at buf[0] due to slicing in formatIp - const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&ip_buf[0]); - // Start writing the suffix immediately after the address - const suffix_str = std.fmt.bufPrint(ip_buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; - // The full cidr value is the address + the suffix - const cidr_str = ip_buf[start .. start + addr_str.len + suffix_str.len]; - cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); - } - - interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); - } - - // netmask - { - const str = bun.fmt.formatIp( - // std.net.Address will do ptrCast depending on the family so this is ok - std.net.Address.initPosix(@ptrCast(&iface.netmask.netmask4)), - &ip_buf, - ) catch unreachable; - interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); - } - // family - interface.put(globalThis, JSC.ZigString.static("family"), (switch (iface.address.address4.family) { - std.posix.AF.INET => JSC.ZigString.static("IPv4"), - std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), - else => JSC.ZigString.static("unknown"), - }).toJS(globalThis)); - - // mac - { - const phys = iface.phys_addr; - const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ - phys[0], phys[1], phys[2], phys[3], phys[4], phys[5], - }) catch unreachable; + const mac = "00:00:00:00:00:00"; interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); } - - // internal - { - interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(iface.is_internal != 0)); - } - - // cidr. this is here to keep ordering consistent with the node implementation - interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); - - // scopeid - if (iface.address.address4.family == std.posix.AF.INET6) { - interface.put(globalThis, JSC.ZigString.static("scopeid"), JSC.JSValue.jsNumber(iface.address.address6.scope_id)); - } - - // Does this entry already exist? - const interface_name = bun.span(iface.name); - if (ret.get_unsafe(globalThis, interface_name)) |array| { - // Add this interface entry to the existing array - const next_index = @as(u32, @intCast(array.getLength(globalThis))); - array.putIndex(globalThis, next_index, interface); - } else { - // Add it as an array with this interface as an element - const member_name = JSC.ZigString.init(interface_name); - var array = JSC.JSValue.createEmptyArray(globalThis, 1); - array.putIndex(globalThis, 0, interface); - ret.put(globalThis, &member_name, array); - } } - return ret; - } + // internal true if the network interface is a loopback or similar interface that is not remotely accessible; otherwise false + interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(helpers.isLoopback(iface))); - pub fn platform(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.ZigString.init(Global.os_name).withEncoding().toJS(globalThis); - } - - pub fn release(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; - return JSC.ZigString.init(C.getRelease(&name_buffer)).withEncoding().toJS(globalThis); - } - - pub fn setPriority(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - var args_ = callframe.arguments_old(2); - var arguments: []const JSC.JSValue = args_.ptr[0..args_.len]; - - if (arguments.len == 0) { - const err = JSC.toTypeError( - .ERR_INVALID_ARG_TYPE, - "The \"priority\" argument must be of type number. Received undefined", - .{}, - globalThis, - ); - return globalThis.throwValue(err); + // scopeid The numeric IPv6 scope ID (only specified when family is IPv6) + if (addr.any.family == std.posix.AF.INET6) { + interface.put(globalThis, JSC.ZigString.static("scope_id"), JSC.JSValue.jsNumber(addr.in6.sa.scope_id)); } - const pid = if (arguments.len == 2) arguments[0].coerce(i32, globalThis) else 0; - const priority = if (arguments.len == 2) arguments[1].coerce(i32, globalThis) else arguments[0].coerce(i32, globalThis); - - if (priority < -20 or priority > 19) { - const err = JSC.toTypeError( - .ERR_OUT_OF_RANGE, - "The value of \"priority\" is out of range. It must be >= -20 && <= 19", - .{}, - globalThis, - ); - return globalThis.throwValue(err); - } - - const errcode = C.setProcessPriority(pid, priority); - switch (errcode) { - .SRCH => { - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: uv_os_setpriority returned ESRCH (no such process)"), - .code = bun.String.static(@tagName(.ERR_SYSTEM_ERROR)), - //.info = info, - .errno = -3, - .syscall = bun.String.static("uv_os_setpriority"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - }, - .ACCES => { - const err = JSC.SystemError{ - .message = bun.String.static("A system error occurred: uv_os_setpriority returned EACCESS (permission denied)"), - .code = bun.String.static(@tagName(.ERR_SYSTEM_ERROR)), - //.info = info, - .errno = -13, - .syscall = bun.String.static("uv_os_setpriority"), - }; - - return globalThis.throwValue(err.toErrorInstance(globalThis)); - }, - else => {}, - } - - return .undefined; - } - - pub fn totalmem(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - return JSC.JSValue.jsNumberFromUint64(C.getTotalMemory()); - } - - pub fn @"type"(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - - if (comptime Environment.isWindows) - return bun.String.static("Windows_NT").toJS(globalThis) - else if (comptime Environment.isMac) - return bun.String.static("Darwin").toJS(globalThis) - else if (comptime Environment.isLinux) - return bun.String.static("Linux").toJS(globalThis); - - return JSC.ZigString.init(Global.os_name).withEncoding().toJS(globalThis); - } - - pub fn uptime(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - if (Environment.isWindows) { - var uptime_value: f64 = undefined; - const err = libuv.uv_uptime(&uptime_value); - if (err != 0) { - const sys_err = JSC.SystemError{ - .message = bun.String.static("failed to get system uptime"), - .code = bun.String.static("ERR_SYSTEM_ERROR"), - .errno = err, - .syscall = bun.String.static("uv_uptime"), - }; - return globalThis.throwValue(sys_err.toErrorInstance(globalThis)); - } - return JSC.JSValue.jsNumber(uptime_value); - } - - return JSC.JSValue.jsNumberFromUint64(C.getSystemUptime()); - } - - pub fn userInfo(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const result = JSC.JSValue.createEmptyObject(globalThis, 5); - - result.put(globalThis, JSC.ZigString.static("homedir"), try homedir(globalThis, callframe)); - - if (comptime Environment.isWindows) { - result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(bun.getenvZ("USERNAME") orelse "unknown").withEncoding().toJS(globalThis)); - result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(-1)); - result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(-1)); - result.put(globalThis, JSC.ZigString.static("shell"), JSC.JSValue.jsNull()); + // Does this entry already exist? + if (ret.get_unsafe(globalThis, interface_name)) |array| { + // Add this interface entry to the existing array + const next_index = @as(u32, @intCast(array.getLength(globalThis))); + array.putIndex(globalThis, next_index, interface); } else { - const username = bun.getenvZ("USER") orelse "unknown"; + // Add it as an array with this interface as an element + const member_name = JSC.ZigString.init(interface_name); + var array = JSC.JSValue.createEmptyArray(globalThis, 1); + array.putIndex(globalThis, 0, interface); + ret.put(globalThis, &member_name, array); + } + } - result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(username).withEncoding().toJS(globalThis)); - result.put(globalThis, JSC.ZigString.static("shell"), JSC.ZigString.init(bun.getenvZ("SHELL") orelse "unknown").withEncoding().toJS(globalThis)); + return ret; +} - result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(C.getuid())); - result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(C.getgid())); +fn networkInterfacesWindows(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + var ifaces: [*]libuv.uv_interface_address_t = undefined; + var count: c_int = undefined; + const err = libuv.uv_interface_addresses(&ifaces, &count); + if (err != 0) { + const sys_err = JSC.SystemError{ + .message = bun.String.static("uv_interface_addresses failed"), + .code = bun.String.static("ERR_SYSTEM_ERROR"), + //.info = info, + .errno = err, + .syscall = bun.String.static("uv_interface_addresses"), + }; + return globalThis.throwValue(sys_err.toErrorInstance(globalThis)); + } + defer libuv.uv_free_interface_addresses(ifaces, count); + + var ret = JSC.JSValue.createEmptyObject(globalThis, 8); + + // 65 comes from: https://stackoverflow.com/questions/39443413/why-is-inet6-addrstrlen-defined-as-46-in-c + var ip_buf: [65]u8 = undefined; + var mac_buf: [17]u8 = undefined; + + for (ifaces[0..@intCast(count)]) |iface| { + var interface = JSC.JSValue.createEmptyObject(globalThis, 7); + + // address The assigned IPv4 or IPv6 address + // cidr The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. + var cidr = JSC.JSValue.null; + { + // Compute the CIDR suffix; returns null if the netmask cannot + // be converted to a CIDR suffix + const maybe_suffix: ?u8 = switch (iface.address.address4.family) { + std.posix.AF.INET => netmaskToCIDRSuffix(iface.netmask.netmask4.addr), + std.posix.AF.INET6 => netmaskToCIDRSuffix(@as(u128, @bitCast(iface.netmask.netmask6.addr))), + else => null, + }; + + // Format the address and then, if valid, the CIDR suffix; both + // the address and cidr values can be slices into this same buffer + // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" + const addr_str = bun.fmt.formatIp( + // std.net.Address will do ptrCast depending on the family so this is ok + std.net.Address.initPosix(@ptrCast(&iface.address.address4)), + &ip_buf, + ) catch unreachable; + if (maybe_suffix) |suffix| { + //NOTE addr_str might not start at buf[0] due to slicing in formatIp + const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&ip_buf[0]); + // Start writing the suffix immediately after the address + const suffix_str = std.fmt.bufPrint(ip_buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; + // The full cidr value is the address + the suffix + const cidr_str = ip_buf[start .. start + addr_str.len + suffix_str.len]; + cidr = JSC.ZigString.init(cidr_str).withEncoding().toJS(globalThis); + } + + interface.put(globalThis, JSC.ZigString.static("address"), JSC.ZigString.init(addr_str).withEncoding().toJS(globalThis)); } - return result; + // netmask + { + const str = bun.fmt.formatIp( + // std.net.Address will do ptrCast depending on the family so this is ok + std.net.Address.initPosix(@ptrCast(&iface.netmask.netmask4)), + &ip_buf, + ) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("netmask"), JSC.ZigString.init(str).withEncoding().toJS(globalThis)); + } + // family + interface.put(globalThis, JSC.ZigString.static("family"), (switch (iface.address.address4.family) { + std.posix.AF.INET => JSC.ZigString.static("IPv4"), + std.posix.AF.INET6 => JSC.ZigString.static("IPv6"), + else => JSC.ZigString.static("unknown"), + }).toJS(globalThis)); + + // mac + { + const phys = iface.phys_addr; + const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ + phys[0], phys[1], phys[2], phys[3], phys[4], phys[5], + }) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("mac"), JSC.ZigString.init(mac).withEncoding().toJS(globalThis)); + } + + // internal + { + interface.put(globalThis, JSC.ZigString.static("internal"), JSC.JSValue.jsBoolean(iface.is_internal != 0)); + } + + // cidr. this is here to keep ordering consistent with the node implementation + interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); + + // scopeid + if (iface.address.address4.family == std.posix.AF.INET6) { + interface.put(globalThis, JSC.ZigString.static("scopeid"), JSC.JSValue.jsNumber(iface.address.address6.scope_id)); + } + + // Does this entry already exist? + const interface_name = bun.span(iface.name); + if (ret.get_unsafe(globalThis, interface_name)) |array| { + // Add this interface entry to the existing array + const next_index = @as(u32, @intCast(array.getLength(globalThis))); + array.putIndex(globalThis, next_index, interface); + } else { + // Add it as an array with this interface as an element + const member_name = JSC.ZigString.init(interface_name); + var array = JSC.JSValue.createEmptyArray(globalThis, 1); + array.putIndex(globalThis, 0, interface); + ret.put(globalThis, &member_name, array); + } } - pub fn version(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; - return JSC.ZigString.init(C.getVersion(&name_buffer)).withEncoding().toJS(globalThis); + return ret; +} + +pub fn release() bun.String { + var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; + return bun.String.createUTF8(C.getRelease(&name_buffer)); +} + +pub fn setPriority1(global: *JSC.JSGlobalObject, pid: i32, priority: i32) !void { + const errcode = C.setProcessPriority(pid, priority); + switch (errcode) { + .SRCH => { + const err = JSC.SystemError{ + .message = bun.String.static("no such process"), + .code = bun.String.static("ESRCH"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.SRCH)), + .windows => libuv.UV_ESRCH, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }, + .ACCES => { + const err = JSC.SystemError{ + .message = bun.String.static("permission denied"), + .code = bun.String.static("EACCES"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.ACCES)), + .windows => libuv.UV_EACCES, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }, + .PERM => { + const err = JSC.SystemError{ + .message = bun.String.static("operation not permitted"), + .code = bun.String.static("EPERM"), + .errno = comptime switch (bun.Environment.os) { + else => -@as(c_int, @intFromEnum(std.posix.E.SRCH)), + .windows => libuv.UV_ESRCH, + }, + .syscall = bun.String.static("uv_os_getpriority"), + }; + return global.throwValue(err.toErrorInstanceWithInfoObject(global)); + }, + else => { + // no other error codes can be emitted + }, + } +} + +pub fn setPriority2(global: *JSC.JSGlobalObject, priority: i32) !void { + return setPriority1(global, 0, priority); +} + +pub fn totalmem() u64 { + return C.getTotalMemory(); +} + +pub fn uptime(global: *JSC.JSGlobalObject) bun.JSError!f64 { + if (Environment.isWindows) { + var uptime_value: f64 = undefined; + const err = libuv.uv_uptime(&uptime_value); + if (err != 0) { + const sys_err = JSC.SystemError{ + .message = bun.String.static("failed to get system uptime"), + .code = bun.String.static("ERR_SYSTEM_ERROR"), + .errno = err, + .syscall = bun.String.static("uv_uptime"), + }; + return global.throwValue(sys_err.toErrorInstance(global)); + } + return uptime_value; } - inline fn getMachineName() [:0]const u8 { - return switch (@import("builtin").target.cpu.arch) { - .arm => "arm", - .aarch64 => "arm64", - .mips => "mips", - .mips64 => "mips64", - .powerpc64 => "ppc64", - .powerpc64le => "ppc64le", - .s390x => "s390x", - .x86 => "i386", - .x86_64 => "x86_64", - else => "unknown", - }; + return @floatFromInt(C.getSystemUptime()); +} + +pub fn userInfo(globalThis: *JSC.JSGlobalObject, options: gen.UserInfoOptions) bun.JSError!JSC.JSValue { + _ = options; // TODO: + + const result = JSC.JSValue.createEmptyObject(globalThis, 5); + + const home = try homedir(globalThis); + defer home.deref(); + + result.put(globalThis, JSC.ZigString.static("homedir"), home.toJS(globalThis)); + + if (comptime Environment.isWindows) { + result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(bun.getenvZ("USERNAME") orelse "unknown").withEncoding().toJS(globalThis)); + result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(-1)); + result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(-1)); + result.put(globalThis, JSC.ZigString.static("shell"), JSC.JSValue.jsNull()); + } else { + const username = bun.getenvZ("USER") orelse "unknown"; + + result.put(globalThis, JSC.ZigString.static("username"), JSC.ZigString.init(username).withEncoding().toJS(globalThis)); + result.put(globalThis, JSC.ZigString.static("shell"), JSC.ZigString.init(bun.getenvZ("SHELL") orelse "unknown").withEncoding().toJS(globalThis)); + result.put(globalThis, JSC.ZigString.static("uid"), JSC.JSValue.jsNumber(C.getuid())); + result.put(globalThis, JSC.ZigString.static("gid"), JSC.JSValue.jsNumber(C.getgid())); } - pub fn machine(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - return JSC.ZigString.static(comptime getMachineName()).toJS(globalThis); - } -}; + return result; +} + +pub fn version() bun.JSError!bun.String { + var name_buffer: [bun.HOST_NAME_MAX]u8 = undefined; + return bun.String.createUTF8(C.getVersion(&name_buffer)); +} /// Given a netmask returns a CIDR suffix. Returns null if the mask is not valid. /// `@TypeOf(mask)` must be one of u32 (IPv4) or u128 (IPv6) diff --git a/src/bun.js/node/node_util_binding.zig b/src/bun.js/node/node_util_binding.zig index 2f886cb0fa..4c982e3f88 100644 --- a/src/bun.js/node/node_util_binding.zig +++ b/src/bun.js/node/node_util_binding.zig @@ -1,5 +1,6 @@ const std = @import("std"); const bun = @import("root").bun; +const Allocator = std.mem.Allocator; const Environment = bun.Environment; const JSC = bun.JSC; const string = bun.string; @@ -105,3 +106,109 @@ pub fn internalErrorName(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFr var fmtstring = bun.String.createFormat("Unknown system error {d}", .{err_int}) catch bun.outOfMemory(); return fmtstring.transferToJS(globalThis); } + +/// `extractedSplitNewLines` for ASCII/Latin1 strings. Panics if passed a non-string. +/// Returns `undefined` if param is utf8 or utf16 and not fully ascii. +/// +/// ```js +/// // util.js +/// const extractedNewLineRe = new RegExp("(?<=\\n)"); +/// extractedSplitNewLines = value => RegExpPrototypeSymbolSplit(extractedNewLineRe, value); +/// ``` +pub fn extractedSplitNewLinesFastPathStringsOnly(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + bun.assert(callframe.argumentsCount() == 1); + const value = callframe.argument(0); + bun.assert(value.isString()); + + const str = try value.toBunString2(globalThis); + defer str.deref(); + + return switch (str.encoding()) { + inline .utf16, .latin1 => |encoding| split(encoding, globalThis, bun.default_allocator, &str), + .utf8 => if (bun.strings.isAllASCII(str.byteSlice())) + return split(.utf8, globalThis, bun.default_allocator, &str) + else + return JSC.JSValue.jsUndefined(), + }; +} + +extern fn Bun__util__isInsideNodeModules(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bool; +/// Walks the call stack from bottom to top, returning `true` when it finds a +/// frame within a `node_modules` directory. +pub fn isInsideNodeModules(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const res = Bun__util__isInsideNodeModules(globalObject, callframe); + return JSC.JSValue.jsBoolean(res); +} + +fn split( + comptime encoding: bun.strings.EncodingNonAscii, + globalThis: *JSC.JSGlobalObject, + allocator: Allocator, + str: *const bun.String, +) bun.JSError!JSC.JSValue { + var fallback = std.heap.stackFallback(1024, allocator); + const alloc = fallback.get(); + const Char = switch (encoding) { + .utf8, .latin1 => u8, + .utf16 => u16, + }; + + var lines: std.ArrayListUnmanaged(bun.String) = .{}; + defer { + for (lines.items) |out| { + out.deref(); + } + lines.deinit(alloc); + } + + const buffer: []const Char = if (encoding == .utf16) + str.utf16() + else + str.byteSlice(); + var it: SplitNewlineIterator(Char) = .{ .buffer = buffer, .index = 0 }; + while (it.next()) |line| { + const encoded_line = switch (encoding) { + inline .utf8 => bun.String.fromUTF8(line), + inline .latin1 => bun.String.createLatin1(line), + inline .utf16 => bun.String.fromUTF16(line), + }; + errdefer encoded_line.deref(); + try lines.append(alloc, encoded_line); + } + + return bun.String.toJSArray(globalThis, lines.items); +} + +pub fn SplitNewlineIterator(comptime T: type) type { + return struct { + buffer: []const T, + index: ?usize, + + const Self = @This(); + + /// Returns a slice of the next field, or null if splitting is complete. + pub fn next(self: *Self) ?[]const T { + const start = self.index orelse return null; + + if (std.mem.indexOfScalarPos(T, self.buffer, start, '\n')) |delim_start| { + const end = delim_start + 1; + const slice = self.buffer[start..end]; + self.index = end; + return slice; + } else { + self.index = null; + return self.buffer[start..]; + } + } + }; +} + +pub fn normalizeEncoding(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const input = callframe.argument(0); + const str = bun.String.fromJS(input, globalThis); + bun.assert(str.tag != .Dead); + defer str.deref(); + if (str.length() == 0) return JSC.Node.Encoding.utf8.toJS(globalThis); + if (str.inMapCaseInsensitive(JSC.Node.Encoding.map)) |enc| return enc.toJS(globalThis); + return JSC.JSValue.jsUndefined(); +} diff --git a/src/bun.js/node/node_zlib_binding.zig b/src/bun.js/node/node_zlib_binding.zig index 8531c209d5..cd233554c7 100644 --- a/src/bun.js/node/node_zlib_binding.zig +++ b/src/bun.js/node/node_zlib_binding.zig @@ -6,6 +6,7 @@ const string = bun.string; const Output = bun.Output; const ZigString = JSC.ZigString; const validators = @import("./util/validators.zig"); +const debug = bun.Output.scoped(.zlib, true); pub fn crc32(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2).ptr; @@ -71,6 +72,8 @@ pub fn CompressionStream(comptime T: type) type { var in: ?[]const u8 = null; var out: ?[]u8 = null; + const this_value = callframe.this(); + bun.assert(!arguments[0].isUndefined()); // must provide flush value flush = arguments[0].toU32(); _ = std.meta.intToEnum(bun.zlib.FlushValue, flush) catch bun.assert(false); // Invalid flush value @@ -102,7 +105,9 @@ pub fn CompressionStream(comptime T: type) type { this.stream.setBuffers(in, out); this.stream.setFlush(@intCast(flush)); - // + // Only create the strong handle when we have a pending write + // And make sure to clear it when we are done. + this.this_value.set(globalThis, this_value); const vm = globalThis.bunVM(); this.task = .{ .callback = &AsyncJob.runTask }; @@ -136,13 +141,23 @@ pub fn CompressionStream(comptime T: type) type { this.write_in_progress = false; - if (!(this.checkError(globalThis) catch return globalThis.reportActiveExceptionAsUnhandled(error.JSError))) { + // Clear the strong handle before we call any callbacks. + const this_value = this.this_value.trySwap() orelse { + debug("this_value is null in runFromJSThread", .{}); + return; + }; + + this_value.ensureStillAlive(); + + if (!(this.checkError(globalThis, this_value) catch return globalThis.reportActiveExceptionAsUnhandled(error.JSError))) { return; } this.stream.updateWriteResult(&this.write_result.?[1], &this.write_result.?[0]); + this_value.ensureStillAlive(); - _ = this.write_callback.get().?.call(globalThis, this.this_value.get().?, &.{}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err); + const write_callback: JSC.JSValue = T.writeCallbackGetCached(this_value).?; + _ = write_callback.call(globalThis, this_value, &.{}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err); if (this.pending_close) _ = this._close(); } @@ -192,11 +207,10 @@ pub fn CompressionStream(comptime T: type) type { this.stream.setBuffers(in, out); this.stream.setFlush(@intCast(flush)); - - // + const this_value = callframe.this(); this.stream.doWork(); - if (try this.checkError(globalThis)) { + if (try this.checkError(globalThis, this_value)) { this.stream.updateWriteResult(&this.write_result.?[1], &this.write_result.?[0]); this.write_in_progress = false; } @@ -206,11 +220,9 @@ pub fn CompressionStream(comptime T: type) type { } pub fn reset(this: *T, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - _ = callframe; - const err = this.stream.reset(); if (err.isError()) { - try this.emitError(globalThis, err); + try this.emitError(globalThis, callframe.this(), err); } return .undefined; } @@ -233,34 +245,34 @@ pub fn CompressionStream(comptime T: type) type { this.stream.close(); } - pub fn setOnError(this: *T, globalThis: *JSC.JSGlobalObject, value: JSC.JSValue) bool { + pub fn setOnError(_: *T, this_value: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) bool { if (value.isFunction()) { - this.onerror_value.set(globalThis, value); + T.errorCallbackSetCached(this_value, globalObject, value); } return true; } - pub fn getOnError(this: *T, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - _ = globalThis; - return this.onerror_value.get() orelse .undefined; + pub fn getOnError(_: *T, this_value: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { + return T.errorCallbackGetCached(this_value) orelse .undefined; } /// returns true if no error was detected/emitted - fn checkError(this: *T, globalThis: *JSC.JSGlobalObject) !bool { + fn checkError(this: *T, globalThis: *JSC.JSGlobalObject, this_value: JSC.JSValue) !bool { const err = this.stream.getErrorInfo(); if (!err.isError()) return true; - try this.emitError(globalThis, err); + try this.emitError(globalThis, this_value, err); return false; } - fn emitError(this: *T, globalThis: *JSC.JSGlobalObject, err_: Error) !void { + fn emitError(this: *T, globalThis: *JSC.JSGlobalObject, this_value: JSC.JSValue, err_: Error) !void { var msg_str = bun.String.createFormat("{s}", .{std.mem.sliceTo(err_.msg, 0) orelse ""}) catch bun.outOfMemory(); const msg_value = msg_str.transferToJS(globalThis); const err_value = JSC.jsNumber(err_.err); var code_str = bun.String.createFormat("{s}", .{std.mem.sliceTo(err_.code, 0) orelse ""}) catch bun.outOfMemory(); const code_value = code_str.transferToJS(globalThis); - _ = try this.onerror_value.get().?.call(globalThis, this.this_value.get().?, &.{ msg_value, err_value, code_value }); + const callback: JSC.JSValue = T.errorCallbackGetCached(this_value) orelse Output.panic("Assertion failure: cachedErrorCallback is null in node:zlib binding", .{}); + _ = try callback.call(globalThis, this_value, &.{ msg_value, err_value, code_value }); this.write_in_progress = false; if (this.pending_close) _ = this._close(); @@ -307,8 +319,6 @@ pub const SNativeZlib = struct { globalThis: *JSC.JSGlobalObject, stream: ZlibContext = .{}, write_result: ?[*]u32 = null, - write_callback: JSC.Strong = .{}, - onerror_value: JSC.Strong = .{}, poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, @@ -341,14 +351,14 @@ pub const SNativeZlib = struct { } //// adding this didnt help much but leaving it here to compare the number with later - // pub fn estimatedSize(this: *const SNativeZlib) usize { - // _ = this; - // const internal_state_size = 3309; // @sizeOf(@cImport(@cInclude("deflate.h")).internal_state) @ cloudflare/zlib @ 92530568d2c128b4432467b76a3b54d93d6350bd - // return @sizeOf(SNativeZlib) + internal_state_size; - // } + pub fn estimatedSize(_: *const SNativeZlib) usize { + const internal_state_size = 3309; // @sizeOf(@cImport(@cInclude("deflate.h")).internal_state) @ cloudflare/zlib @ 92530568d2c128b4432467b76a3b54d93d6350bd + return @sizeOf(SNativeZlib) + internal_state_size; + } pub fn init(this: *SNativeZlib, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.argumentsUndef(7).slice(); + const this_value = callframe.this(); if (arguments.len != 7) { return globalThis.ERR_MISSING_ARGS("init(windowBits, level, memLevel, strategy, writeResult, writeCallback, dictionary)", .{}).throw(); @@ -364,7 +374,12 @@ pub const SNativeZlib = struct { const dictionary = if (arguments[6].isUndefined()) null else arguments[6].asArrayBuffer(globalThis).?.byteSlice(); this.write_result = writeResult; - this.write_callback.set(globalThis, writeCallback); + SNativeZlib.writeCallbackSetCached(this_value, globalThis, writeCallback); + + // Keep the dictionary alive by keeping a reference to it in the JS object. + if (dictionary != null) { + SNativeZlib.dictionarySetCached(this_value, globalThis, arguments[6]); + } this.stream.init(level, windowBits, memLevel, strategy, dictionary); @@ -383,15 +398,15 @@ pub const SNativeZlib = struct { const err = this.stream.setParams(level, strategy); if (err.isError()) { - try this.emitError(globalThis, err); + try this.emitError(globalThis, callframe.this(), err); } return .undefined; } pub fn deinit(this: *@This()) void { - this.write_callback.deinit(); - this.onerror_value.deinit(); + this.this_value.deinit(); this.poll_ref.deinit(); + this.stream.close(); this.destroy(); } }; @@ -497,7 +512,17 @@ const ZlibContext = struct { return .{ .msg = message, .err = @intFromEnum(this.err), - .code = @tagName(this.err), + .code = switch (this.err) { + .Ok => "Z_OK", + .StreamEnd => "Z_STREAM_END", + .NeedDict => "Z_NEED_DICT", + .ErrNo => "Z_ERRNO", + .StreamError => "Z_STREAM_ERROR", + .DataError => "Z_DATA_ERROR", + .MemError => "Z_MEM_ERROR", + .BufError => "Z_BUF_ERROR", + .VersionError => "Z_VERSION_ERROR", + }, }; } @@ -652,7 +677,7 @@ pub const NativeBrotli = JSC.Codegen.JSNativeBrotli.getConstructor; pub const SNativeBrotli = struct { pub usingnamespace bun.NewRefCounted(@This(), deinit); - pub usingnamespace JSC.Codegen.JSNativeZlib; + pub usingnamespace JSC.Codegen.JSNativeBrotli; pub usingnamespace CompressionStream(@This()); ref_count: u32 = 1, @@ -660,8 +685,6 @@ pub const SNativeBrotli = struct { globalThis: *JSC.JSGlobalObject, stream: BrotliContext = .{}, write_result: ?[*]u32 = null, - write_callback: JSC.Strong = .{}, - onerror_value: JSC.Strong = .{}, poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, @@ -706,8 +729,9 @@ pub const SNativeBrotli = struct { }; } - pub fn init(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn init(this: *SNativeBrotli, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.argumentsUndef(3).slice(); + const this_value = callframe.this(); if (arguments.len != 3) { return globalThis.ERR_MISSING_ARGS("init(params, writeResult, writeCallback)", .{}).throw(); } @@ -715,12 +739,14 @@ pub const SNativeBrotli = struct { // this does not get gc'd because it is stored in the JS object's `this._writeState`. and the JS object is tied to the native handle as `_handle[owner_symbol]`. const writeResult = arguments[1].asArrayBuffer(globalThis).?.asU32().ptr; const writeCallback = try validators.validateFunction(globalThis, arguments[2], "writeCallback", .{}); + this.write_result = writeResult; - this.write_callback.set(globalThis, writeCallback); + + SNativeBrotli.writeCallbackSetCached(this_value, globalThis, writeCallback); var err = this.stream.init(); if (err.isError()) { - try this.emitError(globalThis, err); + try this.emitError(globalThis, this_value, err); return JSC.jsBoolean(false); } @@ -749,9 +775,12 @@ pub const SNativeBrotli = struct { } pub fn deinit(this: *@This()) void { - this.write_callback.deinit(); - this.onerror_value.deinit(); + this.this_value.deinit(); this.poll_ref.deinit(); + switch (this.stream.mode) { + .BROTLI_ENCODE, .BROTLI_DECODE => this.stream.close(), + else => {}, + } this.destroy(); } }; diff --git a/src/bun.js/node/path.zig b/src/bun.js/node/path.zig index 6e73e687ca..3545f4418d 100644 --- a/src/bun.js/node/path.zig +++ b/src/bun.js/node/path.zig @@ -52,7 +52,7 @@ fn MaybeBuf(comptime T: type) type { } fn MaybeSlice(comptime T: type) type { - return JSC.Node.Maybe([]const T, Syscall.Error); + return JSC.Node.Maybe([:0]const T, Syscall.Error); } fn validatePathT(comptime T: type, comptime methodName: []const u8) void { @@ -1237,7 +1237,7 @@ pub inline fn joinWindowsJS_T(comptime T: type, globalObject: *JSC.JSGlobalObjec pub fn joinJS_T(comptime T: type, globalObject: *JSC.JSGlobalObject, allocator: std.mem.Allocator, isWindows: bool, paths: []const []const T) JSC.JSValue { // Adding 8 bytes when Windows for the possible UNC root. var bufLen: usize = if (isWindows) 8 else 0; - for (paths) |path| bufLen += if (bufLen > 0 and path.len > 0) path.len + 1 else path.len; + for (paths) |path| bufLen += if (path.len > 0) path.len + 1 else path.len; bufLen = @max(bufLen, PATH_SIZE(T)); const buf = allocator.alloc(T, bufLen) catch bun.outOfMemory(); defer allocator.free(buf); @@ -1274,7 +1274,7 @@ pub fn join(globalObject: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L65C1-L66C77 /// /// Resolves . and .. elements in a path with directory names -fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, separator: T, comptime platform: bun.path.Platform, buf: []T) []const T { +fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, separator: T, comptime platform: bun.path.Platform, buf: []T) [:0]T { const len = path.len; const isSepT = if (platform == .posix) @@ -1285,7 +1285,6 @@ fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, sep var bufOffset: usize = 0; var bufSize: usize = 0; - var res: []const T = &.{}; var lastSegmentLength: usize = 0; // We use an optional value instead of -1, as in Node code, for easier number type use. var lastSlash: ?usize = null; @@ -1320,12 +1319,10 @@ fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, sep if (bufSize > 2) { const lastSlashIndex = std.mem.lastIndexOfScalar(T, buf[0..bufSize], separator); if (lastSlashIndex == null) { - res = &.{}; bufSize = 0; lastSegmentLength = 0; } else { bufSize = lastSlashIndex.?; - res = buf[0..bufSize]; // Translated from the following JS code: // lastSegmentLength = // res.length - 1 - StringPrototypeLastIndexOf(res, separator); @@ -1347,7 +1344,6 @@ fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, sep dots = 0; continue; } else if (bufSize != 0) { - res = &.{}; bufSize = 0; lastSegmentLength = 0; lastSlash = i; @@ -1372,7 +1368,6 @@ fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, sep buf[1] = CHAR_DOT; } - res = buf[0..bufSize]; lastSegmentLength = 2; } } else { @@ -1393,8 +1388,6 @@ fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, sep bufSize += slice.len; bun.memmove(buf[bufOffset..bufSize], slice); - res = buf[0..bufSize]; - // Translated from the following JS code: // lastSegmentLength = i - lastSlash - 1; const subtract = if (lastSlash != null) lastSlash.? + 1 else 2; @@ -1411,7 +1404,8 @@ fn normalizeStringT(comptime T: type, path: []const T, allowAboveRoot: bool, sep } } - return res; + buf[bufSize] = 0; + return buf[0..bufSize :0]; } /// Based on Node v21.6.1 path.posix.normalize @@ -1452,7 +1446,8 @@ pub fn normalizePosixT(comptime T: type, path: []const T, buf: []T) []const T { bufOffset = bufSize; bufSize += 1; buf[bufOffset] = CHAR_FORWARD_SLASH; - normalizedPath = buf[0..bufSize]; + buf[bufSize] = 0; + normalizedPath = buf[0..bufSize :0]; } // Translated from the following JS code: @@ -1465,9 +1460,10 @@ pub fn normalizePosixT(comptime T: type, path: []const T, buf: []T) []const T { bun.copy(T, buf[bufOffset..bufSize], normalizedPath); // Prepend the separator. buf[0] = CHAR_FORWARD_SLASH; - normalizedPath = buf[0..bufSize]; + buf[bufSize] = 0; + normalizedPath = buf[0..bufSize :0]; } - return normalizedPath[0..bufSize]; + return normalizedPath; } /// Based on Node v21.6.1 path.win32.normalize @@ -2060,12 +2056,12 @@ pub fn relativePosixT(comptime T: type, from: []const T, to: []const T, buf: []T if (toOrig[toStart + smallestLength] == CHAR_FORWARD_SLASH) { // We get here if `from` is the exact base path for `to`. // For example: from='/foo/bar'; to='/foo/bar/baz' - return MaybeSlice(T){ .result = toOrig[toStart + smallestLength + 1 .. toOrigLen] }; + return MaybeSlice(T){ .result = toOrig[toStart + smallestLength + 1 .. toOrigLen :0] }; } if (smallestLength == 0) { // We get here if `from` is the root // For example: from='/'; to='/foo' - return MaybeSlice(T){ .result = toOrig[toStart + smallestLength .. toOrigLen] }; + return MaybeSlice(T){ .result = toOrig[toStart + smallestLength .. toOrigLen :0] }; } } else if (fromLen > smallestLength) { if (fromOrig[fromStart + smallestLength] == CHAR_FORWARD_SLASH) { @@ -2131,7 +2127,8 @@ pub fn relativePosixT(comptime T: type, from: []const T, to: []const T, buf: []T if (outLen > 0) { bun.memmove(buf[0..outLen], out); } - return MaybeSlice(T){ .result = buf[0..bufSize] }; + buf[bufSize] = 0; + return MaybeSlice(T){ .result = buf[0..bufSize :0] }; } /// Based on Node v21.6.1 path.win32.relative: @@ -2231,12 +2228,12 @@ pub fn relativeWindowsT(comptime T: type, from: []const T, to: []const T, buf: [ if (toOrig[toStart + smallestLength] == CHAR_BACKWARD_SLASH) { // We get here if `from` is the exact base path for `to`. // For example: from='C:\foo\bar'; to='C:\foo\bar\baz' - return MaybeSlice(T){ .result = toOrig[toStart + smallestLength + 1 .. toOrigLen] }; + return MaybeSlice(T){ .result = toOrig[toStart + smallestLength + 1 .. toOrigLen :0] }; } if (smallestLength == 2) { // We get here if `from` is the device root. // For example: from='C:\'; to='C:\foo' - return MaybeSlice(T){ .result = toOrig[toStart + smallestLength .. toOrigLen] }; + return MaybeSlice(T){ .result = toOrig[toStart + smallestLength .. toOrigLen :0] }; } } if (fromLen > smallestLength) { @@ -2308,13 +2305,14 @@ pub fn relativeWindowsT(comptime T: type, from: []const T, to: []const T, buf: [ bun.copy(T, buf[bufOffset..bufSize], toOrig[toStart..toEnd]); } bun.memmove(buf[0..outLen], out); - return MaybeSlice(T){ .result = buf[0..bufSize] }; + buf[bufSize] = 0; + return MaybeSlice(T){ .result = buf[0..bufSize :0] }; } if (toOrig[toStart] == CHAR_BACKWARD_SLASH) { toStart += 1; } - return MaybeSlice(T){ .result = toOrig[toStart..toEnd] }; + return MaybeSlice(T){ .result = toOrig[toStart..toEnd :0] }; } pub inline fn relativePosixJS_T(comptime T: type, globalObject: *JSC.JSGlobalObject, from: []const T, to: []const T, buf: []T, buf2: []T, buf3: []T) JSC.JSValue { @@ -2377,7 +2375,8 @@ pub fn resolvePosixT(comptime T: type, paths: []const []const T, buf: []T, buf2: // Backed by expandable buf2 because resolvedPath may be long. // We use buf2 here because resolvePosixT is called by other methods and using // buf2 here avoids stepping on others' toes. - var resolvedPath: []const T = &.{}; + var resolvedPath: [:0]const T = undefined; + resolvedPath.len = 0; var resolvedPathLen: usize = 0; var resolvedAbsolute: bool = false; @@ -2420,7 +2419,8 @@ pub fn resolvePosixT(comptime T: type, paths: []const []const T, buf: []T, buf2: buf2[len] = CHAR_FORWARD_SLASH; bufSize += resolvedPathLen; - resolvedPath = buf2[0..bufSize]; + buf2[bufSize] = 0; + resolvedPath = buf2[0..bufSize :0]; resolvedPathLen = bufSize; resolvedAbsolute = path[0] == CHAR_FORWARD_SLASH; } @@ -2447,7 +2447,8 @@ pub fn resolvePosixT(comptime T: type, paths: []const []const T, buf: []T, buf2: // Use bun.copy because resolvedPath and buf overlap. bun.copy(T, buf[1..bufSize], resolvedPath); buf[0] = CHAR_FORWARD_SLASH; - return MaybeSlice(T){ .result = buf[0..bufSize] }; + buf[bufSize] = 0; + return MaybeSlice(T){ .result = buf[0..bufSize :0] }; } // Translated from the following JS code: // return resolvedPath.length > 0 ? resolvedPath : '.'; @@ -2460,7 +2461,7 @@ pub fn resolveWindowsT(comptime T: type, paths: []const []const T, buf: []T, buf comptime validatePathT(T, "resolveWindowsT"); const isSepT = isSepWindowsT; - var tmpBuf: [MAX_PATH_SIZE(T)]T = undefined; + var tmpBuf: [MAX_PATH_SIZE(T):0]T = undefined; // Backed by tmpBuf. var resolvedDevice: []const T = &.{}; @@ -2751,7 +2752,8 @@ pub fn resolveWindowsT(comptime T: type, paths: []const []const T, buf: []T, buf bun.copy(T, buf[bufOffset..bufSize], resolvedTail); buf[resolvedDeviceLen] = CHAR_BACKWARD_SLASH; bun.memmove(buf[0..resolvedDeviceLen], resolvedDevice); - return MaybeSlice(T){ .result = buf[0..bufSize] }; + buf[bufSize] = 0; + return MaybeSlice(T){ .result = buf[0..bufSize :0] }; } // Translated from the following JS code: // : `${resolvedDevice}${resolvedTail}` || '.' @@ -2761,7 +2763,8 @@ pub fn resolveWindowsT(comptime T: type, paths: []const []const T, buf: []T, buf // Use bun.copy because resolvedTail and buf overlap. bun.copy(T, buf[bufOffset..bufSize], resolvedTail); bun.memmove(buf[0..resolvedDeviceLen], resolvedDevice); - return MaybeSlice(T){ .result = buf[0..bufSize] }; + buf[bufSize] = 0; + return MaybeSlice(T){ .result = buf[0..bufSize :0] }; } return MaybeSlice(T){ .result = comptime L(T, CHAR_STR_DOT) }; } @@ -2849,7 +2852,9 @@ pub fn toNamespacedPathWindowsT(comptime T: type, path: []const T, buf: []T, buf const len = resolvedPath.len; if (len <= 2) { - return MaybeSlice(T){ .result = path }; + @memcpy(buf[0..path.len], path); + buf[path.len] = 0; + return MaybeSlice(T){ .result = buf[0..path.len :0] }; } var bufOffset: usize = 0; @@ -2881,7 +2886,8 @@ pub fn toNamespacedPathWindowsT(comptime T: type, path: []const T, buf: []T, buf buf[5] = 'N'; buf[6] = 'C'; buf[7] = CHAR_BACKWARD_SLASH; - return MaybeSlice(T){ .result = buf[0..bufSize] }; + buf[bufSize] = 0; + return MaybeSlice(T){ .result = buf[0..bufSize :0] }; } } } else if (isWindowsDeviceRootT(T, byte0) and @@ -2903,7 +2909,8 @@ pub fn toNamespacedPathWindowsT(comptime T: type, path: []const T, buf: []T, buf buf[1] = CHAR_BACKWARD_SLASH; buf[2] = CHAR_QUESTION_MARK; buf[3] = CHAR_BACKWARD_SLASH; - return MaybeSlice(T){ .result = buf[0..bufSize] }; + buf[bufSize] = 0; + return MaybeSlice(T){ .result = buf[0..bufSize :0] }; } return MaybeSlice(T){ .result = resolvedPath }; } @@ -2950,17 +2957,17 @@ pub fn toNamespacedPath(globalObject: *JSC.JSGlobalObject, isWindows: bool, args pub const Extern = [_][]const u8{"create"}; comptime { - @export(Path.basename, .{ .name = shim.symbolName("basename") }); - @export(Path.dirname, .{ .name = shim.symbolName("dirname") }); - @export(Path.extname, .{ .name = shim.symbolName("extname") }); - @export(path_format, .{ .name = shim.symbolName("format") }); - @export(Path.isAbsolute, .{ .name = shim.symbolName("isAbsolute") }); - @export(Path.join, .{ .name = shim.symbolName("join") }); - @export(Path.normalize, .{ .name = shim.symbolName("normalize") }); - @export(Path.parse, .{ .name = shim.symbolName("parse") }); - @export(Path.relative, .{ .name = shim.symbolName("relative") }); - @export(Path.resolve, .{ .name = shim.symbolName("resolve") }); - @export(Path.toNamespacedPath, .{ .name = shim.symbolName("toNamespacedPath") }); + @export(Path.basename, .{ .name = "Bun__Path__basename" }); + @export(Path.dirname, .{ .name = "Bun__Path__dirname" }); + @export(Path.extname, .{ .name = "Bun__Path__extname" }); + @export(path_format, .{ .name = "Bun__Path__format" }); + @export(Path.isAbsolute, .{ .name = "Bun__Path__isAbsolute" }); + @export(Path.join, .{ .name = "Bun__Path__join" }); + @export(Path.normalize, .{ .name = "Bun__Path__normalize" }); + @export(Path.parse, .{ .name = "Bun__Path__parse" }); + @export(Path.relative, .{ .name = "Bun__Path__relative" }); + @export(Path.resolve, .{ .name = "Bun__Path__resolve" }); + @export(Path.toNamespacedPath, .{ .name = "Bun__Path__toNamespacedPath" }); } fn path_format(globalObject: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(JSC.conv) JSC.JSValue { diff --git a/src/bun.js/node/path_watcher.zig b/src/bun.js/node/path_watcher.zig index 4d1455fdc7..4dd3aae178 100644 --- a/src/bun.js/node/path_watcher.zig +++ b/src/bun.js/node/path_watcher.zig @@ -3,7 +3,7 @@ const std = @import("std"); const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; const Path = @import("../../resolver/resolve_path.zig"); const Fs = @import("../../fs.zig"); -const Mutex = @import("../../lock.zig").Lock; +const Mutex = bun.Mutex; const FSEvents = @import("./fs_events.zig"); const bun = @import("root").bun; @@ -151,7 +151,7 @@ pub const PathWatcherManager = struct { .main_watcher = try Watcher.init( PathWatcherManager, this, - vm.bundler.fs, + vm.transpiler.fs, bun.default_allocator, ), .vm = vm, diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index c664c48954..de2350021f 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -37,24 +37,6 @@ pub const Buffer = JSC.MarkedArrayBuffer; /// On unix it is what the utimens api expects pub const TimeLike = if (Environment.isWindows) f64 else std.posix.timespec; -pub const Flavor = enum { - sync, - promise, - callback, - - pub fn Wrap(comptime this: Flavor, comptime T: type) type { - return comptime brk: { - switch (this) { - .sync => break :brk T, - // .callback => { - // const Callback = CallbackTask(Type); - // }, - else => @compileError("Not implemented yet"), - } - }; - } -}; - /// Node.js expects the error to include contextual information /// - "syscall" /// - "path" @@ -231,14 +213,14 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn getErrno(this: @This()) posix.E { + pub fn getErrno(this: @This()) posix.E { return switch (this) { .result => posix.E.SUCCESS, .err => |e| @enumFromInt(e.errno), }; } - pub inline fn errnoSys(rc: anytype, syscall: Syscall.Tag) ?@This() { + pub fn errnoSys(rc: anytype, syscall: Syscall.Tag) ?@This() { if (comptime Environment.isWindows) { if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { if (rc != 0) return null; @@ -256,7 +238,7 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn errno(err: anytype, syscall: Syscall.Tag) @This() { + pub fn errno(err: anytype, syscall: Syscall.Tag) @This() { return @This(){ // always truncate .err = .{ @@ -266,7 +248,7 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn errnoSysFd(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor) ?@This() { + pub fn errnoSysFd(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor) ?@This() { if (comptime Environment.isWindows) { if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { if (rc != 0) return null; @@ -285,7 +267,7 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn errnoSysP(rc: anytype, syscall: Syscall.Tag, path: anytype) ?@This() { + pub fn errnoSysP(rc: anytype, syscall: Syscall.Tag, path: anytype) ?@This() { if (bun.meta.Item(@TypeOf(path)) == u16) { @compileError("Do not pass WString path to errnoSysP, it needs the path encoded as utf8"); } @@ -306,6 +288,49 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }, }; } + + pub fn errnoSysFP(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor, path: anytype) ?@This() { + if (comptime Environment.isWindows) { + if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { + if (rc != 0) return null; + } + } + return switch (Syscall.getErrno(rc)) { + .SUCCESS => null, + else => |e| @This(){ + // Always truncate + .err = .{ + .errno = translateToErrInt(e), + .syscall = syscall, + .fd = fd, + .path = bun.asByteSlice(path), + }, + }, + }; + } + + pub fn errnoSysPD(rc: anytype, syscall: Syscall.Tag, path: anytype, dest: anytype) ?@This() { + if (bun.meta.Item(@TypeOf(path)) == u16) { + @compileError("Do not pass WString path to errnoSysPD, it needs the path encoded as utf8"); + } + if (comptime Environment.isWindows) { + if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { + if (rc != 0) return null; + } + } + return switch (Syscall.getErrno(rc)) { + .SUCCESS => null, + else => |e| @This(){ + // Always truncate + .err = .{ + .errno = translateToErrInt(e), + .syscall = syscall, + .path = bun.asByteSlice(path), + .dest = bun.asByteSlice(dest), + }, + }, + }; + } }; } @@ -469,12 +494,7 @@ pub const StringOrBuffer = union(enum) { pub fn toJS(this: *StringOrBuffer, ctx: JSC.C.JSContextRef) JSC.JSValue { return switch (this.*) { inline .threadsafe_string, .string => |*str| { - defer { - str.deinit(); - str.* = .{}; - } - - return str.toJS(ctx); + return str.transferToJS(ctx); }, .encoded_slice => { defer { @@ -482,9 +502,7 @@ pub const StringOrBuffer = union(enum) { this.encoded_slice = .{}; } - const str = bun.String.createUTF8(this.encoded_slice.slice()); - defer str.deref(); - return str.toJS(ctx); + return bun.String.createUTF8ForJS(ctx, this.encoded_slice.slice()); }, .buffer => { if (this.buffer.buffer.value != .zero) { @@ -592,18 +610,22 @@ pub const StringOrBuffer = union(enum) { return fromJSMaybeAsync(global, allocator, value, is_async); } - var str = try bun.String.fromJS2(value, global); - defer str.deref(); - if (str.isEmpty()) { - return fromJSMaybeAsync(global, allocator, value, is_async); + if (value.isString()) { + var str = try bun.String.fromJS2(value, global); + defer str.deref(); + if (str.isEmpty()) { + return fromJSMaybeAsync(global, allocator, value, is_async); + } + + const out = str.encode(encoding); + defer global.vm().reportExtraMemory(out.len); + + return .{ + .encoded_slice = JSC.ZigString.Slice.init(bun.default_allocator, out), + }; } - const out = str.encode(encoding); - defer global.vm().reportExtraMemory(out.len); - - return .{ - .encoded_slice = JSC.ZigString.Slice.init(bun.default_allocator, out), - }; + return null; } pub fn fromJSWithEncodingValue(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, encoding_value: JSC.JSValue) bun.JSError!?StringOrBuffer { @@ -632,6 +654,7 @@ pub const ErrorCode = @import("./nodejs_error_code.zig").Code; // and various issues with std.posix that make it too unstable for arbitrary user input (e.g. how .BADF is marked as unreachable) /// https://github.com/nodejs/node/blob/master/lib/buffer.js#L587 +/// must match src/bun.js/bindings/BufferEncodingType.h pub const Encoding = enum(u8) { utf8, ucs2, @@ -740,10 +763,9 @@ pub const Encoding = enum(u8) { .base64 => { var base64_buf: [std.base64.standard.Encoder.calcSize(max_size * 4)]u8 = undefined; const encoded_len = bun.base64.encode(&base64_buf, input); - const encoded, const bytes = bun.String.createUninitialized(.latin1, encoded_len); - defer encoded.deref(); + var encoded, const bytes = bun.String.createUninitialized(.latin1, encoded_len); @memcpy(@constCast(bytes), base64_buf[0..encoded_len]); - return encoded.toJS(globalObject); + return encoded.transferToJS(globalObject); }, .base64url => { var buf: [std.base64.url_safe_no_pad.Encoder.calcSize(max_size * 4)]u8 = undefined; @@ -770,6 +792,11 @@ pub const Encoding = enum(u8) { }, } } + + extern fn WebCore_BufferEncodingType_toJS(globalObject: *JSC.JSGlobalObject, encoding: Encoding) JSC.JSValue; + pub fn toJS(encoding: Encoding, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return WebCore_BufferEncodingType_toJS(globalObject, encoding); + } }; const PathOrBuffer = union(Tag) { @@ -885,11 +912,11 @@ pub const PathLike = union(enum) { return sliceZWithForceCopy(this, buf, false); } - pub inline fn sliceW(this: PathLike, buf: *bun.PathBuffer) [:0]const u16 { - return strings.toWPath(@alignCast(std.mem.bytesAsSlice(u16, buf)), this.slice()); + pub inline fn sliceW(this: PathLike, buf: *bun.WPathBuffer) [:0]const u16 { + return strings.toWPath(buf, this.slice()); } - pub inline fn osPath(this: PathLike, buf: *bun.PathBuffer) bun.OSPathSliceZ { + pub inline fn osPath(this: PathLike, buf: *bun.OSPathBuffer) bun.OSPathSliceZ { if (comptime Environment.isWindows) { return sliceW(this, buf); } @@ -897,25 +924,6 @@ pub const PathLike = union(enum) { return sliceZWithForceCopy(this, buf, false); } - pub fn toJS(this: *const PathLike, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - return switch (this.*) { - .string => this.string.toJS(globalObject, null), - .buffer => this.buffer.toJS(globalObject), - inline .threadsafe_string, .slice_with_underlying_string => |*str| str.toJS(globalObject), - .encoded_slice => |encoded| { - if (this.encoded_slice.allocator.get()) |allocator| { - // Is this a globally-allocated slice? - if (allocator.vtable == bun.default_allocator.vtable) {} - } - - const str = bun.String.createUTF8(encoded.slice()); - defer str.deref(); - return str.toJS(globalObject); - }, - else => unreachable, - }; - } - pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!?PathLike { return fromJSWithAllocator(ctx, arguments, bun.default_allocator); } @@ -923,59 +931,36 @@ pub const PathLike = union(enum) { pub fn fromJSWithAllocator(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, allocator: std.mem.Allocator) bun.JSError!?PathLike { const arg = arguments.next() orelse return null; switch (arg.jsType()) { - JSC.JSValue.JSType.Uint8Array, - JSC.JSValue.JSType.DataView, + .Uint8Array, + .DataView, => { const buffer = Buffer.fromTypedArray(ctx, arg); try Valid.pathBuffer(buffer, ctx); + try Valid.pathNullBytes(buffer.slice(), ctx); arguments.protectEat(); - return PathLike{ .buffer = buffer }; + return .{ .buffer = buffer }; }, - JSC.JSValue.JSType.ArrayBuffer => { + .ArrayBuffer => { const buffer = Buffer.fromArrayBuffer(ctx, arg); try Valid.pathBuffer(buffer, ctx); + try Valid.pathNullBytes(buffer.slice(), ctx); arguments.protectEat(); - - return PathLike{ .buffer = buffer }; + return .{ .buffer = buffer }; }, - JSC.JSValue.JSType.String, - JSC.JSValue.JSType.StringObject, - JSC.JSValue.JSType.DerivedStringObject, + .String, + .StringObject, + .DerivedStringObject, => { var str = arg.toBunString(ctx); defer str.deref(); arguments.eat(); - try Valid.pathStringLength(str.length(), ctx); - - if (arguments.will_be_async) { - var sliced = str.toThreadSafeSlice(allocator); - sliced.reportExtraMemory(ctx.vm()); - - if (sliced.underlying.isEmpty()) { - return PathLike{ .encoded_slice = sliced.utf8 }; - } - - return PathLike{ .threadsafe_string = sliced }; - } else { - var sliced = str.toSlice(allocator); - - // Costs nothing to keep both around. - if (sliced.isWTFAllocated()) { - str.ref(); - return PathLike{ .slice_with_underlying_string = sliced }; - } - - sliced.reportExtraMemory(ctx.vm()); - - // It is expensive to keep both around. - return PathLike{ .encoded_slice = sliced.utf8 }; - } + return try fromBunString(ctx, str, arguments.will_be_async, allocator); }, else => { if (arg.as(JSC.DOMURL)) |domurl| { @@ -986,49 +971,58 @@ pub const PathLike = union(enum) { } arguments.eat(); - try Valid.pathStringLength(str.length(), ctx); - - if (arguments.will_be_async) { - var sliced = str.toThreadSafeSlice(allocator); - sliced.reportExtraMemory(ctx.vm()); - - if (sliced.underlying.isEmpty()) { - return PathLike{ .encoded_slice = sliced.utf8 }; - } - - return PathLike{ .threadsafe_string = sliced }; - } else { - var sliced = str.toSlice(allocator); - - // Costs nothing to keep both around. - if (sliced.isWTFAllocated()) { - str.ref(); - return PathLike{ .slice_with_underlying_string = sliced }; - } - - sliced.reportExtraMemory(ctx.vm()); - - // It is expensive to keep both around. - return PathLike{ .encoded_slice = sliced.utf8 }; - } + return try fromBunString(ctx, str, arguments.will_be_async, allocator); } return null; }, } } + + pub fn fromBunString(global: *JSC.JSGlobalObject, str: bun.String, will_be_async: bool, allocator: std.mem.Allocator) !PathLike { + try Valid.pathStringLength(str.length(), global); + + if (will_be_async) { + var sliced = str.toThreadSafeSlice(allocator); + errdefer sliced.deinit(); + + try Valid.pathNullBytes(sliced.slice(), global); + + sliced.reportExtraMemory(global.vm()); + + if (sliced.underlying.isEmpty()) { + return .{ .encoded_slice = sliced.utf8 }; + } + return .{ .threadsafe_string = sliced }; + } else { + var sliced = str.toSlice(allocator); + errdefer if (!sliced.isWTFAllocated()) sliced.deinit(); + + try Valid.pathNullBytes(sliced.slice(), global); + + // Costs nothing to keep both around. + if (sliced.isWTFAllocated()) { + str.ref(); + return .{ .slice_with_underlying_string = sliced }; + } + + sliced.reportExtraMemory(global.vm()); + + // It is expensive to keep both around. + return .{ .encoded_slice = sliced.utf8 }; + } + } }; pub const Valid = struct { - pub fn fileDescriptor(fd: i64, ctx: JSC.C.JSContextRef) bun.JSError!void { - if (fd < 0) { - return ctx.throwInvalidArguments("Invalid file descriptor, must not be negative number", .{}); - } - + pub fn fileDescriptor(fd: i64, global: JSC.C.JSContextRef) bun.JSError!void { const fd_t = if (Environment.isWindows) bun.windows.libuv.uv_file else bun.FileDescriptorInt; - - if (fd > std.math.maxInt(fd_t)) { - return ctx.throwInvalidArguments("Invalid file descriptor, must not be greater than {d}", .{std.math.maxInt(fd_t)}); + if (fd < 0 or fd > std.math.maxInt(fd_t)) { + return global.throwRangeError(fd, .{ + .min = 0, + .max = std.math.maxInt(fd_t), + .field_name = "fd", + }); } } @@ -1036,26 +1030,24 @@ pub const Valid = struct { switch (zig_str.len) { 0...bun.MAX_PATH_BYTES => return, else => { - // TODO: should this be an EINVAL? var system_error = bun.sys.Error.fromCode(.NAMETOOLONG, .open).withPath(zig_str.slice()).toSystemError(); system_error.syscall = bun.String.dead; return ctx.throwValue(system_error.toErrorInstance(ctx)); }, } - unreachable; + comptime unreachable; } pub fn pathStringLength(len: usize, ctx: JSC.C.JSContextRef) bun.JSError!void { switch (len) { 0...bun.MAX_PATH_BYTES => return, else => { - // TODO: should this be an EINVAL? var system_error = bun.sys.Error.fromCode(.NAMETOOLONG, .open).toSystemError(); system_error.syscall = bun.String.dead; return ctx.throwValue(system_error.toErrorInstance(ctx)); }, } - unreachable; + comptime unreachable; } pub fn pathString(zig_str: JSC.ZigString, ctx: JSC.C.JSContextRef) bun.JSError!void { @@ -1075,7 +1067,13 @@ pub const Valid = struct { }, 1...bun.MAX_PATH_BYTES => return, } - unreachable; + comptime unreachable; + } + + pub fn pathNullBytes(slice: []const u8, global: *JSC.JSGlobalObject) bun.JSError!void { + if (bun.strings.indexOfChar(slice, 0) != null) { + return global.ERR_INVALID_ARG_VALUE("The argument 'path' must be a string, Uint8Array, or URL without null bytes. Received {}", .{bun.fmt.quote(slice)}).throw(); + } } }; @@ -1123,7 +1121,7 @@ pub const ArgumentsSlice = struct { arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator), all: []const JSC.JSValue, threw: bool = false, - protected: std.bit_set.IntegerBitSet(32) = std.bit_set.IntegerBitSet(32).initEmpty(), + protected: bun.bit_set.IntegerBitSet(32) = bun.bit_set.IntegerBitSet(32).initEmpty(), will_be_async: bool = false, pub fn unprotect(this: *ArgumentsSlice) void { @@ -1132,7 +1130,7 @@ pub const ArgumentsSlice = struct { while (iter.next()) |i| { JSC.C.JSValueUnprotect(ctx, this.all[i].asObjectRef()); } - this.protected = std.bit_set.IntegerBitSet(32).initEmpty(); + this.protected = bun.bit_set.IntegerBitSet(32).initEmpty(); } pub fn deinit(this: *ArgumentsSlice) void { @@ -1209,42 +1207,68 @@ pub fn fileDescriptorFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue) bun.JSE null; } +// Equivalent to `toUnixTimestamp` +// // Node.js docs: // > Values can be either numbers representing Unix epoch time in seconds, Dates, or a numeric string like '123456789.0'. // > If the value can not be converted to a number, or is NaN, Infinity, or -Infinity, an Error will be thrown. pub fn timeLikeFromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) ?TimeLike { - if (value.jsType() == .JSDate) { - const milliseconds = value.getUnixTimestamp(); - if (!std.math.isFinite(milliseconds)) { - return null; + // Number is most common case + if (value.isNumber()) { + const seconds = value.asNumber(); + if (std.math.isFinite(seconds)) { + if (seconds < 0) { + return timeLikeFromNow(); + } + return timeLikeFromSeconds(seconds); } - - if (comptime Environment.isWindows) { - return milliseconds / 1000.0; - } - - return TimeLike{ - .tv_sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)), - .tv_nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms), - }; - } - - if (!value.isNumber() and !value.isString()) { return null; + } else switch (value.jsType()) { + .JSDate => { + const milliseconds = value.getUnixTimestamp(); + if (std.math.isFinite(milliseconds)) { + return timeLikeFromMilliseconds(milliseconds); + } + }, + .String => { + const seconds = value.coerceToDouble(globalObject); + if (std.math.isFinite(seconds)) { + return timeLikeFromSeconds(seconds); + } + }, + else => {}, } + return null; +} - const seconds = value.coerce(f64, globalObject); - if (!std.math.isFinite(seconds)) { - return null; - } - - if (comptime Environment.isWindows) { +fn timeLikeFromSeconds(seconds: f64) TimeLike { + if (Environment.isWindows) { return seconds; } - - return TimeLike{ + return .{ .tv_sec = @intFromFloat(seconds), - .tv_nsec = @intFromFloat(@mod(seconds, 1.0) * std.time.ns_per_s), + .tv_nsec = @intFromFloat(@mod(seconds, 1) * std.time.ns_per_s), + }; +} + +fn timeLikeFromMilliseconds(milliseconds: f64) TimeLike { + if (Environment.isWindows) { + return milliseconds / 1000.0; + } + return .{ + .tv_sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)), + .tv_nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms), + }; +} + +fn timeLikeFromNow() TimeLike { + const nanos = std.time.nanoTimestamp(); + if (Environment.isWindows) { + return @as(TimeLike, @floatFromInt(nanos)) / std.time.ns_per_s; + } + return .{ + .tv_sec = @truncate(@divFloor(nanos, std.time.ns_per_s)), + .tv_nsec = @truncate(@mod(nanos, std.time.ns_per_s)), }; } @@ -1259,11 +1283,11 @@ pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue) bun.JSError!?Mode return ctx.throwInvalidArgumentTypeValue("mode", "number", value); } - // An easier method of constructing the mode is to use a sequence of - // three octal digits (e.g. 765). The left-most digit (7 in the example), - // specifies the permissions for the file owner. The middle digit (6 in - // the example), specifies permissions for the group. The right-most - // digit (5 in the example), specifies the permissions for others. + // An easier method of constructing the mode is to use a sequence of + // three octal digits (e.g. 765). The left-most digit (7 in the example), + // specifies the permissions for the file owner. The middle digit (6 in + // the example), specifies permissions for the group. The right-most + // digit (5 in the example), specifies the permissions for others. var zig_str = JSC.ZigString.Empty; value.toZigString(&zig_str, ctx); @@ -1492,13 +1516,41 @@ pub const FileSystemFlags = enum(Mode) { return null; } + + /// Equivalent of GetValidFileMode, which is used to implement fs.access and copyFile + pub fn fromJSNumberOnly(global: *JSC.JSGlobalObject, value: JSC.JSValue, comptime kind: enum { access, copy_file }) bun.JSError!FileSystemFlags { + // Allow only int32 or null/undefined values. + if (!value.isNumber()) { + if (value.isUndefinedOrNull()) { + return @enumFromInt(switch (kind) { + .access => 0, // F_OK + .copy_file => 0, // constexpr int kDefaultCopyMode = 0; + }); + } + return global.ERR_INVALID_ARG_TYPE("mode must be int32 or null/undefined", .{}).throw(); + } + const min, const max = .{ 0, 7 }; + if (value.isInt32()) { + const int: i32 = value.asInt32(); + if (int < min or int > max) { + return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} and <= {d}", .{ min, max }), .{}).throw(); + } + return @enumFromInt(int); + } else { + const float = value.asNumber(); + if (std.math.isNan(float) or std.math.isInf(float) or float < min or float > max) { + return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} and <= {d}", .{ min, max }), .{}).throw(); + } + return @enumFromInt(@as(i32, @intFromFloat(float))); + } + } }; /// Stats and BigIntStats classes from node:fs -pub fn StatType(comptime Big: bool) type { - const Int = if (Big) i64 else i32; - const Float = if (Big) i64 else f64; - const Timestamp = if (Big) u64 else u0; +pub fn StatType(comptime big: bool) type { + const Int = if (big) i64 else i32; + const Float = if (big) i64 else f64; + const Timestamp = if (big) u64 else u0; const Date = packed struct { value: Float, @@ -1510,7 +1562,7 @@ pub fn StatType(comptime Big: bool) type { }; return extern struct { - pub usingnamespace if (Big) JSC.Codegen.JSBigIntStats else JSC.Codegen.JSStats; + pub usingnamespace if (big) JSC.Codegen.JSBigIntStats else JSC.Codegen.JSStats; pub usingnamespace bun.New(@This()); // Stats stores these as i32, but BigIntStats stores all of these as i64 @@ -1551,12 +1603,21 @@ pub fn StatType(comptime Big: bool) type { } fn toTimeMS(ts: StatTimespec) Float { - if (Big) { - const tv_sec: i64 = @intCast(ts.tv_sec); - const tv_nsec: i64 = @intCast(ts.tv_nsec); - return @as(i64, @intCast(tv_sec * std.time.ms_per_s)) + @as(i64, @intCast(@divTrunc(tv_nsec, std.time.ns_per_ms))); + // On windows, Node.js purposefully mis-interprets time values + // > On win32, time is stored in uint64_t and starts from 1601-01-01. + // > libuv calculates tv_sec and tv_nsec from it and converts to signed long, + // > which causes Y2038 overflow. On the other platforms it is safe to treat + // > negative values as pre-epoch time. + const tv_sec = if (Environment.isWindows) @as(u32, @bitCast(ts.tv_sec)) else ts.tv_sec; + const tv_nsec = if (Environment.isWindows) @as(u32, @bitCast(ts.tv_nsec)) else ts.tv_nsec; + if (big) { + const sec: i64 = @intCast(tv_sec); + const nsec: i64 = @intCast(tv_nsec); + return @as(i64, @intCast(sec * std.time.ms_per_s)) + + @as(i64, @intCast(@divTrunc(nsec, std.time.ns_per_ms))); } else { - return (@as(f64, @floatFromInt(@max(ts.tv_sec, 0))) * std.time.ms_per_s) + (@as(f64, @floatFromInt(@as(usize, @intCast(@max(ts.tv_nsec, 0))))) / std.time.ns_per_ms); + return (@as(f64, @floatFromInt(tv_sec)) * std.time.ms_per_s) + + (@as(f64, @floatFromInt(tv_nsec)) / std.time.ns_per_ms); } } @@ -1567,7 +1628,7 @@ pub fn StatType(comptime Big: bool) type { pub fn callback(this: *This, globalObject: *JSC.JSGlobalObject) JSC.JSValue { const value = @field(this, @tagName(field)); const Type = @TypeOf(value); - if (comptime Big and @typeInfo(Type) == .Int) { + if (comptime big and @typeInfo(Type) == .Int) { if (Type == u64) { return JSC.JSValue.fromUInt64NoTruncate(globalObject, value); } @@ -1709,24 +1770,24 @@ pub fn StatType(comptime Big: bool) type { .atime_ms = toTimeMS(aTime), .mtime_ms = toTimeMS(mTime), .ctime_ms = toTimeMS(cTime), - .atime_ns = if (Big) toNanoseconds(aTime) else 0, - .mtime_ns = if (Big) toNanoseconds(mTime) else 0, - .ctime_ns = if (Big) toNanoseconds(cTime) else 0, + .atime_ns = if (big) toNanoseconds(aTime) else 0, + .mtime_ns = if (big) toNanoseconds(mTime) else 0, + .ctime_ns = if (big) toNanoseconds(cTime) else 0, // Linux doesn't include this info in stat // maybe it does in statx, but do you really need birthtime? If you do please file an issue. .birthtime_ms = if (Environment.isLinux) 0 else toTimeMS(stat_.birthtime()), - .birthtime_ns = if (Big and !Environment.isLinux) toNanoseconds(stat_.birthtime()) else 0, + .birthtime_ns = if (big and !Environment.isLinux) toNanoseconds(stat_.birthtime()) else 0, }; } pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!*This { - if (Big) { + if (big) { return globalObject.throwInvalidArguments("BigIntStats is not a constructor", .{}); } // dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs - var args = callFrame.argumentsPtr()[0..@min(callFrame.argumentsCount(), 14)]; + var args = callFrame.arguments(); const atime_ms: f64 = if (args.len > 10 and args[10].isNumber()) args[10].asNumber() else 0; const mtime_ms: f64 = if (args.len > 11 and args[11].isNumber()) args[11].asNumber() else 0; @@ -2081,34 +2142,34 @@ pub const Process = struct { if (to.len == 0) { return globalObject.throwInvalidArguments("Expected path to be a non-empty string", .{}); } + const vm = globalObject.bunVM(); + const fs = vm.transpiler.fs; var buf: bun.PathBuffer = undefined; - const slice = to.sliceZBuf(&buf) catch { - return globalObject.throw("Invalid path", .{}); - }; + const slice = to.sliceZBuf(&buf) catch return globalObject.throw("Invalid path", .{}); - switch (Syscall.chdir(slice)) { + switch (Syscall.chdir(fs.top_level_dir, slice)) { .result => { // When we update the cwd from JS, we have to update the bundler's version as well // However, this might be called many times in a row, so we use a pre-allocated buffer // that way we don't have to worry about garbage collector - const fs = JSC.VirtualMachine.get().bundler.fs; const into_cwd_buf = switch (bun.sys.getcwd(&buf)) { .result => |r| r, .err => |err| { - _ = Syscall.chdir(@as([:0]const u8, @ptrCast(fs.top_level_dir))); + _ = Syscall.chdir(fs.top_level_dir, fs.top_level_dir); return globalObject.throwValue(err.toJSC(globalObject)); }, }; @memcpy(fs.top_level_dir_buf[0..into_cwd_buf.len], into_cwd_buf); - fs.top_level_dir = fs.top_level_dir_buf[0..into_cwd_buf.len]; + fs.top_level_dir_buf[into_cwd_buf.len] = 0; + fs.top_level_dir = fs.top_level_dir_buf[0..into_cwd_buf.len :0]; const len = fs.top_level_dir.len; // Ensure the path ends with a slash if (fs.top_level_dir_buf[len - 1] != std.fs.path.sep) { fs.top_level_dir_buf[len] = std.fs.path.sep; fs.top_level_dir_buf[len + 1] = 0; - fs.top_level_dir = fs.top_level_dir_buf[0 .. len + 1]; + fs.top_level_dir = fs.top_level_dir_buf[0 .. len + 1 :0]; } const withoutTrailingSlash = if (Environment.isWindows) strings.withoutTrailingSlashWindowsPath else strings.withoutTrailingSlash; var str = bun.String.createUTF8(withoutTrailingSlash(fs.top_level_dir)); @@ -2133,6 +2194,46 @@ pub const Process = struct { vm.globalExit(); } + // TODO: switch this to using *bun.wtf.String when it is added + pub fn Bun__Process__editWindowsEnvVar(k: bun.String, v: bun.String) callconv(.C) void { + if (k.tag == .Empty) return; + const wtf1 = k.value.WTFStringImpl; + var fixed_stack_allocator = std.heap.stackFallback(1025, bun.default_allocator); + const allocator = fixed_stack_allocator.get(); + var buf1 = allocator.alloc(u16, k.utf16ByteLength() + 1) catch bun.outOfMemory(); + defer allocator.free(buf1); + var buf2 = allocator.alloc(u16, v.utf16ByteLength() + 1) catch bun.outOfMemory(); + defer allocator.free(buf2); + const len1: usize = switch (wtf1.is8Bit()) { + true => bun.strings.copyLatin1IntoUTF16([]u16, buf1, []const u8, wtf1.latin1Slice()).written, + false => b: { + @memcpy(buf1[0..wtf1.length()], wtf1.utf16Slice()); + break :b wtf1.length(); + }, + }; + buf1[len1] = 0; + const str2: ?[*:0]const u16 = if (v.tag != .Dead) str: { + if (v.tag == .Empty) break :str (&[_]u16{0})[0..0 :0]; + const wtf2 = v.value.WTFStringImpl; + const len2: usize = switch (wtf2.is8Bit()) { + true => bun.strings.copyLatin1IntoUTF16([]u16, buf2, []const u8, wtf2.latin1Slice()).written, + false => b: { + @memcpy(buf2[0..wtf2.length()], wtf2.utf16Slice()); + break :b wtf2.length(); + }, + }; + buf2[len2] = 0; + break :str buf2[0..len2 :0].ptr; + } else null; + _ = bun.windows.SetEnvironmentVariableW(buf1[0..len1 :0].ptr, str2); + } + + comptime { + if (Environment.export_cpp_apis and Environment.isWindows) { + @export(Bun__Process__editWindowsEnvVar, .{ .name = "Bun__Process__editWindowsEnvVar" }); + } + } + pub export const Bun__version: [*:0]const u8 = "v" ++ bun.Global.package_json_version; pub export const Bun__version_with_sha: [*:0]const u8 = "v" ++ bun.Global.package_json_version_with_sha; pub export const Bun__versions_boringssl: [*:0]const u8 = bun.Global.versions.boringssl; @@ -2153,6 +2254,31 @@ pub const Process = struct { pub export const Bun__versions_zstd: [*:0]const u8 = bun.Global.versions.zstd; }; +pub const PathOrBlob = union(enum) { + path: JSC.Node.PathOrFileDescriptor, + blob: Blob, + + const Blob = JSC.WebCore.Blob; + + pub fn fromJSNoCopy(ctx: *JSC.JSGlobalObject, args: *JSC.Node.ArgumentsSlice) bun.JSError!PathOrBlob { + if (try JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, bun.default_allocator)) |path| { + return PathOrBlob{ + .path = path, + }; + } + + const arg = args.nextEat() orelse { + return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", .undefined); + }; + if (arg.as(Blob)) |blob| { + return PathOrBlob{ + .blob = blob.*, + }; + } + return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", arg); + } +}; + comptime { std.testing.refAllDecls(Process); } diff --git a/src/bun.js/node/util/parse_args.zig b/src/bun.js/node/util/parse_args.zig index 221823fcb1..3c1e61e160 100644 --- a/src/bun.js/node/util/parse_args.zig +++ b/src/bun.js/node/util/parse_args.zig @@ -300,13 +300,13 @@ fn storeOption(globalThis: *JSGlobalObject, option_name: ValueRef, option_value: fn parseOptionDefinitions(globalThis: *JSGlobalObject, options_obj: JSValue, option_definitions: *std.ArrayList(OptionDefinition)) bun.JSError!void { try validateObject(globalThis, options_obj, "options", .{}, .{}); - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, options_obj); defer iter.deinit(); - while (iter.next()) |long_option| { + while (try iter.next()) |long_option| { var option = OptionDefinition{ .long_name = String.init(long_option), }; diff --git a/src/bun.js/node/util/validators.zig b/src/bun.js/node/util/validators.zig index cf8f3b1e04..a657aa5f25 100644 --- a/src/bun.js/node/util/validators.zig +++ b/src/bun.js/node/util/validators.zig @@ -60,12 +60,12 @@ pub fn validateInteger(globalThis: *JSGlobalObject, value: JSValue, comptime nam if (!value.isNumber()) return throwErrInvalidArgType(globalThis, name_fmt, name_args, "number", value); if (!value.isAnyInt()) { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {s}", name_args ++ .{value}); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {}", name_args ++ .{bun.fmt.double(value.asNumber())}); } const num = value.asInt52(); if (num < min or num > max) { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {s}", name_args ++ .{ min, max, value }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {}", name_args ++ .{ min, max, num }); } return num; } @@ -77,16 +77,17 @@ pub fn validateInt32(globalThis: *JSGlobalObject, value: JSValue, comptime name_ if (!value.isNumber()) { return throwErrInvalidArgType(globalThis, name_fmt, name_args, "number", value); } - if (!value.isInt32()) { + if (!value.isAnyInt()) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {}", name_args ++ .{value.toFmt(&formatter)}); } - const num = value.asInt32(); - if (num < min or num > max) { + const num = value.asNumber(); + // Use floating point comparison here to ensure values out of i32 range get caught instead of clamp/truncated. + if (num < @as(f64, @floatFromInt(min)) or num > @as(f64, @floatFromInt(max))) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); } - return num; + return @intFromFloat(num); } pub fn validateUint32(globalThis: *JSGlobalObject, value: JSValue, comptime name_fmt: string, name_args: anytype, greater_than_zero: bool) bun.JSError!u32 { @@ -133,7 +134,7 @@ pub fn validateNumber(globalThis: *JSGlobalObject, value: JSValue, comptime name } else if (min != null) { return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d}. Received {s}", name_args ++ .{ max, value }); } else { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must and <= {d}. Received {s}", name_args ++ .{ max, value }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be <= {d}. Received {s}", name_args ++ .{ max, value }); } } return num; diff --git a/src/bun.js/node/win_watcher.zig b/src/bun.js/node/win_watcher.zig index 9469ccc62a..93a96faf4a 100644 --- a/src/bun.js/node/win_watcher.zig +++ b/src/bun.js/node/win_watcher.zig @@ -4,7 +4,7 @@ const windows = bun.windows; const uv = windows.libuv; const Path = @import("../../resolver/resolve_path.zig"); const Fs = @import("../../fs.zig"); -const Mutex = @import("../../lock.zig").Lock; +const Mutex = bun.Mutex; const string = bun.string; const JSC = bun.JSC; const VirtualMachine = JSC.VirtualMachine; diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index 45ebac3282..e49925e148 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -44,11 +44,62 @@ mime_types: ?bun.http.MimeType.Map = null, node_fs_stat_watcher_scheduler: ?*StatWatcherScheduler = null, listening_sockets_for_watch_mode: std.ArrayListUnmanaged(bun.FileDescriptor) = .{}, -listening_sockets_for_watch_mode_lock: bun.Lock = .{}, +listening_sockets_for_watch_mode_lock: bun.Mutex = .{}, temp_pipe_read_buffer: ?*PipeReadBuffer = null, +aws_signature_cache: AWSSignatureCache = .{}, + const PipeReadBuffer = [256 * 1024]u8; +const DIGESTED_HMAC_256_LEN = 32; +pub const AWSSignatureCache = struct { + cache: bun.StringArrayHashMap([DIGESTED_HMAC_256_LEN]u8) = bun.StringArrayHashMap([DIGESTED_HMAC_256_LEN]u8).init(bun.default_allocator), + date: u64 = 0, + lock: bun.Mutex = .{}, + + pub fn clean(this: *@This()) void { + for (this.cache.keys()) |cached_key| { + bun.default_allocator.free(cached_key); + } + this.cache.clearRetainingCapacity(); + } + + pub fn get(this: *@This(), numeric_day: u64, key: []const u8) ?[]const u8 { + this.lock.lock(); + defer this.lock.unlock(); + if (this.date == 0) { + return null; + } + if (this.date == numeric_day) { + if (this.cache.getKey(key)) |cached| { + return cached; + } + } + return null; + } + + pub fn set(this: *@This(), numeric_day: u64, key: []const u8, value: [DIGESTED_HMAC_256_LEN]u8) void { + this.lock.lock(); + defer this.lock.unlock(); + if (this.date == 0) { + this.cache = bun.StringArrayHashMap([DIGESTED_HMAC_256_LEN]u8).init(bun.default_allocator); + } else if (this.date != numeric_day) { + // day changed so we clean the old cache + this.clean(); + } + this.date = numeric_day; + this.cache.put(bun.default_allocator.dupe(u8, key) catch bun.outOfMemory(), value) catch bun.outOfMemory(); + } + pub fn deinit(this: *@This()) void { + this.date = 0; + this.clean(); + this.cache.deinit(); + } +}; + +pub fn awsCache(this: *RareData) *AWSSignatureCache { + return &this.aws_signature_cache; +} pub fn pipeReadBuffer(this: *RareData) *PipeReadBuffer { return this.temp_pipe_read_buffer orelse { @@ -371,6 +422,7 @@ pub fn spawnIPCContext(rare: *RareData, vm: *JSC.VirtualMachine) *uws.SocketCont pub fn globalDNSResolver(rare: *RareData, vm: *JSC.VirtualMachine) *JSC.DNS.DNSResolver { if (rare.global_dns_data == null) { rare.global_dns_data = JSC.DNS.GlobalData.init(vm.allocator, vm); + rare.global_dns_data.?.resolver.ref(); // live forever } return &rare.global_dns_data.?.resolver; @@ -389,6 +441,8 @@ pub fn deinit(this: *RareData) void { bun.default_allocator.destroy(pipe); } + this.aws_signature_cache.deinit(); + if (this.boring_ssl_engine) |engine| { _ = bun.BoringSSL.ENGINE_free(engine); } diff --git a/src/bun.js/script_execution_context.zig b/src/bun.js/script_execution_context.zig deleted file mode 100644 index 3753952363..0000000000 --- a/src/bun.js/script_execution_context.zig +++ /dev/null @@ -1,7 +0,0 @@ -const JSC = bun.JSC; - -pub const ScriptExecutionContext = extern struct { - main_file_path: JSC.ZigString, - is_macro: bool = false, - js_global_object: bool = false, -}; diff --git a/src/bun.js/test/diff_format.zig b/src/bun.js/test/diff_format.zig index fc04a74e33..e198e474cd 100644 --- a/src/bun.js/test/diff_format.zig +++ b/src/bun.js/test/diff_format.zig @@ -111,7 +111,7 @@ pub const DiffFormatter = struct { Writer, buf_writer, fmt_options, - ); + ) catch {}; // TODO: buffered_writer.flush() catch unreachable; buffered_writer_.context = &expected_buf; @@ -125,7 +125,7 @@ pub const DiffFormatter = struct { Writer, buf_writer, fmt_options, - ); + ) catch {}; // TODO: buffered_writer.flush() catch unreachable; } diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 1650e7dd26..222aeeebab 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2152,7 +2152,6 @@ pub const Expect = struct { pub fn toThrow(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); - const vm = globalThis.bunVM(); const thisValue = callFrame.this(); const arguments = callFrame.argumentsAsArray(1); @@ -2173,68 +2172,18 @@ pub const Expect = struct { break :brk innerConstructorValue; } } + } else if (value.isString()) { + // `.toThrow("") behaves the same as `.toThrow()` + const s = value.toString(globalThis); + if (s.length() == 0) break :brk .zero; } break :brk value; }; expected_value.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toThrow", "expected"); - const not = this.flags.not; - var return_value_from_function: JSValue = .zero; - const result_: ?JSValue = brk: { - if (!value.jsType().isFunction()) { - if (this.flags.promise != .none) { - break :brk value; - } - - return globalThis.throw("Expected value must be a function", .{}); - } - - var return_value: JSValue = .zero; - - // Drain existing unhandled rejections - vm.global.handleRejectedPromises(); - - var scope = vm.unhandledRejectionScope(); - const prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; - vm.unhandled_pending_rejection_to_capture = &return_value; - vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue; - return_value_from_function = value.call(globalThis, .undefined, &.{}) catch |err| globalThis.takeException(err); - vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture; - - vm.global.handleRejectedPromises(); - - if (return_value == .zero) { - return_value = return_value_from_function; - } - - if (return_value.asAnyPromise()) |promise| { - vm.waitForPromise(promise); - scope.apply(vm); - switch (promise.unwrap(globalThis.vm(), .mark_handled)) { - .fulfilled => { - break :brk null; - }, - .rejected => |rejected| { - // since we know for sure it rejected, we should always return the error - break :brk rejected.toError() orelse rejected; - }, - .pending => unreachable, - } - } - - if (return_value != return_value_from_function) { - if (return_value_from_function.asAnyPromise()) |existing| { - existing.setHandled(globalThis.vm()); - } - } - - scope.apply(vm); - - break :brk return_value.toError() orelse return_value_from_function.toError(); - }; + const result_, const return_value_from_function = try this.getValueAsToThrow(globalThis, try this.getValue(globalThis, thisValue, "toThrow", "expected")); const did_throw = result_ != null; @@ -2506,6 +2455,289 @@ pub const Expect = struct { expected_value.getClassName(globalThis, &expected_class); return this.throw(globalThis, signature, expected_fmt, .{ expected_class, result.toFmt(&formatter) }); } + fn getValueAsToThrow(this: *Expect, globalThis: *JSGlobalObject, value: JSValue) bun.JSError!struct { ?JSValue, JSValue } { + const vm = globalThis.bunVM(); + + var return_value_from_function: JSValue = .zero; + + if (!value.jsType().isFunction()) { + if (this.flags.promise != .none) { + return .{ value, return_value_from_function }; + } + + return globalThis.throw("Expected value must be a function", .{}); + } + + var return_value: JSValue = .zero; + + // Drain existing unhandled rejections + vm.global.handleRejectedPromises(); + + var scope = vm.unhandledRejectionScope(); + const prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; + vm.unhandled_pending_rejection_to_capture = &return_value; + vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue; + return_value_from_function = value.call(globalThis, .undefined, &.{}) catch |err| globalThis.takeException(err); + vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture; + + vm.global.handleRejectedPromises(); + + if (return_value == .zero) { + return_value = return_value_from_function; + } + + if (return_value.asAnyPromise()) |promise| { + vm.waitForPromise(promise); + scope.apply(vm); + switch (promise.unwrap(globalThis.vm(), .mark_handled)) { + .fulfilled => { + return .{ null, return_value_from_function }; + }, + .rejected => |rejected| { + // since we know for sure it rejected, we should always return the error + return .{ rejected.toError() orelse rejected, return_value_from_function }; + }, + .pending => unreachable, + } + } + + if (return_value != return_value_from_function) { + if (return_value_from_function.asAnyPromise()) |existing| { + existing.setHandled(globalThis.vm()); + } + } + + scope.apply(vm); + + return .{ return_value.toError() orelse return_value_from_function.toError(), return_value_from_function }; + } + pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + defer this.postMatch(globalThis); + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments_old(2); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + incrementExpectCallCounter(); + + const not = this.flags.not; + if (not) { + const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", true); + return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); + } + + if (this.testScope() == null) { + const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", true); + return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); + } + + var hint_string: ZigString = ZigString.Empty; + switch (arguments.len) { + 0 => {}, + 1 => { + if (arguments[0].isString()) { + arguments[0].toZigString(&hint_string, globalThis); + } else { + return this.throw(globalThis, "", "\n\nMatcher error: Expected first argument to be a string\n", .{}); + } + }, + else => return this.throw(globalThis, "", "\n\nMatcher error: Expected zero or one arguments\n", .{}), + } + + var hint = hint_string.toSlice(default_allocator); + defer hint.deinit(); + + const value: JSValue = try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingSnapshot", "properties, hint")); + + return this.snapshot(globalThis, value, null, hint.slice(), "toThrowErrorMatchingSnapshot"); + } + fn fnToErrStringOrUndefined(this: *Expect, globalThis: *JSGlobalObject, value: JSValue) !JSValue { + const err_value, _ = try this.getValueAsToThrow(globalThis, value); + + var err_value_res = err_value orelse JSValue.undefined; + if (err_value_res.isAnyError()) { + const message = try err_value_res.getTruthyComptime(globalThis, "message") orelse JSValue.undefined; + err_value_res = message; + } else { + err_value_res = JSValue.undefined; + } + return err_value_res; + } + pub fn toThrowErrorMatchingInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + defer this.postMatch(globalThis); + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments_old(2); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + incrementExpectCallCounter(); + + const not = this.flags.not; + if (not) { + const signature = comptime getSignature("toThrowErrorMatchingInlineSnapshot", "", true); + return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); + } + + var has_expected = false; + var expected_string: ZigString = ZigString.Empty; + switch (arguments.len) { + 0 => {}, + 1 => { + if (arguments[0].isString()) { + has_expected = true; + arguments[0].toZigString(&expected_string, globalThis); + } else { + return this.throw(globalThis, "", "\n\nMatcher error: Expected first argument to be a string\n", .{}); + } + }, + else => return this.throw(globalThis, "", "\n\nMatcher error: Expected zero or one arguments\n", .{}), + } + + var expected = expected_string.toSlice(default_allocator); + defer expected.deinit(); + + const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null; + + const value: JSValue = try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingInlineSnapshot", "properties, hint")); + + return this.inlineSnapshot(globalThis, callFrame, value, null, expected_slice, "toThrowErrorMatchingInlineSnapshot"); + } + pub fn toMatchInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + defer this.postMatch(globalThis); + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments_old(2); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + incrementExpectCallCounter(); + + const not = this.flags.not; + if (not) { + const signature = comptime getSignature("toMatchInlineSnapshot", "", true); + return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); + } + + var has_expected = false; + var expected_string: ZigString = ZigString.Empty; + var property_matchers: ?JSValue = null; + switch (arguments.len) { + 0 => {}, + 1 => { + if (arguments[0].isString()) { + has_expected = true; + arguments[0].toZigString(&expected_string, globalThis); + } else if (arguments[0].isObject()) { + property_matchers = arguments[0]; + } else { + return this.throw(globalThis, "", "\n\nMatcher error: Expected first argument to be a string or object\n", .{}); + } + }, + else => { + if (!arguments[0].isObject()) { + const signature = comptime getSignature("toMatchInlineSnapshot", "properties, hint", false); + return this.throw(globalThis, signature, "\n\nMatcher error: Expected properties must be an object\n", .{}); + } + + property_matchers = arguments[0]; + + if (arguments[1].isString()) { + has_expected = true; + arguments[1].toZigString(&expected_string, globalThis); + } + }, + } + + var expected = expected_string.toSlice(default_allocator); + defer expected.deinit(); + + const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null; + + const value = try this.getValue(globalThis, thisValue, "toMatchInlineSnapshot", "properties, hint"); + return this.inlineSnapshot(globalThis, callFrame, value, property_matchers, expected_slice, "toMatchInlineSnapshot"); + } + fn inlineSnapshot( + this: *Expect, + globalThis: *JSGlobalObject, + callFrame: *CallFrame, + value: JSValue, + property_matchers: ?JSValue, + result: ?[]const u8, + comptime fn_name: []const u8, + ) bun.JSError!JSValue { + // jest counts inline snapshots towards the snapshot counter for some reason + _ = Jest.runner.?.snapshots.addCount(this, "") catch |e| switch (e) { + error.OutOfMemory => return error.OutOfMemory, + error.NoTest => {}, + }; + + const update = Jest.runner.?.snapshots.update_snapshots; + var needs_write = false; + + var pretty_value: MutableString = try MutableString.init(default_allocator, 0); + defer pretty_value.deinit(); + try this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value, fn_name); + + if (result) |saved_value| { + if (strings.eqlLong(pretty_value.slice(), saved_value, true)) { + Jest.runner.?.snapshots.passed += 1; + return .undefined; + } else if (update) { + Jest.runner.?.snapshots.passed += 1; + needs_write = true; + } else { + Jest.runner.?.snapshots.failed += 1; + const signature = comptime getSignature(fn_name, "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + const diff_format = DiffFormatter{ + .received_string = pretty_value.slice(), + .expected_string = saved_value, + .globalThis = globalThis, + }; + + return globalThis.throwPretty(fmt, .{diff_format}); + } + } else { + needs_write = true; + } + + if (needs_write) { + if (this.testScope() == null) { + const signature = comptime getSignature(fn_name, "", true); + return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); + } + + // 1. find the src loc of the snapshot + const srcloc = callFrame.getCallerSrcLoc(globalThis); + defer srcloc.str.deref(); + const describe = this.testScope().?.describe; + const fget = Jest.runner.?.files.get(describe.file_id); + + if (!srcloc.str.eqlUTF8(fget.source.path.text)) { + const signature = comptime getSignature(fn_name, "", true); + return this.throw(globalThis, signature, + \\ + \\ + \\Matcher error: Inline snapshot matchers must be called from the test file: + \\ Expected to be called from file: "{}" + \\ {s} called from file: "{}" + \\ + , .{ + std.zig.fmtEscapes(fget.source.path.text), + fn_name, + std.zig.fmtEscapes(srcloc.str.toUTF8(Jest.runner.?.snapshots.allocator).slice()), + }); + } + + // 2. save to write later + try Jest.runner.?.snapshots.addInlineSnapshotToWrite(describe.file_id, .{ + .line = srcloc.line, + .col = srcloc.column, + .value = pretty_value.toOwnedSlice(), + .has_matchers = property_matchers != null, + .is_added = result == null, + .kind = fn_name, + }); + } + + return .undefined; + } pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); const thisValue = callFrame.this(); @@ -2534,6 +2766,8 @@ pub const Expect = struct { arguments[0].toZigString(&hint_string, globalThis); } else if (arguments[0].isObject()) { property_matchers = arguments[0]; + } else { + return this.throw(globalThis, "", "\n\nMatcher error: Expected first argument to be a string or object\n", .{}); } }, else => { @@ -2546,6 +2780,8 @@ pub const Expect = struct { if (arguments[1].isString()) { arguments[1].toZigString(&hint_string, globalThis); + } else { + return this.throw(globalThis, "", "\n\nMatcher error: Expected second argument to be a string\n", .{}); } }, } @@ -2555,17 +2791,20 @@ pub const Expect = struct { const value: JSValue = try this.getValue(globalThis, thisValue, "toMatchSnapshot", "properties, hint"); - if (!value.isObject() and property_matchers != null) { - const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); - return this.throw(globalThis, signature, "\n\nMatcher error: received values must be an object when the matcher has properties\n", .{}); - } - + return this.snapshot(globalThis, value, property_matchers, hint.slice(), "toMatchSnapshot"); + } + fn matchAndFmtSnapshot(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, property_matchers: ?JSValue, pretty_value: *MutableString, comptime fn_name: []const u8) bun.JSError!void { if (property_matchers) |_prop_matchers| { + if (!value.isObject()) { + const signature = comptime getSignature(fn_name, "properties, hint", false); + return this.throw(globalThis, signature, "\n\nMatcher error: received values must be an object when the matcher has properties\n", .{}); + } + const prop_matchers = _prop_matchers; if (!value.jestDeepMatch(prop_matchers, globalThis, true)) { // TODO: print diff with properties from propertyMatchers - const signature = comptime getSignature("toMatchSnapshot", "propertyMatchers", false); + const signature = comptime getSignature(fn_name, "propertyMatchers", false); const fmt = signature ++ "\n\nExpected propertyMatchers to match properties from received object" ++ "\n\nReceived: {any}\n"; @@ -2574,7 +2813,17 @@ pub const Expect = struct { } } - const result = Jest.runner.?.snapshots.getOrPut(this, value, hint.slice(), globalThis) catch |err| { + value.jestSnapshotPrettyFormat(pretty_value, globalThis) catch { + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + return globalThis.throw("Failed to pretty format value: {s}", .{value.toFmt(&formatter)}); + }; + } + fn snapshot(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, property_matchers: ?JSValue, hint: []const u8, comptime fn_name: []const u8) bun.JSError!JSValue { + var pretty_value: MutableString = try MutableString.init(default_allocator, 0); + defer pretty_value.deinit(); + try this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value, fn_name); + + const existing_value = Jest.runner.?.snapshots.getOrPut(this, pretty_value.slice(), hint) catch |err| { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; const test_file_path = Jest.runner.?.files.get(this.testScope().?.describe.file_id).source.path.text; return switch (err) { @@ -2586,21 +2835,14 @@ pub const Expect = struct { }; }; - if (result) |saved_value| { - var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable; - value.jestSnapshotPrettyFormat(&pretty_value, globalThis) catch { - var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; - return globalThis.throw("Failed to pretty format value: {s}", .{value.toFmt(&formatter)}); - }; - defer pretty_value.deinit(); - + if (existing_value) |saved_value| { if (strings.eqlLong(pretty_value.slice(), saved_value, true)) { Jest.runner.?.snapshots.passed += 1; return .undefined; } Jest.runner.?.snapshots.failed += 1; - const signature = comptime getSignature("toMatchSnapshot", "expected", false); + const signature = comptime getSignature(fn_name, "expected", false); const fmt = signature ++ "\n\n{any}\n"; const diff_format = DiffFormatter{ .received_string = pretty_value.slice(), @@ -2644,9 +2886,9 @@ pub const Expect = struct { }.anythingInIterator); pass = !any_properties_in_iterator; } else { - var props_iter = JSC.JSPropertyIterator(.{ + var props_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, - + .own_properties_only = false, .include_value = true, }).init(globalThis, value); defer props_iter.deinit(); @@ -3822,6 +4064,37 @@ pub const Expect = struct { return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: \\>= 1\n" ++ "Received number of calls: {any}\n", .{calls.getLength(globalThis)}); } + pub fn toHaveBeenCalledOnce(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + JSC.markBinding(@src()); + + const thisValue = callframe.this(); + defer this.postMatch(globalThis); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledOnce", "expected"); + + incrementExpectCallCounter(); + + const calls = JSMockFunction__getCalls(value); + + if (calls == .zero or !calls.jsType().isArray()) { + return globalThis.throw("Expected value must be a mock function: {}", .{value}); + } + + var pass = @as(i32, @intCast(calls.getLength(globalThis))) == 1; + + const not = this.flags.not; + if (not) pass = !pass; + if (pass) return .undefined; + + // handle failure + if (not) { + const signature = comptime getSignature("toHaveBeenCalledOnce", "expected", true); + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: not 1\n" ++ "Received number of calls: {d}\n", .{calls.getLength(globalThis)}); + } + + const signature = comptime getSignature("toHaveBeenCalledOnce", "expected", false); + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: 1\n" ++ "Received number of calls: {d}\n", .{calls.getLength(globalThis)}); + } + pub fn toHaveBeenCalledTimes(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { JSC.markBinding(@src()); @@ -3923,7 +4196,7 @@ pub const Expect = struct { JSC.markBinding(@src()); const thisValue = callframe.this(); - const arguments = callframe.argumentsPtr()[0..callframe.argumentsCount()]; + const arguments = callframe.arguments(); defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledWith", "expected"); @@ -3982,7 +4255,7 @@ pub const Expect = struct { JSC.markBinding(@src()); const thisValue = callframe.this(); - const arguments = callframe.argumentsPtr()[0..callframe.argumentsCount()]; + const arguments = callframe.arguments(); defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastCalledWith", "expected"); @@ -4040,7 +4313,7 @@ pub const Expect = struct { JSC.markBinding(@src()); const thisValue = callframe.this(); - const arguments = callframe.argumentsPtr()[0..callframe.argumentsCount()]; + const arguments = callframe.arguments(); defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenNthCalledWith", "expected"); @@ -4201,9 +4474,6 @@ pub const Expect = struct { pub const toHaveReturnedWith = notImplementedJSCFn; pub const toHaveLastReturnedWith = notImplementedJSCFn; pub const toHaveNthReturnedWith = notImplementedJSCFn; - pub const toMatchInlineSnapshot = notImplementedJSCFn; - pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn; - pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn; pub fn getStaticNot(globalThis: *JSGlobalObject, _: JSValue, _: JSValue) JSValue { return ExpectStatic.create(globalThis, .{ .not = true }); @@ -4259,13 +4529,14 @@ pub const Expect = struct { const matchers_to_register = args[0]; { - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, + .own_properties_only = false, }).init(globalThis, matchers_to_register); defer iter.deinit(); - while (iter.next()) |*matcher_name| { + while (try iter.next()) |*matcher_name| { const matcher_fn: JSValue = iter.value; if (!matcher_fn.jsType().isFunction()) { @@ -4398,7 +4669,6 @@ pub const Expect = struct { if (result.isObject()) { if (try result.get(globalThis, "pass")) |pass_value| { pass = pass_value.toBoolean(); - if (globalThis.hasException()) return false; if (result.fastGet(globalThis, .message)) |message_value| { if (!message_value.isString() and !message_value.isCallable(globalThis.vm())) { @@ -4486,12 +4756,11 @@ pub const Expect = struct { incrementExpectCallCounter(); // prepare the args array - const args_ptr = callFrame.argumentsPtr(); - const args_count = callFrame.argumentsCount(); + const args = callFrame.arguments(); var allocator = std.heap.stackFallback(8 * @sizeOf(JSValue), globalThis.allocator()); - var matcher_args = try std.ArrayList(JSValue).initCapacity(allocator.get(), args_count + 1); + var matcher_args = try std.ArrayList(JSValue).initCapacity(allocator.get(), args.len + 1); matcher_args.appendAssumeCapacity(value); - for (0..args_count) |i| matcher_args.appendAssumeCapacity(args_ptr[i]); + for (args) |arg| matcher_args.appendAssumeCapacity(arg); _ = try executeCustomMatcher(globalThis, matcher_name, matcher_fn, matcher_args.items, expect.flags, false); @@ -4967,14 +5236,13 @@ pub const ExpectCustomAsymmetricMatcher = struct { ExpectCustomAsymmetricMatcher.matcherFnSetCached(instance_jsvalue, globalThis, matcher_fn); // capture the args as a JS array saved in the instance, so the matcher can be executed later on with them - const args_ptr = callFrame.argumentsPtr(); - const args_count: usize = callFrame.argumentsCount(); - var args = JSValue.createEmptyArray(globalThis, args_count); - for (0..args_count) |i| { - args.putIndex(globalThis, @truncate(i), args_ptr[i]); + const args = callFrame.arguments(); + const array = JSValue.createEmptyArray(globalThis, args.len); + for (args, 0..) |arg, i| { + array.putIndex(globalThis, @truncate(i), arg); } - args.ensureStillAlive(); - ExpectCustomAsymmetricMatcher.capturedArgsSetCached(instance_jsvalue, globalThis, args); + ExpectCustomAsymmetricMatcher.capturedArgsSetCached(instance_jsvalue, globalThis, array); + array.ensureStillAlive(); // return the same instance, now fully initialized including the captured args (previously it was incomplete) return instance_jsvalue; @@ -5155,9 +5423,7 @@ pub const ExpectMatcherUtils = struct { try buffered_writer.flush(); - const str = bun.String.createUTF8(mutable_string.toOwnedSlice()); - defer str.deref(); - return str.toJS(globalThis); + return bun.String.createUTF8ForJS(globalThis, mutable_string.toOwnedSlice()); } inline fn printValueCatched(globalThis: *JSGlobalObject, value: JSValue, comptime color_or_null: ?[]const u8) JSValue { diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index 38c0397a03..57b4273aba 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -297,6 +297,10 @@ export default [ fn: "toHaveBeenCalled", length: 0, }, + toHaveBeenCalledOnce: { + fn: "toHaveBeenCalledOnce", + length: 0, + }, toHaveBeenCalledTimes: { fn: "toHaveBeenCalledTimes", length: 1, diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 4be73143aa..ab6c6163eb 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1931,7 +1931,7 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa .quote_strings = true, }; const value_fmt = current_arg.toFmt(&formatter); - const test_index_str = std.fmt.allocPrint(allocator, "{any}", .{value_fmt}) catch bun.outOfMemory(); + const test_index_str = std.fmt.allocPrint(allocator, "{}", .{value_fmt}) catch bun.outOfMemory(); defer allocator.free(test_index_str); list.appendSlice(allocator, test_index_str) catch bun.outOfMemory(); idx += 1; diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index dd119384c7..ac9b1fdcd7 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -95,7 +95,7 @@ pub const JestPrettyFormat = struct { comptime Writer: type, writer: Writer, options: FormatOptions, - ) void { + ) bun.JSError!void { var fmt: JestPrettyFormat.Formatter = undefined; defer { if (fmt.map_node) |node| { @@ -123,7 +123,7 @@ pub const JestPrettyFormat = struct { if (level == .Error) { unbuffered_writer.writeAll(comptime Output.prettyFmt("", true)) catch unreachable; } - fmt.format( + try fmt.format( tag, @TypeOf(unbuffered_writer), unbuffered_writer, @@ -135,7 +135,7 @@ pub const JestPrettyFormat = struct { unbuffered_writer.writeAll(comptime Output.prettyFmt("", true)) catch unreachable; } } else { - fmt.format( + try fmt.format( tag, @TypeOf(unbuffered_writer), unbuffered_writer, @@ -152,7 +152,7 @@ pub const JestPrettyFormat = struct { } } if (options.enable_colors) { - fmt.format( + try fmt.format( tag, Writer, writer, @@ -161,7 +161,7 @@ pub const JestPrettyFormat = struct { true, ); } else { - fmt.format( + try fmt.format( tag, Writer, writer, @@ -206,7 +206,7 @@ pub const JestPrettyFormat = struct { tag.tag = .StringPossiblyFormatted; } - fmt.format(tag, Writer, writer, this_value, global, true); + try fmt.format(tag, Writer, writer, this_value, global, true); if (fmt.remaining_values.len == 0) { break; } @@ -228,7 +228,7 @@ pub const JestPrettyFormat = struct { tag.tag = .StringPossiblyFormatted; } - fmt.format(tag, Writer, writer, this_value, global, false); + try fmt.format(tag, Writer, writer, this_value, global, false); if (fmt.remaining_values.len == 0) break; @@ -574,13 +574,13 @@ pub const JestPrettyFormat = struct { const next_value = this.remaining_values[0]; this.remaining_values = this.remaining_values[1..]; switch (token) { - Tag.String => this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), - Tag.Double => this.printAs(Tag.Double, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), - Tag.Object => this.printAs(Tag.Object, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), - Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + Tag.String => this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, + Tag.Double => this.printAs(Tag.Double, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, + Tag.Object => this.printAs(Tag.Object, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, + Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, // undefined is overloaded to mean the '%o" field - Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors), + Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors) catch return, else => unreachable, } @@ -680,9 +680,10 @@ pub const JestPrettyFormat = struct { writer: Writer, pub fn forEach(_: [*c]JSC.VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { var this: *@This() = bun.cast(*@This(), ctx orelse return); + if (this.formatter.failed) return; const key = JSC.JSObject.getIndex(nextValue, globalObject, 0); const value = JSC.JSObject.getIndex(nextValue, globalObject, 1); - this.formatter.writeIndent(Writer, this.writer) catch unreachable; + this.formatter.writeIndent(Writer, this.writer) catch return; const key_tag = Tag.get(key, globalObject); this.formatter.format( @@ -692,8 +693,8 @@ pub const JestPrettyFormat = struct { key, this.formatter.globalThis, enable_ansi_colors, - ); - this.writer.writeAll(" => ") catch unreachable; + ) catch return; + this.writer.writeAll(" => ") catch return; const value_tag = Tag.get(value, globalObject); this.formatter.format( value_tag, @@ -702,9 +703,9 @@ pub const JestPrettyFormat = struct { value, this.formatter.globalThis, enable_ansi_colors, - ); - this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; - this.writer.writeAll("\n") catch unreachable; + ) catch return; + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch return; + this.writer.writeAll("\n") catch return; } }; } @@ -715,7 +716,8 @@ pub const JestPrettyFormat = struct { writer: Writer, pub fn forEach(_: [*c]JSC.VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { var this: *@This() = bun.cast(*@This(), ctx orelse return); - this.formatter.writeIndent(Writer, this.writer) catch {}; + if (this.formatter.failed) return; + this.formatter.writeIndent(Writer, this.writer) catch return; const key_tag = Tag.get(nextValue, globalObject); this.formatter.format( key_tag, @@ -724,10 +726,9 @@ pub const JestPrettyFormat = struct { nextValue, this.formatter.globalThis, enable_ansi_colors, - ); - - this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; - this.writer.writeAll("\n") catch unreachable; + ) catch return; + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch return; + this.writer.writeAll("\n") catch return; } }; } @@ -790,6 +791,8 @@ pub const JestPrettyFormat = struct { var ctx: *@This() = bun.cast(*@This(), ctx_ptr orelse return); var this = ctx.formatter; const writer_ = ctx.writer; + if (this.failed) return; + var writer = WrappedWriter(Writer){ .ctx = writer_, .failed = false, @@ -801,14 +804,14 @@ pub const JestPrettyFormat = struct { if (ctx.i == 0) { handleFirstProperty(ctx, globalThis, ctx.parent); } else { - this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + this.printComma(Writer, writer_, enable_ansi_colors) catch return; } defer ctx.i += 1; if (ctx.i > 0) { if (ctx.always_newline or this.always_newline_scope or this.goodTimeForANewLine()) { writer.writeAll("\n"); - this.writeIndent(Writer, writer_) catch {}; + this.writeIndent(Writer, writer_) catch return; this.resetLine(); } else { this.estimated_line_length += 1; @@ -861,7 +864,7 @@ pub const JestPrettyFormat = struct { writer.print( comptime Output.prettyFmt("{s}: ", enable_ansi_colors), - .{bun.fmt.formatJSONString(key.slice())}, + .{bun.fmt.formatJSONStringLatin1(key.slice())}, ); } } else { @@ -880,7 +883,7 @@ pub const JestPrettyFormat = struct { } } - this.format(tag, Writer, ctx.writer, value, globalThis, enable_ansi_colors); + this.format(tag, Writer, ctx.writer, value, globalThis, enable_ansi_colors) catch return; if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -899,7 +902,7 @@ pub const JestPrettyFormat = struct { value: JSValue, jsType: JSValue.JSType, comptime enable_ansi_colors: bool, - ) void { + ) bun.JSError!void { if (this.failed) return; var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length }; @@ -1060,7 +1063,7 @@ pub const JestPrettyFormat = struct { }, .Double => { if (value.isCell()) { - this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); + try this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); return; } @@ -1174,7 +1177,7 @@ pub const JestPrettyFormat = struct { this.writeIndent(Writer, writer_) catch unreachable; this.addForNewLine(1); - this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -1197,7 +1200,7 @@ pub const JestPrettyFormat = struct { const element = JSValue.fromRef(CAPI.JSObjectGetPropertyAtIndex(this.globalThis, ref, i, null)); const tag = Tag.get(element, this.globalThis); - this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -1223,16 +1226,42 @@ pub const JestPrettyFormat = struct { }, .Private => { if (value.as(JSC.WebCore.Response)) |response| { - response.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; - return; + response.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print Response"); + } + return error.JSError; + }; } else if (value.as(JSC.WebCore.Request)) |request| { - request.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + request.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print Request"); + } + return error.JSError; + }; return; } else if (value.as(JSC.API.BuildArtifact)) |build| { - build.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; - return; + build.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print BuildArtifact"); + } + return error.JSError; + }; } else if (value.as(JSC.WebCore.Blob)) |blob| { - blob.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + blob.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print Blob"); + } + return error.JSError; + }; return; } else if (value.as(JSC.DOMFormData) != null) { const toJSONFunction = value.get_unsafe(this.globalThis, "toJSON").?; @@ -1240,12 +1269,11 @@ pub const JestPrettyFormat = struct { this.addForNewLine("FormData (entries) ".len); writer.writeAll(comptime Output.prettyFmt("FormData (entries) ", enable_ansi_colors)); - return this.printAs( + return try this.printAs( .Object, Writer, writer_, - toJSONFunction.call(this.globalThis, value, &.{}) catch |err| - this.globalThis.takeException(err), + try toJSONFunction.call(this.globalThis, value, &.{}), .Object, enable_ansi_colors, ); @@ -1273,12 +1301,12 @@ pub const JestPrettyFormat = struct { return; } else if (jsType != .DOMWrapper) { if (value.isCallable(this.globalThis.vm())) { - return this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors); + return try this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors); } - return this.printAs(.Object, Writer, writer_, value, jsType, enable_ansi_colors); + return try this.printAs(.Object, Writer, writer_, value, jsType, enable_ansi_colors); } - return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); }, .NativeCode => { this.addForNewLine("[native code]".len); @@ -1293,7 +1321,7 @@ pub const JestPrettyFormat = struct { }, .Boolean => { if (value.isCell()) { - this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); + try this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); return; } if (value.toBoolean()) { @@ -1401,7 +1429,7 @@ pub const JestPrettyFormat = struct { const event_type = switch (EventType.map.fromJS(this.globalThis, event_type_value) orelse .unknown) { .MessageEvent, .ErrorEvent => |evt| evt, else => { - return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); }, }; @@ -1435,7 +1463,7 @@ pub const JestPrettyFormat = struct { ); const tag = Tag.get(message_value, this.globalThis); - this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); writer.writeAll(", \n"); } } @@ -1451,9 +1479,9 @@ pub const JestPrettyFormat = struct { const tag = Tag.get(data, this.globalThis); if (tag.cell.isStringLike()) { - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); } else { - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); } writer.writeAll(", \n"); }, @@ -1466,7 +1494,7 @@ pub const JestPrettyFormat = struct { ); const tag = Tag.get(data, this.globalThis); - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); writer.writeAll("\n"); } }, @@ -1531,7 +1559,7 @@ pub const JestPrettyFormat = struct { this.quote_strings = true; defer this.quote_strings = old_quote_strings; - this.format(Tag.get(key_value, this.globalThis), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); + try this.format(Tag.get(key_value, this.globalThis), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); needs_space = true; } @@ -1542,7 +1570,7 @@ pub const JestPrettyFormat = struct { this.quote_strings = true; defer this.quote_strings = prev_quote_strings; - var props_iter = JSC.JSPropertyIterator(.{ + var props_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, @@ -1556,7 +1584,7 @@ pub const JestPrettyFormat = struct { defer this.indent -|= 1; const count_without_children = props_iter.len - @as(usize, @intFromBool(children_prop != null)); - while (props_iter.next()) |prop| { + while (try props_iter.next()) |prop| { if (prop.eqlComptime("children")) continue; @@ -1579,7 +1607,7 @@ pub const JestPrettyFormat = struct { } } - this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -1642,7 +1670,7 @@ pub const JestPrettyFormat = struct { this.indent += 1; this.writeIndent(Writer, writer_) catch unreachable; defer this.indent -|= 1; - this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); + try this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); } writer.writeAll("\n"); @@ -1665,7 +1693,7 @@ pub const JestPrettyFormat = struct { var j: usize = 0; while (j < length) : (j += 1) { const child = JSC.JSObject.getIndex(children, this.globalThis, @as(u32, @intCast(j))); - this.format(Tag.get(child, this.globalThis), Writer, writer_, child, this.globalThis, enable_ansi_colors); + try this.format(Tag.get(child, this.globalThis), Writer, writer_, child, this.globalThis, enable_ansi_colors); if (j + 1 < length) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; @@ -1950,7 +1978,7 @@ pub const JestPrettyFormat = struct { } } - pub fn format(this: *JestPrettyFormat.Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) void { + pub fn format(this: *JestPrettyFormat.Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) bun.JSError!void { if (comptime is_bindgen) { return; } @@ -1962,7 +1990,7 @@ pub const JestPrettyFormat = struct { // comptime var so we have to repeat it here. The rationale there is // it _should_ limit the stack usage because each version of the // function will be relatively small - return switch (result.tag) { + return try switch (result.tag) { .StringPossiblyFormatted => this.printAs(.StringPossiblyFormatted, Writer, writer, value, result.cell, enable_ansi_colors), .String => this.printAs(.String, Writer, writer, value, result.cell, enable_ansi_colors), .Undefined => this.printAs(.Undefined, Writer, writer, value, result.cell, enable_ansi_colors), @@ -2076,7 +2104,7 @@ pub const JestPrettyFormat = struct { this.addForNewLine("ObjectContaining ".len); writer.writeAll("ObjectContaining "); } - this.printAs(.Object, @TypeOf(writer_), writer_, object_value, .Object, enable_ansi_colors); + this.printAs(.Object, @TypeOf(writer_), writer_, object_value, .Object, enable_ansi_colors) catch {}; // TODO: } else if (value.as(expect.ExpectStringContaining)) |matcher| { const substring_value = expect.ExpectStringContaining.stringValueGetCached(value) orelse return true; @@ -2088,7 +2116,7 @@ pub const JestPrettyFormat = struct { this.addForNewLine("StringContaining ".len); writer.writeAll("StringContaining "); } - this.printAs(.String, @TypeOf(writer_), writer_, substring_value, .String, enable_ansi_colors); + this.printAs(.String, @TypeOf(writer_), writer_, substring_value, .String, enable_ansi_colors) catch {}; // TODO: } else if (value.as(expect.ExpectStringMatching)) |matcher| { const test_value = expect.ExpectStringMatching.testValueGetCached(value) orelse return true; @@ -2103,7 +2131,7 @@ pub const JestPrettyFormat = struct { const original_quote_strings = this.quote_strings; if (test_value.isRegExp()) this.quote_strings = false; - this.printAs(.String, @TypeOf(writer_), writer_, test_value, .String, enable_ansi_colors); + this.printAs(.String, @TypeOf(writer_), writer_, test_value, .String, enable_ansi_colors) catch {}; // TODO: this.quote_strings = original_quote_strings; } else if (value.as(expect.ExpectCustomAsymmetricMatcher)) |instance| { const printed = instance.customPrint(value, this.globalThis, writer_, true) catch unreachable; @@ -2121,7 +2149,7 @@ pub const JestPrettyFormat = struct { this.addForNewLine(matcher_name.length() + 1); writer.print("{s}", .{matcher_name}); writer.writeAll(" "); - this.printAs(.Array, @TypeOf(writer_), writer_, args_value, .Array, enable_ansi_colors); + this.printAs(.Array, @TypeOf(writer_), writer_, args_value, .Array, enable_ansi_colors) catch {}; // TODO: } } else { return false; diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig index 39536232a7..b9380d4f68 100644 --- a/src/bun.js/test/snapshot.zig +++ b/src/bun.js/test/snapshot.zig @@ -32,13 +32,42 @@ pub const Snapshots = struct { counts: *bun.StringHashMap(usize), _current_file: ?File = null, snapshot_dir_path: ?string = null, + inline_snapshots_to_write: *std.AutoArrayHashMap(TestRunner.File.ID, std.ArrayList(InlineSnapshotToWrite)), + + pub const InlineSnapshotToWrite = struct { + line: c_ulong, + col: c_ulong, + value: []const u8, // owned by Snapshots.allocator + has_matchers: bool, + is_added: bool, + kind: []const u8, // static lifetime + + fn lessThanFn(_: void, a: InlineSnapshotToWrite, b: InlineSnapshotToWrite) bool { + if (a.line < b.line) return true; + if (a.line > b.line) return false; + if (a.col < b.col) return true; + return false; + } + }; const File = struct { id: TestRunner.File.ID, file: std.fs.File, }; - pub fn getOrPut(this: *Snapshots, expect: *Expect, value: JSValue, hint: string, globalObject: *JSC.JSGlobalObject) !?string { + pub fn addCount(this: *Snapshots, expect: *Expect, hint: []const u8) !struct { []const u8, usize } { + this.total += 1; + const snapshot_name = try expect.getSnapshotName(this.allocator, hint); + const count_entry = try this.counts.getOrPut(snapshot_name); + if (count_entry.found_existing) { + this.allocator.free(snapshot_name); + count_entry.value_ptr.* += 1; + return .{ count_entry.key_ptr.*, count_entry.value_ptr.* }; + } + count_entry.value_ptr.* = 1; + return .{ count_entry.key_ptr.*, count_entry.value_ptr.* }; + } + pub fn getOrPut(this: *Snapshots, expect: *Expect, target_value: []const u8, hint: string) !?string { switch (try this.getSnapshotFile(expect.testScope().?.describe.file_id)) { .result => {}, .err => |err| { @@ -50,21 +79,7 @@ pub const Snapshots = struct { }, } - const snapshot_name = try expect.getSnapshotName(this.allocator, hint); - this.total += 1; - - const count_entry = try this.counts.getOrPut(snapshot_name); - const counter = brk: { - if (count_entry.found_existing) { - this.allocator.free(snapshot_name); - count_entry.value_ptr.* += 1; - break :brk count_entry.value_ptr.*; - } - count_entry.value_ptr.* = 1; - break :brk count_entry.value_ptr.*; - }; - - const name = count_entry.key_ptr.*; + const name, const counter = try this.addCount(expect, hint); var counter_string_buf = [_]u8{0} ** 32; const counter_string = try std.fmt.bufPrint(&counter_string_buf, "{d}", .{counter}); @@ -81,21 +96,18 @@ pub const Snapshots = struct { } // doesn't exist. append to file bytes and add to hashmap. - var pretty_value = try MutableString.init(this.allocator, 0); - try value.jestSnapshotPrettyFormat(&pretty_value, globalObject); - - const estimated_length = "\nexports[`".len + name_with_counter.len + "`] = `".len + pretty_value.list.items.len + "`;\n".len; + const estimated_length = "\nexports[`".len + name_with_counter.len + "`] = `".len + target_value.len + "`;\n".len; try this.file_buf.ensureUnusedCapacity(estimated_length + 10); try this.file_buf.writer().print( "\nexports[`{}`] = `{}`;\n", .{ strings.formatEscapes(name_with_counter, .{ .quote_char = '`' }), - strings.formatEscapes(pretty_value.list.items, .{ .quote_char = '`' }), + strings.formatEscapes(target_value, .{ .quote_char = '`' }), }, ); this.added += 1; - try this.values.put(name_hash, pretty_value.toOwnedSlice()); + try this.values.put(name_hash, try this.allocator.dupe(u8, target_value)); return null; } @@ -103,7 +115,7 @@ pub const Snapshots = struct { if (this.file_buf.items.len == 0) return; const vm = VirtualMachine.get(); - const opts = js_parser.Parser.Options.init(vm.bundler.options.jsx, .js); + const opts = js_parser.Parser.Options.init(vm.transpiler.options.jsx, .js); var temp_log = logger.Log.init(this.allocator); const test_file = Jest.runner.?.files.get(file.id); @@ -129,7 +141,7 @@ pub const Snapshots = struct { opts, &temp_log, &source, - vm.bundler.options.define, + vm.transpiler.options.define, this.allocator, ); @@ -196,6 +208,229 @@ pub const Snapshots = struct { } } + pub fn addInlineSnapshotToWrite(self: *Snapshots, file_id: TestRunner.File.ID, value: InlineSnapshotToWrite) !void { + const gpres = try self.inline_snapshots_to_write.getOrPut(file_id); + if (!gpres.found_existing) { + gpres.value_ptr.* = std.ArrayList(InlineSnapshotToWrite).init(self.allocator); + } + try gpres.value_ptr.append(value); + } + + const inline_snapshot_dbg = bun.Output.scoped(.inline_snapshot, false); + pub fn writeInlineSnapshots(this: *Snapshots) !bool { + var arena_backing = bun.ArenaAllocator.init(this.allocator); + defer arena_backing.deinit(); + const arena = arena_backing.allocator(); + + var success = true; + const vm = VirtualMachine.get(); + const opts = js_parser.Parser.Options.init(vm.transpiler.options.jsx, .js); + + for (this.inline_snapshots_to_write.keys(), this.inline_snapshots_to_write.values()) |file_id, *ils_info| { + _ = arena_backing.reset(.retain_capacity); + + var log = bun.logger.Log.init(arena); + defer if (log.errors > 0) { + log.print(bun.Output.errorWriter()) catch {}; + success = false; + }; + + // 1. sort ils_info by row, col + std.mem.sort(InlineSnapshotToWrite, ils_info.items, {}, InlineSnapshotToWrite.lessThanFn); + + // 2. load file text + const test_file = Jest.runner.?.files.get(file_id); + const test_filename = try arena.dupeZ(u8, test_file.source.path.text); + + const fd = switch (bun.sys.open(test_filename, bun.O.RDWR, 0o644)) { + .result => |r| r, + .err => |e| { + try log.addErrorFmt(&bun.logger.Source.initEmptyFile(test_filename), .{ .start = 0 }, arena, "Failed to update inline snapshot: Failed to open file: {s}", .{e.name()}); + continue; + }, + }; + var file: File = .{ + .id = file_id, + .file = fd.asFile(), + }; + errdefer file.file.close(); + + const file_text = try file.file.readToEndAlloc(arena, std.math.maxInt(usize)); + + var source = bun.logger.Source.initPathString(test_filename, file_text); + + var result_text = std.ArrayList(u8).init(arena); + + // 3. start looping, finding bytes from line/col + + var uncommitted_segment_end: usize = 0; + var last_byte: usize = 0; + var last_line: c_ulong = 1; + var last_col: c_ulong = 1; + for (ils_info.items) |ils| { + if (ils.line == last_line and ils.col == last_col) { + try log.addErrorFmt(&source, .{ .start = @intCast(uncommitted_segment_end) }, arena, "Failed to update inline snapshot: Multiple inline snapshots for the same call are not supported", .{}); + continue; + } + + inline_snapshot_dbg("Finding byte for {}/{}", .{ ils.line, ils.col }); + const byte_offset_add = logger.Source.lineColToByteOffset(file_text[last_byte..], last_line, last_col, ils.line, ils.col) orelse { + inline_snapshot_dbg("-> Could not find byte", .{}); + try log.addErrorFmt(&source, .{ .start = @intCast(uncommitted_segment_end) }, arena, "Failed to update inline snapshot: Ln {d}, Col {d} not found", .{ ils.line, ils.col }); + continue; + }; + + // found + last_byte += byte_offset_add; + last_line = ils.line; + last_col = ils.col; + + var next_start = last_byte; + inline_snapshot_dbg("-> Found byte {}", .{next_start}); + + const final_start: i32, const final_end: i32, const needs_pre_comma: bool = blk: { + if (file_text[next_start..].len > 0) switch (file_text[next_start]) { + ' ', '.' => { + // work around off-by-1 error in `expect("§").toMatchInlineSnapshot()` + next_start += 1; + }, + else => {}, + }; + const fn_name = ils.kind; + if (!bun.strings.startsWith(file_text[next_start..], fn_name)) { + try log.addErrorFmt(&source, .{ .start = @intCast(next_start) }, arena, "Failed to update inline snapshot: Could not find 'toMatchInlineSnapshot' here", .{}); + continue; + } + next_start += fn_name.len; + + var lexer = bun.js_lexer.Lexer.initWithoutReading(&log, source, arena); + if (next_start > 0) { + // equivalent to lexer.consumeRemainderBytes(next_start) + lexer.current += next_start - (lexer.current - lexer.end); + lexer.step(); + } + try lexer.next(); + var parser: bun.js_parser.TSXParser = undefined; + try bun.js_parser.TSXParser.init(arena, &log, &source, vm.transpiler.options.define, lexer, opts, &parser); + + try parser.lexer.expect(.t_open_paren); + const after_open_paren_loc = parser.lexer.loc().start; + if (parser.lexer.token == .t_close_paren) { + // zero args + if (ils.has_matchers) { + try log.addErrorFmt(&source, parser.lexer.loc(), arena, "Failed to update inline snapshot: Snapshot has matchers and yet has no arguments", .{}); + continue; + } + const close_paren_loc = parser.lexer.loc().start; + try parser.lexer.expect(.t_close_paren); + break :blk .{ after_open_paren_loc, close_paren_loc, false }; + } + if (parser.lexer.token == .t_dot_dot_dot) { + try log.addErrorFmt(&source, parser.lexer.loc(), arena, "Failed to update inline snapshot: Spread is not allowed", .{}); + continue; + } + + const before_expr_loc = parser.lexer.loc().start; + const expr_1 = try parser.parseExpr(.comma); + const after_expr_loc = parser.lexer.loc().start; + + var is_one_arg = false; + if (parser.lexer.token == .t_comma) { + try parser.lexer.expect(.t_comma); + if (parser.lexer.token == .t_close_paren) is_one_arg = true; + } else is_one_arg = true; + const after_comma_loc = parser.lexer.loc().start; + + if (is_one_arg) { + try parser.lexer.expect(.t_close_paren); + if (ils.has_matchers) { + break :blk .{ after_expr_loc, after_comma_loc, true }; + } else { + if (expr_1.data != .e_string) { + try log.addErrorFmt(&source, expr_1.loc, arena, "Failed to update inline snapshot: Argument must be a string literal", .{}); + continue; + } + break :blk .{ before_expr_loc, after_expr_loc, false }; + } + } + + if (parser.lexer.token == .t_dot_dot_dot) { + try log.addErrorFmt(&source, parser.lexer.loc(), arena, "Failed to update inline snapshot: Spread is not allowed", .{}); + continue; + } + + const before_expr_2_loc = parser.lexer.loc().start; + const expr_2 = try parser.parseExpr(.comma); + const after_expr_2_loc = parser.lexer.loc().start; + + if (!ils.has_matchers) { + try log.addErrorFmt(&source, parser.lexer.loc(), arena, "Failed to update inline snapshot: Snapshot does not have matchers and yet has two arguments", .{}); + continue; + } + if (expr_2.data != .e_string) { + try log.addErrorFmt(&source, expr_2.loc, arena, "Failed to update inline snapshot: Argument must be a string literal", .{}); + continue; + } + + if (parser.lexer.token == .t_comma) { + try parser.lexer.expect(.t_comma); + } + if (parser.lexer.token != .t_close_paren) { + try log.addErrorFmt(&source, parser.lexer.loc(), arena, "Failed to update inline snapshot: Snapshot expects at most two arguments", .{}); + continue; + } + try parser.lexer.expect(.t_close_paren); + + break :blk .{ before_expr_2_loc, after_expr_2_loc, false }; + }; + const final_start_usize = std.math.cast(usize, final_start) orelse 0; + const final_end_usize = std.math.cast(usize, final_end) orelse 0; + inline_snapshot_dbg(" -> Found update range {}-{}", .{ final_start_usize, final_end_usize }); + + if (final_end_usize < final_start_usize or final_start_usize < uncommitted_segment_end) { + try log.addErrorFmt(&source, .{ .start = final_start }, arena, "Failed to update inline snapshot: Did not advance.", .{}); + continue; + } + + try result_text.appendSlice(file_text[uncommitted_segment_end..final_start_usize]); + uncommitted_segment_end = final_end_usize; + + if (needs_pre_comma) try result_text.appendSlice(", "); + const result_text_writer = result_text.writer(); + try result_text.appendSlice("`"); + try bun.js_printer.writePreQuotedString(ils.value, @TypeOf(result_text_writer), result_text_writer, '`', false, false, .utf8); + try result_text.appendSlice("`"); + + if (ils.is_added) Jest.runner.?.snapshots.added += 1; + } + + // commit the last segment + try result_text.appendSlice(file_text[uncommitted_segment_end..]); + + if (log.errors > 0) { + // skip writing the file if there were errors + continue; + } + + // 4. write out result_text to the file + file.file.seekTo(0) catch |e| { + try log.addErrorFmt(&source, .{ .start = 0 }, arena, "Failed to update inline snapshot: Seek file error: {s}", .{@errorName(e)}); + continue; + }; + + file.file.writeAll(result_text.items) catch |e| { + try log.addErrorFmt(&source, .{ .start = 0 }, arena, "Failed to update inline snapshot: Write file error: {s}", .{@errorName(e)}); + continue; + }; + if (result_text.items.len < file_text.len) { + file.file.setEndPos(result_text.items.len) catch { + @panic("Failed to update inline snapshot: File was left in an invalid state"); + }; + } + } + return success; + } + fn getSnapshotFile(this: *Snapshots, file_id: TestRunner.File.ID) !JSC.Maybe(void) { if (this._current_file == null or this._current_file.?.id != file_id) { try this.writeSnapshotFile(); diff --git a/src/bun.js/uuid.zig b/src/bun.js/uuid.zig index 0d59acd532..428e12b5cc 100644 --- a/src/bun.js/uuid.zig +++ b/src/bun.js/uuid.zig @@ -140,7 +140,7 @@ pub fn newV4() UUID { pub const UUID7 = struct { bytes: [16]u8, - var uuid_v7_lock = bun.Lock{}; + var uuid_v7_lock = bun.Mutex{}; var uuid_v7_last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 }; var uuid_v7_counter: std.atomic.Value(u32) = .{ .raw = 0 }; diff --git a/src/bun.js/web_worker.zig b/src/bun.js/web_worker.zig index b91b917496..c8ae517d90 100644 --- a/src/bun.js/web_worker.zig +++ b/src/bun.js/web_worker.zig @@ -152,7 +152,7 @@ pub const WebWorker = struct { } } - var resolved_entry_point: bun.resolver.Result = parent.bundler.resolveEntryPoint(str) catch { + var resolved_entry_point: bun.resolver.Result = parent.transpiler.resolveEntryPoint(str) catch { const out = logger.toJS(parent.global, bun.default_allocator, "Error resolving Worker entry point").toBunString(parent.global); error_message.* = out; return null; @@ -186,10 +186,10 @@ pub const WebWorker = struct { log("[{d}] WebWorker.create", .{this_context_id}); var spec_slice = specifier_str.toUTF8(bun.default_allocator); defer spec_slice.deinit(); - const prev_log = parent.bundler.log; + const prev_log = parent.transpiler.log; var temp_log = bun.logger.Log.init(bun.default_allocator); - parent.bundler.setLog(&temp_log); - defer parent.bundler.setLog(prev_log); + parent.transpiler.setLog(&temp_log); + defer parent.transpiler.setLog(prev_log); defer temp_log.deinit(); const preload_modules = if (preload_modules_ptr) |ptr| @@ -226,7 +226,7 @@ pub const WebWorker = struct { .execution_context_id = this_context_id, .mini = mini, .specifier = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), - .store_fd = parent.bundler.resolver.store_fd, + .store_fd = parent.transpiler.resolver.store_fd, .name = brk: { if (!name_str.isEmpty()) { break :brk std.fmt.allocPrintZ(bun.default_allocator, "{}", .{name_str}) catch bun.outOfMemory(); @@ -274,14 +274,14 @@ pub const WebWorker = struct { this.arena = try bun.MimallocArena.init(); var vm = try JSC.VirtualMachine.initWorker(this, .{ .allocator = this.arena.?.allocator(), - .args = this.parent.bundler.options.transform_options, + .args = this.parent.transpiler.options.transform_options, .store_fd = this.store_fd, .graph = this.parent.standalone_module_graph, }); vm.allocator = this.arena.?.allocator(); vm.arena = &this.arena.?; - var b = &vm.bundler; + var b = &vm.transpiler; b.configureDefines() catch { this.flushLogs(); @@ -292,12 +292,12 @@ pub const WebWorker = struct { // TODO: we may have to clone other parts of vm state. this will be more // important when implementing vm.deinit() const map = try vm.allocator.create(bun.DotEnv.Map); - map.* = try vm.bundler.env.map.cloneWithAllocator(vm.allocator); + map.* = try vm.transpiler.env.map.cloneWithAllocator(vm.allocator); const loader = try vm.allocator.create(bun.DotEnv.Loader); loader.* = bun.DotEnv.Loader.init(map, vm.allocator); - vm.bundler.env = loader; + vm.transpiler.env = loader; vm.loadExtraEnvAndSourceCodePrinter(); vm.is_main_thread = false; @@ -337,7 +337,7 @@ pub const WebWorker = struct { // Prevent recursion vm.onUnhandledRejection = &JSC.VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue; - const error_instance = error_instance_or_exception.toError() orelse error_instance_or_exception; + var error_instance = error_instance_or_exception.toError() orelse error_instance_or_exception; var array = bun.MutableString.init(bun.default_allocator, 0) catch unreachable; defer array.deinit(); @@ -364,7 +364,13 @@ pub const WebWorker = struct { .flush = false, .max_depth = 32, }, - ); + ) catch |err| { + switch (err) { + error.JSError => {}, + error.OutOfMemory => globalObject.throwOutOfMemory() catch {}, + } + error_instance = globalObject.tryTakeException().?; + }; buffered_writer.flush() catch { bun.outOfMemory(); }; @@ -513,15 +519,18 @@ pub const WebWorker = struct { if (loop) |loop_| { loop_.internal_loop_data.jsc_vm = null; } + bun.uws.onThreadExit(); this.deinit(); if (vm_to_deinit) |vm| { vm.deinit(); // NOTE: deinit here isn't implemented, so freeing workers will leak the vm. } + bun.deleteAllPoolsForThreadExit(); if (arena) |*arena_| { arena_.deinit(); } + bun.exitThread(); } diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index 3484590697..36446d2dce 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -2,6 +2,8 @@ pub usingnamespace @import("./webcore/response.zig"); pub usingnamespace @import("./webcore/encoding.zig"); pub usingnamespace @import("./webcore/streams.zig"); pub usingnamespace @import("./webcore/blob.zig"); +pub usingnamespace @import("./webcore/S3Stat.zig"); +pub usingnamespace @import("./webcore/S3Client.zig"); pub usingnamespace @import("./webcore/request.zig"); pub usingnamespace @import("./webcore/body.zig"); pub const ObjectURLRegistry = @import("./webcore/ObjectURLRegistry.zig"); @@ -265,8 +267,7 @@ pub const Prompt = struct { // unset `ENABLE_VIRTUAL_TERMINAL_INPUT` on windows. This prevents backspace from // deleting the entire line const original_mode: if (Environment.isWindows) ?bun.windows.DWORD else void = if (comptime Environment.isWindows) - bun.win32.unsetStdioModeFlags(0, bun.windows.ENABLE_VIRTUAL_TERMINAL_INPUT) catch null - else {}; + bun.win32.unsetStdioModeFlags(0, bun.windows.ENABLE_VIRTUAL_TERMINAL_INPUT) catch null; defer if (comptime Environment.isWindows) { if (original_mode) |mode| { @@ -633,13 +634,12 @@ pub const Crypto = struct { globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) bun.JSError!JSC.JSValue { - const str, var bytes = bun.String.createUninitialized(.latin1, 36); - defer str.deref(); + var str, var bytes = bun.String.createUninitialized(.latin1, 36); const uuid = globalThis.bunVM().rareData().nextUUID(); uuid.print(bytes[0..36]); - return str.toJS(globalThis); + return str.transferToJS(globalThis); } comptime { @@ -714,7 +714,7 @@ pub const Crypto = struct { } pub fn constructor(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*Crypto { - return globalThis.throw("Crypto is not constructable", .{}); + return JSC.Error.ERR_ILLEGAL_CONSTRUCTOR.throw(globalThis, "Crypto is not constructable", .{}); } pub export fn CryptoObject__create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { diff --git a/src/bun.js/webcore/ObjectURLRegistry.zig b/src/bun.js/webcore/ObjectURLRegistry.zig index 846c1c0434..5bf2ed7803 100644 --- a/src/bun.js/webcore/ObjectURLRegistry.zig +++ b/src/bun.js/webcore/ObjectURLRegistry.zig @@ -5,7 +5,7 @@ const UUID = bun.UUID; const assert = bun.assert; const ObjectURLRegistry = @This(); -lock: bun.Lock = .{}, +lock: bun.Mutex = .{}, map: std.AutoHashMap(UUID, *RegistryEntry) = std.AutoHashMap(UUID, *RegistryEntry).init(bun.default_allocator), pub const RegistryEntry = struct { diff --git a/src/bun.js/webcore/S3Client.zig b/src/bun.js/webcore/S3Client.zig new file mode 100644 index 0000000000..37b1799cb4 --- /dev/null +++ b/src/bun.js/webcore/S3Client.zig @@ -0,0 +1,298 @@ +const bun = @import("root").bun; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const Blob = JSC.WebCore.Blob; +const PathOrBlob = JSC.Node.PathOrBlob; +const ZigString = JSC.ZigString; +const Method = bun.http.Method; +const S3File = @import("./S3File.zig"); +const S3Credentials = bun.S3.S3Credentials; + +pub fn writeFormatCredentials(credentials: *S3Credentials, options: bun.S3.MultiPartUploadOptions, acl: ?bun.S3.ACL, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll("\n"); + + { + const Writer = @TypeOf(writer); + + formatter.indent += 1; + defer formatter.indent -|= 1; + + const endpoint = if (credentials.endpoint.len > 0) credentials.endpoint else "https://s3..amazonaws.com"; + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("endpoint: \"", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{endpoint}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + const region = if (credentials.region.len > 0) credentials.region else S3Credentials.guessRegion(credentials.endpoint); + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("region: \"", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{region}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + // PS: We don't want to print the credentials if they are empty just signal that they are there without revealing them + if (credentials.accessKeyId.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("accessKeyId: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (credentials.secretAccessKey.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("secretAccessKey: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (credentials.sessionToken.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("sessionToken: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (acl) |acl_value| { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("acl: ", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{acl_value.toString()}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("partSize: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.partSize), .NumberObject, enable_ansi_colors); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("queueSize: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.queueSize), .NumberObject, enable_ansi_colors); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("retry: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.retry), .NumberObject, enable_ansi_colors); + try writer.writeAll("\n"); + } +} + +pub const S3Client = struct { + const log = bun.Output.scoped(.S3Client, false); + pub usingnamespace JSC.Codegen.JSS3Client; + + pub usingnamespace bun.New(@This()); + credentials: *S3Credentials, + options: bun.S3.MultiPartUploadOptions = .{}, + acl: ?bun.S3.ACL = null, + + pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*@This() { + const arguments = callframe.arguments_old(1).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + var aws_options = try S3Credentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getS3Credentials(), .{}, args.nextEat(), null, globalThis); + defer aws_options.deinit(); + return S3Client.new(.{ + .credentials = aws_options.credentials.dupe(), + .options = aws_options.options, + .acl = aws_options.acl, + }); + } + + pub fn writeFormat(this: *@This(), comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll(comptime bun.Output.prettyFmt("S3Client", enable_ansi_colors)); + if (this.credentials.bucket.len > 0) { + try writer.print( + comptime bun.Output.prettyFmt(" (\"{s}\") {{", enable_ansi_colors), + .{ + this.credentials.bucket, + }, + ); + } else { + try writer.writeAll(comptime bun.Output.prettyFmt(" {{", enable_ansi_colors)); + } + + try writeFormatCredentials(this.credentials, this.options, this.acl, Formatter, formatter, writer, enable_ansi_colors); + try formatter.writeIndent(@TypeOf(writer), writer); + try writer.writeAll("}"); + formatter.resetLine(); + } + pub fn file(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path ", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl)); + blob.allocator = bun.default_allocator; + return blob.toJS(globalThis); + } + + pub fn presign(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to presign", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to presign", .{}); + }; + errdefer path.deinit(); + + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.getPresignUrlFrom(&blob, globalThis, options); + } + + pub fn exists(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check if it exists", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check if it exists", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.exists(globalThis, &blob); + } + + pub fn size(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check the size of", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check the size of", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.size(globalThis, &blob); + } + + pub fn stat(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check the stat of", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check the stat of", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.stat(globalThis, &blob); + } + + pub fn write(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.ERR_MISSING_ARGS("Expected a path to write to", .{}).throw(); + }; + errdefer path.deinit(); + const data = args.nextEat() orelse { + return globalThis.ERR_MISSING_ARGS("Expected a Blob-y thing to write", .{}).throw(); + }; + + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + var blob_internal: PathOrBlob = .{ .blob = blob }; + return Blob.writeFileInternal(globalThis, &blob_internal, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = options, + }); + } + + pub fn unlink(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.ERR_MISSING_ARGS("Expected a path to unlink", .{}).throw(); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); + } + + pub fn deinit(this: *@This()) void { + this.credentials.deref(); + this.destroy(); + } + + pub fn finalize( + this: *@This(), + ) void { + this.deinit(); + } + + // Static methods + + pub fn staticWrite(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.write(globalThis, callframe); + } + + pub fn staticPresign(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.presign(globalThis, callframe); + } + + pub fn staticExists(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.exists(globalThis, callframe); + } + + pub fn staticSize(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.size(globalThis, callframe); + } + + pub fn staticUnlink(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.unlink(globalThis, callframe); + } + + pub fn staticFile(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + const path = (try JSC.Node.PathLike.fromJS(globalThis, &args)) orelse { + return globalThis.throwInvalidArguments("Expected file path string", .{}); + }; + + return try S3File.constructInternalJS(globalThis, path, args.nextEat()); + } + pub fn staticStat(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.stat(globalThis, callframe); + } +}; diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig new file mode 100644 index 0000000000..7b02077244 --- /dev/null +++ b/src/bun.js/webcore/S3File.zig @@ -0,0 +1,616 @@ +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const Blob = JSC.WebCore.Blob; +const PathOrBlob = JSC.Node.PathOrBlob; +const ZigString = JSC.ZigString; +const Method = bun.http.Method; +const strings = bun.strings; +const Output = bun.Output; +const S3Client = @import("./S3Client.zig"); +const S3 = bun.S3; +const S3Stat = @import("./S3Stat.zig").S3Stat; +pub fn writeFormat(s3: *Blob.S3Store, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll(comptime Output.prettyFmt("S3Ref", enable_ansi_colors)); + const credentials = s3.getCredentials(); + + if (credentials.bucket.len > 0) { + try writer.print( + comptime Output.prettyFmt(" (\"{s}/{s}\") {{", enable_ansi_colors), + .{ + credentials.bucket, + s3.path(), + }, + ); + } else { + try writer.print( + comptime Output.prettyFmt(" (\"{s}\") {{", enable_ansi_colors), + .{ + s3.path(), + }, + ); + } + + try S3Client.writeFormatCredentials(credentials, s3.options, s3.acl, Formatter, formatter, writer, enable_ansi_colors); + try formatter.writeIndent(@TypeOf(writer), writer); + try writer.writeAll("}"); + formatter.resetLine(); +} +pub fn presign(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to presign", .{}); + } + + switch (path_or_blob) { + .path => |path| { + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to presign", .{}); + } + const options = args.nextEat(); + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + return try getPresignUrlFrom(&blob, globalThis, options); + }, + .blob => return try getPresignUrlFrom(&path_or_blob.blob, globalThis, args.nextEat()), + } +} + +pub fn unlink(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to delete", .{}); + } + + switch (path_or_blob) { + .path => |path| { + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to delete", .{}); + } + const options = args.nextEat(); + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + return try blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); + }, + .blob => |blob| { + return try blob.store.?.data.s3.unlink(blob.store.?, globalThis, args.nextEat()); + }, + } +} + +pub fn write(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to upload", .{}); + } + + const data = args.nextEat() orelse { + return globalThis.ERR_MISSING_ARGS("Expected a Blob-y thing to upload", .{}).throw(); + }; + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to upload", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + var blob_internal: PathOrBlob = .{ .blob = blob }; + return try Blob.writeFileInternal(globalThis, &blob_internal, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = options, + }); + }, + .blob => return try Blob.writeFileInternal(globalThis, &path_or_blob, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = args.nextEat(), + }), + } +} + +pub fn size(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + return S3BlobStatTask.size(globalThis, &blob); + }, + .blob => |*blob| { + return Blob.getSize(blob, globalThis); + }, + } +} +pub fn exists(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to check if it exists", .{}); + } + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to check if it exists", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + return S3BlobStatTask.exists(globalThis, &blob); + }, + .blob => |*blob| { + return Blob.getExists(blob, globalThis, callframe); + }, + } +} + +fn constructS3FileInternalStore( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, +) bun.JSError!Blob { + // get credentials from env + const existing_credentials = globalObject.bunVM().transpiler.env.getS3Credentials(); + return constructS3FileWithS3Credentials(globalObject, path, options, existing_credentials); +} +/// if the credentials have changed, we need to clone it, if not we can just ref/deref it +pub fn constructS3FileWithS3CredentialsAndOptions( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, + default_credentials: *S3.S3Credentials, + default_options: bun.S3.MultiPartUploadOptions, + default_acl: ?bun.S3.ACL, +) bun.JSError!Blob { + var aws_options = try S3.S3Credentials.getCredentialsWithOptions(default_credentials.*, default_options, options, default_acl, globalObject); + defer aws_options.deinit(); + + const store = brk: { + if (aws_options.changed_credentials) { + break :brk Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory(); + } else { + break :brk Blob.Store.initS3WithReferencedCredentials(path, null, default_credentials, bun.default_allocator) catch bun.outOfMemory(); + } + }; + errdefer store.deinit(); + store.data.s3.options = aws_options.options; + store.data.s3.acl = aws_options.acl; + var blob = Blob.initWithStore(store, globalObject); + if (options) |opts| { + if (opts.isObject()) { + if (try opts.getTruthyComptime(globalObject, "type")) |file_type| { + inner: { + if (file_type.isString()) { + var allocator = bun.default_allocator; + var str = file_type.toSlice(globalObject, bun.default_allocator); + defer str.deinit(); + const slice = str.slice(); + if (!strings.isAllASCII(slice)) { + break :inner; + } + blob.content_type_was_set = true; + if (globalObject.bunVM().mimeType(str.slice())) |entry| { + blob.content_type = entry.value; + break :inner; + } + const content_type_buf = allocator.alloc(u8, slice.len) catch bun.outOfMemory(); + blob.content_type = strings.copyLowercase(slice, content_type_buf); + blob.content_type_allocated = true; + } + } + } + } + } + return blob; +} + +pub fn constructS3FileWithS3Credentials( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, + existing_credentials: S3.S3Credentials, +) bun.JSError!Blob { + var aws_options = try S3.S3Credentials.getCredentialsWithOptions(existing_credentials, .{}, options, null, globalObject); + defer aws_options.deinit(); + const store = Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory(); + errdefer store.deinit(); + store.data.s3.options = aws_options.options; + store.data.s3.acl = aws_options.acl; + var blob = Blob.initWithStore(store, globalObject); + if (options) |opts| { + if (opts.isObject()) { + if (try opts.getTruthyComptime(globalObject, "type")) |file_type| { + inner: { + if (file_type.isString()) { + var allocator = bun.default_allocator; + var str = file_type.toSlice(globalObject, bun.default_allocator); + defer str.deinit(); + const slice = str.slice(); + if (!strings.isAllASCII(slice)) { + break :inner; + } + blob.content_type_was_set = true; + if (globalObject.bunVM().mimeType(str.slice())) |entry| { + blob.content_type = entry.value; + break :inner; + } + const content_type_buf = allocator.alloc(u8, slice.len) catch bun.outOfMemory(); + blob.content_type = strings.copyLowercase(slice, content_type_buf); + blob.content_type_allocated = true; + } + } + } + } + } + return blob; +} +fn constructS3FileInternal( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, +) bun.JSError!*Blob { + var ptr = Blob.new(try constructS3FileInternalStore(globalObject, path, options)); + ptr.allocator = bun.default_allocator; + return ptr; +} + +pub const S3BlobStatTask = struct { + promise: JSC.JSPromise.Strong, + store: *Blob.Store, + usingnamespace bun.New(S3BlobStatTask); + + pub fn onS3ExistsResolved(result: S3.S3StatResult, this: *S3BlobStatTask) void { + defer this.deinit(); + const globalThis = this.promise.globalObject().?; + switch (result) { + .not_found => { + this.promise.resolve(globalThis, .false); + }, + .success => |_| { + // calling .exists() should not prevent it to download a bigger file + // this would make it download a slice of the actual value, if the file changes before we download it + // if (this.blob.size == Blob.max_size) { + // this.blob.size = @truncate(stat.size); + // } + this.promise.resolve(globalThis, .true); + }, + .failure => |err| { + this.promise.reject(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + }, + } + } + + pub fn onS3SizeResolved(result: S3.S3StatResult, this: *S3BlobStatTask) void { + defer this.deinit(); + const globalThis = this.promise.globalObject().?; + + switch (result) { + .success => |stat_result| { + this.promise.resolve(globalThis, JSValue.jsNumber(stat_result.size)); + }, + inline .not_found, .failure => |err| { + this.promise.reject(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + }, + } + } + + pub fn onS3StatResolved(result: S3.S3StatResult, this: *S3BlobStatTask) void { + defer this.deinit(); + const globalThis = this.promise.globalObject().?; + switch (result) { + .success => |stat_result| { + this.promise.resolve(globalThis, S3Stat.init( + stat_result.size, + stat_result.etag, + stat_result.contentType, + stat_result.lastModified, + globalThis, + ).toJS(globalThis)); + }, + inline .not_found, .failure => |err| { + this.promise.reject(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + }, + } + } + + pub fn exists(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { + const this = S3BlobStatTask.new(.{ + .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, + }); + this.store.ref(); + const promise = this.promise.value(); + const credentials = blob.store.?.data.s3.getCredentials(); + const path = blob.store.?.data.s3.path(); + const env = globalThis.bunVM().transpiler.env; + + S3.stat(credentials, path, @ptrCast(&S3BlobStatTask.onS3ExistsResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + return promise; + } + pub fn stat(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { + const this = S3BlobStatTask.new(.{ + .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, + }); + this.store.ref(); + const promise = this.promise.value(); + const credentials = blob.store.?.data.s3.getCredentials(); + const path = blob.store.?.data.s3.path(); + const env = globalThis.bunVM().transpiler.env; + + S3.stat(credentials, path, @ptrCast(&S3BlobStatTask.onS3StatResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + return promise; + } + pub fn size(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { + const this = S3BlobStatTask.new(.{ + .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, + }); + this.store.ref(); + const promise = this.promise.value(); + const credentials = blob.store.?.data.s3.getCredentials(); + const path = blob.store.?.data.s3.path(); + const env = globalThis.bunVM().transpiler.env; + + S3.stat(credentials, path, @ptrCast(&S3BlobStatTask.onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + return promise; + } + + pub fn deinit(this: *S3BlobStatTask) void { + this.store.deref(); + this.promise.deinit(); + this.destroy(); + } +}; + +pub fn getPresignUrlFrom(this: *Blob, globalThis: *JSC.JSGlobalObject, extra_options: ?JSValue) bun.JSError!JSValue { + if (!this.isS3()) { + return globalThis.ERR_INVALID_THIS("presign is only possible for s3:// files", .{}).throw(); + } + + var method: bun.http.Method = .GET; + var expires: usize = 86400; // 1 day default + + var credentialsWithOptions: S3.S3CredentialsWithOptions = .{ + .credentials = this.store.?.data.s3.getCredentials().*, + }; + defer { + credentialsWithOptions.deinit(); + } + const s3 = &this.store.?.data.s3; + + if (extra_options) |options| { + if (options.isObject()) { + if (try options.getTruthyComptime(globalThis, "method")) |method_| { + method = Method.fromJS(globalThis, method_) orelse { + return globalThis.throwInvalidArguments("method must be GET, PUT, DELETE or HEAD when using s3 protocol", .{}); + }; + } + if (try options.getOptional(globalThis, "expiresIn", i32)) |expires_| { + if (expires_ <= 0) return globalThis.throwInvalidArguments("expiresIn must be greather than 0", .{}); + expires = @intCast(expires_); + } + } + credentialsWithOptions = try s3.getCredentialsWithOptions(options, globalThis); + } + const path = s3.path(); + + const result = credentialsWithOptions.credentials.signRequest(.{ + .path = path, + .method = method, + .acl = credentialsWithOptions.acl, + }, .{ .expires = expires }) catch |sign_err| { + return S3.throwSignError(sign_err, globalThis); + }; + defer result.deinit(); + var str = bun.String.fromUTF8(result.url); + return str.transferToJS(this.globalThis); +} +pub fn getBucketName( + this: *const Blob, +) ?[]const u8 { + const store = this.store orelse return null; + if (store.data != .s3) return null; + const credentials = store.data.s3.getCredentials(); + var full_path = store.data.s3.path(); + if (strings.startsWith(full_path, "/")) { + full_path = full_path[1..]; + } + var bucket: []const u8 = credentials.bucket; + + if (bucket.len == 0) { + if (strings.indexOf(full_path, "/")) |end| { + bucket = full_path[0..end]; + if (bucket.len > 0) { + return bucket; + } + } + return null; + } + return bucket; +} + +pub fn getBucket( + this: *Blob, + globalThis: *JSC.JSGlobalObject, +) callconv(JSC.conv) JSValue { + if (getBucketName(this)) |name| { + return bun.String.createUTF8ForJS(globalThis, name); + } + return .undefined; +} +pub fn getPresignUrl(this: *Blob, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const args = callframe.arguments_old(1); + return getPresignUrlFrom(this, globalThis, if (args.len > 0) args.ptr[0] else null); +} + +pub fn getStat(this: *Blob, globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(JSC.conv) JSValue { + return S3BlobStatTask.stat(globalThis, this); +} + +pub fn stat(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + return S3BlobStatTask.stat(globalThis, &blob); + }, + .blob => |*blob| { + return S3BlobStatTask.stat(globalThis, blob); + }, + } +} + +pub fn constructInternalJS( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, +) bun.JSError!JSValue { + const blob = try constructS3FileInternal(globalObject, path, options); + return blob.toJS(globalObject); +} + +pub fn toJSUnchecked( + globalObject: *JSC.JSGlobalObject, + this: *Blob, +) JSValue { + return BUN__createJSS3FileUnsafely(globalObject, this); +} + +pub fn constructInternal( + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!*Blob { + const vm = globalObject.bunVM(); + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(vm, arguments); + defer args.deinit(); + + const path = (try JSC.Node.PathLike.fromJS(globalObject, &args)) orelse { + return globalObject.throwInvalidArguments("Expected file path string", .{}); + }; + return constructS3FileInternal(globalObject, path, args.nextEat()); +} + +pub fn construct( + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) callconv(JSC.conv) ?*Blob { + return constructInternal(globalObject, callframe) catch |err| switch (err) { + error.JSError => null, + error.OutOfMemory => { + _ = globalObject.throwOutOfMemoryValue(); + return null; + }, + }; +} +pub fn hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(JSC.conv) bool { + JSC.markBinding(@src()); + const blob = value.as(Blob) orelse return false; + return blob.isS3(); +} + +comptime { + @export(exports.JSS3File__presign, .{ .name = "JSS3File__presign" }); + @export(construct, .{ .name = "JSS3File__construct" }); + @export(hasInstance, .{ .name = "JSS3File__hasInstance" }); + @export(getBucket, .{ .name = "JSS3File__bucket" }); + @export(getStat, .{ .name = "JSS3File__stat" }); +} + +pub const exports = struct { + pub const JSS3File__presign = JSC.toJSHostFunctionWithContext(Blob, getPresignUrl); + pub const JSS3File__stat = JSC.toJSHostFunctionWithContext(Blob, getStat); +}; +extern fn BUN__createJSS3File(*JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSValue; +extern fn BUN__createJSS3FileUnsafely(*JSC.JSGlobalObject, *Blob) callconv(JSC.conv) JSValue; +pub fn createJSS3File(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSValue { + return BUN__createJSS3File(globalObject, callframe); +} diff --git a/src/bun.js/webcore/S3Stat.zig b/src/bun.js/webcore/S3Stat.zig new file mode 100644 index 0000000000..53deb25bcb --- /dev/null +++ b/src/bun.js/webcore/S3Stat.zig @@ -0,0 +1,58 @@ +const bun = @import("../../bun.zig"); +const JSC = @import("../../jsc.zig"); + +pub const S3Stat = struct { + const log = bun.Output.scoped(.S3Stat, false); + pub usingnamespace JSC.Codegen.JSS3Stat; + pub usingnamespace bun.New(@This()); + + size: u64, + etag: bun.String, + contentType: bun.String, + lastModified: f64, + + pub fn constructor(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*@This() { + return globalThis.throwInvalidArguments("S3Stat is not constructable", .{}); + } + + pub fn init( + size: u64, + etag: []const u8, + contentType: []const u8, + lastModified: []const u8, + globalThis: *JSC.JSGlobalObject, + ) *@This() { + var date_str = bun.String.init(lastModified); + defer date_str.deref(); + const last_modified = date_str.parseDate(globalThis); + + return S3Stat.new(.{ + .size = size, + .etag = bun.String.createUTF8(etag), + .contentType = bun.String.createUTF8(contentType), + .lastModified = last_modified, + }); + } + + pub fn getSize(this: *@This(), _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsNumber(this.size); + } + + pub fn getEtag(this: *@This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return this.etag.toJS(globalObject); + } + + pub fn getContentType(this: *@This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return this.contentType.toJS(globalObject); + } + + pub fn getLastModified(this: *@This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.fromDateNumber(globalObject, this.lastModified); + } + + pub fn finalize(this: *@This()) void { + this.etag.deref(); + this.contentType.deref(); + this.destroy(); + } +}; diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 9c1467bf5d..cc75cc0eeb 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -19,7 +19,6 @@ const default_allocator = bun.default_allocator; const FeatureFlags = bun.FeatureFlags; const ArrayBuffer = @import("../base.zig").ArrayBuffer; const Properties = @import("../base.zig").Properties; - const getAllocator = @import("../base.zig").getAllocator; const Environment = @import("../../env.zig"); @@ -44,29 +43,9 @@ const Request = JSC.WebCore.Request; const libuv = bun.windows.libuv; -const PathOrBlob = union(enum) { - path: JSC.Node.PathOrFileDescriptor, - blob: Blob, - - pub fn fromJSNoCopy(ctx: js.JSContextRef, args: *JSC.Node.ArgumentsSlice) bun.JSError!PathOrBlob { - if (try JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, bun.default_allocator)) |path| { - return PathOrBlob{ - .path = path, - }; - } - - const arg = args.nextEat() orelse { - return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", .undefined); - }; - if (arg.as(Blob)) |blob| { - return PathOrBlob{ - .blob = blob.*, - }; - } - return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", arg); - } -}; - +const S3 = bun.S3; +const S3Credentials = S3.S3Credentials; +const PathOrBlob = JSC.Node.PathOrBlob; const WriteFilePromise = @import("./blob/WriteFile.zig").WriteFilePromise; const WriteFileWaitFromLockedValueTask = @import("./blob/WriteFile.zig").WriteFileWaitFromLockedValueTask; const NewReadFileHandler = @import("./blob/ReadFile.zig").NewReadFileHandler; @@ -74,6 +53,8 @@ const WriteFile = @import("./blob/WriteFile.zig").WriteFile; const ReadFile = @import("./blob/ReadFile.zig").ReadFile; const WriteFileWindows = @import("./blob/WriteFile.zig").WriteFileWindows; +const S3File = @import("./S3File.zig"); + pub const Blob = struct { const bloblog = Output.scoped(.Blob, false); @@ -134,7 +115,25 @@ pub const Blob = struct { } pub fn hasContentTypeFromUser(this: *const Blob) bool { - return this.content_type_was_set or (this.store != null and this.store.?.data == .file); + return this.content_type_was_set or (this.store != null and (this.store.?.data == .file or this.store.?.data == .s3)); + } + + pub fn contentTypeOrMimeType(this: *const Blob) ?[]const u8 { + if (this.content_type.len > 0) { + return this.content_type; + } + if (this.store) |store| { + switch (store.data) { + .file => |file| { + return file.mime_type.value; + }, + .s3 => |s3| { + return s3.mime_type.value; + }, + else => return null, + } + } + return null; } pub fn isBunFile(this: *const Blob) bool { @@ -144,7 +143,16 @@ pub const Blob = struct { } const ReadFileUV = @import("./blob/ReadFile.zig").ReadFileUV; + pub fn doReadFromS3(this: *Blob, comptime Function: anytype, global: *JSGlobalObject) JSValue { + bloblog("doReadFromS3", .{}); + const WrappedFn = struct { + pub fn wrapped(b: *Blob, g: *JSGlobalObject, by: []u8) JSC.JSValue { + return JSC.toJSHostValue(g, Function(b, g, by, .clone)); + } + }; + return S3BlobDownloadTask.init(global, this, WrappedFn.wrapped); + } pub fn doReadFile(this: *Blob, comptime Function: anytype, global: *JSGlobalObject) JSValue { bloblog("doReadFile", .{}); @@ -263,6 +271,10 @@ pub const Blob = struct { blob.resolveSize(); } switch (store.data) { + .s3 => |_| { + // TODO: s3 + // we need to make this async and use download/downloadSlice + }, .file => |file| { // TODO: make this async + lazy @@ -464,6 +476,7 @@ pub const Blob = struct { const blob = Blob.new(Blob.findOrCreateFileFromPath( &path_or_fd, globalThis, + true, )); break :file blob; @@ -480,6 +493,7 @@ pub const Blob = struct { const blob = Blob.new(Blob.findOrCreateFileFromPath( &dest, globalThis, + true, )); break :file blob; @@ -682,6 +696,9 @@ pub const Blob = struct { { const store = this.store.?; switch (store.data) { + .s3 => |*s3| { + try S3File.writeFormat(s3, Formatter, formatter, writer, enable_ansi_colors); + }, .file => |file| { try writer.writeAll(comptime Output.prettyFmt("FileRef", enable_ansi_colors)); switch (file.pathlike) { @@ -813,7 +830,6 @@ pub const Blob = struct { .recursive = true, .always_return_none = true, }, - .sync, )) { .result => { this.mkdirp_if_not_exists = false; @@ -839,7 +855,7 @@ pub const Blob = struct { ctx: JSC.C.JSContextRef, source_blob: *Blob, destination_blob: *Blob, - mkdirp_if_not_exists: bool, + options: WriteFileOptions, ) JSC.JSValue { const destination_type = std.meta.activeTag(destination_blob.store.?.data); @@ -867,6 +883,58 @@ pub const Blob = struct { return JSC.JSPromise.rejectedPromiseValue(ctx, result.toJS(ctx)); } + } else if (destination_type == .s3) { + + // create empty file + const s3 = &destination_blob.store.?.data.s3; + var aws_options = s3.getCredentialsWithOptions(options.extra_options, ctx) catch |err| { + return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.takeException(err)); + }; + defer aws_options.deinit(); + + const Wrapper = struct { + promise: JSC.JSPromise.Strong, + store: *Store, + pub usingnamespace bun.New(@This()); + + pub fn resolve(result: S3.S3UploadResult, this: *@This()) void { + if (this.promise.globalObject()) |globalObject| { + switch (result) { + .success => this.promise.resolve(globalObject, JSC.jsNumber(0)), + .failure => |err| { + this.promise.reject(globalObject, err.toJS(globalObject, this.store.getPath())); + }, + } + } + this.deinit(); + } + + fn deinit(this: *@This()) void { + this.promise.deinit(); + this.store.deref(); + this.destroy(); + } + }; + + const promise = JSC.JSPromise.Strong.init(ctx); + const promise_value = promise.value(); + const proxy = ctx.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy_url = if (proxy) |p| p.href else null; + destination_blob.store.?.ref(); + S3.upload( + &aws_options.credentials, + s3.path(), + "", + destination_blob.contentTypeOrMimeType(), + aws_options.acl, + proxy_url, + @ptrCast(&Wrapper.resolve), + Wrapper.new(.{ + .promise = promise, + .store = destination_blob.store.?, + }), + ); + return promise_value; } return JSC.JSPromise.resolvedPromiseValue(ctx, JSC.JSValue.jsNumber(0)); @@ -891,7 +959,7 @@ pub const Blob = struct { *WriteFilePromise, write_file_promise, &WriteFilePromise.run, - mkdirp_if_not_exists, + options.mkdirp_if_not_exists orelse true, ); return promise_value; } @@ -902,7 +970,7 @@ pub const Blob = struct { *WriteFilePromise, write_file_promise, WriteFilePromise.run, - mkdirp_if_not_exists, + options.mkdirp_if_not_exists orelse true, ) catch unreachable; var task = WriteFile.WriteFileTask.createOnJSThread(bun.default_allocator, ctx, file_copier) catch bun.outOfMemory(); // Defer promise creation until we're just about to schedule the task @@ -920,7 +988,7 @@ pub const Blob = struct { destination_blob.store.?, source_blob.store.?, ctx.bunVM().eventLoop(), - mkdirp_if_not_exists, + options.mkdirp_if_not_exists orelse true, destination_blob.size, ); } @@ -932,10 +1000,21 @@ pub const Blob = struct { destination_blob.offset, destination_blob.size, ctx, - mkdirp_if_not_exists, + options.mkdirp_if_not_exists orelse true, ) catch unreachable; file_copier.schedule(); return file_copier.promise.value(); + } else if (destination_type == .file and source_type == .s3) { + const s3 = &source_blob.store.?.data.s3; + if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( + ctx, + source_blob, + @truncate(s3.options.partSize), + ), ctx)) |stream| { + return destination_blob.pipeReadableStreamToBlob(ctx, stream, options.extra_options); + } else { + return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.createErrorInstance("Failed to stream bytes from s3 bucket", .{})); + } } else if (destination_type == .bytes and source_type == .bytes) { // If this is bytes <> bytes, we can just duplicate it // this is an edgecase @@ -946,41 +1025,126 @@ pub const Blob = struct { const cloned = Blob.new(clone); cloned.allocator = bun.default_allocator; return JSPromise.resolvedPromiseValue(ctx, cloned.toJS(ctx)); - } else if (destination_type == .bytes and source_type == .file) { - var fake_call_frame: [8]JSC.JSValue = undefined; - @memset(@as([*]u8, @ptrCast(&fake_call_frame))[0..@sizeOf(@TypeOf(fake_call_frame))], 0); - const blob_value = source_blob.getSlice(ctx, @as(*JSC.CallFrame, @ptrCast(&fake_call_frame))) catch .zero; // TODO: + } else if (destination_type == .bytes and (source_type == .file or source_type == .s3)) { + const blob_value = source_blob.getSliceFrom(ctx, 0, 0, "", false); return JSPromise.resolvedPromiseValue( ctx, blob_value, ); + } else if (destination_type == .s3) { + const s3 = &destination_blob.store.?.data.s3; + var aws_options = s3.getCredentialsWithOptions(options.extra_options, ctx) catch |err| { + return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.takeException(err)); + }; + defer aws_options.deinit(); + const store = source_blob.store.?; + const proxy = ctx.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy_url = if (proxy) |p| p.href else null; + switch (store.data) { + .bytes => |bytes| { + if (bytes.len > S3.MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE) { + if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( + ctx, + source_blob, + @truncate(s3.options.partSize), + ), ctx)) |stream| { + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + stream, + ctx, + aws_options.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); + } else { + return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.createErrorInstance("Failed to stream bytes to s3 bucket", .{})); + } + } else { + const Wrapper = struct { + store: *Store, + promise: JSC.JSPromise.Strong, + pub usingnamespace bun.New(@This()); + + pub fn resolve(result: S3.S3UploadResult, this: *@This()) void { + if (this.promise.globalObject()) |globalObject| { + switch (result) { + .success => this.promise.resolve(globalObject, JSC.jsNumber(this.store.data.bytes.len)), + .failure => |err| { + this.promise.reject(globalObject, err.toJS(globalObject, this.store.getPath())); + }, + } + } + this.deinit(); + } + + fn deinit(this: *@This()) void { + this.promise.deinit(); + this.store.deref(); + } + }; + store.ref(); + const promise = JSC.JSPromise.Strong.init(ctx); + const promise_value = promise.value(); + + S3.upload( + &aws_options.credentials, + s3.path(), + bytes.slice(), + destination_blob.contentTypeOrMimeType(), + aws_options.acl, + proxy_url, + @ptrCast(&Wrapper.resolve), + Wrapper.new(.{ + .store = store, + .promise = promise, + }), + ); + return promise_value; + } + }, + .file, .s3 => { + // stream + if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( + ctx, + source_blob, + @truncate(s3.options.partSize), + ), ctx)) |stream| { + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + stream, + ctx, + s3.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); + } else { + return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.createErrorInstance("Failed to stream bytes to s3 bucket", .{})); + } + }, + } } unreachable; } - pub fn writeFile(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(3).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - - // accept a path or a blob - var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); - defer { - if (path_or_blob == .path) { - path_or_blob.path.deinit(); - } - } - - var data = args.nextEat() orelse { - return globalThis.throwInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}); - }; - + const WriteFileOptions = struct { + mkdirp_if_not_exists: ?bool = null, + extra_options: ?JSValue = null, + }; + pub fn writeFileInternal(globalThis: *JSC.JSGlobalObject, path_or_blob_: *PathOrBlob, data: JSC.JSValue, options: WriteFileOptions) bun.JSError!JSC.JSValue { if (data.isEmptyOrUndefinedOrNull()) { return globalThis.throwInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}); } - + var path_or_blob = path_or_blob_.*; if (path_or_blob == .blob) { if (path_or_blob.blob.store == null) { return globalThis.throwInvalidArguments("Blob is detached", .{}); @@ -1000,22 +1164,7 @@ pub const Blob = struct { var needs_async = false; - var mkdirp_if_not_exists: ?bool = null; - - if (args.nextEat()) |options_object| { - if (options_object.isObject()) { - if (try options_object.getTruthy(globalThis, "createPath")) |create_directory| { - if (!create_directory.isBoolean()) { - return globalThis.throwInvalidArgumentType("write", "options.createPath", "boolean"); - } - mkdirp_if_not_exists = create_directory.toBoolean(); - } - } else if (!options_object.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArgumentType("write", "options", "object"); - } - } - - if (mkdirp_if_not_exists) |mkdir| { + if (options.mkdirp_if_not_exists) |mkdir| { if (mkdir and path_or_blob == .blob and path_or_blob.blob.store != null and @@ -1033,7 +1182,7 @@ pub const Blob = struct { if (comptime !Environment.isWindows) { if (path_or_blob == .path or // If they try to set an offset, its a little more complicated so let's avoid that - (path_or_blob.blob.offset == 0 and + (path_or_blob.blob.offset == 0 and !path_or_blob.blob.isS3() and // Is this a file that is known to be a pipe? Let's avoid blocking the main thread on it. !(path_or_blob.blob.store != null and path_or_blob.blob.store.?.data == .file and @@ -1115,7 +1264,7 @@ pub const Blob = struct { // if path_or_blob is a path, convert it into a file blob var destination_blob: Blob = if (path_or_blob == .path) brk: { - break :brk Blob.findOrCreateFileFromPath(&path_or_blob.path, globalThis); + break :brk Blob.findOrCreateFileFromPath(&path_or_blob_.path, globalThis, true); } else path_or_blob.blob.dupe(); if (destination_blob.store == null) { @@ -1140,12 +1289,41 @@ pub const Blob = struct { _ = response.body.value.use(); return JSC.JSPromise.rejectedPromiseValue(globalThis, err_ref.toJS(globalThis)); }, - .Locked => { + .Locked => |*locked| { + if (destination_blob.isS3()) { + const s3 = &destination_blob.store.?.data.s3; + var aws_options = try s3.getCredentialsWithOptions(options.extra_options, globalThis); + defer aws_options.deinit(); + _ = response.body.value.toReadableStream(globalThis); + if (locked.readable.get()) |readable| { + if (readable.isDisturbed(globalThis)) { + destination_blob.detach(); + return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); + } + const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy_url = if (proxy) |p| p.href else null; + + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + readable, + globalThis, + aws_options.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); + } + destination_blob.detach(); + return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); + } var task = bun.new(WriteFileWaitFromLockedValueTask, .{ .globalThis = globalThis, .file_blob = destination_blob, .promise = JSC.JSPromise.Strong.init(globalThis), - .mkdirp_if_not_exists = mkdirp_if_not_exists orelse true, + .mkdirp_if_not_exists = options.mkdirp_if_not_exists orelse true, }); response.body.value.Locked.task = task; @@ -1171,12 +1349,40 @@ pub const Blob = struct { _ = request.body.value.use(); return JSC.JSPromise.rejectedPromiseValue(globalThis, err_ref.toJS(globalThis)); }, - .Locked => { + .Locked => |locked| { + if (destination_blob.isS3()) { + const s3 = &destination_blob.store.?.data.s3; + var aws_options = try s3.getCredentialsWithOptions(options.extra_options, globalThis); + defer aws_options.deinit(); + _ = request.body.value.toReadableStream(globalThis); + if (locked.readable.get()) |readable| { + if (readable.isDisturbed(globalThis)) { + destination_blob.detach(); + return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); + } + const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy_url = if (proxy) |p| p.href else null; + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + readable, + globalThis, + aws_options.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); + } + destination_blob.detach(); + return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); + } var task = bun.new(WriteFileWaitFromLockedValueTask, .{ .globalThis = globalThis, .file_blob = destination_blob, .promise = JSC.JSPromise.Strong.init(globalThis), - .mkdirp_if_not_exists = mkdirp_if_not_exists orelse true, + .mkdirp_if_not_exists = options.mkdirp_if_not_exists orelse true, }); request.body.value.Locked.task = task; @@ -1212,7 +1418,42 @@ pub const Blob = struct { } } - return writeFileWithSourceDestination(globalThis, &source_blob, &destination_blob, mkdirp_if_not_exists orelse true); + return writeFileWithSourceDestination(globalThis, &source_blob, &destination_blob, options); + } + pub fn writeFile(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + defer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + const data = args.nextEat() orelse { + return globalThis.throwInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}); + }; + var mkdirp_if_not_exists: ?bool = null; + const options = args.nextEat(); + if (options) |options_object| { + if (options_object.isObject()) { + if (try options_object.getTruthy(globalThis, "createPath")) |create_directory| { + if (!create_directory.isBoolean()) { + return globalThis.throwInvalidArgumentType("write", "options.createPath", "boolean"); + } + mkdirp_if_not_exists = create_directory.toBoolean(); + } + } else if (!options_object.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArgumentType("write", "options", "object"); + } + } + return writeFileInternal(globalThis, &path_or_blob, data, .{ + .mkdirp_if_not_exists = mkdirp_if_not_exists, + .extra_options = options, + }); } const write_permissions = 0o664; @@ -1390,13 +1631,6 @@ pub const Blob = struct { return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written)); } - - pub export fn JSDOMFile__hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(JSC.conv) bool { - JSC.markBinding(@src()); - const blob = value.as(Blob) orelse return false; - return blob.is_jsdom_file; - } - export fn JSDOMFile__construct(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) ?*Blob { return JSDOMFile__construct_(globalThis, callframe) catch |err| switch (err) { error.JSError => null, @@ -1431,7 +1665,6 @@ pub const Blob = struct { return globalThis.throwInvalidArguments("new Blob() expects an Array", .{}); }, }; - if (blob.store) |store_| { switch (store_.data) { .bytes => |*bytes| { @@ -1439,7 +1672,7 @@ pub const Blob = struct { (name_value_str.toUTF8WithoutRef(bun.default_allocator).clone(bun.default_allocator) catch unreachable).slice(), ); }, - .file => { + .s3, .file => { blob.name = name_value_str.dupeRef(); }, } @@ -1516,6 +1749,7 @@ pub const Blob = struct { store.data.bytes.len; }, .file => size += store.data.file.pathlike.estimatedSize(), + .s3 => size += store.data.s3.estimatedSize(), } } @@ -1527,9 +1761,7 @@ pub const Blob = struct { } comptime { - if (!JSC.is_bindgen) { - _ = JSDOMFile__hasInstance; - } + _ = JSDOMFile__hasInstance; } pub fn constructBunFile( @@ -1544,13 +1776,18 @@ pub const Blob = struct { var path = (try JSC.Node.PathOrFileDescriptor.fromJS(globalObject, &args, bun.default_allocator)) orelse { return globalObject.throwInvalidArguments("Expected file path string or file descriptor", .{}); }; + const options = if (arguments.len >= 2) arguments[1] else null; + + if (path == .path) { + if (strings.hasPrefixComptime(path.path.slice(), "s3://")) { + return try S3File.constructInternalJS(globalObject, path.path, options); + } + } defer path.deinitAndUnprotect(); - var blob = Blob.findOrCreateFileFromPath(&path, globalObject); - - if (arguments.len >= 2) { - const opts = arguments[1]; + var blob = Blob.findOrCreateFileFromPath(&path, globalObject, false); + if (options) |opts| { if (opts.isObject()) { if (try opts.getTruthy(globalObject, "type")) |file_type| { inner: { @@ -1584,10 +1821,19 @@ pub const Blob = struct { return ptr.toJS(globalObject); } - pub fn findOrCreateFileFromPath(path_or_fd: *JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob { + pub fn findOrCreateFileFromPath(path_or_fd: *JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject, comptime check_s3: bool) Blob { var vm = globalThis.bunVM(); const allocator = bun.default_allocator; - + if (check_s3) { + if (path_or_fd.* == .path) { + if (strings.startsWith(path_or_fd.path.slice(), "s3://")) { + const credentials = globalThis.bunVM().transpiler.env.getS3Credentials(); + const copy = path_or_fd.*; + path_or_fd.* = .{ .path = .{ .string = bun.PathString.empty } }; + return Blob.initWithStore(Blob.Store.initS3(copy.path, null, credentials, allocator) catch bun.outOfMemory(), globalThis); + } + } + } const path: JSC.Node.PathOrFileDescriptor = brk: { switch (path_or_fd.*) { .path => { @@ -1655,10 +1901,26 @@ pub const Blob = struct { pub usingnamespace bun.New(@This()); + pub fn memoryCost(this: *const Store) usize { + return if (this.hasOneRef()) @sizeOf(@This()) + switch (this.data) { + .bytes => this.data.bytes.len, + .file => 0, + .s3 => |s3| s3.estimatedSize(), + } else 0; + } + + pub fn getPath(this: *const Store) ?[]const u8 { + return switch (this.data) { + .bytes => |*bytes| if (bytes.stored_name.len > 0) bytes.stored_name.slice() else null, + .file => |*file| if (file.pathlike == .path) file.pathlike.path.slice() else null, + .s3 => |*s3| s3.pathlike.slice(), + }; + } + pub fn size(this: *const Store) SizeType { return switch (this.data) { .bytes => this.data.bytes.len, - .file => Blob.max_size, + .s3, .file => Blob.max_size, }; } @@ -1667,6 +1929,7 @@ pub const Blob = struct { pub const Data = union(enum) { bytes: ByteStore, file: FileStore, + s3: S3Store, }; pub fn ref(this: *Store) void { @@ -1694,7 +1957,62 @@ pub const Blob = struct { var this = bun.cast(*Store, ptr); this.deref(); } + pub fn initS3WithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *S3Credentials, allocator: std.mem.Allocator) !*Store { + var path = pathlike; + // this actually protects/refs the pathlike + path.toThreadSafe(); + const store = Blob.Store.new(.{ + .data = .{ + .s3 = S3Store.initWithReferencedCredentials( + path, + mime_type orelse brk: { + const sliced = path.slice(); + if (sliced.len > 0) { + var extname = std.fs.path.extension(sliced); + extname = std.mem.trim(u8, extname, "."); + if (http.MimeType.byExtensionNoDefault(extname)) |mime| { + break :brk mime; + } + } + break :brk null; + }, + credentials, + ), + }, + .allocator = allocator, + .ref_count = std.atomic.Value(u32).init(1), + }); + return store; + } + pub fn initS3(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: S3Credentials, allocator: std.mem.Allocator) !*Store { + var path = pathlike; + // this actually protects/refs the pathlike + path.toThreadSafe(); + + const store = Blob.Store.new(.{ + .data = .{ + .s3 = S3Store.init( + path, + mime_type orelse brk: { + const sliced = path.slice(); + if (sliced.len > 0) { + var extname = std.fs.path.extension(sliced); + extname = std.mem.trim(u8, extname, "."); + if (http.MimeType.byExtensionNoDefault(extname)) |mime| { + break :brk mime; + } + } + break :brk null; + }, + credentials, + ), + }, + .allocator = allocator, + .ref_count = std.atomic.Value(u32).init(1), + }); + return store; + } pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?http.MimeType, allocator: std.mem.Allocator) !*Store { const store = Blob.Store.new(.{ .data = .{ @@ -1764,6 +2082,9 @@ pub const Blob = struct { } } }, + .s3 => |*s3| { + s3.deinit(allocator); + }, } this.destroy(); @@ -1792,6 +2113,14 @@ pub const Blob = struct { }, } }, + .s3 => |s3| { + const pathlike_tag: JSC.Node.PathOrFileDescriptor.SerializeTag = .path; + try writer.writeInt(u8, @intFromEnum(pathlike_tag), .little); + + const path_slice = s3.pathlike.slice(); + try writer.writeInt(u32, @as(u32, @truncate(path_slice.len)), .little); + try writer.writeAll(path_slice); + }, .bytes => |bytes| { const slice = bytes.slice(); try writer.writeInt(u32, @truncate(slice.len), .little); @@ -2427,7 +2756,7 @@ pub const Blob = struct { pub fn throw(this: *CopyFileWindows, err: bun.sys.Error) void { const globalThis = this.promise.strong.globalThis.?; const promise = this.promise.swap(); - const err_instance = err.toSystemError().toErrorInstance(globalThis); + const err_instance = err.toJSC(globalThis); var event_loop = this.event_loop; event_loop.enter(); defer event_loop.exit(); @@ -3146,6 +3475,14 @@ pub const Blob = struct { // milliseconds since ECMAScript epoch last_modified: JSC.JSTimeType = JSC.init_timestamp, + pub fn unlink(this: *const FileStore, globalThis: *JSC.JSGlobalObject) JSValue { + return switch (this.pathlike) { + .path => |path_like| JSC.Node.Async.unlink.create(globalThis, undefined, .{ + .path = .{ .encoded_slice = ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator) }, + }, globalThis.bunVM()), + .fd => JSC.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Is not possible to unlink a file descriptor", .{})), + }; + } pub fn isSeekable(this: *const FileStore) ?bool { if (this.seekable) |seekable| { return seekable; @@ -3163,6 +3500,116 @@ pub const Blob = struct { } }; + pub const S3Store = struct { + pathlike: JSC.Node.PathLike, + mime_type: http.MimeType = http.MimeType.other, + credentials: ?*S3Credentials, + options: bun.S3.MultiPartUploadOptions = .{}, + acl: ?S3.ACL = null, + pub fn isSeekable(_: *const @This()) ?bool { + return true; + } + + pub fn getCredentials(this: *const @This()) *S3Credentials { + bun.assert(this.credentials != null); + return this.credentials.?; + } + + pub fn getCredentialsWithOptions(this: *const @This(), options: ?JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!S3.S3CredentialsWithOptions { + return S3Credentials.getCredentialsWithOptions(this.getCredentials().*, this.options, options, this.acl, globalObject); + } + + pub fn path(this: *@This()) []const u8 { + var path_name = bun.URL.parse(this.pathlike.slice()).s3Path(); + // normalize start and ending + if (strings.endsWith(path_name, "/")) { + path_name = path_name[0..path_name.len]; + } else if (strings.endsWith(path_name, "\\")) { + path_name = path_name[0 .. path_name.len - 1]; + } + if (strings.startsWith(path_name, "/")) { + path_name = path_name[1..]; + } else if (strings.startsWith(path_name, "\\")) { + path_name = path_name[1..]; + } + return path_name; + } + + pub fn unlink(this: *@This(), store: *Store, globalThis: *JSC.JSGlobalObject, extra_options: ?JSValue) bun.JSError!JSValue { + const Wrapper = struct { + promise: JSC.JSPromise.Strong, + store: *Store, + + pub usingnamespace bun.New(@This()); + + pub fn resolve(result: S3.S3DeleteResult, self: *@This()) void { + defer self.deinit(); + const globalObject = self.promise.globalObject().?; + switch (result) { + .success => { + self.promise.resolve(globalObject, .true); + }, + inline .not_found, .failure => |err| { + self.promise.reject(globalObject, err.toJS(globalObject, self.store.getPath())); + }, + } + } + + fn deinit(self: *@This()) void { + self.store.deref(); + self.promise.deinit(); + self.destroy(); + } + }; + const promise = JSC.JSPromise.Strong.init(globalThis); + const value = promise.value(); + const proxy_url = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy = if (proxy_url) |url| url.href else null; + var aws_options = try this.getCredentialsWithOptions(extra_options, globalThis); + defer aws_options.deinit(); + S3.delete(&aws_options.credentials, this.path(), @ptrCast(&Wrapper.resolve), Wrapper.new(.{ + .promise = promise, + .store = store, // store is needed in case of not found error + }), proxy); + store.ref(); + + return value; + } + pub fn initWithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *S3Credentials) S3Store { + credentials.ref(); + return .{ + .credentials = credentials, + .pathlike = pathlike, + .mime_type = mime_type orelse http.MimeType.other, + }; + } + pub fn init(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: S3Credentials) S3Store { + return .{ + .credentials = credentials.dupe(), + .pathlike = pathlike, + .mime_type = mime_type orelse http.MimeType.other, + }; + } + pub fn estimatedSize(this: *const @This()) usize { + return this.pathlike.estimatedSize() + if (this.credentials) |credentials| credentials.estimatedSize() else 0; + } + + pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { + if (this.pathlike == .string) { + allocator.free(@constCast(this.pathlike.slice())); + } else { + this.pathlike.deinit(); + } + this.pathlike = .{ + .string = bun.PathString.empty, + }; + if (this.credentials) |credentials| { + credentials.deref(); + this.credentials = null; + } + } + }; + pub const ByteStore = struct { ptr: [*]u8 = undefined, len: SizeType = 0, @@ -3423,15 +3870,420 @@ pub const Blob = struct { return JSValue.jsBoolean(bun.isRegularFile(store.data.file.mode) or bun.C.S.ISFIFO(store.data.file.mode)); } + pub fn isS3(this: *const Blob) bool { + if (this.store) |store| { + return store.data == .s3; + } + return false; + } + + const S3BlobDownloadTask = struct { + blob: Blob, + globalThis: *JSC.JSGlobalObject, + promise: JSC.JSPromise.Strong, + poll_ref: bun.Async.KeepAlive = .{}, + + handler: S3ReadHandler, + usingnamespace bun.New(S3BlobDownloadTask); + pub const S3ReadHandler = *const fn (this: *Blob, globalthis: *JSGlobalObject, raw_bytes: []u8) JSValue; + + pub fn callHandler(this: *S3BlobDownloadTask, raw_bytes: []u8) JSValue { + return this.handler(&this.blob, this.globalThis, raw_bytes); + } + pub fn onS3DownloadResolved(result: S3.S3DownloadResult, this: *S3BlobDownloadTask) void { + defer this.deinit(); + switch (result) { + .success => |response| { + const bytes = response.body.list.items; + if (this.blob.size == Blob.max_size) { + this.blob.size = @truncate(bytes.len); + } + JSC.AnyPromise.wrap(.{ .normal = this.promise.get() }, this.globalThis, S3BlobDownloadTask.callHandler, .{ this, bytes }); + }, + inline .not_found, .failure => |err| { + this.promise.reject(this.globalThis, err.toJS(this.globalThis, this.blob.store.?.getPath())); + }, + } + } + + pub fn init(globalThis: *JSC.JSGlobalObject, blob: *Blob, handler: S3BlobDownloadTask.S3ReadHandler) JSValue { + blob.store.?.ref(); + + const this = S3BlobDownloadTask.new(.{ + .globalThis = globalThis, + .blob = blob.*, + .promise = JSC.JSPromise.Strong.init(globalThis), + .handler = handler, + }); + const promise = this.promise.value(); + const env = this.globalThis.bunVM().transpiler.env; + const credentials = this.blob.store.?.data.s3.getCredentials(); + const path = this.blob.store.?.data.s3.path(); + + this.poll_ref.ref(globalThis.bunVM()); + if (blob.offset > 0) { + const len: ?usize = if (blob.size != Blob.max_size) @intCast(blob.size) else null; + const offset: usize = @intCast(blob.offset); + S3.downloadSlice(credentials, path, offset, len, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + } else if (blob.size == Blob.max_size) { + S3.download(credentials, path, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + } else { + const len: usize = @intCast(blob.size); + const offset: usize = @intCast(blob.offset); + S3.downloadSlice(credentials, path, offset, len, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + } + return promise; + } + + pub fn deinit(this: *S3BlobDownloadTask) void { + this.blob.store.?.deref(); + this.poll_ref.unref(this.globalThis.bunVM()); + this.promise.deinit(); + this.destroy(); + } + }; + + pub fn doWrite(this: *Blob, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + const data = args.nextEat() orelse { + return globalThis.throwInvalidArguments("blob.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}); + }; + if (data.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("blob.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}); + } + var mkdirp_if_not_exists: ?bool = null; + const options = args.nextEat(); + if (options) |options_object| { + if (options_object.isObject()) { + if (try options_object.getTruthy(globalThis, "createPath")) |create_directory| { + if (!create_directory.isBoolean()) { + return globalThis.throwInvalidArgumentType("write", "options.createPath", "boolean"); + } + mkdirp_if_not_exists = create_directory.toBoolean(); + } + if (try options_object.getTruthy(globalThis, "type")) |content_type| { + //override the content type + if (!content_type.isString()) { + return globalThis.throwInvalidArgumentType("write", "options.type", "string"); + } + var content_type_str = content_type.toSlice(globalThis, bun.default_allocator); + defer content_type_str.deinit(); + const slice = content_type_str.slice(); + if (strings.isAllASCII(slice)) { + if (this.content_type_allocated) { + bun.default_allocator.free(this.content_type); + } + this.content_type_was_set = true; + + if (globalThis.bunVM().mimeType(slice)) |mime| { + this.content_type = mime.value; + } else { + const content_type_buf = bun.default_allocator.alloc(u8, slice.len) catch bun.outOfMemory(); + this.content_type = strings.copyLowercase(slice, content_type_buf); + this.content_type_allocated = true; + } + } + } + } else if (!options_object.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArgumentType("write", "options", "object"); + } + } + var blob_internal: PathOrBlob = .{ .blob = this.* }; + return writeFileInternal(globalThis, &blob_internal, data, .{ .mkdirp_if_not_exists = mkdirp_if_not_exists, .extra_options = options }); + } + + pub fn doUnlink(this: *Blob, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(1).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const store = this.store orelse { + return JSC.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Blob is detached", .{})); + }; + return switch (store.data) { + .s3 => |*s3| try s3.unlink(store, globalThis, args.nextEat()), + .file => |file| file.unlink(globalThis), + else => JSC.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Blob is read-only", .{})), + }; + } + // This mostly means 'can it be read?' pub fn getExists( this: *Blob, globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) bun.JSError!JSValue { + if (this.isS3()) { + return S3File.S3BlobStatTask.exists(globalThis, this); + } return JSC.JSPromise.resolvedPromiseValue(globalThis, this.getExistsSync()); } + pub const FileStreamWrapper = struct { + promise: JSC.JSPromise.Strong, + readable_stream_ref: JSC.WebCore.ReadableStream.Strong, + sink: *JSC.WebCore.FileSink, + + pub usingnamespace bun.New(@This()); + + pub fn deinit(this: *@This()) void { + this.promise.deinit(); + this.readable_stream_ref.deinit(); + this.sink.deref(); + this.destroy(); + } + }; + + pub const shim = JSC.Shimmer("Bun", "FileStreamWrapper", @This()); + pub fn onFileStreamResolveRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(FileStreamWrapper); + defer this.deinit(); + var strong = this.readable_stream_ref; + defer strong.deinit(); + this.readable_stream_ref = .{}; + if (strong.get()) |stream| { + stream.done(globalThis); + } + this.promise.resolve(globalThis, JSC.JSValue.jsNumber(0)); + return .undefined; + } + + pub fn onFileStreamRejectRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(FileStreamWrapper); + defer this.sink.deinit(); + const err = args.ptr[0]; + + var strong = this.readable_stream_ref; + defer strong.deinit(); + this.readable_stream_ref = .{}; + + this.promise.reject(globalThis, err); + + if (strong.get()) |stream| { + stream.cancel(globalThis); + } + return .undefined; + } + pub const Export = shim.exportFunctions(.{ + .onResolveRequestStream = onFileStreamResolveRequestStream, + .onRejectRequestStream = onFileStreamRejectRequestStream, + }); + comptime { + const jsonResolveRequestStream = JSC.toJSHostFunction(onFileStreamResolveRequestStream); + @export(jsonResolveRequestStream, .{ .name = Export[0].symbol_name }); + const jsonRejectRequestStream = JSC.toJSHostFunction(onFileStreamRejectRequestStream); + @export(jsonRejectRequestStream, .{ .name = Export[1].symbol_name }); + } + pub fn pipeReadableStreamToBlob(this: *Blob, globalThis: *JSC.JSGlobalObject, readable_stream: JSC.WebCore.ReadableStream, extra_options: ?JSValue) JSC.JSValue { + var store = this.store orelse { + return JSC.JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Blob is detached", .{})); + }; + + if (this.isS3()) { + const s3 = &this.store.?.data.s3; + var aws_options = s3.getCredentialsWithOptions(extra_options, globalThis) catch |err| { + return JSC.JSPromise.rejectedPromiseValue(globalThis, globalThis.takeException(err)); + }; + defer aws_options.deinit(); + + const path = s3.path(); + const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy_url = if (proxy) |p| p.href else null; + + return S3.uploadStream( + (if (extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + path, + readable_stream, + globalThis, + aws_options.options, + aws_options.acl, + this.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); + } + + if (store.data != .file) { + return JSC.JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Blob is read-only", .{})); + } + + const file_sink = brk_sink: { + if (Environment.isWindows) { + const pathlike = store.data.file.pathlike; + const fd: bun.FileDescriptor = if (pathlike == .fd) pathlike.fd else brk: { + var file_path: bun.PathBuffer = undefined; + const path = pathlike.path.sliceZ(&file_path); + switch (bun.sys.open( + path, + bun.O.WRONLY | bun.O.CREAT | bun.O.NONBLOCK, + write_permissions, + )) { + .result => |result| { + break :brk result; + }, + .err => |err| { + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.withPath(path).toJSC(globalThis)); + }, + } + unreachable; + }; + + const is_stdout_or_stderr = brk: { + if (pathlike != .fd) { + break :brk false; + } + + if (globalThis.bunVM().rare_data) |rare| { + if (store == rare.stdout_store) { + break :brk true; + } + + if (store == rare.stderr_store) { + break :brk true; + } + } + + break :brk switch (bun.FDTag.get(fd)) { + .stdout, .stderr => true, + else => false, + }; + }; + var sink = JSC.WebCore.FileSink.init(fd, this.globalThis.bunVM().eventLoop()); + sink.writer.owns_fd = pathlike != .fd; + + if (is_stdout_or_stderr) { + switch (sink.writer.startSync(fd, false)) { + .err => |err| { + sink.deref(); + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + else => {}, + } + } else { + switch (sink.writer.start(fd, true)) { + .err => |err| { + sink.deref(); + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + else => {}, + } + } + + break :brk_sink sink; + } + + var sink = JSC.WebCore.FileSink.init(bun.invalid_fd, this.globalThis.bunVM().eventLoop()); + + const input_path: JSC.WebCore.PathOrFileDescriptor = brk: { + if (store.data.file.pathlike == .fd) { + break :brk .{ .fd = store.data.file.pathlike.fd }; + } else { + break :brk .{ + .path = ZigString.Slice.fromUTF8NeverFree( + store.data.file.pathlike.path.slice(), + ).clone( + bun.default_allocator, + ) catch bun.outOfMemory(), + }; + } + }; + defer input_path.deinit(); + + const stream_start: JSC.WebCore.StreamStart = .{ + .FileSink = .{ + .input_path = input_path, + }, + }; + + switch (sink.start(stream_start)) { + .err => |err| { + sink.deref(); + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + else => {}, + } + break :brk_sink sink; + }; + var signal = &file_sink.signal; + + signal.* = JSC.WebCore.FileSink.JSSink.SinkSignal.init(.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + bun.assert(signal.isDead()); + + const assignment_result: JSC.JSValue = JSC.WebCore.FileSink.JSSink.assignToStream( + globalThis, + readable_stream.value, + file_sink, + @as(**anyopaque, @ptrCast(&signal.ptr)), + ); + + assignment_result.ensureStillAlive(); + + // assert that it was updated + bun.assert(!signal.isDead()); + + if (assignment_result.toError()) |err| { + file_sink.deref(); + return JSC.JSPromise.rejectedPromiseValue(globalThis, err); + } + + if (!assignment_result.isEmptyOrUndefinedOrNull()) { + globalThis.bunVM().drainMicrotasks(); + + assignment_result.ensureStillAlive(); + // it returns a Promise when it goes through ReadableStreamDefaultReader + if (assignment_result.asAnyPromise()) |promise| { + switch (promise.status(globalThis.vm())) { + .pending => { + const wrapper = FileStreamWrapper.new(.{ + .promise = JSC.JSPromise.Strong.init(globalThis), + .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable_stream, globalThis), + .sink = file_sink, + }); + const promise_value = wrapper.promise.value(); + + assignment_result.then( + globalThis, + wrapper, + onFileStreamResolveRequestStream, + onFileStreamRejectRequestStream, + ); + return promise_value; + }, + .fulfilled => { + file_sink.deref(); + readable_stream.done(globalThis); + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(0)); + }, + .rejected => { + file_sink.deref(); + + readable_stream.cancel(globalThis); + + return JSC.JSPromise.rejectedPromiseValue(globalThis, promise.result(globalThis.vm())); + }, + } + } else { + file_sink.deref(); + + readable_stream.cancel(globalThis); + + return JSC.JSPromise.rejectedPromiseValue(globalThis, assignment_result); + } + } + file_sink.deref(); + + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(0)); + } + pub fn getWriter( this: *Blob, globalThis: *JSC.JSGlobalObject, @@ -3447,7 +4299,57 @@ pub const Blob = struct { var store = this.store orelse { return globalThis.throwInvalidArguments("Blob is detached", .{}); }; + if (this.isS3()) { + const s3 = &this.store.?.data.s3; + const path = s3.path(); + const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy_url = if (proxy) |p| p.href else null; + if (arguments.len > 0) { + const options = arguments.ptr[0]; + if (options.isObject()) { + if (try options.getTruthy(globalThis, "type")) |content_type| { + //override the content type + if (!content_type.isString()) { + return globalThis.throwInvalidArgumentType("write", "options.type", "string"); + } + var content_type_str = content_type.toSlice(globalThis, bun.default_allocator); + defer content_type_str.deinit(); + const slice = content_type_str.slice(); + if (strings.isAllASCII(slice)) { + if (this.content_type_allocated) { + bun.default_allocator.free(this.content_type); + } + this.content_type_was_set = true; + if (globalThis.bunVM().mimeType(slice)) |mime| { + this.content_type = mime.value; + } else { + const content_type_buf = bun.default_allocator.alloc(u8, slice.len) catch bun.outOfMemory(); + this.content_type = strings.copyLowercase(slice, content_type_buf); + this.content_type_allocated = true; + } + } + } + const credentialsWithOptions = try s3.getCredentialsWithOptions(options, globalThis); + return try S3.writableStream( + credentialsWithOptions.credentials.dupe(), + path, + globalThis, + credentialsWithOptions.options, + this.contentTypeOrMimeType(), + proxy_url, + ); + } + } + return try S3.writableStream( + s3.getCredentials(), + path, + globalThis, + .{}, + this.contentTypeOrMimeType(), + proxy_url, + ); + } if (store.data != .file) { return globalThis.throwInvalidArguments("Blob is read-only", .{}); } @@ -3556,6 +4458,30 @@ pub const Blob = struct { return sink.toJS(globalThis); } + pub fn getSliceFrom(this: *Blob, globalThis: *JSC.JSGlobalObject, relativeStart: i64, relativeEnd: i64, content_type: []const u8, content_type_was_allocated: bool) JSValue { + const offset = this.offset +| @as(SizeType, @intCast(relativeStart)); + const len = @as(SizeType, @intCast(@max(relativeEnd -| relativeStart, 0))); + + // This copies over the is_all_ascii flag + // which is okay because this will only be a <= slice + var blob = this.dupe(); + blob.offset = offset; + blob.size = len; + + // infer the content type if it was not specified + if (content_type.len == 0 and this.content_type.len > 0 and !this.content_type_allocated) { + blob.content_type = this.content_type; + } else { + blob.content_type = content_type; + } + blob.content_type_allocated = content_type_was_allocated; + blob.content_type_was_set = this.content_type_was_set or content_type_was_allocated; + + var blob_ = Blob.new(blob); + blob_.allocator = bun.default_allocator; + return blob_.toJS(globalThis); + } + /// https://w3c.github.io/FileAPI/#slice-method-algo /// The slice() method returns a new Blob object with bytes ranging from the /// optional start parameter up to but not including the optional end @@ -3647,26 +4573,7 @@ pub const Blob = struct { } } - const offset = this.offset +| @as(SizeType, @intCast(relativeStart)); - const len = @as(SizeType, @intCast(@max(relativeEnd -| relativeStart, 0))); - - // This copies over the is_all_ascii flag - // which is okay because this will only be a <= slice - var blob = this.dupe(); - blob.offset = offset; - blob.size = len; - - // infer the content type if it was not specified - if (content_type.len == 0 and this.content_type.len > 0 and !this.content_type_allocated) - content_type = this.content_type; - - blob.content_type = content_type; - blob.content_type_allocated = content_type_was_allocated; - blob.content_type_was_set = this.content_type_was_set or content_type_was_allocated; - - var blob_ = Blob.new(blob); - blob_.allocator = allocator; - return blob_.toJS(globalThis); + return this.getSliceFrom(globalThis, relativeStart, relativeEnd, content_type, content_type_was_allocated); } pub fn getMimeType(this: *const Blob) ?bun.http.MimeType { @@ -3769,6 +4676,8 @@ pub const Blob = struct { } else if (store.data == .bytes) { if (store.data.bytes.stored_name.slice().len > 0) return store.data.bytes.stored_name.slice(); + } else if (store.data == .s3) { + return store.data.s3.path(); } } @@ -3783,7 +4692,7 @@ pub const Blob = struct { if (this.store) |store| { if (store.data == .file) { // last_modified can be already set during read. - if (store.data.file.last_modified == JSC.init_timestamp) { + if (store.data.file.last_modified == JSC.init_timestamp and !this.isS3()) { resolveFileStat(store); } return JSValue.jsNumber(store.data.file.last_modified); @@ -3825,9 +4734,27 @@ pub const Blob = struct { _ = Bun__Blob__getSizeForBindings; } } - + pub fn getStat(this: *Blob, globalThis: *JSC.JSGlobalObject, callback: *JSC.CallFrame) JSC.JSValue { + const store = this.store orelse return JSC.JSValue.jsUndefined(); + // TODO: make this async for files + return switch (store.data) { + .file => |*file| { + return switch (file.pathlike) { + .path => |path_like| JSC.Node.Async.stat.create(globalThis, undefined, .{ + .path = .{ .encoded_slice = ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator) }, + }, globalThis.bunVM()), + .fd => |fd| JSC.Node.Async.fstat.create(globalThis, undefined, .{ .fd = fd }, globalThis.bunVM()), + }; + }, + .s3 => S3File.getStat(this, globalThis, callback), + else => JSC.JSValue.jsUndefined(), + }; + } pub fn getSize(this: *Blob, _: *JSC.JSGlobalObject) JSValue { if (this.size == Blob.max_size) { + if (this.isS3()) { + return JSC.JSValue.jsNumber(std.math.nan(f64)); + } this.resolveSize(); if (this.size == Blob.max_size and this.store != null) { return JSC.jsNumber(std.math.inf(f64)); @@ -4141,8 +5068,12 @@ pub const Blob = struct { // if (comptime Environment.allow_assert) { // assert(this.allocator != null); // } - this.calculateEstimatedByteSize(); + + if (this.isS3()) { + return S3File.toJSUnchecked(globalObject, this); + } + return Blob.toJSUnchecked(globalObject, this); } @@ -4182,7 +5113,7 @@ pub const Blob = struct { } pub fn needsToReadFile(this: *const Blob) bool { - return this.store != null and this.store.?.data == .file; + return this.store != null and (this.store.?.data == .file); } pub fn toStringWithBytes(this: *Blob, global: *JSGlobalObject, raw_bytes: []const u8, comptime lifetime: Lifetime) bun.JSError!JSValue { @@ -4274,6 +5205,9 @@ pub const Blob = struct { if (this.needsToReadFile()) { return this.doReadFile(toStringWithBytes, global); } + if (this.isS3()) { + return this.doReadFromS3(toStringWithBytes, global); + } const view_: []u8 = @constCast(this.sharedView()); @@ -4288,6 +5222,9 @@ pub const Blob = struct { if (this.needsToReadFile()) { return this.doReadFile(toJSONWithBytes, global); } + if (this.isS3()) { + return this.doReadFromS3(toJSONWithBytes, global); + } const view_ = this.sharedView(); @@ -4450,6 +5387,10 @@ pub const Blob = struct { return this.doReadFile(WithBytesFn, global); } + if (this.isS3()) { + return this.doReadFromS3(WithBytesFn, global); + } + const view_ = this.sharedView(); if (view_.len == 0) return JSC.ArrayBuffer.create(global, "", TypedArrayView); @@ -4461,6 +5402,9 @@ pub const Blob = struct { if (this.needsToReadFile()) { return this.doReadFile(toFormDataWithBytes, global); } + if (this.isS3()) { + return this.doReadFromS3(toFormDataWithBytes, global); + } const view_ = this.sharedView(); @@ -4770,6 +5714,15 @@ pub const AnyBlob = union(enum) { InternalBlob: InternalBlob, WTFStringImpl: bun.WTF.StringImpl, + /// Assumed that AnyBlob itself is covered by the caller. + pub fn memoryCost(this: *const AnyBlob) usize { + return switch (this.*) { + .Blob => |*blob| if (blob.store) |blob_store| blob_store.memoryCost() else 0, + .WTFStringImpl => |str| if (str.refCount() == 1) str.memoryCost() else 0, + .InternalBlob => |*internal_blob| internal_blob.memoryCost(), + }; + } + pub fn hasOneRef(this: *const AnyBlob) bool { if (this.store()) |s| { return s.hasOneRef(); @@ -5090,6 +6043,13 @@ pub const AnyBlob = union(enum) { }; } + pub fn isS3(self: *const @This()) bool { + return switch (self.*) { + .Blob => self.Blob.isS3(), + .WTFStringImpl, .InternalBlob => false, + }; + } + pub fn detach(self: *@This()) void { return switch (self.*) { .Blob => { @@ -5122,6 +6082,10 @@ pub const InternalBlob = struct { bytes: std.ArrayList(u8), was_string: bool = false, + pub fn memoryCost(this: *const @This()) usize { + return this.bytes.capacity; + } + pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue { const bytes_without_bom = strings.withoutUTF8BOM(this.bytes.items); if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false, false) catch &[_]u16{}) |out| { @@ -5273,3 +6237,9 @@ pub const InlineBlob = extern struct { }; const assert = bun.assert; + +pub export fn JSDOMFile__hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(JSC.conv) bool { + JSC.markBinding(@src()); + const blob = value.as(Blob) orelse return false; + return blob.is_jsdom_file; +} diff --git a/src/bun.js/webcore/blob/WriteFile.zig b/src/bun.js/webcore/blob/WriteFile.zig index 13804680ae..9560fc4cd2 100644 --- a/src/bun.js/webcore/blob/WriteFile.zig +++ b/src/bun.js/webcore/blob/WriteFile.zig @@ -709,7 +709,7 @@ pub const WriteFileWaitFromLockedValueTask = struct { => { var blob = value.use(); // TODO: this should be one promise not two! - const new_promise = Blob.writeFileWithSourceDestination(globalThis, &blob, &file_blob, this.mkdirp_if_not_exists); + const new_promise = Blob.writeFileWithSourceDestination(globalThis, &blob, &file_blob, .{ .mkdirp_if_not_exists = this.mkdirp_if_not_exists }); if (new_promise.asAnyPromise()) |p| { switch (p.unwrap(globalThis.vm(), .mark_handled)) { // Fulfill the new promise using the pending promise diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index 89c2e4cf9d..abdfa55d87 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -72,7 +72,7 @@ pub const Body = struct { try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("bodyUsed: ", enable_ansi_colors)); - formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors); + try formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors); if (this.value == .Blob) { try formatter.printComma(Writer, writer, enable_ansi_colors); @@ -89,7 +89,7 @@ pub const Body = struct { try formatter.printComma(Writer, writer, enable_ansi_colors); try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); - formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors); + try formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors); } } } @@ -158,6 +158,18 @@ pub const Body = struct { return false; } + + pub fn isDisturbed2(this: *const PendingValue, globalObject: *JSC.JSGlobalObject) bool { + if (this.promise != null) { + return true; + } + + if (this.readable.get()) |readable| { + return readable.isDisturbed(globalObject); + } + + return false; + } pub fn isStreamingOrBuffering(this: *PendingValue) bool { return this.readable.held.has() or (this.promise != null and !this.promise.?.isEmptyOrUndefinedOrNull()); } @@ -402,6 +414,16 @@ pub const Body = struct { }; } + pub fn memoryCost(this: *const Value) usize { + return switch (this.*) { + .InternalBlob => this.InternalBlob.bytes.items.len, + .WTFStringImpl => this.WTFStringImpl.memoryCost(), + .Locked => this.Locked.sizeHint(), + // .InlineBlob => this.InlineBlob.sliceConst().len, + else => 0, + }; + } + pub fn estimatedSize(this: *const Value) usize { return switch (this.*) { .InternalBlob => this.InternalBlob.sliceConst().len, @@ -1089,9 +1111,9 @@ pub const Body = struct { var body = Body{ .value = Value{ .Null = {} } }; body.value = try Value.fromJS(globalThis, value); - if (body.value == .Blob) + if (body.value == .Blob) { assert(body.value.Blob.allocator == null); // owned by Body - + } return body; } }; diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 6d745017ec..7274373be0 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -77,6 +77,10 @@ pub const Request = struct { pub const getBlobWithoutCallFrame = RequestMixin.getBlobWithoutCallFrame; pub const WeakRef = bun.WeakPtr(Request, .weak_ptr_data); + pub fn memoryCost(this: *const Request) usize { + return @sizeOf(Request) + this.request_context.memoryCost() + this.url.byteSlice().len + this.body.value.memoryCost(); + } + pub export fn Request__getUWSRequest( this: *Request, ) ?*uws.Request { @@ -223,7 +227,7 @@ pub const Request = struct { try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("headers: ", enable_ansi_colors)); - formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors); + try formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors); if (this.body.value == .Blob) { try writer.writeAll("\n"); @@ -243,7 +247,7 @@ pub const Request = struct { if (this.body.value.Locked.readable.get()) |stream| { try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); - formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors); + try formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors); } } } diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 157f0abc38..bbb6112950 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -10,6 +10,7 @@ export default [ estimatedSize: true, configurable: false, overridesToJS: true, + memoryCost: true, proto: { text: { fn: "getText" }, json: { fn: "getJSON" }, @@ -124,6 +125,7 @@ export default [ }), define({ name: "Blob", + final: false, construct: true, finalize: true, JSType: "0b11101110", @@ -164,9 +166,14 @@ export default [ getter: "getLastModified", }, + // Non-standard, s3 + BunFile support + unlink: { fn: "doUnlink", length: 0 }, + delete: { fn: "doUnlink", length: 0 }, + write: { fn: "doWrite", length: 2 }, size: { getter: "getSize", }, + stat: { fn: "getStat", length: 0 }, writer: { fn: "getWriter", diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 30c4299d3f..9190b800bc 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -41,7 +41,7 @@ const JSPrinter = bun.js_printer; const picohttp = bun.picohttp; const StringJoiner = bun.StringJoiner; const uws = bun.uws; -const Mutex = @import("../../lock.zig").Lock; +const Mutex = bun.Mutex; const InlineBlob = JSC.WebCore.InlineBlob; const AnyBlob = JSC.WebCore.AnyBlob; @@ -55,6 +55,7 @@ const Async = bun.Async; const BoringSSL = bun.BoringSSL; const X509 = @import("../api/bun/x509.zig"); const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; +const s3 = bun.S3; pub const Response = struct { const ResponseMixin = BodyMixin(@This()); @@ -143,7 +144,7 @@ pub const Response = struct { try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("ok: ", enable_ansi_colors)); - formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors); + try formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); try writer.writeAll("\n"); @@ -156,7 +157,7 @@ pub const Response = struct { try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("status: ", enable_ansi_colors)); - formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); try writer.writeAll("\n"); @@ -168,13 +169,13 @@ pub const Response = struct { try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("headers: ", enable_ansi_colors)); - formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors); + try formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("redirected: ", enable_ansi_colors)); - formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors); + try formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); try writer.writeAll("\n"); @@ -512,6 +513,39 @@ pub const Response = struct { pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*Response { const arguments = callframe.argumentsAsArray(2); + if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) { + if (arguments[0].as(Blob)) |blob| { + if (blob.isS3()) { + if (!arguments[1].isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("new Response(s3File) do not support ResponseInit options", .{}); + } + var response: Response = .{ + .init = Response.Init{ + .status_code = 302, + }, + .body = Body{ + .value = .{ .Empty = {} }, + }, + .url = bun.String.empty, + }; + + const credentials = blob.store.?.data.s3.getCredentials(); + + const result = credentials.signRequest(.{ + .path = blob.store.?.data.s3.path(), + .method = .GET, + }, .{ .expires = 15 * 60 }) catch |sign_err| { + return s3.throwSignError(sign_err, globalThis); + }; + defer result.deinit(); + response.init.headers = response.getOrCreateHeaders(globalThis); + response.redirected = true; + var headers_ref = response.init.headers.?; + headers_ref.put(.Location, result.url, globalThis); + return bun.new(Response, response); + } + } + } var init: Init = (brk: { if (arguments[1].isUndefinedOrNull()) { break :brk Init{ @@ -697,7 +731,14 @@ pub const Response = struct { }; const null_fd = bun.invalid_fd; +fn setHeaders(headers: *?Headers, new_headers: []const picohttp.Header, allocator: std.mem.Allocator) void { + var old = headers.*; + headers.* = Headers.fromPicoHttpHeaders(new_headers, allocator) catch bun.outOfMemory(); + if (old) |*headers_| { + headers_.deinit(); + } +} pub const Fetch = struct { const headers_string = "headers"; const method_string = "method"; @@ -765,14 +806,17 @@ pub const Fetch = struct { }; pub const FetchTasklet = struct { - const log = Output.scoped(.FetchTasklet, false); + pub const FetchTaskletStream = JSC.WebCore.NetworkSink; + const log = Output.scoped(.FetchTasklet, false); + sink: ?*FetchTaskletStream.JSSink = null, http: ?*http.AsyncHTTP = null, result: http.HTTPClientResult = .{}, metadata: ?http.HTTPResponseMetadata = null, javascript_vm: *VirtualMachine = undefined, global_this: *JSGlobalObject = undefined, request_body: HTTPRequestBody = undefined, + /// buffer being used by AsyncHTTP response_buffer: MutableString = undefined, /// buffer used to stream response to JS @@ -814,6 +858,7 @@ pub const Fetch = struct { hostname: ?[]u8 = null, is_waiting_body: bool = false, is_waiting_abort: bool = false, + is_waiting_request_stream_start: bool = false, mutex: Mutex, tracker: JSC.AsyncTaskTracker, @@ -849,6 +894,9 @@ pub const Fetch = struct { pub const HTTPRequestBody = union(enum) { AnyBlob: AnyBlob, Sendfile: http.Sendfile, + ReadableStream: JSC.WebCore.ReadableStream.Strong, + + pub const Empty: HTTPRequestBody = .{ .AnyBlob = .{ .Blob = .{} } }; pub fn store(this: *HTTPRequestBody) ?*JSC.WebCore.Blob.Store { return switch (this.*) { @@ -867,6 +915,9 @@ pub const Fetch = struct { pub fn detach(this: *HTTPRequestBody) void { switch (this.*) { .AnyBlob => this.AnyBlob.detach(), + .ReadableStream => |*stream| { + stream.deinit(); + }, .Sendfile => { if (@max(this.Sendfile.offset, this.Sendfile.remain) > 0) _ = bun.sys.close(this.Sendfile.fd); @@ -875,12 +926,78 @@ pub const Fetch = struct { }, } } + + pub fn fromJS(globalThis: *JSGlobalObject, value: JSValue) bun.JSError!HTTPRequestBody { + var body_value = try Body.Value.fromJS(globalThis, value); + if (body_value == .Used or (body_value == .Locked and (body_value.Locked.action != .none or body_value.Locked.isDisturbed2(globalThis)))) { + return globalThis.ERR_BODY_ALREADY_USED("body already used", .{}).throw(); + } + if (body_value == .Locked) { + if (body_value.Locked.readable.has()) { + // just grab the ref + return FetchTasklet.HTTPRequestBody{ .ReadableStream = body_value.Locked.readable }; + } + const readable = body_value.toReadableStream(globalThis); + if (!readable.isEmptyOrUndefinedOrNull() and body_value == .Locked and body_value.Locked.readable.has()) { + return FetchTasklet.HTTPRequestBody{ .ReadableStream = body_value.Locked.readable }; + } + } + return FetchTasklet.HTTPRequestBody{ .AnyBlob = body_value.useAsAnyBlob() }; + } + + pub fn needsToReadFile(this: *HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |blob| blob.needsToReadFile(), + else => false, + }; + } + + pub fn isS3(this: *const HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |*blob| blob.isS3(), + else => false, + }; + } + + pub fn hasContentTypeFromUser(this: *HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |blob| blob.hasContentTypeFromUser(), + else => false, + }; + } + + pub fn getAnyBlob(this: *HTTPRequestBody) ?*AnyBlob { + return switch (this.*) { + .AnyBlob => &this.AnyBlob, + else => null, + }; + } + + pub fn hasBody(this: *HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |blob| blob.size() > 0, + .ReadableStream => |*stream| stream.has(), + .Sendfile => true, + }; + } }; pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet { return FetchTasklet{}; } + fn clearSink(this: *FetchTasklet) void { + if (this.sink) |wrapper| { + this.sink = null; + + wrapper.sink.done = true; + wrapper.sink.ended = true; + wrapper.sink.finalize(); + wrapper.detach(); + wrapper.sink.finalizeAndDestroy(); + } + } + fn clearData(this: *FetchTasklet) void { log("clearData", .{}); const allocator = this.memory_reporter.allocator(); @@ -923,7 +1040,9 @@ pub const Fetch = struct { this.readable_stream_ref.deinit(); this.scheduled_response_buffer.deinit(); - this.request_body.detach(); + if (this.request_body != .ReadableStream or this.is_waiting_request_stream_start) { + this.request_body.detach(); + } this.abort_reason.deinit(); this.check_server_identity.deinit(); @@ -965,6 +1084,142 @@ pub const Fetch = struct { return null; } + pub fn onResolveRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var args = callframe.arguments_old(2); + var this: *@This() = args.ptr[args.len - 1].asPromisePtr(@This()); + defer this.deref(); + if (this.request_body == .ReadableStream) { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer readable_stream_ref.deinit(); + if (readable_stream_ref.get()) |stream| { + stream.done(globalThis); + this.clearSink(); + } + } + + return JSValue.jsUndefined(); + } + + pub fn onRejectRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(@This()); + defer this.deref(); + const err = args.ptr[0]; + if (this.request_body == .ReadableStream) { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer readable_stream_ref.deinit(); + if (readable_stream_ref.get()) |stream| { + stream.cancel(globalThis); + this.clearSink(); + } + } + + this.abortListener(err); + return JSValue.jsUndefined(); + } + pub const shim = JSC.Shimmer("Bun", "FetchTasklet", @This()); + + pub const Export = shim.exportFunctions(.{ + .onResolveRequestStream = onResolveRequestStream, + .onRejectRequestStream = onRejectRequestStream, + }); + comptime { + const jsonResolveRequestStream = JSC.toJSHostFunction(onResolveRequestStream); + @export(jsonResolveRequestStream, .{ .name = Export[0].symbol_name }); + const jsonRejectRequestStream = JSC.toJSHostFunction(onRejectRequestStream); + @export(jsonRejectRequestStream, .{ .name = Export[1].symbol_name }); + } + + pub fn startRequestStream(this: *FetchTasklet) void { + this.is_waiting_request_stream_start = false; + bun.assert(this.request_body == .ReadableStream); + if (this.request_body.ReadableStream.get()) |stream| { + this.ref(); // lets only unref when sink is done + + const globalThis = this.global_this; + var response_stream = FetchTaskletStream.new(.{ + .task = .{ .fetch = this }, + .buffer = .{}, + .globalThis = globalThis, + }).toSink(); + var signal = &response_stream.sink.signal; + this.sink = response_stream; + + signal.* = FetchTaskletStream.JSSink.SinkSignal.init(JSValue.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + bun.assert(signal.isDead()); + + // We are already corked! + const assignment_result: JSValue = FetchTaskletStream.JSSink.assignToStream( + globalThis, + stream.value, + response_stream, + @as(**anyopaque, @ptrCast(&signal.ptr)), + ); + + assignment_result.ensureStillAlive(); + + // assert that it was updated + bun.assert(!signal.isDead()); + + if (assignment_result.toError()) |err_value| { + response_stream.detach(); + this.sink = null; + response_stream.sink.finalizeAndDestroy(); + return this.abortListener(err_value); + } + + if (!assignment_result.isEmptyOrUndefinedOrNull()) { + assignment_result.ensureStillAlive(); + // it returns a Promise when it goes through ReadableStreamDefaultReader + if (assignment_result.asAnyPromise()) |promise| { + switch (promise.status(globalThis.vm())) { + .pending => { + this.ref(); + assignment_result.then( + globalThis, + this, + onResolveRequestStream, + onRejectRequestStream, + ); + }, + .fulfilled => { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer { + stream.done(globalThis); + this.clearSink(); + readable_stream_ref.deinit(); + } + }, + .rejected => { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer { + stream.cancel(globalThis); + this.clearSink(); + readable_stream_ref.deinit(); + } + + this.abortListener(promise.result(globalThis.vm())); + }, + } + return; + } else { + // if is not a promise we treat it as Error + response_stream.detach(); + this.sink = null; + response_stream.sink.finalizeAndDestroy(); + return this.abortListener(assignment_result); + } + } + } + } pub fn onBodyReceived(this: *FetchTasklet) void { const success = this.result.isSuccess(); const globalThis = this.global_this; @@ -1141,11 +1396,17 @@ pub const Fetch = struct { this.deref(); } } + if (this.is_waiting_request_stream_start and this.result.can_stream) { + // start streaming + this.startRequestStream(); + } // if we already respond the metadata and still need to process the body if (this.is_waiting_body) { this.onBodyReceived(); return; } + if (this.metadata == null and this.result.isSuccess()) return; + // if we abort because of cert error // we wait the Http Client because we already have the response // we just need to deinit @@ -1195,7 +1456,6 @@ pub const Fetch = struct { this.promise.deinit(); } const success = this.result.isSuccess(); - const result = switch (success) { true => JSC.Strong.create(this.onResolve(), globalThis), false => brk: { @@ -1262,8 +1522,8 @@ pub const Fetch = struct { const cert = certificate_info.cert; var cert_ptr = cert.ptr; if (BoringSSL.d2i_X509(null, &cert_ptr, @intCast(cert.len))) |x509| { - defer BoringSSL.X509_free(x509); const globalObject = this.global_this; + defer x509.free(); const js_cert = X509.toJS(x509, globalObject) catch |err| { switch (err) { error.JSError => {}, @@ -1675,10 +1935,10 @@ pub const Fetch = struct { var proxy: ?ZigURL = null; if (fetch_options.proxy) |proxy_opt| { if (!proxy_opt.isEmpty()) { //if is empty just ignore proxy - proxy = fetch_options.proxy orelse jsc_vm.bundler.env.getHttpProxy(fetch_options.url); + proxy = fetch_options.proxy orelse jsc_vm.transpiler.env.getHttpProxyFor(fetch_options.url); } } else { - proxy = jsc_vm.bundler.env.getHttpProxy(fetch_options.url); + proxy = jsc_vm.transpiler.env.getHttpProxyFor(fetch_options.url); } if (fetch_tasklet.check_server_identity.has() and fetch_tasklet.reject_unauthorized) { @@ -1713,7 +1973,18 @@ pub const Fetch = struct { .tls_props = fetch_options.ssl_config, }, ); - + // enable streaming the write side + const isStream = fetch_tasklet.request_body == .ReadableStream; + fetch_tasklet.http.?.client.flags.is_streaming_request_body = isStream; + fetch_tasklet.is_waiting_request_stream_start = isStream; + if (isStream) { + fetch_tasklet.http.?.request_body = .{ + .stream = .{ + .buffer = .{}, + .ended = false, + }, + }; + } // TODO is this necessary? the http client already sets the redirect type, // so manually setting it here seems redundant if (fetch_options.redirect_type != FetchRedirect.follow) { @@ -1740,6 +2011,22 @@ pub const Fetch = struct { log("abortListener", .{}); reason.ensureStillAlive(); this.abort_reason.set(this.global_this, reason); + this.abortTask(); + if (this.sink) |wrapper| { + wrapper.sink.abort(); + return; + } + } + + pub fn sendRequestData(this: *FetchTasklet, data: []const u8, ended: bool) void { + if (this.http) |http_| { + http.http_thread.scheduleRequestWrite(http_, data, ended); + } else if (data.len != 3) { + bun.default_allocator.free(data); + } + } + + pub fn abortTask(this: *FetchTasklet) void { this.signal_store.aborted.store(true, .monotonic); this.tracker.didCancel(this.global_this); @@ -1941,7 +2228,7 @@ pub const Fetch = struct { } const url = ZigURL.parse(url_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory()); - if (!url.isHTTP() and !url.isHTTPS()) { + if (!url.isHTTP() and !url.isHTTPS() and !url.isS3()) { bun.default_allocator.free(url.href); return globalObject.throwInvalidArguments("URL must be HTTP or HTTPS", .{}); } @@ -2017,9 +2304,7 @@ pub const Fetch = struct { // which is important for FormData. // https://github.com/oven-sh/bun/issues/2264 // - var body: AnyBlob = AnyBlob{ - .Blob = .{}, - }; + var body: FetchTasklet.HTTPRequestBody = FetchTasklet.HTTPRequestBody.Empty; var disable_timeout = false; var disable_keepalive = false; @@ -2034,6 +2319,7 @@ pub const Fetch = struct { var signal: ?*JSC.WebCore.AbortSignal = null; // Custom Hostname var hostname: ?[]u8 = null; + var range: ?[]u8 = null; var unix_socket_path: ZigString.Slice = ZigString.Slice.empty; var url_proxy_buffer: []const u8 = ""; @@ -2072,6 +2358,10 @@ pub const Fetch = struct { bun.default_allocator.free(hn); hostname = null; } + if (range) |range_| { + bun.default_allocator.free(range_); + range = null; + } if (ssl_config) |conf| { ssl_config = null; @@ -2223,7 +2513,7 @@ pub const Fetch = struct { return .zero; } - // "decompression: boolean" + // "decompress: boolean" disable_decompression = extract_disable_decompression: { const objects_to_try = [_]JSValue{ options_object orelse .zero, @@ -2559,8 +2849,7 @@ pub const Fetch = struct { if (options_object) |options| { if (options.fastGet(globalThis, .body)) |body__| { if (!body__.isUndefined()) { - var body_value = try Body.Value.fromJS(ctx, body__); - break :extract_body body_value.useAsAnyBlob(); + break :extract_body try FetchTasklet.HTTPRequestBody.fromJS(ctx, body__); } } @@ -2575,20 +2864,29 @@ pub const Fetch = struct { return globalThis.ERR_BODY_ALREADY_USED("Request body already used", .{}).throw(); } - break :extract_body req.body.value.useAsAnyBlob(); + if (req.body.value == .Locked) { + if (req.body.value.Locked.readable.has()) { + break :extract_body FetchTasklet.HTTPRequestBody{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(req.body.value.Locked.readable.get().?, globalThis) }; + } + const readable = req.body.value.toReadableStream(globalThis); + if (!readable.isEmptyOrUndefinedOrNull() and req.body.value == .Locked and req.body.value.Locked.readable.has()) { + break :extract_body FetchTasklet.HTTPRequestBody{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(req.body.value.Locked.readable.get().?, globalThis) }; + } + } + + break :extract_body FetchTasklet.HTTPRequestBody{ .AnyBlob = req.body.value.useAsAnyBlob() }; } if (request_init_object) |req| { if (req.fastGet(globalThis, .body)) |body__| { if (!body__.isUndefined()) { - var body_value = try Body.Value.fromJS(ctx, body__); - break :extract_body body_value.useAsAnyBlob(); + break :extract_body try FetchTasklet.HTTPRequestBody.fromJS(ctx, body__); } } } break :extract_body null; - } orelse AnyBlob{ .Blob = .{} }; + } orelse FetchTasklet.HTTPRequestBody.Empty; if (globalThis.hasException()) { is_error = true; @@ -2681,8 +2979,17 @@ pub const Fetch = struct { } hostname = _hostname.toOwnedSliceZ(allocator) catch bun.outOfMemory(); } + if (url.isS3()) { + if (headers_.fastGet(JSC.FetchHeaders.HTTPHeaderName.Range)) |_range| { + if (range) |range_| { + range = null; + allocator.free(range_); + } + range = _range.toOwnedSliceZ(allocator) catch bun.outOfMemory(); + } + } - break :extract_headers Headers.from(headers_, allocator, .{ .body = &body }) catch bun.outOfMemory(); + break :extract_headers Headers.from(headers_, allocator, .{ .body = body.getAnyBlob() }) catch bun.outOfMemory(); } break :extract_headers headers; @@ -2761,7 +3068,7 @@ pub const Fetch = struct { var cwd_buf: bun.PathBuffer = undefined; const cwd = if (Environment.isWindows) (bun.getcwd(&cwd_buf) catch |err| { return globalThis.throwError(err, "Failed to resolve file url"); - }) else globalThis.bunVM().bundler.fs.top_level_dir; + }) else globalThis.bunVM().transpiler.fs.top_level_dir; const fullpath = bun.path.joinAbsStringBuf( cwd, @@ -2792,6 +3099,7 @@ pub const Fetch = struct { break :blob Blob.findOrCreateFileFromPath( &pathlike, globalThis, + true, ); }; @@ -2809,34 +3117,47 @@ pub const Fetch = struct { } if (url.protocol.len > 0) { - if (!(url.isHTTP() or url.isHTTPS())) { - const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "protocol must be http: or https:", .{}, ctx); + if (!(url.isHTTP() or url.isHTTPS() or url.isS3())) { + const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "protocol must be http:, https: or s3:", .{}, ctx); is_error = true; return JSPromise.rejectedPromiseValue(globalThis, err); } } - if (!method.hasRequestBody() and body.size() > 0) { + if (!method.hasRequestBody() and body.hasBody()) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_unexpected_body, .{}, ctx); is_error = true; return JSPromise.rejectedPromiseValue(globalThis, err); } - if (headers == null and body.size() > 0 and body.hasContentTypeFromUser()) { + if (headers == null and body.hasBody() and body.hasContentTypeFromUser()) { headers = Headers.from( null, allocator, - .{ .body = &body }, + .{ .body = body.getAnyBlob() }, ) catch bun.outOfMemory(); } - var http_body = FetchTasklet.HTTPRequestBody{ - .AnyBlob = body, - }; + var http_body = body; + if (body.isS3()) { + prepare_body: { + // is a S3 file we can use chunked here + if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob(globalThis, &body.AnyBlob.Blob, s3.MultiPartUploadOptions.DefaultPartSize), globalThis)) |stream| { + var old = body; + defer old.detach(); + body = .{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis) }; + break :prepare_body; + } + const rejected_value = JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Failed to start s3 stream", .{})); + body.detach(); + + return rejected_value; + } + } if (body.needsToReadFile()) { prepare_body: { - const opened_fd_res: JSC.Maybe(bun.FileDescriptor) = switch (body.Blob.store.?.data.file.pathlike) { + const opened_fd_res: JSC.Maybe(bun.FileDescriptor) = switch (body.store().?.data.file.pathlike) { .fd => |fd| bun.sys.dup(fd), .path => |path| bun.sys.open(path.sliceZ(&globalThis.bunVM().nodeFS().sync_error_buf), if (Environment.isWindows) bun.O.RDONLY else bun.O.RDONLY | bun.O.NOCTTY, 0), }; @@ -2870,7 +3191,7 @@ pub const Fetch = struct { break :use_sendfile; } - const original_size = body.Blob.size; + const original_size = body.AnyBlob.Blob.size; const stat_size = @as(Blob.SizeType, @intCast(stat.size)); const blob_size = if (bun.isRegularFile(stat.mode)) stat_size @@ -2880,8 +3201,8 @@ pub const Fetch = struct { http_body = .{ .Sendfile = .{ .fd = opened_fd, - .remain = body.Blob.offset + original_size, - .offset = body.Blob.offset, + .remain = body.AnyBlob.Blob.offset + original_size, + .offset = body.AnyBlob.Blob.offset, .content_size = blob_size, }, }; @@ -2902,13 +3223,13 @@ pub const Fetch = struct { .{ .encoding = .buffer, .path = .{ .fd = opened_fd }, - .offset = body.Blob.offset, - .max_size = body.Blob.size, + .offset = body.AnyBlob.Blob.offset, + .max_size = body.AnyBlob.Blob.size, }, .sync, ); - if (body.Blob.store.?.data.file.pathlike == .path) { + if (body.store().?.data.file.pathlike == .path) { _ = bun.sys.close(opened_fd); } @@ -2922,13 +3243,162 @@ pub const Fetch = struct { }, .result => |result| { body.detach(); - body.from(std.ArrayList(u8).fromOwnedSlice(allocator, @constCast(result.slice()))); - http_body = .{ .AnyBlob = body }; + body.AnyBlob.from(std.ArrayList(u8).fromOwnedSlice(allocator, @constCast(result.slice()))); + http_body = .{ .AnyBlob = body.AnyBlob }; }, } } } + if (url.isS3()) { + // get ENV config + var credentialsWithOptions: s3.S3CredentialsWithOptions = .{ + .credentials = globalThis.bunVM().transpiler.env.getS3Credentials(), + .options = .{}, + .acl = null, + }; + defer { + credentialsWithOptions.deinit(); + } + + if (options_object) |options| { + if (try options.getTruthyComptime(globalThis, "s3")) |s3_options| { + if (s3_options.isObject()) { + s3_options.ensureStillAlive(); + credentialsWithOptions = try s3.S3Credentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, globalThis); + } + } + } + + if (body == .ReadableStream) { + // we cannot direct stream to s3 we need to use multi part upload + defer body.ReadableStream.deinit(); + const Wrapper = struct { + promise: JSC.JSPromise.Strong, + url: ZigURL, + url_proxy_buffer: []const u8, + pub usingnamespace bun.New(@This()); + + pub fn resolve(result: s3.S3UploadResult, self: *@This()) void { + if (self.promise.globalObject()) |global| { + switch (result) { + .success => { + const response = bun.new(Response, Response{ + .body = .{ .value = .Empty }, + .redirected = false, + .init = .{ .method = .PUT, .status_code = 200 }, + .url = bun.String.createAtomIfPossible(self.url.href), + }); + const response_js = Response.makeMaybePooled(@as(js.JSContextRef, global), response); + response_js.ensureStillAlive(); + self.promise.resolve(global, response_js); + }, + .failure => |err| { + const response = bun.new(Response, Response{ + .body = .{ + .value = .{ + .InternalBlob = .{ + .bytes = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, bun.default_allocator.dupe(u8, err.message) catch bun.outOfMemory()), + .was_string = true, + }, + }, + }, + .redirected = false, + .init = .{ + .method = .PUT, + .status_code = 500, + .status_text = bun.String.createAtomIfPossible(err.code), + }, + .url = bun.String.createAtomIfPossible(self.url.href), + }); + const response_js = Response.makeMaybePooled(@as(js.JSContextRef, global), response); + response_js.ensureStillAlive(); + self.promise.resolve(global, response_js); + }, + } + } + bun.default_allocator.free(self.url_proxy_buffer); + self.destroy(); + } + }; + if (method != .PUT and method != .POST) { + return JSC.JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Only POST and PUT do support body when using S3", .{})); + } + const promise = JSC.JSPromise.Strong.init(globalThis); + + const s3_stream = Wrapper.new(.{ + .url = url, + .url_proxy_buffer = url_proxy_buffer, + .promise = promise, + }); + + const promise_value = promise.value(); + const proxy_url = if (proxy) |p| p.href else ""; + _ = bun.S3.uploadStream( + credentialsWithOptions.credentials.dupe(), + url.s3Path(), + body.ReadableStream.get().?, + globalThis, + credentialsWithOptions.options, + credentialsWithOptions.acl, + if (headers) |h| h.getContentType() else null, + proxy_url, + @ptrCast(&Wrapper.resolve), + s3_stream, + ); + url = .{}; + url_proxy_buffer = ""; + return promise_value; + } + if (method == .POST) { + method = .PUT; + } + + var result = credentialsWithOptions.credentials.signRequest(.{ + .path = url.s3Path(), + .method = method, + }, null) catch |sign_err| { + is_error = true; + return JSPromise.rejectedPromiseValue(globalThis, s3.getJSSignError(sign_err, globalThis)); + }; + defer result.deinit(); + if (proxy) |proxy_| { + // proxy and url are in the same buffer lets replace it + const old_buffer = url_proxy_buffer; + defer allocator.free(old_buffer); + var buffer = allocator.alloc(u8, result.url.len + proxy_.href.len) catch bun.outOfMemory(); + bun.copy(u8, buffer[0..result.url.len], result.url); + bun.copy(u8, buffer[proxy_.href.len..], proxy_.href); + url_proxy_buffer = buffer; + + url = ZigURL.parse(url_proxy_buffer[0..result.url.len]); + proxy = ZigURL.parse(url_proxy_buffer[result.url.len..]); + } else { + // replace headers and url of the request + allocator.free(url_proxy_buffer); + url_proxy_buffer = result.url; + url = ZigURL.parse(result.url); + result.url = ""; // fetch now owns this + } + + const content_type = if (headers) |h| h.getContentType() else null; + var header_buffer: [10]picohttp.Header = undefined; + + if (range) |range_| { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + setHeaders(&headers, _headers, allocator); + } else if (content_type) |ct| { + if (ct.len > 0) { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = ct }); + setHeaders(&headers, _headers, allocator); + } else { + setHeaders(&headers, result.headers(), allocator); + } + } else { + setHeaders(&headers, result.headers(), allocator); + } + } + // Only create this after we have validated all the input. // or else we will leak it var promise = JSPromise.Strong.init(globalThis); @@ -2993,9 +3463,7 @@ pub const Fetch = struct { body.detach(); } else { // These are single-use, and have effectively been moved to the FetchTasklet. - body = .{ - .Blob = .{}, - }; + body = FetchTasklet.HTTPRequestBody.Empty; } proxy = null; url_proxy_buffer = ""; @@ -3015,11 +3483,58 @@ pub const Headers = struct { buf: std.ArrayListUnmanaged(u8) = .{}, allocator: std.mem.Allocator, + pub fn memoryCost(this: *const Headers) usize { + return this.buf.items.len + this.entries.memoryCost(); + } + + pub fn clone(this: *Headers) !Headers { + return Headers{ + .entries = try this.entries.clone(this.allocator), + .buf = try this.buf.clone(this.allocator), + .allocator = this.allocator, + }; + } + + pub fn append(this: *Headers, name: []const u8, value: []const u8) !void { + var offset: u32 = @truncate(this.buf.items.len); + try this.buf.ensureUnusedCapacity(this.allocator, name.len + value.len); + const name_ptr = Api.StringPointer{ + .offset = offset, + .length = @truncate(name.len), + }; + this.buf.appendSliceAssumeCapacity(name); + offset = @truncate(this.buf.items.len); + this.buf.appendSliceAssumeCapacity(value); + + const value_ptr = Api.StringPointer{ + .offset = offset, + .length = @truncate(value.len), + }; + try this.entries.append(this.allocator, .{ + .name = name_ptr, + .value = value_ptr, + }); + } + pub fn deinit(this: *Headers) void { this.entries.deinit(this.allocator); this.buf.clearAndFree(this.allocator); } + pub fn getContentType(this: *const Headers) ?[]const u8 { + if (this.entries.len == 0 or this.buf.items.len == 0) { + return null; + } + const header_entries = this.entries.slice(); + const header_names = header_entries.items(.name); + const header_values = header_entries.items(.value); + for (header_names, 0..header_names.len) |name, i| { + if (bun.strings.eqlCaseInsensitiveASCII(this.asStr(name), "content-type", true)) { + return this.asStr(header_values[i]); + } + } + return null; + } pub fn asStr(this: *const Headers, ptr: Api.StringPointer) []const u8 { return if (ptr.offset + ptr.length <= this.buf.items.len) this.buf.items[ptr.offset..][0..ptr.length] @@ -3031,6 +3546,45 @@ pub const Headers = struct { body: ?*const AnyBlob = null, }; + pub fn fromPicoHttpHeaders(headers: []const picohttp.Header, allocator: std.mem.Allocator) !Headers { + const header_count = headers.len; + var result = Headers{ + .entries = .{}, + .buf = .{}, + .allocator = allocator, + }; + + var buf_len: usize = 0; + for (headers) |header| { + buf_len += header.name.len + header.value.len; + } + result.entries.ensureTotalCapacity(allocator, header_count) catch bun.outOfMemory(); + result.entries.len = headers.len; + result.buf.ensureTotalCapacityPrecise(allocator, buf_len) catch bun.outOfMemory(); + result.buf.items.len = buf_len; + var offset: u32 = 0; + for (headers, 0..headers.len) |header, i| { + const name_offset = offset; + bun.copy(u8, result.buf.items[offset..][0..header.name.len], header.name); + offset += @truncate(header.name.len); + const value_offset = offset; + bun.copy(u8, result.buf.items[offset..][0..header.value.len], header.value); + offset += @truncate(header.value.len); + + result.entries.set(i, .{ + .name = .{ + .offset = name_offset, + .length = @truncate(header.name.len), + }, + .value = .{ + .offset = value_offset, + .length = @truncate(header.value.len), + }, + }); + } + return result; + } + pub fn from(fetch_headers_ref: ?*FetchHeaders, allocator: std.mem.Allocator, options: Options) !Headers { var header_count: u32 = 0; var buf_len: u32 = 0; diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index de93c7502f..e4b436a713 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -360,6 +360,9 @@ pub const ReadableStream = struct { .globalThis = globalThis, .context = .{ .event_loop = JSC.EventLoopHandle.init(globalThis.bunVM().eventLoop()), + .start_offset = blob.offset, + .max_size = if (blob.size != Blob.max_size) blob.size else null, + .lazy = .{ .blob = store, }, @@ -369,6 +372,14 @@ pub const ReadableStream = struct { return reader.toReadableStream(globalThis); }, + .s3 => |*s3| { + const credentials = s3.getCredentials(); + const path = s3.path(); + const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); + const proxy_url = if (proxy) |p| p.href else null; + + return bun.S3.readableStream(credentials, path, blob.offset, if (blob.size != Blob.max_size) blob.size else null, proxy_url, globalThis); + }, } } @@ -470,6 +481,7 @@ pub const StreamStart = union(Tag) { FileSink: FileSinkOptions, HTTPSResponseSink: void, HTTPResponseSink: void, + NetworkSink: void, ready: void, owned_and_done: bun.ByteList, done: bun.ByteList, @@ -496,6 +508,7 @@ pub const StreamStart = union(Tag) { FileSink, HTTPSResponseSink, HTTPResponseSink, + NetworkSink, ready, owned_and_done, done, @@ -646,7 +659,7 @@ pub const StreamStart = union(Tag) { }, }; }, - .HTTPSResponseSink, .HTTPResponseSink => { + .NetworkSink, .HTTPSResponseSink, .HTTPResponseSink => { var empty = true; var chunk_size: JSC.WebCore.Blob.SizeType = 2048; @@ -1542,6 +1555,11 @@ pub const ArrayBufferSink = struct { return Sink.init(this); } + pub fn memoryCost(this: *const ArrayBufferSink) usize { + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(ArrayBufferSink). + return this.bytes.cap; + } + pub const JSSink = NewJSSink(@This(), "ArrayBufferSink"); }; @@ -1635,6 +1653,10 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { pub fn start(_: *@This()) void {} }; + pub fn memoryCost(this: *ThisSink) callconv(.C) usize { + return @sizeOf(ThisSink) + SinkType.memoryCost(&this.sink); + } + pub fn onClose(ptr: JSValue, reason: JSValue) callconv(.C) void { JSC.markBinding(@src()); @@ -1767,16 +1789,23 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { return globalThis.throwValue(JSC.toTypeError(.ERR_INVALID_ARG_TYPE, "write() expects a string, ArrayBufferView, or ArrayBuffer", .{}, globalThis)); } - const str = arg.getZigString(globalThis); - if (str.len == 0) { + const str = arg.toString(globalThis); + if (globalThis.hasException()) { + return .zero; + } + + const view = str.view(globalThis); + + if (view.isEmpty()) { return JSC.JSValue.jsNumber(0); } - if (str.is16Bit()) { - return this.sink.writeUTF16(.{ .temporary = bun.ByteList.initConst(std.mem.sliceAsBytes(str.utf16SliceAligned())) }).toJS(globalThis); + defer str.ensureStillAlive(); + if (view.is16Bit()) { + return this.sink.writeUTF16(.{ .temporary = bun.ByteList.initConst(std.mem.sliceAsBytes(view.utf16SliceAligned())) }).toJS(globalThis); } - return this.sink.writeLatin1(.{ .temporary = bun.ByteList.initConst(str.slice()) }).toJS(globalThis); + return this.sink.writeLatin1(.{ .temporary = bun.ByteList.initConst(view.slice()) }).toJS(globalThis); } pub fn writeUTF8(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { @@ -1804,16 +1833,22 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { const arg = args[0]; - const str = arg.getZigString(globalThis); - if (str.len == 0) { + const str = arg.toString(globalThis); + if (globalThis.hasException()) { + return .zero; + } + + const view = str.view(globalThis); + if (view.isEmpty()) { return JSC.JSValue.jsNumber(0); } + defer str.ensureStillAlive(); if (str.is16Bit()) { - return this.sink.writeUTF16(.{ .temporary = str.utf16SliceAligned() }).toJS(globalThis); + return this.sink.writeUTF16(.{ .temporary = view.utf16SliceAligned() }).toJS(globalThis); } - return this.sink.writeLatin1(.{ .temporary = str.slice() }).toJS(globalThis); + return this.sink.writeLatin1(.{ .temporary = view.slice() }).toJS(globalThis); } pub fn close(globalThis: *JSGlobalObject, sink_ptr: ?*anyopaque) callconv(.C) JSValue { @@ -1949,6 +1984,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { @export(jsConstruct, .{ .name = shim.symbolName("construct") }); @export(endWithSink, .{ .name = shim.symbolName("endWithSink") }); @export(updateRef, .{ .name = shim.symbolName("updateRef") }); + @export(memoryCost, .{ .name = shim.symbolName("memoryCost") }); shim.assertJSFunction(.{ write, @@ -1984,7 +2020,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { // TODO: make this JSGlobalObject local // for better security -const ByteListPool = ObjectPool( +pub const ByteListPool = ObjectPool( bun.ByteList, null, true, @@ -2027,6 +2063,14 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { this.signal = signal; } + // Don't include @sizeOf(This) because it's already included in the memoryCost of the sink + pub fn memoryCost(this: *@This()) usize { + // TODO: include Socket send buffer size. We can't here because we + // don't track if it's still accessible. + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(ArrayBufferSink). + return this.buffer.cap; + } + fn handleWrote(this: *@This(), amount1: usize) void { defer log("handleWrote: {d} offset: {d}, {d}", .{ amount1, this.offset, this.buffer.len }); const amount = @as(Blob.SizeType, @truncate(amount1)); @@ -2568,7 +2612,12 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { if (this.pooled_buffer) |pooled| { this.buffer.len = 0; + if (this.buffer.cap > 64 * 1024) { + this.buffer.deinitWithAllocator(bun.default_allocator); + this.buffer = bun.ByteList.init(""); + } pooled.data = this.buffer; + this.buffer = bun.ByteList.init(""); this.pooled_buffer = null; pooled.release(); @@ -2600,7 +2649,334 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { } pub const HTTPSResponseSink = HTTPServerWritable(true); pub const HTTPResponseSink = HTTPServerWritable(false); +pub const NetworkSink = struct { + task: ?HTTPWritableStream = null, + signal: Signal = .{}, + globalThis: *JSGlobalObject = undefined, + highWaterMark: Blob.SizeType = 2048, + buffer: bun.io.StreamBuffer, + ended: bool = false, + done: bool = false, + cancel: bool = false, + encoded: bool = true, + endPromise: JSC.JSPromise.Strong = .{}, + + auto_flusher: AutoFlusher = AutoFlusher{}, + + pub usingnamespace bun.New(NetworkSink); + const HTTPWritableStream = union(enum) { + fetch: *JSC.WebCore.Fetch.FetchTasklet, + s3_upload: *bun.S3.MultiPartUpload, + }; + + fn getHighWaterMark(this: *@This()) Blob.SizeType { + if (this.task) |task| { + return switch (task) { + .s3_upload => |s3| @truncate(s3.partSizeInBytes()), + else => this.highWaterMark, + }; + } + return this.highWaterMark; + } + fn unregisterAutoFlusher(this: *@This()) void { + if (this.auto_flusher.registered) + AutoFlusher.unregisterDeferredMicrotaskWithTypeUnchecked(@This(), this, this.globalThis.bunVM()); + } + + fn registerAutoFlusher(this: *@This()) void { + if (!this.auto_flusher.registered) + AutoFlusher.registerDeferredMicrotaskWithTypeUnchecked(@This(), this, this.globalThis.bunVM()); + } + + pub fn path(this: *@This()) ?[]const u8 { + if (this.task) |task| { + return switch (task) { + .s3_upload => |s3| s3.path, + else => null, + }; + } + return null; + } + + pub fn onAutoFlush(this: *@This()) bool { + if (this.done) { + this.auto_flusher.registered = false; + return false; + } + + _ = this.internalFlush() catch 0; + if (this.buffer.isEmpty()) { + this.auto_flusher.registered = false; + return false; + } + return true; + } + + pub fn start(this: *@This(), stream_start: StreamStart) JSC.Maybe(void) { + if (this.ended) { + return .{ .result = {} }; + } + + switch (stream_start) { + .chunk_size => |chunk_size| { + if (chunk_size > 0) { + this.highWaterMark = chunk_size; + } + }, + else => {}, + } + this.ended = false; + this.signal.start(); + return .{ .result = {} }; + } + + pub fn connect(this: *@This(), signal: Signal) void { + this.signal = signal; + } + pub fn sink(this: *@This()) Sink { + return Sink.init(this); + } + pub fn toSink(this: *@This()) *@This().JSSink { + return @ptrCast(this); + } + pub fn finalize(this: *@This()) void { + this.unregisterAutoFlusher(); + + var buffer = this.buffer; + this.buffer = .{}; + buffer.deinit(); + + this.detachWritable(); + } + + fn detachWritable(this: *@This()) void { + if (this.task) |task| { + this.task = null; + switch (task) { + inline .fetch, .s3_upload => |writable| { + writable.deref(); + }, + } + } + } + + fn sendRequestData(writable: HTTPWritableStream, data: []const u8, is_last: bool) void { + switch (writable) { + inline .fetch, .s3_upload => |task| task.sendRequestData(data, is_last), + } + } + + pub fn send(this: *@This(), data: []const u8, is_last: bool) !void { + if (this.done) return; + + if (this.task) |task| { + if (is_last) this.done = true; + if (this.encoded) { + if (data.len == 0) { + sendRequestData(task, bun.http.end_of_chunked_http1_1_encoding_response_body, true); + return; + } + + // chunk encoding is really simple + if (is_last) { + const chunk = std.fmt.allocPrint(bun.default_allocator, "{x}\r\n{s}\r\n0\r\n\r\n", .{ data.len, data }) catch return error.OOM; + sendRequestData(task, chunk, true); + } else { + const chunk = std.fmt.allocPrint(bun.default_allocator, "{x}\r\n{s}\r\n", .{ data.len, data }) catch return error.OOM; + sendRequestData(task, chunk, false); + } + } else { + sendRequestData(task, data, is_last); + } + } + } + + pub fn internalFlush(this: *@This()) !usize { + if (this.done) return 0; + var flushed: usize = 0; + // we need to respect the max len for the chunk + while (this.buffer.isNotEmpty()) { + const bytes = this.buffer.slice(); + const len: u32 = @min(bytes.len, std.math.maxInt(u32)); + try this.send(bytes, this.buffer.list.items.len - (this.buffer.cursor + len) == 0 and this.ended); + flushed += len; + this.buffer.cursor = len; + if (this.buffer.isEmpty()) { + this.buffer.reset(); + } + } + if (this.ended and !this.done) { + try this.send("", true); + this.finalize(); + } + return flushed; + } + + pub fn flush(this: *@This()) JSC.Maybe(void) { + _ = this.internalFlush() catch 0; + return .{ .result = {} }; + } + pub fn flushFromJS(this: *@This(), globalThis: *JSGlobalObject, _: bool) JSC.Maybe(JSValue) { + return .{ .result = JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(this.internalFlush() catch 0)) }; + } + pub fn finalizeAndDestroy(this: *@This()) void { + this.finalize(); + this.destroy(); + } + + pub fn abort(this: *@This()) void { + this.ended = true; + this.done = true; + this.signal.close(null); + this.cancel = true; + this.finalize(); + } + + pub fn write(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.ended) { + return .{ .owned = 0 }; + } + const bytes = data.slice(); + const len = @as(Blob.SizeType, @truncate(bytes.len)); + + if (this.buffer.size() == 0 and len >= this.getHighWaterMark()) { + // fast path: + // - large-ish chunk + this.send(bytes, false) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + return .{ .owned = len }; + } else if (this.buffer.size() + len >= this.getHighWaterMark()) { + _ = this.buffer.write(bytes) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + _ = this.internalFlush() catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + return .{ .owned = len }; + } else { + // queue the data wait until highWaterMark is reached or the auto flusher kicks in + this.buffer.write(bytes) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + } + this.registerAutoFlusher(); + return .{ .owned = len }; + } + + pub const writeBytes = write; + pub fn writeLatin1(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.ended) { + return .{ .owned = 0 }; + } + + const bytes = data.slice(); + const len = @as(Blob.SizeType, @truncate(bytes.len)); + + if (this.buffer.size() == 0 and len >= this.getHighWaterMark()) { + // common case + if (strings.isAllASCII(bytes)) { + // fast path: + // - large-ish chunk + this.send(bytes, false) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + return .{ .owned = len }; + } + + this.buffer.writeLatin1(bytes) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + + _ = this.internalFlush() catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + return .{ .owned = len }; + } else if (this.buffer.size() + len >= this.getHighWaterMark()) { + // kinda fast path: + // - combined chunk is large enough to flush automatically + this.buffer.writeLatin1(bytes) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + _ = this.internalFlush() catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + return .{ .owned = len }; + } else { + this.buffer.writeLatin1(bytes) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + } + + this.registerAutoFlusher(); + + return .{ .owned = len }; + } + pub fn writeUTF16(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.ended) { + return .{ .owned = 0 }; + } + const bytes = data.slice(); + // we must always buffer UTF-16 + // we assume the case of all-ascii UTF-16 string is pretty uncommon + this.buffer.writeUTF16(@alignCast(std.mem.bytesAsSlice(u16, bytes))) catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + + const readable = this.buffer.slice(); + if (readable.len >= this.getHighWaterMark()) { + _ = this.internalFlush() catch { + return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; + }; + return .{ .owned = @as(Blob.SizeType, @intCast(bytes.len)) }; + } + + this.registerAutoFlusher(); + return .{ .owned = @as(Blob.SizeType, @intCast(bytes.len)) }; + } + + pub fn end(this: *@This(), err: ?Syscall.Error) JSC.Maybe(void) { + if (this.ended) { + return .{ .result = {} }; + } + + // send EOF + this.ended = true; + // flush everything and send EOF + _ = this.internalFlush() catch 0; + + this.signal.close(err); + return .{ .result = {} }; + } + pub fn endFromJS(this: *@This(), _: *JSGlobalObject) JSC.Maybe(JSValue) { + if (!this.ended) { + if (this.done) { + this.ended = true; + this.signal.close(null); + this.finalize(); + } else { + _ = this.end(null); + } + } + const promise = this.endPromise.valueOrEmpty(); + if (promise.isEmptyOrUndefinedOrNull()) { + return .{ .result = JSC.JSValue.jsNumber(0) }; + } + return .{ .result = promise }; + } + pub fn toJS(this: *@This(), globalThis: *JSGlobalObject) JSValue { + return JSSink.createObject(globalThis, this, 0); + } + + pub fn memoryCost(this: *const @This()) usize { + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(ArrayBufferSink). + return this.buffer.memoryCost(); + } + + const name = "NetworkSink"; + pub const JSSink = NewJSSink(@This(), name); +}; pub const BufferedReadableStreamAction = enum { text, arrayBuffer, @@ -2618,6 +2994,7 @@ pub fn ReadableStreamSource( comptime deinit_fn: fn (this: *Context) void, comptime setRefUnrefFn: ?fn (this: *Context, enable: bool) void, comptime drainInternalBuffer: ?fn (this: *Context) bun.ByteList, + comptime memoryCostFn: ?fn (this: *const Context) usize, comptime toBufferedValue: ?fn (this: *Context, globalThis: *JSC.JSGlobalObject, action: BufferedReadableStreamAction) bun.JSError!JSC.JSValue, ) type { return struct { @@ -2782,6 +3159,14 @@ pub fn ReadableStreamSource( pub const arrayBufferFromJS = JSReadableStreamSource.arrayBuffer; pub const blobFromJS = JSReadableStreamSource.blob; pub const bytesFromJS = JSReadableStreamSource.bytes; + + pub fn memoryCost(this: *const ReadableStreamSourceType) usize { + if (memoryCostFn) |function| { + return function(&this.context) + @sizeOf(@This()); + } + return @sizeOf(@This()); + } + pub const JSReadableStreamSource = struct { pub fn pull(this: *ReadableStreamSourceType, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { JSC.markBinding(@src()); @@ -3039,6 +3424,11 @@ pub const FileSink = struct { pub const IOWriter = bun.io.StreamingWriter(@This(), onWrite, onError, onReady, onClose); pub const Poll = IOWriter; + pub fn memoryCost(this: *const FileSink) usize { + // Since this is a JSSink, the NewJSSink function does @sizeOf(JSSink) which includes @sizeOf(FileSink). + return this.writer.memoryCost(); + } + fn Bun__ForceFileSinkToBeSynchronousOnWindows(globalObject: *JSC.JSGlobalObject, jsvalue: JSC.JSValue) callconv(.C) void { comptime bun.assert(Environment.isWindows); @@ -3542,6 +3932,8 @@ pub const FileReader = struct { pending_view: []u8 = &.{}, fd: bun.FileDescriptor = bun.invalid_fd, start_offset: ?usize = null, + max_size: ?usize = null, + total_readed: usize = 0, started: bool = false, waiting_for_onReaderDone: bool = false, event_loop: JSC.EventLoopHandle, @@ -3697,7 +4089,7 @@ pub const FileReader = struct { var file_type: bun.io.FileType = .file; if (this.lazy == .blob) { switch (this.lazy.blob.data) { - .bytes => @panic("Invalid state in FileReader: expected file "), + .s3, .bytes => @panic("Invalid state in FileReader: expected file "), .file => |*file| { defer { this.lazy.blob.deref(); @@ -3820,15 +4212,32 @@ pub const FileReader = struct { } pub fn onReadChunk(this: *@This(), init_buf: []const u8, state: bun.io.ReadState) bool { - const buf = init_buf; + var buf = init_buf; log("onReadChunk() = {d} ({s})", .{ buf.len, @tagName(state) }); if (this.done) { this.reader.close(); return false; } + var close = false; + defer if (close) this.reader.close(); + var hasMore = state != .eof; - const hasMore = state != .eof; + if (buf.len > 0) { + if (this.max_size) |max_size| { + if (this.total_readed >= max_size) return false; + const len = @min(max_size - this.total_readed, buf.len); + if (buf.len > len) { + buf = buf[0..len]; + } + this.total_readed += len; + + if (buf.len == 0) { + close = true; + hasMore = false; + } + } + } if (this.read_inside_on_pull != .none) { switch (this.read_inside_on_pull) { @@ -4149,6 +4558,11 @@ pub const FileReader = struct { return this.reader.setRawMode(flag); } + pub fn memoryCost(this: *const FileReader) usize { + // ReadableStreamSource covers @sizeOf(FileReader) + return this.reader.memoryCost() + this.buffered.capacity; + } + pub const Source = ReadableStreamSource( @This(), "File", @@ -4158,6 +4572,7 @@ pub const FileReader = struct { deinit, setRefOrUnref, drain, + memoryCost, null, ); }; @@ -4299,6 +4714,14 @@ pub const ByteBlobLoader = struct { return .zero; } + pub fn memoryCost(this: *const ByteBlobLoader) usize { + // ReadableStreamSource covers @sizeOf(FileReader) + if (this.store) |store| { + return store.memoryCost(); + } + return 0; + } + pub const Source = ReadableStreamSource( @This(), "Blob", @@ -4308,6 +4731,7 @@ pub const ByteBlobLoader = struct { deinit, null, drain, + memoryCost, toBufferedValue, ); }; @@ -4362,6 +4786,8 @@ pub const ByteStream = struct { size_hint: Blob.SizeType = 0, buffer_action: ?BufferAction = null, + const log = Output.scoped(.ByteStream, false); + const BufferAction = union(BufferedReadableStreamAction) { text: JSC.JSPromise.Strong, arrayBuffer: JSC.JSPromise.Strong, @@ -4466,6 +4892,9 @@ pub const ByteStream = struct { if (stream == .owned) allocator.free(stream.owned.slice()); if (stream == .owned_and_done) allocator.free(stream.owned_and_done.slice()); } + this.has_received_last_chunk = stream.isDone(); + + log("ByteStream.onData already done... do nothing", .{}); return; } @@ -4489,6 +4918,8 @@ pub const ByteStream = struct { this.buffer_action = null; } + log("ByteStream.onData err action.reject()", .{}); + action.reject(stream.err); return; } @@ -4498,7 +4929,16 @@ pub const ByteStream = struct { this.buffer_action = null; } + if (this.buffer.capacity == 0 and stream == .done) { + log("ByteStream.onData done and action.fulfill()", .{}); + + var blob = this.toAnyBlob().?; + action.fulfill(&blob); + return; + } if (this.buffer.capacity == 0 and stream == .owned_and_done) { + log("ByteStream.onData owned_and_done and action.fulfill()", .{}); + this.buffer = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, @constCast(chunk)); var blob = this.toAnyBlob().?; action.fulfill(&blob); @@ -4509,10 +4949,12 @@ pub const ByteStream = struct { allocator.free(stream.slice()); } } + log("ByteStream.onData appendSlice and action.fulfill()", .{}); this.buffer.appendSlice(chunk) catch bun.outOfMemory(); var blob = this.toAnyBlob().?; action.fulfill(&blob); + return; } else { this.buffer.appendSlice(chunk) catch bun.outOfMemory(); @@ -4566,13 +5008,18 @@ pub const ByteStream = struct { } const remaining = chunk[to_copy.len..]; - if (remaining.len > 0) + if (remaining.len > 0 and chunk.len > 0) this.append(stream, to_copy.len, chunk, allocator) catch @panic("Out of memory while copying request body"); + log("ByteStream.onData pending.run()", .{}); + this.pending.run(); + return; } + log("ByteStream.onData no action just append", .{}); + this.append(stream, 0, chunk, allocator) catch @panic("Out of memory while copying request body"); } @@ -4602,6 +5049,7 @@ pub const ByteStream = struct { .err => { this.pending.result = .{ .err = stream.err }; }, + .done => {}, else => unreachable, } return; @@ -4622,6 +5070,7 @@ pub const ByteStream = struct { this.pending.result = .{ .err = stream.err }; }, + .done => {}, // We don't support the rest of these yet else => unreachable, } @@ -4714,6 +5163,11 @@ pub const ByteStream = struct { } } + pub fn memoryCost(this: *const @This()) usize { + // ReadableStreamSource covers @sizeOf(ByteStream) + return this.buffer.capacity; + } + pub fn deinit(this: *@This()) void { JSC.markBinding(@src()); if (this.buffer.capacity > 0) this.buffer.clearAndFree(); @@ -4809,6 +5263,7 @@ pub const ByteStream = struct { deinit, null, drain, + memoryCost, toBufferedValue, ); }; diff --git a/src/bun.zig b/src/bun.zig index 352c6c9148..806cf42ae7 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -11,28 +11,28 @@ const bun = @This(); pub const Environment = @import("env.zig"); -pub const use_mimalloc = !Environment.isTest; +pub const use_mimalloc = true; pub const default_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").c_allocator; + @import("./allocators/memory_allocator.zig").c_allocator; /// Zeroing memory allocator pub const z_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").z_allocator; + @import("./allocators/memory_allocator.zig").z_allocator; pub const huge_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").huge_allocator; + @import("./allocators/memory_allocator.zig").huge_allocator; pub const auto_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").auto_allocator; + @import("./allocators/memory_allocator.zig").auto_allocator; pub const callmod_inline: std.builtin.CallModifier = if (builtin.mode == .Debug) .auto else .always_inline; pub const callconv_inline: std.builtin.CallingConvention = if (builtin.mode == .Debug) .Unspecified else .Inline; @@ -115,6 +115,12 @@ pub const fmt = @import("./fmt.zig"); pub const allocators = @import("./allocators.zig"); pub const bun_js = @import("./bun_js.zig"); +/// All functions and interfaces provided from Bun's `bindgen` utility. +pub const gen = @import("bun.js/bindings/GeneratedBindings.zig"); +comptime { + _ = &gen; // reference bindings +} + /// Copied from Zig std.trait pub const trait = @import("./trait.zig"); /// Copied from Zig std.Progress before 0.13 rewrite @@ -550,7 +556,7 @@ pub const StringBuilder = @import("./string_builder.zig"); pub const LinearFifo = @import("./linear_fifo.zig").LinearFifo; pub const linux = struct { - pub const memfd_allocator = @import("./linux_memfd_allocator.zig").LinuxMemFdAllocator; + pub const memfd_allocator = @import("./allocators/linux_memfd_allocator.zig").LinuxMemFdAllocator; }; /// hash a string @@ -881,7 +887,7 @@ pub fn openDirAbsoluteNotForDeletingOrRenaming(path_: []const u8) !std.fs.Dir { return fd.asDir(); } -pub const MimallocArena = @import("./mimalloc_arena.zig").Arena; +pub const MimallocArena = @import("./allocators/mimalloc_arena.zig").Arena; pub fn getRuntimeFeatureFlag(comptime flag: [:0]const u8) bool { return struct { const state = enum(u8) { idk, disabled, enabled }; @@ -1275,7 +1281,7 @@ pub const SignalCode = enum(u8) { return @enumFromInt(std.mem.asBytes(&value)[0]); } - // This wrapper struct is lame, what if bun's color formatter was more versitile + // This wrapper struct is lame, what if bun's color formatter was more versatile const Fmt = struct { signal: SignalCode, enable_ansi_colors: bool, @@ -1325,8 +1331,8 @@ pub const PackageManager = install.PackageManager; pub const RunCommand = @import("./cli/run_command.zig").RunCommand; pub const fs = @import("./fs.zig"); -pub const Bundler = bundler.Bundler; -pub const bundler = @import("./bundler.zig"); +pub const Transpiler = transpiler.Transpiler; +pub const transpiler = @import("./transpiler.zig"); pub const which = @import("./which.zig").which; pub const js_parser = @import("./js_parser.zig"); pub const js_printer = @import("./js_printer.zig"); @@ -1393,10 +1399,10 @@ fn getFdPathViaCWD(fd: std.posix.fd_t, buf: *[@This().MAX_PATH_BYTES]u8) ![]u8 { pub const getcwd = std.posix.getcwd; -pub fn getcwdAlloc(allocator: std.mem.Allocator) ![]u8 { +pub fn getcwdAlloc(allocator: std.mem.Allocator) ![:0]u8 { var temp: PathBuffer = undefined; const temp_slice = try getcwd(&temp); - return allocator.dupe(u8, temp_slice); + return allocator.dupeZ(u8, temp_slice); } /// Get the absolute path to a file descriptor. @@ -1601,12 +1607,13 @@ pub const fast_debug_build_mode = fast_debug_build_cmd != .None and pub const MultiArrayList = @import("./multi_array_list.zig").MultiArrayList; pub const StringJoiner = @import("./StringJoiner.zig"); -pub const NullableAllocator = @import("./NullableAllocator.zig"); +pub const NullableAllocator = @import("./allocators/NullableAllocator.zig"); pub const renamer = @import("./renamer.zig"); // TODO: Rename to SourceMap as this is a struct. pub const sourcemap = @import("./sourcemap/sourcemap.zig"); +/// Attempt to coerce some value into a byte slice. pub fn asByteSlice(buffer: anytype) []const u8 { return switch (@TypeOf(buffer)) { []const u8, []u8, [:0]const u8, [:0]u8 => buffer.ptr[0..buffer.len], @@ -1848,6 +1855,17 @@ pub const StringSet = struct { pub const Map = StringArrayHashMap(void); + pub fn clone(self: StringSet) !StringSet { + var new_map = Map.init(self.map.allocator); + try new_map.ensureTotalCapacity(self.map.count()); + for (self.map.keys()) |key| { + new_map.putAssumeCapacity(try self.map.allocator.dupe(u8, key), {}); + } + return StringSet{ + .map = new_map, + }; + } + pub fn init(allocator: std.mem.Allocator) StringSet { return StringSet{ .map = Map.init(allocator), @@ -1865,6 +1883,14 @@ pub const StringSet = struct { } } + pub fn contains(self: *StringSet, key: []const u8) bool { + return self.map.contains(key); + } + + pub fn swapRemove(self: *StringSet, key: []const u8) bool { + return self.map.swapRemove(key); + } + pub fn deinit(self: *StringSet) void { for (self.map.keys()) |key| { self.map.allocator.free(key); @@ -1882,6 +1908,13 @@ pub const StringMap = struct { pub const Map = StringArrayHashMap(string); + pub fn clone(self: StringMap) !StringMap { + return StringMap{ + .map = try self.map.clone(), + .dupe_keys = self.dupe_keys, + }; + } + pub fn init(allocator: std.mem.Allocator, dupe_keys: bool) StringMap { return StringMap{ .map = Map.init(allocator), @@ -1949,7 +1982,8 @@ pub const bundle_v2 = @import("./bundler/bundle_v2.zig"); pub const BundleV2 = bundle_v2.BundleV2; pub const ParseTask = bundle_v2.ParseTask; -pub const Lock = @import("./lock.zig").Lock; +pub const Lock = @compileError("Use bun.Mutex instead"); +pub const Mutex = @import("./Mutex.zig"); pub const UnboundedQueue = @import("./bun.js/unbounded_queue.zig").UnboundedQueue; pub fn threadlocalAllocator() std.mem.Allocator { @@ -2030,7 +2064,7 @@ pub fn HiveRef(comptime T: type, comptime capacity: u16) type { }; } -pub const MaxHeapAllocator = @import("./max_heap_allocator.zig").MaxHeapAllocator; +pub const MaxHeapAllocator = @import("./allocators/max_heap_allocator.zig").MaxHeapAllocator; pub const tracy = @import("./tracy.zig"); pub const trace = tracy.trace; @@ -2758,8 +2792,6 @@ pub const MakePath = struct { @ptrCast(component.path)) else try w.sliceToPrefixedFileW(self.fd, component.path); - const is_last = it.peekNext() == null; - _ = is_last; // autofix var result = makeOpenDirAccessMaskW(self, sub_path_w.span().ptr, access_mask, .{ .no_follow = no_follow, .create_disposition = w.FILE_OPEN_IF, @@ -3225,6 +3257,19 @@ pub fn exitThread() noreturn { } } +pub fn deleteAllPoolsForThreadExit() void { + const pools_to_delete = .{ + JSC.WebCore.ByteListPool, + bun.WPathBufferPool, + bun.PathBufferPool, + bun.JSC.ConsoleObject.Formatter.Visited.Pool, + bun.js_parser.StringVoidMap.Pool, + }; + inline for (pools_to_delete) |pool| { + pool.deleteAll(); + } +} + pub const Tmpfile = @import("./tmp.zig").Tmpfile; pub const io = @import("./io/io.zig"); @@ -3326,12 +3371,23 @@ pub inline fn resolveSourcePath( } const RuntimeEmbedRoot = enum { + /// Relative to `/codegen`. codegen, + /// Relative to `src` src, + /// Reallocates the slice at every call. Avoid this if possible. An example + /// using this reasonably is referencing incremental_visualizer.html, which + /// is reloaded from disk for each request, but more importantly allows + /// maintaining the DevServer state while hacking on the visualizer. src_eager, + /// Avoid this if possible. See `.src_eager`. codegen_eager, }; +/// Load a file at runtime. This is only to be used in debug builds, +/// specifically when `Environment.codegen_embed` is false. This allows quick +/// iteration on files, as this skips the Zig compiler. Once Zig gains good +/// incremental support, the non-eager cases can be deleted. pub fn runtimeEmbedFile( comptime root: RuntimeEmbedRoot, comptime sub_path: []const u8, @@ -3414,7 +3470,7 @@ pub fn selfExePath() ![:0]u8 { 4096 + 1 // + 1 for the null terminator ]u8 = undefined; var len: usize = 0; - var lock: Lock = .{}; + var lock: Mutex = .{}; pub fn load() ![:0]u8 { const init = try std.fs.selfExePath(&value); @@ -3874,7 +3930,7 @@ pub fn WeakPtr(comptime T: type, comptime weakable_field: std.meta.FieldEnum(T)) pub const DebugThreadLock = if (Environment.allow_assert) struct { - owning_thread: ?std.Thread.Id = null, + owning_thread: ?std.Thread.Id, locked_at: crash_handler.StoredTrace, pub const unlocked: DebugThreadLock = .{ @@ -4050,7 +4106,7 @@ pub fn Once(comptime f: anytype) type { done: bool = false, payload: Return = undefined, - mutex: std.Thread.Mutex = .{}, + mutex: bun.Mutex = .{}, /// Call the function `f`. /// If `call` is invoked multiple times `f` will be executed only the @@ -4118,3 +4174,193 @@ pub inline fn isComptimeKnown(x: anytype) bool { pub inline fn itemOrNull(comptime T: type, slice: []const T, index: usize) ?T { return if (index < slice.len) slice[index] else null; } + +/// To handle stack overflows: +/// 1. StackCheck.init() +/// 2. .isSafeToRecurse() +pub const StackCheck = struct { + cached_stack_end: usize = 0, + + extern fn Bun__StackCheck__initialize() void; + pub fn configureThread() void { + Bun__StackCheck__initialize(); + } + + extern "C" fn Bun__StackCheck__getMaxStack() usize; + fn getStackEnd() usize { + return Bun__StackCheck__getMaxStack(); + } + + pub fn init() StackCheck { + return StackCheck{ .cached_stack_end = getStackEnd() }; + } + + pub fn update(this: *StackCheck) void { + this.cached_stack_end = getStackEnd(); + } + + /// Is there at least 128 KB of stack space available? + pub fn isSafeToRecurse(this: StackCheck) bool { + const stack_ptr: usize = @frameAddress(); + const remaining_stack = stack_ptr -| this.cached_stack_end; + return remaining_stack > 1024 * if (Environment.isWindows) 256 else 128; + } +}; + +// Workaround for lack of branch hints. +pub noinline fn throwStackOverflow() StackOverflow!void { + @setCold(true); + return error.StackOverflow; +} +const StackOverflow = error{StackOverflow}; + +// This pool exists because on Windows, each path buffer costs 64 KB. +// This makes the stack memory usage very unpredictable, which means we can't really know how much stack space we have left. +// This pool is a workaround to make the stack memory usage more predictable. +// We keep up to 4 path buffers alive per thread at a time. +pub fn PathBufferPoolT(comptime T: type) type { + return struct { + const Pool = ObjectPool(PathBuf, null, true, 4); + pub const PathBuf = struct { + bytes: T, + + pub fn deinit(this: *PathBuf) void { + var node: *Pool.Node = @alignCast(@fieldParentPtr("data", this)); + node.release(); + } + }; + + pub fn get() *T { + // use a threadlocal allocator so mimalloc deletes it on thread deinit. + return &Pool.get(bun.threadlocalAllocator()).data.bytes; + } + + pub fn put(buffer: *T) void { + var path_buf: *PathBuf = @alignCast(@fieldParentPtr("bytes", buffer)); + path_buf.deinit(); + } + + pub fn deleteAll() void { + Pool.deleteAll(); + } + }; +} + +pub const PathBufferPool = PathBufferPoolT(bun.PathBuffer); +pub const WPathBufferPool = if (Environment.isWindows) PathBufferPoolT(bun.WPathBuffer) else struct { + // So it can be used in code that deletes all the pools. + pub fn deleteAll() void {} +}; +pub const OSPathBufferPool = if (Environment.isWindows) WPathBufferPool else PathBufferPool; + +pub const S3 = @import("./s3/client.zig"); + +const CowString = CowSlice(u8); + +/// "Copy on write" slice. There are many instances when it is desired to re-use +/// a slice, but doing so would make it unknown if that slice should be freed. +/// This structure, in release builds, is the same size as `[]const T`, but +/// stores one bit for if deinitialziation should free the underlying memory. +/// +/// const str = CowSlice(u8).initOwned(try alloc.dupe(u8, "hello!"), alloc); +/// const borrow = str.borrow(); +/// assert(borrow.slice().ptr == str.slice().ptr) +/// borrow.deinit(alloc); // knows it is borrowed, no free +/// str.deinit(alloc); // calls free +/// +/// In a debug build, there are aggressive assertions to ensure unintentional +/// frees do not happen. But in a release build, the developer is expected to +/// keep slice owners alive beyond the lifetimes of the borrowed instances. +/// +/// CowSlice does not support slices longer than 2^(@bitSizeOf(usize)-1). +pub fn CowSlice(T: type) type { + const DebugData = if (Environment.allow_assert) struct { + mutex: std.Thread.Mutex, + allocator: Allocator, + borrows: usize, + }; + return struct { + ptr: [*]const T, + flags: packed struct(usize) { + len: @Type(.{ .Int = .{ + .bits = @bitSizeOf(usize) - 1, + .signedness = .unsigned, + } }), + is_owned: bool, + }, + debug: if (Environment.allow_assert) ?*DebugData else void, + + const cow_str_assertions = Environment.isDebug; + + /// `data` is transferred into the returned string, and must be freed with + /// `.deinit()` when the string and its borrows are done being used. + pub fn initOwned(data: []const T, allocator: Allocator) @This() { + return .{ + .ptr = data.ptr, + .flags = .{ + .is_owned = true, + .len = @intCast(data.len), + }, + .debug = if (cow_str_assertions) + bun.new(DebugData(.{ + .mutex = .{}, + .allocator = allocator, + .borrows = 0, + })), + }; + } + + /// `.deinit` will not free memory from this slice. + pub fn initNeverFree(data: []const T) @This() { + return .{ + .ptr = data.ptr, + .flags = .{ + .is_owned = false, + .len = @intCast(data.len), + }, + .debug = null, + }; + } + + pub fn slice(str: @This()) []const T { + return str.ptr[0..str.flags.len]; + } + + /// Returns a new string. The borrowed string should be deinitialized + /// so that debug assertions that perform. + pub fn borrow(str: @This()) @This() { + if (cow_str_assertions) if (str.debug) |debug| { + debug.mutex.lock(); + defer debug.mutex.unlock(); + debug.borrows += 1; + }; + return .{ + .ptr = str.ptr, + .flags = .{ + .is_owned = false, + .len = str.flags.len, + }, + .debug = str.debug, + }; + } + + pub fn deinit(str: @This(), allocator: Allocator) void { + if (cow_str_assertions) if (str.debug) |debug| { + debug.mutex.lock(); + defer debug.mutex.unlock(); + bun.assert(debug.allocator == allocator); + if (str.flags.is_owned) { + bun.assert(debug.borrows == 0); // active borrows become invalid data + } else { + debug.borrows -= 1; // double deinit of a borrowed string + } + bun.destroy(debug); + }; + if (str.flags.is_owned) { + allocator.free(str.slice()); + } + } + }; +} + +const Allocator = std.mem.Allocator; diff --git a/src/bun_js.zig b/src/bun_js.zig index 29198e5a73..3c25404f89 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -24,12 +24,13 @@ const Api = @import("api/schema.zig").Api; const resolve_path = @import("./resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("./bun.js/config.zig").configureTransformOptionsForBun; const Command = @import("cli.zig").Command; -const bundler = bun.bundler; +const transpiler = bun.transpiler; const DotEnv = @import("env_loader.zig"); const which = @import("which.zig").which; const JSC = bun.JSC; const AsyncHTTP = bun.http.AsyncHTTP; -const Arena = @import("./mimalloc_arena.zig").Arena; +const Arena = @import("./allocators/mimalloc_arena.zig").Arena; +const DNSResolver = @import("bun.js/api/bun/dns_resolver.zig").DNSResolver; const OpaqueWrap = JSC.OpaqueWrap; const VirtualMachine = JSC.VirtualMachine; @@ -64,6 +65,7 @@ pub const Run = struct { .log = ctx.log, .args = ctx.args, .graph = graph_ptr, + .is_main_thread = true, }), .arena = arena, .ctx = ctx, @@ -71,7 +73,7 @@ pub const Run = struct { }; var vm = run.vm; - var b = &vm.bundler; + var b = &vm.transpiler; vm.preload = ctx.preloads; vm.argv = ctx.passthrough; vm.arena = &run.arena; @@ -93,7 +95,7 @@ pub const Run = struct { b.resolver.opts.minify_identifiers = ctx.bundler_options.minify_identifiers; b.resolver.opts.minify_whitespace = ctx.bundler_options.minify_whitespace; - b.options.experimental_css = ctx.bundler_options.experimental_css; + b.options.experimental = ctx.bundler_options.experimental; // b.options.minify_syntax = ctx.bundler_options.minify_syntax; @@ -155,7 +157,7 @@ pub const Run = struct { @setCold(true); // this is a hack: make dummy bundler so we can use its `.runEnvLoader()` function to populate environment variables probably should split out the functionality - var bundle = try bun.Bundler.init( + var bundle = try bun.Transpiler.init( ctx.allocator, ctx.log, try @import("./bun.js/config.zig").configureTransformOptionsForBunVM(ctx.allocator, ctx.args), @@ -198,6 +200,8 @@ pub const Run = struct { .smol = ctx.runtime_options.smol, .eval = ctx.runtime_options.eval.eval_and_print, .debugger = ctx.runtime_options.debugger, + .dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order), + .is_main_thread = true, }, ), .arena = arena, @@ -206,7 +210,7 @@ pub const Run = struct { }; var vm = run.vm; - var b = &vm.bundler; + var b = &vm.transpiler; vm.preload = ctx.preloads; vm.argv = ctx.passthrough; vm.arena = &run.arena; @@ -262,13 +266,13 @@ pub const Run = struct { JSC.VirtualMachine.is_main_thread_vm = true; // Allow setting a custom timezone - if (vm.bundler.env.get("TZ")) |tz| { + if (vm.transpiler.env.get("TZ")) |tz| { if (tz.len > 0) { _ = vm.global.setTimeZone(&JSC.ZigString.init(tz)); } } - vm.bundler.env.loadTracy(); + vm.transpiler.env.loadTracy(); doPreconnect(ctx.runtime_options.preconnect); @@ -281,16 +285,12 @@ pub const Run = struct { run.any_unhandled = true; } - extern fn Bun__ExposeNodeModuleGlobals(*JSC.JSGlobalObject) void; - pub fn start(this: *Run) void { var vm = this.vm; vm.hot_reload = this.ctx.debug.hot_reload; vm.onUnhandledRejection = &onUnhandledRejectionBeforeClose; - if (this.ctx.runtime_options.eval.script.len > 0) { - Bun__ExposeNodeModuleGlobals(vm.global); - } + this.addConditionalGlobals(); switch (this.ctx.debug.hot_reload) { .hot => JSC.HotReloader.enableHotModuleReloading(vm), @@ -298,8 +298,8 @@ pub const Run = struct { else => {}, } - if (strings.eqlComptime(this.entry_path, ".") and vm.bundler.fs.top_level_dir.len > 0) { - this.entry_path = vm.bundler.fs.top_level_dir; + if (strings.eqlComptime(this.entry_path, ".") and vm.transpiler.fs.top_level_dir.len > 0) { + this.entry_path = vm.transpiler.fs.top_level_dir; } if (vm.loadEntryPoint(this.entry_path)) |promise| { @@ -446,6 +446,22 @@ pub const Run = struct { if (!JSC.is_bindgen) JSC.napi.fixDeadCodeElimination(); vm.globalExit(); } + + extern fn Bun__ExposeNodeModuleGlobals(*JSC.JSGlobalObject) void; + /// add `gc()` to `globalThis`. + extern fn JSC__JSGlobalObject__addGc(*JSC.JSGlobalObject) void; + + fn addConditionalGlobals(this: *Run) void { + const vm = this.vm; + const runtime_options: *const Command.RuntimeOptions = &this.ctx.runtime_options; + + if (runtime_options.eval.script.len > 0) { + Bun__ExposeNodeModuleGlobals(vm.global); + } + if (runtime_options.expose_gc) { + JSC__JSGlobalObject__addGc(vm.global); + } + } }; pub export fn Bun__onResolveEntryPointResult(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) noreturn { diff --git a/src/bundler.zig b/src/bundler.zig deleted file mode 100644 index 5a11d98b28..0000000000 --- a/src/bundler.zig +++ /dev/null @@ -1,1967 +0,0 @@ -const bun = @import("root").bun; -const string = bun.string; -const Output = bun.Output; -const Global = bun.Global; -const Environment = bun.Environment; -const strings = bun.strings; -const MutableString = bun.MutableString; -const stringZ = bun.stringZ; -const default_allocator = bun.default_allocator; -const StoredFileDescriptorType = bun.StoredFileDescriptorType; -const FeatureFlags = bun.FeatureFlags; -const C = bun.C; -const std = @import("std"); -const lex = bun.js_lexer; -const logger = bun.logger; -const options = @import("options.zig"); -const js_parser = bun.js_parser; -const JSON = bun.JSON; -const js_printer = bun.js_printer; -const js_ast = bun.JSAst; -const linker = @import("linker.zig"); -const Ref = @import("ast/base.zig").Ref; -const Define = @import("defines.zig").Define; -const DebugOptions = @import("./cli.zig").Command.DebugOptions; -const ThreadPoolLib = @import("./thread_pool.zig"); - -const Fs = @import("fs.zig"); -const schema = @import("api/schema.zig"); -const Api = schema.Api; -const _resolver = @import("./resolver/resolver.zig"); -const sync = @import("sync.zig"); -const ImportRecord = @import("./import_record.zig").ImportRecord; -const allocators = @import("./allocators.zig"); -const MimeType = @import("./http/mime_type.zig"); -const resolve_path = @import("./resolver/resolve_path.zig"); -const runtime = @import("./runtime.zig"); -const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; -const MacroRemap = @import("./resolver/package_json.zig").MacroMap; -const DebugLogs = _resolver.DebugLogs; -const Router = @import("./router.zig"); -const isPackagePath = _resolver.isPackagePath; -const Css = @import("css_scanner.zig"); -const DotEnv = @import("./env_loader.zig"); -const Lock = @import("./lock.zig").Lock; -const NodeFallbackModules = @import("./node_fallbacks.zig"); -const CacheEntry = @import("./cache.zig").FsCacheEntry; -const Analytics = @import("./analytics/analytics_thread.zig"); -const URL = @import("./url.zig").URL; -const Linker = linker.Linker; -const Resolver = _resolver.Resolver; -const TOML = @import("./toml/toml_parser.zig").TOML; -const JSC = bun.JSC; -const PackageManager = @import("./install/install.zig").PackageManager; -const DataURL = @import("./resolver/data_url.zig").DataURL; - -pub fn MacroJSValueType_() type { - if (comptime JSC.is_bindgen) { - return struct { - pub const zero = @This(){}; - }; - } - return JSC.JSValue; -} -pub const MacroJSValueType = MacroJSValueType_(); -const default_macro_js_value = if (JSC.is_bindgen) MacroJSValueType{} else JSC.JSValue.zero; - -const EntryPoints = @import("./bundler/entry_points.zig"); -const SystemTimer = @import("./system_timer.zig").Timer; -pub usingnamespace EntryPoints; -pub const ParseResult = struct { - source: logger.Source, - loader: options.Loader, - ast: js_ast.Ast, - already_bundled: AlreadyBundled = .none, - input_fd: ?StoredFileDescriptorType = null, - empty: bool = false, - pending_imports: _resolver.PendingResolution.List = .{}, - - runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache = null, - - pub const AlreadyBundled = union(enum) { - none: void, - source_code: void, - source_code_cjs: void, - bytecode: []u8, - bytecode_cjs: []u8, - - pub fn bytecodeSlice(this: AlreadyBundled) []u8 { - return switch (this) { - inline .bytecode, .bytecode_cjs => |slice| slice, - else => &.{}, - }; - } - - pub fn isBytecode(this: AlreadyBundled) bool { - return this == .bytecode or this == .bytecode_cjs; - } - - pub fn isCommonJS(this: AlreadyBundled) bool { - return this == .source_code_cjs or this == .bytecode_cjs; - } - }; - - pub fn isPendingImport(this: *const ParseResult, id: u32) bool { - const import_record_ids = this.pending_imports.items(.import_record_id); - - return std.mem.indexOfScalar(u32, import_record_ids, id) != null; - } - - /// **DO NOT CALL THIS UNDER NORMAL CIRCUMSTANCES** - /// Normally, we allocate each AST in an arena and free all at once - /// So this function only should be used when we globally allocate an AST - pub fn deinit(this: *ParseResult) void { - _resolver.PendingResolution.deinitListItems(this.pending_imports, bun.default_allocator); - this.pending_imports.deinit(bun.default_allocator); - this.ast.deinit(); - bun.default_allocator.free(@constCast(this.source.contents)); - } -}; - -const cache_files = false; - -pub const PluginRunner = struct { - global_object: *JSC.JSGlobalObject, - allocator: std.mem.Allocator, - - pub fn extractNamespace(specifier: string) string { - const colon = strings.indexOfChar(specifier, ':') orelse return ""; - if (Environment.isWindows and - colon == 1 and - specifier.len > 3 and - bun.path.isSepAny(specifier[2]) and - ((specifier[0] > 'a' and specifier[0] < 'z') or (specifier[0] > 'A' and specifier[0] < 'Z'))) - return ""; - return specifier[0..colon]; - } - - pub fn couldBePlugin(specifier: string) bool { - if (strings.lastIndexOfChar(specifier, '.')) |last_dor| { - const ext = specifier[last_dor + 1 ..]; - // '.' followed by either a letter or a non-ascii character - // maybe there are non-ascii file extensions? - // we mostly want to cheaply rule out "../" and ".." and "./" - if (ext.len > 0 and ((ext[0] >= 'a' and ext[0] <= 'z') or (ext[0] >= 'A' and ext[0] <= 'Z') or ext[0] > 127)) - return true; - } - return (!std.fs.path.isAbsolute(specifier) and strings.containsChar(specifier, ':')); - } - - pub fn onResolve( - this: *PluginRunner, - specifier: []const u8, - importer: []const u8, - log: *logger.Log, - loc: logger.Loc, - target: JSC.JSGlobalObject.BunPluginTarget, - ) bun.JSError!?Fs.Path { - var global = this.global_object; - const namespace_slice = extractNamespace(specifier); - const namespace = if (namespace_slice.len > 0 and !strings.eqlComptime(namespace_slice, "file")) - bun.String.init(namespace_slice) - else - bun.String.empty; - const on_resolve_plugin = global.runOnResolvePlugins( - namespace, - bun.String.init(specifier).substring(if (namespace.length() > 0) namespace.length() + 1 else 0), - bun.String.init(importer), - target, - ) orelse return null; - const path_value = try on_resolve_plugin.get(global, "path") orelse return null; - if (path_value.isEmptyOrUndefinedOrNull()) return null; - if (!path_value.isString()) { - log.addError(null, loc, "Expected \"path\" to be a string") catch unreachable; - return null; - } - - const file_path = path_value.toBunString(global); - defer file_path.deref(); - - if (file_path.length() == 0) { - log.addError( - null, - loc, - "Expected \"path\" to be a non-empty string in onResolve plugin", - ) catch unreachable; - return null; - } else if - // TODO: validate this better - (file_path.eqlComptime(".") or - file_path.eqlComptime("..") or - file_path.eqlComptime("...") or - file_path.eqlComptime(" ")) - { - log.addError( - null, - loc, - "Invalid file path from onResolve plugin", - ) catch unreachable; - return null; - } - var static_namespace = true; - const user_namespace: bun.String = brk: { - if (try on_resolve_plugin.get(global, "namespace")) |namespace_value| { - if (!namespace_value.isString()) { - log.addError(null, loc, "Expected \"namespace\" to be a string") catch unreachable; - return null; - } - - const namespace_str = namespace_value.toBunString(global); - if (namespace_str.length() == 0) { - namespace_str.deref(); - break :brk bun.String.init("file"); - } - - if (namespace_str.eqlComptime("file")) { - namespace_str.deref(); - break :brk bun.String.init("file"); - } - - if (namespace_str.eqlComptime("bun")) { - namespace_str.deref(); - break :brk bun.String.init("bun"); - } - - if (namespace_str.eqlComptime("node")) { - namespace_str.deref(); - break :brk bun.String.init("node"); - } - - static_namespace = false; - - break :brk namespace_str; - } - - break :brk bun.String.init("file"); - }; - defer user_namespace.deref(); - - if (static_namespace) { - return Fs.Path.initWithNamespace( - std.fmt.allocPrint(this.allocator, "{any}", .{file_path}) catch unreachable, - user_namespace.byteSlice(), - ); - } else { - return Fs.Path.initWithNamespace( - std.fmt.allocPrint(this.allocator, "{any}", .{file_path}) catch unreachable, - std.fmt.allocPrint(this.allocator, "{any}", .{user_namespace}) catch unreachable, - ); - } - } - - pub fn onResolveJSC(this: *const PluginRunner, namespace: bun.String, specifier: bun.String, importer: bun.String, target: JSC.JSGlobalObject.BunPluginTarget) bun.JSError!?JSC.ErrorableString { - var global = this.global_object; - const on_resolve_plugin = global.runOnResolvePlugins( - if (namespace.length() > 0 and !namespace.eqlComptime("file")) - namespace - else - bun.String.static(""), - specifier, - importer, - target, - ) orelse return null; - const path_value = try on_resolve_plugin.get(global, "path") orelse return null; - if (path_value.isEmptyOrUndefinedOrNull()) return null; - if (!path_value.isString()) { - return JSC.ErrorableString.err( - error.JSErrorObject, - bun.String.static("Expected \"path\" to be a string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), - ); - } - - const file_path = path_value.toBunString(global); - - if (file_path.length() == 0) { - return JSC.ErrorableString.err( - error.JSErrorObject, - bun.String.static("Expected \"path\" to be a non-empty string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), - ); - } else if - // TODO: validate this better - (file_path.eqlComptime(".") or - file_path.eqlComptime("..") or - file_path.eqlComptime("...") or - file_path.eqlComptime(" ")) - { - return JSC.ErrorableString.err( - error.JSErrorObject, - bun.String.static("\"path\" is invalid in onResolve plugin").toErrorInstance(this.global_object).asVoid(), - ); - } - var static_namespace = true; - const user_namespace: bun.String = brk: { - if (try on_resolve_plugin.get(global, "namespace")) |namespace_value| { - if (!namespace_value.isString()) { - return JSC.ErrorableString.err( - error.JSErrorObject, - bun.String.static("Expected \"namespace\" to be a string").toErrorInstance(this.global_object).asVoid(), - ); - } - - const namespace_str = namespace_value.toBunString(global); - if (namespace_str.length() == 0) { - break :brk bun.String.static("file"); - } - - if (namespace_str.eqlComptime("file")) { - defer namespace_str.deref(); - break :brk bun.String.static("file"); - } - - if (namespace_str.eqlComptime("bun")) { - defer namespace_str.deref(); - break :brk bun.String.static("bun"); - } - - if (namespace_str.eqlComptime("node")) { - defer namespace_str.deref(); - break :brk bun.String.static("node"); - } - - static_namespace = false; - - break :brk namespace_str; - } - - break :brk bun.String.static("file"); - }; - defer user_namespace.deref(); - - // Our super slow way of cloning the string into memory owned by JSC - const combined_string = std.fmt.allocPrint( - this.allocator, - "{any}:{any}", - .{ user_namespace, file_path }, - ) catch unreachable; - var out_ = bun.String.init(combined_string); - const out = out_.toJS(this.global_object).toBunString(this.global_object); - this.allocator.free(combined_string); - return JSC.ErrorableString.ok(out); - } -}; - -/// This structure was the JavaScript bundler before bundle_v2 was written. It now -/// acts mostly as a configuration object, but it also contains stateful logic around -/// logging errors (.log) and module resolution (.resolve_queue) -/// -/// This object is not exclusive to bundle_v2/Bun.build, one of these is stored -/// on every VM so that the options can be used for transpilation. -pub const Bundler = struct { - options: options.BundleOptions, - log: *logger.Log, - allocator: std.mem.Allocator, - result: options.TransformResult, - resolver: Resolver, - fs: *Fs.FileSystem, - output_files: std.ArrayList(options.OutputFile), - resolve_results: *ResolveResults, - resolve_queue: ResolveQueue, - elapsed: u64 = 0, - needs_runtime: bool = false, - router: ?Router = null, - source_map: options.SourceMapOption = .none, - - linker: Linker, - timer: SystemTimer, - env: *DotEnv.Loader, - - macro_context: ?js_ast.Macro.MacroContext = null, - - pub const isCacheEnabled = cache_files; - - pub fn clone(this: *Bundler, allocator: std.mem.Allocator, to: *Bundler) !void { - to.* = this.*; - to.setAllocator(allocator); - to.log = try allocator.create(logger.Log); - to.log.* = logger.Log.init(allocator); - to.setLog(to.log); - to.macro_context = null; - to.linker.resolver = &to.resolver; - } - - pub inline fn getPackageManager(this: *Bundler) *PackageManager { - return this.resolver.getPackageManager(); - } - - pub fn setLog(this: *Bundler, log: *logger.Log) void { - this.log = log; - this.linker.log = log; - this.resolver.log = log; - } - - pub fn setAllocator(this: *Bundler, allocator: std.mem.Allocator) void { - this.allocator = allocator; - this.linker.allocator = allocator; - this.resolver.allocator = allocator; - } - - fn _resolveEntryPoint(bundler: *Bundler, entry_point: string) !_resolver.Result { - return bundler.resolver.resolveWithFramework(bundler.fs.top_level_dir, entry_point, .entry_point) catch |err| { - // Relative entry points that were not resolved to a node_modules package are - // interpreted as relative to the current working directory. - if (!std.fs.path.isAbsolute(entry_point) and - !(strings.hasPrefix(entry_point, "./") or 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.PathBuffer = 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| { - // Normalized with trailing slash - break :name bun.strings.normalizeSlashesOnly(&cache_bust_buf, dir, std.fs.path.sep); - } - } - - var parts = [_]string{ - entry_point, - bun.pathLiteral(".."), - }; - - break :name bun.path.joinAbsStringBufZ( - bundler.fs.top_level_dir, - &cache_bust_buf, - &parts, - .auto, - ); - }; - - // Only re-query if we previously had something cached. - if (bundler.resolver.bustDirCache(bun.strings.withoutTrailingSlashWindowsPath(buster_name))) { - if (_resolveEntryPoint(bundler, entry_point)) |result| - return result - else |_| { - // ignore this error, we will print the original error - } - } - - 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, - opts: Api.TransformOptions, - env_loader_: ?*DotEnv.Loader, - ) !Bundler { - js_ast.Expr.Data.Store.create(); - js_ast.Stmt.Data.Store.create(); - - const fs = try Fs.FileSystem.init(opts.absolute_working_dir); - const bundle_options = try options.BundleOptions.fromApi( - allocator, - fs, - log, - opts, - ); - - var env_loader: *DotEnv.Loader = env_loader_ orelse DotEnv.instance orelse brk: { - const map = try allocator.create(DotEnv.Map); - map.* = DotEnv.Map.init(allocator); - - const loader = try allocator.create(DotEnv.Loader); - loader.* = DotEnv.Loader.init(map, allocator); - break :brk loader; - }; - - if (DotEnv.instance == null) { - DotEnv.instance = env_loader; - } - - // hide elapsed time when loglevel is warn or error - env_loader.quiet = !log.level.atLeast(.info); - - // var pool = try allocator.create(ThreadPool); - // try pool.init(ThreadPool.InitConfig{ - // .allocator = allocator, - // }); - const resolve_results = try allocator.create(ResolveResults); - resolve_results.* = ResolveResults.init(allocator); - return Bundler{ - .options = bundle_options, - .fs = fs, - .allocator = allocator, - .timer = SystemTimer.start() catch @panic("Timer fail"), - .resolver = Resolver.init1(allocator, log, fs, bundle_options), - .log = log, - // .thread_pool = pool, - .linker = undefined, - .result = options.TransformResult{ .outbase = bundle_options.output_dir }, - .resolve_results = resolve_results, - .resolve_queue = ResolveQueue.init(allocator), - .output_files = std.ArrayList(options.OutputFile).init(allocator), - .env = env_loader, - }; - } - - pub fn configureLinkerWithAutoJSX(bundler: *Bundler, auto_jsx: bool) void { - bundler.linker = Linker.init( - bundler.allocator, - bundler.log, - &bundler.resolve_queue, - &bundler.options, - &bundler.resolver, - bundler.resolve_results, - bundler.fs, - ); - - if (auto_jsx) { - // Most of the time, this will already be cached - if (bundler.resolver.readDirInfo(bundler.fs.top_level_dir) catch null) |root_dir| { - if (root_dir.tsconfig_json) |tsconfig| { - // If we don't explicitly pass JSX, try to get it from the root tsconfig - if (bundler.options.transform_options.jsx == null) { - bundler.options.jsx = tsconfig.jsx; - } - bundler.options.emit_decorator_metadata = tsconfig.emit_decorator_metadata; - } - } - } - } - - pub fn configureLinker(bundler: *Bundler) void { - bundler.configureLinkerWithAutoJSX(true); - } - - pub fn runEnvLoader(this: *Bundler, skip_default_env: bool) !void { - switch (this.options.env.behavior) { - .prefix, .load_all, .load_all_without_inlining => { - // Step 1. Load the project root. - const dir_info = this.resolver.readDirInfo(this.fs.top_level_dir) catch return orelse return; - - if (dir_info.tsconfig_json) |tsconfig| { - this.options.jsx = tsconfig.mergeJSX(this.options.jsx); - } - - const dir = dir_info.getEntries(this.resolver.generation) orelse return; - - // Process always has highest priority. - const was_production = this.options.production; - this.env.loadProcess(); - const has_production_env = this.env.isProduction(); - if (!was_production and has_production_env) { - this.options.setProduction(true); - } - - if (this.options.isTest() or this.env.isTest()) { - try this.env.load(dir, this.options.env.files, .@"test", skip_default_env); - } else if (this.options.production) { - try this.env.load(dir, this.options.env.files, .production, skip_default_env); - } else { - try this.env.load(dir, this.options.env.files, .development, skip_default_env); - } - }, - .disable => { - this.env.loadProcess(); - if (this.env.isProduction()) { - this.options.setProduction(true); - } - }, - else => {}, - } - - if (strings.eqlComptime(this.env.get("BUN_DISABLE_TRANSPILER") orelse "0", "1")) { - this.options.disable_transpilation = true; - } - } - - // This must be run after a framework is configured, if a framework is enabled - pub fn configureDefines(this: *Bundler) !void { - if (this.options.defines_loaded) { - return; - } - - if (this.options.target == .bun_macro) { - this.options.env.behavior = .prefix; - this.options.env.prefix = "BUN_"; - } - - try this.runEnvLoader(false); - - this.options.jsx.setProduction(this.env.isProduction()); - - js_ast.Expr.Data.Store.create(); - js_ast.Stmt.Data.Store.create(); - - defer js_ast.Expr.Data.Store.reset(); - defer js_ast.Stmt.Data.Store.reset(); - - try this.options.loadDefines(this.allocator, this.env, &this.options.env); - - if (this.options.define.dots.get("NODE_ENV")) |NODE_ENV| { - if (NODE_ENV.len > 0 and NODE_ENV[0].data.value == .e_string and NODE_ENV[0].data.value.e_string.eqlComptime("production")) { - this.options.production = true; - } - } - } - - pub fn resetStore(_: *const Bundler) void { - js_ast.Expr.Data.Store.reset(); - js_ast.Stmt.Data.Store.reset(); - } - - pub noinline fn dumpEnvironmentVariables(bundler: *const Bundler) void { - @setCold(true); - const opts = std.json.StringifyOptions{ - .whitespace = .indent_2, - }; - Output.flush(); - std.json.stringify(bundler.env.map.*, opts, Output.writer()) catch unreachable; - Output.flush(); - } - - pub const BuildResolveResultPair = struct { - written: usize, - input_fd: ?StoredFileDescriptorType, - empty: bool = false, - }; - - pub fn buildWithResolveResult( - bundler: *Bundler, - resolve_result: _resolver.Result, - allocator: std.mem.Allocator, - loader: options.Loader, - comptime Writer: type, - writer: Writer, - comptime import_path_format: options.BundleOptions.ImportPathFormat, - file_descriptor: ?StoredFileDescriptorType, - filepath_hash: u32, - comptime WatcherType: type, - watcher: *WatcherType, - client_entry_point: ?*EntryPoints.ClientEntryPoint, - origin: URL, - comptime is_source_map: bool, - source_map_handler: ?js_printer.SourceMapHandler, - ) !BuildResolveResultPair { - if (resolve_result.is_external) { - return BuildResolveResultPair{ - .written = 0, - .input_fd = null, - }; - } - - errdefer bundler.resetStore(); - - var file_path = (resolve_result.pathConst() orelse { - return BuildResolveResultPair{ - .written = 0, - .input_fd = null, - }; - }).*; - - if (strings.indexOf(file_path.text, bundler.fs.top_level_dir)) |i| { - file_path.pretty = file_path.text[i + bundler.fs.top_level_dir.len ..]; - } else if (!file_path.is_symlink) { - file_path.pretty = allocator.dupe(u8, bundler.fs.relativeTo(file_path.text)) catch unreachable; - } - - const old_bundler_allocator = bundler.allocator; - bundler.allocator = allocator; - defer bundler.allocator = old_bundler_allocator; - const old_linker_allocator = bundler.linker.allocator; - defer bundler.linker.allocator = old_linker_allocator; - bundler.linker.allocator = allocator; - - switch (loader) { - .css => { - const CSSBundlerHMR = Css.NewBundler( - Writer, - @TypeOf(&bundler.linker), - @TypeOf(&bundler.resolver.caches.fs), - WatcherType, - @TypeOf(bundler.fs), - true, - import_path_format, - ); - - const CSSBundler = Css.NewBundler( - Writer, - @TypeOf(&bundler.linker), - @TypeOf(&bundler.resolver.caches.fs), - WatcherType, - @TypeOf(bundler.fs), - false, - import_path_format, - ); - - const written = brk: { - if (bundler.options.hot_module_reloading) { - break :brk (try CSSBundlerHMR.bundle( - file_path.text, - bundler.fs, - writer, - watcher, - &bundler.resolver.caches.fs, - filepath_hash, - file_descriptor, - allocator, - bundler.log, - &bundler.linker, - origin, - )).written; - } else { - break :brk (try CSSBundler.bundle( - file_path.text, - bundler.fs, - writer, - watcher, - &bundler.resolver.caches.fs, - filepath_hash, - file_descriptor, - allocator, - bundler.log, - &bundler.linker, - origin, - )).written; - } - }; - - return BuildResolveResultPair{ - .written = written, - .input_fd = file_descriptor, - }; - }, - else => { - var result = bundler.parse( - ParseOptions{ - .allocator = allocator, - .path = file_path, - .loader = loader, - .dirname_fd = resolve_result.dirname_fd, - .file_descriptor = file_descriptor, - .file_hash = filepath_hash, - .macro_remappings = bundler.options.macro_remap, - .emit_decorator_metadata = resolve_result.emit_decorator_metadata, - .jsx = resolve_result.jsx, - }, - client_entry_point, - ) orelse { - bundler.resetStore(); - return BuildResolveResultPair{ - .written = 0, - .input_fd = null, - }; - }; - - if (result.empty) { - return BuildResolveResultPair{ .written = 0, .input_fd = result.input_fd, .empty = true }; - } - - if (bundler.options.target.isBun()) { - if (!bundler.options.transform_only) { - try bundler.linker.link(file_path, &result, origin, import_path_format, false, true); - } - - return BuildResolveResultPair{ - .written = switch (result.ast.exports_kind) { - .esm => try bundler.printWithSourceMapMaybe( - result.ast, - &result.source, - Writer, - writer, - .esm_ascii, - is_source_map, - source_map_handler, - null, - ), - .cjs => try bundler.printWithSourceMapMaybe( - result.ast, - &result.source, - Writer, - writer, - .cjs, - is_source_map, - source_map_handler, - null, - ), - else => unreachable, - }, - .input_fd = result.input_fd, - }; - } - - if (!bundler.options.transform_only) { - try bundler.linker.link(file_path, &result, origin, import_path_format, false, false); - } - - return BuildResolveResultPair{ - .written = switch (result.ast.exports_kind) { - .none, .esm => try bundler.printWithSourceMapMaybe( - result.ast, - &result.source, - Writer, - writer, - .esm, - is_source_map, - source_map_handler, - null, - ), - .cjs => try bundler.printWithSourceMapMaybe( - result.ast, - &result.source, - Writer, - writer, - .cjs, - is_source_map, - source_map_handler, - null, - ), - else => unreachable, - }, - .input_fd = result.input_fd, - }; - }, - } - } - - pub fn buildWithResolveResultEager( - bundler: *Bundler, - resolve_result: _resolver.Result, - comptime import_path_format: options.BundleOptions.ImportPathFormat, - comptime Outstream: type, - outstream: Outstream, - client_entry_point_: ?*EntryPoints.ClientEntryPoint, - ) !?options.OutputFile { - if (resolve_result.is_external) { - return null; - } - - var file_path = (resolve_result.pathConst() orelse return null).*; - - // Step 1. Parse & scan - const loader = bundler.options.loader(file_path.name.ext); - - if (client_entry_point_) |client_entry_point| { - file_path = client_entry_point.source.path; - } - - file_path.pretty = Linker.relative_paths_list.append(string, bundler.fs.relativeTo(file_path.text)) catch unreachable; - - var output_file = options.OutputFile{ - .src_path = file_path, - .loader = loader, - .value = undefined, - .side = null, - .entry_point_index = null, - .output_kind = .chunk, - }; - - switch (loader) { - .jsx, .tsx, .js, .ts, .json, .toml, .text => { - var result = bundler.parse( - ParseOptions{ - .allocator = bundler.allocator, - .path = file_path, - .loader = loader, - .dirname_fd = resolve_result.dirname_fd, - .file_descriptor = null, - .file_hash = null, - .macro_remappings = bundler.options.macro_remap, - .jsx = resolve_result.jsx, - .emit_decorator_metadata = resolve_result.emit_decorator_metadata, - }, - client_entry_point_, - ) orelse { - return null; - }; - if (!bundler.options.transform_only) { - if (!bundler.options.target.isBun()) - try bundler.linker.link( - file_path, - &result, - bundler.options.origin, - import_path_format, - false, - false, - ) - else - try bundler.linker.link( - file_path, - &result, - bundler.options.origin, - import_path_format, - false, - true, - ); - } - - const buffer_writer = try js_printer.BufferWriter.init(bundler.allocator); - var writer = js_printer.BufferPrinter.init(buffer_writer); - - output_file.size = switch (bundler.options.target) { - .browser, .node => try bundler.print( - result, - *js_printer.BufferPrinter, - &writer, - .esm, - ), - .bun, .bun_macro, .bake_server_components_ssr => try bundler.print( - result, - *js_printer.BufferPrinter, - &writer, - .esm_ascii, - ), - }; - output_file.value = .{ - .buffer = .{ - .allocator = bundler.allocator, - .bytes = writer.ctx.written, - }, - }; - }, - .dataurl, .base64 => { - Output.panic("TODO: dataurl, base64", .{}); // TODO - }, - .css => { - if (bundler.options.experimental_css) { - const alloc = bundler.allocator; - - const entry = bundler.resolver.caches.fs.readFileWithAllocator( - bundler.allocator, - bundler.fs, - file_path.text, - resolve_result.dirname_fd, - false, - null, - ) catch |err| { - bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{s} reading \"{s}\"", .{ @errorName(err), file_path.pretty }) catch {}; - return null; - }; - var sheet = switch (bun.css.StyleSheet(bun.css.DefaultAtRule).parse(alloc, entry.contents, bun.css.ParserOptions.default(alloc, bundler.log), null)) { - .result => |v| v, - .err => |e| { - bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{} parsing", .{e}) catch unreachable; - return null; - }, - }; - if (sheet.minify(alloc, bun.css.MinifyOptions.default()).asErr()) |e| { - bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{} while minifying", .{e.kind}) catch bun.outOfMemory(); - return null; - } - const result = sheet.toCss(alloc, bun.css.PrinterOptions{ - .minify = bundler.options.minify_whitespace, - }, null) catch |e| { - bun.handleErrorReturnTrace(e, @errorReturnTrace()); - return null; - }; - output_file.value = .{ .buffer = .{ .allocator = alloc, .bytes = result.code } }; - } else { - var file: bun.sys.File = undefined; - - if (Outstream == std.fs.Dir) { - const output_dir = outstream; - - if (std.fs.path.dirname(file_path.pretty)) |dirname| { - try output_dir.makePath(dirname); - } - file = bun.sys.File.from(try output_dir.createFile(file_path.pretty, .{})); - } else { - file = bun.sys.File.from(outstream); - } - - const CSSBuildContext = struct { - origin: URL, - }; - const build_ctx = CSSBuildContext{ .origin = bundler.options.origin }; - - const BufferedWriter = std.io.CountingWriter(std.io.BufferedWriter(8192, bun.sys.File.Writer)); - const CSSWriter = Css.NewWriter( - BufferedWriter.Writer, - @TypeOf(&bundler.linker), - import_path_format, - CSSBuildContext, - ); - var buffered_writer = BufferedWriter{ - .child_stream = .{ .unbuffered_writer = file.writer() }, - .bytes_written = 0, - }; - const entry = bundler.resolver.caches.fs.readFile( - bundler.fs, - file_path.text, - resolve_result.dirname_fd, - !cache_files, - null, - ) catch return null; - - const _file = Fs.PathContentsPair{ .path = file_path, .contents = entry.contents }; - var source = try logger.Source.initFile(_file, bundler.allocator); - source.contents_is_recycled = !cache_files; - - var css_writer = CSSWriter.init( - &source, - buffered_writer.writer(), - &bundler.linker, - bundler.log, - ); - - css_writer.buildCtx = build_ctx; - - try css_writer.run(bundler.log, bundler.allocator); - try css_writer.ctx.context.child_stream.flush(); - output_file.size = css_writer.ctx.context.bytes_written; - var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty); - - file_op.fd = bun.toFD(file.handle); - - file_op.is_tmpdir = false; - - if (Outstream == std.fs.Dir) { - file_op.dir = bun.toFD(outstream.fd); - - if (bundler.fs.fs.needToCloseFiles()) { - file.close(); - file_op.fd = .zero; - } - } - - output_file.value = .{ .move = file_op }; - } - }, - - .bunsh, .sqlite_embedded, .sqlite, .wasm, .file, .napi => { - const hashed_name = try bundler.linker.getHashedFilename(file_path, null); - var pathname = try bundler.allocator.alloc(u8, hashed_name.len + file_path.name.ext.len); - bun.copy(u8, pathname, hashed_name); - bun.copy(u8, pathname[hashed_name.len..], file_path.name.ext); - const dir = if (bundler.options.output_dir_handle) |output_handle| bun.toFD(output_handle.fd) else .zero; - - output_file.value = .{ - .copy = options.OutputFile.FileOperation{ - .pathname = pathname, - .dir = dir, - .is_outdir = true, - }, - }; - }, - } - - return output_file; - } - - pub fn printWithSourceMapMaybe( - bundler: *Bundler, - ast: js_ast.Ast, - source: *const logger.Source, - comptime Writer: type, - writer: Writer, - comptime format: js_printer.Format, - comptime enable_source_map: bool, - source_map_context: ?js_printer.SourceMapHandler, - runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache, - ) !usize { - const tracer = bun.tracy.traceNamed(@src(), if (enable_source_map) "JSPrinter.printWithSourceMap" else "JSPrinter.print"); - defer tracer.end(); - - const symbols = js_ast.Symbol.NestedList.init(&[_]js_ast.Symbol.List{ast.symbols}); - - return switch (format) { - .cjs => try js_printer.printCommonJS( - Writer, - writer, - ast, - js_ast.Symbol.Map.initList(symbols), - source, - false, - .{ - .runtime_imports = ast.runtime_imports, - .require_ref = ast.require_ref, - .css_import_behavior = bundler.options.cssImportBehavior(), - .source_map_handler = source_map_context, - .minify_whitespace = bundler.options.minify_whitespace, - .minify_syntax = bundler.options.minify_syntax, - .minify_identifiers = bundler.options.minify_identifiers, - .transform_only = bundler.options.transform_only, - .runtime_transpiler_cache = runtime_transpiler_cache, - .print_dce_annotations = bundler.options.emit_dce_annotations, - }, - enable_source_map, - ), - - .esm => try js_printer.printAst( - Writer, - writer, - ast, - js_ast.Symbol.Map.initList(symbols), - source, - false, - .{ - .runtime_imports = ast.runtime_imports, - .require_ref = ast.require_ref, - .source_map_handler = source_map_context, - .css_import_behavior = bundler.options.cssImportBehavior(), - .minify_whitespace = bundler.options.minify_whitespace, - .minify_syntax = bundler.options.minify_syntax, - .minify_identifiers = bundler.options.minify_identifiers, - .transform_only = bundler.options.transform_only, - .import_meta_ref = ast.import_meta_ref, - .runtime_transpiler_cache = runtime_transpiler_cache, - .print_dce_annotations = bundler.options.emit_dce_annotations, - }, - enable_source_map, - ), - .esm_ascii => switch (bundler.options.target.isBun()) { - inline else => |is_bun| try js_printer.printAst( - Writer, - writer, - ast, - js_ast.Symbol.Map.initList(symbols), - source, - is_bun, - .{ - .runtime_imports = ast.runtime_imports, - .require_ref = ast.require_ref, - .css_import_behavior = bundler.options.cssImportBehavior(), - .source_map_handler = source_map_context, - .minify_whitespace = bundler.options.minify_whitespace, - .minify_syntax = bundler.options.minify_syntax, - .minify_identifiers = bundler.options.minify_identifiers, - .transform_only = bundler.options.transform_only, - .module_type = if (is_bun and bundler.options.transform_only) - // this is for when using `bun build --no-bundle` - // it should copy what was passed for the cli - bundler.options.output_format - else if (ast.exports_kind == .cjs) - .cjs - else - .esm, - .inline_require_and_import_errors = false, - .import_meta_ref = ast.import_meta_ref, - .runtime_transpiler_cache = runtime_transpiler_cache, - .target = bundler.options.target, - .print_dce_annotations = bundler.options.emit_dce_annotations, - }, - enable_source_map, - ), - }, - else => unreachable, - }; - } - - pub fn print( - bundler: *Bundler, - result: ParseResult, - comptime Writer: type, - writer: Writer, - comptime format: js_printer.Format, - ) !usize { - return bundler.printWithSourceMapMaybe( - result.ast, - &result.source, - Writer, - writer, - format, - false, - null, - null, - ); - } - - pub fn printWithSourceMap( - bundler: *Bundler, - result: ParseResult, - comptime Writer: type, - writer: Writer, - comptime format: js_printer.Format, - handler: js_printer.SourceMapHandler, - ) !usize { - if (bun.getRuntimeFeatureFlag("BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS")) { - return bundler.printWithSourceMapMaybe( - result.ast, - &result.source, - Writer, - writer, - format, - false, - handler, - result.runtime_transpiler_cache, - ); - } - return bundler.printWithSourceMapMaybe( - result.ast, - &result.source, - Writer, - writer, - format, - true, - handler, - result.runtime_transpiler_cache, - ); - } - - pub const ParseOptions = struct { - allocator: std.mem.Allocator, - dirname_fd: StoredFileDescriptorType, - file_descriptor: ?StoredFileDescriptorType = null, - file_hash: ?u32 = null, - - /// On exception, we might still want to watch the file. - file_fd_ptr: ?*StoredFileDescriptorType = null, - - path: Fs.Path, - loader: options.Loader, - jsx: options.JSX.Pragma, - macro_remappings: MacroRemap, - macro_js_ctx: MacroJSValueType = default_macro_js_value, - virtual_source: ?*const logger.Source = null, - replace_exports: runtime.Runtime.Features.ReplaceableExport.Map = .{}, - inject_jest_globals: bool = false, - set_breakpoint_on_first_line: bool = false, - emit_decorator_metadata: bool = false, - remove_cjs_module_wrapper: bool = false, - - dont_bundle_twice: bool = false, - allow_commonjs: bool = false, - - runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache = null, - - keep_json_and_toml_as_one_statement: bool = false, - allow_bytecode_cache: bool = false, - }; - - pub fn parse( - bundler: *Bundler, - this_parse: ParseOptions, - client_entry_point_: anytype, - ) ?ParseResult { - return parseMaybeReturnFileOnly(bundler, this_parse, client_entry_point_, false); - } - - pub fn parseMaybeReturnFileOnly( - bundler: *Bundler, - this_parse: ParseOptions, - client_entry_point_: anytype, - comptime return_file_only: bool, - ) ?ParseResult { - return parseMaybeReturnFileOnlyAllowSharedBuffer( - bundler, - this_parse, - client_entry_point_, - return_file_only, - false, - ); - } - - pub fn parseMaybeReturnFileOnlyAllowSharedBuffer( - bundler: *Bundler, - this_parse: ParseOptions, - client_entry_point_: anytype, - comptime return_file_only: bool, - comptime use_shared_buffer: bool, - ) ?ParseResult { - var allocator = this_parse.allocator; - const dirname_fd = this_parse.dirname_fd; - const file_descriptor = this_parse.file_descriptor; - const file_hash = this_parse.file_hash; - const path = this_parse.path; - const loader = this_parse.loader; - - var input_fd: ?StoredFileDescriptorType = null; - - const source: logger.Source = brk: { - if (this_parse.virtual_source) |virtual_source| { - break :brk virtual_source.*; - } - - if (client_entry_point_) |client_entry_point| { - if (@hasField(std.meta.Child(@TypeOf(client_entry_point)), "source")) { - break :brk client_entry_point.source; - } - } - - if (strings.eqlComptime(path.namespace, "node")) { - if (NodeFallbackModules.contentsFromPath(path.text)) |code| { - break :brk logger.Source.initPathString(path.text, code); - } - - break :brk logger.Source.initPathString(path.text, ""); - } - - if (strings.startsWith(path.text, "data:")) { - const data_url = DataURL.parseWithoutCheck(path.text) catch |err| { - bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{s} parsing data url \"{s}\"", .{ @errorName(err), path.text }) catch {}; - return null; - }; - const body = data_url.decodeData(this_parse.allocator) catch |err| { - bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{s} decoding data \"{s}\"", .{ @errorName(err), path.text }) catch {}; - return null; - }; - break :brk logger.Source.initPathString(path.text, body); - } - - const entry = bundler.resolver.caches.fs.readFileWithAllocator( - if (use_shared_buffer) bun.fs_allocator else this_parse.allocator, - bundler.fs, - path.text, - dirname_fd, - use_shared_buffer, - file_descriptor, - ) catch |err| { - bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{s} reading \"{s}\"", .{ @errorName(err), path.text }) catch {}; - return null; - }; - input_fd = entry.fd; - if (this_parse.file_fd_ptr) |file_fd_ptr| { - file_fd_ptr.* = entry.fd; - } - break :brk logger.Source.initRecycledFile(.{ .path = path, .contents = entry.contents }, bundler.allocator) catch return null; - }; - - if (comptime return_file_only) { - return ParseResult{ .source = source, .input_fd = input_fd, .loader = loader, .empty = true, .ast = js_ast.Ast.empty }; - } - - if (loader != .wasm and source.contents.len == 0 and source.contents.len < 33 and std.mem.trim(u8, source.contents, "\n\r ").len == 0) { - return ParseResult{ .source = source, .input_fd = input_fd, .loader = loader, .empty = true, .ast = js_ast.Ast.empty }; - } - - switch (loader) { - .js, - .jsx, - .ts, - .tsx, - => { - // wasm magic number - if (source.isWebAssembly()) { - return ParseResult{ - .source = source, - .input_fd = input_fd, - .loader = .wasm, - .empty = true, - .ast = js_ast.Ast.empty, - }; - } - - const target = bundler.options.target; - - var jsx = this_parse.jsx; - jsx.parse = loader.isJSX(); - - var opts = js_parser.Parser.Options.init(jsx, loader); - - opts.features.emit_decorator_metadata = this_parse.emit_decorator_metadata; - opts.features.allow_runtime = bundler.options.allow_runtime; - opts.features.set_breakpoint_on_first_line = this_parse.set_breakpoint_on_first_line; - opts.features.trim_unused_imports = bundler.options.trim_unused_imports orelse loader.isTypeScript(); - opts.features.use_import_meta_require = target.isBun(); - opts.features.no_macros = bundler.options.no_macros; - opts.features.runtime_transpiler_cache = this_parse.runtime_transpiler_cache; - opts.transform_only = bundler.options.transform_only; - - opts.ignore_dce_annotations = bundler.options.ignore_dce_annotations; - - // @bun annotation - opts.features.dont_bundle_twice = this_parse.dont_bundle_twice; - - opts.features.commonjs_at_runtime = this_parse.allow_commonjs; - - opts.tree_shaking = bundler.options.tree_shaking; - opts.features.inlining = bundler.options.inlining; - - opts.filepath_hash_for_hmr = file_hash orelse 0; - opts.features.auto_import_jsx = bundler.options.auto_import_jsx; - opts.warn_about_unbundled_modules = !target.isBun(); - - opts.features.inject_jest_globals = this_parse.inject_jest_globals; - opts.features.minify_syntax = bundler.options.minify_syntax; - opts.features.minify_identifiers = bundler.options.minify_identifiers; - opts.features.dead_code_elimination = bundler.options.dead_code_elimination; - opts.features.remove_cjs_module_wrapper = this_parse.remove_cjs_module_wrapper; - - if (bundler.macro_context == null) { - bundler.macro_context = js_ast.Macro.MacroContext.init(bundler); - } - - // we'll just always enable top-level await - // this is incorrect for Node.js files which are CommonJS modules - opts.features.top_level_await = true; - - opts.macro_context = &bundler.macro_context.?; - if (comptime !JSC.is_bindgen) { - if (target != .bun_macro) { - opts.macro_context.javascript_object = this_parse.macro_js_ctx; - } - } - - opts.features.is_macro_runtime = target == .bun_macro; - opts.features.replace_exports = this_parse.replace_exports; - - return switch ((bundler.resolver.caches.js.parse( - allocator, - opts, - bundler.options.define, - bundler.log, - &source, - ) catch null) orelse return null) { - .ast => |value| ParseResult{ - .ast = value, - .source = source, - .loader = loader, - .input_fd = input_fd, - .runtime_transpiler_cache = this_parse.runtime_transpiler_cache, - }, - .cached => ParseResult{ - .ast = undefined, - .runtime_transpiler_cache = this_parse.runtime_transpiler_cache, - .source = source, - .loader = loader, - .input_fd = input_fd, - }, - .already_bundled => |already_bundled| ParseResult{ - .ast = undefined, - .already_bundled = switch (already_bundled) { - .bun => .source_code, - .bun_cjs => .source_code_cjs, - .bytecode_cjs, .bytecode => brk: { - const default_value: ParseResult.AlreadyBundled = if (already_bundled == .bytecode_cjs) .source_code_cjs else .source_code; - if (this_parse.virtual_source == null and this_parse.allow_bytecode_cache) { - var path_buf2: bun.PathBuffer = undefined; - @memcpy(path_buf2[0..path.text.len], path.text); - path_buf2[path.text.len..][0..bun.bytecode_extension.len].* = bun.bytecode_extension.*; - const bytecode = bun.sys.File.toSourceAt(dirname_fd, path_buf2[0 .. path.text.len + bun.bytecode_extension.len], bun.default_allocator).asValue() orelse break :brk default_value; - if (bytecode.contents.len == 0) { - break :brk default_value; - } - break :brk if (already_bundled == .bytecode_cjs) .{ .bytecode_cjs = @constCast(bytecode.contents) } else .{ .bytecode = @constCast(bytecode.contents) }; - } - break :brk default_value; - }, - }, - .source = source, - .loader = loader, - .input_fd = input_fd, - }, - }; - }, - // TODO: use lazy export AST - inline .toml, .json => |kind| { - var expr = if (kind == .json) - // We allow importing tsconfig.*.json or jsconfig.*.json with comments - // These files implicitly become JSONC files, which aligns with the behavior of text editors. - if (source.path.isJSONCFile()) - JSON.parseTSConfig(&source, bundler.log, allocator, false) catch return null - else - JSON.parse(&source, bundler.log, allocator, false) catch return null - else if (kind == .toml) - TOML.parse(&source, bundler.log, allocator, false) catch return null - else - @compileError("unreachable"); - - var symbols: []js_ast.Symbol = &.{}; - - const parts = brk: { - if (this_parse.keep_json_and_toml_as_one_statement) { - var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable; - stmts[0] = js_ast.Stmt.allocate(allocator, js_ast.S.SExpr, js_ast.S.SExpr{ .value = expr }, logger.Loc{ .start = 0 }); - var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable; - parts_[0] = js_ast.Part{ .stmts = stmts }; - break :brk parts_; - } - - if (expr.data == .e_object) { - const properties: []js_ast.G.Property = expr.data.e_object.properties.slice(); - if (properties.len > 0) { - var stmts = allocator.alloc(js_ast.Stmt, 3) catch return null; - var decls = allocator.alloc(js_ast.G.Decl, properties.len) catch return null; - symbols = allocator.alloc(js_ast.Symbol, properties.len) catch return null; - var export_clauses = allocator.alloc(js_ast.ClauseItem, properties.len) catch return null; - var duplicate_key_checker = bun.StringHashMap(u32).init(allocator); - defer duplicate_key_checker.deinit(); - var count: usize = 0; - for (properties, decls, symbols, 0..) |*prop, *decl, *symbol, i| { - const name = prop.key.?.data.e_string.slice(allocator); - // Do not make named exports for "default" exports - if (strings.eqlComptime(name, "default")) - continue; - - const visited = duplicate_key_checker.getOrPut(name) catch continue; - if (visited.found_existing) { - decls[visited.value_ptr.*].value = prop.value.?; - continue; - } - visited.value_ptr.* = @truncate(i); - - symbol.* = js_ast.Symbol{ - .original_name = MutableString.ensureValidIdentifier(name, allocator) catch return null, - }; - - const ref = Ref.init(@truncate(i), 0, false); - decl.* = js_ast.G.Decl{ - .binding = js_ast.Binding.alloc(allocator, js_ast.B.Identifier{ - .ref = ref, - }, prop.key.?.loc), - .value = prop.value.?, - }; - export_clauses[i] = js_ast.ClauseItem{ - .name = .{ - .ref = ref, - .loc = prop.key.?.loc, - }, - .alias = name, - .alias_loc = prop.key.?.loc, - }; - prop.value = js_ast.Expr.initIdentifier(ref, prop.value.?.loc); - count += 1; - } - - stmts[0] = js_ast.Stmt.alloc( - js_ast.S.Local, - js_ast.S.Local{ - .decls = js_ast.G.Decl.List.init(decls[0..count]), - .kind = .k_var, - }, - logger.Loc{ - .start = 0, - }, - ); - stmts[1] = js_ast.Stmt.alloc( - js_ast.S.ExportClause, - js_ast.S.ExportClause{ - .items = export_clauses[0..count], - }, - logger.Loc{ - .start = 0, - }, - ); - stmts[2] = js_ast.Stmt.alloc( - js_ast.S.ExportDefault, - js_ast.S.ExportDefault{ - .value = js_ast.StmtOrExpr{ .expr = expr }, - .default_name = js_ast.LocRef{ - .loc = logger.Loc{}, - .ref = Ref.None, - }, - }, - logger.Loc{ - .start = 0, - }, - ); - - var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable; - parts_[0] = js_ast.Part{ .stmts = stmts }; - break :brk parts_; - } - } - - { - var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable; - stmts[0] = js_ast.Stmt.alloc(js_ast.S.ExportDefault, js_ast.S.ExportDefault{ - .value = js_ast.StmtOrExpr{ .expr = expr }, - .default_name = js_ast.LocRef{ - .loc = logger.Loc{}, - .ref = Ref.None, - }, - }, logger.Loc{ .start = 0 }); - - var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable; - parts_[0] = js_ast.Part{ .stmts = stmts }; - break :brk parts_; - } - }; - var ast = js_ast.Ast.fromParts(parts); - ast.symbols = js_ast.Symbol.List.init(symbols); - - return ParseResult{ - .ast = ast, - .source = source, - .loader = loader, - .input_fd = input_fd, - }; - }, - // TODO: use lazy export AST - .text => { - const expr = js_ast.Expr.init(js_ast.E.String, js_ast.E.String{ - .data = source.contents, - }, logger.Loc.Empty); - const stmt = js_ast.Stmt.alloc(js_ast.S.ExportDefault, js_ast.S.ExportDefault{ - .value = js_ast.StmtOrExpr{ .expr = expr }, - .default_name = js_ast.LocRef{ - .loc = logger.Loc{}, - .ref = Ref.None, - }, - }, logger.Loc{ .start = 0 }); - var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable; - stmts[0] = stmt; - var parts = allocator.alloc(js_ast.Part, 1) catch unreachable; - parts[0] = js_ast.Part{ .stmts = stmts }; - - return ParseResult{ - .ast = js_ast.Ast.initTest(parts), - .source = source, - .loader = loader, - .input_fd = input_fd, - }; - }, - .wasm => { - if (bundler.options.target.isBun()) { - if (!source.isWebAssembly()) { - bundler.log.addErrorFmt( - null, - logger.Loc.Empty, - bundler.allocator, - "Invalid wasm file \"{s}\" (missing magic header)", - .{path.text}, - ) catch {}; - return null; - } - - return ParseResult{ - .ast = js_ast.Ast.empty, - .source = source, - .loader = loader, - .input_fd = input_fd, - }; - } - }, - .css => {}, - else => Output.panic("Unsupported loader {s} for path: {s}", .{ @tagName(loader), source.path.text }), - } - - return null; - } - - // This is public so it can be used by the HTTP handler when matching against public dir. - pub threadlocal var tmp_buildfile_buf: bun.PathBuffer = undefined; - threadlocal var tmp_buildfile_buf2: bun.PathBuffer = undefined; - threadlocal var tmp_buildfile_buf3: bun.PathBuffer = undefined; - - // We try to be mostly stateless when serving - // This means we need a slightly different resolver setup - pub fn buildFile( - bundler: *Bundler, - log: *logger.Log, - path_to_use_: string, - comptime client_entry_point_enabled: bool, - ) !ServeResult { - const old_log = bundler.log; - - bundler.setLog(log); - defer bundler.setLog(old_log); - - var path_to_use = path_to_use_; - - defer { - js_ast.Expr.Data.Store.reset(); - js_ast.Stmt.Data.Store.reset(); - } - - // If the extension is .js, omit it. - // if (absolute_path.len > ".js".len and strings.eqlComptime(absolute_path[absolute_path.len - ".js".len ..], ".js")) { - // absolute_path = absolute_path[0 .. absolute_path.len - ".js".len]; - // } - - // All non-absolute paths are ./paths - if (path_to_use[0] != '/' and path_to_use[0] != '.') { - tmp_buildfile_buf3[0..2].* = "./".*; - @memcpy(tmp_buildfile_buf3[2..][0..path_to_use.len], path_to_use); - path_to_use = tmp_buildfile_buf3[0 .. 2 + path_to_use.len]; - } - - const resolved = if (comptime !client_entry_point_enabled) (try bundler.resolver.resolve(bundler.fs.top_level_dir, path_to_use, .stmt)) else brk: { - const absolute_pathname = Fs.PathName.init(path_to_use); - - const loader_for_ext = bundler.options.loader(absolute_pathname.ext); - - // The expected pathname looks like: - // /pages/index.entry.tsx - // /pages/index.entry.js - // /pages/index.entry.ts - // /pages/index.entry.jsx - if (loader_for_ext.supportsClientEntryPoint()) { - const absolute_pathname_pathname = Fs.PathName.init(absolute_pathname.base); - - if (strings.eqlComptime(absolute_pathname_pathname.ext, ".entry")) { - const trail_dir = absolute_pathname.dirWithTrailingSlash(); - var len = trail_dir.len; - - bun.copy(u8, &tmp_buildfile_buf2, trail_dir); - bun.copy(u8, tmp_buildfile_buf2[len..], absolute_pathname_pathname.base); - len += absolute_pathname_pathname.base.len; - bun.copy(u8, tmp_buildfile_buf2[len..], absolute_pathname.ext); - len += absolute_pathname.ext.len; - - if (comptime Environment.allow_assert) bun.assert(len > 0); - - const decoded_entry_point_path = tmp_buildfile_buf2[0..len]; - break :brk try bundler.resolver.resolve(bundler.fs.top_level_dir, decoded_entry_point_path, .entry_point); - } - } - - break :brk (try bundler.resolver.resolve(bundler.fs.top_level_dir, path_to_use, .stmt)); - }; - - const path = (resolved.pathConst() orelse return error.ModuleNotFound); - - const loader = bundler.options.loader(path.name.ext); - const mime_type_ext = bundler.options.out_extensions.get(path.name.ext) orelse path.name.ext; - - switch (loader) { - .js, .jsx, .ts, .tsx, .css => { - return ServeResult{ - .file = options.OutputFile.initPending(loader, resolved), - .mime_type = MimeType.byLoader( - loader, - mime_type_ext[1..], - ), - }; - }, - .toml, .json => { - return ServeResult{ - .file = options.OutputFile.initPending(loader, resolved), - .mime_type = MimeType.transpiled_json, - }; - }, - else => { - const abs_path = path.text; - const file = try std.fs.openFileAbsolute(abs_path, .{ .mode = .read_only }); - const size = try file.getEndPos(); - return ServeResult{ - .file = options.OutputFile.initFile(file, abs_path, size), - .mime_type = MimeType.byLoader( - loader, - mime_type_ext[1..], - ), - }; - }, - } - } - - pub fn normalizeEntryPointPath(bundler: *Bundler, _entry: string) string { - var paths = [_]string{_entry}; - var entry = bundler.fs.abs(&paths); - - std.fs.accessAbsolute(entry, .{}) catch - return _entry; - - entry = bundler.fs.relativeTo(entry); - - if (!strings.startsWith(entry, "./")) { - // Entry point paths without a leading "./" are interpreted as package - // paths. This happens because they go through general path resolution - // like all other import paths so that plugins can run on them. Requiring - // a leading "./" for a relative path simplifies writing plugins because - // entry points aren't a special case. - // - // However, requiring a leading "./" also breaks backward compatibility - // and makes working with the CLI more difficult. So attempt to insert - // "./" automatically when needed. We don't want to unconditionally insert - // a leading "./" because the path may not be a file system path. For - // example, it may be a URL. So only insert a leading "./" when the path - // is an exact match for an existing file. - var __entry = bundler.allocator.alloc(u8, "./".len + entry.len) catch unreachable; - __entry[0] = '.'; - __entry[1] = '/'; - bun.copy(u8, __entry[2..__entry.len], entry); - entry = __entry; - } - - return entry; - } - - fn enqueueEntryPoints(bundler: *Bundler, entry_points: []_resolver.Result, comptime normalize_entry_point: bool) usize { - var entry_point_i: usize = 0; - - for (bundler.options.entry_points) |_entry| { - const entry: string = if (comptime normalize_entry_point) bundler.normalizeEntryPointPath(_entry) else _entry; - - defer { - js_ast.Expr.Data.Store.reset(); - js_ast.Stmt.Data.Store.reset(); - } - - const result = bundler.resolver.resolve(bundler.fs.top_level_dir, entry, .entry_point) catch |err| { - Output.prettyError("Error resolving \"{s}\": {s}\n", .{ entry, @errorName(err) }); - continue; - }; - - if (result.pathConst() == null) { - Output.prettyError("\"{s}\" is disabled due to \"browser\" field in package.json.\n", .{ - entry, - }); - continue; - } - - if (bundler.linker.enqueueResolveResult(&result) catch unreachable) { - entry_points[entry_point_i] = result; - entry_point_i += 1; - } - } - - return entry_point_i; - } - - pub fn transform( - bundler: *Bundler, - allocator: std.mem.Allocator, - log: *logger.Log, - opts: Api.TransformOptions, - ) !options.TransformResult { - _ = opts; - var entry_points = try allocator.alloc(_resolver.Result, bundler.options.entry_points.len); - entry_points = entry_points[0..bundler.enqueueEntryPoints(entry_points, true)]; - - if (log.level.atLeast(.debug)) { - bundler.resolver.debug_logs = try DebugLogs.init(allocator); - } - bundler.options.transform_only = true; - const did_start = false; - - if (bundler.options.output_dir_handle == null) { - const outstream = bun.sys.File.from(std.io.getStdOut()); - - if (!did_start) { - try switch (bundler.options.import_path_format) { - .relative => bundler.processResolveQueue(.relative, false, @TypeOf(outstream), outstream), - .absolute_url => bundler.processResolveQueue(.absolute_url, false, @TypeOf(outstream), outstream), - .absolute_path => bundler.processResolveQueue(.absolute_path, false, @TypeOf(outstream), outstream), - .package_path => bundler.processResolveQueue(.package_path, false, @TypeOf(outstream), outstream), - }; - } - } else { - const output_dir = bundler.options.output_dir_handle orelse { - Output.printError("Invalid or missing output directory.", .{}); - Global.crash(); - }; - - if (!did_start) { - try switch (bundler.options.import_path_format) { - .relative => bundler.processResolveQueue(.relative, false, std.fs.Dir, output_dir), - .absolute_url => bundler.processResolveQueue(.absolute_url, false, std.fs.Dir, output_dir), - .absolute_path => bundler.processResolveQueue(.absolute_path, false, std.fs.Dir, output_dir), - .package_path => bundler.processResolveQueue(.package_path, false, std.fs.Dir, output_dir), - }; - } - } - - // if (log.level == .verbose) { - // for (log.msgs.items) |msg| { - // try msg.writeFormat(std.io.getStdOut().writer()); - // } - // } - - if (bundler.linker.any_needs_runtime) { - // try bundler.output_files.append( - // options.OutputFile.initBuf( - // runtime.Runtime.source_code, - // bun.default_allocator, - // Linker.runtime_source_path, - // .js, - // null, - // null, - // ), - // ); - } - - if (FeatureFlags.tracing and bundler.options.log.level.atLeast(.info)) { - Output.prettyErrorln( - "\n---Tracing---\nResolve time: {d}\nParsing time: {d}\n---Tracing--\n\n", - .{ - bundler.resolver.elapsed, - bundler.elapsed, - }, - ); - } - - var final_result = try options.TransformResult.init(try allocator.dupe(u8, bundler.result.outbase), try bundler.output_files.toOwnedSlice(), log, allocator); - final_result.root_dir = bundler.options.output_dir_handle; - return final_result; - } - - // pub fn processResolveQueueWithThreadPool(bundler) - - pub fn processResolveQueue( - bundler: *Bundler, - comptime import_path_format: options.BundleOptions.ImportPathFormat, - comptime wrap_entry_point: bool, - comptime Outstream: type, - outstream: Outstream, - ) !void { - // var count: u8 = 0; - while (bundler.resolve_queue.readItem()) |item| { - js_ast.Expr.Data.Store.reset(); - js_ast.Stmt.Data.Store.reset(); - - // defer count += 1; - - if (comptime wrap_entry_point) { - const path = item.pathConst() orelse unreachable; - const loader = bundler.options.loader(path.name.ext); - - if (item.import_kind == .entry_point and loader.supportsClientEntryPoint()) { - var client_entry_point = try bundler.allocator.create(EntryPoints.ClientEntryPoint); - client_entry_point.* = EntryPoints.ClientEntryPoint{}; - try client_entry_point.generate(Bundler, bundler, path.name, bundler.options.framework.?.client.path); - - const entry_point_output_file = bundler.buildWithResolveResultEager( - item, - import_path_format, - Outstream, - outstream, - client_entry_point, - ) catch continue orelse continue; - bundler.output_files.append(entry_point_output_file) catch unreachable; - - js_ast.Expr.Data.Store.reset(); - js_ast.Stmt.Data.Store.reset(); - - // At this point, the entry point will be de-duped. - // So we just immediately build it. - var item_not_entrypointed = item; - item_not_entrypointed.import_kind = .stmt; - const original_output_file = bundler.buildWithResolveResultEager( - item_not_entrypointed, - import_path_format, - Outstream, - outstream, - null, - ) catch continue orelse continue; - bundler.output_files.append(original_output_file) catch unreachable; - - continue; - } - } - - const output_file = bundler.buildWithResolveResultEager( - item, - import_path_format, - Outstream, - outstream, - null, - ) catch continue orelse continue; - bundler.output_files.append(output_file) catch unreachable; - - // if (count >= 3) return try bundler.processResolveQueueWithThreadPool(import_path_format, wrap_entry_point, Outstream, outstream); - } - } -}; - -pub const ServeResult = struct { - file: options.OutputFile, - mime_type: MimeType, -}; -pub const ResolveResults = std.AutoHashMap( - u64, - void, -); -pub const ResolveQueue = std.fifo.LinearFifo( - _resolver.Result, - std.fifo.LinearFifoBufferType.Dynamic, -); diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 4d7d94e190..54abb91627 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1,4 +1,4 @@ -// This is Bun's JavaScript/TypeScript bundler +// This is Bun's JavaScript/TypeScript transpiler // // A lot of the implementation is based on the Go implementation of esbuild. Thank you Evan Wallace. // @@ -41,7 +41,7 @@ // // make mimalloc-debug // -const Bundler = bun.Bundler; +const Transpiler = bun.Transpiler; const bun = @import("root").bun; const string = bun.string; const Output = bun.Output; @@ -59,6 +59,7 @@ const lex = @import("../js_lexer.zig"); const Logger = @import("../logger.zig"); const options = @import("../options.zig"); const js_parser = bun.js_parser; +const Part = js_ast.Part; const json_parser = @import("../json_parser.zig"); const js_printer = @import("../js_printer.zig"); const js_ast = @import("../js_ast.zig"); @@ -70,7 +71,7 @@ const Ref = @import("../ast/base.zig").Ref; const Define = @import("../defines.zig").Define; const DebugOptions = @import("../cli.zig").Command.DebugOptions; const ThreadPoolLib = @import("../thread_pool.zig"); -const ThreadlocalArena = @import("../mimalloc_arena.zig").Arena; +const ThreadlocalArena = @import("../allocators/mimalloc_arena.zig").Arena; const BabyList = @import("../baby_list.zig").BabyList; const Fs = @import("../fs.zig"); const schema = @import("../api/schema.zig"); @@ -89,9 +90,10 @@ const MacroRemap = @import("../resolver/package_json.zig").MacroMap; const DebugLogs = _resolver.DebugLogs; const OOM = bun.OOM; +const HTMLScanner = @import("../HTMLScanner.zig"); const Router = @import("../router.zig"); const isPackagePath = _resolver.isPackagePath; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const NodeFallbackModules = @import("../node_fallbacks.zig"); const CacheEntry = @import("../cache.zig").Fs.Entry; const Analytics = @import("../analytics/analytics_thread.zig"); @@ -100,7 +102,6 @@ const Linker = linker.Linker; const Resolver = _resolver.Resolver; const TOML = @import("../toml/toml_parser.zig").TOML; const EntryPoints = @import("./entry_points.zig"); -const ThisBundler = @import("../bundler.zig").Bundler; const Dependency = js_ast.Dependency; const JSAst = js_ast.BundledAst; const Loader = options.Loader; @@ -128,19 +129,19 @@ const BitSet = bun.bit_set.DynamicBitSetUnmanaged; const Async = bun.Async; const Loc = Logger.Loc; const bake = bun.bake; - +const lol = bun.LOLHTML; const debug_deferred = bun.Output.scoped(.BUNDLER_DEFERRED, true); const logPartDependencyTree = Output.scoped(.part_dep_tree, false); fn tracer(comptime src: std.builtin.SourceLocation, comptime name: [:0]const u8) bun.tracy.Ctx { - return bun.tracy.traceNamed(src, "Bundler." ++ name); + return bun.tracy.traceNamed(src, "Transpiler." ++ name); } pub const ThreadPool = struct { pool: *ThreadPoolLib = undefined, workers_assignments: std.AutoArrayHashMap(std.Thread.Id, *Worker) = std.AutoArrayHashMap(std.Thread.Id, *Worker).init(bun.default_allocator), - workers_assignments_lock: bun.Lock = .{}, + workers_assignments_lock: bun.Mutex = .{}, v2: *BundleV2 = undefined, @@ -264,7 +265,7 @@ pub const ThreadPool = struct { log: *Logger.Log, estimated_input_lines_of_code: usize = 0, macro_context: js_ast.Macro.MacroContext, - bundler: Bundler = undefined, + transpiler: Transpiler = undefined, }; pub fn init(worker: *Worker, v2: *BundleV2) void { @@ -292,18 +293,18 @@ pub const ThreadPool = struct { }; this.data.log.* = Logger.Log.init(allocator); this.ctx = ctx; - this.data.bundler = ctx.bundler.*; - this.data.bundler.setLog(this.data.log); - this.data.bundler.setAllocator(allocator); - this.data.bundler.linker.resolver = &this.data.bundler.resolver; - this.data.bundler.macro_context = js_ast.Macro.MacroContext.init(&this.data.bundler); - this.data.macro_context = this.data.bundler.macro_context.?; + this.data.transpiler = ctx.transpiler.*; + this.data.transpiler.setLog(this.data.log); + this.data.transpiler.setAllocator(allocator); + this.data.transpiler.linker.resolver = &this.data.transpiler.resolver; + this.data.transpiler.macro_context = js_ast.Macro.MacroContext.init(&this.data.transpiler); + this.data.macro_context = this.data.transpiler.macro_context.?; this.temporary_arena = bun.ArenaAllocator.init(this.allocator); this.stmt_list = LinkerContext.StmtList.init(this.allocator); const CacheSet = @import("../cache.zig"); - this.data.bundler.resolver.caches = CacheSet.Set.init(this.allocator); + this.data.transpiler.resolver.caches = CacheSet.Set.init(this.allocator); debug("Worker.create()", .{}); } @@ -325,7 +326,16 @@ const Watcher = bun.JSC.NewHotReloader(BundleV2, EventLoop, true); fn genericPathWithPrettyInitialized(path: Fs.Path, target: options.Target, top_level_dir: string, allocator: std.mem.Allocator) !Fs.Path { // TODO: outbase var buf: bun.PathBuffer = undefined; - if (path.isFile()) { + + const is_node = bun.strings.eqlComptime(path.namespace, "node"); + if (is_node and bun.strings.hasPrefixComptime(path.text, NodeFallbackModules.import_path)) { + return path; + } + + // "file" namespace should use the relative file path for its display name. + // the "node" namespace is also put through this code path so that the + // "node:" prefix is not emitted. + if (path.isFile() or is_node) { const rel = bun.path.relativePlatform(top_level_dir, path.text, .loose, false); var path_clone = path; // stack-allocated temporary is not leaked because dupeAlloc on the path will @@ -363,12 +373,12 @@ fn fmtEscapedNamespace(slice: []const u8, comptime fmt: []const u8, _: std.fmt.F } pub const BundleV2 = struct { - bundler: *Bundler, + transpiler: *Transpiler, /// When Server Component is enabled, this is used for the client bundles - /// and `bundler` is used for the server bundles. - client_bundler: *Bundler, + /// and `transpiler` is used for the server bundles. + client_bundler: *Transpiler, /// See bake.Framework.ServerComponents.separate_ssr_graph - ssr_bundler: *Bundler, + ssr_bundler: *Transpiler, /// When Bun Bake is used, the resolved framework is passed here framework: ?bake.Framework, graph: Graph, @@ -387,10 +397,13 @@ pub const BundleV2 = struct { /// See the comment in `Chunk.OutputPiece` unique_key: u64 = 0, dynamic_import_entry_points: std.AutoArrayHashMap(Index.Int, void) = undefined, + has_on_parse_plugins: bool = false, + + finalizers: std.ArrayListUnmanaged(CacheEntry.External) = .{}, drain_defer_task: DeferredBatchTask = .{}, - /// Set true by DevServer. Currently every usage of the bundler (Bun.build + /// Set true by DevServer. Currently every usage of the transpiler (Bun.build /// and `bun build` cli) runs at the top of an event loop. When this is /// true, a callback is executed after all work is complete. asynchronous: bool = false, @@ -398,8 +411,9 @@ pub const BundleV2 = struct { const BakeOptions = struct { framework: bake.Framework, - client_bundler: *Bundler, - ssr_bundler: *Bundler, + client_bundler: *Transpiler, + ssr_bundler: *Transpiler, + plugins: ?*JSC.API.JSBundler.Plugin, }; const debug = Output.scoped(.Bundle, false); @@ -419,28 +433,32 @@ pub const BundleV2 = struct { // running the plugins. .js => |jsc_event_loop| return jsc_event_loop, // The CLI currently has no JSC event loop; for now, no plugin support - .mini => @panic("No JavaScript event loop for bundler plugins to run on"), + .mini => @panic("No JavaScript event loop for transpiler plugins to run on"), } } - /// Most of the time, accessing .bundler directly is OK. This is only + /// Most of the time, accessing .transpiler directly is OK. This is only /// needed when it is important to distinct between client and server /// /// Note that .log, .allocator, and other things are shared - /// between the three bundler configurations - pub inline fn bundlerForTarget(this: *BundleV2, target: options.Target) *Bundler { - return if (!this.bundler.options.server_components) - this.bundler + /// between the three transpiler configurations + pub inline fn bundlerForTarget(this: *BundleV2, target: options.Target) *Transpiler { + return if (!this.transpiler.options.server_components) + this.transpiler else switch (target) { - else => this.bundler, + else => this.transpiler, .browser => this.client_bundler, .bake_server_components_ssr => this.ssr_bundler, }; } + pub fn hasOnParsePlugins(this: *const BundleV2) bool { + return this.has_on_parse_plugins; + } + /// Same semantics as bundlerForTarget for `path_to_source_index_map` pub inline fn pathToSourceIndexMap(this: *BundleV2, target: options.Target) *PathToSourceIndexMap { - return if (!this.bundler.options.server_components) + return if (!this.transpiler.options.server_components) &this.graph.path_to_source_index_map else switch (target) { else => &this.graph.path_to_source_index_map, @@ -571,7 +589,7 @@ pub const BundleV2 = struct { // imported and weird things will happen visitor.visit(Index.runtime, false, false); - switch (this.bundler.options.code_splitting) { + switch (this.transpiler.options.code_splitting) { inline else => |check_dynamic_imports| { for (this.graph.entry_points.items) |entry_point| { visitor.visit(entry_point, false, comptime check_dynamic_imports); @@ -624,19 +642,19 @@ pub const BundleV2 = struct { import_record: bun.JSC.API.JSBundler.Resolve.MiniImportRecord, target: options.Target, ) void { - const bundler = this.bundlerForTarget(target); + const transpiler = this.bundlerForTarget(target); var had_busted_dir_cache: bool = false; - var resolve_result = while (true) break bundler.resolver.resolve( + var resolve_result = while (true) break transpiler.resolver.resolve( Fs.PathName.init(import_record.source_file).dirWithTrailingSlash(), import_record.specifier, import_record.kind, ) catch |err| { // Only perform directory busting when hot-reloading is enabled if (err == error.ModuleNotFound) { - if (this.bundler.options.dev_server) |dev| { + if (this.transpiler.options.dev_server) |dev| { if (!had_busted_dir_cache) { // Only re-query if we previously had something cached. - if (bundler.resolver.bustDirCacheFromSpecifier(import_record.source_file, import_record.specifier)) { + if (transpiler.resolver.bustDirCacheFromSpecifier(import_record.source_file, import_record.specifier)) { had_busted_dir_cache = true; continue; } @@ -739,7 +757,7 @@ pub const BundleV2 = struct { if (path.pretty.ptr == path.text.ptr) { // TODO: outbase - const rel = bun.path.relativePlatform(bundler.fs.top_level_dir, path.text, .loose, false); + const rel = bun.path.relativePlatform(transpiler.fs.top_level_dir, path.text, .loose, false); path.pretty = this.graph.allocator.dupe(u8, rel) catch bun.outOfMemory(); } path.assertPrettyIsValid(); @@ -757,7 +775,7 @@ pub const BundleV2 = struct { const entry = this.pathToSourceIndexMap(target).getOrPut(this.graph.allocator, path.hashKey()) catch bun.outOfMemory(); if (!entry.found_existing) { path.* = this.pathWithPrettyInitialized(path.*, target) catch bun.outOfMemory(); - const loader = brk: { + const loader: Loader = (brk: { if (import_record.importer_source_index) |importer| { var record: *ImportRecord = &this.graph.ast.items(.import_records)[importer].slice()[import_record.import_record_index]; if (record.loader()) |out_loader| { @@ -765,8 +783,9 @@ pub const BundleV2 = struct { } } - break :brk path.loader(&bundler.options.loaders) orelse options.Loader.file; - }; + break :brk path.loader(&transpiler.options.loaders) orelse options.Loader.file; + // HTML is only allowed at the entry point. + }).disableHTML(); const idx = this.enqueueParseTask( &resolve_result, .{ @@ -782,7 +801,7 @@ pub const BundleV2 = struct { // For non-javascript files, make all of these files share indices. // For example, it is silly to bundle index.css depended on by client+server twice. // It makes sense to separate these for JS because the target affects DCE - if (this.bundler.options.server_components and !loader.isJavaScriptLike()) { + if (this.transpiler.options.server_components and !loader.isJavaScriptLike()) { const a, const b = switch (target) { else => .{ &this.graph.client_path_to_source_index_map, &this.graph.ssr_path_to_source_index_map }, .browser => .{ &this.graph.path_to_source_index_map, &this.graph.ssr_path_to_source_index_map }, @@ -821,7 +840,15 @@ pub const BundleV2 = struct { } this.incrementScanCounter(); const source_index = Index.source(this.graph.input_files.len); - const loader = this.bundler.options.loaders.get(path.name.ext) orelse .file; + const loader = brk: { + const default = path.loader(&this.transpiler.options.loaders) orelse .file; + + if (!this.transpiler.options.experimental.html) { + break :brk default.disableHTML(); + } + + break :brk default; + }; path.* = this.pathWithPrettyInitialized(path.*, target) catch bun.outOfMemory(); path.assertPrettyIsValid(); @@ -848,7 +875,7 @@ pub const BundleV2 = struct { // Handle onLoad plugins as entry points if (!this.enqueueOnLoadPluginIfNeeded(task)) { - if (loader.shouldCopyForBundling(this.bundler.options.experimental_css)) { + if (loader.shouldCopyForBundling(this.transpiler.options.experimental)) { var additional_files: *BabyList(AdditionalFile) = &this.graph.input_files.items(.additional_files)[source_index.get()]; additional_files.push(this.graph.allocator, .{ .source_index = task.source_index.get() }) catch unreachable; this.graph.input_files.items(.side_effects)[source_index.get()] = _resolver.SideEffects.no_side_effects__pure_data; @@ -864,7 +891,7 @@ pub const BundleV2 = struct { } pub fn init( - bundler: *ThisBundler, + transpiler: *Transpiler, bake_options: ?BakeOptions, allocator: std.mem.Allocator, event_loop: EventLoop, @@ -872,16 +899,16 @@ pub const BundleV2 = struct { thread_pool: ?*ThreadPoolLib, heap: ?ThreadlocalArena, ) !*BundleV2 { - bundler.env.loadTracy(); + transpiler.env.loadTracy(); const this = try allocator.create(BundleV2); - bundler.options.mark_builtins_as_external = bundler.options.target.isBun() or bundler.options.target == .node; - bundler.resolver.opts.mark_builtins_as_external = bundler.options.target.isBun() or bundler.options.target == .node; + transpiler.options.mark_builtins_as_external = transpiler.options.target.isBun() or transpiler.options.target == .node; + transpiler.resolver.opts.mark_builtins_as_external = transpiler.options.target.isBun() or transpiler.options.target == .node; this.* = .{ - .bundler = bundler, - .client_bundler = bundler, - .ssr_bundler = bundler, + .transpiler = transpiler, + .client_bundler = transpiler, + .ssr_bundler = transpiler, .framework = null, .graph = .{ .pool = undefined, @@ -907,50 +934,51 @@ pub const BundleV2 = struct { this.ssr_bundler = bo.ssr_bundler; this.framework = bo.framework; this.linker.framework = &this.framework.?; - bun.assert(bundler.options.server_components); + this.plugins = bo.plugins; + bun.assert(transpiler.options.server_components); bun.assert(this.client_bundler.options.server_components); if (bo.framework.server_components.?.separate_ssr_graph) bun.assert(this.ssr_bundler.options.server_components); } this.linker.graph.allocator = this.graph.heap.allocator(); this.graph.allocator = this.linker.graph.allocator; - this.bundler.allocator = this.graph.allocator; - this.bundler.resolver.allocator = this.graph.allocator; - this.bundler.linker.allocator = this.graph.allocator; - this.bundler.log.msgs.allocator = this.graph.allocator; - this.bundler.log.clone_line_text = true; + this.transpiler.allocator = this.graph.allocator; + this.transpiler.resolver.allocator = this.graph.allocator; + this.transpiler.linker.allocator = this.graph.allocator; + this.transpiler.log.msgs.allocator = this.graph.allocator; + this.transpiler.log.clone_line_text = true; // We don't expose an option to disable this. Bake forbids tree-shaking // since every export must is always exist in case a future module // starts depending on it. - if (this.bundler.options.output_format == .internal_bake_dev) { - this.bundler.options.tree_shaking = false; - this.bundler.resolver.opts.tree_shaking = false; + if (this.transpiler.options.output_format == .internal_bake_dev) { + this.transpiler.options.tree_shaking = false; + this.transpiler.resolver.opts.tree_shaking = false; } else { - this.bundler.options.tree_shaking = true; - this.bundler.resolver.opts.tree_shaking = true; + this.transpiler.options.tree_shaking = true; + this.transpiler.resolver.opts.tree_shaking = true; } - this.linker.resolver = &this.bundler.resolver; - this.linker.graph.code_splitting = bundler.options.code_splitting; + this.linker.resolver = &this.transpiler.resolver; + this.linker.graph.code_splitting = transpiler.options.code_splitting; - this.linker.options.minify_syntax = bundler.options.minify_syntax; - this.linker.options.minify_identifiers = bundler.options.minify_identifiers; - this.linker.options.minify_whitespace = bundler.options.minify_whitespace; - this.linker.options.emit_dce_annotations = bundler.options.emit_dce_annotations; - this.linker.options.ignore_dce_annotations = bundler.options.ignore_dce_annotations; - this.linker.options.banner = bundler.options.banner; - this.linker.options.footer = bundler.options.footer; - this.linker.options.experimental_css = bundler.options.experimental_css; - this.linker.options.css_chunking = bundler.options.css_chunking; - this.linker.options.source_maps = bundler.options.source_map; - this.linker.options.tree_shaking = bundler.options.tree_shaking; - this.linker.options.public_path = bundler.options.public_path; - this.linker.options.target = bundler.options.target; - this.linker.options.output_format = bundler.options.output_format; - this.linker.options.generate_bytecode_cache = bundler.options.bytecode; + this.linker.options.minify_syntax = transpiler.options.minify_syntax; + this.linker.options.minify_identifiers = transpiler.options.minify_identifiers; + this.linker.options.minify_whitespace = transpiler.options.minify_whitespace; + this.linker.options.emit_dce_annotations = transpiler.options.emit_dce_annotations; + this.linker.options.ignore_dce_annotations = transpiler.options.ignore_dce_annotations; + this.linker.options.banner = transpiler.options.banner; + this.linker.options.footer = transpiler.options.footer; + this.linker.options.experimental = transpiler.options.experimental; + this.linker.options.css_chunking = transpiler.options.css_chunking; + this.linker.options.source_maps = transpiler.options.source_map; + this.linker.options.tree_shaking = transpiler.options.tree_shaking; + this.linker.options.public_path = transpiler.options.public_path; + this.linker.options.target = transpiler.options.target; + this.linker.options.output_format = transpiler.options.output_format; + this.linker.options.generate_bytecode_cache = transpiler.options.bytecode; - this.linker.dev_server = bundler.options.dev_server; + this.linker.dev_server = transpiler.options.dev_server; var pool = try this.graph.allocator.create(ThreadPool); if (cli_watch_flag) { @@ -986,7 +1014,7 @@ pub const BundleV2 = struct { pub fn onAfterDecrementScanCounter(this: *BundleV2) void { if (this.asynchronous and this.isDone()) { - this.finishFromBakeDevServer(this.bundler.options.dev_server orelse + this.finishFromBakeDevServer(this.transpiler.options.dev_server orelse @panic("No dev server attached in asynchronous bundle job")) catch bun.outOfMemory(); } @@ -1008,7 +1036,7 @@ pub const BundleV2 = struct { { // Add the runtime - const rt = ParseTask.getRuntimeSource(this.bundler.options.target); + const rt = ParseTask.getRuntimeSource(this.transpiler.options.target); try this.graph.input_files.append(bun.default_allocator, Graph.InputFile{ .source = rt.source, .loader = .js, @@ -1032,7 +1060,7 @@ pub const BundleV2 = struct { // gets its content set after the scan+parse phase, but before linking. // // The dev server does not use these, as it is implement in the HMR runtime. - if (this.bundler.options.dev_server == null) { + if (this.transpiler.options.dev_server == null) { try this.reserveSourceIndexesForBake(); } @@ -1051,14 +1079,15 @@ pub const BundleV2 = struct { switch (variant) { .normal => { for (data) |entry_point| { - const resolved = this.bundler.resolveEntryPoint(entry_point) catch + const resolved = this.transpiler.resolveEntryPoint(entry_point) catch continue; - _ = try this.enqueueEntryItem(null, &batch, resolved, true, this.bundler.options.target); + + _ = try this.enqueueEntryItem(null, &batch, resolved, true, this.transpiler.options.target); } }, .dev_server => { for (data.files.set.keys(), data.files.set.values()) |abs_path, flags| { - const resolved = this.bundler.resolveEntryPoint(abs_path) catch + const resolved = this.transpiler.resolveEntryPoint(abs_path) catch continue; if (flags.client) brk: { @@ -1067,19 +1096,19 @@ pub const BundleV2 = struct { try data.css_data.putNoClobber(this.graph.allocator, Index.init(source_index), .{ .imported_on_server = false }); } } - if (flags.server) _ = try this.enqueueEntryItem(null, &batch, resolved, true, this.bundler.options.target); + if (flags.server) _ = try this.enqueueEntryItem(null, &batch, resolved, true, this.transpiler.options.target); if (flags.ssr) _ = try this.enqueueEntryItem(null, &batch, resolved, true, .bake_server_components_ssr); } }, .bake_production => { for (data.files.keys()) |key| { - const resolved = this.bundler.resolveEntryPoint(key.absPath()) catch + const resolved = this.transpiler.resolveEntryPoint(key.absPath()) catch continue; // TODO: wrap client files so the exports arent preserved. _ = try this.enqueueEntryItem(null, &batch, resolved, true, switch (key.side) { .client => .browser, - .server => this.bundler.options.target, + .server => this.transpiler.options.target, }) orelse continue; } }, @@ -1092,8 +1121,8 @@ pub const BundleV2 = struct { fn cloneAST(this: *BundleV2) !void { const trace = tracer(@src(), "cloneAST"); defer trace.end(); - this.linker.allocator = this.bundler.allocator; - this.linker.graph.allocator = this.bundler.allocator; + this.linker.allocator = this.transpiler.allocator; + this.linker.graph.allocator = this.transpiler.allocator; this.linker.graph.ast = try this.graph.ast.clone(this.linker.allocator); var ast = this.linker.graph.ast.slice(); for (ast.items(.module_scope)) |*module_scope| { @@ -1122,8 +1151,8 @@ pub const BundleV2 = struct { const alloc = this.graph.allocator; - var server = try AstBuilder.init(this.graph.allocator, &bake.server_virtual_source, this.bundler.options.hot_module_reloading); - var client = try AstBuilder.init(this.graph.allocator, &bake.client_virtual_source, this.bundler.options.hot_module_reloading); + var server = try AstBuilder.init(this.graph.allocator, &bake.server_virtual_source, this.transpiler.options.hot_module_reloading); + var client = try AstBuilder.init(this.graph.allocator, &bake.client_virtual_source, this.transpiler.options.hot_module_reloading); var server_manifest_props: std.ArrayListUnmanaged(G.Property) = .{}; var client_manifest_props: std.ArrayListUnmanaged(G.Property) = .{}; @@ -1248,11 +1277,13 @@ pub const BundleV2 = struct { this: *BundleV2, resolve_result: *const _resolver.Result, source: Logger.Source, - loader: Loader, + loader_: Loader, known_target: options.Target, ) OOM!Index.Int { const source_index = Index.init(@as(u32, @intCast(this.graph.ast.len))); this.graph.ast.append(bun.default_allocator, JSAst.empty) catch unreachable; + // Only enable HTML loader when it's an entry point. + const loader = loader_.disableHTML(); this.graph.input_files.append(bun.default_allocator, .{ .source = source, @@ -1274,7 +1305,7 @@ pub const BundleV2 = struct { // Handle onLoad plugins if (!this.enqueueOnLoadPluginIfNeeded(task)) { - if (loader.shouldCopyForBundling(this.bundler.options.experimental_css)) { + if (loader.shouldCopyForBundling(this.transpiler.options.experimental)) { var additional_files: *BabyList(AdditionalFile) = &this.graph.input_files.items(.additional_files)[source_index.get()]; additional_files.push(this.graph.allocator, .{ .source_index = task.source_index.get() }) catch unreachable; this.graph.input_files.items(.side_effects)[source_index.get()] = _resolver.SideEffects.no_side_effects__pure_data; @@ -1313,7 +1344,7 @@ pub const BundleV2 = struct { }, .side_effects = .has_side_effects, .jsx = if (known_target == .bake_server_components_ssr and !this.framework.?.server_components.?.separate_ssr_graph) - this.bundler.options.jsx + this.transpiler.options.jsx else this.bundlerForTarget(known_target).options.jsx, .source_index = source_index, @@ -1330,7 +1361,7 @@ pub const BundleV2 = struct { // Handle onLoad plugins if (!this.enqueueOnLoadPluginIfNeeded(task)) { - if (loader.shouldCopyForBundling(this.bundler.options.experimental_css)) { + if (loader.shouldCopyForBundling(this.transpiler.options.experimental)) { var additional_files: *BabyList(AdditionalFile) = &this.graph.input_files.items(.additional_files)[source_index.get()]; additional_files.push(this.graph.allocator, .{ .source_index = task.source_index.get() }) catch unreachable; this.graph.input_files.items(.side_effects)[source_index.get()] = _resolver.SideEffects.no_side_effects__pure_data; @@ -1373,7 +1404,7 @@ pub const BundleV2 = struct { } pub fn generateFromCLI( - bundler: *ThisBundler, + transpiler: *Transpiler, allocator: std.mem.Allocator, event_loop: EventLoop, enable_reloading: bool, @@ -1381,16 +1412,16 @@ pub const BundleV2 = struct { minify_duration: *u64, source_code_size: *u64, ) !std.ArrayList(options.OutputFile) { - var this = try BundleV2.init(bundler, null, allocator, event_loop, enable_reloading, null, null); + var this = try BundleV2.init(transpiler, null, allocator, event_loop, enable_reloading, null, null); this.unique_key = generateUniqueKey(); - if (this.bundler.log.hasErrors()) { + if (this.transpiler.log.hasErrors()) { return error.BuildFailed; } - this.graph.pool.pool.schedule(try this.enqueueEntryPoints(.normal, this.bundler.options.entry_points)); + this.graph.pool.pool.schedule(try this.enqueueEntryPoints(.normal, this.transpiler.options.entry_points)); - if (this.bundler.log.hasErrors()) { + if (this.transpiler.log.hasErrors()) { return error.BuildFailed; } @@ -1399,7 +1430,7 @@ pub const BundleV2 = struct { minify_duration.* = @as(u64, @intCast(@divTrunc(@as(i64, @truncate(std.time.nanoTimestamp())) - @as(i64, @truncate(bun.CLI.start_time)), @as(i64, std.time.ns_per_ms)))); source_code_size.* = this.source_code_length; - if (this.bundler.log.hasErrors()) { + if (this.transpiler.log.hasErrors()) { return error.BuildFailed; } @@ -1426,7 +1457,7 @@ pub const BundleV2 = struct { pub fn generateFromBakeProductionCLI( entry_points: bake.production.EntryPointMap, - server_bundler: *ThisBundler, + server_bundler: *Transpiler, kit_options: BakeOptions, allocator: std.mem.Allocator, event_loop: EventLoop, @@ -1434,19 +1465,19 @@ pub const BundleV2 = struct { var this = try BundleV2.init(server_bundler, kit_options, allocator, event_loop, false, null, null); this.unique_key = generateUniqueKey(); - if (this.bundler.log.hasErrors()) { + if (this.transpiler.log.hasErrors()) { return error.BuildFailed; } this.graph.pool.pool.schedule(try this.enqueueEntryPoints(.bake_production, entry_points)); - if (this.bundler.log.hasErrors()) { + if (this.transpiler.log.hasErrors()) { return error.BuildFailed; } this.waitForParse(); - if (this.bundler.log.hasErrors()) { + if (this.transpiler.log.hasErrors()) { return error.BuildFailed; } @@ -1490,10 +1521,11 @@ pub const BundleV2 = struct { pub fn processFilesToCopy(this: *BundleV2, reachable_files: []const Index) !void { if (this.graph.estimated_file_loader_count > 0) { + const file_allocators = this.graph.input_files.items(.allocator); const unique_key_for_additional_files = this.graph.input_files.items(.unique_key_for_additional_file); const content_hashes_for_additional_files = this.graph.input_files.items(.content_hash_for_additional_file); const sources = this.graph.input_files.items(.source); - var additional_output_files = std.ArrayList(options.OutputFile).init(this.bundler.allocator); + var additional_output_files = std.ArrayList(options.OutputFile).init(this.transpiler.allocator); const additional_files: []BabyList(AdditionalFile) = this.graph.input_files.items(.additional_files); const loaders = this.graph.input_files.items(.loader); @@ -1503,13 +1535,13 @@ pub const BundleV2 = struct { const key = unique_key_for_additional_files[index]; if (key.len > 0) { var template = PathTemplate.asset; - if (this.bundler.options.asset_naming.len > 0) - template.data = this.bundler.options.asset_naming; + if (this.transpiler.options.asset_naming.len > 0) + template.data = this.transpiler.options.asset_naming; const source = &sources[index]; var pathname = source.path.name; // TODO: outbase - pathname = Fs.PathName.init(bun.path.relativePlatform(this.bundler.options.root_dir, source.path.text, .loose, false)); + pathname = Fs.PathName.init(bun.path.relativePlatform(this.transpiler.options.root_dir, source.path.text, .loose, false)); template.placeholder.name = pathname.base; template.placeholder.dir = pathname.dir; @@ -1526,7 +1558,7 @@ pub const BundleV2 = struct { additional_output_files.append(options.OutputFile.init(.{ .data = .{ .buffer = .{ .data = source.contents, - .allocator = bun.default_allocator, + .allocator = file_allocators[index], } }, .size = source.contents.len, .output_path = std.fmt.allocPrint(bun.default_allocator, "{}", .{template}) catch bun.outOfMemory(), @@ -1551,28 +1583,29 @@ pub const BundleV2 = struct { pub const JSBundleThread = BundleThread(JSBundleCompletionTask); - pub fn generateFromJavaScript( + pub fn createAndScheduleCompletionTask( config: bun.JSC.API.JSBundler.Config, plugins: ?*bun.JSC.API.JSBundler.Plugin, globalThis: *JSC.JSGlobalObject, event_loop: *bun.JSC.EventLoop, allocator: std.mem.Allocator, - ) OOM!bun.JSC.JSValue { - var completion = try allocator.create(JSBundleCompletionTask); - completion.* = JSBundleCompletionTask{ + ) OOM!*JSBundleCompletionTask { + _ = allocator; // autofix + const completion = JSBundleCompletionTask.new(.{ .config = config, .jsc_event_loop = event_loop, - .promise = JSC.JSPromise.Strong.init(globalThis), .globalThis = globalThis, .poll_ref = Async.KeepAlive.init(), - .env = globalThis.bunVM().bundler.env, + .env = globalThis.bunVM().transpiler.env, .plugins = plugins, .log = Logger.Log.init(bun.default_allocator), - .task = JSBundleCompletionTask.TaskCompletion.init(completion), - }; + .task = undefined, + }); + completion.task = JSBundleCompletionTask.TaskCompletion.init(completion); - if (plugins) |plugin| + if (plugins) |plugin| { plugin.setConfig(completion); + } // Ensure this exists before we spawn the thread to prevent any race // conditions from creating two @@ -1582,17 +1615,46 @@ pub const BundleV2 = struct { completion.poll_ref.ref(globalThis.bunVM()); + return completion; + } + + pub fn generateFromJavaScript( + config: bun.JSC.API.JSBundler.Config, + plugins: ?*bun.JSC.API.JSBundler.Plugin, + globalThis: *JSC.JSGlobalObject, + event_loop: *bun.JSC.EventLoop, + allocator: std.mem.Allocator, + ) OOM!bun.JSC.JSValue { + const completion = try createAndScheduleCompletionTask(config, plugins, globalThis, event_loop, allocator); + completion.promise = JSC.JSPromise.Strong.init(globalThis); return completion.promise.value(); } pub const BuildResult = struct { output_files: std.ArrayList(options.OutputFile), + + pub fn deinit(this: *BuildResult) void { + for (this.output_files.items) |*output_file| { + output_file.deinit(); + } + + this.output_files.clearAndFree(); + } }; pub const Result = union(enum) { pending: void, err: anyerror, value: BuildResult, + + pub fn deinit(this: *Result) void { + switch (this.*) { + .value => |*value| { + value.deinit(); + }, + else => {}, + } + } }; pub const JSBundleCompletionTask = struct { @@ -1600,26 +1662,32 @@ pub const BundleV2 = struct { jsc_event_loop: *bun.JSC.EventLoop, task: bun.JSC.AnyTask, globalThis: *JSC.JSGlobalObject, - promise: JSC.JSPromise.Strong, + promise: JSC.JSPromise.Strong = .{}, poll_ref: Async.KeepAlive = Async.KeepAlive.init(), env: *bun.DotEnv.Loader, log: Logger.Log, + cancelled: bool = false, + + html_build_task: ?*JSC.API.HTMLBundle.HTMLBundleRoute = null, result: Result = .{ .pending = {} }, next: ?*JSBundleCompletionTask = null, - bundler: *BundleV2 = undefined, + transpiler: *BundleV2 = undefined, plugins: ?*bun.JSC.API.JSBundler.Plugin = null, ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), + started_at_ns: u64 = 0, + + pub usingnamespace bun.NewThreadSafeRefCounted(JSBundleCompletionTask, @This().deinit); pub fn configureBundler( completion: *JSBundleCompletionTask, - bundler: *Bundler, + transpiler: *Transpiler, allocator: std.mem.Allocator, ) !void { const config = &completion.config; - bundler.* = try bun.Bundler.init( + transpiler.* = try bun.Transpiler.init( allocator, &completion.log, Api.TransformOptions{ @@ -1627,7 +1695,7 @@ pub const BundleV2 = struct { .entry_points = config.entry_points.keys(), .target = config.target.toAPI(), .absolute_working_dir = if (config.dir.list.items.len > 0) - config.dir.slice() + config.dir.sliceWithSentinel() else null, .inject = &.{}, @@ -1636,44 +1704,52 @@ pub const BundleV2 = struct { .extension_order = &.{}, .env_files = &.{}, .conditions = config.conditions.map.keys(), - .ignore_dce_annotations = bundler.options.ignore_dce_annotations, + .ignore_dce_annotations = transpiler.options.ignore_dce_annotations, .drop = config.drop.map.keys(), }, completion.env, ); + transpiler.options.env.behavior = config.env_behavior; + transpiler.options.env.prefix = config.env_prefix.slice(); - bundler.options.entry_points = config.entry_points.keys(); - bundler.options.jsx = config.jsx; - bundler.options.no_macros = config.no_macros; - bundler.options.loaders = try options.loadersFromTransformOptions(allocator, config.loaders, config.target); - bundler.options.entry_naming = config.names.entry_point.data; - bundler.options.chunk_naming = config.names.chunk.data; - bundler.options.asset_naming = config.names.asset.data; + transpiler.options.entry_points = config.entry_points.keys(); + transpiler.options.jsx = config.jsx; + transpiler.options.no_macros = config.no_macros; + transpiler.options.loaders = try options.loadersFromTransformOptions(allocator, config.loaders, config.target); + transpiler.options.entry_naming = config.names.entry_point.data; + transpiler.options.chunk_naming = config.names.chunk.data; + transpiler.options.asset_naming = config.names.asset.data; - bundler.options.public_path = config.public_path.list.items; - bundler.options.output_format = config.format; - bundler.options.bytecode = config.bytecode; + transpiler.options.public_path = config.public_path.list.items; + transpiler.options.output_format = config.format; + transpiler.options.bytecode = config.bytecode; - bundler.options.output_dir = config.outdir.slice(); - bundler.options.root_dir = config.rootdir.slice(); - bundler.options.minify_syntax = config.minify.syntax; - bundler.options.minify_whitespace = config.minify.whitespace; - bundler.options.minify_identifiers = config.minify.identifiers; - bundler.options.inlining = config.minify.syntax; - bundler.options.source_map = config.source_map; - bundler.options.packages = config.packages; - bundler.options.code_splitting = config.code_splitting; - bundler.options.emit_dce_annotations = config.emit_dce_annotations orelse !config.minify.whitespace; - bundler.options.ignore_dce_annotations = config.ignore_dce_annotations; - bundler.options.experimental_css = config.experimental_css; - bundler.options.css_chunking = config.css_chunking; - bundler.options.banner = config.banner.slice(); - bundler.options.footer = config.footer.slice(); + transpiler.options.output_dir = config.outdir.slice(); + transpiler.options.root_dir = config.rootdir.slice(); + transpiler.options.minify_syntax = config.minify.syntax; + transpiler.options.minify_whitespace = config.minify.whitespace; + transpiler.options.minify_identifiers = config.minify.identifiers; + transpiler.options.inlining = config.minify.syntax; + transpiler.options.source_map = config.source_map; + transpiler.options.packages = config.packages; + transpiler.options.code_splitting = config.code_splitting; + transpiler.options.emit_dce_annotations = config.emit_dce_annotations orelse !config.minify.whitespace; + transpiler.options.ignore_dce_annotations = config.ignore_dce_annotations; + transpiler.options.experimental = config.experimental; + transpiler.options.css_chunking = config.css_chunking; + transpiler.options.banner = config.banner.slice(); + transpiler.options.footer = config.footer.slice(); - bundler.configureLinker(); - try bundler.configureDefines(); + transpiler.configureLinker(); + try transpiler.configureDefines(); - bundler.resolver.opts = bundler.options; + if (bun.FeatureFlags.breaking_changes_1_2) { + if (!transpiler.options.production) { + try transpiler.options.conditions.appendSlice(&.{"development"}); + } + } + + transpiler.resolver.opts = transpiler.options; } pub fn completeOnBundleThread(completion: *JSBundleCompletionTask) void { @@ -1682,16 +1758,16 @@ pub const BundleV2 = struct { pub const TaskCompletion = bun.JSC.AnyTask.New(JSBundleCompletionTask, onComplete); - pub fn deref(this: *JSBundleCompletionTask) void { - if (this.ref_count.fetchSub(1, .monotonic) == 1) { - this.config.deinit(bun.default_allocator); - debug("Deinit JSBundleCompletionTask(0{x})", .{@intFromPtr(this)}); - bun.default_allocator.destroy(this); + pub fn deinit(this: *JSBundleCompletionTask) void { + this.result.deinit(); + this.log.deinit(); + this.poll_ref.disable(); + if (this.plugins) |plugin| { + plugin.deinit(); } - } - - pub fn ref(this: *JSBundleCompletionTask) void { - _ = this.ref_count.fetchAdd(1, .monotonic); + this.config.deinit(bun.default_allocator); + this.promise.deinit(); + this.destroy(); } pub fn onComplete(this: *JSBundleCompletionTask) void { @@ -1699,12 +1775,26 @@ pub const BundleV2 = struct { defer this.deref(); this.poll_ref.unref(globalThis.bunVM()); + if (this.cancelled) { + return; + } + + if (this.html_build_task) |html_build_task| { + html_build_task.onComplete(this); + return; + } + const promise = this.promise.swap(); - const root_obj = JSC.JSValue.createEmptyObject(globalThis, 2); switch (this.result) { .pending => unreachable, - .err => { + .err => brk: { + if (this.config.throw_on_error) { + promise.reject(globalThis, this.log.toJSAggregateError(globalThis, bun.String.static("Bundle failed"))); + break :brk; + } + + const root_obj = JSC.JSValue.createEmptyObject(globalThis, 3); root_obj.put(globalThis, JSC.ZigString.static("outputs"), JSC.JSValue.createEmptyArray(globalThis, 0)); root_obj.put( globalThis, @@ -1716,19 +1806,18 @@ pub const BundleV2 = struct { JSC.ZigString.static("logs"), this.log.toJSArray(globalThis, bun.default_allocator), ); + promise.resolve(globalThis, root_obj); }, .value => |*build| { + const root_obj = JSC.JSValue.createEmptyObject(globalThis, 3); const output_files: []options.OutputFile = build.output_files.items; const output_files_js = JSC.JSValue.createEmptyArray(globalThis, output_files.len); if (output_files_js == .zero) { @panic("Unexpected pending JavaScript exception in JSBundleCompletionTask.onComplete. This is a bug in Bun."); } - defer build.output_files.deinit(); var to_assign_on_sourcemap: JSC.JSValue = .zero; for (output_files, 0..) |*output_file, i| { - defer bun.default_allocator.free(output_file.src_path.text); - defer bun.default_allocator.free(output_file.dest_path); const result = output_file.toJS( if (!this.config.outdir.isEmpty()) if (std.fs.path.isAbsolute(this.config.outdir.list.items)) @@ -1782,10 +1871,13 @@ pub const BundleV2 = struct { JSC.ZigString.static("logs"), this.log.toJSArray(globalThis, bun.default_allocator), ); + promise.resolve(globalThis, root_obj); }, } - promise.resolve(globalThis, root_obj); + if (Environment.isDebug) { + bun.assert(promise.status(globalThis.vm()) != .pending); + } } }; @@ -1835,7 +1927,7 @@ pub const BundleV2 = struct { this.graph.heap.gc(true); } } - const log = this.bundler.log; + const log = this.transpiler.log; // TODO: watcher @@ -1860,7 +1952,7 @@ pub const BundleV2 = struct { this.decrementScanCounter(); }, .success => |code| { - const should_copy_for_bundling = load.parse_task.defer_copy_for_bundling and code.loader.shouldCopyForBundling(this.bundler.options.experimental_css); + const should_copy_for_bundling = load.parse_task.defer_copy_for_bundling and code.loader.shouldCopyForBundling(this.transpiler.options.experimental); if (should_copy_for_bundling) { const source_index = load.source_index; var additional_files: *BabyList(AdditionalFile) = &this.graph.input_files.items(.additional_files)[source_index.get()]; @@ -1879,7 +1971,7 @@ pub const BundleV2 = struct { this.graph.pool.pool.schedule(ThreadPoolLib.Batch.from(&parse_task.task)); }, .err => |msg| { - if (this.bundler.options.dev_server) |dev| { + if (this.transpiler.options.dev_server) |dev| { const source = &this.graph.input_files.items(.source)[load.source_index.get()]; // A stack-allocated Log object containing the singular message var msg_mut = msg; @@ -1922,7 +2014,7 @@ pub const BundleV2 = struct { this.graph.heap.gc(true); } } - const log = this.bundler.log; + const log = this.transpiler.log; switch (resolve.value.consume()) { .no_match => { @@ -1937,7 +2029,7 @@ pub const BundleV2 = struct { // When it's not a file, this is an error and we should report it. // // We have no way of loading non-files. - if (resolve.import_record.kind == .entry_point or resolve.import_record.importer_source_index == null) { + if (resolve.import_record.kind == .entry_point_build or resolve.import_record.importer_source_index == null) { log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Module not found {} in namespace {}", .{ bun.fmt.quote(resolve.import_record.specifier), bun.fmt.quote(resolve.import_record.namespace), @@ -1977,7 +2069,7 @@ pub const BundleV2 = struct { existing.value_ptr.* = source_index.get(); out_source_index = source_index; this.graph.ast.append(bun.default_allocator, JSAst.empty) catch unreachable; - const loader = path.loader(&this.bundler.options.loaders) orelse options.Loader.file; + const loader = path.loader(&this.transpiler.options.loaders) orelse options.Loader.file; this.graph.input_files.append(bun.default_allocator, .{ .source = .{ @@ -2013,7 +2105,7 @@ pub const BundleV2 = struct { // Handle onLoad plugins if (!this.enqueueOnLoadPluginIfNeeded(task)) { - if (loader.shouldCopyForBundling(this.bundler.options.experimental_css)) { + if (loader.shouldCopyForBundling(this.transpiler.options.experimental)) { var additional_files: *BabyList(AdditionalFile) = &this.graph.input_files.items(.additional_files)[source_index.get()]; additional_files.push(this.graph.allocator, .{ .source_index = task.source_index.get() }) catch unreachable; this.graph.input_files.items(.side_effects)[source_index.get()] = _resolver.SideEffects.no_side_effects__pure_data; @@ -2064,6 +2156,16 @@ pub const BundleV2 = struct { } pub fn deinit(this: *BundleV2) void { + { + // We do this first to make it harder for any dangling pointers to data to be used in there. + var on_parse_finalizers = this.finalizers; + this.finalizers = .{}; + for (on_parse_finalizers.items) |finalizer| { + finalizer.call(); + } + on_parse_finalizers.deinit(bun.default_allocator); + } + defer this.graph.ast.deinit(bun.default_allocator); defer this.graph.input_files.deinit(bun.default_allocator); if (this.graph.pool.workers_assignments.count() > 0) { @@ -2092,7 +2194,7 @@ pub const BundleV2 = struct { ) !std.ArrayList(options.OutputFile) { this.unique_key = generateUniqueKey(); - if (this.bundler.log.errors > 0) { + if (this.transpiler.log.errors > 0) { return error.BuildFailed; } @@ -2105,7 +2207,7 @@ pub const BundleV2 = struct { this.graph.heap.helpCatchMemoryIssues(); - if (this.bundler.log.errors > 0) { + if (this.transpiler.log.errors > 0) { return error.BuildFailed; } @@ -2130,14 +2232,14 @@ pub const BundleV2 = struct { reachable_files, ); - if (this.bundler.log.errors > 0) { + if (this.transpiler.log.errors > 0) { return error.BuildFailed; } return try this.linker.generateChunksInParallel(chunks, false); } - /// Dev Server uses this instead to run a subset of the bundler, and to run it asynchronously. + /// Dev Server uses this instead to run a subset of the transpiler, and to run it asynchronously. pub fn startFromBakeDevServer(this: *BundleV2, bake_entry_points: bake.DevServer.EntryPointList) !BakeBundleStart { this.unique_key = generateUniqueKey(); @@ -2340,7 +2442,7 @@ pub const BundleV2 = struct { pub fn enqueueOnLoadPluginIfNeeded(this: *BundleV2, parse: *ParseTask) bool { if (this.plugins) |plugins| { if (plugins.hasAnyMatches(&parse.path, true)) { - if (parse.is_entry_point and parse.loader != null and parse.loader.?.shouldCopyForBundling(this.bundler.options.experimental_css)) { + if (parse.is_entry_point and parse.loader != null and parse.loader.?.shouldCopyForBundling(this.transpiler.options.experimental)) { parse.defer_copy_for_bundling = true; } // This is where onLoad plugins are enqueued @@ -2359,7 +2461,7 @@ pub const BundleV2 = struct { } fn pathWithPrettyInitialized(this: *BundleV2, path: Fs.Path, target: options.Target) !Fs.Path { - return genericPathWithPrettyInitialized(path, target, this.bundler.fs.top_level_dir, this.graph.allocator); + return genericPathWithPrettyInitialized(path, target, this.transpiler.fs.top_level_dir, this.graph.allocator); } fn reserveSourceIndexesForBake(this: *BundleV2) !void { @@ -2441,7 +2543,7 @@ pub const BundleV2 = struct { inline else => |is_server| { const src = if (is_server) bake.server_virtual_source else bake.client_virtual_source; if (strings.eqlComptime(import_record.path.text, src.path.pretty)) { - if (this.bundler.options.dev_server != null) { + if (this.transpiler.options.dev_server != null) { import_record.is_external_without_side_effects = true; import_record.source_index = Index.invalid; } else { @@ -2468,7 +2570,7 @@ pub const BundleV2 = struct { continue; } - if (this.bundler.options.rewrite_jest_for_tests) { + if (this.transpiler.options.rewrite_jest_for_tests) { if (strings.eqlComptime( import_record.path.text, "@jest/globals", @@ -2513,13 +2615,13 @@ pub const BundleV2 = struct { continue; } - const bundler, const renderer: bake.Graph, const target = + const transpiler, const renderer: bake.Graph, const target = if (import_record.tag == .bake_resolve_to_ssr_graph) brk: { // TODO: consider moving this error into js_parser so it is caught more reliably // Then we can assert(this.framework != null) if (this.framework == null) { - this.bundler.log.addErrorFmt( + this.transpiler.log.addErrorFmt( source, import_record.range.loc, this.graph.allocator, @@ -2532,7 +2634,7 @@ pub const BundleV2 = struct { const is_supported = this.framework.?.server_components != null and this.framework.?.server_components.?.separate_ssr_graph; if (!is_supported) { - this.bundler.log.addErrorFmt( + this.transpiler.log.addErrorFmt( source, import_record.range.loc, this.graph.allocator, @@ -2554,17 +2656,17 @@ pub const BundleV2 = struct { }; var had_busted_dir_cache = false; - var resolve_result = inner: while (true) break bundler.resolver.resolveWithFramework( + var resolve_result = inner: while (true) break transpiler.resolver.resolveWithFramework( source_dir, import_record.path.text, import_record.kind, ) catch |err| { // Only perform directory busting when hot-reloading is enabled if (err == error.ModuleNotFound) { - if (this.bundler.options.dev_server) |dev| { + if (this.transpiler.options.dev_server) |dev| { if (!had_busted_dir_cache) { // Only re-query if we previously had something cached. - if (bundler.resolver.bustDirCacheFromSpecifier( + if (transpiler.resolver.bustDirCacheFromSpecifier( source.path.text, import_record.path.text, )) { @@ -2597,7 +2699,7 @@ pub const BundleV2 = struct { if (isPackagePath(import_record.path.text)) { if (ast.target == .browser and options.ExternalModules.isNodeBuiltin(import_record.path.text)) { addError( - this.bundler.log, + this.transpiler.log, source, import_record.range, this.graph.allocator, @@ -2607,7 +2709,7 @@ pub const BundleV2 = struct { ) catch @panic("unexpected log error"); } else { addError( - this.bundler.log, + this.transpiler.log, source, import_record.range, this.graph.allocator, @@ -2618,7 +2720,7 @@ pub const BundleV2 = struct { } } else { addError( - this.bundler.log, + this.transpiler.log, source, import_record.range, this.graph.allocator, @@ -2656,12 +2758,12 @@ pub const BundleV2 = struct { continue; } - if (this.bundler.options.dev_server) |dev_server| { + if (this.transpiler.options.dev_server) |dev_server| { import_record.source_index = Index.invalid; import_record.is_external_without_side_effects = true; if (dev_server.isFileCached(path.text, renderer)) |entry| { - const rel = bun.path.relativePlatform(this.bundler.fs.top_level_dir, path.text, .loose, false); + const rel = bun.path.relativePlatform(this.transpiler.fs.top_level_dir, path.text, .loose, false); import_record.path.text = rel; import_record.path.pretty = rel; import_record.path = this.pathWithPrettyInitialized(path.*, target) catch bun.outOfMemory(); @@ -2675,7 +2777,7 @@ pub const BundleV2 = struct { const hash_key = path.hashKey(); if (this.pathToSourceIndexMap(target).get(hash_key)) |id| { - if (this.bundler.options.dev_server != null) { + if (this.transpiler.options.dev_server != null) { import_record.path = this.graph.input_files.items(.source)[id].path; } else { import_record.source_index = Index.init(id); @@ -2711,13 +2813,21 @@ pub const BundleV2 = struct { resolve_task.jsx = resolve_result.jsx; resolve_task.jsx.development = this.bundlerForTarget(target).options.jsx.development; - if (import_record.tag.loader()) |loader| { - resolve_task.loader = loader; - } + // Figure out the loader. + { + if (import_record.tag.loader()) |loader| { + resolve_task.loader = loader; + } - if (resolve_task.loader == null) { - resolve_task.loader = path.loader(&this.bundler.options.loaders); - resolve_task.tree_shaking = this.bundler.options.tree_shaking; + if (resolve_task.loader == null) { + resolve_task.loader = path.loader(&this.transpiler.options.loaders); + resolve_task.tree_shaking = this.transpiler.options.tree_shaking; + } + + // HTML must be an entry point. + if (resolve_task.loader) |*loader| { + loader.* = loader.disableHTML(); + } } resolve_entry.value_ptr.* = resolve_task; @@ -2755,6 +2865,19 @@ pub const BundleV2 = struct { pub fn onParseTaskComplete(parse_result: *ParseTask.Result, this: *BundleV2) void { const trace = tracer(@src(), "onParseTaskComplete"); defer trace.end(); + if (parse_result.external.function != null) { + const source = switch (parse_result.value) { + inline .empty, .err => |data| data.source_index.get(), + .success => |val| val.source.index.get(), + }; + const loader: Loader = this.graph.input_files.items(.loader)[source]; + if (!loader.shouldCopyForBundling(this.transpiler.options.experimental)) { + this.finalizers.append(bun.default_allocator, parse_result.external) catch bun.outOfMemory(); + } else { + this.graph.input_files.items(.allocator)[source] = ExternalFreeFunctionAllocator.create(@ptrCast(parse_result.external.function.?), parse_result.external.ctx.?); + } + } + defer bun.default_allocator.destroy(parse_result); const graph = &this.graph; @@ -2778,7 +2901,7 @@ pub const BundleV2 = struct { } } - // To minimize contention, watchers are appended by the bundler thread. + // To minimize contention, watchers are appended by the transpiler thread. if (this.bun_watcher) |watcher| { if (parse_result.watcher_data.fd != bun.invalid_fd and parse_result.watcher_data.fd != .zero) { const source = switch (parse_result.value) { @@ -2810,7 +2933,7 @@ pub const BundleV2 = struct { } }, .success => |*result| { - result.log.cloneToWithRecycled(this.bundler.log, true) catch unreachable; + result.log.cloneToWithRecycled(this.transpiler.log, true) catch unreachable; // Warning: `input_files` and `ast` arrays may resize in this function call // It is not safe to cache slices from them. @@ -2822,6 +2945,9 @@ pub const BundleV2 = struct { graph.input_files.items(.unique_key_for_additional_file)[result.source.index.get()] = result.unique_key_for_additional_file; graph.input_files.items(.content_hash_for_additional_file)[result.source.index.get()] = result.content_hash_for_additional_file; + // Record which loader we used for this file + graph.input_files.items(.loader)[result.source.index.get()] = result.loader; + debug("onParse({d}, {s}) = {d} imports, {d} exports", .{ result.source.index.get(), result.source.path.text, @@ -2855,7 +2981,7 @@ pub const BundleV2 = struct { .side_effects = value.side_effects, }; - const loader = new_task.loader orelse new_input_file.source.path.loader(&this.bundler.options.loaders) orelse options.Loader.file; + const loader = new_task.loader orelse new_input_file.source.path.loader(&this.transpiler.options.loaders) orelse options.Loader.file; new_input_file.source.index = Index.source(graph.input_files.len); new_input_file.source.path = new_task.path; @@ -2875,7 +3001,7 @@ pub const BundleV2 = struct { continue; } - if (loader.shouldCopyForBundling(this.bundler.options.experimental_css)) { + if (loader.shouldCopyForBundling(this.transpiler.options.experimental)) { var additional_files: *BabyList(AdditionalFile) = &graph.input_files.items(.additional_files)[result.source.index.get()]; additional_files.push(this.graph.allocator, .{ .source_index = new_task.source_index.get() }) catch unreachable; new_input_file.side_effects = _resolver.SideEffects.no_side_effects__pure_data; @@ -2885,8 +3011,11 @@ pub const BundleV2 = struct { // schedule as early as possible graph.pool.pool.schedule(ThreadPoolLib.Batch.from(&new_task.task)); } else { - const loader = value.loader orelse graph.input_files.items(.source)[existing.value_ptr.*].path.loader(&this.bundler.options.loaders) orelse options.Loader.file; - if (loader.shouldCopyForBundling(this.bundler.options.experimental_css)) { + const loader = value.loader orelse + graph.input_files.items(.source)[existing.value_ptr.*].path.loader(&this.transpiler.options.loaders) orelse + options.Loader.file; + + if (loader.shouldCopyForBundling(this.transpiler.options.experimental)) { var additional_files: *BabyList(AdditionalFile) = &graph.input_files.items(.additional_files)[result.source.index.get()]; additional_files.push(this.graph.allocator, .{ .source_index = existing.value_ptr.* }) catch unreachable; graph.estimated_file_loader_count += 1; @@ -2902,7 +3031,7 @@ pub const BundleV2 = struct { if (this.resolve_tasks_waiting_for_import_source_index.fetchSwapRemove(result.source.index.get())) |pending_entry| { for (pending_entry.value.slice()) |to_assign| { - if (this.bundler.options.dev_server == null or + if (this.transpiler.options.dev_server == null or input_file_loaders[to_assign.to_source_index.get()] == .css) { import_records.slice()[to_assign.import_record_index].source_index = to_assign.to_source_index; @@ -2919,7 +3048,7 @@ pub const BundleV2 = struct { for (import_records.slice(), 0..) |*record, i| { if (path_to_source_index_map.get(record.path.hashKey())) |source_index| { - if (this.bundler.options.dev_server == null or + if (this.transpiler.options.dev_server == null or input_file_loaders[source_index] == .css) record.source_index.value = source_index; @@ -2939,7 +3068,9 @@ pub const BundleV2 = struct { graph.ast.set(result.source.index.get(), result.ast); // For files with use directives, index and prepare the other side. - if (result.use_directive != .none and + if (result.use_directive != .none and if (this.framework.?.server_components.?.separate_ssr_graph) + ((result.use_directive == .client) == (result.ast.target == .browser)) + else ((result.use_directive == .client) != (result.ast.target == .browser))) { if (result.use_directive == .server) @@ -2969,7 +3100,7 @@ pub const BundleV2 = struct { // Enqueue only one file var server_source = result.source; server_source.path.pretty = server_source.path.text; - server_source.path = this.pathWithPrettyInitialized(server_source.path, this.bundler.options.target) catch bun.outOfMemory(); + server_source.path = this.pathWithPrettyInitialized(server_source.path, this.transpiler.options.target) catch bun.outOfMemory(); const server_index = this.enqueueParseTask2( server_source, this.graph.input_files.items(.loader)[result.source.index.get()], @@ -3000,7 +3131,7 @@ pub const BundleV2 = struct { } if (process_log) { - if (this.bundler.options.dev_server) |dev_server| { + if (this.transpiler.options.dev_server) |dev_server| { dev_server.handleParseTaskFailure( err.err, err.target.bakeGraph(), @@ -3008,19 +3139,19 @@ pub const BundleV2 = struct { &err.log, ) catch bun.outOfMemory(); } else if (err.log.msgs.items.len > 0) { - err.log.cloneToWithRecycled(this.bundler.log, true) catch unreachable; + err.log.cloneToWithRecycled(this.transpiler.log, true) catch unreachable; } else { - this.bundler.log.addErrorFmt( + this.transpiler.log.addErrorFmt( null, Logger.Loc.Empty, - this.bundler.log.msgs.allocator, + this.transpiler.log.msgs.allocator, "{s} while {s}", .{ @errorName(err.err), @tagName(err.step) }, ) catch unreachable; } } - if (Environment.allow_assert and this.bundler.options.dev_server != null) { + if (Environment.allow_assert and this.transpiler.options.dev_server != null) { bun.assert(this.graph.ast.items(.parts)[err.source_index.get()].len == 0); } }, @@ -3029,12 +3160,12 @@ pub const BundleV2 = struct { /// To satisfy the interface from NewHotReloader() pub fn getLoaders(vm: *BundleV2) *bun.options.Loader.HashTable { - return &vm.bundler.options.loaders; + return &vm.transpiler.options.loaders; } /// To satisfy the interface from NewHotReloader() pub fn bustDirCache(vm: *BundleV2, path: []const u8) bool { - return vm.bundler.resolver.bustDirCache(path); + return vm.transpiler.resolver.bustDirCache(path); } }; @@ -3151,14 +3282,14 @@ pub fn BundleThread(CompletionStruct: type) type { ast_memory_allocator.reset(); ast_memory_allocator.push(); - const bundler = try allocator.create(bun.Bundler); + const transpiler = try allocator.create(bun.Transpiler); - try completion.configureBundler(bundler, allocator); + try completion.configureBundler(transpiler, allocator); - bundler.resolver.generation = generation; + transpiler.resolver.generation = generation; const this = try BundleV2.init( - bundler, + transpiler, null, // TODO: Kit allocator, JSC.AnyEventLoop.init(allocator), @@ -3172,7 +3303,7 @@ pub fn BundleThread(CompletionStruct: type) type { BundleV2.JSBundleCompletionTask => completion, else => @compileError("Unknown completion struct: " ++ CompletionStruct), }; - completion.bundler = this; + completion.transpiler = this; defer { if (this.graph.pool.pool.threadpool_context == @as(?*anyopaque, @ptrCast(this.graph.pool))) { @@ -3189,16 +3320,16 @@ pub fn BundleThread(CompletionStruct: type) type { this.linker.source_maps.quoted_contents_wait_group.wait(); var out_log = Logger.Log.init(bun.default_allocator); - this.bundler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); + this.transpiler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); completion.log = out_log; } completion.result = .{ .value = .{ - .output_files = try this.runFromJSInNewThread(bundler.options.entry_points), + .output_files = try this.runFromJSInNewThread(transpiler.options.entry_points), } }; var out_log = Logger.Log.init(bun.default_allocator); - this.bundler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); + this.transpiler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); completion.log = out_log; completion.completeOnBundleThread(); } @@ -3226,8 +3357,8 @@ pub const DeferredBatchTask = struct { } pub fn getCompletion(this: *DeferredBatchTask) ?*bun.BundleV2.JSBundleCompletionTask { - const bundler: *BundleV2 = @alignCast(@fieldParentPtr("drain_defer_task", this)); - return bundler.completion; + const transpiler: *BundleV2 = @alignCast(@fieldParentPtr("drain_defer_task", this)); + return transpiler.completion; } pub fn schedule(this: *DeferredBatchTask) void { @@ -3250,7 +3381,7 @@ pub const DeferredBatchTask = struct { return; }; - completion.bundler.plugins.?.drainDeferred(completion.result == .err); + completion.transpiler.plugins.?.drainDeferred(completion.result == .err); } }; @@ -3268,6 +3399,7 @@ pub const ParseTask = struct { path: Fs.Path, secondary_path_for_commonjs_interop: ?Fs.Path = null, contents_or_fd: ContentsOrFd, + external: CacheEntry.External = .{}, side_effects: _resolver.SideEffects, loader: ?Loader = null, jsx: options.JSX.Pragma, @@ -3291,6 +3423,10 @@ pub const ParseTask = struct { ctx: *BundleV2, value: Value, watcher_data: WatcherData, + /// This is used for native onBeforeParsePlugins to store + /// a function pointer and context pointer to free the + /// returned source code by the plugin. + external: CacheEntry.External = .{}, pub const Value = union(enum) { success: Success, @@ -3322,6 +3458,8 @@ pub const ParseTask = struct { unique_key_for_additional_file: []const u8 = "", /// Used by "file" loader files. content_hash_for_additional_file: u64 = 0, + + loader: Loader, }; pub const Error = struct { @@ -3358,7 +3496,7 @@ pub const ParseTask = struct { .module_type = resolve_result.module_type, .emit_decorator_metadata = resolve_result.emit_decorator_metadata, .package_version = if (resolve_result.package_json) |package_json| package_json.version else "", - .known_target = ctx.bundler.options.target, + .known_target = ctx.transpiler.options.target, }; } @@ -3372,10 +3510,12 @@ pub const ParseTask = struct { // and then that is either replaced with the module itself, or an import to the // runtime here. const runtime_require = switch (target) { - // __require is intentionally not implemented here, as we - // always inline 'import.meta.require' and 'import.meta.require.resolve' - // Omitting it here acts as an extra assertion. - .bun, .bun_macro => "", + // Previously, Bun inlined `import.meta.require` at all usages. This broke + // code that called `fn.toString()` and parsed the code outside a module + // context. + .bun, .bun_macro => + \\export var __require = import.meta.require; + , .node => \\import { createRequire } from "node:module"; @@ -3513,25 +3653,25 @@ pub const ParseTask = struct { fn getEmptyCSSAST( log: *Logger.Log, - bundler: *Bundler, + transpiler: *Transpiler, opts: js_parser.Parser.Options, allocator: std.mem.Allocator, source: Logger.Source, ) !JSAst { const root = Expr.init(E.Object, E.Object{}, Logger.Loc{ .start = 0 }); - var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); ast.css = bun.create(allocator, bun.css.BundlerStyleSheet, bun.css.BundlerStyleSheet.empty(allocator)); return ast; } - fn getEmptyAST(log: *Logger.Log, bundler: *Bundler, opts: js_parser.Parser.Options, allocator: std.mem.Allocator, source: Logger.Source, comptime RootType: type) !JSAst { + fn getEmptyAST(log: *Logger.Log, transpiler: *Transpiler, opts: js_parser.Parser.Options, allocator: std.mem.Allocator, source: Logger.Source, comptime RootType: type) !JSAst { const root = Expr.init(RootType, RootType{}, Logger.Loc.Empty); - return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); } fn getAST( log: *Logger.Log, - bundler: *Bundler, + transpiler: *Transpiler, opts: js_parser.Parser.Options, allocator: std.mem.Allocator, resolver: *Resolver, @@ -3545,9 +3685,9 @@ pub const ParseTask = struct { const trace = tracer(@src(), "ParseJS"); defer trace.end(); return if (try resolver.caches.js.parse( - bundler.allocator, + transpiler.allocator, opts, - bundler.options.define, + transpiler.options.define, log, &source, )) |res| @@ -3555,7 +3695,7 @@ pub const ParseTask = struct { else switch (opts.module_type == .esm) { inline else => |as_undefined| try getEmptyAST( log, - bundler, + transpiler, opts, allocator, source, @@ -3567,25 +3707,25 @@ pub const ParseTask = struct { const trace = tracer(@src(), "ParseJSON"); defer trace.end(); const root = (try resolver.caches.json.parsePackageJSON(log, source, allocator, false)) orelse Expr.init(E.Object, E.Object{}, Logger.Loc.Empty); - return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); }, .toml => { const trace = tracer(@src(), "ParseTOML"); defer trace.end(); const root = try TOML.parse(&source, log, allocator, false); - return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); }, .text => { const root = Expr.init(E.String, E.String{ .data = source.contents, }, Logger.Loc{ .start = 0 }); - var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); - ast.addUrlForCss(allocator, bundler.options.experimental_css, &source, "text/plain", null); + var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + ast.addUrlForCss(allocator, transpiler.options.experimental, &source, "text/plain", null); return ast; }, .sqlite_embedded, .sqlite => { - if (!bundler.options.target.isBun()) { + if (!transpiler.options.target.isBun()) { log.addError( null, Logger.Loc.Empty, @@ -3645,11 +3785,11 @@ pub const ParseTask = struct { .name = "db", }, Logger.Loc{ .start = 0 }); - return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); }, .napi => { // (dap-eval-cb "source.contents.ptr") - if (bundler.options.target == .browser) { + if (transpiler.options.target == .browser) { log.addError( null, Logger.Loc.Empty, @@ -3676,18 +3816,68 @@ pub const ParseTask = struct { }, Logger.Loc{ .start = 0 }); unique_key_for_additional_file.* = unique_key; - return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + }, + .html => { + if (transpiler.options.experimental.html) { + var scanner = HTMLScanner.init(allocator, log, &source); + try scanner.scan(source.contents); + + // Reuse existing code for creating the AST + // because it handles the various Ref and other structs we + // need in order to print code later. + var ast = (try js_parser.newLazyExportAST( + allocator, + transpiler.options.define, + opts, + log, + Expr.init(E.Missing, E.Missing{}, Logger.Loc.Empty), + &source, + "", + )).?; + ast.import_records = scanner.import_records; + + // We're banning import default of html loader files for now. + // + // TLDR: it kept including: + // + // var name_default = ...; + // + // in the bundle because of the exports AST, and + // gave up on figuring out how to fix it so that + // this feature could ship. + ast.has_lazy_export = false; + ast.parts.ptr[1] = .{ + .stmts = &.{}, + .is_live = true, + .import_record_indices = brk2: { + // Generate a single part that depends on all the import records. + // This is to ensure that we generate a JavaScript bundle containing all the user's code. + var import_record_indices = try Part.ImportRecordIndices.initCapacity(allocator, scanner.import_records.len); + import_record_indices.len = @truncate(scanner.import_records.len); + for (import_record_indices.slice(), 0..) |*import_record, index| { + import_record.* = @intCast(index); + } + break :brk2 import_record_indices; + }, + }; + + // Try to avoid generating unnecessary ESM <> CJS wrapper code. + if (opts.output_format == .esm or opts.output_format == .iife) { + ast.exports_kind = .esm; + } + + return JSAst.init(ast); + } }, .css => { - if (bundler.options.experimental_css) { - // const unique_key = std.fmt.allocPrint(allocator, "{any}A{d:0>8}", .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }) catch unreachable; - // unique_key_for_additional_file.* = unique_key; + if (transpiler.options.experimental.css) { var import_records = BabyList(ImportRecord){}; const source_code = source.contents; var css_ast = switch (bun.css.BundlerStyleSheet.parseBundler( allocator, source_code, - bun.css.ParserOptions.default(allocator, bundler.log), + bun.css.ParserOptions.default(allocator, transpiler.log), &import_records, )) { .result => |v| v, @@ -3697,7 +3887,7 @@ pub const ParseTask = struct { }, }; if (css_ast.minify(allocator, bun.css.MinifyOptions{ - .targets = .{}, + .targets = bun.css.Targets.forBundlerTarget(transpiler.options.target), .unused_symbols = .{}, }).asErr()) |e| { try e.addToLogger(log, &source); @@ -3705,7 +3895,7 @@ pub const ParseTask = struct { } const root = Expr.init(E.Object, E.Object{}, Logger.Loc{ .start = 0 }); const css_ast_heap = bun.create(allocator, bun.css.BundlerStyleSheet, css_ast); - var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); ast.css = css_ast_heap; ast.import_records = import_records; return ast; @@ -3718,29 +3908,23 @@ pub const ParseTask = struct { .data = unique_key, }, Logger.Loc{ .start = 0 }); unique_key_for_additional_file.* = unique_key; - var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?); + var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); ast.url_for_css = unique_key; - ast.addUrlForCss(allocator, bundler.options.experimental_css, &source, null, unique_key); + ast.addUrlForCss(allocator, transpiler.options.experimental, &source, null, unique_key); return ast; } - fn run( + fn getCodeForParseTaskWithoutPlugins( task: *ParseTask, - this: *ThreadPool.Worker, - step: *ParseTask.Result.Error.Step, log: *Logger.Log, - ) anyerror!Result.Success { - const allocator = this.allocator; - - var data = this.data; - var bundler = &data.bundler; - errdefer bundler.resetStore(); - var resolver: *Resolver = &bundler.resolver; - var file_path = task.path; - step.* = .read_file; - const loader = task.loader orelse file_path.loader(&bundler.options.loaders) orelse options.Loader.file; - - var entry: CacheEntry = switch (task.contents_or_fd) { + transpiler: *Transpiler, + resolver: *Resolver, + allocator: std.mem.Allocator, + file_path: *Fs.Path, + loader: Loader, + experimental: Loader.Experimental, + ) !CacheEntry { + return switch (task.contents_or_fd) { .fd => |contents| brk: { const trace = tracer(@src(), "readFile"); defer trace.end(); @@ -3751,7 +3935,7 @@ pub const ParseTask = struct { switch (file) { .code => |code| break :brk .{ .contents = code }, .import => |path| { - file_path = Fs.Path.init(path); + file_path.* = Fs.Path.init(path); break :lookup_builtin; }, } @@ -3764,12 +3948,12 @@ pub const ParseTask = struct { } break :brk resolver.caches.fs.readFileWithAllocator( - if (loader.shouldCopyForBundling(this.ctx.bundler.options.experimental_css)) + if (loader.shouldCopyForBundling(experimental)) // The OutputFile will own the memory for the contents bun.default_allocator else allocator, - bundler.fs, + transpiler.fs, file_path.text, task.contents_or_fd.fd.dir, false, @@ -3808,6 +3992,382 @@ pub const ParseTask = struct { .fd = bun.invalid_fd, }, }; + } + + fn getCodeForParseTask( + task: *ParseTask, + log: *Logger.Log, + transpiler: *Transpiler, + resolver: *Resolver, + allocator: std.mem.Allocator, + file_path: *Fs.Path, + loader: *Loader, + experimental: Loader.Experimental, + from_plugin: *bool, + ) !CacheEntry { + const might_have_on_parse_plugins = brk: { + if (task.source_index.isRuntime()) break :brk false; + const plugin = task.ctx.plugins orelse break :brk false; + if (!plugin.hasOnBeforeParsePlugins()) break :brk false; + + if (strings.eqlComptime(file_path.namespace, "node")) { + break :brk false; + } + break :brk true; + }; + + if (!might_have_on_parse_plugins) { + return getCodeForParseTaskWithoutPlugins(task, log, transpiler, resolver, allocator, file_path, loader.*, experimental); + } + + var should_continue_running: i32 = 1; + + var ctx = OnBeforeParsePlugin{ + .task = task, + .log = log, + .transpiler = transpiler, + .resolver = resolver, + .allocator = allocator, + .file_path = file_path, + .loader = loader, + .experimental = experimental, + .deferred_error = null, + .should_continue_running = &should_continue_running, + }; + + return try ctx.run(task.ctx.plugins.?, from_plugin); + } + + const OnBeforeParsePlugin = struct { + task: *ParseTask, + log: *Logger.Log, + transpiler: *Transpiler, + resolver: *Resolver, + allocator: std.mem.Allocator, + file_path: *Fs.Path, + loader: *Loader, + experimental: Loader.Experimental, + deferred_error: ?anyerror = null, + should_continue_running: *i32, + + result: ?*OnBeforeParseResult = null, + + const headers = bun.C.translated; + + comptime { + bun.assert(@sizeOf(OnBeforeParseArguments) == @sizeOf(headers.OnBeforeParseArguments)); + bun.assert(@alignOf(OnBeforeParseArguments) == @alignOf(headers.OnBeforeParseArguments)); + + bun.assert(@sizeOf(BunLogOptions) == @sizeOf(headers.BunLogOptions)); + bun.assert(@alignOf(BunLogOptions) == @alignOf(headers.BunLogOptions)); + + bun.assert(@sizeOf(OnBeforeParseResult) == @sizeOf(headers.OnBeforeParseResult)); + bun.assert(@alignOf(OnBeforeParseResult) == @alignOf(headers.OnBeforeParseResult)); + + bun.assert(@sizeOf(BunLogOptions) == @sizeOf(headers.BunLogOptions)); + bun.assert(@alignOf(BunLogOptions) == @alignOf(headers.BunLogOptions)); + } + + const OnBeforeParseArguments = extern struct { + struct_size: usize = @sizeOf(OnBeforeParseArguments), + context: *OnBeforeParsePlugin, + path_ptr: [*]const u8 = "", + path_len: usize = 0, + namespace_ptr: [*]const u8 = "file", + namespace_len: usize = "file".len, + default_loader: Loader = .file, + external: ?*void = null, + }; + + const BunLogOptions = extern struct { + struct_size: usize = @sizeOf(BunLogOptions), + message_ptr: ?[*]const u8 = null, + message_len: usize = 0, + path_ptr: ?[*]const u8 = null, + path_len: usize = 0, + source_line_text_ptr: ?[*]const u8 = null, + source_line_text_len: usize = 0, + level: Logger.Log.Level = .err, + line: i32 = 0, + column: i32 = 0, + line_end: i32 = 0, + column_end: i32 = 0, + + pub fn sourceLineText(this: *const BunLogOptions) string { + if (this.source_line_text_ptr) |ptr| { + if (this.source_line_text_len > 0) { + return ptr[0..this.source_line_text_len]; + } + } + return ""; + } + + pub fn path(this: *const BunLogOptions) string { + if (this.path_ptr) |ptr| { + if (this.path_len > 0) { + return ptr[0..this.path_len]; + } + } + return ""; + } + + pub fn message(this: *const BunLogOptions) string { + if (this.message_ptr) |ptr| { + if (this.message_len > 0) { + return ptr[0..this.message_len]; + } + } + return ""; + } + + pub fn append(this: *const BunLogOptions, log: *Logger.Log, namespace: string) void { + const allocator = log.msgs.allocator; + const source_line_text = this.sourceLineText(); + const location = Logger.Location.init( + this.path(), + namespace, + @max(this.line, -1), + @max(this.column, -1), + @max(this.column_end - this.column, 0), + if (source_line_text.len > 0) allocator.dupe(u8, source_line_text) catch bun.outOfMemory() else null, + null, + ); + var msg = Logger.Msg{ .data = .{ .location = location, .text = allocator.dupe(u8, this.message()) catch bun.outOfMemory() } }; + switch (this.level) { + .err => msg.kind = .err, + .warn => msg.kind = .warn, + .verbose => msg.kind = .verbose, + .debug => msg.kind = .debug, + else => {}, + } + if (msg.kind == .err) { + log.errors += 1; + } else if (msg.kind == .warn) { + log.warnings += 1; + } + log.addMsg(msg) catch bun.outOfMemory(); + } + + pub fn logFn( + args_: ?*OnBeforeParseArguments, + log_options_: ?*BunLogOptions, + ) callconv(.C) void { + const args = args_ orelse return; + const log_options = log_options_ orelse return; + log_options.append(args.context.log, args.context.file_path.namespace); + } + }; + + const OnBeforeParseResultWrapper = struct { + original_source: ?[]const u8 = null, + loader: Loader, + check: if (bun.Environment.isDebug) u32 else u0 = if (bun.Environment.isDebug) 42069 else 0, // Value to ensure OnBeforeParseResult is wrapped in this struct + result: OnBeforeParseResult, + }; + + const OnBeforeParseResult = extern struct { + struct_size: usize = @sizeOf(OnBeforeParseResult), + source_ptr: ?[*]const u8 = null, + source_len: usize = 0, + loader: Loader, + + fetch_source_code_fn: *const fn (*OnBeforeParseArguments, *OnBeforeParseResult) callconv(.C) i32 = &fetchSourceCode, + + user_context: ?*anyopaque = null, + free_user_context: ?*const fn (?*anyopaque) callconv(.C) void = null, + + log: *const fn ( + args_: ?*OnBeforeParseArguments, + log_options_: ?*BunLogOptions, + ) callconv(.C) void = &BunLogOptions.logFn, + + pub fn getWrapper(result: *OnBeforeParseResult) *OnBeforeParseResultWrapper { + const wrapper: *OnBeforeParseResultWrapper = @fieldParentPtr("result", result); + bun.debugAssert(wrapper.check == 42069); + return wrapper; + } + }; + + pub fn fetchSourceCode(args: *OnBeforeParseArguments, result: *OnBeforeParseResult) callconv(.C) i32 { + debug("fetchSourceCode", .{}); + const this = args.context; + if (this.log.errors > 0 or this.deferred_error != null or this.should_continue_running.* != 1) { + return 1; + } + + if (result.source_ptr != null) { + return 0; + } + + const entry = getCodeForParseTaskWithoutPlugins( + this.task, + this.log, + this.transpiler, + this.resolver, + this.allocator, + this.file_path, + + result.loader, + + this.experimental, + ) catch |err| { + this.deferred_error = err; + this.should_continue_running.* = 0; + return 1; + }; + result.source_ptr = entry.contents.ptr; + result.source_len = entry.contents.len; + result.free_user_context = null; + result.user_context = null; + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + wrapper.original_source = entry.contents; + return 0; + } + + pub export fn OnBeforeParseResult__reset(this: *OnBeforeParseResult) void { + const wrapper = this.getWrapper(); + this.loader = wrapper.loader; + if (wrapper.original_source) |src| { + this.source_ptr = src.ptr; + this.source_len = src.len; + } else { + this.source_ptr = null; + this.source_len = 0; + } + } + + pub export fn OnBeforeParsePlugin__isDone(this: *OnBeforeParsePlugin) i32 { + if (this.should_continue_running.* != 1) { + return 1; + } + + const result = this.result orelse return 1; + // The first plugin to set the source wins. + // But, we must check that they actually modified it + // since fetching the source stores it inside `result.source_ptr` + if (result.source_ptr != null) { + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + return @intFromBool(result.source_ptr.? != wrapper.original_source.?.ptr); + } + + return 0; + } + + pub fn run(this: *OnBeforeParsePlugin, plugin: *JSC.API.JSBundler.Plugin, from_plugin: *bool) !CacheEntry { + var args = OnBeforeParseArguments{ + .context = this, + .path_ptr = this.file_path.text.ptr, + .path_len = this.file_path.text.len, + .default_loader = this.loader.*, + }; + if (this.file_path.namespace.len > 0) { + args.namespace_ptr = this.file_path.namespace.ptr; + args.namespace_len = this.file_path.namespace.len; + } + var wrapper = OnBeforeParseResultWrapper{ + .loader = this.loader.*, + .result = OnBeforeParseResult{ + .loader = this.loader.*, + }, + }; + + this.result = &wrapper.result; + const count = plugin.callOnBeforeParsePlugins( + this, + if (bun.strings.eqlComptime(this.file_path.namespace, "file")) + &bun.String.empty + else + &bun.String.init(this.file_path.namespace), + + &bun.String.init(this.file_path.text), + &args, + &wrapper.result, + this.should_continue_running, + ); + if (comptime Environment.enable_logs) + debug("callOnBeforeParsePlugins({s}:{s}) = {d}", .{ this.file_path.namespace, this.file_path.text, count }); + if (count > 0) { + if (this.deferred_error) |err| { + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); + } + + return err; + } + + // If the plugin sets the `free_user_context` function pointer, it _must_ set the `user_context` pointer. + // Otherwise this is just invalid behavior. + if (wrapper.result.user_context == null and wrapper.result.free_user_context != null) { + var msg = Logger.Msg{ .data = .{ .location = null, .text = bun.default_allocator.dupe( + u8, + "Native plugin set the `free_plugin_source_code_context` field without setting the `plugin_source_code_context` field.", + ) catch bun.outOfMemory() } }; + msg.kind = .err; + args.context.log.errors += 1; + args.context.log.addMsg(msg) catch bun.outOfMemory(); + return error.InvalidNativePlugin; + } + + if (this.log.errors > 0) { + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); + } + + return error.SyntaxError; + } + + if (wrapper.result.source_ptr) |ptr| { + if (wrapper.result.free_user_context != null) { + this.task.external = CacheEntry.External{ + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, + }; + } + from_plugin.* = true; + this.loader.* = wrapper.result.loader; + return CacheEntry{ + .contents = ptr[0..wrapper.result.source_len], + .external = .{ + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, + }, + }; + } + } + + return try getCodeForParseTaskWithoutPlugins(this.task, this.log, this.transpiler, this.resolver, this.allocator, this.file_path, this.loader.*, this.experimental); + } + }; + + fn run( + task: *ParseTask, + this: *ThreadPool.Worker, + step: *ParseTask.Result.Error.Step, + log: *Logger.Log, + ) anyerror!Result.Success { + const allocator = this.allocator; + + var data = this.data; + var transpiler = &data.transpiler; + errdefer transpiler.resetStore(); + var resolver: *Resolver = &transpiler.resolver; + var file_path = task.path; + step.* = .read_file; + var loader = task.loader orelse file_path.loader(&transpiler.options.loaders) orelse options.Loader.file; + + // Do not process files as HTML if any of the following are true: + // - building for node or bun.js + // - the experimental.html flag is not enabled. + // + // We allow non-entrypoints to import HTML so that people could + // potentially use an onLoad plugin that returns HTML. + if (!transpiler.options.experimental.html or task.known_target != .browser) { + loader = loader.disableHTML(); + task.loader = loader; + } + + var contents_came_from_plugin: bool = false; + var entry = try getCodeForParseTask(task, log, transpiler, resolver, allocator, &file_path, &loader, this.ctx.transpiler.options.experimental, &contents_came_from_plugin); // WARNING: Do not change the variant of `task.contents_or_fd` from // `.fd` to `.contents` (or back) after this point! @@ -3851,7 +4411,7 @@ pub const ParseTask = struct { const is_empty = strings.isAllWhitespace(entry.contents); - const use_directive: UseDirective = if (!is_empty and bundler.options.server_components) + const use_directive: UseDirective = if (!is_empty and transpiler.options.server_components) if (UseDirective.parse(entry.contents)) |use| use else @@ -3866,11 +4426,11 @@ pub const ParseTask = struct { task.known_target != .bake_server_components_ssr and this.ctx.framework.?.server_components.?.separate_ssr_graph) or // set the target to the client when bundling client-side files - (bundler.options.server_components and task.known_target == .browser)) + (transpiler.options.server_components and task.known_target == .browser)) { - bundler = this.ctx.client_bundler; - resolver = &bundler.resolver; - bun.assert(bundler.options.target == .browser); + transpiler = this.ctx.client_bundler; + resolver = &transpiler.resolver; + bun.assert(transpiler.options.target == .browser); } var source = Logger.Source{ @@ -3881,12 +4441,12 @@ pub const ParseTask = struct { }; const target = (if (task.source_index.get() == 1) targetFromHashbang(entry.contents) else null) orelse - if (task.known_target == .bake_server_components_ssr and bundler.options.framework.?.server_components.?.separate_ssr_graph) + if (task.known_target == .bake_server_components_ssr and transpiler.options.framework.?.server_components.?.separate_ssr_graph) .bake_server_components_ssr else - bundler.options.target; + transpiler.options.target; - const output_format = bundler.options.output_format; + const output_format = transpiler.options.output_format; var opts = js_parser.Parser.Options.init(task.jsx, loader); opts.bundle = true; @@ -3894,30 +4454,29 @@ pub const ParseTask = struct { opts.macro_context = &this.data.macro_context; opts.package_version = task.package_version; - opts.features.auto_polyfill_require = output_format == .esm and !target.isBun(); + opts.features.auto_polyfill_require = output_format == .esm; opts.features.allow_runtime = !source.index.isRuntime(); opts.features.unwrap_commonjs_to_esm = output_format == .esm and FeatureFlags.unwrap_commonjs_to_esm; - opts.features.use_import_meta_require = target.isBun(); opts.features.top_level_await = output_format == .esm or output_format == .internal_bake_dev; - opts.features.auto_import_jsx = task.jsx.parse and bundler.options.auto_import_jsx; - opts.features.trim_unused_imports = loader.isTypeScript() or (bundler.options.trim_unused_imports orelse false); - opts.features.inlining = bundler.options.minify_syntax; + opts.features.auto_import_jsx = task.jsx.parse and transpiler.options.auto_import_jsx; + opts.features.trim_unused_imports = loader.isTypeScript() or (transpiler.options.trim_unused_imports orelse false); + opts.features.inlining = transpiler.options.minify_syntax; opts.output_format = output_format; - opts.features.minify_syntax = bundler.options.minify_syntax; - opts.features.minify_identifiers = bundler.options.minify_identifiers; - opts.features.emit_decorator_metadata = bundler.options.emit_decorator_metadata; - opts.features.unwrap_commonjs_packages = bundler.options.unwrap_commonjs_packages; + opts.features.minify_syntax = transpiler.options.minify_syntax; + opts.features.minify_identifiers = transpiler.options.minify_identifiers; + opts.features.emit_decorator_metadata = transpiler.options.emit_decorator_metadata; + opts.features.unwrap_commonjs_packages = transpiler.options.unwrap_commonjs_packages; opts.features.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime(); opts.features.react_fast_refresh = target == .browser and - bundler.options.react_fast_refresh and + transpiler.options.react_fast_refresh and loader.isJSX() and !source.path.isNodeModule(); - opts.features.server_components = if (bundler.options.server_components) switch (target) { + opts.features.server_components = if (transpiler.options.server_components) switch (target) { .browser => .client_side, else => switch (use_directive) { .none => .wrap_anon_server_functions, - .client => if (bundler.options.framework.?.server_components.?.separate_ssr_graph) + .client => if (transpiler.options.framework.?.server_components.?.separate_ssr_graph) .client_side else .wrap_exports_for_client_reference, @@ -3925,37 +4484,37 @@ pub const ParseTask = struct { }, } else .none; - opts.framework = bundler.options.framework; + opts.framework = transpiler.options.framework; - opts.ignore_dce_annotations = bundler.options.ignore_dce_annotations and !source.index.isRuntime(); + opts.ignore_dce_annotations = transpiler.options.ignore_dce_annotations and !source.index.isRuntime(); // For files that are not user-specified entrypoints, set `import.meta.main` to `false`. // Entrypoints will have `import.meta.main` set as "unknown", unless we use `--compile`, // in which we inline `true`. - if (bundler.options.inline_entrypoint_import_meta_main or !task.is_entry_point) { + if (transpiler.options.inline_entrypoint_import_meta_main or !task.is_entry_point) { opts.import_meta_main_value = task.is_entry_point; - } else if (bundler.options.target == .node) { + } else if (transpiler.options.target == .node) { opts.lower_import_meta_main_for_node_js = true; } - opts.tree_shaking = if (source.index.isRuntime()) true else bundler.options.tree_shaking; + opts.tree_shaking = if (source.index.isRuntime()) true else transpiler.options.tree_shaking; opts.module_type = task.module_type; task.jsx.parse = loader.isJSX(); var unique_key_for_additional_file: []const u8 = ""; var ast: JSAst = if (!is_empty) - try getAST(log, bundler, opts, allocator, resolver, source, loader, task.ctx.unique_key, &unique_key_for_additional_file) + try getAST(log, transpiler, opts, allocator, resolver, source, loader, task.ctx.unique_key, &unique_key_for_additional_file) else switch (opts.module_type == .esm) { - inline else => |as_undefined| if (loader == .css and this.ctx.bundler.options.experimental_css) try getEmptyCSSAST( + inline else => |as_undefined| if (loader == .css and this.ctx.transpiler.options.experimental.css) try getEmptyCSSAST( log, - bundler, + transpiler, opts, allocator, source, ) else try getEmptyAST( log, - bundler, + transpiler, opts, allocator, source, @@ -3964,7 +4523,7 @@ pub const ParseTask = struct { }; ast.target = target; - if (ast.parts.len <= 1 and ast.css == null) { + if (ast.parts.len <= 1 and ast.css == null and (task.loader == null or task.loader.? != .html)) { task.side_effects = .no_side_effects__empty_ast; } @@ -3977,9 +4536,10 @@ pub const ParseTask = struct { .use_directive = use_directive, .unique_key_for_additional_file = unique_key_for_additional_file, .side_effects = task.side_effects, + .loader = loader, // Hash the files in here so that we do it in parallel. - .content_hash_for_additional_file = if (loader.shouldCopyForBundling(this.ctx.bundler.options.experimental_css)) + .content_hash_for_additional_file = if (loader.shouldCopyForBundling(this.ctx.transpiler.options.experimental)) ContentHasher.run(source.contents) else 0, @@ -4000,7 +4560,7 @@ pub const ParseTask = struct { const value: ParseTask.Result.Value = if (run(this, worker, &step, &log)) |ast| value: { // When using HMR, always flag asts with errors as parse failures. // Not done outside of the dev server out of fear of breaking existing code. - if (this.ctx.bundler.options.dev_server != null and ast.log.hasErrors()) { + if (this.ctx.transpiler.options.dev_server != null and ast.log.hasErrors()) { break :value .{ .err = .{ .err = error.SyntaxError, @@ -4033,6 +4593,7 @@ pub const ParseTask = struct { .ctx = this.ctx, .task = undefined, .value = value, + .external = this.external, .watcher_data = .{ .fd = if (this.contents_or_fd == .fd) this.contents_or_fd.fd.file else bun.invalid_fd, .dir_fd = if (this.contents_or_fd == .fd) this.contents_or_fd.fd.dir else bun.invalid_fd, @@ -4131,7 +4692,7 @@ pub const ServerComponentParseTask = struct { log: *Logger.Log, allocator: std.mem.Allocator, ) bun.OOM!ParseTask.Result.Success { - var ab = try AstBuilder.init(allocator, &task.source, task.ctx.bundler.options.hot_module_reloading); + var ab = try AstBuilder.init(allocator, &task.source, task.ctx.transpiler.options.hot_module_reloading); switch (task.data) { .client_reference_proxy => |data| try task.generateClientReferenceProxy(data, &ab), @@ -4141,11 +4702,12 @@ pub const ServerComponentParseTask = struct { return .{ .ast = try ab.toBundledAst(switch (task.data) { // Server-side - .client_reference_proxy => task.ctx.bundler.options.target, + .client_reference_proxy => task.ctx.transpiler.options.target, // Client-side, .client_entry_wrapper => .browser, }), .source = task.source, + .loader = .js, .log = log.*, .use_directive = .none, .side_effects = .no_side_effects__pure_data, @@ -4181,7 +4743,7 @@ pub const ServerComponentParseTask = struct { // In production, the path here must be the final chunk path, but // that information is not yet available since chunks are not // computed. The unique_key replacement system is used here. - .data = if (task.ctx.bundler.options.dev_server != null) + .data = if (task.ctx.transpiler.options.dev_server != null) data.other_source.path.pretty else try std.fmt.allocPrint(b.allocator, "{}S{d:0>8}", .{ @@ -4511,6 +5073,7 @@ pub const Graph = struct { source: Logger.Source, loader: options.Loader = options.Loader.file, side_effects: _resolver.SideEffects, + allocator: std.mem.Allocator = bun.default_allocator, additional_files: BabyList(AdditionalFile) = .{}, unique_key_for_additional_file: string = "", content_hash_for_additional_file: u64 = 0, @@ -4520,15 +5083,15 @@ pub const Graph = struct { /// each `.defer()` called in an onLoad plugin. /// /// Returns true if there were more tasks queued. - pub fn drainDeferredTasks(this: *@This(), bundler: *BundleV2) bool { - bundler.thread_lock.assertLocked(); + pub fn drainDeferredTasks(this: *@This(), transpiler: *BundleV2) bool { + transpiler.thread_lock.assertLocked(); if (this.deferred_pending > 0) { this.pending_items += this.deferred_pending; this.deferred_pending = 0; - bundler.drain_defer_task.init(); - bundler.drain_defer_task.schedule(); + transpiler.drain_defer_task.init(); + transpiler.drain_defer_task.schedule(); return true; } @@ -4566,6 +5129,7 @@ const EntryPoint = struct { none, user_specified, dynamic_import, + html, pub fn outputKind(this: Kind) JSC.API.BuildArtifact.OutputKind { return switch (this) { @@ -4688,9 +5252,9 @@ const LinkerGraph = struct { pub fn addPartToFile( graph: *LinkerGraph, id: u32, - part: js_ast.Part, + part: Part, ) !u32 { - var parts: *js_ast.Part.List = &graph.ast.items(.parts)[id]; + var parts: *Part.List = &graph.ast.items(.parts)[id]; const part_id = @as(u32, @truncate(parts.len)); try parts.push(graph.allocator, part); var top_level_symbol_to_parts_overlay: ?*TopLevelSymbolToParts = null; @@ -4760,7 +5324,7 @@ const LinkerGraph = struct { if (use_count == 0) return; var parts_list = g.ast.items(.parts)[source_index].slice(); - var part: *js_ast.Part = &parts_list[part_index]; + var part: *Part = &parts_list[part_index]; // Mark this symbol as used by this part @@ -5100,7 +5664,7 @@ pub const LinkerContext = struct { minify_identifiers: bool = false, banner: []const u8 = "", footer: []const u8 = "", - experimental_css: bool = false, + experimental: Loader.Experimental = .{}, css_chunking: bool = false, source_maps: options.SourceMapOption = .none, target: options.Target = .browser, @@ -5198,7 +5762,7 @@ pub const LinkerContext = struct { return format != .cjs; } - pub fn shouldIncludePart(c: *LinkerContext, source_index: Index.Int, part: js_ast.Part) bool { + pub fn shouldIncludePart(c: *LinkerContext, source_index: Index.Int, part: Part) bool { // As an optimization, ignore parts containing a single import statement to // an internal non-wrapped file. These will be ignored anyway and it's a // performance hit to spin up a goroutine only to discover this later. @@ -5225,10 +5789,10 @@ pub const LinkerContext = struct { defer trace.end(); this.parse_graph = &bundle.graph; - this.graph.code_splitting = bundle.bundler.options.code_splitting; - this.log = bundle.bundler.log; + this.graph.code_splitting = bundle.transpiler.options.code_splitting; + this.log = bundle.transpiler.log; - this.resolver = &bundle.bundler.resolver; + this.resolver = &bundle.transpiler.resolver; this.cycle_detector = std.ArrayList(ImportTracker).init(this.allocator); this.graph.reachable_files = reachable; @@ -5382,6 +5946,15 @@ pub const LinkerContext = struct { this.parse_graph.heap.gc(true); } + const JSChunkKeyFormatter = struct { + has_html: bool, + entry_bits: []const u8, + + pub fn format(this: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { + try writer.writeAll(&[_]u8{@intFromBool(!this.has_html)}); + try writer.writeAll(this.entry_bits); + } + }; pub noinline fn computeChunks( this: *LinkerContext, unique_key: u64, @@ -5406,6 +5979,13 @@ pub const LinkerContext = struct { const entry_source_indices = this.graph.entry_points.items(.source_index); const css_asts = this.graph.ast.items(.css); + const experimental_css = this.options.experimental.css; + const experimental_html = this.options.experimental.html; + const css_chunking = this.options.css_chunking; + var html_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator); + const loaders = this.parse_graph.input_files.items(.loader); + + const code_splitting = this.graph.code_splitting; // Create chunks for entry points for (entry_source_indices, 0..) |source_index, entry_id_| { @@ -5414,7 +5994,41 @@ pub const LinkerContext = struct { var entry_bits = &this.graph.files.items(.entry_bits)[source_index]; entry_bits.set(entry_bit); - if (this.options.experimental_css and css_asts[source_index] != null) { + const has_html_chunk = experimental_html and loaders[source_index] == .html; + const js_chunk_key = brk: { + if (code_splitting) { + break :brk try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)); + } else { + // Force HTML chunks to always be generated, even if there's an identical JS file. + break :brk try std.fmt.allocPrint(temp_allocator, "{}", .{JSChunkKeyFormatter{ + .has_html = has_html_chunk, + .entry_bits = entry_bits.bytes(this.graph.entry_points.len), + }}); + } + }; + + // Put this early on in this loop so that CSS-only entry points work. + if (has_html_chunk) { + const html_chunk_entry = try html_chunks.getOrPut( + js_chunk_key, + ); + if (!html_chunk_entry.found_existing) { + html_chunk_entry.value_ptr.* = .{ + .entry_point = .{ + .entry_point_id = entry_bit, + .source_index = source_index, + .is_entry_point = true, + }, + .entry_bits = entry_bits.*, + .content = .{ + .html = .{}, + }, + .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), + }; + } + } + + if (experimental_css and css_asts[source_index] != null) { const order = this.findImportedFilesInCSSOrder(temp_allocator, &.{Index.init(source_index)}); // Create a chunk for the entry point here to ensure that the chunk is // always generated even if the resulting file is empty @@ -5443,14 +6057,16 @@ pub const LinkerContext = struct { }, }, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), + .has_html_chunk = has_html_chunk, }; } continue; } + // Create a chunk for the entry point here to ensure that the chunk is // always generated even if the resulting file is empty - const js_chunk_entry = try js_chunks.getOrPut(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len))); + const js_chunk_entry = try js_chunks.getOrPut(js_chunk_key); js_chunk_entry.value_ptr.* = .{ .entry_point = .{ .entry_point_id = entry_bit, @@ -5461,10 +6077,11 @@ pub const LinkerContext = struct { .content = .{ .javascript = .{}, }, + .has_html_chunk = has_html_chunk, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), }; - if (this.options.experimental_css) { + if (experimental_css) { // If this JS entry point has an associated CSS entry point, generate it // now. This is essentially done by generating a virtual CSS file that // only contains "@import" statements in the order that the files were @@ -5475,7 +6092,7 @@ pub const LinkerContext = struct { if (css_source_indices.len > 0) { const order = this.findImportedFilesInCSSOrder(temp_allocator, css_source_indices.slice()); - const hash_to_use = if (!this.options.css_chunking) + const hash_to_use = if (!css_chunking) bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len))) else brk: { var hasher = std.hash.Wyhash.init(5); @@ -5513,6 +6130,7 @@ pub const LinkerContext = struct { }, .files_with_parts_in_chunk = css_files_with_parts_in_chunk, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), + .has_html_chunk = has_html_chunk, }; } } @@ -5541,9 +6159,8 @@ pub const LinkerContext = struct { if (css_reprs[source_index.get()] != null) continue; if (this.graph.code_splitting) { - var js_chunk_entry = try js_chunks.getOrPut( - try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)), - ); + const js_chunk_key = try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)); + var js_chunk_entry = try js_chunks.getOrPut(js_chunk_key); if (!js_chunk_entry.found_existing) { js_chunk_entry.value_ptr.* = .{ @@ -5575,7 +6192,7 @@ pub const LinkerContext = struct { // Sort the chunks for determinism. This matters because we use chunk indices // as sorting keys in a few places. const chunks: []Chunk = sort_chunks: { - var sorted_chunks = try BabyList(Chunk).initCapacity(this.allocator, js_chunks.count() + css_chunks.count()); + var sorted_chunks = try BabyList(Chunk).initCapacity(this.allocator, js_chunks.count() + css_chunks.count() + html_chunks.count()); var sorted_keys = try BabyList(string).initCapacity(temp_allocator, js_chunks.count()); @@ -5583,12 +6200,20 @@ pub const LinkerContext = struct { sorted_keys.appendSliceAssumeCapacity(js_chunks.keys()); sorted_keys.sortAsc(); var js_chunk_indices_with_css = try BabyList(u32).initCapacity(temp_allocator, js_chunks_with_css); - for (sorted_keys.slice(), 0..) |key, i| { + for (sorted_keys.slice()) |key| { const chunk = js_chunks.get(key) orelse unreachable; - sorted_chunks.appendAssumeCapacity(chunk); if (chunk.content.javascript.css_chunks.len > 0) - js_chunk_indices_with_css.appendAssumeCapacity(@intCast(i)); + js_chunk_indices_with_css.appendAssumeCapacity(sorted_chunks.len); + + sorted_chunks.appendAssumeCapacity(chunk); + + // Attempt to order the JS HTML chunk immediately after the non-html one. + if (chunk.has_html_chunk and experimental_html) { + if (html_chunks.fetchSwapRemove(key)) |html_chunk| { + sorted_chunks.appendAssumeCapacity(html_chunk.value); + } + } } if (css_chunks.count() > 0) { @@ -5613,6 +6238,12 @@ pub const LinkerContext = struct { } } + // We don't care about the order of the HTML chunks that have + // no JS chunks. + if (experimental_html) { + try sorted_chunks.append(this.allocator, html_chunks.values()); + } + break :sort_chunks sorted_chunks.slice(); }; @@ -5622,7 +6253,7 @@ pub const LinkerContext = struct { // to look up the path for this chunk to use with the import. for (chunks, 0..) |*chunk, chunk_id| { if (chunk.entry_point.is_entry_point) { - entry_point_chunk_indices[chunk.entry_point.source_index] = @as(u32, @truncate(chunk_id)); + entry_point_chunk_indices[chunk.entry_point.source_index] = @intCast(chunk_id); } } @@ -5649,7 +6280,7 @@ pub const LinkerContext = struct { this.unique_key_prefix = chunk.unique_key[0..std.fmt.count("{}", .{bun.fmt.hexIntLower(unique_key)})]; if (chunk.entry_point.is_entry_point and - kinds[chunk.entry_point.source_index] == .user_specified) + (chunk.content == .html or (kinds[chunk.entry_point.source_index] == .user_specified and !chunk.has_html_chunk))) { chunk.template = PathTemplate.file; if (this.resolver.opts.entry_naming.len > 0) @@ -5702,6 +6333,7 @@ pub const LinkerContext = struct { ); }, .css => {}, // handled in `findImportedCSSFilesInJSOrder` + .html => {}, } } } @@ -5731,7 +6363,7 @@ pub const LinkerContext = struct { const FindImportedPartsVisitor = struct { entry_bits: *const AutoBitSet, flags: []const JSMeta.Flags, - parts: []BabyList(js_ast.Part), + parts: []BabyList(Part), import_records: []BabyList(ImportRecord), files: std.ArrayList(Index.Int), part_ranges: std.ArrayList(PartRange), @@ -6381,7 +7013,7 @@ pub const LinkerContext = struct { @panic("Internal error: expected at least one part for lazy export"); } - var part: *js_ast.Part = &parts.ptr[1]; + var part: *Part = &parts.ptr[1]; if (part.stmts.len == 0) { @panic("Internal error: expected at least one statement in the lazy export"); @@ -6393,7 +7025,7 @@ pub const LinkerContext = struct { } const expr = Expr{ - .data = stmt.data.s_lazy_export, + .data = stmt.data.s_lazy_export.*, .loc = stmt.loc, }; const module_ref = this.graph.ast.items(.module_ref)[source_index]; @@ -6415,18 +7047,17 @@ pub const LinkerContext = struct { try this.graph.generateSymbolImportAndUse(source_index, 0, module_ref, 1, Index.init(source_index)); // If this is a .napi addon and it's not node, we need to generate a require() call to the runtime - if (expr.data == .e_call and expr.data.e_call.target.data == .e_require_call_target and + if (expr.data == .e_call and + expr.data.e_call.target.data == .e_require_call_target and // if it's commonjs, use require() - this.options.output_format != .cjs and - // if it's esm and bun, use import.meta.require(). the code for __require is not injected into the bundle. - !this.options.target.isBun()) + this.options.output_format != .cjs) { - this.graph.generateRuntimeSymbolImportAndUse( + try this.graph.generateRuntimeSymbolImportAndUse( source_index, Index.part(1), "__require", 1, - ) catch {}; + ); } }, else => { @@ -6525,7 +7156,7 @@ pub const LinkerContext = struct { { var import_records_list: []ImportRecord.List = this.graph.ast.items(.import_records); - // var parts_list: [][]js_ast.Part = this.graph.ast.items(.parts); + // var parts_list: [][]Part = this.graph.ast.items(.parts); var exports_kind: []js_ast.ExportsKind = this.graph.ast.items(.exports_kind); var entry_point_kinds: []EntryPoint.Kind = this.graph.files.items(.entry_point_kind); var named_imports: []js_ast.Ast.NamedImports = this.graph.ast.items(.named_imports); @@ -6558,15 +7189,14 @@ pub const LinkerContext = struct { const import_records: []ImportRecord = import_records_list[id].slice(); // Is it CSS? - if (css_asts[id]) |css| { - _ = css; // autofix + if (css_asts[id] != null) { // Inline URLs for non-CSS files into the CSS file - for (import_records, 0..) |*record, import_record_idx| { - _ = import_record_idx; // autofix + for (import_records) |*record| { if (record.source_index.isValid()) { // Other file is not CSS if (css_asts[record.source_index.get()] == null) { - if (urls_for_css[record.source_index.get()]) |url| { + const url = urls_for_css[record.source_index.get()]; + if (url.len > 0) { record.path.text = url; } } @@ -7016,9 +7646,9 @@ pub const LinkerContext = struct { ) catch unreachable; } var imports_to_bind_list: []RefImportData = this.graph.meta.items(.imports_to_bind); - var parts_list: []js_ast.Part.List = ast_fields.items(.parts); + var parts_list: []Part.List = ast_fields.items(.parts); - var parts: []js_ast.Part = parts_list[id].slice(); + var parts: []Part = parts_list[id].slice(); const imports_to_bind = &imports_to_bind_list[id]; for (imports_to_bind.keys(), imports_to_bind.values()) |ref_untyped, import_untyped| { @@ -7029,7 +7659,7 @@ pub const LinkerContext = struct { if (named_imports[id].get(ref)) |named_import| { for (named_import.local_parts_with_uses.slice()) |part_index| { - var part: *js_ast.Part = &parts[part_index]; + var part: *Part = &parts[part_index]; const parts_declaring_symbol: []const u32 = this.graph.topLevelSymbolToParts(import_source_index, import.data.import_ref); const total_len = parts_declaring_symbol.len + @as(usize, import.re_exports.len) + @as(usize, part.dependencies.len); @@ -7168,11 +7798,9 @@ pub const LinkerContext = struct { continue; } else { - // We should use "__require" instead of "require" if we're not // generating a CommonJS output file, since it won't exist otherwise. - // Disabled for target bun because `import.meta.require` will be inlined. - if (shouldCallRuntimeRequire(output_format) and !this.resolver.opts.target.isBun()) { + if (shouldCallRuntimeRequire(output_format)) { runtime_require_uses += 1; } @@ -7248,7 +7876,7 @@ pub const LinkerContext = struct { // module may be simultaneously imported and required, and the // importing code should not see "__esModule" while the requiring // code should see "__esModule". This is an extremely complex - // and subtle set of bundler interop issues. See for example + // and subtle set of transpiler interop issues. See for example // https://github.com/evanw/esbuild/issues/1591. if (kind == .require) { record.wrap_with_to_commonjs = true; @@ -7381,7 +8009,7 @@ pub const LinkerContext = struct { var properties = std.ArrayList(js_ast.G.Property) .initCapacity(allocator, export_aliases.len) catch bun.outOfMemory(); - var ns_export_symbol_uses = js_ast.Part.SymbolUseMap{}; + var ns_export_symbol_uses = Part.SymbolUseMap{}; ns_export_symbol_uses.ensureTotalCapacity(allocator, export_aliases.len) catch bun.outOfMemory(); const initial_flags = c.graph.meta.items(.flags)[id]; @@ -7703,7 +8331,7 @@ pub const LinkerContext = struct { var local_dependencies = std.AutoHashMap(u32, u32).init(allocator); defer local_dependencies.deinit(); - const parts_slice: []js_ast.Part = c.graph.ast.items(.parts)[id].slice(); + const parts_slice: []Part = c.graph.ast.items(.parts)[id].slice(); const named_imports: *js_ast.Ast.NamedImports = &c.graph.ast.items(.named_imports)[id]; const our_imports_to_bind = imports_to_bind[id]; @@ -7939,7 +8567,7 @@ pub const LinkerContext = struct { const CrossChunkDependencies = struct { chunk_meta: []ChunkMeta, chunks: []Chunk, - parts: []BabyList(js_ast.Part), + parts: []BabyList(Part), import_records: []BabyList(bun.ImportRecord), flags: []const JSMeta.Flags, entry_point_chunk_indices: []Index.Int, @@ -8370,6 +8998,7 @@ pub const LinkerContext = struct { switch (chunk.content) { .javascript => postProcessJSChunk(ctx, worker, chunk, chunk_index) catch |err| Output.panic("TODO: handle error: {s}", .{@errorName(err)}), .css => postProcessCSSChunk(ctx, worker, chunk) catch |err| Output.panic("TODO: handle error: {s}", .{@errorName(err)}), + .html => postProcessHTMLChunk(ctx, worker, chunk) catch |err| Output.panic("TODO: handle error: {s}", .{@errorName(err)}), } } @@ -8385,7 +9014,7 @@ pub const LinkerContext = struct { defer trace.end(); const all_module_scopes = c.graph.ast.items(.module_scope); const all_flags: []const JSMeta.Flags = c.graph.meta.items(.flags); - const all_parts: []const js_ast.Part.List = c.graph.ast.items(.parts); + const all_parts: []const Part.List = c.graph.ast.items(.parts); const all_wrapper_refs: []const Ref = c.graph.ast.items(.wrapper_ref); const all_import_records: []const ImportRecord.List = c.graph.ast.items(.import_records); @@ -8494,8 +9123,8 @@ pub const LinkerContext = struct { } top_level_symbols.clearRetainingCapacity(); - for (sorted_imports_from_other_chunks.items) |stable| { - try minify_renamer.accumulateSymbolUseCount(&top_level_symbols, stable.ref, 1, stable_source_indices); + for (sorted_imports_from_other_chunks.items) |stable_ref| { + try minify_renamer.accumulateSymbolUseCount(&top_level_symbols, stable_ref.ref, 1, stable_source_indices); } top_level_symbols_all.appendSlice(top_level_symbols.items) catch unreachable; try minify_renamer.allocateTopLevelSymbolSlots(top_level_symbols_all); @@ -8522,7 +9151,7 @@ pub const LinkerContext = struct { for (files_in_order) |source_index| { const wrap = all_flags[source_index].wrap; - const parts: []const js_ast.Part = all_parts[source_index].slice(); + const parts: []const Part = all_parts[source_index].slice(); switch (wrap) { // Modules wrapped in a CommonJS closure look like this: @@ -8634,6 +9263,7 @@ pub const LinkerContext = struct { switch (chunk.content) { .javascript => generateJSRenamer_(ctx, worker, chunk, chunk_index), .css => {}, + .html => {}, } } @@ -8666,6 +9296,16 @@ pub const LinkerContext = struct { ctx.chunk.compile_results_for_chunk[part_range.i] = generateCompileResultForCssChunkImpl(worker, ctx.c, ctx.chunk, part_range.i); } + fn generateCompileResultForHtmlChunk(task: *ThreadPoolLib.Task) void { + const part_range: *const PendingPartRange = @fieldParentPtr("task", task); + const ctx = part_range.ctx; + defer ctx.wg.finish(); + var worker = ThreadPool.Worker.get(@fieldParentPtr("linker", ctx.c)); + defer worker.unget(); + + ctx.chunk.compile_results_for_chunk[part_range.i] = generateCompileResultForHTMLChunkImpl(worker, ctx.c, ctx.chunk, ctx.chunks); + } + fn generateCompileResultForCssChunkImpl(worker: *ThreadPool.Worker, c: *LinkerContext, chunk: *Chunk, imports_in_chunk_index: u32) CompileResult { const trace = tracer(@src(), "generateCodeForFileInChunkCss"); defer trace.end(); @@ -8698,13 +9338,15 @@ pub const LinkerContext = struct { }; var import_records = BabyList(ImportRecord).init(&import_records_); const css: *const bun.css.BundlerStyleSheet = &chunk.content.css.asts[imports_in_chunk_index]; + const printer_options = bun.css.PrinterOptions{ + // TODO: make this more configurable + .minify = c.options.minify_whitespace, + .targets = bun.css.Targets.forBundlerTarget(c.options.target), + }; _ = css.toCssWithWriter( worker.allocator, &buffer_writer, - bun.css.PrinterOptions{ - // TODO: make this more configurable - .minify = c.options.minify_whitespace, - }, + printer_options, &import_records, ) catch { @panic("TODO: HANDLE THIS ERROR!"); @@ -8718,13 +9360,15 @@ pub const LinkerContext = struct { }, .source_index => |idx| { const css: *const bun.css.BundlerStyleSheet = &chunk.content.css.asts[imports_in_chunk_index]; + const printer_options = bun.css.PrinterOptions{ + .targets = bun.css.Targets.forBundlerTarget(c.options.target), + // TODO: make this more configurable + .minify = c.options.minify_whitespace or c.options.minify_syntax or c.options.minify_identifiers, + }; _ = css.toCssWithWriter( worker.allocator, &buffer_writer, - bun.css.PrinterOptions{ - // TODO: make this more configurable - .minify = c.options.minify_whitespace or c.options.minify_syntax or c.options.minify_identifiers, - }, + printer_options, &c.graph.ast.items(.import_records)[idx.get()], ) catch { @panic("TODO: HANDLE THIS ERROR!"); @@ -8786,7 +9430,7 @@ pub const LinkerContext = struct { var runtime_members = &runtime_scope.members; const toCommonJSRef = c.graph.symbols.follow(runtime_members.get("__toCommonJS").?.ref); const toESMRef = c.graph.symbols.follow(runtime_members.get("__toESM").?.ref); - const runtimeRequireRef = if (c.resolver.opts.target.isBun()) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref); + const runtimeRequireRef = if (c.options.output_format == .cjs) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref); const result = c.generateCodeForFileInChunkJS( &buffer_writer, @@ -8962,6 +9606,161 @@ pub const LinkerContext = struct { } } + fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerContext, chunk: *Chunk, chunks: []Chunk) CompileResult { + const parse_graph = c.parse_graph; + const input_files = parse_graph.input_files.slice(); + const sources = input_files.items(.source); + const import_records = c.graph.ast.items(.import_records); + + // We want to rewrite the HTML with the following transforms: + // 1. Remove all ", .{js_chunk.unique_key}) catch bun.outOfMemory(); + defer allocator.free(script); + element.append(script, true) catch bun.outOfMemory(); + } + } + + const processor = HTMLScanner.HTMLProcessor(@This(), true); + + pub fn run(this: *@This(), input: []const u8) !void { + processor.run(this, input) catch bun.outOfMemory(); + } + }; + + var html_loader = HTMLLoader{ + .linker = c, + .source_index = chunk.entry_point.source_index, + .import_records = import_records[chunk.entry_point.source_index].slice(), + .log = c.log, + .allocator = worker.allocator, + .minify_whitespace = c.options.minify_whitespace, + .chunk = chunk, + .chunks = chunks, + .output = std.ArrayList(u8).init(worker.allocator), + .current_import_record_index = 0, + }; + + html_loader.run(sources[chunk.entry_point.source_index].contents) catch bun.outOfMemory(); + + return .{ + .html = .{ + .code = html_loader.output.items, + .source_index = chunk.entry_point.source_index, + }, + }; + } + + fn postProcessHTMLChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chunk: *Chunk) !void { + + // This is where we split output into pieces + + const c = ctx.c; + var j = StringJoiner{ + .allocator = worker.allocator, + .watcher = .{ + .input = chunk.unique_key, + }, + }; + + const compile_results = chunk.compile_results_for_chunk; + + for (compile_results) |compile_result| { + j.push(compile_result.code(), bun.default_allocator); + } + + j.ensureNewlineAtEnd(); + + chunk.intermediate_output = c.breakOutputIntoPieces( + worker.allocator, + &j, + @as(u32, @truncate(ctx.chunks.len)), + ) catch bun.outOfMemory(); + + chunk.isolated_hash = c.generateIsolatedHash(chunk); + } + // This runs after we've already populated the compile results fn postProcessCSSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chunk: *Chunk) !void { const c = ctx.c; @@ -9100,10 +9899,11 @@ pub const LinkerContext = struct { var runtime_members = &runtime_scope.members; const toCommonJSRef = c.graph.symbols.follow(runtime_members.get("__toCommonJS").?.ref); const toESMRef = c.graph.symbols.follow(runtime_members.get("__toESM").?.ref); - const runtimeRequireRef = if (c.resolver.opts.target.isBun() or c.options.output_format == .cjs) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref); + const runtimeRequireRef = if (c.options.output_format == .cjs) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref); { const print_options = js_printer.Options{ + .bundling = true, .indent = .{}, .has_run_symbol_renamer = true, @@ -9138,7 +9938,7 @@ pub const LinkerContext = struct { c.source_(chunk.entry_point.source_index), print_options, cross_chunk_import_records.slice(), - &[_]js_ast.Part{ + &[_]Part{ .{ .stmts = chunk.content.javascript.cross_chunk_prefix_stmts.slice() }, }, chunk.renamer, @@ -9151,7 +9951,7 @@ pub const LinkerContext = struct { c.source_(chunk.entry_point.source_index), print_options, &.{}, - &[_]js_ast.Part{ + &[_]Part{ .{ .stmts = chunk.content.javascript.cross_chunk_suffix_stmts.slice() }, }, chunk.renamer, @@ -9260,7 +10060,7 @@ pub const LinkerContext = struct { switch (c.options.output_format) { .internal_bake_dev => { - const start = bun.bake.getHmrRuntime(if (c.options.target.isBun()) .server else .client); + const start = bun.bake.getHmrRuntime(if (c.options.target.isServerSide()) .server else .client); j.pushStatic(start); line_offset.advance(start); }, @@ -10261,7 +11061,7 @@ pub const LinkerContext = struct { c.source_(source_index), print_options, ast.import_records.slice(), - &[_]js_ast.Part{ + &[_]Part{ .{ .stmts = stmts.items, }, @@ -11046,7 +11846,7 @@ pub const LinkerContext = struct { // hmr-runtime.ts defines `module.importSync` to be // a synchronous import. this is different from // require in that esm <-> cjs is handled - // automatically, instead of with bundler-added + // automatically, instead of with transpiler-added // annotations like '__commonJS'. // // this cannot be done in the parse step because the final @@ -11078,7 +11878,7 @@ pub const LinkerContext = struct { const items = try allocator.alloc(Expr, st.items.len); for (st.items, items) |item, *str| { - str.* = Expr.init(E.String, .{ .data = item.original_name }, item.name.loc); + str.* = Expr.init(E.String, .{ .data = item.alias }, item.name.loc); } break :call Expr.init(E.Call, .{ @@ -11156,7 +11956,7 @@ pub const LinkerContext = struct { allocator: std.mem.Allocator, temp_allocator: std.mem.Allocator, ) js_printer.PrintResult { - const parts: []js_ast.Part = c.graph.ast.items(.parts)[part_range.source_index.get()].slice()[part_range.part_index_begin..part_range.part_index_end]; + const parts: []Part = c.graph.ast.items(.parts)[part_range.source_index.get()].slice()[part_range.part_index_begin..part_range.part_index_end]; const all_flags: []const JSMeta.Flags = c.graph.meta.items(.flags); const flags = all_flags[part_range.source_index.get()]; const wrapper_part_index = if (flags.wrap != .none) @@ -11753,11 +12553,12 @@ pub const LinkerContext = struct { runtime_require_ref: ?Ref, source_index: Index, ) js_printer.PrintResult { - const parts_to_print = &[_]js_ast.Part{ - js_ast.Part{ .stmts = out_stmts }, + const parts_to_print = &[_]Part{ + Part{ .stmts = out_stmts }, }; const print_options = js_printer.Options{ + .bundling = true, // TODO: IIFE .indent = .{}, .commonjs_named_exports = ast.commonjs_named_exports, @@ -11856,7 +12657,7 @@ pub const LinkerContext = struct { var has_js_chunk = false; var has_css_chunk = false; - + var has_html_chunk = false; bun.assert(chunks.len > 0); { @@ -11882,7 +12683,7 @@ pub const LinkerContext = struct { c.source_maps.line_offset_tasks.len = 0; } - if (c.options.experimental_css) { + if (c.options.experimental.css) { // Per CSS chunk: // Remove duplicate rules across files. This must be done in serial, not // in parallel, and must be done from the last rule to the first rule. @@ -11938,6 +12739,7 @@ pub const LinkerContext = struct { defer c.allocator.free(chunk_contexts); var wait_group = try c.allocator.create(sync.WaitGroup); wait_group.init(); + defer { wait_group.deinit(); c.allocator.destroy(wait_group); @@ -11959,6 +12761,13 @@ pub const LinkerContext = struct { total_count += chunk.content.css.imports_in_chunk_in_order.len; chunk.compile_results_for_chunk = c.allocator.alloc(CompileResult, chunk.content.css.imports_in_chunk_in_order.len) catch bun.outOfMemory(); }, + .html => { + has_html_chunk = true; + // HTML gets only one chunk. + chunk_ctx.* = .{ .wg = wait_group, .c = c, .chunks = chunks, .chunk = chunk }; + total_count += 1; + chunk.compile_results_for_chunk = c.allocator.alloc(CompileResult, 1) catch bun.outOfMemory(); + }, } } @@ -11998,8 +12807,7 @@ pub const LinkerContext = struct { } }, .css => { - for (chunk.content.css.imports_in_chunk_in_order.slice(), 0..) |css_import, i| { - _ = css_import; // autofix + for (0..chunk.content.css.imports_in_chunk_in_order.len) |i| { remaining_part_ranges[0] = .{ .part_range = .{}, .i = @as(u32, @truncate(i)), @@ -12013,6 +12821,19 @@ pub const LinkerContext = struct { remaining_part_ranges = remaining_part_ranges[1..]; } }, + .html => { + remaining_part_ranges[0] = .{ + .part_range = .{}, + .i = 0, + .task = ThreadPoolLib.Task{ + .callback = &generateCompileResultForHtmlChunk, + }, + .ctx = chunk_ctx, + }; + + batch.push(ThreadPoolLib.Batch.from(&remaining_part_ranges[0].task)); + remaining_part_ranges = remaining_part_ranges[1..]; + }, } } wait_group.counter = @as(u32, @truncate(total_count)); @@ -12054,7 +12875,7 @@ pub const LinkerContext = struct { // - Reuse unchanged parts to assemble the full bundle if Cmd+R is used in the browser // - Send only the newly changed code through a socket. // - // When this isnt the initial bundle, concatenation as usual would produce a + // When this isn't the initial bundle, concatenation as usual would produce a // broken module. It is DevServer's job to create and send HMR patches. if (is_dev_server) return; @@ -12164,7 +12985,7 @@ pub const LinkerContext = struct { ) catch unreachable; const root_path = c.resolver.opts.output_dir; - const more_than_one_output = c.parse_graph.additional_output_files.items.len > 0 or c.options.generate_bytecode_cache or (has_css_chunk and has_js_chunk); + const more_than_one_output = c.parse_graph.additional_output_files.items.len > 0 or c.options.generate_bytecode_cache or (has_css_chunk and has_js_chunk) or (has_html_chunk and (has_js_chunk or has_css_chunk)); if (!c.resolver.opts.compile and more_than_one_output and !c.resolver.opts.supports_multiple_outputs) { try c.log.addError(null, Logger.Loc.Empty, "cannot write multiple output files without an output directory"); @@ -12186,7 +13007,7 @@ pub const LinkerContext = struct { chunk, chunks, &display_size, - c.options.source_maps != .none, + chunk.content.sourcemap(c.options.source_maps) != .none, ); var code_result = _code_result catch @panic("Failed to allocate memory for output file"); @@ -12199,7 +13020,7 @@ pub const LinkerContext = struct { chunk.final_rel_path, ); - switch (c.options.source_maps) { + switch (chunk.content.sourcemap(c.options.source_maps)) { .external, .linked => |tag| { const output_source_map = chunk.output_source_map.finalize(bun.default_allocator, code_result.shifts) catch @panic("Failed to allocate memory for external source map"); var source_map_final_rel_path = default_allocator.alloc(u8, chunk.final_rel_path.len + ".map".len) catch unreachable; @@ -12352,7 +13173,9 @@ pub const LinkerContext = struct { .is_executable = chunk.is_executable, .source_map_index = source_map_index, .bytecode_index = bytecode_index, - .side = switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { + .side = if (chunk.content == .css) + .client + else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { .browser => .client, else => .server, }, @@ -12363,6 +13186,7 @@ pub const LinkerContext = struct { .referenced_css_files = switch (chunk.content) { .javascript => |js| @ptrCast(try bun.default_allocator.dupe(u32, js.css_chunks)), .css => &.{}, + .html => &.{}, }, })); if (sourcemap_output_file) |sourcemap_file| { @@ -12502,7 +13326,7 @@ pub const LinkerContext = struct { chunk, chunks, &display_size, - c.options.source_maps != .none, + chunk.content.sourcemap(c.options.source_maps) != .none, ) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)}); var source_map_output_file: ?options.OutputFile = null; @@ -12515,7 +13339,7 @@ pub const LinkerContext = struct { chunk.final_rel_path, ); - switch (c.options.source_maps) { + switch (chunk.content.sourcemap(c.options.source_maps)) { .external, .linked => |tag| { const output_source_map = chunk.output_source_map.finalize(source_map_allocator, code_result.shifts) catch @panic("Failed to allocate memory for external source map"); const source_map_final_rel_path = strings.concat(default_allocator, &.{ @@ -12563,12 +13387,9 @@ pub const LinkerContext = struct { }, )) { .err => |err| { - var message = err.toSystemError().message.toUTF8(bun.default_allocator); - defer message.deinit(); - c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing sourcemap for chunk {}", .{ - bun.fmt.quote(message.slice()), + try c.log.addSysError(bun.default_allocator, err, "writing sourcemap for chunk {}", .{ bun.fmt.quote(chunk.final_rel_path), - }) catch unreachable; + }); return error.WriteFailed; }, .result => {}, @@ -12714,12 +13535,9 @@ pub const LinkerContext = struct { }, )) { .err => |err| { - var message = err.toSystemError().message.toUTF8(bun.default_allocator); - defer message.deinit(); - c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing chunk {}", .{ - bun.fmt.quote(message.slice()), + try c.log.addSysError(bun.default_allocator, err, "writing chunk {}", .{ bun.fmt.quote(chunk.final_rel_path), - }) catch unreachable; + }); return error.WriteFailed; }, .result => {}, @@ -12761,7 +13579,9 @@ pub const LinkerContext = struct { .data = .{ .saved = 0, }, - .side = switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { + .side = if (chunk.content == .css) + .client + else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { .browser => .client, else => .server, }, @@ -12772,6 +13592,7 @@ pub const LinkerContext = struct { .referenced_css_files = switch (chunk.content) { .javascript => |js| @ptrCast(try bun.default_allocator.dupe(u32, js.css_chunks)), .css => &.{}, + .html => &.{}, }, })); @@ -12816,7 +13637,6 @@ pub const LinkerContext = struct { .buffer = JSC.Buffer{ .buffer = .{ .ptr = @constCast(bytes.ptr), - // TODO: handle > 4 GB files .len = @as(u32, @truncate(bytes.len)), .byte_len = @as(u32, @truncate(bytes.len)), }, @@ -12832,10 +13652,7 @@ pub const LinkerContext = struct { }, )) { .err => |err| { - const utf8 = err.toSystemError().message.toUTF8(bun.default_allocator); - defer utf8.deinit(); - c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing file {}", .{ - bun.fmt.quote(utf8.slice()), + c.log.addSysError(bun.default_allocator, err, "writing file {}", .{ bun.fmt.quote(src.src_path.text), }) catch unreachable; return error.WriteFailed; @@ -12883,7 +13700,7 @@ pub const LinkerContext = struct { entry_points_count: usize, distances: []u32, distance: u32, - parts: []bun.BabyList(js_ast.Part), + parts: []bun.BabyList(Part), import_records: []bun.BabyList(bun.ImportRecord), file_entry_bits: []AutoBitSet, css_reprs: []?*bun.css.BundlerStyleSheet, @@ -12973,7 +13790,7 @@ pub const LinkerContext = struct { c: *LinkerContext, source_index: Index.Int, side_effects: []_resolver.SideEffects, - parts: []bun.BabyList(js_ast.Part), + parts: []bun.BabyList(Part), import_records: []bun.BabyList(bun.ImportRecord), entry_point_kinds: []EntryPoint.Kind, css_reprs: []?*bun.css.BundlerStyleSheet, @@ -13089,12 +13906,12 @@ pub const LinkerContext = struct { part_index: Index.Int, source_index: Index.Int, side_effects: []_resolver.SideEffects, - parts: []bun.BabyList(js_ast.Part), + parts: []bun.BabyList(Part), import_records: []bun.BabyList(bun.ImportRecord), entry_point_kinds: []EntryPoint.Kind, css_reprs: []?*bun.css.BundlerStyleSheet, ) void { - const part: *js_ast.Part = &parts[source_index].slice()[part_index]; + const part: *Part = &parts[source_index].slice()[part_index]; // only once if (part.is_live) { @@ -13303,7 +14120,7 @@ pub const LinkerContext = struct { next_source.path.pretty, named_import.alias.?, }, - "Bun's bundler defaults to browser builds instead of node or bun builds. If you want to use node or bun builds, you can set the target to \"node\" or \"bun\" in the bundler options.", + "Bun's bundler defaults to browser builds instead of node or bun builds. If you want to use node or bun builds, you can set the target to \"node\" or \"bun\" in the transpiler options.", .{}, r, ) catch unreachable; @@ -13319,7 +14136,7 @@ pub const LinkerContext = struct { }, ) catch unreachable; } - } else if (c.resolver.opts.target == .browser and JSC.HardcodedModule.Aliases.has(next_source.path.pretty, .browser)) { + } else if (c.resolver.opts.target == .browser and bun.strings.hasPrefixComptime(next_source.path.text, NodeFallbackModules.import_path)) { c.log.addRangeErrorFmtWithNote( source, r, @@ -13329,7 +14146,7 @@ pub const LinkerContext = struct { next_source.path.pretty, named_import.alias.?, }, - "Bun's bundler defaults to browser builds instead of node or bun builds. If you want to use node or bun builds, you can set the target to \"node\" or \"bun\" in the bundler options.", + "Bun's bundler defaults to browser builds instead of node or bun builds. If you want to use node or bun builds, you can set the target to \"node\" or \"bun\" in the transpiler options.", .{}, r, ) catch unreachable; @@ -13469,7 +14286,7 @@ pub const LinkerContext = struct { for (common_js_parts) |part_id| { const runtime_parts = c.graph.ast.items(.parts)[Index.runtime.get()].slice(); - const part: *js_ast.Part = &runtime_parts[part_id]; + const part: *Part = &runtime_parts[part_id]; const symbol_refs = part.symbol_uses.keys(); for (symbol_refs) |ref| { if (ref.eql(c.cjs_runtime_ref)) continue; @@ -13492,7 +14309,7 @@ pub const LinkerContext = struct { .{ .stmts = &.{}, .symbol_uses = bun.from( - js_ast.Part.SymbolUseMap, + Part.SymbolUseMap, c.allocator, .{ .{ wrapper_ref, .{ .count_estimate = 1 } }, @@ -13553,7 +14370,7 @@ pub const LinkerContext = struct { source_index, .{ .symbol_uses = bun.from( - js_ast.Part.SymbolUseMap, + Part.SymbolUseMap, c.allocator, .{ .{ wrapper_ref, .{ .count_estimate = 1 } }, @@ -14185,6 +15002,7 @@ pub const Chunk = struct { entry_point: Chunk.EntryPoint = .{}, is_executable: bool = false, + has_html_chunk: bool = false, output_source_map: sourcemap.SourceMapPieces, @@ -14199,6 +15017,30 @@ pub const Chunk = struct { return this.entry_point.is_entry_point; } + pub fn getJSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk { + const entry_point_id = this.entry_point.entry_point_id; + for (chunks) |*other| { + if (other.content == .javascript) { + if (other.entry_point.entry_point_id == entry_point_id) { + return other; + } + } + } + return null; + } + + pub fn getCSSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk { + const entry_point_id = this.entry_point.entry_point_id; + for (chunks) |*other| { + if (other.content == .css) { + if (other.entry_point.entry_point_id == entry_point_id) { + return other; + } + } + } + return null; + } + pub inline fn entryBits(this: *const Chunk) *const AutoBitSet { return &this.entry_bits; } @@ -14550,11 +15392,12 @@ pub const Chunk = struct { pub const EntryPoint = packed struct(u64) { /// Index into `Graph.input_files` source_index: u32 = 0, - entry_point_id: u31 = 0, + entry_point_id: ID = 0, is_entry_point: bool = false, + is_html: bool = false, /// so `EntryPoint` can be a u64 - pub const ID = u31; + pub const ID = u30; }; pub const JavaScriptChunk = struct { @@ -14648,16 +15491,32 @@ pub const Chunk = struct { pub const ContentKind = enum { javascript, css, + html, }; + pub const HtmlChunk = struct {}; + pub const Content = union(ContentKind) { javascript: JavaScriptChunk, css: CssChunk, + html: HtmlChunk, + + pub fn sourcemap(this: *const Content, default: options.SourceMapOption) options.SourceMapOption { + return switch (this.*) { + .javascript => default, + // TODO: + .css => options.SourceMapOption.none, + + // probably never + .html => options.SourceMapOption.none, + }; + } pub fn loader(this: *const Content) Loader { return switch (this.*) { .javascript => .js, .css => .css, + .html => .html, }; } @@ -14665,6 +15524,7 @@ pub const Chunk = struct { return switch (this.*) { .javascript => "js", .css => "css", + .html => "html", }; } }; @@ -14744,6 +15604,10 @@ pub const CompileResult = union(enum) { // TODO: we need to do this source_map: ?bun.sourcemap.Chunk = null, }, + html: struct { + source_index: Index.Int, + code: []const u8, + }, pub const empty = CompileResult{ .javascript = .{ @@ -14762,7 +15626,7 @@ pub const CompileResult = union(enum) { .result => |r2| r2.code, else => "", }, - .css => |*c| c.code, + inline .html, .css => |*c| c.code, }; } @@ -14773,14 +15637,13 @@ pub const CompileResult = union(enum) { else => null, }, .css => |*c| c.source_map, + .html => null, }; } pub fn sourceIndex(this: *const CompileResult) Index.Int { return switch (this.*) { - .javascript => |r| r.source_index, - .css => |*c| c.source_index, - // else => 0, + inline else => |*r| r.source_index, }; } }; @@ -15073,7 +15936,7 @@ pub const AstBuilder = struct { bun.assert(p.scopes.items.len == 0); const module_scope = p.current_scope; - var parts = try js_ast.Part.List.initCapacity(p.allocator, 2); + var parts = try Part.List.initCapacity(p.allocator, 2); parts.len = 2; parts.mut(0).* = .{}; parts.mut(1).* = .{ @@ -15082,7 +15945,7 @@ pub const AstBuilder = struct { // pretend that every symbol was used .symbol_uses = uses: { - var map: js_ast.Part.SymbolUseMap = .{}; + var map: Part.SymbolUseMap = .{}; try map.ensureTotalCapacity(p.allocator, p.symbols.items.len); for (0..p.symbols.items.len) |i| { map.putAssumeCapacity(Ref{ @@ -15126,7 +15989,8 @@ pub const AstBuilder = struct { _ = try js_parser.ImportScanner.scan(AstBuilder, p, p.stmts.items, false, true, &hmr_transform_ctx); - const new_parts = try hmr_transform_ctx.finalize(p, parts.slice()); + try hmr_transform_ctx.finalize(p, parts.slice()); + const new_parts = parts.slice(); // preserve original capacity parts.len = @intCast(new_parts.len); bun.assert(new_parts.ptr == parts.ptr); @@ -15221,12 +16085,12 @@ pub const CssEntryPointMeta = struct { imported_on_server: bool, }; -/// The lifetime of this structure is tied to the bundler's arena +/// The lifetime of this structure is tied to the transpiler's arena pub const BakeBundleStart = struct { css_entry_points: std.AutoArrayHashMapUnmanaged(Index, CssEntryPointMeta), }; -/// The lifetime of this structure is tied to the bundler's arena +/// The lifetime of this structure is tied to the transpiler's arena pub const BakeBundleOutput = struct { chunks: []Chunk, css_file_list: std.AutoArrayHashMapUnmanaged(Index, CssEntryPointMeta), @@ -15256,3 +16120,38 @@ pub fn generateUniqueKey() u64 { } return key; } + +const ExternalFreeFunctionAllocator = struct { + free_callback: *const fn (ctx: *anyopaque) void, + context: *anyopaque, + + const vtable: std.mem.Allocator.VTable = .{ + .alloc = &alloc, + .free = &free, + .resize = &resize, + }; + + pub fn create(free_callback: *const fn (ctx: *anyopaque) void, context: *anyopaque) std.mem.Allocator { + return .{ + .ptr = bun.create(bun.default_allocator, ExternalFreeFunctionAllocator, .{ + .free_callback = free_callback, + .context = context, + }), + .vtable = &vtable, + }; + } + + fn alloc(_: *anyopaque, _: usize, _: u8, _: usize) ?[*]u8 { + return null; + } + + fn resize(_: *anyopaque, _: []u8, _: u8, _: usize, _: usize) bool { + return false; + } + + fn free(ext_free_function: *anyopaque, _: []u8, _: u8, _: usize) void { + const info: *ExternalFreeFunctionAllocator = @alignCast(@ptrCast(ext_free_function)); + info.free_callback(info.context); + bun.default_allocator.destroy(info); + } +}; diff --git a/src/bundler/entry_points.zig b/src/bundler/entry_points.zig index 306116742f..2636cc7960 100644 --- a/src/bundler/entry_points.zig +++ b/src/bundler/entry_points.zig @@ -4,7 +4,7 @@ const bun = @import("root").bun; const string = bun.string; const Fs = @import("../fs.zig"); const js_ast = bun.JSAst; -const Bundler = bun.Bundler; +const Transpiler = bun.Transpiler; const strings = bun.strings; pub const FallbackEntryPoint = struct { @@ -16,8 +16,8 @@ pub const FallbackEntryPoint = struct { pub fn generate( entry: *FallbackEntryPoint, input_path: string, - comptime BundlerType: type, - bundler: *BundlerType, + comptime TranspilerType: type, + transpiler: *TranspilerType, ) !void { // This is *extremely* naive. // The basic idea here is this: @@ -29,7 +29,7 @@ pub const FallbackEntryPoint = struct { // We go through the steps of printing the code -- only to then parse/transpile it because // we want it to go through the linker and the rest of the transpilation process - const disable_css_imports = bundler.options.framework.?.client_css_in_js != .auto_onimportcss; + const disable_css_imports = transpiler.options.framework.?.client_css_in_js != .auto_onimportcss; var code: string = undefined; @@ -48,7 +48,7 @@ pub const FallbackEntryPoint = struct { if (count < entry.code_buffer.len) { code = try std.fmt.bufPrint(&entry.code_buffer, fmt, args); } else { - code = try std.fmt.allocPrint(bundler.allocator, fmt, args); + code = try std.fmt.allocPrint(transpiler.allocator, fmt, args); } } else { const fmt = @@ -64,7 +64,7 @@ pub const FallbackEntryPoint = struct { if (count < entry.code_buffer.len) { code = try std.fmt.bufPrint(&entry.code_buffer, fmt, args); } else { - code = try std.fmt.allocPrint(bundler.allocator, fmt, args); + code = try std.fmt.allocPrint(transpiler.allocator, fmt, args); } } @@ -105,7 +105,7 @@ pub const ClientEntryPoint = struct { return outbuffer[0 .. generated_path.len + original_ext.len]; } - pub fn generate(entry: *ClientEntryPoint, comptime BundlerType: type, bundler: *BundlerType, original_path: Fs.PathName, client: string) !void { + pub fn generate(entry: *ClientEntryPoint, comptime TranspilerType: type, transpiler: *TranspilerType, original_path: Fs.PathName, client: string) !void { // This is *extremely* naive. // The basic idea here is this: @@ -118,7 +118,7 @@ pub const ClientEntryPoint = struct { // we want it to go through the linker and the rest of the transpilation process const dir_to_use: string = original_path.dirWithTrailingSlash(); - const disable_css_imports = bundler.options.framework.?.client_css_in_js != .auto_onimportcss; + const disable_css_imports = transpiler.options.framework.?.client_css_in_js != .auto_onimportcss; var code: string = undefined; @@ -266,7 +266,7 @@ pub const MacroEntryPoint = struct { pub fn generate( entry: *MacroEntryPoint, - _: *Bundler, + _: *Transpiler, import_path: Fs.PathName, function_name: string, macro_id: i32, diff --git a/src/bunfig.zig b/src/bunfig.zig index f5e41c434d..8ca56ec0ee 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -469,6 +469,12 @@ pub const Bunfig = struct { } } + if (install_obj.get("saveTextLockfile")) |save_text_lockfile| { + if (save_text_lockfile.asBool()) |value| { + install.save_text_lockfile = value; + } + } + if (install_obj.get("concurrentScripts")) |jobs| { if (jobs.data == .e_number) { install.concurrent_scripts = jobs.data.e_number.toU32(); @@ -591,6 +597,14 @@ pub const Bunfig = struct { } } + if (run_expr.get("elide-lines")) |elide_lines| { + if (elide_lines.data == .e_number) { + this.ctx.bundler_options.elide_lines = @intFromFloat(elide_lines.data.e_number.value); + } else { + try this.addError(elide_lines.loc, "Expected number"); + } + } + if (run_expr.get("shell")) |shell| { if (shell.asString(allocator)) |value| { if (strings.eqlComptime(value, "bun")) { diff --git a/src/c-headers-for-zig.h b/src/c-headers-for-zig.h new file mode 100644 index 0000000000..42c03aca64 --- /dev/null +++ b/src/c-headers-for-zig.h @@ -0,0 +1,21 @@ +// This file is run through translate-c and exposed to Zig code +// under the namespace bun.C.translated. Prefer adding includes +// to this file instead of manually porting struct definitions +// into Zig code. By using automatic translation, differences +// in platforms can be avoided. +// +// When Zig is translating this file, it will define these macros: +// - WINDOWS +// - DARWIN +// - LINUX +// - POSIX + +// OnBeforeParseResult, etc... +#include "../packages/bun-native-bundler-plugin-api/bundler_plugin.h" + +#if POSIX +// passwd, getpwuid_r +#include "pwd.h" +// geteuid +#include +#endif diff --git a/src/c.zig b/src/c.zig index 1579667ab9..1541b9872e 100644 --- a/src/c.zig +++ b/src/c.zig @@ -2,6 +2,8 @@ const std = @import("std"); const bun = @import("root").bun; const Environment = @import("./env.zig"); +pub const translated = @import("translated-c-headers"); + const PlatformSpecific = switch (Environment.os) { .mac => @import("./darwin_c.zig"), .linux => @import("./linux_c.zig"), @@ -42,29 +44,13 @@ pub extern "c" fn memchr(s: [*]const u8, c: u8, n: usize) ?[*]const u8; pub extern "c" fn strchr(str: [*]const u8, char: u8) ?[*]const u8; -pub const lstat = blk: { - const T = *const fn ([*c]const u8, [*c]libc_stat) callconv(.C) c_int; // TODO: this is wrong on Windows - if (bun.Environment.isMusl) break :blk @extern(T, .{ .library_name = "c", .name = "lstat" }); - break :blk @extern(T, .{ .name = "lstat64" }); -}; -pub const fstat = blk: { - const T = *const fn (c_int, [*c]libc_stat) callconv(.C) c_int; // TODO: this is wrong on Windows - if (bun.Environment.isMusl) break :blk @extern(T, .{ .library_name = "c", .name = "fstat" }); - break :blk @extern(T, .{ .name = "fstat64" }); -}; -pub const stat = blk: { - const T = *const fn ([*c]const u8, [*c]libc_stat) callconv(.C) c_int; // TODO: this is wrong on Windows - if (bun.Environment.isMusl) break :blk @extern(T, .{ .library_name = "c", .name = "stat" }); - break :blk @extern(T, .{ .name = "stat64" }); -}; - pub fn lstat_absolute(path: [:0]const u8) !Stat { if (builtin.os.tag == .windows) { - @compileError("Not implemented yet, conside using bun.sys.lstat()"); + @compileError("Not implemented yet, consider using bun.sys.lstat()"); } var st = zeroes(libc_stat); - switch (errno(lstat(path.ptr, &st))) { + switch (errno(bun.C.lstat(path.ptr, &st))) { .SUCCESS => {}, .NOENT => return error.FileNotFound, // .EINVAL => unreachable, @@ -357,8 +343,8 @@ pub fn getSelfExeSharedLibPaths(allocator: std.mem.Allocator) error{OutOfMemory} /// Same as MADV_DONTNEED but used with posix_madvise() sys-tem system /// tem call. /// -/// MADV_FREE Indicates that the application will not need the infor-mation information -/// mation contained in this address range, so the pages may +/// MADV_FREE Indicates that the application will not need the information +/// contained in this address range, so the pages may /// be reused right away. The address range will remain /// valid. This is used with madvise() system call. /// @@ -366,16 +352,8 @@ pub fn getSelfExeSharedLibPaths(allocator: std.mem.Allocator) error{OutOfMemory} /// with POSIX_ prefix for the advice system call argument. pub extern "c" fn posix_madvise(ptr: *anyopaque, len: usize, advice: i32) c_int; -pub fn getProcessPriority(pid_: i32) i32 { - const pid = @as(c_uint, @intCast(pid_)); - return get_process_priority(pid); -} - -pub fn setProcessPriority(pid_: i32, priority_: i32) std.c.E { - if (pid_ < 0) return .SRCH; - - const pid = @as(c_uint, @intCast(pid_)); - const priority = @as(c_int, @intCast(priority_)); +pub fn setProcessPriority(pid: i32, priority: i32) std.c.E { + if (pid < 0) return .SRCH; const code: i32 = set_process_priority(pid, priority); @@ -420,7 +398,6 @@ pub fn getRelease(buf: []u8) []const u8 { } } -pub extern fn memmem(haystack: [*]const u8, haystacklen: usize, needle: [*]const u8, needlelen: usize) ?[*]const u8; pub extern fn cfmakeraw(*std.posix.termios) void; const LazyStatus = enum { @@ -484,9 +461,18 @@ pub fn dlsym(comptime Type: type, comptime name: [:0]const u8) ?Type { return dlsymWithHandle(Type, name, handle_getter); } +/// Error condition is encoded as null +/// The only error in this function is ESRCH (no process found) +pub fn getProcessPriority(pid: i32) ?i32 { + return switch (get_process_priority(pid)) { + std.math.maxInt(i32) => null, + else => |prio| prio, + }; +} + // set in c-bindings.cpp -pub extern fn get_process_priority(pid: c_uint) i32; -pub extern fn set_process_priority(pid: c_uint, priority: c_int) i32; +extern fn get_process_priority(pid: i32) i32; +pub extern fn set_process_priority(pid: i32, priority: i32) i32; pub extern fn strncasecmp(s1: [*]const u8, s2: [*]const u8, n: usize) i32; pub extern fn memmove(dest: [*]u8, src: [*]const u8, n: usize) void; @@ -509,3 +495,11 @@ pub extern "C" fn bun_restore_stdio() void; pub extern "C" fn open_as_nonblocking_tty(i32, i32) i32; pub extern fn strlen(ptr: [*c]const u8) usize; + +pub const passwd = translated.passwd; +pub const geteuid = translated.geteuid; +pub const getpwuid_r = translated.getpwuid_r; + +export fn Bun__errnoName(err: c_int) ?[*:0]const u8 { + return @tagName(bun.C.SystemErrno.init(err) orelse return null); +} diff --git a/src/cache.zig b/src/cache.zig index 4ea8616cac..96ecf3484c 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -20,7 +20,6 @@ const Define = @import("./defines.zig").Define; const std = @import("std"); const fs = @import("./fs.zig"); const sync = @import("sync.zig"); -const Mutex = @import("./lock.zig").Lock; const import_record = @import("./import_record.zig"); @@ -47,9 +46,23 @@ pub const Fs = struct { pub const Entry = struct { contents: string, fd: StoredFileDescriptorType = bun.invalid_fd, + external: External = .{}, + + pub const External = struct { + ctx: ?*anyopaque = null, + function: ?*const fn (?*anyopaque) callconv(.C) void = null, + + pub fn call(this: *const @This()) void { + if (this.function) |func| { + func(this.ctx); + } + } + }; pub fn deinit(entry: *Entry, allocator: std.mem.Allocator) void { - if (entry.contents.len > 0) { + if (entry.external.function) |func| { + func(entry.external.ctx); + } else if (entry.contents.len > 0) { allocator.free(entry.contents); entry.contents = ""; } diff --git a/src/cli.zig b/src/cli.zig index 02a8abf23d..0f77727b7b 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -21,6 +21,7 @@ const js_ast = bun.JSAst; const linker = @import("linker.zig"); const RegularExpression = bun.RegularExpression; const builtin = @import("builtin"); +const File = bun.sys.File; const debug = Output.scoped(.CLI, true); @@ -31,7 +32,7 @@ const configureTransformOptionsForBun = @import("./bun.js/config.zig").configure const clap = bun.clap; const BunJS = @import("./bun_js.zig"); const Install = @import("./install/install.zig"); -const bundler = bun.bundler; +const transpiler = bun.transpiler; const DotEnv = @import("./env_loader.zig"); const RunCommand_ = @import("./cli/run_command.zig").RunCommand; const CreateCommand_ = @import("./cli/create_command.zig").CreateCommand; @@ -46,6 +47,11 @@ pub var start_time: i128 = undefined; const Bunfig = @import("./bunfig.zig").Bunfig; const OOM = bun.OOM; +export var Bun__Node__ProcessNoDeprecation = false; +export var Bun__Node__ProcessThrowDeprecation = false; + +pub var Bun__Node__ProcessTitle: ?string = null; + pub const Cli = struct { pub const CompileTarget = @import("./compile_target.zig"); var wait_group: sync.WaitGroup = undefined; @@ -222,18 +228,24 @@ pub const Arguments = struct { clap.parseParam("--install Configure auto-install behavior. One of \"auto\" (default, auto-installs when no node_modules), \"fallback\" (missing packages only), \"force\" (always).") catch unreachable, clap.parseParam("-i Auto-install dependencies during execution. Equivalent to --install=fallback.") catch unreachable, clap.parseParam("-e, --eval Evaluate argument as a script") catch unreachable, - clap.parseParam("--print Evaluate argument as a script and print the result") catch unreachable, + clap.parseParam("-p, --print Evaluate argument as a script and print the result") catch unreachable, clap.parseParam("--prefer-offline Skip staleness checks for packages in the Bun runtime and resolve from disk") catch unreachable, clap.parseParam("--prefer-latest Use the latest matching versions of packages in the Bun runtime, always checking npm") catch unreachable, - clap.parseParam("-p, --port Set the default port for Bun.serve") catch unreachable, + clap.parseParam("--port Set the default port for Bun.serve") catch unreachable, clap.parseParam("-u, --origin ") catch unreachable, clap.parseParam("--conditions ... Pass custom conditions to resolve") catch unreachable, clap.parseParam("--fetch-preconnect ... Preconnect to a URL while code is loading") catch unreachable, clap.parseParam("--max-http-header-size Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable, + clap.parseParam("--dns-result-order Set the default order of DNS lookup results. Valid orders: verbatim (default), ipv4first, ipv6first") catch unreachable, + clap.parseParam("--expose-gc Expose gc() on the global object. Has no effect on Bun.gc().") catch unreachable, + clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable, + clap.parseParam("--throw-deprecation Determine whether or not deprecation warnings result in errors.") catch unreachable, + clap.parseParam("--title Set the process title") catch unreachable, + clap.parseParam("--experimental-html Bundle .html imports as JavaScript & CSS") catch unreachable, }; const auto_or_run_params = [_]ParamType{ - clap.parseParam("--filter ... Run a script in all workspace packages matching the pattern") catch unreachable, + clap.parseParam("-F, --filter ... Run a script in all workspace packages matching the pattern") catch unreachable, clap.parseParam("-b, --bun Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)") catch unreachable, clap.parseParam("--shell Control the shell used for package.json scripts. Supports either 'bun' or 'system'") catch unreachable, }; @@ -241,6 +253,7 @@ pub const Arguments = struct { const auto_only_params = [_]ParamType{ // clap.parseParam("--all") catch unreachable, clap.parseParam("--silent Don't print the script command") catch unreachable, + clap.parseParam("--elide-lines Number of lines of script output shown when using --filter (default: 10). Set to 0 to show all lines.") catch unreachable, clap.parseParam("-v, --version Print version and exit") catch unreachable, clap.parseParam("--revision Print version with revision and exit") catch unreachable, } ++ auto_or_run_params; @@ -248,6 +261,7 @@ pub const Arguments = struct { const run_only_params = [_]ParamType{ clap.parseParam("--silent Don't print the script command") catch unreachable, + clap.parseParam("--elide-lines Number of lines of script output shown when using --filter (default: 10). Set to 0 to show all lines.") catch unreachable, } ++ auto_or_run_params; pub const run_params = run_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; @@ -282,14 +296,19 @@ pub const Arguments = struct { clap.parseParam("--minify-syntax Minify syntax and inline data") catch unreachable, clap.parseParam("--minify-whitespace Minify whitespace") catch unreachable, clap.parseParam("--minify-identifiers Minify identifiers") catch unreachable, - clap.parseParam("--experimental-css Enabled experimental CSS bundling") catch unreachable, + clap.parseParam("--experimental-css Enable experimental CSS bundling") catch unreachable, clap.parseParam("--experimental-css-chunking Chunk CSS files together to reduce duplicated CSS loaded in a browser. Only has an affect when multiple entrypoints import CSS") catch unreachable, + clap.parseParam("--experimental-html Use .html files as entry points for JavaScript & CSS") catch unreachable, clap.parseParam("--dump-environment-variables") catch unreachable, clap.parseParam("--conditions ... Pass custom conditions to resolve") catch unreachable, clap.parseParam("--app (EXPERIMENTAL) Build a web app for production using Bun Bake.") catch unreachable, clap.parseParam("--server-components (EXPERIMENTAL) Enable server components") catch unreachable, + clap.parseParam("--env Inline environment variables into the bundle as process.env.${name}. Defaults to 'inline'. To inline environment variables matching a prefix, use my prefix like 'FOO_PUBLIC_*'. To disable, use 'disable'. In Bun v1.2+, the default is 'disable'.") catch unreachable, + clap.parseParam("--windows-hide-console When using --compile targeting Windows, prevent a Command prompt from opening alongside the executable") catch unreachable, + clap.parseParam("--windows-icon When using --compile targeting Windows, assign an executable icon") catch unreachable, } ++ if (FeatureFlags.bake_debugging_features) [_]ParamType{ clap.parseParam("--debug-dump-server-files When --app is set, dump all server files to disk even when building statically") catch unreachable, + clap.parseParam("--debug-no-minify When --app is set, do not minify anything") catch unreachable, } else .{}; pub const build_params = build_only_params ++ transpiler_params_ ++ base_params_; @@ -403,7 +422,7 @@ pub const Arguments = struct { var secondbuf: bun.PathBuffer = undefined; const cwd = bun.getcwd(&secondbuf) catch return; - ctx.args.absolute_working_dir = try allocator.dupe(u8, cwd); + ctx.args.absolute_working_dir = try allocator.dupeZ(u8, cwd); } var parts = [_]string{ ctx.args.absolute_working_dir.?, config_path_ }; @@ -478,16 +497,16 @@ pub const Arguments = struct { } } - var cwd: []u8 = undefined; + var cwd: [:0]u8 = undefined; if (args.option("--cwd")) |cwd_arg| { cwd = brk: { var outbuf: bun.PathBuffer = undefined; const out = bun.path.joinAbs(try bun.getcwd(&outbuf), .loose, cwd_arg); - bun.sys.chdir(out).unwrap() catch |err| { + bun.sys.chdir("", out).unwrap() catch |err| { Output.err(err, "Could not change directory to \"{s}\"\n", .{cwd_arg}); Global.exit(1); }; - break :brk try allocator.dupe(u8, out); + break :brk try allocator.dupeZ(u8, out); }; } else { cwd = try bun.getcwdAlloc(allocator); @@ -495,6 +514,15 @@ pub const Arguments = struct { if (cmd == .RunCommand or cmd == .AutoCommand) { ctx.filters = args.options("--filter"); + + if (args.option("--elide-lines")) |elide_lines| { + if (elide_lines.len > 0) { + ctx.bundler_options.elide_lines = std.fmt.parseInt(usize, elide_lines, 10) catch { + Output.prettyErrorln("error: Invalid elide-lines: \"{s}\"", .{elide_lines}); + Global.exit(1); + }; + } + } } if (cmd == .TestCommand) { @@ -736,6 +764,11 @@ pub const Arguments = struct { ctx.runtime_options.if_present = args.flag("--if-present"); ctx.runtime_options.smol = args.flag("--smol"); ctx.runtime_options.preconnect = args.options("--fetch-preconnect"); + ctx.runtime_options.expose_gc = args.flag("--expose-gc"); + + if (args.option("--dns-result-order")) |order| { + ctx.runtime_options.dns_result_order = order; + } if (args.option("--inspect")) |inspect_flag| { ctx.runtime_options.debugger = if (inspect_flag.len == 0) @@ -773,6 +806,16 @@ pub const Arguments = struct { bun.JSC.RuntimeTranspilerCache.is_disabled = true; } + + if (args.flag("--no-deprecation")) { + Bun__Node__ProcessNoDeprecation = true; + } + if (args.flag("--throw-deprecation")) { + Bun__Node__ProcessThrowDeprecation = true; + } + if (args.option("--title")) |title| { + Bun__Node__ProcessTitle = title; + } } if (opts.port != null and opts.origin == null) { @@ -797,6 +840,8 @@ pub const Arguments = struct { ctx.bundler_options.bake = true; ctx.bundler_options.bake_debug_dump_server = bun.FeatureFlags.bake_debugging_features and args.flag("--debug-dump-server-files"); + ctx.bundler_options.bake_debug_disable_minify = bun.FeatureFlags.bake_debugging_features and + args.flag("--debug-no-minify"); } // TODO: support --format=esm @@ -818,7 +863,9 @@ pub const Arguments = struct { } const experimental_css = args.flag("--experimental-css"); - ctx.bundler_options.experimental_css = experimental_css; + const experimental_html = args.flag("--experimental-html"); + ctx.bundler_options.experimental.css = experimental_css; + ctx.bundler_options.experimental.html = experimental_html; ctx.bundler_options.css_chunking = args.flag("--experimental-css-chunking"); const minify_flag = args.flag("--minify"); @@ -848,6 +895,24 @@ pub const Arguments = struct { } } + if (args.option("--env")) |env| { + if (strings.indexOfChar(env, '*')) |asterisk| { + if (asterisk == 0) { + ctx.bundler_options.env_behavior = .load_all; + } else { + ctx.bundler_options.env_behavior = .prefix; + ctx.bundler_options.env_prefix = env[0..asterisk]; + } + } else if (strings.eqlComptime(env, "inline") or strings.eqlComptime(env, "1")) { + ctx.bundler_options.env_behavior = .load_all; + } else if (strings.eqlComptime(env, "disable") or strings.eqlComptime(env, "0")) { + ctx.bundler_options.env_behavior = .load_all_without_inlining; + } else { + Output.prettyErrorln("error: Expected 'env' to be 'inline', 'disable', or a prefix with a '*' character", .{}); + Global.crash(); + } + } + const TargetMatcher = strings.ExactSizeMatcher(8); if (args.option("--target")) |_target| brk: { if (comptime cmd == .BuildCommand) { @@ -900,6 +965,31 @@ pub const Arguments = struct { ctx.bundler_options.inline_entrypoint_import_meta_main = true; } + if (args.flag("--windows-hide-console")) { + // --windows-hide-console technically doesnt depend on WinAPI, but since since --windows-icon + // does, all of these customization options have been gated to windows-only + if (!Environment.isWindows) { + Output.errGeneric("Using --windows-hide-console is only available when compiling on Windows", .{}); + Global.crash(); + } + if (!ctx.bundler_options.compile) { + Output.errGeneric("--windows-hide-console requires --compile", .{}); + Global.crash(); + } + ctx.bundler_options.windows_hide_console = true; + } + if (args.option("--windows-icon")) |path| { + if (!Environment.isWindows) { + Output.errGeneric("Using --windows-icon is only available when compiling on Windows", .{}); + Global.crash(); + } + if (!ctx.bundler_options.compile) { + Output.errGeneric("--windows-icon requires --compile", .{}); + Global.crash(); + } + ctx.bundler_options.windows_icon = path; + } + if (args.option("--outdir")) |outdir| { if (outdir.len > 0) { ctx.bundler_options.outdir = outdir; @@ -1051,6 +1141,15 @@ pub const Arguments = struct { ctx.debug.silent = true; } + if (args.option("--elide-lines")) |elide_lines| { + if (elide_lines.len > 0) { + ctx.bundler_options.elide_lines = std.fmt.parseInt(usize, elide_lines, 10) catch { + Output.prettyErrorln("error: Invalid elide-lines: \"{s}\"", .{elide_lines}); + Global.exit(1); + }; + } + } + if (opts.define) |define| { if (define.keys.len > 0) bun.JSC.RuntimeTranspilerCache.is_disabled = true; @@ -1065,6 +1164,7 @@ pub const Arguments = struct { } opts.resolve = Api.ResolveMode.lazy; + ctx.bundler_options.experimental.html = args.flag("--experimental-html"); if (jsx_factory != null or jsx_fragment != null or @@ -1307,7 +1407,10 @@ pub const HelpCommand = struct { pub const ReservedCommand = struct { pub fn exec(_: std.mem.Allocator) !void { @setCold(true); - const command_name = bun.argv[1]; + const command_name = for (bun.argv[1..]) |arg| { + if (arg.len > 1 and arg[0] == '-') continue; + break arg; + } else bun.argv[1]; Output.prettyError( \\Uh-oh. bun {s} is a subcommand reserved for future use by Bun. \\ @@ -1333,8 +1436,6 @@ pub var pretend_to_be_node = false; pub var is_bunx_exe = false; pub const Command = struct { - var script_name_buf: bun.PathBuffer = undefined; - pub fn get() Context { return global_cli_ctx; } @@ -1401,6 +1502,10 @@ pub const Command = struct { eval_and_print: bool = false, } = .{}, preconnect: []const []const u8 = &[_][]const u8{}, + dns_result_order: []const u8 = "verbatim", + /// `--expose-gc` makes `globalThis.gc()` available. Added for Node + /// compatibility. + expose_gc: bool = false, }; var global_cli_ctx: Context = undefined; @@ -1428,9 +1533,6 @@ pub const Command = struct { has_loaded_global_config: bool = false, pub const BundlerOptions = struct { - compile: bool = false, - compile_target: Cli.CompileTarget = .{}, - outdir: []const u8 = "", outfile: []const u8 = "", root_dir: []const u8 = "", @@ -1452,11 +1554,21 @@ pub const Command = struct { bytecode: bool = false, banner: []const u8 = "", footer: []const u8 = "", - experimental_css: bool = false, + experimental: options.Loader.Experimental = .{}, css_chunking: bool = false, bake: bool = false, bake_debug_dump_server: bool = false, + bake_debug_disable_minify: bool = false, + + env_behavior: Api.DotEnvBehavior = .disable, + env_prefix: []const u8 = "", + elide_lines: ?usize = null, + // Compile options + compile: bool = false, + compile_target: Cli.CompileTarget = .{}, + windows_hide_console: bool = false, + windows_icon: ?[]const u8 = null, }; pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context { @@ -1856,7 +1968,6 @@ pub const Command = struct { completions = try RunCommand.completions(ctx, null, &reject_list, .script_and_descriptions); } else if (strings.eqlComptime(filter[0], "a")) { const FirstLetter = AddCompletions.FirstLetter; - const index = AddCompletions.index; outer: { if (filter.len > 1 and filter[1].len > 0) { @@ -1889,7 +2000,8 @@ pub const Command = struct { 'z' => FirstLetter.z, else => break :outer, }; - const results = index.get(first_letter); + AddCompletions.init(bun.default_allocator) catch bun.outOfMemory(); + const results = AddCompletions.getPackages(first_letter); var prefilled_i: usize = 0; for (results) |cur| { @@ -2034,6 +2146,7 @@ pub const Command = struct { .RunCommand => { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .RunCommand) unreachable; const ctx = try Command.init(allocator, log, .RunCommand); + ctx.args.target = .bun; if (ctx.filters.len > 0) { FilterRun.runScriptsWithFilter(ctx) catch |err| { @@ -2043,7 +2156,7 @@ pub const Command = struct { } if (ctx.positionals.len > 0) { - if (try RunCommand.exec(ctx, false, true, false)) { + if (try RunCommand.exec(ctx, .{ .bin_dirs_only = false, .log_errors = true, .allow_fast_run_for_extensions = false })) { return; } @@ -2076,6 +2189,7 @@ pub const Command = struct { }, } }; + ctx.args.target = .bun; if (ctx.filters.len > 0) { FilterRun.runScriptsWithFilter(ctx) catch |err| { @@ -2102,7 +2216,15 @@ pub const Command = struct { if (strings.eqlComptime(extension, ".lockb")) { for (bun.argv) |arg| { if (strings.eqlComptime(arg, "--hash")) { - try PackageManagerCommand.printHash(ctx, ctx.args.entry_points[0]); + var path_buf: bun.PathBuffer = undefined; + @memcpy(path_buf[0..ctx.args.entry_points[0].len], ctx.args.entry_points[0]); + path_buf[ctx.args.entry_points[0].len] = 0; + const lockfile_path = path_buf[0..ctx.args.entry_points[0].len :0]; + const file = File.open(lockfile_path, bun.O.RDONLY, 0).unwrap() catch |err| { + Output.err(err, "failed to open lockfile", .{}); + Global.crash(); + }; + try PackageManagerCommand.printHash(ctx, file); return; } } @@ -2118,96 +2240,21 @@ pub const Command = struct { } } - var was_js_like = false; - // If we start bun with: - // 1. `bun foo.js`, assume it's a JavaScript file. - // 2. `bun /absolute/path/to/bin/foo` assume its a JavaScript file. - // ^ no file extension - // - // #!/usr/bin/env bun - // will pass us an absolute path to the script. - // This means a non-standard file extension will not work, but that is better than the current state - // which is file extension-less doesn't work - const default_loader = options.defaultLoaders.get(extension) orelse brk: { - if (extension.len == 0 and ctx.args.entry_points.len > 0 and ctx.args.entry_points[0].len > 0 and std.fs.path.isAbsolute(ctx.args.entry_points[0])) { - break :brk options.Loader.js; - } - - if (extension.len > 0) { - if (strings.endsWithComptime(ctx.args.entry_points[0], ".sh")) { - break :brk options.Loader.bunsh; - } - - if (!ctx.debug.loaded_bunfig) { - try bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand); - } - - if (ctx.preloads.len > 0) - break :brk options.Loader.js; - } - - break :brk null; - }; - - const force_using_bun = ctx.debug.run_in_bun; - var did_check = false; - if (default_loader) |loader| { - if (loader.canBeRunByBun()) { - was_js_like = true; - if (maybeOpenWithBunJS(ctx)) { - return; - } - did_check = true; - } - } - - if (force_using_bun and !did_check) { - if (maybeOpenWithBunJS(ctx)) { - return; - } - } - - if (ctx.positionals.len > 0 and extension.len == 0) { + if (ctx.positionals.len > 0) { if (ctx.filters.len > 0) { Output.prettyln("warn: Filters are ignored for auto command", .{}); } - if (try RunCommand.exec(ctx, true, false, true)) { + if (try RunCommand.exec(ctx, .{ .bin_dirs_only = true, .log_errors = !ctx.runtime_options.if_present, .allow_fast_run_for_extensions = true })) { return; } - - Output.prettyErrorln("error: Script not found \"{s}\"", .{ - ctx.positionals[0], - }); - - Global.exit(1); - } - - if (ctx.runtime_options.if_present) { return; } - if (was_js_like) { - Output.prettyErrorln("error: Module not found \"{s}\"", .{ - ctx.positionals[0], - }); - Global.exit(1); - } else if (ctx.positionals.len > 0) { - Output.prettyErrorln("error: File not found: \"{s}\"", .{ - ctx.positionals[0], - }); - Global.exit(1); - } - - // if we get here, the command was not parsed - // or the user just ran `bun` with no arguments - if (ctx.positionals.len > 0) { - Output.warn("failed to parse command\n", .{}); - } Output.flush(); try HelpCommand.exec(allocator); }, .ExecCommand => { - const ctx = try Command.init(allocator, log, .RunCommand); + const ctx = try Command.init(allocator, log, .ExecCommand); if (ctx.positionals.len > 1) { try ExecCommand.exec(ctx); @@ -2216,94 +2263,6 @@ pub const Command = struct { } } - fn maybeOpenWithBunJS(ctx: Command.Context) bool { - if (ctx.args.entry_points.len == 0) - return false; - - const script_name_to_search = ctx.args.entry_points[0]; - - var absolute_script_path: ?string = null; - - // TODO: optimize this pass for Windows. we can make better use of system apis available - var file_path = script_name_to_search; - { - const file = bun.toLibUVOwnedFD(((brk: { - if (std.fs.path.isAbsolute(script_name_to_search)) { - var win_resolver = resolve_path.PosixToWinNormalizer{}; - var resolved = win_resolver.resolveCWD(script_name_to_search) catch @panic("Could not resolve path"); - if (comptime Environment.isWindows) { - resolved = resolve_path.normalizeString(resolved, false, .windows); - } - break :brk bun.openFile( - resolved, - .{ .mode = .read_only }, - ); - } else if (!strings.hasPrefix(script_name_to_search, "..") and script_name_to_search[0] != '~') { - const file_pathZ = brk2: { - @memcpy(script_name_buf[0..file_path.len], file_path); - script_name_buf[file_path.len] = 0; - break :brk2 script_name_buf[0..file_path.len :0]; - }; - - break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); - } else { - var path_buf: bun.PathBuffer = undefined; - const cwd = bun.getcwd(&path_buf) catch return false; - path_buf[cwd.len] = std.fs.path.sep; - var parts = [_]string{script_name_to_search}; - file_path = resolve_path.joinAbsStringBuf( - path_buf[0 .. cwd.len + 1], - &script_name_buf, - &parts, - .auto, - ); - if (file_path.len == 0) return false; - script_name_buf[file_path.len] = 0; - const file_pathZ = script_name_buf[0..file_path.len :0]; - break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); - } - }) catch return false).handle) catch return false; - defer _ = bun.sys.close(file); - - switch (bun.sys.fstat(file)) { - .result => |stat| { - // directories cannot be run. if only there was a faster way to check this - if (bun.S.ISDIR(@intCast(stat.mode))) return false; - }, - .err => return false, - } - - Global.configureAllocator(.{ .long_running = true }); - - absolute_script_path = brk: { - if (comptime !Environment.isWindows) break :brk bun.getFdPath(file, &script_name_buf) catch return false; - - var fd_path_buf: bun.PathBuffer = undefined; - break :brk bun.getFdPath(file, &fd_path_buf) catch return false; - }; - } - - if (!ctx.debug.loaded_bunfig) { - bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand) catch {}; - } - - BunJS.Run.boot( - ctx, - absolute_script_path.?, - ) catch |err| { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - ctx.log.print(Output.errorWriter()) catch {}; - - Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ - std.fs.path.basename(file_path), - @errorName(err), - }); - Global.exit(1); - }; - return true; - } - pub const Tag = enum { AddCommand, AutoCommand, @@ -2382,6 +2341,11 @@ pub const Command = struct { pub fn printHelp(comptime cmd: Tag, show_all_flags: bool) void { switch (cmd) { + // the output of --help uses the following syntax highlighting + // template: Usage: bun [flags] [arguments] + // use [foo] for multiple arguments or flags for foo. + // use to emphasize 'bar' + // these commands do not use Context // .DiscordCommand => return try DiscordCommand.exec(allocator), // .HelpCommand => return try HelpCommand.exec(allocator), @@ -2406,7 +2370,7 @@ pub const Command = struct { .InitCommand => { const intro_text = - \\Usage: bun init [...flags] [\ ...] + \\Usage: bun init [flags] [\] \\ Initialize a Bun project in the current directory. \\ Creates a package.json, tsconfig.json, and bunfig.toml if they don't exist. \\ @@ -2416,7 +2380,7 @@ pub const Command = struct { \\ \\Examples: \\ bun init - \\ bun init --yes + \\ bun init --yes ; Output.pretty(intro_text ++ "\n", .{}); @@ -2425,16 +2389,16 @@ pub const Command = struct { Command.Tag.BunxCommand => { Output.prettyErrorln( - \\Usage: bunx [...flags] \[@version] [...flags and arguments] + \\Usage: bunx [flags] \\<@version\> [flags and arguments for the package] \\Execute an npm package executable (CLI), automatically installing into a global shared cache if not installed in node_modules. \\ \\Flags: \\ --bun Force the command to run with Bun instead of Node.js \\ \\Examples: - \\ bunx prisma migrate - \\ bunx prettier foo.js - \\ bunx --bun vite dev foo.js + \\ bunx prisma migrate + \\ bunx prettier foo.js + \\ bunx --bun vite dev foo.js \\ , .{}); }, @@ -2442,20 +2406,20 @@ pub const Command = struct { const intro_text = \\Usage: \\ Transpile and bundle one or more files. - \\ bun build [...flags] [...entrypoints] + \\ bun build [flags] \ ; const outro_text = \\Examples: \\ Frontend web apps: - \\ bun build ./src/index.ts --outfile=bundle.js - \\ bun build ./index.jsx ./lib/worker.ts --minify --splitting --outdir=out + \\ bun build --outfile=bundle.js ./src/index.ts + \\ bun build --minify --splitting --outdir=out ./index.jsx ./lib/worker.ts \\ \\ Bundle code to be run in Bun (reduces server startup time) - \\ bun build ./server.ts --target=bun --outfile=server.js + \\ bun build --target=bun --outfile=server.js ./server.ts \\ \\ Creating a standalone executable (see https://bun.sh/docs/bundler/executables) - \\ bun build ./cli.ts --compile --outfile=my-app + \\ bun build --compile --outfile=my-app ./cli.ts \\ \\A full list of flags is available at https://bun.sh/docs/bundler \\ @@ -2471,24 +2435,24 @@ pub const Command = struct { }, Command.Tag.TestCommand => { const intro_text = - \\Usage: bun test [...flags] [\...] + \\Usage: bun test [flags] [\] \\ Run all matching test files and print the results to stdout ; const outro_text = \\Examples: - \\ Run all test files + \\ Run all test files \\ bun test \\ \\ Run all test files with "foo" or "bar" in the file name - \\ bun test foo bar + \\ bun test foo bar \\ \\ Run all test files, only including tests whose names includes "baz" - \\ bun test --test-name-pattern baz + \\ bun test --test-name-pattern baz \\ \\Full documentation is available at https://bun.sh/docs/cli/test \\ ; - // Output.pretty("\n", .{}); + Output.pretty(intro_text, .{}); Output.flush(); Output.pretty("\n\nFlags:", .{}); @@ -2501,8 +2465,8 @@ pub const Command = struct { Command.Tag.CreateCommand => { const intro_text = \\Usage: - \\ bun create \ [...flags] [dest] - \\ bun create \ [...flags] [dest] + \\ bun create \ [...flags] dest + \\ bun create \ [...flags] dest \\ \\Environment variables: \\ GITHUB_TOKEN Supply a token to download code from GitHub with a higher rate limit @@ -2528,7 +2492,7 @@ pub const Command = struct { }, Command.Tag.UpgradeCommand => { const intro_text = - \\Usage: bun upgrade [...flags] + \\Usage: bun upgrade [flags] \\ Upgrade Bun ; const outro_text = @@ -2556,7 +2520,7 @@ pub const Command = struct { }, Command.Tag.ReplCommand => { const intro_text = - \\Usage: bun repl [...flags] + \\Usage: bun repl [flags] \\ Open a Bun REPL \\ ; diff --git a/src/cli/add_completions.txt b/src/cli/add_completions.txt index 7f2670cf7f..b7c7b6a1f6 100644 --- a/src/cli/add_completions.txt +++ b/src/cli/add_completions.txt @@ -2218,6 +2218,7 @@ elm-hot elm-hot-webpack-loader elm-test elm-webpack-loader +elysia email-validator emailjs-base64 emailjs-imap-client @@ -3700,6 +3701,7 @@ homebridge-config-ui-x homedir homedir-polyfill honeybadger-js +hono hook-std hookable hooks diff --git a/src/cli/add_completions.zig b/src/cli/add_completions.zig index b96f1c1e81..70d7e497d9 100644 --- a/src/cli/add_completions.zig +++ b/src/cli/add_completions.zig @@ -1,6 +1,16 @@ -pub const add_completions: []const u8 = @embedFile("add_completions.txt"); +// Auto-generated file. Do not edit. +// To regenerate this file, run: +// +// bun misctools/generate-add-completions.ts +// +// If you update add_completions.txt, then you should run this script again. +// +// This used to be a comptime block, but it made the build too slow. +// Compressing the completions list saves about 100 KB of binary size. const std = @import("std"); -const Environment = @import("../env.zig"); +const bun = @import("root").bun; +const zstd = bun.zstd; +const Environment = bun.Environment; pub const FirstLetter = enum(u8) { a = 'a', @@ -31,58 +41,64 @@ pub const FirstLetter = enum(u8) { z = 'z', }; -pub const Index = std.EnumArray(FirstLetter, []const []const u8); -pub const index: Index = if (Environment.isDebug) Index.initFill(&.{"OOMWorkAround"}) else brk: { - var array: Index = Index.initFill(&[_][]const u8{}); +const compressed_data = [_]u8{ 40, 181, 47, 253, 164, 156, 121, 2, 0, 148, 101, 6, 158, 128, 11, 130, 29, 50, 176, 110, 136, 168, 13, 197, 255, 138, 8, 111, 46, 215, 144, 108, 221, 73, 168, 182, 179, 179, 179, 179, 179, 179, 51, 237, 133, 67, 129, 22, 40, 149, 155, 13, 77, 238, 238, 175, 34, 93, 183, 136, 45, 12, 248, 199, 52, 72, 79, 1, 55, 29, 153, 29, 166, 29, 235, 37, 92, 124, 219, 151, 239, 224, 193, 181, 73, 119, 101, 17, 191, 237, 69, 28, 40, 90, 23, 246, 210, 75, 200, 80, 249, 38, 68, 112, 232, 47, 107, 117, 80, 218, 229, 37, 108, 104, 236, 17, 254, 137, 39, 108, 176, 212, 110, 10, 196, 195, 108, 211, 246, 97, 118, 226, 9, 27, 38, 158, 208, 49, 241, 132, 10, 152, 126, 158, 112, 225, 61, 161, 130, 79, 135, 226, 158, 112, 225, 158, 176, 193, 216, 61, 225, 2, 187, 39, 84, 240, 134, 111, 218, 233, 144, 178, 116, 7, 148, 108, 39, 125, 178, 111, 202, 94, 211, 137, 94, 27, 180, 246, 223, 2, 14, 11, 126, 189, 100, 84, 49, 80, 154, 90, 78, 84, 54, 224, 150, 205, 61, 225, 162, 45, 115, 127, 45, 35, 238, 9, 25, 223, 152, 113, 79, 216, 144, 176, 177, 48, 78, 238, 9, 23, 205, 139, 147, 123, 122, 169, 139, 123, 194, 7, 4, 247, 132, 142, 79, 135, 178, 214, 50, 79, 99, 135, 188, 35, 222, 202, 73, 175, 126, 173, 191, 237, 233, 100, 63, 27, 106, 204, 180, 116, 190, 217, 241, 214, 138, 109, 42, 225, 111, 245, 78, 55, 209, 76, 241, 73, 151, 186, 39, 108, 124, 82, 197, 173, 85, 146, 210, 77, 128, 56, 32, 97, 118, 180, 57, 33, 135, 239, 130, 144, 164, 246, 130, 180, 237, 242, 8, 114, 223, 95, 72, 4, 181, 69, 238, 9, 31, 216, 113, 152, 120, 167, 89, 165, 56, 88, 79, 3, 39, 189, 246, 63, 208, 138, 117, 244, 141, 153, 111, 204, 32, 247, 132, 12, 215, 109, 125, 19, 144, 123, 194, 137, 214, 111, 118, 148, 43, 169, 66, 238, 9, 25, 184, 178, 108, 14, 174, 154, 41, 143, 74, 60, 161, 4, 114, 79, 216, 224, 158, 80, 193, 90, 234, 60, 161, 68, 115, 5, 156, 123, 66, 5, 247, 132, 141, 102, 104, 42, 225, 111, 218, 170, 150, 77, 225, 120, 58, 15, 136, 43, 226, 164, 138, 19, 106, 165, 18, 69, 48, 131, 235, 218, 198, 187, 167, 19, 46, 124, 211, 73, 4, 136, 219, 10, 228, 158, 147, 134, 219, 138, 246, 38, 34, 220, 185, 31, 147, 166, 13, 119, 238, 132, 18, 30, 105, 180, 58, 199, 157, 112, 209, 236, 160, 13, 221, 9, 31, 141, 59, 161, 130, 183, 254, 202, 180, 191, 157, 84, 103, 66, 191, 74, 59, 33, 4, 51, 52, 119, 107, 83, 231, 221, 115, 37, 9, 231, 18, 125, 74, 109, 136, 37, 61, 247, 143, 41, 156, 75, 244, 21, 109, 15, 109, 43, 203, 32, 41, 153, 243, 112, 60, 29, 101, 217, 76, 171, 163, 5, 141, 247, 153, 57, 56, 42, 41, 210, 79, 237, 132, 143, 8, 18, 82, 39, 132, 68, 144, 128, 82, 39, 100, 72, 157, 80, 65, 42, 150, 157, 144, 161, 65, 118, 66, 200, 227, 18, 42, 168, 184, 132, 10, 158, 75, 8, 105, 151, 80, 65, 196, 181, 132, 24, 151, 80, 66, 215, 50, 200, 37, 92, 248, 90, 141, 245, 150, 150, 239, 139, 208, 193, 243, 155, 144, 126, 190, 55, 64, 68, 64, 252, 72, 2, 98, 95, 244, 72, 120, 227, 180, 168, 201, 197, 160, 167, 243, 83, 161, 7, 162, 56, 161, 167, 191, 61, 174, 80, 182, 143, 84, 75, 228, 186, 253, 237, 113, 194, 6, 232, 219, 83, 201, 14, 44, 115, 78, 156, 144, 193, 155, 19, 42, 168, 69, 141, 155, 19, 58, 168, 236, 198, 61, 7, 84, 170, 147, 234, 158, 71, 30, 209, 230, 135, 246, 115, 16, 228, 17, 164, 141, 84, 155, 19, 62, 164, 125, 231, 214, 228, 42, 8, 14, 193, 117, 13, 160, 77, 254, 174, 9, 206, 64, 39, 173, 222, 180, 237, 116, 252, 129, 206, 197, 223, 231, 187, 3, 138, 238, 100, 81, 10, 106, 32, 109, 227, 165, 136, 208, 218, 212, 49, 210, 181, 12, 3, 11, 156, 75, 180, 192, 178, 203, 121, 31, 78, 122, 101, 82, 219, 65, 173, 102, 10, 226, 181, 212, 65, 206, 53, 120, 200, 9, 171, 230, 132, 11, 215, 181, 204, 4, 226, 71, 95, 97, 208, 171, 80, 132, 196, 243, 27, 143, 123, 80, 133, 30, 199, 9, 33, 30, 199, 9, 21, 124, 161, 107, 25, 199, 55, 245, 198, 233, 43, 175, 161, 5, 16, 14, 13, 157, 17, 132, 14, 231, 151, 130, 62, 237, 167, 93, 172, 245, 28, 84, 169, 168, 131, 190, 117, 194, 30, 65, 252, 104, 105, 170, 232, 2, 247, 132, 190, 37, 244, 215, 50, 236, 203, 24, 63, 56, 192, 183, 117, 252, 219, 46, 119, 160, 117, 97, 218, 128, 54, 39, 111, 64, 82, 87, 0, 2, 236, 74, 11, 48, 126, 6, 58, 215, 107, 90, 162, 7, 208, 202, 198, 251, 77, 39, 141, 19, 50, 200, 102, 120, 94, 174, 2, 253, 184, 241, 254, 2, 186, 150, 124, 109, 89, 106, 127, 131, 36, 229, 187, 51, 90, 159, 92, 218, 188, 30, 211, 120, 63, 187, 89, 138, 111, 202, 22, 172, 199, 68, 180, 123, 30, 130, 158, 231, 183, 95, 82, 23, 128, 175, 66, 19, 184, 112, 62, 29, 242, 6, 102, 218, 162, 118, 240, 85, 188, 173, 14, 41, 109, 155, 255, 192, 63, 189, 126, 103, 60, 204, 58, 133, 46, 199, 109, 191, 229, 59, 168, 103, 66, 223, 242, 35, 152, 224, 147, 23, 131, 152, 182, 14, 37, 92, 233, 167, 218, 9, 253, 84, 187, 216, 91, 62, 132, 134, 125, 142, 214, 111, 219, 14, 233, 90, 242, 23, 52, 188, 34, 28, 45, 238, 6, 185, 216, 91, 167, 67, 237, 49, 3, 165, 16, 255, 88, 217, 190, 86, 236, 210, 55, 65, 253, 65, 69, 155, 4, 81, 60, 173, 111, 194, 114, 108, 29, 53, 46, 66, 253, 165, 83, 204, 36, 24, 180, 46, 209, 211, 252, 190, 78, 3, 142, 231, 98, 26, 180, 159, 55, 165, 104, 88, 229, 167, 122, 112, 192, 133, 175, 92, 120, 53, 8, 53, 109, 253, 49, 141, 19, 62, 200, 212, 44, 211, 222, 147, 161, 102, 153, 150, 78, 66, 16, 100, 96, 26, 39, 84, 152, 112, 109, 187, 40, 218, 144, 122, 83, 213, 56, 97, 132, 166, 150, 170, 249, 209, 146, 233, 68, 63, 155, 62, 220, 37, 115, 126, 41, 12, 26, 92, 50, 228, 64, 6, 138, 238, 4, 241, 183, 213, 127, 14, 156, 173, 66, 250, 47, 226, 174, 189, 64, 227, 135, 174, 101, 221, 181, 33, 77, 62, 114, 215, 118, 215, 134, 248, 164, 145, 229, 57, 82, 237, 65, 48, 97, 150, 131, 154, 29, 124, 83, 135, 180, 101, 42, 81, 171, 75, 168, 175, 126, 14, 77, 149, 242, 142, 219, 61, 252, 182, 58, 223, 116, 18, 225, 164, 84, 213, 118, 65, 21, 93, 206, 225, 109, 43, 203, 144, 123, 58, 136, 198, 235, 173, 147, 7, 95, 253, 90, 39, 76, 221, 97, 45, 77, 72, 233, 2, 196, 96, 45, 77, 16, 93, 77, 75, 132, 13, 173, 173, 170, 105, 35, 73, 39, 123, 208, 202, 232, 85, 141, 202, 50, 218, 14, 57, 159, 78, 162, 225, 133, 26, 55, 180, 93, 80, 227, 236, 141, 48, 248, 182, 171, 105, 197, 190, 227, 125, 25, 227, 132, 139, 138, 54, 218, 13, 14, 241, 77, 25, 227, 132, 144, 197, 252, 72, 219, 174, 8, 127, 223, 138, 34, 213, 28, 92, 223, 202, 9, 82, 154, 140, 113, 66, 6, 17, 32, 212, 131, 75, 38, 225, 109, 23, 198, 9, 29, 30, 102, 27, 123, 139, 19, 54, 250, 49, 186, 206, 59, 218, 186, 100, 12, 213, 249, 26, 197, 32, 137, 176, 104, 126, 228, 210, 52, 106, 246, 78, 247, 120, 223, 133, 148, 101, 123, 80, 150, 13, 194, 48, 171, 28, 223, 58, 105, 156, 80, 243, 226, 132, 13, 191, 190, 19, 8, 151, 12, 53, 94, 7, 143, 64, 112, 104, 147, 173, 19, 78, 104, 2, 19, 244, 73, 215, 74, 244, 73, 215, 226, 9, 253, 117, 40, 72, 221, 229, 90, 156, 144, 162, 45, 39, 116, 52, 156, 80, 193, 195, 108, 163, 65, 149, 97, 56, 33, 131, 219, 95, 204, 143, 42, 238, 96, 237, 63, 84, 225, 69, 181, 116, 85, 156, 208, 1, 109, 210, 49, 188, 181, 81, 120, 29, 127, 141, 207, 134, 230, 110, 157, 64, 170, 56, 97, 132, 175, 205, 70, 141, 57, 113, 66, 200, 210, 253, 148, 36, 78, 216, 120, 146, 235, 74, 78, 200, 248, 77, 10, 226, 132, 11, 245, 135, 56, 33, 67, 246, 87, 20, 57, 131, 56, 225, 131, 113, 51, 136, 19, 58, 180, 50, 170, 16, 39, 100, 32, 78, 216, 120, 20, 47, 136, 19, 46, 56, 225, 163, 178, 204, 179, 131, 216, 58, 142, 20, 75, 248, 160, 88, 194, 134, 214, 247, 183, 18, 58, 236, 91, 9, 21, 42, 190, 18, 42, 180, 166, 236, 111, 228, 43, 225, 67, 169, 132, 217, 65, 190, 18, 58, 144, 175, 132, 144, 167, 179, 45, 107, 102, 114, 49, 14, 255, 166, 19, 125, 80, 120, 169, 199, 43, 225, 195, 90, 6, 189, 171, 179, 58, 175, 132, 11, 103, 240, 207, 247, 135, 122, 99, 231, 14, 78, 85, 65, 180, 241, 74, 184, 144, 120, 37, 84, 216, 183, 43, 225, 194, 3, 121, 28, 39, 228, 160, 58, 121, 113, 187, 18, 54, 24, 102, 87, 194, 5, 111, 188, 238, 234, 232, 155, 62, 56, 175, 150, 210, 164, 191, 134, 126, 166, 246, 250, 95, 62, 36, 172, 132, 14, 230, 219, 182, 163, 232, 78, 180, 29, 74, 40, 209, 250, 204, 12, 154, 112, 215, 110, 16, 178, 75, 39, 191, 11, 163, 41, 36, 41, 255, 241, 234, 13, 72, 179, 220, 57, 24, 109, 244, 219, 52, 172, 126, 45, 117, 37, 116, 240, 8, 67, 196, 90, 143, 65, 174, 15, 141, 194, 10, 81, 228, 167, 210, 230, 132, 90, 173, 132, 13, 87, 150, 205, 225, 17, 8, 239, 187, 124, 151, 196, 103, 130, 208, 216, 35, 104, 129, 7, 95, 251, 239, 193, 64, 107, 196, 251, 46, 135, 47, 67, 53, 253, 138, 34, 196, 229, 74, 168, 32, 193, 167, 212, 126, 240, 198, 251, 217, 14, 68, 208, 104, 254, 85, 42, 73, 200, 104, 236, 145, 132, 11, 173, 21, 93, 200, 35, 9, 33, 252, 173, 202, 110, 253, 166, 137, 65, 154, 70, 81, 51, 131, 26, 123, 100, 193, 211, 185, 94, 234, 77, 184, 128, 122, 46, 8, 31, 223, 54, 161, 130, 171, 77, 168, 160, 54, 161, 195, 59, 39, 8, 109, 66, 136, 133, 35, 104, 19, 54, 76, 154, 162, 77, 200, 160, 24, 36, 237, 57, 9, 25, 240, 8, 210, 37, 73, 235, 144, 4, 123, 203, 128, 52, 120, 223, 133, 92, 233, 231, 122, 76, 27, 240, 197, 252, 19, 85, 14, 82, 45, 44, 23, 114, 74, 34, 141, 240, 8, 74, 198, 237, 36, 108, 116, 163, 176, 147, 144, 97, 87, 66, 157, 106, 37, 236, 36, 132, 184, 166, 101, 39, 97, 68, 43, 102, 78, 66, 134, 10, 52, 124, 69, 213, 90, 39, 97, 68, 219, 124, 238, 137, 58, 9, 29, 188, 173, 234, 36, 92, 184, 183, 96, 143, 65, 161, 205, 239, 223, 116, 87, 138, 240, 102, 214, 220, 173, 72, 226, 11, 225, 186, 146, 164, 78, 66, 9, 39, 125, 82, 144, 75, 117, 18, 62, 36, 105, 37, 13, 198, 92, 49, 225, 75, 151, 92, 249, 169, 184, 181, 68, 30, 121, 164, 138, 219, 68, 243, 226, 212, 240, 77, 25, 51, 6, 30, 249, 247, 253, 78, 39, 33, 227, 213, 59, 157, 132, 11, 214, 233, 36, 92, 80, 187, 114, 210, 22, 105, 106, 218, 202, 104, 51, 0, 241, 164, 108, 116, 61, 136, 96, 177, 119, 144, 3, 215, 253, 253, 166, 168, 245, 83, 118, 58, 9, 37, 58, 157, 132, 14, 136, 138, 54, 218, 107, 25, 212, 240, 114, 240, 160, 158, 9, 105, 211, 82, 75, 39, 33, 67, 75, 39, 161, 194, 174, 212, 234, 144, 172, 116, 18, 46, 176, 108, 73, 58, 9, 23, 148, 244, 214, 28, 212, 190, 231, 186, 206, 29, 74, 191, 217, 157, 170, 66, 20, 94, 43, 29, 111, 160, 146, 93, 141, 198, 30, 137, 168, 122, 43, 85, 20, 162, 236, 169, 104, 202, 152, 161, 108, 95, 63, 85, 67, 91, 22, 190, 233, 36, 124, 72, 154, 153, 132, 11, 254, 141, 153, 132, 12, 223, 118, 161, 198, 36, 100, 76, 154, 50, 9, 23, 22, 38, 161, 2, 143, 64, 88, 120, 45, 136, 73, 232, 104, 205, 174, 68, 173, 223, 52, 33, 164, 241, 126, 126, 211, 109, 70, 107, 105, 66, 8, 253, 236, 69, 151, 38, 108, 248, 4, 36, 42, 225, 194, 87, 82, 66, 5, 100, 104, 154, 151, 132, 11, 188, 36, 108, 52, 47, 9, 21, 40, 153, 80, 129, 175, 119, 58, 159, 9, 27, 43, 159, 103, 66, 6, 198, 10, 57, 158, 9, 25, 206, 231, 122, 83, 71, 234, 153, 208, 0, 234, 153, 80, 161, 237, 226, 153, 144, 161, 117, 187, 51, 225, 130, 87, 103, 194, 133, 149, 109, 85, 103, 66, 134, 234, 165, 19, 119, 240, 41, 181, 81, 3, 255, 166, 173, 241, 210, 153, 48, 226, 183, 162, 72, 251, 61, 47, 228, 109, 243, 93, 38, 132, 172, 108, 118, 153, 112, 145, 191, 46, 19, 66, 154, 73, 203, 132, 11, 191, 12, 114, 120, 78, 24, 180, 98, 87, 166, 109, 252, 187, 32, 139, 114, 172, 75, 164, 254, 150, 177, 66, 234, 79, 210, 118, 177, 199, 81, 203, 132, 143, 39, 151, 227, 127, 137, 90, 38, 132, 104, 197, 50, 212, 51, 53, 212, 51, 161, 165, 44, 19, 58, 84, 115, 210, 70, 250, 235, 15, 106, 81, 171, 254, 186, 182, 174, 149, 9, 25, 240, 193, 1, 162, 218, 46, 104, 173, 76, 216, 112, 139, 82, 154, 137, 67, 154, 230, 26, 117, 145, 6, 102, 27, 4, 241, 86, 183, 43, 19, 50, 180, 151, 93, 153, 112, 129, 14, 250, 169, 86, 38, 92, 52, 10, 55, 116, 178, 159, 75, 81, 68, 184, 39, 97, 94, 72, 215, 146, 16, 46, 41, 238, 206, 167, 243, 85, 218, 15, 124, 149, 54, 175, 227, 233, 32, 103, 208, 130, 214, 38, 91, 178, 134, 166, 223, 56, 73, 168, 231, 130, 142, 9, 36, 12, 128, 66, 16, 33, 161, 130, 250, 82, 60, 194, 82, 251, 33, 142, 47, 35, 233, 34, 215, 127, 30, 92, 255, 89, 112, 65, 255, 89, 208, 161, 21, 235, 13, 231, 155, 166, 11, 233, 63, 11, 62, 244, 159, 5, 21, 180, 237, 122, 22, 100, 240, 8, 114, 75, 89, 112, 241, 240, 226, 111, 65, 6, 254, 237, 126, 11, 46, 52, 101, 191, 66, 220, 80, 69, 49, 190, 224, 249, 215, 118, 46, 61, 114, 60, 223, 27, 94, 19, 45, 159, 124, 218, 91, 144, 193, 73, 159, 148, 199, 82, 237, 45, 216, 208, 222, 130, 10, 146, 173, 183, 224, 66, 54, 237, 68, 107, 189, 5, 29, 107, 189, 5, 21, 156, 225, 170, 56, 33, 127, 223, 246, 215, 38, 107, 172, 253, 71, 209, 90, 255, 211, 225, 3, 165, 153, 183, 224, 194, 218, 127, 190, 144, 164, 116, 146, 100, 30, 97, 52, 74, 58, 71, 74, 29, 154, 43, 79, 219, 201, 3, 255, 2, 103, 235, 56, 68, 189, 117, 162, 253, 244, 77, 128, 168, 91, 167, 234, 36, 214, 90, 230, 49, 129, 59, 188, 65, 217, 62, 250, 150, 43, 223, 4, 93, 64, 29, 148, 73, 111, 15, 77, 36, 94, 43, 81, 43, 195, 188, 5, 33, 15, 179, 186, 150, 183, 32, 67, 127, 23, 74, 254, 5, 27, 201, 191, 160, 66, 99, 95, 80, 65, 146, 212, 73, 19, 144, 192, 23, 108, 76, 64, 2, 95, 80, 161, 213, 57, 125, 71, 190, 160, 163, 249, 147, 70, 218, 162, 87, 231, 182, 57, 242, 5, 39, 92, 221, 38, 109, 228, 11, 58, 124, 193, 134, 36, 229, 251, 174, 132, 220, 181, 221, 147, 144, 3, 73, 202, 247, 86, 167, 64, 190, 105, 147, 173, 254, 208, 251, 46, 111, 117, 138, 239, 122, 44, 187, 42, 30, 102, 23, 180, 102, 253, 33, 189, 27, 123, 4, 105, 147, 146, 148, 174, 193, 202, 135, 217, 137, 149, 223, 148, 105, 167, 182, 232, 97, 214, 25, 212, 224, 170, 208, 195, 108, 171, 227, 54, 161, 135, 89, 71, 241, 48, 235, 144, 164, 116, 237, 73, 18, 47, 228, 75, 126, 196, 62, 148, 218, 227, 186, 235, 119, 57, 111, 88, 196, 143, 88, 106, 191, 235, 90, 82, 165, 118, 3, 202, 246, 219, 65, 12, 36, 41, 29, 68, 21, 55, 212, 44, 65, 173, 132, 183, 170, 84, 167, 93, 107, 211, 150, 153, 152, 52, 125, 124, 210, 87, 229, 224, 173, 78, 225, 109, 202, 195, 195, 236, 106, 222, 59, 133, 75, 214, 234, 173, 255, 174, 204, 182, 100, 72, 215, 243, 222, 128, 162, 239, 13, 142, 167, 131, 182, 93, 181, 31, 81, 120, 185, 75, 134, 220, 211, 221, 91, 139, 187, 105, 235, 200, 61, 189, 179, 251, 166, 11, 48, 211, 22, 169, 226, 22, 209, 218, 94, 2, 90, 43, 118, 37, 107, 110, 104, 1, 237, 164, 232, 82, 106, 153, 174, 101, 22, 52, 111, 81, 229, 208, 224, 51, 53, 78, 15, 238, 9, 181, 212, 149, 90, 215, 58, 111, 217, 255, 64, 127, 155, 194, 96, 226, 18, 75, 91, 123, 12, 82, 44, 209, 46, 78, 137, 123, 193, 9, 103, 122, 65, 5, 213, 76, 105, 184, 167, 163, 111, 203, 78, 146, 218, 11, 62, 56, 175, 82, 123, 65, 135, 247, 76, 189, 32, 131, 68, 57, 114, 82, 203, 94, 208, 177, 40, 5, 169, 246, 183, 205, 94, 176, 193, 25, 26, 175, 183, 236, 5, 31, 116, 41, 197, 12, 53, 230, 22, 124, 52, 188, 239, 114, 110, 65, 6, 111, 156, 182, 194, 11, 50, 154, 86, 120, 193, 5, 137, 170, 240, 130, 144, 79, 234, 21, 94, 176, 161, 149, 226, 17, 135, 82, 120, 65, 136, 182, 126, 188, 32, 195, 167, 54, 4, 105, 188, 244, 227, 5, 23, 106, 223, 163, 240, 71, 37, 232, 151, 49, 242, 181, 255, 30, 52, 222, 207, 102, 39, 217, 208, 163, 18, 119, 93, 15, 60, 157, 170, 61, 110, 14, 143, 232, 250, 231, 5, 25, 142, 55, 47, 184, 224, 142, 23, 84, 112, 210, 39, 197, 81, 225, 229, 125, 215, 202, 167, 80, 4, 12, 244, 77, 26, 116, 53, 13, 127, 171, 218, 229, 233, 14, 15, 179, 207, 13, 194, 27, 167, 125, 208, 79, 69, 241, 172, 198, 200, 93, 162, 85, 146, 214, 53, 47, 78, 104, 129, 203, 126, 111, 84, 120, 121, 230, 5, 23, 76, 225, 97, 22, 23, 186, 120, 65, 5, 73, 226, 5, 177, 88, 5, 61, 60, 138, 23, 124, 60, 138, 23, 84, 72, 170, 120, 193, 133, 102, 196, 11, 62, 218, 130, 1, 240, 8, 114, 108, 65, 6, 199, 211, 233, 183, 22, 100, 44, 199, 30, 34, 129, 98, 143, 133, 215, 130, 248, 145, 231, 164, 101, 66, 142, 173, 190, 181, 32, 131, 167, 95, 27, 169, 222, 69, 223, 90, 208, 225, 169, 124, 34, 135, 210, 69, 132, 6, 8, 58, 172, 253, 215, 248, 69, 236, 224, 216, 163, 133, 215, 99, 225, 181, 96, 3, 195, 172, 66, 146, 182, 107, 65, 199, 178, 253, 141, 19, 82, 234, 174, 107, 193, 7, 143, 168, 36, 215, 130, 139, 247, 93, 146, 148, 107, 193, 198, 90, 48, 0, 191, 11, 54, 210, 54, 102, 218, 24, 196, 178, 11, 105, 187, 99, 212, 150, 211, 239, 130, 15, 93, 74, 45, 229, 209, 239, 130, 143, 9, 38, 112, 187, 224, 2, 5, 36, 184, 93, 112, 33, 2, 9, 64, 224, 118, 65, 6, 151, 77, 89, 184, 106, 191, 99, 47, 210, 240, 166, 173, 82, 236, 188, 50, 20, 89, 24, 194, 97, 173, 68, 170, 237, 130, 140, 221, 228, 217, 118, 65, 198, 68, 226, 208, 208, 44, 211, 56, 161, 230, 238, 101, 20, 17, 234, 153, 208, 227, 233, 108, 187, 32, 67, 196, 163, 245, 215, 50, 200, 155, 92, 12, 154, 192, 4, 181, 93, 112, 66, 146, 210, 161, 182, 11, 50, 60, 2, 161, 217, 5, 33, 202, 246, 63, 27, 53, 187, 224, 162, 217, 5, 29, 141, 61, 130, 154, 93, 144, 209, 218, 232, 183, 69, 205, 46, 248, 240, 8, 90, 181, 11, 46, 244, 233, 155, 0, 113, 62, 221, 68, 115, 236, 248, 85, 47, 17, 115, 13, 194, 19, 43, 212, 250, 175, 11, 70, 184, 173, 120, 215, 5, 23, 75, 221, 117, 65, 6, 109, 90, 54, 146, 32, 129, 211, 5, 31, 223, 116, 65, 5, 73, 39, 115, 82, 211, 5, 27, 173, 191, 116, 65, 134, 86, 167, 186, 32, 131, 46, 216, 88, 118, 33, 180, 28, 66, 234, 185, 32, 4, 193, 4, 69, 0, 69, 192, 17, 66, 78, 66, 42, 73, 186, 160, 2, 34, 72, 112, 146, 46, 200, 208, 57, 65, 79, 69, 46, 164, 212, 130, 16, 143, 48, 60, 162, 138, 27, 106, 120, 247, 116, 90, 144, 193, 35, 170, 56, 45, 11, 50, 36, 112, 69, 206, 231, 130, 140, 198, 251, 235, 12, 209, 180, 85, 42, 221, 67, 219, 166, 147, 57, 252, 227, 39, 151, 218, 110, 144, 123, 114, 111, 94, 28, 90, 179, 43, 145, 123, 114, 79, 238, 185, 224, 194, 61, 211, 248, 151, 40, 173, 231, 130, 18, 11, 175, 6, 57, 158, 11, 54, 86, 191, 8, 38, 104, 168, 141, 96, 130, 8, 38, 136, 96, 2, 138, 134, 45, 138, 96, 130, 8, 38, 136, 48, 129, 58, 132, 70, 211, 8, 18, 42, 12, 18, 207, 215, 230, 119, 135, 55, 184, 254, 227, 78, 233, 92, 89, 54, 7, 247, 73, 83, 134, 68, 179, 76, 155, 80, 103, 199, 24, 242, 224, 174, 137, 219, 161, 93, 9, 169, 51, 38, 77, 103, 35, 130, 9, 144, 122, 46, 184, 72, 219, 223, 52, 61, 157, 11, 46, 236, 74, 40, 155, 174, 92, 208, 161, 169, 229, 202, 5, 29, 212, 173, 107, 114, 193, 133, 54, 217, 52, 185, 160, 195, 251, 46, 228, 170, 153, 162, 114, 193, 134, 182, 141, 202, 5, 25, 60, 226, 248, 29, 142, 231, 114, 73, 202, 5, 27, 30, 169, 52, 98, 24, 8, 47, 56, 151, 232, 153, 108, 165, 255, 218, 120, 59, 25, 132, 255, 245, 103, 201, 5, 194, 4, 171, 246, 26, 188, 162, 205, 21, 49, 208, 198, 139, 36, 229, 130, 12, 39, 229, 242, 44, 185, 96, 163, 241, 122, 150, 92, 112, 241, 48, 203, 144, 36, 117, 210, 90, 153, 220, 246, 4, 78, 250, 164, 60, 36, 43, 41, 234, 158, 71, 227, 149, 170, 229, 59, 68, 131, 195, 155, 188, 9, 3, 17, 32, 64, 218, 228, 59, 133, 115, 57, 65, 225, 229, 145, 133, 153, 52, 237, 126, 175, 174, 189, 150, 121, 22, 59, 120, 238, 202, 244, 214, 201, 182, 167, 150, 39, 136, 31, 73, 146, 58, 9, 41, 237, 92, 73, 85, 123, 15, 180, 190, 111, 163, 29, 65, 115, 107, 182, 162, 223, 56, 33, 237, 125, 19, 18, 144, 203, 105, 55, 64, 89, 54, 109, 213, 158, 2, 18, 224, 174, 162, 141, 118, 3, 180, 249, 81, 63, 167, 169, 31, 51, 75, 29, 85, 60, 242, 233, 24, 39, 3, 44, 128, 1, 147, 166, 72, 21, 183, 150, 186, 152, 151, 4, 52, 109, 215, 178, 14, 1, 2, 9, 174, 84, 34, 157, 236, 191, 164, 222, 56, 185, 237, 119, 68, 225, 197, 40, 96, 1, 16, 36, 60, 64, 130, 4, 170, 56, 109, 123, 12, 171, 123, 222, 1, 16, 168, 104, 163, 141, 30, 73, 218, 247, 6, 44, 160, 213, 61, 87, 76, 89, 54, 196, 48, 106, 102, 30, 149, 32, 253, 76, 187, 220, 83, 77, 219, 126, 170, 28, 160, 244, 159, 99, 12, 129, 171, 123, 174, 212, 180, 117, 128, 82, 199, 201, 145, 36, 241, 90, 34, 144, 0, 130, 8, 68, 112, 69, 8, 64, 0, 1, 20, 128, 90, 157, 203, 166, 32, 166, 239, 143, 35, 143, 36, 197, 239, 17, 104, 125, 215, 247, 100, 140, 83, 211, 118, 109, 246, 63, 192, 181, 19, 80, 241, 136, 106, 187, 32, 2, 80, 120, 49, 223, 8, 112, 188, 21, 117, 64, 1, 248, 1, 254, 244, 250, 189, 113, 66, 238, 185, 146, 163, 74, 126, 123, 12, 224, 146, 33, 10, 230, 118, 160, 189, 20, 109, 93, 236, 141, 83, 46, 109, 137, 102, 226, 186, 235, 219, 54, 109, 18, 43, 203, 134, 82, 177, 108, 137, 214, 172, 243, 115, 187, 231, 91, 29, 183, 139, 48, 223, 118, 20, 202, 178, 33, 73, 123, 78, 66, 206, 167, 107, 125, 101, 217, 233, 6, 160, 44, 219, 186, 178, 108, 72, 87, 211, 146, 13, 224, 249, 109, 203, 180, 85, 170, 58, 95, 211, 216, 183, 221, 46, 166, 241, 126, 182, 101, 187, 84, 165, 42, 6, 28, 192, 219, 254, 218, 183, 160, 135, 217, 182, 204, 153, 2, 28, 79, 197, 107, 98, 169, 98, 134, 38, 168, 226, 17, 181, 249, 169, 144, 78, 152, 105, 31, 192, 91, 151, 89, 197, 12, 85, 60, 210, 76, 220, 117, 45, 185, 157, 19, 244, 77, 89, 227, 222, 73, 83, 6, 76, 154, 62, 171, 111, 118, 108, 162, 162, 22, 66, 181, 29, 210, 182, 171, 45, 242, 206, 101, 177, 119, 154, 124, 228, 192, 35, 13, 172, 156, 68, 52, 222, 231, 92, 235, 24, 39, 180, 46, 91, 23, 166, 10, 104, 230, 160, 156, 168, 137, 42, 6, 154, 183, 232, 3, 111, 12, 210, 79, 181, 192, 221, 83, 211, 86, 45, 102, 208, 183, 100, 175, 41, 122, 111, 12, 180, 145, 178, 108, 79, 46, 231, 171, 10, 224, 74, 37, 119, 74, 214, 170, 146, 33, 239, 137, 248, 209, 202, 214, 223, 69, 241, 174, 17, 111, 112, 199, 237, 144, 54, 173, 12, 85, 60, 130, 244, 77, 6, 176, 28, 63, 154, 236, 155, 160, 200, 151, 129, 231, 183, 179, 113, 183, 139, 113, 192, 61, 87, 106, 188, 170, 120, 185, 74, 3, 242, 173, 156, 168, 75, 134, 180, 45, 163, 170, 129, 126, 54, 186, 84, 161, 181, 50, 45, 80, 241, 8, 250, 148, 218, 15, 120, 243, 103, 115, 82, 133, 30, 102, 85, 219, 5, 9, 224, 225, 219, 46, 87, 136, 182, 67, 21, 67, 236, 74, 168, 177, 116, 201, 5, 23, 150, 46, 207, 178, 228, 130, 140, 206, 37, 23, 92, 208, 223, 133, 94, 4, 19, 44, 216, 136, 96, 130, 5, 21, 52, 44, 207, 129, 252, 59, 232, 121, 39, 32, 212, 115, 65, 110, 2, 75, 244, 188, 72, 251, 85, 184, 227, 137, 170, 5, 141, 25, 227, 4, 97, 157, 72, 251, 85, 36, 74, 60, 173, 111, 2, 170, 36, 58, 252, 178, 93, 72, 63, 187, 178, 12, 122, 42, 220, 142, 173, 111, 14, 73, 98, 38, 209, 218, 52, 188, 32, 46, 97, 118, 30, 16, 13, 47, 212, 188, 69, 213, 67, 103, 68, 87, 194, 220, 96, 194, 121, 43, 137, 11, 173, 90, 209, 74, 34, 195, 121, 43, 137, 34, 112, 74, 162, 35, 2, 167, 52, 239, 236, 22, 172, 9, 254, 48, 119, 109, 87, 81, 10, 109, 78, 72, 162, 28, 61, 217, 21, 72, 107, 214, 37, 137, 25, 68, 146, 152, 73, 148, 35, 167, 36, 78, 104, 243, 59, 98, 217, 233, 144, 83, 18, 31, 36, 202, 145, 166, 157, 168, 66, 207, 18, 204, 180, 69, 78, 73, 100, 52, 127, 210, 214, 56, 33, 167, 36, 50, 240, 157, 144, 83, 18, 23, 143, 79, 169, 141, 156, 146, 24, 81, 217, 181, 148, 68, 6, 109, 236, 184, 137, 247, 92, 200, 117, 178, 216, 181, 209, 179, 26, 23, 96, 178, 159, 173, 142, 237, 2, 169, 61, 94, 84, 33, 101, 236, 74, 73, 116, 224, 84, 151, 107, 179, 227, 146, 169, 63, 188, 247, 50, 191, 73, 73, 124, 36, 159, 36, 42, 84, 242, 115, 146, 200, 224, 106, 201, 181, 12, 126, 219, 162, 156, 36, 50, 114, 146, 168, 224, 157, 102, 151, 211, 172, 131, 222, 181, 105, 222, 2, 173, 88, 127, 215, 230, 127, 137, 140, 137, 214, 202, 50, 218, 142, 255, 37, 66, 248, 95, 226, 163, 85, 210, 118, 57, 154, 70, 145, 82, 11, 250, 169, 237, 17, 8, 250, 186, 28, 227, 135, 198, 173, 101, 226, 127, 137, 142, 134, 151, 54, 45, 157, 228, 160, 218, 46, 136, 255, 37, 54, 92, 55, 237, 98, 135, 84, 51, 197, 61, 93, 131, 93, 9, 61, 188, 156, 42, 20, 1, 231, 95, 34, 132, 243, 47, 241, 177, 9, 253, 170, 151, 200, 240, 198, 235, 188, 234, 37, 66, 160, 254, 79, 124, 56, 213, 229, 144, 5, 165, 101, 252, 137, 14, 78, 209, 157, 160, 166, 14, 105, 74, 231, 136, 241, 39, 82, 232, 239, 66, 11, 127, 98, 99, 225, 79, 84, 72, 254, 68, 133, 117, 217, 212, 113, 196, 83, 34, 168, 229, 186, 231, 250, 196, 6, 110, 143, 44, 244, 115, 149, 50, 182, 175, 222, 128, 101, 167, 123, 215, 158, 240, 71, 37, 16, 214, 254, 131, 240, 251, 168, 68, 37, 187, 86, 34, 181, 199, 83, 251, 121, 251, 101, 187, 24, 118, 82, 42, 3, 39, 165, 62, 217, 157, 16, 214, 254, 163, 208, 167, 111, 2, 132, 199, 47, 219, 181, 116, 173, 236, 116, 16, 214, 126, 123, 232, 87, 223, 202, 201, 132, 190, 149, 19, 93, 255, 220, 206, 59, 40, 99, 95, 255, 234, 144, 166, 161, 167, 95, 251, 233, 215, 126, 248, 42, 109, 125, 250, 38, 160, 137, 138, 254, 99, 188, 28, 90, 127, 217, 174, 134, 21, 65, 18, 230, 182, 201, 88, 33, 9, 115, 132, 55, 72, 152, 29, 244, 91, 121, 104, 253, 95, 166, 223, 116, 34, 225, 164, 212, 198, 14, 117, 82, 38, 36, 241, 85, 16, 19, 79, 168, 145, 218, 227, 137, 11, 148, 247, 68, 5, 10, 47, 6, 185, 186, 199, 65, 189, 117, 2, 97, 205, 204, 123, 98, 131, 122, 235, 36, 251, 209, 163, 146, 54, 225, 235, 41, 134, 104, 109, 101, 218, 162, 252, 84, 206, 28, 190, 120, 97, 143, 163, 214, 172, 123, 162, 195, 2, 127, 117, 127, 208, 120, 191, 27, 239, 59, 118, 80, 197, 13, 77, 124, 171, 59, 131, 40, 116, 169, 150, 206, 131, 51, 72, 215, 194, 212, 61, 241, 209, 180, 109, 85, 77, 91, 219, 161, 215, 230, 60, 113, 34, 2, 19, 136, 32, 65, 98, 26, 121, 4, 2, 4, 205, 249, 221, 211, 137, 121, 34, 196, 19, 27, 118, 165, 221, 228, 203, 90, 153, 60, 209, 209, 233, 137, 10, 46, 61, 81, 33, 91, 122, 226, 130, 174, 37, 31, 121, 34, 195, 19, 27, 79, 118, 39, 242, 68, 134, 242, 207, 119, 212, 224, 105, 93, 107, 191, 19, 27, 223, 137, 16, 95, 57, 217, 238, 68, 198, 195, 108, 119, 34, 195, 164, 233, 78, 92, 248, 148, 142, 113, 66, 79, 191, 35, 102, 209, 195, 48, 235, 146, 77, 56, 41, 181, 185, 233, 187, 100, 159, 171, 65, 69, 85, 91, 213, 30, 64, 80, 247, 60, 63, 183, 131, 192, 96, 194, 182, 151, 154, 126, 83, 143, 120, 7, 77, 253, 248, 155, 221, 161, 105, 151, 47, 134, 121, 78, 131, 235, 182, 122, 122, 74, 91, 155, 150, 221, 169, 15, 218, 180, 236, 135, 78, 152, 185, 100, 141, 247, 92, 14, 253, 108, 253, 198, 251, 73, 189, 245, 37, 86, 238, 111, 91, 134, 43, 170, 218, 54, 253, 134, 207, 53, 81, 81, 213, 22, 65, 168, 182, 140, 163, 245, 215, 50, 14, 79, 235, 100, 229, 155, 160, 143, 86, 134, 89, 5, 145, 164, 92, 208, 147, 221, 137, 142, 198, 219, 94, 195, 37, 197, 245, 243, 185, 81, 119, 162, 131, 97, 86, 161, 238, 68, 70, 67, 207, 111, 39, 46, 158, 214, 55, 1, 229, 118, 162, 67, 169, 106, 250, 21, 69, 254, 77, 217, 107, 58, 81, 226, 201, 238, 244, 166, 19, 27, 222, 116, 162, 130, 122, 167, 107, 58, 145, 17, 161, 113, 211, 137, 12, 78, 211, 137, 10, 77, 39, 42, 80, 170, 147, 117, 79, 135, 180, 19, 27, 240, 181, 255, 144, 67, 51, 212, 61, 143, 116, 178, 78, 234, 68, 72, 46, 220, 46, 145, 193, 223, 105, 214, 219, 78, 56, 159, 14, 53, 203, 52, 78, 104, 57, 151, 216, 96, 31, 63, 14, 84, 113, 170, 64, 40, 172, 218, 126, 211, 38, 23, 165, 44, 104, 254, 100, 252, 47, 247, 45, 128, 154, 86, 236, 127, 179, 115, 207, 29, 90, 155, 230, 45, 170, 144, 62, 51, 109, 6, 17, 82, 50, 137, 148, 204, 153, 194, 57, 98, 81, 235, 144, 36, 35, 193, 238, 218, 206, 149, 129, 50, 110, 39, 77, 168, 231, 34, 209, 58, 217, 86, 11, 22, 149, 136, 189, 117, 46, 209, 161, 238, 121, 176, 102, 198, 91, 246, 67, 60, 173, 111, 2, 164, 149, 97, 222, 130, 84, 211, 6, 213, 212, 185, 196, 71, 195, 148, 115, 137, 12, 30, 241, 94, 229, 92, 98, 195, 145, 164, 237, 98, 160, 138, 155, 174, 101, 8, 224, 234, 120, 118, 196, 174, 212, 128, 154, 38, 231, 18, 27, 18, 229, 188, 56, 151, 232, 224, 239, 179, 211, 185, 196, 134, 243, 233, 26, 122, 21, 122, 230, 70, 206, 37, 66, 218, 46, 168, 85, 173, 125, 11, 114, 46, 241, 65, 130, 238, 239, 218, 127, 16, 77, 219, 148, 253, 147, 166, 77, 91, 95, 165, 221, 58, 105, 245, 7, 146, 182, 11, 181, 54, 13, 39, 83, 39, 185, 68, 115, 183, 106, 115, 66, 146, 182, 11, 194, 73, 159, 11, 146, 180, 93, 146, 182, 235, 225, 205, 221, 218, 192, 188, 133, 159, 219, 161, 166, 237, 55, 59, 109, 164, 109, 155, 166, 249, 169, 34, 212, 61, 63, 128, 39, 101, 163, 11, 81, 120, 49, 104, 129, 87, 215, 118, 232, 155, 238, 218, 213, 240, 190, 222, 234, 218, 46, 64, 20, 234, 164, 76, 42, 121, 185, 135, 38, 222, 153, 23, 8, 175, 197, 46, 209, 161, 185, 68, 5, 154, 94, 250, 186, 68, 70, 98, 181, 46, 145, 65, 157, 148, 105, 93, 34, 67, 127, 21, 183, 227, 18, 27, 21, 117, 137, 14, 9, 20, 115, 188, 123, 58, 81, 184, 59, 145, 58, 41, 93, 34, 196, 37, 6, 192, 223, 245, 209, 115, 226, 196, 55, 251, 211, 18, 23, 78, 250, 100, 79, 75, 100, 172, 165, 142, 163, 167, 37, 66, 168, 111, 75, 124, 248, 51, 34, 232, 245, 45, 145, 65, 215, 46, 204, 30, 142, 59, 161, 134, 109, 131, 71, 158, 84, 72, 125, 75, 124, 160, 190, 37, 42, 232, 155, 160, 214, 172, 171, 66, 223, 18, 29, 24, 131, 123, 21, 88, 111, 173, 170, 61, 135, 79, 169, 141, 248, 23, 123, 7, 241, 67, 88, 235, 235, 122, 222, 157, 230, 113, 59, 175, 146, 237, 119, 137, 87, 215, 118, 84, 180, 209, 214, 102, 248, 55, 101, 77, 54, 227, 174, 169, 233, 155, 160, 142, 212, 101, 114, 93, 192, 86, 49, 90, 62, 169, 225, 205, 139, 83, 243, 226, 228, 80, 139, 245, 173, 134, 36, 229, 162, 240, 90, 250, 52, 232, 175, 67, 81, 218, 142, 252, 166, 77, 50, 135, 166, 218, 13, 164, 229, 147, 244, 61, 36, 41, 215, 210, 44, 104, 189, 37, 66, 214, 91, 162, 66, 171, 183, 196, 5, 101, 217, 12, 106, 188, 14, 248, 145, 146, 112, 79, 16, 240, 209, 182, 225, 133, 86, 110, 98, 71, 149, 5, 170, 56, 245, 4, 84, 105, 73, 90, 247, 185, 24, 84, 145, 136, 224, 105, 223, 211, 254, 67, 237, 123, 40, 66, 67, 235, 123, 242, 163, 8, 18, 79, 134, 190, 162, 143, 34, 72, 168, 116, 218, 239, 179, 53, 235, 40, 66, 163, 176, 114, 220, 10, 125, 210, 165, 190, 191, 30, 67, 17, 28, 208, 165, 22, 181, 186, 106, 63, 59, 218, 247, 184, 209, 190, 8, 181, 221, 160, 125, 46, 251, 253, 153, 23, 180, 143, 226, 155, 54, 118, 140, 19, 82, 237, 31, 128, 235, 90, 6, 1, 250, 185, 188, 101, 71, 56, 233, 241, 55, 252, 46, 231, 107, 113, 39, 7, 173, 216, 223, 150, 14, 161, 255, 184, 181, 117, 45, 89, 177, 174, 237, 18, 74, 4, 57, 72, 106, 25, 69, 112, 254, 37, 79, 184, 36, 49, 115, 136, 149, 219, 250, 201, 22, 212, 180, 85, 201, 107, 93, 162, 69, 41, 232, 105, 102, 202, 80, 123, 32, 241, 100, 13, 109, 2, 16, 44, 100, 177, 119, 38, 156, 102, 157, 8, 173, 239, 106, 59, 194, 117, 53, 204, 77, 243, 80, 214, 31, 10, 202, 149, 220, 12, 131, 214, 73, 58, 210, 181, 140, 246, 187, 46, 72, 251, 158, 75, 117, 101, 226, 197, 64, 215, 146, 206, 113, 43, 212, 52, 217, 12, 184, 174, 37, 63, 63, 149, 132, 235, 74, 152, 31, 60, 2, 225, 59, 157, 78, 213, 116, 101, 130, 200, 109, 136, 126, 178, 109, 249, 128, 86, 38, 94, 40, 2, 106, 128, 11, 71, 8, 101, 205, 78, 106, 234, 160, 70, 97, 37, 233, 100, 136, 65, 179, 147, 90, 34, 131, 42, 110, 14, 142, 16, 203, 46, 55, 129, 37, 46, 220, 4, 150, 168, 224, 17, 164, 234, 61, 228, 249, 77, 188, 116, 34, 73, 204, 34, 214, 254, 251, 231, 214, 254, 99, 160, 177, 243, 143, 107, 188, 234, 241, 74, 78, 73, 212, 170, 118, 82, 0, 127, 199, 184, 221, 3, 3, 60, 162, 138, 219, 2, 20, 144, 147, 140, 36, 192, 93, 151, 74, 117, 22, 80, 247, 188, 0, 60, 177, 66, 107, 130, 227, 178, 155, 7, 16, 192, 141, 166, 100, 90, 191, 45, 34, 192, 187, 237, 228, 57, 201, 8, 3, 206, 75, 209, 250, 255, 22, 163, 150, 91, 181, 68, 170, 233, 55, 110, 198, 193, 162, 152, 101, 123, 22, 248, 92, 142, 233, 51, 160, 85, 237, 132, 219, 33, 101, 251, 237, 53, 122, 109, 207, 106, 1, 173, 109, 243, 27, 63, 208, 197, 222, 26, 28, 160, 105, 163, 138, 31, 90, 155, 134, 151, 132, 54, 39, 180, 11, 162, 43, 19, 171, 180, 93, 3, 222, 120, 63, 211, 50, 251, 220, 30, 104, 253, 182, 14, 52, 109, 223, 217, 113, 192, 218, 80, 52, 222, 79, 237, 49, 158, 246, 159, 149, 45, 209, 211, 220, 232, 103, 90, 160, 249, 115, 61, 182, 142, 82, 123, 252, 129, 115, 137, 30, 102, 151, 99, 137, 143, 230, 79, 181, 43, 39, 104, 177, 119, 28, 75, 140, 104, 188, 222, 58, 65, 170, 165, 183, 58, 150, 24, 161, 77, 186, 43, 67, 142, 37, 62, 28, 75, 84, 160, 109, 25, 85, 76, 177, 68, 135, 71, 208, 243, 251, 77, 213, 34, 254, 109, 13, 39, 212, 184, 34, 89, 226, 2, 79, 106, 170, 154, 58, 85, 40, 209, 34, 150, 24, 177, 54, 31, 249, 147, 148, 149, 248, 240, 136, 182, 117, 43, 145, 161, 46, 29, 127, 84, 116, 57, 198, 9, 165, 227, 86, 98, 195, 202, 55, 65, 209, 59, 179, 149, 8, 81, 141, 217, 74, 100, 240, 8, 132, 103, 173, 196, 133, 182, 93, 170, 37, 99, 156, 144, 246, 91, 43, 81, 226, 43, 250, 168, 53, 187, 18, 29, 238, 140, 118, 37, 46, 90, 221, 82, 7, 81, 56, 169, 83, 61, 119, 113, 179, 18, 27, 60, 242, 48, 251, 173, 43, 209, 161, 180, 211, 113, 164, 141, 43, 86, 166, 149, 8, 33, 73, 233, 24, 155, 76, 1, 8, 34, 16, 225, 153, 108, 133, 22, 248, 39, 107, 102, 208, 36, 146, 248, 240, 230, 110, 69, 147, 72, 162, 99, 87, 66, 147, 72, 226, 226, 93, 223, 4, 109, 84, 201, 72, 34, 68, 215, 146, 223, 155, 200, 240, 8, 132, 135, 157, 196, 133, 75, 152, 157, 68, 134, 166, 18, 126, 132, 178, 185, 73, 148, 208, 201, 174, 116, 170, 18, 7, 245, 92, 208, 55, 109, 18, 29, 169, 154, 54, 137, 12, 106, 187, 65, 77, 226, 226, 249, 109, 143, 49, 78, 200, 191, 211, 73, 168, 73, 156, 144, 96, 2, 8, 80, 107, 203, 36, 54, 214, 122, 14, 210, 182, 12, 147, 248, 240, 8, 114, 77, 100, 104, 125, 247, 116, 82, 133, 92, 19, 27, 82, 123, 156, 105, 226, 162, 249, 179, 181, 105, 131, 150, 93, 223, 116, 87, 66, 76, 19, 27, 184, 173, 64, 76, 37, 46, 34, 72, 64, 173, 75, 37, 50, 90, 85, 242, 66, 46, 153, 166, 126, 236, 232, 253, 69, 84, 98, 3, 119, 109, 165, 18, 27, 88, 106, 191, 35, 165, 18, 29, 186, 158, 119, 196, 143, 42, 14, 41, 149, 248, 224, 173, 78, 250, 100, 72, 169, 68, 136, 183, 46, 236, 53, 164, 109, 23, 82, 42, 17, 162, 181, 233, 251, 46, 244, 77, 39, 138, 24, 43, 164, 84, 34, 3, 73, 74, 183, 40, 5, 45, 246, 14, 122, 117, 79, 164, 84, 162, 3, 74, 37, 42, 232, 92, 175, 105, 137, 86, 170, 68, 136, 54, 217, 252, 222, 208, 252, 153, 252, 49, 11, 163, 202, 68, 235, 187, 84, 137, 150, 42, 102, 72, 39, 20, 191, 146, 197, 252, 205, 159, 218, 9, 249, 131, 174, 37, 83, 227, 253, 148, 248, 200, 79, 137, 10, 174, 253, 62, 149, 73, 251, 249, 206, 128, 4, 109, 242, 93, 177, 243, 202, 22, 248, 55, 101, 186, 84, 63, 39, 219, 174, 1, 149, 218, 206, 209, 39, 141, 44, 60, 209, 252, 217, 233, 212, 81, 218, 86, 246, 154, 42, 110, 142, 111, 202, 90, 179, 238, 137, 86, 166, 109, 120, 57, 144, 172, 164, 160, 138, 71, 28, 242, 149, 69, 120, 227, 253, 86, 247, 100, 219, 64, 165, 58, 173, 140, 165, 195, 184, 25, 228, 13, 125, 179, 171, 104, 219, 230, 45, 135, 36, 224, 10, 241, 205, 142, 45, 74, 203, 178, 61, 138, 194, 213, 210, 101, 144, 123, 174, 36, 241, 109, 155, 93, 80, 107, 78, 137, 16, 143, 160, 166, 237, 183, 199, 139, 249, 81, 171, 227, 165, 19, 85, 232, 249, 93, 187, 172, 3, 238, 154, 82, 98, 131, 47, 90, 189, 211, 33, 109, 242, 53, 45, 23, 98, 240, 78, 83, 74, 100, 104, 20, 86, 75, 39, 186, 208, 167, 236, 247, 37, 81, 194, 249, 116, 72, 255, 45, 246, 37, 177, 193, 151, 68, 5, 107, 45, 243, 44, 118, 168, 117, 45, 238, 111, 156, 190, 121, 73, 116, 176, 216, 17, 63, 218, 37, 209, 209, 170, 56, 169, 66, 187, 36, 62, 150, 92, 18, 21, 214, 74, 166, 245, 147, 130, 82, 123, 28, 37, 66, 232, 106, 90, 110, 63, 106, 118, 65, 137, 14, 136, 10, 120, 8, 205, 58, 232, 120, 22, 164, 180, 149, 194, 14, 169, 90, 7, 31, 34, 72, 80, 207, 5, 173, 131, 141, 71, 29, 116, 60, 21, 117, 80, 65, 45, 87, 212, 65, 134, 181, 255, 26, 13, 47, 103, 22, 187, 18, 226, 138, 58, 200, 200, 165, 233, 8, 21, 117, 144, 145, 84, 69, 168, 168, 131, 139, 92, 154, 70, 252, 168, 162, 14, 62, 150, 166, 81, 69, 29, 92, 84, 212, 193, 7, 173, 40, 196, 55, 109, 88, 9, 61, 9, 183, 58, 220, 110, 1, 228, 202, 90, 29, 108, 208, 86, 7, 21, 84, 3, 31, 148, 33, 183, 212, 193, 133, 115, 71, 219, 100, 172, 220, 185, 19, 146, 120, 101, 75, 29, 100, 144, 40, 111, 224, 181, 212, 65, 137, 214, 166, 225, 21, 225, 145, 181, 82, 177, 85, 7, 33, 158, 150, 17, 210, 170, 131, 140, 103, 146, 234, 224, 130, 99, 79, 185, 84, 7, 25, 78, 193, 35, 16, 24, 115, 55, 52, 3, 245, 76, 8, 31, 234, 96, 0, 34, 11, 35, 215, 181, 14, 240, 251, 58, 168, 149, 131, 16, 143, 74, 148, 131, 11, 190, 56, 168, 64, 66, 73, 7, 21, 180, 173, 183, 78, 210, 193, 198, 167, 131, 10, 86, 63, 79, 39, 251, 121, 58, 232, 120, 79, 7, 29, 19, 239, 169, 24, 226, 95, 236, 29, 196, 31, 65, 201, 110, 144, 180, 93, 72, 194, 232, 81, 188, 32, 223, 7, 207, 167, 195, 64, 169, 82, 186, 48, 69, 158, 14, 74, 52, 109, 219, 241, 116, 144, 225, 146, 45, 28, 223, 166, 237, 163, 241, 50, 248, 93, 56, 85, 168, 153, 65, 142, 167, 131, 144, 182, 139, 167, 131, 12, 173, 243, 209, 183, 114, 210, 80, 197, 105, 219, 107, 242, 249, 43, 168, 201, 71, 252, 200, 211, 193, 137, 10, 242, 116, 144, 194, 169, 42, 212, 40, 140, 60, 29, 132, 60, 171, 49, 242, 9, 59, 200, 211, 193, 198, 99, 225, 213, 52, 59, 52, 185, 24, 79, 7, 37, 220, 211, 193, 135, 75, 152, 29, 148, 218, 227, 15, 235, 45, 145, 167, 131, 12, 79, 7, 21, 124, 210, 197, 104, 163, 167, 49, 235, 116, 240, 129, 226, 211, 81, 157, 14, 46, 180, 145, 234, 116, 208, 33, 29, 108, 232, 116, 208, 33, 73, 233, 30, 231, 210, 193, 6, 47, 4, 118, 46, 29, 100, 64, 22, 52, 149, 240, 163, 214, 78, 93, 214, 165, 131, 12, 214, 165, 131, 10, 30, 81, 197, 13, 57, 93, 75, 126, 99, 95, 251, 15, 226, 93, 35, 15, 46, 161, 44, 155, 121, 192, 59, 23, 71, 211, 214, 121, 132, 16, 127, 203, 85, 224, 89, 58, 216, 104, 218, 170, 78, 199, 209, 179, 116, 176, 193, 35, 173, 140, 165, 131, 12, 143, 44, 150, 14, 50, 120, 107, 211, 111, 97, 233, 160, 195, 35, 72, 130, 131, 11, 18, 28, 12, 128, 171, 123, 26, 138, 42, 17, 28, 132, 68, 224, 8, 14, 42, 180, 58, 183, 21, 13, 46, 182, 162, 193, 134, 46, 79, 50, 228, 182, 162, 189, 6, 33, 13, 175, 133, 243, 203, 92, 127, 219, 57, 4, 183, 21, 155, 148, 253, 231, 138, 28, 34, 48, 12, 83, 34, 48, 255, 200, 173, 231, 175, 239, 242, 25, 218, 252, 222, 76, 188, 153, 248, 55, 102, 154, 137, 71, 240, 198, 105, 23, 48, 255, 17, 154, 9, 37, 157, 86, 172, 55, 214, 82, 199, 191, 237, 66, 238, 233, 238, 233, 175, 31, 205, 115, 220, 220, 212, 113, 91, 214, 202, 24, 226, 161, 42, 35, 13, 234, 60, 90, 159, 153, 129, 232, 90, 166, 194, 32, 191, 225, 192, 45, 155, 123, 91, 230, 238, 158, 208, 103, 106, 252, 160, 141, 151, 225, 208, 82, 151, 58, 115, 132, 210, 166, 165, 3, 81, 75, 151, 92, 28, 78, 73, 228, 214, 107, 240, 193, 173, 215, 160, 194, 187, 237, 164, 246, 53, 248, 224, 174, 175, 65, 72, 83, 9, 127, 131, 12, 206, 248, 138, 190, 68, 91, 214, 76, 252, 219, 46, 93, 207, 63, 52, 74, 58, 95, 75, 29, 127, 180, 215, 13, 15, 179, 207, 16, 109, 90, 68, 43, 69, 155, 147, 152, 245, 42, 9, 91, 150, 65, 195, 11, 61, 41, 27, 93, 201, 58, 37, 143, 196, 202, 35, 73, 43, 234, 152, 52, 117, 210, 39, 165, 121, 238, 233, 237, 77, 152, 112, 215, 229, 145, 136, 111, 235, 120, 164, 45, 211, 38, 217, 46, 91, 236, 23, 120, 231, 226, 160, 45, 115, 127, 28, 39, 244, 153, 26, 5, 131, 150, 186, 18, 90, 182, 127, 162, 113, 218, 149, 26, 15, 179, 16, 75, 151, 92, 30, 18, 254, 237, 117, 131, 13, 22, 165, 184, 212, 48, 242, 83, 229, 43, 67, 201, 117, 131, 20, 201, 117, 131, 10, 15, 195, 162, 112, 55, 184, 160, 79, 223, 4, 164, 180, 193, 155, 78, 228, 30, 119, 131, 14, 103, 144, 132, 36, 241, 106, 170, 221, 96, 67, 155, 252, 166, 169, 213, 161, 39, 63, 39, 140, 138, 157, 112, 167, 100, 16, 26, 156, 130, 214, 132, 214, 38, 31, 158, 60, 40, 117, 224, 65, 91, 70, 149, 106, 104, 26, 47, 247, 104, 183, 170, 82, 6, 53, 32, 105, 102, 144, 47, 230, 119, 221, 95, 230, 71, 107, 2, 3, 73, 122, 203, 161, 38, 31, 173, 9, 77, 254, 163, 245, 219, 174, 9, 223, 244, 245, 147, 171, 33, 23, 2, 188, 241, 46, 143, 129, 86, 102, 218, 34, 245, 78, 183, 128, 111, 91, 214, 180, 117, 46, 153, 251, 62, 32, 128, 2, 56, 120, 84, 210, 32, 21, 71, 126, 35, 15, 253, 84, 136, 125, 33, 220, 191, 83, 217, 165, 78, 131, 43, 132, 171, 165, 203, 72, 16, 64, 39, 251, 73, 55, 251, 189, 113, 3, 237, 1, 7, 245, 53, 65, 59, 39, 90, 157, 127, 14, 180, 213, 161, 60, 45, 17, 196, 154, 176, 120, 26, 94, 236, 88, 118, 49, 214, 132, 117, 144, 174, 125, 104, 253, 223, 229, 28, 41, 211, 136, 197, 156, 28, 151, 221, 224, 2, 143, 184, 186, 236, 6, 25, 201, 18, 212, 248, 150, 221, 224, 130, 227, 119, 149, 221, 32, 163, 105, 171, 171, 178, 27, 124, 88, 143, 209, 148, 221, 32, 195, 215, 229, 171, 58, 206, 207, 237, 36, 90, 93, 118, 114, 244, 228, 114, 159, 82, 59, 162, 194, 139, 75, 182, 46, 145, 187, 100, 46, 217, 99, 217, 181, 160, 146, 141, 195, 191, 49, 51, 193, 192, 5, 80, 113, 102, 160, 242, 218, 46, 7, 124, 129, 156, 100, 4, 53, 208, 186, 214, 57, 175, 212, 224, 233, 236, 151, 72, 87, 51, 161, 21, 170, 111, 130, 55, 232, 98, 111, 156, 144, 98, 110, 16, 162, 152, 27, 84, 88, 142, 28, 212, 33, 215, 149, 21, 141, 27, 124, 244, 91, 13, 42, 180, 215, 232, 91, 13, 46, 220, 169, 66, 144, 135, 217, 229, 171, 193, 69, 83, 9, 127, 3, 134, 209, 63, 142, 80, 95, 110, 14, 215, 182, 140, 42, 52, 161, 190, 220, 80, 203, 109, 20, 238, 5, 173, 252, 148, 205, 160, 229, 171, 65, 6, 154, 134, 87, 131, 11, 190, 80, 209, 230, 133, 182, 28, 121, 22, 134, 214, 138, 103, 157, 119, 120, 98, 133, 124, 161, 21, 187, 178, 105, 104, 105, 107, 239, 39, 86, 58, 48, 86, 30, 65, 43, 189, 87, 161, 149, 189, 10, 173, 84, 207, 37, 194, 194, 171, 193, 134, 227, 185, 28, 45, 188, 26, 7, 228, 17, 177, 86, 162, 133, 87, 131, 140, 133, 87, 131, 10, 254, 184, 209, 204, 114, 53, 200, 192, 114, 53, 168, 16, 33, 233, 4, 173, 6, 23, 107, 255, 105, 191, 175, 232, 87, 244, 83, 50, 253, 124, 121, 207, 82, 251, 65, 63, 159, 161, 36, 168, 8, 174, 146, 117, 142, 86, 131, 15, 136, 10, 47, 104, 53, 216, 160, 36, 180, 254, 103, 114, 180, 26, 132, 124, 190, 9, 104, 53, 184, 80, 17, 212, 227, 149, 190, 45, 59, 180, 26, 124, 80, 18, 158, 151, 123, 94, 206, 113, 173, 108, 55, 34, 144, 16, 225, 147, 161, 213, 224, 98, 87, 4, 151, 236, 83, 29, 180, 26, 132, 180, 174, 117, 13, 58, 224, 133, 65, 171, 65, 6, 37, 97, 53, 232, 80, 17, 250, 241, 67, 148, 4, 109, 156, 208, 106, 176, 161, 36, 184, 86, 18, 173, 6, 27, 171, 193, 0, 172, 126, 191, 13, 50, 52, 188, 126, 155, 77, 200, 1, 25, 152, 105, 251, 219, 224, 162, 17, 130, 130, 157, 54, 114, 224, 183, 233, 231, 158, 16, 55, 51, 161, 214, 190, 5, 178, 46, 145, 3, 25, 26, 133, 213, 111, 131, 11, 230, 229, 183, 213, 95, 171, 63, 165, 141, 146, 78, 2, 130, 116, 24, 222, 228, 163, 245, 157, 48, 0, 161, 44, 27, 51, 109, 17, 187, 134, 213, 15, 181, 109, 182, 25, 233, 51, 91, 160, 109, 23, 122, 137, 67, 42, 181, 35, 252, 151, 49, 78, 136, 146, 173, 222, 13, 109, 164, 116, 27, 164, 120, 223, 37, 73, 137, 126, 27, 116, 208, 229, 183, 65, 72, 5, 131, 243, 233, 208, 111, 131, 15, 46, 155, 194, 108, 131, 11, 143, 160, 79, 143, 217, 6, 27, 186, 210, 58, 179, 13, 50, 152, 109, 240, 209, 208, 120, 127, 91, 179, 232, 157, 157, 6, 29, 190, 237, 66, 144, 182, 146, 86, 167, 193, 133, 243, 47, 157, 6, 25, 92, 155, 116, 200, 105, 144, 241, 220, 32, 167, 65, 6, 7, 125, 19, 180, 160, 25, 125, 69, 255, 181, 193, 136, 215, 6, 27, 206, 175, 13, 46, 164, 100, 175, 13, 50, 168, 69, 186, 188, 127, 252, 248, 107, 131, 19, 254, 218, 160, 131, 131, 214, 101, 155, 60, 93, 254, 86, 191, 237, 183, 240, 166, 19, 49, 117, 30, 61, 227, 102, 208, 226, 133, 61, 4, 104, 171, 190, 145, 218, 209, 107, 131, 139, 215, 6, 35, 42, 203, 104, 59, 199, 55, 59, 118, 218, 168, 145, 64, 124, 105, 101, 157, 101, 59, 152, 105, 139, 94, 27, 132, 240, 231, 91, 93, 43, 123, 77, 81, 163, 48, 122, 109, 208, 0, 236, 180, 185, 49, 67, 175, 13, 46, 208, 6, 25, 191, 12, 105, 189, 109, 127, 91, 231, 211, 33, 213, 94, 126, 42, 164, 13, 54, 228, 167, 66, 238, 144, 199, 87, 60, 39, 77, 98, 173, 76, 232, 181, 193, 7, 215, 6, 29, 48, 204, 62, 107, 131, 11, 215, 148, 204, 43, 99, 109, 112, 98, 217, 197, 218, 32, 195, 79, 28, 170, 45, 69, 219, 161, 61, 60, 191, 16, 169, 157, 136, 181, 193, 133, 254, 46, 228, 150, 181, 65, 7, 243, 22, 214, 6, 25, 42, 30, 177, 54, 200, 144, 218, 227, 77, 27, 92, 52, 109, 240, 209, 30, 211, 180, 65, 134, 197, 236, 32, 105, 27, 244, 115, 125, 46, 215, 186, 150, 151, 230, 5, 60, 124, 229, 253, 121, 225, 150, 80, 205, 20, 247, 116, 207, 59, 96, 143, 52, 128, 17, 27, 97, 166, 13, 50, 180, 78, 182, 21, 75, 146, 54, 248, 88, 146, 61, 215, 70, 158, 26, 164, 112, 77, 13, 58, 90, 174, 251, 203, 6, 25, 218, 188, 108, 112, 33, 194, 4, 45, 64, 130, 67, 124, 229, 145, 63, 47, 236, 160, 85, 210, 76, 209, 180, 147, 86, 247, 164, 77, 63, 199, 224, 65, 187, 35, 255, 166, 141, 23, 2, 92, 29, 207, 229, 217, 143, 52, 53, 51, 7, 160, 240, 9, 92, 81, 1, 60, 119, 37, 3, 170, 184, 77, 236, 74, 232, 89, 252, 179, 65, 71, 131, 13, 201, 88, 54, 184, 224, 239, 83, 174, 108, 144, 161, 109, 151, 186, 117, 168, 65, 0, 65, 131, 13, 254, 144, 225, 104, 81, 131, 11, 13, 2, 0, 65, 158, 138, 138, 199, 224, 194, 90, 185, 84, 84, 60, 6, 25, 254, 150, 67, 15, 109, 91, 173, 172, 168, 120, 12, 58, 24, 37, 113, 80, 91, 254, 199, 224, 3, 25, 182, 31, 131, 10, 79, 133, 219, 37, 107, 117, 237, 49, 216, 224, 147, 116, 144, 83, 168, 246, 35, 165, 141, 86, 62, 80, 150, 13, 130, 178, 108, 11, 28, 243, 82, 109, 23, 132, 13, 202, 178, 173, 245, 24, 132, 188, 180, 104, 173, 199, 32, 195, 221, 51, 168, 224, 158, 193, 9, 247, 12, 42, 104, 75, 113, 6, 23, 152, 51, 8, 105, 170, 157, 156, 193, 69, 4, 103, 80, 129, 243, 233, 86, 106, 67, 112, 6, 31, 16, 156, 193, 70, 246, 87, 20, 130, 51, 200, 80, 95, 166, 191, 77, 65, 206, 32, 133, 99, 250, 200, 25, 92, 184, 51, 216, 96, 208, 225, 111, 185, 197, 163, 146, 198, 174, 87, 73, 24, 114, 6, 33, 90, 155, 86, 134, 156, 193, 134, 36, 165, 123, 141, 32, 103, 208, 1, 145, 253, 140, 212, 30, 79, 153, 28, 69, 232, 90, 6, 61, 14, 57, 131, 16, 175, 88, 139, 59, 57, 82, 77, 77, 84, 211, 5, 180, 254, 39, 67, 223, 184, 25, 132, 60, 34, 48, 110, 6, 23, 24, 55, 131, 10, 207, 226, 219, 12, 50, 56, 174, 205, 36, 114, 248, 102, 167, 218, 12, 50, 180, 25, 116, 164, 150, 45, 105, 166, 60, 164, 246, 120, 3, 117, 131, 234, 124, 141, 178, 108, 6, 29, 30, 129, 208, 12, 62, 82, 54, 131, 10, 13, 254, 200, 49, 184, 208, 140, 84, 75, 228, 24, 108, 184, 254, 58, 132, 175, 77, 166, 17, 170, 61, 102, 112, 65, 169, 234, 164, 52, 51, 248, 224, 17, 102, 236, 67, 43, 183, 245, 147, 33, 201, 74, 72, 86, 215, 146, 106, 29, 249, 169, 84, 113, 67, 249, 169, 88, 51, 131, 13, 107, 255, 61, 90, 235, 127, 58, 244, 44, 200, 193, 147, 146, 37, 131, 90, 253, 30, 28, 150, 45, 207, 27, 240, 236, 175, 232, 4, 202, 79, 21, 177, 137, 221, 19, 177, 102, 6, 31, 60, 192, 13, 249, 43, 160, 153, 193, 71, 243, 111, 99, 246, 240, 77, 155, 68, 13, 173, 147, 69, 75, 166, 218, 227, 110, 180, 153, 129, 56, 41, 181, 153, 193, 133, 83, 85, 136, 193, 243, 173, 168, 153, 193, 8, 126, 94, 184, 209, 55, 102, 48, 226, 87, 242, 141, 25, 92, 160, 111, 204, 32, 196, 23, 51, 8, 161, 43, 95, 178, 152, 65, 198, 195, 108, 163, 85, 45, 102, 240, 225, 178, 31, 249, 75, 220, 118, 253, 91, 224, 141, 247, 29, 51, 112, 143, 187, 65, 254, 18, 51, 248, 128, 208, 24, 92, 40, 203, 134, 26, 131, 139, 202, 91, 12, 82, 84, 222, 98, 80, 65, 215, 146, 191, 77, 105, 144, 112, 199, 223, 98, 208, 193, 213, 190, 197, 32, 131, 82, 247, 188, 227, 157, 102, 151, 99, 7, 213, 114, 33, 8, 136, 171, 123, 30, 81, 120, 49, 15, 138, 182, 11, 162, 240, 98, 144, 241, 188, 24, 84, 248, 149, 44, 230, 71, 110, 23, 131, 144, 231, 87, 146, 118, 49, 200, 104, 101, 152, 231, 136, 160, 182, 155, 10, 47, 21, 93, 12, 66, 186, 117, 146, 139, 193, 69, 126, 42, 84, 209, 111, 157, 160, 137, 166, 244, 251, 253, 115, 49, 184, 240, 207, 197, 224, 67, 157, 148, 201, 145, 139, 65, 135, 207, 197, 160, 130, 55, 185, 24, 92, 112, 79, 8, 27, 214, 90, 9, 131, 11, 251, 208, 83, 81, 177, 12, 58, 68, 60, 204, 246, 251, 202, 50, 216, 136, 128, 36, 116, 45, 249, 40, 63, 21, 82, 90, 89, 6, 33, 158, 255, 101, 112, 225, 105, 117, 124, 25, 92, 188, 247, 50, 40, 193, 154, 41, 12, 95, 204, 223, 160, 45, 83, 218, 142, 111, 91, 6, 25, 17, 60, 130, 14, 74, 219, 50, 184, 208, 218, 150, 65, 238, 61, 34, 252, 174, 106, 203, 32, 67, 132, 103, 110, 73, 51, 5, 181, 206, 231, 129, 243, 75, 209, 183, 114, 226, 112, 207, 117, 45, 185, 233, 11, 184, 178, 108, 232, 93, 255, 153, 27, 158, 122, 241, 204, 141, 32, 141, 132, 209, 171, 208, 55, 237, 135, 198, 30, 65, 17, 79, 82, 208, 195, 148, 177, 131, 114, 146, 17, 135, 86, 143, 168, 168, 114, 120, 131, 235, 178, 165, 11, 84, 91, 6, 31, 170, 45, 131, 10, 206, 80, 234, 164, 199, 143, 94, 189, 1, 127, 137, 25, 8, 253, 92, 47, 173, 76, 20, 30, 193, 154, 25, 180, 178, 45, 131, 142, 200, 194, 14, 198, 10, 173, 3, 117, 104, 253, 181, 12, 66, 214, 90, 6, 173, 181, 204, 106, 187, 100, 66, 77, 219, 181, 12, 229, 83, 106, 63, 120, 68, 43, 214, 25, 175, 78, 209, 157, 104, 59, 164, 77, 178, 117, 64, 221, 243, 90, 177, 142, 148, 157, 174, 101, 176, 0, 171, 31, 114, 120, 152, 101, 217, 148, 78, 90, 189, 129, 174, 101, 112, 129, 247, 93, 12, 213, 30, 59, 30, 129, 208, 150, 211, 99, 245, 115, 62, 29, 226, 71, 219, 15, 66, 221, 243, 19, 159, 175, 21, 219, 120, 134, 147, 222, 209, 144, 135, 71, 22, 79, 103, 219, 5, 249, 54, 94, 111, 250, 14, 146, 102, 166, 249, 179, 109, 69, 42, 228, 239, 91, 121, 240, 244, 55, 133, 240, 111, 186, 150, 193, 6, 93, 203, 160, 130, 107, 191, 102, 25, 92, 188, 174, 102, 25, 100, 152, 192, 164, 2, 170, 184, 85, 192, 91, 151, 101, 208, 193, 61, 169, 228, 133, 26, 6, 29, 16, 141, 58, 198, 48, 200, 160, 159, 139, 97, 144, 97, 93, 54, 180, 12, 131, 140, 167, 95, 155, 194, 65, 127, 21, 55, 9, 73, 98, 230, 112, 141, 61, 242, 192, 7, 199, 15, 219, 217, 52, 101, 112, 241, 252, 115, 114, 228, 30, 82, 134, 148, 42, 131, 17, 74, 149, 65, 133, 116, 146, 50, 184, 48, 129, 43, 66, 9, 151, 48, 59, 200, 23, 180, 58, 159, 202, 96, 67, 175, 98, 80, 65, 49, 24, 0, 109, 188, 18, 131, 12, 191, 48, 168, 192, 33, 200, 35, 11, 131, 13, 30, 89, 24, 84, 88, 24, 108, 180, 100, 11, 131, 143, 133, 193, 0, 232, 147, 12, 58, 232, 147, 12, 42, 120, 227, 180, 147, 100, 176, 193, 132, 5, 245, 78, 183, 116, 173, 236, 116, 139, 82, 154, 182, 173, 147, 210, 52, 73, 113, 93, 203, 122, 83, 71, 15, 179, 207, 114, 218, 232, 97, 246, 97, 246, 41, 17, 101, 75, 157, 71, 241, 194, 154, 153, 214, 149, 176, 187, 54, 179, 254, 48, 251, 176, 86, 50, 253, 21, 253, 119, 93, 43, 153, 19, 224, 93, 35, 212, 129, 78, 72, 210, 174, 244, 204, 203, 51, 47, 12, 86, 97, 169, 253, 254, 249, 200, 37, 91, 249, 38, 168, 68, 186, 50, 233, 232, 125, 151, 123, 117, 164, 244, 61, 25, 202, 35, 109, 77, 189, 23, 104, 197, 50, 83, 6, 113, 199, 188, 222, 147, 153, 120, 152, 93, 75, 190, 47, 224, 164, 143, 164, 109, 101, 175, 249, 58, 44, 188, 154, 6, 51, 203, 248, 202, 55, 65, 31, 12, 179, 234, 225, 193, 196, 25, 41, 130, 132, 212, 233, 33, 73, 234, 36, 213, 118, 105, 188, 39, 131, 14, 199, 211, 97, 157, 12, 50, 52, 117, 50, 184, 208, 56, 237, 123, 126, 75, 6, 31, 239, 250, 142, 134, 87, 195, 115, 210, 50, 161, 39, 181, 100, 16, 242, 168, 175, 100, 144, 97, 41, 147, 43, 25, 92, 180, 74, 146, 193, 133, 38, 159, 177, 40, 165, 21, 73, 50, 232, 224, 15, 193, 177, 122, 27, 244, 52, 127, 74, 6, 33, 13, 105, 123, 73, 6, 25, 92, 31, 69, 5, 230, 219, 182, 243, 175, 40, 58, 148, 66, 184, 173, 248, 138, 62, 3, 146, 148, 239, 239, 24, 226, 186, 150, 105, 112, 199, 199, 194, 171, 249, 138, 226, 162, 249, 87, 185, 250, 67, 43, 163, 185, 27, 34, 194, 2, 25, 86, 78, 20, 53, 222, 135, 198, 239, 109, 41, 205, 17, 141, 119, 245, 147, 112, 215, 213, 94, 55, 112, 91, 209, 160, 180, 159, 239, 15, 179, 203, 221, 225, 161, 165, 46, 230, 165, 245, 31, 55, 242, 149, 251, 157, 174, 249, 129, 179, 236, 116, 136, 1, 141, 215, 93, 29, 173, 9, 254, 176, 5, 77, 219, 166, 225, 229, 223, 150, 157, 106, 138, 222, 119, 49, 120, 74, 164, 241, 126, 114, 132, 65, 235, 175, 101, 220, 66, 184, 255, 6, 36, 73, 157, 228, 252, 74, 117, 144, 187, 246, 242, 240, 206, 229, 193, 241, 92, 140, 166, 157, 168, 210, 127, 221, 224, 252, 75, 137, 5, 20, 184, 88, 57, 81, 212, 174, 162, 216, 120, 152, 101, 168, 251, 247, 196, 19, 226, 111, 112, 93, 203, 32, 132, 97, 88, 146, 114, 65, 43, 183, 162, 56, 225, 250, 169, 220, 131, 72, 148, 163, 214, 73, 171, 63, 146, 226, 220, 161, 177, 71, 30, 118, 37, 70, 191, 181, 52, 38, 139, 87, 119, 199, 174, 166, 173, 75, 196, 174, 162, 8, 161, 116, 129, 71, 32, 240, 74, 180, 234, 171, 115, 15, 226, 157, 102, 155, 242, 228, 114, 17, 218, 204, 30, 159, 16, 54, 58, 245, 121, 168, 90, 11, 193, 7, 108, 52, 179, 21, 197, 5, 143, 32, 102, 43, 138, 11, 95, 215, 70, 43, 138, 26, 175, 131, 99, 173, 116, 124, 1, 105, 101, 152, 183, 160, 164, 21, 197, 7, 66, 146, 81, 20, 21, 30, 149, 112, 5, 220, 68, 209, 97, 121, 237, 56, 106, 170, 221, 180, 78, 20, 31, 52, 245, 227, 71, 107, 211, 247, 93, 15, 239, 52, 185, 218, 69, 4, 126, 182, 142, 163, 215, 166, 243, 35, 150, 231, 203, 115, 16, 255, 124, 8, 199, 188, 36, 154, 92, 200, 49, 47, 254, 164, 206, 219, 34, 199, 76, 161, 191, 203, 241, 43, 105, 56, 96, 195, 210, 116, 92, 54, 210, 137, 226, 67, 39, 138, 10, 114, 162, 216, 64, 19, 69, 199, 218, 255, 109, 207, 225, 188, 98, 195, 25, 94, 215, 74, 10, 242, 200, 218, 127, 15, 201, 245, 187, 254, 183, 199, 41, 73, 236, 146, 200, 35, 14, 186, 24, 38, 41, 139, 19, 63, 183, 139, 96, 79, 91, 157, 87, 124, 168, 252, 218, 127, 168, 89, 135, 56, 175, 232, 224, 188, 98, 99, 217, 133, 156, 87, 92, 52, 240, 228, 26, 53, 246, 8, 114, 94, 113, 194, 179, 65, 206, 43, 66, 56, 233, 21, 21, 154, 124, 69, 133, 124, 69, 5, 173, 146, 149, 20, 164, 138, 248, 145, 190, 149, 19, 101, 217, 124, 31, 168, 120, 4, 169, 181, 111, 89, 224, 164, 252, 79, 180, 246, 31, 3, 139, 80, 98, 97, 80, 131, 43, 58, 152, 237, 187, 34, 131, 175, 220, 111, 234, 174, 232, 120, 93, 76, 234, 66, 219, 174, 8, 105, 157, 104, 187, 226, 194, 21, 27, 79, 235, 155, 224, 138, 139, 133, 113, 56, 72, 182, 237, 220, 185, 34, 195, 162, 176, 115, 197, 5, 195, 108, 115, 69, 6, 87, 148, 96, 174, 216, 96, 174, 168, 64, 85, 128, 225, 164, 79, 202, 132, 175, 253, 215, 192, 193, 63, 95, 29, 39, 103, 239, 129, 39, 86, 72, 215, 107, 109, 78, 222, 170, 218, 67, 74, 181, 161, 111, 172, 208, 99, 180, 216, 19, 225, 94, 225, 85, 20, 137, 135, 217, 215, 55, 81, 234, 175, 61, 78, 219, 158, 63, 42, 65, 223, 236, 168, 0, 137, 149, 79, 84, 44, 135, 88, 241, 211, 217, 203, 46, 212, 180, 125, 215, 119, 64, 153, 76, 239, 250, 18, 244, 151, 78, 22, 48, 144, 36, 117, 210, 90, 234, 252, 155, 189, 117, 97, 202, 80, 179, 15, 192, 35, 157, 141, 253, 53, 242, 72, 203, 228, 11, 120, 164, 210, 11, 244, 42, 215, 245, 58, 162, 189, 111, 66, 131, 39, 87, 147, 77, 58, 170, 60, 229, 160, 242, 148, 182, 78, 246, 179, 185, 132, 217, 121, 160, 111, 162, 82, 219, 121, 43, 163, 237, 10, 80, 177, 108, 223, 113, 235, 164, 160, 103, 53, 174, 188, 166, 138, 155, 210, 138, 253, 109, 233, 237, 33, 110, 254, 218, 73, 29, 113, 115, 224, 142, 121, 33, 110, 218, 118, 181, 69, 220, 30, 168, 116, 211, 28, 120, 86, 99, 196, 13, 165, 147, 152, 165, 147, 152, 193, 239, 122, 32, 128, 32, 81, 197, 35, 12, 82, 99, 143, 52, 109, 93, 35, 174, 17, 9, 9, 44, 35, 190, 60, 231, 183, 97, 32, 61, 250, 182, 203, 129, 79, 6, 18, 184, 250, 2, 117, 82, 54, 141, 86, 248, 85, 185, 39, 117, 173, 255, 186, 180, 92, 34, 42, 203, 240, 191, 212, 30, 211, 185, 94, 211, 18, 45, 203, 182, 134, 137, 138, 98, 13, 15, 16, 64, 21, 183, 119, 141, 48, 96, 0, 207, 93, 137, 186, 245, 159, 165, 154, 110, 51, 79, 39, 99, 207, 151, 92, 191, 13, 106, 253, 244, 234, 208, 148, 206, 17, 87, 184, 226, 152, 87, 211, 184, 223, 29, 56, 191, 148, 4, 124, 74, 237, 6, 42, 30, 65, 11, 48, 88, 166, 190, 128, 115, 217, 224, 83, 106, 11, 128, 1, 179, 43, 241, 196, 87, 180, 81, 120, 100, 97, 36, 154, 70, 35, 22, 32, 0, 245, 92, 208, 210, 150, 222, 68, 15, 60, 17, 66, 180, 87, 0, 93, 102, 149, 182, 119, 128, 182, 72, 221, 243, 12, 116, 170, 150, 239, 11, 216, 59, 200, 241, 93, 203, 115, 15, 218, 174, 136, 119, 125, 6, 94, 75, 134, 212, 129, 149, 173, 245, 43, 233, 236, 184, 29, 74, 150, 116, 0, 11, 84, 203, 94, 36, 154, 221, 53, 165, 68, 137, 149, 43, 46, 80, 199, 201, 21, 25, 30, 64, 149, 252, 246, 248, 0, 210, 246, 43, 67, 174, 40, 128, 103, 201, 181, 146, 130, 34, 154, 54, 170, 56, 185, 98, 35, 159, 76, 174, 200, 160, 138, 211, 182, 231, 224, 40, 42, 250, 185, 32, 139, 189, 243, 248, 228, 6, 169, 107, 114, 187, 51, 33, 124, 112, 210, 39, 91, 15, 85, 185, 50, 185, 34, 35, 130, 4, 228, 204, 147, 2, 137, 96, 2, 228, 220, 158, 183, 70, 48, 65, 83, 199, 18, 175, 77, 87, 4, 19, 36, 231, 116, 69, 48, 129, 187, 101, 12, 34, 152, 128, 117, 78, 176, 206, 8, 18, 80, 229, 181, 93, 16, 17, 76, 128, 148, 47, 170, 205, 72, 68, 48, 1, 235, 164, 136, 96, 2, 212, 150, 210, 169, 34, 34, 152, 128, 117, 162, 5, 106, 237, 91, 92, 113, 225, 190, 120, 97, 143, 131, 173, 227, 104, 2, 87, 100, 188, 58, 154, 128, 98, 2, 87, 124, 76, 224, 104, 2, 87, 92, 160, 9, 92, 17, 130, 38, 112, 197, 6, 182, 142, 35, 9, 92, 145, 225, 107, 89, 182, 72, 2, 87, 124, 240, 21, 69, 18, 184, 4, 87, 100, 120, 58, 39, 200, 21, 23, 58, 217, 95, 182, 237, 16, 191, 179, 117, 188, 181, 162, 11, 185, 226, 131, 227, 218, 12, 114, 69, 198, 179, 180, 194, 11, 82, 236, 188, 50, 87, 124, 224, 179, 209, 110, 154, 68, 174, 40, 225, 221, 138, 10, 46, 73, 249, 254, 144, 164, 124, 151, 200, 213, 58, 225, 6, 62, 105, 4, 33, 142, 87, 183, 34, 67, 183, 162, 2, 151, 12, 233, 202, 201, 190, 9, 138, 18, 141, 155, 116, 167, 184, 240, 136, 179, 226, 66, 171, 36, 173, 107, 169, 60, 27, 43, 18, 160, 177, 71, 16, 191, 131, 247, 195, 4, 16, 160, 6, 202, 158, 138, 166, 200, 192, 76, 219, 73, 83, 92, 60, 204, 78, 154, 78, 154, 226, 130, 105, 156, 38, 77, 113, 161, 118, 229, 162, 105, 219, 63, 105, 138, 141, 73, 83, 116, 108, 66, 147, 166, 200, 240, 48, 203, 94, 83, 92, 176, 215, 20, 21, 144, 241, 100, 119, 34, 68, 155, 223, 155, 226, 162, 25, 30, 129, 160, 189, 111, 66, 83, 116, 232, 211, 55, 161, 41, 46, 92, 178, 166, 184, 160, 188, 249, 215, 119, 130, 52, 181, 116, 120, 33, 64, 248, 218, 127, 15, 100, 112, 126, 41, 200, 85, 211, 70, 45, 87, 53, 197, 134, 111, 170, 154, 34, 3, 195, 72, 53, 197, 134, 254, 79, 164, 154, 162, 67, 43, 78, 77, 247, 185, 209, 54, 161, 174, 45, 93, 70, 34, 26, 133, 21, 106, 218, 42, 78, 77, 177, 33, 53, 197, 134, 90, 32, 3, 115, 45, 162, 87, 161, 198, 243, 142, 41, 46, 104, 115, 66, 144, 111, 117, 79, 10, 59, 166, 184, 208, 138, 245, 116, 28, 83, 108, 52, 109, 153, 58, 255, 208, 58, 105, 117, 135, 136, 198, 251, 204, 20, 23, 239, 187, 144, 63, 180, 49, 99, 217, 149, 24, 42, 218, 104, 59, 168, 231, 210, 216, 8, 51, 230, 240, 8, 99, 35, 204, 20, 33, 30, 134, 234, 164, 232, 59, 106, 253, 214, 247, 95, 166, 184, 240, 203, 20, 27, 158, 78, 247, 101, 138, 140, 165, 254, 184, 51, 161, 92, 11, 83, 116, 208, 197, 14, 107, 166, 72, 18, 179, 8, 151, 76, 146, 74, 241, 126, 158, 206, 235, 59, 122, 215, 247, 76, 218, 11, 50, 130, 154, 137, 55, 232, 122, 222, 87, 63, 228, 64, 83, 63, 246, 198, 109, 61, 2, 181, 254, 227, 254, 199, 253, 32, 63, 21, 154, 64, 2, 16, 36, 166, 155, 201, 158, 248, 109, 111, 2, 106, 165, 176, 107, 117, 142, 29, 228, 73, 149, 146, 117, 186, 126, 31, 105, 147, 14, 130, 157, 177, 114, 160, 148, 101, 99, 160, 233, 242, 36, 243, 214, 138, 85, 201, 171, 65, 227, 253, 148, 72, 37, 60, 105, 187, 190, 45, 211, 150, 162, 59, 65, 109, 181, 27, 224, 71, 140, 182, 243, 218, 56, 233, 31, 222, 228, 98, 208, 4, 203, 138, 186, 182, 109, 154, 54, 120, 227, 245, 85, 175, 14, 238, 16, 249, 169, 156, 227, 73, 141, 35, 24, 187, 167, 86, 101, 217, 144, 218, 149, 19, 102, 218, 183, 93, 200, 87, 194, 217, 58, 238, 240, 219, 222, 4, 180, 43, 61, 173, 237, 208, 174, 228, 128, 123, 122, 103, 135, 118, 37, 231, 211, 81, 236, 242, 87, 119, 182, 142, 163, 93, 73, 155, 236, 101, 104, 33, 118, 165, 2, 80, 120, 49, 146, 102, 10, 146, 224, 15, 212, 218, 183, 60, 188, 32, 26, 208, 6, 169, 226, 137, 72, 66, 171, 214, 62, 7, 252, 1, 180, 12, 154, 116, 232, 113, 144, 214, 61, 87, 106, 32, 128, 135, 180, 158, 11, 5, 255, 139, 104, 118, 129, 75, 36, 158, 180, 69, 175, 146, 148, 189, 160, 214, 133, 41, 42, 64, 2, 204, 63, 136, 75, 47, 65, 145, 108, 39, 144, 71, 28, 154, 241, 33, 215, 159, 165, 200, 192, 150, 162, 196, 187, 62, 98, 75, 25, 60, 59, 136, 45, 197, 5, 91, 138, 142, 180, 207, 208, 138, 117, 183, 107, 41, 54, 60, 34, 89, 138, 11, 44, 19, 98, 150, 226, 66, 181, 255, 161, 244, 247, 165, 222, 132, 158, 206, 2, 236, 155, 40, 69, 197, 122, 3, 7, 127, 203, 65, 92, 157, 244, 201, 30, 28, 184, 231, 74, 8, 27, 90, 91, 91, 231, 232, 91, 54, 186, 20, 31, 40, 19, 250, 249, 142, 40, 172, 218, 82, 216, 91, 254, 142, 208, 138, 237, 124, 110, 19, 85, 15, 75, 83, 74, 228, 13, 90, 238, 162, 170, 49, 99, 156, 208, 3, 39, 189, 50, 14, 17, 235, 209, 186, 22, 3, 175, 41, 67, 11, 26, 247, 39, 93, 138, 12, 199, 158, 122, 168, 226, 6, 193, 92, 34, 108, 80, 225, 133, 37, 18, 197, 134, 86, 149, 236, 248, 182, 11, 121, 68, 145, 81, 209, 70, 27, 169, 122, 43, 181, 69, 30, 81, 140, 240, 136, 162, 130, 103, 133, 84, 190, 171, 165, 17, 70, 181, 157, 172, 226, 132, 4, 10, 64, 0, 129, 91, 69, 136, 86, 39, 125, 82, 80, 211, 40, 106, 26, 69, 198, 55, 164, 253, 124, 119, 93, 79, 53, 138, 144, 166, 173, 243, 233, 26, 127, 103, 131, 36, 49, 107, 254, 124, 110, 135, 166, 209, 166, 81, 124, 208, 102, 60, 54, 161, 166, 81, 100, 104, 187, 160, 166, 81, 92, 124, 227, 102, 20, 25, 154, 63, 41, 195, 40, 50, 26, 94, 72, 21, 25, 222, 123, 25, 164, 138, 140, 214, 253, 215, 118, 136, 31, 57, 64, 136, 55, 222, 95, 165, 200, 104, 218, 42, 197, 199, 251, 46, 244, 164, 166, 41, 35, 42, 17, 63, 114, 109, 148, 98, 131, 86, 39, 181, 151, 11, 105, 163, 20, 27, 116, 210, 47, 69, 80, 138, 12, 157, 244, 75, 138, 12, 174, 75, 146, 148, 157, 164, 200, 96, 6, 41, 46, 218, 226, 17, 148, 146, 34, 131, 105, 139, 226, 99, 177, 69, 209, 177, 216, 162, 168, 240, 84, 164, 226, 67, 21, 123, 60, 245, 29, 146, 148, 239, 46, 217, 35, 30, 79, 117, 142, 214, 199, 83, 177, 193, 121, 181, 148, 150, 235, 249, 158, 138, 14, 238, 9, 121, 211, 214, 83, 241, 241, 77, 27, 167, 162, 195, 191, 212, 169, 200, 208, 78, 74, 197, 5, 119, 220, 14, 57, 41, 21, 29, 234, 220, 168, 240, 178, 152, 31, 226, 157, 29, 137, 149, 137, 23, 90, 20, 55, 80, 247, 148, 42, 148, 150, 101, 123, 20, 58, 232, 239, 202, 246, 40, 100, 180, 50, 182, 40, 39, 10, 27, 109, 149, 74, 218, 174, 7, 71, 30, 121, 127, 10, 29, 188, 209, 120, 63, 251, 41, 100, 180, 74, 146, 58, 105, 1, 15, 179, 18, 34, 52, 90, 25, 168, 162, 203, 61, 133, 20, 238, 41, 84, 32, 73, 79, 161, 130, 186, 231, 21, 46, 32, 127, 133, 13, 175, 174, 80, 65, 33, 67, 27, 87, 56, 110, 133, 12, 143, 64, 192, 134, 187, 174, 103, 182, 21, 54, 144, 174, 181, 148, 108, 133, 14, 38, 142, 40, 217, 10, 27, 180, 105, 217, 10, 25, 90, 37, 41, 251, 81, 147, 173, 16, 210, 240, 98, 120, 228, 193, 35, 16, 90, 29, 200, 89, 3, 182, 139, 189, 131, 190, 217, 85, 148, 194, 35, 16, 76, 182, 194, 6, 37, 129, 83, 182, 194, 5, 83, 78, 161, 194, 51, 235, 120, 195, 63, 223, 145, 74, 94, 207, 220, 72, 85, 118, 211, 40, 172, 208, 225, 201, 238, 244, 223, 5, 233, 90, 30, 43, 124, 232, 254, 243, 239, 114, 254, 172, 240, 241, 219, 60, 43, 132, 40, 147, 254, 120, 80, 45, 93, 69, 27, 118, 37, 228, 64, 6, 159, 128, 158, 21, 50, 232, 90, 242, 31, 99, 139, 28, 183, 132, 227, 118, 254, 157, 174, 209, 216, 35, 172, 144, 241, 172, 80, 129, 50, 78, 75, 41, 104, 201, 197, 188, 48, 43, 140, 104, 253, 77, 10, 99, 165, 82, 155, 177, 66, 198, 195, 108, 63, 199, 88, 97, 131, 219, 50, 86, 200, 208, 140, 157, 166, 138, 50, 86, 8, 81, 247, 60, 99, 133, 14, 18, 207, 127, 160, 131, 4, 140, 21, 42, 68, 192, 88, 161, 130, 175, 126, 136, 177, 66, 134, 54, 201, 88, 33, 228, 209, 90, 177, 43, 183, 213, 17, 99, 133, 14, 218, 114, 185, 198, 178, 139, 177, 194, 7, 247, 5, 99, 133, 13, 201, 249, 31, 68, 107, 118, 37, 98, 172, 176, 161, 254, 150, 21, 50, 172, 203, 134, 21, 50, 248, 218, 127, 14, 109, 78, 168, 34, 177, 194, 70, 69, 98, 133, 10, 93, 73, 172, 176, 161, 146, 88, 161, 66, 107, 214, 21, 181, 102, 253, 37, 86, 232, 208, 120, 63, 27, 190, 41, 123, 137, 21, 62, 112, 117, 207, 39, 86, 200, 144, 52, 51, 232, 233, 108, 118, 76, 31, 105, 219, 254, 196, 10, 27, 36, 254, 196, 10, 25, 118, 37, 244, 48, 251, 240, 8, 4, 39, 117, 170, 39, 86, 8, 121, 199, 137, 21, 50, 168, 226, 6, 83, 197, 12, 178, 150, 145, 80, 103, 149, 36, 86, 232, 160, 144, 241, 149, 71, 222, 20, 46, 94, 37, 137, 29, 121, 83, 232, 112, 210, 43, 163, 237, 144, 55, 133, 20, 221, 58, 97, 228, 77, 33, 3, 213, 84, 146, 114, 65, 251, 235, 49, 213, 20, 50, 20, 83, 168, 96, 178, 20, 42, 104, 109, 205, 58, 98, 43, 81, 248, 152, 68, 20, 42, 72, 237, 113, 165, 77, 235, 100, 21, 50, 214, 37, 170, 108, 127, 50, 180, 10, 39, 124, 109, 50, 69, 186, 150, 68, 7, 249, 186, 32, 143, 64, 120, 88, 151, 72, 53, 39, 95, 133, 17, 155, 78, 66, 207, 247, 42, 116, 60, 15, 52, 35, 131, 31, 245, 42, 108, 232, 85, 248, 224, 71, 159, 75, 211, 86, 161, 132, 71, 32, 112, 107, 91, 133, 141, 119, 109, 86, 97, 67, 106, 143, 89, 245, 26, 203, 190, 46, 134, 89, 133, 139, 150, 108, 45, 117, 144, 46, 195, 172, 66, 135, 181, 36, 195, 172, 194, 134, 197, 222, 137, 192, 48, 171, 176, 225, 164, 79, 135, 24, 102, 21, 66, 48, 204, 42, 84, 104, 169, 236, 241, 163, 86, 161, 99, 213, 42, 71, 195, 43, 87, 225, 67, 211, 246, 87, 41, 250, 164, 189, 10, 61, 240, 8, 98, 217, 77, 174, 66, 135, 63, 150, 182, 150, 78, 66, 40, 225, 146, 33, 7, 119, 228, 42, 108, 172, 194, 0, 176, 236, 70, 225, 66, 50, 10, 21, 232, 86, 84, 225, 2, 50, 60, 169, 115, 136, 71, 144, 166, 157, 168, 66, 137, 70, 97, 69, 97, 103, 194, 211, 254, 130, 181, 246, 77, 64, 202, 180, 255, 191, 101, 131, 38, 170, 16, 66, 157, 148, 201, 35, 90, 58, 122, 124, 107, 154, 168, 194, 133, 190, 149, 19, 85, 184, 88, 235, 53, 52, 81, 133, 12, 183, 104, 162, 10, 25, 42, 252, 170, 112, 129, 98, 2, 87, 164, 147, 109, 112, 210, 227, 159, 248, 245, 240, 205, 110, 65, 90, 118, 61, 154, 182, 173, 85, 242, 13, 165, 107, 229, 123, 91, 136, 231, 26, 135, 174, 101, 30, 20, 94, 76, 235, 132, 27, 36, 125, 85, 232, 176, 54, 95, 21, 58, 88, 242, 85, 225, 66, 191, 171, 114, 229, 198, 7, 30, 129, 224, 170, 144, 65, 21, 54, 86, 190, 9, 170, 144, 225, 155, 29, 185, 115, 167, 10, 29, 141, 247, 65, 209, 157, 188, 239, 66, 58, 217, 207, 54, 0, 110, 135, 40, 148, 126, 38, 109, 167, 10, 29, 170, 253, 15, 142, 203, 110, 246, 181, 196, 131, 16, 202, 169, 114, 170, 144, 225, 84, 97, 163, 105, 139, 158, 95, 111, 170, 80, 226, 97, 86, 21, 55, 85, 216, 208, 135, 95, 182, 75, 21, 58, 120, 67, 63, 27, 93, 170, 144, 161, 159, 235, 33, 197, 174, 132, 150, 42, 92, 44, 85, 168, 96, 225, 213, 168, 66, 134, 230, 207, 214, 166, 141, 42, 124, 60, 70, 21, 42, 56, 163, 234, 115, 49, 170, 208, 241, 48, 140, 42, 92, 104, 20, 86, 74, 187, 85, 155, 193, 143, 90, 25, 85, 8, 241, 77, 155, 68, 173, 254, 212, 190, 183, 216, 181, 125, 129, 39, 41, 250, 217, 60, 168, 69, 40, 161, 77, 43, 107, 56, 158, 234, 211, 246, 71, 124, 179, 183, 250, 179, 178, 37, 82, 199, 201, 39, 252, 81, 9, 82, 150, 77, 27, 226, 143, 74, 144, 75, 214, 224, 146, 249, 46, 62, 109, 191, 35, 137, 9, 92, 145, 106, 51, 170, 233, 87, 30, 173, 170, 25, 61, 45, 81, 107, 0, 177, 242, 57, 214, 4, 101, 48, 204, 42, 228, 84, 21, 54, 220, 86, 56, 184, 173, 104, 175, 162, 129, 136, 80, 177, 68, 116, 190, 131, 42, 108, 108, 63, 10, 134, 217, 127, 168, 226, 164, 10, 7, 224, 17, 8, 138, 142, 213, 64, 144, 36, 85, 184, 120, 167, 217, 133, 32, 60, 2, 97, 37, 219, 164, 10, 29, 254, 168, 4, 61, 168, 82, 133, 139, 134, 23, 195, 31, 175, 183, 36, 131, 28, 154, 183, 168, 194, 5, 132, 72, 237, 241, 84, 133, 11, 151, 170, 80, 65, 210, 118, 169, 84, 133, 140, 111, 170, 218, 34, 85, 216, 104, 222, 162, 80, 65, 105, 167, 227, 17, 141, 107, 88, 19, 252, 97, 72, 23, 133, 16, 190, 40, 108, 40, 173, 120, 82, 33, 67, 250, 7, 225, 145, 39, 21, 46, 248, 218, 127, 74, 251, 33, 103, 137, 63, 21, 58, 224, 17, 253, 84, 200, 224, 17, 8, 250, 169, 28, 202, 246, 27, 20, 110, 12, 127, 82, 237, 98, 71, 189, 211, 121, 99, 34, 148, 182, 190, 167, 62, 212, 181, 173, 147, 32, 74, 125, 39, 218, 104, 87, 54, 77, 23, 138, 180, 175, 204, 249, 237, 7, 253, 84, 216, 176, 253, 242, 83, 225, 34, 2, 95, 208, 39, 93, 234, 11, 90, 85, 179, 54, 133, 251, 129, 123, 220, 13, 133, 91, 53, 16, 129, 47, 200, 1, 157, 236, 3, 173, 42, 213, 81, 90, 89, 230, 85, 196, 171, 8, 22, 240, 5, 61, 240, 198, 255, 143, 65, 4, 18, 190, 160, 215, 6, 33, 2, 95, 16, 3, 107, 45, 195, 255, 18, 53, 120, 109, 16, 28, 92, 178, 252, 84, 200, 240, 180, 253, 202, 80, 195, 202, 35, 8, 35, 124, 245, 107, 104, 243, 187, 99, 93, 162, 252, 84, 248, 176, 175, 129, 240, 169, 112, 241, 60, 21, 42, 120, 196, 241, 84, 200, 240, 48, 235, 120, 42, 92, 104, 197, 182, 58, 158, 10, 27, 239, 137, 28, 79, 133, 140, 78, 133, 13, 111, 90, 210, 82, 33, 163, 181, 105, 169, 144, 161, 173, 190, 227, 77, 39, 186, 208, 203, 86, 150, 10, 39, 60, 178, 144, 160, 16, 67, 187, 73, 133, 11, 46, 25, 106, 82, 225, 34, 130, 9, 20, 42, 88, 110, 220, 60, 65, 18, 20, 58, 28, 67, 51, 28, 148, 237, 187, 116, 210, 62, 146, 208, 149, 201, 145, 4, 133, 140, 71, 37, 45, 221, 245, 77, 112, 52, 218, 72, 130, 194, 134, 134, 62, 125, 19, 84, 251, 85, 190, 9, 72, 130, 194, 71, 227, 230, 89, 236, 144, 4, 133, 14, 231, 211, 125, 46, 118, 18, 148, 163, 237, 82, 81, 7, 73, 80, 216, 248, 100, 13, 77, 205, 78, 122, 101, 144, 4, 133, 12, 245, 84, 36, 65, 225, 98, 226, 206, 247, 201, 246, 114, 169, 66, 18, 20, 62, 250, 241, 55, 135, 55, 222, 79, 26, 225, 118, 72, 127, 157, 29, 115, 208, 188, 24, 73, 80, 216, 160, 154, 157, 132, 36, 40, 100, 248, 86, 18, 73, 80, 184, 208, 181, 36, 170, 72, 248, 80, 145, 240, 129, 186, 76, 206, 255, 84, 154, 124, 136, 36, 229, 242, 126, 73, 219, 215, 68, 243, 195, 146, 239, 14, 124, 127, 189, 6, 78, 226, 6, 250, 165, 243, 218, 100, 17, 43, 19, 47, 7, 222, 53, 242, 236, 184, 203, 230, 72, 155, 100, 203, 192, 105, 55, 74, 37, 222, 147, 65, 107, 147, 137, 208, 181, 140, 74, 94, 141, 66, 225, 224, 16, 90, 121, 252, 170, 151, 168, 1, 23, 186, 210, 147, 112, 1, 210, 200, 65, 125, 41, 9, 23, 218, 46, 200, 45, 74, 73, 216, 200, 78, 72, 43, 182, 161, 233, 228, 155, 78, 32, 154, 90, 190, 183, 69, 140, 223, 95, 194, 5, 30, 65, 20, 128, 64, 2, 9, 34, 232, 164, 95, 194, 6, 165, 75, 181, 116, 218, 67, 207, 154, 64, 191, 13, 122, 9, 143, 64, 104, 125, 10, 143, 64, 120, 120, 106, 218, 253, 18, 58, 124, 235, 36, 190, 14, 226, 155, 23, 236, 146, 72, 21, 55, 100, 168, 226, 134, 10, 146, 196, 13, 21, 212, 73, 153, 184, 225, 130, 27, 54, 184, 33, 67, 61, 19, 226, 134, 11, 213, 126, 214, 144, 193, 35, 16, 252, 81, 73, 67, 134, 250, 54, 84, 72, 198, 182, 225, 2, 58, 150, 54, 219, 112, 225, 233, 71, 154, 182, 33, 99, 217, 134, 10, 154, 74, 248, 17, 4, 131, 69, 41, 173, 43, 223, 247, 78, 40, 156, 244, 75, 241, 215, 224, 254, 29, 180, 191, 158, 0, 14, 208, 239, 201, 175, 110, 251, 29, 85, 154, 8, 187, 36, 237, 242, 215, 165, 18, 159, 88, 107, 223, 132, 111, 219, 234, 214, 53, 188, 241, 186, 47, 107, 117, 185, 124, 82, 166, 213, 53, 94, 70, 20, 118, 235, 80, 171, 106, 102, 251, 232, 23, 72, 156, 1, 209, 253, 95, 139, 59, 57, 106, 19, 13, 91, 231, 184, 19, 170, 52, 13, 35, 60, 242, 78, 179, 203, 105, 214, 65, 149, 166, 225, 3, 163, 28, 106, 0, 105, 20, 86, 223, 30, 75, 232, 74, 216, 209, 188, 24, 162, 121, 113, 131, 187, 117, 26, 168, 210, 52, 124, 248, 91, 174, 210, 52, 116, 208, 138, 117, 254, 151, 168, 210, 52, 132, 52, 13, 27, 149, 110, 26, 54, 144, 112, 55, 13, 23, 62, 165, 118, 211, 112, 209, 52, 124, 52, 13, 31, 18, 52, 13, 29, 173, 20, 221, 9, 242, 8, 163, 28, 138, 208, 180, 125, 220, 80, 132, 166, 161, 195, 130, 92, 175, 201, 86, 18, 79, 22, 225, 186, 18, 126, 180, 190, 46, 199, 184, 1, 69, 104, 26, 58, 188, 105, 208, 133, 140, 166, 225, 35, 157, 244, 46, 147, 54, 106, 26, 66, 36, 105, 29, 106, 26, 66, 184, 36, 49, 67, 77, 67, 134, 54, 201, 158, 138, 92, 168, 105, 24, 209, 52, 164, 104, 26, 62, 188, 174, 245, 22, 212, 52, 212, 52, 108, 64, 137, 134, 215, 130, 148, 169, 243, 40, 59, 251, 43, 218, 144, 193, 155, 54, 14, 173, 207, 164, 243, 75, 129, 240, 181, 142, 129, 126, 46, 111, 217, 14, 174, 76, 157, 127, 184, 100, 250, 235, 218, 47, 169, 79, 184, 254, 227, 150, 208, 181, 100, 229, 41, 237, 164, 8, 192, 93, 23, 82, 223, 18, 185, 210, 138, 101, 251, 142, 23, 104, 219, 197, 48, 92, 209, 246, 240, 111, 204, 56, 92, 87, 194, 16, 231, 95, 99, 245, 67, 250, 111, 177, 47, 137, 190, 162, 13, 35, 190, 162, 13, 21, 252, 145, 186, 231, 41, 218, 208, 145, 253, 21, 93, 236, 29, 244, 160, 37, 163, 104, 67, 134, 213, 15, 233, 115, 95, 138, 71, 144, 106, 203, 32, 138, 54, 68, 209, 134, 12, 119, 109, 165, 18, 73, 154, 41, 175, 13, 31, 82, 191, 54, 92, 144, 120, 178, 215, 134, 11, 6, 218, 168, 165, 32, 103, 15, 236, 74, 232, 97, 118, 189, 54, 124, 188, 54, 84, 192, 154, 213, 61, 223, 192, 35, 109, 2, 16, 44, 242, 136, 46, 246, 119, 109, 232, 240, 60, 40, 125, 215, 134, 20, 141, 223, 219, 82, 32, 173, 255, 248, 209, 116, 146, 141, 222, 181, 161, 227, 59, 21, 98, 90, 27, 54, 18, 43, 214, 22, 177, 120, 65, 13, 68, 34, 96, 109, 8, 97, 109, 248, 96, 109, 216, 64, 135, 46, 165, 218, 144, 97, 229, 155, 160, 159, 218, 176, 49, 241, 132, 62, 181, 33, 227, 209, 213, 169, 13, 23, 105, 21, 111, 171, 99, 240, 174, 157, 218, 176, 161, 177, 71, 84, 67, 6, 215, 73, 249, 102, 135, 44, 240, 56, 46, 187, 97, 198, 14, 146, 120, 178, 59, 85, 195, 133, 75, 13, 21, 40, 203, 150, 26, 50, 112, 179, 147, 108, 200, 160, 191, 11, 81, 104, 63, 224, 194, 23, 171, 95, 123, 14, 7, 213, 187, 232, 91, 11, 106, 150, 112, 210, 43, 131, 16, 199, 179, 161, 130, 115, 167, 100, 104, 93, 54, 124, 168, 181, 111, 65, 235, 178, 237, 59, 90, 91, 91, 231, 13, 142, 221, 39, 83, 203, 45, 27, 50, 44, 246, 14, 195, 191, 233, 68, 189, 121, 113, 114, 104, 227, 149, 252, 45, 156, 212, 116, 225, 150, 13, 27, 223, 234, 158, 220, 178, 161, 99, 121, 190, 60, 135, 184, 101, 75, 109, 224, 158, 16, 183, 108, 200, 224, 150, 13, 21, 90, 54, 84, 208, 204, 178, 225, 2, 194, 8, 164, 148, 101, 67, 137, 181, 18, 41, 203, 134, 13, 42, 25, 201, 134, 11, 174, 109, 151, 74, 109, 212, 240, 193, 178, 27, 212, 144, 161, 33, 0, 42, 24, 6, 32, 2, 195, 154, 224, 15, 195, 133, 135, 217, 138, 254, 99, 200, 96, 235, 56, 90, 240, 128, 3, 111, 218, 190, 235, 63, 96, 235, 120, 107, 139, 8, 240, 24, 70, 60, 134, 16, 239, 143, 161, 130, 71, 36, 52, 56, 158, 78, 63, 214, 143, 161, 163, 85, 151, 0, 212, 173, 251, 100, 13, 39, 125, 50, 151, 140, 161, 109, 151, 55, 45, 215, 2, 77, 91, 137, 71, 82, 197, 201, 155, 66, 239, 153, 26, 212, 115, 217, 126, 136, 31, 181, 82, 9, 209, 74, 229, 126, 167, 67, 251, 235, 49, 116, 72, 112, 212, 250, 235, 49, 108, 56, 110, 135, 36, 143, 33, 3, 237, 99, 232, 240, 12, 27, 20, 103, 168, 96, 77, 112, 103, 184, 192, 239, 158, 78, 237, 12, 27, 45, 1, 67, 127, 23, 196, 35, 72, 146, 210, 53, 206, 208, 241, 174, 141, 156, 225, 194, 25, 54, 148, 45, 107, 134, 12, 218, 156, 16, 107, 134, 140, 109, 134, 10, 36, 43, 41, 200, 169, 66, 205, 240, 241, 63, 129, 225, 195, 255, 4, 134, 10, 19, 24, 50, 188, 57, 134, 10, 142, 161, 35, 173, 231, 226, 24, 46, 224, 194, 121, 43, 204, 144, 225, 31, 51, 84, 208, 166, 165, 147, 144, 250, 99, 134, 144, 111, 203, 14, 125, 123, 204, 240, 177, 15, 53, 222, 103, 134, 13, 183, 142, 183, 58, 207, 12, 29, 239, 204, 80, 193, 35, 16, 124, 187, 153, 97, 131, 177, 114, 92, 51, 123, 180, 108, 154, 25, 50, 248, 174, 108, 102, 184, 80, 17, 144, 54, 126, 110, 204, 176, 193, 147, 75, 21, 114, 204, 11, 161, 131, 8, 19, 164, 159, 109, 155, 116, 20, 77, 227, 6, 173, 88, 117, 254, 228, 197, 56, 232, 235, 82, 133, 30, 212, 153, 225, 145, 237, 247, 208, 230, 132, 22, 64, 203, 162, 86, 63, 212, 26, 59, 208, 120, 209, 55, 221, 149, 208, 131, 101, 225, 120, 170, 6, 55, 141, 25, 66, 30, 109, 204, 112, 97, 177, 124, 16, 167, 108, 140, 7, 245, 78, 39, 177, 109, 217, 195, 35, 2, 96, 224, 79, 42, 68, 225, 174, 109, 253, 45, 232, 225, 155, 50, 102, 200, 208, 186, 94, 210, 197, 12, 27, 139, 25, 62, 180, 199, 12, 122, 110, 12, 20, 75, 228, 158, 132, 121, 81, 68, 48, 232, 202, 228, 157, 235, 41, 102, 8, 65, 15, 179, 138, 25, 62, 116, 197, 182, 110, 240, 82, 197, 12, 25, 22, 171, 32, 135, 46, 165, 152, 33, 131, 63, 104, 169, 43, 41, 102, 200, 88, 148, 130, 82, 49, 67, 6, 238, 223, 35, 146, 196, 12, 25, 158, 214, 197, 164, 41, 82, 170, 159, 170, 173, 122, 119, 184, 54, 67, 162, 162, 77, 226, 18, 74, 164, 177, 128, 66, 130, 35, 167, 60, 144, 36, 49, 67, 137, 230, 6, 34, 148, 177, 43, 69, 146, 210, 53, 26, 220, 233, 251, 195, 73, 159, 148, 119, 154, 109, 16, 173, 223, 236, 38, 40, 188, 173, 164, 105, 132, 65, 58, 137, 25, 46, 240, 181, 255, 84, 251, 81, 101, 97, 54, 81, 89, 152, 161, 163, 178, 48, 67, 133, 134, 192, 223, 67, 218, 9, 197, 218, 133, 25, 54, 188, 99, 3, 45, 138, 49, 100, 40, 198, 208, 241, 172, 197, 80, 161, 109, 37, 25, 122, 178, 59, 209, 98, 8, 233, 85, 142, 189, 132, 33, 195, 49, 125, 9, 67, 6, 9, 67, 71, 132, 69, 83, 231, 31, 18, 232, 208, 168, 139, 48, 132, 56, 124, 69, 95, 215, 146, 159, 210, 249, 99, 229, 38, 246, 79, 169, 77, 201, 70, 139, 69, 24, 196, 98, 21, 244, 208, 185, 76, 178, 65, 180, 73, 182, 220, 13, 93, 191, 175, 235, 121, 127, 136, 80, 169, 206, 131, 111, 204, 32, 149, 234, 168, 84, 135, 66, 165, 58, 238, 113, 55, 142, 86, 181, 147, 85, 16, 109, 78, 236, 45, 78, 187, 26, 26, 255, 191, 199, 163, 120, 105, 52, 94, 213, 156, 82, 162, 197, 34, 12, 29, 22, 139, 48, 84, 112, 126, 41, 149, 101, 184, 224, 175, 44, 195, 5, 136, 165, 45, 93, 101, 25, 50, 148, 85, 150, 225, 194, 86, 150, 161, 99, 157, 91, 247, 117, 95, 134, 15, 143, 32, 163, 161, 180, 98, 61, 194, 241, 183, 156, 244, 202, 56, 32, 8, 247, 116, 90, 177, 140, 161, 244, 125, 153, 47, 196, 251, 178, 109, 203, 88, 227, 181, 121, 152, 109, 203, 144, 97, 251, 53, 147, 8, 109, 126, 72, 175, 197, 182, 101, 232, 128, 155, 150, 218, 50, 100, 248, 166, 170, 45, 106, 203, 208, 193, 22, 109, 25, 66, 156, 217, 50, 92, 104, 12, 74, 203, 150, 33, 163, 149, 45, 83, 182, 12, 27, 203, 46, 101, 203, 112, 225, 108, 25, 115, 40, 91, 134, 15, 101, 203, 80, 97, 215, 90, 134, 143, 182, 75, 54, 106, 93, 107, 25, 66, 112, 241, 40, 181, 12, 23, 148, 237, 55, 180, 98, 87, 254, 186, 76, 232, 25, 118, 87, 141, 147, 58, 196, 73, 239, 112, 117, 207, 83, 44, 207, 183, 31, 66, 107, 255, 173, 149, 104, 129, 214, 101, 144, 1, 148, 101, 67, 173, 57, 61, 208, 116, 129, 54, 106, 101, 175, 33, 85, 156, 84, 65, 224, 4, 120, 152, 117, 207, 165, 84, 210, 30, 3, 93, 191, 223, 64, 83, 58, 215, 37, 87, 227, 253, 108, 251, 174, 10, 85, 60, 194, 64, 1, 156, 79, 198, 244, 87, 226, 9, 104, 8, 160, 107, 201, 119, 192, 105, 30, 183, 243, 42, 97, 26, 4, 12, 96, 33, 73, 219, 197, 255, 18, 45, 128, 49, 77, 131, 214, 255, 100, 223, 184, 5, 208, 128, 164, 147, 117, 178, 7, 254, 16, 190, 233, 68, 85, 23, 187, 18, 99, 117, 66, 15, 15, 111, 58, 27, 173, 159, 180, 155, 147, 58, 119, 192, 157, 150, 97, 195, 118, 90, 134, 11, 156, 150, 33, 196, 245, 51, 237, 226, 133, 61, 17, 16, 202, 246, 219, 107, 104, 238, 63, 128, 0, 38, 158, 10, 183, 67, 189, 64, 155, 29, 143, 74, 208, 131, 58, 41, 211, 67, 2, 6, 104, 109, 192, 253, 202, 144, 226, 53, 101, 190, 175, 12, 27, 248, 224, 188, 50, 231, 149, 33, 195, 193, 151, 247, 205, 250, 202, 48, 98, 66, 215, 239, 107, 147, 108, 35, 26, 111, 131, 124, 101, 248, 112, 126, 153, 175, 124, 101, 232, 64, 10, 110, 101, 184, 160, 218, 143, 114, 211, 246, 167, 163, 88, 118, 33, 93, 146, 86, 134, 14, 39, 159, 109, 101, 184, 248, 102, 143, 192, 160, 111, 229, 196, 85, 251, 31, 46, 25, 130, 112, 70, 219, 225, 141, 61, 178, 64, 211, 128, 248, 3, 215, 237, 108, 188, 159, 86, 194, 93, 89, 54, 228, 158, 43, 69, 52, 109, 157, 127, 90, 34, 7, 109, 90, 25, 46, 80, 88, 25, 42, 100, 5, 171, 166, 12, 23, 218, 252, 254, 180, 59, 180, 54, 233, 208, 211, 206, 160, 167, 27, 105, 153, 237, 212, 30, 79, 136, 54, 126, 96, 132, 71, 152, 50, 92, 208, 138, 101, 202, 144, 129, 31, 185, 100, 136, 145, 51, 76, 25, 22, 192, 25, 166, 140, 41, 195, 69, 131, 51, 136, 41, 67, 6, 198, 92, 129, 216, 99, 111, 149, 50, 108, 120, 90, 223, 4, 71, 171, 123, 254, 129, 144, 111, 169, 12, 23, 154, 98, 216, 224, 138, 97, 163, 41, 134, 13, 62, 165, 54, 90, 197, 176, 177, 15, 161, 85, 12, 31, 18, 63, 5, 63, 115, 69, 30, 169, 40, 70, 81, 241, 8, 170, 40, 86, 81, 236, 65, 69, 49, 231, 147, 61, 68, 232, 90, 242, 25, 211, 160, 181, 255, 24, 211, 56, 32, 198, 237, 0, 169, 150, 72, 181, 101, 24, 240, 198, 203, 224, 83, 238, 63, 9, 119, 92, 118, 147, 157, 14, 105, 133, 234, 155, 224, 14, 40, 242, 83, 249, 46, 164, 84, 162, 247, 84, 12, 27, 164, 253, 100, 136, 145, 4, 138, 225, 163, 162, 14, 146, 64, 49, 92, 232, 90, 6, 73, 160, 24, 50, 92, 145, 4, 138, 33, 3, 243, 164, 160, 149, 216, 123, 37, 25, 66, 168, 69, 91, 86, 73, 134, 12, 30, 113, 62, 25, 164, 146, 145, 213, 64, 112, 62, 25, 46, 30, 20, 205, 159, 207, 249, 100, 248, 192, 61, 33, 173, 216, 125, 104, 223, 196, 62, 111, 156, 214, 65, 127, 23, 197, 55, 102, 26, 167, 69, 15, 120, 227, 180, 168, 53, 235, 170, 22, 44, 74, 65, 222, 192, 155, 23, 167, 135, 92, 165, 12, 162, 117, 225, 84, 169, 206, 164, 214, 82, 208, 234, 39, 145, 171, 148, 57, 62, 145, 241, 116, 99, 79, 134, 36, 158, 12, 27, 43, 29, 239, 100, 184, 120, 22, 30, 127, 203, 161, 86, 212, 186, 64, 169, 82, 73, 39, 67, 6, 93, 175, 31, 236, 74, 8, 226, 219, 174, 4, 169, 120, 4, 241, 63, 52, 144, 180, 93, 13, 232, 151, 130, 97, 164, 107, 201, 119, 192, 37, 109, 25, 240, 167, 34, 151, 131, 138, 126, 74, 244, 185, 61, 51, 67, 241, 219, 255, 137, 126, 155, 157, 212, 18, 73, 252, 62, 38, 28, 194, 35, 26, 133, 149, 164, 147, 33, 195, 35, 16, 36, 157, 12, 23, 139, 202, 237, 100, 149, 166, 173, 147, 58, 25, 50, 112, 126, 153, 183, 58, 151, 12, 31, 171, 65, 21, 94, 26, 191, 190, 147, 206, 181, 108, 29, 181, 67, 100, 97, 228, 146, 225, 226, 151, 237, 66, 46, 153, 75, 134, 142, 182, 146, 108, 201, 180, 201, 150, 12, 31, 206, 51, 99, 201, 112, 193, 157, 146, 37, 195, 197, 174, 100, 168, 160, 76, 58, 114, 137, 6, 119, 220, 14, 162, 87, 33, 149, 36, 147, 36, 67, 7, 71, 146, 161, 194, 239, 114, 142, 13, 143, 160, 246, 154, 181, 201, 208, 1, 145, 48, 65, 235, 18, 57, 182, 78, 90, 155, 12, 33, 220, 115, 236, 80, 90, 102, 91, 147, 12, 29, 182, 31, 122, 109, 77, 50, 116, 236, 74, 219, 36, 75, 219, 36, 195, 198, 51, 38, 25, 46, 32, 231, 148, 12, 27, 124, 245, 67, 146, 148, 14, 66, 179, 173, 147, 146, 77, 224, 73, 96, 232, 152, 192, 147, 192, 240, 209, 250, 109, 23, 122, 18, 24, 58, 38, 192, 252, 171, 88, 200, 144, 172, 98, 161, 66, 197, 194, 70, 165, 61, 11, 21, 210, 126, 190, 35, 157, 35, 167, 44, 92, 232, 111, 83, 180, 145, 83, 22, 62, 42, 170, 218, 34, 167, 80, 22, 66, 156, 178, 80, 65, 227, 202, 91, 184, 144, 201, 223, 194, 133, 110, 111, 161, 130, 4, 19, 151, 36, 246, 10, 176, 183, 240, 225, 159, 239, 168, 2, 236, 45, 140, 168, 0, 123, 11, 29, 149, 95, 168, 224, 155, 95, 168, 224, 146, 85, 96, 161, 79, 223, 132, 70, 175, 23, 66, 94, 47, 108, 188, 211, 236, 162, 240, 66, 134, 103, 114, 128, 64, 199, 162, 40, 188, 112, 225, 241, 66, 7, 52, 241, 41, 181, 33, 206, 139, 116, 255, 121, 161, 195, 117, 185, 243, 194, 6, 199, 11, 21, 56, 206, 193, 33, 190, 45, 59, 214, 6, 169, 123, 62, 63, 213, 106, 154, 23, 65, 15, 22, 197, 77, 99, 182, 64, 75, 124, 179, 115, 79, 135, 24, 120, 230, 5, 53, 72, 82, 46, 142, 121, 33, 3, 23, 40, 203, 133, 142, 230, 112, 164, 105, 188, 144, 33, 121, 215, 166, 241, 66, 135, 127, 220, 120, 33, 164, 105, 188, 80, 161, 162, 139, 23, 46, 120, 100, 37, 188, 144, 97, 37, 188, 80, 129, 1, 82, 218, 188, 56, 33, 7, 150, 78, 116, 53, 188, 144, 241, 52, 13, 47, 92, 104, 26, 94, 248, 192, 154, 25, 198, 52, 188, 26, 94, 184, 120, 94, 217, 240, 194, 133, 71, 34, 104, 120, 33, 195, 130, 134, 151, 68, 195, 11, 29, 13, 47, 84, 224, 220, 115, 238, 213, 219, 115, 126, 41, 19, 182, 189, 213, 55, 97, 219, 155, 176, 237, 53, 76, 216, 246, 156, 95, 74, 227, 109, 15, 226, 89, 26, 111, 123, 141, 183, 189, 135, 239, 124, 103, 219, 175, 108, 251, 29, 216, 246, 59, 247, 184, 109, 46, 199, 79, 193, 239, 192, 207, 239, 224, 202, 246, 215, 63, 183, 243, 203, 55, 59, 99, 87, 73, 54, 215, 138, 245, 87, 87, 95, 142, 165, 175, 231, 239, 218, 234, 116, 194, 172, 55, 29, 228, 59, 223, 85, 186, 166, 42, 157, 74, 231, 120, 150, 86, 213, 156, 26, 63, 203, 47, 227, 95, 198, 20, 110, 14, 199, 220, 180, 98, 153, 41, 115, 120, 150, 74, 178, 74, 50, 135, 75, 6, 225, 143, 74, 92, 178, 247, 94, 166, 149, 194, 43, 66, 61, 83, 43, 133, 151, 231, 122, 84, 242, 120, 150, 71, 37, 234, 153, 150, 92, 84, 2, 209, 222, 195, 90, 175, 173, 109, 16, 39, 125, 178, 166, 233, 242, 8, 253, 109, 203, 248, 82, 36, 86, 190, 9, 207, 59, 166, 238, 160, 159, 202, 221, 215, 37, 83, 9, 79, 218, 66, 124, 235, 164, 173, 131, 191, 74, 117, 30, 109, 61, 29, 79, 199, 209, 244, 63, 23, 227, 240, 158, 75, 181, 101, 84, 162, 218, 50, 14, 204, 67, 39, 234, 42, 218, 104, 55, 56, 191, 20, 87, 77, 157, 95, 202, 146, 154, 122, 191, 69, 213, 195, 211, 253, 18, 167, 7, 87, 207, 180, 148, 101, 130, 0, 161, 218, 46, 16, 62, 33, 56, 191, 20, 231, 151, 210, 246, 233, 116, 56, 56, 159, 142, 255, 53, 237, 106, 240, 71, 37, 108, 29, 87, 109, 151, 87, 103, 86, 237, 53, 240, 86, 181, 216, 209, 201, 126, 54, 109, 136, 39, 187, 83, 61, 117, 105, 235, 231, 98, 215, 250, 109, 181, 23, 60, 217, 157, 175, 77, 167, 147, 58, 157, 180, 216, 59, 175, 222, 170, 218, 99, 199, 184, 25, 56, 41, 19, 133, 36, 229, 178, 86, 38, 213, 214, 245, 173, 156, 72, 76, 216, 246, 92, 41, 156, 244, 201, 86, 190, 9, 234, 19, 175, 77, 243, 131, 122, 46, 21, 85, 109, 85, 91, 70, 61, 23, 136, 9, 39, 165, 62, 142, 211, 147, 221, 89, 1, 8, 17, 15, 181, 174, 168, 225, 120, 42, 94, 200, 208, 146, 196, 75, 146, 120, 33, 67, 146, 120, 161, 194, 63, 154, 182, 21, 27, 161, 77, 54, 51, 143, 141, 48, 83, 164, 139, 82, 32, 150, 251, 67, 35, 113, 104, 101, 226, 118, 79, 106, 88, 153, 120, 33, 196, 35, 12, 238, 9, 130, 183, 122, 4, 66, 163, 181, 181, 181, 215, 13, 206, 160, 86, 199, 83, 37, 47, 92, 248, 131, 174, 117, 120, 64, 92, 226, 249, 16, 205, 50, 17, 20, 13, 147, 144, 74, 94, 200, 88, 20, 91, 232, 240, 166, 173, 106, 187, 32, 54, 89, 11, 27, 84, 242, 210, 214, 201, 46, 116, 80, 252, 239, 194, 134, 86, 73, 90, 167, 191, 11, 29, 173, 191, 11, 31, 187, 144, 177, 80, 129, 126, 42, 247, 90, 91, 85, 211, 70, 223, 116, 173, 47, 248, 231, 218, 162, 71, 47, 60, 204, 54, 117, 34, 16, 222, 119, 225, 194, 125, 23, 66, 48, 253, 69, 239, 106, 223, 132, 93, 248, 96, 166, 45, 98, 205, 204, 231, 114, 168, 242, 218, 46, 156, 80, 218, 228, 59, 250, 246, 48, 43, 105, 187, 208, 129, 36, 109, 23, 70, 248, 251, 190, 166, 237, 66, 137, 138, 5, 73, 106, 47, 205, 159, 116, 130, 58, 181, 237, 66, 10, 109, 219, 118, 53, 246, 22, 39, 8, 109, 187, 208, 179, 32, 109, 187, 16, 210, 186, 104, 148, 116, 254, 248, 69, 191, 108, 23, 50, 214, 46, 108, 168, 125, 15, 173, 93, 200, 216, 215, 160, 181, 11, 23, 18, 110, 29, 111, 180, 54, 13, 47, 164, 254, 216, 81, 127, 236, 56, 158, 214, 181, 178, 129, 180, 70, 68, 168, 240, 210, 120, 24, 122, 167, 217, 133, 16, 238, 61, 179, 11, 25, 24, 134, 159, 217, 133, 11, 221, 133, 10, 24, 168, 55, 77, 187, 240, 33, 66, 195, 35, 139, 85, 150, 93, 200, 112, 249, 203, 46, 100, 112, 9, 246, 86, 177, 47, 187, 176, 49, 193, 45, 187, 26, 120, 4, 97, 68, 3, 115, 109, 217, 133, 144, 101, 23, 54, 212, 113, 114, 180, 50, 109, 69, 23, 54, 180, 46, 116, 84, 116, 161, 130, 54, 209, 133, 10, 232, 87, 41, 122, 208, 86, 223, 241, 246, 116, 54, 70, 115, 111, 209, 40, 172, 36, 220, 225, 157, 37, 170, 142, 118, 112, 217, 20, 212, 120, 223, 113, 4, 83, 214, 168, 44, 243, 240, 153, 218, 130, 164, 138, 19, 122, 44, 157, 232, 194, 133, 5, 12, 36, 216, 215, 212, 49, 68, 23, 62, 220, 117, 161, 66, 182, 214, 133, 11, 237, 165, 147, 180, 157, 46, 116, 168, 116, 232, 25, 93, 200, 144, 168, 46, 84, 104, 125, 127, 43, 181, 74, 58, 217, 130, 111, 249, 143, 65, 104, 125, 114, 169, 237, 6, 194, 3, 172, 77, 4, 37, 91, 33, 132, 168, 212, 133, 10, 30, 113, 169, 22, 50, 184, 84, 43, 45, 116, 88, 207, 181, 37, 38, 156, 22, 58, 212, 166, 133, 10, 174, 21, 235, 13, 119, 109, 30, 129, 176, 88, 57, 153, 52, 125, 86, 104, 193, 4, 117, 70, 69, 46, 116, 60, 21, 185, 80, 161, 129, 198, 140, 73, 148, 163, 200, 2, 207, 146, 116, 42, 116, 160, 21, 219, 246, 115, 33, 163, 245, 115, 225, 66, 123, 205, 208, 182, 11, 98, 173, 76, 232, 61, 23, 58, 112, 104, 242, 151, 231, 8, 29, 120, 211, 114, 225, 130, 51, 140, 134, 23, 58, 30, 9, 66, 30, 9, 6, 96, 35, 204, 184, 159, 4, 25, 173, 235, 73, 112, 65, 159, 4, 21, 184, 190, 182, 214, 229, 73, 48, 162, 242, 18, 108, 168, 125, 46, 193, 5, 119, 9, 42, 104, 171, 239, 94, 75, 144, 145, 147, 198, 18, 92, 104, 217, 88, 130, 11, 239, 219, 188, 88, 130, 12, 207, 149, 44, 65, 134, 207, 118, 240, 230, 132, 34, 36, 75, 176, 241, 112, 138, 238, 196, 73, 153, 16, 196, 67, 53, 245, 118, 232, 63, 238, 134, 54, 45, 123, 2, 67, 243, 156, 170, 237, 214, 9, 163, 100, 9, 66, 50, 249, 107, 80, 34, 139, 198, 30, 241, 8, 242, 125, 180, 9, 64, 176, 76, 123, 98, 120, 115, 250, 214, 201, 107, 179, 170, 77, 168, 51, 75, 240, 241, 40, 126, 20, 51, 22, 36, 75, 208, 33, 89, 130, 10, 156, 234, 114, 196, 18, 100, 40, 85, 170, 142, 116, 49, 204, 115, 28, 57, 255, 154, 4, 41, 42, 220, 36, 168, 224, 186, 77, 130, 20, 255, 88, 146, 184, 73, 144, 145, 141, 151, 4, 23, 250, 233, 146, 224, 130, 182, 101, 152, 68, 106, 73, 208, 145, 218, 227, 136, 146, 75, 130, 13, 134, 145, 106, 43, 65, 107, 173, 4, 27, 201, 184, 29, 9, 50, 32, 109, 178, 249, 93, 31, 83, 134, 38, 143, 74, 176, 97, 242, 168, 4, 21, 188, 55, 221, 147, 110, 209, 220, 189, 143, 74, 176, 225, 182, 155, 71, 37, 184, 240, 8, 82, 197, 233, 81, 9, 58, 36, 201, 43, 61, 42, 65, 8, 196, 184, 25, 213, 201, 203, 163, 18, 84, 128, 78, 250, 37, 69, 234, 185, 60, 42, 65, 7, 138, 74, 80, 129, 115, 235, 210, 42, 65, 70, 47, 163, 18, 167, 42, 209, 201, 174, 68, 78, 85, 130, 16, 168, 169, 74, 240, 49, 129, 4, 17, 26, 146, 76, 73, 37, 200, 128, 14, 111, 78, 18, 92, 112, 141, 91, 36, 184, 80, 209, 79, 9, 46, 56, 56, 47, 173, 19, 69, 186, 188, 9, 157, 141, 157, 219, 122, 227, 182, 143, 5, 233, 165, 83, 108, 39, 147, 190, 192, 189, 181, 184, 127, 219, 155, 240, 104, 154, 3, 233, 36, 102, 143, 134, 141, 208, 181, 76, 3, 143, 68, 88, 96, 98, 210, 148, 129, 15, 224, 193, 174, 134, 50, 238, 5, 155, 78, 114, 48, 240, 48, 11, 209, 188, 116, 201, 5, 37, 235, 148, 160, 3, 166, 218, 41, 65, 6, 93, 207, 93, 74, 112, 209, 250, 190, 202, 214, 82, 151, 18, 108, 240, 72, 74, 152, 148, 188, 5, 73, 144, 81, 137, 32, 67, 151, 99, 235, 250, 148, 8, 58, 152, 58, 196, 143, 36, 148, 8, 58, 94, 253, 181, 65, 202, 164, 55, 168, 103, 66, 75, 46, 42, 105, 240, 120, 150, 115, 137, 30, 24, 102, 27, 191, 16, 191, 232, 233, 126, 9, 189, 191, 8, 54, 244, 139, 224, 194, 195, 109, 133, 71, 112, 161, 105, 235, 168, 226, 17, 108, 248, 156, 120, 4, 23, 38, 30, 65, 5, 219, 207, 35, 184, 80, 241, 30, 65, 5, 151, 172, 161, 218, 223, 224, 17, 124, 44, 86, 225, 247, 8, 46, 60, 130, 16, 159, 239, 17, 92, 112, 143, 32, 164, 155, 85, 211, 201, 2, 19, 186, 150, 28, 192, 122, 253, 192, 55, 59, 114, 199, 237, 148, 190, 107, 139, 144, 208, 143, 136, 104, 32, 177, 64, 63, 23, 251, 234, 187, 71, 208, 241, 219, 30, 114, 143, 32, 196, 67, 89, 38, 228, 30, 65, 7, 247, 8, 42, 80, 154, 158, 210, 70, 17, 173, 89, 79, 111, 143, 96, 163, 129, 185, 198, 96, 206, 35, 248, 96, 148, 243, 8, 46, 56, 159, 206, 35, 139, 82, 126, 23, 74, 174, 55, 157, 182, 227, 225, 188, 122, 167, 115, 141, 61, 130, 11, 138, 89, 219, 133, 190, 237, 114, 248, 182, 75, 219, 54, 212, 74, 219, 216, 35, 200, 224, 112, 210, 167, 99, 152, 85, 144, 180, 88, 62, 141, 197, 124, 85, 34, 48, 30, 156, 227, 34, 76, 60, 69, 96, 60, 204, 126, 179, 67, 88, 44, 31, 244, 48, 233, 154, 182, 165, 125, 95, 85, 168, 129, 135, 87, 73, 98, 138, 119, 219, 201, 159, 223, 213, 249, 4, 195, 107, 101, 131, 5, 173, 146, 100, 24, 102, 25, 109, 85, 207, 239, 218, 101, 255, 129, 118, 58, 198, 105, 189, 8, 181, 201, 117, 99, 111, 203, 16, 255, 215, 5, 53, 157, 168, 3, 78, 250, 116, 16, 18, 79, 230, 17, 148, 182, 225, 134, 133, 26, 87, 116, 49, 165, 15, 205, 42, 65, 238, 41, 6, 186, 210, 147, 42, 143, 169, 66, 18, 173, 12, 243, 216, 107, 143, 94, 151, 168, 177, 71, 112, 98, 226, 9, 69, 64, 141, 61, 130, 10, 202, 216, 35, 184, 16, 129, 161, 45, 243, 8, 46, 124, 219, 229, 17, 100, 144, 36, 75, 237, 143, 252, 87, 103, 136, 36, 89, 74, 135, 118, 61, 130, 143, 93, 143, 160, 194, 183, 117, 60, 130, 12, 174, 142, 167, 227, 17, 100, 168, 237, 198, 35, 200, 224, 158, 241, 8, 46, 104, 106, 102, 60, 130, 139, 228, 234, 17, 92, 112, 234, 17, 84, 104, 234, 17, 84, 128, 176, 5, 125, 147, 207, 118, 72, 221, 211, 128, 63, 162, 162, 141, 182, 122, 167, 91, 186, 228, 242, 88, 151, 168, 145, 88, 121, 4, 23, 242, 83, 161, 166, 18, 234, 201, 46, 85, 30, 193, 136, 92, 165, 108, 37, 75, 65, 175, 174, 237, 22, 36, 86, 30, 65, 158, 60, 130, 14, 229, 146, 71, 112, 225, 153, 117, 92, 21, 39, 143, 224, 227, 85, 117, 58, 201, 35, 216, 104, 139, 71, 80, 97, 23, 143, 160, 66, 75, 143, 160, 194, 98, 149, 8, 30, 65, 136, 195, 98, 249, 32, 143, 224, 226, 97, 22, 162, 46, 147, 43, 68, 4, 8, 36, 144, 48, 129, 4, 17, 210, 129, 32, 157, 197, 39, 93, 30, 193, 134, 71, 144, 225, 30, 65, 136, 71, 112, 194, 35, 232, 144, 64, 49, 228, 17, 100, 64, 79, 103, 47, 187, 144, 71, 112, 34, 21, 71, 144, 71, 112, 225, 204, 249, 109, 110, 86, 34, 156, 136, 32, 132, 167, 109, 254, 108, 187, 48, 78, 200, 35, 8, 17, 193, 6, 200, 35, 248, 136, 96, 99, 101, 91, 214, 250, 252, 16, 190, 217, 121, 203, 254, 6, 77, 91, 79, 213, 149, 48, 39, 8, 13, 175, 157, 9, 130, 78, 214, 95, 255, 194, 237, 33, 7, 26, 175, 147, 218, 115, 251, 22, 232, 82, 237, 45, 104, 223, 190, 198, 90, 203, 124, 69, 95, 219, 46, 73, 167, 131, 96, 187, 252, 105, 125, 19, 88, 118, 58, 200, 82, 117, 40, 234, 143, 89, 132, 126, 118, 179, 236, 157, 102, 213, 65, 78, 250, 137, 165, 233, 146, 53, 176, 102, 230, 193, 178, 211, 57, 40, 237, 86, 70, 213, 131, 47, 249, 168, 105, 187, 22, 119, 114, 1, 32, 3, 219, 126, 52, 33, 130, 139, 182, 11, 106, 217, 223, 168, 139, 32, 196, 35, 168, 81, 23, 193, 197, 230, 115, 4, 23, 212, 57, 130, 10, 217, 28, 65, 5, 118, 28, 90, 142, 32, 196, 114, 4, 31, 203, 17, 84, 240, 171, 56, 130, 11, 20, 128, 64, 2, 9, 34, 168, 85, 28, 97, 231, 150, 68, 240, 241, 237, 117, 131, 126, 35, 216, 104, 156, 161, 95, 150, 78, 219, 53, 104, 118, 200, 222, 8, 62, 28, 219, 8, 42, 44, 86, 209, 38, 130, 12, 171, 162, 17, 84, 80, 139, 208, 209, 42, 105, 126, 184, 147, 62, 41, 77, 155, 108, 245, 39, 73, 204, 16, 255, 75, 228, 17, 139, 229, 227, 158, 144, 196, 111, 98, 169, 223, 76, 40, 252, 155, 54, 165, 235, 121, 237, 124, 71, 90, 102, 147, 58, 103, 208, 224, 250, 136, 194, 171, 193, 98, 239, 160, 86, 182, 15, 113, 247, 184, 27, 137, 214, 214, 94, 63, 208, 201, 98, 215, 118, 208, 220, 173, 238, 83, 186, 134, 251, 119, 112, 222, 74, 34, 73, 98, 150, 171, 148, 57, 233, 147, 181, 126, 183, 23, 153, 248, 109, 47, 18, 65, 61, 19, 106, 235, 173, 223, 236, 244, 61, 157, 6, 36, 137, 153, 46, 118, 180, 54, 25, 165, 73, 93, 101, 98, 1, 85, 220, 22, 52, 213, 175, 172, 59, 208, 84, 191, 31, 11, 160, 169, 62, 179, 12, 122, 128, 41, 107, 220, 59, 105, 12, 32, 140, 213, 68, 31, 36, 137, 25, 210, 79, 229, 208, 224, 218, 168, 92, 144, 54, 217, 58, 161, 104, 150, 105, 252, 216, 149, 252, 165, 222, 212, 54, 157, 228, 208, 10, 67, 251, 173, 149, 232, 93, 35, 248, 120, 215, 8, 42, 52, 78, 251, 77, 85, 4, 27, 220, 36, 82, 182, 63, 65, 161, 109, 157, 148, 204, 65, 243, 103, 242, 199, 44, 188, 224, 155, 29, 27, 76, 50, 130, 143, 8, 138, 226, 239, 72, 234, 124, 129, 42, 110, 174, 107, 233, 100, 15, 173, 20, 238, 134, 193, 39, 117, 21, 7, 134, 89, 165, 205, 169, 146, 145, 68, 16, 109, 203, 60, 35, 184, 112, 156, 240, 8, 218, 149, 185, 50, 130, 14, 149, 100, 4, 21, 214, 102, 4, 31, 173, 141, 102, 4, 27, 36, 169, 206, 37, 41, 35, 216, 120, 118, 146, 148, 17, 92, 188, 39, 255, 86, 108, 131, 67, 51, 210, 135, 88, 107, 193, 255, 139, 18, 191, 232, 112, 166, 191, 184, 208, 12, 42, 201, 71, 191, 184, 104, 197, 134, 174, 37, 37, 170, 14, 61, 250, 69, 134, 247, 92, 232, 23, 23, 139, 139, 247, 102, 252, 98, 163, 109, 182, 66, 191, 200, 248, 92, 108, 252, 34, 99, 173, 68, 191, 200, 176, 250, 101, 219, 165, 249, 43, 250, 190, 232, 208, 170, 22, 59, 174, 237, 252, 2, 14, 161, 77, 43, 115, 152, 144, 48, 59, 17, 203, 174, 5, 236, 245, 123, 91, 200, 227, 185, 175, 47, 50, 254, 241, 147, 11, 169, 227, 103, 95, 116, 240, 197, 198, 111, 227, 64, 225, 164, 87, 6, 34, 173, 227, 154, 25, 251, 98, 4, 4, 17, 152, 96, 2, 236, 139, 16, 254, 45, 217, 23, 27, 36, 52, 156, 10, 4, 1, 162, 128, 132, 9, 16, 251, 34, 36, 2, 130, 0, 77, 48, 1, 4, 18, 16, 251, 162, 68, 218, 214, 134, 47, 50, 60, 162, 107, 23, 102, 221, 234, 139, 11, 95, 108, 60, 47, 190, 184, 160, 107, 25, 95, 95, 92, 68, 240, 69, 5, 110, 43, 156, 65, 53, 83, 144, 47, 70, 52, 248, 116, 22, 175, 238, 146, 73, 82, 41, 200, 215, 249, 198, 204, 65, 83, 203, 247, 22, 49, 177, 240, 106, 26, 208, 226, 195, 210, 234, 158, 163, 213, 175, 97, 251, 57, 247, 144, 47, 54, 44, 82, 56, 159, 14, 165, 246, 26, 28, 24, 70, 142, 253, 138, 126, 62, 195, 226, 133, 61, 159, 14, 53, 19, 71, 190, 248, 104, 114, 49, 200, 57, 232, 85, 203, 147, 180, 92, 203, 115, 67, 170, 37, 74, 237, 111, 56, 254, 12, 77, 91, 231, 149, 16, 179, 30, 94, 59, 169, 55, 154, 182, 173, 189, 166, 240, 90, 137, 158, 126, 237, 37, 125, 157, 79, 135, 32, 114, 189, 7, 165, 173, 239, 219, 224, 42, 218, 52, 180, 145, 175, 179, 8, 105, 120, 190, 213, 61, 124, 209, 161, 107, 73, 167, 217, 33, 111, 155, 11, 162, 106, 221, 65, 81, 18, 106, 139, 156, 79, 167, 34, 11, 35, 95, 100, 80, 201, 46, 180, 232, 192, 92, 67, 190, 203, 173, 21, 72, 39, 11, 241, 217, 104, 67, 190, 248, 176, 178, 95, 68, 54, 228, 139, 12, 106, 87, 78, 30, 8, 93, 79, 233, 218, 133, 25, 242, 197, 198, 90, 114, 113, 194, 37, 67, 190, 200, 64, 225, 165, 56, 33, 95, 132, 104, 188, 136, 32, 95, 116, 68, 22, 142, 32, 95, 108, 104, 92, 4, 249, 34, 131, 51, 110, 225, 22, 249, 226, 130, 78, 246, 83, 182, 190, 195, 179, 22, 52, 104, 147, 108, 155, 151, 171, 232, 34, 95, 132, 44, 246, 14, 242, 197, 197, 163, 146, 103, 7, 249, 98, 35, 29, 84, 69, 29, 228, 139, 15, 139, 139, 213, 207, 241, 52, 255, 58, 59, 237, 136, 231, 123, 213, 47, 179, 13, 242, 69, 6, 117, 244, 15, 144, 198, 160, 166, 13, 239, 167, 246, 30, 223, 150, 29, 82, 197, 9, 57, 175, 150, 178, 214, 107, 12, 242, 197, 133, 167, 27, 239, 103, 197, 174, 108, 13, 152, 171, 55, 52, 157, 40, 242, 197, 133, 106, 166, 60, 42, 65, 19, 64, 16, 129, 8, 30, 209, 220, 237, 139, 16, 207, 59, 166, 140, 181, 201, 84, 146, 88, 33, 135, 244, 50, 40, 109, 203, 32, 95, 116, 52, 43, 180, 40, 225, 248, 221, 23, 27, 220, 115, 49, 58, 215, 75, 189, 173, 54, 33, 95, 92, 232, 90, 114, 173, 76, 200, 23, 31, 109, 127, 237, 91, 144, 47, 70, 248, 34, 133, 234, 93, 244, 61, 36, 45, 215, 130, 124, 145, 209, 217, 28, 190, 109, 179, 17, 76, 160, 158, 11, 242, 197, 70, 99, 80, 106, 143, 63, 28, 223, 39, 187, 23, 25, 173, 20, 111, 40, 253, 100, 219, 18, 249, 226, 99, 101, 196, 202, 180, 175, 75, 58, 110, 37, 242, 197, 7, 95, 12, 128, 107, 247, 98, 196, 162, 163, 113, 47, 42, 76, 48, 113, 139, 14, 148, 108, 198, 226, 30, 205, 75, 66, 77, 170, 134, 229, 22, 29, 190, 246, 159, 227, 155, 238, 130, 240, 181, 255, 24, 183, 248, 208, 74, 229, 4, 110, 113, 49, 129, 91, 84, 208, 110, 177, 81, 209, 70, 27, 185, 69, 134, 91, 116, 112, 139, 1, 96, 235, 200, 121, 113, 145, 182, 241, 226, 66, 171, 218, 229, 141, 23, 31, 90, 215, 226, 142, 64, 104, 149, 112, 47, 188, 200, 192, 155, 182, 255, 234, 16, 216, 91, 231, 18, 2, 47, 74, 184, 186, 231, 31, 254, 136, 23, 31, 26, 60, 204, 122, 235, 36, 183, 115, 210, 22, 27, 206, 191, 100, 244, 251, 190, 45, 66, 60, 2, 129, 251, 219, 34, 131, 165, 182, 197, 199, 63, 215, 22, 45, 125, 180, 190, 245, 202, 126, 203, 65, 60, 144, 80, 234, 235, 219, 162, 130, 78, 82, 41, 14, 6, 90, 125, 193, 107, 59, 151, 172, 241, 188, 28, 53, 172, 108, 12, 77, 201, 60, 252, 99, 245, 142, 222, 105, 130, 216, 135, 90, 191, 161, 93, 12, 150, 126, 91, 92, 104, 109, 244, 219, 226, 66, 130, 4, 223, 22, 23, 220, 19, 132, 111, 17, 136, 113, 51, 232, 83, 50, 251, 157, 104, 173, 6, 214, 229, 123, 91, 92, 208, 252, 160, 218, 74, 48, 226, 157, 102, 189, 45, 46, 220, 19, 58, 34, 52, 208, 3, 231, 211, 77, 188, 99, 186, 28, 227, 196, 255, 18, 53, 208, 218, 251, 38, 180, 69, 134, 54, 39, 228, 32, 241, 100, 141, 207, 231, 218, 34, 100, 129, 187, 3, 31, 30, 73, 91, 84, 144, 180, 197, 135, 203, 126, 79, 172, 144, 164, 45, 66, 42, 188, 32, 73, 91, 92, 72, 90, 227, 133, 188, 170, 131, 17, 200, 216, 182, 168, 192, 25, 191, 173, 78, 91, 92, 48, 211, 22, 31, 141, 23, 29, 76, 91, 108, 124, 211, 38, 17, 58, 120, 147, 107, 50, 161, 180, 211, 241, 207, 86, 143, 243, 224, 217, 95, 81, 6, 238, 185, 18, 197, 167, 99, 236, 160, 138, 219, 4, 77, 112, 91, 180, 54, 10, 163, 86, 166, 45, 62, 190, 181, 45, 46, 84, 84, 91, 84, 88, 249, 212, 78, 86, 181, 69, 71, 197, 193, 48, 114, 173, 32, 165, 21, 85, 109, 209, 129, 23, 139, 12, 207, 154, 180, 250, 194, 104, 129, 254, 46, 58, 214, 122, 14, 170, 232, 164, 113, 66, 216, 160, 213, 249, 116, 11, 90, 213, 98, 166, 45, 58, 160, 182, 232, 72, 206, 22, 21, 126, 213, 75, 157, 112, 59, 182, 56, 225, 174, 235, 41, 118, 200, 87, 54, 81, 241, 136, 243, 168, 45, 69, 3, 227, 135, 227, 218, 12, 68, 81, 18, 114, 32, 131, 46, 247, 254, 193, 23, 243, 79, 84, 33, 9, 93, 141, 231, 119, 45, 78, 232, 231, 42, 101, 108, 177, 225, 108, 177, 209, 202, 22, 29, 30, 65, 12, 179, 10, 53, 108, 241, 161, 203, 177, 117, 228, 146, 45, 62, 92, 178, 69, 5, 143, 180, 166, 236, 111, 80, 170, 146, 151, 55, 88, 214, 192, 205, 196, 81, 91, 246, 45, 223, 132, 166, 185, 136, 166, 185, 125, 199, 168, 45, 131, 104, 212, 69, 42, 152, 161, 182, 108, 87, 66, 144, 182, 140, 65, 161, 238, 121, 182, 94, 177, 238, 208, 127, 93, 233, 42, 150, 109, 219, 7, 242, 149, 57, 32, 180, 223, 187, 76, 72, 155, 100, 139, 13, 75, 243, 175, 69, 6, 119, 112, 134, 209, 176, 76, 23, 64, 207, 130, 34, 180, 185, 53, 139, 12, 20, 173, 21, 235, 205, 44, 58, 116, 43, 179, 184, 48, 105, 138, 152, 69, 134, 166, 142, 185, 233, 34, 35, 2, 9, 36, 160, 170, 139, 139, 138, 90, 84, 224, 106, 81, 129, 71, 144, 46, 181, 184, 88, 57, 121, 84, 130, 22, 181, 232, 88, 212, 162, 2, 79, 139, 14, 79, 139, 10, 244, 119, 33, 199, 164, 113, 90, 92, 144, 164, 117, 154, 22, 23, 16, 165, 105, 209, 225, 50, 33, 77, 139, 11, 143, 64, 88, 153, 22, 23, 173, 104, 101, 90, 108, 104, 102, 203, 226, 2, 245, 120, 37, 196, 150, 69, 9, 95, 204, 223, 114, 89, 108, 180, 92, 22, 21, 28, 84, 113, 107, 80, 177, 137, 23, 131, 6, 94, 218, 135, 251, 119, 84, 83, 73, 162, 112, 70, 194, 73, 13, 154, 43, 218, 38, 0, 193, 226, 0, 28, 211, 111, 56, 36, 2, 106, 19, 128, 96, 145, 161, 77, 0, 130, 69, 5, 101, 217, 208, 39, 69, 83, 199, 61, 14, 50, 60, 223, 234, 8, 240, 174, 111, 130, 163, 85, 210, 118, 61, 212, 46, 90, 221, 227, 32, 164, 105, 243, 56, 184, 240, 72, 252, 57, 184, 208, 218, 158, 131, 11, 86, 63, 136, 191, 229, 30, 156, 33, 173, 88, 173, 88, 127, 68, 129, 180, 98, 35, 180, 98, 149, 237, 59, 124, 101, 107, 214, 27, 86, 78, 90, 191, 57, 168, 69, 45, 217, 115, 144, 209, 118, 65, 220, 60, 7, 25, 207, 206, 23, 146, 148, 203, 127, 221, 58, 14, 105, 170, 238, 13, 174, 235, 117, 219, 236, 69, 89, 243, 156, 67, 63, 151, 183, 236, 103, 224, 19, 142, 34, 158, 151, 231, 224, 194, 55, 109, 210, 29, 194, 221, 191, 131, 11, 186, 17, 34, 72, 168, 226, 132, 30, 141, 194, 234, 159, 227, 252, 19, 79, 133, 123, 193, 59, 59, 143, 32, 135, 166, 45, 55, 255, 88, 210, 118, 65, 52, 109, 25, 167, 182, 16, 119, 63, 89, 77, 170, 26, 214, 202, 228, 105, 191, 37, 83, 71, 234, 164, 76, 20, 105, 61, 23, 135, 43, 114, 74, 132, 243, 175, 241, 253, 239, 224, 195, 253, 59, 168, 240, 216, 249, 165, 160, 8, 15, 7, 183, 108, 223, 118, 49, 84, 113, 170, 0, 132, 90, 20, 65, 4, 2, 163, 92, 132, 135, 231, 164, 101, 138, 224, 202, 178, 161, 38, 31, 233, 100, 223, 0, 238, 86, 226, 2, 152, 232, 199, 4, 144, 248, 42, 213, 233, 52, 224, 106, 121, 210, 236, 160, 13, 95, 209, 71, 252, 42, 73, 198, 40, 135, 248, 189, 209, 228, 51, 92, 215, 243, 159, 116, 169, 63, 24, 136, 136, 240, 48, 75, 209, 186, 204, 182, 101, 239, 26, 65, 173, 46, 147, 55, 180, 50, 204, 54, 95, 224, 146, 33, 166, 75, 33, 140, 149, 132, 235, 191, 101, 251, 63, 209, 98, 239, 224, 195, 98, 239, 160, 194, 228, 45, 119, 112, 33, 221, 65, 199, 114, 7, 27, 223, 24, 109, 7, 33, 19, 156, 131, 10, 156, 115, 80, 129, 147, 56, 73, 74, 231, 32, 163, 117, 81, 216, 65, 6, 143, 32, 127, 84, 242, 236, 160, 195, 159, 29, 71, 207, 14, 54, 114, 61, 118, 80, 66, 215, 190, 179, 131, 139, 199, 49, 59, 184, 160, 180, 178, 12, 122, 248, 130, 174, 37, 213, 59, 146, 48, 59, 200, 224, 198, 14, 42, 120, 4, 181, 54, 42, 151, 71, 27, 59, 232, 0, 209, 171, 148, 177, 131, 139, 10, 218, 199, 64, 27, 181, 189, 40, 99, 7, 29, 206, 167, 67, 202, 216, 193, 134, 99, 143, 154, 145, 50, 118, 212, 98, 7, 25, 206, 13, 189, 47, 59, 216, 192, 35, 16, 90, 54, 236, 32, 67, 162, 236, 160, 2, 4, 233, 120, 74, 230, 32, 163, 89, 28, 108, 120, 4, 130, 47, 7, 25, 26, 123, 196, 193, 133, 165, 32, 9, 92, 248, 42, 237, 117, 112, 209, 186, 233, 36, 183, 14, 66, 60, 191, 106, 23, 59, 242, 72, 4, 2, 3, 107, 194, 132, 54, 39, 228, 170, 153, 66, 225, 138, 222, 105, 118, 65, 84, 203, 229, 52, 235, 32, 67, 223, 196, 193, 129, 132, 127, 167, 147, 156, 75, 29, 27, 124, 210, 165, 142, 12, 170, 173, 106, 169, 35, 131, 23, 33, 126, 189, 100, 26, 117, 132, 248, 199, 18, 85, 199, 133, 219, 138, 5, 73, 84, 29, 27, 74, 213, 209, 161, 84, 29, 31, 13, 190, 26, 253, 146, 58, 52, 59, 66, 64, 32, 153, 64, 130, 8, 46, 169, 99, 195, 89, 118, 186, 6, 127, 167, 89, 111, 139, 240, 193, 129, 187, 46, 150, 234, 40, 225, 148, 163, 2, 245, 92, 144, 68, 57, 50, 90, 43, 214, 61, 57, 58, 60, 206, 240, 72, 195, 61, 157, 28, 33, 144, 180, 77, 178, 135, 243, 106, 41, 168, 115, 105, 80, 170, 84, 27, 98, 22, 45, 64, 209, 157, 32, 254, 182, 14, 154, 119, 146, 142, 46, 232, 161, 105, 139, 12, 13, 21, 69, 248, 128, 187, 133, 227, 151, 184, 42, 244, 236, 184, 196, 90, 143, 65, 135, 227, 228, 232, 112, 93, 184, 238, 111, 218, 167, 240, 181, 255, 16, 210, 200, 72, 142, 143, 214, 79, 142, 144, 165, 173, 165, 147, 80, 114, 116, 60, 204, 58, 66, 22, 199, 0, 120, 211, 74, 58, 46, 84, 210, 81, 193, 99, 162, 10, 233, 36, 29, 25, 58, 73, 71, 5, 164, 233, 165, 35, 131, 111, 187, 26, 159, 60, 29, 33, 238, 233, 168, 208, 234, 92, 58, 50, 56, 233, 211, 61, 238, 9, 2, 100, 98, 177, 119, 26, 48, 162, 177, 71, 216, 241, 35, 109, 233, 216, 208, 170, 90, 58, 50, 120, 235, 50, 235, 158, 95, 128, 194, 142, 163, 167, 243, 125, 106, 233, 232, 192, 210, 89, 58, 50, 60, 204, 46, 150, 142, 11, 150, 142, 143, 247, 85, 200, 35, 16, 120, 33, 232, 90, 6, 130, 53, 35, 9, 179, 131, 28, 36, 190, 10, 249, 218, 116, 92, 88, 57, 81, 212, 142, 134, 248, 2, 157, 44, 246, 8, 21, 109, 180, 43, 218, 104, 79, 168, 247, 67, 91, 142, 124, 46, 166, 1, 81, 38, 29, 31, 173, 78, 106, 47, 215, 67, 101, 25, 180, 168, 69, 17, 64, 128, 182, 147, 73, 199, 8, 85, 233, 168, 32, 81, 142, 210, 37, 56, 50, 40, 109, 205, 186, 4, 71, 70, 235, 164, 37, 56, 50, 72, 112, 132, 120, 120, 57, 85, 72, 130, 99, 35, 130, 227, 35, 226, 219, 46, 255, 135, 243, 75, 65, 46, 217, 251, 75, 167, 226, 64, 122, 69, 30, 169, 52, 50, 60, 82, 105, 84, 112, 9, 253, 93, 168, 210, 184, 168, 52, 6, 128, 210, 24, 0, 87, 247, 124, 123, 141, 140, 246, 26, 27, 77, 155, 246, 26, 25, 92, 219, 107, 92, 104, 175, 189, 198, 7, 18, 168, 189, 70, 200, 107, 100, 52, 78, 168, 189, 198, 69, 106, 143, 39, 106, 175, 177, 225, 158, 214, 55, 50, 172, 111, 116, 184, 137, 55, 42, 232, 90, 242, 17, 68, 127, 159, 55, 50, 184, 55, 42, 112, 224, 234, 158, 111, 208, 181, 36, 133, 23, 211, 8, 153, 192, 188, 241, 49, 129, 121, 163, 130, 171, 40, 82, 204, 152, 58, 148, 158, 222, 232, 208, 245, 186, 113, 193, 147, 107, 116, 240, 8, 132, 165, 19, 110, 100, 184, 91, 212, 205, 208, 90, 177, 173, 225, 164, 13, 1, 81, 197, 205, 161, 255, 184, 241, 193, 143, 84, 123, 220, 200, 80, 199, 141, 13, 206, 167, 123, 230, 70, 198, 51, 55, 58, 92, 194, 220, 184, 224, 107, 255, 73, 232, 155, 60, 223, 137, 131, 135, 217, 213, 188, 8, 175, 238, 169, 147, 86, 159, 112, 79, 174, 138, 27, 242, 6, 92, 21, 55, 87, 197, 173, 177, 71, 36, 156, 79, 183, 128, 86, 172, 235, 90, 70, 173, 125, 139, 171, 127, 4, 197, 179, 26, 35, 119, 143, 184, 215, 82, 87, 90, 160, 21, 235, 72, 151, 106, 239, 193, 210, 37, 151, 9, 254, 199, 64, 107, 197, 179, 178, 37, 50, 24, 64, 231, 178, 22, 235, 226, 133, 101, 226, 198, 135, 79, 234, 14, 92, 63, 211, 42, 109, 23, 161, 147, 197, 62, 241, 232, 249, 91, 182, 101, 173, 175, 74, 194, 93, 251, 45, 220, 248, 128, 176, 161, 59, 185, 113, 129, 169, 214, 168, 240, 206, 14, 106, 141, 14, 11, 175, 165, 53, 54, 52, 195, 26, 21, 18, 107, 124, 240, 163, 181, 158, 67, 137, 53, 78, 160, 196, 26, 35, 18, 107, 132, 96, 141, 142, 196, 26, 33, 110, 53, 42, 104, 203, 80, 67, 4, 38, 128, 64, 49, 117, 219, 216, 80, 233, 40, 220, 26, 239, 52, 219, 248, 96, 204, 118, 170, 211, 8, 73, 117, 26, 21, 154, 23, 167, 165, 105, 100, 84, 212, 137, 176, 52, 141, 139, 138, 54, 218, 104, 105, 26, 27, 21, 117, 208, 210, 52, 46, 150, 166, 81, 65, 82, 76, 163, 66, 132, 8, 80, 96, 130, 196, 52, 46, 38, 16, 193, 4, 17, 36, 166, 113, 241, 235, 59, 129, 48, 144, 52, 83, 180, 145, 65, 146, 152, 57, 180, 151, 162, 141, 12, 205, 15, 109, 52, 209, 70, 198, 195, 172, 63, 237, 68, 79, 191, 54, 46, 104, 239, 179, 125, 109, 92, 80, 184, 157, 215, 198, 197, 162, 36, 109, 157, 215, 198, 6, 132, 127, 75, 247, 124, 39, 115, 137, 182, 159, 11, 226, 240, 21, 253, 70, 5, 173, 236, 69, 188, 185, 91, 29, 252, 225, 97, 246, 93, 27, 23, 170, 50, 37, 131, 222, 181, 241, 241, 174, 141, 10, 147, 141, 80, 201, 46, 136, 55, 118, 174, 141, 13, 254, 205, 174, 162, 16, 86, 63, 137, 213, 207, 213, 115, 87, 34, 127, 9, 230, 45, 252, 220, 78, 89, 54, 231, 33, 38, 139, 31, 24, 183, 235, 90, 6, 210, 171, 82, 83, 213, 212, 169, 106, 160, 40, 233, 209, 220, 74, 132, 144, 132, 13, 7, 165, 147, 126, 87, 74, 51, 232, 170, 168, 208, 138, 109, 104, 239, 130, 138, 170, 182, 13, 99, 101, 227, 125, 199, 21, 112, 82, 166, 246, 32, 84, 251, 117, 178, 216, 145, 246, 42, 93, 235, 148, 101, 163, 80, 150, 45, 153, 74, 118, 53, 195, 189, 234, 193, 157, 146, 65, 88, 240, 14, 111, 126, 215, 212, 18, 97, 196, 211, 175, 157, 239, 15, 47, 185, 161, 201, 87, 4, 121, 215, 199, 205, 48, 20, 37, 173, 149, 104, 130, 54, 46, 152, 111, 187, 92, 190, 217, 105, 163, 163, 97, 173, 116, 60, 160, 107, 25, 212, 246, 1, 105, 202, 229, 208, 3, 23, 13, 169, 78, 27, 29, 212, 74, 176, 161, 191, 77, 105, 157, 240, 195, 215, 202, 132, 88, 54, 109, 92, 224, 154, 210, 121, 174, 236, 116, 234, 168, 105, 187, 54, 251, 31, 60, 48, 94, 144, 78, 144, 97, 162, 106, 98, 245, 67, 109, 155, 109, 70, 250, 204, 158, 153, 54, 58, 52, 78, 91, 89, 166, 141, 141, 84, 202, 180, 145, 161, 178, 253, 201, 150, 244, 5, 137, 95, 63, 151, 35, 73, 39, 123, 156, 212, 86, 170, 180, 85, 237, 107, 188, 211, 69, 56, 159, 236, 209, 30, 107, 205, 174, 68, 234, 214, 53, 109, 149, 74, 135, 214, 210, 244, 64, 211, 184, 223, 31, 232, 122, 205, 64, 223, 202, 137, 239, 66, 128, 43, 245, 71, 0, 87, 237, 25, 192, 39, 84, 91, 164, 107, 201, 31, 128, 164, 237, 74, 128, 54, 201, 22, 253, 75, 223, 172, 0, 239, 92, 139, 21, 226, 134, 190, 237, 106, 96, 177, 119, 24, 211, 56, 36, 92, 159, 219, 51, 51, 16, 46, 105, 187, 22, 108, 132, 153, 66, 42, 18, 171, 135, 75, 82, 123, 105, 64, 7, 46, 153, 54, 58, 112, 179, 178, 2, 161, 3, 226, 113, 9, 179, 243, 120, 10, 240, 228, 82, 165, 149, 92, 218, 8, 81, 182, 169, 101, 91, 246, 150, 132, 126, 42, 108, 88, 57, 105, 253, 182, 218, 184, 224, 12, 244, 223, 178, 205, 13, 60, 72, 58, 25, 132, 146, 237, 168, 72, 172, 30, 222, 180, 85, 169, 142, 186, 199, 209, 70, 7, 140, 151, 115, 168, 8, 173, 255, 153, 220, 65, 69, 208, 182, 235, 161, 34, 52, 119, 43, 90, 141, 54, 66, 86, 163, 141, 10, 30, 105, 180, 241, 209, 84, 194, 255, 244, 4, 54, 208, 230, 132, 32, 207, 185, 202, 130, 101, 167, 171, 120, 147, 139, 129, 0, 225, 159, 106, 63, 114, 149, 135, 175, 63, 43, 91, 162, 247, 68, 174, 210, 180, 97, 180, 122, 167, 171, 104, 163, 141, 144, 5, 19, 205, 15, 215, 253, 86, 197, 105, 219, 67, 14, 168, 54, 58, 214, 254, 107, 248, 125, 206, 144, 224, 200, 241, 93, 203, 115, 11, 158, 70, 27, 29, 108, 131, 154, 54, 143, 74, 80, 163, 141, 11, 248, 98, 126, 70, 27, 25, 143, 202, 91, 140, 54, 50, 84, 120, 169, 52, 252, 81, 73, 163, 240, 3, 195, 175, 13, 4, 6, 73, 23, 163, 141, 16, 79, 218, 197, 14, 53, 62, 36, 163, 141, 10, 171, 80, 3, 163, 141, 142, 117, 217, 16, 163, 141, 12, 70, 27, 21, 104, 122, 233, 149, 167, 218, 235, 111, 234, 45, 151, 203, 38, 221, 181, 109, 147, 158, 77, 186, 163, 242, 212, 122, 236, 81, 121, 202, 185, 93, 12, 170, 184, 43, 83, 231, 23, 52, 188, 22, 112, 208, 252, 219, 152, 33, 73, 98, 133, 38, 42, 139, 57, 57, 26, 167, 84, 136, 46, 13, 148, 58, 233, 115, 89, 224, 252, 147, 88, 151, 168, 242, 41, 81, 163, 242, 148, 54, 46, 52, 123, 67, 52, 84, 86, 110, 218, 229, 216, 250, 131, 181, 50, 33, 138, 166, 255, 185, 24, 135, 180, 50, 33, 247, 132, 15, 157, 239, 74, 27, 23, 187, 18, 106, 36, 86, 218, 184, 32, 73, 188, 148, 54, 46, 36, 137, 153, 36, 241, 98, 166, 173, 195, 215, 161, 223, 223, 74, 200, 119, 149, 54, 46, 40, 125, 84, 105, 227, 226, 83, 106, 163, 10, 47, 74, 27, 33, 254, 218, 73, 29, 125, 74, 165, 141, 14, 20, 221, 201, 251, 174, 110, 132, 112, 210, 39, 229, 33, 181, 199, 17, 3, 132, 17, 169, 61, 142, 22, 37, 59, 105, 35, 164, 105, 218, 200, 96, 45, 39, 109, 100, 232, 101, 39, 105, 227, 66, 87, 74, 116, 72, 152, 160, 199, 139, 54, 46, 220, 115, 209, 70, 134, 214, 73, 106, 35, 195, 171, 58, 142, 24, 255, 243, 130, 176, 129, 243, 233, 86, 106, 227, 3, 91, 11, 77, 219, 214, 73, 65, 191, 18, 200, 175, 100, 127, 31, 210, 182, 75, 219, 46, 213, 118, 45, 100, 225, 213, 160, 94, 165, 82, 27, 33, 46, 187, 129, 31, 45, 74, 73, 237, 127, 96, 249, 56, 60, 26, 137, 91, 141, 149, 90, 154, 42, 58, 105, 138, 84, 106, 67, 72, 218, 115, 212, 73, 153, 28, 238, 161, 212, 75, 151, 92, 214, 74, 164, 20, 92, 246, 123, 229, 169, 201, 126, 182, 93, 141, 251, 217, 211, 178, 75, 226, 170, 154, 86, 52, 232, 242, 254, 241, 227, 252, 82, 156, 95, 138, 55, 117, 52, 97, 231, 247, 189, 19, 138, 238, 100, 223, 99, 180, 245, 243, 93, 253, 69, 44, 74, 153, 240, 23, 161, 254, 40, 212, 223, 254, 122, 175, 141, 90, 10, 90, 224, 160, 58, 117, 81, 109, 230, 32, 9, 183, 191, 152, 127, 241, 227, 97, 246, 61, 249, 159, 182, 223, 128, 237, 71, 176, 85, 166, 206, 68, 69, 223, 245, 39, 26, 59, 78, 151, 122, 147, 255, 208, 228, 47, 249, 159, 223, 30, 95, 251, 79, 155, 19, 162, 232, 78, 208, 90, 82, 221, 82, 220, 82, 118, 37, 52, 241, 230, 132, 154, 137, 55, 19, 111, 188, 175, 218, 215, 232, 122, 222, 117, 61, 207, 251, 227, 182, 253, 18, 102, 7, 105, 147, 205, 19, 170, 184, 45, 229, 5, 65, 60, 146, 180, 239, 21, 75, 65, 78, 63, 237, 231, 123, 231, 123, 91, 231, 218, 118, 73, 58, 157, 71, 84, 83, 8, 2, 105, 20, 86, 170, 118, 57, 211, 10, 8, 211, 229, 152, 42, 78, 57, 105, 153, 144, 246, 251, 108, 205, 122, 46, 170, 214, 27, 154, 25, 162, 176, 227, 20, 218, 233, 56, 3, 10, 171, 116, 24, 43, 244, 190, 171, 61, 158, 12, 124, 210, 165, 238, 168, 115, 105, 240, 247, 92, 72, 210, 118, 33, 199, 211, 97, 172, 254, 113, 55, 212, 91, 39, 20, 43, 117, 178, 111, 165, 98, 198, 224, 129, 89, 164, 180, 247, 77, 208, 70, 107, 49, 179, 6, 79, 46, 109, 70, 59, 155, 169, 232, 98, 180, 223, 147, 95, 162, 242, 148, 246, 218, 212, 56, 105, 107, 127, 182, 123, 220, 13, 69, 79, 208, 213, 52, 252, 200, 113, 171, 179, 99, 229, 55, 63, 120, 4, 194, 179, 158, 151, 131, 72, 218, 46, 164, 54, 151, 139, 216, 181, 218, 164, 115, 247, 77, 219, 86, 213, 236, 208, 212, 161, 160, 103, 53, 110, 93, 216, 155, 80, 197, 238, 136, 155, 47, 249, 13, 22, 165, 60, 59, 142, 40, 186, 19, 6, 172, 106, 39, 252, 73, 39, 15, 212, 190, 215, 120, 155, 182, 238, 121, 7, 158, 212, 180, 211, 181, 149, 164, 236, 47, 64, 3, 173, 110, 41, 110, 41, 205, 196, 189, 153, 56, 243, 242, 52, 191, 3, 157, 139, 106, 143, 37, 92, 182, 5, 90, 255, 243, 129, 118, 58, 190, 228, 163, 197, 223, 148, 53, 90, 0, 167, 232, 78, 82, 59, 145, 50, 233, 223, 249, 78, 161, 147, 93, 236, 29, 7, 142, 167, 210, 86, 154, 173, 255, 44, 205, 4, 53, 51, 120, 77, 25, 114, 207, 49, 163, 156, 75, 167, 77, 58, 94, 168, 233, 54, 55, 80, 204, 86, 42, 102, 18, 95, 209, 71, 186, 96, 65, 219, 116, 210, 163, 18, 138, 238, 4, 105, 39, 37, 145, 2, 84, 146, 148, 68, 78, 85, 210, 252, 153, 86, 181, 109, 205, 139, 54, 114, 46, 219, 34, 231, 18, 181, 93, 128, 49, 87, 48, 230, 10, 212, 210, 33, 109, 146, 49, 224, 184, 147, 203, 165, 232, 78, 22, 123, 39, 229, 162, 84, 25, 148, 246, 93, 58, 13, 124, 58, 9, 41, 125, 102, 27, 244, 218, 60, 160, 105, 39, 173, 142, 120, 177, 54, 238, 120, 46, 102, 91, 41, 186, 235, 18, 57, 233, 149, 249, 138, 54, 164, 124, 97, 36, 36, 109, 23, 90, 155, 140, 146, 61, 105, 250, 60, 193, 76, 91, 164, 105, 39, 234, 224, 83, 53, 240, 8, 226, 119, 79, 127, 202, 87, 197, 201, 250, 0, 205, 50, 109, 34, 109, 123, 140, 113, 98, 64, 49, 106, 169, 43, 45, 240, 43, 65, 223, 233, 164, 212, 178, 151, 9, 199, 115, 57, 127, 230, 229, 89, 140, 43, 71, 15, 47, 206, 191, 68, 206, 249, 139, 40, 85, 199, 1, 179, 191, 94, 196, 132, 39, 204, 170, 118, 18, 177, 82, 49, 219, 202, 178, 166, 140, 43, 26, 228, 42, 101, 253, 60, 149, 53, 168, 98, 3, 216, 231, 73, 169, 196, 147, 173, 230, 201, 229, 144, 238, 63, 47, 222, 207, 138, 93, 153, 120, 53, 126, 93, 42, 113, 6, 75, 243, 114, 36, 193, 4, 53, 86, 42, 81, 187, 35, 93, 75, 170, 100, 3, 168, 100, 199, 195, 172, 164, 237, 90, 119, 187, 150, 34, 215, 182, 203, 117, 165, 39, 41, 179, 139, 129, 215, 197, 164, 54, 14, 79, 69, 46, 136, 156, 52, 150, 120, 196, 61, 194, 154, 153, 198, 30, 65, 141, 61, 194, 141, 61, 194, 58, 146, 168, 249, 87, 194, 236, 32, 165, 223, 30, 51, 228, 187, 64, 233, 103, 210, 118, 170, 144, 239, 50, 214, 183, 149, 48, 59, 136, 1, 198, 169, 245, 219, 62, 215, 154, 137, 75, 152, 29, 180, 64, 147, 173, 80, 182, 92, 213, 94, 101, 155, 135, 196, 155, 182, 206, 192, 181, 73, 86, 225, 229, 157, 102, 87, 235, 90, 217, 180, 54, 201, 214, 113, 39, 164, 77, 50, 8, 109, 146, 65, 152, 237, 165, 118, 213, 115, 65, 141, 211, 62, 124, 69, 219, 4, 32, 88, 9, 179, 131, 26, 222, 147, 31, 61, 59, 235, 18, 61, 59, 18, 102, 167, 213, 85, 51, 5, 61, 56, 205, 58, 175, 108, 169, 3, 97, 233, 176, 87, 219, 13, 63, 185, 144, 218, 110, 36, 76, 144, 54, 3, 215, 166, 121, 206, 218, 64, 68, 144, 77, 54, 140, 167, 162, 226, 49, 77, 181, 147, 163, 181, 94, 4, 99, 133, 24, 119, 133, 106, 51, 16, 150, 205, 154, 153, 198, 200, 95, 98, 7, 73, 226, 133, 252, 37, 94, 220, 46, 9, 157, 48, 67, 110, 151, 218, 110, 80, 133, 151, 138, 46, 137, 95, 9, 106, 114, 253, 50, 141, 125, 234, 185, 104, 47, 227, 236, 116, 45, 227, 160, 107, 25, 231, 83, 25, 125, 19, 58, 25, 122, 79, 70, 226, 93, 98, 105, 101, 93, 211, 166, 147, 39, 87, 235, 132, 39, 158, 78, 213, 30, 47, 168, 185, 91, 155, 230, 17, 196, 143, 30, 79, 133, 112, 220, 9, 61, 158, 218, 64, 27, 87, 56, 110, 181, 150, 108, 154, 108, 215, 109, 254, 148, 12, 98, 204, 152, 73, 18, 43, 199, 3, 13, 92, 221, 243, 142, 219, 41, 160, 91, 231, 251, 148, 168, 105, 147, 88, 53, 109, 18, 67, 156, 244, 184, 193, 187, 126, 83, 12, 180, 113, 5, 133, 4, 139, 189, 131, 84, 83, 13, 84, 83, 206, 36, 2, 17, 128, 0, 77, 246, 65, 83, 46, 231, 203, 236, 68, 41, 158, 5, 202, 246, 245, 83, 233, 167, 250, 84, 249, 169, 22, 80, 219, 13, 202, 79, 21, 1, 117, 46, 13, 124, 149, 191, 102, 239, 55, 241, 4, 49, 241, 212, 224, 170, 51, 161, 95, 165, 205, 104, 35, 253, 212, 134, 112, 239, 219, 107, 110, 188, 141, 182, 3, 77, 46, 6, 61, 253, 237, 49, 133, 27, 154, 104, 20, 86, 223, 158, 51, 243, 9, 103, 230, 252, 232, 113, 156, 26, 168, 90, 255, 199, 146, 180, 43, 53, 70, 139, 57, 249, 55, 245, 198, 9, 173, 132, 33, 175, 41, 251, 138, 126, 171, 240, 178, 152, 31, 125, 227, 244, 208, 244, 155, 55, 94, 111, 202, 86, 161, 214, 182, 249, 141, 211, 2, 172, 151, 105, 19, 251, 175, 76, 123, 20, 173, 206, 142, 121, 35, 7, 235, 49, 205, 139, 155, 221, 229, 90, 17, 84, 113, 82, 30, 129, 240, 73, 25, 70, 145, 170, 245, 111, 186, 171, 129, 107, 219, 133, 28, 203, 149, 58, 23, 212, 180, 213, 90, 7, 17, 144, 71, 190, 211, 113, 187, 24, 164, 250, 225, 89, 218, 118, 41, 219, 71, 13, 28, 94, 83, 134, 90, 155, 126, 75, 115, 132, 1, 124, 69, 27, 210, 181, 228, 127, 235, 202, 87, 165, 184, 55, 78, 219, 180, 108, 253, 84, 205, 170, 228, 213, 158, 54, 51, 186, 72, 52, 109, 215, 243, 142, 38, 128, 64, 61, 23, 85, 156, 58, 151, 6, 173, 180, 170, 181, 111, 97, 160, 243, 115, 2, 73, 237, 252, 138, 62, 210, 138, 245, 6, 15, 82, 123, 252, 241, 253, 255, 150, 215, 148, 161, 220, 94, 144, 219, 74, 123, 41, 218, 17, 206, 73, 120, 227, 133, 208, 79, 182, 20, 206, 51, 51, 71, 146, 182, 75, 0, 223, 186, 30, 19, 13, 180, 173, 44, 67, 173, 170, 41, 242, 198, 74, 61, 160, 41, 45, 240, 64, 0, 65, 162, 227, 83, 106, 59, 41, 181, 177, 17, 65, 27, 21, 232, 90, 82, 27, 33, 141, 14, 143, 32, 70, 53, 50, 104, 197, 54, 110, 153, 16, 163, 26, 33, 109, 153, 212, 200, 248, 85, 154, 26, 25, 222, 119, 165, 198, 135, 212, 216, 80, 109, 213, 70, 202, 246, 81, 106, 116, 240, 37, 31, 165, 198, 197, 63, 86, 183, 20, 71, 169, 241, 225, 78, 223, 81, 106, 100, 164, 198, 6, 210, 138, 117, 109, 148, 26, 37, 26, 47, 74, 141, 13, 169, 241, 145, 26, 31, 94, 83, 134, 82, 227, 66, 251, 189, 107, 131, 82, 163, 131, 53, 51, 40, 117, 106, 74, 182, 66, 169, 177, 209, 44, 211, 32, 212, 93, 174, 197, 170, 50, 121, 167, 106, 186, 50, 161, 212, 248, 208, 79, 182, 45, 81, 106, 116, 164, 198, 0, 56, 254, 150, 198, 5, 74, 54, 58, 30, 197, 11, 162, 100, 35, 195, 35, 168, 233, 36, 27, 25, 173, 248, 208, 181, 108, 69, 23, 131, 90, 23, 232, 103, 35, 68, 210, 9, 250, 108, 92, 164, 227, 217, 184, 176, 52, 29, 151, 141, 11, 39, 61, 126, 199, 55, 187, 227, 207, 61, 180, 105, 101, 207, 114, 46, 187, 105, 217, 8, 129, 84, 212, 89, 107, 25, 198, 142, 180, 159, 140, 253, 43, 160, 207, 111, 66, 5, 30, 92, 53, 83, 32, 26, 60, 210, 96, 250, 251, 128, 48, 86, 45, 27, 25, 92, 69, 85, 203, 198, 133, 106, 217, 232, 200, 198, 135, 166, 151, 238, 248, 212, 94, 68, 37, 106, 72, 197, 178, 113, 33, 213, 194, 178, 145, 193, 35, 175, 177, 116, 35, 132, 171, 228, 94, 155, 141, 17, 21, 76, 54, 42, 44, 38, 27, 21, 148, 147, 124, 49, 59, 196, 100, 35, 131, 201, 70, 5, 188, 42, 27, 33, 204, 147, 130, 218, 46, 217, 232, 136, 208, 24, 128, 198, 30, 65, 141, 14, 200, 144, 175, 76, 117, 45, 169, 32, 244, 249, 77, 104, 175, 129, 223, 4, 132, 188, 9, 216, 240, 200, 226, 183, 189, 9, 184, 144, 191, 84, 77, 240, 90, 16, 197, 90, 251, 38, 224, 194, 227, 245, 66, 146, 180, 111, 2, 54, 36, 105, 223, 33, 241, 124, 238, 180, 12, 233, 123, 208, 230, 119, 117, 82, 186, 190, 9, 232, 240, 180, 190, 9, 16, 198, 10, 81, 88, 165, 227, 168, 245, 77, 192, 7, 157, 158, 111, 2, 50, 184, 79, 64, 5, 15, 179, 62, 1, 33, 250, 38, 16, 28, 15, 61, 1, 25, 60, 50, 97, 2, 42, 180, 46, 11, 175, 9, 184, 240, 133, 35, 19, 112, 33, 50, 1, 3, 192, 139, 38, 160, 194, 162, 9, 168, 96, 2, 2, 224, 211, 235, 111, 88, 75, 29, 71, 141, 157, 127, 28, 66, 124, 85, 28, 42, 248, 218, 127, 19, 247, 207, 33, 195, 35, 168, 213, 241, 231, 176, 209, 150, 211, 239, 130, 242, 83, 57, 254, 28, 66, 120, 4, 130, 227, 207, 225, 194, 31, 61, 204, 246, 115, 232, 104, 176, 216, 59, 136, 113, 51, 72, 2, 25, 239, 218, 207, 33, 67, 191, 70, 218, 207, 97, 195, 249, 149, 250, 142, 178, 191, 162, 15, 104, 115, 66, 14, 216, 72, 247, 28, 42, 168, 226, 244, 180, 231, 144, 65, 97, 213, 150, 242, 208, 181, 164, 227, 207, 161, 181, 94, 99, 233, 68, 87, 43, 163, 202, 241, 153, 208, 90, 207, 33, 35, 37, 207, 161, 130, 100, 37, 197, 109, 191, 163, 230, 185, 205, 5, 225, 80, 201, 207, 104, 149, 180, 93, 168, 121, 14, 29, 254, 104, 238, 214, 166, 77, 243, 28, 66, 188, 255, 19, 53, 207, 97, 163, 121, 14, 21, 120, 4, 97, 99, 121, 109, 121, 14, 33, 239, 250, 72, 169, 227, 228, 237, 53, 224, 182, 145, 118, 45, 207, 161, 195, 195, 180, 33, 168, 246, 187, 195, 135, 75, 214, 208, 149, 137, 23, 66, 84, 251, 145, 187, 195, 197, 251, 46, 228, 14, 23, 221, 14, 21, 104, 239, 155, 224, 104, 218, 66, 76, 154, 67, 67, 219, 116, 210, 227, 233, 215, 134, 224, 217, 241, 166, 218, 201, 21, 128, 34, 16, 64, 37, 185, 24, 8, 116, 192, 27, 13, 220, 23, 167, 5, 60, 194, 188, 133, 159, 159, 219, 161, 131, 162, 59, 129, 240, 93, 13, 28, 44, 85, 28, 225, 146, 61, 42, 89, 175, 255, 185, 182, 79, 243, 251, 58, 19, 217, 44, 104, 246, 126, 201, 113, 43, 6, 174, 90, 50, 70, 177, 228, 90, 156, 80, 115, 247, 50, 117, 9, 199, 237, 156, 179, 227, 118, 184, 104, 87, 209, 6, 111, 126, 36, 34, 16, 62, 160, 166, 45, 133, 126, 54, 233, 184, 29, 130, 64, 135, 234, 172, 96, 248, 187, 103, 185, 68, 239, 222, 49, 110, 135, 12, 15, 179, 107, 101, 66, 15, 179, 14, 79, 75, 244, 48, 235, 44, 120, 145, 234, 92, 148, 36, 229, 2, 1, 194, 3, 8, 142, 167, 3, 65, 125, 159, 150, 16, 212, 23, 241, 79, 252, 99, 101, 251, 235, 95, 125, 229, 190, 196, 211, 255, 244, 107, 47, 240, 85, 218, 79, 223, 132, 6, 157, 172, 247, 75, 218, 200, 29, 183, 107, 254, 151, 180, 162, 104, 241, 130, 135, 89, 85, 220, 36, 222, 105, 118, 49, 109, 157, 148, 10, 81, 72, 125, 89, 219, 116, 28, 92, 146, 114, 137, 88, 43, 147, 68, 171, 36, 181, 151, 7, 253, 92, 104, 173, 68, 13, 112, 225, 159, 239, 40, 53, 200, 159, 58, 175, 140, 219, 225, 4, 227, 118, 248, 104, 26, 225, 118, 200, 160, 44, 27, 250, 164, 17, 110, 247, 208, 185, 32, 6, 22, 164, 233, 41, 110, 135, 15, 14, 26, 67, 120, 211, 137, 36, 105, 191, 162, 141, 194, 223, 183, 226, 48, 129, 43, 122, 224, 162, 225, 133, 18, 183, 67, 198, 243, 194, 237, 144, 97, 53, 16, 116, 225, 118, 200, 208, 207, 201, 194, 35, 159, 82, 59, 39, 25, 65, 105, 31, 24, 43, 148, 246, 145, 76, 157, 228, 72, 130, 249, 182, 67, 136, 71, 244, 233, 155, 240, 252, 54, 111, 59, 124, 136, 48, 56, 133, 174, 37, 27, 190, 206, 163, 111, 219, 140, 79, 169, 221, 180, 133, 55, 78, 235, 168, 232, 98, 148, 109, 59, 108, 112, 167, 100, 16, 86, 78, 180, 98, 221, 177, 43, 61, 36, 105, 223, 4, 103, 232, 81, 182, 223, 78, 67, 115, 183, 230, 167, 122, 84, 120, 121, 30, 30, 72, 165, 33, 17, 161, 191, 116, 226, 12, 188, 98, 91, 54, 205, 3, 200, 73, 70, 154, 29, 49, 80, 81, 151, 20, 223, 118, 185, 62, 168, 78, 213, 242, 29, 53, 203, 52, 118, 32, 73, 234, 36, 215, 127, 22, 212, 186, 255, 218, 14, 29, 190, 169, 107, 59, 92, 168, 226, 166, 237, 208, 1, 169, 166, 78, 219, 33, 68, 37, 47, 109, 135, 12, 22, 29, 152, 67, 74, 27, 33, 218, 174, 29, 50, 124, 106, 155, 180, 29, 50, 156, 38, 109, 135, 14, 10, 33, 220, 227, 110, 80, 203, 254, 6, 95, 209, 111, 14, 13, 159, 82, 219, 33, 131, 227, 207, 33, 109, 135, 12, 143, 52, 109, 42, 239, 28, 54, 158, 206, 94, 118, 245, 59, 135, 20, 12, 183, 115, 184, 224, 75, 21, 179, 93, 28, 116, 45, 195, 178, 155, 92, 229, 64, 99, 95, 245, 92, 4, 192, 96, 165, 39, 234, 1, 124, 229, 1, 109, 78, 232, 65, 155, 86, 38, 129, 98, 143, 6, 141, 61, 2, 81, 109, 25, 39, 189, 50, 13, 14, 218, 16, 111, 188, 159, 116, 66, 63, 215, 226, 7, 95, 251, 111, 193, 154, 224, 139, 82, 144, 42, 110, 12, 48, 64, 173, 239, 82, 53, 224, 146, 45, 176, 40, 39, 61, 118, 184, 208, 148, 253, 236, 112, 33, 73, 233, 158, 29, 46, 116, 217, 78, 46, 233, 100, 239, 172, 153, 225, 109, 201, 212, 218, 183, 188, 179, 91, 236, 157, 5, 139, 189, 131, 26, 118, 60, 130, 26, 118, 28, 169, 255, 44, 218, 141, 118, 243, 238, 184, 236, 6, 82, 81, 213, 182, 98, 173, 165, 76, 180, 101, 147, 149, 19, 138, 213, 175, 146, 223, 30, 7, 110, 38, 77, 219, 111, 246, 135, 239, 100, 222, 57, 65, 186, 28, 219, 103, 79, 87, 38, 125, 98, 125, 251, 131, 244, 148, 54, 211, 230, 175, 184, 7, 73, 219, 133, 158, 150, 200, 61, 87, 66, 172, 158, 186, 123, 26, 76, 154, 162, 87, 215, 174, 44, 163, 237, 26, 104, 202, 229, 24, 72, 82, 123, 65, 218, 164, 83, 45, 157, 111, 118, 90, 39, 188, 0, 131, 231, 55, 165, 227, 6, 86, 191, 5, 170, 56, 109, 123, 222, 180, 101, 251, 5, 96, 224, 201, 229, 124, 21, 210, 138, 109, 15, 164, 90, 251, 169, 61, 78, 177, 50, 241, 26, 128, 11, 128, 101, 167, 91, 224, 160, 49, 131, 116, 189, 78, 167, 105, 188, 190, 169, 90, 7, 149, 156, 188, 38, 150, 92, 30, 113, 52, 147, 182, 221, 236, 22, 120, 245, 180, 202, 178, 153, 71, 37, 200, 61, 45, 131, 197, 222, 201, 165, 233, 138, 58, 15, 222, 234, 150, 58, 217, 48, 104, 102, 246, 33, 166, 239, 143, 163, 111, 14, 180, 117, 126, 41, 20, 186, 150, 245, 157, 32, 127, 137, 153, 202, 50, 200, 155, 183, 84, 116, 57, 3, 93, 175, 145, 106, 250, 21, 135, 73, 211, 199, 83, 39, 28, 33, 93, 203, 99, 2, 40, 245, 215, 158, 170, 231, 29, 211, 207, 180, 203, 61, 181, 128, 3, 213, 153, 124, 213, 62, 200, 85, 202, 16, 227, 247, 71, 193, 238, 9, 65, 190, 162, 205, 61, 33, 247, 180, 160, 117, 210, 24, 180, 101, 16, 75, 211, 186, 92, 63, 151, 41, 233, 162, 205, 128, 98, 182, 192, 66, 89, 30, 145, 142, 91, 137, 42, 234, 52, 120, 178, 27, 212, 188, 56, 237, 74, 15, 7, 76, 23, 137, 103, 238, 247, 87, 119, 135, 103, 57, 215, 144, 90, 78, 84, 162, 5, 220, 211, 59, 175, 124, 103, 135, 15, 77, 219, 198, 251, 235, 236, 212, 217, 97, 195, 217, 161, 130, 214, 214, 50, 61, 252, 155, 29, 46, 180, 170, 182, 204, 55, 59, 116, 232, 155, 160, 149, 223, 172, 12, 114, 232, 160, 212, 31, 250, 102, 135, 141, 86, 127, 19, 237, 84, 16, 131, 135, 167, 95, 219, 129, 162, 105, 171, 248, 129, 174, 180, 238, 216, 33, 163, 117, 153, 85, 204, 144, 86, 172, 163, 74, 50, 212, 234, 28, 59, 116, 160, 206, 14, 237, 51, 115, 236, 176, 161, 236, 212, 241, 63, 139, 29, 50, 96, 235, 248, 179, 216, 33, 195, 181, 237, 250, 101, 8, 39, 154, 182, 206, 237, 74, 8, 33, 148, 58, 190, 139, 29, 50, 116, 45, 131, 142, 86, 138, 238, 4, 49, 220, 184, 89, 8, 135, 111, 203, 14, 27, 116, 45, 195, 14, 25, 34, 17, 216, 185, 230, 80, 162, 237, 130, 16, 178, 236, 170, 224, 6, 121, 120, 164, 105, 14, 23, 154, 195, 0, 84, 152, 67, 5, 191, 141, 153, 63, 142, 34, 48, 101, 17, 152, 67, 7, 253, 124, 238, 134, 23, 138, 192, 28, 54, 68, 96, 14, 29, 141, 43, 137, 34, 48, 135, 12, 108, 52, 188, 28, 205, 221, 138, 16, 199, 203, 161, 194, 251, 46, 135, 11, 173, 21, 93, 14, 25, 42, 204, 104, 47, 35, 161, 205, 15, 236, 45, 227, 118, 18, 154, 64, 7, 99, 238, 39, 151, 67, 70, 179, 247, 45, 151, 67, 8, 109, 187, 158, 197, 14, 241, 163, 206, 150, 203, 161, 3, 181, 185, 92, 83, 46, 135, 13, 143, 172, 196, 225, 2, 51, 109, 27, 137, 195, 133, 123, 168, 145, 56, 100, 104, 36, 14, 27, 205, 58, 152, 198, 9, 53, 18, 135, 12, 9, 120, 146, 56, 92, 240, 8, 4, 16, 120, 146, 56, 100, 48, 237, 202, 58, 100, 240, 8, 132, 93, 56, 191, 204, 25, 102, 21, 90, 90, 89, 135, 142, 108, 155, 124, 29, 46, 92, 63, 123, 29, 46, 250, 233, 191, 186, 117, 216, 224, 69, 234, 214, 225, 226, 189, 173, 195, 5, 87, 239, 20, 251, 235, 177, 8, 247, 239, 52, 82, 229, 226, 244, 248, 102, 247, 41, 251, 125, 73, 196, 109, 29, 54, 224, 182, 14, 21, 28, 116, 37, 109, 29, 50, 228, 182, 117, 184, 208, 180, 117, 168, 224, 17, 102, 43, 186, 14, 25, 218, 164, 63, 181, 14, 25, 170, 253, 106, 29, 46, 214, 90, 70, 251, 125, 90, 199, 56, 173, 195, 6, 167, 117, 232, 120, 22, 36, 73, 235, 176, 33, 211, 58, 84, 112, 28, 58, 90, 255, 83, 33, 231, 56, 148, 104, 118, 156, 179, 227, 208, 65, 27, 59, 14, 23, 42, 219, 148, 29, 135, 139, 95, 251, 22, 109, 135, 36, 60, 194, 248, 91, 58, 14, 27, 238, 233, 26, 135, 12, 174, 113, 216, 136, 176, 38, 56, 106, 28, 46, 22, 167, 198, 161, 3, 53, 14, 33, 26, 135, 14, 212, 56, 108, 160, 198, 97, 99, 2, 223, 22, 53, 14, 23, 141, 195, 134, 195, 134, 123, 104, 28, 82, 52, 14, 3, 80, 161, 14, 21, 72, 90, 54, 196, 243, 219, 233, 254, 233, 231, 98, 84, 33, 137, 180, 142, 147, 111, 4, 130, 178, 71, 225, 134, 52, 181, 100, 205, 76, 171, 195, 135, 206, 37, 66, 181, 116, 84, 155, 65, 159, 20, 194, 37, 204, 78, 5, 92, 210, 201, 16, 211, 234, 208, 241, 164, 86, 135, 11, 30, 113, 234, 112, 65, 253, 177, 243, 77, 29, 54, 90, 91, 83, 135, 12, 170, 169, 67, 5, 166, 14, 21, 120, 164, 81, 135, 11, 106, 81, 171, 58, 92, 168, 58, 124, 48, 202, 97, 227, 125, 21, 114, 120, 132, 81, 14, 23, 222, 147, 81, 14, 25, 150, 93, 136, 81, 14, 23, 159, 28, 58, 24, 43, 20, 225, 118, 149, 197, 225, 227, 27, 34, 48, 74, 178, 56, 100, 144, 40, 71, 239, 218, 104, 165, 123, 210, 61, 233, 112, 225, 47, 29, 66, 86, 78, 156, 119, 38, 130, 243, 233, 112, 193, 35, 16, 22, 94, 13, 226, 167, 208, 212, 143, 189, 194, 237, 168, 172, 115, 67, 59, 207, 200, 254, 138, 42, 203, 246, 21, 69, 11, 212, 51, 53, 156, 79, 135, 14, 231, 211, 161, 130, 147, 62, 29, 54, 168, 102, 202, 67, 143, 19, 114, 79, 135, 19, 173, 255, 75, 121, 136, 166, 18, 254, 167, 209, 207, 126, 23, 64, 132, 149, 137, 151, 131, 243, 47, 93, 170, 245, 148, 72, 167, 46, 200, 253, 59, 20, 17, 104, 16, 1, 2, 6, 206, 191, 228, 5, 17, 32, 36, 204, 139, 166, 100, 34, 24, 191, 191, 132, 240, 65, 61, 211, 202, 228, 43, 147, 167, 195, 135, 71, 30, 93, 140, 109, 99, 109, 63, 198, 178, 171, 37, 139, 249, 81, 183, 254, 39, 67, 61, 97, 241, 232, 98, 52, 246, 136, 59, 191, 20, 36, 65, 58, 146, 32, 66, 181, 31, 69, 16, 64, 211, 182, 1, 9, 24, 58, 245, 249, 109, 79, 187, 249, 211, 126, 62, 197, 171, 167, 110, 125, 151, 42, 145, 51, 168, 129, 55, 205, 161, 40, 238, 233, 148, 101, 67, 141, 61, 194, 64, 89, 54, 148, 182, 149, 189, 230, 140, 17, 209, 246, 115, 61, 42, 105, 52, 246, 200, 183, 6, 196, 45, 117, 22, 94, 142, 213, 15, 45, 207, 89, 253, 16, 59, 237, 5, 202, 178, 161, 181, 30, 243, 224, 224, 248, 174, 166, 169, 67, 219, 100, 172, 24, 102, 29, 219, 15, 229, 167, 114, 72, 187, 152, 198, 205, 235, 241, 21, 85, 156, 22, 171, 64, 76, 124, 99, 134, 45, 18, 205, 206, 63, 8, 54, 248, 194, 215, 213, 121, 124, 83, 135, 95, 228, 82, 61, 19, 98, 30, 113, 64, 160, 196, 42, 168, 119, 186, 116, 233, 176, 241, 154, 150, 14, 23, 88, 146, 14, 21, 254, 39, 48, 196, 78, 163, 249, 19, 29, 84, 211, 86, 155, 116, 216, 224, 208, 241, 78, 179, 141, 84, 58, 108, 168, 116, 168, 64, 61, 130, 86, 63, 8, 15, 179, 13, 156, 207, 8, 199, 211, 65, 16, 92, 120, 4, 105, 74, 135, 139, 148, 14, 33, 41, 29, 62, 104, 37, 33, 38, 54, 161, 7, 2, 8, 210, 33, 99, 173, 100, 90, 63, 41, 200, 225, 163, 153, 65, 191, 73, 65, 14, 29, 79, 63, 114, 184, 192, 154, 25, 124, 244, 3, 57, 116, 40, 99, 127, 109, 228, 112, 225, 112, 225, 112, 161, 110, 29, 114, 200, 224, 146, 33, 231, 112, 225, 169, 200, 133, 28, 50, 252, 34, 135, 28, 66, 158, 181, 178, 45, 131, 28, 54, 152, 89, 6, 57, 100, 120, 218, 225, 194, 117, 91, 29, 57, 41, 21, 57, 108, 104, 101, 84, 33, 135, 11, 228, 150, 166, 150, 9, 185, 212, 30, 79, 228, 176, 225, 105, 137, 28, 46, 32, 68, 235, 132, 81, 33, 181, 199, 209, 115, 255, 24, 33, 90, 159, 92, 255, 24, 25, 223, 84, 253, 99, 92, 188, 63, 70, 5, 39, 61, 70, 133, 101, 151, 123, 70, 134, 138, 51, 62, 156, 79, 231, 140, 12, 46, 155, 51, 46, 48, 46, 212, 25, 27, 207, 90, 150, 12, 114, 198, 134, 51, 6, 96, 101, 219, 102, 155, 145, 81, 209, 70, 27, 53, 205, 216, 88, 217, 86, 53, 227, 194, 83, 219, 29, 183, 91, 128, 148, 182, 170, 102, 164, 200, 86, 150, 9, 253, 46, 205, 216, 96, 81, 186, 82, 54, 35, 35, 155, 81, 129, 191, 99, 84, 240, 8, 90, 9, 65, 134, 2, 48, 71, 70, 235, 59, 198, 133, 199, 157, 99, 92, 104, 221, 126, 237, 136, 213, 207, 35, 16, 212, 61, 255, 104, 218, 166, 236, 151, 240, 35, 36, 225, 127, 32, 225, 119, 119, 141, 107, 234, 24, 23, 106, 213, 54, 117, 140, 12, 58, 217, 166, 142, 209, 193, 31, 73, 120, 104, 39, 165, 185, 129, 166, 141, 42, 78, 104, 1, 87, 247, 252, 154, 192, 143, 214, 101, 63, 191, 253, 146, 58, 240, 100, 234, 36, 7, 141, 119, 57, 150, 254, 176, 192, 131, 214, 212, 202, 9, 53, 216, 149, 144, 54, 173, 76, 155, 86, 214, 208, 216, 35, 14, 72, 154, 41, 202, 178, 153, 8, 134, 89, 72, 235, 191, 58, 98, 128, 15, 77, 29, 163, 68, 83, 199, 168, 176, 210, 49, 66, 84, 251, 209, 110, 242, 68, 204, 216, 176, 175, 169, 55, 198, 5, 55, 70, 5, 223, 30, 179, 198, 184, 88, 141, 209, 241, 172, 198, 168, 160, 108, 191, 237, 210, 198, 184, 80, 182, 143, 180, 49, 50, 180, 49, 163, 163, 49, 50, 52, 53, 70, 133, 166, 239, 187, 208, 107, 67, 141, 177, 161, 49, 58, 84, 155, 65, 146, 212, 133, 126, 43, 138, 26, 99, 132, 71, 32, 252, 50, 198, 197, 243, 142, 169, 195, 241, 84, 188, 16, 132, 83, 85, 232, 151, 49, 54, 152, 109, 26, 198, 200, 160, 255, 186, 148, 49, 50, 42, 188, 184, 100, 104, 49, 62, 44, 198, 0, 36, 181, 140, 15, 39, 189, 50, 15, 115, 24, 27, 12, 180, 57, 161, 180, 189, 36, 131, 246, 45, 80, 42, 145, 166, 101, 24, 70, 136, 214, 87, 198, 138, 145, 241, 180, 190, 9, 143, 79, 197, 8, 113, 212, 120, 23, 248, 149, 240, 39, 198, 197, 202, 79, 140, 11, 204, 174, 196, 184, 144, 24, 29, 204, 91, 24, 21, 188, 49, 8, 29, 148, 23, 70, 133, 137, 39, 196, 143, 220, 225, 201, 238, 108, 230, 109, 52, 52, 10, 123, 58, 222, 172, 26, 133, 95, 155, 1, 48, 110, 70, 162, 129, 66, 128, 240, 235, 11, 209, 10, 213, 102, 218, 123, 120, 152, 85, 237, 247, 166, 19, 241, 83, 48, 172, 21, 235, 254, 168, 196, 193, 37, 67, 207, 79, 56, 158, 78, 4, 131, 6, 165, 15, 143, 123, 32, 132, 59, 110, 135, 34, 11, 99, 67, 61, 19, 138, 44, 140, 12, 8, 22, 70, 5, 90, 177, 205, 17, 30, 206, 191, 100, 92, 144, 172, 164, 32, 79, 198, 136, 214, 137, 39, 35, 196, 147, 177, 193, 147, 209, 209, 120, 37, 200, 147, 209, 193, 147, 81, 193, 164, 233, 100, 92, 72, 79, 67, 5, 149, 134, 1, 88, 151, 78, 105, 200, 208, 20, 109, 250, 53, 104, 104, 19, 128, 96, 209, 87, 94, 195, 198, 87, 94, 67, 133, 149, 173, 236, 53, 92, 112, 47, 236, 53, 100, 80, 202, 104, 11, 226, 111, 8, 113, 10, 183, 243, 143, 181, 94, 67, 71, 74, 94, 195, 134, 229, 53, 84, 208, 36, 197, 27, 46, 120, 67, 134, 243, 134, 10, 180, 115, 37, 85, 222, 176, 209, 19, 26, 42, 112, 215, 176, 209, 176, 129, 11, 39, 173, 67, 65, 204, 173, 149, 168, 189, 126, 180, 215, 238, 124, 186, 134, 243, 233, 190, 45, 82, 237, 71, 43, 153, 107, 8, 193, 92, 195, 135, 183, 42, 115, 13, 39, 188, 241, 254, 75, 205, 12, 98, 174, 225, 2, 166, 49, 163, 109, 195, 11, 237, 106, 112, 253, 84, 238, 161, 8, 230, 26, 54, 126, 213, 75, 196, 28, 115, 13, 23, 204, 53, 84, 224, 26, 6, 160, 245, 115, 33, 213, 18, 81, 184, 97, 3, 58, 158, 231, 134, 10, 238, 243, 125, 159, 27, 54, 120, 33, 72, 82, 58, 125, 110, 248, 64, 134, 71, 32, 56, 213, 229, 220, 208, 225, 139, 110, 184, 39, 8, 75, 213, 161, 52, 147, 228, 14, 207, 232, 66, 15, 141, 196, 97, 195, 195, 236, 59, 110, 184, 208, 38, 253, 161, 119, 220, 208, 49, 65, 4, 199, 220, 112, 225, 209, 24, 228, 116, 37, 204, 13, 23, 206, 184, 161, 2, 227, 198, 184, 33, 3, 55, 100, 124, 107, 15, 141, 89, 163, 112, 147, 172, 165, 20, 138, 229, 253, 131, 197, 236, 144, 123, 166, 180, 211, 241, 111, 118, 234, 14, 212, 118, 163, 154, 186, 3, 13, 124, 179, 99, 160, 117, 109, 50, 39, 165, 238, 82, 197, 13, 31, 44, 74, 65, 30, 129, 200, 69, 55, 242, 219, 44, 40, 188, 24, 8, 77, 191, 162, 16, 156, 170, 132, 185, 194, 109, 5, 99, 177, 124, 244, 119, 161, 197, 42, 13, 21, 95, 149, 197, 236, 144, 171, 212, 118, 30, 181, 74, 154, 29, 68, 48, 65, 5, 181, 165, 188, 54, 148, 150, 189, 44, 162, 120, 47, 2, 164, 19, 163, 79, 217, 234, 174, 191, 46, 209, 243, 130, 88, 118, 163, 75, 127, 129, 147, 94, 153, 86, 247, 220, 86, 32, 87, 197, 9, 69, 248, 83, 254, 249, 15, 199, 93, 146, 114, 57, 32, 72, 167, 21, 169, 90, 251, 14, 144, 123, 186, 123, 122, 35, 61, 2, 225, 207, 130, 26, 76, 208, 246, 103, 85, 39, 47, 142, 167, 146, 104, 203, 84, 34, 165, 237, 144, 18, 38, 37, 170, 159, 119, 140, 231, 213, 60, 139, 93, 227, 84, 177, 3, 129, 243, 47, 185, 105, 235, 159, 109, 66, 146, 216, 73, 255, 26, 89, 11, 81, 139, 180, 173, 147, 146, 61, 72, 214, 91, 208, 179, 154, 134, 87, 4, 19, 84, 80, 229, 181, 93, 14, 234, 28, 73, 142, 17, 193, 4, 223, 86, 25, 104, 218, 137, 170, 141, 224, 254, 29, 135, 90, 199, 109, 69, 195, 61, 13, 132, 169, 96, 124, 241, 244, 107, 71, 112, 156, 244, 202, 56, 62, 93, 197, 31, 86, 78, 212, 155, 92, 12, 3, 93, 203, 160, 73, 211, 103, 57, 109, 180, 54, 153, 34, 85, 220, 156, 166, 138, 178, 8, 229, 159, 239, 13, 59, 244, 102, 191, 75, 34, 171, 26, 38, 170, 186, 161, 88, 114, 72, 2, 167, 13, 168, 228, 138, 136, 3, 132, 68, 242, 76, 162, 70, 73, 142, 41, 197, 148, 162, 162, 218, 0, 51, 18, 200, 97, 35, 113, 56, 26, 11, 3, 73, 22, 237, 1, 20, 128, 2, 11, 10, 197, 57, 237, 74, 18, 131, 36, 133, 138, 4, 4, 32, 3, 32, 1, 160, 0, 202, 5, 25, 88, 80, 108, 229, 116, 212, 199, 83, 154, 70, 218, 47, 23, 43, 66, 24, 212, 49, 246, 133, 246, 114, 219, 107, 207, 51, 13, 240, 61, 228, 153, 1, 201, 127, 238, 54, 60, 224, 38, 190, 105, 108, 181, 160, 198, 73, 159, 69, 4, 184, 13, 22, 57, 183, 4, 88, 110, 186, 46, 161, 202, 138, 254, 93, 79, 10, 7, 232, 252, 70, 80, 41, 74, 232, 241, 175, 54, 129, 81, 18, 103, 140, 64, 127, 208, 134, 126, 79, 65, 178, 6, 37, 173, 21, 60, 246, 46, 57, 234, 16, 109, 117, 139, 26, 251, 201, 9, 76, 180, 223, 14, 158, 143, 253, 103, 209, 7, 81, 21, 74, 161, 112, 245, 40, 202, 206, 142, 201, 43, 183, 186, 247, 75, 203, 227, 214, 192, 173, 26, 16, 118, 125, 130, 107, 130, 20, 210, 59, 137, 204, 5, 134, 71, 170, 34, 28, 77, 187, 65, 134, 153, 221, 102, 22, 137, 19, 146, 89, 121, 160, 123, 167, 193, 172, 104, 92, 112, 141, 195, 191, 248, 131, 125, 114, 117, 97, 15, 10, 79, 84, 50, 156, 242, 96, 129, 63, 17, 224, 192, 61, 90, 16, 74, 207, 236, 187, 120, 3, 89, 243, 53, 36, 135, 46, 160, 156, 126, 167, 238, 51, 156, 104, 60, 170, 181, 187, 24, 28, 99, 112, 9, 69, 163, 207, 157, 149, 60, 15, 176, 126, 109, 103, 59, 22, 163, 85, 122, 34, 169, 85, 146, 246, 67, 135, 73, 2, 13, 28, 166, 225, 80, 19, 172, 193, 210, 143, 66, 250, 129, 197, 103, 45, 249, 137, 180, 225, 3, 152, 208, 23, 7, 94, 198, 29, 226, 233, 10, 128, 67, 233, 213, 88, 106, 214, 76, 124, 235, 88, 80, 66, 124, 175, 72, 184, 233, 113, 232, 161, 22, 166, 134, 14, 92, 19, 152, 227, 22, 37, 49, 139, 110, 207, 254, 74, 86, 195, 129, 49, 58, 106, 71, 67, 137, 213, 67, 168, 111, 132, 253, 62, 134, 220, 185, 86, 83, 76, 215, 64, 164, 171, 42, 10, 217, 20, 211, 129, 219, 133, 126, 63, 37, 216, 182, 5, 184, 214, 178, 138, 69, 61, 180, 112, 181, 248, 172, 153, 181, 66, 159, 193, 83, 250, 231, 17, 80, 138, 142, 161, 163, 200, 220, 62, 178, 111, 182, 188, 43, 208, 174, 87, 60, 136, 95, 236, 46, 37, 2, 153, 30, 49, 107, 33, 69, 182, 88, 138, 67, 125, 179, 61, 23, 126, 27, 246, 111, 181, 94, 31, 241, 173, 1, 245, 32, 177, 247, 218, 85, 17, 6, 15, 158, 121, 150, 149, 55, 134, 141, 26, 93, 32, 101, 97, 62, 148, 237, 148, 191, 204, 8, 79, 6, 45, 246, 134, 59, 78, 188, 81, 109, 30, 102, 185, 92, 26, 31, 100, 85, 33, 89, 183, 64, 40, 72, 159, 102, 187, 225, 197, 209, 137, 183, 207, 71, 196, 252, 18, 8, 181, 171, 62, 2, 230, 27, 49, 241, 129, 160, 217, 204, 236, 134, 131, 184, 141, 132, 58, 34, 132, 5, 153, 234, 166, 4, 95, 196, 151, 115, 221, 72, 80, 4, 100, 212, 127, 158, 47, 26, 48, 129, 232, 9, 237, 71, 2, 139, 56, 0, 198, 96, 194, 110, 99, 56, 209, 203, 16, 178, 201, 192, 203, 206, 54, 199, 254, 135, 43, 83, 153, 187, 135, 218, 167, 74, 8, 214, 95, 44, 163, 211, 150, 81, 32, 238, 28, 240, 177, 82, 154, 0, 243, 129, 85, 224, 157, 187, 187, 216, 7, 197, 95, 21, 29, 92, 126, 75, 82, 65, 114, 98, 241, 2, 110, 25, 12, 137, 96, 156, 174, 102, 155, 41, 170, 190, 97, 59, 239, 0, 225, 22, 9, 32, 235, 26, 146, 190, 35, 45, 113, 0, 94, 132, 34, 170, 175, 22, 24, 136, 202, 193, 16, 93, 157, 170, 209, 233, 71, 153, 209, 45, 6, 217, 116, 35, 162, 52, 42, 232, 179, 232, 32, 47, 36, 227, 159, 99, 113, 95, 118, 159, 216, 203, 141, 136, 179, 248, 111, 187, 69, 198, 104, 127, 228, 217, 83, 14, 164, 121, 190, 182, 191, 114, 70, 71, 236, 180, 244, 240, 239, 87, 235, 243, 38, 163, 91, 216, 182, 125, 207, 79, 63, 27, 87, 133, 95, 64, 113, 150, 70, 195, 180, 117, 236, 242, 187, 74, 27, 236, 165, 241, 124, 175, 221, 214, 115, 133, 249, 148, 67, 4, 174, 218, 7, 207, 142, 13, 67, 43, 100, 48, 136, 206, 127, 23, 137, 1, 133, 165, 77, 75, 153, 102, 71, 127, 208, 78, 30, 248, 61, 23, 51, 235, 173, 25, 38, 129, 160, 178, 48, 249, 75, 175, 134, 45, 245, 200, 11, 218, 89, 243, 248, 86, 198, 99, 69, 122, 244, 41, 148, 71, 143, 210, 25, 68, 135, 149, 44, 48, 195, 90, 26, 65, 158, 226, 40, 56, 143, 59, 250, 64, 38, 79, 59, 93, 218, 84, 183, 57, 93, 243, 176, 187, 163, 8, 144, 147, 106, 112, 171, 37, 129, 151, 85, 49, 140, 95, 28, 185, 160, 118, 174, 241, 179, 238, 12, 84, 33, 135, 17, 187, 168, 197, 234, 35, 12, 133, 224, 157, 60, 237, 115, 84, 62, 162, 91, 157, 130, 71, 129, 122, 179, 72, 208, 176, 247, 77, 115, 169, 71, 37, 243, 26, 250, 178, 50, 106, 193, 219, 159, 80, 35, 88, 19, 137, 20, 18, 230, 78, 219, 10, 127, 197, 51, 127, 132, 30, 33, 13, 210, 194, 71, 231, 181, 118, 137, 136, 86, 252, 183, 57, 26, 108, 95, 140, 120, 1, 70, 232, 242, 26, 105, 70, 34, 65, 40, 210, 111, 74, 188, 210, 225, 81, 222, 64, 240, 126, 148, 147, 198, 11, 254, 49, 42, 14, 80, 226, 24, 33, 114, 33, 26, 130, 8, 29, 133, 98, 83, 183, 148, 90, 156, 121, 98, 182, 172, 147, 66, 126, 76, 56, 39, 181, 102, 227, 163, 225, 127, 164, 118, 233, 53, 202, 183, 252, 122, 77, 85, 179, 252, 24, 202, 28, 64, 64, 162, 244, 117, 202, 57, 181, 104, 17, 247, 167, 91, 240, 188, 26, 27, 109, 37, 113, 54, 205, 48, 83, 146, 89, 82, 244, 46, 244, 56, 133, 52, 246, 226, 28, 72, 57, 213, 180, 6, 232, 196, 27, 146, 252, 144, 215, 87, 149, 15, 31, 182, 155, 81, 96, 173, 162, 16, 207, 103, 152, 83, 8, 233, 188, 92, 117, 180, 129, 158, 136, 65, 111, 196, 11, 80, 8, 217, 142, 33, 244, 123, 234, 131, 226, 153, 34, 201, 99, 86, 101, 73, 219, 41, 171, 82, 110, 30, 24, 80, 218, 161, 57, 118, 132, 254, 161, 205, 70, 139, 114, 145, 181, 138, 36, 166, 229, 216, 124, 113, 67, 139, 139, 42, 210, 34, 28, 19, 83, 60, 115, 97, 48, 152, 99, 213, 85, 59, 66, 54, 146, 189, 194, 100, 65, 112, 186, 5, 45, 119, 128, 213, 130, 141, 187, 211, 80, 175, 146, 192, 168, 137, 11, 83, 90, 85, 97, 156, 129, 116, 247, 246, 62, 67, 246, 149, 75, 183, 134, 114, 209, 200, 156, 85, 27, 196, 163, 218, 151, 177, 75, 204, 11, 133, 187, 48, 183, 244, 239, 178, 85, 91, 207, 160, 35, 185, 112, 156, 174, 113, 139, 215, 50, 175, 84, 58, 145, 185, 31, 70, 1, 242, 22, 7, 232, 63, 31, 208, 117, 3, 231, 100, 131, 120, 64, 108, 184, 216, 120, 76, 153, 248, 0, 140, 4, 81, 23, 178, 109, 89, 188, 128, 104, 182, 190, 237, 240, 63, 102, 247, 62, 0, 77, 100, 61, 72, 130, 145, 111, 222, 236, 16, 105, 203, 133, 160, 255, 96, 45, 113, 170, 201, 43, 31, 229, 26, 148, 168, 201, 251, 176, 247, 164, 178, 181, 245, 152, 62, 37, 14, 156, 99, 234, 30, 136, 232, 249, 179, 51, 67, 140, 150, 134, 21, 156, 120, 201, 89, 141, 254, 62, 105, 243, 216, 231, 225, 190, 14, 40, 198, 229, 47, 100, 217, 228, 239, 142, 211, 234, 235, 199, 110, 76, 154, 105, 132, 76, 35, 180, 162, 147, 151, 203, 238, 50, 61, 97, 249, 205, 87, 217, 39, 42, 148, 88, 155, 6, 52, 248, 130, 238, 214, 57, 24, 127, 153, 16, 233, 94, 172, 42, 232, 209, 2, 172, 201, 181, 110, 11, 208, 108, 72, 150, 122, 13, 55, 95, 179, 245, 154, 250, 196, 59, 101, 67, 236, 179, 214, 62, 110, 80, 55, 7, 94, 8, 65, 200, 140, 180, 189, 17, 25, 80, 74, 177, 183, 142, 51, 171, 61, 236, 108, 124, 193, 137, 74, 41, 252, 255, 224, 0, 143, 238, 14, 143, 62, 124, 16, 185, 20, 22, 119, 13, 61, 4, 18, 245, 123, 141, 197, 35, 224, 233, 132, 38, 64, 50, 254, 201, 114, 99, 174, 35, 173, 227, 238, 158, 66, 161, 113, 222, 159, 0, 164, 69, 27, 136, 28, 142, 204, 215, 137, 32, 115, 54, 66, 41, 62, 108, 200, 15, 221, 73, 229, 119, 83, 27, 238, 110, 251, 107, 6, 112, 156, 22, 0, 164, 239, 236, 148, 88, 1, 125, 246, 48, 156, 103, 16, 97, 88, 221, 93, 133, 133, 204, 50, 191, 86, 234, 111, 172, 110, 55, 53, 56, 154, 29, 150, 175, 174, 200, 142, 104, 85, 246, 208, 238, 46, 210, 55, 118, 49, 188, 243, 131, 90, 252, 120, 156, 234, 136, 0, 49, 22, 193, 107, 106, 226, 169, 246, 33, 74, 182, 200, 25, 223, 69, 142, 221, 148, 177, 144, 17, 171, 49, 158, 220, 255, 66, 156, 252, 56, 41, 64, 40, 156, 75, 84, 67, 50, 226, 97, 246, 189, 4, 133, 173, 21, 26, 240, 155, 19, 48, 56, 93, 82, 133, 233, 49, 86, 82, 73, 3, 30, 108, 163, 177, 173, 195, 106, 124, 149, 249, 75, 107, 107, 144, 43, 91, 205, 78, 102, 156, 58, 167, 78, 246, 233, 68, 79, 152, 254, 129, 203, 232, 139, 227, 59, 84, 159, 110, 92, 229, 23, 87, 29, 201, 215, 233, 161, 84, 210, 16, 58, 35, 123, 166, 209, 32, 79, 3, 168, 243, 195, 112, 152, 178, 26, 118, 1, 115, 76, 222, 8, 225, 161, 178, 123, 249, 95, 104, 229, 115, 155, 52, 68, 234, 1, 229, 144, 201, 119, 179, 94, 18, 180, 151, 5, 80, 162, 22, 199, 5, 96, 30, 197, 247, 27, 238, 59, 171, 145, 11, 240, 6, 248, 246, 150, 236, 8, 223, 178, 159, 1, 164, 233, 240, 118, 55, 35, 88, 89, 114, 124, 112, 243, 38, 155, 74, 57, 132, 201, 69, 227, 170, 233, 28, 232, 227, 19, 231, 217, 252, 205, 5, 85, 206, 18, 188, 175, 57, 85, 81, 224, 186, 112, 146, 16, 48, 72, 197, 245, 124, 9, 131, 197, 240, 75, 227, 235, 242, 225, 184, 10, 151, 128, 124, 182, 65, 96, 101, 109, 102, 103, 47, 79, 46, 130, 203, 190, 205, 114, 228, 131, 170, 182, 175, 178, 196, 195, 199, 188, 195, 151, 130, 106, 98, 14, 165, 6, 82, 116, 164, 152, 156, 24, 3, 49, 85, 136, 12, 68, 95, 35, 212, 168, 35, 52, 71, 19, 91, 142, 184, 95, 195, 252, 224, 182, 173, 198, 204, 137, 13, 254, 34, 70, 140, 14, 16, 48, 160, 251, 148, 53, 73, 13, 152, 82, 226, 194, 44, 137, 60, 15, 71, 208, 199, 67, 75, 55, 58, 55, 9, 247, 179, 48, 56, 42, 209, 127, 6, 61, 192, 59, 8, 170, 64, 219, 250, 7, 173, 219, 68, 161, 113, 244, 101, 80, 39, 170, 175, 58, 175, 86, 129, 145, 40, 164, 234, 20, 66, 39, 102, 111, 16, 123, 10, 192, 27, 28, 80, 26, 87, 184, 186, 202, 126, 124, 141, 7, 35, 21, 235, 3, 81, 129, 79, 146, 19, 154, 190, 2, 175, 177, 0, 231, 28, 151, 73, 147, 239, 144, 128, 220, 180, 167, 167, 82, 221, 99, 57, 168, 246, 251, 35, 203, 180, 84, 102, 68, 249, 138, 37, 17, 229, 65, 217, 99, 21, 82, 37, 161, 253, 12, 34, 106, 17, 229, 1, 213, 70, 84, 105, 154, 252, 102, 128, 24, 19, 132, 116, 214, 240, 6, 110, 7, 59, 51, 83, 32, 133, 222, 175, 69, 134, 191, 64, 62, 176, 195, 3, 64, 113, 58, 188, 63, 68, 60, 35, 189, 198, 58, 75, 114, 132, 252, 179, 192, 181, 128, 93, 189, 255, 233, 78, 219, 176, 108, 60, 143, 48, 53, 63, 213, 167, 98, 75, 141, 73, 189, 59, 190, 147, 106, 69, 125, 206, 230, 251, 49, 147, 171, 252, 26, 47, 159, 230, 72, 26, 26, 145, 40, 64, 33, 172, 124, 166, 119, 90, 122, 137, 104, 254, 60, 25, 69, 224, 149, 111, 138, 191, 244, 81, 108, 205, 168, 48, 97, 15, 4, 98, 39, 156, 188, 162, 45, 29, 176, 254, 62, 121, 81, 106, 76, 238, 248, 113, 3, 13, 152, 253, 160, 145, 156, 86, 173, 8, 197, 252, 140, 86, 45, 39, 67, 54, 97, 56, 191, 161, 220, 69, 216, 215, 84, 171, 128, 2, 95, 181, 207, 238, 45, 221, 53, 147, 208, 68, 110, 18, 58, 255, 57, 143, 78, 52, 207, 148, 174, 10, 18, 196, 40, 213, 253, 101, 4, 7, 3, 223, 92, 125, 36, 183, 229, 71, 248, 120, 154, 33, 58, 211, 231, 17, 232, 151, 216, 255, 74, 201, 61, 253, 156, 209, 255, 167, 44, 202, 161, 56, 77, 180, 117, 5, 180, 52, 24, 8, 33, 72, 74, 30, 119, 13, 179, 187, 181, 115, 68, 197, 239, 10, 69, 79, 81, 200, 145, 209, 198, 237, 12, 152, 124, 200, 203, 170, 99, 151, 22, 62, 89, 198, 68, 116, 237, 139, 217, 107, 209, 109, 9, 96, 35, 250, 54, 149, 111, 53, 192, 225, 79, 26, 97, 60, 243, 162, 104, 147, 24, 175, 84, 2, 49, 174, 202, 38, 53, 34, 160, 82, 158, 101, 180, 144, 18, 238, 65, 63, 165, 108, 15, 108, 140, 170, 32, 122, 73, 76, 64, 188, 103, 225, 239, 100, 240, 12, 25, 39, 9, 187, 164, 112, 162, 100, 185, 27, 72, 122, 218, 28, 2, 20, 247, 7, 190, 131, 66, 116, 242, 180, 95, 189, 79, 158, 86, 246, 22, 14, 0, 104, 154, 105, 100, 39, 227, 139, 56, 221, 153, 88, 164, 163, 19, 248, 164, 72, 149, 49, 161, 247, 6, 100, 233, 26, 245, 44, 43, 115, 214, 91, 164, 205, 224, 59, 5, 48, 170, 187, 210, 184, 4, 83, 123, 170, 24, 194, 169, 181, 127, 47, 225, 198, 59, 145, 144, 8, 106, 166, 8, 121, 198, 120, 198, 155, 26, 143, 137, 243, 213, 27, 235, 203, 195, 28, 143, 144, 33, 123, 252, 32, 18, 248, 224, 59, 193, 210, 78, 21, 149, 16, 232, 130, 47, 93, 161, 37, 98, 80, 219, 199, 46, 45, 177, 231, 28, 127, 238, 28, 144, 0, 39, 54, 5, 215, 8, 70, 41, 196, 118, 117, 233, 11, 252, 41, 217, 123, 250, 53, 74, 20, 228, 84, 231, 254, 19, 211, 177, 231, 41, 16, 38, 47, 21, 158, 64, 255, 107, 29, 83, 142, 37, 37, 153, 116, 81, 210, 58, 227, 49, 184, 188, 219, 170, 168, 29, 19, 228, 8, 78, 45, 162, 75, 40, 157, 32, 224, 130, 72, 105, 24, 87, 20, 250, 9, 123, 187, 110, 77, 232, 243, 110, 45, 175, 67, 117, 168, 169, 57, 142, 12, 225, 19, 132, 2, 113, 42, 233, 47, 132, 241, 11, 189, 145, 11, 227, 175, 254, 14, 168, 95, 4, 241, 224, 97, 0, 196, 105, 146, 205, 254, 212, 1, 252, 81, 109, 177, 226, 171, 172, 44, 153, 216, 153, 245, 78, 167, 181, 63, 105, 184, 114, 92, 13, 107, 5, 21, 150, 22, 208, 65, 169, 159, 237, 9, 48, 206, 61, 24, 121, 172, 137, 75, 105, 105, 85, 149, 162, 190, 212, 129, 15, 74, 239, 185, 44, 162, 12, 192, 218, 84, 23, 132, 130, 120, 242, 243, 202, 192, 150, 177, 158, 242, 191, 184, 97, 16, 204, 35, 173, 159, 99, 33, 109, 204, 111, 3, 249, 253, 75, 113, 207, 98, 66, 218, 7, 81, 164, 106, 108, 77, 225, 64, 109, 33, 143, 88, 87, 76, 52, 48, 200, 172, 169, 190, 246, 172, 170, 230, 2, 43, 138, 226, 155, 225, 150, 250, 3, 234, 86, 75, 20, 18, 62, 163, 10, 172, 69, 38, 122, 35, 48, 48, 234, 21, 189, 158, 78, 153, 72, 62, 77, 157, 102, 248, 38, 151, 164, 15, 230, 232, 230, 230, 146, 195, 221, 244, 233, 250, 93, 118, 64, 249, 223, 158, 216, 33, 138, 44, 50, 225, 112, 56, 94, 44, 98, 154, 44, 31, 190, 150, 194, 223, 110, 165, 225, 157, 146, 115, 174, 157, 44, 76, 168, 155, 77, 222, 205, 138, 49, 160, 195, 193, 154, 228, 1, 60, 37, 239, 9, 18, 10, 238, 72, 220, 33, 88, 93, 40, 58, 88, 251, 117, 183, 246, 20, 109, 188, 79, 127, 251, 206, 90, 125, 178, 113, 34, 218, 135, 250, 57, 242, 163, 23, 203, 191, 231, 174, 34, 16, 210, 238, 64, 175, 248, 180, 16, 182, 177, 98, 231, 121, 51, 52, 88, 91, 236, 10, 11, 179, 145, 3, 66, 177, 33, 92, 244, 102, 231, 182, 106, 10, 237, 184, 148, 178, 39, 56, 14, 45, 223, 14, 14, 204, 22, 49, 75, 1, 241, 117, 246, 7, 29, 24, 122, 120, 132, 15, 171, 69, 223, 192, 162, 79, 44, 136, 9, 174, 170, 129, 118, 1, 74, 30, 218, 247, 39, 55, 99, 150, 72, 199, 214, 184, 4, 129, 72, 67, 121, 183, 235, 143, 183, 228, 130, 92, 160, 3, 194, 145, 242, 95, 85, 45, 170, 174, 157, 173, 73, 121, 213, 168, 95, 101, 58, 207, 57, 237, 51, 156, 67, 35, 207, 85, 161, 33, 129, 138, 8, 241, 58, 58, 218, 43, 97, 220, 177, 200, 65, 164, 152, 238, 19, 59, 174, 149, 178, 12, 116, 134, 0, 72, 43, 107, 23, 254, 57, 7, 86, 85, 22, 185, 194, 226, 25, 46, 23, 81, 132, 78, 65, 99, 41, 37, 91, 202, 168, 4, 103, 176, 70, 247, 112, 34, 178, 26, 38, 234, 249, 169, 232, 213, 141, 182, 155, 255, 2, 79, 139, 67, 182, 27, 8, 131, 32, 114, 21, 192, 243, 151, 58, 13, 202, 255, 52, 6, 209, 44, 79, 60, 83, 180, 39, 94, 235, 226, 156, 32, 152, 44, 75, 50, 22, 249, 148, 230, 69, 98, 136, 120, 252, 39, 11, 85, 109, 117, 181, 106, 249, 82, 163, 207, 240, 192, 56, 34, 98, 6, 11, 36, 89, 64, 181, 52, 148, 28, 174, 129, 74, 125, 2, 118, 246, 6, 140, 76, 196, 122, 215, 85, 51, 192, 137, 93, 192, 37, 30, 68, 155, 100, 224, 59, 192, 218, 1, 33, 57, 247, 155, 123, 126, 79, 101, 74, 8, 19, 169, 9, 41, 232, 229, 24, 102, 75, 78, 210, 239, 226, 219, 215, 170, 229, 116, 116, 190, 121, 212, 59, 20, 55, 80, 94, 246, 251, 114, 1, 252, 136, 224, 51, 243, 18, 200, 101, 13, 102, 133, 25, 157, 53, 245, 48, 241, 153, 68, 160, 148, 126, 93, 251, 113, 245, 174, 211, 43, 242, 95, 62, 43, 198, 25, 136, 140, 219, 98, 19, 42, 63, 93, 130, 63, 120, 203, 45, 113, 5, 110, 148, 163, 223, 73, 8, 202, 136, 41, 122, 54, 247, 148, 76, 211, 44, 33, 86, 36, 182, 163, 204, 160, 22, 126, 202, 172, 52, 174, 18, 132, 159, 92, 76, 90, 114, 170, 105, 4, 154, 131, 248, 128, 228, 101, 160, 212, 203, 39, 171, 149, 239, 57, 123, 168, 81, 5, 74, 147, 83, 42, 173, 196, 87, 198, 143, 248, 5, 165, 138, 198, 34, 196, 25, 1, 203, 100, 203, 238, 217, 141, 27, 158, 38, 149, 145, 45, 104, 34, 27, 237, 64, 74, 35, 68, 226, 142, 114, 174, 122, 142, 239, 104, 153, 247, 155, 19, 0, 113, 104, 228, 155, 119, 147, 216, 215, 90, 53, 47, 92, 112, 75, 201, 85, 95, 45, 29, 232, 100, 73, 61, 145, 220, 235, 94, 68, 16, 170, 210, 162, 205, 5, 118, 225, 147, 229, 142, 31, 91, 98, 235, 125, 214, 111, 222, 45, 79, 180, 40, 29, 126, 112, 149, 52, 52, 158, 53, 59, 209, 240, 231, 220, 239, 9, 17, 78, 58, 227, 80, 167, 87, 133, 110, 67, 101, 39, 147, 188, 60, 15, 243, 6, 162, 34, 162, 125, 184, 237, 10, 56, 81, 154, 213, 251, 47, 54, 245, 10, 115, 127, 166, 254, 237, 197, 167, 153, 118, 105, 92, 96, 254, 188, 140, 228, 183, 106, 153, 82, 122, 162, 242, 31, 233, 215, 186, 110, 200, 169, 178, 107, 83, 113, 130, 168, 158, 111, 20, 150, 140, 162, 125, 53, 200, 205, 128, 219, 38, 242, 196, 139, 150, 239, 78, 224, 157, 25, 104, 63, 159, 50, 48, 186, 33, 219, 82, 157, 194, 209, 13, 65, 120, 100, 254, 44, 9, 120, 31, 103, 89, 94, 79, 150, 142, 167, 115, 117, 146, 214, 96, 152, 213, 142, 228, 72, 127, 85, 102, 237, 129, 12, 220, 220, 105, 45, 43, 38, 6, 128, 133, 87, 48, 105, 136, 177, 240, 42, 185, 72, 0, 107, 230, 26, 172, 122, 251, 110, 2, 32, 170, 103, 101, 69, 49, 107, 46, 36, 226, 208, 68, 31, 178, 81, 21, 31, 30, 236, 191, 244, 50, 30, 54, 143, 67, 207, 180, 19, 116, 96, 16, 123, 210, 82, 168, 221, 199, 147, 189, 133, 60, 91, 41, 205, 62, 61, 134, 36, 46, 18, 78, 133, 139, 190, 114, 11, 16, 28, 71, 53, 175, 35, 164, 173, 144, 111, 33, 184, 196, 125, 36, 101, 243, 29, 80, 169, 81, 186, 41, 196, 124, 120, 63, 118, 15, 61, 0, 60, 177, 247, 142, 158, 220, 202, 24, 204, 68, 10, 180, 34, 54, 16, 114, 151, 107, 6, 32, 220, 210, 191, 129, 80, 190, 124, 140, 214, 61, 1, 34, 193, 136, 141, 251, 157, 193, 46, 232, 84, 160, 249, 138, 207, 67, 2, 2, 7, 8, 244, 179, 136, 166, 131, 1, 145, 14, 5, 47, 81, 190, 198, 188, 245, 5, 50, 60, 55, 70, 217, 89, 28, 3, 41, 47, 251, 77, 248, 33, 68, 6, 116, 121, 191, 24, 67, 197, 16, 215, 111, 229, 132, 37, 212, 180, 190, 21, 69, 211, 154, 114, 141, 69, 69, 16, 0, 60, 110, 57, 13, 184, 190, 241, 198, 7, 19, 89, 150, 66, 157, 141, 175, 0, 136, 156, 163, 218, 215, 127, 82, 255, 152, 215, 37, 251, 186, 223, 111, 242, 253, 233, 175, 46, 179, 1, 247, 225, 30, 19, 192, 85, 130, 137, 155, 219, 41, 33, 185, 33, 126, 131, 237, 0, 45, 246, 54, 86, 176, 133, 211, 6, 141, 230, 90, 73, 65, 75, 120, 47, 179, 96, 28, 193, 30, 17, 22, 43, 98, 93, 196, 105, 88, 201, 40, 82, 5, 186, 29, 165, 158, 119, 102, 58, 14, 85, 89, 47, 85, 29, 140, 17, 27, 113, 21, 217, 143, 195, 91, 38, 192, 181, 229, 0, 150, 248, 97, 36, 158, 62, 103, 179, 116, 191, 114, 121, 95, 124, 249, 112, 228, 21, 127, 248, 123, 5, 192, 167, 229, 174, 2, 242, 151, 132, 55, 183, 139, 51, 103, 138, 69, 210, 136, 33, 104, 169, 119, 127, 45, 152, 138, 226, 157, 201, 29, 80, 106, 224, 12, 30, 1, 132, 36, 248, 62, 0, 147, 132, 226, 104, 67, 211, 87, 38, 35, 99, 160, 131, 195, 32, 144, 10, 32, 252, 197, 133, 128, 180, 123, 104, 143, 82, 64, 74, 70, 188, 208, 2, 59, 77, 212, 247, 109, 161, 52, 248, 78, 230, 221, 102, 142, 40, 136, 142, 172, 69, 135, 25, 92, 126, 241, 174, 200, 85, 205, 44, 175, 144, 83, 54, 250, 11, 206, 204, 162, 120, 232, 215, 8, 198, 110, 71, 48, 136, 146, 12, 184, 0, 18, 30, 55, 48, 216, 77, 109, 17, 9, 249, 51, 71, 255, 170, 218, 62, 243, 195, 71, 45, 149, 217, 142, 83, 181, 38, 44, 9, 9, 66, 57, 175, 69, 15, 60, 170, 97, 112, 61, 249, 182, 34, 100, 11, 216, 137, 101, 195, 80, 14, 35, 111, 223, 11, 61, 106, 208, 91, 81, 142, 145, 96, 145, 226, 159, 88, 22, 18, 211, 192, 154, 185, 67, 93, 73, 144, 206, 221, 201, 179, 138, 223, 88, 151, 122, 128, 150, 6, 102, 9, 91, 68, 130, 217, 147, 125, 1, 225, 102, 245, 212, 7, 178, 203, 91, 139, 175, 43, 102, 221, 172, 214, 188, 107, 116, 54, 78, 92, 225, 4, 6, 241, 194, 148, 237, 44, 171, 149, 17, 180, 104, 33, 144, 86, 133, 205, 70, 236, 87, 40, 195, 147, 67, 8, 34, 112, 64, 22, 111, 196, 96, 246, 129, 131, 226, 36, 181, 250, 68, 98, 86, 159, 77, 5, 182, 147, 224, 21, 65, 18, 180, 36, 85, 224, 100, 229, 166, 85, 59, 255, 113, 41, 130, 120, 232, 2, 113, 114, 211, 53, 200, 45, 183, 238, 240, 174, 1, 68, 82, 167, 6, 198, 172, 55, 103, 200, 2, 8, 30, 53, 196, 99, 40, 138, 239, 185, 86, 191, 230, 174, 97, 61, 7, 120, 85, 142, 6, 202, 107, 114, 20, 202, 13, 209, 82, 46, 122, 14, 21, 27, 44, 152, 132, 217, 40, 254, 113, 170, 186, 229, 228, 81, 15, 148, 77, 26, 43, 113, 17, 100, 156, 33, 90, 38, 88, 129, 171, 199, 148, 30, 16, 94, 95, 182, 24, 159, 4, 96, 106, 98, 219, 36, 73, 218, 79, 152, 152, 176, 159, 34, 13, 80, 54, 176, 157, 191, 128, 46, 230, 117, 199, 127, 8, 36, 208, 7, 237, 41, 241, 160, 116, 172, 181, 90, 94, 233, 130, 233, 225, 35, 53, 155, 106, 115, 118, 192, 220, 118, 190, 248, 170, 110, 113, 23, 171, 120, 176, 247, 181, 130, 172, 197, 44, 111, 239, 3, 89, 217, 108, 182, 120, 190, 62, 138, 226, 232, 31, 233, 3, 77, 114, 53, 228, 157, 129, 209, 233, 26, 245, 254, 235, 164, 195, 194, 50, 73, 225, 14, 225, 251, 100, 59, 93, 167, 134, 171, 186, 46, 101, 169, 205, 33, 87, 23, 97, 138, 116, 226, 51, 2, 229, 221, 188, 16, 219, 175, 168, 202, 198, 113, 51, 183, 116, 137, 139, 224, 233, 20, 110, 121, 47, 206, 155, 201, 166, 114, 226, 30, 186, 157, 124, 241, 5, 119, 99, 101, 106, 151, 49, 60, 206, 210, 16, 66, 2, 247, 168, 159, 166, 192, 198, 55, 217, 22, 126, 235, 14, 203, 81, 72, 142, 194, 187, 42, 118, 189, 167, 4, 199, 59, 110, 8, 88, 121, 247, 138, 57, 180, 45, 96, 77, 227, 228, 29, 187, 65, 173, 142, 96, 100, 26, 22, 160, 88, 108, 210, 83, 138, 196, 35, 9, 58, 176, 71, 59, 21, 128, 59, 166, 47, 114, 158, 226, 145, 144, 199, 35, 25, 219, 22, 38, 104, 119, 65, 4, 222, 170, 224, 89, 206, 26, 96, 161, 59, 62, 37, 201, 27, 92, 91, 201, 64, 220, 109, 103, 222, 7, 100, 225, 114, 221, 19, 47, 192, 117, 243, 236, 23, 100, 113, 251, 111, 69, 196, 144, 237, 22, 0, 243, 251, 138, 240, 123, 95, 168, 224, 167, 223, 133, 197, 143, 144, 126, 100, 139, 36, 120, 185, 19, 175, 240, 251, 66, 46, 56, 197, 169, 168, 17, 168, 218, 183, 9, 186, 123, 140, 168, 226, 74, 117, 140, 191, 210, 161, 96, 53, 229, 192, 250, 224, 73, 189, 235, 153, 225, 28, 2, 12, 95, 25, 130, 167, 139, 189, 24, 71, 2, 102, 46, 149, 137, 237, 194, 61, 67, 138, 20, 249, 40, 35, 24, 176, 188, 60, 122, 73, 231, 156, 204, 253, 245, 41, 229, 144, 192, 152, 102, 71, 6, 241, 0, 37, 176, 223, 91, 213, 236, 216, 245, 235, 127, 83, 130, 229, 102, 244, 219, 40, 236, 135, 5, 153, 88, 186, 213, 62, 120, 187, 115, 9, 181, 44, 8, 212, 111, 78, 255, 237, 244, 101, 133, 113, 169, 31, 28, 202, 147, 120, 190, 147, 133, 123, 14, 223, 120, 41, 71, 114, 51, 165, 6, 90, 90, 113, 207, 246, 150, 40, 143, 124, 73, 170, 193, 232, 107, 43, 98, 59, 25, 123, 59, 250, 108, 7, 21, 105, 79, 72, 113, 27, 1, 88, 37, 20, 218, 254, 183, 233, 14, 124, 16, 59, 17, 164, 162, 242, 247, 9, 80, 7, 183, 151, 68, 41, 48, 22, 53, 134, 88, 223, 113, 154, 95, 205, 178, 170, 125, 216, 45, 152, 141, 76, 33, 184, 43, 186, 41, 151, 115, 223, 224, 97, 100, 154, 30, 74, 20, 148, 113, 180, 100, 226, 221, 19, 143, 119, 182, 45, 90, 99, 157, 90, 57, 80, 58, 227, 107, 36, 177, 138, 144, 245, 78, 4, 218, 205, 154, 147, 59, 62, 163, 67, 7, 174, 32, 91, 254, 145, 126, 187, 221, 208, 92, 111, 73, 170, 173, 107, 59, 69, 78, 7, 171, 135, 241, 148, 138, 50, 40, 228, 138, 10, 229, 228, 145, 95, 223, 13, 161, 183, 245, 41, 150, 149, 199, 7, 255, 138, 221, 142, 172, 65, 53, 31, 230, 47, 229, 126, 42, 131, 66, 18, 77, 203, 6, 206, 228, 215, 192, 209, 175, 98, 184, 159, 241, 106, 95, 255, 111, 55, 45, 234, 212, 4, 32, 208, 111, 37, 212, 53, 129, 70, 168, 156, 151, 188, 2, 121, 154, 140, 84, 155, 164, 51, 114, 142, 45, 24, 51, 99, 137, 224, 147, 212, 26, 220, 163, 19, 39, 180, 215, 53, 165, 162, 182, 233, 59, 221, 11, 90, 237, 112, 78, 36, 117, 233, 23, 229, 192, 94, 99, 96, 118, 171, 57, 42, 16, 98, 175, 43, 171, 196, 248, 18, 57, 19, 98, 181, 237, 235, 184, 177, 155, 6, 83, 179, 70, 162, 221, 121, 216, 116, 116, 200, 193, 164, 252, 59, 216, 48, 104, 112, 42, 207, 152, 246, 91, 109, 141, 68, 104, 49, 225, 210, 8, 79, 179, 62, 10, 31, 156, 189, 216, 179, 81, 102, 220, 79, 187, 167, 207, 11, 119, 83, 130, 24, 100, 215, 115, 143, 96, 213, 132, 109, 77, 35, 168, 40, 247, 133, 60, 53, 179, 26, 159, 70, 149, 59, 249, 130, 175, 113, 97, 166, 123, 156, 54, 129, 126, 197, 171, 18, 105, 91, 254, 211, 78, 233, 206, 145, 166, 101, 97, 32, 157, 236, 137, 252, 183, 59, 234, 219, 181, 236, 190, 76, 56, 145, 170, 245, 59, 208, 29, 16, 116, 45, 138, 135, 248, 118, 228, 3, 251, 244, 177, 85, 107, 13, 57, 60, 35, 237, 73, 5, 58, 140, 120, 165, 47, 223, 127, 98, 136, 84, 16, 18, 225, 126, 38, 100, 120, 35, 194, 204, 170, 144, 253, 217, 18, 86, 133, 225, 191, 220, 178, 177, 11, 51, 146, 205, 132, 128, 33, 90, 195, 234, 116, 68, 20, 73, 194, 135, 137, 0, 16, 169, 243, 123, 5, 130, 172, 162, 48, 240, 232, 208, 158, 32, 49, 140, 245, 51, 37, 131, 92, 67, 126, 44, 13, 31, 62, 240, 80, 131, 136, 187, 222, 199, 19, 28, 229, 240, 122, 208, 244, 180, 58, 232, 46, 1, 83, 171, 236, 220, 69, 6, 43, 152, 203, 3, 16, 246, 17, 69, 116, 54, 64, 9, 240, 109, 189, 32, 237, 189, 153, 148, 107, 2, 7, 224, 144, 242, 114, 76, 46, 66, 91, 31, 228, 112, 115, 124, 108, 39, 108, 225, 232, 245, 175, 122, 57, 47, 175, 12, 179, 39, 27, 172, 167, 249, 76, 135, 106, 133, 82, 162, 27, 220, 11, 94, 144, 228, 95, 114, 158, 217, 58, 24, 211, 24, 42, 118, 88, 178, 81, 163, 27, 41, 116, 193, 231, 88, 164, 166, 21, 95, 207, 245, 47, 245, 173, 56, 11, 144, 87, 124, 231, 79, 214, 60, 160, 185, 39, 102, 146, 204, 70, 202, 222, 203, 101, 183, 165, 180, 33, 216, 127, 96, 249, 7, 143, 237, 96, 186, 72, 40, 64, 213, 23, 251, 103, 123, 251, 135, 182, 237, 169, 43, 46, 30, 140, 133, 53, 127, 17, 18, 37, 31, 123, 86, 187, 5, 75, 64, 27, 13, 175, 180, 152, 249, 135, 82, 16, 139, 136, 204, 175, 27, 135, 255, 204, 39, 223, 160, 78, 6, 151, 95, 65, 13, 41, 64, 93, 64, 1, 218, 99, 109, 134, 201, 25, 62, 0, 191, 212, 140, 170, 8, 19, 28, 119, 45, 2, 53, 228, 169, 150, 252, 218, 233, 121, 176, 198, 208, 137, 58, 62, 37, 8, 150, 83, 247, 152, 133, 121, 85, 85, 2, 54, 179, 179, 105, 251, 99, 239, 223, 90, 42, 76, 49, 187, 250, 187, 197, 150, 141, 139, 36, 178, 143, 241, 206, 44, 80, 255, 14, 198, 156, 112, 154, 232, 11, 75, 53, 213, 83, 123, 94, 141, 155, 201, 113, 241, 56, 70, 252, 64, 158, 166, 165, 96, 152, 186, 2, 92, 3, 114, 52, 156, 134, 7, 222, 31, 90, 137, 52, 48, 12, 201, 84, 147, 254, 138, 19, 184, 114, 245, 138, 7, 90, 49, 167, 145, 107, 182, 83, 253, 40, 52, 160, 187, 243, 67, 151, 81, 3, 218, 68, 56, 202, 178, 144, 175, 238, 77, 145, 191, 171, 245, 128, 4, 208, 195, 102, 119, 57, 240, 6, 66, 254, 211, 45, 130, 58, 225, 51, 99, 73, 13, 56, 58, 28, 35, 110, 33, 180, 51, 26, 140, 70, 26, 229, 180, 229, 176, 138, 226, 9, 134, 129, 185, 187, 239, 223, 2, 229, 233, 150, 121, 138, 51, 41, 143, 68, 57, 118, 56, 31, 87, 195, 1, 75, 87, 156, 253, 225, 48, 243, 60, 94, 212, 205, 205, 151, 180, 100, 135, 245, 23, 108, 18, 5, 132, 244, 135, 177, 10, 37, 12, 28, 10, 127, 93, 103, 207, 148, 27, 227, 93, 148, 246, 204, 17, 88, 121, 29, 100, 198, 40, 185, 62, 104, 157, 5, 56, 71, 213, 100, 127, 249, 128, 244, 137, 235, 154, 144, 197, 8, 208, 124, 114, 242, 13, 251, 50, 254, 34, 91, 56, 10, 193, 195, 147, 85, 84, 21, 114, 112, 185, 50, 108, 118, 235, 123, 103, 247, 179, 202, 50, 17, 200, 66, 193, 58, 63, 43, 104, 163, 165, 140, 252, 54, 59, 102, 13, 117, 138, 175, 183, 48, 220, 215, 173, 86, 24, 135, 190, 226, 209, 212, 8, 111, 15, 152, 3, 126, 226, 149, 168, 204, 178, 165, 4, 230, 32, 34, 8, 79, 242, 94, 100, 141, 5, 125, 159, 109, 205, 161, 61, 95, 14, 59, 3, 132, 196, 133, 176, 198, 172, 25, 243, 70, 148, 96, 196, 241, 24, 65, 116, 30, 177, 162, 251, 228, 156, 147, 77, 244, 65, 138, 223, 212, 22, 40, 122, 1, 202, 219, 51, 101, 78, 118, 96, 101, 146, 112, 10, 172, 36, 211, 254, 136, 208, 48, 88, 82, 217, 33, 32, 148, 185, 234, 217, 100, 29, 141, 88, 247, 168, 34, 215, 165, 136, 149, 42, 203, 97, 227, 69, 216, 67, 65, 14, 27, 19, 84, 33, 141, 58, 75, 72, 178, 44, 16, 160, 251, 174, 94, 248, 52, 184, 125, 190, 77, 251, 88, 72, 11, 153, 160, 127, 77, 22, 127, 219, 245, 89, 76, 136, 192, 202, 239, 219, 248, 248, 171, 164, 198, 78, 148, 85, 159, 72, 218, 223, 99, 53, 38, 201, 186, 121, 68, 137, 194, 174, 136, 239, 224, 160, 67, 111, 220, 229, 93, 245, 31, 250, 193, 91, 110, 252, 12, 7, 131, 47, 62, 73, 10, 100, 129, 88, 40, 230, 245, 208, 146, 14, 132, 120, 204, 222, 135, 68, 47, 222, 19, 60, 161, 7, 67, 64, 68, 70, 29, 152, 64, 7, 183, 251, 225, 148, 61, 155, 127, 233, 89, 112, 169, 73, 157, 76, 150, 85, 219, 41, 71, 220, 234, 15, 173, 98, 124, 209, 206, 71, 56, 201, 187, 64, 16, 103, 159, 78, 9, 84, 187, 250, 154, 196, 49, 125, 168, 154, 127, 40, 15, 139, 198, 155, 71, 7, 80, 98, 219, 105, 202, 183, 96, 32, 115, 28, 168, 81, 69, 23, 103, 33, 219, 47, 154, 131, 125, 7, 229, 140, 163, 248, 62, 156, 231, 8, 141, 34, 232, 193, 150, 171, 224, 190, 62, 76, 47, 203, 161, 1, 186, 3, 67, 55, 0, 117, 153, 152, 165, 90, 146, 124, 169, 150, 90, 241, 163, 67, 146, 64, 13, 165, 218, 131, 135, 166, 193, 186, 200, 149, 8, 55, 9, 227, 66, 157, 212, 226, 97, 91, 172, 199, 196, 59, 131, 75, 170, 88, 8, 242, 30, 148, 211, 214, 208, 102, 225, 102, 165, 119, 155, 69, 208, 18, 168, 81, 84, 153, 79, 240, 92, 216, 36, 99, 75, 125, 249, 93, 182, 54, 232, 169, 235, 184, 210, 169, 104, 11, 163, 110, 116, 174, 218, 205, 75, 188, 68, 8, 194, 219, 137, 103, 93, 23, 10, 64, 49, 217, 198, 215, 166, 151, 189, 209, 148, 92, 61, 14, 11, 101, 129, 79, 230, 112, 234, 42, 125, 185, 107, 178, 135, 186, 78, 220, 33, 145, 91, 144, 176, 224, 134, 93, 16, 202, 31, 211, 10, 221, 21, 202, 245, 141, 245, 51, 147, 27, 192, 146, 205, 175, 255, 21, 186, 205, 62, 177, 127, 20, 231, 2, 81, 3, 161, 67, 225, 24, 66, 9, 102, 150, 218, 178, 3, 255, 65, 4, 141, 81, 0, 197, 134, 168, 72, 163, 246, 235, 224, 14, 62, 70, 205, 42, 161, 183, 151, 76, 86, 78, 101, 168, 253, 154, 46, 206, 132, 135, 104, 60, 217, 8, 128, 64, 60, 88, 6, 152, 85, 189, 49, 186, 230, 62, 156, 64, 47, 172, 148, 77, 114, 199, 27, 222, 122, 225, 213, 230, 57, 233, 175, 32, 113, 1, 36, 30, 146, 82, 127, 199, 75, 195, 196, 117, 250, 190, 80, 4, 229, 168, 125, 109, 55, 135, 0, 189, 75, 14, 58, 177, 239, 93, 131, 234, 227, 168, 209, 224, 140, 99, 240, 36, 225, 141, 184, 71, 253, 235, 142, 103, 83, 120, 97, 74, 94, 20, 13, 176, 36, 55, 204, 132, 64, 162, 157, 251, 233, 127, 119, 51, 143, 194, 94, 249, 60, 10, 141, 252, 138, 129, 160, 193, 213, 122, 197, 216, 74, 4, 20, 59, 47, 197, 124, 199, 186, 87, 30, 88, 120, 70, 15, 171, 134, 218, 144, 167, 229, 168, 35, 179, 203, 208, 151, 121, 47, 192, 83, 179, 102, 112, 147, 20, 118, 169, 148, 69, 103, 200, 134, 204, 254, 162, 24, 3, 120, 7, 194, 166, 163, 175, 153, 35, 252, 209, 15, 164, 29, 133, 23, 74, 177, 174, 196, 118, 97, 220, 139, 245, 22, 58, 125, 146, 250, 198, 163, 228, 225, 101, 103, 214, 180, 18, 111, 105, 127, 33, 249, 2, 217, 144, 86, 167, 107, 236, 76, 40, 72, 64, 24, 94, 2, 238, 173, 42, 26, 117, 41, 124, 44, 108, 0, 7, 49, 228, 158, 225, 78, 88, 247, 141, 142, 242, 207, 128, 58, 142, 40, 141, 36, 93, 17, 54, 9, 210, 175, 248, 170, 142, 121, 4, 221, 108, 1, 184, 162, 113, 221, 64, 181, 30, 9, 32, 13, 69, 14, 154, 11, 237, 136, 237, 188, 146, 233, 171, 57, 159, 80, 235, 12, 80, 151, 11, 42, 157, 137, 44, 104, 53, 241, 188, 62, 99, 253, 118, 220, 242, 169, 182, 146, 31, 113, 19, 16, 61, 6, 75, 128, 18, 5, 52, 168, 187, 7, 139, 94, 14, 12, 107, 223, 20, 134, 162, 13, 12, 142, 51, 39, 158, 99, 191, 124, 233, 4, 96, 172, 172, 84, 138, 84, 49, 91, 8, 133, 190, 160, 59, 122, 114, 72, 82, 57, 6, 93, 108, 9, 88, 252, 240, 247, 185, 170, 28, 163, 243, 228, 81, 212, 118, 86, 5, 30, 20, 80, 46, 173, 177, 119, 216, 111, 84, 139, 132, 97, 122, 67, 108, 176, 171, 31, 192, 71, 108, 22, 65, 42, 83, 221, 166, 15, 32, 207, 141, 92, 177, 170, 65, 169, 83, 65, 54, 118, 184, 156, 7, 197, 130, 29, 9, 157, 87, 71, 235, 66, 54, 225, 220, 48, 160, 193, 88, 185, 231, 251, 229, 157, 118, 66, 206, 143, 242, 42, 72, 52, 178, 243, 198, 193, 43, 12, 81, 121, 178, 65, 82, 39, 113, 135, 191, 197, 25, 142, 148, 52, 46, 123, 192, 188, 246, 95, 87, 173, 225, 162, 39, 182, 51, 169, 185, 228, 25, 115, 30, 210, 225, 47, 168, 93, 139, 76, 121, 224, 61, 30, 24, 45, 34, 37, 181, 180, 128, 113, 155, 43, 70, 224, 60, 218, 30, 252, 249, 131, 154, 168, 9, 191, 85, 234, 233, 1, 44, 212, 224, 137, 138, 217, 13, 22, 242, 242, 53, 56, 105, 189, 221, 117, 62, 23, 174, 5, 224, 90, 248, 165, 13, 60, 67, 82, 189, 152, 105, 3, 139, 183, 12, 172, 166, 171, 255, 217, 124, 209, 99, 54, 24, 190, 163, 255, 2, 240, 164, 227, 85, 33, 161, 145, 116, 165, 236, 167, 159, 144, 242, 3, 146, 19, 50, 41, 147, 107, 91, 175, 196, 255, 199, 237, 77, 251, 219, 134, 189, 244, 68, 254, 6, 75, 127, 198, 102, 251, 159, 11, 184, 93, 83, 138, 212, 140, 140, 6, 36, 173, 187, 180, 108, 151, 133, 118, 66, 79, 12, 118, 208, 46, 125, 226, 42, 57, 213, 210, 197, 108, 152, 71, 173, 149, 135, 237, 62, 232, 173, 29, 172, 126, 113, 71, 106, 218, 86, 188, 1, 33, 158, 245, 155, 87, 41, 192, 89, 211, 249, 179, 104, 18, 33, 85, 24, 147, 141, 184, 80, 179, 223, 73, 21, 49, 7, 51, 100, 176, 0, 147, 244, 197, 172, 193, 132, 53, 85, 13, 245, 234, 12, 200, 81, 229, 103, 54, 92, 103, 85, 26, 128, 20, 35, 187, 4, 27, 213, 225, 155, 33, 131, 173, 198, 30, 7, 9, 53, 196, 10, 94, 237, 163, 5, 172, 28, 113, 80, 194, 43, 145, 4, 236, 3, 72, 133, 153, 203, 227, 3, 246, 104, 97, 142, 157, 88, 45, 227, 179, 186, 37, 91, 31, 179, 62, 102, 144, 76, 199, 215, 113, 163, 177, 129, 129, 96, 215, 157, 4, 245, 192, 127, 80, 107, 219, 227, 216, 148, 203, 227, 212, 164, 165, 165, 83, 55, 18, 157, 178, 38, 72, 132, 180, 238, 70, 3, 12, 5, 97, 58, 155, 168, 184, 115, 175, 123, 17, 168, 187, 243, 74, 109, 199, 96, 126, 228, 98, 132, 15, 144, 164, 246, 57, 19, 83, 230, 184, 222, 50, 245, 22, 162, 245, 162, 146, 89, 186, 89, 5, 177, 128, 97, 29, 86, 147, 172, 67, 139, 249, 165, 137, 110, 149, 113, 229, 162, 164, 59, 218, 41, 17, 124, 43, 130, 13, 113, 199, 215, 165, 33, 146, 8, 220, 14, 151, 124, 1, 70, 231, 102, 246, 217, 176, 97, 79, 191, 243, 204, 172, 78, 71, 4, 239, 151, 227, 221, 255, 133, 178, 140, 248, 91, 235, 79, 111, 48, 181, 151, 236, 142, 165, 198, 189, 28, 53, 136, 250, 153, 75, 195, 82, 131, 204, 217, 175, 219, 137, 72, 87, 207, 28, 73, 112, 14, 44, 55, 68, 97, 88, 27, 154, 179, 9, 200, 145, 146, 232, 191, 19, 150, 223, 15, 205, 21, 161, 118, 30, 188, 186, 171, 208, 8, 63, 88, 180, 56, 192, 72, 228, 135, 104, 243, 190, 238, 39, 179, 179, 122, 84, 54, 120, 48, 154, 174, 253, 192, 59, 249, 111, 119, 73, 17, 139, 121, 27, 98, 57, 161, 222, 147, 146, 37, 54, 159, 66, 191, 158, 89, 60, 147, 6, 126, 84, 165, 19, 22, 147, 43, 148, 87, 63, 174, 214, 178, 22, 233, 112, 25, 157, 123, 118, 219, 179, 249, 182, 114, 217, 218, 80, 33, 248, 103, 89, 0, 87, 200, 154, 249, 23, 157, 67, 106, 97, 249, 183, 20, 67, 56, 75, 84, 176, 40, 0, 50, 144, 58, 38, 89, 93, 107, 197, 123, 90, 164, 99, 48, 17, 13, 212, 121, 152, 96, 84, 166, 118, 106, 128, 230, 107, 95, 26, 107, 4, 161, 48, 191, 181, 197, 150, 66, 18, 252, 11, 235, 79, 238, 66, 233, 26, 137, 89, 16, 5, 249, 174, 96, 250, 242, 12, 16, 241, 94, 73, 232, 36, 244, 54, 140, 38, 43, 213, 89, 53, 115, 212, 223, 21, 84, 167, 119, 246, 0, 5, 218, 87, 141, 124, 140, 206, 146, 171, 26, 171, 128, 9, 221, 33, 116, 110, 98, 87, 46, 11, 34, 41, 60, 123, 115, 8, 168, 183, 63, 117, 171, 129, 157, 216, 92, 75, 128, 46, 59, 169, 53, 33, 115, 65, 162, 155, 178, 128, 25, 109, 237, 41, 89, 107, 48, 23, 79, 191, 16, 205, 172, 148, 171, 201, 246, 104, 166, 6, 134, 23, 233, 136, 106, 157, 246, 136, 102, 57, 76, 34, 221, 231, 144, 129, 9, 26, 234, 97, 208, 33, 94, 134, 86, 25, 206, 224, 129, 138, 240, 108, 64, 17, 97, 248, 188, 16, 16, 184, 9, 123, 43, 18, 135, 33, 252, 11, 100, 46, 42, 20, 209, 144, 173, 136, 115, 65, 129, 236, 137, 53, 9, 5, 109, 211, 100, 75, 199, 57, 141, 40, 42, 32, 10, 66, 177, 93, 91, 215, 170, 24, 79, 41, 9, 230, 65, 104, 193, 139, 75, 233, 180, 90, 204, 152, 170, 244, 35, 149, 77, 41, 120, 50, 106, 72, 8, 103, 152, 32, 198, 161, 133, 129, 250, 38, 148, 229, 225, 133, 5, 81, 134, 16, 45, 17, 145, 114, 193, 12, 41, 18, 161, 219, 118, 197, 208, 151, 77, 227, 115, 137, 121, 225, 156, 42, 251, 13, 233, 206, 235, 129, 94, 197, 158, 104, 19, 26, 117, 164, 6, 236, 54, 97, 246, 84, 239, 138, 233, 18, 151, 202, 152, 137, 218, 168, 73, 128, 167, 7, 162, 193, 86, 129, 128, 95, 5, 237, 98, 57, 8, 155, 11, 199, 247, 109, 71, 90, 58, 206, 232, 80, 164, 210, 115, 126, 230, 26, 251, 216, 109, 3, 37, 203, 62, 194, 123, 27, 87, 107, 146, 154, 64, 194, 130, 148, 138, 89, 40, 242, 253, 235, 64, 128, 99, 74, 225, 162, 211, 246, 170, 219, 168, 158, 47, 173, 210, 184, 231, 53, 225, 124, 18, 57, 137, 223, 144, 48, 164, 114, 106, 209, 216, 70, 29, 69, 114, 131, 174, 112, 40, 234, 73, 138, 176, 194, 110, 119, 204, 79, 241, 81, 1, 18, 72, 164, 80, 97, 203, 114, 152, 4, 27, 118, 67, 177, 81, 49, 58, 15, 106, 163, 227, 43, 14, 91, 251, 72, 129, 99, 15, 173, 239, 36, 41, 54, 107, 131, 4, 66, 9, 42, 152, 125, 34, 68, 170, 80, 188, 250, 63, 62, 198, 231, 62, 246, 254, 127, 11, 248, 242, 76, 15, 173, 32, 230, 106, 145, 228, 85, 175, 156, 71, 40, 30, 78, 202, 15, 233, 254, 63, 68, 191, 238, 126, 87, 79, 31, 78, 135, 113, 199, 114, 193, 181, 215, 192, 108, 128, 120, 62, 34, 247, 87, 5, 111, 105, 244, 85, 56, 77, 112, 206, 109, 50, 74, 1, 163, 18, 158, 27, 21, 65, 122, 11, 232, 11, 85, 145, 177, 4, 246, 1, 133, 84, 70, 68, 61, 155, 51, 232, 150, 77, 34, 2, 64, 205, 110, 1, 179, 229, 251, 168, 238, 45, 9, 246, 52, 91, 13, 93, 109, 252, 70, 184, 105, 79, 40, 199, 91, 65, 31, 2, 163, 248, 150, 245, 2, 208, 223, 162, 18, 170, 125, 201, 16, 195, 242, 148, 17, 197, 153, 149, 234, 129, 134, 71, 30, 146, 2, 227, 248, 213, 121, 212, 153, 29, 101, 7, 246, 119, 92, 30, 63, 55, 73, 129, 27, 128, 162, 96, 10, 248, 250, 55, 99, 115, 129, 101, 230, 163, 14, 8, 158, 92, 159, 97, 2, 192, 149, 22, 3, 196, 9, 176, 114, 137, 2, 104, 182, 5, 134, 58, 60, 97, 146, 148, 6, 54, 33, 203, 162, 53, 23, 50, 45, 30, 228, 13, 233, 55, 200, 65, 150, 39, 104, 168, 200, 140, 65, 178, 143, 114, 40, 64, 225, 60, 43, 248, 40, 191, 0, 138, 172, 254, 185, 91, 2, 67, 184, 237, 169, 95, 69, 57, 162, 110, 78, 143, 92, 24, 7, 4, 63, 151, 107, 144, 82, 60, 19, 170, 227, 90, 163, 221, 81, 180, 224, 102, 136, 144, 152, 237, 77, 247, 168, 212, 87, 160, 135, 76, 58, 139, 227, 103, 212, 184, 10, 77, 150, 189, 39, 90, 149, 197, 204, 253, 99, 171, 79, 66, 202, 131, 89, 1, 15, 10, 85, 202, 30, 107, 110, 156, 181, 213, 87, 68, 44, 0, 13, 19, 69, 235, 242, 150, 69, 138, 65, 200, 55, 48, 178, 69, 211, 139, 121, 53, 182, 165, 252, 203, 117, 40, 100, 248, 16, 110, 70, 94, 39, 211, 236, 178, 194, 163, 191, 249, 233, 188, 244, 124, 122, 192, 107, 230, 78, 27, 175, 65, 222, 1, 217, 198, 37, 181, 19, 162, 241, 159, 133, 55, 110, 82, 134, 43, 192, 149, 126, 201, 214, 254, 223, 73, 62, 227, 220, 138, 83, 117, 190, 169, 25, 161, 135, 38, 83, 133, 204, 56, 212, 115, 38, 88, 46, 237, 27, 228, 40, 68, 24, 169, 116, 28, 119, 95, 36, 205, 134, 129, 220, 194, 44, 198, 222, 121, 227, 126, 192, 26, 175, 204, 149, 162, 71, 159, 158, 238, 116, 123, 249, 201, 74, 206, 30, 11, 40, 16, 72, 162, 42, 127, 197, 252, 159, 6, 9, 3, 110, 70, 213, 133, 181, 153, 92, 140, 105, 86, 185, 71, 115, 231, 85, 241, 218, 137, 34, 32, 52, 195, 234, 49, 220, 123, 165, 126, 247, 177, 10, 69, 93, 145, 71, 89, 211, 206, 234, 148, 198, 199, 206, 211, 35, 19, 96, 130, 231, 137, 108, 217, 123, 17, 167, 78, 141, 242, 196, 17, 129, 9, 159, 237, 183, 133, 1, 95, 26, 231, 8, 204, 152, 184, 96, 56, 144, 136, 128, 42, 236, 247, 18, 217, 172, 141, 57, 187, 21, 23, 198, 26, 109, 218, 180, 199, 199, 52, 118, 164, 120, 51, 3, 19, 152, 144, 249, 152, 74, 129, 82, 55, 204, 102, 66, 211, 97, 180, 104, 113, 209, 27, 229, 238, 44, 123, 251, 114, 201, 0, 8, 11, 126, 113, 102, 100, 1, 233, 218, 52, 166, 231, 69, 75, 10, 166, 179, 78, 191, 117, 254, 97, 201, 153, 1, 179, 188, 140, 137, 132, 34, 108, 132, 26, 216, 175, 234, 36, 196, 161, 45, 149, 180, 217, 174, 75, 129, 84, 205, 21, 110, 248, 108, 53, 154, 47, 4, 152, 72, 146, 46, 208, 106, 168, 37, 201, 232, 44, 211, 129, 57, 134, 251, 150, 29, 46, 60, 104, 20, 67, 56, 116, 61, 181, 52, 191, 143, 128, 5, 214, 81, 68, 73, 182, 179, 32, 23, 84, 124, 205, 49, 41, 137, 195, 167, 189, 38, 89, 22, 118, 242, 76, 52, 158, 92, 160, 224, 36, 154, 168, 212, 60, 182, 143, 12, 51, 123, 142, 80, 60, 112, 0, 213, 34, 3, 27, 161, 9, 108, 141, 28, 97, 102, 130, 29, 95, 191, 220, 148, 138, 190, 64, 49, 170, 159, 245, 227, 255, 3, 116, 128, 174, 124, 214, 165, 98, 78, 9, 216, 65, 206, 0, 240, 109, 119, 67, 165, 0, 22, 168, 170, 130, 2, 94, 14, 206, 158, 149, 157, 26, 44, 241, 21, 176, 84, 200, 91, 59, 14, 3, 138, 69, 84, 146, 150, 138, 226, 172, 18, 96, 49, 156, 163, 18, 212, 26, 169, 99, 98, 8, 205, 231, 18, 191, 36, 118, 189, 185, 60, 128, 83, 107, 88, 253, 42, 205, 163, 48, 163, 18, 221, 148, 128, 219, 103, 135, 36, 168, 232, 116, 68, 122, 38, 10, 62, 71, 41, 169, 34, 131, 174, 122, 58, 59, 82, 24, 59, 79, 184, 118, 242, 27, 214, 199, 140, 39, 105, 78, 148, 153, 43, 176, 126, 83, 217, 240, 118, 100, 76, 234, 70, 92, 22, 48, 46, 216, 75, 133, 119, 92, 108, 32, 99, 211, 25, 142, 175, 226, 156, 53, 178, 36, 117, 218, 218, 130, 5, 49, 215, 164, 191, 34, 234, 90, 1, 60, 151, 105, 226, 255, 94, 126, 182, 52, 164, 105, 139, 126, 178, 244, 160, 252, 170, 140, 160, 99, 186, 139, 43, 143, 92, 129, 250, 33, 139, 91, 252, 207, 17, 154, 169, 53, 140, 170, 64, 216, 181, 181, 69, 231, 198, 224, 170, 71, 21, 82, 187, 224, 216, 164, 51, 161, 2, 68, 131, 137, 107, 206, 196, 203, 20, 77, 51, 160, 234, 153, 61, 217, 109, 184, 164, 61, 249, 210, 156, 67, 254, 94, 254, 35, 139, 189, 123, 49, 111, 194, 101, 82, 135, 234, 205, 215, 109, 63, 84, 79, 221, 20, 132, 133, 79, 208, 84, 170, 123, 187, 24, 159, 243, 26, 124, 217, 103, 225, 238, 200, 80, 47, 245, 48, 17, 219, 60, 162, 146, 241, 182, 133, 72, 132, 4, 206, 197, 20, 181, 1, 135, 121, 121, 102, 202, 98, 211, 29, 88, 61, 100, 149, 6, 24, 103, 138, 194, 101, 107, 177, 206, 250, 179, 154, 47, 29, 144, 246, 120, 115, 27, 188, 252, 18, 150, 214, 198, 144, 182, 149, 178, 71, 12, 252, 147, 243, 126, 143, 187, 181, 21, 250, 253, 120, 103, 240, 28, 73, 36, 139, 61, 124, 6, 180, 161, 218, 142, 82, 245, 168, 60, 7, 197, 252, 83, 83, 68, 102, 64, 138, 25, 132, 194, 88, 236, 135, 232, 53, 70, 19, 212, 62, 102, 170, 32, 22, 5, 208, 231, 207, 89, 208, 157, 87, 96, 148, 67, 164, 172, 97, 249, 238, 116, 233, 217, 10, 225, 246, 202, 56, 222, 205, 23, 245, 76, 19, 201, 146, 68, 28, 166, 127, 67, 168, 145, 135, 194, 8, 129, 29, 180, 70, 38, 80, 142, 94, 9, 254, 58, 249, 113, 90, 121, 11, 27, 55, 110, 80, 43, 62, 116, 209, 48, 7, 133, 100, 72, 227, 79, 69, 25, 253, 109, 6, 127, 214, 182, 234, 26, 231, 79, 201, 180, 42, 209, 200, 185, 110, 118, 71, 103, 233, 178, 98, 56, 70, 193, 27, 30, 133, 115, 89, 37, 192, 103, 138, 49, 222, 9, 166, 94, 84, 214, 119, 147, 161, 190, 93, 253, 110, 110, 208, 89, 174, 143, 83, 164, 147, 39, 228, 244, 128, 66, 8, 46, 42, 225, 45, 150, 234, 138, 17, 139, 48, 33, 36, 88, 43, 219, 159, 3, 28, 230, 122, 23, 4, 116, 204, 220, 129, 173, 42, 0, 176, 17, 23, 221, 184, 218, 68, 210, 178, 163, 102, 122, 148, 105, 208, 188, 158, 52, 134, 84, 62, 90, 242, 66, 174, 54, 14, 51, 96, 205, 239, 250, 121, 155, 33, 163, 224, 42, 57, 199, 183, 224, 242, 114, 96, 160, 253, 199, 53, 253, 61, 158, 9, 32, 103, 93, 232, 0, 125, 247, 146, 32, 217, 137, 214, 186, 110, 15, 91, 127, 246, 127, 33, 135, 7, 44, 2, 162, 224, 205, 152, 69, 128, 162, 87, 10, 160, 38, 36, 59, 72, 118, 204, 166, 236, 102, 149, 44, 13, 60, 187, 63, 83, 19, 49, 253, 110, 147, 55, 20, 172, 83, 120, 0, 177, 96, 25, 45, 154, 37, 135, 8, 145, 124, 182, 7, 12, 47, 170, 118, 175, 60, 131, 98, 0, 128, 43, 58, 251, 84, 47, 10, 77, 140, 175, 223, 52, 132, 221, 193, 62, 191, 80, 150, 188, 62, 194, 149, 9, 145, 250, 79, 199, 24, 233, 21, 13, 55, 35, 212, 59, 6, 124, 64, 93, 67, 61, 98, 219, 129, 3, 163, 178, 107, 8, 173, 253, 110, 79, 167, 255, 253, 215, 170, 129, 49, 148, 31, 58, 203, 79, 101, 113, 18, 123, 212, 29, 194, 173, 8, 1, 14, 232, 64, 95, 205, 2, 18, 64, 181, 240, 112, 39, 155, 254, 68, 182, 53, 203, 108, 109, 215, 168, 12, 186, 184, 4, 45, 21, 222, 54, 155, 212, 64, 96, 186, 246, 158, 161, 32, 68, 205, 3, 240, 2, 207, 218, 168, 147, 50, 219, 208, 252, 74, 112, 131, 200, 28, 86, 42, 228, 62, 6, 128, 202, 234, 205, 32, 102, 143, 154, 128, 165, 77, 208, 44, 20, 134, 200, 75, 221, 112, 218, 178, 194, 170, 245, 151, 185, 16, 201, 98, 209, 37, 129, 168, 175, 23, 99, 72, 251, 40, 217, 139, 209, 131, 185, 222, 232, 197, 152, 198, 147, 33, 226, 225, 66, 211, 51, 150, 105, 103, 241, 27, 44, 236, 24, 170, 51, 142, 222, 8, 157, 106, 232, 145, 236, 170, 1, 17, 89, 91, 216, 40, 238, 187, 81, 30, 53, 51, 146, 206, 216, 142, 62, 189, 71, 145, 64, 40, 244, 96, 92, 51, 186, 135, 165, 172, 17, 180, 165, 204, 104, 232, 84, 147, 125, 49, 217, 183, 133, 14, 208, 208, 207, 195, 139, 234, 193, 139, 16, 25, 22, 131, 233, 222, 232, 96, 177, 245, 73, 223, 134, 164, 24, 121, 190, 150, 229, 85, 177, 233, 82, 164, 96, 175, 62, 105, 247, 93, 3, 84, 254, 101, 226, 65, 71, 70, 161, 190, 127, 111, 31, 239, 186, 185, 119, 191, 66, 238, 32, 58, 187, 136, 8, 3, 210, 127, 197, 17, 70, 225, 84, 248, 206, 18, 94, 163, 26, 93, 105, 96, 217, 210, 142, 12, 14, 91, 182, 61, 62, 208, 0, 91, 118, 195, 221, 38, 127, 81, 166, 106, 206, 141, 43, 191, 27, 205, 130, 156, 33, 135, 135, 38, 245, 222, 64, 39, 162, 70, 207, 152, 249, 105, 249, 4, 200, 130, 77, 65, 132, 230, 174, 163, 227, 57, 235, 106, 70, 186, 163, 244, 173, 248, 45, 238, 86, 108, 215, 68, 243, 114, 185, 124, 203, 135, 227, 122, 201, 215, 100, 19, 206, 13, 5, 29, 116, 117, 80, 19, 19, 112, 63, 63, 235, 42, 57, 7, 19, 253, 206, 106, 4, 143, 78, 61, 244, 217, 71, 252, 226, 229, 130, 205, 45, 254, 194, 233, 40, 250, 208, 32, 163, 106, 241, 161, 218, 23, 238, 94, 126, 208, 142, 223, 231, 43, 103, 178, 149, 172, 62, 33, 0, 198, 45, 197, 208, 157, 106, 21, 40, 88, 9, 211, 112, 8, 130, 246, 234, 102, 193, 185, 158, 176, 61, 40, 85, 16, 39, 238, 25, 205, 29, 100, 166, 5, 109, 34, 165, 187, 210, 36, 190, 170, 57, 243, 144, 157, 243, 24, 251, 95, 239, 23, 44, 149, 164, 179, 147, 231, 141, 111, 223, 3, 48, 167, 187, 172, 174, 225, 98, 187, 98, 180, 154, 99, 207, 38, 79, 233, 142, 163, 169, 59, 63, 151, 235, 70, 63, 32, 110, 112, 210, 11, 31, 221, 250, 245, 142, 90, 123, 8, 207, 96, 97, 29, 1, 178, 16, 151, 21, 4, 34, 19, 170, 74, 136, 70, 75, 164, 117, 97, 7, 209, 49, 64, 39, 67, 211, 233, 201, 255, 147, 85, 141, 238, 60, 253, 3, 26, 62, 66, 50, 69, 64, 4, 218, 85, 154, 254, 54, 24, 27, 136, 127, 254, 10, 77, 5, 60, 74, 116, 36, 4, 98, 129, 167, 228, 134, 187, 106, 250, 136, 174, 45, 180, 102, 38, 209, 246, 192, 231, 164, 95, 56, 74, 228, 22, 67, 141, 36, 124, 53, 192, 99, 254, 21, 110, 116, 72, 112, 231, 100, 195, 39, 222, 90, 212, 189, 174, 78, 220, 228, 36, 54, 54, 138, 230, 117, 22, 228, 24, 146, 90, 182, 180, 37, 142, 173, 194, 115, 146, 236, 191, 82, 132, 141, 5, 126, 82, 203, 70, 33, 147, 197, 234, 90, 201, 200, 180, 161, 34, 90, 156, 99, 108, 181, 185, 244, 105, 182, 12, 152, 53, 171, 149, 41, 243, 158, 153, 243, 14, 38, 161, 177, 181, 33, 27, 15, 194, 135, 20, 31, 156, 61, 53, 84, 209, 114, 179, 83, 178, 160, 170, 46, 15, 101, 178, 94, 60, 43, 22, 149, 141, 49, 199, 172, 178, 186, 23, 138, 173, 41, 244, 95, 129, 232, 165, 232, 32, 172, 45, 78, 227, 148, 56, 20, 163, 104, 35, 236, 99, 73, 132, 19, 138, 182, 171, 73, 185, 246, 99, 11, 62, 190, 255, 149, 83, 20, 37, 221, 202, 233, 83, 224, 78, 153, 149, 224, 4, 209, 138, 204, 161, 38, 50, 79, 187, 219, 29, 201, 189, 191, 107, 171, 0, 6, 23, 119, 144, 217, 146, 27, 249, 243, 234, 22, 146, 198, 248, 99, 200, 164, 120, 12, 147, 174, 101, 88, 248, 180, 111, 219, 38, 236, 203, 74, 76, 14, 73, 204, 131, 220, 92, 255, 176, 63, 133, 157, 99, 165, 237, 108, 195, 247, 104, 180, 228, 0, 128, 47, 135, 74, 184, 226, 134, 106, 118, 36, 34, 95, 80, 243, 17, 191, 4, 68, 48, 19, 28, 227, 232, 85, 147, 24, 173, 54, 20, 138, 240, 105, 203, 133, 145, 20, 134, 169, 85, 21, 168, 214, 136, 206, 46, 18, 85, 191, 167, 15, 255, 182, 60, 63, 93, 42, 61, 47, 22, 18, 82, 120, 223, 24, 50, 136, 141, 218, 227, 224, 120, 171, 49, 221, 134, 68, 222, 222, 123, 72, 17, 112, 23, 183, 227, 200, 170, 121, 13, 251, 113, 0, 184, 12, 128, 123, 75, 103, 100, 200, 161, 51, 255, 63, 83, 237, 198, 191, 117, 144, 109, 212, 34, 220, 180, 68, 69, 137, 55, 96, 216, 89, 42, 186, 104, 144, 227, 5, 102, 170, 118, 161, 133, 41, 227, 25, 244, 127, 249, 252, 132, 81, 115, 107, 59, 151, 158, 71, 69, 36, 52, 220, 254, 120, 107, 3, 100, 8, 25, 64, 174, 6, 71, 203, 11, 43, 41, 79, 75, 55, 50, 190, 208, 109, 48, 185, 253, 105, 126, 25, 1, 9, 136, 131, 150, 92, 200, 64, 154, 187, 74, 55, 167, 21, 193, 217, 202, 181, 65, 28, 213, 40, 105, 129, 212, 54, 141, 205, 106, 110, 7, 68, 109, 21, 73, 201, 173, 56, 193, 154, 144, 174, 152, 3, 81, 195, 27, 100, 231, 16, 28, 217, 11, 186, 123, 79, 149, 180, 167, 126, 93, 206, 180, 33, 45, 178, 250, 52, 96, 242, 26, 96, 128, 119, 128, 5, 24, 142, 156, 173, 159, 47, 97, 153, 16, 102, 86, 217, 161, 236, 31, 99, 33, 255, 206, 150, 165, 247, 136, 45, 175, 175, 211, 130, 174, 3, 21, 95, 41, 216, 230, 109, 168, 48, 10, 48, 97, 87, 201, 3, 221, 206, 15, 59, 6, 33, 88, 107, 115, 33, 0, 90, 170, 162, 68, 98, 150, 102, 85, 114, 79, 125, 72, 153, 218, 188, 221, 145, 206, 165, 157, 136, 96, 46, 175, 41, 99, 190, 46, 249, 224, 131, 74, 125, 229, 129, 156, 152, 248, 100, 215, 64, 40, 64, 98, 75, 249, 120, 171, 74, 65, 160, 210, 0, 215, 248, 207, 129, 148, 171, 192, 96, 79, 35, 196, 165, 232, 4, 48, 107, 145, 31, 1, 64, 145, 143, 235, 75, 43, 122, 197, 193, 97, 73, 13, 142, 36, 62, 34, 109, 47, 161, 122, 207, 184, 192, 76, 238, 209, 142, 195, 205, 178, 136, 233, 41, 184, 135, 53, 251, 69, 167, 190, 247, 16, 95, 116, 70, 225, 177, 113, 197, 94, 21, 252, 105, 245, 235, 93, 100, 226, 203, 253, 224, 214, 107, 187, 133, 152, 220, 200, 234, 237, 64, 252, 25, 76, 96, 58, 80, 201, 68, 154, 131, 141, 202, 221, 199, 26, 186, 87, 110, 199, 201, 251, 60, 170, 68, 240, 6, 15, 122, 149, 213, 110, 94, 246, 11, 50, 31, 170, 9, 3, 4, 84, 132, 9, 73, 147, 39, 120, 106, 52, 35, 73, 2, 226, 214, 68, 214, 228, 91, 4, 31, 196, 39, 129, 78, 61, 170, 247, 156, 136, 233, 67, 80, 154, 115, 140, 149, 159, 250, 206, 22, 26, 152, 199, 195, 240, 163, 80, 78, 92, 97, 238, 106, 145, 105, 18, 191, 108, 86, 197, 194, 227, 157, 51, 127, 25, 178, 11, 56, 114, 15, 102, 78, 165, 59, 53, 200, 28, 206, 141, 235, 10, 61, 154, 192, 58, 228, 132, 135, 241, 19, 116, 114, 185, 103, 210, 146, 117, 162, 19, 163, 244, 251, 123, 194, 148, 19, 209, 218, 105, 158, 87, 242, 132, 219, 4, 188, 104, 0, 219, 95, 47, 151, 15, 115, 87, 217, 154, 124, 220, 237, 40, 45, 188, 241, 81, 110, 249, 17, 69, 70, 13, 230, 76, 68, 26, 118, 147, 250, 254, 76, 195, 46, 65, 79, 181, 24, 143, 125, 55, 49, 109, 57, 171, 53, 45, 93, 198, 137, 254, 229, 160, 116, 221, 4, 20, 22, 202, 4, 87, 64, 235, 68, 180, 214, 192, 169, 224, 96, 104, 250, 56, 49, 87, 70, 38, 239, 58, 124, 27, 69, 83, 232, 127, 154, 42, 186, 53, 91, 76, 129, 202, 72, 209, 197, 182, 57, 64, 2, 34, 82, 214, 42, 126, 198, 170, 222, 241, 87, 230, 6, 162, 158, 199, 39, 215, 119, 83, 72, 172, 176, 208, 204, 67, 174, 212, 153, 70, 2, 150, 162, 175, 242, 13, 170, 143, 4, 45, 96, 44, 190, 62, 148, 184, 239, 234, 134, 116, 239, 63, 195, 15, 136, 12, 238, 69, 18, 46, 22, 187, 103, 212, 202, 66, 185, 44, 68, 55, 85, 29, 82, 181, 184, 183, 61, 200, 152, 103, 13, 82, 236, 12, 213, 158, 205, 33, 177, 139, 117, 206, 105, 78, 80, 153, 27, 222, 169, 37, 112, 79, 99, 182, 217, 35, 9, 210, 28, 241, 196, 236, 51, 104, 204, 247, 74, 118, 216, 247, 107, 182, 56, 105, 185, 131, 225, 2, 58, 78, 59, 75, 218, 52, 166, 17, 52, 0, 149, 191, 153, 67, 56, 36, 10, 169, 247, 143, 175, 203, 160, 41, 77, 251, 166, 119, 153, 146, 101, 149, 114, 22, 23, 25, 95, 128, 254, 130, 30, 100, 28, 164, 202, 224, 231, 159, 170, 9, 101, 210, 211, 144, 16, 83, 146, 204, 101, 168, 56, 133, 169, 168, 7, 65, 46, 224, 177, 28, 233, 43, 7, 27, 93, 58, 239, 75, 237, 14, 197, 213, 232, 57, 30, 39, 211, 248, 243, 192, 246, 85, 190, 64, 215, 7, 136, 60, 10, 67, 168, 185, 128, 60, 77, 210, 235, 211, 123, 24, 55, 66, 163, 201, 141, 30, 212, 130, 125, 171, 104, 55, 254, 60, 106, 25, 41, 109, 172, 195, 46, 50, 183, 222, 15, 55, 171, 47, 79, 1, 40, 32, 241, 140, 103, 13, 70, 187, 151, 50, 192, 125, 54, 108, 98, 39, 134, 10, 211, 94, 62, 198, 206, 100, 58, 220, 107, 30, 237, 113, 236, 157, 202, 244, 132, 239, 230, 147, 248, 166, 249, 79, 251, 123, 198, 203, 207, 46, 147, 37, 200, 158, 18, 237, 175, 91, 12, 225, 179, 140, 145, 84, 137, 5, 34, 56, 36, 102, 199, 89, 101, 225, 185, 38, 200, 1, 251, 73, 98, 211, 137, 135, 22, 32, 241, 144, 245, 140, 106, 22, 158, 105, 45, 192, 109, 25, 249, 251, 42, 206, 58, 20, 71, 169, 175, 96, 37, 147, 146, 34, 56, 220, 19, 137, 142, 151, 49, 174, 177, 103, 143, 251, 245, 165, 93, 105, 122, 52, 78, 57, 65, 25, 21, 241, 1, 221, 30, 228, 250, 89, 49, 51, 171, 21, 155, 92, 201, 1, 143, 132, 91, 124, 192, 40, 59, 79, 10, 185, 189, 76, 188, 123, 120, 181, 78, 208, 139, 217, 222, 121, 208, 224, 212, 175, 25, 202, 29, 122, 175, 168, 211, 52, 146, 3, 4, 50, 37, 161, 159, 183, 223, 7, 181, 180, 190, 147, 118, 119, 120, 81, 27, 132, 2, 111, 182, 81, 192, 95, 145, 229, 95, 61, 226, 51, 208, 197, 151, 207, 221, 165, 180, 219, 71, 50, 248, 72, 250, 38, 118, 202, 190, 235, 60, 246, 243, 163, 217, 101, 132, 31, 163, 89, 1, 1, 248, 13, 148, 100, 176, 29, 186, 10, 154, 87, 253, 143, 209, 162, 216, 130, 91, 229, 29, 187, 250, 150, 193, 67, 80, 165, 162, 148, 91, 54, 17, 187, 179, 154, 233, 234, 41, 185, 102, 176, 132, 48, 233, 133, 97, 225, 68, 142, 131, 90, 239, 63, 224, 150, 164, 91, 24, 251, 33, 2, 135, 117, 188, 154, 104, 59, 129, 88, 186, 141, 130, 165, 198, 149, 66, 110, 201, 20, 104, 195, 30, 7, 14, 66, 9, 32, 73, 120, 168, 111, 28, 58, 171, 103, 164, 57, 173, 10, 182, 125, 27, 169, 15, 97, 200, 167, 41, 187, 184, 141, 180, 176, 77, 43, 242, 232, 131, 252, 98, 76, 67, 254, 210, 239, 183, 232, 132, 16, 253, 129, 155, 236, 170, 13, 42, 244, 88, 86, 150, 60, 103, 59, 214, 240, 75, 20, 46, 9, 212, 71, 63, 64, 64, 135, 240, 67, 72, 253, 112, 73, 152, 250, 231, 142, 192, 149, 23, 137, 14, 115, 60, 135, 29, 134, 12, 4, 138, 86, 47, 7, 122, 51, 134, 249, 28, 222, 227, 191, 96, 107, 151, 157, 4, 200, 161, 131, 50, 248, 55, 222, 99, 97, 171, 84, 209, 146, 75, 78, 33, 37, 129, 172, 194, 235, 181, 248, 146, 127, 66, 72, 189, 59, 77, 211, 157, 5, 241, 113, 111, 47, 185, 24, 142, 240, 212, 125, 96, 166, 132, 16, 57, 30, 83, 164, 63, 243, 225, 56, 62, 87, 111, 75, 102, 154, 175, 213, 41, 186, 116, 42, 51, 144, 94, 22, 179, 252, 103, 231, 125, 45, 235, 218, 57, 232, 207, 132, 2, 221, 118, 176, 232, 181, 222, 247, 46, 2, 49, 120, 172, 47, 206, 38, 148, 117, 106, 135, 201, 193, 118, 63, 253, 24, 133, 68, 133, 95, 1, 152, 35, 6, 109, 234, 25, 183, 59, 113, 192, 238, 0, 112, 90, 30, 61, 39, 240, 75, 65, 75, 158, 162, 45, 38, 104, 227, 112, 194, 159, 141, 220, 187, 60, 121, 227, 233, 192, 197, 207, 249, 183, 20, 57, 3, 235, 71, 88, 146, 178, 101, 78, 32, 20, 255, 17, 95, 139, 112, 195, 73, 69, 86, 222, 21, 157, 86, 30, 42, 152, 188, 77, 191, 35, 212, 40, 88, 107, 162, 186, 102, 98, 40, 110, 179, 174, 198, 123, 69, 177, 169, 179, 107, 182, 249, 232, 28, 37, 193, 162, 99, 36, 171, 32, 27, 152, 97, 167, 55, 141, 248, 67, 132, 152, 125, 113, 60, 183, 139, 232, 96, 196, 91, 47, 104, 109, 249, 128, 171, 19, 153, 23, 112, 142, 191, 90, 146, 188, 161, 112, 31, 110, 31, 50, 65, 137, 55, 6, 44, 234, 57, 112, 190, 39, 71, 112, 115, 56, 177, 158, 35, 190, 153, 119, 97, 128, 72, 38, 252, 67, 27, 130, 18, 68, 149, 0, 246, 236, 9, 194, 140, 219, 18, 131, 227, 224, 116, 68, 154, 140, 79, 135, 223, 154, 73, 141, 84, 177, 6, 149, 233, 48, 234, 166, 74, 8, 9, 228, 241, 33, 84, 205, 226, 241, 43, 63, 9, 70, 163, 184, 58, 204, 53, 160, 46, 248, 38, 67, 155, 157, 207, 182, 112, 171, 65, 140, 50, 174, 80, 17, 226, 170, 243, 169, 80, 130, 101, 242, 89, 137, 5, 158, 52, 16, 139, 119, 175, 210, 112, 63, 50, 53, 173, 35, 64, 13, 40, 133, 102, 197, 242, 170, 152, 13, 102, 105, 138, 61, 100, 1, 109, 248, 107, 190, 169, 15, 45, 182, 36, 236, 34, 188, 125, 44, 119, 140, 63, 87, 74, 197, 201, 85, 193, 36, 71, 152, 130, 186, 122, 234, 175, 99, 73, 241, 207, 131, 134, 210, 136, 211, 25, 61, 202, 242, 167, 189, 163, 112, 162, 132, 55, 64, 94, 113, 111, 60, 51, 100, 136, 83, 238, 135, 190, 14, 147, 201, 36, 240, 173, 37, 45, 25, 153, 71, 15, 36, 200, 0, 241, 149, 194, 180, 141, 54, 37, 17, 72, 88, 157, 107, 137, 145, 203, 178, 208, 5, 190, 112, 19, 200, 90, 152, 8, 122, 212, 172, 130, 0, 156, 83, 106, 199, 126, 53, 126, 118, 78, 142, 164, 110, 2, 51, 206, 229, 115, 164, 191, 188, 96, 242, 143, 76, 210, 19, 63, 160, 210, 53, 167, 191, 74, 137, 36, 107, 94, 236, 129, 8, 128, 42, 156, 72, 104, 220, 101, 196, 198, 141, 191, 235, 202, 210, 187, 143, 3, 222, 146, 109, 142, 94, 19, 176, 150, 161, 130, 14, 191, 77, 100, 101, 96, 10, 249, 193, 34, 40, 141, 99, 39, 170, 25, 64, 196, 228, 160, 7, 219, 209, 252, 9, 114, 137, 126, 157, 36, 124, 50, 106, 130, 138, 182, 166, 54, 247, 227, 157, 200, 78, 225, 82, 14, 233, 140, 69, 26, 10, 67, 15, 41, 109, 11, 178, 125, 76, 165, 155, 68, 205, 194, 3, 146, 142, 85, 112, 3, 191, 97, 46, 210, 36, 135, 212, 160, 164, 87, 223, 8, 242, 110, 0, 138, 113, 115, 3, 255, 95, 124, 195, 237, 171, 192, 244, 168, 74, 174, 184, 181, 149, 17, 63, 110, 106, 173, 219, 66, 203, 120, 137, 222, 69, 236, 124, 226, 216, 137, 75, 217, 128, 79, 121, 189, 237, 116, 142, 247, 17, 9, 193, 66, 160, 232, 27, 85, 174, 153, 198, 4, 238, 28, 199, 138, 28, 204, 64, 178, 151, 43, 24, 208, 179, 88, 149, 94, 59, 255, 108, 237, 122, 86, 199, 172, 246, 24, 3, 13, 142, 84, 140, 207, 186, 6, 112, 201, 69, 44, 94, 160, 76, 155, 125, 146, 24, 254, 41, 204, 125, 175, 32, 233, 112, 251, 221, 52, 2, 96, 153, 128, 81, 191, 165, 233, 101, 20, 22, 49, 15, 235, 228, 193, 200, 48, 165, 179, 192, 154, 192, 52, 56, 235, 8, 216, 21, 72, 50, 4, 173, 34, 50, 135, 19, 114, 142, 54, 97, 247, 23, 218, 64, 223, 120, 196, 97, 37, 123, 214, 227, 245, 42, 110, 7, 225, 122, 146, 39, 159, 151, 108, 229, 130, 237, 169, 6, 61, 178, 237, 105, 121, 90, 22, 228, 249, 12, 29, 143, 207, 179, 52, 17, 163, 169, 120, 149, 242, 51, 228, 235, 95, 194, 26, 35, 226, 224, 47, 203, 138, 159, 183, 208, 41, 20, 130, 86, 9, 99, 131, 192, 175, 123, 54, 43, 120, 190, 0, 203, 178, 80, 119, 181, 39, 159, 210, 140, 147, 69, 24, 220, 168, 50, 255, 51, 34, 123, 137, 167, 160, 138, 67, 189, 206, 207, 230, 193, 95, 190, 161, 33, 231, 80, 77, 15, 134, 216, 33, 208, 235, 88, 131, 41, 146, 214, 30, 229, 220, 248, 114, 132, 10, 175, 38, 124, 55, 160, 172, 152, 37, 64, 124, 21, 233, 63, 1, 36, 239, 231, 54, 4, 79, 48, 109, 203, 128, 130, 25, 64, 83, 151, 227, 15, 226, 78, 8, 14, 113, 100, 58, 192, 243, 65, 6, 69, 216, 195, 180, 11, 140, 127, 182, 70, 187, 24, 156, 129, 20, 88, 212, 145, 235, 158, 107, 37, 39, 141, 1, 234, 111, 244, 25, 209, 37, 187, 30, 130, 177, 141, 161, 170, 129, 255, 139, 111, 40, 20, 61, 23, 60, 17, 104, 35, 134, 120, 241, 132, 25, 171, 119, 184, 22, 54, 66, 79, 153, 9, 78, 173, 9, 104, 93, 75, 177, 0, 250, 15, 254, 84, 67, 40, 99, 19, 171, 46, 3, 229, 22, 164, 144, 224, 219, 23, 98, 161, 224, 118, 52, 129, 201, 15, 164, 180, 156, 217, 139, 213, 108, 43, 61, 36, 218, 200, 85, 54, 157, 11, 47, 121, 6, 249, 158, 231, 22, 143, 28, 5, 82, 88, 188, 160, 250, 244, 237, 15, 63, 139, 235, 244, 224, 12, 62, 238, 177, 75, 197, 192, 225, 203, 38, 43, 197, 17, 178, 92, 224, 234, 100, 204, 83, 28, 209, 46, 156, 213, 31, 48, 32, 94, 118, 226, 27, 36, 212, 223, 0, 153, 208, 193, 156, 145, 100, 148, 119, 213, 20, 106, 96, 6, 105, 229, 182, 199, 33, 153, 118, 148, 36, 54, 80, 199, 254, 116, 202, 39, 201, 245, 163, 177, 236, 75, 159, 233, 37, 27, 37, 31, 25, 237, 65, 19, 190, 222, 251, 179, 199, 245, 147, 197, 238, 205, 16, 169, 250, 233, 174, 212, 235, 9, 115, 105, 161, 145, 199, 19, 202, 208, 98, 213, 47, 27, 53, 57, 124, 248, 12, 245, 2, 234, 64, 107, 213, 182, 5, 243, 29, 238, 192, 110, 132, 174, 114, 243, 123, 102, 119, 241, 100, 86, 27, 13, 210, 46, 7, 204, 144, 180, 223, 55, 47, 37, 126, 104, 16, 62, 77, 75, 245, 4, 128, 241, 149, 0, 93, 154, 82, 0, 146, 255, 99, 57, 57, 247, 120, 185, 201, 183, 102, 10, 66, 221, 252, 235, 33, 64, 60, 69, 250, 119, 211, 223, 118, 189, 253, 229, 194, 13, 230, 100, 111, 209, 71, 90, 217, 141, 40, 196, 171, 168, 118, 252, 16, 192, 124, 148, 41, 109, 126, 234, 85, 219, 95, 122, 167, 217, 196, 199, 231, 219, 77, 100, 190, 106, 253, 225, 180, 190, 193, 0, 76, 181, 254, 210, 241, 196, 76, 50, 158, 234, 120, 116, 136, 75, 52, 192, 192, 6, 16, 9, 0, 137, 110, 112, 246, 55, 14, 204, 135, 140, 93, 226, 121, 120, 0, 112, 2, 165, 151, 57, 96, 215, 201, 178, 237, 229, 193, 76, 145, 83, 87, 17, 121, 130, 192, 21, 116, 6, 252, 97, 219, 72, 173, 174, 227, 26, 128, 206, 197, 51, 31, 122, 29, 187, 60, 132, 168, 148, 90, 39, 219, 87, 117, 178, 210, 99, 121, 43, 116, 82, 21, 209, 134, 11, 3, 24, 117, 15, 222, 207, 206, 41, 242, 2, 150, 44, 174, 28, 218, 155, 181, 71, 196, 137, 193, 135, 105, 16, 172, 54, 79, 115, 79, 192, 207, 130, 21, 195, 213, 37, 244, 50, 82, 36, 62, 236, 31, 79, 193, 244, 166, 96, 239, 250, 238, 181, 65, 254, 193, 246, 135, 204, 94, 71, 6, 198, 168, 141, 48, 0, 228, 153, 70, 86, 4, 28, 87, 208, 182, 82, 12, 93, 32, 70, 176, 196, 176, 106, 36, 30, 166, 151, 128, 33, 89, 28, 171, 26, 120, 184, 227, 229, 5, 106, 165, 100, 57, 82, 248, 165, 3, 172, 98, 114, 235, 68, 178, 80, 228, 66, 139, 50, 107, 235, 68, 148, 115, 127, 111, 199, 8, 95, 64, 190, 239, 90, 232, 64, 0, 6, 234, 225, 143, 45, 171, 9, 242, 88, 109, 135, 246, 54, 31, 158, 46, 196, 2, 189, 147, 152, 70, 147, 170, 150, 134, 77, 87, 165, 64, 134, 254, 202, 193, 190, 200, 89, 21, 217, 241, 229, 110, 154, 16, 15, 20, 111, 112, 183, 208, 75, 130, 111, 201, 84, 41, 9, 133, 8, 7, 224, 162, 168, 20, 118, 191, 48, 134, 215, 129, 68, 223, 44, 193, 132, 2, 94, 177, 33, 166, 248, 249, 106, 16, 36, 231, 134, 245, 48, 213, 23, 155, 109, 206, 95, 77, 211, 49, 193, 95, 6, 63, 113, 52, 75, 163, 225, 78, 242, 245, 34, 153, 240, 53, 187, 224, 205, 223, 173, 78, 196, 129, 100, 150, 139, 157, 111, 145, 145, 176, 203, 100, 190, 115, 50, 94, 43, 137, 96, 175, 169, 91, 204, 155, 101, 35, 121, 68, 254, 116, 65, 85, 63, 75, 187, 28, 104, 224, 7, 117, 149, 129, 209, 50, 89, 60, 199, 215, 117, 147, 205, 221, 36, 39, 103, 69, 5, 91, 43, 115, 26, 131, 98, 153, 144, 123, 149, 82, 120, 45, 177, 229, 37, 3, 134, 3, 25, 25, 184, 6, 41, 142, 180, 82, 218, 18, 152, 89, 155, 83, 237, 55, 171, 31, 193, 37, 232, 246, 138, 105, 215, 81, 37, 35, 15, 80, 198, 146, 21, 41, 18, 128, 242, 250, 32, 56, 47, 150, 116, 193, 46, 178, 84, 7, 39, 124, 147, 132, 29, 94, 255, 48, 164, 66, 255, 9, 228, 33, 195, 122, 6, 125, 64, 113, 90, 186, 179, 51, 52, 90, 224, 112, 204, 14, 92, 37, 179, 184, 62, 169, 189, 97, 143, 199, 179, 212, 55, 196, 106, 26, 201, 36, 192, 253, 157, 170, 209, 155, 153, 2, 232, 152, 154, 107, 233, 138, 245, 110, 208, 41, 38, 7, 216, 79, 185, 21, 17, 47, 80, 0, 116, 242, 11, 247, 93, 85, 116, 81, 108, 129, 246, 103, 41, 56, 161, 167, 108, 143, 107, 93, 165, 115, 215, 33, 66, 113, 122, 49, 89, 38, 225, 162, 94, 221, 169, 44, 81, 225, 195, 233, 14, 65, 188, 17, 86, 245, 52, 185, 69, 2, 179, 210, 185, 45, 211, 134, 178, 166, 115, 53, 239, 161, 187, 176, 206, 216, 31, 191, 63, 65, 193, 192, 150, 237, 9, 132, 13, 147, 243, 87, 148, 116, 213, 205, 43, 243, 242, 188, 76, 28, 49, 49, 23, 25, 240, 67, 227, 120, 46, 124, 153, 165, 127, 13, 91, 250, 140, 65, 79, 67, 59, 49, 37, 124, 125, 245, 226, 224, 150, 193, 165, 6, 92, 189, 189, 114, 199, 239, 13, 11, 78, 42, 4, 195, 24, 115, 23, 231, 32, 232, 201, 24, 166, 12, 113, 53, 46, 6, 126, 74, 245, 144, 37, 22, 63, 36, 29, 149, 11, 229, 83, 109, 7, 78, 199, 22, 129, 204, 162, 118, 44, 133, 11, 234, 253, 12, 40, 110, 150, 187, 80, 66, 126, 16, 121, 32, 36, 237, 19, 235, 217, 50, 94, 253, 46, 25, 54, 124, 191, 67, 128, 143, 42, 184, 117, 108, 218, 112, 116, 123, 163, 187, 1, 209, 135, 24, 107, 156, 168, 193, 71, 247, 36, 117, 246, 4, 6, 51, 32, 147, 42, 170, 142, 101, 142, 216, 148, 9, 170, 154, 108, 165, 208, 106, 179, 236, 100, 17, 214, 232, 158, 145, 58, 210, 148, 100, 65, 255, 69, 232, 172, 194, 200, 56, 80, 66, 244, 90, 42, 3, 244, 123, 171, 22, 102, 1, 134, 9, 165, 208, 173, 136, 221, 156, 4, 57, 185, 34, 149, 106, 140, 21, 227, 60, 35, 1, 107, 176, 74, 109, 81, 66, 147, 172, 91, 32, 251, 21, 168, 167, 188, 93, 142, 159, 188, 16, 134, 67, 51, 137, 234, 158, 123, 112, 31, 186, 28, 160, 137, 8, 7, 202, 94, 190, 250, 24, 169, 229, 130, 232, 112, 166, 225, 153, 46, 29, 148, 108, 241, 111, 97, 103, 125, 32, 21, 119, 25, 185, 241, 35, 116, 211, 138, 35, 11, 140, 1, 50, 178, 91, 134, 31, 89, 69, 22, 53, 119, 150, 81, 48, 39, 29, 224, 77, 102, 117, 139, 139, 40, 32, 17, 153, 24, 17, 228, 3, 142, 9, 43, 147, 22, 88, 113, 105, 213, 11, 252, 157, 187, 174, 53, 201, 193, 239, 146, 244, 163, 122, 203, 41, 99, 32, 211, 22, 29, 32, 147, 13, 142, 239, 67, 44, 69, 185, 0, 18, 242, 222, 11, 86, 211, 59, 104, 196, 181, 152, 143, 248, 62, 42, 193, 1, 88, 157, 77, 37, 227, 81, 165, 213, 176, 198, 92, 178, 15, 200, 176, 67, 190, 21, 168, 146, 219, 164, 0, 168, 147, 110, 132, 1, 72, 131, 228, 128, 52, 183, 124, 198, 223, 172, 123, 135, 244, 217, 13, 246, 241, 207, 7, 149, 50, 180, 21, 206, 254, 34, 64, 200, 235, 210, 217, 43, 28, 171, 8, 111, 66, 96, 122, 29, 178, 7, 31, 150, 156, 42, 26, 16, 42, 117, 8, 141, 189, 104, 175, 27, 159, 237, 255, 232, 62, 221, 210, 6, 138, 159, 14, 199, 115, 146, 70, 143, 242, 80, 233, 9, 182, 197, 191, 70, 246, 98, 66, 216, 11, 123, 31, 236, 123, 39, 144, 67, 118, 202, 86, 217, 24, 86, 20, 21, 245, 34, 247, 186, 201, 134, 65, 169, 196, 231, 87, 152, 62, 97, 122, 38, 69, 180, 26, 81, 100, 88, 175, 203, 26, 107, 1, 209, 180, 202, 141, 4, 254, 108, 129, 190, 124, 23, 214, 130, 213, 6, 154, 17, 9, 204, 169, 47, 142, 113, 161, 125, 89, 56, 230, 112, 129, 212, 189, 164, 89, 145, 95, 107, 253, 37, 251, 80, 174, 134, 57, 230, 71, 96, 211, 74, 226, 191, 103, 45, 9, 49, 20, 141, 205, 246, 134, 236, 194, 62, 102, 23, 155, 67, 50, 74, 18, 164, 52, 20, 149, 193, 121, 75, 253, 217, 224, 74, 157, 179, 202, 11, 73, 63, 224, 175, 9, 228, 182, 51, 193, 218, 108, 140, 217, 141, 107, 243, 41, 170, 215, 184, 109, 9, 250, 187, 123, 28, 254, 94, 4, 116, 24, 193, 28, 106, 54, 71, 147, 229, 246, 248, 241, 143, 163, 219, 146, 137, 12, 6, 240, 226, 7, 207, 24, 234, 243, 108, 0, 120, 104, 189, 104, 203, 153, 201, 109, 142, 89, 214, 89, 59, 244, 210, 187, 164, 135, 156, 122, 42, 206, 6, 108, 97, 197, 73, 145, 78, 24, 204, 168, 7, 224, 221, 209, 48, 254, 29, 67, 34, 1, 144, 220, 143, 43, 109, 52, 56, 200, 150, 169, 17, 236, 11, 252, 209, 36, 155, 147, 36, 4, 48, 13, 172, 29, 178, 65, 206, 245, 57, 7, 228, 239, 210, 132, 79, 68, 219, 165, 174, 169, 132, 212, 11, 236, 243, 130, 195, 118, 57, 109, 137, 186, 208, 62, 202, 14, 138, 60, 6, 86, 116, 129, 172, 242, 27, 124, 14, 184, 68, 81, 195, 59, 124, 98, 214, 22, 50, 47, 218, 125, 181, 77, 125, 0, 170, 53, 202, 202, 10, 227, 195, 167, 32, 230, 102, 193, 187, 80, 246, 42, 143, 48, 167, 17, 129, 246, 5, 195, 134, 79, 251, 148, 55, 84, 215, 101, 73, 52, 134, 60, 65, 211, 14, 226, 131, 243, 136, 83, 3, 246, 249, 130, 137, 71, 141, 250, 152, 45, 20, 92, 98, 248, 223, 50, 149, 86, 182, 191, 107, 178, 14, 96, 1, 120, 83, 109, 65, 165, 174, 171, 166, 91, 6, 51, 72, 123, 128, 169, 104, 194, 53, 154, 59, 122, 16, 111, 220, 253, 113, 207, 131, 236, 170, 244, 16, 176, 188, 9, 234, 126, 78, 210, 169, 162, 105, 165, 138, 179, 56, 233, 48, 51, 170, 188, 215, 233, 186, 145, 18, 54, 209, 203, 75, 92, 158, 198, 58, 10, 120, 51, 52, 244, 55, 117, 188, 177, 145, 41, 173, 98, 110, 28, 74, 244, 141, 60, 198, 80, 174, 246, 95, 120, 176, 247, 149, 91, 22, 218, 94, 113, 108, 250, 0, 200, 17, 127, 144, 206, 107, 9, 139, 229, 206, 233, 44, 253, 100, 155, 142, 72, 20, 118, 157, 254, 144, 236, 79, 219, 33, 174, 104, 14, 217, 193, 249, 59, 28, 238, 207, 106, 38, 25, 6, 75, 32, 88, 124, 158, 30, 98, 60, 34, 172, 35, 163, 216, 134, 111, 198, 166, 120, 54, 109, 118, 250, 86, 31, 6, 27, 5, 81, 182, 187, 226, 162, 43, 80, 61, 35, 235, 127, 28, 113, 172, 157, 225, 69, 2, 9, 94, 11, 117, 103, 40, 189, 236, 131, 75, 136, 12, 60, 112, 66, 159, 208, 178, 200, 175, 37, 217, 225, 97, 130, 113, 221, 120, 69, 149, 8, 155, 10, 107, 183, 247, 75, 108, 165, 115, 121, 133, 46, 35, 11, 18, 59, 5, 5, 31, 102, 199, 163, 159, 112, 58, 19, 229, 72, 185, 188, 120, 198, 252, 213, 139, 7, 68, 33, 139, 255, 154, 14, 229, 156, 23, 2, 129, 169, 220, 13, 222, 23, 70, 251, 249, 106, 81, 145, 11, 38, 185, 108, 97, 133, 15, 27, 180, 3, 179, 138, 129, 132, 234, 199, 28, 42, 194, 246, 31, 201, 124, 189, 160, 217, 209, 19, 53, 204, 199, 181, 194, 149, 166, 142, 223, 241, 81, 154, 26, 68, 151, 41, 188, 90, 32, 208, 162, 247, 39, 43, 139, 64, 145, 170, 245, 56, 210, 102, 166, 86, 205, 54, 225, 128, 151, 208, 125, 131, 204, 124, 175, 67, 44, 11, 198, 180, 230, 51, 96, 122, 30, 212, 104, 225, 105, 213, 104, 166, 37, 42, 72, 184, 90, 67, 184, 20, 62, 83, 127, 162, 165, 72, 147, 219, 30, 23, 12, 215, 100, 101, 172, 87, 108, 46, 154, 157, 227, 1, 164, 46, 254, 50, 176, 155, 39, 230, 243, 36, 169, 17, 141, 100, 223, 68, 196, 200, 144, 39, 37, 154, 252, 70, 211, 122, 69, 94, 74, 12, 36, 224, 193, 39, 66, 245, 15, 17, 157, 254, 38, 162, 51, 11, 213, 83, 116, 216, 206, 23, 14, 4, 204, 128, 247, 192, 199, 4, 33, 169, 186, 117, 231, 31, 18, 195, 225, 185, 58, 22, 196, 57, 23, 197, 112, 17, 115, 45, 120, 250, 109, 147, 116, 133, 129, 68, 170, 73, 60, 59, 58, 182, 18, 194, 225, 75, 250, 59, 172, 151, 228, 238, 198, 181, 73, 27, 154, 238, 125, 222, 1, 9, 211, 17, 28, 144, 53, 8, 102, 12, 121, 73, 180, 146, 61, 226, 64, 77, 22, 154, 93, 141, 56, 68, 61, 65, 61, 26, 41, 32, 18, 19, 12, 237, 175, 159, 130, 7, 80, 17, 176, 104, 146, 7, 178, 13, 131, 14, 17, 110, 41, 100, 171, 214, 92, 131, 187, 184, 45, 39, 17, 46, 24, 143, 4, 142, 146, 225, 225, 143, 131, 95, 65, 127, 64, 58, 87, 205, 141, 120, 246, 189, 17, 0, 252, 254, 231, 37, 76, 71, 7, 171, 92, 227, 247, 196, 133, 212, 211, 20, 103, 109, 4, 77, 56, 62, 60, 227, 229, 55, 65, 143, 126, 163, 198, 85, 187, 241, 95, 116, 132, 34, 20, 234, 153, 211, 106, 192, 144, 104, 112, 18, 131, 232, 80, 180, 202, 178, 238, 64, 228, 35, 250, 236, 188, 178, 67, 140, 158, 187, 164, 18, 188, 27, 249, 67, 94, 91, 71, 3, 196, 103, 222, 127, 153, 23, 112, 252, 55, 113, 197, 22, 31, 84, 186, 253, 218, 242, 39, 232, 116, 246, 142, 219, 140, 107, 85, 10, 226, 137, 151, 115, 242, 6, 166, 179, 68, 47, 80, 100, 148, 160, 101, 219, 89, 28, 125, 180, 28, 232, 26, 227, 234, 141, 225, 18, 223, 106, 109, 0, 7, 225, 103, 41, 193, 96, 43, 242, 206, 192, 50, 110, 39, 187, 177, 241, 184, 80, 40, 163, 147, 130, 55, 99, 218, 9, 192, 169, 157, 123, 22, 129, 220, 254, 30, 118, 32, 49, 76, 232, 255, 194, 113, 153, 79, 137, 156, 100, 13, 162, 96, 155, 105, 176, 159, 81, 21, 120, 210, 200, 53, 145, 169, 55, 212, 244, 24, 26, 113, 70, 57, 9, 203, 202, 143, 148, 190, 118, 22, 76, 210, 58, 92, 23, 33, 110, 6, 250, 95, 3, 103, 223, 131, 219, 102, 135, 43, 20, 124, 179, 65, 100, 236, 109, 117, 39, 199, 101, 238, 202, 5, 103, 167, 137, 30, 164, 154, 214, 200, 89, 146, 147, 158, 76, 170, 105, 179, 73, 208, 65, 188, 47, 85, 213, 232, 30, 207, 144, 53, 18, 106, 3, 54, 209, 3, 209, 104, 173, 123, 183, 62, 46, 117, 90, 172, 157, 181, 117, 252, 125, 72, 11, 42, 234, 98, 214, 80, 46, 143, 44, 122, 31, 193, 78, 109, 225, 129, 148, 217, 190, 20, 206, 141, 129, 149, 225, 204, 69, 171, 33, 195, 123, 252, 54, 114, 155, 71, 159, 44, 209, 10, 220, 246, 70, 192, 122, 105, 15, 113, 212, 224, 80, 144, 33, 215, 60, 214, 119, 84, 6, 168, 24, 201, 202, 51, 52, 8, 199, 123, 153, 253, 213, 152, 186, 167, 206, 92, 32, 169, 53, 219, 212, 166, 121, 198, 131, 95, 146, 229, 11, 146, 43, 112, 224, 134, 189, 110, 216, 90, 32, 228, 85, 83, 91, 184, 229, 244, 177, 178, 247, 168, 28, 196, 178, 243, 100, 34, 128, 106, 232, 218, 219, 215, 216, 87, 186, 192, 46, 111, 102, 111, 27, 6, 229, 233, 246, 144, 64, 119, 189, 95, 211, 215, 109, 202, 86, 223, 33, 160, 104, 17, 224, 223, 126, 96, 0, 213, 44, 186, 217, 236, 100, 93, 104, 176, 212, 210, 56, 32, 101, 205, 28, 11, 6, 95, 251, 209, 232, 12, 107, 182, 231, 64, 129, 68, 65, 110, 67, 137, 5, 100, 167, 145, 36, 133, 187, 35, 43, 77, 150, 129, 198, 82, 234, 220, 170, 168, 205, 15, 132, 186, 88, 5, 78, 80, 20, 44, 103, 202, 194, 36, 149, 217, 148, 212, 103, 64, 114, 46, 214, 32, 18, 36, 93, 129, 48, 177, 201, 168, 0, 137, 192, 55, 156, 172, 226, 87, 45, 63, 240, 88, 65, 12, 121, 22, 124, 6, 8, 87, 182, 161, 180, 87, 215, 226, 144, 190, 229, 12, 123, 62, 249, 14, 177, 133, 196, 193, 37, 50, 161, 83, 156, 233, 128, 162, 167, 26, 114, 112, 95, 252, 115, 64, 243, 140, 176, 41, 220, 155, 105, 105, 47, 7, 118, 96, 244, 98, 67, 34, 145, 166, 175, 166, 223, 195, 3, 194, 35, 243, 59, 68, 24, 245, 166, 161, 234, 14, 121, 172, 1, 142, 157, 105, 246, 173, 212, 14, 11, 184, 212, 187, 133, 140, 122, 190, 165, 233, 148, 156, 126, 175, 58, 64, 91, 67, 124, 199, 38, 132, 191, 150, 93, 53, 135, 147, 224, 134, 4, 20, 135, 62, 17, 247, 251, 18, 251, 190, 75, 177, 68, 105, 131, 38, 176, 194, 169, 197, 20, 103, 75, 112, 0, 35, 34, 166, 130, 115, 151, 220, 190, 6, 169, 185, 164, 56, 14, 121, 13, 78, 166, 131, 99, 94, 57, 217, 200, 235, 86, 46, 76, 77, 20, 40, 73, 52, 72, 50, 245, 196, 59, 193, 6, 25, 120, 42, 75, 17, 229, 190, 218, 25, 28, 20, 137, 21, 126, 76, 244, 82, 205, 135, 86, 127, 119, 248, 147, 16, 168, 178, 127, 3, 192, 138, 57, 139, 1, 249, 9, 63, 10, 119, 83, 83, 3, 230, 194, 13, 249, 235, 246, 34, 92, 165, 92, 55, 251, 48, 177, 251, 236, 227, 34, 124, 60, 222, 117, 76, 163, 190, 192, 54, 200, 62, 51, 136, 114, 206, 70, 16, 187, 107, 140, 105, 142, 9, 251, 53, 236, 30, 143, 236, 169, 96, 196, 90, 0, 228, 52, 33, 87, 67, 112, 54, 27, 42, 204, 164, 113, 163, 186, 217, 107, 197, 47, 76, 21, 49, 98, 176, 227, 144, 236, 110, 43, 57, 243, 104, 97, 59, 58, 48, 172, 84, 193, 103, 140, 45, 254, 226, 179, 79, 26, 27, 16, 208, 62, 222, 105, 22, 56, 4, 219, 67, 42, 186, 142, 113, 162, 162, 224, 178, 115, 245, 128, 221, 194, 5, 13, 52, 108, 95, 54, 12, 226, 37, 130, 184, 38, 14, 136, 32, 28, 43, 132, 25, 126, 69, 79, 113, 70, 14, 153, 45, 3, 82, 233, 253, 19, 198, 10, 233, 213, 133, 52, 152, 217, 146, 2, 147, 13, 102, 45, 80, 119, 254, 163, 164, 113, 161, 125, 79, 199, 185, 88, 144, 72, 233, 21, 6, 125, 231, 23, 8, 151, 49, 152, 154, 119, 37, 130, 196, 194, 200, 151, 24, 223, 184, 25, 215, 215, 193, 224, 8, 73, 73, 244, 201, 117, 209, 217, 196, 21, 239, 114, 161, 138, 250, 248, 240, 190, 225, 219, 50, 191, 58, 232, 218, 126, 243, 50, 28, 232, 161, 156, 192, 60, 97, 161, 23, 4, 77, 54, 148, 232, 42, 140, 80, 224, 103, 9, 166, 101, 20, 14, 148, 138, 81, 64, 129, 70, 78, 235, 132, 67, 48, 227, 194, 205, 246, 154, 169, 100, 127, 12, 231, 111, 88, 23, 191, 63, 251, 4, 111, 236, 137, 55, 122, 240, 209, 166, 184, 132, 186, 10, 138, 42, 194, 197, 48, 243, 206, 219, 79, 53, 145, 24, 100, 119, 116, 128, 99, 12, 17, 173, 79, 74, 222, 170, 193, 228, 22, 108, 137, 218, 186, 133, 160, 193, 190, 131, 202, 160, 182, 236, 87, 222, 197, 25, 89, 59, 3, 59, 224, 228, 72, 83, 187, 12, 38, 156, 224, 171, 23, 35, 234, 206, 56, 34, 132, 9, 25, 224, 28, 15, 27, 31, 122, 206, 89, 66, 46, 184, 212, 69, 112, 93, 153, 12, 68, 44, 132, 116, 115, 128, 127, 45, 128, 248, 30, 123, 214, 86, 25, 214, 46, 40, 31, 180, 238, 34, 71, 78, 85, 178, 19, 28, 237, 133, 16, 183, 58, 220, 88, 117, 171, 40, 50, 57, 229, 39, 28, 162, 89, 224, 32, 48, 42, 9, 70, 69, 200, 114, 164, 18, 80, 139, 255, 8, 245, 214, 198, 186, 112, 109, 51, 40, 154, 173, 3, 248, 41, 252, 186, 21, 152, 99, 162, 138, 72, 74, 98, 234, 124, 57, 135, 125, 75, 184, 107, 91, 35, 89, 149, 170, 101, 45, 255, 52, 194, 231, 8, 71, 53, 151, 232, 147, 106, 193, 32, 171, 218, 107, 175, 130, 147, 58, 199, 14, 166, 56, 84, 245, 182, 131, 210, 77, 140, 145, 67, 39, 36, 81, 74, 50, 59, 175, 65, 108, 69, 176, 119, 111, 5, 220, 228, 215, 249, 151, 25, 193, 88, 104, 245, 3, 166, 241, 56, 24, 85, 116, 252, 110, 31, 45, 31, 53, 135, 25, 184, 240, 2, 165, 102, 56, 14, 203, 151, 32, 183, 137, 174, 112, 232, 137, 49, 192, 76, 194, 48, 51, 3, 18, 202, 128, 171, 244, 35, 28, 200, 1, 21, 28, 132, 83, 66, 47, 117, 147, 113, 37, 106, 80, 118, 217, 100, 229, 126, 182, 183, 183, 81, 65, 33, 181, 235, 223, 130, 118, 54, 177, 90, 10, 187, 200, 6, 102, 189, 93, 76, 254, 55, 69, 103, 130, 201, 57, 98, 207, 5, 152, 231, 64, 101, 0, 222, 78, 244, 13, 38, 248, 63, 163, 31, 175, 155, 203, 77, 249, 24, 139, 52, 224, 68, 199, 157, 103, 40, 131, 224, 242, 137, 225, 181, 131, 29, 128, 229, 242, 92, 42, 58, 85, 101, 69, 120, 167, 86, 99, 57, 220, 94, 231, 162, 99, 132, 221, 55, 37, 174, 194, 195, 194, 178, 232, 61, 38, 66, 42, 168, 182, 23, 106, 241, 198, 192, 217, 215, 50, 59, 225, 217, 8, 24, 147, 240, 54, 209, 158, 154, 47, 7, 212, 225, 38, 19, 4, 241, 170, 42, 2, 107, 13, 240, 77, 158, 228, 103, 237, 64, 173, 177, 143, 48, 208, 217, 140, 31, 108, 229, 140, 70, 10, 51, 122, 64, 75, 12, 145, 211, 165, 169, 145, 240, 228, 255, 115, 84, 244, 84, 125, 231, 86, 8, 52, 248, 112, 145, 221, 45, 53, 9, 92, 53, 66, 180, 172, 242, 22, 113, 106, 211, 138, 42, 63, 66, 178, 2, 160, 86, 191, 169, 237, 86, 87, 70, 78, 62, 1, 213, 97, 167, 104, 63, 60, 222, 61, 77, 55, 70, 198, 23, 8, 135, 250, 188, 93, 66, 134, 227, 192, 20, 182, 81, 129, 108, 60, 88, 141, 93, 205, 132, 52, 179, 95, 168, 181, 39, 38, 7, 206, 107, 214, 181, 101, 190, 56, 89, 60, 178, 166, 107, 99, 154, 3, 129, 39, 242, 99, 206, 124, 171, 178, 132, 138, 177, 200, 26, 11, 83, 208, 126, 142, 130, 38, 169, 82, 225, 242, 213, 101, 234, 176, 14, 229, 79, 19, 58, 72, 4, 201, 238, 77, 94, 151, 153, 10, 128, 254, 183, 134, 62, 42, 170, 39, 226, 231, 213, 223, 31, 74, 61, 94, 199, 136, 181, 153, 45, 48, 157, 50, 0, 60, 251, 155, 235, 37, 42, 168, 203, 8, 246, 116, 56, 66, 99, 34, 92, 170, 73, 29, 32, 185, 65, 23, 178, 124, 2, 254, 140, 196, 162, 33, 208, 190, 145, 237, 96, 137, 104, 94, 114, 2, 37, 83, 2, 59, 241, 115, 1, 208, 157, 193, 165, 118, 17, 156, 91, 135, 158, 192, 108, 229, 212, 236, 9, 28, 232, 75, 152, 109, 156, 152, 207, 180, 242, 34, 100, 12, 112, 243, 72, 166, 219, 31, 150, 89, 73, 243, 4, 219, 157, 46, 75, 247, 234, 143, 10, 88, 150, 67, 124, 219, 10, 6, 26, 15, 78, 177, 77, 114, 221, 121, 214, 54, 107, 254, 196, 239, 216, 168, 230, 230, 28, 94, 69, 11, 67, 161, 37, 39, 191, 18, 246, 116, 211, 14, 236, 250, 100, 23, 87, 174, 118, 164, 193, 229, 87, 38, 151, 122, 79, 217, 111, 231, 135, 82, 222, 183, 14, 208, 217, 233, 1, 121, 21, 121, 251, 162, 112, 186, 24, 65, 154, 7, 50, 186, 246, 24, 173, 198, 186, 89, 38, 117, 87, 26, 10, 94, 50, 193, 16, 135, 68, 167, 148, 251, 144, 115, 60, 48, 28, 187, 104, 161, 255, 192, 223, 214, 21, 143, 212, 214, 194, 44, 128, 162, 174, 96, 166, 91, 17, 247, 83, 230, 57, 106, 16, 104, 142, 57, 78, 235, 132, 164, 183, 174, 244, 56, 103, 113, 118, 96, 65, 40, 163, 7, 30, 134, 159, 76, 247, 31, 213, 30, 211, 203, 74, 188, 201, 38, 148, 123, 234, 157, 17, 192, 247, 26, 71, 201, 37, 226, 94, 89, 80, 180, 133, 94, 22, 94, 64, 39, 236, 227, 143, 163, 140, 126, 160, 125, 29, 99, 25, 56, 107, 89, 224, 93, 213, 48, 6, 174, 250, 127, 228, 165, 247, 193, 169, 53, 114, 176, 103, 191, 116, 118, 23, 97, 219, 73, 32, 245, 48, 81, 200, 42, 64, 190, 25, 126, 59, 198, 254, 136, 198, 30, 154, 159, 115, 133, 150, 85, 84, 81, 198, 220, 150, 67, 242, 28, 203, 189, 175, 18, 152, 74, 40, 37, 157, 101, 110, 50, 144, 94, 53, 103, 188, 60, 29, 26, 46, 38, 83, 193, 5, 41, 184, 184, 43, 65, 187, 143, 212, 123, 83, 233, 166, 99, 246, 45, 86, 5, 154, 252, 76, 189, 21, 225, 45, 46, 102, 104, 8, 225, 163, 7, 24, 22, 178, 179, 22, 211, 176, 8, 21, 171, 190, 105, 171, 201, 153, 254, 143, 184, 143, 79, 44, 99, 68, 83, 14, 221, 163, 40, 137, 30, 178, 150, 183, 168, 122, 148, 165, 126, 56, 195, 175, 162, 202, 69, 219, 175, 193, 166, 186, 252, 212, 135, 62, 240, 153, 66, 35, 240, 111, 129, 19, 45, 140, 227, 201, 185, 173, 12, 198, 97, 228, 10, 41, 101, 107, 165, 106, 140, 26, 165, 166, 212, 16, 53, 231, 73, 16, 53, 1, 76, 73, 68, 160, 75, 156, 36, 124, 36, 235, 158, 89, 98, 139, 130, 113, 19, 7, 232, 95, 112, 121, 125, 246, 130, 143, 5, 190, 216, 150, 246, 149, 254, 80, 102, 35, 174, 82, 169, 90, 189, 234, 242, 87, 91, 255, 78, 98, 197, 208, 42, 141, 31, 17, 233, 217, 151, 135, 31, 87, 71, 132, 255, 79, 98, 62, 168, 221, 172, 247, 225, 46, 155, 228, 196, 76, 95, 161, 68, 233, 203, 192, 236, 146, 149, 43, 85, 98, 110, 35, 242, 116, 216, 226, 78, 157, 114, 40, 157, 241, 33, 12, 14, 205, 192, 201, 12, 219, 255, 249, 6, 105, 86, 80, 101, 184, 26, 56, 101, 227, 25, 69, 140, 68, 35, 245, 46, 110, 221, 50, 51, 237, 20, 23, 174, 4, 137, 44, 207, 0, 201, 52, 147, 202, 111, 190, 72, 177, 228, 97, 210, 163, 150, 154, 2, 241, 97, 151, 93, 210, 199, 127, 218, 57, 45, 91, 96, 194, 160, 131, 205, 120, 102, 209, 51, 28, 68, 48, 199, 130, 77, 209, 13, 204, 141, 240, 217, 165, 87, 56, 119, 167, 53, 207, 74, 106, 163, 27, 100, 6, 252, 238, 114, 63, 40, 162, 86, 5, 153, 91, 98, 106, 124, 126, 91, 105, 173, 98, 74, 222, 85, 125, 64, 213, 102, 107, 3, 158, 243, 8, 167, 224, 50, 213, 167, 203, 19, 16, 235, 4, 43, 104, 115, 28, 1, 168, 41, 94, 241, 67, 161, 140, 45, 89, 118, 172, 162, 117, 224, 95, 245, 214, 214, 220, 117, 32, 163, 250, 84, 55, 174, 20, 7, 238, 213, 19, 177, 36, 105, 6, 75, 15, 72, 154, 18, 64, 85, 108, 213, 56, 86, 240, 124, 142, 134, 82, 100, 199, 198, 243, 12, 146, 80, 148, 251, 223, 79, 153, 181, 188, 100, 167, 228, 175, 227, 241, 164, 28, 120, 133, 170, 177, 202, 232, 101, 171, 207, 2, 32, 212, 173, 147, 117, 171, 218, 83, 216, 241, 94, 3, 103, 187, 230, 26, 168, 180, 159, 4, 173, 108, 61, 76, 182, 54, 115, 135, 144, 2, 39, 81, 126, 253, 172, 101, 144, 19, 3, 202, 12, 234, 87, 11, 222, 188, 82, 204, 27, 81, 216, 250, 163, 94, 103, 73, 149, 233, 251, 140, 172, 85, 138, 51, 203, 121, 30, 64, 184, 120, 46, 233, 199, 59, 106, 115, 123, 102, 145, 129, 213, 216, 74, 92, 11, 166, 50, 79, 3, 13, 80, 245, 241, 248, 193, 208, 78, 77, 229, 21, 67, 229, 192, 61, 115, 213, 93, 52, 225, 217, 147, 236, 135, 88, 184, 240, 49, 182, 109, 95, 67, 184, 204, 181, 176, 213, 34, 36, 149, 98, 143, 193, 225, 48, 180, 219, 69, 244, 64, 96, 234, 83, 5, 113, 122, 23, 115, 55, 87, 212, 130, 154, 96, 152, 92, 29, 136, 202, 217, 58, 236, 195, 153, 17, 14, 13, 213, 145, 36, 159, 163, 58, 191, 3, 11, 124, 34, 155, 237, 58, 57, 3, 144, 222, 25, 234, 107, 15, 183, 234, 160, 27, 44, 77, 149, 128, 57, 87, 142, 236, 113, 192, 44, 95, 140, 168, 65, 170, 167, 167, 38, 131, 33, 140, 27, 10, 248, 16, 71, 99, 115, 210, 177, 65, 198, 48, 11, 13, 1, 247, 24, 222, 248, 24, 73, 117, 11, 249, 29, 226, 114, 41, 64, 147, 58, 10, 52, 76, 19, 128, 25, 96, 78, 56, 108, 40, 200, 163, 225, 39, 179, 7, 84, 210, 59, 167, 133, 135, 35, 177, 111, 9, 96, 25, 142, 26, 71, 203, 43, 18, 176, 75, 224, 206, 24, 157, 10, 0, 133, 216, 218, 121, 210, 52, 164, 86, 148, 80, 18, 156, 150, 18, 197, 44, 74, 110, 158, 202, 194, 40, 132, 234, 166, 98, 208, 166, 193, 25, 225, 231, 118, 0, 136, 194, 251, 212, 237, 49, 94, 92, 250, 222, 143, 52, 229, 213, 147, 96, 189, 214, 182, 11, 149, 41, 202, 15, 64, 33, 216, 207, 177, 186, 8, 104, 161, 240, 145, 252, 200, 39, 45, 17, 161, 97, 92, 19, 73, 212, 7, 244, 26, 121, 77, 143, 178, 161, 48, 158, 113, 14, 2, 65, 152, 91, 51, 229, 240, 117, 52, 69, 236, 248, 75, 195, 136, 161, 56, 0, 23, 112, 252, 156, 183, 148, 116, 31, 215, 235, 78, 32, 227, 131, 219, 238, 212, 252, 43, 42, 128, 55, 12, 182, 80, 113, 168, 229, 158, 225, 54, 105, 162, 106, 41, 139, 28, 74, 213, 152, 209, 193, 3, 150, 77, 235, 193, 20, 105, 99, 66, 169, 242, 111, 229, 169, 14, 6, 26, 138, 220, 112, 172, 16, 75, 232, 233, 186, 153, 185, 253, 168, 242, 85, 202, 128, 167, 112, 84, 210, 132, 250, 61, 132, 1, 125, 51, 232, 128, 164, 2, 53, 141, 55, 237, 54, 114, 104, 0, 129, 0, 193, 109, 124, 129, 83, 48, 234, 47, 210, 47, 50, 95, 62, 156, 180, 113, 73, 81, 202, 149, 63, 161, 102, 201, 28, 165, 21, 108, 118, 205, 67, 107, 1, 242, 249, 196, 248, 179, 171, 198, 129, 145, 136, 228, 121, 198, 99, 87, 83, 133, 192, 17, 176, 12, 81, 54, 19, 66, 108, 58, 203, 110, 50, 112, 189, 12, 124, 32, 108, 70, 3, 184, 2, 11, 57, 206, 188, 255, 33, 137, 78, 63, 170, 63, 236, 212, 38, 102, 226, 116, 148, 26, 22, 30, 220, 80, 0, 244, 42, 166, 19, 192, 1, 151, 213, 35, 69, 244, 40, 88, 47, 251, 77, 95, 95, 77, 88, 40, 37, 119, 122, 50, 235, 233, 158, 215, 48, 199, 214, 84, 232, 211, 139, 193, 220, 144, 193, 94, 223, 110, 90, 146, 145, 98, 176, 143, 66, 215, 70, 157, 227, 114, 24, 187, 64, 249, 185, 134, 81, 219, 11, 187, 148, 239, 34, 232, 15, 29, 132, 225, 157, 46, 100, 93, 168, 237, 161, 86, 129, 132, 59, 67, 32, 58, 231, 132, 191, 42, 194, 2, 45, 50, 22, 180, 120, 142, 49, 191, 153, 215, 200, 224, 118, 235, 211, 196, 116, 27, 45, 116, 240, 68, 85, 168, 162, 100, 58, 10, 82, 102, 116, 177, 52, 16, 153, 61, 221, 111, 245, 128, 163, 191, 101, 108, 13, 58, 102, 171, 160, 161, 170, 129, 12, 197, 172, 228, 29, 225, 174, 214, 114, 97, 65, 160, 66, 90, 250, 229, 36, 204, 142, 163, 106, 182, 120, 67, 199, 184, 80, 147, 5, 167, 59, 117, 16, 135, 89, 69, 171, 210, 17, 226, 147, 117, 58, 33, 60, 80, 78, 103, 65, 79, 221, 48, 40, 148, 165, 149, 169, 106, 174, 50, 174, 186, 228, 122, 103, 65, 100, 3, 252, 57, 16, 176, 193, 199, 52, 103, 50, 146, 89, 87, 218, 84, 108, 175, 13, 195, 161, 111, 69, 128, 6, 191, 152, 120, 143, 177, 211, 94, 130, 41, 47, 136, 158, 165, 217, 25, 65, 234, 139, 212, 146, 51, 216, 59, 224, 4, 97, 77, 223, 128, 248, 95, 81, 39, 225, 183, 242, 69, 191, 48, 37, 177, 24, 119, 11, 227, 80, 254, 109, 164, 229, 55, 101, 88, 89, 168, 113, 244, 176, 100, 126, 79, 164, 212, 180, 241, 82, 196, 39, 81, 185, 106, 20, 3, 132, 98, 158, 244, 6, 125, 8, 77, 234, 34, 142, 170, 146, 224, 189, 127, 215, 124, 34, 24, 17, 243, 7, 74, 146, 166, 128, 179, 6, 98, 114, 190, 79, 67, 219, 171, 197, 129, 213, 239, 132, 144, 44, 234, 52, 230, 16, 94, 220, 92, 136, 0, 186, 244, 229, 53, 148, 233, 73, 249, 152, 217, 100, 6, 196, 77, 97, 138, 6, 5, 41, 196, 19, 142, 92, 46, 110, 54, 151, 156, 156, 177, 129, 2, 62, 200, 189, 193, 181, 93, 68, 60, 90, 149, 84, 203, 83, 201, 26, 18, 52, 156, 65, 179, 59, 3, 125, 19, 204, 236, 61, 163, 85, 36, 237, 47, 144, 140, 87, 154, 95, 60, 0, 22, 37, 155, 6, 7, 0, 176, 46, 247, 159, 37, 173, 224, 210, 12, 217, 248, 79, 132, 93, 138, 135, 78, 17, 227, 18, 45, 252, 48, 58, 254, 85, 36, 176, 69, 114, 1, 92, 207, 85, 193, 254, 112, 204, 78, 157, 146, 103, 123, 196, 100, 10, 46, 184, 161, 162, 78, 144, 141, 50, 86, 252, 50, 235, 184, 151, 55, 73, 187, 10, 37, 50, 198, 0, 207, 155, 104, 37, 137, 196, 54, 132, 15, 22, 93, 114, 237, 90, 250, 96, 61, 18, 152, 194, 63, 239, 16, 92, 110, 63, 209, 229, 27, 61, 154, 245, 208, 226, 145, 184, 219, 48, 187, 2, 193, 78, 24, 51, 81, 34, 172, 109, 33, 8, 23, 232, 115, 57, 152, 233, 140, 200, 196, 108, 8, 65, 251, 72, 68, 76, 181, 220, 62, 34, 119, 9, 126, 13, 186, 100, 214, 191, 149, 154, 131, 254, 200, 126, 152, 142, 103, 127, 57, 81, 255, 99, 129, 250, 33, 25, 183, 235, 134, 117, 115, 108, 95, 28, 86, 141, 217, 180, 116, 61, 201, 51, 90, 59, 160, 254, 213, 93, 21, 55, 204, 97, 62, 113, 100, 6, 107, 57, 64, 201, 157, 228, 173, 229, 35, 61, 235, 102, 161, 14, 189, 164, 112, 76, 75, 127, 18, 231, 145, 130, 79, 66, 150, 63, 30, 164, 181, 68, 176, 81, 217, 135, 54, 172, 66, 60, 232, 8, 206, 224, 245, 102, 177, 243, 64, 87, 233, 193, 244, 95, 68, 78, 255, 14, 144, 223, 147, 32, 36, 230, 212, 218, 185, 120, 243, 3, 64, 139, 233, 70, 90, 136, 5, 171, 221, 87, 98, 15, 43, 200, 131, 182, 75, 81, 139, 243, 233, 218, 246, 61, 246, 189, 204, 39, 168, 208, 131, 142, 200, 109, 158, 159, 54, 35, 91, 244, 135, 204, 241, 171, 25, 55, 255, 101, 15, 169, 221, 208, 223, 109, 244, 74, 78, 126, 25, 199, 151, 191, 110, 7, 74, 60, 169, 101, 212, 84, 228, 234, 187, 204, 110, 158, 131, 28, 21, 186, 158, 193, 129, 10, 199, 206, 160, 140, 85, 209, 196, 9, 167, 6, 157, 187, 234, 179, 190, 131, 250, 100, 34, 13, 98, 40, 94, 92, 80, 253, 7, 188, 233, 45, 45, 253, 101, 65, 250, 138, 64, 231, 109, 103, 163, 213, 33, 183, 99, 129, 181, 119, 96, 30, 234, 220, 121, 7, 109, 255, 191, 243, 188, 128, 33, 88, 16, 143, 190, 7, 175, 181, 7, 136, 203, 156, 61, 136, 2, 69, 232, 15, 168, 44, 87, 109, 95, 162, 79, 8, 203, 107, 170, 255, 6, 142, 43, 24, 69, 126, 184, 121, 213, 43, 132, 164, 198, 90, 213, 28, 144, 14, 208, 129, 24, 87, 170, 144, 6, 210, 3, 76, 220, 223, 12, 223, 191, 33, 42, 214, 218, 4, 236, 194, 78, 55, 215, 50, 24, 189, 64, 174, 166, 176, 110, 190, 23, 1, 240, 177, 109, 233, 48, 246, 116, 99, 168, 128, 246, 212, 113, 10, 168, 40, 72, 17, 184, 129, 191, 38, 158, 23, 192, 155, 21, 229, 218, 137, 168, 21, 5, 65, 28, 231, 85, 4, 162, 146, 77, 99, 228, 4, 175, 0, 27, 51, 79, 18, 111, 97, 103, 172, 198, 6, 91, 139, 38, 66, 197, 242, 219, 5, 14, 8, 127, 229, 156, 139, 219, 109, 240, 174, 20, 236, 133, 43, 9, 16, 154, 83, 150, 171, 193, 155, 217, 243, 153, 236, 53, 153, 145, 224, 243, 157, 117, 96, 254, 115, 134, 149, 101, 59, 101, 15, 198, 164, 71, 7, 151, 178, 173, 211, 235, 229, 9, 9, 216, 99, 178, 234, 127, 134, 80, 226, 219, 50, 22, 218, 153, 150, 38, 209, 194, 32, 66, 149, 30, 149, 124, 77, 60, 85, 118, 172, 161, 20, 91, 231, 157, 210, 37, 69, 134, 33, 41, 44, 94, 151, 67, 239, 251, 3, 113, 169, 83, 73, 192, 111, 144, 160, 228, 150, 146, 175, 125, 222, 155, 123, 120, 75, 181, 150, 133, 17, 143, 51, 247, 178, 66, 113, 249, 92, 118, 95, 180, 201, 132, 159, 86, 91, 191, 124, 12, 73, 198, 125, 183, 26, 228, 56, 235, 78, 33, 22, 9, 155, 187, 49, 250, 236, 170, 144, 254, 195, 101, 129, 204, 91, 228, 232, 83, 34, 133, 250, 46, 165, 21, 160, 235, 156, 158, 172, 126, 180, 15, 106, 81, 212, 178, 143, 108, 132, 145, 237, 39, 123, 72, 143, 134, 212, 135, 27, 181, 113, 35, 239, 62, 130, 93, 164, 198, 100, 252, 46, 187, 226, 50, 86, 186, 179, 196, 12, 114, 191, 240, 54, 83, 30, 201, 49, 217, 127, 202, 74, 23, 109, 216, 68, 15, 233, 13, 104, 156, 230, 200, 214, 101, 233, 175, 45, 5, 172, 142, 11, 77, 59, 228, 0, 66, 239, 70, 115, 148, 7, 67, 156, 0, 219, 132, 98, 27, 9, 144, 144, 30, 53, 134, 115, 149, 61, 197, 154, 111, 239, 224, 80, 8, 112, 195, 96, 237, 218, 127, 219, 175, 24, 53, 17, 216, 165, 180, 141, 129, 29, 101, 54, 56, 100, 229, 159, 192, 145, 52, 2, 153, 108, 127, 155, 32, 254, 200, 17, 46, 107, 205, 54, 29, 73, 214, 178, 101, 9, 180, 115, 31, 88, 253, 161, 26, 155, 120, 10, 213, 21, 61, 170, 228, 187, 227, 229, 20, 13, 78, 254, 11, 124, 55, 50, 212, 53, 166, 57, 186, 207, 11, 213, 242, 19, 215, 140, 210, 48, 136, 185, 131, 193, 183, 132, 145, 191, 195, 179, 86, 138, 83, 78, 219, 176, 27, 23, 80, 193, 113, 109, 148, 119, 97, 141, 92, 177, 70, 159, 13, 162, 139, 226, 84, 187, 120, 201, 21, 43, 242, 165, 195, 138, 70, 31, 24, 41, 58, 171, 25, 33, 30, 148, 108, 195, 238, 74, 70, 117, 66, 159, 139, 32, 185, 29, 159, 14, 174, 35, 224, 103, 147, 56, 1, 125, 46, 177, 13, 15, 3, 210, 81, 64, 203, 182, 202, 93, 111, 154, 2, 2, 86, 44, 152, 8, 167, 132, 60, 182, 67, 198, 182, 59, 84, 227, 96, 138, 251, 2, 21, 239, 58, 230, 57, 146, 193, 221, 12, 196, 61, 207, 231, 98, 11, 4, 177, 246, 216, 157, 89, 172, 178, 170, 175, 209, 162, 131, 47, 188, 103, 192, 165, 115, 97, 209, 252, 98, 231, 65, 187, 46, 16, 110, 192, 45, 167, 91, 57, 155, 96, 143, 39, 54, 55, 180, 107, 144, 184, 96, 68, 85, 87, 192, 17, 40, 90, 175, 225, 73, 82, 213, 27, 132, 105, 207, 130, 108, 238, 253, 31, 9, 225, 127, 214, 181, 236, 100, 148, 115, 164, 35, 73, 14, 16, 234, 43, 172, 86, 134, 180, 41, 169, 247, 54, 159, 144, 53, 44, 127, 207, 23, 253, 167, 50, 123, 106, 3, 74, 109, 111, 6, 105, 58, 228, 41, 2, 35, 131, 31, 117, 123, 73, 250, 146, 154, 175, 173, 76, 76, 196, 238, 132, 163, 150, 146, 219, 219, 162, 17, 80, 195, 1, 218, 225, 161, 23, 203, 47, 33, 130, 142, 104, 196, 107, 96, 40, 22, 79, 138, 143, 242, 139, 17, 13, 26, 49, 232, 163, 53, 24, 253, 191, 27, 53, 110, 165, 218, 243, 64, 82, 172, 181, 41, 86, 38, 50, 91, 32, 139, 109, 78, 246, 77, 178, 120, 213, 175, 168, 119, 85, 63, 220, 60, 161, 201, 134, 51, 147, 129, 22, 230, 17, 123, 201, 180, 236, 130, 33, 64, 141, 142, 12, 201, 155, 226, 224, 151, 19, 253, 200, 162, 85, 156, 199, 249, 221, 157, 217, 185, 123, 36, 209, 173, 203, 130, 132, 217, 6, 18, 248, 105, 46, 201, 184, 214, 35, 26, 85, 104, 33, 158, 236, 154, 152, 193, 82, 146, 188, 134, 42, 198, 199, 161, 4, 182, 101, 172, 233, 217, 160, 119, 161, 2, 90, 189, 238, 44, 85, 141, 207, 17, 249, 162, 228, 140, 189, 155, 68, 199, 162, 0, 132, 149, 176, 161, 112, 217, 249, 30, 64, 124, 242, 238, 62, 5, 87, 48, 254, 192, 199, 77, 152, 131, 116, 175, 242, 42, 211, 29, 157, 15, 182, 203, 230, 173, 217, 218, 88, 177, 68, 11, 33, 110, 198, 0, 105, 207, 231, 126, 70, 112, 244, 48, 89, 31, 101, 149, 200, 80, 177, 197, 120, 228, 183, 140, 107, 113, 148, 66, 159, 249, 136, 175, 156, 119, 43, 167, 238, 171, 11, 20, 101, 203, 192, 18, 167, 199, 29, 251, 102, 76, 232, 240, 168, 229, 75, 93, 95, 228, 194, 163, 91, 45, 178, 60, 161, 107, 85, 229, 36, 214, 247, 122, 140, 102, 109, 158, 125, 93, 248, 250, 188, 104, 44, 4, 222, 138, 241, 141, 40, 203, 30, 217, 206, 190, 170, 131, 112, 1, 69, 65, 14, 85, 36, 244, 2, 205, 80, 101, 222, 204, 48, 231, 92, 211, 65, 7, 227, 18, 31, 115, 153, 150, 78, 41, 226, 101, 210, 13, 110, 156, 132, 29, 245, 109, 49, 141, 108, 77, 166, 30, 11, 52, 79, 198, 110, 30, 15, 49, 55, 48, 13, 23, 185, 127, 78, 123, 135, 67, 15, 112, 114, 38, 222, 58, 98, 160, 206, 51, 76, 172, 101, 138, 116, 230, 163, 165, 233, 145, 62, 14, 124, 232, 137, 116, 177, 16, 22, 226, 104, 163, 35, 74, 61, 203, 245, 215, 72, 251, 49, 65, 7, 77, 7, 6, 138, 248, 242, 40, 136, 207, 167, 248, 67, 30, 30, 136, 55, 159, 249, 37, 174, 247, 63, 39, 253, 106, 206, 229, 75, 69, 189, 151, 181, 237, 72, 64, 125, 125, 232, 145, 140, 5, 99, 41, 42, 34, 117, 161, 73, 35, 72, 40, 188, 200, 69, 218, 45, 5, 149, 3, 137, 122, 109, 28, 57, 220, 58, 248, 252, 221, 14, 6, 21, 190, 98, 192, 220, 163, 225, 169, 23, 1, 133, 25, 85, 34, 126, 242, 240, 22, 158, 123, 116, 121, 129, 90, 72, 137, 80, 197, 206, 210, 141, 232, 107, 126, 235, 147, 34, 192, 217, 34, 110, 226, 16, 91, 2, 36, 156, 26, 187, 34, 68, 135, 197, 228, 157, 229, 166, 149, 134, 41, 163, 123, 119, 92, 136, 33, 73, 41, 246, 241, 122, 172, 240, 245, 6, 233, 221, 207, 2, 65, 153, 58, 47, 74, 210, 112, 19, 215, 194, 139, 148, 53, 188, 199, 134, 143, 188, 37, 27, 27, 0, 106, 213, 176, 59, 56, 180, 90, 175, 191, 202, 177, 181, 124, 101, 249, 87, 12, 177, 136, 127, 99, 141, 23, 131, 241, 241, 51, 40, 251, 127, 172, 55, 185, 112, 200, 105, 211, 95, 230, 55, 128, 199, 59, 150, 209, 84, 43, 66, 147, 102, 177, 240, 2, 51, 21, 170, 129, 145, 154, 228, 251, 62, 199, 21, 217, 126, 2, 9, 72, 23, 123, 67, 77, 145, 188, 116, 239, 164, 9, 180, 17, 229, 93, 167, 200, 35, 53, 20, 148, 11, 90, 7, 128, 38, 149, 96, 76, 211, 162, 86, 247, 47, 219, 153, 228, 208, 174, 1, 77, 130, 206, 8, 56, 77, 178, 126, 103, 64, 66, 173, 130, 178, 15, 157, 82, 151, 6, 31, 32, 24, 254, 116, 177, 125, 94, 103, 50, 23, 88, 167, 177, 178, 127, 154, 65, 221, 58, 221, 59, 226, 100, 212, 90, 195, 98, 94, 191, 30, 232, 150, 153, 106, 253, 109, 235, 75, 64, 207, 222, 250, 116, 71, 33, 65, 213, 195, 126, 156, 152, 66, 192, 144, 40, 227, 151, 193, 175, 99, 224, 91, 60, 164, 255, 6, 122, 7, 67, 247, 68, 73, 212, 29, 46, 88, 182, 8, 140, 159, 12, 53, 10, 134, 178, 137, 239, 153, 185, 79, 82, 248, 233, 246, 212, 236, 69, 141, 78, 223, 143, 137, 36, 152, 203, 115, 15, 73, 227, 247, 216, 141, 74, 85, 92, 252, 219, 11, 86, 24, 36, 132, 246, 133, 77, 210, 94, 247, 210, 48, 169, 100, 17, 141, 75, 195, 158, 42, 251, 39, 47, 60, 157, 65, 57, 125, 28, 71, 111, 42, 158, 47, 14, 42, 28, 117, 50, 96, 232, 48, 42, 244, 34, 57, 12, 138, 203, 106, 250, 25, 67, 23, 176, 220, 128, 5, 19, 7, 102, 171, 23, 9, 30, 96, 230, 35, 15, 34, 132, 122, 139, 178, 231, 226, 252, 122, 142, 51, 52, 11, 84, 228, 93, 72, 237, 102, 129, 75, 238, 112, 182, 84, 130, 158, 130, 41, 139, 174, 56, 166, 174, 139, 242, 44, 145, 148, 133, 123, 42, 62, 163, 116, 84, 154, 57, 28, 58, 32, 154, 34, 151, 21, 127, 143, 122, 136, 139, 252, 101, 165, 227, 105, 4, 244, 58, 116, 125, 45, 98, 76, 103, 94, 178, 138, 217, 234, 42, 40, 152, 61, 28, 185, 62, 43, 88, 177, 3, 57, 129, 36, 0, 238, 63, 194, 253, 71, 206, 114, 243, 137, 92, 79, 175, 23, 62, 120, 193, 114, 13, 142, 244, 4, 131, 205, 14, 186, 248, 29, 212, 164, 52, 12, 111, 31, 124, 143, 212, 208, 173, 96, 133, 27, 194, 239, 242, 62, 141, 119, 203, 52, 113, 243, 31, 84, 235, 5, 5, 64, 191, 252, 81, 217, 200, 232, 68, 61, 213, 171, 145, 221, 224, 102, 138, 6, 53, 167, 130, 6, 206, 196, 226, 4, 174, 108, 86, 154, 134, 37, 59, 110, 9, 191, 10, 117, 191, 133, 114, 72, 86, 93, 113, 19, 75, 34, 46, 2, 185, 169, 224, 240, 13, 200, 128, 24, 118, 36, 233, 120, 115, 6, 192, 92, 12, 228, 224, 111, 192, 137, 188, 16, 155, 62, 198, 60, 154, 52, 196, 125, 70, 58, 33, 43, 11, 168, 35, 190, 161, 124, 108, 153, 90, 186, 113, 57, 176, 154, 172, 202, 179, 55, 179, 115, 45, 183, 90, 4, 216, 112, 48, 228, 230, 219, 58, 70, 142, 78, 74, 9, 135, 22, 40, 70, 146, 224, 171, 128, 68, 8, 228, 90, 35, 220, 41, 184, 91, 212, 34, 57, 143, 189, 6, 215, 17, 90, 143, 19, 92, 97, 0, 32, 3, 71, 160, 179, 249, 172, 83, 31, 79, 228, 235, 186, 237, 88, 60, 223, 16, 150, 62, 200, 199, 30, 185, 44, 207, 192, 77, 194, 159, 154, 169, 139, 30, 66, 92, 141, 190, 237, 210, 202, 206, 242, 79, 195, 207, 22, 106, 63, 29, 195, 112, 209, 66, 25, 0, 132, 115, 0, 202, 40, 194, 111, 241, 33, 135, 53, 223, 250, 204, 154, 93, 179, 199, 141, 86, 243, 91, 194, 109, 181, 226, 69, 138, 254, 216, 121, 193, 63, 120, 2, 134, 176, 156, 166, 74, 11, 81, 70, 173, 20, 22, 53, 20, 94, 78, 223, 122, 51, 83, 235, 222, 40, 243, 124, 92, 42, 236, 168, 151, 228, 224, 219, 96, 235, 105, 253, 68, 151, 63, 159, 213, 61, 96, 8, 224, 24, 28, 90, 129, 156, 22, 25, 66, 66, 143, 239, 196, 171, 29, 13, 95, 163, 56, 87, 48, 212, 215, 72, 227, 81, 104, 53, 139, 236, 188, 128, 188, 39, 63, 223, 221, 87, 39, 250, 188, 147, 17, 199, 187, 191, 223, 149, 255, 172, 2, 174, 180, 30, 3, 20, 9, 179, 209, 14, 135, 177, 230, 230, 197, 6, 214, 127, 184, 220, 182, 147, 196, 1, 233, 166, 208, 117, 53, 27, 239, 167, 250, 146, 188, 191, 44, 63, 126, 25, 131, 251, 155, 209, 124, 244, 61, 128, 33, 161, 254, 124, 17, 223, 157, 9, 174, 72, 136, 130, 99, 2, 218, 76, 248, 115, 52, 140, 75, 220, 169, 145, 119, 90, 154, 55, 179, 3, 247, 245, 224, 97, 120, 11, 63, 95, 248, 227, 205, 148, 9, 156, 121, 111, 198, 183, 194, 240, 224, 127, 172, 161, 40, 176, 174, 198, 115, 39, 21, 135, 93, 120, 52, 77, 227, 203, 226, 47, 228, 208, 254, 86, 42, 178, 168, 118, 24, 219, 101, 28, 233, 205, 124, 126, 162, 249, 141, 40, 110, 247, 247, 167, 36, 186, 206, 2, 218, 68, 198, 144, 175, 3, 72, 97, 133, 245, 21, 59, 121, 75, 124, 96, 81, 44, 114, 14, 155, 78, 83, 4, 100, 18, 134, 220, 4, 88, 1, 176, 47, 235, 54, 199, 40, 55, 162, 55, 29, 186, 218, 192, 34, 234, 96, 241, 128, 217, 64, 204, 95, 238, 170, 235, 230, 242, 14, 87, 111, 159, 173, 38, 146, 75, 197, 5, 115, 239, 112, 251, 19, 178, 192, 186, 135, 123, 127, 45, 136, 135, 4, 78, 178, 164, 252, 8, 36, 171, 74, 21, 243, 236, 22, 204, 242, 123, 6, 114, 138, 102, 239, 74, 238, 255, 233, 3, 164, 62, 58, 143, 197, 248, 147, 172, 165, 4, 14, 1, 147, 251, 79, 85, 50, 48, 118, 108, 30, 77, 81, 140, 29, 143, 198, 121, 197, 19, 225, 49, 93, 53, 73, 33, 122, 100, 234, 165, 67, 183, 100, 195, 168, 123, 37, 235, 86, 147, 164, 71, 20, 232, 206, 20, 228, 22, 17, 177, 2, 116, 163, 136, 187, 43, 160, 176, 83, 52, 39, 224, 140, 250, 66, 232, 255, 36, 166, 132, 97, 252, 44, 246, 106, 21, 113, 16, 166, 70, 54, 222, 228, 10, 51, 19, 106, 141, 31, 102, 129, 132, 219, 84, 27, 192, 66, 145, 133, 208, 111, 137, 81, 145, 121, 176, 155, 133, 222, 27, 156, 93, 19, 244, 230, 154, 5, 113, 177, 247, 70, 125, 202, 138, 60, 3, 29, 182, 33, 211, 112, 136, 107, 223, 175, 131, 92, 13, 23, 133, 238, 136, 130, 231, 215, 211, 199, 47, 70, 75, 104, 253, 172, 61, 246, 99, 80, 154, 169, 205, 208, 105, 43, 2, 110, 203, 246, 16, 176, 11, 152, 190, 185, 100, 102, 6, 125, 63, 101, 184, 157, 6, 52, 237, 85, 121, 193, 4, 245, 227, 178, 121, 91, 208, 31, 149, 32, 198, 6, 44, 160, 127, 164, 97, 113, 61, 8, 129, 126, 48, 10, 29, 100, 71, 211, 225, 40, 9, 0, 3, 68, 93, 210, 80, 229, 218, 36, 186, 188, 107, 182, 149, 10, 6, 152, 149, 89, 117, 111, 83, 95, 195, 187, 170, 69, 248, 36, 46, 55, 27, 32, 176, 49, 156, 89, 40, 187, 234, 208, 124, 4, 208, 147, 55, 234, 237, 170, 226, 161, 111, 205, 227, 202, 198, 15, 149, 253, 102, 56, 203, 135, 155, 57, 133, 7, 109, 38, 45, 84, 8, 201, 120, 98, 237, 208, 163, 213, 53, 176, 121, 188, 246, 40, 141, 178, 27, 12, 74, 154, 55, 231, 250, 214, 249, 165, 18, 187, 143, 85, 131, 175, 192, 203, 220, 173, 219, 162, 181, 130, 100, 203, 31, 68, 146, 157, 10, 1, 49, 123, 56, 175, 34, 92, 31, 205, 201, 39, 97, 89, 162, 176, 166, 198, 242, 192, 212, 230, 208, 253, 110, 118, 141, 136, 68, 241, 174, 93, 119, 157, 164, 70, 52, 222, 125, 170, 235, 170, 115, 129, 254, 58, 191, 19, 12, 9, 76, 159, 116, 183, 185, 166, 169, 247, 179, 96, 43, 63, 202, 50, 91, 14, 150, 45, 142, 89, 161, 224, 94, 7, 25, 127, 143, 190, 148, 251, 243, 246, 157, 36, 116, 199, 185, 196, 245, 6, 4, 255, 135, 86, 236, 255, 223, 240, 191, 8, 186, 178, 34, 247, 77, 40, 193, 224, 191, 235, 56, 14, 246, 154, 127, 15, 214, 232, 95, 120, 83, 143, 147, 159, 97, 207, 96, 9, 174, 129, 106, 21, 250, 164, 65, 103, 111, 23, 160, 242, 127, 5, 172, 168, 68, 41, 119, 189, 125, 79, 82, 22, 58, 113, 205, 193, 107, 168, 144, 220, 34, 198, 164, 235, 153, 160, 200, 39, 196, 87, 81, 195, 247, 81, 215, 98, 5, 153, 67, 60, 255, 1, 229, 149, 1, 43, 195, 242, 114, 27, 7, 17, 7, 48, 7, 15, 179, 77, 91, 7, 173, 203, 172, 82, 199, 183, 45, 3, 129, 165, 46, 118, 88, 51, 37, 119, 226, 88, 128, 248, 41, 126, 17, 63, 63, 4, 255, 115, 59, 255, 240, 220, 172, 123, 102, 29, 135, 40, 107, 120, 53, 184, 127, 231, 193, 184, 153, 137, 247, 151, 78, 69, 91, 191, 61, 101, 207, 63, 105, 107, 14, 17, 32, 90, 219, 46, 140, 31, 62, 162, 109, 58, 9, 130, 12, 43, 19, 47, 212, 0, 90, 155, 76, 81, 171, 218, 9, 247, 3, 109, 2, 16, 172, 132, 3, 110, 169, 67, 209, 224, 73, 187, 152, 1, 69, 219, 82, 197, 108, 65, 235, 90, 151, 104, 66, 213, 115, 201, 79, 245, 192, 44, 117, 92, 194, 188, 160, 230, 5, 218, 50, 149, 8, 178, 46, 251, 29, 25, 152, 183, 68, 232, 119, 92, 180, 46, 74, 54, 196, 101, 98, 232, 155, 48, 80, 197, 12, 73, 58, 21, 146, 48, 187, 167, 6, 244, 119, 130, 90, 213, 218, 183, 48, 80, 166, 206, 163, 4, 184, 126, 190, 35, 86, 79, 221, 49, 160, 109, 215, 63, 102, 96, 125, 91, 10, 111, 205, 186, 170, 6, 107, 177, 75, 228, 128, 132, 185, 17, 65, 18, 94, 203, 211, 252, 238, 28, 191, 99, 67, 146, 56, 57, 126, 71, 134, 71, 28, 227, 119, 92, 120, 100, 173, 101, 30, 73, 218, 119, 132, 56, 125, 71, 5, 181, 139, 117, 185, 40, 5, 226, 158, 223, 241, 42, 73, 251, 206, 37, 98, 169, 239, 248, 240, 88, 189, 99, 3, 175, 122, 199, 133, 73, 83, 164, 222, 113, 161, 222, 81, 129, 114, 143, 64, 96, 60, 133, 174, 253, 166, 169, 213, 33, 255, 223, 133, 252, 243, 29, 29, 250, 123, 0, 213, 244, 43, 218, 0, 161, 152, 29, 213, 213, 52, 252, 144, 149, 137, 23, 62, 84, 49, 67, 159, 239, 216, 112, 126, 41, 200, 31, 217, 249, 142, 11, 26, 137, 131, 52, 246, 72, 131, 182, 109, 154, 162, 7, 213, 140, 116, 126, 0, 100, 192, 175, 99, 249, 142, 11, 213, 210, 209, 38, 223, 177, 17, 225, 174, 65, 154, 178, 223, 161, 37, 99, 40, 203, 102, 28, 30, 65, 190, 192, 115, 210, 50, 161, 126, 107, 129, 208, 148, 253, 15, 72, 179, 36, 229, 35, 195, 59, 6, 192, 19, 45, 119, 132, 120, 4, 2, 91, 119, 92, 64, 24, 71, 17, 28, 119, 132, 136, 224, 184, 163, 194, 51, 211, 70, 142, 59, 54, 112, 220, 209, 193, 184, 163, 2, 117, 71, 5, 12, 147, 201, 29, 25, 182, 29, 31, 175, 158, 218, 145, 33, 181, 163, 163, 181, 149, 61, 111, 156, 144, 79, 112, 132, 52, 213, 158, 224, 200, 176, 38, 56, 54, 244, 119, 77, 112, 100, 240, 53, 193, 145, 34, 237, 251, 50, 52, 193, 49, 98, 77, 112, 108, 224, 164, 104, 130, 35, 228, 155, 54, 118, 236, 113, 142, 16, 206, 47, 115, 125, 253, 92, 142, 233, 163, 87, 117, 92, 127, 219, 57, 46, 248, 149, 56, 152, 111, 219, 206, 113, 193, 76, 91, 180, 32, 182, 31, 122, 152, 125, 118, 142, 143, 166, 177, 115, 124, 72, 218, 115, 112, 109, 233, 188, 233, 108, 52, 109, 251, 33, 110, 37, 238, 120, 239, 101, 80, 99, 135, 76, 140, 93, 206, 81, 226, 119, 57, 127, 114, 57, 199, 134, 235, 182, 182, 206, 145, 225, 17, 8, 109, 157, 227, 162, 162, 206, 81, 65, 251, 121, 42, 67, 74, 213, 57, 54, 208, 42, 73, 235, 154, 23, 39, 196, 30, 69, 227, 254, 164, 206, 145, 241, 252, 166, 101, 54, 169, 115, 124, 60, 41, 169, 115, 100, 208, 246, 42, 40, 169, 243, 71, 57, 71, 135, 71, 57, 71, 5, 215, 166, 101, 63, 92, 45, 132, 58, 80, 119, 215, 118, 11, 252, 225, 147, 118, 87, 148, 115, 132, 84, 148, 115, 84, 80, 12, 46, 155, 226, 72, 237, 241, 181, 212, 241, 71, 165, 25, 173, 170, 147, 210, 204, 160, 230, 110, 93, 176, 214, 74, 26, 52, 120, 211, 137, 26, 197, 161, 150, 46, 243, 187, 32, 117, 132, 120, 36, 123, 209, 102, 229, 154, 208, 233, 28, 31, 92, 23, 67, 2, 197, 144, 164, 149, 237, 55, 117, 72, 83, 58, 199, 70, 82, 197, 9, 185, 100, 15, 171, 159, 131, 75, 22, 225, 148, 54, 120, 68, 215, 146, 174, 177, 71, 208, 183, 106, 246, 71, 37, 159, 52, 178, 240, 67, 194, 249, 166, 233, 66, 223, 30, 105, 228, 42, 101, 200, 61, 49, 72, 206, 120, 178, 59, 209, 55, 59, 58, 24, 243, 130, 210, 54, 118, 116, 120, 218, 126, 86, 211, 216, 209, 161, 203, 155, 16, 209, 217, 216, 177, 161, 117, 162, 140, 29, 23, 189, 216, 241, 161, 77, 182, 78, 34, 154, 159, 95, 181, 139, 29, 25, 96, 111, 21, 59, 54, 248, 132, 4, 18, 200, 61, 34, 73, 236, 200, 192, 203, 75, 225, 171, 180, 33, 90, 223, 165, 74, 20, 193, 11, 161, 129, 15, 141, 211, 38, 118, 92, 64, 44, 254, 240, 164, 93, 236, 30, 206, 113, 173, 146, 116, 17, 107, 25, 73, 82, 220, 24, 104, 147, 7, 93, 79, 49, 123, 180, 109, 222, 226, 198, 65, 66, 242, 52, 199, 135, 228, 105, 142, 10, 238, 155, 163, 2, 242, 230, 216, 32, 73, 233, 40, 220, 28, 25, 46, 155, 194, 220, 28, 25, 78, 122, 101, 36, 218, 28, 27, 79, 69, 69, 54, 71, 6, 143, 36, 244, 104, 38, 11, 134, 89, 133, 176, 97, 237, 63, 212, 184, 201, 86, 46, 155, 163, 67, 155, 223, 31, 126, 33, 218, 156, 208, 179, 227, 200, 101, 115, 116, 144, 100, 115, 116, 172, 75, 247, 204, 113, 241, 104, 188, 15, 174, 173, 84, 34, 247, 204, 113, 194, 57, 230, 168, 176, 128, 0, 13, 15, 146, 148, 206, 113, 102, 230, 232, 104, 91, 89, 182, 28, 23, 106, 155, 183, 28, 25, 154, 183, 28, 29, 154, 183, 28, 21, 36, 219, 120, 57, 50, 36, 94, 142, 10, 232, 160, 24, 214, 91, 34, 77, 187, 28, 29, 13, 78, 117, 57, 46, 60, 32, 54, 151, 99, 176, 216, 59, 13, 223, 148, 53, 238, 157, 52, 69, 19, 85, 11, 90, 81, 3, 253, 92, 142, 11, 174, 139, 8, 12, 142, 231, 114, 92, 104, 90, 46, 199, 133, 135, 217, 8, 9, 179, 179, 40, 165, 225, 97, 246, 29, 232, 208, 120, 127, 21, 90, 114, 57, 74, 120, 75, 101, 18, 199, 69, 47, 92, 169, 115, 43, 113, 108, 240, 168, 88, 71, 199, 122, 203, 214, 247, 117, 116, 76, 122, 29, 21, 244, 189, 75, 56, 108, 58, 9, 57, 26, 183, 117, 92, 208, 182, 139, 162, 173, 1, 74, 231, 8, 33, 208, 195, 236, 2, 143, 160, 135, 217, 5, 84, 49, 230, 73, 65, 41, 53, 232, 155, 160, 206, 37, 151, 135, 206, 69, 87, 69, 197, 82, 28, 60, 237, 251, 126, 190, 71, 64, 144, 142, 63, 240, 190, 203, 169, 123, 68, 112, 254, 37, 43, 91, 246, 104, 187, 34, 188, 225, 141, 211, 66, 22, 172, 156, 60, 154, 33, 24, 43, 7, 54, 172, 79, 108, 29, 25, 154, 187, 21, 33, 236, 181, 102, 29, 25, 32, 223, 84, 186, 10, 104, 191, 111, 104, 93, 217, 154, 117, 92, 180, 102, 29, 29, 173, 207, 172, 35, 195, 51, 235, 168, 160, 106, 29, 33, 107, 242, 28, 199, 133, 181, 255, 30, 13, 43, 223, 4, 69, 16, 181, 30, 59, 142, 12, 207, 142, 163, 130, 227, 233, 52, 154, 29, 71, 135, 42, 110, 104, 93, 162, 135, 89, 199, 142, 35, 68, 69, 151, 227, 184, 240, 168, 4, 125, 211, 137, 178, 117, 28, 27, 172, 100, 29, 199, 133, 231, 153, 117, 28, 25, 60, 2, 65, 181, 58, 46, 105, 69, 33, 120, 68, 146, 212, 73, 207, 111, 194, 67, 93, 38, 135, 76, 154, 162, 181, 212, 113, 108, 56, 52, 45, 23, 117, 28, 31, 80, 3, 123, 158, 142, 35, 131, 235, 116, 28, 23, 28, 199, 134, 174, 101, 212, 53, 86, 190, 9, 234, 43, 223, 4, 117, 88, 249, 38, 232, 227, 24, 194, 214, 113, 36, 161, 141, 23, 54, 180, 62, 185, 28, 74, 73, 58, 142, 144, 206, 8, 122, 240, 200, 74, 248, 144, 144, 142, 163, 130, 174, 101, 208, 4, 18, 68, 72, 199, 241, 161, 107, 25, 4, 65, 58, 142, 13, 8, 210, 113, 84, 240, 8, 132, 116, 28, 25, 118, 37, 84, 81, 140, 35, 195, 209, 97, 93, 54, 20, 241, 234, 234, 188, 58, 54, 216, 58, 142, 36, 208, 171, 99, 195, 202, 73, 218, 198, 172, 211, 65, 175, 142, 11, 90, 255, 181, 65, 175, 142, 14, 85, 173, 190, 18, 198, 136, 166, 173, 36, 241, 90, 208, 171, 35, 228, 97, 22, 186, 147, 86, 71, 6, 36, 73, 249, 254, 77, 89, 99, 231, 159, 7, 88, 61, 245, 246, 186, 129, 123, 172, 147, 210, 180, 109, 154, 124, 164, 93, 0, 215, 77, 128, 250, 82, 102, 27, 86, 249, 169, 156, 146, 200, 39, 56, 98, 32, 225, 173, 79, 46, 148, 106, 97, 185, 80, 218, 165, 77, 209, 214, 234, 144, 235, 226, 70, 44, 151, 0, 18, 175, 146, 244, 22, 244, 77, 153, 46, 213, 222, 226, 148, 6, 152, 164, 32, 253, 220, 196, 238, 14, 30, 80, 154, 117, 137, 26, 184, 235, 106, 7, 36, 98, 89, 18, 72, 0, 65, 84, 84, 181, 125, 52, 78, 59, 73, 6, 233, 164, 213, 177, 161, 27, 221, 15, 143, 32, 126, 6, 141, 61, 130, 15, 15, 179, 169, 57, 40, 22, 179, 147, 36, 102, 14, 187, 18, 106, 208, 138, 93, 250, 38, 168, 99, 99, 130, 58, 42, 248, 149, 188, 235, 59, 176, 171, 40, 122, 250, 219, 46, 231, 17, 234, 153, 144, 166, 166, 223, 212, 177, 97, 109, 50, 213, 212, 81, 131, 218, 110, 84, 239, 162, 154, 58, 50, 124, 237, 63, 200, 35, 241, 208, 0, 161, 205, 50, 190, 159, 142, 222, 245, 83, 187, 17, 0, 195, 172, 66, 173, 19, 166, 142, 15, 213, 11, 113, 215, 133, 246, 121, 66, 251, 30, 188, 0, 254, 203, 24, 35, 0, 33, 188, 201, 71, 24, 209, 58, 217, 86, 139, 82, 36, 38, 58, 35, 72, 27, 47, 246, 182, 233, 82, 199, 7, 218, 66, 43, 182, 161, 76, 157, 8, 159, 52, 218, 32, 228, 143, 13, 175, 236, 144, 63, 46, 92, 115, 200, 31, 25, 252, 209, 241, 184, 240, 71, 37, 13, 249, 35, 131, 127, 37, 137, 37, 137, 153, 147, 184, 194, 136, 112, 9, 253, 236, 102, 29, 18, 40, 151, 12, 249, 35, 67, 195, 240, 216, 144, 36, 102, 40, 178, 248, 166, 174, 237, 144, 47, 242, 199, 6, 127, 132, 184, 63, 58, 214, 4, 199, 101, 55, 200, 31, 35, 252, 17, 242, 73, 93, 69, 145, 63, 54, 190, 169, 106, 248, 227, 227, 241, 33, 157, 63, 82, 52, 63, 253, 184, 240, 251, 244, 227, 2, 63, 122, 250, 17, 162, 189, 157, 208, 211, 143, 13, 100, 188, 58, 246, 250, 113, 161, 139, 97, 94, 63, 46, 156, 127, 24, 210, 215, 143, 13, 167, 121, 220, 143, 12, 19, 190, 246, 159, 196, 87, 244, 81, 74, 150, 251, 49, 98, 229, 78, 182, 31, 33, 88, 234, 130, 116, 185, 237, 199, 135, 107, 219, 53, 241, 78, 179, 173, 180, 193, 25, 134, 35, 92, 219, 46, 182, 253, 40, 193, 182, 31, 21, 60, 130, 13, 10, 93, 94, 106, 117, 207, 107, 90, 110, 63, 46, 120, 152, 93, 164, 246, 248, 82, 197, 204, 177, 214, 74, 26, 174, 141, 246, 35, 164, 109, 58, 9, 57, 168, 183, 78, 178, 127, 33, 30, 65, 14, 42, 219, 159, 108, 1, 132, 71, 36, 254, 234, 72, 53, 165, 146, 148, 253, 184, 160, 223, 162, 25, 190, 105, 167, 211, 148, 253, 248, 152, 32, 130, 126, 84, 112, 154, 199, 143, 16, 39, 61, 126, 92, 232, 236, 230, 71, 9, 247, 92, 14, 223, 236, 223, 236, 104, 101, 218, 134, 23, 59, 126, 132, 120, 152, 93, 142, 109, 196, 3, 50, 216, 241, 163, 66, 231, 226, 248, 145, 193, 35, 16, 208, 17, 1, 132, 8, 68, 208, 195, 236, 98, 252, 8, 89, 140, 31, 21, 186, 98, 25, 77, 37, 252, 8, 105, 93, 102, 191, 169, 90, 126, 132, 52, 94, 132, 224, 71, 134, 71, 16, 62, 116, 45, 51, 65, 252, 200, 152, 0, 243, 15, 241, 227, 98, 2, 223, 246, 33, 126, 126, 92, 32, 126, 116, 240, 35, 131, 223, 129, 31, 25, 21, 135, 24, 241, 35, 126, 116, 64, 252, 252, 232, 128, 24, 211, 32, 126, 140, 120, 68, 144, 240, 201, 235, 113, 225, 18, 15, 63, 58, 92, 159, 223, 4, 126, 132, 248, 84, 136, 31, 33, 174, 105, 217, 73, 136, 31, 29, 252, 216, 128, 141, 119, 6, 135, 7, 126, 132, 136, 128, 4, 16, 64, 208, 30, 25, 28, 123, 124, 56, 233, 147, 61, 50, 120, 68, 173, 199, 133, 245, 251, 232, 208, 245, 251, 16, 87, 45, 243, 221, 190, 115, 251, 200, 88, 253, 22, 250, 244, 77, 208, 196, 158, 182, 143, 11, 253, 84, 140, 230, 197, 73, 169, 123, 254, 233, 155, 224, 112, 82, 43, 131, 120, 12, 73, 51, 131, 122, 65, 220, 191, 131, 26, 123, 196, 65, 63, 23, 211, 22, 61, 109, 191, 225, 164, 212, 167, 73, 86, 82, 208, 47, 99, 156, 36, 22, 115, 162, 167, 237, 227, 195, 55, 59, 219, 199, 5, 99, 251, 168, 0, 33, 73, 204, 144, 54, 174, 104, 240, 8, 210, 198, 21, 200, 112, 124, 69, 145, 247, 195, 108, 132, 55, 67, 171, 36, 173, 67, 7, 143, 86, 167, 42, 241, 8, 4, 110, 210, 161, 141, 43, 32, 149, 101, 30, 24, 102, 21, 82, 182, 143, 141, 135, 89, 181, 246, 113, 241, 72, 210, 62, 46, 60, 46, 105, 207, 121, 100, 144, 164, 92, 40, 220, 206, 99, 195, 37, 131, 60, 124, 211, 238, 151, 26, 39, 52, 17, 193, 105, 30, 183, 243, 200, 208, 245, 31, 209, 180, 79, 110, 231, 145, 129, 210, 140, 103, 182, 157, 71, 6, 136, 101, 151, 122, 135, 56, 133, 51, 89, 144, 227, 202, 144, 131, 106, 59, 143, 11, 190, 80, 169, 237, 60, 54, 48, 229, 209, 243, 189, 206, 163, 163, 173, 243, 8, 73, 172, 180, 213, 249, 135, 217, 166, 206, 227, 227, 89, 154, 58, 143, 12, 206, 160, 166, 206, 227, 162, 105, 203, 212, 121, 92, 236, 67, 202, 212, 121, 100, 72, 218, 99, 30, 23, 214, 254, 107, 160, 235, 53, 162, 248, 182, 235, 43, 250, 200, 120, 237, 76, 232, 208, 120, 63, 35, 38, 128, 152, 127, 19, 12, 26, 24, 102, 21, 250, 138, 254, 87, 244, 209, 209, 234, 188, 62, 50, 172, 75, 180, 54, 219, 127, 219, 139, 52, 88, 236, 29, 228, 139, 249, 25, 32, 67, 31, 29, 30, 186, 216, 245, 17, 242, 174, 143, 10, 218, 156, 80, 132, 71, 32, 32, 100, 65, 171, 183, 62, 50, 72, 40, 136, 65, 118, 235, 227, 163, 249, 179, 109, 147, 78, 226, 87, 2, 105, 109, 45, 23, 180, 240, 65, 43, 214, 17, 107, 125, 108, 120, 68, 21, 55, 247, 239, 32, 119, 93, 77, 83, 211, 199, 6, 143, 99, 250, 184, 16, 1, 122, 109, 25, 43, 127, 142, 233, 227, 131, 167, 179, 185, 161, 244, 201, 181, 250, 65, 28, 211, 71, 134, 254, 54, 197, 23, 76, 31, 27, 142, 233, 163, 194, 107, 91, 250, 216, 240, 205, 110, 162, 177, 71, 152, 236, 70, 151, 62, 50, 92, 126, 227, 93, 155, 234, 99, 67, 5, 154, 234, 35, 196, 241, 129, 82, 127, 46, 61, 54, 60, 50, 26, 239, 194, 56, 61, 66, 188, 239, 90, 107, 37, 18, 207, 71, 70, 119, 62, 42, 56, 131, 58, 31, 25, 212, 229, 187, 124, 92, 36, 117, 142, 92, 62, 50, 254, 49, 114, 249, 184, 144, 172, 124, 149, 228, 35, 195, 61, 169, 182, 148, 181, 249, 248, 88, 155, 143, 10, 139, 122, 24, 38, 31, 23, 222, 248, 113, 82, 62, 50, 80, 243, 231, 122, 75, 62, 66, 44, 249, 75, 62, 50, 124, 179, 55, 180, 98, 31, 32, 186, 150, 124, 92, 208, 181, 228, 178, 228, 35, 195, 113, 235, 183, 117, 208, 10, 225, 17, 166, 239, 143, 35, 227, 87, 146, 218, 227, 184, 72, 142, 1, 113, 210, 43, 227, 72, 90, 81, 148, 218, 227, 216, 120, 28, 39, 212, 140, 240, 65, 181, 255, 17, 129, 12, 173, 110, 41, 142, 12, 232, 72, 20, 71, 199, 147, 20, 71, 5, 143, 32, 214, 73, 113, 151, 20, 199, 70, 4, 8, 137, 247, 85, 232, 97, 159, 195, 23, 243, 251, 231, 55, 172, 220, 180, 12, 195, 13, 18, 43, 143, 188, 197, 232, 164, 113, 196, 55, 117, 104, 146, 226, 248, 160, 56, 58, 22, 80, 39, 101, 210, 118, 136, 226, 248, 80, 127, 149, 102, 226, 184, 64, 70, 99, 111, 38, 142, 12, 220, 76, 28, 21, 16, 226, 81, 188, 60, 173, 19, 141, 160, 153, 56, 54, 48, 255, 17, 22, 30, 129, 240, 73, 151, 186, 227, 154, 153, 243, 233, 32, 52, 19, 199, 9, 143, 64, 104, 38, 173, 191, 150, 129, 208, 76, 154, 182, 158, 10, 161, 153, 56, 54, 56, 80, 232, 155, 32, 126, 111, 242, 81, 51, 113, 100, 52, 192, 40, 135, 56, 63, 1, 225, 205, 196, 145, 129, 55, 185, 24, 140, 104, 149, 228, 130, 154, 137, 99, 163, 117, 43, 116, 65, 105, 151, 151, 16, 63, 74, 157, 168, 153, 56, 54, 248, 214, 137, 227, 163, 153, 227, 152, 168, 138, 144, 36, 133, 56, 232, 47, 157, 56, 50, 60, 58, 233, 231, 8, 89, 185, 221, 207, 113, 225, 175, 13, 105, 74, 166, 159, 35, 196, 45, 74, 105, 117, 143, 51, 225, 223, 236, 28, 154, 125, 39, 239, 218, 112, 249, 140, 79, 175, 211, 126, 254, 227, 43, 250, 8, 242, 84, 32, 36, 36, 137, 25, 106, 117, 207, 209, 161, 207, 173, 149, 177, 231, 232, 104, 149, 36, 117, 18, 123, 139, 15, 247, 69, 103, 243, 28, 23, 42, 203, 48, 207, 145, 225, 29, 227, 118, 15, 86, 207, 209, 161, 158, 157, 158, 227, 194, 93, 83, 122, 142, 14, 95, 209, 207, 95, 158, 35, 131, 174, 231, 125, 121, 142, 12, 73, 91, 95, 158, 227, 194, 41, 200, 27, 239, 187, 5, 246, 33, 148, 152, 240, 229, 57, 62, 26, 133, 21, 4, 45, 157, 232, 90, 158, 35, 132, 247, 170, 229, 57, 46, 154, 206, 229, 57, 50, 184, 39, 225, 231, 79, 215, 235, 245, 141, 124, 25, 190, 237, 162, 113, 255, 167, 76, 203, 248, 19, 53, 238, 111, 224, 192, 61, 185, 67, 51, 2, 84, 243, 131, 107, 191, 79, 101, 144, 210, 246, 16, 55, 6, 180, 190, 211, 172, 115, 0, 245, 167, 180, 185, 53, 139, 184, 161, 92, 73, 21, 4, 205, 206, 128, 47, 148, 47, 128, 123, 114, 244, 192, 3, 215, 182, 75, 213, 190, 6, 185, 167, 15, 192, 61, 57, 34, 128, 187, 38, 117, 160, 85, 53, 183, 0, 220, 83, 50, 71, 173, 170, 25, 57, 96, 177, 119, 208, 130, 7, 2, 8, 18, 53, 92, 146, 210, 209, 94, 166, 81, 129, 134, 149, 138, 217, 35, 81, 241, 8, 82, 14, 111, 156, 22, 169, 183, 114, 162, 235, 249, 199, 39, 141, 180, 237, 255, 68, 135, 194, 8, 9, 18, 254, 126, 199, 133, 63, 203, 129, 166, 237, 51, 211, 126, 104, 101, 205, 12, 47, 6, 72, 161, 12, 206, 191, 68, 171, 159, 254, 99, 230, 145, 9, 255, 166, 158, 144, 115, 44, 181, 223, 25, 215, 192, 120, 65, 110, 189, 37, 114, 21, 11, 150, 218, 15, 121, 117, 151, 172, 225, 159, 188, 152, 135, 89, 9, 92, 209, 195, 172, 235, 62, 204, 62, 204, 110, 211, 86, 113, 82, 85, 1, 135, 239, 124, 119, 154, 109, 182, 253, 11, 60, 163, 139, 109, 191, 131, 181, 146, 129, 239, 124, 127, 160, 154, 170, 116, 16, 32, 168, 165, 60, 171, 49, 98, 219, 207, 191, 216, 59, 200, 109, 255, 179, 60, 60, 191, 157, 107, 217, 186, 3, 11, 52, 229, 114, 74, 65, 105, 72, 231, 34, 161, 157, 253, 62, 27, 131, 6, 207, 232, 138, 96, 2, 228, 120, 58, 14, 142, 167, 19, 89, 24, 69, 52, 40, 125, 104, 188, 14, 11, 21, 64, 63, 27, 238, 126, 142, 86, 246, 123, 34, 95, 116, 128, 73, 211, 137, 30, 200, 240, 191, 76, 63, 109, 191, 227, 2, 146, 182, 222, 186, 22, 55, 68, 159, 190, 9, 1, 19, 108, 200, 196, 19, 84, 72, 60, 65, 5, 157, 17, 244, 218, 4, 23, 254, 142, 219, 4, 25, 38, 172, 9, 42, 104, 188, 21, 59, 65, 6, 247, 180, 170, 45, 133, 34, 130, 190, 73, 55, 19, 135, 72, 138, 171, 40, 182, 19, 132, 208, 223, 9, 42, 52, 94, 223, 9, 50, 120, 4, 33, 234, 59, 65, 133, 181, 255, 144, 239, 4, 25, 218, 118, 41, 219, 71, 190, 19, 132, 32, 223, 9, 62, 24, 184, 100, 15, 19, 238, 124, 58, 165, 44, 219, 131, 233, 175, 59, 36, 90, 179, 238, 137, 32, 235, 105, 59, 193, 133, 230, 207, 212, 202, 240, 129, 71, 144, 225, 17, 8, 255, 19, 7, 134, 226, 200, 35, 143, 9, 232, 34, 143, 60, 56, 158, 139, 105, 240, 165, 19, 93, 232, 249, 93, 249, 38, 60, 224, 154, 250, 177, 59, 224, 203, 65, 203, 246, 79, 120, 4, 2, 227, 83, 106, 63, 84, 237, 4, 33, 24, 115, 5, 131, 7, 15, 146, 148, 239, 218, 233, 248, 2, 6, 90, 255, 51, 249, 131, 111, 37, 81, 231, 98, 39, 161, 235, 116, 178, 159, 205, 65, 219, 46, 213, 180, 209, 197, 239, 235, 68, 184, 199, 221, 116, 46, 205, 221, 10, 209, 218, 54, 181, 147, 3, 109, 78, 223, 233, 52, 120, 78, 28, 36, 105, 223, 117, 169, 150, 105, 66, 219, 46, 137, 215, 13, 158, 151, 67, 21, 134, 232, 92, 30, 190, 21, 4, 121, 154, 63, 237, 4, 23, 239, 236, 76, 112, 1, 226, 155, 221, 209, 234, 224, 30, 82, 109, 25, 164, 107, 153, 201, 4, 23, 154, 187, 117, 130, 12, 138, 226, 158, 238, 29, 137, 95, 245, 18, 61, 240, 166, 148, 170, 117, 130, 141, 165, 169, 117, 130, 12, 30, 129, 208, 154, 85, 211, 9, 46, 60, 226, 128, 158, 223, 196, 75, 39, 8, 161, 30, 175, 164, 154, 41, 14, 203, 141, 155, 39, 206, 154, 25, 101, 219, 240, 198, 139, 146, 169, 195, 192, 113, 217, 141, 54, 77, 254, 160, 43, 147, 43, 219, 111, 175, 33, 141, 118, 227, 93, 186, 228, 114, 180, 190, 182, 107, 253, 182, 143, 147, 58, 91, 215, 113, 96, 109, 220, 65, 83, 51, 227, 104, 188, 15, 147, 137, 135, 59, 55, 196, 174, 132, 178, 209, 9, 50, 178, 209, 9, 42, 168, 78, 16, 50, 65, 134, 87, 192, 101, 154, 96, 195, 211, 57, 65, 133, 206, 9, 54, 190, 109, 231, 4, 25, 24, 102, 21, 194, 134, 111, 57, 65, 133, 136, 86, 137, 39, 115, 80, 153, 107, 109, 203, 9, 66, 40, 4, 71, 172, 124, 19, 60, 242, 88, 8, 42, 217, 229, 64, 37, 187, 80, 3, 7, 30, 76, 80, 180, 50, 204, 91, 36, 84, 178, 11, 49, 88, 8, 17, 173, 234, 22, 96, 174, 53, 160, 159, 171, 31, 82, 16, 171, 159, 218, 149, 19, 116, 208, 79, 133, 118, 229, 4, 27, 57, 153, 192, 4, 23, 19, 152, 32, 196, 4, 25, 18, 79, 134, 38, 200, 160, 58, 121, 113, 143, 10, 82, 181, 190, 43, 161, 9, 70, 76, 208, 225, 182, 226, 161, 66, 4, 19, 181, 239, 33, 3, 4, 106, 223, 195, 133, 134, 8, 38, 11, 245, 78, 231, 120, 28, 246, 61, 132, 232, 123, 168, 64, 183, 255, 225, 66, 69, 27, 237, 6, 77, 255, 115, 49, 200, 149, 99, 195, 195, 44, 132, 181, 15, 81, 77, 255, 81, 81, 180, 246, 31, 46, 214, 254, 67, 5, 85, 156, 154, 62, 220, 161, 255, 44, 104, 2, 204, 63, 132, 152, 0, 243, 15, 21, 36, 192, 252, 67, 5, 157, 0, 98, 254, 161, 67, 106, 143, 163, 6, 101, 217, 24, 223, 180, 201, 124, 101, 168, 25, 59, 13, 127, 52, 185, 32, 116, 104, 245, 135, 10, 219, 239, 104, 247, 133, 106, 143, 189, 51, 169, 90, 71, 234, 15, 31, 173, 16, 74, 25, 115, 5, 82, 45, 81, 75, 7, 161, 141, 17, 51, 101, 234, 91, 82, 228, 42, 101, 141, 125, 253, 225, 4, 164, 149, 177, 69, 154, 154, 153, 164, 12, 163, 72, 219, 169, 42, 164, 254, 176, 65, 243, 39, 93, 20, 144, 199, 113, 114, 187, 36, 24, 109, 36, 73, 185, 60, 132, 112, 222, 74, 196, 171, 123, 34, 245, 135, 13, 245, 135, 10, 22, 127, 168, 96, 101, 122, 63, 108, 240, 126, 168, 96, 77, 232, 135, 10, 17, 76, 160, 158, 139, 101, 215, 234, 135, 12, 109, 57, 178, 250, 225, 194, 241, 116, 86, 63, 92, 96, 131, 29, 32, 205, 91, 254, 180, 190, 9, 232, 213, 27, 22, 112, 150, 157, 46, 194, 85, 180, 209, 94, 253, 176, 1, 105, 173, 255, 15, 79, 212, 15, 41, 208, 209, 150, 35, 104, 245, 67, 198, 234, 135, 10, 42, 219, 15, 21, 182, 31, 62, 60, 178, 253, 112, 193, 27, 244, 119, 109, 63, 140, 240, 8, 218, 126, 200, 128, 90, 157, 107, 154, 123, 184, 120, 94, 238, 225, 194, 107, 163, 150, 130, 180, 51, 97, 132, 67, 235, 131, 243, 106, 41, 232, 159, 107, 43, 193, 48, 171, 30, 146, 164, 78, 66, 16, 253, 84, 238, 33, 67, 195, 11, 41, 247, 30, 46, 180, 58, 6, 68, 29, 36, 160, 74, 39, 244, 112, 91, 209, 30, 46, 168, 165, 251, 180, 135, 11, 124, 104, 15, 27, 95, 209, 134, 148, 126, 190, 183, 135, 13, 175, 222, 30, 62, 40, 217, 237, 225, 66, 123, 200, 80, 183, 174, 61, 100, 96, 205, 237, 225, 130, 227, 169, 171, 61, 116, 120, 152, 173, 108, 123, 184, 112, 44, 81, 101, 187, 81, 14, 143, 32, 245, 168, 178, 237, 33, 196, 195, 44, 100, 81, 202, 2, 171, 95, 195, 83, 34, 200, 185, 68, 201, 18, 10, 7, 43, 189, 117, 178, 237, 33, 132, 71, 32, 252, 182, 135, 11, 119, 176, 190, 147, 7, 15, 179, 14, 44, 32, 0, 151, 204, 225, 147, 58, 111, 43, 177, 214, 131, 214, 103, 18, 169, 182, 192, 183, 61, 140, 120, 215, 222, 246, 112, 209, 171, 144, 99, 250, 15, 19, 140, 85, 203, 110, 64, 6, 62, 97, 219, 195, 5, 181, 19, 182, 61, 100, 152, 176, 237, 161, 66, 235, 183, 101, 183, 237, 161, 67, 223, 202, 9, 106, 188, 237, 225, 67, 233, 55, 59, 71, 3, 3, 80, 201, 4, 208, 148, 76, 227, 7, 72, 215, 14, 64, 83, 50, 156, 146, 41, 128, 166, 100, 190, 229, 123, 227, 7, 24, 208, 148, 204, 1, 92, 34, 223, 246, 112, 65, 132, 182, 109, 154, 58, 120, 211, 137, 148, 46, 179, 237, 33, 132, 97, 86, 109, 123, 170, 182, 61, 108, 232, 246, 67, 150, 231, 17, 143, 1, 92, 87, 66, 1, 79, 250, 100, 4, 72, 150, 20, 185, 178, 108, 232, 249, 214, 212, 160, 46, 147, 107, 123, 200, 104, 149, 228, 79, 180, 74, 242, 33, 68, 180, 170, 228, 5, 252, 45, 247, 224, 124, 186, 71, 37, 203, 74, 104, 115, 114, 135, 246, 115, 215, 118, 168, 85, 181, 135, 14, 202, 164, 51, 170, 218, 195, 70, 123, 200, 192, 61, 181, 135, 11, 21, 109, 180, 145, 243, 233, 80, 106, 15, 29, 34, 184, 173, 88, 218, 67, 7, 157, 236, 47, 219, 118, 168, 194, 75, 123, 248, 208, 30, 50, 86, 78, 20, 181, 135, 139, 230, 85, 176, 135, 11, 203, 30, 62, 150, 61, 84, 160, 216, 67, 5, 18, 79, 246, 112, 161, 113, 255, 122, 200, 160, 118, 61, 84, 88, 15, 3, 48, 217, 135, 10, 126, 37, 251, 112, 33, 130, 218, 135, 10, 202, 164, 35, 181, 15, 25, 15, 179, 13, 247, 196, 79, 161, 147, 244, 9, 143, 184, 34, 108, 188, 247, 50, 233, 225, 130, 159, 219, 181, 46, 15, 27, 30, 65, 249, 42, 30, 25, 94, 157, 97, 24, 85, 60, 54, 30, 231, 158, 199, 5, 143, 32, 118, 207, 227, 2, 53, 51, 15, 52, 13, 226, 77, 123, 152, 117, 207, 99, 195, 202, 180, 238, 121, 92, 52, 88, 251, 143, 226, 129, 55, 102, 140, 19, 90, 192, 85, 194, 19, 121, 164, 177, 227, 158, 199, 137, 214, 223, 166, 168, 123, 30, 29, 234, 75, 209, 245, 124, 163, 177, 67, 52, 118, 247, 32, 244, 33, 164, 51, 239, 186, 0, 114, 165, 18, 102, 7, 249, 74, 104, 37, 107, 30, 64, 211, 86, 169, 58, 98, 205, 186, 13, 177, 126, 192, 154, 17, 195, 19, 152, 45, 52, 53, 51, 220, 14, 121, 75, 71, 154, 146, 113, 224, 45, 29, 53, 117, 140, 52, 37, 147, 221, 234, 223, 152, 33, 77, 201, 168, 123, 30, 39, 84, 161, 196, 12, 150, 225, 193, 95, 161, 39, 187, 83, 121, 4, 2, 195, 145, 122, 215, 86, 17, 223, 234, 158, 8, 121, 180, 77, 39, 49, 188, 80, 150, 205, 61, 76, 67, 221, 243, 18, 13, 223, 180, 177, 123, 30, 29, 212, 61, 143, 10, 139, 85, 30, 21, 34, 84, 30, 21, 176, 231, 136, 41, 143, 139, 103, 202, 227, 227, 151, 242, 168, 160, 109, 41, 143, 11, 220, 41, 25, 132, 182, 148, 71, 7, 171, 218, 9, 132, 34, 177, 136, 6, 222, 181, 148, 71, 6, 95, 233, 94, 43, 212, 44, 229, 113, 194, 97, 35, 204, 148, 242, 184, 240, 8, 132, 214, 10, 85, 202, 163, 227, 177, 246, 31, 114, 88, 41, 219, 171, 82, 30, 27, 222, 84, 41, 143, 15, 173, 171, 223, 35, 3, 254, 30, 65, 76, 191, 199, 134, 75, 218, 46, 135, 55, 47, 253, 30, 25, 56, 238, 200, 177, 180, 181, 247, 15, 237, 123, 132, 120, 254, 177, 241, 252, 163, 130, 8, 13, 134, 97, 10, 227, 89, 35, 48, 255, 184, 208, 166, 109, 63, 254, 200, 72, 175, 200, 31, 23, 238, 249, 163, 130, 235, 74, 251, 157, 206, 31, 31, 201, 254, 168, 176, 152, 63, 42, 52, 188, 224, 223, 217, 216, 83, 106, 67, 26, 210, 171, 131, 193, 210, 150, 222, 164, 112, 64, 134, 101, 151, 63, 46, 232, 146, 164, 245, 199, 69, 69, 249, 163, 130, 242, 71, 5, 238, 124, 186, 228, 143, 12, 7, 220, 249, 116, 200, 193, 159, 226, 186, 150, 65, 201, 31, 27, 201, 31, 21, 40, 75, 127, 92, 104, 236, 17, 8, 249, 169, 32, 248, 35, 164, 185, 130, 252, 145, 193, 191, 217, 85, 20, 41, 245, 215, 30, 242, 199, 8, 117, 207, 47, 245, 199, 7, 242, 199, 136, 198, 141, 161, 238, 121, 135, 247, 68, 234, 143, 13, 254, 8, 209, 2, 109, 153, 164, 74, 5, 50, 84, 42, 48, 0, 170, 229, 251, 171, 192, 69, 107, 5, 54, 90, 85, 211, 174, 192, 5, 99, 174, 64, 135, 71, 32, 104, 227, 10, 92, 84, 224, 194, 35, 203, 174, 10, 100, 72, 237, 241, 198, 195, 236, 178, 171, 2, 31, 150, 93, 21, 168, 224, 254, 29, 180, 128, 189, 126, 111, 251, 128, 175, 132, 36, 218, 74, 220, 86, 32, 131, 171, 226, 228, 182, 2, 25, 110, 43, 208, 209, 171, 144, 219, 10, 92, 76, 96, 220, 90, 129, 12, 222, 180, 229, 214, 10, 116, 224, 119, 90, 129, 11, 90, 129, 140, 181, 184, 147, 55, 173, 192, 134, 227, 207, 161, 166, 21, 200, 240, 71, 77, 43, 208, 193, 149, 101, 5, 46, 168, 111, 179, 178, 2, 27, 222, 119, 101, 235, 247, 96, 195, 189, 7, 21, 120, 36, 177, 218, 247, 32, 99, 223, 131, 10, 84, 115, 242, 85, 40, 66, 155, 19, 90, 234, 159, 127, 176, 209, 238, 104, 125, 114, 57, 132, 14, 78, 122, 252, 224, 194, 147, 206, 15, 46, 240, 131, 1, 104, 15, 6, 224, 65, 199, 235, 245, 160, 130, 71, 30, 84, 208, 158, 132, 171, 165, 251, 32, 132, 155, 235, 131, 11, 239, 250, 15, 136, 230, 79, 172, 80, 75, 198, 250, 96, 195, 123, 234, 131, 11, 157, 250, 160, 66, 91, 230, 158, 30, 92, 172, 138, 124, 80, 97, 85, 188, 124, 112, 65, 194, 236, 180, 206, 7, 25, 204, 53, 212, 252, 208, 207, 180, 203, 61, 165, 10, 162, 117, 62, 248, 160, 61, 7, 136, 47, 252, 203, 198, 77, 231, 131, 141, 197, 236, 242, 65, 6, 143, 160, 202, 99, 249, 32, 163, 85, 37, 63, 190, 237, 106, 120, 100, 209, 152, 132, 22, 203, 135, 241, 251, 75, 136, 129, 126, 79, 180, 88, 62, 248, 192, 242, 65, 5, 15, 46, 210, 202, 7, 21, 158, 36, 31, 84, 216, 215, 124, 112, 161, 241, 230, 131, 143, 54, 1, 8, 30, 84, 64, 15, 62, 252, 193, 199, 162, 176, 225, 17, 199, 211, 169, 84, 144, 177, 48, 147, 10, 42, 32, 164, 130, 13, 137, 95, 6, 177, 214, 175, 224, 3, 107, 253, 10, 46, 184, 123, 5, 31, 238, 21, 84, 224, 17, 8, 206, 167, 67, 206, 167, 171, 84, 112, 33, 185, 94, 172, 130, 12, 203, 29, 185, 127, 103, 177, 10, 74, 104, 59, 114, 176, 196, 106, 177, 10, 50, 44, 86, 193, 134, 119, 58, 105, 177, 10, 50, 22, 171, 68, 88, 172, 194, 82, 251, 181, 185, 130, 42, 184, 192, 85, 113, 163, 153, 85, 80, 194, 35, 104, 177, 10, 50, 176, 10, 54, 240, 187, 167, 19, 90, 172, 130, 142, 183, 160, 197, 42, 8, 177, 88, 5, 21, 248, 170, 60, 199, 14, 85, 124, 85, 176, 161, 226, 171, 130, 10, 30, 241, 85, 193, 5, 134, 217, 85, 193, 133, 8, 237, 84, 80, 161, 157, 10, 62, 36, 137, 25, 106, 167, 130, 140, 118, 42, 168, 64, 21, 55, 196, 173, 21, 108, 44, 85, 65, 5, 189, 202, 241, 236, 71, 141, 170, 32, 164, 87, 169, 231, 130, 26, 85, 193, 135, 126, 170, 74, 165, 130, 11, 30, 137, 80, 65, 133, 9, 36, 136, 80, 65, 133, 231, 118, 200, 189, 130, 42, 157, 28, 61, 217, 168, 130, 13, 141, 247, 83, 123, 76, 131, 47, 170, 160, 195, 35, 13, 91, 84, 193, 6, 93, 75, 254, 42, 69, 21, 116, 60, 21, 21, 143, 130, 11, 19, 64, 204, 83, 112, 193, 41, 24, 128, 197, 242, 89, 44, 31, 72, 35, 113, 232, 161, 177, 71, 28, 107, 25, 244, 64, 127, 155, 130, 11, 78, 122, 109, 10, 50, 56, 169, 105, 83, 144, 65, 215, 190, 203, 166, 32, 131, 129, 194, 197, 226, 160, 39, 101, 163, 107, 0, 217, 20, 108, 180, 174, 166, 101, 83, 144, 177, 46, 29, 133, 130, 12, 201, 53, 106, 117, 52, 176, 78, 6, 181, 82, 88, 181, 117, 160, 56, 156, 165, 74, 52, 109, 255, 177, 186, 77, 250, 224, 12, 154, 182, 78, 202, 164, 237, 156, 3, 9, 109, 134, 52, 181, 228, 2, 235, 42, 170, 214, 190, 6, 7, 239, 153, 84, 114, 63, 144, 218, 227, 72, 181, 199, 221, 72, 82, 46, 168, 194, 139, 164, 153, 130, 13, 60, 226, 178, 41, 46, 155, 162, 154, 41, 200, 112, 217, 148, 134, 106, 166, 160, 35, 37, 141, 41, 184, 32, 129, 9, 60, 204, 98, 10, 82, 44, 236, 53, 10, 46, 84, 36, 20, 84, 192, 40, 199, 191, 216, 59, 18, 74, 83, 199, 14, 206, 47, 5, 27, 36, 212, 73, 153, 212, 45, 5, 31, 34, 26, 94, 254, 248, 55, 59, 1, 172, 205, 254, 86, 151, 110, 81, 220, 52, 102, 15, 188, 21, 14, 142, 231, 59, 132, 100, 189, 5, 61, 171, 105, 120, 77, 164, 125, 207, 165, 11, 116, 45, 131, 208, 179, 54, 213, 3, 39, 125, 82, 212, 227, 149, 144, 68, 227, 247, 7, 95, 204, 143, 212, 196, 83, 225, 118, 200, 217, 61, 88, 201, 16, 188, 16, 133, 85, 91, 10, 50, 104, 233, 164, 182, 20, 92, 244, 127, 182, 165, 224, 66, 61, 117, 105, 163, 182, 20, 124, 72, 154, 25, 212, 150, 130, 12, 181, 150, 130, 14, 181, 150, 130, 15, 102, 41, 168, 64, 155, 90, 10, 62, 190, 181, 45, 106, 210, 82, 208, 209, 180, 109, 237, 53, 90, 244, 56, 20, 156, 120, 178, 59, 35, 188, 189, 70, 13, 204, 49, 116, 61, 197, 12, 162, 238, 121, 212, 246, 215, 62, 9, 165, 21, 94, 208, 2, 39, 173, 67, 193, 133, 87, 135, 130, 143, 71, 165, 54, 98, 172, 80, 83, 135, 130, 14, 77, 29, 10, 42, 44, 85, 135, 130, 11, 104, 129, 213, 207, 161, 238, 121, 101, 203, 156, 244, 202, 64, 26, 202, 167, 67, 193, 7, 231, 27, 210, 161, 160, 35, 130, 9, 120, 67, 193, 5, 165, 19, 110, 199, 22, 189, 54, 20, 108, 120, 109, 40, 168, 128, 13, 71, 107, 181, 85, 10, 50, 124, 231, 187, 68, 149, 130, 13, 137, 230, 135, 89, 73, 74, 202, 162, 20, 116, 172, 9, 190, 40, 5, 25, 36, 64, 240, 80, 202, 178, 25, 4, 129, 141, 244, 173, 77, 107, 231, 226, 120, 248, 138, 182, 7, 232, 233, 215, 22, 128, 196, 87, 177, 108, 212, 252, 153, 252, 49, 11, 175, 108, 173, 175, 77, 43, 115, 60, 29, 36, 129, 98, 16, 13, 175, 199, 194, 171, 65, 223, 236, 216, 46, 240, 236, 176, 227, 159, 172, 185, 129, 75, 218, 115, 18, 114, 252, 45, 6, 73, 82, 186, 237, 111, 138, 158, 213, 184, 178, 204, 3, 18, 19, 101, 128, 156, 234, 131, 136, 207, 212, 120, 193, 62, 218, 166, 147, 26, 90, 151, 106, 90, 177, 222, 64, 171, 90, 219, 100, 243, 230, 45, 239, 202, 108, 75, 134, 252, 213, 189, 129, 59, 224, 194, 27, 175, 47, 181, 139, 82, 80, 98, 53, 139, 82, 36, 190, 49, 179, 40, 5, 27, 22, 165, 224, 2, 222, 71, 196, 162, 20, 92, 96, 146, 106, 81, 10, 50, 104, 115, 66, 252, 238, 233, 180, 40, 101, 81, 10, 46, 180, 109, 211, 36, 44, 74, 65, 10, 239, 69, 41, 72, 225, 154, 210, 45, 74, 65, 135, 175, 44, 22, 165, 32, 100, 81, 10, 50, 240, 8, 242, 198, 187, 40, 5, 35, 42, 234, 82, 41, 144, 100, 146, 84, 10, 66, 188, 94, 72, 146, 74, 65, 135, 212, 158, 198, 0, 210, 73, 204, 34, 18, 154, 214, 41, 3, 7, 90, 63, 98, 162, 1, 175, 162, 32, 133, 162, 96, 0, 222, 179, 201, 68, 193, 6, 255, 182, 236, 158, 164, 96, 163, 105, 249, 36, 133, 233, 231, 58, 73, 10, 54, 168, 199, 188, 116, 162, 10, 53, 246, 79, 10, 50, 240, 181, 255, 30, 146, 169, 147, 28, 34, 183, 245, 183, 19, 146, 208, 182, 171, 245, 147, 130, 141, 8, 19, 146, 148, 174, 241, 48, 235, 164, 79, 10, 46, 160, 131, 118, 82, 80, 161, 105, 73, 65, 135, 171, 100, 37, 5, 25, 122, 149, 100, 37, 5, 23, 206, 167, 67, 238, 65, 36, 41, 23, 36, 89, 73, 193, 134, 100, 37, 5, 21, 26, 163, 223, 164, 224, 98, 177, 10, 74, 18, 40, 24, 161, 44, 219, 163, 18, 148, 36, 80, 240, 177, 240, 90, 80, 146, 64, 65, 70, 99, 143, 32, 10, 50, 84, 42, 19, 84, 224, 21, 125, 19, 84, 209, 55, 153, 112, 181, 116, 25, 228, 146, 61, 184, 231, 74, 168, 162, 111, 130, 14, 7, 92, 100, 235, 155, 224, 66, 3, 238, 218, 78, 223, 4, 25, 250, 38, 216, 80, 7, 143, 56, 233, 113, 4, 3, 119, 201, 32, 28, 18, 65, 146, 214, 233, 167, 66, 17, 52, 37, 138, 160, 111, 130, 15, 173, 43, 223, 247, 78, 146, 255, 162, 31, 255, 42, 125, 79, 254, 199, 227, 220, 190, 3, 145, 0, 130, 8, 68, 112, 109, 160, 23, 44, 253, 8, 105, 155, 137, 67, 107, 235, 210, 113, 180, 114, 19, 75, 60, 42, 249, 102, 167, 181, 85, 53, 109, 254, 181, 184, 147, 35, 125, 147, 201, 126, 210, 94, 69, 241, 248, 54, 243, 109, 55, 225, 34, 160, 198, 187, 191, 94, 190, 50, 164, 47, 45, 122, 186, 177, 39, 115, 60, 21, 47, 71, 171, 199, 65, 79, 69, 174, 198, 255, 111, 65, 30, 161, 88, 169, 152, 33, 143, 52, 19, 215, 223, 182, 76, 91, 138, 79, 217, 95, 81, 212, 38, 0, 193, 182, 9, 64, 176, 140, 38, 191, 209, 213, 48, 55, 13, 116, 45, 249, 72, 49, 55, 218, 180, 178, 149, 15, 190, 41, 107, 178, 25, 214, 204, 60, 154, 54, 137, 213, 50, 85, 250, 169, 60, 2, 193, 143, 190, 61, 214, 149, 48, 59, 154, 23, 55, 20, 51, 85, 172, 56, 130, 195, 179, 26, 163, 135, 39, 41, 41, 73, 187, 82, 68, 2, 30, 102, 95, 223, 132, 129, 54, 210, 245, 32, 192, 193, 174, 5, 30, 249, 78, 231, 241, 38, 8, 225, 127, 15, 74, 255, 57, 198, 9, 165, 246, 184, 4, 175, 197, 206, 225, 105, 255, 89, 217, 18, 233, 155, 96, 3, 179, 56, 244, 19, 100, 208, 55, 217, 126, 14, 2, 232, 39, 251, 138, 254, 67, 243, 156, 175, 230, 248, 138, 126, 1, 22, 63, 240, 77, 119, 165, 5, 201, 117, 99, 143, 56, 48, 192, 222, 97, 224, 233, 87, 166, 75, 31, 185, 39, 164, 138, 147, 79, 112, 1, 77, 62, 65, 5, 215, 245, 186, 39, 184, 192, 41, 60, 65, 5, 125, 250, 38, 32, 10, 79, 176, 225, 12, 162, 240, 4, 25, 86, 186, 230, 9, 50, 168, 229, 137, 145, 168, 100, 171, 73, 227, 131, 196, 233, 68, 30, 6, 57, 144, 67, 202, 49, 165, 142, 202, 6, 147, 17, 40, 210, 3, 97, 44, 20, 137, 2, 73, 21, 133, 15, 20, 0, 3, 14, 64, 173, 25, 205, 76, 19, 164, 76, 169, 170, 13, 64, 16, 0, 4, 109, 75, 111, 237, 120, 170, 176, 201, 1, 78, 65, 242, 207, 98, 231, 248, 155, 37, 98, 142, 118, 116, 122, 6, 69, 240, 26, 157, 196, 61, 207, 229, 171, 211, 118, 68, 198, 142, 40, 103, 168, 102, 48, 188, 43, 26, 45, 249, 235, 140, 168, 165, 124, 54, 255, 142, 34, 25, 28, 15, 15, 7, 51, 179, 7, 109, 171, 165, 175, 67, 121, 9, 25, 190, 128, 198, 47, 71, 166, 61, 171, 12, 194, 125, 81, 62, 133, 61, 176, 25, 172, 66, 207, 46, 240, 250, 136, 21, 95, 93, 21, 109, 2, 45, 170, 136, 127, 78, 167, 63, 71, 118, 198, 110, 67, 167, 243, 24, 31, 119, 14, 61, 110, 109, 68, 17, 154, 243, 229, 149, 199, 135, 120, 58, 46, 81, 50, 71, 174, 122, 85, 31, 36, 158, 218, 166, 121, 26, 120, 230, 52, 172, 117, 155, 193, 237, 43, 141, 84, 148, 247, 191, 123, 49, 197, 218, 15, 73, 29, 124, 250, 198, 47, 7, 129, 50, 172, 194, 193, 55, 97, 35, 146, 73, 103, 73, 25, 137, 248, 242, 119, 10, 222, 11, 114, 8, 161, 29, 217, 2, 31, 195, 156, 139, 31, 144, 19, 49, 77, 243, 250, 250, 158, 29, 91, 131, 133, 11, 173, 87, 119, 15, 209, 216, 4, 111, 201, 119, 204, 170, 12, 229, 220, 185, 218, 97, 208, 149, 132, 246, 107, 162, 130, 91, 222, 97, 123, 201, 73, 22, 157, 3, 139, 213, 122, 136, 16, 211, 171, 145, 253, 126, 175, 213, 63, 36, 80, 40, 74, 46, 229, 152, 185, 30, 29, 213, 127, 236, 167, 160, 71, 141, 178, 173, 45, 2, 4, 65, 100, 96, 16, 109, 18, 223, 179, 37, 88, 62, 97, 198, 128, 89, 31, 28, 171, 155, 67, 234, 24, 166, 229, 210, 1, 36, 125, 123, 33, 32, 159, 235, 21, 154, 114, 49, 161, 209, 38, 248, 131, 160, 239, 52, 206, 165, 78, 153, 191, 179, 16, 42, 160, 128, 112, 38, 251, 192, 90, 206, 130, 88, 119, 70, 252, 18, 11, 179, 240, 5, 100, 198, 75, 115, 52, 66, 150, 128, 138, 172, 98, 221, 8, 158, 17, 246, 14, 141, 120, 235, 229, 96, 229, 38, 166, 254, 35, 123, 81, 186, 64, 22, 66, 16, 101, 234, 75, 141, 194, 154, 204, 160, 33, 197, 1, 164, 244, 196, 56, 52, 1, 230, 17, 165, 50, 203, 106, 231, 107, 255, 71, 126, 255, 83, 70, 32, 14, 208, 139, 229, 226, 138, 222, 163, 27, 134, 240, 219, 246, 188, 154, 62, 151, 167, 106, 0, 111, 96, 182, 220, 50, 223, 221, 34, 135, 240, 83, 156, 114, 29, 53, 107, 46, 148, 169, 111, 250, 153, 57, 135, 197, 216, 102, 163, 127, 53, 245, 203, 58, 142, 97, 151, 166, 77, 26, 90, 165, 113, 133, 3, 211, 169, 168, 233, 135, 37, 226, 23, 139, 175, 179, 198, 136, 20, 22, 109, 224, 185, 222, 51, 2, 77, 1, 21, 91, 58, 141, 1, 254, 39, 222, 220, 161, 150, 232, 214, 206, 133, 116, 175, 101, 131, 104, 229, 86, 114, 173, 94, 26, 84, 77, 121, 235, 211, 159, 120, 165, 227, 125, 45, 11, 6, 120, 215, 203, 97, 132, 167, 107, 114, 2, 63, 167, 80, 100, 118, 8, 168, 20, 10, 88, 130, 247, 36, 104, 167, 86, 49, 242, 147, 141, 55, 121, 206, 224, 73, 188, 54, 218, 224, 30, 234, 52, 66, 117, 125, 196, 8, 206, 95, 111, 5, 137, 155, 199, 14, 204, 128, 209, 175, 40, 9, 170, 39, 57, 63, 34, 25, 41, 144, 20, 52, 191, 143, 140, 47, 200, 15, 254, 180, 209, 74, 88, 18, 237, 88, 104, 188, 136, 146, 188, 32, 113, 74, 111, 82, 169, 247, 76, 73, 222, 208, 129, 181, 51, 131, 210, 3, 142, 9, 254, 221, 46, 51, 186, 85, 24, 193, 137, 43, 2, 177, 133, 141, 182, 235, 30, 173, 240, 78, 104, 184, 120, 239, 75, 106, 178, 243, 105, 169, 57, 11, 252, 172, 139, 247, 36, 4, 3, 60, 142, 188, 214, 138, 149, 137, 212, 43, 30, 208, 251, 181, 41, 81, 153, 107, 139, 70, 60, 202, 192, 178, 31, 69, 153, 26, 178, 170, 5, 27, 159, 164, 135, 6, 75, 231, 231, 210, 179, 22, 92, 107, 192, 110, 86, 174, 53, 125, 52, 210, 243, 11, 243, 206, 53, 185, 58, 34, 50, 117, 222, 30, 26, 97, 122, 209, 129, 141, 246, 76, 101, 208, 79, 111, 243, 217, 105, 178, 227, 88, 219, 40, 54, 211, 46, 12, 238, 22, 201, 176, 24, 246, 117, 145, 177, 7, 158, 173, 6, 203, 30, 67, 205, 203, 18, 44, 171, 177, 238, 84, 167, 123, 168, 52, 65, 199, 170, 77, 117, 50, 201, 0, 42, 105, 27, 193, 91, 60, 152, 238, 83, 218, 246, 151, 33, 138, 16, 172, 65, 228, 30, 38, 200, 137, 214, 31, 140, 12, 227, 197, 164, 80, 251, 215, 153, 59, 87, 118, 248, 74, 117, 152, 44, 112, 169, 155, 66, 63, 219, 82, 190, 113, 213, 217, 15, 80, 28, 133, 75, 234, 156, 56, 109, 240, 28, 229, 19, 244, 25, 106, 11, 28, 210, 191, 115, 178, 226, 128, 7, 130, 213, 90, 188, 33, 63, 184, 31, 99, 153, 36, 128, 28, 123, 36, 197, 40, 166, 51, 83, 234, 195, 42, 92, 172, 162, 110, 231, 8, 171, 250, 65, 99, 198, 137, 235, 224, 73, 81, 207, 4, 250, 204, 78, 72, 102, 82, 141, 85, 202, 92, 226, 36, 230, 22, 137, 67, 234, 142, 136, 96, 14, 83, 162, 53, 9, 7, 165, 66, 241, 199, 74, 141, 188, 63, 253, 107, 148, 2, 248, 65, 138, 123, 227, 220, 48, 249, 213, 32, 2, 252, 42, 190, 107, 99, 205, 110, 239, 3, 119, 218, 246, 52, 167, 46, 50, 232, 66, 164, 80, 68, 64, 194, 41, 8, 0, 25, 216, 162, 206, 222, 137, 107, 149, 114, 205, 198, 16, 67, 146, 212, 141, 181, 149, 247, 252, 14, 66, 173, 223, 125, 254, 126, 152, 34, 75, 107, 91, 204, 195, 185, 206, 199, 232, 6, 226, 128, 104, 142, 209, 160, 21, 238, 81, 44, 113, 3, 230, 250, 236, 161, 142, 124, 40, 106, 132, 206, 233, 89, 99, 100, 226, 246, 177, 31, 127, 158, 221, 200, 67, 35, 143, 140, 9, 22, 99, 102, 143, 127, 120, 255, 225, 234, 103, 88, 205, 128, 128, 156, 166, 127, 60, 20, 180, 231, 246, 237, 66, 13, 97, 121, 166, 188, 236, 4, 62, 227, 174, 89, 64, 34, 176, 95, 159, 49, 41, 137, 69, 199, 48, 188, 93, 219, 103, 140, 33, 11, 199, 65, 241, 100, 133, 160, 3, 18, 26, 119, 136, 229, 38, 129, 136, 121, 34, 168, 144, 85, 241, 96, 72, 29, 93, 16, 4, 136, 97, 53, 59, 183, 36, 44, 141, 140, 136, 131, 91, 74, 19, 4, 207, 189, 0, 227, 93, 81, 151, 91, 67, 216, 122, 118, 153, 22, 246, 242, 231, 69, 102, 95, 173, 97, 72, 66, 136, 12, 217, 206, 79, 140, 24, 11, 79, 251, 209, 203, 35, 198, 121, 231, 229, 6, 214, 88, 132, 224, 126, 255, 176, 129, 36, 204, 94, 130, 235, 86, 226, 14, 197, 149, 44, 136, 165, 139, 195, 229, 180, 195, 241, 164, 112, 219, 37, 62, 195, 245, 100, 77, 180, 229, 144, 60, 222, 135, 160, 63, 106, 78, 34, 17, 216, 152, 173, 81, 27, 1, 44, 232, 36, 81, 104, 58, 47, 32, 100, 164, 211, 188, 125, 238, 219, 179, 131, 97, 199, 137, 25, 205, 162, 200, 145, 197, 38, 220, 24, 102, 152, 150, 158, 43, 234, 238, 66, 49, 90, 131, 174, 97, 219, 26, 117, 38, 36, 82, 247, 130, 156, 205, 161, 248, 80, 56, 181, 70, 137, 146, 17, 103, 30, 168, 35, 99, 3, 7, 215, 81, 169, 118, 55, 162, 131, 132, 124, 156, 193, 241, 85, 147, 187, 47, 54, 51, 125, 20, 241, 20, 2, 41, 229, 62, 1, 73, 240, 126, 190, 178, 71, 69, 141, 94, 180, 131, 67, 102, 8, 233, 80, 73, 0, 137, 161, 142, 175, 0, 181, 86, 206, 144, 205, 103, 78, 53, 100, 139, 158, 99, 187, 111, 238, 109, 201, 241, 130, 246, 174, 163, 51, 123, 115, 225, 33, 255, 12, 41, 84, 69, 209, 109, 151, 96, 244, 151, 3, 201, 255, 96, 125, 221, 123, 239, 163, 182, 138, 138, 211, 200, 82, 228, 110, 239, 131, 44, 191, 123, 159, 156, 94, 133, 218, 142, 212, 20, 204, 53, 39, 71, 138, 197, 147, 130, 27, 240, 124, 116, 113, 175, 215, 58, 106, 133, 224, 9, 122, 175, 33, 23, 20, 105, 244, 166, 1, 167, 118, 19, 156, 50, 192, 195, 128, 104, 11, 14, 245, 88, 3, 174, 184, 169, 6, 74, 156, 128, 16, 68, 121, 198, 229, 50, 42, 176, 176, 144, 44, 60, 112, 243, 55, 105, 196, 184, 102, 91, 98, 202, 57, 77, 55, 3, 38, 102, 130, 51, 111, 14, 71, 16, 118, 208, 180, 36, 59, 37, 237, 59, 13, 253, 193, 65, 175, 92, 79, 64, 11, 31, 18, 22, 69, 86, 156, 188, 165, 63, 50, 105, 49, 18, 243, 114, 12, 222, 78, 3, 228, 175, 206, 231, 72, 59, 227, 42, 144, 198, 225, 19, 67, 27, 120, 192, 164, 5, 65, 42, 224, 33, 69, 218, 64, 176, 6, 217, 104, 169, 68, 229, 218, 167, 153, 63, 26, 41, 17, 202, 243, 12, 158, 44, 126, 84, 25, 123, 196, 72, 197, 135, 197, 101, 122, 191, 150, 226, 170, 135, 18, 107, 234, 63, 237, 94, 236, 227, 111, 120, 82, 193, 182, 52, 206, 104, 46, 234, 81, 23, 88, 116, 248, 108, 48, 1, 48, 224, 78, 162, 208, 197, 208, 223, 130, 202, 206, 60, 16, 56, 168, 240, 8, 236, 142, 216, 107, 200, 76, 111, 113, 208, 32, 13, 125, 137, 223, 163, 172, 14, 38, 32, 22, 177, 74, 204, 99, 133, 115, 241, 136, 232, 253, 159, 254, 10, 251, 114, 134, 179, 0, 161, 120, 211, 89, 35, 171, 183, 203, 244, 93, 1, 68, 226, 185, 162, 79, 90, 158, 189, 50, 138, 140, 213, 157, 193, 44, 153, 98, 228, 165, 31, 125, 231, 218, 227, 247, 27, 144, 177, 87, 202, 76, 177, 177, 53, 109, 142, 205, 233, 240, 33, 1, 99, 37, 93, 6, 131, 91, 90, 96, 193, 79, 93, 148, 73, 192, 197, 20, 52, 223, 108, 236, 179, 19, 132, 66, 186, 56, 102, 179, 253, 89, 73, 2, 235, 83, 198, 40, 96, 44, 102, 146, 46, 169, 83, 199, 47, 246, 150, 46, 109, 108, 22, 254, 126, 150, 9, 178, 41, 89, 64, 250, 213, 59, 79, 1, 244, 241, 234, 115, 98, 254, 65, 255, 57, 170, 52, 107, 66, 220, 186, 201, 89, 168, 10, 172, 179, 35, 64, 249, 77, 10, 247, 2, 18, 172, 205, 249, 222, 139, 12, 100, 170, 19, 227, 128, 16, 6, 114, 119, 238, 186, 239, 137, 248, 167, 92, 234, 55, 185, 107, 53, 153, 93, 155, 192, 110, 179, 205, 8, 153, 194, 237, 243, 203, 164, 98, 16, 217, 19, 56, 31, 248, 92, 68, 233, 200, 21, 182, 81, 250, 9, 20, 50, 144, 187, 251, 15, 112, 203, 42, 187, 23, 72, 60, 64, 191, 61, 86, 64, 158, 58, 196, 8, 154, 174, 30, 134, 63, 47, 75, 34, 37, 64, 70, 28, 71, 45, 126, 101, 5, 53, 224, 55, 131, 48, 193, 101, 47, 134, 186, 42, 33, 139, 111, 155, 224, 118, 149, 39, 113, 5, 115, 233, 10, 234, 171, 166, 229, 119, 156, 46, 142, 251, 136, 104, 141, 89, 204, 70, 186, 163, 201, 194, 100, 91, 202, 104, 186, 138, 59, 146, 178, 205, 30, 65, 180, 84, 252, 209, 3, 104, 240, 87, 70, 186, 159, 42, 177, 146, 202, 224, 12, 212, 201, 213, 153, 210, 57, 210, 248, 98, 254, 29, 32, 97, 185, 161, 108, 163, 129, 30, 7, 194, 181, 208, 198, 166, 212, 199, 224, 41, 38, 39, 135, 165, 0, 170, 159, 11, 89, 254, 21, 62, 205, 234, 106, 0, 34, 211, 99, 251, 21, 19, 96, 2, 87, 230, 112, 58, 22, 118, 90, 88, 169, 121, 193, 135, 88, 231, 199, 126, 92, 130, 166, 96, 54, 94, 15, 242, 32, 154, 89, 104, 68, 205, 96, 62, 26, 208, 50, 54, 157, 233, 33, 81, 119, 9, 160, 106, 227, 212, 41, 212, 67, 184, 103, 48, 192, 238, 84, 165, 216, 147, 210, 115, 111, 204, 27, 228, 166, 65, 203, 22, 59, 95, 190, 99, 11, 75, 78, 247, 225, 101, 151, 132, 170, 49, 180, 158, 246, 37, 201, 39, 123, 135, 220, 27, 11, 167, 112, 80, 230, 106, 222, 6, 247, 225, 31, 194, 71, 200, 131, 231, 45, 90, 156, 154, 232, 167, 170, 177, 237, 227, 119, 28, 55, 228, 236, 238, 39, 217, 50, 119, 196, 118, 58, 251, 215, 242, 158, 99, 185, 144, 92, 100, 54, 70, 46, 55, 75, 34, 7, 229, 14, 230, 142, 197, 187, 70, 205, 184, 84, 165, 174, 106, 88, 14, 34, 61, 31, 182, 23, 253, 12, 162, 15, 75, 97, 250, 12, 121, 46, 20, 228, 134, 253, 99, 234, 77, 215, 60, 79, 136, 31, 237, 138, 29, 140, 41, 23, 161, 248, 114, 31, 204, 57, 68, 251, 57, 129, 254, 238, 80, 121, 3, 36, 131, 95, 224, 238, 82, 171, 251, 120, 36, 54, 132, 51, 252, 54, 51, 224, 92, 147, 71, 59, 163, 0, 220, 235, 240, 63, 9, 64, 53, 216, 243, 45, 197, 172, 8, 128, 90, 142, 111, 253, 211, 246, 132, 135, 69, 238, 108, 176, 0, 27, 170, 29, 128, 185, 79, 0, 28, 20, 203, 187, 210, 42, 103, 117, 37, 146, 173, 238, 50, 71, 89, 168, 56, 153, 195, 240, 73, 243, 54, 132, 77, 86, 4, 67, 144, 18, 43, 22, 201, 42, 47, 112, 148, 26, 36, 248, 106, 102, 149, 109, 22, 239, 49, 125, 40, 207, 63, 170, 109, 250, 137, 124, 138, 162, 59, 181, 59, 104, 92, 84, 204, 41, 243, 11, 238, 140, 251, 124, 96, 34, 103, 205, 15, 103, 28, 31, 135, 135, 169, 109, 156, 15, 146, 88, 56, 32, 145, 202, 98, 26, 220, 183, 103, 206, 155, 153, 12, 229, 242, 159, 2, 27, 76, 236, 62, 111, 155, 67, 193, 137, 153, 4, 113, 139, 114, 34, 3, 174, 131, 14, 248, 85, 206, 93, 53, 27, 60, 145, 134, 206, 139, 234, 95, 197, 100, 141, 251, 244, 126, 224, 138, 66, 147, 145, 126, 144, 176, 161, 253, 52, 211, 112, 130, 203, 246, 230, 28, 196, 111, 224, 187, 29, 169, 176, 54, 64, 148, 77, 105, 80, 11, 172, 27, 133, 66, 74, 239, 14, 248, 40, 15, 40, 51, 180, 174, 93, 198, 216, 116, 169, 66, 249, 86, 118, 41, 141, 203, 30, 46, 196, 104, 50, 116, 140, 148, 22, 236, 95, 123, 212, 133, 132, 226, 136, 121, 83, 160, 218, 8, 155, 241, 141, 216, 16, 103, 212, 72, 246, 184, 233, 174, 42, 10, 148, 249, 214, 70, 76, 108, 133, 117, 170, 234, 183, 183, 58, 174, 67, 204, 100, 227, 57, 44, 236, 152, 74, 15, 225, 72, 92, 196, 93, 40, 1, 68, 53, 254, 128, 78, 178, 93, 193, 234, 133, 86, 160, 31, 139, 64, 123, 190, 94, 76, 221, 66, 83, 115, 217, 144, 195, 72, 109, 213, 158, 3, 33, 247, 113, 166, 29, 144, 125, 218, 141, 165, 149, 54, 227, 59, 230, 115, 153, 148, 226, 141, 61, 180, 247, 171, 103, 219, 88, 37, 1, 74, 64, 37, 55, 162, 27, 236, 34, 45, 24, 136, 71, 228, 211, 55, 5, 134, 137, 162, 166, 175, 75, 13, 230, 120, 212, 54, 133, 213, 13, 212, 4, 75, 141, 17, 248, 142, 201, 250, 5, 221, 163, 237, 12, 161, 28, 238, 54, 158, 171, 228, 241, 92, 6, 0, 81, 232, 157, 213, 166, 240, 149, 3, 113, 11, 68, 217, 104, 163, 44, 113, 158, 213, 129, 142, 169, 83, 128, 174, 159, 118, 192, 38, 166, 163, 1, 198, 235, 155, 12, 16, 156, 12, 21, 244, 216, 229, 30, 161, 210, 22, 35, 22, 117, 126, 237, 94, 7, 217, 143, 145, 188, 52, 104, 98, 103, 53, 225, 105, 49, 246, 72, 3, 53, 240, 210, 195, 27, 186, 43, 247, 24, 204, 81, 229, 242, 32, 97, 84, 239, 144, 247, 4, 84, 158, 212, 187, 14, 18, 4, 12, 88, 248, 37, 10, 225, 196, 28, 170, 41, 138, 32, 149, 30, 134, 240, 43, 63, 144, 55, 220, 192, 49, 179, 137, 88, 163, 118, 187, 115, 167, 230, 99, 71, 225, 136, 190, 250, 55, 29, 186, 209, 106, 249, 128, 137, 153, 173, 182, 206, 68, 108, 38, 17, 165, 211, 69, 183, 74, 75, 43, 189, 59, 81, 189, 108, 108, 161, 183, 4, 24, 213, 237, 145, 162, 209, 80, 110, 35, 125, 172, 242, 209, 135, 3, 47, 212, 183, 137, 131, 95, 148, 95, 62, 211, 138, 17, 206, 113, 108, 150, 15, 132, 147, 227, 65, 39, 108, 224, 222, 148, 167, 68, 47, 10, 35, 104, 77, 79, 57, 240, 199, 184, 236, 76, 184, 130, 167, 150, 221, 17, 116, 132, 135, 128, 90, 63, 149, 158, 32, 27, 228, 82, 235, 61, 29, 137, 135, 245, 134, 183, 172, 78, 150, 234, 243, 196, 68, 162, 80, 79, 63, 108, 251, 114, 40, 250, 180, 84, 215, 162, 255, 38, 126, 85, 181, 5, 122, 4, 46, 93, 4, 68, 237, 84, 160, 191, 255, 46, 160, 103, 118, 242, 126, 40, 214, 157, 129, 245, 101, 13, 236, 36, 243, 11, 0, 91, 229, 253, 187, 217, 117, 133, 3, 22, 84, 25, 68, 237, 53, 127, 36, 59, 0, 69, 6, 33, 119, 202, 100, 131, 170, 240, 138, 178, 10, 104, 63, 129, 237, 145, 53, 80, 160, 152, 191, 62, 87, 0, 46, 14, 44, 15, 181, 64, 114, 89, 156, 238, 183, 153, 199, 9, 4, 31, 41, 91, 240, 109, 188, 209, 54, 117, 214, 86, 210, 150, 231, 193, 192, 70, 103, 109, 220, 148, 144, 50, 100, 184, 197, 35, 4, 22, 39, 246, 65, 168, 134, 144, 151, 125, 65, 42, 9, 167, 46, 35, 220, 208, 188, 211, 115, 84, 28, 221, 254, 21, 54, 67, 63, 235, 214, 42, 108, 118, 91, 172, 249, 252, 172, 208, 154, 238, 152, 19, 3, 16, 227, 124, 68, 72, 176, 240, 36, 48, 13, 194, 216, 227, 147, 231, 16, 242, 32, 42, 23, 93, 204, 3, 206, 10, 141, 255, 94, 43, 37, 107, 174, 61, 165, 221, 27, 52, 144, 8, 138, 50, 46, 64, 184, 168, 144, 13, 240, 20, 72, 174, 105, 84, 5, 79, 112, 55, 230, 170, 37, 229, 197, 61, 209, 24, 171, 3, 35, 170, 168, 96, 254, 84, 149, 170, 114, 252, 101, 78, 60, 21, 88, 69, 214, 79, 140, 244, 150, 181, 197, 17, 69, 184, 194, 144, 32, 18, 202, 36, 146, 124, 28, 46, 218, 237, 137, 139, 169, 242, 0, 122, 218, 44, 162, 77, 23, 221, 129, 111, 172, 145, 215, 86, 43, 197, 176, 242, 197, 38, 224, 189, 169, 229, 0, 20, 96, 50, 139, 4, 13, 213, 152, 45, 231, 24, 122, 11, 106, 2, 244, 114, 179, 184, 10, 12, 244, 131, 160, 22, 145, 48, 136, 195, 101, 65, 8, 210, 237, 182, 111, 4, 42, 50, 190, 43, 134, 121, 52, 197, 165, 89, 188, 30, 226, 161, 50, 182, 104, 200, 166, 254, 251, 178, 9, 14, 69, 204, 74, 147, 182, 204, 18, 97, 247, 255, 10, 191, 221, 122, 72, 106, 173, 224, 247, 239, 23, 93, 79, 108, 207, 58, 202, 143, 217, 194, 55, 99, 147, 192, 171, 139, 19, 49, 204, 51, 137, 63, 238, 115, 140, 41, 12, 27, 194, 16, 50, 168, 155, 208, 27, 33, 229, 87, 219, 56, 37, 100, 62, 97, 215, 183, 41, 134, 54, 211, 199, 33, 109, 83, 184, 155, 13, 156, 140, 180, 220, 142, 220, 57, 232, 80, 151, 241, 250, 170, 52, 224, 59, 38, 10, 214, 33, 219, 21, 82, 10, 227, 243, 215, 241, 165, 244, 248, 115, 103, 1, 28, 111, 6, 135, 178, 161, 167, 193, 5, 39, 62, 100, 216, 217, 155, 211, 203, 128, 81, 161, 77, 44, 82, 85, 255, 30, 80, 161, 236, 128, 216, 9, 229, 56, 174, 224, 68, 69, 25, 119, 169, 240, 7, 229, 5, 149, 141, 47, 195, 102, 190, 163, 179, 195, 64, 207, 61, 154, 71, 100, 111, 192, 98, 205, 22, 243, 108, 217, 14, 228, 118, 44, 87, 247, 34, 171, 180, 164, 124, 187, 194, 161, 39, 140, 22, 63, 147, 114, 190, 241, 248, 33, 44, 226, 29, 195, 99, 36, 213, 180, 66, 3, 7, 91, 49, 150, 140, 233, 99, 57, 0, 197, 106, 6, 93, 147, 230, 140, 51, 124, 154, 97, 181, 225, 247, 163, 35, 64, 5, 127, 110, 225, 62, 121, 23, 180, 214, 110, 230, 61, 127, 57, 36, 180, 102, 130, 75, 102, 155, 195, 154, 204, 254, 189, 198, 20, 142, 55, 155, 172, 181, 221, 78, 98, 177, 117, 165, 40, 52, 57, 72, 1, 246, 213, 159, 72, 190, 31, 1, 130, 136, 177, 107, 25, 127, 211, 228, 27, 115, 158, 59, 21, 83, 96, 82, 83, 78, 151, 73, 224, 226, 200, 79, 24, 11, 139, 201, 166, 188, 99, 228, 10, 203, 185, 161, 36, 167, 119, 158, 107, 165, 221, 161, 174, 87, 61, 239, 20, 255, 96, 122, 181, 239, 138, 8, 155, 82, 37, 24, 168, 227, 21, 177, 28, 252, 190, 236, 89, 243, 60, 232, 103, 12, 126, 129, 246, 143, 27, 220, 252, 141, 96, 57, 123, 250, 121, 43, 212, 247, 125, 148, 33, 204, 137, 190, 145, 57, 101, 52, 101, 78, 219, 199, 194, 126, 106, 250, 86, 44, 141, 40, 54, 67, 131, 80, 228, 105, 117, 39, 240, 128, 208, 17, 215, 11, 32, 205, 2, 75, 200, 89, 219, 28, 205, 141, 117, 3, 36, 58, 156, 18, 95, 7, 220, 252, 7, 146, 100, 221, 69, 120, 42, 233, 247, 2, 143, 214, 188, 58, 214, 99, 212, 91, 176, 9, 117, 105, 22, 0, 54, 26, 131, 171, 61, 54, 73, 51, 100, 178, 32, 183, 148, 180, 44, 211, 42, 251, 138, 157, 218, 244, 165, 0, 136, 40, 235, 192, 96, 65, 41, 8, 116, 114, 238, 41, 69, 200, 36, 38, 75, 46, 49, 233, 130, 9, 192, 57, 3, 132, 31, 129, 229, 96, 211, 88, 150, 88, 135, 115, 51, 242, 53, 239, 34, 181, 206, 166, 191, 126, 161, 78, 134, 189, 234, 187, 73, 54, 177, 160, 242, 183, 54, 231, 36, 57, 127, 88, 252, 167, 112, 141, 34, 51, 11, 9, 242, 221, 44, 252, 147, 250, 150, 130, 253, 149, 211, 14, 128, 75, 230, 68, 63, 2, 101, 233, 1, 58, 32, 24, 141, 146, 26, 21, 234, 83, 111, 16, 230, 187, 229, 152, 84, 156, 173, 80, 81, 249, 76, 162, 41, 213, 27, 116, 151, 13, 3, 101, 21, 10, 86, 12, 80, 192, 118, 104, 133, 14, 0, 19, 94, 45, 238, 229, 92, 229, 32, 159, 80, 84, 178, 110, 14, 102, 97, 127, 136, 172, 5, 180, 38, 114, 82, 196, 67, 140, 218, 180, 253, 26, 217, 160, 0, 125, 2, 211, 3, 173, 198, 92, 217, 167, 195, 177, 114, 214, 11, 169, 243, 69, 112, 238, 2, 121, 54, 163, 138, 43, 144, 39, 96, 145, 201, 251, 165, 166, 228, 248, 51, 55, 230, 24, 194, 83, 112, 125, 245, 213, 36, 79, 51, 79, 168, 198, 164, 236, 143, 176, 131, 243, 161, 201, 46, 54, 132, 9, 14, 154, 182, 31, 122, 35, 238, 233, 115, 224, 83, 44, 176, 26, 15, 31, 1, 160, 104, 18, 235, 103, 107, 1, 35, 221, 74, 206, 8, 207, 168, 166, 147, 51, 189, 213, 6, 244, 172, 145, 114, 56, 83, 121, 231, 215, 136, 174, 58, 174, 7, 7, 197, 1, 1, 125, 219, 115, 221, 25, 96, 85, 3, 84, 218, 47, 191, 148, 134, 97, 114, 195, 228, 121, 5, 246, 10, 236, 83, 85, 149, 179, 42, 154, 148, 51, 43, 192, 175, 226, 7, 128, 38, 31, 224, 253, 76, 199, 37, 214, 163, 56, 83, 74, 238, 47, 117, 139, 224, 68, 75, 114, 0, 161, 88, 29, 134, 54, 0, 209, 128, 171, 150, 147, 177, 37, 136, 69, 253, 89, 179, 182, 182, 129, 43, 66, 66, 166, 122, 229, 14, 203, 70, 224, 38, 57, 147, 253, 178, 93, 201, 209, 3, 61, 29, 201, 12, 224, 69, 215, 123, 221, 204, 40, 198, 0, 60, 131, 98, 92, 63, 215, 92, 84, 230, 186, 64, 76, 239, 206, 243, 182, 224, 248, 52, 68, 151, 193, 126, 238, 86, 66, 89, 164, 231, 3, 68, 92, 5, 133, 241, 173, 96, 244, 149, 127, 102, 188, 225, 225, 199, 181, 57, 58, 236, 245, 151, 65, 26, 41, 47, 84, 34, 18, 220, 240, 249, 129, 83, 195, 163, 216, 167, 229, 159, 139, 254, 230, 24, 95, 17, 3, 212, 112, 22, 132, 55, 235, 139, 43, 52, 5, 180, 64, 156, 237, 225, 44, 76, 230, 113, 77, 39, 249, 183, 75, 245, 23, 229, 33, 3, 255, 236, 41, 64, 197, 104, 98, 86, 113, 10, 97, 25, 232, 124, 28, 28, 13, 28, 5, 0, 76, 223, 89, 151, 78, 120, 139, 143, 23, 80, 70, 17, 185, 14, 76, 220, 4, 140, 132, 111, 71, 40, 67, 166, 146, 48, 10, 199, 51, 72, 10, 207, 66, 236, 30, 131, 16, 199, 104, 159, 95, 112, 223, 199, 155, 158, 231, 212, 162, 86, 80, 72, 203, 7, 71, 69, 143, 120, 84, 39, 69, 20, 19, 129, 61, 83, 135, 140, 137, 135, 203, 237, 204, 128, 97, 253, 214, 98, 104, 214, 57, 104, 31, 185, 67, 108, 147, 133, 178, 191, 153, 253, 17, 91, 149, 88, 215, 128, 103, 204, 95, 44, 137, 163, 117, 134, 129, 225, 185, 62, 180, 16, 71, 75, 123, 94, 10, 140, 37, 137, 217, 1, 204, 195, 70, 85, 57, 15, 47, 16, 22, 62, 22, 152, 207, 75, 135, 103, 142, 1, 10, 166, 177, 174, 152, 62, 125, 202, 98, 92, 153, 103, 19, 164, 53, 181, 76, 113, 146, 65, 17, 185, 94, 193, 156, 254, 185, 59, 100, 173, 227, 33, 17, 202, 189, 127, 255, 246, 97, 167, 94, 48, 168, 119, 119, 141, 122, 205, 213, 26, 16, 231, 11, 86, 94, 38, 116, 105, 177, 52, 147, 41, 211, 150, 137, 95, 237, 18, 245, 10, 209, 100, 209, 3, 84, 180, 213, 47, 251, 48, 51, 81, 101, 188, 67, 111, 243, 231, 65, 95, 163, 138, 2, 156, 129, 245, 98, 146, 246, 212, 166, 160, 147, 75, 176, 2, 193, 65, 230, 199, 117, 21, 56, 134, 163, 186, 115, 148, 207, 251, 145, 153, 232, 227, 97, 195, 93, 243, 90, 112, 35, 168, 151, 232, 225, 196, 156, 207, 188, 208, 79, 109, 82, 148, 83, 159, 63, 96, 211, 132, 178, 248, 83, 106, 231, 202, 1, 7, 57, 62, 177, 220, 133, 160, 136, 150, 242, 53, 20, 186, 201, 19, 90, 9, 216, 62, 193, 83, 199, 46, 14, 248, 154, 58, 3, 168, 45, 5, 40, 209, 223, 233, 69, 232, 67, 167, 163, 56, 59, 160, 53, 125, 231, 88, 116, 243, 171, 250, 109, 195, 85, 109, 125, 29, 163, 168, 124, 240, 24, 154, 38, 51, 221, 131, 223, 34, 172, 147, 3, 222, 255, 25, 202, 143, 78, 171, 22, 185, 198, 161, 51, 41, 108, 112, 84, 23, 98, 76, 54, 251, 149, 141, 211, 163, 249, 138, 135, 107, 206, 114, 177, 202, 1, 181, 2, 200, 131, 83, 148, 69, 170, 248, 81, 208, 38, 194, 73, 128, 147, 121, 166, 212, 43, 71, 243, 66, 197, 248, 75, 128, 23, 175, 106, 130, 202, 207, 162, 252, 11, 28, 152, 130, 64, 238, 200, 95, 112, 141, 48, 51, 201, 198, 145, 66, 173, 213, 142, 28, 159, 241, 183, 190, 244, 33, 236, 73, 234, 210, 67, 128, 185, 62, 249, 26, 192, 136, 142, 49, 17, 23, 71, 95, 254, 136, 134, 169, 83, 138, 250, 134, 239, 52, 180, 27, 199, 12, 33, 45, 121, 247, 201, 26, 151, 111, 26, 185, 154, 195, 200, 32, 233, 88, 215, 70, 140, 137, 124, 98, 40, 137, 70, 94, 2, 129, 6, 69, 219, 11, 11, 233, 157, 3, 191, 98, 89, 118, 17, 185, 8, 218, 170, 105, 36, 64, 202, 56, 13, 248, 35, 219, 17, 234, 34, 182, 212, 127, 153, 2, 20, 139, 136, 15, 160, 250, 152, 212, 24, 100, 193, 202, 238, 233, 111, 141, 233, 246, 127, 82, 31, 77, 32, 129, 81, 34, 227, 203, 50, 1, 179, 148, 50, 159, 28, 148, 197, 236, 248, 181, 16, 232, 45, 6, 212, 121, 53, 231, 179, 97, 176, 66, 68, 178, 176, 64, 206, 122, 146, 46, 86, 100, 43, 125, 98, 238, 201, 108, 167, 87, 246, 20, 70, 167, 9, 98, 115, 253, 11, 122, 70, 219, 71, 20, 11, 184, 196, 181, 104, 159, 124, 33, 228, 107, 190, 30, 77, 220, 233, 77, 37, 8, 32, 61, 168, 19, 112, 200, 59, 212, 26, 93, 215, 190, 221, 58, 10, 159, 81, 98, 31, 5, 64, 55, 137, 23, 19, 47, 105, 61, 92, 73, 37, 192, 86, 225, 7, 69, 208, 234, 164, 138, 196, 241, 187, 83, 240, 59, 239, 135, 10, 9, 157, 63, 26, 207, 125, 54, 146, 102, 164, 92, 105, 98, 234, 147, 100, 146, 131, 164, 153, 222, 32, 242, 137, 137, 6, 93, 194, 25, 4, 85, 69, 116, 155, 46, 107, 181, 14, 170, 101, 166, 210, 11, 124, 190, 214, 186, 48, 124, 38, 153, 128, 95, 244, 227, 65, 191, 230, 171, 15, 192, 252, 21, 253, 26, 222, 25, 201, 58, 59, 2, 65, 191, 108, 219, 99, 75, 201, 251, 137, 57, 123, 47, 114, 85, 163, 249, 196, 216, 235, 49, 27, 141, 95, 18, 62, 102, 174, 81, 173, 56, 102, 34, 47, 156, 77, 3, 232, 149, 155, 227, 226, 254, 41, 105, 252, 134, 212, 44, 64, 9, 34, 166, 234, 46, 3, 50, 132, 105, 178, 110, 38, 86, 139, 8, 239, 218, 122, 32, 241, 79, 43, 149, 192, 90, 108, 28, 35, 47, 26, 227, 169, 92, 150, 131, 155, 226, 250, 239, 73, 67, 151, 84, 92, 173, 115, 7, 36, 103, 184, 10, 160, 237, 247, 189, 188, 145, 114, 181, 151, 28, 71, 251, 238, 90, 0, 26, 208, 123, 104, 136, 144, 214, 242, 137, 178, 119, 23, 84, 98, 7, 239, 17, 195, 130, 185, 135, 27, 23, 111, 205, 19, 52, 140, 121, 134, 18, 50, 32, 141, 79, 134, 177, 59, 5, 190, 232, 112, 128, 96, 165, 99, 88, 167, 240, 186, 170, 216, 176, 130, 24, 214, 56, 223, 20, 188, 0, 30, 31, 66, 71, 110, 3, 233, 80, 220, 147, 135, 90, 24, 160, 137, 158, 58, 134, 240, 221, 75, 243, 151, 214, 28, 4, 150, 8, 15, 202, 178, 240, 122, 231, 219, 197, 46, 240, 70, 92, 232, 184, 178, 56, 64, 249, 10, 207, 230, 172, 181, 0, 23, 112, 50, 205, 183, 16, 58, 140, 50, 242, 197, 108, 158, 39, 225, 184, 211, 246, 21, 255, 56, 232, 67, 5, 130, 161, 104, 6, 176, 142, 184, 21, 42, 246, 172, 246, 236, 28, 255, 231, 35, 19, 126, 17, 176, 37, 121, 72, 202, 113, 71, 188, 184, 196, 105, 45, 64, 221, 191, 53, 81, 32, 160, 112, 0, 36, 243, 56, 127, 157, 117, 150, 212, 134, 150, 112, 210, 162, 214, 122, 87, 222, 74, 153, 116, 238, 170, 81, 103, 63, 230, 46, 69, 31, 223, 12, 32, 81, 157, 2, 65, 40, 142, 77, 92, 190, 214, 70, 94, 248, 87, 30, 148, 92, 243, 200, 146, 220, 185, 135, 174, 232, 81, 8, 126, 130, 15, 250, 76, 19, 112, 155, 218, 8, 166, 137, 248, 134, 156, 22, 173, 131, 44, 96, 115, 211, 148, 140, 93, 14, 189, 54, 32, 240, 150, 75, 99, 191, 74, 189, 29, 52, 209, 168, 229, 100, 80, 228, 100, 220, 176, 104, 115, 202, 29, 34, 136, 103, 39, 79, 14, 221, 51, 130, 152, 229, 68, 132, 22, 33, 21 }; - var i: u8 = 'a'; - var tokenizer = std.mem.tokenize(u8, add_completions, "\n"); +const uncompressed_size: usize = 162204; - while (i <= 'z') { - var init_tokenizer = tokenizer; - var count: usize = 0; - @setEvalBranchQuota(9999999); - while (init_tokenizer.next()) |pkg| { - if (pkg.len == 0) continue; - if (pkg[0] == i) { - count += 1; - } else { - break; - } - } - - var record: [count][]const u8 = undefined; - var record_i: usize = 0; - var next_i = i + 1; - - while (tokenizer.next()) |pkg| { - if (pkg.len == 0) continue; - - if (pkg[0] == i) { - record[record_i] = pkg; - record_i += 1; - } else { - next_i = pkg[0]; - break; - } - } - - const cloned = record; - array.set(@as(FirstLetter, @enumFromInt(i)), &cloned); - - @setEvalBranchQuota(999999); - i = next_i; - } - break :brk array; -}; -pub const biggest_list: usize = brk: { - var a = index; - var iter = a.iterator(); - var max: usize = 0; - while (iter.next()) |list| { - max = @max(list.value.len, max); - } - break :brk max; +pub const IndexEntry = struct { + offset: usize, + length: usize, }; -const index_blob = "add_completions.index.blob"; +pub const Index = std.EnumArray(FirstLetter, IndexEntry); + +pub const index = Index.init(.{ .a = .{ .offset = 0, .length = 540 }, .b = .{ .offset = 540, .length = 513 }, .c = .{ .offset = 1053, .length = 691 }, .d = .{ .offset = 1744, .length = 377 }, .e = .{ .offset = 2121, .length = 660 }, .f = .{ .offset = 2781, .length = 344 }, .g = .{ .offset = 3125, .length = 434 }, .h = .{ .offset = 3559, .length = 275 }, .i = .{ .offset = 3834, .length = 391 }, .j = .{ .offset = 4225, .length = 335 }, .k = .{ .offset = 4560, .length = 117 }, .l = .{ .offset = 4677, .length = 398 }, .m = .{ .offset = 5075, .length = 497 }, .n = .{ .offset = 5572, .length = 386 }, .o = .{ .offset = 5958, .length = 149 }, .p = .{ .offset = 6107, .length = 709 }, .q = .{ .offset = 6816, .length = 33 }, .r = .{ .offset = 6849, .length = 1034 }, .s = .{ .offset = 7883, .length = 817 }, .t = .{ .offset = 8700, .length = 417 }, .u = .{ .offset = 9117, .length = 217 }, .v = .{ .offset = 9334, .length = 302 }, .w = .{ .offset = 9636, .length = 220 }, .x = .{ .offset = 9856, .length = 60 }, .y = .{ .offset = 9916, .length = 53 }, .z = .{ .offset = 9969, .length = 30 } }); + +var decompressed_data: ?[]u8 = null; +var packages_list: ?[][]const u8 = null; + +pub fn init(allocator: std.mem.Allocator) !void { + // Decompress data + var data = try allocator.alloc(u8, uncompressed_size); + errdefer allocator.free(data); + + const result = zstd.decompress(data, &compressed_data); + decompressed_data = data[0..result.success]; + + // Parse package list + const total_count = std.mem.readInt(u32, data[0..4], .little); + var packages = try allocator.alloc([]const u8, total_count); + errdefer allocator.free(packages); + + var pos: usize = 4; + var i: usize = 0; + while (i < total_count) : (i += 1) { + const len = std.mem.readInt(u16, data[pos..][0..2], .little); + pos += 2; + packages[i] = data[pos .. pos + len]; + pos += len; + } + + packages_list = packages; +} + +pub fn deinit(allocator: std.mem.Allocator) void { + if (packages_list) |pkgs| { + allocator.free(pkgs); + packages_list = null; + } + + if (decompressed_data) |data| { + allocator.free(data); + decompressed_data = null; + } +} + +pub fn getPackages(letter: FirstLetter) []const []const u8 { + const entry = index.get(letter); + if (entry.length == 0) return &[_][]const u8{}; + + return packages_list.?[entry.offset .. entry.offset + entry.length]; +} + +pub const biggest_list: usize = 1034; diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 8b02cc40e6..073bceb129 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -25,7 +25,7 @@ const sync = @import("../sync.zig"); const Api = @import("../api/schema.zig").Api; const resolve_path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; -const bundler = bun.bundler; +const transpiler = bun.transpiler; const DotEnv = @import("../env_loader.zig"); @@ -74,13 +74,13 @@ pub const BuildCommand = struct { } } - var this_bundler = try bundler.Bundler.init(allocator, log, ctx.args, null); + var this_transpiler = try transpiler.Transpiler.init(allocator, log, ctx.args, null); - this_bundler.options.source_map = options.SourceMapOption.fromApi(ctx.args.source_map); + this_transpiler.options.source_map = options.SourceMapOption.fromApi(ctx.args.source_map); - this_bundler.options.compile = ctx.bundler_options.compile; + this_transpiler.options.compile = ctx.bundler_options.compile; - if (this_bundler.options.source_map == .external and ctx.bundler_options.outdir.len == 0 and !ctx.bundler_options.compile) { + if (this_transpiler.options.source_map == .external and ctx.bundler_options.outdir.len == 0 and !ctx.bundler_options.compile) { Output.prettyErrorln("error: cannot use an external source map without --outdir", .{}); Global.exit(1); return; @@ -89,37 +89,37 @@ pub const BuildCommand = struct { var outfile = ctx.bundler_options.outfile; const output_to_stdout = !ctx.bundler_options.compile and outfile.len == 0 and ctx.bundler_options.outdir.len == 0; - this_bundler.options.supports_multiple_outputs = !(output_to_stdout or outfile.len > 0); + this_transpiler.options.supports_multiple_outputs = !(output_to_stdout or outfile.len > 0); - this_bundler.options.public_path = ctx.bundler_options.public_path; - this_bundler.options.entry_naming = ctx.bundler_options.entry_naming; - this_bundler.options.chunk_naming = ctx.bundler_options.chunk_naming; - this_bundler.options.asset_naming = ctx.bundler_options.asset_naming; - this_bundler.options.server_components = ctx.bundler_options.server_components; - this_bundler.options.react_fast_refresh = ctx.bundler_options.react_fast_refresh; - this_bundler.options.inline_entrypoint_import_meta_main = ctx.bundler_options.inline_entrypoint_import_meta_main; - this_bundler.options.code_splitting = ctx.bundler_options.code_splitting; - this_bundler.options.minify_syntax = ctx.bundler_options.minify_syntax; - this_bundler.options.minify_whitespace = ctx.bundler_options.minify_whitespace; - this_bundler.options.minify_identifiers = ctx.bundler_options.minify_identifiers; - this_bundler.options.emit_dce_annotations = ctx.bundler_options.emit_dce_annotations; - this_bundler.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; + this_transpiler.options.public_path = ctx.bundler_options.public_path; + this_transpiler.options.entry_naming = ctx.bundler_options.entry_naming; + this_transpiler.options.chunk_naming = ctx.bundler_options.chunk_naming; + this_transpiler.options.asset_naming = ctx.bundler_options.asset_naming; + this_transpiler.options.server_components = ctx.bundler_options.server_components; + this_transpiler.options.react_fast_refresh = ctx.bundler_options.react_fast_refresh; + this_transpiler.options.inline_entrypoint_import_meta_main = ctx.bundler_options.inline_entrypoint_import_meta_main; + this_transpiler.options.code_splitting = ctx.bundler_options.code_splitting; + this_transpiler.options.minify_syntax = ctx.bundler_options.minify_syntax; + this_transpiler.options.minify_whitespace = ctx.bundler_options.minify_whitespace; + this_transpiler.options.minify_identifiers = ctx.bundler_options.minify_identifiers; + this_transpiler.options.emit_dce_annotations = ctx.bundler_options.emit_dce_annotations; + this_transpiler.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; - this_bundler.options.banner = ctx.bundler_options.banner; - this_bundler.options.footer = ctx.bundler_options.footer; - this_bundler.options.drop = ctx.args.drop; + this_transpiler.options.banner = ctx.bundler_options.banner; + this_transpiler.options.footer = ctx.bundler_options.footer; + this_transpiler.options.drop = ctx.args.drop; - this_bundler.options.experimental_css = ctx.bundler_options.experimental_css; - this_bundler.options.css_chunking = ctx.bundler_options.css_chunking; + this_transpiler.options.experimental = ctx.bundler_options.experimental; + this_transpiler.options.css_chunking = ctx.bundler_options.css_chunking; - this_bundler.options.output_dir = ctx.bundler_options.outdir; - this_bundler.options.output_format = ctx.bundler_options.output_format; + this_transpiler.options.output_dir = ctx.bundler_options.outdir; + this_transpiler.options.output_format = ctx.bundler_options.output_format; if (ctx.bundler_options.output_format == .internal_bake_dev) { - this_bundler.options.tree_shaking = false; + this_transpiler.options.tree_shaking = false; } - this_bundler.options.bytecode = ctx.bundler_options.bytecode; + this_transpiler.options.bytecode = ctx.bundler_options.bytecode; if (ctx.bundler_options.compile) { if (ctx.bundler_options.code_splitting) { @@ -136,21 +136,21 @@ pub const BuildCommand = struct { const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(compile_target.os, "root/"); - this_bundler.options.public_path = base_public_path; + this_transpiler.options.public_path = base_public_path; if (outfile.len == 0) { - outfile = std.fs.path.basename(this_bundler.options.entry_points[0]); + outfile = std.fs.path.basename(this_transpiler.options.entry_points[0]); const ext = std.fs.path.extension(outfile); if (ext.len > 0) { outfile = outfile[0 .. outfile.len - ext.len]; } if (strings.eqlComptime(outfile, "index")) { - outfile = std.fs.path.basename(std.fs.path.dirname(this_bundler.options.entry_points[0]) orelse "index"); + outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "index"); } if (strings.eqlComptime(outfile, "bun")) { - outfile = std.fs.path.basename(std.fs.path.dirname(this_bundler.options.entry_points[0]) orelse "bun"); + outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "bun"); } } @@ -169,12 +169,12 @@ pub const BuildCommand = struct { } if (ctx.bundler_options.outdir.len == 0 and !ctx.bundler_options.compile) { - if (this_bundler.options.entry_points.len > 1) { + if (this_transpiler.options.entry_points.len > 1) { Output.prettyErrorln("error: Must use --outdir when specifying more than one entry point.", .{}); Global.exit(1); return; } - if (this_bundler.options.code_splitting) { + if (this_transpiler.options.code_splitting) { Output.prettyErrorln("error: Must use --outdir when code splitting is enabled", .{}); Global.exit(1); return; @@ -188,11 +188,11 @@ pub const BuildCommand = struct { break :brk2 ctx.bundler_options.root_dir; } - if (this_bundler.options.entry_points.len == 1) { - break :brk2 std.fs.path.dirname(this_bundler.options.entry_points[0]) orelse "."; + if (this_transpiler.options.entry_points.len == 1) { + break :brk2 std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "."; } - break :brk2 resolve_path.getIfExistsLongestCommonPath(this_bundler.options.entry_points) orelse "."; + break :brk2 resolve_path.getIfExistsLongestCommonPath(this_transpiler.options.entry_points) orelse "."; }; var dir = bun.openDirForPath(&(try std.posix.toPosixPath(path))) catch |err| { @@ -207,36 +207,47 @@ pub const BuildCommand = struct { }; }; - this_bundler.options.root_dir = src_root_dir; - this_bundler.options.code_splitting = ctx.bundler_options.code_splitting; - this_bundler.options.transform_only = ctx.bundler_options.transform_only; + this_transpiler.options.root_dir = src_root_dir; + this_transpiler.options.code_splitting = ctx.bundler_options.code_splitting; + this_transpiler.options.transform_only = ctx.bundler_options.transform_only; - try this_bundler.configureDefines(); - this_bundler.configureLinker(); + this_transpiler.options.env.behavior = ctx.bundler_options.env_behavior; + this_transpiler.options.env.prefix = ctx.bundler_options.env_prefix; - this_bundler.resolver.opts = this_bundler.options; - this_bundler.options.jsx.development = !this_bundler.options.production; - this_bundler.resolver.opts.jsx.development = this_bundler.options.jsx.development; + try this_transpiler.configureDefines(); + this_transpiler.configureLinker(); + + if (bun.FeatureFlags.breaking_changes_1_2) { + // This is currently done in DevServer by default, but not in Bun.build + if (!this_transpiler.options.production) { + try this_transpiler.options.conditions.appendSlice(&.{"development"}); + } + } + + this_transpiler.resolver.opts = this_transpiler.options; + this_transpiler.options.jsx.development = !this_transpiler.options.production; + this_transpiler.resolver.opts.jsx.development = this_transpiler.options.jsx.development; switch (ctx.debug.macros) { .disable => { - this_bundler.options.no_macros = true; + this_transpiler.options.no_macros = true; }, .map => |macros| { - this_bundler.options.macro_remap = macros; + this_transpiler.options.macro_remap = macros; }, .unspecified => {}, } - var client_bundler: bundler.Bundler = undefined; - if (this_bundler.options.server_components) { - client_bundler = try bundler.Bundler.init(allocator, log, ctx.args, null); - client_bundler.options = this_bundler.options; + var client_bundler: transpiler.Transpiler = undefined; + if (this_transpiler.options.server_components) { + client_bundler = try transpiler.Transpiler.init(allocator, log, ctx.args, null); + client_bundler.options = this_transpiler.options; client_bundler.options.target = .browser; client_bundler.options.server_components = true; - try this_bundler.options.conditions.appendSlice(&.{"react-server"}); - this_bundler.options.react_fast_refresh = false; - this_bundler.options.minify_syntax = true; + client_bundler.options.conditions = try this_transpiler.options.conditions.clone(); + try this_transpiler.options.conditions.appendSlice(&.{"react-server"}); + this_transpiler.options.react_fast_refresh = false; + this_transpiler.options.minify_syntax = true; client_bundler.options.minify_syntax = true; client_bundler.options.define = try options.Define.init( allocator, @@ -250,20 +261,20 @@ pub const BuildCommand = struct { else null, null, - this_bundler.options.define.drop_debugger, + this_transpiler.options.define.drop_debugger, ); - try bun.bake.addImportMetaDefines(allocator, this_bundler.options.define, .development, .server); + try bun.bake.addImportMetaDefines(allocator, this_transpiler.options.define, .development, .server); try bun.bake.addImportMetaDefines(allocator, client_bundler.options.define, .development, .client); - this_bundler.resolver.opts = this_bundler.options; + this_transpiler.resolver.opts = this_transpiler.options; client_bundler.resolver.opts = client_bundler.options; } - // var env_loader = this_bundler.env; + // var env_loader = this_transpiler.env; if (ctx.debug.dump_environment_variables) { - this_bundler.dumpEnvironmentVariables(); + this_transpiler.dumpEnvironmentVariables(); return; } @@ -273,12 +284,12 @@ pub const BuildCommand = struct { const output_files: []options.OutputFile = brk: { if (ctx.bundler_options.transform_only) { - this_bundler.options.import_path_format = .relative; - this_bundler.options.allow_runtime = false; - this_bundler.resolver.opts.allow_runtime = false; + this_transpiler.options.import_path_format = .relative; + this_transpiler.options.allow_runtime = false; + this_transpiler.resolver.opts.allow_runtime = false; // TODO: refactor this .transform function - const result = try this_bundler.transform( + const result = try this_transpiler.transform( ctx.allocator, ctx.log, ctx.args, @@ -298,7 +309,7 @@ pub const BuildCommand = struct { } break :brk (BundleV2.generateFromCLI( - &this_bundler, + &this_transpiler, allocator, bun.JSC.AnyEventLoop.init(ctx.allocator), ctx.debug.hot_reload == .watch, @@ -324,7 +335,7 @@ pub const BuildCommand = struct { dump: { defer Output.flush(); var writer = Output.writer(); - var output_dir = this_bundler.options.output_dir; + var output_dir = this_transpiler.options.output_dir; const will_be_one_file = // --outdir is not supported with --compile @@ -380,7 +391,7 @@ pub const BuildCommand = struct { printSummary( bundled_end, minify_duration, - this_bundler.options.minify_identifiers or this_bundler.options.minify_whitespace or this_bundler.options.minify_syntax, + this_transpiler.options.minify_identifiers or this_transpiler.options.minify_whitespace or this_transpiler.options.minify_syntax, input_code_length, reachable_file_count, output_files, @@ -403,10 +414,12 @@ pub const BuildCommand = struct { allocator, output_files, root_dir, - this_bundler.options.public_path, + this_transpiler.options.public_path, outfile, - this_bundler.env, - this_bundler.options.output_format, + this_transpiler.env, + this_transpiler.options.output_format, + ctx.bundler_options.windows_hide_console, + ctx.bundler_options.windows_icon, ); const compiled_elapsed = @divTrunc(@as(i64, @truncate(std.time.nanoTimestamp() - bundled_end)), @as(i64, std.time.ns_per_ms)); const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) { @@ -465,7 +478,7 @@ pub const BuildCommand = struct { Output.printElapsedStdoutTrim( @as(f64, @floatFromInt((@divTrunc(@as(i64, @truncate(std.time.nanoTimestamp() - bun.CLI.start_time)), @as(i64, std.time.ns_per_ms))))), ); - if (this_bundler.options.transform_only) { + if (this_transpiler.options.transform_only) { Output.prettyln(" transpile", .{}); } else { Output.prettyln(" bundle {d} modules", .{ diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index 7f3113ec8f..3256f302ed 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -1,5 +1,7 @@ +const std = @import("std"); const bun = @import("root").bun; const string = bun.string; +const Allocator = std.mem.Allocator; const Output = bun.Output; const Global = bun.Global; const Environment = bun.Environment; @@ -8,16 +10,99 @@ const MutableString = bun.MutableString; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const C = bun.C; -const std = @import("std"); const cli = @import("../cli.zig"); + const Command = cli.Command; const Run = @import("./run_command.zig").RunCommand; +const UpdateRequest = bun.PackageManager.UpdateRequest; const debug = Output.scoped(.bunx, false); pub const BunxCommand = struct { var path_buf: bun.PathBuffer = undefined; + /// bunx-specific options parsed from argv. + const Options = struct { + /// CLI arguments to pass to the command being run. + passthrough_list: std.ArrayListUnmanaged(string) = .{}, + /// `bunx ` + package_name: string, + // `--silent` and `--verbose` are not mutually exclusive. Both the + // global CLI parser and `bun add` parser use them for different + // purposes. + verbose_install: bool = false, + silent_install: bool = false, + /// Skip installing the package, only running the target command if its + /// already downloaded. If its not, `bunx` exits with an error. + no_install: bool = false, + allocator: Allocator, + + /// Create a new `Options` instance by parsing CLI arguments. `ctx` may be mutated. + /// + /// ## Exits + /// - `--revision` or `--version` flags are passed without a target + /// command also being provided. This is not a failure. + /// - Incorrect arguments are passed. Prints usage and exits with a failure code. + fn parse(ctx: bun.CLI.Command.Context, argv: [][:0]const u8) Allocator.Error!Options { + var found_subcommand_name = false; + var maybe_package_name: ?string = null; + var has_version = false; // --version + var has_revision = false; // --revision + + // SAFETY: `opts` is only ever returned when a package name is found, otherwise the process exits. + var opts = Options{ .package_name = undefined, .allocator = ctx.allocator }; + try opts.passthrough_list.ensureTotalCapacityPrecise(opts.allocator, argv.len); + + for (argv) |positional| { + if (maybe_package_name != null) { + opts.passthrough_list.appendAssumeCapacity(positional); + continue; + } + + if (positional.len > 0 and positional[0] == '-') { + if (strings.eqlComptime(positional, "--version") or strings.eqlComptime(positional, "-v")) { + has_version = true; + } else if (strings.eqlComptime(positional, "--revision")) { + has_revision = true; + } else if (strings.eqlComptime(positional, "--verbose")) { + opts.verbose_install = true; + } else if (strings.eqlComptime(positional, "--silent")) { + opts.silent_install = true; + } else if (strings.eqlComptime(positional, "--bun") or strings.eqlComptime(positional, "-b")) { + ctx.debug.run_in_bun = true; + } else if (strings.eqlComptime(positional, "--no-install")) { + opts.no_install = true; + } + } else { + if (!found_subcommand_name) { + found_subcommand_name = true; + } else { + maybe_package_name = positional; + } + } + } + + // check if package_name_for_update_request is empty string or " " + if (maybe_package_name == null or maybe_package_name.?.len == 0) { + // no need to free memory b/c we're exiting + if (has_revision) { + cli.printRevisionAndExit(); + } else if (has_version) { + cli.printVersionAndExit(); + } else { + exitWithUsage(); + } + } + opts.package_name = maybe_package_name.?; + return opts; + } + + fn deinit(self: *Options) void { + self.passthrough_list.deinit(self.allocator); + self.* = undefined; + } + }; + /// Adds `create-` to the string, but also handles scoped packages correctly. /// Always clones the string in the process. pub fn addCreatePrefix(allocator: std.mem.Allocator, input: []const u8) ![:0]const u8 { @@ -63,13 +148,13 @@ pub const BunxCommand = struct { /// 1 day const nanoseconds_cache_valid = seconds_cache_valid * 1000000000; - fn getBinNameFromSubpath(bundler: *bun.Bundler, dir_fd: bun.FileDescriptor, subpath_z: [:0]const u8) ![]const u8 { + fn getBinNameFromSubpath(transpiler: *bun.Transpiler, dir_fd: bun.FileDescriptor, subpath_z: [:0]const u8) ![]const u8 { const target_package_json_fd = try bun.sys.openat(dir_fd, subpath_z, bun.O.RDONLY, 0).unwrap(); const target_package_json = bun.sys.File{ .handle = target_package_json_fd }; defer target_package_json.close(); - const package_json_read = target_package_json.readToEnd(bundler.allocator); + const package_json_read = target_package_json.readToEnd(transpiler.allocator); // TODO: make this better if (package_json_read.err) |err| { @@ -82,7 +167,7 @@ pub const BunxCommand = struct { bun.JSAst.Expr.Data.Store.create(); bun.JSAst.Stmt.Data.Store.create(); - const expr = try bun.JSON.parsePackageJSONUTF8(&source, bundler.log, bundler.allocator); + const expr = try bun.JSON.parsePackageJSONUTF8(&source, transpiler.log, transpiler.allocator); // choose the first package that fits if (expr.get("bin")) |bin_expr| { @@ -90,7 +175,7 @@ pub const BunxCommand = struct { .e_object => |object| { for (object.properties.slice()) |prop| { if (prop.key) |key| { - if (key.asString(bundler.allocator)) |bin_name| { + if (key.asString(transpiler.allocator)) |bin_name| { if (bin_name.len == 0) continue; return bin_name; } @@ -99,7 +184,7 @@ pub const BunxCommand = struct { }, .e_string => { if (expr.get("name")) |name_expr| { - if (name_expr.asString(bundler.allocator)) |name| { + if (name_expr.asString(transpiler.allocator)) |name| { return name; } } @@ -110,7 +195,7 @@ pub const BunxCommand = struct { if (expr.asProperty("directories")) |dirs| { if (dirs.expr.asProperty("bin")) |bin_prop| { - if (bin_prop.expr.asString(bundler.allocator)) |dir_name| { + if (bin_prop.expr.asString(transpiler.allocator)) |dir_name| { const bin_dir = try bun.sys.openatA(dir_fd, dir_name, bun.O.RDONLY | bun.O.DIRECTORY, 0).unwrap(); defer _ = bun.sys.close(bin_dir); const dir = std.fs.Dir{ .fd = bin_dir.cast() }; @@ -124,7 +209,7 @@ pub const BunxCommand = struct { if (current.kind == .file) { if (current.name.len == 0) continue; - return try bundler.allocator.dupe(u8, current.name.slice()); + return try transpiler.allocator.dupe(u8, current.name.slice()); } } } @@ -134,13 +219,13 @@ pub const BunxCommand = struct { return error.NoBinFound; } - fn getBinNameFromProjectDirectory(bundler: *bun.Bundler, dir_fd: bun.FileDescriptor, package_name: []const u8) ![]const u8 { + fn getBinNameFromProjectDirectory(transpiler: *bun.Transpiler, dir_fd: bun.FileDescriptor, package_name: []const u8) ![]const u8 { var subpath: bun.PathBuffer = undefined; const subpath_z = std.fmt.bufPrintZ(&subpath, bun.pathLiteral("node_modules/{s}/package.json"), .{package_name}) catch unreachable; - return try getBinNameFromSubpath(bundler, dir_fd, subpath_z); + return try getBinNameFromSubpath(transpiler, dir_fd, subpath_z); } - fn getBinNameFromTempDirectory(bundler: *bun.Bundler, tempdir_name: []const u8, package_name: []const u8, with_stale_check: bool) ![]const u8 { + fn getBinNameFromTempDirectory(transpiler: *bun.Transpiler, tempdir_name: []const u8, package_name: []const u8, with_stale_check: bool) ![]const u8 { var subpath: bun.PathBuffer = undefined; if (with_stale_check) { const subpath_z = std.fmt.bufPrintZ( @@ -186,19 +271,19 @@ pub const BunxCommand = struct { .{ tempdir_name, package_name }, ) catch unreachable; - return try getBinNameFromSubpath(bundler, bun.FD.cwd(), subpath_z); + return try getBinNameFromSubpath(transpiler, bun.FD.cwd(), subpath_z); } /// Check the enclosing package.json for a matching "bin" /// If not found, check bunx cache dir - fn getBinName(bundler: *bun.Bundler, toplevel_fd: bun.FileDescriptor, tempdir_name: []const u8, package_name: []const u8) error{ NoBinFound, NeedToInstall }![]const u8 { + fn getBinName(transpiler: *bun.Transpiler, toplevel_fd: bun.FileDescriptor, tempdir_name: []const u8, package_name: []const u8) error{ NoBinFound, NeedToInstall }![]const u8 { toplevel_fd.assertValid(); - return getBinNameFromProjectDirectory(bundler, toplevel_fd, package_name) catch |err| { + return getBinNameFromProjectDirectory(transpiler, toplevel_fd, package_name) catch |err| { if (err == error.NoBinFound) { return error.NoBinFound; } - return getBinNameFromTempDirectory(bundler, tempdir_name, package_name, true) catch |err2| { + return getBinNameFromTempDirectory(transpiler, tempdir_name, package_name, true) catch |err2| { if (err2 == error.NoBinFound) { return error.NoBinFound; } @@ -217,63 +302,16 @@ pub const BunxCommand = struct { // Don't log stuff ctx.debug.silent = true; - var passthrough_list = try std.ArrayList(string).initCapacity(ctx.allocator, argv.len); - var maybe_package_name: ?string = null; - var verbose_install = false; - var silent_install = false; - var has_version = false; - var has_revision = false; - { - var found_subcommand_name = false; + var opts = try Options.parse(ctx, argv); + defer opts.deinit(); - for (argv) |positional| { - if (maybe_package_name != null) { - passthrough_list.appendAssumeCapacity(positional); - continue; - } - - if (positional.len > 0 and positional[0] == '-') { - if (strings.eqlComptime(positional, "--version") or strings.eqlComptime(positional, "-v")) { - has_version = true; - } else if (strings.eqlComptime(positional, "--revision")) { - has_revision = true; - } else if (strings.eqlComptime(positional, "--verbose")) { - verbose_install = true; - } else if (strings.eqlComptime(positional, "--silent")) { - silent_install = true; - } else if (strings.eqlComptime(positional, "--bun") or strings.eqlComptime(positional, "-b")) { - ctx.debug.run_in_bun = true; - } - } else { - if (!found_subcommand_name) { - found_subcommand_name = true; - } else { - maybe_package_name = positional; - } - } - } - } - - // check if package_name_for_update_request is empty string or " " - if (maybe_package_name == null or maybe_package_name.?.len == 0) { - if (has_revision) { - cli.printRevisionAndExit(); - } else if (has_version) { - cli.printVersionAndExit(); - } else { - exitWithUsage(); - } - } - - const package_name = maybe_package_name.?; - - var requests_buf = bun.PackageManager.UpdateRequest.Array.initCapacity(ctx.allocator, 64) catch bun.outOfMemory(); + var requests_buf = UpdateRequest.Array.initCapacity(ctx.allocator, 64) catch bun.outOfMemory(); defer requests_buf.deinit(ctx.allocator); - const update_requests = bun.PackageManager.UpdateRequest.parse( + const update_requests = UpdateRequest.parse( ctx.allocator, null, ctx.log, - &.{package_name}, + &.{opts.package_name}, &requests_buf, .add, ); @@ -304,12 +342,13 @@ pub const BunxCommand = struct { // fast path: they're actually using this interchangeably with `bun run` // so we use Bun.which to check - var this_bundler: bun.Bundler = undefined; + // SAFETY: initialized by Run.configureEnvForRun + var this_transpiler: bun.Transpiler = undefined; var ORIGINAL_PATH: string = ""; const root_dir_info = try Run.configureEnvForRun( ctx, - &this_bundler, + &this_transpiler, null, true, true, @@ -318,22 +357,28 @@ pub const BunxCommand = struct { try Run.configurePathForRun( ctx, root_dir_info, - &this_bundler, + &this_transpiler, &ORIGINAL_PATH, root_dir_info.abs_path, ctx.debug.run_in_bun, ); - this_bundler.env.map.put("npm_command", "exec") catch unreachable; - this_bundler.env.map.put("npm_lifecycle_event", "bunx") catch unreachable; - this_bundler.env.map.put("npm_lifecycle_script", package_name) catch unreachable; + this_transpiler.env.map.put("npm_command", "exec") catch unreachable; + this_transpiler.env.map.put("npm_lifecycle_event", "bunx") catch unreachable; + this_transpiler.env.map.put("npm_lifecycle_script", opts.package_name) catch unreachable; - const ignore_cwd = this_bundler.env.get("BUN_WHICH_IGNORE_CWD") orelse ""; - - if (ignore_cwd.len > 0) { - _ = this_bundler.env.map.map.swapRemove("BUN_WHICH_IGNORE_CWD"); + if (strings.eqlComptime(opts.package_name, "bun-repl")) { + this_transpiler.env.map.remove("BUN_INSPECT_CONNECT_TO"); + this_transpiler.env.map.remove("BUN_INSPECT_NOTIFY"); + this_transpiler.env.map.remove("BUN_INSPECT"); } - var PATH = this_bundler.env.get("PATH").?; + const ignore_cwd = this_transpiler.env.get("BUN_WHICH_IGNORE_CWD") orelse ""; + + if (ignore_cwd.len > 0) { + _ = this_transpiler.env.map.map.swapRemove("BUN_WHICH_IGNORE_CWD"); + } + + var PATH = this_transpiler.env.get("PATH").?; const display_version = if (update_request.version.literal.isEmpty()) "latest" else @@ -347,7 +392,7 @@ pub const BunxCommand = struct { else => ":", }; - const has_banned_char = bun.strings.indexAnyComptime(update_request.name, banned_path_chars) != null or bun.strings.indexAnyComptime(display_version, banned_path_chars) != null; + const has_banned_char = strings.indexAnyComptime(update_request.name, banned_path_chars) != null or strings.indexAnyComptime(display_version, banned_path_chars) != null; break :brk try if (has_banned_char) // This branch gets hit usually when a URL is requested as the package @@ -448,7 +493,7 @@ pub const BunxCommand = struct { ), }; - try this_bundler.env.map.put("PATH", PATH); + try this_transpiler.env.map.put("PATH", PATH); const bunx_cache_dir = PATH[0 .. temp_dir.len + "/bunx--".len + package_fmt.len + @@ -463,11 +508,13 @@ pub const BunxCommand = struct { .{ bunx_cache_dir, initial_bin_name, bun.exe_suffix }, ) catch return error.PathTooLong; - const passthrough = passthrough_list.items; + const passthrough = opts.passthrough_list.items; var do_cache_bust = update_request.version.tag == .dist_tag; + const look_for_existing_bin = update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag; - if (update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag) try_run_existing: { + debug("try run existing? {}", .{look_for_existing_bin}); + if (look_for_existing_bin) try_run_existing: { var destination_: ?[:0]const u8 = null; // Only use the system-installed version if there is no version specified @@ -475,7 +522,7 @@ pub const BunxCommand = struct { destination_ = bun.which( &path_buf, PATH_FOR_BIN_DIRS, - if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, + if (ignore_cwd.len > 0) "" else this_transpiler.fs.top_level_dir, initial_bin_name, ); } @@ -486,7 +533,7 @@ pub const BunxCommand = struct { if (destination_ orelse bun.which( &path_buf, bunx_cache_dir, - if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, + if (ignore_cwd.len > 0) "" else this_transpiler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); @@ -526,17 +573,23 @@ pub const BunxCommand = struct { }; if (is_stale) { + debug("found stale binary: {s}", .{out}); do_cache_bust = true; - break :try_run_existing; + if (opts.no_install) { + Output.warn("Using a stale installation of {s} because --no-install was passed. Run `bunx` without --no-install to use a fresh binary.", .{update_request.name}); + } else { + break :try_run_existing; + } } } + debug("running existing binary: {s}", .{destination}); try Run.runBinary( ctx, - try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + try this_transpiler.fs.dirname_store.append(@TypeOf(out), out), destination, - this_bundler.fs.top_level_dir, - this_bundler.env, + this_transpiler.fs.top_level_dir, + this_transpiler.env, passthrough, null, ); @@ -547,7 +600,7 @@ pub const BunxCommand = struct { // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use const root_dir_fd = root_dir_info.getFileDescriptor(); bun.assert(root_dir_fd != .zero); - if (getBinName(&this_bundler, root_dir_fd, bunx_cache_dir, initial_bin_name)) |package_name_for_bin| { + if (getBinName(&this_transpiler, root_dir_fd, bunx_cache_dir, initial_bin_name)) |package_name_for_bin| { // if we check the bin name and its actually the same, we don't need to check $PATH here again if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) { absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}{s}"), .{ bunx_cache_dir, package_name_for_bin, bun.exe_suffix }) catch unreachable; @@ -557,7 +610,7 @@ pub const BunxCommand = struct { destination_ = bun.which( &path_buf, bunx_cache_dir, - if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, + if (ignore_cwd.len > 0) "" else this_transpiler.fs.top_level_dir, package_name_for_bin, ); } @@ -565,16 +618,16 @@ pub const BunxCommand = struct { if (destination_ orelse bun.which( &path_buf, bunx_cache_dir, - if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, + if (ignore_cwd.len > 0) "" else this_transpiler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); try Run.runBinary( ctx, - try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + try this_transpiler.fs.dirname_store.append(@TypeOf(out), out), destination, - this_bundler.fs.top_level_dir, - this_bundler.env, + this_transpiler.fs.top_level_dir, + this_transpiler.env, passthrough, null, ); @@ -589,6 +642,24 @@ pub const BunxCommand = struct { } } } + // If we've reached this point, it means we couldn't find an existing binary to run. + // Next step is to install, then run it. + + // NOTE: npx prints errors like this: + // + // npm error npx canceled due to missing packages and no YES option: ["foo@1.2.3"] + // npm error A complete log of this run can be found in: [folder]/debug.log + // + // Which is not very helpful. + + if (opts.no_install) { + Output.errGeneric( + "Could not find an existing '{s}' binary to run. Stopping because --no-install was passed.", + .{initial_bin_name}, + ); + Global.exit(1); + } + const bunx_install_dir = try std.fs.cwd().makeOpenPath(bunx_cache_dir, .{}); create_package_json: { @@ -617,12 +688,12 @@ pub const BunxCommand = struct { unreachable; // upper bound is known } - if (verbose_install) { + if (opts.verbose_install) { args.append("--verbose") catch unreachable; // upper bound is known } - if (silent_install) { + if (opts.silent_install) { args.append("--silent") catch unreachable; // upper bound is known } @@ -630,12 +701,12 @@ pub const BunxCommand = struct { const argv_to_use = args.slice(); debug("installing package: {s}", .{bun.fmt.fmtSlice(argv_to_use, " ")}); - this_bundler.env.map.put("BUN_INTERNAL_BUNX_INSTALL", "true") catch bun.outOfMemory(); + this_transpiler.env.map.put("BUN_INTERNAL_BUNX_INSTALL", "true") catch bun.outOfMemory(); const spawn_result = switch ((bun.spawnSync(&.{ .argv = argv_to_use, - .envp = try this_bundler.env.map.createNullDelimitedEnvMap(bun.default_allocator), + .envp = try this_transpiler.env.map.createNullDelimitedEnvMap(bun.default_allocator), .cwd = bunx_cache_dir, .stderr = .inherit, @@ -643,8 +714,8 @@ pub const BunxCommand = struct { .stdin = .inherit, .windows = if (Environment.isWindows) .{ - .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(this_bundler.env)), - } else {}, + .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(this_transpiler.env)), + }, }) catch |err| { Output.prettyErrorln("error: bunx failed to install {s} due to error {s}", .{ install_param, @errorName(err) }); Global.exit(1); @@ -685,16 +756,16 @@ pub const BunxCommand = struct { if (bun.which( &path_buf, bunx_cache_dir, - if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, + if (ignore_cwd.len > 0) "" else this_transpiler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); try Run.runBinary( ctx, - try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + try this_transpiler.fs.dirname_store.append(@TypeOf(out), out), destination, - this_bundler.fs.top_level_dir, - this_bundler.env, + this_transpiler.fs.top_level_dir, + this_transpiler.env, passthrough, null, ); @@ -703,23 +774,23 @@ pub const BunxCommand = struct { } // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use - if (getBinNameFromTempDirectory(&this_bundler, bunx_cache_dir, result_package_name, false)) |package_name_for_bin| { + if (getBinNameFromTempDirectory(&this_transpiler, bunx_cache_dir, result_package_name, false)) |package_name_for_bin| { if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) { absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}{s}", .{ bunx_cache_dir, package_name_for_bin, bun.exe_suffix }) catch unreachable; if (bun.which( &path_buf, bunx_cache_dir, - if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, + if (ignore_cwd.len > 0) "" else this_transpiler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); try Run.runBinary( ctx, - try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + try this_transpiler.fs.dirname_store.append(@TypeOf(out), out), destination, - this_bundler.fs.top_level_dir, - this_bundler.env, + this_transpiler.fs.top_level_dir, + this_transpiler.env, passthrough, null, ); diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 0c81ee3730..bc85f95331 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -25,7 +25,6 @@ const Api = @import("../api/schema.zig").Api; const resolve_path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; const Command = @import("../cli.zig").Command; -const bundler = bun.bundler; const fs = @import("../fs.zig"); const URL = @import("../url.zig").URL; @@ -39,7 +38,7 @@ const DotEnv = @import("../env_loader.zig"); const NPMClient = @import("../which_npm_client.zig").NPMClient; const which = @import("../which.zig").which; const clap = bun.clap; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const Headers = bun.http.Headers; const CopyFile = @import("../copy_file.zig"); var bun_path_buf: bun.PathBuffer = undefined; @@ -155,7 +154,7 @@ fn execTask(allocator: std.mem.Allocator, task_: string, cwd: string, _: string, .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), - } else {}, + }, }) catch return; } @@ -476,18 +475,14 @@ pub const CreateCommand = struct { }; var destination_buf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; - const dst_without_trailing_slash: if (Environment.isWindows) string else void = if (comptime Environment.isWindows) - strings.withoutTrailingSlash(destination) - else {}; + const dst_without_trailing_slash: if (Environment.isWindows) string else void = if (comptime Environment.isWindows) strings.withoutTrailingSlash(destination); if (comptime Environment.isWindows) { strings.copyU8IntoU16(&destination_buf, dst_without_trailing_slash); destination_buf[dst_without_trailing_slash.len] = std.fs.path.sep; } var template_path_buf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; - const src_without_trailing_slash: if (Environment.isWindows) string else void = if (comptime Environment.isWindows) - strings.withoutTrailingSlash(abs_template_path) - else {}; + const src_without_trailing_slash: if (Environment.isWindows) string else void = if (comptime Environment.isWindows) strings.withoutTrailingSlash(abs_template_path); if (comptime Environment.isWindows) { strings.copyU8IntoU16(&template_path_buf, src_without_trailing_slash); template_path_buf[src_without_trailing_slash.len] = std.fs.path.sep; @@ -598,10 +593,10 @@ pub const CreateCommand = struct { &walker_, node, &progress, - if (comptime Environment.isWindows) dst_without_trailing_slash.len + 1 else {}, - if (comptime Environment.isWindows) &destination_buf else {}, - if (comptime Environment.isWindows) src_without_trailing_slash.len + 1 else {}, - if (comptime Environment.isWindows) &template_path_buf else {}, + if (comptime Environment.isWindows) dst_without_trailing_slash.len + 1, + if (comptime Environment.isWindows) &destination_buf, + if (comptime Environment.isWindows) src_without_trailing_slash.len + 1, + if (comptime Environment.isWindows) &template_path_buf, ); package_json_file = destination_dir.openFile("package.json", .{ .mode = .read_write }) catch null; @@ -1518,7 +1513,7 @@ pub const CreateCommand = struct { .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), - } else {}, + }, }); _ = try process.unwrap(); } @@ -1958,7 +1953,7 @@ pub const Example = struct { } } - const http_proxy: ?URL = env_loader.getHttpProxy(api_url); + const http_proxy: ?URL = env_loader.getHttpProxyFor(api_url); const mutable = try ctx.allocator.create(MutableString); mutable.* = try MutableString.init(ctx.allocator, 8192); @@ -2037,7 +2032,7 @@ pub const Example = struct { url = URL.parse(try std.fmt.bufPrint(&url_buf, "https://registry.npmjs.org/@bun-examples/{s}/latest", .{name})); - var http_proxy: ?URL = env_loader.getHttpProxy(url); + var http_proxy: ?URL = env_loader.getHttpProxyFor(url); // ensure very stable memory address var async_http: *HTTP.AsyncHTTP = ctx.allocator.create(HTTP.AsyncHTTP) catch unreachable; @@ -2120,7 +2115,7 @@ pub const Example = struct { // ensure very stable memory address const parsed_tarball_url = URL.parse(tarball_url); - http_proxy = env_loader.getHttpProxy(parsed_tarball_url); + http_proxy = env_loader.getHttpProxyFor(parsed_tarball_url); async_http.* = HTTP.AsyncHTTP.initSync( ctx.allocator, @@ -2158,7 +2153,7 @@ pub const Example = struct { pub fn fetchAll(ctx: Command.Context, env_loader: *DotEnv.Loader, progress_node: ?*Progress.Node) ![]Example { url = URL.parse(examples_url); - const http_proxy: ?URL = env_loader.getHttpProxy(url); + const http_proxy: ?URL = env_loader.getHttpProxyFor(url); var async_http: *HTTP.AsyncHTTP = ctx.allocator.create(HTTP.AsyncHTTP) catch unreachable; const mutable = try ctx.allocator.create(MutableString); diff --git a/src/cli/exec_command.zig b/src/cli/exec_command.zig index 5ea2fd36cc..13a76166c4 100644 --- a/src/cli/exec_command.zig +++ b/src/cli/exec_command.zig @@ -16,7 +16,7 @@ pub const ExecCommand = struct { pub fn exec(ctx: Command.Context) !void { const script = ctx.positionals[1]; // this is a hack: make dummy bundler so we can use its `.runEnvLoader()` function to populate environment variables probably should split out the functionality - var bundle = try bun.Bundler.init( + var bundle = try bun.Transpiler.init( ctx.allocator, ctx.log, try @import("../bun.js/config.zig").configureTransformOptionsForBunVM(ctx.allocator, ctx.args), @@ -29,7 +29,7 @@ pub const ExecCommand = struct { const cwd = switch (bun.sys.getcwd(&buf)) { .result => |p| p, .err => |e| { - Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ script, e.toSystemError() }); + Output.err(e, "failed to run script {s}", .{script}); Global.exit(1); }, }; @@ -39,8 +39,8 @@ pub const ExecCommand = struct { }; const script_path = bun.path.join(parts, .auto); - const code = bun.shell.Interpreter.initAndRunFromSource(ctx, mini, script_path, script) catch |err| { - Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ script_path, @errorName(err) }); + const code = bun.shell.Interpreter.initAndRunFromSource(ctx, mini, script_path, script, null) catch |err| { + Output.err(err, "failed to run script {s}", .{script_path}); Global.exit(1); }; diff --git a/src/cli/filter_arg.zig b/src/cli/filter_arg.zig index 99031058d5..2bf9df67a9 100644 --- a/src/cli/filter_arg.zig +++ b/src/cli/filter_arg.zig @@ -34,7 +34,7 @@ fn globIgnoreFn(val: []const u8) bool { return false; } -const GlobWalker = Glob.GlobWalker_(globIgnoreFn, Glob.DirEntryAccessor, false); +const GlobWalker = Glob.GlobWalker(globIgnoreFn, Glob.walk.DirEntryAccessor, false); pub fn getCandidatePackagePatterns(allocator: std.mem.Allocator, log: *bun.logger.Log, out_patterns: *std.ArrayList([]u8), workdir_: []const u8, root_buf: *bun.PathBuffer) ![]const u8 { bun.JSAst.Expr.Data.Store.create(); @@ -187,7 +187,7 @@ pub const FilterSet = struct { pub fn matchesPath(self: *const FilterSet, path: []const u8) bool { for (self.filters) |filter| { - if (Glob.matchImpl(filter.codepoints, path).matches()) { + if (Glob.walk.matchImpl(filter.codepoints, path).matches()) { return true; } } @@ -200,7 +200,7 @@ pub const FilterSet = struct { .name => name, .path => path, }; - if (Glob.matchImpl(filter.codepoints, target).matches()) { + if (Glob.walk.matchImpl(filter.codepoints, target).matches()) { return true; } } diff --git a/src/cli/filter_run.zig b/src/cli/filter_run.zig index aad627d3d2..ba1a00ed90 100644 --- a/src/cli/filter_run.zig +++ b/src/cli/filter_run.zig @@ -11,7 +11,7 @@ const SemverString = @import("../install/semver.zig").String; const CLI = bun.CLI; const Command = CLI.Command; -const bundler = bun.bundler; +const transpiler = bun.transpiler; const FilterArg = @import("filter_arg.zig"); @@ -29,6 +29,7 @@ const ScriptConfig = struct { // ../../node_modules/.bin // and so forth, in addition to the user's $PATH. PATH: []const u8, + elide_count: ?usize, fn cmp(_: void, a: @This(), b: @This()) bool { return bun.strings.cmpStringsAsc({}, a.package_name, b.package_name); @@ -247,7 +248,7 @@ const State = struct { if (data[data.len - 1] == '\n') { data = data[0 .. data.len - 1]; } - if (max_lines == null) return .{ .content = data, .elided_count = 0 }; + if (max_lines == null or max_lines.? == 0) return .{ .content = data, .elided_count = 0 }; var i: usize = data.len; var lines: usize = 0; while (i > 0) : (i -= 1) { @@ -282,7 +283,9 @@ const State = struct { } for (this.handles) |*handle| { // normally we truncate the output to 10 lines, but on abort we print everything to aid debugging - const e = elide(handle.buffer.items, if (is_abort) null else 10); + const elide_lines = if (is_abort) null else handle.config.elide_count orelse 10; + const e = elide(handle.buffer.items, elide_lines); + try this.draw_buf.writer().print(fmt("{s} {s} $ {s}\n"), .{ handle.config.package_name, handle.config.script_name, handle.config.script_content }); if (e.elided_count > 0) { try this.draw_buf.writer().print( @@ -459,8 +462,8 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn { var root_buf: bun.PathBuffer = undefined; const resolve_root = try FilterArg.getCandidatePackagePatterns(ctx.allocator, ctx.log, &patterns, fsinstance.top_level_dir, &root_buf); - var this_bundler: bundler.Bundler = undefined; - _ = try RunCommand.configureEnvForRun(ctx, &this_bundler, null, true, false); + var this_transpiler: transpiler.Transpiler = undefined; + _ = try RunCommand.configureEnvForRun(ctx, &this_transpiler, null, true, false); var package_json_iter = try FilterArg.PackageFilterIterator.init(ctx.allocator, patterns.items, resolve_root); defer package_json_iter.deinit(); @@ -472,7 +475,7 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn { const dirpath = std.fs.path.dirname(package_json_path) orelse Global.crash(); const path = bun.strings.withoutTrailingSlash(dirpath); - const pkgjson = bun.PackageJSON.parse(&this_bundler.resolver, dirpath, .zero, null, .include_scripts, .main, .no_hash) orelse { + const pkgjson = bun.PackageJSON.parse(&this_transpiler.resolver, dirpath, .zero, null, .include_scripts, .main, .no_hash) orelse { Output.warn("Failed to read package.json\n", .{}); continue; }; @@ -482,7 +485,7 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn { if (!filter_instance.matches(path, pkgjson.name)) continue; - const PATH = try RunCommand.configurePathForRunWithPackageJsonDir(ctx, dirpath, &this_bundler, null, dirpath, ctx.debug.run_in_bun); + const PATH = try RunCommand.configurePathForRunWithPackageJsonDir(ctx, dirpath, &this_transpiler, null, dirpath, ctx.debug.run_in_bun); for (&[3][]const u8{ pre_script_name, script_name, post_script_name }) |name| { const original_content = pkgscripts.get(name) orelse continue; @@ -513,6 +516,7 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn { .combined = copy_script.items[0 .. copy_script.items.len - 1 :0], .deps = pkgjson.dependencies, .PATH = PATH, + .elide_count = ctx.bundler_options.elide_lines, }); } } @@ -522,9 +526,9 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn { Global.exit(1); } - const event_loop = bun.JSC.MiniEventLoop.initGlobal(this_bundler.env); + const event_loop = bun.JSC.MiniEventLoop.initGlobal(this_transpiler.env); const shell_bin: [:0]const u8 = if (Environment.isPosix) - RunCommand.findShell(this_bundler.env.get("PATH") orelse "", fsinstance.top_level_dir) orelse return error.MissingShell + RunCommand.findShell(this_transpiler.env.get("PATH") orelse "", fsinstance.top_level_dir) orelse return error.MissingShell else bun.selfExePath() catch return error.MissingShell; @@ -533,9 +537,15 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn { .event_loop = event_loop, .pretty_output = if (Environment.isWindows) windowsIsTerminal() else Output.enable_ansi_colors_stdout, .shell_bin = shell_bin, - .env = this_bundler.env, + .env = this_transpiler.env, }; + // Check if elide-lines is used in a non-terminal environment + if (ctx.bundler_options.elide_lines != null and !state.pretty_output) { + Output.prettyErrorln("error: --elide-lines is only supported in terminal environments", .{}); + Global.exit(1); + } + // initialize the handles var map = bun.StringHashMap(std.ArrayList(*ProcessHandle)).init(ctx.allocator); for (scripts.items, 0..) |*script, i| { @@ -547,7 +557,7 @@ pub fn runScriptsWithFilter(ctx: Command.Context) !noreturn { .stdout = if (Environment.isPosix) .buffer else .{ .buffer = try bun.default_allocator.create(bun.windows.libuv.Pipe) }, .stderr = if (Environment.isPosix) .buffer else .{ .buffer = try bun.default_allocator.create(bun.windows.libuv.Pipe) }, .cwd = std.fs.path.dirname(script.package_json_path) orelse "", - .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(event_loop) } else {}, + .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(event_loop) }, .stream = true, }, }; diff --git a/src/cli/README-for-init.md b/src/cli/init/README.default.md similarity index 100% rename from src/cli/README-for-init.md rename to src/cli/init/README.default.md diff --git a/src/cli/gitignore-for-init b/src/cli/init/gitignore.default similarity index 100% rename from src/cli/gitignore-for-init rename to src/cli/init/gitignore.default diff --git a/src/cli/tsconfig-for-init.json b/src/cli/init/tsconfig.default.json similarity index 100% rename from src/cli/tsconfig-for-init.json rename to src/cli/init/tsconfig.default.json diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 86f6efd224..5cbb0ad95c 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -40,8 +40,7 @@ pub const InitCommand = struct { // unset `ENABLE_VIRTUAL_TERMINAL_INPUT` on windows. This prevents backspace from // deleting the entire line const original_mode: if (Environment.isWindows) ?bun.windows.DWORD else void = if (comptime Environment.isWindows) - bun.win32.unsetStdioModeFlags(0, bun.windows.ENABLE_VIRTUAL_TERMINAL_INPUT) catch null - else {}; + bun.win32.unsetStdioModeFlags(0, bun.windows.ENABLE_VIRTUAL_TERMINAL_INPUT) catch null; defer if (comptime Environment.isWindows) { if (original_mode) |mode| { @@ -60,9 +59,57 @@ pub const InitCommand = struct { } } - const default_gitignore = @embedFile("gitignore-for-init"); - const default_tsconfig = @embedFile("tsconfig-for-init.json"); - const README = @embedFile("README-for-init.md"); + const Assets = struct { + // "known" assets + const @".gitignore" = @embedFile("init/gitignore.default"); + const @"tsconfig.json" = @embedFile("init/tsconfig.default.json"); + const @"README.md" = @embedFile("init/README.default.md"); + + /// Create a new asset file, overriding anything that already exists. Known + /// assets will have their contents pre-populated; otherwise the file will be empty. + fn create(comptime asset_name: []const u8, args: anytype) !void { + const is_template = comptime (@TypeOf(args) != @TypeOf(null)) and @typeInfo(@TypeOf(args)).Struct.fields.len > 0; + return createFull(asset_name, asset_name, "", is_template, args); + } + + fn createNew(filename: []const u8, contents: []const u8) !void { + var file = try std.fs.cwd().createFile(filename, .{ .truncate = true }); + defer file.close(); + try file.writeAll(contents); + + Output.prettyln(" + {s}", .{filename}); + Output.flush(); + } + + fn createFull( + /// name of possibly-existing asset + comptime asset_name: []const u8, + /// name of asset file to create + filename: []const u8, + /// optionally add a suffix to the end of the `+ filename` message. Must have a leading space. + comptime message_suffix: []const u8, + /// Treat the asset as a format string, using `args` to populate it. Only applies to known assets. + comptime is_template: bool, + /// Format arguments + args: anytype, + ) !void { + var file = try std.fs.cwd().createFile(filename, .{ .truncate = true }); + defer file.close(); + + // Write contents of known assets to the new file. Template assets get formatted. + if (comptime @hasDecl(Assets, asset_name)) { + const asset = @field(Assets, asset_name); + if (comptime is_template) { + try file.writer().print(asset, args); + } else { + try file.writeAll(asset); + } + } + + Output.prettyln(" + {s}{s}", .{ filename, message_suffix }); + Output.flush(); + } + }; // TODO: unicode case folding fn normalizePackageName(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { @@ -381,21 +428,15 @@ pub const InitCommand = struct { } } - var entry = try cwd.createFile(fields.entry_point, .{ .truncate = true }); - entry.writeAll("console.log(\"Hello via Bun!\");") catch {}; - entry.close(); - Output.prettyln(" + {s}", .{fields.entry_point}); - Output.flush(); + Assets.createNew(fields.entry_point, "console.log(\"Hello via Bun!\");") catch { + // suppress + }; } if (steps.write_gitignore) { - brk: { - var file = std.fs.cwd().createFileZ(".gitignore", .{ .truncate = true }) catch break :brk; - defer file.close(); - file.writeAll(default_gitignore) catch break :brk; - Output.prettyln(" + .gitignore", .{}); - Output.flush(); - } + Assets.create(".gitignore", .{}) catch { + // suppressed + }; } if (steps.write_tsconfig) { @@ -406,27 +447,18 @@ pub const InitCommand = struct { "tsconfig.json" else "jsconfig.json"; - var file = std.fs.cwd().createFileZ(filename, .{ .truncate = true }) catch break :brk; - defer file.close(); - file.writeAll(default_tsconfig) catch break :brk; - Output.prettyln(" + {s} (for editor auto-complete)", .{filename}); - Output.flush(); + Assets.createFull("tsconfig.json", filename, " (for editor autocomplete)", false, .{}) catch break :brk; } } if (steps.write_readme) { - brk: { - const filename = "README.md"; - var file = std.fs.cwd().createFileZ(filename, .{ .truncate = true }) catch break :brk; - defer file.close(); - file.writer().print(README, .{ - .name = fields.name, - .bunVersion = Environment.version_string, - .entryPoint = fields.entry_point, - }) catch break :brk; - Output.prettyln(" + {s}", .{filename}); - Output.flush(); - } + Assets.create("README.md", .{ + .name = fields.name, + .bunVersion = Environment.version_string, + .entryPoint = fields.entry_point, + }) catch { + // suppressed + }; } if (fields.entry_point.len > 0) { @@ -435,7 +467,7 @@ pub const InitCommand = struct { " \"'", fields.entry_point, )) { - Output.prettyln(" bun run {any}", .{bun.fmt.formatJSONString(fields.entry_point)}); + Output.prettyln(" bun run {any}", .{bun.fmt.formatJSONStringLatin1(fields.entry_point)}); } else { Output.prettyln(" bun run {s}", .{fields.entry_point}); } diff --git a/src/cli/install_completions_command.zig b/src/cli/install_completions_command.zig index 575c79f354..ed9e5d02ac 100644 --- a/src/cli/install_completions_command.zig +++ b/src/cli/install_completions_command.zig @@ -24,7 +24,6 @@ const Api = @import("../api/schema.zig").Api; const resolve_path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; const Command = @import("../cli.zig").Command; -const bundler = bun.bundler; const fs = @import("../fs.zig"); const URL = @import("../url.zig").URL; @@ -36,7 +35,7 @@ const DotEnv = @import("../env_loader.zig"); const NPMClient = @import("../which_npm_client.zig").NPMClient; const which = @import("../which.zig").which; const clap = bun.clap; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const Headers = bun.http.Headers; const CopyFile = @import("../copy_file.zig"); const ShellCompletions = @import("./shell_completions.zig"); diff --git a/src/cli/list-of-yarn-commands.zig b/src/cli/list-of-yarn-commands.zig index 12d0d23a2e..647cf2d29d 100644 --- a/src/cli/list-of-yarn-commands.zig +++ b/src/cli/list-of-yarn-commands.zig @@ -1,109 +1,77 @@ const std = @import("std"); const bun = @import("root").bun; -// yarn v2.3 commands -const yarn_v2 = [_][]const u8{ - "add", - "bin", - "cache", - "config", - "dedupe", - "dlx", - "exec", - "explain", - "info", - "init", - "install", - "link", - "node", - "npm", - "pack", - "patch", - "plugin", - "rebuild", - "remove", - "run", - "set", - "unplug", - "up", - "why", - "workspace", - "workspaces", -}; +pub const all_yarn_commands = bun.ComptimeStringMap(void, .{ + // yarn v2.3 commands + .{"add"}, + .{"bin"}, + .{"cache"}, + .{"config"}, + .{"dedupe"}, + .{"dlx"}, + .{"exec"}, + .{"explain"}, + .{"info"}, + .{"init"}, + .{"install"}, + .{"link"}, + .{"node"}, + .{"npm"}, + .{"pack"}, + .{"patch"}, + .{"plugin"}, + .{"rebuild"}, + .{"remove"}, + .{"run"}, + .{"set"}, + .{"unplug"}, + .{"up"}, + .{"why"}, + .{"workspace"}, + .{"workspaces"}, -// yarn v1 commands -const yarn_v1 = [_][]const u8{ - "access", - "add", - "audit", - "autoclean", - "bin", - "cache", - "check", - "config", - "create", - "exec", - "generate-lock-entry", - "generateLockEntry", - "global", - "help", - "import", - "info", - "init", - "install", - "licenses", - "link", - "list", - "login", - "logout", - "node", - "outdated", - "owner", - "pack", - "policies", - "publish", - "remove", - "run", - "tag", - "team", - "unlink", - "unplug", - "upgrade", - "upgrade-interactive", - "upgradeInteractive", - "version", - "versions", - "why", - "workspace", - "workspaces", -}; - -pub const all_yarn_commands = brk: { - @setEvalBranchQuota(9999); - var array: [yarn_v2.len + yarn_v1.len]u64 = undefined; - var array_i: usize = 0; - for (yarn_v2) |yarn| { - const hash = bun.hash(yarn); - @setEvalBranchQuota(9999); - if (std.mem.indexOfScalar(u64, array[0..array_i], hash) == null) { - @setEvalBranchQuota(9999); - array[array_i] = hash; - array_i += 1; - } - } - - for (yarn_v1) |yarn| { - @setEvalBranchQuota(9999); - - const hash = bun.hash(yarn); - if (std.mem.indexOfScalar(u64, array[0..array_i], hash) == null) { - @setEvalBranchQuota(9999); - - array[array_i] = hash; - array_i += 1; - } - } - - const final = array[0..array_i].*; - break :brk &final; -}; + // yarn v1 commands + .{"access"}, + .{"add"}, + .{"audit"}, + .{"autoclean"}, + .{"bin"}, + .{"cache"}, + .{"check"}, + .{"config"}, + .{"create"}, + .{"exec"}, + .{"generate-lock-entry"}, + .{"generateLockEntry"}, + .{"global"}, + .{"help"}, + .{"import"}, + .{"info"}, + .{"init"}, + .{"install"}, + .{"licenses"}, + .{"link"}, + .{"list"}, + .{"login"}, + .{"logout"}, + .{"node"}, + .{"outdated"}, + .{"owner"}, + .{"pack"}, + .{"policies"}, + .{"publish"}, + .{"remove"}, + .{"run"}, + .{"tag"}, + .{"team"}, + .{"unlink"}, + .{"unplug"}, + .{"upgrade"}, + .{"upgrade-interactive"}, + .{"upgradeInteractive"}, + .{"version"}, + .{"versions"}, + .{"why"}, + .{"workspace"}, + .{"workspaces"}, +}); diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index 0a951002ba..af827fcbcb 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -18,6 +18,8 @@ const FileSystem = bun.fs.FileSystem; const path = bun.path; const glob = bun.glob; const Table = bun.fmt.Table; +const WorkspaceFilter = PackageManager.WorkspaceFilter; +const OOM = bun.OOM; pub const OutdatedCommand = struct { pub fn exec(ctx: Command.Context) !void { @@ -44,11 +46,10 @@ pub const OutdatedCommand = struct { } fn outdated(ctx: Command.Context, original_cwd: string, manager: *PackageManager, comptime log_level: PackageManager.Options.LogLevel) !void { - const load_lockfile_result = manager.lockfile.loadFromDisk( + const load_lockfile_result = manager.lockfile.loadFromCwd( manager, manager.allocator, manager.log, - manager.options.lockfile_path, true, ); @@ -139,7 +140,7 @@ pub const OutdatedCommand = struct { original_cwd: string, manager: *PackageManager, filters: []const string, - ) error{OutOfMemory}![]const PackageID { + ) OOM![]const PackageID { const lockfile = manager.lockfile; const packages = lockfile.packages.slice(); const pkg_names = packages.items(.name); @@ -153,36 +154,10 @@ pub const OutdatedCommand = struct { } const converted_filters = converted_filters: { - const buf = try allocator.alloc(FilterType, filters.len); + const buf = try allocator.alloc(WorkspaceFilter, filters.len); + var path_buf: bun.PathBuffer = undefined; for (filters, buf) |filter, *converted| { - if ((filter.len == 1 and filter[0] == '*') or strings.eqlComptime(filter, "**")) { - converted.* = .all; - continue; - } - - const is_path = filter.len > 0 and filter[0] == '.'; - - const joined_filter = if (is_path) - strings.withoutTrailingSlash(path.joinAbsString(original_cwd, &[_]string{filter}, .posix)) - else - filter; - - if (joined_filter.len == 0) { - converted.* = FilterType.init(&.{}, is_path); - continue; - } - - const length = bun.simdutf.length.utf32.from.utf8.le(joined_filter); - const convert_buf = try allocator.alloc(u32, length); - - const convert_result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(joined_filter, convert_buf); - if (!convert_result.isSuccessful()) { - // nothing would match - converted.* = FilterType.init(&.{}, false); - continue; - } - - converted.* = FilterType.init(convert_buf[0..convert_result.count], is_path); + converted.* = try WorkspaceFilter.init(allocator, filter, original_cwd, &path_buf); } break :converted_filters buf; }; @@ -213,14 +188,14 @@ pub const OutdatedCommand = struct { const abs_res_path = path.joinAbsString(FileSystem.instance.top_level_dir, &[_]string{res_path}, .posix); - if (!glob.matchImpl(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { + if (!glob.walk.matchImpl(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); - if (!glob.matchImpl(pattern, name).matches()) { + if (!glob.walk.matchImpl(pattern, name).matches()) { break :matched false; } }, @@ -332,7 +307,7 @@ pub const OutdatedCommand = struct { .path => unreachable, .name => |name_pattern| { if (name_pattern.len == 0) continue; - if (!glob.matchImpl(name_pattern, dep.name.slice(string_buf)).matches()) { + if (!glob.walk.matchImpl(name_pattern, dep.name.slice(string_buf)).matches()) { break :match false; } }, @@ -454,10 +429,10 @@ pub const OutdatedCommand = struct { for (workspace_pkg_ids) |workspace_pkg_id| { inline for ( .{ - Behavior{ .normal = true }, - Behavior{ .dev = true }, - Behavior{ .peer = true }, - Behavior{ .optional = true }, + Behavior.prod, + Behavior.dev, + Behavior.peer, + Behavior.optional, }, ) |group_behavior| { for (outdated_ids.items) |ids| { @@ -466,7 +441,7 @@ pub const OutdatedCommand = struct { const dep_id = ids.dep_id; const dep = dependencies[dep_id]; - if (@as(u8, @bitCast(group_behavior)) & @as(u8, @bitCast(dep.behavior)) == 0) continue; + if (!dep.behavior.includes(group_behavior)) continue; const package_name = pkg_names[package_id].slice(string_buf); const resolution = pkg_resolutions[package_id]; diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index 1ad073b265..7a89ca9fbb 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -102,11 +102,10 @@ pub const PackCommand = struct { Output.flush(); var lockfile: Lockfile = undefined; - const load_from_disk_result = lockfile.loadFromDisk( + const load_from_disk_result = lockfile.loadFromCwd( manager, manager.allocator, manager.log, - manager.options.lockfile_path, false, ); @@ -336,7 +335,7 @@ pub const PackCommand = struct { // normally the behavior of `index.js` and `**/index.js` are the same, // but includes require `**/` const match_path = if (include.@"leading **/") entry_name else entry_subpath; - switch (glob.matchImpl(include.glob, match_path)) { + switch (glob.walk.matchImpl(include.glob, match_path)) { .match => included = true, .negate_no_match => included = false, @@ -509,10 +508,10 @@ pub const PackCommand = struct { var bundled_pack_queue = PackQueue.init(ctx.allocator, {}); if (ctx.bundled_deps.items.len == 0) return bundled_pack_queue; - const dir = root_dir.openDirZ("node_modules", .{ .iterate = true }) catch |err| { + var dir = root_dir.openDirZ("node_modules", .{ .iterate = true }) catch |err| { switch (err) { - // ignore node_modules if it isn't a directory - error.NotDir => return bundled_pack_queue, + // ignore node_modules if it isn't a directory, or doesn't exist + error.NotDir, error.FileNotFound => return bundled_pack_queue, else => { Output.err(err, "failed to open \"node_modules\" to pack bundled dependencies", .{}); @@ -520,6 +519,7 @@ pub const PackCommand = struct { }, } }; + defer dir.close(); // A set of bundled dependency locations // - node_modules/is-even @@ -536,34 +536,77 @@ pub const PackCommand = struct { while (iter.next().unwrap() catch null) |entry| { if (entry.kind != .directory) continue; - const entry_name = entry.name.slice(); + const _entry_name = entry.name.slice(); - for (ctx.bundled_deps.items) |*dep| { - bun.assertWithLocation(dep.from_root_package_json, @src()); - if (!strings.eqlLong(entry_name, dep.name, true)) continue; + if (strings.startsWithChar(_entry_name, '@')) { + const concat = try entrySubpath(ctx.allocator, "node_modules", _entry_name); - const entry_subpath = try entrySubpath(ctx.allocator, "node_modules", entry_name); + var scoped_dir = root_dir.openDirZ(concat, .{ .iterate = true }) catch { + continue; + }; + defer scoped_dir.close(); - const dedupe_entry = try dedupe.getOrPut(entry_subpath); - if (dedupe_entry.found_existing) { - // already got to it in `addBundledDep` below + var scoped_iter = DirIterator.iterate(scoped_dir, .u8); + while (scoped_iter.next().unwrap() catch null) |sub_entry| { + const entry_name = try entrySubpath(ctx.allocator, _entry_name, sub_entry.name.slice()); + + for (ctx.bundled_deps.items) |*dep| { + bun.assertWithLocation(dep.from_root_package_json, @src()); + if (!strings.eqlLong(entry_name, dep.name, true)) continue; + + const entry_subpath = try entrySubpath(ctx.allocator, "node_modules", entry_name); + + const dedupe_entry = try dedupe.getOrPut(entry_subpath); + if (dedupe_entry.found_existing) { + // already got to it in `addBundledDep` below + dep.was_packed = true; + break; + } + + const subdir = openSubdir(dir, entry_name, entry_subpath); + dep.was_packed = true; + try addBundledDep( + ctx, + root_dir, + .{ subdir, entry_subpath, 2 }, + &bundled_pack_queue, + &dedupe, + &additional_bundled_deps, + log_level, + ); + + break; + } + } + } else { + const entry_name = _entry_name; + for (ctx.bundled_deps.items) |*dep| { + bun.assertWithLocation(dep.from_root_package_json, @src()); + if (!strings.eqlLong(entry_name, dep.name, true)) continue; + + const entry_subpath = try entrySubpath(ctx.allocator, "node_modules", entry_name); + + const dedupe_entry = try dedupe.getOrPut(entry_subpath); + if (dedupe_entry.found_existing) { + // already got to it in `addBundledDep` below + dep.was_packed = true; + break; + } + + const subdir = openSubdir(dir, entry_name, entry_subpath); dep.was_packed = true; + try addBundledDep( + ctx, + root_dir, + .{ subdir, entry_subpath, 2 }, + &bundled_pack_queue, + &dedupe, + &additional_bundled_deps, + log_level, + ); + break; } - - const subdir = openSubdir(dir, entry_name, entry_subpath); - dep.was_packed = true; - try addBundledDep( - ctx, - root_dir, - .{ subdir, entry_subpath, 2 }, - &bundled_pack_queue, - &dedupe, - &additional_bundled_deps, - log_level, - ); - - break; } } @@ -836,22 +879,47 @@ pub const PackCommand = struct { const bundled_deps = json.get(field) orelse return null; invalid_field: { - var iter = bundled_deps.asArray() orelse switch (bundled_deps.data) { - .e_array => return .{}, + switch (bundled_deps.data) { + .e_array => { + var iter = bundled_deps.asArray() orelse return .{}; + + while (iter.next()) |bundled_dep_item| { + const bundled_dep = try bundled_dep_item.asStringCloned(allocator) orelse break :invalid_field; + try deps.append(allocator, .{ + .name = bundled_dep, + .from_root_package_json = true, + }); + } + }, + .e_boolean => { + const b = bundled_deps.asBool() orelse return .{}; + if (!b == true) return .{}; + + if (json.get("dependencies")) |dependencies_expr| { + switch (dependencies_expr.data) { + .e_object => |dependencies| { + for (dependencies.properties.slice()) |*dependency| { + if (dependency.key == null) continue; + if (dependency.value == null) continue; + + const bundled_dep = try dependency.key.?.asStringCloned(allocator) orelse break :invalid_field; + try deps.append(allocator, .{ + .name = bundled_dep, + .from_root_package_json = true, + }); + } + }, + else => {}, + } + } + }, else => break :invalid_field, - }; - while (iter.next()) |bundled_dep_item| { - const bundled_dep = try bundled_dep_item.asStringCloned(allocator) orelse break :invalid_field; - try deps.append(allocator, .{ - .name = bundled_dep, - .from_root_package_json = true, - }); } return deps; } - Output.errGeneric("expected `{s}` to be an array of strings", .{field}); + Output.errGeneric("expected `{s}` to be a boolean or an array of strings", .{field}); Global.crash(); } @@ -977,7 +1045,7 @@ pub const PackCommand = struct { // check default ignores that only apply to the root project directory for (root_default_ignore_patterns) |pattern| { - switch (glob.matchImpl(pattern, entry_name)) { + switch (glob.walk.matchImpl(pattern, entry_name)) { .match => { // cannot be reversed return .{ @@ -1004,7 +1072,7 @@ pub const PackCommand = struct { for (default_ignore_patterns) |pattern_info| { const pattern, const can_override = pattern_info; - switch (glob.matchImpl(pattern, entry_name)) { + switch (glob.walk.matchImpl(pattern, entry_name)) { .match => { if (can_override) { ignored = true; @@ -1046,7 +1114,7 @@ pub const PackCommand = struct { if (pattern.dirs_only and entry.kind != .directory) continue; const match_path = if (pattern.rel_path) rel else entry_name; - switch (glob.matchImpl(pattern.glob, match_path)) { + switch (glob.walk.matchImpl(pattern.glob, match_path)) { .match => { ignored = true; ignore_pattern = pattern.glob; @@ -1141,11 +1209,11 @@ pub const PackCommand = struct { const edited_package_json = try editRootPackageJSON(ctx.allocator, ctx.lockfile, json); - var this_bundler: bun.bundler.Bundler = undefined; + var this_transpiler: bun.transpiler.Transpiler = undefined; _ = RunCommand.configureEnvForRun( ctx.command_ctx, - &this_bundler, + &this_transpiler, manager.env, manager.options.log_level != .silent, false, @@ -1178,7 +1246,7 @@ pub const PackCommand = struct { prepublish_only, "prepublishOnly", abs_workspace_path, - this_bundler.env, + this_transpiler.env, &.{}, manager.options.log_level == .silent, ctx.command_ctx.debug.use_system_shell, @@ -1203,7 +1271,7 @@ pub const PackCommand = struct { prepack_script_str, "prepack", abs_workspace_path, - this_bundler.env, + this_transpiler.env, &.{}, manager.options.log_level == .silent, ctx.command_ctx.debug.use_system_shell, @@ -1227,7 +1295,7 @@ pub const PackCommand = struct { prepare_script_str, "prepare", abs_workspace_path, - this_bundler.env, + this_transpiler.env, &.{}, manager.options.log_level == .silent, ctx.command_ctx.debug.use_system_shell, @@ -1394,7 +1462,7 @@ pub const PackCommand = struct { .uses_workspaces = false, .publish_script = publish_script, .postpublish_script = postpublish_script, - .script_env = this_bundler.env, + .script_env = this_transpiler.env, .normalized_pkg_info = "", }; } @@ -1720,7 +1788,7 @@ pub const PackCommand = struct { .uses_workspaces = false, .publish_script = publish_script, .postpublish_script = postpublish_script, - .script_env = this_bundler.env, + .script_env = this_transpiler.env, .normalized_pkg_info = normalized_pkg_info, }; } diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index 03080a5094..8a246ec74b 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -15,7 +15,7 @@ const PackageID = Install.PackageID; const DependencyID = Install.DependencyID; const PackageManager = Install.PackageManager; const Lockfile = @import("../install/lockfile.zig"); -const NodeModulesFolder = Lockfile.Tree.NodeModulesFolder; +const NodeModulesFolder = Lockfile.Tree.Iterator(.node_modules).Next; const Path = @import("../resolver/resolve_path.zig"); const String = @import("../install/semver.zig").String; const ArrayIdentityContext = bun.ArrayIdentityContext; @@ -26,6 +26,7 @@ const DefaultTrustedCommand = @import("./pm_trusted_command.zig").DefaultTrusted const Environment = bun.Environment; pub const PackCommand = @import("./pack_command.zig").PackCommand; const Npm = Install.Npm; +const File = bun.sys.File; const ByName = struct { dependencies: []const Dependency, @@ -41,7 +42,7 @@ const ByName = struct { }; pub const PackageManagerCommand = struct { - pub fn handleLoadLockfileErrors(load_lockfile: Lockfile.LoadFromDiskResult, pm: *PackageManager) void { + pub fn handleLoadLockfileErrors(load_lockfile: Lockfile.LoadResult, pm: *PackageManager) void { if (load_lockfile == .not_found) { if (pm.options.log_level != .silent) { Output.errGeneric("Lockfile not found", .{}); @@ -57,17 +58,20 @@ pub const PackageManagerCommand = struct { } } - pub fn printHash(ctx: Command.Context, lockfile_: []const u8) !void { + pub fn printHash(ctx: Command.Context, file: File) !void { @setCold(true); - var lockfile_buffer: bun.PathBuffer = undefined; - @memcpy(lockfile_buffer[0..lockfile_.len], lockfile_); - lockfile_buffer[lockfile_.len] = 0; - const lockfile = lockfile_buffer[0..lockfile_.len :0]; + const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .pm); var pm, const cwd = try PackageManager.init(ctx, cli, PackageManager.Subcommand.pm); defer ctx.allocator.free(cwd); - const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, lockfile, true); + const bytes = file.readToEnd(ctx.allocator).unwrap() catch |err| { + Output.err(err, "failed to read lockfile", .{}); + Global.crash(); + }; + + const load_lockfile = pm.lockfile.loadFromBytes(pm, bytes, ctx.allocator, ctx.log); + handleLoadLockfileErrors(load_lockfile, pm); Output.flush(); @@ -98,33 +102,49 @@ pub const PackageManagerCommand = struct { } pub fn printHelp() void { - Output.prettyln( - \\bun pm: Package manager utilities + + // the output of --help uses the following syntax highlighting + // template: Usage: bun [flags] [arguments] + // use [foo] for multiple arguments or flags for foo. + // use to emphasize 'bar' + + const intro_text = + \\Usage: bun pm [flags] [\] + \\ Run package manager utilities + ; + const outro_text = + \\Examples: \\ - \\ bun pm pack create a tarball of the current workspace - \\ --dry-run do everything except for writing the tarball to disk - \\ --destination the directory the tarball will be saved in - \\ --ignore-scripts don't run pre/postpack and prepare scripts - \\ --gzip-level specify a custom compression level for gzip (0-9, default is 9) - \\ bun pm bin print the path to bin folder - \\ -g print the global path to bin folder - \\ bun pm ls list the dependency tree according to the current lockfile - \\ --all list the entire dependency tree according to the current lockfile - \\ bun pm whoami print the current npm username - \\ bun pm hash generate & print the hash of the current lockfile - \\ bun pm hash-string print the string used to hash the lockfile - \\ bun pm hash-print print the hash stored in the current lockfile - \\ bun pm cache print the path to the cache folder - \\ bun pm cache rm clear the cache - \\ bun pm migrate migrate another package manager's lockfile without installing anything - \\ bun pm untrusted print current untrusted dependencies with scripts - \\ bun pm trust names ... run scripts for untrusted dependencies and add to `trustedDependencies` + \\ bun pm pack create a tarball of the current workspace + \\ --dry-run do everything except for writing the tarball to disk + \\ --destination the directory the tarball will be saved in + \\ --ignore-scripts don't run pre/postpack and prepare scripts + \\ --gzip-level specify a custom compression level for gzip (0-9, default is 9) + \\ bun pm bin print the path to bin folder + \\ -g print the global path to bin folder + \\ bun pm ls list the dependency tree according to the current lockfile + \\ --all list the entire dependency tree according to the current lockfile + \\ bun pm whoami print the current npm username + \\ bun pm hash generate & print the hash of the current lockfile + \\ bun pm hash-string print the string used to hash the lockfile + \\ bun pm hash-print print the hash stored in the current lockfile + \\ bun pm cache print the path to the cache folder + \\ bun pm cache rm clear the cache + \\ bun pm migrate migrate another package manager's lockfile without installing anything + \\ bun pm untrusted print current untrusted dependencies with scripts + \\ bun pm trust names ... run scripts for untrusted dependencies and add to `trustedDependencies` \\ --all trust all untrusted dependencies - \\ bun pm default-trusted print the default trusted dependencies list + \\ bun pm default-trusted print the default trusted dependencies list \\ \\Learn more about these at https://bun.sh/docs/cli/pm \\ - , .{}); + ; + + Output.pretty(intro_text, .{}); + Output.flush(); + Output.pretty("\n\n", .{}); + Output.pretty(outro_text, .{}); + Output.flush(); } pub fn exec(ctx: Command.Context) !void { @@ -198,7 +218,7 @@ pub const PackageManagerCommand = struct { Output.flush(); return; } else if (strings.eqlComptime(subcommand, "hash")) { - const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, "bun.lockb", true); + const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true); handleLoadLockfileErrors(load_lockfile, pm); _ = try pm.lockfile.hasMetaHashChanged(false, pm.lockfile.packages.len); @@ -209,7 +229,7 @@ pub const PackageManagerCommand = struct { Output.enableBuffering(); Global.exit(0); } else if (strings.eqlComptime(subcommand, "hash-print")) { - const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, "bun.lockb", true); + const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true); handleLoadLockfileErrors(load_lockfile, pm); Output.flush(); @@ -218,7 +238,7 @@ pub const PackageManagerCommand = struct { Output.enableBuffering(); Global.exit(0); } else if (strings.eqlComptime(subcommand, "hash-string")) { - const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, "bun.lockb", true); + const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true); handleLoadLockfileErrors(load_lockfile, pm); _ = try pm.lockfile.hasMetaHashChanged(true, pm.lockfile.packages.len); @@ -291,19 +311,19 @@ pub const PackageManagerCommand = struct { try TrustCommand.exec(ctx, pm, args); Global.exit(0); } else if (strings.eqlComptime(subcommand, "ls")) { - const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, "bun.lockb", true); + const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true); handleLoadLockfileErrors(load_lockfile, pm); Output.flush(); Output.disableBuffering(); const lockfile = load_lockfile.ok.lockfile; - var iterator = Lockfile.Tree.Iterator.init(lockfile); + var iterator = Lockfile.Tree.Iterator(.node_modules).init(lockfile); var max_depth: usize = 0; var directories = std.ArrayList(NodeModulesFolder).init(ctx.allocator); defer directories.deinit(); - while (iterator.nextNodeModulesFolder(null)) |node_modules| { + while (iterator.next(null)) |node_modules| { const path_len = node_modules.relative_path.len; const path = try ctx.allocator.alloc(u8, path_len + 1); bun.copy(u8, path, node_modules.relative_path); @@ -341,7 +361,7 @@ pub const PackageManagerCommand = struct { const resolutions = slice.items(.resolution); const root_deps = slice.items(.dependencies)[0]; - Output.println("{s} node_modules ({d})", .{ path, dependencies.len }); + Output.println("{s} node_modules ({d})", .{ path, lockfile.buffers.hoisted_dependencies.items.len }); const string_bytes = lockfile.buffers.string_bytes.items; const sorted_dependencies = try ctx.allocator.alloc(DependencyID, root_deps.len); defer ctx.allocator.free(sorted_dependencies); @@ -369,21 +389,29 @@ pub const PackageManagerCommand = struct { Global.exit(0); } else if (strings.eqlComptime(subcommand, "migrate")) { - if (!pm.options.enable.force_save_lockfile) try_load_bun: { - std.fs.cwd().accessZ("bun.lockb", .{ .mode = .read_only }) catch break :try_load_bun; + if (!pm.options.enable.force_save_lockfile) { + if (bun.sys.existsZ("bun.lock")) { + Output.prettyErrorln( + \\error: bun.lock already exists + \\run with --force to overwrite + , .{}); + Global.exit(1); + } - Output.prettyErrorln( - \\error: bun.lockb already exists - \\run with --force to overwrite - , .{}); - Global.exit(1); + if (bun.sys.existsZ("bun.lockb")) { + Output.prettyErrorln( + \\error: bun.lockb already exists + \\run with --force to overwrite + , .{}); + Global.exit(1); + } } const load_lockfile = @import("../install/migration.zig").detectAndLoadOtherLockfile( pm.lockfile, + bun.FD.cwd(), pm, ctx.allocator, pm.log, - pm.options.lockfile_path, ); if (load_lockfile == .not_found) { Output.prettyErrorln( @@ -393,7 +421,9 @@ pub const PackageManagerCommand = struct { } handleLoadLockfileErrors(load_lockfile, pm); const lockfile = load_lockfile.ok.lockfile; - lockfile.saveToDisk(pm.options.lockfile_path, pm.options.log_level.isVerbose()); + + const save_format: Lockfile.LoadResult.LockfileFormat = if (pm.options.save_text_lockfile) .text else .binary; + lockfile.saveToDisk(save_format, pm.options.log_level.isVerbose()); Global.exit(0); } diff --git a/src/cli/pm_trusted_command.zig b/src/cli/pm_trusted_command.zig index 4528ce8bfa..1341a9aa47 100644 --- a/src/cli/pm_trusted_command.zig +++ b/src/cli/pm_trusted_command.zig @@ -37,12 +37,11 @@ pub const UntrustedCommand = struct { Output.prettyError("bun pm untrusted v" ++ Global.package_json_version_with_sha ++ "\n\n", .{}); Output.flush(); - const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, "bun.lockb", true); + const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true); PackageManagerCommand.handleLoadLockfileErrors(load_lockfile, pm); try pm.updateLockfileIfNeeded(load_lockfile); const packages = pm.lockfile.packages.slice(); - const metas: []Lockfile.Package.Meta = packages.items(.meta); const scripts: []Lockfile.Package.Scripts = packages.items(.scripts); const resolutions: []Install.Resolution = packages.items(.resolution); const buf = pm.lockfile.buffers.string_bytes.items; @@ -59,10 +58,8 @@ pub const UntrustedCommand = struct { // called alias because a dependency name is not always the package name const alias = dep.name.slice(buf); - if (metas[package_id].hasInstallScript()) { - if (!pm.lockfile.hasTrustedDependency(alias)) { - try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); - } + if (!pm.lockfile.hasTrustedDependency(alias)) { + try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); } } @@ -74,7 +71,7 @@ pub const UntrustedCommand = struct { var untrusted_deps: std.AutoArrayHashMapUnmanaged(DependencyID, Lockfile.Package.Scripts.List) = .{}; defer untrusted_deps.deinit(ctx.allocator); - var tree_iterator = Lockfile.Tree.Iterator.init(pm.lockfile); + var tree_iterator = Lockfile.Tree.Iterator(.node_modules).init(pm.lockfile); const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; @@ -82,7 +79,7 @@ pub const UntrustedCommand = struct { try abs_node_modules_path.appendSlice(ctx.allocator, top_level_without_trailing_slash); try abs_node_modules_path.append(ctx.allocator, std.fs.path.sep); - while (tree_iterator.nextNodeModulesFolder(null)) |node_modules| { + while (tree_iterator.next(null)) |node_modules| { // + 1 because we want to keep the path separator abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); @@ -100,11 +97,11 @@ pub const UntrustedCommand = struct { const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; const resolution = &resolutions[package_id]; var package_scripts = scripts[package_id]; - + var not_lazy: PackageManager.LazyPackageDestinationDir = .{ .dir = node_modules_dir }; const maybe_scripts_list = package_scripts.getList( pm.log, pm.lockfile, - node_modules_dir, + ¬_lazy, abs_node_modules_path.items, alias, resolution, @@ -187,7 +184,7 @@ pub const TrustCommand = struct { if (args.len == 2) errorExpectedArgs(); - const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, "bun.lockb", true); + const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true); PackageManagerCommand.handleLoadLockfileErrors(load_lockfile, pm); try pm.updateLockfileIfNeeded(load_lockfile); @@ -203,7 +200,6 @@ pub const TrustCommand = struct { const buf = pm.lockfile.buffers.string_bytes.items; const packages = pm.lockfile.packages.slice(); - const metas: []Lockfile.Package.Meta = packages.items(.meta); const resolutions: []Install.Resolution = packages.items(.resolution); const scripts: []Lockfile.Package.Scripts = packages.items(.scripts); @@ -216,10 +212,8 @@ pub const TrustCommand = struct { const alias = dep.name.slice(buf); - if (metas[package_id].hasInstallScript()) { - if (!pm.lockfile.hasTrustedDependency(alias)) { - try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); - } + if (!pm.lockfile.hasTrustedDependency(alias)) { + try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); } } @@ -231,7 +225,7 @@ pub const TrustCommand = struct { // Instead of running them right away, we group scripts by depth in the node_modules // file structure, then run them starting at max depth. This ensures lifecycle scripts are run // in the correct order as they would during a normal install - var tree_iter = Lockfile.Tree.Iterator.init(pm.lockfile); + var tree_iter = Lockfile.Tree.Iterator(.node_modules).init(pm.lockfile); const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; @@ -248,7 +242,7 @@ pub const TrustCommand = struct { var scripts_count: usize = 0; - while (tree_iter.nextNodeModulesFolder(null)) |node_modules| { + while (tree_iter.next(null)) |node_modules| { abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); @@ -268,11 +262,11 @@ pub const TrustCommand = struct { } const resolution = &resolutions[package_id]; var package_scripts = scripts[package_id]; - + var not_lazy = PackageManager.LazyPackageDestinationDir{ .dir = node_modules_dir }; const maybe_scripts_list = package_scripts.getList( pm.log, pm.lockfile, - node_modules_dir, + ¬_lazy, abs_node_modules_path.items, alias, resolution, @@ -423,7 +417,14 @@ pub const TrustCommand = struct { try pm.lockfile.trusted_dependencies.?.put(ctx.allocator, @truncate(String.Builder.stringHash(name)), {}); } - pm.lockfile.saveToDisk(pm.options.lockfile_path, pm.options.log_level.isVerbose()); + const save_format: Lockfile.LoadResult.LockfileFormat = if (pm.options.save_text_lockfile) + .text + else switch (load_lockfile) { + .not_found => .binary, + .err => |err| err.format, + .ok => |ok| ok.format, + }; + pm.lockfile.saveToDisk(save_format, pm.options.log_level.isVerbose()); var buffer_writer = try bun.js_printer.BufferWriter.init(ctx.allocator); try buffer_writer.buffer.list.ensureTotalCapacity(ctx.allocator, package_json_contents.len + 1); diff --git a/src/cli/publish_command.zig b/src/cli/publish_command.zig index 466f7ef488..d1e26d06fc 100644 --- a/src/cli/publish_command.zig +++ b/src/cli/publish_command.zig @@ -53,8 +53,8 @@ pub const PublishCommand = struct { normalized_pkg_info: string, - publish_script: if (directory_publish) ?[]const u8 else void = if (directory_publish) null else {}, - postpublish_script: if (directory_publish) ?[]const u8 else void = if (directory_publish) null else {}, + publish_script: if (directory_publish) ?[]const u8 else void = if (directory_publish) null, + postpublish_script: if (directory_publish) ?[]const u8 else void = if (directory_publish) null, script_env: if (directory_publish) *DotEnv.Loader else void, const FromTarballError = OOM || error{ @@ -281,11 +281,10 @@ pub const PublishCommand = struct { manager: *PackageManager, ) FromWorkspaceError!Context(directory_publish) { var lockfile: Lockfile = undefined; - const load_from_disk_result = lockfile.loadFromDisk( + const load_from_disk_result = lockfile.loadFromCwd( manager, manager.allocator, manager.log, - manager.options.lockfile_path, false, ); @@ -684,8 +683,7 @@ pub const PublishCommand = struct { // unset `ENABLE_VIRTUAL_TERMINAL_INPUT` on windows. This prevents backspace from // deleting the entire line const original_mode: if (Environment.isWindows) ?bun.windows.DWORD else void = if (comptime Environment.isWindows) - bun.win32.unsetStdioModeFlags(0, bun.windows.ENABLE_VIRTUAL_TERMINAL_INPUT) catch null - else {}; + bun.win32.unsetStdioModeFlags(0, bun.windows.ENABLE_VIRTUAL_TERMINAL_INPUT) catch null; defer if (comptime Environment.isWindows) { if (original_mode) |mode| { diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 51fea5bed3..f0343fa121 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -33,7 +33,7 @@ const sync = @import("../sync.zig"); const Api = @import("../api/schema.zig").Api; const resolve_path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; -const bundler = bun.bundler; +const transpiler = bun.transpiler; const DotEnv = @import("../env_loader.zig"); const which = @import("../which.zig").which; @@ -46,7 +46,7 @@ const NpmArgs = struct { pub const package_version: string = "npm_package_version"; }; const PackageJSON = @import("../resolver/package_json.zig").PackageJSON; -const yarn_commands: []const u64 = @import("./list-of-yarn-commands.zig").all_yarn_commands; +const yarn_commands = @import("./list-of-yarn-commands.zig").all_yarn_commands; const ShellCompletions = @import("./shell_completions.zig"); const PosixSpawn = bun.posix.spawn; @@ -179,7 +179,7 @@ pub const RunCommand = struct { } // implicit yarn commands - if (std.mem.indexOfScalar(u64, yarn_commands, bun.hash(yarn_cmd)) == null) { + if (!yarn_commands.has(yarn_cmd)) { try copy_script.appendSlice(BUN_RUN); try copy_script.append(' '); try copy_script.appendSlice(yarn_cmd); @@ -231,6 +231,18 @@ pub const RunCommand = struct { delimiter = 0; continue; } + if (strings.hasPrefixComptime(script[start..], "pnpm dlx ")) { + try copy_script.appendSlice(BUN_BIN_NAME ++ " x "); + entry_i += "pnpm dlx ".len; + delimiter = 0; + continue; + } + if (strings.hasPrefixComptime(script[start..], "pnpx ")) { + try copy_script.appendSlice(BUN_BIN_NAME ++ " x "); + entry_i += "pnpx ".len; + delimiter = 0; + continue; + } } delimiter = 0; @@ -289,7 +301,7 @@ pub const RunCommand = struct { if (!use_system_shell) { const mini = bun.JSC.MiniEventLoop.initGlobal(env); - const code = bun.shell.Interpreter.initAndRunFromSource(ctx, mini, name, copy_script.items) catch |err| { + const code = bun.shell.Interpreter.initAndRunFromSource(ctx, mini, name, copy_script.items, cwd) catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); } @@ -337,7 +349,7 @@ pub const RunCommand = struct { .windows = if (Environment.isWindows) .{ .loop = JSC.EventLoopHandle.init(JSC.MiniEventLoop.initGlobal(env)), - } else {}, + }, }) catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); @@ -380,9 +392,8 @@ pub const RunCommand = struct { if (signal.valid() and signal != .SIGINT and !silent) { Output.prettyErrorln("error: script \"{s}\" was terminated by signal {}", .{ name, signal.fmt(Output.enable_ansi_colors_stderr) }); Output.flush(); - - Global.raiseIgnoringPanicHandler(signal); } + Global.raiseIgnoringPanicHandler(signal); }, .err => |err| { @@ -502,7 +513,7 @@ pub const RunCommand = struct { .windows = if (Environment.isWindows) .{ .loop = JSC.EventLoopHandle.init(JSC.MiniEventLoop.initGlobal(env)), - } else {}, + }, }) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); @@ -548,7 +559,7 @@ pub const RunCommand = struct { }, .signaled => |signal| { - if (!silent) { + if (signal.valid() and signal != .SIGINT and !silent) { Output.prettyErrorln("error: Failed to run \"{s}\" due to signal {s}", .{ basenameOrBun(executable), signal.name() orelse "unknown", @@ -616,13 +627,13 @@ pub const RunCommand = struct { pub fn ls(ctx: Command.Context) !void { const args = ctx.args; - var this_bundler = try bundler.Bundler.init(ctx.allocator, ctx.log, args, null); - this_bundler.options.env.behavior = Api.DotEnvBehavior.load_all; - this_bundler.options.env.prefix = ""; + var this_transpiler = try transpiler.Transpiler.init(ctx.allocator, ctx.log, args, null); + this_transpiler.options.env.behavior = Api.DotEnvBehavior.load_all; + this_transpiler.options.env.prefix = ""; - this_bundler.resolver.care_about_bin_folder = true; - this_bundler.resolver.care_about_scripts = true; - this_bundler.configureLinker(); + this_transpiler.resolver.care_about_bin_folder = true; + this_transpiler.resolver.care_about_scripts = true; + this_transpiler.configureLinker(); } pub const bun_node_dir = switch (Environment.os) { @@ -793,30 +804,30 @@ pub const RunCommand = struct { const DirInfo = @import("../resolver/dir_info.zig"); pub fn configureEnvForRun( ctx: Command.Context, - this_bundler: *bundler.Bundler, + this_transpiler: *transpiler.Transpiler, env: ?*DotEnv.Loader, log_errors: bool, store_root_fd: bool, ) !*DirInfo { const args = ctx.args; - this_bundler.* = try bundler.Bundler.init(ctx.allocator, ctx.log, args, env); - this_bundler.options.env.behavior = Api.DotEnvBehavior.load_all; - this_bundler.env.quiet = true; - this_bundler.options.env.prefix = ""; + this_transpiler.* = try transpiler.Transpiler.init(ctx.allocator, ctx.log, args, env); + this_transpiler.options.env.behavior = Api.DotEnvBehavior.load_all; + this_transpiler.env.quiet = true; + this_transpiler.options.env.prefix = ""; - this_bundler.resolver.care_about_bin_folder = true; - this_bundler.resolver.care_about_scripts = true; - this_bundler.resolver.store_fd = store_root_fd; + this_transpiler.resolver.care_about_bin_folder = true; + this_transpiler.resolver.care_about_scripts = true; + this_transpiler.resolver.store_fd = store_root_fd; - this_bundler.resolver.opts.load_tsconfig_json = false; - this_bundler.options.load_tsconfig_json = false; + this_transpiler.resolver.opts.load_tsconfig_json = true; + this_transpiler.options.load_tsconfig_json = true; - this_bundler.configureLinker(); + this_transpiler.configureLinker(); - const root_dir_info = this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch |err| { + const root_dir_info = this_transpiler.resolver.readDirInfo(this_transpiler.fs.top_level_dir) catch |err| { if (!log_errors) return error.CouldntReadCurrentDirectory; ctx.log.print(Output.errorWriter()) catch {}; - Output.prettyErrorln("error: {s} loading directory {}", .{ @errorName(err), bun.fmt.QuotedFormatter{ .text = this_bundler.fs.top_level_dir } }); + Output.prettyErrorln("error: {s} loading directory {}", .{ @errorName(err), bun.fmt.QuotedFormatter{ .text = this_transpiler.fs.top_level_dir } }); Output.flush(); return err; } orelse { @@ -826,26 +837,26 @@ pub const RunCommand = struct { return error.CouldntReadCurrentDirectory; }; - this_bundler.resolver.store_fd = false; + this_transpiler.resolver.store_fd = false; if (env == null) { - this_bundler.env.loadProcess(); + this_transpiler.env.loadProcess(); - if (this_bundler.env.get("NODE_ENV")) |node_env| { + if (this_transpiler.env.get("NODE_ENV")) |node_env| { if (strings.eqlComptime(node_env, "production")) { - this_bundler.options.production = true; + this_transpiler.options.production = true; } } - this_bundler.runEnvLoader(true) catch {}; + this_transpiler.runEnvLoader(true) catch {}; } - this_bundler.env.map.putDefault("npm_config_local_prefix", this_bundler.fs.top_level_dir) catch unreachable; + this_transpiler.env.map.putDefault("npm_config_local_prefix", this_transpiler.fs.top_level_dir) catch unreachable; // we have no way of knowing what version they're expecting without running the node executable // running the node executable is too slow // so we will just hardcode it to LTS - this_bundler.env.map.putDefault( + this_transpiler.env.map.putDefault( "npm_config_user_agent", // the use of npm/? is copying yarn // e.g. @@ -853,33 +864,33 @@ pub const RunCommand = struct { "bun/" ++ Global.package_json_version ++ " npm/? node/v" ++ Environment.reported_nodejs_version ++ " " ++ Global.os_name ++ " " ++ Global.arch_name, ) catch unreachable; - if (this_bundler.env.get("npm_execpath") == null) { + if (this_transpiler.env.get("npm_execpath") == null) { // we don't care if this fails if (bun.selfExePath()) |self_exe_path| { - this_bundler.env.map.putDefault("npm_execpath", self_exe_path) catch unreachable; + this_transpiler.env.map.putDefault("npm_execpath", self_exe_path) catch unreachable; } else |_| {} } if (root_dir_info.enclosing_package_json) |package_json| { if (package_json.name.len > 0) { - if (this_bundler.env.map.get(NpmArgs.package_name) == null) { - this_bundler.env.map.put(NpmArgs.package_name, package_json.name) catch unreachable; + if (this_transpiler.env.map.get(NpmArgs.package_name) == null) { + this_transpiler.env.map.put(NpmArgs.package_name, package_json.name) catch unreachable; } } - this_bundler.env.map.putDefault("npm_package_json", package_json.source.path.text) catch unreachable; + this_transpiler.env.map.putDefault("npm_package_json", package_json.source.path.text) catch unreachable; if (package_json.version.len > 0) { - if (this_bundler.env.map.get(NpmArgs.package_version) == null) { - this_bundler.env.map.put(NpmArgs.package_version, package_json.version) catch unreachable; + if (this_transpiler.env.map.get(NpmArgs.package_version) == null) { + this_transpiler.env.map.put(NpmArgs.package_version, package_json.version) catch unreachable; } } if (package_json.config) |config| { - try this_bundler.env.map.ensureUnusedCapacity(config.count()); + try this_transpiler.env.map.ensureUnusedCapacity(config.count()); for (config.keys(), config.values()) |k, v| { const key = try bun.strings.concat(bun.default_allocator, &.{ "npm_package_config_", k }); - this_bundler.env.map.putAssumeCapacity(key, v); + this_transpiler.env.map.putAssumeCapacity(key, v); } } } @@ -890,20 +901,20 @@ pub const RunCommand = struct { pub fn configurePathForRunWithPackageJsonDir( ctx: Command.Context, package_json_dir: string, - this_bundler: *bundler.Bundler, + this_transpiler: *transpiler.Transpiler, ORIGINAL_PATH: ?*string, cwd: string, force_using_bun: bool, ) ![]u8 { - const PATH = this_bundler.env.get("PATH") orelse ""; + const PATH = this_transpiler.env.get("PATH") orelse ""; if (ORIGINAL_PATH) |original_path| { original_path.* = PATH; } const bun_node_exe = try bunNodeFileUtf8(ctx.allocator); const bun_node_dir_win = bun.Dirname.dirname(u8, bun_node_exe) orelse return error.FailedToGetTempPath; - const found_node = this_bundler.env.loadNodeJSConfig( - this_bundler.fs, + const found_node = this_transpiler.env.loadNodeJSConfig( + this_transpiler.fs, if (force_using_bun) bun_node_exe else "", ) catch false; @@ -935,9 +946,9 @@ pub const RunCommand = struct { if (needs_to_force_bun) { createFakeTemporaryNodeExecutable(&new_path, &optional_bun_self_path) catch bun.outOfMemory(); if (!force_using_bun) { - this_bundler.env.map.put("NODE", bun_node_exe) catch bun.outOfMemory(); - this_bundler.env.map.put("npm_node_execpath", bun_node_exe) catch bun.outOfMemory(); - this_bundler.env.map.put("npm_execpath", optional_bun_self_path) catch bun.outOfMemory(); + this_transpiler.env.map.put("NODE", bun_node_exe) catch bun.outOfMemory(); + this_transpiler.env.map.put("npm_node_execpath", bun_node_exe) catch bun.outOfMemory(); + this_transpiler.env.map.put("npm_execpath", optional_bun_self_path) catch bun.outOfMemory(); } needs_to_force_bun = false; @@ -970,7 +981,7 @@ pub const RunCommand = struct { pub fn configurePathForRun( ctx: Command.Context, root_dir_info: *DirInfo, - this_bundler: *bundler.Bundler, + this_transpiler: *transpiler.Transpiler, ORIGINAL_PATH: ?*string, cwd: string, force_using_bun: bool, @@ -985,8 +996,8 @@ pub const RunCommand = struct { } } - const new_path = try configurePathForRunWithPackageJsonDir(ctx, package_json_dir, this_bundler, ORIGINAL_PATH, cwd, force_using_bun); - this_bundler.env.map.put("PATH", new_path) catch bun.outOfMemory(); + const new_path = try configurePathForRunWithPackageJsonDir(ctx, package_json_dir, this_transpiler, ORIGINAL_PATH, cwd, force_using_bun); + this_transpiler.env.map.put("PATH", new_path) catch bun.outOfMemory(); } pub fn completions(ctx: Command.Context, default_completions: ?[]const string, reject_list: []const string, comptime filter: Filter) !ShellCompletions { @@ -999,35 +1010,35 @@ pub const RunCommand = struct { const args = ctx.args; - var this_bundler = bundler.Bundler.init(ctx.allocator, ctx.log, args, null) catch return shell_out; - this_bundler.options.env.behavior = Api.DotEnvBehavior.load_all; - this_bundler.options.env.prefix = ""; - this_bundler.env.quiet = true; + var this_transpiler = transpiler.Transpiler.init(ctx.allocator, ctx.log, args, null) catch return shell_out; + this_transpiler.options.env.behavior = Api.DotEnvBehavior.load_all; + this_transpiler.options.env.prefix = ""; + this_transpiler.env.quiet = true; - this_bundler.resolver.care_about_bin_folder = true; - this_bundler.resolver.care_about_scripts = true; - this_bundler.resolver.store_fd = true; + this_transpiler.resolver.care_about_bin_folder = true; + this_transpiler.resolver.care_about_scripts = true; + this_transpiler.resolver.store_fd = true; defer { - this_bundler.resolver.care_about_bin_folder = false; - this_bundler.resolver.care_about_scripts = false; + this_transpiler.resolver.care_about_bin_folder = false; + this_transpiler.resolver.care_about_scripts = false; } - this_bundler.configureLinker(); + this_transpiler.configureLinker(); - const root_dir_info = (this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch null) orelse return shell_out; + const root_dir_info = (this_transpiler.resolver.readDirInfo(this_transpiler.fs.top_level_dir) catch null) orelse return shell_out; { - this_bundler.env.loadProcess(); + this_transpiler.env.loadProcess(); - if (this_bundler.env.get("NODE_ENV")) |node_env| { + if (this_transpiler.env.get("NODE_ENV")) |node_env| { if (strings.eqlComptime(node_env, "production")) { - this_bundler.options.production = true; + this_transpiler.options.production = true; } } } const ResultList = bun.StringArrayHashMap(void); - if (this_bundler.env.get("SHELL")) |shell| { + if (this_transpiler.env.get("SHELL")) |shell| { shell_out.shell = ShellCompletions.Shell.fromEnv(@TypeOf(shell), shell); } @@ -1044,15 +1055,15 @@ pub const RunCommand = struct { } if (filter == Filter.bin or filter == Filter.all or filter == Filter.all_plus_bun_js) { - for (this_bundler.resolver.binDirs()) |bin_path| { - if (this_bundler.resolver.readDirInfo(bin_path) catch null) |bin_dir| { + for (this_transpiler.resolver.binDirs()) |bin_path| { + if (this_transpiler.resolver.readDirInfo(bin_path) catch null) |bin_dir| { if (bin_dir.getEntriesConst()) |entries| { var iter = entries.data.iterator(); var has_copied = false; var dir_slice: string = ""; while (iter.next()) |entry| { const value = entry.value_ptr.*; - if (value.kind(&this_bundler.fs.fs, true) == .file) { + if (value.kind(&this_transpiler.fs.fs, true) == .file) { if (!has_copied) { bun.copy(u8, &path_buf, value.dir); dir_slice = path_buf[0..value.dir.len]; @@ -1071,7 +1082,7 @@ pub const RunCommand = struct { } if (!(bun.sys.isExecutableFilePath(slice))) continue; // we need to dupe because the string pay point to a pointer that only exists in the current scope - _ = try results.getOrPut(this_bundler.fs.filename_store.append(@TypeOf(base), base) catch continue); + _ = try results.getOrPut(this_transpiler.fs.filename_store.append(@TypeOf(base), base) catch continue); } } } @@ -1080,21 +1091,21 @@ pub const RunCommand = struct { } if (filter == Filter.all_plus_bun_js or filter == Filter.bun_js) { - if (this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch null) |dir_info| { + if (this_transpiler.resolver.readDirInfo(this_transpiler.fs.top_level_dir) catch null) |dir_info| { if (dir_info.getEntriesConst()) |entries| { var iter = entries.data.iterator(); while (iter.next()) |entry| { const value = entry.value_ptr.*; const name = value.base(); - if (name[0] != '.' and this_bundler.options.loader(std.fs.path.extension(name)).canBeRunByBun() and + if (name[0] != '.' and this_transpiler.options.loader(std.fs.path.extension(name)).canBeRunByBun() and !strings.contains(name, ".config") and !strings.contains(name, ".d.ts") and !strings.contains(name, ".d.mts") and !strings.contains(name, ".d.cts") and - value.kind(&this_bundler.fs.fs, true) == .file) + value.kind(&this_transpiler.fs.fs, true) == .file) { - _ = try results.getOrPut(this_bundler.fs.filename_store.append(@TypeOf(name), name) catch continue); + _ = try results.getOrPut(this_transpiler.fs.filename_store.append(@TypeOf(name), name) catch continue); } } } @@ -1110,7 +1121,7 @@ pub const RunCommand = struct { } var max_description_len: usize = 20; - if (this_bundler.env.get("MAX_DESCRIPTION_LEN")) |max| { + if (this_transpiler.env.get("MAX_DESCRIPTION_LEN")) |max| { if (std.fmt.parseInt(usize, max, 10) catch null) |max_len| { max_description_len = max_len; } @@ -1251,153 +1262,155 @@ pub const RunCommand = struct { Output.flush(); } + fn _bootAndHandleError(ctx: Command.Context, path: string) bool { + Global.configureAllocator(.{ .long_running = true }); + Run.boot(ctx, ctx.allocator.dupe(u8, path) catch return false) catch |err| { + ctx.log.print(Output.errorWriter()) catch {}; + + Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ + std.fs.path.basename(path), + @errorName(err), + }); + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + Global.exit(1); + }; + return true; + } + fn maybeOpenWithBunJS(ctx: Command.Context) bool { + if (ctx.args.entry_points.len == 0) + return false; + var script_name_buf: bun.PathBuffer = undefined; + + const script_name_to_search = ctx.args.entry_points[0]; + + var absolute_script_path: ?string = null; + + // TODO: optimize this pass for Windows. we can make better use of system apis available + var file_path = script_name_to_search; + { + const file = bun.toLibUVOwnedFD(((brk: { + if (std.fs.path.isAbsolute(script_name_to_search)) { + var win_resolver = resolve_path.PosixToWinNormalizer{}; + var resolved = win_resolver.resolveCWD(script_name_to_search) catch @panic("Could not resolve path"); + if (comptime Environment.isWindows) { + resolved = resolve_path.normalizeString(resolved, false, .windows); + } + break :brk bun.openFile( + resolved, + .{ .mode = .read_only }, + ); + } else if (!strings.hasPrefix(script_name_to_search, "..") and script_name_to_search[0] != '~') { + const file_pathZ = brk2: { + @memcpy(script_name_buf[0..file_path.len], file_path); + script_name_buf[file_path.len] = 0; + break :brk2 script_name_buf[0..file_path.len :0]; + }; + + break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + } else { + var path_buf_2: bun.PathBuffer = undefined; + const cwd = bun.getcwd(&path_buf_2) catch return false; + path_buf_2[cwd.len] = std.fs.path.sep; + var parts = [_]string{script_name_to_search}; + file_path = resolve_path.joinAbsStringBuf( + path_buf_2[0 .. cwd.len + 1], + &script_name_buf, + &parts, + .auto, + ); + if (file_path.len == 0) return false; + script_name_buf[file_path.len] = 0; + const file_pathZ = script_name_buf[0..file_path.len :0]; + break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + } + }) catch return false).handle) catch return false; + defer _ = bun.sys.close(file); + + switch (bun.sys.fstat(file)) { + .result => |stat| { + // directories cannot be run. if only there was a faster way to check this + if (bun.S.ISDIR(@intCast(stat.mode))) return false; + }, + .err => return false, + } + + Global.configureAllocator(.{ .long_running = true }); + + absolute_script_path = brk: { + if (comptime !Environment.isWindows) break :brk bun.getFdPath(file, &script_name_buf) catch return false; + + var fd_path_buf: bun.PathBuffer = undefined; + break :brk bun.getFdPath(file, &fd_path_buf) catch return false; + }; + } + + if (!ctx.debug.loaded_bunfig) { + bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand) catch {}; + } + + _ = _bootAndHandleError(ctx, absolute_script_path.?); + return true; + } pub fn exec( ctx: Command.Context, - comptime bin_dirs_only: bool, - comptime log_errors: bool, - comptime did_try_open_with_bun_js: bool, + cfg: struct { + bin_dirs_only: bool, + log_errors: bool, + allow_fast_run_for_extensions: bool, + }, ) !bool { - // Step 1. Figure out what we're trying to run + const bin_dirs_only = cfg.bin_dirs_only; + const log_errors = cfg.log_errors; + + // find what to run + var positionals = ctx.positionals; - if (positionals.len > 0 and strings.eqlComptime(positionals[0], "run") or strings.eqlComptime(positionals[0], "r")) { + if (positionals.len > 0 and strings.eqlComptime(positionals[0], "run")) { positionals = positionals[1..]; } - var script_name_to_search: string = ""; - + var target_name: string = ""; if (positionals.len > 0) { - script_name_to_search = positionals[0]; + target_name = positionals[0]; + positionals = positionals[1..]; } + const passthrough = ctx.passthrough; // unclear why passthrough is an escaped string, it should probably be []const []const u8 and allow its users to escape it. - const passthrough = ctx.passthrough; - const force_using_bun = ctx.debug.run_in_bun; - - // This doesn't cover every case - if ((script_name_to_search.len == 1 and script_name_to_search[0] == '.') or - (script_name_to_search.len == 2 and @as(u16, @bitCast(script_name_to_search[0..2].*)) == @as(u16, @bitCast([_]u8{ '.', '/' })))) - { - Run.boot(ctx, ".") catch |err| { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - ctx.log.print(Output.errorWriter()) catch {}; - - Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ - script_name_to_search, - @errorName(err), - }); - Global.exit(1); - }; - return true; - } - - if (!did_try_open_with_bun_js and (log_errors or force_using_bun)) { - if (script_name_to_search.len > 0) { - possibly_open_with_bun_js: { - const ext = std.fs.path.extension(script_name_to_search); - var has_loader = false; - if (!force_using_bun) { - if (options.defaultLoaders.get(ext)) |load| { - has_loader = true; - if (!load.canBeRunByBun()) - break :possibly_open_with_bun_js; - // if there are preloads, allow weirdo file extensions - } else { - // you can have package.json scripts with file extensions in the name - // eg "foo.zip" - // in those cases, we don't know - if (ext.len == 0 or strings.containsChar(script_name_to_search, ':')) - break :possibly_open_with_bun_js; - } - } - - var file_path = script_name_to_search; - const file_: anyerror!std.fs.File = brk: { - if (std.fs.path.isAbsolute(script_name_to_search)) { - var resolver = resolve_path.PosixToWinNormalizer{}; - break :brk bun.openFile(try resolver.resolveCWD(script_name_to_search), .{ .mode = .read_only }); - } else { - const cwd = bun.getcwd(&path_buf) catch break :possibly_open_with_bun_js; - path_buf[cwd.len] = std.fs.path.sep_posix; - var parts = [_]string{script_name_to_search}; - file_path = resolve_path.joinAbsStringBuf( - path_buf[0 .. cwd.len + 1], - &path_buf2, - &parts, - .auto, - ); - if (file_path.len == 0) break :possibly_open_with_bun_js; - path_buf2[file_path.len] = 0; - const file_pathZ = path_buf2[0..file_path.len :0]; - break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); - } - }; - - const file = file_ catch break :possibly_open_with_bun_js; - - if (!force_using_bun) { - // Due to preload, we don't know if they intend to run - // this as a script or as a regular file - // once we know it's a file, check if they have any preloads - if (ext.len > 0 and !has_loader) { - if (!ctx.debug.loaded_bunfig) { - try CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand); - } - - if (ctx.preloads.len == 0) - break :possibly_open_with_bun_js; - } - - // ignore the shebang if they explicitly passed `--bun` - // "White space after #! is optional." - var shebang_buf: [64]u8 = undefined; - const shebang_size = file.pread(&shebang_buf, 0) catch |err| { - if (!ctx.debug.silent) - Output.prettyErrorln("error: Failed to read file {s} due to error {s}", .{ file_path, @errorName(err) }); - Global.exit(1); - }; - - var shebang: string = shebang_buf[0..shebang_size]; - - shebang = std.mem.trim(u8, shebang, " \r\n\t"); - if (strings.hasPrefixComptime(shebang, "#!")) { - const first_arg: string = if (bun.argv.len > 0) bun.argv[0] else ""; - const filename = std.fs.path.basename(first_arg); - // are we attempting to run the script with bun? - if (!strings.contains(shebang, filename)) { - break :possibly_open_with_bun_js; - } - } - } - - Global.configureAllocator(.{ .long_running = true }); - const out_path = ctx.allocator.dupe(u8, file_path) catch unreachable; - Run.boot(ctx, out_path) catch |err| { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - ctx.log.print(Output.errorWriter()) catch {}; - - Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ - std.fs.path.basename(file_path), - @errorName(err), - }); - Global.exit(1); - }; - - return true; - } + var try_fast_run = false; + var skip_script_check = false; + if (target_name.len > 0 and target_name[0] == '.') { + try_fast_run = true; + skip_script_check = true; + } else if (std.fs.path.isAbsolute(target_name)) { + try_fast_run = true; + skip_script_check = true; + } else if (cfg.allow_fast_run_for_extensions) { + const ext = std.fs.path.extension(target_name); + const default_loader = options.defaultLoaders.get(ext); + if (default_loader != null and default_loader.?.canBeRunByBun()) { + try_fast_run = true; } } - Global.configureAllocator(.{ .long_running = false }); + // try fast run (check if the file exists and is not a folder, then run it) + if (try_fast_run and maybeOpenWithBunJS(ctx)) return true; + // setup + + const force_using_bun = ctx.debug.run_in_bun; var ORIGINAL_PATH: string = ""; - var this_bundler: bundler.Bundler = undefined; - const root_dir_info = try configureEnvForRun(ctx, &this_bundler, null, log_errors, false); - try configurePathForRun(ctx, root_dir_info, &this_bundler, &ORIGINAL_PATH, root_dir_info.abs_path, force_using_bun); - this_bundler.env.map.put("npm_command", "run-script") catch unreachable; + var this_transpiler: transpiler.Transpiler = undefined; + const root_dir_info = try configureEnvForRun(ctx, &this_transpiler, null, log_errors, false); + try configurePathForRun(ctx, root_dir_info, &this_transpiler, &ORIGINAL_PATH, root_dir_info.abs_path, force_using_bun); + this_transpiler.env.map.put("npm_command", "run-script") catch unreachable; - if (script_name_to_search.len == 0) { - // naked "bun run" + if (!ctx.debug.loaded_bunfig) { + bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand) catch {}; + } + + // check for empty command + + if (target_name.len == 0) { if (root_dir_info.enclosing_package_json) |package_json| { RunCommand.printHelp(package_json); } else { @@ -1409,81 +1422,11 @@ pub const RunCommand = struct { return true; } - // Run package.json script - if (root_dir_info.enclosing_package_json) |package_json| { - if (package_json.scripts) |scripts| { - if (scripts.get(script_name_to_search)) |script_content| { - // allocate enough to hold "post${scriptname}" - var temp_script_buffer = try std.fmt.allocPrint(ctx.allocator, "ppre{s}", .{script_name_to_search}); - defer ctx.allocator.free(temp_script_buffer); + // check for stdin - if (scripts.get(temp_script_buffer[1..])) |prescript| { - try runPackageScriptForeground( - ctx, - ctx.allocator, - prescript, - temp_script_buffer[1..], - this_bundler.fs.top_level_dir, - this_bundler.env, - &.{}, - ctx.debug.silent, - ctx.debug.use_system_shell, - ); - } + if (target_name.len == 1 and target_name[0] == '-') { + log("Executing from stdin", .{}); - { - try runPackageScriptForeground( - ctx, - ctx.allocator, - script_content, - script_name_to_search, - this_bundler.fs.top_level_dir, - this_bundler.env, - passthrough, - ctx.debug.silent, - ctx.debug.use_system_shell, - ); - } - - temp_script_buffer[0.."post".len].* = "post".*; - - if (scripts.get(temp_script_buffer)) |postscript| { - try runPackageScriptForeground( - ctx, - ctx.allocator, - postscript, - temp_script_buffer, - this_bundler.fs.top_level_dir, - this_bundler.env, - &.{}, - ctx.debug.silent, - ctx.debug.use_system_shell, - ); - } - - return true; - } - } - } - - // Run absolute/relative path - if (std.fs.path.isAbsolute(script_name_to_search) or - (script_name_to_search.len > 2 and script_name_to_search[0] == '.' and script_name_to_search[1] == '/')) - { - Run.boot(ctx, ctx.allocator.dupe(u8, script_name_to_search) catch unreachable) catch |err| { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - ctx.log.print(Output.errorWriter()) catch {}; - - Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ - std.fs.path.basename(script_name_to_search), - @errorName(err), - }); - Global.exit(1); - }; - } - - if (script_name_to_search.len == 1 and script_name_to_search[0] == '-') { // read from stdin var stack_fallback = std.heap.stackFallback(2048, bun.default_allocator); var list = std.ArrayList(u8).init(stack_fallback.get()); @@ -1502,7 +1445,7 @@ pub const RunCommand = struct { ctx.log.print(Output.errorWriter()) catch {}; Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ - std.fs.path.basename(script_name_to_search), + std.fs.path.basename(target_name), @errorName(err), }); bun.handleErrorReturnTrace(err, @errorReturnTrace()); @@ -1511,6 +1454,86 @@ pub const RunCommand = struct { return true; } + // run script with matching name + + if (!skip_script_check) if (root_dir_info.enclosing_package_json) |package_json| { + if (package_json.scripts) |scripts| { + if (scripts.get(target_name)) |script_content| { + log("Found matching script `{s}`", .{script_content}); + Global.configureAllocator(.{ .long_running = false }); + this_transpiler.env.map.put("npm_lifecycle_event", target_name) catch unreachable; + + // allocate enough to hold "post${scriptname}" + var temp_script_buffer = try std.fmt.allocPrint(ctx.allocator, "\x00pre{s}", .{target_name}); + defer ctx.allocator.free(temp_script_buffer); + + const package_json_path = root_dir_info.enclosing_package_json.?.source.path.text; + const package_json_dir = strings.withoutTrailingSlash(strings.withoutSuffixComptime(package_json_path, "package.json")); + log("Running in dir `{s}`", .{package_json_dir}); + + if (scripts.get(temp_script_buffer[1..])) |prescript| { + try runPackageScriptForeground( + ctx, + ctx.allocator, + prescript, + temp_script_buffer[1..], + package_json_dir, + this_transpiler.env, + &.{}, + ctx.debug.silent, + ctx.debug.use_system_shell, + ); + } + + try runPackageScriptForeground( + ctx, + ctx.allocator, + script_content, + target_name, + package_json_dir, + this_transpiler.env, + passthrough, + ctx.debug.silent, + ctx.debug.use_system_shell, + ); + + temp_script_buffer[0.."post".len].* = "post".*; + + if (scripts.get(temp_script_buffer)) |postscript| { + try runPackageScriptForeground( + ctx, + ctx.allocator, + postscript, + temp_script_buffer, + package_json_dir, + this_transpiler.env, + &.{}, + ctx.debug.silent, + ctx.debug.use_system_shell, + ); + } + + return true; + } + } + }; + + // load module and run that module + // TODO: run module resolution here - try the next condition if the module can't be found + + log("Try resolve `{s}` in `{s}`", .{ target_name, this_transpiler.fs.top_level_dir }); + if (this_transpiler.resolver.resolve(this_transpiler.fs.top_level_dir, target_name, .entry_point_run)) |resolved| { + var resolved_mutable = resolved; + log("Resolved to: `{s}`", .{resolved_mutable.path().?.text}); + return _bootAndHandleError(ctx, resolved_mutable.path().?.text); + } else |_| if (this_transpiler.resolver.resolve(this_transpiler.fs.top_level_dir, try std.mem.join(ctx.allocator, "", &.{ "./", target_name }), .entry_point_run)) |resolved| { + var resolved_mutable = resolved; + log("Resolved with `./` to: `{s}`", .{resolved_mutable.path().?.text}); + return _bootAndHandleError(ctx, resolved_mutable.path().?.text); + } else |_| {} + + // execute a node_modules/.bin/ command, or (run only) a system command like 'ls' + if (Environment.isWindows and bun.FeatureFlags.windows_bunx_fast_path) try_bunx_file: { // Attempt to find a ".bunx" file on disk, and run it, skipping the // wrapper exe. we build the full exe path even though we could do @@ -1529,18 +1552,18 @@ pub const RunCommand = struct { const prefix = comptime bun.strings.w("\\node_modules\\.bin\\"); @memcpy(ptr[0..prefix.len], prefix); ptr = ptr[prefix.len..]; - const encoded = bun.strings.convertUTF8toUTF16InBuffer(ptr[0..], script_name_to_search); + const encoded = bun.strings.convertUTF8toUTF16InBuffer(ptr[0..], target_name); ptr = ptr[encoded.len..]; const ext = comptime bun.strings.w(".bunx"); @memcpy(ptr[0..ext.len], ext); ptr[ext.len] = 0; - const l = root.len + cwd_len + prefix.len + script_name_to_search.len + ext.len; + const l = root.len + cwd_len + prefix.len + target_name.len + ext.len; const path_to_use = BunXFastPath.direct_launch_buffer[0..l :0]; - BunXFastPath.tryLaunch(ctx, path_to_use, this_bundler.env, ctx.passthrough); + BunXFastPath.tryLaunch(ctx, path_to_use, this_transpiler.env, ctx.passthrough); } - const PATH = this_bundler.env.get("PATH") orelse ""; + const PATH = this_transpiler.env.get("PATH") orelse ""; var path_for_which = PATH; if (comptime bin_dirs_only) { if (ORIGINAL_PATH.len < PATH.len) { @@ -1551,26 +1574,37 @@ pub const RunCommand = struct { } if (path_for_which.len > 0) { - if (which(&path_buf, path_for_which, this_bundler.fs.top_level_dir, script_name_to_search)) |destination| { + if (which(&path_buf, path_for_which, this_transpiler.fs.top_level_dir, target_name)) |destination| { const out = bun.asByteSlice(destination); return try runBinaryWithoutBunxPath( ctx, - try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + try this_transpiler.fs.dirname_store.append(@TypeOf(out), out), destination, - this_bundler.fs.top_level_dir, - this_bundler.env, + this_transpiler.fs.top_level_dir, + this_transpiler.env, passthrough, - script_name_to_search, + target_name, ); } } + // failure + if (ctx.runtime_options.if_present) { return true; } if (comptime log_errors) { - Output.prettyError("error: Script not found \"{s}\"\n", .{script_name_to_search}); + const ext = std.fs.path.extension(target_name); + const default_loader = options.defaultLoaders.get(ext); + if (default_loader != null and default_loader.?.isJavaScriptLikeOrJSON() or target_name.len > 0 and (target_name[0] == '.' or target_name[0] == '/' or std.fs.path.isAbsolute(target_name))) { + Output.prettyError("error: Module not found \"{s}\"\n", .{target_name}); + } else if (ext.len > 0) { + Output.prettyError("error: File not found \"{s}\"\n", .{target_name}); + } else { + Output.prettyError("error: Script not found \"{s}\"\n", .{target_name}); + } + Global.exit(1); } diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 405b113880..d76218c15e 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -764,7 +764,7 @@ pub const CommandLineReporter = struct { @compileError("No reporters enabled"); } - const relative_dir = vm.bundler.fs.top_level_dir; + const relative_dir = vm.transpiler.fs.top_level_dir; // --- Text --- const max_filepath_length: usize = if (reporters.text) brk: { @@ -820,7 +820,6 @@ pub const CommandLineReporter = struct { }, .always_return_none = true, }, - .sync, ); // Write the lcov.info file to a temporary file we atomically rename to the final name after it succeeds @@ -1051,7 +1050,7 @@ const Scanner = struct { } const ext = std.fs.path.extension(slice); - const loader_by_ext = JSC.VirtualMachine.get().bundler.options.loader(ext); + const loader_by_ext = JSC.VirtualMachine.get().transpiler.options.loader(ext); // allow file loader just incase they use a custom loader with a non-standard extension if (!(loader_by_ext.isJavaScriptLike() or loader_by_ext == .file)) { @@ -1200,6 +1199,7 @@ pub const TestCommand = struct { var snapshot_file_buf = std.ArrayList(u8).init(ctx.allocator); var snapshot_values = Snapshots.ValuesHashMap.init(ctx.allocator); var snapshot_counts = bun.StringHashMap(usize).init(ctx.allocator); + var inline_snapshots_to_write = std.AutoArrayHashMap(TestRunner.File.ID, std.ArrayList(Snapshots.InlineSnapshotToWrite)).init(ctx.allocator); JSC.isBunTest = true; var reporter = try ctx.allocator.create(CommandLineReporter); @@ -1220,6 +1220,7 @@ pub const TestCommand = struct { .file_buf = &snapshot_file_buf, .values = &snapshot_values, .counts = &snapshot_counts, + .inline_snapshots_to_write = &inline_snapshots_to_write, }, }, .callback = undefined, @@ -1259,12 +1260,13 @@ pub const TestCommand = struct { .store_fd = true, .smol = ctx.runtime_options.smol, .debugger = ctx.runtime_options.debugger, + .is_main_thread = true, }, ); vm.argv = ctx.passthrough; vm.preload = ctx.preloads; - vm.bundler.options.rewrite_jest_for_tests = true; - vm.bundler.options.env.behavior = .load_all_without_inlining; + vm.transpiler.options.rewrite_jest_for_tests = true; + vm.transpiler.options.env.behavior = .load_all_without_inlining; const node_env_entry = try env_loader.map.getOrPutWithoutValue("NODE_ENV"); if (!node_env_entry.found_existing) { @@ -1275,18 +1277,18 @@ pub const TestCommand = struct { }; } - try vm.bundler.configureDefines(); + try vm.transpiler.configureDefines(); vm.loadExtraEnvAndSourceCodePrinter(); vm.is_main_thread = true; JSC.VirtualMachine.is_main_thread_vm = true; if (ctx.test_options.coverage.enabled) { - vm.bundler.options.code_coverage = true; - vm.bundler.options.minify_syntax = false; - vm.bundler.options.minify_identifiers = false; - vm.bundler.options.minify_whitespace = false; - vm.bundler.options.dead_code_elimination = false; + vm.transpiler.options.code_coverage = true; + vm.transpiler.options.minify_syntax = false; + vm.transpiler.options.minify_identifiers = false; + vm.transpiler.options.minify_whitespace = false; + vm.transpiler.options.dead_code_elimination = false; vm.global.vm().setControlFlowProfiler(true); } @@ -1296,7 +1298,7 @@ pub const TestCommand = struct { // We use the string "Etc/UTC" instead of "UTC" so there is no normalization difference. "Etc/UTC"; - if (vm.bundler.env.get("TZ")) |tz| { + if (vm.transpiler.env.get("TZ")) |tz| { TZ_NAME = tz; } @@ -1349,8 +1351,8 @@ pub const TestCommand = struct { var scanner = Scanner{ .dirs_to_scan = Scanner.Fifo.init(ctx.allocator), - .options = &vm.bundler.options, - .fs = vm.bundler.fs, + .options = &vm.transpiler.options, + .fs = vm.transpiler.fs, .filter_names = filter_names_normalized, .results = &results, }; @@ -1377,10 +1379,11 @@ pub const TestCommand = struct { else => {}, } - // vm.bundler.fs.fs.readDirectory(_dir: string, _handle: ?std.fs.Dir) + // vm.transpiler.fs.fs.readDirectory(_dir: string, _handle: ?std.fs.Dir) runAllTests(reporter, vm, test_files, ctx.allocator); } + const write_snapshots_success = try jest.Jest.runner.?.snapshots.writeInlineSnapshots(); try jest.Jest.runner.?.snapshots.writeSnapshotFile(); var coverage = ctx.test_options.coverage; @@ -1567,25 +1570,29 @@ pub const TestCommand = struct { } if (vm.hot_reload == .watch) { - vm.eventLoop().tickPossiblyForever(); - - while (true) { - while (vm.isEventLoopAlive()) { - vm.tick(); - vm.eventLoop().autoTickActive(); - } - - vm.eventLoop().tickPossiblyForever(); - } + vm.runWithAPILock(JSC.VirtualMachine, vm, runEventLoopForWatch); } - if (reporter.summary.fail > 0 or (coverage.enabled and coverage.fractions.failing and coverage.fail_on_low_coverage)) { + if (reporter.summary.fail > 0 or (coverage.enabled and coverage.fractions.failing and coverage.fail_on_low_coverage) or !write_snapshots_success) { Global.exit(1); } else if (reporter.jest.unhandled_errors_between_tests > 0) { Global.exit(reporter.jest.unhandled_errors_between_tests); } } + fn runEventLoopForWatch(vm: *JSC.VirtualMachine) void { + vm.eventLoop().tickPossiblyForever(); + + while (true) { + while (vm.isEventLoopAlive()) { + vm.tick(); + vm.eventLoop().autoTickActive(); + } + + vm.eventLoop().tickPossiblyForever(); + } + } + pub fn runAllTests( reporter_: *CommandLineReporter, vm_: *JSC.VirtualMachine, @@ -1651,7 +1658,7 @@ pub const TestCommand = struct { defer reporter.jest.only = prev_only; const file_start = reporter.jest.files.len; - const resolution = try vm.bundler.resolveEntryPoint(file_name); + const resolution = try vm.transpiler.resolveEntryPoint(file_name); vm.clearEntryPoint(); const file_path = resolution.path_pair.primary.text; diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index d55776ad24..21219d9a21 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -25,7 +25,6 @@ const Api = @import("../api/schema.zig").Api; const resolve_path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; const Command = @import("../cli.zig").Command; -const bundler = bun.bundler; const fs = @import("../fs.zig"); const URL = @import("../url.zig").URL; @@ -37,7 +36,7 @@ const JSPrinter = bun.js_printer; const DotEnv = @import("../env_loader.zig"); const which = @import("../which.zig").which; const clap = bun.clap; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const Headers = bun.http.Headers; const CopyFile = @import("../copy_file.zig"); @@ -232,7 +231,7 @@ pub const UpgradeCommand = struct { } } - const http_proxy: ?URL = env_loader.getHttpProxy(api_url); + const http_proxy: ?URL = env_loader.getHttpProxyFor(api_url); var metadata_body = try MutableString.init(allocator, 2048); @@ -457,7 +456,7 @@ pub const UpgradeCommand = struct { const use_profile = strings.containsAny(bun.argv, "--profile"); - const version: Version = if (!use_canary) v: { + var version: Version = if (!use_canary) v: { var refresher = Progress{}; var progress = refresher.start("Fetching version tags", 0); @@ -502,7 +501,7 @@ pub const UpgradeCommand = struct { }; const zip_url = URL.parse(version.zip_url); - const http_proxy: ?URL = env_loader.getHttpProxy(zip_url); + const http_proxy: ?URL = env_loader.getHttpProxyFor(zip_url); { var refresher = Progress{}; @@ -582,7 +581,7 @@ pub const UpgradeCommand = struct { tmpdir_path_buf[tmpdir_path.len] = 0; const tmpdir_z = tmpdir_path_buf[0..tmpdir_path.len :0]; - _ = bun.sys.chdir(tmpdir_z); + _ = bun.sys.chdir("", tmpdir_z); const tmpname = "bun.zip"; const exe = @@ -687,7 +686,7 @@ pub const UpgradeCommand = struct { .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), - } else {}, + }, }) catch |err| { Output.prettyErrorln("error: Failed to spawn Expand-Archive on {s} due to error {s}", .{ tmpname, @errorName(err) }); Global.exit(1); @@ -700,7 +699,7 @@ pub const UpgradeCommand = struct { { var verify_argv = [_]string{ exe, - "--version", + if (use_canary) "--revision" else "--version", }; const result = std.process.Child.run(.{ @@ -746,7 +745,12 @@ pub const UpgradeCommand = struct { // It should run successfully // but we don't care about the version number if we're doing a canary build - if (!use_canary) { + if (use_canary) { + var version_string = result.stdout; + if (strings.indexOfChar(version_string, '+')) |i| { + version.tag = version_string[i + 1 .. version_string.len]; + } + } else { var version_string = result.stdout; if (strings.indexOfChar(version_string, ' ')) |i| { version_string = version_string[0..i]; @@ -926,10 +930,10 @@ pub const UpgradeCommand = struct { \\ \\Changelog: \\ - \\ https://github.com/oven-sh/bun/compare/{s}...main + \\ https://github.com/oven-sh/bun/compare/{s}...{s} \\ , - .{Environment.git_sha}, + .{ Environment.git_sha_short, version.tag }, ); } else { const bun_v = "bun-v" ++ Global.package_json_version; diff --git a/src/codegen/bake-codegen.ts b/src/codegen/bake-codegen.ts index 22d389ce77..a48fbaae4e 100644 --- a/src/codegen/bake-codegen.ts +++ b/src/codegen/bake-codegen.ts @@ -1,26 +1,15 @@ import assert from "node:assert"; -import { existsSync, writeFileSync, rmSync, readFileSync } from "node:fs"; -import { watch } from "node:fs/promises"; +import { existsSync, readFileSync, rmSync } from "node:fs"; import { basename, join } from "node:path"; +import { argParse, writeIfNotChanged } from "./helpers"; // arg parsing -const options = {}; -for (const arg of process.argv.slice(2)) { - if (!arg.startsWith("--")) { - console.error("Unknown argument " + arg); - process.exit(1); - } - const split = arg.split("="); - const value = split[1] || "true"; - options[split[0].slice(2)] = value; -} - -let { codegen_root, debug, live } = options as any; -if (!codegen_root) { - console.error("Missing --codegen_root=..."); +let { "codegen-root": codegenRoot, debug, ...rest } = argParse(["codegen-root", "debug"]); +if (debug === "false" || debug === "0" || debug == "OFF") debug = false; +if (!codegenRoot) { + console.error("Missing --codegen-root=..."); process.exit(1); } -if (debug === "false" || debug === "0" || debug == "OFF") debug = false; const base_dir = join(import.meta.dirname, "../bake"); process.chdir(base_dir); // to make bun build predictable in development @@ -39,7 +28,7 @@ function convertZigEnum(zig: string) { async function run() { const devServerZig = readFileSync(join(base_dir, "DevServer.zig"), "utf-8"); - writeFileSync(join(base_dir, "generated.ts"), convertZigEnum(devServerZig)); + writeIfNotChanged(join(base_dir, "generated.ts"), convertZigEnum(devServerZig)); const results = await Promise.allSettled( ["client", "server", "error"].map(async file => { @@ -53,7 +42,7 @@ async function run() { minify: { syntax: !debug, }, - target: side === 'server' ? 'bun' : 'browser', + target: side === "server" ? "bun" : "browser", }); if (!result.success) throw new AggregateError(result.logs); assert(result.outputs.length === 1, "must bundle to a single file"); @@ -80,7 +69,7 @@ async function run() { `; const generated_entrypoint = join(base_dir, `.runtime-${file}.generated.ts`); - writeFileSync(generated_entrypoint, combined_source); + writeIfNotChanged(generated_entrypoint, combined_source); result = await Bun.build({ entrypoints: [generated_entrypoint], @@ -92,8 +81,10 @@ async function run() { rmSync(generated_entrypoint); - if (code.includes('export default ')) { - throw new AggregateError([new Error('export default is not allowed in bake codegen. this became a commonjs module!')]); + if (code.includes("export default ")) { + throw new AggregateError([ + new Error("export default is not allowed in bake codegen. this became a commonjs module!"), + ]); } if (file !== "error") { @@ -119,13 +110,13 @@ async function run() { if (code[code.length - 1] === ";") code = code.slice(0, -1); if (side === "server") { - code = debug - ? `${code} return ${outName('server_exports')};\n` - : `${code};return ${outName('server_exports')};`; + code = debug + ? `${code} return ${outName("server_exports")};\n` + : `${code};return ${outName("server_exports")};`; - const params = `${outName('$separateSSRGraph')},${outName('$importMeta')}`; - code = code.replaceAll('import.meta', outName('$importMeta')); - code = `let ${outName('input_graph')}={},${outName('config')}={separateSSRGraph:${outName('$separateSSRGraph')}},${outName('server_exports')};${code}`; + const params = `${outName("$separateSSRGraph")},${outName("$importMeta")}`; + code = code.replaceAll("import.meta", outName("$importMeta")); + code = `let ${outName("input_graph")}={},${outName("config")}={separateSSRGraph:${outName("$separateSSRGraph")}},${outName("server_exports")};${code}`; code = debug ? `((${params}) => {${code}})\n` : `((${params})=>{${code}})\n`; } else { @@ -133,7 +124,7 @@ async function run() { } } - writeFileSync(join(codegen_root, `bake.${file}.js`), code); + writeIfNotChanged(join(codegenRoot, `bake.${file}.js`), code); }), ); @@ -174,26 +165,12 @@ async function run() { console.error(`Errors while bundling Bake ${kind.map(x => map[x]).join(" and ")}:`); console.error(err); } - if (!live) process.exit(1); } else { console.log("-> bake.client.js, bake.server.js, bake.error.js"); - const empty_file = join(codegen_root, "bake_empty_file"); - if (!existsSync(empty_file)) writeFileSync(empty_file, "this is used to fulfill a cmake dependency"); + const empty_file = join(codegenRoot, "bake_empty_file"); + if (!existsSync(empty_file)) writeIfNotChanged(empty_file, "this is used to fulfill a cmake dependency"); } } await run(); - -if (live) { - const watcher = watch(base_dir, { recursive: true }) as any; - for await (const event of watcher) { - if (event.filename.endsWith(".zig")) continue; - if (event.filename.startsWith(".")) continue; - try { - await run(); - } catch (e) { - console.log(e); - } - } -} diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts new file mode 100644 index 0000000000..f969c30fd1 --- /dev/null +++ b/src/codegen/bindgen-lib-internal.ts @@ -0,0 +1,1124 @@ +// While working on this file, it is important to have very rigorous errors +// and checking on input data. The goal is to allow people not aware of +// various footguns in JavaScript, C++, and the bindings generator to +// always produce correct code, or bail with an error. +import { expect } from "bun:test"; +import type { FuncOptions, Type, t } from "./bindgen-lib"; +import * as path from "node:path"; +import assert from "node:assert"; + +export const src = path.join(import.meta.dirname, "../"); + +export type TypeKind = keyof typeof t; + +export let allFunctions: Func[] = []; +export let files = new Map(); +/** A reachable type is one that is required for code generation */ +export let typeHashToReachableType = new Map(); +export let typeHashToStruct = new Map(); +export let typeHashToNamespace = new Map(); +export let structHashToSelf = new Map(); + +/** String literal */ +export const str = (v: any) => JSON.stringify(v); +/** Capitalize */ +export const cap = (s: string) => s[0].toUpperCase() + s.slice(1); +/** Escape a Zig Identifier */ +export const zid = (s: string) => (s.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) ? s : "@" + str(s)); +/** Snake Case */ +export const snake = (s: string) => + s[0].toLowerCase() + + s + .slice(1) + .replace(/([A-Z])/g, "_$1") + .replace(/-/g, "_") + .toLowerCase(); +/** Camel Case */ +export const camel = (s: string) => + s[0].toLowerCase() + s.slice(1).replace(/[_-](\w)?/g, (_, letter) => letter?.toUpperCase() ?? ""); +/** Pascal Case */ +export const pascal = (s: string) => cap(camel(s)); + +// Return symbol names of extern values (must be equivalent between C++ and Zig) + +/** The JS Host function, aka fn (*JSC.JSGlobalObject, *JSC.CallFrame) JSValue.MaybeException */ +export const extJsFunction = (namespaceVar: string, fnLabel: string) => + `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}`; +/** Each variant gets a dispatcher function. */ +export const extDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: number) => + `bindgen_${cap(namespaceVar)}_dispatch${cap(fnLabel)}${variantNumber}`; +export const extInternalDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: string | number) => + `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}_v${variantNumber}`; + +interface TypeDataDefs { + /** The name */ + ref: string; + + sequence: { + element: TypeImpl; + repr: "slice"; + }; + record: { + value: TypeImpl; + repr: "kv-slices"; + }; + zigEnum: { + file: string; + impl: string; + }; + stringEnum: string[]; + oneOf: TypeImpl[]; + dictionary: DictionaryField[]; +} +type TypeData = K extends keyof TypeDataDefs ? TypeDataDefs[K] : any; + +export const enum NodeValidator { + validateInteger = "validateInteger", +} + +interface Flags { + nodeValidator?: NodeValidator; + optional?: boolean; + required?: boolean; + nonNull?: boolean; + default?: any; + range?: ["clamp" | "enforce", bigint, bigint] | ["clamp" | "enforce", "abi", "abi"]; + finite?: boolean; +} + +export interface DictionaryField { + key: string; + type: TypeImpl; +} + +export declare const isType: unique symbol; + +const numericTypes = new Set(["f64", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "usize"]); + +/** + * Implementation of the Type interface. All types are immutable and hashable. + * Hashes de-duplicate structure and union definitions. Flags do not account for + * the hash, so `oneOf(A, B)` and `oneOf(A, B).optional` will point to the same + * generated struct type, the purpose of the flags are to inform receivers like + * `t.dictionary` and `fn` to mark uses as optional or provide default values. + */ +export class TypeImpl { + kind: K; + data: TypeData; + flags: Flags; + /** Access via .name(). */ + nameDeduplicated: string | null | undefined = undefined; + /** Access via .hash() */ + #hash: string | undefined = undefined; + ownerFile: string; + + declare [isType]: true; + + constructor(kind: K, data: TypeData, flags: Flags = {}) { + this.kind = kind; + this.data = data; + this.flags = flags; + this.ownerFile = path.basename(stackTraceFileName(snapshotCallerLocation()), ".bind.ts"); + } + + isVirtualArgument() { + return this.kind === "globalObject" || this.kind === "zigVirtualMachine"; + } + + hash() { + if (this.#hash) { + return this.#hash; + } + let h = `${this.kind}:`; + switch (this.kind) { + case "ref": + throw new Error("TODO"); + case "sequence": + h += this.data.element.hash(); + break; + case "record": + h += this.data.value.hash(); + break; + case "zigEnum": + h += `${this.data.file}:${this.data.impl}`; + break; + case "stringEnum": + h += this.data.join(","); + break; + case "oneOf": + h += this.data.map(t => t.hash()).join(","); + break; + case "dictionary": + h += this.data.map(({ key, required, type }) => `${key}:${required}:${type.hash()}`).join(","); + break; + } + let hash = String(Bun.hash(h)); + this.#hash = hash; + return hash; + } + + /** + * If this type lowers to a named type (struct, union, enum) + */ + lowersToNamedType() { + switch (this.kind) { + case "ref": + throw new Error("TODO"); + case "sequence": + case "record": + case "oneOf": + case "dictionary": + case "stringEnum": + case "zigEnum": + return true; + default: + return false; + } + } + + canDirectlyMapToCAbi(): CAbiType | null { + let kind = this.kind; + switch (kind) { + case "ref": + throw new Error("TODO"); + case "any": + return "JSValue"; + case "ByteString": + case "DOMString": + case "USVString": + case "UTF8String": + return "bun.String"; + case "boolean": + return "bool"; + case "strictBoolean": + return "bool"; + case "f64": + case "i8": + case "i16": + case "i32": + case "i64": + case "u8": + case "u16": + case "u32": + case "u64": + case "usize": + return kind; + case "globalObject": + case "zigVirtualMachine": + return "*JSGlobalObject"; + case "stringEnum": + return cAbiTypeForEnum(this.data.length); + case "zigEnum": + throw new Error("TODO"); + case "undefined": + return "u0"; + case "oneOf": // `union(enum)` + case "UTF8String": // []const u8 + case "record": // undecided how to lower records + case "sequence": // []const T + return null; + case "externalClass": + throw new Error("TODO"); + return "*anyopaque"; + case "dictionary": { + let existing = typeHashToStruct.get(this.hash()); + if (existing) return existing; + existing = new Struct(); + for (const { key, type } of this.data as DictionaryField[]) { + if (type.flags.optional && !("default" in type.flags)) { + return null; // ?T + } + const repr = type.canDirectlyMapToCAbi(); + if (!repr) return null; + + existing.add(key, repr); + } + existing.reorderForSmallestSize(); + if (!structHashToSelf.has(existing.hash())) { + structHashToSelf.set(existing.hash(), existing); + } + existing.assignName(this.name()); + typeHashToStruct.set(this.hash(), existing); + return existing; + } + case "sequence": { + return null; + } + default: { + throw new Error("unexpected: " + (kind satisfies never)); + } + } + } + + name() { + if (this.nameDeduplicated) { + return this.nameDeduplicated; + } + const hash = this.hash(); + const existing = typeHashToReachableType.get(hash); + if (existing) return (this.nameDeduplicated = existing.nameDeduplicated ??= this.#generateName()); + return (this.nameDeduplicated = `anon_${this.kind}_${hash}`); + } + + cppInternalName() { + const name = this.name(); + const cAbiType = this.canDirectlyMapToCAbi(); + const namespace = typeHashToNamespace.get(this.hash()); + if (cAbiType) { + if (typeof cAbiType === "string") { + return cAbiType; + } + } + return namespace ? `${namespace}${name}` : name; + } + + cppClassName() { + assert(this.lowersToNamedType(), `Does not lower to named type: ${inspect(this)}`); + const name = this.name(); + const namespace = typeHashToNamespace.get(this.hash()); + return namespace ? `${namespace}::${cap(name)}` : name; + } + + cppName() { + const name = this.name(); + const cAbiType = this.canDirectlyMapToCAbi(); + const namespace = typeHashToNamespace.get(this.hash()); + if (cAbiType && typeof cAbiType === "string" && this.kind !== "zigEnum" && this.kind !== "stringEnum") { + return cAbiTypeName(cAbiType); + } + return namespace ? `${namespace}::${cap(name)}` : name; + } + + #generateName() { + return `bindgen_${this.ownerFile}_${this.hash()}`; + } + + /** + * Name assignment is done to give readable names. + * The first name to a unique hash wins. + */ + assignName(name: string) { + if (this.nameDeduplicated) return; + const hash = this.hash(); + const existing = typeHashToReachableType.get(hash); + if (existing) { + this.nameDeduplicated = existing.nameDeduplicated ??= name; + return; + } + this.nameDeduplicated = name; + } + + markReachable() { + if (!this.lowersToNamedType()) return; + const hash = this.hash(); + const existing = typeHashToReachableType.get(hash); + this.nameDeduplicated ??= existing?.name() ?? `anon_${this.kind}_${hash}`; + if (!existing) typeHashToReachableType.set(hash, this); + + switch (this.kind) { + case "ref": + throw new Error("TODO"); + case "sequence": + this.data.element.markReachable(); + break; + case "record": + this.data.value.markReachable(); + break; + case "oneOf": + for (const type of this.data as TypeImpl[]) { + type.markReachable(); + } + break; + case "dictionary": + for (const { type } of this.data as DictionaryField[]) { + type.markReachable(); + } + break; + } + } + + #rangeModifier(min: undefined | number | bigint, max: undefined | number | bigint, kind: "clamp" | "enforce") { + if (this.flags.range) { + throw new Error("This type already has a range modifier set"); + } + + // cAbiIntegerLimits throws on non-integer types + const range = cAbiIntegerLimits(this.kind as CAbiType); + const abiMin = BigInt(range[0]); + const abiMax = BigInt(range[1]); + if (min === undefined) { + min = abiMin; + max = abiMax; + } else { + if (max === undefined) { + throw new Error("Expected min and max to be both set or both unset"); + } + min = BigInt(min); + max = BigInt(max); + + if (min < abiMin || min > abiMax) { + throw new Error(`Expected integer in range ${range}, got ${inspect(min)}`); + } + if (max < abiMin || max > abiMax) { + throw new Error(`Expected integer in range ${range}, got ${inspect(max)}`); + } + if (min > max) { + throw new Error(`Expected min <= max, got ${inspect(min)} > ${inspect(max)}`); + } + } + + return new TypeImpl(this.kind, this.data, { + ...this.flags, + range: min === BigInt(range[0]) && max === BigInt(range[1]) ? [kind, "abi", "abi"] : [kind, min, max], + }); + } + + assertDefaultIsValid(value: unknown) { + switch (this.kind) { + case "DOMString": + case "ByteString": + case "USVString": + case "UTF8String": + if (typeof value !== "string") { + throw new Error(`Expected string, got ${inspect(value)}`); + } + break; + case "boolean": + if (typeof value !== "boolean") { + throw new Error(`Expected boolean, got ${inspect(value)}`); + } + break; + case "f64": + if (typeof value !== "number") { + throw new Error(`Expected number, got ${inspect(value)}`); + } + break; + case "usize": + case "u8": + case "u16": + case "u32": + case "u64": + case "i8": + case "i16": + case "i32": + case "i64": + const range = this.flags.range?.slice(1) ?? cAbiIntegerLimits(this.kind); + if (typeof value === "number") { + if (value % 1 !== 0) { + throw new Error(`Expected integer, got ${inspect(value)}`); + } + if (value >= Number.MAX_SAFE_INTEGER || value <= Number.MIN_SAFE_INTEGER) { + throw new Error( + `Specify default ${this.kind} outside of max safe integer range as a BigInt to avoid precision loss`, + ); + } + if (value < Number(range[0]) || value > Number(range[1])) { + throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`); + } + } else if (typeof value === "bigint") { + if (value < BigInt(range[0]) || value > BigInt(range[1])) { + throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`); + } + } else { + throw new Error(`Expected integer, got ${inspect(value)}`); + } + break; + case "dictionary": + if (typeof value !== "object" || value === null) { + throw new Error(`Expected object, got ${inspect(value)}`); + } + for (const { key, type } of this.data as DictionaryField[]) { + if (key in value) { + type.assertDefaultIsValid(value[key]); + } else if (type.flags.required) { + throw new Error(`Missing key ${key} in dictionary`); + } + } + break; + case "undefined": + assert(value === undefined, `Expected undefined, got ${inspect(value)}`); + break; + default: + throw new Error(`TODO: set default value on type ${this.kind}`); + } + } + + emitCppDefaultValue(w: CodeWriter) { + const value = this.flags.default; + switch (this.kind) { + case "boolean": + w.add(value ? "true" : "false"); + break; + case "f64": + w.add(String(value)); + break; + case "usize": + case "u8": + case "u16": + case "u32": + case "u64": + case "i8": + case "i16": + case "i32": + case "i64": + w.add(String(value)); + break; + case "dictionary": + const struct = this.structType(); + w.line(`${this.cppName()} {`); + w.indent(); + for (const { name } of struct.fields) { + w.add(`.${name} = `); + const type = this.data.find(f => f.key === name)!.type; + type.emitCppDefaultValue(w); + w.line(","); + } + w.dedent(); + w.add(`}`); + break; + case "DOMString": + case "ByteString": + case "USVString": + case "UTF8String": + if (typeof value === "string") { + w.add("Bun::BunStringEmpty"); + } else { + throw new Error(`TODO: non-empty string default`); + } + break; + case "undefined": + throw new Error("Zero-sized type"); + default: + throw new Error(`TODO: set default value on type ${this.kind}`); + } + } + + structType() { + const direct = this.canDirectlyMapToCAbi(); + assert(typeof direct !== "string"); + if (direct) return direct; + throw new Error("TODO: generate non-extern struct for representing this data type"); + } + + isIgnoredUndefinedType() { + return this.kind === "undefined"; + } + + isStringType() { + return ( + this.kind === "DOMString" || this.kind === "ByteString" || this.kind === "USVString" || this.kind === "UTF8String" + ); + } + + isNumberType() { + return numericTypes.has(this.kind); + } + + isObjectType() { + return this.kind === "externalClass" || this.kind === "dictionary"; + } + + [Symbol.toStringTag] = "Type"; + [Bun.inspect.custom](depth, options, inspect) { + return ( + `${options.stylize("Type", "special")} ${ + this.lowersToNamedType() && this.nameDeduplicated + ? options.stylize(JSON.stringify(this.nameDeduplicated), "string") + " " + : "" + }${options.stylize( + `[${this.kind}${["required", "optional", "nullable"] + .filter(k => this.flags[k]) + .map(x => ", " + x) + .join("")}]`, + "regexp", + )}` + + (this.data + ? " " + + inspect(this.data, { + ...options, + depth: options.depth === null ? null : options.depth - 1, + }).replace(/\n/g, "\n") + : "") + ); + } + + // Public interface definition API + get optional() { + if (this.flags.required) { + throw new Error("Cannot derive optional on a required type"); + } + if (this.flags.default) { + throw new Error("Cannot derive optional on a something with a default value (default implies optional)"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + optional: true, + }); + } + + get finite() { + if (this.kind !== "f64") { + throw new Error("finite can only be used on f64"); + } + if (this.flags.finite) { + throw new Error("This type already has finite set"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + finite: true, + }); + } + + get required() { + if (this.flags.required) { + throw new Error("This type already has required set"); + } + if (this.flags.required) { + throw new Error("Cannot derive required on an optional type"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + required: true, + }); + } + + default(def: any) { + if ("default" in this.flags) { + throw new Error("This type already has a default value"); + } + if (this.flags.required) { + throw new Error("Cannot derive default on a required type"); + } + this.assertDefaultIsValid(def); + return new TypeImpl(this.kind, this.data, { + ...this.flags, + default: def, + }); + } + + clamp(min?: number | bigint, max?: number | bigint) { + return this.#rangeModifier(min, max, "clamp"); + } + + enforceRange(min?: number | bigint, max?: number | bigint) { + return this.#rangeModifier(min, max, "enforce"); + } + + get nonNull() { + if (this.flags.nonNull) { + throw new Error("Cannot derive nonNull on a nonNull type"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + nonNull: true, + }); + } + + validateInt32(min?: number, max?: number) { + if (this.kind !== "i32") { + throw new Error("validateInt32 can only be used on i32 or u32"); + } + const rangeInfo = cAbiIntegerLimits("i32"); + return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]); + } + + validateUint32(min?: number, max?: number) { + if (this.kind !== "u32") { + throw new Error("validateUint32 can only be used on i32 or u32"); + } + const rangeInfo = cAbiIntegerLimits("u32"); + return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]); + } + + validateInteger(min?: number | bigint, max?: number | bigint) { + min ??= Number.MIN_SAFE_INTEGER; + max ??= Number.MAX_SAFE_INTEGER; + const enforceRange = this.#rangeModifier(min, max, "enforce") as TypeImpl; + enforceRange.flags.nodeValidator = NodeValidator.validateInteger; + return enforceRange; + } +} + +export function cAbiIntegerLimits(type: CAbiType) { + switch (type) { + case "u8": + return [0, 255]; + case "u16": + return [0, 65535]; + case "u32": + return [0, 4294967295]; + case "u64": + return [0, 18446744073709551615n]; + case "usize": + return [0, 18446744073709551615n]; + case "i8": + return [-128, 127]; + case "i16": + return [-32768, 32767]; + case "i32": + return [-2147483648, 2147483647]; + case "i64": + return [-9223372036854775808n, 9223372036854775807n]; + case "f64": + return [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; + default: + throw new Error(`Unexpected type ${type}`); + } +} + +export function cAbiTypeForEnum(length: number): CAbiType { + return ("u" + alignForward(length, 8)) as CAbiType; +} + +export function inspect(value: any) { + return Bun.inspect(value, { colors: Bun.enableANSIColors }); +} + +export function oneOfImpl(types: TypeImpl[]): TypeImpl { + const out: TypeImpl[] = []; + for (const type of types) { + if (type.kind === "oneOf") { + out.push(...type.data); + } else { + if (type.flags.default) { + throw new Error( + "Union type cannot include a default value. Instead, set a default value on the union type itself", + ); + } + if (type.isVirtualArgument()) { + throw new Error(`t.${type.kind} can only be used as a function argument type`); + } + out.push(type); + } + } + return new TypeImpl("oneOf", out); +} + +export function dictionaryImpl(record: Record): TypeImpl { + const out: DictionaryField[] = []; + for (const key in record) { + const type = record[key]; + if (type.isVirtualArgument()) { + throw new Error(`t.${type.kind} can only be used as a function argument type`); + } + out.push({ + key, + type: type, + }); + } + return new TypeImpl("dictionary", out); +} + +export const isFunc = Symbol("isFunc"); + +export interface Func { + [isFunc]: true; + name: string; + zigPrefix: string; + snapshot: string; + zigFile: string; + variants: Variant[]; +} + +export interface Variant { + suffix: string; + args: Arg[]; + ret: TypeImpl; + returnStrategy?: ReturnStrategy; + argStruct?: Struct; + globalObjectArg?: number | "hidden"; + minRequiredArgs: number; + communicationStruct?: Struct; +} + +export interface Arg { + name: string; + type: TypeImpl; + loweringStrategy?: ArgStrategy; + zigMappedName?: string; +} + +/** + * The strategy for moving arguments over the ABI boundary are computed before + * any code is generated so that the proper definitions can be easily made, + * while allow new special cases to be added. + */ +export type ArgStrategy = + // The argument is communicated as a C parameter + | { type: "c-abi-pointer"; abiType: CAbiType } + // The argument is communicated as a C parameter + | { type: "c-abi-value"; abiType: CAbiType } + // The data is added as a field on `.communicationStruct` + | { + type: "uses-communication-buffer"; + /** + * Unique prefix for fields. For example, moving an optional over the ABI + * boundary uses two fields, `bool {prefix}_set` and `T {prefix}_value`. + */ + prefix: string; + /** + * For compound complex types, such as `?union(enum) { a: u32, b: + * bun.String }`, the child item is assigned the prefix + * `{prefix_of_optional}_value`. The interpretation of this array depends + * on `arg.type.kind`. + */ + children: ArgStrategyChildItem[]; + }; + +export type ArgStrategyChildItem = + | { + type: "c-abi-compatible"; + abiType: CAbiType; + } + | { + type: "uses-communication-buffer"; + prefix: string; + children: ArgStrategyChildItem[]; + }; +/** + * In addition to moving a payload over, an additional bit of information + * crosses the ABI boundary indicating if the function threw an exception. + * + * For simplicity, the possibility of any Zig binding returning an error/calling + * `throw` is assumed and there isnt a way to disable the exception check. + */ +export type ReturnStrategy = + // JSValue is special cased because it encodes exception as 0x0 + | { type: "jsvalue" } + // Return value doesnt exist. function returns a boolean indicating success/error. + | { type: "void" } + // For primitives and simple structures where direct assignment into a + // pointer is possible. function returns a boolean indicating success/error. + | { type: "basic-out-param"; abiType: CAbiType }; + +export interface File { + functions: Func[]; + typedefs: TypeDef[]; +} + +export interface TypeDef { + name: string; + type: TypeImpl; +} + +export function registerFunction(opts: FuncOptions) { + const snapshot = snapshotCallerLocation(); + const filename = stackTraceFileName(snapshot); + expect(filename).toEndWith(".bind.ts"); + const zigFile = path.relative(src, filename.replace(/\.bind\.ts$/, ".zig")); + let file = files.get(zigFile); + if (!file) { + file = { functions: [], typedefs: [] }; + files.set(zigFile, file); + } + const variants: Variant[] = []; + if ("variants" in opts) { + let i = 1; + for (const variant of opts.variants) { + const { minRequiredArgs } = validateVariant(variant); + variants.push({ + args: Object.entries(variant.args).map(([name, type]) => ({ name, type })) as Arg[], + ret: variant.ret as TypeImpl, + suffix: `${i}`, + minRequiredArgs, + } as unknown as Variant); + i++; + } + } else { + const { minRequiredArgs } = validateVariant(opts); + variants.push({ + suffix: "", + args: Object.entries(opts.args).map(([name, type]) => ({ name, type })) as Arg[], + ret: opts.ret as TypeImpl, + minRequiredArgs, + }); + } + + const func: Func = { + [isFunc]: true, + name: "", + zigPrefix: opts.implNamespace ? `${opts.implNamespace}.` : "", + snapshot, + zigFile, + variants, + }; + allFunctions.push(func); + file.functions.push(func); + return func; +} + +function validateVariant(variant: any) { + let minRequiredArgs = 0; + let seenOptionalArgument = false; + let i = 0; + + for (const [name, type] of Object.entries(variant.args) as [string, TypeImpl][]) { + if (!(type instanceof TypeImpl)) { + throw new Error(`Expected type for argument ${name}, got ${inspect(type)}`); + } + i += 1; + if (type.isVirtualArgument()) { + continue; + } + if (!type.flags.optional && !("default" in type.flags)) { + if (seenOptionalArgument) { + throw new Error(`Required argument ${name} cannot follow an optional argument`); + } + minRequiredArgs++; + } else { + seenOptionalArgument = true; + } + } + + return { minRequiredArgs }; +} + +function snapshotCallerLocation(): string { + const stack = new Error().stack!; + const lines = stack.split("\n"); + let i = 1; + for (; i < lines.length; i++) { + if (!lines[i].includes(import.meta.dir)) { + return lines[i]; + } + } + throw new Error("Couldn't find caller location in stack trace"); +} + +function stackTraceFileName(line: string): string { + const match = /(?:at\s+|\()(.:?[^:\n(\)]*)[^(\n]*$/i.exec(line); + assert(match, `Couldn't extract filename from stack trace line: ${line}`); + return match[1].replaceAll("\\", "/"); +} + +export type CAbiType = + | "*anyopaque" + | "*JSGlobalObject" + | "JSValue" + | "JSValue.MaybeException" + | "u0" + | "bun.String" + | "bool" + | "u8" + | "u16" + | "u32" + | "u64" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "f64" + | Struct; + +export function cAbiTypeInfo(type: CAbiType): [size: number, align: number] { + if (typeof type !== "string") { + return type.abiInfo(); + } + switch (type) { + case "u0": + return [0, 0]; // no-op + case "bool": + case "u8": + case "i8": + return [1, 1]; + case "u16": + case "i16": + return [2, 2]; + case "u32": + case "i32": + return [4, 4]; + case "usize": + case "u64": + case "i64": + case "f64": + return [8, 8]; + case "*anyopaque": + case "*JSGlobalObject": + case "JSValue": + case "JSValue.MaybeException": + return [8, 8]; // pointer size + case "bun.String": + return [24, 8]; + default: + throw new Error("unexpected: " + (type satisfies never)); + } +} + +export function cAbiTypeName(type: CAbiType) { + if (typeof type !== "string") { + return type.name(); + } + return ( + { + "*anyopaque": "void*", + "*JSGlobalObject": "JSC::JSGlobalObject*", + "JSValue": "JSValue", + "JSValue.MaybeException": "JSValue", + "bool": "bool", + "u8": "uint8_t", + "u16": "uint16_t", + "u32": "uint32_t", + "u64": "uint64_t", + "i8": "int8_t", + "i16": "int16_t", + "i32": "int32_t", + "i64": "int64_t", + "f64": "double", + "usize": "size_t", + "bun.String": "BunString", + u0: "void", + } satisfies Record, string> + )[type]; +} + +export function alignForward(size: number, alignment: number) { + return Math.floor((size + alignment - 1) / alignment) * alignment; +} + +export class Struct { + fields: StructField[] = []; + #hash?: string; + #name?: string; + namespace?: string; + + abiInfo(): [size: number, align: number] { + let size = 0; + let align = 0; + for (const field of this.fields) { + size = alignForward(size, field.naturalAlignment); + size += field.size; + align = Math.max(align, field.naturalAlignment); + } + return [size, align]; + } + + reorderForSmallestSize() { + // for conistency sort by alignment, then size, then name + this.fields.sort((a, b) => { + if (a.naturalAlignment !== b.naturalAlignment) { + return a.naturalAlignment - b.naturalAlignment; + } + if (a.size !== b.size) { + return a.size - b.size; + } + return a.name.localeCompare(b.name); + }); + } + + hash() { + return (this.#hash ??= String( + Bun.hash( + this.fields + .map(f => { + if (f.type instanceof Struct) { + return f.name + `:` + f.type.hash(); + } + return f.name + `:` + f.type; + }) + .join(","), + ), + )); + } + + name() { + if (this.#name) return this.#name; + const hash = this.hash(); + const existing = structHashToSelf.get(hash); + if (existing && existing !== this) return (this.#name = existing.name()); + return (this.#name = `anon_extern_struct_${hash}`); + } + + toString() { + return this.namespace ? `${this.namespace}.${this.name()}` : this.name(); + } + + assignName(name: string) { + if (this.#name) return; + const hash = this.hash(); + const existing = structHashToSelf.get(hash); + if (existing && existing.#name) name = existing.#name; + this.#name = name; + if (existing) existing.#name = name; + } + + assignGeneratedName(name: string) { + if (this.#name) return; + this.assignName(name); + } + + add(name: string, cType: CAbiType) { + const [size, naturalAlignment] = cAbiTypeInfo(cType); + this.fields.push({ name, type: cType, size, naturalAlignment }); + } + + emitZig(zig: CodeWriter, semi: "with-semi" | "no-semi") { + zig.line("extern struct {"); + zig.indent(); + for (const field of this.fields) { + zig.line(`${snake(field.name)}: ${field.type},`); + } + zig.dedent(); + zig.line("}" + (semi === "with-semi" ? ";" : "")); + } + + emitCpp(cpp: CodeWriter, structName: string) { + cpp.line(`struct ${structName} {`); + cpp.indent(); + for (const field of this.fields) { + cpp.line(`${cAbiTypeName(field.type)} ${field.name};`); + } + cpp.dedent(); + cpp.line("};"); + } +} + +export interface StructField { + /** camel case */ + name: string; + type: CAbiType; + size: number; + naturalAlignment: number; +} + +export class CodeWriter { + level = 0; + buffer = ""; + + temporaries = new Set(); + + line(s?: string) { + this.add((s ?? "") + "\n"); + } + + add(s: string) { + this.buffer += (this.buffer.endsWith("\n") ? " ".repeat(this.level) : "") + s; + } + + indent() { + this.level += 1; + } + + dedent() { + this.level -= 1; + } + + trimLastNewline() { + this.buffer = this.buffer.trimEnd(); + } + + resetTemporaries() { + this.temporaries.clear(); + } + + nextTemporaryName(label: string) { + let i = 0; + let name = `${label}_${i}`; + while (this.temporaries.has(name)) { + i++; + name = `${label}_${i}`; + } + this.temporaries.add(name); + return name; + } +} diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts new file mode 100644 index 0000000000..5594df29fc --- /dev/null +++ b/src/codegen/bindgen-lib.ts @@ -0,0 +1,363 @@ +/** + * This is the public API for `bind.ts` files + * It is aliased as `import {} from 'bindgen'` + * @see https://bun.sh/docs/project/bindgen + */ + +import { + isType, + dictionaryImpl, + oneOfImpl, + registerFunction, + TypeImpl, + type TypeKind, + isFunc, +} from "./bindgen-lib-internal"; + +/** A type definition for argument parsing. See `bindgen.md` for usage details. */ +export type Type< + /** T = JavaScript type that the Type represents */ + T, + /** K = "kind" a string pertaining to the `t.` that created this type. affects method listing */ + K extends TypeKind = TypeKind, + /** F = "flags" defining if the value is optional. null = not set, false = required, true = optional. */ + F extends TypeFlag = null, +> = F extends null + ? Props + : F extends true + ? { + [isType]: true | [T, K, F]; + nonNull: Type; + } + : { [isType]: true | [T, K, F] }; + +type TypeFlag = boolean | "opt-nonnull" | null; + +interface BaseTypeProps { + [isType]: true | [T, K]; + /** + * Optional means the value may be omitted from a parameter definition. + * Parameters are required by default. + */ + optional: Type; + /** + * When this is used as a dictionary value, this makes that parameter + * required. Dictionary entries are optional by default. + */ + required: Type, K, false>; + + /** Implies `optional`, this sets a default value if omitted */ + default(def: T): Type; +} + +interface NumericTypeProps extends BaseTypeProps { + /** + * Applies [Clamp] semantics + * https://webidl.spec.whatwg.org/#Clamp + * If a custom numeric range is provided, it will be used instead of the built-in clamp rules. + */ + clamp(min?: T, max?: T): Type; + /** + * Applies [EnforceRange] semantics + * https://webidl.spec.whatwg.org/#EnforceRange + * If a custom numeric range is provided, it will be used instead of the built-in enforce rules. + */ + enforceRange(min?: T, max?: T): Type; + + /** + * Equivalent to calling Node.js' `validateInteger(val, prop, min, max)` + */ + validateInteger(min?: T, max?: T): Type; +} + +interface I32TypeProps extends NumericTypeProps { + /** + * Equivalent to calling Node.js' `validateInt32(val, prop, min, max)` + */ + validateInt32(min?: number, max?: number): Type; +} + +interface U32TypeProps extends NumericTypeProps { + /** + * Equivalent to calling Node.js' `validateUint32(val, prop, min, max)` + */ + validateUint32(min?: number, max?: number): Type; +} + +interface F64TypeProps extends NumericTypeProps { + /** + * Throws an error if the input is non-finite (NaN, ±Infinity) + */ + finite: Type; + /** + * Equivalent to calling Node.js' `validateNumber(val, prop, min, max)` + */ + validateNumber(min?: number, max?: number): Type; +} + +// If an entry does not exist, then `BaseTypeProps` is assumed. +// T = JavaScript type that the Type represents +interface TypePropsMap { + // Integer types are always numbers, so T is not passed + ["u8"]: NumericTypeProps; + ["i8"]: NumericTypeProps; + ["u16"]: NumericTypeProps; + ["i16"]: NumericTypeProps; + ["u32"]: U32TypeProps; + ["i32"]: I32TypeProps; + ["u64"]: NumericTypeProps; + ["i64"]: NumericTypeProps; + // F64 is always a number, so T is not passed. + ["f64"]: F64TypeProps; +} + +type PropertyMapKeys = keyof TypePropsMap; +type Props = K extends PropertyMapKeys ? TypePropsMap[K] : BaseTypeProps; + +export type AcceptedDictionaryTypeKind = Exclude; + +function builtinType() { + return (kind: K) => new TypeImpl(kind, undefined as any, {}) as Type as Type; +} + +/** Contains all primitive types provided by the bindings generator */ +export namespace t { + /** + * Can only be used as an argument type. + * Tells the code generator to pass `*JSC.JSGlobalObject` as a parameter + */ + export const globalObject = builtinType()("globalObject"); + /** + * Can only be used as an argument type. + * Tells the code generator to pass `*JSC.VirtualMachine` as a parameter + */ + export const zigVirtualMachine = builtinType()("zigVirtualMachine"); + + /** + * Provides the raw JSValue from the JavaScriptCore API. Avoid using this if + * possible. This indicates the bindings generator is incapable of processing + * your use case. + */ + export const any = builtinType()("any"); + /** Void function type */ + export const undefined = builtinType()("undefined"); + /** Does not throw on parse. Equivalent to `!!value` */ + export const boolean = builtinType()("boolean"); + /** Throws if the value is not a boolean. */ + export const strictBoolean = builtinType()("strictBoolean"); + + /** + * Equivalent to IDL's `unrestricted double`, allowing NaN and Infinity. + * To restrict to finite values, use `f64.finite`. + */ + export const f64 = builtinType()("f64"); + + export const u8 = builtinType()("u8"); + export const u16 = builtinType()("u16"); + export const u32 = builtinType()("u32"); + export const u64 = builtinType()("u64"); + export const i8 = builtinType()("i8"); + export const i16 = builtinType()("i16"); + export const i32 = builtinType()("i32"); + export const i64 = builtinType()("i64"); + export const usize = builtinType()("usize"); + + /** + * The DOMString type corresponds to strings. + * + * @note A DOMString value might include unmatched surrogate code points. + * Use USVString if this is not desirable. + * + * @see https://webidl.spec.whatwg.org/#idl-DOMString + */ + export const DOMString = builtinType()("DOMString"); + /** + * The {@link USVString} type corresponds to scalar value strings. Depending on the + * context, these can be treated as sequences of code units or scalar values. + * + * Specifications should only use USVString for APIs that perform text + * processing and need a string of scalar values to operate on. Most APIs that + * use strings should instead be using {@link DOMString}, which does not make any + * interpretations of the code units in the string. When in doubt, use + * {@link DOMString} + * + * @see https://webidl.spec.whatwg.org/#idl-USVString + */ + export const USVString = builtinType()("USVString"); + /** + * The ByteString type corresponds to byte sequences. + * + * WARNING: Specifications should only use ByteString for interfacing with + * protocols that use bytes and strings interchangeably, such as HTTP. In + * general, strings should be represented with {@link DOMString} values, even + * if it is expected that values of the string will always be in ASCII or some + * 8-bit character encoding. Sequences or frozen arrays with octet or byte + * elements, {@link Uint8Array}, or {@link Int8Array} should be used for + * holding 8-bit data rather than `ByteString`. + * + * https://webidl.spec.whatwg.org/#idl-ByteString + */ + export const ByteString = builtinType()("ByteString"); + /** + * DOMString but encoded as `[]const u8` + * + * ```ts + * // foo.bind.ts + * import { fn, t } from "bindgen"; + * + * export const foo = fn({ + * args: { bar: t.UTF8String }, + * }) + * ``` + * + * ```zig + * // foo.zig + * pub fn foo(bar: []const u8) void { + * // ... + * } + * ``` + */ + export const UTF8String = builtinType()("UTF8String"); + + /** An array or iterable type of T */ + export function sequence(itemType: Type): Type, "sequence"> { + return new TypeImpl("sequence", { + element: itemType as TypeImpl, + repr: "slice", + }); + } + + /** Object with arbitrary keys but a specific value type */ + export function record(valueType: Type): Type, "record"> { + return new TypeImpl("record", { + value: valueType as TypeImpl, + repr: "kv-slices", + }); + } + + /** + * Reference a type by string name instead of by object reference. This is + * required in some siutations like {@link Request} which can take an existing + * request object in as itself. + */ + export function ref(name: string): Type { + return new TypeImpl("ref", name); + } + + /** + * Reference an external class type that is not defined with `bindgen`, + * from either WebCore, JavaScriptCore, or Bun. + */ + export function externalClass(name: string): Type { + return new TypeImpl("ref", name); + } + + export function oneOf[]>( + ...types: T + ): Type< + { + [K in keyof T]: T[K] extends Type ? U : never; + }[number], + "oneOf" + > { + return oneOfImpl(types as unknown[] as TypeImpl[]); + } + + export function dictionary>>( + fields: R, + ): Type< + { + [K in keyof R]?: R[K] extends Type ? T : never; + }, + "dictionary" + > { + return dictionaryImpl(fields as Record); + } + + /** Create an enum from a list of strings. */ + export function stringEnum( + ...values: T + ): Type< + { + [K in keyof T]: K; + }[number], + "stringEnum" + > { + return new TypeImpl("stringEnum", values.sort()); + } + + /** + * Equivalent to `stringEnum`, but using an enum sourced from the given Zig + * file. Use this to get an enum type that can have functions added. + */ + export function zigEnum(file: string, impl: string): Type { + return new TypeImpl("zigEnum", { file, impl }); + } +} + +interface FuncOptionsWithVariant extends FuncMetadata { + /** + * Declare a function with multiple overloads. Each overload gets its own + * native function named "name`n`" where `n` is the 1-based index of the + * overload. + * + * ## Example + * ```ts + * // foo.bind.ts + * import { fn } from "bindgen"; + * + * export const foo = fn({ + * variants: [ + * { + * args: { a: t.i32 }, + * ret: t.i32, + * }, + * { + * args: { a: t.i32, b: t.i32 }, + * ret: t.boolean, + * } + * ] + * }); + * ``` + * + * ```zig + * // foo.zig + * pub fn foo1(a: i32) i32 { + * return a; + * } + * + * pub fn foo2(a: i32, b: i32) bool { + * return a == b; + * } + * ``` + */ + variants: FuncVariant[]; +} +type FuncWithoutOverloads = FuncMetadata & FuncVariant; +type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; + +export interface FuncMetadata { + /** + * The namespace where the implementation is, by default it's in the root. + */ + implNamespace?: string; + /** + * TODO: + * Automatically generate code to expose this function on a well-known object + */ + exposedOn?: ExposedOn; +} + +export type FuncReference = { [isFunc]: true }; + +export type ExposedOn = "JSGlobalObject" | "BunObject"; + +export interface FuncVariant { + /** Ordered record. Cannot include ".required" types since required is the default. */ + args: Record>; + ret: Type; +} + +export function fn(opts: FuncOptions) { + return registerFunction(opts) as FuncReference; +} diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts new file mode 100644 index 0000000000..b3d8be92c6 --- /dev/null +++ b/src/codegen/bindgen.ts @@ -0,0 +1,1583 @@ +// The binding generator to rule them all. +// Converts binding definition files (.bind.ts) into C++ and Zig code. +// +// Generated bindings are available in `bun.generated..*` in Zig, +// or `Generated::::*` in C++ from including `Generated.h`. +import * as path from "node:path"; +import fs from "node:fs"; +import { + CodeWriter, + TypeImpl, + cAbiTypeName, + cap, + extDispatchVariant, + extJsFunction, + files, + snake, + src, + str, + Struct, + type CAbiType, + type DictionaryField, + type ReturnStrategy, + type TypeKind, + type Variant, + typeHashToNamespace, + typeHashToReachableType, + zid, + ArgStrategyChildItem, + inspect, + pascal, + alignForward, + isFunc, + Func, + NodeValidator, + cAbiIntegerLimits, + extInternalDispatchVariant, +} from "./bindgen-lib-internal"; +import assert from "node:assert"; +import { argParse, readdirRecursiveWithExclusionsAndExtensionsSync, writeIfNotChanged } from "./helpers"; + +// arg parsing +let { "codegen-root": codegenRoot, debug } = argParse(["codegen-root", "debug"]); +if (debug === "false" || debug === "0" || debug == "OFF") debug = false; +if (!codegenRoot) { + console.error("Missing --codegen-root=..."); + process.exit(1); +} + +function resolveVariantStrategies(vari: Variant, name: string) { + let argIndex = 0; + let communicationStruct: Struct | undefined; + for (const arg of vari.args) { + if (arg.type.isVirtualArgument() && vari.globalObjectArg === undefined) { + vari.globalObjectArg = argIndex; + } + argIndex += 1; + + // If `extern struct` can represent this type, that is the simplest way to cross the C-ABI boundary. + const isNullable = arg.type.flags.optional && !("default" in arg.type.flags); + const abiType = !isNullable && arg.type.canDirectlyMapToCAbi(); + if (abiType) { + arg.loweringStrategy = { + // This does not work in release builds, possibly due to a Zig 0.13 bug + // regarding by-value extern structs in C functions. + // type: cAbiTypeInfo(abiType)[0] > 8 ? "c-abi-pointer" : "c-abi-value", + // Always pass an argument by-pointer for now. + type: abiType === "*anyopaque" || abiType === "*JSGlobalObject" ? "c-abi-value" : "c-abi-pointer", + abiType, + }; + continue; + } + + communicationStruct ??= new Struct(); + const prefix = `${arg.name}`; + const children = isNullable + ? resolveNullableArgumentStrategy(arg.type, prefix, communicationStruct) + : resolveComplexArgumentStrategy(arg.type, prefix, communicationStruct); + arg.loweringStrategy = { + type: "uses-communication-buffer", + prefix, + children, + }; + } + + if (vari.globalObjectArg === undefined) { + vari.globalObjectArg = "hidden"; + } + + return_strategy: { + if (vari.ret.kind === "undefined") { + vari.returnStrategy = { type: "void" }; + break return_strategy; + } + if (vari.ret.kind === "any") { + vari.returnStrategy = { type: "jsvalue" }; + break return_strategy; + } + const abiType = vari.ret.canDirectlyMapToCAbi(); + if (abiType) { + vari.returnStrategy = { + type: "basic-out-param", + abiType, + }; + break return_strategy; + } + } + + communicationStruct?.reorderForSmallestSize(); + communicationStruct?.assignGeneratedName(name); + vari.communicationStruct = communicationStruct; +} + +function resolveNullableArgumentStrategy( + type: TypeImpl, + prefix: string, + communicationStruct: Struct, +): ArgStrategyChildItem[] { + assert(type.flags.optional && !("default" in type.flags)); + communicationStruct.add(`${prefix}Set`, "bool"); + return resolveComplexArgumentStrategy(type, `${prefix}Value`, communicationStruct); +} + +function resolveComplexArgumentStrategy( + type: TypeImpl, + prefix: string, + communicationStruct: Struct, +): ArgStrategyChildItem[] { + const abiType = type.canDirectlyMapToCAbi(); + if (abiType) { + communicationStruct.add(prefix, abiType); + return [ + { + type: "c-abi-compatible", + abiType, + }, + ]; + } + + switch (type.kind) { + default: + throw new Error(`TODO: resolveComplexArgumentStrategy for ${type.kind}`); + } +} + +function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionName: string) { + cpp.line(`auto& vm = JSC::getVM(global);`); + cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); + if (variant.minRequiredArgs > 0) { + cpp.line(`size_t argumentCount = callFrame->argumentCount();`); + cpp.line(`if (argumentCount < ${variant.minRequiredArgs}) {`); + cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); + cpp.line(`}`); + } + const communicationStruct = variant.communicationStruct; + if (communicationStruct) { + cpp.line(`${communicationStruct.name()} buf;`); + communicationStruct.emitCpp(cppInternal, communicationStruct.name()); + } + + let i = 0; + for (const arg of variant.args) { + const type = arg.type; + if (type.isVirtualArgument()) continue; + if (type.isIgnoredUndefinedType()) { + i += 1; + continue; + } + + const exceptionContext: ExceptionContext = { + type: "argument", + argumentIndex: i, + name: arg.name, + functionName: name, + }; + + const strategy = arg.loweringStrategy!; + assert(strategy); + + const get = variant.minRequiredArgs > i ? "uncheckedArgument" : "argument"; + cpp.line(`JSC::EnsureStillAliveScope arg${i} = callFrame->${get}(${i});`); + + let storageLocation; + let needDeclare = true; + switch (strategy.type) { + case "c-abi-pointer": + case "c-abi-value": + storageLocation = "arg" + cap(arg.name); + break; + case "uses-communication-buffer": + storageLocation = `buf.${strategy.prefix}`; + needDeclare = false; + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); + } + + const jsValueRef = `arg${i}.value()`; + + /** If JavaScript may pass null or undefined */ + const isOptionalToUser = type.flags.optional || "default" in type.flags; + /** If the final representation may include null */ + const isNullable = type.flags.optional && !("default" in type.flags); + + if (isOptionalToUser) { + if (needDeclare) { + addHeaderForType(type); + cpp.line(`${type.cppName()} ${storageLocation};`); + } + const isUndefinedOrNull = type.flags.nonNull ? "isUndefined" : "isUndefinedOrNull"; + if (isNullable) { + assert(strategy.type === "uses-communication-buffer"); + cpp.line(`if ((${storageLocation}Set = !${jsValueRef}.${isUndefinedOrNull}())) {`); + storageLocation = `${storageLocation}Value`; + } else { + cpp.line(`if (!${jsValueRef}.${isUndefinedOrNull}()) {`); + } + cpp.indent(); + emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, "assign"); + cpp.dedent(); + if ("default" in type.flags) { + cpp.line(`} else {`); + cpp.indent(); + cpp.add(`${storageLocation} = `); + type.emitCppDefaultValue(cpp); + cpp.line(";"); + cpp.dedent(); + } else { + assert(isNullable); + } + cpp.line(`}`); + } else { + emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, needDeclare ? "declare" : "assign"); + } + + i += 1; + } + + const returnStrategy = variant.returnStrategy!; + switch (returnStrategy.type) { + case "jsvalue": + cpp.line(`return ${dispatchFunctionName}(`); + break; + case "basic-out-param": + cpp.line(`${cAbiTypeName(returnStrategy.abiType)} out;`); + cpp.line(`if (!${dispatchFunctionName}(`); + break; + case "void": + cpp.line(`if (!${dispatchFunctionName}(`); + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); + } + + let emittedFirstArgument = false; + function addCommaAfterArgument() { + if (emittedFirstArgument) { + cpp.line(","); + } else { + emittedFirstArgument = true; + } + } + + const totalArgs = variant.args.length; + i = 0; + cpp.indent(); + + if (variant.globalObjectArg === "hidden") { + addCommaAfterArgument(); + cpp.add("global"); + } + + for (const arg of variant.args) { + i += 1; + if (arg.type.isIgnoredUndefinedType()) continue; + + if (arg.type.isVirtualArgument()) { + switch (arg.type.kind) { + case "zigVirtualMachine": + case "globalObject": + addCommaAfterArgument(); + cpp.add("global"); + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(arg.type)}`); + } + } else { + const storageLocation = `arg${cap(arg.name)}`; + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + addCommaAfterArgument(); + cpp.add(`&${storageLocation}`); + break; + case "c-abi-value": + addCommaAfterArgument(); + cpp.add(`${storageLocation}`); + break; + case "uses-communication-buffer": + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); + } + } + } + + if (communicationStruct) { + addCommaAfterArgument(); + cpp.add("&buf"); + } + + switch (returnStrategy.type) { + case "jsvalue": + cpp.dedent(); + if (totalArgs === 0) { + cpp.trimLastNewline(); + } + cpp.line(");"); + break; + case "void": + cpp.dedent(); + cpp.line(")) {"); + cpp.line(` return {};`); + cpp.line("}"); + cpp.line("return JSC::JSValue::encode(JSC::jsUndefined());"); + break; + case "basic-out-param": + addCommaAfterArgument(); + cpp.add("&out"); + cpp.line(); + cpp.dedent(); + cpp.line(")) {"); + cpp.line(` return {};`); + cpp.line("}"); + const simpleType = getSimpleIdlType(variant.ret); + if (simpleType) { + cpp.line(`return JSC::JSValue::encode(WebCore::toJS<${simpleType}>(*global, out));`); + break; + } + switch (variant.ret.kind) { + case "UTF8String": + throw new Error("Memory lifetime is ambiguous when returning UTF8String"); + case "DOMString": + case "USVString": + case "ByteString": + cpp.line( + `return JSC::JSValue::encode(WebCore::toJS(*global, out.toWTFString()));`, + ); + break; + } + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); + } +} + +/** If a simple IDL type mapping exists, it also currently means there is a direct C ABI mapping */ +function getSimpleIdlType(type: TypeImpl): string | undefined { + const map: { [K in TypeKind]?: string } = { + boolean: "WebCore::IDLBoolean", + undefined: "WebCore::IDLUndefined", + usize: "WebCore::IDLUnsignedLongLong", + u8: "WebCore::IDLOctet", + u16: "WebCore::IDLUnsignedShort", + u32: "WebCore::IDLUnsignedLong", + u64: "WebCore::IDLUnsignedLongLong", + i8: "WebCore::IDLByte", + i16: "WebCore::IDLShort", + i32: "WebCore::IDLLong", + i64: "WebCore::IDLLongLong", + }; + let entry = map[type.kind]; + if (!entry) { + switch (type.kind) { + case "f64": + entry = type.flags.finite // + ? "WebCore::IDLDouble" + : "WebCore::IDLUnrestrictedDouble"; + break; + case "stringEnum": + type.lowersToNamedType; + // const cType = cAbiTypeForEnum(type.data.length); + // entry = map[cType as IntegerTypeKind]!; + entry = `WebCore::IDLEnumeration<${type.cppClassName()}>`; + break; + default: + return; + } + } + + if (type.flags.range) { + const { range, nodeValidator } = type.flags; + if ((range[0] === "enforce" && range[1] !== "abi") || nodeValidator) { + if (nodeValidator) assert(nodeValidator === NodeValidator.validateInteger); // TODO? + + const [abiMin, abiMax] = cAbiIntegerLimits(type.kind as CAbiType); + let [_, min, max] = range as [string, bigint | number | "abi", bigint | number | "abi"]; + if (min === "abi") min = abiMin; + if (max === "abi") max = abiMax; + + headers.add("BindgenCustomEnforceRange.h"); + entry = `Bun::BindgenCustomEnforceRange<${cAbiTypeName(type.kind as CAbiType)}, ${min}, ${max}, Bun::BindgenCustomEnforceRangeKind::${ + nodeValidator ? "Node" : "Web" + }>`; + } else { + const rangeAdaptor = { + "clamp": "WebCore::IDLClampAdaptor", + "enforce": "WebCore::IDLEnforceRangeAdaptor", + }[range[0]]; + assert(rangeAdaptor); + entry = `${rangeAdaptor}<${entry}>`; + } + } + + return entry; +} + +type ExceptionContext = + | { type: "none" } + | { type: "argument"; argumentIndex: number; name: string; functionName: string }; + +function emitConvertValue( + storageLocation: string, + type: TypeImpl, + jsValueRef: string, + exceptionContext: ExceptionContext, + decl: "declare" | "assign", +) { + if (decl === "declare") { + addHeaderForType(type); + } + + const simpleType = getSimpleIdlType(type); + if (simpleType) { + const cAbiType = type.canDirectlyMapToCAbi(); + assert(cAbiType); + let exceptionHandler: ExceptionHandler | undefined; + switch (exceptionContext.type) { + case "none": + break; + case "argument": + exceptionHandler = getArgumentExceptionHandler( + type, + exceptionContext.argumentIndex, + exceptionContext.name, + exceptionContext.functionName, + ); + } + + switch (type.kind) { + } + + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + + let exceptionHandlerText = exceptionHandler ? `, ${exceptionHandler.params} { ${exceptionHandler.body} }` : ""; + cpp.line(`${storageLocation} = WebCore::convert<${simpleType}>(*global, ${jsValueRef}${exceptionHandlerText});`); + + if (type.flags.range && type.flags.range[0] === "clamp" && type.flags.range[1] !== "abi") { + emitRangeModifierCheck(cAbiType, storageLocation, type.flags.range); + } + + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + } else { + switch (type.kind) { + case "any": { + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + cpp.line(`${storageLocation} = JSC::JSValue::encode(${jsValueRef});`); + break; + } + case "USVString": + case "DOMString": + case "ByteString": { + const temp = cpp.nextTemporaryName("wtfString"); + cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + cpp.line(`${storageLocation} = Bun::toString(${temp});`); + break; + } + case "UTF8String": { + const temp = cpp.nextTemporaryName("wtfString"); + cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + cpp.line(`${storageLocation} = Bun::toString(${temp});`); + break; + } + case "dictionary": { + if (decl === "declare") { + cpp.line(`${type.cppName()} ${storageLocation};`); + } + cpp.line(`if (!convert${type.cppInternalName()}(&${storageLocation}, global, ${jsValueRef}))`); + cpp.indent(); + cpp.line(`return {};`); + cpp.dedent(); + break; + } + default: + throw new Error(`TODO: emitConvertValue for Type ${type.kind}`); + } + } +} + +interface ExceptionHandler { + /** @example "[](JSC::JSGlobalObject& global, ThrowScope& scope)" */ + params: string; + /** @example "WebCore::throwTypeError(global, scope)" */ + body: string; +} + +function getArgumentExceptionHandler(type: TypeImpl, argumentIndex: number, name: string, functionName: string) { + const { nodeValidator } = type.flags; + if (nodeValidator) { + switch (nodeValidator) { + case NodeValidator.validateInteger: + headers.add("ErrorCode.h"); + return { + params: `[]()`, + body: `return ${str(name)}_s;`, + }; + default: + throw new Error(`TODO: implement exception thrower for node validator ${nodeValidator}`); + } + } + switch (type.kind) { + case "zigEnum": + case "stringEnum": { + return { + params: `[](JSC::JSGlobalObject& global, JSC::ThrowScope& scope)`, + body: `WebCore::throwArgumentMustBeEnumError(${[ + `global`, + `scope`, + `${argumentIndex}`, + `${str(name)}_s`, + `${str(type.name())}_s`, + `${str(functionName)}_s`, + `WebCore::expectedEnumerationValues<${type.cppClassName()}>()`, + ].join(", ")});`, + }; + break; + } + } +} + +/** + * The built in WebCore range adaptors do not support arbitrary ranges, but that + * is something we want to have. They aren't common, so they are just tacked + * onto the webkit one. + */ +function emitRangeModifierCheck( + cAbiType: CAbiType, + storageLocation: string, + range: ["clamp" | "enforce", bigint, bigint], +) { + const [kind, min, max] = range; + if (kind === "clamp") { + cpp.line(`if (${storageLocation} < ${min}) ${storageLocation} = ${min};`); + cpp.line(`else if (${storageLocation} > ${max}) ${storageLocation} = ${max};`); + } else { + // Implemented in BindgenCustomEnforceRange + throw new Error(`This should not be called for 'enforceRange' types.`); + } +} + +function addHeaderForType(type: TypeImpl) { + if (type.lowersToNamedType() && type.ownerFile) { + headers.add(`Generated${pascal(type.ownerFile)}.h`); + } +} + +function emitConvertDictionaryFunction(type: TypeImpl) { + assert(type.kind === "dictionary"); + const fields = type.data as DictionaryField[]; + + addHeaderForType(type); + + cpp.line(`// Internal dictionary parse for ${type.name()}`); + cpp.line( + `bool convert${type.cppInternalName()}(${type.cppName()}* result, JSC::JSGlobalObject* global, JSC::JSValue value) {`, + ); + cpp.indent(); + + cpp.line(`auto& vm = JSC::getVM(global);`); + cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); + cpp.line(`bool isNullOrUndefined = value.isUndefinedOrNull();`); + cpp.line(`auto* object = isNullOrUndefined ? nullptr : value.getObject();`); + cpp.line(`if (UNLIKELY(!isNullOrUndefined && !object)) {`); + cpp.line(` throwTypeError(global, throwScope);`); + cpp.line(` return false;`); + cpp.line(`}`); + cpp.line(`JSC::JSValue propValue;`); + + for (const field of fields) { + const { key, type: fieldType } = field; + cpp.line("// " + key); + cpp.line(`if (isNullOrUndefined) {`); + cpp.line(` propValue = JSC::jsUndefined();`); + cpp.line(`} else {`); + headers.add("ObjectBindings.h"); + cpp.line( + ` propValue = Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, global, object, JSC::Identifier::fromString(vm, ${str(key)}_s));`, + ); + cpp.line(` RETURN_IF_EXCEPTION(throwScope, false);`); + cpp.line(`}`); + cpp.line(`if (!propValue.isUndefined()) {`); + cpp.indent(); + emitConvertValue(`result->${key}`, fieldType, "propValue", { type: "none" }, "assign"); + cpp.dedent(); + cpp.line(`} else {`); + cpp.indent(); + if (type.flags.required) { + cpp.line(`throwTypeError(global, throwScope);`); + cpp.line(`return false;`); + } else if ("default" in fieldType.flags) { + cpp.add(`result->${key} = `); + fieldType.emitCppDefaultValue(cpp); + cpp.line(";"); + } else { + throw new Error(`TODO: optional dictionary field`); + } + cpp.dedent(); + cpp.line(`}`); + } + + cpp.line(`return true;`); + cpp.dedent(); + cpp.line(`}`); + cpp.line(); +} + +function emitZigStruct(type: TypeImpl) { + zig.add(`pub const ${type.name()} = `); + + switch (type.kind) { + case "zigEnum": + case "stringEnum": { + const signPrefix = "u"; + const tagType = `${signPrefix}${alignForward(type.data.length, 8)}`; + zig.line(`enum(${tagType}) {`); + zig.indent(); + for (const value of type.data) { + zig.line(`${snake(value)},`); + } + zig.dedent(); + zig.line("};"); + return; + } + } + + const externLayout = type.canDirectlyMapToCAbi(); + if (externLayout) { + if (typeof externLayout === "string") { + zig.line(externLayout + ";"); + } else { + externLayout.emitZig(zig, "with-semi"); + } + return; + } + + switch (type.kind) { + case "dictionary": { + zig.line("struct {"); + zig.indent(); + for (const { key, type: fieldType } of type.data as DictionaryField[]) { + zig.line(` ${snake(key)}: ${zigTypeName(fieldType)},`); + } + zig.dedent(); + zig.line(`};`); + break; + } + default: { + throw new Error(`TODO: emitZigStruct for Type ${type.kind}`); + } + } +} + +function emitCppStructHeader(w: CodeWriter, type: TypeImpl) { + if (type.kind === "zigEnum" || type.kind === "stringEnum") { + emitCppEnumHeader(w, type); + return; + } + + const externLayout = type.canDirectlyMapToCAbi(); + if (externLayout) { + if (typeof externLayout === "string") { + w.line(`typedef ${externLayout} ${type.name()};`); + console.warn("should this really be done lol", type); + } else { + externLayout.emitCpp(w, type.name()); + w.line(); + } + return; + } + + switch (type.kind) { + default: { + throw new Error(`TODO: emitZigStruct for Type ${type.kind}`); + } + } +} + +function emitCppEnumHeader(w: CodeWriter, type: TypeImpl) { + assert(type.kind === "zigEnum" || type.kind === "stringEnum"); + + assert(type.kind === "stringEnum"); // TODO + assert(type.data.length > 0); + const signPrefix = "u"; + const intBits = alignForward(type.data.length, 8); + const tagType = `${signPrefix}int${intBits}_t`; + w.line(`enum class ${type.name()} : ${tagType} {`); + for (const value of type.data) { + w.line(` ${pascal(value)},`); + } + w.line(`};`); + w.line(); +} + +// This function assumes in the WebCore namespace +function emitConvertEnumFunction(w: CodeWriter, type: TypeImpl) { + assert(type.kind === "zigEnum" || type.kind === "stringEnum"); + assert(type.kind === "stringEnum"); // TODO + assert(type.data.length > 0); + + const name = "Generated::" + type.cppName(); + headers.add("JavaScriptCore/JSCInlines.h"); + headers.add("JavaScriptCore/JSString.h"); + headers.add("wtf/NeverDestroyed.h"); + headers.add("wtf/SortedArrayMap.h"); + + w.line(`String convertEnumerationToString(${name} enumerationValue) {`); + w.indent(); + w.line(` static const NeverDestroyed values[] = {`); + w.indent(); + for (const value of type.data) { + w.line(` MAKE_STATIC_STRING_IMPL(${str(value)}),`); + } + w.dedent(); + w.line(` };`); + w.line(` return values[static_cast(enumerationValue)];`); + w.dedent(); + w.line(`}`); + w.line(); + w.line(`template<> JSString* convertEnumerationToJS(JSC::JSGlobalObject& global, ${name} enumerationValue) {`); + w.line(` return jsStringWithCache(global.vm(), convertEnumerationToString(enumerationValue));`); + w.line(`}`); + w.line(); + w.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String& stringValue)`); + w.line(`{`); + w.line(` static constexpr std::pair mappings[] = {`); + for (const value of type.data) { + w.line(` { ${str(value)}_s, ${name}::${pascal(value)} },`); + } + w.line(` };`); + w.line(` static constexpr SortedArrayMap enumerationMapping { mappings };`); + w.line(` if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); LIKELY(enumerationValue))`); + w.line(` return *enumerationValue;`); + w.line(` return std::nullopt;`); + w.line(`}`); + w.line(); + w.line( + `template<> std::optional<${name}> parseEnumeration<${name}>(JSGlobalObject& lexicalGlobalObject, JSValue value)`, + ); + w.line(`{`); + w.line(` return parseEnumerationFromString<${name}>(value.toWTFString(&lexicalGlobalObject));`); + w.line(`}`); + w.line(); + w.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>()`); + w.line(`{`); + w.line(` return ${str(type.data.map(value => `${str(value)}`).join(", "))}_s;`); + w.line(`}`); + w.line(); +} + +function zigTypeName(type: TypeImpl): string { + let name = zigTypeNameInner(type); + if (type.flags.optional) { + name = "?" + name; + } + return name; +} + +function zigTypeNameInner(type: TypeImpl): string { + if (type.lowersToNamedType()) { + const namespace = typeHashToNamespace.get(type.hash()); + return namespace ? `${namespace}.${type.name()}` : type.name(); + } + switch (type.kind) { + case "USVString": + case "DOMString": + case "ByteString": + case "UTF8String": + return "bun.String"; + case "boolean": + return "bool"; + case "usize": + return "usize"; + case "globalObject": + case "zigVirtualMachine": + return "*JSC.JSGlobalObject"; + default: + const cAbiType = type.canDirectlyMapToCAbi(); + if (cAbiType) { + if (typeof cAbiType === "string") { + return cAbiType; + } + return cAbiType.name(); + } + throw new Error(`TODO: emitZigTypeName for Type ${type.kind}`); + } +} + +function returnStrategyCppType(strategy: ReturnStrategy): string { + switch (strategy.type) { + case "basic-out-param": + case "void": + return "bool"; // true=success, false=exception + case "jsvalue": + return "JSC::EncodedJSValue"; + default: + throw new Error( + `TODO: returnStrategyCppType for ${Bun.inspect(strategy satisfies never, { colors: Bun.enableANSIColors })}`, + ); + } +} + +function returnStrategyZigType(strategy: ReturnStrategy): string { + switch (strategy.type) { + case "basic-out-param": + case "void": + return "bool"; // true=success, false=exception + case "jsvalue": + return "JSC.JSValue"; + default: + throw new Error( + `TODO: returnStrategyZigType for ${Bun.inspect(strategy satisfies never, { colors: Bun.enableANSIColors })}`, + ); + } +} + +function emitNullableZigDecoder(w: CodeWriter, prefix: string, type: TypeImpl, children: ArgStrategyChildItem[]) { + assert(children.length > 0); + const indent = children[0].type !== "c-abi-compatible"; + w.add(`if (${prefix}_set)`); + if (indent) { + w.indent(); + } else { + w.add(` `); + } + emitComplexZigDecoder(w, prefix + "_value", type, children); + if (indent) { + w.line(); + w.dedent(); + } else { + w.add(` `); + } + w.add(`else`); + if (indent) { + w.indent(); + } else { + w.add(` `); + } + w.add(`null`); + if (indent) w.dedent(); +} + +function emitComplexZigDecoder(w: CodeWriter, prefix: string, type: TypeImpl, children: ArgStrategyChildItem[]) { + assert(children.length > 0); + if (children[0].type === "c-abi-compatible") { + w.add(`${prefix}`); + return; + } + + switch (type.kind) { + default: + throw new Error(`TODO: emitComplexZigDecoder for Type ${type.kind}`); + } +} + +type DistinguishablePrimitive = "undefined" | "string" | "number" | "boolean" | "object"; +type DistinguishStrategy = DistinguishablePrimitive; + +function typeCanDistinguish(t: TypeImpl[]) { + const seen: Record = { + undefined: false, + string: false, + number: false, + boolean: false, + object: false, + }; + let strategies: DistinguishStrategy[] = []; + + for (const type of t) { + let primitive: DistinguishablePrimitive | null = null; + if (type.kind === "undefined") { + primitive = "undefined"; + } else if (type.isStringType()) { + primitive = "string"; + } else if (type.isNumberType()) { + primitive = "number"; + } else if (type.kind === "boolean") { + primitive = "boolean"; + } else if (type.isObjectType()) { + primitive = "object"; + } + if (primitive) { + if (seen[primitive]) { + return null; + } + seen[primitive] = true; + strategies.push(primitive); + continue; + } + return null; // TODO: + } + + return strategies; +} + +/** This is an arbitrary classifier to allow consistent sorting for distinguishing arguments */ +function typeDistinguishmentWeight(type: TypeImpl): number { + if (type.kind === "undefined") { + return 100; + } + + if (type.isObjectType()) { + return 10; + } + + if (type.isStringType()) { + return 5; + } + + if (type.isNumberType()) { + return 3; + } + + if (type.kind === "boolean") { + return -1; + } + + return 0; +} + +function getDistinguishCode(strategy: DistinguishStrategy, type: TypeImpl, value: string) { + switch (strategy) { + case "string": + return { condition: `${value}.isString()`, canThrow: false }; + case "number": + return { condition: `${value}.isNumber()`, canThrow: false }; + case "boolean": + return { condition: `${value}.isBoolean()`, canThrow: false }; + case "object": + return { condition: `${value}.isObject()`, canThrow: false }; + case "undefined": + return { condition: `${value}.isUndefined()`, canThrow: false }; + default: + throw new Error(`TODO: getDistinguishCode for ${strategy}`); + } +} + +/** The variation selector implementation decides which variation dispatch to call. */ +function emitCppVariationSelector(fn: Func, namespaceVar: string) { + let minRequiredArgs = Infinity; + let maxArgs = 0; + + const variationsByArgumentCount = new Map(); + + const pushToList = (argCount: number, vari: Variant) => { + assert(typeof argCount === "number"); + let list = variationsByArgumentCount.get(argCount); + if (!list) { + list = []; + variationsByArgumentCount.set(argCount, list); + } + list.push(vari); + }; + + for (const vari of fn.variants) { + const vmra = vari.minRequiredArgs; + minRequiredArgs = Math.min(minRequiredArgs, vmra); + maxArgs = Math.max(maxArgs, vari.args.length); + const allArgCount = vari.args.filter(arg => !arg.type.isVirtualArgument()).length; + pushToList(vmra, vari); + if (allArgCount != vmra) { + pushToList(allArgCount, vari); + } + } + + cpp.line(`auto& vm = JSC::getVM(global);`); + cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); + if (minRequiredArgs > 0) { + cpp.line(`size_t argumentCount = std::min(callFrame->argumentCount(), ${maxArgs});`); + cpp.line(`if (argumentCount < ${minRequiredArgs}) {`); + cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); + cpp.line(`}`); + } + + const sorted = [...variationsByArgumentCount.entries()] + .map(([key, value]) => ({ argCount: key, variants: value })) + .sort((a, b) => b.argCount - a.argCount); + let argCountI = 0; + for (const { argCount, variants } of sorted) { + argCountI++; + const checkArgCount = argCountI < sorted.length && argCount !== minRequiredArgs; + if (checkArgCount) { + cpp.line(`if (argumentCount >= ${argCount}) {`); + cpp.indent(); + } + + if (variants.length === 1) { + cpp.line(`return ${extInternalDispatchVariant(namespaceVar, fn.name, variants[0].suffix)}(global, callFrame);`); + } else { + let argIndex = 0; + let strategies: DistinguishStrategy[] | null = null; + while (argIndex < argCount) { + strategies = typeCanDistinguish( + variants.map(v => v.args.filter(v => !v.type.isVirtualArgument())[argIndex].type), + ); + if (strategies) { + break; + } + argIndex++; + } + if (!strategies) { + const err = new Error( + `\x1b[0mVariations with ${argCount} required arguments must have at least one argument that can distinguish between them.\n` + + `Variations:\n${variants.map(v => ` ${inspect(v.args.filter(a => !a.type.isVirtualArgument()).map(x => x.type))}`).join("\n")}`, + ); + err.stack = `Error: ${err.message}\n${fn.snapshot}`; + throw err; + } + + const getArgument = minRequiredArgs > 0 ? "uncheckedArgument" : "argument"; + cpp.line(`JSC::JSValue distinguishingValue = callFrame->${getArgument}(${argIndex});`); + const sortedVariants = variants + .map((v, i) => ({ + variant: v, + type: v.args.filter(a => !a.type.isVirtualArgument())[argIndex].type, + strategy: strategies[i], + })) + .sort((a, b) => typeDistinguishmentWeight(a.type) - typeDistinguishmentWeight(b.type)); + for (const { variant: v, strategy: s } of sortedVariants) { + const arg = v.args[argIndex]; + const { condition, canThrow } = getDistinguishCode(s, arg.type, "distinguishingValue"); + cpp.line(`if (${condition}) {`); + cpp.indent(); + cpp.line(`return ${extInternalDispatchVariant(namespaceVar, fn.name, v.suffix)}(global, callFrame);`); + cpp.dedent(); + cpp.line(`}`); + if (canThrow) { + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + } + } + } + + if (checkArgCount) { + cpp.dedent(); + cpp.line(`}`); + } + } +} + +// BEGIN MAIN CODE GENERATION + +// Search for all .bind.ts files +const unsortedFiles = readdirRecursiveWithExclusionsAndExtensionsSync(src, ["node_modules", ".git"], [".bind.ts"]); + +// Sort for deterministic output +for (const fileName of [...unsortedFiles].sort()) { + const zigFile = path.relative(src, fileName.replace(/\.bind\.ts$/, ".zig")); + const zigFilePath = path.join(src, zigFile); + let file = files.get(zigFile); + if (!fs.existsSync(zigFilePath)) { + // It would be nice if this would generate the file with the correct boilerplate + const bindName = path.basename(fileName); + throw new Error( + `${bindName} is missing a corresponding Zig file at ${zigFile}. Please create it and make sure it matches signatures in ${bindName}.`, + ); + } + if (!file) { + file = { functions: [], typedefs: [] }; + files.set(zigFile, file); + } + + const exports = import.meta.require(fileName); + + // Mark all exported TypeImpl as reachable + for (let [key, value] of Object.entries(exports)) { + if (value == null || typeof value !== "object") continue; + + if (value instanceof TypeImpl) { + value.assignName(key); + value.markReachable(); + file.typedefs.push({ name: key, type: value }); + } + + if (value[isFunc]) { + const func = value as Func; + func.name = key; + } + } + + for (const fn of file.functions) { + if (fn.name === "") { + const err = new Error(`This function definition needs to be exported`); + err.stack = `Error: ${err.message}\n${fn.snapshot}`; + throw err; + } + } +} + +const zig = new CodeWriter(); +const zigInternal = new CodeWriter(); +// TODO: split each *.bind file into a separate .cpp file +const cpp = new CodeWriter(); +const cppInternal = new CodeWriter(); +const headers = new Set(); + +zig.line('const bun = @import("root").bun;'); +zig.line("const JSC = bun.JSC;"); +zig.line("const JSHostFunctionType = JSC.JSHostFunctionType;\n"); + +zigInternal.line("const binding_internals = struct {"); +zigInternal.indent(); + +cpp.line("namespace Generated {"); +cpp.line(); + +cppInternal.line('// These "Arguments" definitions are for communication between C++ and Zig.'); +cppInternal.line('// Field layout depends on implementation details in "bindgen.ts", and'); +cppInternal.line("// is not intended for usage outside generated binding code."); + +headers.add("root.h"); +headers.add("IDLTypes.h"); +headers.add("JSDOMBinding.h"); +headers.add("JSDOMConvertBase.h"); +headers.add("JSDOMConvertBoolean.h"); +headers.add("JSDOMConvertNumbers.h"); +headers.add("JSDOMConvertStrings.h"); +headers.add("JSDOMExceptionHandling.h"); +headers.add("JSDOMOperation.h"); + +/** + * Indexed by `zigFile`, values are the generated zig identifier name, without + * collisions. + */ +const fileMap = new Map(); +const fileNames = new Set(); + +for (const [filename, { functions, typedefs }] of files) { + const basename = path.basename(filename, ".zig"); + let varName = basename; + if (fileNames.has(varName)) { + throw new Error(`File name collision: ${basename}.zig`); + } + fileNames.add(varName); + fileMap.set(filename, varName); + + if (functions.length === 0) continue; + + for (const td of typedefs) { + typeHashToNamespace.set(td.type.hash(), varName); + } + + for (const fn of functions) { + for (const vari of fn.variants) { + for (const arg of vari.args) { + arg.type.markReachable(); + } + } + } +} + +let needsWebCore = false; +for (const type of typeHashToReachableType.values()) { + // Emit convert functions for compound types in the Generated namespace + switch (type.kind) { + case "dictionary": + emitConvertDictionaryFunction(type); + break; + case "stringEnum": + case "zigEnum": + needsWebCore = true; + break; + } +} + +for (const [filename, { functions, typedefs }] of files) { + const namespaceVar = fileMap.get(filename)!; + assert(namespaceVar, `namespaceVar not found for ${filename}, ${inspect(fileMap)}`); + zigInternal.line(`const import_${namespaceVar} = @import(${str(path.relative(src + "/bun.js", filename))});`); + + zig.line(`/// Generated for "src/${filename}"`); + zig.line(`pub const ${namespaceVar} = struct {`); + zig.indent(); + + for (const fn of functions) { + cpp.line(`// Dispatch for \"fn ${zid(fn.name)}(...)\" in \"src/${fn.zigFile}\"`); + const externName = extJsFunction(namespaceVar, fn.name); + + // C++ forward declarations + let variNum = 1; + for (const vari of fn.variants) { + resolveVariantStrategies( + vari, + `${pascal(namespaceVar)}${pascal(fn.name)}Arguments${fn.variants.length > 1 ? variNum : ""}`, + ); + const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); + const internalDispatchName = extInternalDispatchVariant(namespaceVar, fn.name, variNum); + + const args: string[] = []; + + if (vari.globalObjectArg === "hidden") { + args.push("JSC::JSGlobalObject*"); + } + for (const arg of vari.args) { + if (arg.type.isIgnoredUndefinedType()) continue; + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + addHeaderForType(arg.type); + args.push(`const ${arg.type.cppName()}*`); + break; + case "c-abi-value": + addHeaderForType(arg.type); + args.push(arg.type.cppName()); + break; + case "uses-communication-buffer": + break; + default: + throw new Error(`TODO: C++ dispatch function for ${inspect(strategy)}`); + } + } + const { communicationStruct } = vari; + if (communicationStruct) { + args.push(`${communicationStruct.name()}*`); + } + const returnStrategy = vari.returnStrategy!; + if (returnStrategy.type === "basic-out-param") { + args.push(cAbiTypeName(returnStrategy.abiType) + "*"); + } + + cpp.line(`extern "C" ${returnStrategyCppType(vari.returnStrategy!)} ${dispatchName}(${args.join(", ")});`); + + if (fn.variants.length > 1) { + // Emit separate variant dispatch functions + cpp.line( + `extern "C" SYSV_ABI JSC::EncodedJSValue ${internalDispatchName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, + ); + cpp.line(`{`); + cpp.indent(); + cpp.resetTemporaries(); + emitCppCallToVariant(fn.name, vari, dispatchName); + cpp.dedent(); + cpp.line(`}`); + } + variNum += 1; + } + + // Public function + zig.line( + `pub const ${zid("js" + cap(fn.name))} = @extern(*const JSHostFunctionType, .{ .name = ${str(externName)} });`, + ); + + // Generated JSC host function + cpp.line( + `extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, + ); + cpp.line(`{`); + cpp.indent(); + cpp.resetTemporaries(); + + if (fn.variants.length === 1) { + emitCppCallToVariant(fn.name, fn.variants[0], extDispatchVariant(namespaceVar, fn.name, 1)); + } else { + emitCppVariationSelector(fn, namespaceVar); + } + + cpp.dedent(); + cpp.line(`}`); + cpp.line(); + + // Generated Zig dispatch functions + variNum = 1; + for (const vari of fn.variants) { + const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); + const args: string[] = []; + const returnStrategy = vari.returnStrategy!; + const { communicationStruct } = vari; + if (communicationStruct) { + zigInternal.add(`const ${communicationStruct.name()} = `); + communicationStruct.emitZig(zigInternal, "with-semi"); + } + + assert(vari.globalObjectArg !== undefined); + + let globalObjectArg = ""; + if (vari.globalObjectArg === "hidden") { + args.push(`global: *JSC.JSGlobalObject`); + globalObjectArg = "global"; + } + let argNum = 0; + for (const arg of vari.args) { + if (arg.type.isIgnoredUndefinedType()) continue; + let argName = `arg_${snake(arg.name)}`; + if (vari.globalObjectArg === argNum) { + if (arg.type.kind !== "globalObject") { + argName = "global"; + } + globalObjectArg = argName; + } + argNum += 1; + arg.zigMappedName = argName; + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + args.push(`${argName}: *const ${zigTypeName(arg.type)}`); + break; + case "c-abi-value": + args.push(`${argName}: ${zigTypeName(arg.type)}`); + break; + case "uses-communication-buffer": + break; + default: + throw new Error(`TODO: zig dispatch function for ${inspect(strategy)}`); + } + } + assert(globalObjectArg, `globalObjectArg not found from ${vari.globalObjectArg}`); + + if (communicationStruct) { + args.push(`buf: *${communicationStruct.name()}`); + } + + if (returnStrategy.type === "basic-out-param") { + args.push(`out: *${zigTypeName(vari.ret)}`); + } + + zigInternal.line(`export fn ${zid(dispatchName)}(${args.join(", ")}) ${returnStrategyZigType(returnStrategy)} {`); + zigInternal.indent(); + + zigInternal.line( + `if (!@hasDecl(import_${namespaceVar}${fn.zigPrefix.length > 0 ? "." + fn.zigPrefix.slice(0, -1) : ""}, ${str(fn.name + vari.suffix)}))`, + ); + zigInternal.line( + ` @compileError(${str(`Missing binding declaration "${fn.zigPrefix}${fn.name + vari.suffix}" in "${path.basename(filename)}"`)});`, + ); + + for (const arg of vari.args) { + if (arg.type.kind === "UTF8String") { + zigInternal.line(`const ${arg.zigMappedName}_utf8 = ${arg.zigMappedName}.toUTF8(bun.default_allocator);`); + zigInternal.line(`defer ${arg.zigMappedName}_utf8.deinit();`); + } + } + + switch (returnStrategy.type) { + case "jsvalue": + zigInternal.add(`return JSC.toJSHostValue(${globalObjectArg}, `); + break; + case "basic-out-param": + zigInternal.add(`out.* = @as(bun.JSError!${returnStrategy.abiType}, `); + break; + case "void": + zigInternal.add(`@as(bun.JSError!void, `); + break; + } + + zigInternal.line(`${zid("import_" + namespaceVar)}.${fn.zigPrefix}${fn.name + vari.suffix}(`); + zigInternal.indent(); + for (const arg of vari.args) { + const argName = arg.zigMappedName!; + + if (arg.type.isIgnoredUndefinedType()) continue; + + if (arg.type.isVirtualArgument()) { + switch (arg.type.kind) { + case "zigVirtualMachine": + zigInternal.line(`${argName}.bunVM(),`); + break; + case "globalObject": + zigInternal.line(`${argName},`); + break; + default: + throw new Error("unexpected"); + } + continue; + } + + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + if (arg.type.kind === "UTF8String") { + zigInternal.line(`${argName}_utf8.slice(),`); + break; + } + zigInternal.line(`${argName}.*,`); + break; + case "c-abi-value": + zigInternal.line(`${argName},`); + break; + case "uses-communication-buffer": + const prefix = `buf.${snake(arg.name)}`; + const type = arg.type; + const isNullable = type.flags.optional && !("default" in type.flags); + if (isNullable) emitNullableZigDecoder(zigInternal, prefix, type, strategy.children); + else emitComplexZigDecoder(zigInternal, prefix, type, strategy.children); + zigInternal.line(`,`); + break; + default: + throw new Error(`TODO: zig dispatch function for ${inspect(strategy satisfies never)}`); + } + } + zigInternal.dedent(); + switch (returnStrategy.type) { + case "jsvalue": + zigInternal.line(`));`); + break; + case "basic-out-param": + case "void": + zigInternal.line(`)) catch |err| switch (err) {`); + zigInternal.line(` error.JSError => return false,`); + zigInternal.line(` error.OutOfMemory => ${globalObjectArg}.throwOutOfMemory() catch return false,`); + zigInternal.line(`};`); + zigInternal.line(`return true;`); + break; + } + zigInternal.dedent(); + zigInternal.line(`}`); + variNum += 1; + } + } + if (functions.length > 0) { + zig.line(); + } + for (const fn of functions) { + // Wrapper to init JSValue + const wrapperName = zid("create" + cap(fn.name) + "Callback"); + const minArgCount = fn.variants.reduce((acc, vari) => Math.min(acc, vari.args.length), Number.MAX_SAFE_INTEGER); + zig.line(`pub fn ${wrapperName}(global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue {`); + zig.line( + ` return JSC.NewRuntimeFunction(global, JSC.ZigString.static(${str(fn.name)}), ${minArgCount}, js${cap(fn.name)}, false, false, null);`, + ); + zig.line(`}`); + } + + if (typedefs.length > 0) { + zig.line(); + } + for (const td of typedefs) { + emitZigStruct(td.type); + } + + zig.dedent(); + zig.line(`};`); + zig.line(); +} + +cpp.line("} // namespace Generated"); +cpp.line(); +if (needsWebCore) { + cpp.line(`namespace WebCore {`); + cpp.line(); + for (const [type, reachableType] of typeHashToReachableType) { + switch (reachableType.kind) { + case "zigEnum": + case "stringEnum": + emitConvertEnumFunction(cpp, reachableType); + break; + } + } + cpp.line(`} // namespace WebCore`); + cpp.line(); +} + +zigInternal.dedent(); +zigInternal.line("};"); +zigInternal.line(); +zigInternal.line("comptime {"); +zigInternal.line(` if (bun.Environment.export_cpp_apis) {`); +zigInternal.line(" for (@typeInfo(binding_internals).Struct.decls) |decl| {"); +zigInternal.line(" _ = &@field(binding_internals, decl.name);"); +zigInternal.line(" }"); +zigInternal.line(" }"); +zigInternal.line("}"); + +writeIfNotChanged( + path.join(codegenRoot, "GeneratedBindings.cpp"), + [...headers].map(name => `#include ${str(name)}\n`).join("") + "\n" + cppInternal.buffer + "\n" + cpp.buffer, +); +writeIfNotChanged(path.join(src, "bun.js/bindings/GeneratedBindings.zig"), zig.buffer + zigInternal.buffer); + +// Headers +for (const [filename, { functions, typedefs }] of files) { + const namespaceVar = fileMap.get(filename)!; + const header = new CodeWriter(); + const headerIncludes = new Set(); + let needsWebCoreNamespace = false; + + headerIncludes.add("root.h"); + + header.line(`namespace {`); + header.line(); + for (const fn of functions) { + const externName = extJsFunction(namespaceVar, fn.name); + header.line(`extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject*, JSC::CallFrame*);`); + } + header.line(); + header.line(`} // namespace`); + header.line(); + + header.line(`namespace Generated {`); + header.line(); + header.line(`/// Generated binding code for src/${filename}`); + header.line(`namespace ${namespaceVar} {`); + header.line(); + for (const td of typedefs) { + emitCppStructHeader(header, td.type); + + switch (td.type.kind) { + case "zigEnum": + case "stringEnum": + case "dictionary": + needsWebCoreNamespace = true; + break; + } + } + for (const fn of functions) { + const externName = extJsFunction(namespaceVar, fn.name); + header.line(`constexpr auto* js${cap(fn.name)} = &${externName};`); + } + header.line(); + header.line(`} // namespace ${namespaceVar}`); + header.line(); + header.line(`} // namespace Generated`); + header.line(); + + if (needsWebCoreNamespace) { + header.line(`namespace WebCore {`); + header.line(); + for (const td of typedefs) { + switch (td.type.kind) { + case "zigEnum": + case "stringEnum": + headerIncludes.add("JSDOMConvertEnumeration.h"); + const basename = td.type.name(); + const name = `Generated::${namespaceVar}::${basename}`; + header.line(`// Implement WebCore::IDLEnumeration trait for ${basename}`); + header.line(`String convertEnumerationToString(${name});`); + header.line(`template<> JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, ${name});`); + header.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String&);`); + header.line( + `template<> std::optional<${name}> parseEnumeration<${name}>(JSC::JSGlobalObject&, JSC::JSValue);`, + ); + header.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>();`); + header.line(); + break; + case "dictionary": + // TODO: + // header.line(`// Implement WebCore::IDLDictionary trait for ${td.type.name()}`); + // header.line( + // "template<> FetchRequestInit convertDictionary(JSC::JSGlobalObject&, JSC::JSValue);", + // ); + // header.line(); + break; + default: + } + } + header.line(`} // namespace WebCore`); + } + + header.buffer = + "#pragma once\n" + [...headerIncludes].map(name => `#include ${str(name)}\n`).join("") + "\n" + header.buffer; + + writeIfNotChanged(path.join(codegenRoot, `Generated${pascal(namespaceVar)}.h`), header.buffer); +} diff --git a/src/codegen/bundle-functions.ts b/src/codegen/bundle-functions.ts index 7756e44afd..96a3fbfcab 100644 --- a/src/codegen/bundle-functions.ts +++ b/src/codegen/bundle-functions.ts @@ -7,7 +7,7 @@ // supported macros that aren't json value -> json value. Otherwise, I'd use a real JS parser/ast // library, instead of RegExp hacks. // -// For explanation on this, please nag @paperdave to write documentation on how everything works. +// For explanation on this, please nag @paperclover to write documentation on how everything works. // // The output is intended to be similar to what WebCore does internally with a couple differences: // @@ -21,6 +21,7 @@ import { createAssertClientJS, createLogClientJS } from "./client-js"; import { getJS2NativeDTS } from "./generate-js2native"; import { addCPPCharArray, cap, low, writeIfNotChanged } from "./helpers"; import { applyGlobalReplacements, define } from "./replacements"; +import assert from "assert"; const PARALLEL = false; const KEEP_TMP = true; @@ -142,22 +143,55 @@ async function processFileSplit(filename: string): Promise<{ functions: BundledB } contents = contents.slice(directive[0].length); } else if (match[1] === "export function" || match[1] === "export async function") { - const declaration = contents.match( - /^export\s+(async\s+)?function\s+([a-zA-Z0-9]+)\s*\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/, - ); - if (!declaration) - throw new SyntaxError("Could not parse function declaration:\n" + contents.slice(0, contents.indexOf("\n"))); + // consume async token and function name + const nameMatch = contents.match(/^export\s+(async\s+)?function\s([a-zA-Z0-9]+)\s*/); + if (!nameMatch) + throw new SyntaxError("Could not parse function name:\n" + contents.slice(0, contents.indexOf("\n"))); + const async = Boolean(nameMatch[1]); + const name = nameMatch[2]; + var remaining = contents.slice(nameMatch[0].length); - const async = !!declaration[1]; - const name = declaration[2]; - const paramString = declaration[3]; + // remove type parameters + if (remaining.startsWith("<")) { + var cursor = 1; // skip peeked '<' + var depth = 1; // already entered first bracket pair + for (; depth > 0 && cursor < remaining.length; cursor++) { + switch (remaining[cursor]) { + case "<": + depth++; + break; + case ">": + depth--; + break; + } + } + + if (depth > 0) { + throw new SyntaxError( + `Function ${name} has an unclosed generic type. Missing ${depth} closing angle bracket(s).`, + ); + } + remaining = remaining.slice(cursor).trimStart(); + } + + // parse function parameters + assert( + remaining.startsWith("("), + new SyntaxError(`Function ${name} is missing parameter list start. Found:\n\n\t${remaining.slice(0, 100)}`), + ); + const paramMatch = remaining.match(/^\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/); + if (!paramMatch) + throw new SyntaxError( + `Could not parse parameters for function ${name}:\n` + contents.slice(0, contents.indexOf("\n")), + ); + const paramString = paramMatch[1]; const params = paramString.trim().length === 0 ? [] : paramString.split(",").map(x => x.replace(/:.+$/, "").trim()); if (params[0] === "this") { params.shift(); } - const { result, rest } = sliceSourceCode(contents.slice(declaration[0].length - 1), true, x => + const { result, rest } = sliceSourceCode(remaining.slice(paramMatch[0].length - 1), true, x => globalThis.requireTransformer(x, SRC_DIR + "/" + basename), ); @@ -223,7 +257,9 @@ $$capture_start$$(${fn.async ? "async " : ""}${ define, target: "bun", minify: { syntax: true, whitespace: false }, + throw: true, }); + // TODO: Wait a few versions before removing this if (!build.success) { throw new AggregateError(build.logs, "Failed bundling builtin function " + fn.name + " from " + basename + ".ts"); } diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 9a1d91d25a..9a6bb44dc9 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -7,7 +7,7 @@ // supported macros that aren't json value -> json value. Otherwise, I'd use a real JS parser/ast // library, instead of RegExp hacks. // -// For explanation on this, please nag @paperdave to write documentation on how everything works. +// For explanation on this, please nag @paperclover to write documentation on how everything works. import fs from "fs"; import { mkdir, writeFile } from "fs/promises"; import { builtinModules } from "node:module"; @@ -19,6 +19,7 @@ import { getJS2NativeCPP, getJS2NativeZig } from "./generate-js2native"; import { cap, declareASCIILiteral, writeIfNotChanged } from "./helpers"; import { createInternalModuleRegistry } from "./internal-module-registry-scanner"; import { define } from "./replacements"; +import jsclasses from "./../bun.js/bindings/js_classes"; const BASE = path.join(import.meta.dir, "../js"); const debug = process.argv[2] === "--debug=ON"; @@ -81,6 +82,16 @@ for (let i = 0; i < moduleList.length; i++) { try { let input = fs.readFileSync(path.join(BASE, moduleList[i]), "utf8"); + // NOTE: internal modules are parsed as functions. They must use ESM to export and require to import. + // TODO: Bother @paperdave and have him check for module.exports, and create/return a module object if detected. + if (!/\bexport\s+(?:function|class|const|default|{)/.test(input)) { + if (input.includes("module.exports")) { + throw new Error("Cannot use CommonJS module.exports in ESM modules. Use `export default { ... }` instead."); + } else { + throw new Error("Internal modules must have an `export default` statement."); + } + } + const scannedImports = t.scanImports(input); for (const imp of scannedImports) { if (imp.kind === "import-statement") { @@ -111,8 +122,7 @@ for (let i = 0; i < moduleList.length; i++) { true, x => requireTransformer(x, moduleList[i]), ); - let fileToTranspile = `// @ts-nocheck -// GENERATED TEMP FILE - DO NOT EDIT + let fileToTranspile = `// GENERATED TEMP FILE - DO NOT EDIT // Sourced from src/js/${moduleList[i]} ${importStatements.join("\n")} @@ -139,7 +149,8 @@ ${processed.result.slice(1).trim()} if (!fs.existsSync(path.dirname(outputPath))) { verbose("directory did not exist after mkdir twice:", path.dirname(outputPath)); } - // await Bun.sleep(10); + + fileToTranspile = "// @ts-nocheck\n" + fileToTranspile; try { await writeFile(outputPath, fileToTranspile); @@ -377,8 +388,12 @@ pub const ResolvedSourceTag = enum(u32) { file = 4, esm = 5, json_for_object_loader = 6, + /// Generate an object with "default" set to all the exports, including a "default" propert exports_object = 7, + /// Generate a module that only exports default the input JSValue + export_default_object = 8, + // Built in modules are loaded through InternalModuleRegistry by numerical ID. // In this enum are represented as \`(1 << 9) & id\` ${moduleList.map((id, n) => ` @"${idToPublicSpecifierOrEnumName(id)}" = ${(1 << 9) | n},`).join("\n")} @@ -402,12 +417,12 @@ writeIfNotChanged( ESM = 5, JSONForObjectLoader = 6, ExportsObject = 7, - + ExportDefaultObject = 8, // Built in modules are loaded through InternalModuleRegistry by numerical ID. // In this enum are represented as \`(1 << 9) & id\` InternalModuleRegistryFlag = 1 << 9, ${moduleList.map((id, n) => ` ${idToEnumName(id)} = ${(1 << 9) | n},`).join("\n")} - + // Native modules run through the same system, but with different underlying initializers. // They also have bit 10 set to differentiate them from JS builtins. NativeModuleFlag = (1 << 10) | (1 << 9), @@ -439,19 +454,37 @@ writeIfNotChanged( (() => { let dts = ` // GENERATED TEMP FILE - DO NOT EDIT +// generated by ${import.meta.path} `; for (let i = 0; i < ErrorCode.length; i++) { - const [code, _, name] = ErrorCode[i]; + const [code, constructor, name, ...other_constructors] = ErrorCode[i]; dts += ` /** - * Generate a ${name} error with the \`code\` property set to ${code}. + * Generate a ${name ?? constructor.name} error with the \`code\` property set to ${code}. * * @param msg The error message * @param args Additional arguments */ declare function $${code}(msg: string, ...args: any[]): ${name}; `; + + for (const con of other_constructors) { + if (con == null) continue; + dts += ` +/** + * Generate a ${con.name} error with the \`code\` property set to ${code}. + * + * @param msg The error message + * @param args Additional arguments + */ +declare function $${code}_${con.name}(msg: string, ...args: any[]): ${name}; +`; + } + } + + for (const [name] of jsclasses) { + dts += `\ndeclare function $inherits${name}(value: any): boolean;`; } return dts; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index fe5944fce4..64d5272f8c 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -58,8 +58,29 @@ export interface ClassDefinition { values?: string[]; JSType?: string; noConstructor?: boolean; - wantsThis?: boolean; + + final?: boolean; + + // Do not try to track the `this` value in the constructor automatically. + // That is a memory leak. + wantsThis?: never; + + /** + * Called from any thread. + * + * Used for GC. + */ estimatedSize?: boolean; + /** + * Used in heap snapshots. + * + * If true, the class will have a `memoryCost` method that returns the size of the object in bytes. + * + * Unlike estimatedSize, this is always called on the main thread and not used for GC. + * + * If none is provided, we use the struct size. + */ + memoryCost?: boolean; hasPendingActivity?: boolean; isEventEmitter?: boolean; supportsObjectCreate?: boolean; diff --git a/src/codegen/create-hash-table.ts b/src/codegen/create-hash-table.ts index e47c1d036f..7c5bd88580 100644 --- a/src/codegen/create-hash-table.ts +++ b/src/codegen/create-hash-table.ts @@ -44,6 +44,7 @@ str = str.replaceAll(/^#include.*$/gm, ""); str = str.replaceAll(`namespace JSC {`, ""); str = str.replaceAll(`} // namespace JSC`, ""); str = str.replaceAll(/NativeFunctionType,\s([a-zA-Z0-99_]+)/gm, "NativeFunctionType, &$1"); +str = str.replaceAll('&Generated::', 'Generated::'); str = "#pragma once" + "\n" + "// File generated via `create-hash-table.ts`\n" + str.trim() + "\n"; writeIfNotChanged(output, str); diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index bc83c72f45..d60f1ae548 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -2,6 +2,7 @@ import path from "path"; import type { ClassDefinition, Field } from "./class-definitions"; import { camelCase, pascalCase, writeIfNotChanged } from "./helpers"; +import jsclasses from "./../bun.js/bindings/js_classes"; if (process.env.BUN_SILENT === "1") { console.log = () => {}; @@ -361,12 +362,6 @@ JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); `; } - if (obj.wantsThis) { - externs += ` -extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "_setThis")}(JSC::JSGlobalObject*, void*, JSC::EncodedJSValue); -`; - } - if (obj.structuredClone) { externs += `extern JSC_CALLCONV void JSC_HOST_CALL_ATTRIBUTES ${symbolName( @@ -462,11 +457,11 @@ void ${proto}::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) `; } -function generatePrototypeHeader(typename) { +function generatePrototypeHeader(typename, final = true) { const proto = prototypeName(typename); return ` -class ${proto} final : public JSC::JSNonFinalObject { +class ${proto} ${final ? "final" : ""} : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; @@ -489,7 +484,7 @@ class ${proto} final : public JSC::JSNonFinalObject { return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - private: + protected: ${proto}(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) : Base(vm, structure) { @@ -543,7 +538,7 @@ class ${name} final : public JSC::InternalFunction { static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject*, JSC::CallFrame*); DECLARE_EXPORT_INFO; - private: + protected: ${name}(JSC::VM& vm, JSC::Structure* structure); void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, ${prototypeName(typeName)}* prototype); }; @@ -652,13 +647,6 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj } auto value = JSValue::encode(instance); -${ - obj.wantsThis - ? ` - ${classSymbolName(typeName, "_setThis")}(globalObject, ptr, value); -` - : "" -} RELEASE_AND_RETURN(scope, value); } @@ -923,6 +911,7 @@ function renderStaticDecls(symbolName, typeName, fields, supportsObjectCreate = return rows.join("\n"); } + function writeBarrier(symbolName, typeName, name, cacheName) { return ` @@ -941,6 +930,7 @@ extern JSC_CALLCONV JSC::EncodedJSValue ${symbolName(typeName, name)}GetCachedVa `.trim(); } + function renderFieldsImpl( symbolName: (typeName: string, name: string) => string, typeName: string, @@ -1190,6 +1180,24 @@ JSC_DEFINE_HOST_FUNCTION(${symbolName(typeName, name)}Callback, (JSGlobalObject return rows.map(a => a.trim()).join("\n"); } +function allCachedValues(obj: ClassDefinition) { + let values = (obj.values ?? []).slice().map(name => [name, `m_${name}`]); + for (const name in obj.proto) { + let cacheName = obj.proto[name].cache; + if (cacheName === true) { + cacheName = "m_" + name; + } else if (cacheName) { + cacheName = `m_${cacheName}`; + } + + if (cacheName) { + values.push([name, cacheName]); + } + } + + return values; +} + var extraIncludes = []; function generateClassHeader(typeName, obj: ClassDefinition) { var { klass, proto, JSType = "ObjectType", values = [], callbacks = {}, zigOnly = false } = obj; @@ -1206,7 +1214,7 @@ function generateClassHeader(typeName, obj: ClassDefinition) { [...Object.values(klass), ...Object.values(proto)].find(a => !!a.cache) ? "DECLARE_VISIT_CHILDREN;\ntemplate void visitAdditionalChildren(Visitor&);\nDECLARE_VISIT_OUTPUT_CONSTRAINTS;\n" : ""; - const sizeEstimator = obj.estimatedSize ? "static size_t estimatedSize(JSCell* cell, VM& vm);" : ""; + const sizeEstimator = "static size_t estimatedSize(JSCell* cell, VM& vm);"; var weakOwner = ""; var weakInit = ``; @@ -1247,8 +1255,10 @@ function generateClassHeader(typeName, obj: ClassDefinition) { suffix += `JSC::JSValue getInternalProperties(JSC::VM &vm, JSC::JSGlobalObject *globalObject, ${name}*);`; } + const final = obj.final ?? true; + return ` - class ${name} final : public JSC::JSDestructibleObject { + class ${name}${final ? " final" : ""} : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; static ${name}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); @@ -1291,6 +1301,16 @@ function generateClassHeader(typeName, obj: ClassDefinition) { static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(${name}, m_ctx); } + /** + * Estimated size of the object from Zig including the JS wrapper. + */ + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + + /** + * Memory cost of the object from Zig, without necessarily having a JS wrapper alive. + */ + static size_t memoryCost(void* ptr); + void* m_ctx { nullptr }; @@ -1367,7 +1387,8 @@ function generateClassImpl(typeName, obj: ClassDefinition) { .join("\n"); for (const name in callbacks) { - DEFINE_VISIT_CHILDREN_LIST += "\n" + ` visitor.append(thisObject->m_callback_${name});`; + // Use appendHidden so it doesn't show up in the heap snapshot twice. + DEFINE_VISIT_CHILDREN_LIST += "\n" + ` visitor.appendHidden(thisObject->m_callback_${name});`; } const values = (obj.values || []) @@ -1397,6 +1418,8 @@ visitor.reportExtraMemoryVisited(size); DEFINE_VISIT_CHILDREN(${name}); + + template void ${name}::visitAdditionalChildren(Visitor& visitor) { @@ -1473,9 +1496,39 @@ ${name}::~${name}() `; } + if (!obj.estimatedSize && !obj.memoryCost) { + externs += `extern "C" const size_t ${symbolName(typeName, "ZigStructSize")};`; + } else if (obj.memoryCost) { + externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "memoryCost")}(void* ptr);`; + } + + if (obj.memoryCost) { + output += ` +size_t ${name}::memoryCost(void* ptr) { + return ptr ? ${symbolName(typeName, "memoryCost")}(ptr) : 0; +} +`; + } else if (obj.estimatedSize) { + output += ` +size_t ${name}::memoryCost(void* ptr) { + return ptr ? ${symbolName(typeName, "estimatedSize")}(ptr) : 0; +} + `; + } else { + output += ` +size_t ${name}::memoryCost(void* ptr) { + return ptr ? ${symbolName(typeName, "ZigStructSize")} : 0; +} + `; + } + output += ` - +size_t ${name}::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) { + auto* thisObject = jsCast<${name}*>(cell); + auto* wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + ${name}::memoryCost(wrapped); +} void ${name}::destroy(JSCell* cell) { @@ -1539,17 +1592,29 @@ extern JSC_CALLCONV bool JSC_HOST_CALL_ATTRIBUTES ${typeName}__dangerouslySetPtr return true; } - extern "C" const size_t ${typeName}__ptrOffset = ${className(typeName)}::offsetOfWrapped(); void ${name}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { auto* thisObject = jsCast<${name}*>(cell); if (void* wrapped = thisObject->wrapped()) { - // if (thisObject->scriptExecutionContext()) - // analyzer.setLabelForCell(cell, makeString("url ", thisObject->scriptExecutionContext()->url().string())); + analyzer.setWrappedObjectForCell(cell, wrapped); } + Base::analyzeHeap(cell, analyzer); + ${allCachedValues(obj).length > 0 ? `auto& vm = thisObject->vm();` : ""} + + ${allCachedValues(obj) + .map( + ([name, cacheName]) => ` +if (JSValue ${cacheName}Value = thisObject->${cacheName}.get()) { + if (${cacheName}Value.isCell()) { + const Identifier& id = Identifier::fromString(vm, "${name}"_s); + analyzer.analyzePropertyNameEdge(cell, ${cacheName}Value.asCell(), id.impl()); + } +}`, + ) + .join("\n ")} } ${ @@ -1594,7 +1659,12 @@ ${DEFINE_VISIT_CHILDREN} } function generateHeader(typeName, obj) { - return generateClassHeader(typeName, obj).trim() + "\n\n"; + const fields = [ + generateClassHeader(typeName, obj).trim() + "\n\n", + !(obj.final ?? true) ? generatePrototypeHeader(typeName, false) : null, + ].filter(Boolean); + + return "\n" + fields.join("\n").trim(); } function generateImpl(typeName, obj) { @@ -1602,7 +1672,7 @@ function generateImpl(typeName, obj) { const proto = obj.proto; return [ - generatePrototypeHeader(typeName), + (obj.final ?? true) ? generatePrototypeHeader(typeName, true) : null, !obj.noConstructor ? generateConstructorHeader(typeName).trim() + "\n" : null, generatePrototype(typeName, obj).trim(), !obj.noConstructor ? generateConstructorImpl(typeName, obj).trim() : null, @@ -1620,10 +1690,10 @@ function generateZig( construct, finalize, noConstructor = false, - wantsThis = false, overridesToJS = false, estimatedSize, call = false, + memoryCost, values = [], hasPendingActivity = false, structuredClone = false, @@ -1695,6 +1765,15 @@ const JavaScriptCoreBindings = struct { `; + if (memoryCost) { + exports.set("memoryCost", symbolName(typeName, "memoryCost")); + output += ` + pub fn ${symbolName(typeName, "memoryCost")}(thisValue: *${typeName}) callconv(JSC.conv) usize { + return @call(.always_inline, ${typeName}.memoryCost, .{thisValue}); + } + `; + } + if (estimatedSize) { exports.set("estimatedSize", symbolName(typeName, "estimatedSize")); output += ` @@ -1702,6 +1781,10 @@ const JavaScriptCoreBindings = struct { return @call(.always_inline, ${typeName}.estimatedSize, .{thisValue}); } `; + } else if (!memoryCost && !estimatedSize) { + output += ` + export const ${symbolName(typeName, "ZigStructSize")}: usize = @sizeOf(${typeName}); + `; } if (hasPendingActivity) { @@ -1739,16 +1822,6 @@ const JavaScriptCoreBindings = struct { `; } - if (construct && !noConstructor && wantsThis) { - exports.set("_setThis", classSymbolName(typeName, "_setThis")); - output += ` - pub fn ${classSymbolName(typeName, "_setThis")}(globalObject: *JSC.JSGlobalObject, ptr: *anyopaque, this: JSC.JSValue) callconv(JSC.conv) void { - const real: *${typeName} = @ptrCast(@alignCast(ptr)); - real.this_value.set(globalObject, this); - } - `; - } - if (call) { exports.set("call", classSymbolName(typeName, "call")); output += ` @@ -1998,7 +2071,7 @@ function generateLazyClassStructureHeader(typeName, { klass = {}, proto = {}, zi return ` JSC::Structure* ${className(typeName)}Structure() const { return m_${className(typeName)}.getInitializedOnMainThread(this); } JSC::JSObject* ${className(typeName)}Constructor() const { return m_${className(typeName)}.constructorInitializedOnMainThread(this); } - JSC::JSValue ${className(typeName)}Prototype() const { return m_${className(typeName)}.prototypeInitializedOnMainThread(this); } + JSC::JSObject* ${className(typeName)}Prototype() const { return m_${className(typeName)}.prototypeInitializedOnMainThread(this); } JSC::LazyClassStructure m_${className(typeName)}; `.trim(); } @@ -2033,6 +2106,9 @@ const GENERATED_CLASSES_HEADER = [ #include "root.h" namespace Zig { + +JSC_DECLARE_HOST_FUNCTION(jsFunctionInherits); + } #include "JSDOMWrapper.h" @@ -2075,6 +2151,7 @@ const GENERATED_CLASSES_IMPL_HEADER_PRE = ` #include "ZigGeneratedClasses.h" #include "ErrorCode+List.h" #include "ErrorCode.h" +#include #if !OS(WINDOWS) #define JSC_CALLCONV "C" @@ -2099,6 +2176,30 @@ const GENERATED_CLASSES_IMPL_FOOTER = ` `; +function jsInheritsCppImpl() { + return ` +${jsclasses + .map(v => v[1]) + .filter(v => v?.length > 0) + .map((v, i) => `#include "${v}"`) + .join("\n")} + +JSC_DEFINE_HOST_FUNCTION(Zig::jsFunctionInherits, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto id = callFrame->argument(0).toInt32(globalObject); + auto value = callFrame->argument(1); + if (!value.isCell()) return JSValue::encode(jsBoolean(false)); + auto cell = value.asCell(); + switch (id) { +${jsclasses + .map(v => v[0]) + .map((v, i) => ` case ${i}: return JSValue::encode(jsBoolean(cell->inherits()));`) + .join("\n")} + } + return JSValue::encode(jsBoolean(false)); +}`; +} + function initLazyClasses(initLaterFunctions) { return ` @@ -2273,6 +2374,7 @@ comptime { `, ]); + if (!process.env.ONLY_ZIG) { const allHeaders = classes.map(a => generateHeader(a.name, a)); await writeIfNotChanged(`${outBase}/ZigGeneratedClasses.h`, [ @@ -2291,7 +2393,9 @@ if (!process.env.ONLY_ZIG) { allImpls.join("\n"), writeCppSerializers(classes), GENERATED_CLASSES_IMPL_FOOTER, + jsInheritsCppImpl(), ]); + await writeIfNotChanged( `${outBase}/ZigGeneratedClasses+lazyStructureHeader.h`, classes.map(a => generateLazyClassStructureHeader(a.name, a)).join("\n"), diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index eb98745618..7034c0b985 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -4,7 +4,7 @@ // For the actual parsing, see replacements.ts import path, { basename, sep } from "path"; -import { readdirRecursiveWithExclusionsAndExtensionsSync } from "./helpers"; +import { cap, readdirRecursiveWithExclusionsAndExtensionsSync } from "./helpers"; // interface NativeCall { @@ -25,7 +25,7 @@ interface WrapperCall { filename: string; } -type NativeCallType = "zig" | "cpp"; +type NativeCallType = "zig" | "cpp" | "bind"; const nativeCalls: NativeCall[] = []; const wrapperCalls: WrapperCall[] = []; @@ -33,7 +33,7 @@ const wrapperCalls: WrapperCall[] = []; const sourceFiles = readdirRecursiveWithExclusionsAndExtensionsSync( path.join(import.meta.dir, "../"), ["deps", "node_modules", "WebKit"], - [".cpp", ".zig"], + [".cpp", ".zig", ".bind.ts"], ); function callBaseName(x: string) { @@ -41,15 +41,15 @@ function callBaseName(x: string) { } function resolveNativeFileId(call_type: NativeCallType, filename: string) { - if (!filename.endsWith("." + call_type)) { - throw new Error( - `Expected filename for $${call_type} to have .${call_type} extension, got ${JSON.stringify(filename)}`, - ); + const ext = call_type === "bind" ? ".bind.ts" : `.${call_type}`; + if (!filename.endsWith(ext)) { + throw new Error(`Expected filename for $${call_type} to have ${ext} extension, got ${JSON.stringify(filename)}`); } const resolved = sourceFiles.find(file => file.endsWith(sep + filename)); if (!resolved) { - throw new Error(`Could not find file ${filename} in $${call_type} call`); + const fnName = call_type === "bind" ? "bindgenFn" : call_type; + throw new Error(`Could not find file ${filename} in $${fnName} call`); } if (call_type === "zig") { @@ -136,7 +136,7 @@ export function getJS2NativeCPP() { externs.push(`extern "C" SYSV_ABI JSC::EncodedJSValue ${symbol(call)}_workaround(Zig::GlobalObject*);` + "\n"), [ `static ALWAYS_INLINE JSC::JSValue ${symbol(call)}(Zig::GlobalObject* global) {`, - ` return JSValue::decode(${symbol(call)}_workaround(global));`, + ` return JSValue::decode(${symbol(call)}_workaround(global));`, `}` + "\n\n", ] ), @@ -180,10 +180,23 @@ export function getJS2NativeCPP() { "using namespace WebCore;" + "\n", ...nativeCallStrings, ...wrapperCallStrings, + ...nativeCalls + .filter(x => x.type === "bind") + .map( + x => + `extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${basename(x.filename.replace(/\.bind\.ts$/, ""))}_${x.symbol}(Zig::GlobalObject*);`, + ), `typedef JSC::JSValue (*JS2NativeFunction)(Zig::GlobalObject*);`, `static ALWAYS_INLINE JSC::JSValue callJS2Native(int32_t index, Zig::GlobalObject* global) {`, ` switch(index) {`, - ...nativeCalls.map(x => ` case ${x.id}: return ${symbol(x)}(global);`), + ...nativeCalls.map( + x => + ` case ${x.id}: return ${ + x.type === "bind" + ? `JSC::JSValue::decode(js2native_bindgen_${basename(x.filename.replace(/\.bind\.ts$/, ""))}_${x.symbol}(global))` + : `${symbol(x)}(global)` + };`, + ), ` default:`, ` __builtin_unreachable();`, ` }`, @@ -196,7 +209,8 @@ export function getJS2NativeCPP() { export function getJS2NativeZig(gs2NativeZigPath: string) { return [ "//! This file is generated by src/codegen/generate-js2native.ts based on seen calls to the $zig() JS macro", - `const JSC = @import("root").bun.JSC;`, + `const bun = @import("root").bun;`, + `const JSC = bun.JSC;`, ...nativeCalls .filter(x => x.type === "zig") .flatMap(call => [ @@ -212,11 +226,22 @@ export function getJS2NativeZig(gs2NativeZigPath: string) { symbol: x.symbol_target, filename: x.filename, })}(global: *JSC.JSGlobalObject, call_frame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue {`, - ` - const function = @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), x.filename))}); - return @call(.always_inline, JSC.toJSHostFunction(function.${x.symbol_target}), .{global, call_frame});`, + ` const function = @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), x.filename))});`, + ` return @call(.always_inline, JSC.toJSHostFunction(function.${x.symbol_target}), .{global, call_frame});`, "}", ]), + "comptime {", + ...nativeCalls + .filter(x => x.type === "bind") + .flatMap(x => { + const base = basename(x.filename.replace(/\.bind\.ts$/, "")); + return [ + ` @export(bun.gen.${base}.create${cap(x.symbol)}Callback, .{ .name = ${JSON.stringify( + `js2native_bindgen_${base}_${x.symbol}`, + )} });`, + ]; + }), + "}", ].join("\n"); } diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index b8e760e3fb..7ec71fa427 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -1,6 +1,6 @@ import { join, resolve } from "path"; -const classes = ["ArrayBufferSink", "FileSink", "HTTPResponseSink", "HTTPSResponseSink"]; +const classes = ["ArrayBufferSink", "FileSink", "HTTPResponseSink", "HTTPSResponseSink", "NetworkSink"]; function names(name) { return { @@ -99,6 +99,8 @@ function header() { } static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSCell* cell, JSC::VM& vm); + static size_t memoryCost(void* sinkPtr); void ref(); void unref(); @@ -156,6 +158,8 @@ function header() { DECLARE_VISIT_CHILDREN; static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSCell* cell, JSC::VM& vm); + static size_t memoryCost(void* sinkPtr); void* m_sinkPtr; mutable WriteBarrier m_onPull; @@ -181,7 +185,7 @@ JSC_DECLARE_CUSTOM_GETTER(function${name}__getter); const outer = ` // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' +// Generated by generate-jssink.ts // #pragma once @@ -217,10 +221,7 @@ Structure* createJSSinkControllerStructure(JSC::VM& vm, JSC::JSGlobalObject* glo async function implementation() { const head = ` // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' -// To regenerate this file, run: -// -// make generate-sink +// Generated by 'generate-jssink.ts' // #include "root.h" #include "headers.h" @@ -278,9 +279,7 @@ extern "C" void Bun__onSinkDestroyed(uintptr_t destructor, void* sinkPtr); namespace WebCore { using namespace JSC; - - - +${classes.map(name => `extern "C" size_t ${name}__memoryCost(void* sinkPtr);`).join("\n")} JSC_DEFINE_HOST_FUNCTION(functionStartDirectStream, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame *callFrame)) { @@ -406,6 +405,28 @@ JSC_DEFINE_CUSTOM_GETTER(function${name}__getter, (JSC::JSGlobalObject * lexical return JSC::JSValue::encode(globalObject->${name}()); } +size_t ${className}::estimatedSize(JSCell* cell, JSC::VM& vm) { + return Base::estimatedSize(cell, vm) + ${className}::memoryCost(jsCast<${className}*>(cell)->wrapped()); +} + +size_t ${className}::memoryCost(void* sinkPtr) { + if (!sinkPtr) + return 0; + + return ${name}__memoryCost(sinkPtr); +} + +size_t ${controller}::memoryCost(void* sinkPtr) { + if (!sinkPtr) + return 0; + + return ${name}__memoryCost(sinkPtr); +} + +size_t ${controller}::estimatedSize(JSCell* cell, JSC::VM& vm) { + return Base::estimatedSize(cell, vm) + ${controller}::memoryCost(jsCast<${controller}*>(cell)->wrapped()); +} + JSC_DECLARE_HOST_FUNCTION(${controller}__close); JSC_DEFINE_HOST_FUNCTION(${controller}__close, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame *callFrame)) { @@ -709,24 +730,43 @@ extern "C" void ${name}__setDestroyCallback(EncodedJSValue encodedValue, uintptr void ${className}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { + Base::analyzeHeap(cell, analyzer); auto* thisObject = jsCast<${className}*>(cell); if (void* wrapped = thisObject->wrapped()) { analyzer.setWrappedObjectForCell(cell, wrapped); // if (thisObject->scriptExecutionContext()) // analyzer.setLabelForCell(cell, makeString("url ", thisObject->scriptExecutionContext()->url().string())); } - Base::analyzeHeap(cell, analyzer); + } void ${controller}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { + Base::analyzeHeap(cell, analyzer); auto* thisObject = jsCast<${controller}*>(cell); if (void* wrapped = thisObject->wrapped()) { analyzer.setWrappedObjectForCell(cell, wrapped); // if (thisObject->scriptExecutionContext()) // analyzer.setLabelForCell(cell, makeString("url ", thisObject->scriptExecutionContext()->url().string())); } - Base::analyzeHeap(cell, analyzer); + + auto& vm = cell->vm(); + + if (thisObject->m_onPull) { + JSValue onPull = thisObject->m_onPull.get(); + if (onPull.isCell()) { + const Identifier& id = Identifier::fromString(vm, "onPull"_s); + analyzer.analyzePropertyNameEdge(cell, onPull.asCell(), id.impl()); + } + } + + if (thisObject->m_onClose) { + JSValue onClose = thisObject->m_onClose.get(); + if (onClose.isCell()) { + const Identifier& id = Identifier::fromString(vm, "onClose"_s); + analyzer.analyzePropertyNameEdge(cell, onClose.asCell(), id.impl()); + } + } } @@ -736,8 +776,11 @@ void ${controller}::visitChildrenImpl(JSCell* cell, Visitor& visitor) ${controller}* thisObject = jsCast<${controller}*>(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); - visitor.append(thisObject->m_onPull); - visitor.append(thisObject->m_onClose); + + // Avoid duplicating in the heap snapshot + visitor.appendHidden(thisObject->m_onPull); + visitor.appendHidden(thisObject->m_onClose); + void* ptr = thisObject->m_sinkPtr; if (ptr) visitor.addOpaqueRoot(ptr); diff --git a/src/codegen/generate-node-errors.ts b/src/codegen/generate-node-errors.ts index 6dfcedb4e1..0b5cc189a7 100644 --- a/src/codegen/generate-node-errors.ts +++ b/src/codegen/generate-node-errors.ts @@ -6,6 +6,18 @@ if (!outputDir) { throw new Error("Missing output directory"); } +const extra_count = NodeErrors.map(x => x.slice(3)) + .filter(x => x.length > 0) + .reduce((ac, cv) => ac + cv.length, 0); +const count = NodeErrors.length + extra_count; + +if (count > 256) { + // increase size of enum's to have more tags + // src/bun.js/node/types.zig#Encoding + // src/bun.js/bindings/BufferEncodingType.h + throw new Error("NodeError count exceeds u8"); +} + let enumHeader = ``; let listHeader = ``; let zig = ``; @@ -13,10 +25,13 @@ let zig = ``; enumHeader = ` // clang-format off // Generated by: src/codegen/generate-node-errors.ts +// Input: src/bun.js/bindings/ErrorCode.ts #pragma once +#include + namespace Bun { - static constexpr size_t NODE_ERROR_COUNT = ${NodeErrors.length}; + static constexpr size_t NODE_ERROR_COUNT = ${count}; enum class ErrorCode : uint8_t { `; @@ -25,12 +40,14 @@ listHeader = ` // Generated by: src/codegen/generate-node-errors.ts #pragma once +#include + struct ErrorCodeData { JSC::ErrorType type; WTF::ASCIILiteral name; WTF::ASCIILiteral code; }; -static constexpr ErrorCodeData errors[${NodeErrors.length}] = { +static constexpr ErrorCodeData errors[${count}] = { `; zig = ` @@ -67,7 +84,8 @@ pub const Error = enum(u8) { let i = 0; let listForUsingNamespace = ""; -for (const [code, constructor, name] of NodeErrors) { +for (let [code, constructor, name, ...other_constructors] of NodeErrors) { + if (name == null) name = constructor.name; enumHeader += ` ${code} = ${i},\n`; listHeader += ` { JSC::ErrorType::${constructor.name}, "${name}"_s, "${code}"_s },\n`; zig += ` ${code} = ${i},\n`; @@ -76,6 +94,19 @@ for (const [code, constructor, name] of NodeErrors) { listForUsingNamespace += ` return .{ .globalThis = globalThis, .args = args };\n`; listForUsingNamespace += ` }\n`; i++; + + for (const con of other_constructors) { + if (con == null) continue; + if (name == null) name = con.name; + enumHeader += ` ${code}_${con.name} = ${i},\n`; + listHeader += ` { JSC::ErrorType::${con.name}, "${con.name}"_s, "${code}"_s },\n`; + zig += ` ${code}_${con.name} = ${i},\n`; + listForUsingNamespace += ` /// ${name}: ${code} (instanceof ${con.name})\n`; + listForUsingNamespace += ` pub inline fn ${code}_${con.name}(globalThis: *JSC.JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) ErrorBuilder(Error.${code}_${con.name}, fmt, @TypeOf(args)) {\n`; + listForUsingNamespace += ` return .{ .globalThis = globalThis, .args = args };\n`; + listForUsingNamespace += ` }\n`; + i++; + } } enumHeader += ` diff --git a/src/codegen/helpers.ts b/src/codegen/helpers.ts index bfd6cdafcc..0b8ef64415 100644 --- a/src/codegen/helpers.ts +++ b/src/codegen/helpers.ts @@ -107,7 +107,7 @@ export function readdirRecursiveWithExclusionsAndExtensionsSync( const fullPath = path.join(dir, entry.name); return entry.isDirectory() ? readdirRecursiveWithExclusionsAndExtensionsSync(fullPath, exclusions, exts) - : exts.includes(path.extname(fullPath)) + : exts.some(ext => fullPath.endsWith(ext)) ? fullPath : []; }); @@ -130,3 +130,26 @@ export function camelCase(string: string) { export function pascalCase(string: string) { return string.split(/[\s_]/).map((e, i) => (i ? e.charAt(0).toUpperCase() + e.slice(1) : e.toLowerCase())); } + +export function argParse(keys: string[]): any { + const options = {}; + for (const arg of process.argv.slice(2)) { + if (!arg.startsWith("--")) { + console.error("Unknown argument " + arg); + process.exit(1); + } + const split = arg.split("="); + const value = split[1] || "true"; + options[split[0].slice(2)] = value; + } + + const unknown = new Set(Object.keys(options)); + for (const key of keys) { + unknown.delete(key); + } + for (const key of unknown) { + console.error("Unknown argument: --" + key); + } + if (unknown.size > 0) process.exit(1); + return options; +} \ No newline at end of file diff --git a/src/codegen/internal-module-registry-scanner.ts b/src/codegen/internal-module-registry-scanner.ts index 71458fd2e2..019040eed6 100644 --- a/src/codegen/internal-module-registry-scanner.ts +++ b/src/codegen/internal-module-registry-scanner.ts @@ -74,7 +74,7 @@ export function createInternalModuleRegistry(basedir: string) { 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. `, + `Builtin Bundler: "${specifier}" cannot be imported from "${from}" 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. `, ); } return codegenRequireId(`${found}/*${path.relative(basedir, relativeMatch)}*/`); diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index 025f0f854d..148291b317 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -2,6 +2,7 @@ import { LoaderKeys } from "../api/schema"; import NodeErrors from "../bun.js/bindings/ErrorCode.ts"; import { sliceSourceCode } from "./builtin-parser"; import { registerNativeCall } from "./generate-js2native"; +import jsclasses from "./../bun.js/bindings/js_classes"; // This is a list of extra syntax replacements to do. Kind of like macros // These are only run on code itself, not string contents or comments. @@ -13,11 +14,29 @@ export const replacements: ReplacementRule[] = [ { from: /\bexport\s*default/g, to: "$exports =" }, ]; +let error_i = 0; for (let i = 0; i < NodeErrors.length; i++) { - const [code] = NodeErrors[i]; + const [code, _constructor, _name, ...other_constructors] = NodeErrors[i]; replacements.push({ from: new RegExp(`\\b\\__intrinsic__${code}\\(`, "g"), - to: `$makeErrorWithCode(${i}, `, + to: `$makeErrorWithCode(${error_i}, `, + }); + error_i += 1; + for (const con of other_constructors) { + if (con == null) continue; + replacements.push({ + from: new RegExp(`\\b\\__intrinsic__${code}_${con.name}\\(`, "g"), + to: `$makeErrorWithCode(${error_i}, `, + }); + error_i += 1; + } +} + +for (let id = 0; id < jsclasses.length; id++) { + const name = jsclasses[id][0]; + replacements.push({ + from: new RegExp(`\\b\\__intrinsic__inherits${name}\\(`, "g"), + to: `$inherits(${id}, `, }); } @@ -74,7 +93,8 @@ replacements.push({ export const enums = { Loader: LoaderKeys, ImportKind: [ - "entry-point", + "entry-point-run", + "entry-point-build", "import-statement", "require-call", "dynamic-import", @@ -141,10 +161,16 @@ export interface ReplacementRule { } export const function_replacements = [ - "$debug", "$assert", "$zig", "$newZigFunction", "$cpp", "$newCppFunction", + "$debug", + "$assert", + "$zig", + "$newZigFunction", + "$cpp", + "$newCppFunction", "$isPromiseResolved", + "$bindgenFn", ]; -const function_regexp = new RegExp(`__intrinsic__(${function_replacements.join("|").replaceAll('$', '')})`); +const function_regexp = new RegExp(`__intrinsic__(${function_replacements.join("|").replaceAll("$", "")})`); /** Applies source code replacements as defined in `replacements` */ export function applyReplacements(src: string, length: number) { @@ -155,10 +181,7 @@ export function applyReplacements(src: string, length: number) { slice = slice.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__").replaceAll("%", "$")); } let match; - if ( - (match = slice.match(function_regexp)) && - rest.startsWith("(") - ) { + if ((match = slice.match(function_regexp)) && rest.startsWith("(")) { const name = match[1]; if (name === "debug") { const innerSlice = sliceSourceCode(rest, true); @@ -233,9 +256,31 @@ export function applyReplacements(src: string, length: number) { // use a property on @lazy as a temporary holder for the expression. only in debug! args = `($assert(__intrinsic__isPromise(__intrinsic__lazy.temp=${inner.result.slice(0, -1)}))),(__intrinsic__getPromiseInternalField(__intrinsic__lazy.temp, __intrinsic__promiseFieldFlags) & __intrinsic__promiseStateMask) === (__intrinsic__lazy.temp = undefined, __intrinsic__promiseStateFulfilled))`; } else { - args = `((__intrinsic__getPromiseInternalField(${inner.result.slice(0,-1)}), __intrinsic__promiseFieldFlags) & __intrinsic__promiseStateMask) === __intrinsic__promiseStateFulfilled)`; + args = `((__intrinsic__getPromiseInternalField(${inner.result.slice(0, -1)}), __intrinsic__promiseFieldFlags) & __intrinsic__promiseStateMask) === __intrinsic__promiseStateFulfilled)`; } return [slice.slice(0, match.index) + args, inner.rest, true]; + } else if (name === "bindgenFn") { + const inner = sliceSourceCode(rest, true); + let args; + try { + const str = + "[" + + inner.result + .slice(1, -1) + .replaceAll("'", '"') + .replace(/,[\s\n]*$/s, "") + + "]"; + args = JSON.parse(str); + } catch { + throw new Error(`Call is not known at bundle-time: '$${name}${inner.result}'`); + } + if (args.length != 2 || typeof args[0] !== "string" || typeof args[1] !== "string") { + throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); + } + + const id = registerNativeCall("bind", args[0], args[1], undefined); + + return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else { throw new Error("Unknown preprocessor macro " + name); } diff --git a/src/compile_target.zig b/src/compile_target.zig index cf0f0acc20..82f0731dd3 100644 --- a/src/compile_target.zig +++ b/src/compile_target.zig @@ -28,6 +28,14 @@ const Libc = enum { /// musl libc musl, + /// npm package name, `@oven-sh/bun-{os}-{arch}` + pub fn npmName(this: Libc) []const u8 { + return switch (this) { + .default => "", + .musl => "-musl", + }; + } + pub fn format(self: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { if (self == .musl) { try writer.writeAll("-musl"); @@ -64,21 +72,25 @@ pub fn toNPMRegistryURL(this: *const CompileTarget, buf: []u8) ![]const u8 { pub fn toNPMRegistryURLWithURL(this: *const CompileTarget, buf: []u8, registry_url: []const u8) ![]const u8 { return switch (this.os) { inline else => |os| switch (this.arch) { - inline else => |arch| switch (this.baseline) { - // https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-0.1.6.tgz - inline else => |is_baseline| try std.fmt.bufPrint(buf, comptime "{s}/@oven/bun-" ++ - os.npmName() ++ "-" ++ arch.npmName() ++ - (if (is_baseline) "-baseline" else "") ++ - "/-/bun-" ++ - os.npmName() ++ "-" ++ arch.npmName() ++ - (if (is_baseline) "-baseline" else "") ++ - "-" ++ - "{d}.{d}.{d}.tgz", .{ - registry_url, - this.version.major, - this.version.minor, - this.version.patch, - }), + inline else => |arch| switch (this.libc) { + inline else => |libc| switch (this.baseline) { + // https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-0.1.6.tgz + inline else => |is_baseline| try std.fmt.bufPrint(buf, comptime "{s}/@oven/bun-" ++ + os.npmName() ++ "-" ++ arch.npmName() ++ + libc.npmName() ++ + (if (is_baseline) "-baseline" else "") ++ + "/-/bun-" ++ + os.npmName() ++ "-" ++ arch.npmName() ++ + libc.npmName() ++ + (if (is_baseline) "-baseline" else "") ++ + "-" ++ + "{d}.{d}.{d}.tgz", .{ + registry_url, + this.version.major, + this.version.minor, + this.version.patch, + }), + }, }, }, }; @@ -120,7 +132,7 @@ pub fn exePath(this: *const CompileTarget, buf: *bun.PathBuffer, version_str: [: bun.fs.FileSystem.instance.top_level_dir, buf, &.{ - bun.install.PackageManager.fetchCacheDirectoryPath(env).path, + bun.install.PackageManager.fetchCacheDirectoryPath(env, null).path, version_str, }, .auto, @@ -153,7 +165,7 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc { var progress = refresher.start("Downloading", 0); defer progress.end(); - const http_proxy: ?bun.URL = env.getHttpProxy(url); + const http_proxy: ?bun.URL = env.getHttpProxyFor(url); async_http.* = HTTP.AsyncHTTP.initSync( allocator, @@ -311,8 +323,10 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc pub fn isSupported(this: *const CompileTarget) bool { return switch (this.os) { .windows => this.arch == .x64, + .mac => true, - .linux => this.libc == .default, + .linux => true, + .wasm => false, }; } diff --git a/src/copy_file.zig b/src/copy_file.zig index 1d66e8ed04..220aeb7733 100644 --- a/src/copy_file.zig +++ b/src/copy_file.zig @@ -42,7 +42,7 @@ const InputType = if (Environment.isWindows) bun.OSPathSliceZ else posix.fd_t; /// This means that it cannot work with TTYs and some special devices /// But it can work with two ordinary files /// -/// on macoS and other platforms, sendfile() only works when one of the ends is a socket +/// on macOS and other platforms, sendfile() only works when one of the ends is a socket /// and in general on macOS, it doesn't seem to have much performance impact. const LinuxCopyFileState = packed struct { /// This is the most important flag for reducing the system call count diff --git a/src/crash_handler.zig b/src/crash_handler.zig index 554fcf5873..846288fc3b 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -44,12 +44,19 @@ var has_printed_message = false; var panicking = std.atomic.Value(u8).init(0); // Locked to avoid interleaving panic messages from multiple threads. -var panic_mutex = std.Thread.Mutex{}; +// TODO: I don't think it's safe to lock/unlock a mutex inside a signal handler. +var panic_mutex = bun.Mutex{}; /// Counts how many times the panic handler is invoked by this thread. /// This is used to catch and handle panics triggered by the panic handler. threadlocal var panic_stage: usize = 0; +threadlocal var inside_native_plugin: ?[*:0]const u8 = null; + +export fn CrashHandler__setInsideNativePlugin(name: ?[*:0]const u8) callconv(.C) void { + inside_native_plugin = name; +} + /// This can be set by various parts of the codebase to indicate a broader /// action being taken. It is printed when a crash happens, which can help /// narrow down what the bug is. Example: "Crashed while parsing /path/to/file.js" @@ -60,7 +67,9 @@ threadlocal var panic_stage: usize = 0; pub threadlocal var current_action: ?Action = null; var before_crash_handlers: std.ArrayListUnmanaged(struct { *anyopaque, *const OnBeforeCrash }) = .{}; -var before_crash_handlers_mutex: std.Thread.Mutex = .{}; + +// TODO: I don't think it's safe to lock/unlock a mutex inside a signal handler. +var before_crash_handlers_mutex: bun.Mutex = .{}; const CPUFeatures = @import("./bun.js/bindings/CPUFeatures.zig").CPUFeatures; @@ -226,6 +235,18 @@ pub fn crashHandler( writer.writeAll("=" ** 60 ++ "\n") catch std.posix.abort(); printMetadata(writer) catch std.posix.abort(); + + if (inside_native_plugin) |name| { + const native_plugin_name = name; + const fmt = + \\ + \\Bun has encountered a crash while running the "{s}" native plugin. + \\ + \\This indicates either a bug in the native plugin or in Bun. + \\ + ; + writer.print(Output.prettyFmt(fmt, true), .{native_plugin_name}) catch std.posix.abort(); + } } else { if (Output.enable_ansi_colors) { writer.writeAll(Output.prettyFmt("", true)) catch std.posix.abort(); @@ -308,7 +329,17 @@ pub fn crashHandler( } else { writer.writeAll(Output.prettyFmt(": ", true)) catch std.posix.abort(); } - if (reason == .out_of_memory) { + if (inside_native_plugin) |name| { + const native_plugin_name = name; + writer.print(Output.prettyFmt( + \\Bun has encountered a crash while running the "{s}" native plugin. + \\ + \\To send a redacted crash report to Bun's team, + \\please file a GitHub issue using the link below: + \\ + \\ + , true), .{native_plugin_name}) catch std.posix.abort(); + } else if (reason == .out_of_memory) { writer.writeAll( \\Bun has ran out of memory. \\ @@ -943,7 +974,7 @@ fn waitForOtherThreadToFinishPanicking() void { // Sleep forever without hammering the CPU var futex = std.atomic.Value(u32).init(0); - while (true) std.Thread.Futex.wait(&futex, 0); + while (true) bun.Futex.waitForever(&futex, 0); comptime unreachable; } } @@ -959,7 +990,7 @@ pub fn sleepForeverIfAnotherThreadIsCrashing() void { if (panicking.load(.acquire) > 0) { // Sleep forever without hammering the CPU var futex = std.atomic.Value(u32).init(0); - while (true) std.Thread.Futex.wait(&futex, 0); + while (true) bun.Futex.waitForever(&futex, 0); comptime unreachable; } } @@ -1621,6 +1652,13 @@ pub fn dumpStackTrace(trace: std.builtin.StackTrace) void { stderr.writeAll(proc.stderr) catch return; } +pub fn dumpCurrentStackTrace(first_address: ?usize) void { + var addrs: [32]usize = undefined; + var stack: std.builtin.StackTrace = .{ .index = 0, .instruction_addresses = &addrs }; + std.debug.captureStackTrace(first_address, &stack); + dumpStackTrace(stack); +} + /// A variant of `std.builtin.StackTrace` that stores its data within itself /// instead of being a pointer. This allows storing captured stack traces /// for later printing. diff --git a/src/css/context.zig b/src/css/context.zig index db6b3964a2..09c9e59373 100644 --- a/src/css/context.zig +++ b/src/css/context.zig @@ -25,9 +25,9 @@ pub const SupportsEntry = struct { important_declarations: ArrayList(css.Property), pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { - _ = this; // autofix - _ = allocator; // autofix - @panic(css.todo_stuff.depth); + this.condition.deinit(allocator); + css.deepDeinit(css.Property, allocator, &this.declarations); + css.deepDeinit(css.Property, allocator, &this.important_declarations); } }; diff --git a/src/css/css_internals.zig b/src/css/css_internals.zig index 46b7f133f0..4c0d86dc45 100644 --- a/src/css/css_internals.zig +++ b/src/css/css_internals.zig @@ -1,7 +1,7 @@ const bun = @import("root").bun; const std = @import("std"); const builtin = @import("builtin"); -const Arena = @import("../mimalloc_arena.zig").Arena; +const Arena = @import("../allocators/mimalloc_arena.zig").Arena; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const JSC = bun.JSC; @@ -60,18 +60,19 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c const expected = expected_bunstr.toUTF8(bun.default_allocator); defer expected.deinit(); - const options_arg = arguments.nextEat(); + const browser_options_arg = arguments.nextEat(); var log = bun.logger.Log.init(alloc); defer log.deinit(); + var browsers: ?bun.css.targets.Browsers = null; const parser_options = parser_options: { const opts = bun.css.ParserOptions.default(alloc, &log); // if (test_kind == .prefix) break :parser_options opts; - if (options_arg) |optargs| { + if (browser_options_arg) |optargs| { if (optargs.isObject()) { - // minify_options.targets.browsers = targetsFromJS(globalThis, optarg); + browsers = try targetsFromJS(globalThis, optargs); } } @@ -88,11 +89,7 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c .result => |stylesheet_| { var stylesheet = stylesheet_; var minify_options: bun.css.MinifyOptions = bun.css.MinifyOptions.default(); - if (options_arg) |optarg| { - if (optarg.isObject()) { - minify_options.targets.browsers = try targetsFromJS(globalThis, optarg); - } - } + minify_options.targets.browsers = browsers; _ = stylesheet.minify(alloc, minify_options).assert(); const result = stylesheet.toCss(alloc, bun.css.PrinterOptions{ diff --git a/src/css/css_modules.zig b/src/css/css_modules.zig index 14767a4a8c..e476b03b4c 100644 --- a/src/css/css_modules.zig +++ b/src/css/css_modules.zig @@ -70,17 +70,71 @@ pub const CssModule = struct { // TODO: deinit } + pub fn getReference(this: *CssModule, allocator: Allocator, name: []const u8, source_index: u32) void { + const gop = this.exports_by_source_index.items[source_index].getOrPut(allocator, name) catch bun.outOfMemory(); + if (gop.found_existing) { + gop.value_ptr.is_referenced = true; + } else { + gop.value_ptr.* = CssModuleExport{ + .name = this.config.pattern.writeToString(allocator, .{}, this.hashes.items[source_index], this.sources.items[source_index], name), + .composes = .{}, + .is_referenced = true, + }; + } + } + pub fn referenceDashed( this: *CssModule, + allocator: std.mem.Allocator, name: []const u8, from: *const ?css.css_properties.css_modules.Specifier, source_index: u32, ) ?[]const u8 { - _ = this; // autofix - _ = name; // autofix - _ = from; // autofix - _ = source_index; // autofix - @panic(css.todo_stuff.depth); + const reference, const key = if (from.*) |specifier| switch (specifier) { + .global => return name[2..], + .file => |file| .{ + CssModuleReference{ .dependency = .{ .name = name[2..], .specifier = file } }, + file, + }, + .source_index => |dep_source_index| return this.config.pattern.writeToString( + allocator, + .{}, + this.hashes.items[dep_source_index], + this.sources.items[dep_source_index], + name[2..], + ), + } else { + // Local export. Mark as used. + const gop = this.exports_by_source_index.items[source_index].getOrPut(allocator, name) catch bun.outOfMemory(); + if (gop.found_existing) { + gop.value_ptr.is_referenced = true; + } else { + var res = ArrayList(u8){}; + res.appendSlice(allocator, "--") catch bun.outOfMemory(); + gop.value_ptr.* = CssModuleExport{ + .name = this.config.pattern.writeToString( + allocator, + res, + this.hashes.items[source_index], + this.sources.items[source_index], + name[2..], + ), + .composes = .{}, + .is_referenced = true, + }; + } + return null; + }; + + const the_hash = hash(allocator, "{s}_{s}_{s}", .{ this.hashes.items[source_index], name, key }, false); + + this.references.put( + allocator, + std.fmt.allocPrint(allocator, "--{s}", .{the_hash}) catch bun.outOfMemory(), + reference, + ) catch bun.outOfMemory(); + + return the_hash; } pub fn handleComposes( @@ -397,10 +451,33 @@ pub const CssModuleReference = union(enum) { // TODO: replace with bun's hash pub fn hash(allocator: Allocator, comptime fmt: []const u8, args: anytype, at_start: bool) []const u8 { - _ = fmt; // autofix - _ = args; // autofix - _ = allocator; // autofix - _ = at_start; // autofix - // @compileError(css.todo_stuff.depth); - @panic(css.todo_stuff.depth); + const count = std.fmt.count(fmt, args); + var stack_fallback = std.heap.stackFallback(128, allocator); + const fmt_alloc = if (count <= 128) stack_fallback.get() else allocator; + var hasher = bun.Wyhash11.init(0); + var fmt_str = std.fmt.allocPrint(fmt_alloc, fmt, args) catch bun.outOfMemory(); + hasher.update(fmt_str); + + const h: u32 = @truncate(hasher.final()); + var h_bytes: [4]u8 = undefined; + std.mem.writeInt(u32, &h_bytes, h, .little); + + const encode_len = bun.base64.encodeLen(h_bytes[0..]); + + var slice_to_write = if (encode_len <= 128 - @as(usize, @intFromBool(at_start))) + allocator.alloc(u8, encode_len + @as(usize, @intFromBool(at_start))) catch bun.outOfMemory() + else + fmt_str[0..]; + + const base64_encoded_hash_len = bun.base64.encode(slice_to_write, &h_bytes); + + const base64_encoded_hash = slice_to_write[0..base64_encoded_hash_len]; + + if (at_start and base64_encoded_hash.len > 0 and base64_encoded_hash[0] >= '0' and base64_encoded_hash[0] <= '9') { + std.mem.copyBackwards(u8, slice_to_write[1..][0..base64_encoded_hash_len], base64_encoded_hash); + slice_to_write[0] = '_'; + return slice_to_write[0 .. base64_encoded_hash_len + 1]; + } + + return base64_encoded_hash; } diff --git a/src/css/css_parser.zig b/src/css/css_parser.zig index ab434d3f0f..83c1b0750f 100644 --- a/src/css/css_parser.zig +++ b/src/css/css_parser.zig @@ -186,8 +186,14 @@ pub const VendorPrefix = packed struct(u8) { } /// Returns VendorPrefix::None if empty. - pub fn orNone(this: VendorPrefix) VendorPrefix { - return this.bitwiseOr(VendorPrefix{ .none = true }); + pub inline fn orNone(this: VendorPrefix) VendorPrefix { + return this._or(VendorPrefix{ .none = true }); + } + + /// **WARNING**: NOT THE SAME as .bitwiseOr!! + pub inline fn _or(this: VendorPrefix, other: VendorPrefix) VendorPrefix { + if (this.isEmpty()) return other; + return this; } }; @@ -356,7 +362,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // const property_id = @unionInit( // PropertyId, // field.name, - // if (@hasDecl(T.VendorPrefixMap, field.name)) vendor_prefix else {}, + // if (@hasDecl(T.VendorPrefixMap, field.name)) vendor_prefix, // ); // const value = property.longhand(&property_id); // if (@as(PropertyIdTag, value) == @as(PropertyIdTag, property_id)) { @@ -384,7 +390,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // } // return null; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } /// Returns a shorthand from the longhand properties defined in the given declaration block. @@ -397,7 +403,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // out[i] = @unionInit( // PropertyId, // field.name, - // if (@hasField(T.VendorPrefixMap, field.name)) vendor_prefix else {}, + // if (@hasField(T.VendorPrefixMap, field.name)) vendor_prefix, // ); // } @@ -405,7 +411,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // }; // return out; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } /// Returns a longhand property for this shorthand. @@ -430,7 +436,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // } // } // return null; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } /// Updates this shorthand from a longhand property. @@ -451,7 +457,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // } // } // return false; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } }; } @@ -916,12 +922,8 @@ pub fn DeriveValueType(comptime T: type) type { } fn consume_until_end_of_block(block_type: BlockType, tokenizer: *Tokenizer) void { - const StackCount = 16; - var sfb = std.heap.stackFallback(@sizeOf(BlockType) * StackCount, tokenizer.allocator); - const alloc = sfb.get(); - var stack = std.ArrayList(BlockType).initCapacity(alloc, StackCount) catch unreachable; - defer stack.deinit(); - + @setCold(true); + var stack = SmallList(BlockType, 16){}; stack.appendAssumeCapacity(block_type); while (switch (tokenizer.next()) { @@ -929,13 +931,13 @@ fn consume_until_end_of_block(block_type: BlockType, tokenizer: *Tokenizer) void .err => null, }) |tok| { if (BlockType.closing(&tok)) |b| { - if (stack.getLast() == b) { + if (stack.getLastUnchecked() == b) { _ = stack.pop(); - if (stack.items.len == 0) return; + if (stack.len() == 0) return; } } - if (BlockType.opening(&tok)) |bt| stack.append(bt) catch unreachable; + if (BlockType.opening(&tok)) |bt| stack.append(tokenizer.allocator, bt); } } @@ -4200,8 +4202,6 @@ const Tokenizer = struct { .position = 0, }; - // make current point to the first token - _ = lexer.next(); lexer.position = 0; return lexer; @@ -6253,6 +6253,20 @@ pub const serializer = struct { }; } else notation: { var buf: [129]u8 = undefined; + // We must pass finite numbers to dtoa_short + if (std.math.isPositiveInf(value)) { + const output = "1e999"; + try writer.writeAll(output); + return; + } else if (std.math.isNegativeInf(value)) { + const output = "-1e999"; + try writer.writeAll(output); + return; + } + // We shouldn't receive NaN here. + // NaN is not a valid CSS token and any inlined calculations from `calc()` we ensure + // are not NaN. + bun.debugAssert(!std.math.isNan(value)); const str, const notation = dtoa_short(&buf, value, 6); try writer.writeAll(str); break :notation notation; @@ -6691,6 +6705,7 @@ const Notation = struct { pub fn dtoa_short(buf: *[129]u8, value: f32, comptime precision: u8) struct { []u8, Notation } { buf[0] = '0'; + bun.debugAssert(std.math.isFinite(value)); const buf_len = bun.fmt.FormatDouble.dtoa(@ptrCast(buf[1..].ptr), @floatCast(value)).len; return restrict_prec(buf[0 .. buf_len + 1], precision); } diff --git a/src/css/declaration.zig b/src/css/declaration.zig index f84c4f9c11..a7ec7e1182 100644 --- a/src/css/declaration.zig +++ b/src/css/declaration.zig @@ -47,7 +47,7 @@ pub const DeclarationBlock = struct { var arraylist = ArrayList(u8){}; const w = arraylist.writer(bun.default_allocator); defer arraylist.deinit(bun.default_allocator); - var printer = css.Printer(@TypeOf(w)).new(bun.default_allocator, std.ArrayList(u8).init(bun.default_allocator), w, .{}, null); + var printer = css.Printer(@TypeOf(w)).new(bun.default_allocator, std.ArrayList(u8).init(bun.default_allocator), w, css.PrinterOptions.default(), null); defer printer.deinit(); this.self.toCss(@TypeOf(w), &printer) catch |e| return try writer.print("\n", .{@errorName(e)}); try writer.writeAll(arraylist.items); diff --git a/src/css/dependencies.zig b/src/css/dependencies.zig index 7d922244dd..453f9cefb3 100644 --- a/src/css/dependencies.zig +++ b/src/css/dependencies.zig @@ -70,7 +70,7 @@ pub const ImportDependency = struct { allocator, css.css_rules.supports.SupportsCondition, supports, - css.PrinterOptions{}, + css.PrinterOptions.default(), null, ) catch bun.Output.panic( "Unreachable code: failed to stringify SupportsCondition.\n\nThis is a bug in Bun's CSS printer. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", @@ -80,7 +80,7 @@ pub const ImportDependency = struct { } else null; const media = if (rule.media.media_queries.items.len > 0) media: { - const s = css.to_css.string(allocator, css.MediaList, &rule.media, css.PrinterOptions{}, null) catch bun.Output.panic( + const s = css.to_css.string(allocator, css.MediaList, &rule.media, css.PrinterOptions.default(), null) catch bun.Output.panic( "Unreachable code: failed to stringify MediaList.\n\nThis is a bug in Bun's CSS printer. Please file a bug report at https://github.com/oven-sh/bun/issues/new/choose", .{}, ); diff --git a/src/css/printer.zig b/src/css/printer.zig index 9d7b029e2a..c50adb3bcd 100644 --- a/src/css/printer.zig +++ b/src/css/printer.zig @@ -26,7 +26,7 @@ pub const PrinterOptions = struct { /// An optional project root path, used to generate relative paths for sources used in CSS module hashes. project_root: ?[]const u8 = null, /// Targets to output the CSS for. - targets: Targets = .{}, + targets: Targets, /// Whether to analyze dependencies (i.e. `@import` and `url()`). /// If true, the dependencies are returned as part of the /// [ToCssResult](super::stylesheet::ToCssResult). @@ -39,6 +39,23 @@ pub const PrinterOptions = struct { /// from JavaScript. Useful for polyfills, for example. pseudo_classes: ?PseudoClasses = null, public_path: []const u8 = "", + + pub fn default() PrinterOptions { + return .{ + .targets = Targets{ + .browsers = null, + }, + }; + } + + pub fn defaultWithMinify(minify: bool) PrinterOptions { + return .{ + .targets = Targets{ + .browsers = null, + }, + .minify = minify, + }; + } }; /// A mapping of user action pseudo classes to replace with class names. diff --git a/src/css/properties/animation.zig b/src/css/properties/animation.zig index b6136db261..d6d8fb198f 100644 --- a/src/css/properties/animation.zig +++ b/src/css/properties/animation.zig @@ -47,9 +47,44 @@ pub const AnimationName = union(enum) { } pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + const css_module_animation_enabled = if (dest.css_module) |css_module| + css_module.config.animation + else + false; + + switch (this.*) { + .none => return dest.writeStr("none"), + .ident => |s| { + if (css_module_animation_enabled) { + if (dest.css_module) |*css_module| { + css_module.getReference(dest.allocator, s.v, dest.loc.source_index); + } + } + return s.toCssWithOptions(W, dest, css_module_animation_enabled); + }, + .string => |s| { + if (css_module_animation_enabled) { + if (dest.css_module) |*css_module| { + css_module.getReference(dest.allocator, s, dest.loc.source_index); + } + } + + // CSS-wide keywords and `none` cannot remove quotes + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "none") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "initial") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "inherit") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "unset") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "default") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert-layer")) + { + css.serializer.serializeString(s, dest) catch return dest.addFmtError(); + return; + } + + return dest.writeIdent(s, css_module_animation_enabled); + }, + } } }; diff --git a/src/css/properties/background.zig b/src/css/properties/background.zig index 438b43ade6..2447a370fa 100644 --- a/src/css/properties/background.zig +++ b/src/css/properties/background.zig @@ -1023,8 +1023,10 @@ pub const BackgroundHandler = struct { // If the last declaration is prefixed, pop the last value // so it isn't duplicated when we flush. if (this.has_prefix) { - var prop = this.decls.pop(); - prop.deinit(allocator); + var prop = this.decls.popOrNull(); + if (prop != null) { + prop.?.deinit(allocator); + } } dest.appendSlice(allocator, this.decls.items) catch bun.outOfMemory(); diff --git a/src/css/properties/css_modules.zig b/src/css/properties/css_modules.zig index fa087a3866..859ca91f57 100644 --- a/src/css/properties/css_modules.zig +++ b/src/css/properties/css_modules.zig @@ -40,8 +40,20 @@ pub const Composes = struct { loc: Location, pub fn parse(input: *css.Parser) css.Result(Composes) { - _ = input; // autofix - @panic(css.todo_stuff.depth); + const loc = input.currentSourceLocation(); + var names: CustomIdentList = .{}; + while (input.tryParse(parseOneIdent, .{}).asValue()) |name| { + names.append(input.allocator(), name); + } + + if (names.len() == 0) return .{ .err = input.newCustomError(css.ParserError{ .invalid_declaration = {} }) }; + + const from = if (input.tryParse(css.Parser.expectIdentMatching, .{"from"}).isOk()) switch (Specifier.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } else null; + + return .{ .result = Composes{ .names = names, .from = from, .loc = Location.fromSourceLocation(loc) } }; } pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { @@ -61,6 +73,17 @@ pub const Composes = struct { } } + fn parseOneIdent(input: *css.Parser) css.Result(CustomIdent) { + const name: CustomIdent = switch (CustomIdent.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + if (bun.strings.eqlCaseInsensitiveASCII(name.v, "from", true)) return .{ .err = input.newErrorForNextToken() }; + + return .{ .result = name }; + } + pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } diff --git a/src/css/properties/custom.zig b/src/css/properties/custom.zig index eaa7ad2f89..cfde458ae0 100644 --- a/src/css/properties/custom.zig +++ b/src/css/properties/custom.zig @@ -1433,6 +1433,10 @@ pub const UnparsedProperty = struct { pub fn deepClone(this: *const @This(), allocator: Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } + + pub fn eql(lhs: *const @This(), rhs: *const @This()) bool { + return css.implementEql(@This(), lhs, rhs); + } }; /// A CSS custom property, representing any unknown property. diff --git a/src/css/properties/font.zig b/src/css/properties/font.zig index fe1980e19a..b5e8f12eff 100644 --- a/src/css/properties/font.zig +++ b/src/css/properties/font.zig @@ -160,17 +160,25 @@ pub const FontStretch = union(enum) { percentage: Percentage, // TODO: implement this - // pub usingnamespace css.DeriveParse(@This()); - - pub fn parse(input: *css.Parser) css.Result(FontStretch) { - _ = input; // autofix - @panic(css.todo_stuff.depth); - } + pub usingnamespace css.DeriveParse(@This()); pub fn toCss(this: *const FontStretch, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + if (dest.minify) { + const percentage: Percentage = this.intoPercentage(); + return percentage.toCss(W, dest); + } + + return switch (this.*) { + .percentage => |*val| val.toCss(W, dest), + .keyword => |*kw| kw.toCss(W, dest), + }; + } + + pub fn intoPercentage(this: *const FontStretch) Percentage { + return switch (this.*) { + .percentage => |*val| val.*, + .keyword => |*kw| kw.intoPercentage(), + }; } pub fn eql(lhs: *const FontStretch, rhs: *const FontStretch) bool { @@ -215,6 +223,21 @@ pub const FontStretchKeyword = enum { pub inline fn default() FontStretchKeyword { return .normal; } + + pub fn intoPercentage(this: *const FontStretchKeyword) Percentage { + const val: f32 = switch (this.*) { + .@"ultra-condensed" => 0.5, + .@"extra-condensed" => 0.625, + .condensed => 0.75, + .@"semi-condensed" => 0.875, + .normal => 1.0, + .@"semi-expanded" => 1.125, + .expanded => 1.25, + .@"extra-expanded" => 1.5, + .@"ultra-expanded" => 2.0, + }; + return .{ .v = val }; + } }; /// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property. diff --git a/src/css/properties/generate_properties.ts b/src/css/properties/generate_properties.ts index e7f79c1e2e..f55f04d022 100644 --- a/src/css/properties/generate_properties.ts +++ b/src/css/properties/generate_properties.ts @@ -287,7 +287,8 @@ function generatePropertyImpl(property_defs: Record): strin return `.${escapeIdent(name)} => |*v| css.generic.eql(${meta.ty}, v, &rhs.${escapeIdent(name)}),`; }) .join("\n")} - .all, .unparsed => true, + .unparsed => |*u| u.eql(&rhs.unparsed), + .all => true, .custom => |*c| c.eql(&rhs.custom), }; } @@ -1077,6 +1078,7 @@ generateCode({ "margin-right": { ty: "LengthPercentageOrAuto", logical_group: { ty: "margin", category: "physical" }, + eval_branch_quota: 5000, }, "margin-block-start": { ty: "LengthPercentageOrAuto", diff --git a/src/css/properties/properties_generated.zig b/src/css/properties/properties_generated.zig index be76ee77a7..46615f071a 100644 --- a/src/css/properties/properties_generated.zig +++ b/src/css/properties/properties_generated.zig @@ -6951,7 +6951,8 @@ pub const Property = union(PropertyIdTag) { .@"mask-box-image-width" => |*v| css.generic.eql(Rect(BorderImageSideWidth), &v[0], &v[0]) and v[1].eq(rhs.@"mask-box-image-width"[1]), .@"mask-box-image-outset" => |*v| css.generic.eql(Rect(LengthOrNumber), &v[0], &v[0]) and v[1].eq(rhs.@"mask-box-image-outset"[1]), .@"mask-box-image-repeat" => |*v| css.generic.eql(BorderImageRepeat, &v[0], &v[0]) and v[1].eq(rhs.@"mask-box-image-repeat"[1]), - .all, .unparsed => true, + .unparsed => |*u| u.eql(&rhs.unparsed), + .all => true, .custom => |*c| c.eql(&rhs.custom), }; } diff --git a/src/css/properties/properties_impl.zig b/src/css/properties/properties_impl.zig index 6a56e20d0e..871a61cdd3 100644 --- a/src/css/properties/properties_impl.zig +++ b/src/css/properties/properties_impl.zig @@ -20,6 +20,7 @@ pub fn PropertyIdImpl() type { var first = true; const name = this.name(this); const prefix_value = this.prefix().orNone(); + inline for (VendorPrefix.FIELDS) |field| { if (@field(prefix_value, field)) { var prefix: VendorPrefix = .{}; diff --git a/src/css/properties/transform.zig b/src/css/properties/transform.zig index 576779ad30..8a1c879798 100644 --- a/src/css/properties/transform.zig +++ b/src/css/properties/transform.zig @@ -38,14 +38,60 @@ pub const TransformList = struct { v: ArrayList(Transform), pub fn parse(input: *css.Parser) Result(@This()) { - _ = input; // autofix - @panic(css.todo_stuff.depth); + if (input.tryParse(css.Parser.expectIdentMatching, .{"none"}).isOk()) { + return .{ .result = .{ .v = .{} } }; + } + + input.skipWhitespace(); + var results = ArrayList(Transform){}; + switch (Transform.parse(input)) { + .result => |first| results.append(input.allocator(), first) catch bun.outOfMemory(), + .err => |e| return .{ .err = e }, + } + + while (true) { + input.skipWhitespace(); + if (input.tryParse(Transform.parse, .{}).asValue()) |item| { + results.append(input.allocator(), item) catch bun.outOfMemory(); + } else { + return .{ .result = .{ .v = results } }; + } + } } pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + if (this.v.items.len == 0) { + return dest.writeStr("none"); + } + + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + if (dest.minify) { + var base = ArrayList(u8){}; + const base_writer = base.writer(dest.allocator); + const WW = @TypeOf(base_writer); + + var scratchbuf = std.ArrayList(u8).init(dest.allocator); + defer scratchbuf.deinit(); + var p = Printer(WW).new( + dest.allocator, + scratchbuf, + base_writer, + css.PrinterOptions.defaultWithMinify(true), + dest.import_records, + ); + defer p.deinit(); + + try this.toCssBase(WW, &p); + + return dest.writeStr(base.items); + } + } + + fn toCssBase(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + for (this.v.items) |*item| { + try item.toCss(W, dest); + } } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { @@ -145,14 +191,583 @@ pub const Transform = union(enum) { matrix_3d: Matrix3d(f32), pub fn parse(input: *css.Parser) Result(Transform) { - _ = input; // autofix - @panic(css.todo_stuff.depth); + const function = switch (input.expectFunction()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + const Closure = struct { function: []const u8 }; + return input.parseNestedBlock( + Transform, + Closure{ .function = function }, + struct { + fn parse(closure: Closure, i: *css.Parser) css.Result(Transform) { + const location = i.currentSourceLocation(); + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "matrix")) { + const a = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const b = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const c = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const d = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const e = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |ee| return .{ .err = ee }; + const f = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |ee| return .{ .err = ee }, + }; + return .{ .result = .{ .matrix = .{ .a = a, .b = b, .c = c, .d = d, .e = e, .f = f } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "matrix3d")) { + const m11 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m12 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m13 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m14 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m21 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m22 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m23 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m24 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m31 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m32 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m33 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m34 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m41 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m42 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m43 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m44 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .matrix_3d = .{ + .m11 = m11, + .m12 = m12, + .m13 = m13, + .m14 = m14, + .m21 = m21, + .m22 = m22, + .m23 = m23, + .m24 = m24, + .m31 = m31, + .m32 = m32, + .m33 = m33, + .m34 = m34, + .m41 = m41, + .m42 = m42, + .m43 = m43, + .m44 = m44, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translate")) { + const x = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.tryParse(struct { + fn parse(p: *css.Parser) css.Result(void) { + return p.expectComma(); + } + }.parse, .{}).isOk()) { + const y = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate = .{ .x = x, .y = y } } }; + } else { + return .{ .result = .{ .translate = .{ .x = x, .y = LengthPercentage.zero() } } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translatex")) { + const x = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_x = x } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translatey")) { + const y = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_y = y } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translatez")) { + const z = switch (Length.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_z = z } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translate3d")) { + const x = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const z = switch (Length.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_3d = .{ .x = x, .y = y, .z = z } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scale")) { + const x = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.tryParse(struct { + fn parse(p: *css.Parser) css.Result(void) { + return p.expectComma(); + } + }.parse, .{}).isOk()) { + const y = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale = .{ .x = x, .y = y } } }; + } else { + return .{ .result = .{ .scale = .{ .x = x, .y = x.deepClone(i.allocator()) } } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scalex")) { + const x = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_x = x } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scaley")) { + const y = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_y = y } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scalez")) { + const z = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_z = z } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scale3d")) { + const x = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const z = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_3d = .{ .x = x, .y = y, .z = z } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotate")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotatex")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_x = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotatey")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_y = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotatez")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_z = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotate3d")) { + const x = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const z = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_3d = .{ .x = x, .y = y, .z = z, .angle = angle } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "skew")) { + const x = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.tryParse(struct { + fn parse(p: *css.Parser) css.Result(void) { + return p.expectComma(); + } + }.parse, .{}).isOk()) { + const y = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .skew = .{ .x = x, .y = y } } }; + } else { + return .{ .result = .{ .skew = .{ .x = x, .y = Angle{ .deg = 0.0 } } } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "skewx")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .skew_x = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "skewy")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .skew_y = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "perspective")) { + const len = switch (Length.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .perspective = len } }; + } else { + return .{ .err = location.newUnexpectedTokenError(.{ .ident = closure.function }) }; + } + } + }.parse, + ); } pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + switch (this.*) { + .translate => |t| { + if (dest.minify and t.x.isZero() and !t.y.isZero()) { + try dest.writeStr("translateY("); + try t.y.toCss(W, dest); + } else { + try dest.writeStr("translate("); + try t.x.toCss(W, dest); + if (!t.y.isZero()) { + try dest.delim(',', false); + try t.y.toCss(W, dest); + } + } + try dest.writeChar(')'); + }, + .translate_x => |x| { + try dest.writeStr(if (dest.minify) "translate(" else "translateX("); + try x.toCss(W, dest); + try dest.writeChar(')'); + }, + .translate_y => |y| { + try dest.writeStr("translateY("); + try y.toCss(W, dest); + try dest.writeChar(')'); + }, + .translate_z => |z| { + try dest.writeStr("translateZ("); + try z.toCss(W, dest); + try dest.writeChar(')'); + }, + .translate_3d => |t| { + if (dest.minify and !t.x.isZero() and t.y.isZero() and t.z.isZero()) { + try dest.writeStr("translate("); + try t.x.toCss(W, dest); + } else if (dest.minify and t.x.isZero() and !t.y.isZero() and t.z.isZero()) { + try dest.writeStr("translateY("); + try t.y.toCss(W, dest); + } else if (dest.minify and t.x.isZero() and t.y.isZero() and !t.z.isZero()) { + try dest.writeStr("translateZ("); + try t.z.toCss(W, dest); + } else if (dest.minify and t.z.isZero()) { + try dest.writeStr("translate("); + try t.x.toCss(W, dest); + try dest.delim(',', false); + try t.y.toCss(W, dest); + } else { + try dest.writeStr("translate3d("); + try t.x.toCss(W, dest); + try dest.delim(',', false); + try t.y.toCss(W, dest); + try dest.delim(',', false); + try t.z.toCss(W, dest); + } + try dest.writeChar(')'); + }, + .scale => |s| { + const x: f32 = s.x.intoF32(); + const y: f32 = s.y.intoF32(); + if (dest.minify and x == 1.0 and y != 1.0) { + try dest.writeStr("scaleY("); + try css.CSSNumberFns.toCss(&y, W, dest); + } else if (dest.minify and x != 1.0 and y == 1.0) { + try dest.writeStr("scaleX("); + try css.CSSNumberFns.toCss(&x, W, dest); + } else { + try dest.writeStr("scale("); + try css.CSSNumberFns.toCss(&x, W, dest); + if (y != x) { + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&y, W, dest); + } + } + try dest.writeChar(')'); + }, + .scale_x => |x| { + try dest.writeStr("scaleX("); + try css.CSSNumberFns.toCss(&x.intoF32(), W, dest); + try dest.writeChar(')'); + }, + .scale_y => |y| { + try dest.writeStr("scaleY("); + try css.CSSNumberFns.toCss(&y.intoF32(), W, dest); + try dest.writeChar(')'); + }, + .scale_z => |z| { + try dest.writeStr("scaleZ("); + try css.CSSNumberFns.toCss(&z.intoF32(), W, dest); + try dest.writeChar(')'); + }, + .scale_3d => |s| { + const x: f32 = s.x.intoF32(); + const y: f32 = s.y.intoF32(); + const z: f32 = s.z.intoF32(); + if (dest.minify and z == 1.0 and x == y) { + try dest.writeStr("scale("); + try css.CSSNumberFns.toCss(&x, W, dest); + } else if (dest.minify and x != 1.0 and y == 1.0 and z == 1.0) { + try dest.writeStr("scaleX("); + try css.CSSNumberFns.toCss(&x, W, dest); + } else if (dest.minify and x == 1.0 and y != 1.0 and z == 1.0) { + try dest.writeStr("scaleY("); + try css.CSSNumberFns.toCss(&y, W, dest); + } else if (dest.minify and x == 1.0 and y == 1.0 and z != 1.0) { + try dest.writeStr("scaleZ("); + try css.CSSNumberFns.toCss(&z, W, dest); + } else if (dest.minify and z == 1.0) { + try dest.writeStr("scale("); + try css.CSSNumberFns.toCss(&x, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&y, W, dest); + } else { + try dest.writeStr("scale3d("); + try css.CSSNumberFns.toCss(&x, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&y, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&z, W, dest); + } + try dest.writeChar(')'); + }, + .rotate => |angle| { + try dest.writeStr("rotate("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_x => |angle| { + try dest.writeStr("rotateX("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_y => |angle| { + try dest.writeStr("rotateY("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_z => |angle| { + try dest.writeStr(if (dest.minify) "rotate(" else "rotateZ("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_3d => |r| { + if (dest.minify and r.x == 1.0 and r.y == 0.0 and r.z == 0.0) { + try dest.writeStr("rotateX("); + try r.angle.toCssWithUnitlessZero(W, dest); + } else if (dest.minify and r.x == 0.0 and r.y == 1.0 and r.z == 0.0) { + try dest.writeStr("rotateY("); + try r.angle.toCssWithUnitlessZero(W, dest); + } else if (dest.minify and r.x == 0.0 and r.y == 0.0 and r.z == 1.0) { + try dest.writeStr("rotate("); + try r.angle.toCssWithUnitlessZero(W, dest); + } else { + try dest.writeStr("rotate3d("); + try css.CSSNumberFns.toCss(&r.x, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&r.y, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&r.z, W, dest); + try dest.delim(',', false); + try r.angle.toCssWithUnitlessZero(W, dest); + } + try dest.writeChar(')'); + }, + .skew => |s| { + if (dest.minify and s.x.isZero() and !s.y.isZero()) { + try dest.writeStr("skewY("); + try s.y.toCssWithUnitlessZero(W, dest); + } else { + try dest.writeStr("skew("); + try s.x.toCss(W, dest); + if (!s.y.isZero()) { + try dest.delim(',', false); + try s.y.toCssWithUnitlessZero(W, dest); + } + } + try dest.writeChar(')'); + }, + .skew_x => |angle| { + try dest.writeStr(if (dest.minify) "skew(" else "skewX("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .skew_y => |angle| { + try dest.writeStr("skewY("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .perspective => |len| { + try dest.writeStr("perspective("); + try len.toCss(W, dest); + try dest.writeChar(')'); + }, + .matrix => |m| { + try dest.writeStr("matrix("); + try css.CSSNumberFns.toCss(&m.a, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.b, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.c, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.d, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.e, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.f, W, dest); + try dest.writeChar(')'); + }, + .matrix_3d => |m| { + try dest.writeStr("matrix3d("); + try css.CSSNumberFns.toCss(&m.m11, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m12, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m13, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m14, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m21, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m22, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m23, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m24, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m31, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m32, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m33, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m34, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m41, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m42, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m43, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m44, W, dest); + try dest.writeChar(')'); + }, + } } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { diff --git a/src/css/rules/keyframes.zig b/src/css/rules/keyframes.zig index 640683d41a..d79eca6a7a 100644 --- a/src/css/rules/keyframes.zig +++ b/src/css/rules/keyframes.zig @@ -306,7 +306,7 @@ pub const KeyframesRule = struct { pub fn getFallbacks(this: *This, comptime T: type, targets: *const css.targets.Targets) []css.CssRule(T) { _ = this; // autofix _ = targets; // autofix - @panic(css.todo_stuff.depth); + @compileError(css.todo_stuff.depth); } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This { diff --git a/src/css/rules/property.zig b/src/css/rules/property.zig index b3044d1836..8ad0892f10 100644 --- a/src/css/rules/property.zig +++ b/src/css/rules/property.zig @@ -123,7 +123,7 @@ pub const PropertyRule = struct { dest.dedent(); try dest.newline(); - try dest.writeChar(';'); + try dest.writeChar('}'); } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This { diff --git a/src/css/rules/rules.zig b/src/css/rules/rules.zig index a66ebd6cd2..951a302c92 100644 --- a/src/css/rules/rules.zig +++ b/src/css/rules/rules.zig @@ -290,8 +290,6 @@ pub fn CssRuleList(comptime AtRule: type) type { // Attempt to merge the new rule with the last rule we added. var merged = false; - const ZACK_REMOVE_THIS = false; - _ = ZACK_REMOVE_THIS; // autofix if (rules.items.len > 0 and rules.items[rules.items.len - 1] == .style) { const last_style_rule = &rules.items[rules.items.len - 1].style; if (mergeStyleRules(AtRule, sty, last_style_rule, context)) { diff --git a/src/css/rules/supports.zig b/src/css/rules/supports.zig index a07a78494e..126720002b 100644 --- a/src/css/rules/supports.zig +++ b/src/css/rules/supports.zig @@ -59,6 +59,21 @@ pub const SupportsCondition = union(enum) { /// An unknown condition. unknown: []const u8, + pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { + switch (this.*) { + .not => |not| { + not.deinit(allocator); + allocator.destroy(not); + }, + inline .@"and", .@"or" => |*list| { + css.deepDeinit(SupportsCondition, allocator, list); + }, + .declaration => {}, + .selector => {}, + .unknown => {}, + } + } + pub fn eql(this: *const SupportsCondition, other: *const SupportsCondition) bool { return css.implementEql(SupportsCondition, this, other); } diff --git a/src/css/selectors/parser.zig b/src/css/selectors/parser.zig index a89dd6345a..bcd01753cc 100644 --- a/src/css/selectors/parser.zig +++ b/src/css/selectors/parser.zig @@ -927,7 +927,7 @@ pub const PseudoClass = union(enum) { const writer = s.writer(dest.allocator); const W2 = @TypeOf(writer); const scratchbuf = std.ArrayList(u8).init(dest.allocator); - var printer = Printer(W2).new(dest.allocator, scratchbuf, writer, css.PrinterOptions{}, dest.import_records); + var printer = Printer(W2).new(dest.allocator, scratchbuf, writer, css.PrinterOptions.default(), dest.import_records); try serialize.serializePseudoClass(this, W2, &printer, null); return dest.writeStr(s.items); } @@ -1050,9 +1050,7 @@ pub const SelectorParser = struct { pub fn parseFunctionalPseudoElement(this: *SelectorParser, name: []const u8, input: *css.Parser) Result(Impl.SelectorImpl.PseudoElement) { _ = this; // autofix - _ = name; // autofix - _ = input; // autofix - @panic(css.todo_stuff.depth); + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unsupported_pseudo_class_or_element = name })) }; } fn parseIsAndWhere(this: *const SelectorParser) bool { @@ -1061,10 +1059,13 @@ pub const SelectorParser = struct { } /// Whether the given function name is an alias for the `:is()` function. - fn parseAnyPrefix(this: *const SelectorParser, name: []const u8) ?css.VendorPrefix { - _ = this; // autofix - _ = name; // autofix - return null; + fn parseAnyPrefix(_: *const SelectorParser, name: []const u8) ?css.VendorPrefix { + const Map = comptime bun.ComptimeStringMap(css.VendorPrefix, .{ + .{ "-webkit-any", css.VendorPrefix{ .webkit = true } }, + .{ "-moz-any", css.VendorPrefix{ .moz = true } }, + }); + + return Map.getAnyCase(name); } pub fn parseNonTsPseudoClass( @@ -1289,6 +1290,10 @@ pub const SelectorParser = struct { return .{ .result = pseudo_class }; } + pub fn parseHost(_: *SelectorParser) bool { + return true; + } + pub fn parseNonTsFunctionalPseudoClass( this: *SelectorParser, name: []const u8, @@ -1691,7 +1696,7 @@ pub fn GenericSelector(comptime Impl: type) type { var arraylist = ArrayList(u8){}; const w = arraylist.writer(bun.default_allocator); defer arraylist.deinit(bun.default_allocator); - var printer = css.Printer(@TypeOf(w)).new(bun.default_allocator, std.ArrayList(u8).init(bun.default_allocator), w, .{}, null); + var printer = css.Printer(@TypeOf(w)).new(bun.default_allocator, std.ArrayList(u8).init(bun.default_allocator), w, css.PrinterOptions.default(), null); defer printer.deinit(); css.selector.tocss_servo.toCss_Selector(this.this, @TypeOf(w), &printer) catch |e| return try writer.print("\n", .{@errorName(e)}); try writer.writeAll(arraylist.items); @@ -2546,7 +2551,7 @@ pub const PseudoElement = union(enum) { const writer = s.writer(dest.allocator); const W2 = @TypeOf(writer); const scratchbuf = std.ArrayList(u8).init(dest.allocator); - var printer = Printer(W2).new(dest.allocator, scratchbuf, writer, css.PrinterOptions{}, dest.import_records); + var printer = Printer(W2).new(dest.allocator, scratchbuf, writer, css.PrinterOptions.default(), dest.import_records); try serialize.serializePseudoElement(this, W2, &printer, null); return dest.writeStr(s.items); } @@ -2691,6 +2696,7 @@ pub fn parse_one_simple_selector( const S = SimpleSelectorParseResult(Impl); const start = input.state(); + const token_location = input.currentSourceLocation(); const token = switch (input.nextIncludingWhitespace()) { .result => |v| v.*, .err => { @@ -2702,7 +2708,7 @@ pub fn parse_one_simple_selector( switch (token) { .idhash => |id| { if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { - return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + return .{ .err = token_location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unexpected_selector_after_pseudo_element = .{ .idhash = id } })) }; } const component: GenericComponent(Impl) = .{ .id = .{ .v = id } }; return .{ .result = S{ @@ -2711,18 +2717,20 @@ pub fn parse_one_simple_selector( }, .open_square => { if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { - return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + return .{ .err = token_location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unexpected_selector_after_pseudo_element = .open_square })) }; } const Closure = struct { parser: *SelectorParser, - pub fn parsefn(this: *@This(), input2: *css.Parser) Result(GenericComponent(Impl)) { - return parse_attribute_selector(Impl, this.parser, input2); - } }; var closure = Closure{ .parser = parser, }; - const attr = switch (input.parseNestedBlock(GenericComponent(Impl), &closure, Closure.parsefn)) { + const attr = switch (input.parseNestedBlock(GenericComponent(Impl), &closure, struct { + pub fn parsefn(this: *Closure, input2: *css.Parser) Result(GenericComponent(Impl)) { + return parse_attribute_selector(Impl, this.parser, input2); + } + } + .parsefn)) { .err => |e| return .{ .err = e }, .result => |v| v, }; @@ -2880,7 +2888,7 @@ pub fn parse_one_simple_selector( switch (d) { '.' => { if (state.intersects(SelectorParsingState.AFTER_PSEUDO)) { - return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.invalid_state)) }; + return .{ .err = token_location.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unexpected_selector_after_pseudo_element = .{ .delim = '.' } })) }; } const location = input.currentSourceLocation(); const class = switch ((switch (input.nextIncludingWhitespace()) { @@ -3169,6 +3177,9 @@ pub fn parse_functional_pseudo_class( return .{ .result = .{ .non_ts_pseudo_class = result } }; } +const TreeStructuralPseudoClass = enum { @"first-child", @"last-child", @"only-child", root, empty, scope, host, @"first-of-type", @"last-of-type", @"only-of-type" }; +const TreeStructuralPseudoClassMap = bun.ComptimeEnumMap(TreeStructuralPseudoClass); + pub fn parse_simple_pseudo_class( comptime Impl: type, parser: *SelectorParser, @@ -3181,28 +3192,20 @@ pub fn parse_simple_pseudo_class( } if (state.allowsTreeStructuralPseudoClasses()) { - // css.todo_stuff.match_ignore_ascii_case - if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-child")) { - return .{ .result = .{ .nth = NthSelectorData.first(false) } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "last-child")) { - return .{ .result = .{ .nth = NthSelectorData.last(false) } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-child")) { - return .{ .result = .{ .nth = NthSelectorData.only(false) } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "root")) { - return .{ .result = .root }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "empty")) { - return .{ .result = .empty }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "scope")) { - return .{ .result = .scope }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "host")) { - return .{ .result = .{ .host = null } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "first-of-type")) { - return .{ .result = .{ .nth = NthSelectorData.first(true) } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "last-of-type")) { - return .{ .result = .{ .nth = NthSelectorData.last(true) } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "only-of-type")) { - return .{ .result = .{ .nth = NthSelectorData.only(true) } }; - } else {} + if (TreeStructuralPseudoClassMap.getAnyCase(name)) |pseudo_class| { + switch (pseudo_class) { + .@"first-child" => return .{ .result = .{ .nth = NthSelectorData.first(false) } }, + .@"last-child" => return .{ .result = .{ .nth = NthSelectorData.last(false) } }, + .@"only-child" => return .{ .result = .{ .nth = NthSelectorData.only(false) } }, + .root => return .{ .result = .root }, + .empty => return .{ .result = .empty }, + .scope => return .{ .result = .scope }, + .host => if (parser.parseHost()) return .{ .result = .{ .host = null } }, + .@"first-of-type" => return .{ .result = .{ .nth = NthSelectorData.first(true) } }, + .@"last-of-type" => return .{ .result = .{ .nth = NthSelectorData.last(true) } }, + .@"only-of-type" => return .{ .result = .{ .nth = NthSelectorData.only(true) } }, + } + } } // The view-transition pseudo elements accept the :only-child pseudo class. diff --git a/src/css/selectors/selector.zig b/src/css/selectors/selector.zig index 84364080ae..6388630930 100644 --- a/src/css/selectors/selector.zig +++ b/src/css/selectors/selector.zig @@ -708,7 +708,7 @@ pub const serialize = struct { const writer = id.writer(); css.serializer.serializeIdentifier(v.value, writer) catch return dest.addFmtError(); - const s = try css.to_css.string(dest.allocator, CSSString, &v.value, css.PrinterOptions{}, dest.import_records); + const s = try css.to_css.string(dest.allocator, CSSString, &v.value, css.PrinterOptions.default(), dest.import_records); if (id.items.len > 0 and id.items.len < s.len) { try dest.writeStr(id.items); @@ -748,7 +748,7 @@ pub const serialize = struct { try dest.writeStr(":not("); }, .any => |v| { - const vp = dest.vendor_prefix.bitwiseOr(v.vendor_prefix); + const vp = dest.vendor_prefix._or(v.vendor_prefix); if (vp.intersects(css.VendorPrefix{ .webkit = true, .moz = true })) { try dest.writeChar(':'); try vp.toCss(W, dest); @@ -1039,6 +1039,7 @@ pub const serialize = struct { // If the printer has a vendor prefix override, use that. const vp = if (!d.vendor_prefix.isEmpty()) d.vendor_prefix.bitwiseAnd(prefix).orNone() else prefix; try vp.toCss(W, d); + debug("VENDOR PREFIX {d} OVERRIDE {d}", .{ vp.asBits(), d.vendor_prefix.asBits() }); return vp; } diff --git a/src/css/small_list.zig b/src/css/small_list.zig index fbc86efdd0..bb48ca8593 100644 --- a/src/css/small_list.zig +++ b/src/css/small_list.zig @@ -87,6 +87,11 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { return ret; } + pub inline fn getLastUnchecked(this: *const @This()) T { + if (this.spilled()) return this.data.heap.ptr[this.data.heap.len - 1]; + return this.data.inlined[this.capacity - 1]; + } + pub inline fn at(this: *const @This(), idx: u32) *const T { return &this.as_const_ptr()[idx]; } @@ -173,6 +178,7 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { pub inline fn helper(comptime prefix: []const u8, pfs: *css.VendorPrefix, pfi: *const SmallList(T, 1), r: *bun.BabyList(This), alloc: Allocator) void { if (pfs.contains(css.VendorPrefix.fromName(prefix))) { var images = SmallList(T, 1).initCapacity(alloc, pfi.len()); + images.setLen(pfi.len()); for (images.slice_mut(), pfi.slice()) |*out, *in| { const image = in.getImage().getPrefixed(alloc, css.VendorPrefix.fromName(prefix)); out.* = in.withImage(alloc, image); @@ -431,6 +437,14 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { len_ptr.* += 1; } + pub fn pop(this: *@This()) ?T { + const ptr, const len_ptr, _ = this.tripleMut(); + if (len_ptr.* == 0) return null; + const last_index = len_ptr.* - 1; + len_ptr.* = last_index; + return ptr[last_index]; + } + pub fn append(this: *@This(), allocator: Allocator, item: T) void { var ptr, var len_ptr, const capp = this.tripleMut(); if (len_ptr.* == capp) { diff --git a/src/css/targets.zig b/src/css/targets.zig index ab720f8304..26f744bc39 100644 --- a/src/css/targets.zig +++ b/src/css/targets.zig @@ -17,6 +17,27 @@ pub const Targets = struct { /// Features that should never be compiled, even when unsupported by targets. exclude: Features = .{}, + /// Set a sane default for bundler + pub fn browserDefault() Targets { + return .{ + .browsers = Browsers.browserDefault, + }; + } + + /// Set a sane default for bundler + pub fn runtimeDefault() Targets { + return .{ + .browsers = null, + }; + } + + pub fn forBundlerTarget(target: bun.transpiler.options.Target) Targets { + return switch (target) { + .node, .bun => runtimeDefault(), + .browser, .bun_macro, .bake_server_components_ssr => browserDefault(), + }; + } + pub fn prefixes(this: *const Targets, prefix: css.VendorPrefix, feature: css.prefixes.Feature) css.VendorPrefix { if (prefix.contains(css.VendorPrefix{ .none = true }) and !this.exclude.contains(css.targets.Features{ .vendor_prefixes = true })) { if (this.include.contains(css.targets.Features{ .vendor_prefixes = true })) { @@ -37,11 +58,10 @@ pub const Targets = struct { return this.include.contains(flag) or (!this.exclude.contains(flag) and !this.isCompatible(feature)); } - pub fn shouldCompileSame(this: *const Targets, comptime prop: @Type(.EnumLiteral)) bool { - const compat_feature: css.compat.Feature = prop; + pub fn shouldCompileSame(this: *const Targets, comptime compat_feature: css.compat.Feature) bool { const target_feature: css.targets.Features = target_feature: { var feature: css.targets.Features = .{}; - @field(feature, @tagName(prop)) = true; + @field(feature, @tagName(compat_feature)) = true; break :target_feature feature; }; @@ -125,8 +145,155 @@ pub const Features = packed struct(u32) { }; pub fn BrowsersImpl(comptime T: type) type { - _ = T; // autofix - return struct {}; + return struct { + pub const browserDefault = convertFromString(&.{ + "es2020", // support import.meta.url + "edge88", + "firefox78", + "chrome87", + "safari14", + }) catch |e| std.debug.panic("WOOPSIE: {s}\n", .{@errorName(e)}); + + // pub const bundlerDefault = T{ + // .chrome = 80 << 16, + // .edge = 80 << 16, + // .firefox = 78 << 16, + // .safari = 14 << 16, + // .opera = 67 << 16, + // }; + + pub fn convertFromString(esbuild_target: []const []const u8) anyerror!T { + var browsers: T = .{}; + + for (esbuild_target) |str| { + var entries_buf: [5][]const u8 = undefined; + const entries_without_es: [][]const u8 = entries_without_es: { + if (str.len <= 2 or !(str[0] == 'e' and str[1] == 's')) { + entries_buf[0] = str; + break :entries_without_es entries_buf[0..1]; + } + + const number_part = str[2..]; + const year = try std.fmt.parseInt(u16, number_part, 10); + switch (year) { + // https://caniuse.com/?search=es2015 + 2015 => { + entries_buf[0..5].* = .{ "chrome49", "edge13", "safari10", "firefox44", "opera36" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2016 + 2016 => { + entries_buf[0..5].* = .{ "chrome50", "edge13", "safari10", "firefox43", "opera37" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2017 + 2017 => { + entries_buf[0..5].* = .{ "chrome58", "edge15", "safari11", "firefox52", "opera45" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2018 + 2018 => { + entries_buf[0..5].* = .{ "chrome63", "edge79", "safari12", "firefox58", "opera50" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2019 + 2019 => { + entries_buf[0..5].* = .{ "chrome73", "edge79", "safari12.1", "firefox64", "opera60" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2020 + 2020 => { + entries_buf[0..5].* = .{ "chrome80", "edge80", "safari14.1", "firefox80", "opera67" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2021 + 2021 => { + entries_buf[0..5].* = .{ "chrome85", "edge85", "safari14.1", "firefox80", "opera71" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2022 + 2022 => { + entries_buf[0..5].* = .{ "chrome94", "edge94", "safari16.4", "firefox93", "opera80" }; + break :entries_without_es entries_buf[0..5]; + }, + // https://caniuse.com/?search=es2023 + 2023 => { + entries_buf[0..4].* = .{ "chrome110", "edge110", "safari16.4", "opera96" }; + break :entries_without_es entries_buf[0..4]; + }, + else => { + if (@inComptime()) { + @compileLog("Invalid target: " ++ str); + } + return error.UnsupportedCSSTarget; + }, + } + }; + + for_loop: for (entries_without_es) |entry| { + if (bun.strings.eql(entry, "esnext")) continue; + const maybe_idx: ?usize = maybe_idx: { + for (entry, 0..) |c, i| { + if (std.ascii.isDigit(c)) break :maybe_idx i; + } + break :maybe_idx null; + }; + + if (maybe_idx) |idx| { + const Browser = enum { + chrome, + edge, + firefox, + ie, + ios_saf, + opera, + safari, + no_mapping, + }; + const Map = bun.ComptimeStringMap(Browser, .{ + .{ "chrome", Browser.chrome }, + .{ "edge", Browser.edge }, + .{ "firefox", Browser.firefox }, + .{ "hermes", Browser.no_mapping }, + .{ "ie", Browser.ie }, + .{ "ios", Browser.ios_saf }, + .{ "node", Browser.no_mapping }, + .{ "opera", Browser.opera }, + .{ "rhino", Browser.no_mapping }, + .{ "safari", Browser.safari }, + }); + const browser = Map.get(entry[0..idx]); + if (browser == null or browser.? == .no_mapping) continue; // No mapping available + + const major, const minor = major_minor: { + const version_str = entry[idx..]; + const dot_index = std.mem.indexOfScalar(u8, version_str, '.') orelse version_str.len; + const major = std.fmt.parseInt(u16, version_str[0..dot_index], 10) catch continue; + const minor = if (dot_index < version_str.len) + std.fmt.parseInt(u16, version_str[dot_index + 1 ..], 10) catch 0 + else + 0; + break :major_minor .{ major, minor }; + }; + + const version: u32 = (@as(u32, major) << 16) | @as(u32, minor << 8); + switch (browser.?) { + inline else => |browser_name| { + if (@field(browsers, @tagName(browser_name)) == null or + version < @field(browsers, @tagName(browser_name)).?) + { + @field(browsers, @tagName(browser_name)) = version; + } + continue :for_loop; + }, + } + } + } + } + + return browsers; + } + }; } pub fn FeaturesImpl(comptime T: type) type { diff --git a/src/css/values/color_js.zig b/src/css/values/color_js.zig index 052d92ed70..d1dd0f6abc 100644 --- a/src/css/values/color_js.zig +++ b/src/css/values/color_js.zig @@ -426,7 +426,7 @@ pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFram allocator, std.ArrayList(u8).init(allocator), writer, - .{}, + css.PrinterOptions.default(), null, ); diff --git a/src/css/values/easing.zig b/src/css/values/easing.zig index 4d7c7cd00c..b5e5ea51c2 100644 --- a/src/css/values/easing.zig +++ b/src/css/values/easing.zig @@ -228,7 +228,7 @@ pub const StepPosition = enum { pub fn toCss(this: *const StepPosition, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { _ = this; // autofix _ = dest; // autofix - @panic(css.todo_stuff.depth); + @compileError(css.todo_stuff.depth); } pub fn parse(input: *css.Parser) Result(StepPosition) { diff --git a/src/css/values/gradient.zig b/src/css/values/gradient.zig index 292db6ec88..0910a65a78 100644 --- a/src/css/values/gradient.zig +++ b/src/css/values/gradient.zig @@ -49,105 +49,146 @@ pub const Gradient = union(enum) { closure: Closure, input_: *css.Parser, ) Result(Gradient) { - // css.todo_stuff.match_ignore_ascii_case - if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "conic-gradient")) { - return .{ .result = .{ .conic = switch (ConicGradient.parse(input_)) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-conic-gradient")) { - return .{ .result = .{ .repeating_conic = switch (ConicGradient.parse(input_)) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-gradient")) { - return .{ .result = .{ .@"webkit-gradient" = switch (WebKitGradient.parse(input_)) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else { + const Map = comptime bun.ComptimeEnumMap(enum { + @"linear-gradient", + @"repeating-linear-gradient", + @"radial-gradient", + @"repeating-radial-gradient", + @"conic-gradient", + @"repeating-conic-gradient", + @"-webkit-linear-gradient", + @"-webkit-repeating-linear-gradient", + @"-webkit-radial-gradient", + @"-webkit-repeating-radial-gradient", + @"-moz-linear-gradient", + @"-moz-repeating-linear-gradient", + @"-moz-radial-gradient", + @"-moz-repeating-radial-gradient", + @"-o-linear-gradient", + @"-o-repeating-linear-gradient", + @"-o-radial-gradient", + @"-o-repeating-radial-gradient", + @"-webkit-gradient", + }); + if (Map.getAnyCase(closure.func)) |matched| + switch (matched) { + .@"linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"conic-gradient" => { + return .{ .result = .{ .conic = switch (ConicGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"repeating-conic-gradient" => { + return .{ .result = .{ .repeating_conic = switch (ConicGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-gradient" => { + return .{ .result = .{ .@"webkit-gradient" = switch (WebKitGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + } + else return .{ .err = closure.location.newUnexpectedTokenError(.{ .ident = closure.func }) }; - } } }.parse); } @@ -157,7 +198,7 @@ pub const Gradient = union(enum) { .linear => |g| .{ "linear-gradient(", g.vendor_prefix }, .repeating_linear => |g| .{ "repeating-linear-gradient(", g.vendor_prefix }, .radial => |g| .{ "radial-gradient(", g.vendor_prefix }, - .repeating_radial => |g| .{ "repeating-linear-gradient(", g.vendor_prefix }, + .repeating_radial => |g| .{ "repeating-radial-gradient(", g.vendor_prefix }, .conic => .{ "conic-gradient(", null }, .repeating_conic => .{ "repeating-conic-gradient(", null }, .@"webkit-gradient" => .{ "-webkit-gradient(", null }, @@ -171,7 +212,7 @@ pub const Gradient = union(enum) { switch (this.*) { .linear, .repeating_linear => |*linear| { - try linear.toCss(W, dest, linear.vendor_prefix.eq(css.VendorPrefix{ .none = true })); + try linear.toCss(W, dest, linear.vendor_prefix.neq(css.VendorPrefix{ .none = true })); }, .radial, .repeating_radial => |*radial| { try radial.toCss(W, dest); @@ -475,7 +516,7 @@ pub const RadialGradient = struct { } pub fn toCss(this: *const RadialGradient, comptime W: type, dest: *Printer(W)) PrintErr!void { - if (std.meta.eql(this.shape, EndingShape.default())) { + if (!std.meta.eql(this.shape, EndingShape.default())) { try this.shape.toCss(W, dest); if (this.position.isCenter()) { try dest.delim(',', false); @@ -544,16 +585,16 @@ pub const ConicGradient = struct { // https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle return Angle.parseWithUnitlessZero(i); } - }.parse, .{}).unwrapOr(Angle{ .deg = 0.0 }); + }.parse, .{}); const position = input.tryParse(struct { inline fn parse(i: *css.Parser) Result(Position) { if (i.expectIdentMatching("at").asErr()) |e| return .{ .err = e }; return Position.parse(i); } - }.parse, .{}).unwrapOr(Position.center()); + }.parse, .{}); - if (!angle.eql(&Angle{ .deg = 0.0 }) or !std.meta.eql(position, Position.center())) { + if (angle.isOk() or position.isOk()) { if (input.expectComma().asErr()) |e| return .{ .err = e }; } @@ -562,8 +603,8 @@ pub const ConicGradient = struct { .err => |e| return .{ .err = e }, }; return .{ .result = ConicGradient{ - .angle = angle, - .position = position, + .angle = angle.unwrapOr(Angle{ .deg = 0.0 }), + .position = position.unwrapOr(Position.center()), .items = items, } }; } @@ -1346,7 +1387,7 @@ pub fn ColorStop(comptime D: type) type { pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { try this.color.toCss(W, dest); if (this.position) |*position| { - try dest.delim(',', false); + try dest.writeChar(' '); try css.generic.toCss(D, position, W, dest); } return; diff --git a/src/css/values/ident.zig b/src/css/values/ident.zig index ee861540c9..9dab1de5be 100644 --- a/src/css/values/ident.zig +++ b/src/css/values/ident.zig @@ -49,7 +49,7 @@ pub const DashedIdentReference = struct { pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { if (dest.css_module) |*css_module| { if (css_module.config.dashed_idents) { - if (css_module.referenceDashed(this.ident.v, &this.from, dest.loc.source_index)) |name| { + if (css_module.referenceDashed(dest.allocator, this.ident.v, &this.from, dest.loc.source_index)) |name| { try dest.writeStr("--"); css.serializer.serializeName(name, dest) catch return dest.addFmtError(); return; diff --git a/src/css/values/percentage.zig b/src/css/values/percentage.zig index bdafae93e6..081206f7d0 100644 --- a/src/css/values/percentage.zig +++ b/src/css/values/percentage.zig @@ -479,6 +479,10 @@ pub const NumberOrPercentage = union(enum) { // @panic(css.todo_stuff.depth); // } + pub fn deepClone(this: *const NumberOrPercentage, allocator: std.mem.Allocator) NumberOrPercentage { + return css.implementDeepClone(@This(), this, allocator); + } + pub fn eql(this: *const NumberOrPercentage, other: *const NumberOrPercentage) bool { return switch (this.*) { .number => |*a| switch (other.*) { diff --git a/src/css/values/syntax.zig b/src/css/values/syntax.zig index 5f8a743367..ee46b209da 100644 --- a/src/css/values/syntax.zig +++ b/src/css/values/syntax.zig @@ -88,7 +88,7 @@ pub const SyntaxString = union(enum) { // PERF(alloc): count first? while (true) { - const component = switch (SyntaxComponent.parseString(trimmed_input)) { + const component = switch (SyntaxComponent.parseString(&trimmed_input)) { .result => |v| v, .err => |e| return .{ .err = e }, }; @@ -260,8 +260,7 @@ pub const SyntaxComponent = struct { kind: SyntaxComponentKind, multiplier: Multiplier, - pub fn parseString(input_: []const u8) css.Maybe(SyntaxComponent, void) { - var input = input_; + pub fn parseString(input: *[]const u8) css.Maybe(SyntaxComponent, void) { const kind = switch (SyntaxComponentKind.parseString(input)) { .result => |vv| vv, .err => |e| return .{ .err = e }, @@ -276,11 +275,11 @@ pub const SyntaxComponent = struct { } var multiplier: Multiplier = .none; - if (bun.strings.startsWithChar(input, '+')) { - input = input[1..]; + if (bun.strings.startsWithChar(input.*, '+')) { + input.* = input.*[1..]; multiplier = .space; - } else if (bun.strings.startsWithChar(input, '#')) { - input = input[1..]; + } else if (bun.strings.startsWithChar(input.*, '#')) { + input.* = input.*[1..]; multiplier = .comma; } @@ -334,13 +333,13 @@ pub const SyntaxComponentKind = union(enum) { /// A literal component. literal: []const u8, - pub fn parseString(input_: []const u8) css.Maybe(SyntaxComponentKind, void) { + pub fn parseString(input: *[]const u8) css.Maybe(SyntaxComponentKind, void) { // https://drafts.css-houdini.org/css-properties-values-api/#consume-syntax-component - var input = std.mem.trimLeft(u8, input_, SPACE_CHARACTERS); - if (bun.strings.startsWithChar(input, '<')) { + input.* = std.mem.trimLeft(u8, input.*, SPACE_CHARACTERS); + if (bun.strings.startsWithChar(input.*, '<')) { // https://drafts.css-houdini.org/css-properties-values-api/#consume-data-type-name - const end_idx = std.mem.indexOfScalar(u8, input, '>') orelse return .{ .err = {} }; - const name = input[1..end_idx]; + const end_idx = std.mem.indexOfScalar(u8, input.*, '>') orelse return .{ .err = {} }; + const name = input.*[1..end_idx]; // todo_stuff.match_ignore_ascii_case const component: SyntaxComponentKind = if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "length")) .length @@ -373,17 +372,17 @@ pub const SyntaxComponentKind = union(enum) { else return .{ .err = {} }; - input = input[end_idx + 1 ..]; + input.* = input.*[end_idx + 1 ..]; return .{ .result = component }; - } else if (input.len > 0 and isIdentStart(input[0])) { + } else if (input.len > 0 and isIdentStart(input.*[0])) { // A literal. var end_idx: usize = 0; while (end_idx < input.len and - isNameCodePoint(input[end_idx])) : (end_idx += - bun.strings.utf8ByteSequenceLengthUnsafe(input[end_idx])) + isNameCodePoint(input.*[end_idx])) : (end_idx += + bun.strings.utf8ByteSequenceLengthUnsafe(input.*[end_idx])) {} - const literal = input[0..end_idx]; - input = input[end_idx..]; + const literal = input.*[0..end_idx]; + input.* = input.*[end_idx..]; return .{ .result = SyntaxComponentKind{ .literal = literal } }; } else { return .{ .err = {} }; diff --git a/src/darwin_c.zig b/src/darwin_c.zig index 2c0268058f..3019c177e7 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -66,6 +66,8 @@ pub const COPYFILE_CONTINUE = @as(c_int, 0); pub const COPYFILE_SKIP = @as(c_int, 1); pub const COPYFILE_QUIT = @as(c_int, 2); +pub extern "C" fn memmem(haystack: [*]const u8, haystacklen: usize, needle: [*]const u8, needlelen: usize) ?[*]const u8; + // int clonefileat(int src_dirfd, const char * src, int dst_dirfd, const char * dst, int flags); pub extern "c" fn clonefileat(c_int, [*:0]const u8, c_int, [*:0]const u8, uint32_t: c_int) c_int; // int fclonefileat(int srcfd, int dst_dirfd, const char * dst, int flags); @@ -73,6 +75,20 @@ pub extern "c" fn fclonefileat(c_int, c_int, [*:0]const u8, uint32_t: c_int) c_i // int clonefile(const char * src, const char * dst, int flags); pub extern "c" fn clonefile(src: [*:0]const u8, dest: [*:0]const u8, flags: c_int) c_int; +pub const lstat = blk: { + const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + break :blk @extern(T, .{ .name = "lstat64" }); +}; + +pub const fstat = blk: { + const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + break :blk @extern(T, .{ .name = "fstat64" }); +}; +pub const stat = blk: { + const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + break :blk @extern(T, .{ .name = "stat64" }); +}; + // pub fn stat_absolute(path: [:0]const u8) StatError!Stat { // if (builtin.os.tag == .windows) { // var io_status_block: windows.IO_STATUS_BLOCK = undefined; @@ -297,122 +313,6 @@ pub const SystemErrno = enum(u8) { if (code >= max) return null; return @enumFromInt(code); } - - pub fn label(this: SystemErrno) ?[:0]const u8 { - return labels.get(this) orelse null; - } - - const LabelMap = std.EnumMap(SystemErrno, [:0]const u8); - pub const labels: LabelMap = brk: { - var map: LabelMap = LabelMap.initFull(""); - map.put(.E2BIG, "Argument list too long"); - map.put(.EACCES, "Permission denied"); - map.put(.EADDRINUSE, "Address already in use"); - map.put(.EADDRNOTAVAIL, "Can't assign requested address"); - map.put(.EAFNOSUPPORT, "Address family not supported by protocol family"); - map.put(.EAGAIN, "non-blocking and interrupt i/o. Resource temporarily unavailable"); - map.put(.EALREADY, "Operation already in progress"); - map.put(.EAUTH, "Authentication error"); - map.put(.EBADARCH, "Bad CPU type in executable"); - map.put(.EBADEXEC, "Program loading errors. Bad executable"); - map.put(.EBADF, "Bad file descriptor"); - map.put(.EBADMACHO, "Malformed Macho file"); - map.put(.EBADMSG, "Bad message"); - map.put(.EBADRPC, "RPC struct is bad"); - map.put(.EBUSY, "Device / Resource busy"); - map.put(.ECANCELED, "Operation canceled"); - map.put(.ECHILD, "No child processes"); - map.put(.ECONNABORTED, "Software caused connection abort"); - map.put(.ECONNREFUSED, "Connection refused"); - map.put(.ECONNRESET, "Connection reset by peer"); - map.put(.EDEADLK, "Resource deadlock avoided"); - map.put(.EDESTADDRREQ, "Destination address required"); - map.put(.EDEVERR, "Device error, for example paper out"); - map.put(.EDOM, "math software. Numerical argument out of domain"); - map.put(.EDQUOT, "Disc quota exceeded"); - map.put(.EEXIST, "File or folder exists"); - map.put(.EFAULT, "Bad address"); - map.put(.EFBIG, "File too large"); - map.put(.EFTYPE, "Inappropriate file type or format"); - map.put(.EHOSTDOWN, "Host is down"); - map.put(.EHOSTUNREACH, "No route to host"); - map.put(.EIDRM, "Identifier removed"); - map.put(.EILSEQ, "Illegal byte sequence"); - map.put(.EINPROGRESS, "Operation now in progress"); - map.put(.EINTR, "Interrupted system call"); - map.put(.EINVAL, "Invalid argument"); - map.put(.EIO, "Input/output error"); - map.put(.EISCONN, "Socket is already connected"); - map.put(.EISDIR, "Is a directory"); - map.put(.ELOOP, "Too many levels of symbolic links"); - map.put(.EMFILE, "Too many open files"); - map.put(.EMLINK, "Too many links"); - map.put(.EMSGSIZE, "Message too long"); - map.put(.EMULTIHOP, "Reserved"); - map.put(.ENAMETOOLONG, "File name too long"); - map.put(.ENEEDAUTH, "Need authenticator"); - map.put(.ENETDOWN, "ipc/network software - operational errors Network is down"); - map.put(.ENETRESET, "Network dropped connection on reset"); - map.put(.ENETUNREACH, "Network is unreachable"); - map.put(.ENFILE, "Too many open files in system"); - map.put(.ENOATTR, "Attribute not found"); - map.put(.ENOBUFS, "No buffer space available"); - map.put(.ENODATA, "No message available on STREAM"); - map.put(.ENODEV, "Operation not supported by device"); - map.put(.ENOENT, "No such file or directory"); - map.put(.ENOEXEC, "Exec format error"); - map.put(.ENOLCK, "No locks available"); - map.put(.ENOLINK, "Reserved"); - map.put(.ENOMEM, "Out of memory"); - map.put(.ENOMSG, "No message of desired type"); - map.put(.ENOPOLICY, "No such policy registered"); - map.put(.ENOPROTOOPT, "Protocol not available"); - map.put(.ENOSPC, "No space left on device"); - map.put(.ENOSR, "No STREAM resources"); - map.put(.ENOSTR, "Not a STREAM"); - map.put(.ENOSYS, "Function not implemented"); - map.put(.ENOTBLK, "Block device required"); - map.put(.ENOTCONN, "Socket is not connected"); - map.put(.ENOTDIR, "Not a directory"); - map.put(.ENOTEMPTY, "Directory not empty"); - map.put(.ENOTRECOVERABLE, "State not recoverable"); - map.put(.ENOTSOCK, "ipc/network software - argument errors. Socket operation on non-socket"); - map.put(.ENOTSUP, "Operation not supported"); - map.put(.ENOTTY, "Inappropriate ioctl for device"); - map.put(.ENXIO, "Device not configured"); - map.put(.EOVERFLOW, "Value too large to be stored in data type"); - map.put(.EOWNERDEAD, "Previous owner died"); - map.put(.EPERM, "Operation not permitted"); - map.put(.EPFNOSUPPORT, "Protocol family not supported"); - map.put(.EPIPE, "Broken pipe"); - map.put(.EPROCLIM, "quotas & mush. Too many processes"); - map.put(.EPROCUNAVAIL, "Bad procedure for program"); - map.put(.EPROGMISMATCH, "Program version wrong"); - map.put(.EPROGUNAVAIL, "RPC prog. not avail"); - map.put(.EPROTO, "Protocol error"); - map.put(.EPROTONOSUPPORT, "Protocol not supported"); - map.put(.EPROTOTYPE, "Protocol wrong type for socket"); - map.put(.EPWROFF, "Intelligent device errors. Device power is off"); - map.put(.EQFULL, "Interface output queue is full"); - map.put(.ERANGE, "Result too large"); - map.put(.EREMOTE, "Too many levels of remote in path"); - map.put(.EROFS, "Read-only file system"); - map.put(.ERPCMISMATCH, "RPC version wrong"); - map.put(.ESHLIBVERS, "Shared library version mismatch"); - map.put(.ESHUTDOWN, "Can’t send after socket shutdown"); - map.put(.ESOCKTNOSUPPORT, "Socket type not supported"); - map.put(.ESPIPE, "Illegal seek"); - map.put(.ESRCH, "No such process"); - map.put(.ESTALE, "Network File System. Stale NFS file handle"); - map.put(.ETIME, "STREAM ioctl timeout"); - map.put(.ETIMEDOUT, "Operation timed out"); - map.put(.ETOOMANYREFS, "Too many references: can't splice"); - map.put(.ETXTBSY, "Text file busy"); - map.put(.EUSERS, "Too many users"); - // map.put(.EWOULDBLOCK, "Operation would block"); - map.put(.EXDEV, "Cross-device link"); - break :brk map; - }; }; pub const UV_E2BIG: i32 = @intFromEnum(SystemErrno.E2BIG); @@ -638,9 +538,6 @@ pub extern fn host_processor_info(host: std.c.host_t, flavor: processor_flavor_t pub extern fn getuid(...) std.posix.uid_t; pub extern fn getgid(...) std.posix.gid_t; -pub extern fn get_process_priority(pid: c_uint) i32; -pub extern fn set_process_priority(pid: c_uint, priority: c_int) i32; - pub fn get_version(buf: []u8) []const u8 { @memset(buf, 0); diff --git a/src/deps/boringssl.translated.zig b/src/deps/boringssl.translated.zig index ee25a0948d..60460ab810 100644 --- a/src/deps/boringssl.translated.zig +++ b/src/deps/boringssl.translated.zig @@ -30,6 +30,17 @@ const timercmp = C.timercmp; const doesnt_exist = C.doesnt_exist; const struct_tm = C.struct_tm; const enum_ssl_verify_result_t = C.enum_ssl_verify_result_t; +/// `isize` alias. Kept for clarity. +/// +/// Docs from OpenSSL: +/// > ossl_ssize_t is a signed type which is large enough to fit the size of any +/// > valid memory allocation. We prefer using |size_t|, but sometimes we need a +/// > signed type for OpenSSL API compatibility. This type can be used in such +/// > cases to avoid overflow. +/// > +/// > Not all |size_t| values fit in |ossl_ssize_t|, but all |size_t| values that +/// > are sizes of or indices into C objects, can be converted without overflow. +const ossl_ssize_t = isize; pub const CRYPTO_THREADID = c_int; pub const struct_asn1_null_st = opaque {}; @@ -143,7 +154,20 @@ pub const struct_X509_crl_st = opaque {}; pub const X509_CRL = struct_X509_crl_st; pub const struct_X509_extension_st = opaque {}; pub const X509_EXTENSION = struct_X509_extension_st; -pub const struct_x509_st = opaque {}; +pub const struct_x509_st = opaque { + pub fn dup(this: *X509) ?*X509 { + return X509_dup(this); + } + + pub fn ref(this: *X509) *X509 { + _ = X509_up_ref(this); + return this; + } + + pub fn free(this: *X509) void { + X509_free(this); + } +}; pub const X509 = struct_x509_st; pub const CRYPTO_refcount_t = u32; pub const struct_openssl_method_common_st = extern struct { @@ -1105,7 +1129,15 @@ pub extern fn BIO_hexdump(bio: [*c]BIO, data: [*c]const u8, len: usize, indent: pub extern fn ERR_print_errors(bio: [*c]BIO) void; pub extern fn BIO_read_asn1(bio: [*c]BIO, out: [*c][*c]u8, out_len: [*c]usize, max_len: usize) c_int; pub extern fn BIO_s_mem() ?*const BIO_METHOD; -// pub extern fn BIO_new_mem_buf(buf: ?*const anyopaque, len: ossl_ssize_t) [*c]BIO; + +/// BIO_new_mem_buf creates read-only BIO that reads from |len| bytes at |buf|. +/// It returns the BIO or NULL on error. This function does not copy or take +/// ownership of |buf|. The caller must ensure the memory pointed to by |buf| +/// outlives the |BIO|. +/// +/// If |len| is negative, then |buf| is treated as a NUL-terminated string, but +/// don't depend on this in new code. +pub extern fn BIO_new_mem_buf(buf: ?*const anyopaque, len: ossl_ssize_t) [*c]BIO; // pub extern fn BIO_mem_contents(bio: [*c]const BIO, out_contents: [*c][*c]const u8, out_len: [*c]usize) c_int; pub extern fn BIO_get_mem_data(bio: [*c]BIO, contents: [*c][*c]u8) c_long; pub extern fn BIO_get_mem_ptr(bio: [*c]BIO, out: [*c][*c]BUF_MEM) c_int; @@ -18778,6 +18810,22 @@ pub const struct_bio_st = extern struct { return BIO_new(BIO_s_mem()) orelse error.OutOfMemory; } + /// Create a read-only `BIO` using an existing buffer. `buffer` is not + /// copied, and ownership is not transfered. + /// + /// `buffer` must outlive the returned `BIO`. + /// + /// Returns an error if + /// - the buffer is empty + /// - BIO initialization fails (same as `.init()`). + pub fn initReadonlyView(buffer: []const u8) !*struct_bio_st { + // NOTE: not exposing len parameter. If we want to ignore their + // suggestion and pass a negative value to make it treat `buffer` as a + // null-terminated string, create a separate `initReadonlyViewZ` + // constructor. + return BIO_new_mem_buf(buffer.ptr, buffer.len); + } + pub fn deinit(this: *struct_bio_st) void { _ = BIO_free(this); } diff --git a/src/deps/c_ares.zig b/src/deps/c_ares.zig index 4ea5141427..09c365a34c 100644 --- a/src/deps/c_ares.zig +++ b/src/deps/c_ares.zig @@ -192,47 +192,46 @@ pub const Options = extern struct { evsys: ares_evsys_t = 0, server_failover_opts: struct_ares_server_failover_options = @import("std").mem.zeroes(struct_ares_server_failover_options), }; + pub const struct_hostent = extern struct { - h_name: [*c]u8, - h_aliases: [*c][*c]u8, - h_addrtype: c_int, - h_length: c_int, - h_addr_list: [*c][*c]u8, + h_name: ?[*:0]u8, + h_aliases: ?[*:null]?[*:0]u8, + h_addrtype: hostent_int, + h_length: hostent_int, + h_addr_list: ?[*:null]?[*:0]u8, + + // hostent in glibc uses int for h_addrtype and h_length, whereas hostent in winsock2.h uses short. + const hostent_int = if (bun.Environment.isWindows) c_short else c_int; pub fn toJSResponse(this: *struct_hostent, _: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime lookup_name: []const u8) JSC.JSValue { - - // A cname lookup always returns a single record but we follow the common API here. if (comptime strings.eqlComptime(lookup_name, "cname")) { - if (this.h_name != null) { - const array = JSC.JSValue.createEmptyArray(globalThis, 1); - const h_name_len = bun.len(this.h_name); - const h_name_slice = this.h_name[0..h_name_len]; - array.putIndex(globalThis, 0, JSC.ZigString.fromUTF8(h_name_slice).toJS(globalThis)); - return array; - } - return JSC.JSValue.createEmptyArray(globalThis, 0); - } else { - if (this.h_aliases == null) { + // A cname lookup always returns a single record but we follow the common API here. + if (this.h_name == null) { return JSC.JSValue.createEmptyArray(globalThis, 0); } - - var count: u32 = 0; - while (this.h_aliases[count] != null) { - count += 1; - } - - const array = JSC.JSValue.createEmptyArray(globalThis, count); - count = 0; - - while (this.h_aliases[count]) |alias| { - const alias_len = bun.len(alias); - const alias_slice = alias[0..alias_len]; - array.putIndex(globalThis, count, JSC.ZigString.fromUTF8(alias_slice).toJS(globalThis)); - count += 1; - } - - return array; + return bun.String.toJSArray(globalThis, &[_]bun.String{bun.String.fromUTF8(this.h_name.?[0..bun.len(this.h_name.?)])}); } + + if (this.h_aliases == null) { + return JSC.JSValue.createEmptyArray(globalThis, 0); + } + + var count: u32 = 0; + while (this.h_aliases.?[count] != null) { + count += 1; + } + + const array = JSC.JSValue.createEmptyArray(globalThis, count); + count = 0; + + while (this.h_aliases.?[count]) |alias| { + const alias_len = bun.len(alias); + const alias_slice = alias[0..alias_len]; + array.putIndex(globalThis, count, JSC.ZigString.fromUTF8(alias_slice).toJS(globalThis)); + count += 1; + } + + return array; } pub fn Callback(comptime Type: type) type { @@ -269,7 +268,17 @@ pub const struct_hostent = extern struct { } var start: [*c]struct_hostent = undefined; - if (comptime strings.eqlComptime(lookup_name, "ns")) { + if (comptime strings.eqlComptime(lookup_name, "cname")) { + var addrttls: [256]struct_ares_addrttl = undefined; + var naddrttls: i32 = 256; + + const result = ares_parse_a_reply(buffer, buffer_length, &start, &addrttls, &naddrttls); + if (result != ARES_SUCCESS) { + function(this, Error.get(result), timeouts, null); + return; + } + function(this, null, timeouts, start); + } else if (comptime strings.eqlComptime(lookup_name, "ns")) { const result = ares_parse_ns_reply(buffer, buffer_length, &start); if (result != ARES_SUCCESS) { function(this, Error.get(result), timeouts, null); @@ -283,16 +292,8 @@ pub const struct_hostent = extern struct { return; } function(this, null, timeouts, start); - } else if (comptime strings.eqlComptime(lookup_name, "cname")) { - var addrttls: [256]struct_ares_addrttl = undefined; - var naddrttls: i32 = 256; - - const result = ares_parse_a_reply(buffer, buffer_length, &start, &addrttls, &naddrttls); - if (result != ARES_SUCCESS) { - function(this, Error.get(result), timeouts, null); - return; - } - function(this, null, timeouts, start); + } else { + @compileError(std.fmt.comptimePrint("Unsupported struct_hostent record type: {s}", .{lookup_name})); } } }.handle; @@ -303,6 +304,129 @@ pub const struct_hostent = extern struct { } }; +pub const hostent_with_ttls = struct { + hostent: *struct_hostent, + ttls: [256]c_int = [_]c_int{-1} ** 256, + + pub fn toJSResponse(this: *hostent_with_ttls, _: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime lookup_name: []const u8) JSC.JSValue { + if (comptime strings.eqlComptime(lookup_name, "a") or strings.eqlComptime(lookup_name, "aaaa")) { + if (this.hostent.h_addr_list == null) { + return JSC.JSValue.createEmptyArray(globalThis, 0); + } + + var count: u32 = 0; + while (this.hostent.h_addr_list.?[count] != null) { + count += 1; + } + + const array = JSC.JSValue.createEmptyArray(globalThis, count); + count = 0; + + const addressKey = JSC.ZigString.static("address").withEncoding(); + const ttlKey = JSC.ZigString.static("ttl").withEncoding(); + + while (this.hostent.h_addr_list.?[count]) |addr| : (count += 1) { + const addrString = (if (this.hostent.h_addrtype == AF.INET6) + bun.dns.addressToJS(&std.net.Address.initIp6(addr[0..16].*, 0, 0, 0), globalThis) + else + bun.dns.addressToJS(&std.net.Address.initIp4(addr[0..4].*, 0), globalThis)) catch return globalThis.throwOutOfMemoryValue(); + + const ttl: ?c_int = if (count < this.ttls.len) this.ttls[count] else null; + const resultObject = JSC.JSValue.createObject2(globalThis, &addressKey, &ttlKey, addrString, if (ttl) |val| JSC.jsNumber(val) else .undefined); + array.putIndex(globalThis, count, resultObject); + } + + return array; + } else { + @compileError(std.fmt.comptimePrint("Unsupported hostent_with_ttls record type: {s}", .{lookup_name})); + } + } + + pub fn Callback(comptime Type: type) type { + return fn (*Type, status: ?Error, timeouts: i32, results: ?*hostent_with_ttls) void; + } + + pub fn hostCallbackWrapper( + comptime Type: type, + comptime function: Callback(Type), + ) ares_host_callback { + return &struct { + pub fn handle(ctx: ?*anyopaque, status: c_int, timeouts: c_int, hostent: ?*hostent_with_ttls) callconv(.C) void { + const this = bun.cast(*Type, ctx.?); + if (status != ARES_SUCCESS) { + function(this, Error.get(status), timeouts, null); + return; + } + function(this, null, timeouts, hostent); + } + }.handle; + } + + pub fn callbackWrapper( + comptime lookup_name: []const u8, + comptime Type: type, + comptime function: Callback(Type), + ) ares_callback { + return &struct { + pub fn handle(ctx: ?*anyopaque, status: c_int, timeouts: c_int, buffer: [*c]u8, buffer_length: c_int) callconv(.C) void { + const this = bun.cast(*Type, ctx.?); + if (status != ARES_SUCCESS) { + function(this, Error.get(status), timeouts, null); + return; + } + + switch (parse(lookup_name, buffer, buffer_length)) { + .result => |result| function(this, null, timeouts, result), + .err => |err| function(this, err, timeouts, null), + } + } + }.handle; + } + + pub fn parse(comptime lookup_name: []const u8, buffer: [*c]u8, buffer_length: c_int) JSC.Node.Maybe(*hostent_with_ttls, Error) { + var start: ?*struct_hostent = null; + + if (comptime strings.eqlComptime(lookup_name, "a")) { + var addrttls: [256]struct_ares_addrttl = undefined; + var naddrttls: c_int = 256; + + const result = ares_parse_a_reply(buffer, buffer_length, &start, &addrttls, &naddrttls); + if (result != ARES_SUCCESS) { + return .{ .err = Error.get(result).? }; + } + var with_ttls = bun.default_allocator.create(hostent_with_ttls) catch bun.outOfMemory(); + with_ttls.hostent = start.?; + for (addrttls[0..@intCast(naddrttls)], 0..) |ttl, i| { + with_ttls.ttls[i] = ttl.ttl; + } + return .{ .result = with_ttls }; + } + + if (comptime strings.eqlComptime(lookup_name, "aaaa")) { + var addr6ttls: [256]struct_ares_addr6ttl = undefined; + var naddr6ttls: c_int = 256; + + const result = ares_parse_aaaa_reply(buffer, buffer_length, &start, &addr6ttls, &naddr6ttls); + if (result != ARES_SUCCESS) { + return .{ .err = Error.get(result).? }; + } + var with_ttls = bun.default_allocator.create(hostent_with_ttls) catch bun.outOfMemory(); + with_ttls.hostent = start.?; + for (addr6ttls[0..@intCast(naddr6ttls)], 0..) |ttl, i| { + with_ttls.ttls[i] = ttl.ttl; + } + return .{ .result = with_ttls }; + } + + @compileError(std.fmt.comptimePrint("Unsupported hostent_with_ttls record type: {s}", .{lookup_name})); + } + + pub fn deinit(this: *hostent_with_ttls) void { + this.hostent.deinit(); + bun.default_allocator.destroy(this); + } +}; + pub const struct_nameinfo = extern struct { node: [*c]u8, service: [*c]u8, @@ -351,7 +475,7 @@ pub const struct_nameinfo = extern struct { } }; -pub const struct_timeval = opaque {}; +pub const struct_timeval = std.posix.timeval; pub const struct_Channeldata = opaque {}; pub const AddrInfo_cname = extern struct { ttl: c_int, @@ -389,10 +513,7 @@ pub const AddrInfo = extern struct { globalThis: *JSC.JSGlobalObject, ) JSC.JSValue { var node = addr_info.node orelse return JSC.JSValue.createEmptyArray(globalThis, 0); - const array = JSC.JSValue.createEmptyArray( - globalThis, - node.count(), - ); + const array = JSC.JSValue.createEmptyArray(globalThis, node.count()); { var j: u32 = 0; @@ -462,8 +583,13 @@ pub const AddrInfo_hints = extern struct { } }; +pub const ChannelOptions = struct { + timeout: ?i32 = null, + tries: ?i32 = null, +}; + pub const Channel = opaque { - pub fn init(comptime Container: type, this: *Container) ?Error { + pub fn init(comptime Container: type, this: *Container, options: ChannelOptions) ?Error { var channel: *Channel = undefined; libraryInit(); @@ -483,8 +609,8 @@ pub const Channel = opaque { opts.flags = ARES_FLAG_NOCHECKRESP; opts.sock_state_cb = &SockStateWrap.onSockState; opts.sock_state_cb_data = @as(*anyopaque, @ptrCast(this)); - opts.timeout = -1; - opts.tries = 4; + opts.timeout = options.timeout orelse -1; + opts.tries = options.tries orelse 4; const optmask: c_int = ARES_OPT_FLAGS | ARES_OPT_TIMEOUTMS | @@ -499,6 +625,10 @@ pub const Channel = opaque { return null; } + pub fn deinit(this: *Channel) void { + ares_destroy(this); + } + /// ///The ares_getaddrinfo function initiates a host query by name on the name service channel identified by channel. The name and service parameters give the hostname and service as NULL-terminated C strings. The hints parameter is an ares_addrinfo_hints structure: /// @@ -719,6 +849,7 @@ pub extern fn ares_create_query(name: [*c]const u8, dnsclass: c_int, @"type": c_ pub extern fn ares_mkquery(name: [*c]const u8, dnsclass: c_int, @"type": c_int, id: c_ushort, rd: c_int, buf: [*c][*c]u8, buflen: [*c]c_int) c_int; pub extern fn ares_expand_name(encoded: [*c]const u8, abuf: [*c]const u8, alen: c_int, s: [*c][*c]u8, enclen: [*c]c_long) c_int; pub extern fn ares_expand_string(encoded: [*c]const u8, abuf: [*c]const u8, alen: c_int, s: [*c][*c]u8, enclen: [*c]c_long) c_int; +pub extern fn ares_queue_active_queries(channel: *const Channel) usize; const union_unnamed_2 = extern union { _S6_u8: [16]u8, }; @@ -726,7 +857,7 @@ pub const struct_ares_in6_addr = extern struct { _S6_un: union_unnamed_2, }; pub const struct_ares_addrttl = extern struct { - ipaddr: struct_in_addr, + ipaddr: u32, ttl: c_int, }; pub const struct_ares_addr6ttl = extern struct { @@ -1009,6 +1140,28 @@ pub const struct_ares_txt_reply = extern struct { return array; } + pub fn toJSForAny(this: *struct_ares_txt_reply, _: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime _: []const u8) JSC.JSValue { + var count: usize = 0; + var txt: ?*struct_ares_txt_reply = this; + while (txt != null) : (txt = txt.?.next) { + count += 1; + } + + const array = JSC.JSValue.createEmptyArray(globalThis, count); + + txt = this; + var i: u32 = 0; + while (txt != null) : (txt = txt.?.next) { + var node = txt.?; + array.putIndex(globalThis, i, JSC.ZigString.fromUTF8(node.txt[0..node.length]).toJS(globalThis)); + i += 1; + } + + return JSC.JSObject.create(.{ + .entries = array, + }, globalThis).toJS(); + } + pub fn Callback(comptime Type: type) type { return fn (*Type, status: ?Error, timeouts: i32, results: ?*struct_ares_txt_reply) void; } @@ -1218,6 +1371,207 @@ pub const struct_ares_uri_reply = extern struct { uri: [*c]u8, ttl: c_int, }; + +pub const struct_any_reply = struct { + a_reply: ?*hostent_with_ttls = null, + aaaa_reply: ?*hostent_with_ttls = null, + mx_reply: ?*struct_ares_mx_reply = null, + ns_reply: ?*struct_hostent = null, + txt_reply: ?*struct_ares_txt_reply = null, + srv_reply: ?*struct_ares_srv_reply = null, + ptr_reply: ?*struct_hostent = null, + naptr_reply: ?*struct_ares_naptr_reply = null, + soa_reply: ?*struct_ares_soa_reply = null, + caa_reply: ?*struct_ares_caa_reply = null, + + pub fn toJSResponse(this: *struct_any_reply, parent_allocator: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime _: []const u8) JSC.JSValue { + var stack = std.heap.stackFallback(2048, parent_allocator); + var arena = bun.ArenaAllocator.init(stack.get()); + defer arena.deinit(); + + const allocator = arena.allocator(); + + return this.toJS(globalThis, allocator); + } + + fn append(globalThis: *JSC.JSGlobalObject, array: JSC.JSValue, i: *u32, response: JSC.JSValue, comptime lookup_name: []const u8) void { + const transformed = if (response.isString()) + JSC.JSObject.create(.{ + .value = response, + }, globalThis).toJS() + else blk: { + bun.assert(response.isObject()); + break :blk response; + }; + + var upper = comptime lookup_name[0..lookup_name.len].*; + inline for (&upper) |*char| { + char.* = std.ascii.toUpper(char.*); + } + + transformed.put(globalThis, "type", bun.String.ascii(&upper).toJS(globalThis)); + array.putIndex(globalThis, i.*, transformed); + i.* += 1; + } + + fn appendAll(globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator, array: JSC.JSValue, i: *u32, reply: anytype, comptime lookup_name: []const u8) void { + const response: JSC.JSValue = if (comptime @hasDecl(@TypeOf(reply.*), "toJSForAny")) + reply.toJSForAny(allocator, globalThis, lookup_name) + else + reply.toJSResponse(allocator, globalThis, lookup_name); + + if (response.isArray()) { + var iterator = response.arrayIterator(globalThis); + while (iterator.next()) |item| { + append(globalThis, array, i, item, lookup_name); + } + } else { + append(globalThis, array, i, response, lookup_name); + } + } + + pub fn toJS(this: *struct_any_reply, globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) JSC.JSValue { + const array = JSC.JSValue.createEmptyArray(globalThis, blk: { + var len: usize = 0; + inline for (comptime @typeInfo(struct_any_reply).Struct.fields) |field| { + if (comptime std.mem.endsWith(u8, field.name, "_reply")) { + len += @intFromBool(@field(this, field.name) != null); + } + } + break :blk len; + }); + + var i: u32 = 0; + + inline for (comptime @typeInfo(struct_any_reply).Struct.fields) |field| { + if (comptime std.mem.endsWith(u8, field.name, "_reply")) { + if (@field(this, field.name)) |reply| { + const lookup_name = comptime field.name[0 .. field.name.len - "_reply".len]; + appendAll(globalThis, allocator, array, &i, reply, lookup_name); + } + } + } + + return array; + } + + pub fn Callback(comptime Type: type) type { + return fn (*Type, status: ?Error, timeouts: i32, results: ?*struct_any_reply) void; + } + + pub fn callbackWrapper( + comptime _: []const u8, + comptime Type: type, + comptime function: Callback(Type), + ) ares_callback { + return &struct { + pub fn handleAny(ctx: ?*anyopaque, status: c_int, timeouts: c_int, buffer: [*c]u8, buffer_length: c_int) callconv(.C) void { + const this = bun.cast(*Type, ctx.?); + if (status != ARES_SUCCESS) { + function(this, Error.get(status), timeouts, null); + return; + } + + var any_success = false; + var last_error: ?c_int = null; + var reply = bun.default_allocator.create(struct_any_reply) catch bun.outOfMemory(); + reply.* = .{}; + + switch (hostent_with_ttls.parse("a", buffer, buffer_length)) { + .result => |result| { + reply.a_reply = result; + any_success = true; + }, + .err => |err| last_error = @intFromEnum(err), + } + + switch (hostent_with_ttls.parse("aaaa", buffer, buffer_length)) { + .result => |result| { + reply.aaaa_reply = result; + any_success = true; + }, + .err => |err| last_error = @intFromEnum(err), + } + + var result = ares_parse_mx_reply(buffer, buffer_length, &reply.mx_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_ns_reply(buffer, buffer_length, &reply.ns_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_txt_reply(buffer, buffer_length, &reply.txt_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_srv_reply(buffer, buffer_length, &reply.srv_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_ptr_reply(buffer, buffer_length, null, 0, AF.INET, &reply.ptr_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_naptr_reply(buffer, buffer_length, &reply.naptr_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_soa_reply(buffer, buffer_length, &reply.soa_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_caa_reply(buffer, buffer_length, &reply.caa_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + if (!any_success) { + reply.deinit(); + function(this, Error.get(last_error.?), timeouts, null); + return; + } + + function(this, null, timeouts, reply); + } + }.handleAny; + } + + pub fn deinit(this: *struct_any_reply) void { + inline for (@typeInfo(struct_any_reply).Struct.fields) |field| { + if (comptime std.mem.endsWith(u8, field.name, "_reply")) { + if (@field(this, field.name)) |reply| { + reply.deinit(); + } + } + } + + bun.default_allocator.destroy(this); + } +}; pub extern fn ares_parse_a_reply(abuf: [*c]const u8, alen: c_int, host: [*c]?*struct_hostent, addrttls: [*c]struct_ares_addrttl, naddrttls: [*c]c_int) c_int; pub extern fn ares_parse_aaaa_reply(abuf: [*c]const u8, alen: c_int, host: [*c]?*struct_hostent, addrttls: [*c]struct_ares_addr6ttl, naddrttls: [*c]c_int) c_int; pub extern fn ares_parse_caa_reply(abuf: [*c]const u8, alen: c_int, caa_out: [*c][*c]struct_ares_caa_reply) c_int; @@ -1288,6 +1642,7 @@ pub const ARES_ELOADIPHLPAPI = 22; pub const ARES_EADDRGETNETWORKPARAMS = 23; pub const ARES_ECANCELLED = 24; pub const ARES_ESERVICE = 25; +pub const ARES_ENOSERVER = 26; pub const Error = enum(i32) { ENODATA = ARES_ENODATA, @@ -1315,25 +1670,105 @@ pub const Error = enum(i32) { EADDRGETNETWORKPARAMS = ARES_EADDRGETNETWORKPARAMS, ECANCELLED = ARES_ECANCELLED, ESERVICE = ARES_ESERVICE, + ENOSERVER = ARES_ENOSERVER, + + const Deferred = struct { + errno: Error, + syscall: []const u8, + hostname: ?bun.String, + promise: JSC.JSPromise.Strong, + + pub usingnamespace bun.New(@This()); + + pub fn init(errno: Error, syscall: []const u8, hostname: ?bun.String, promise: JSC.JSPromise.Strong) *Deferred { + return Deferred.new(.{ + .errno = errno, + .syscall = syscall, + .hostname = hostname, + .promise = promise, + }); + } + + pub fn reject(this: *Deferred, globalThis: *JSC.JSGlobalObject) void { + const system_error = JSC.SystemError{ + .errno = @intFromEnum(this.errno), + .code = bun.String.static(this.errno.code()), + .message = if (this.hostname) |hostname| bun.String.createFormat("{s} {s} {s}", .{ this.syscall, this.errno.code()[4..], hostname }) catch bun.outOfMemory() else bun.String.empty, + .syscall = bun.String.createUTF8(this.syscall), + .hostname = this.hostname orelse bun.String.empty, + }; + + const instance = system_error.toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + + this.promise.reject(globalThis, instance); + this.hostname = null; + this.deinit(); + } + + pub fn rejectLater(this: *Deferred, globalThis: *JSC.JSGlobalObject) void { + const Context = struct { + deferred: *Deferred, + globalThis: *JSC.JSGlobalObject, + pub fn callback(context: *@This()) void { + context.deferred.reject(context.globalThis); + } + }; + + const context = bun.default_allocator.create(Context) catch bun.outOfMemory(); + context.deferred = this; + context.globalThis = globalThis; + // TODO(@heimskr): new custom Task type + globalThis.bunVM().enqueueTask(JSC.ManagedTask.New(Context, Context.callback).init(context)); + } + + pub fn deinit(this: *@This()) void { + if (this.hostname) |hostname| { + hostname.deref(); + } + this.promise.deinit(); + this.destroy(); + } + }; + + pub fn toDeferred(this: Error, syscall: []const u8, hostname: ?[]const u8, promise: *JSC.JSPromise.Strong) *Deferred { + const host_string: ?bun.String = if (hostname) |host| + bun.String.createUTF8(host) + else + null; + defer promise.* = .{}; + return Deferred.init(this, syscall, host_string, promise.*); + } pub fn toJS(this: Error, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const error_value = globalThis.createErrorInstance("{s}", .{this.label()}); - error_value.put( - globalThis, - JSC.ZigString.static("name"), - bun.String.static("DNSException").toJS(globalThis), - ); - error_value.put( - globalThis, - JSC.ZigString.static("code"), - JSC.ZigString.init(this.code()).toJS(globalThis), - ); - error_value.put( - globalThis, - JSC.ZigString.static("errno"), - JSC.jsNumber(@intFromEnum(this)), - ); - return error_value; + const instance = (JSC.SystemError{ + .errno = @intFromEnum(this), + .code = bun.String.static(this.code()), + }).toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + return instance; + } + + pub fn toJSWithSyscall(this: Error, globalThis: *JSC.JSGlobalObject, comptime syscall: []const u8) JSC.JSValue { + const instance = (JSC.SystemError{ + .errno = @intFromEnum(this), + .code = bun.String.static(this.code()), + .syscall = bun.String.static((syscall ++ "\x00")[0..syscall.len :0]), + }).toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + return instance; + } + + pub fn toJSWithSyscallAndHostname(this: Error, globalThis: *JSC.JSGlobalObject, comptime syscall: []const u8, hostname: []const u8) JSC.JSValue { + const instance = (JSC.SystemError{ + .errno = @intFromEnum(this), + .code = bun.String.static(this.code()), + .message = bun.String.createFormat("{s} {s} {s}", .{ syscall, this.code()[4..], hostname }) catch bun.outOfMemory(), + .syscall = bun.String.static((syscall ++ "\x00")[0..syscall.len :0]), + .hostname = bun.String.createUTF8(hostname), + }).toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + return instance; } pub fn initEAI(rc: i32) ?Error { @@ -1422,6 +1857,7 @@ pub const Error = enum(i32) { .{ .EADDRGETNETWORKPARAMS, "DNS_EADDRGETNETWORKPARAMS" }, .{ .ECANCELLED, "DNS_ECANCELLED" }, .{ .ESERVICE, "DNS_ESERVICE" }, + .{ .ENOSERVER, "DNS_ENOSERVER" }, }); pub const label = bun.enumMap(Error, .{ @@ -1450,6 +1886,7 @@ pub const Error = enum(i32) { .{ .EADDRGETNETWORKPARAMS, "EADDRGETNETWORKPARAMS" }, .{ .ECANCELLED, "DNS query cancelled" }, .{ .ESERVICE, "Service not available" }, + .{ .ENOSERVER, "No DNS servers were configured" }, }); pub fn get(rc: i32) ?Error { @@ -1460,8 +1897,8 @@ pub const Error = enum(i32) { return switch (rc) { 0 => null, - 1...ARES_ESERVICE => @as(Error, @enumFromInt(rc)), - -ARES_ESERVICE...-1 => @as(Error, @enumFromInt(-rc)), + 1...ARES_ENOSERVER => @as(Error, @enumFromInt(rc)), + -ARES_ENOSERVER...-1 => @as(Error, @enumFromInt(-rc)), else => unreachable, }; } diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index 0940fdf3de..071b1862b1 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -2327,7 +2327,7 @@ pub const uv_rusage_t = extern struct { ru_nivcsw: u64, }; pub extern fn uv_getrusage(rusage: [*c]uv_rusage_t) c_int; -pub extern fn uv_os_homedir(buffer: [*]u8, size: [*c]usize) c_int; +pub extern fn uv_os_homedir(buffer: [*]u8, size: *usize) ReturnCode; pub extern fn uv_os_tmpdir(buffer: [*]u8, size: [*c]usize) c_int; pub extern fn uv_os_get_passwd(pwd: [*c]uv_passwd_t) c_int; pub extern fn uv_os_free_passwd(pwd: [*c]uv_passwd_t) void; diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index b683362431..d3aafb25af 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -17,6 +17,9 @@ static inline std::string_view stringViewFromC(const char* message, size_t lengt return std::string_view(); } +using TLSWebSocket = uWS::WebSocket; +using TCPWebSocket = uWS::WebSocket; + extern "C" { @@ -722,12 +725,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return *uws->getUserData(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return *uws->getUserData(); } @@ -735,14 +738,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->close(); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->close(); } } @@ -752,13 +755,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode); } @@ -770,8 +773,8 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress, fin); @@ -779,8 +782,8 @@ extern "C" else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->send(stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress, fin); @@ -793,13 +796,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendFragment( stringViewFromC(message, length), compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendFragment(stringViewFromC(message, length), compress); } @@ -809,13 +812,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), uWS::OpCode::BINARY, compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), uWS::OpCode::BINARY, compress); } @@ -826,14 +829,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendFirstFragment( stringViewFromC(message, length), (uWS::OpCode)(unsigned char)opcode, compress); @@ -844,13 +847,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return (uws_sendstatus_t)uws->sendLastFragment( stringViewFromC(message, length), compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return (uws_sendstatus_t)uws->sendLastFragment( stringViewFromC(message, length), compress); } @@ -860,14 +863,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->end(code, stringViewFromC(message, length)); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->end(code, stringViewFromC(message, length)); } } @@ -877,15 +880,15 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->cork([handler, user_data]() { handler(user_data); }); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->cork([handler, user_data]() { handler(user_data); }); @@ -896,12 +899,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->subscribe(stringViewFromC(topic, length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->subscribe(stringViewFromC(topic, length)); } bool uws_ws_unsubscribe(int ssl, uws_websocket_t *ws, const char *topic, @@ -909,12 +912,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->unsubscribe(stringViewFromC(topic, length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->unsubscribe(stringViewFromC(topic, length)); } @@ -923,12 +926,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->isSubscribed(stringViewFromC(topic, length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->isSubscribed(stringViewFromC(topic, length)); } void uws_ws_iterate_topics(int ssl, uws_websocket_t *ws, @@ -938,15 +941,15 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; uws->iterateTopics([callback, user_data](auto topic) { callback(topic.data(), topic.length(), user_data); }); } else { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; uws->iterateTopics([callback, user_data](auto topic) { callback(topic.data(), topic.length(), user_data); }); @@ -959,13 +962,13 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length)); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length)); } @@ -977,14 +980,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length), (uWS::OpCode)(unsigned char)opcode, compress); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->publish(stringViewFromC(topic, topic_length), stringViewFromC(message, message_length), (uWS::OpCode)(unsigned char)opcode, compress); @@ -994,12 +997,12 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; return uws->getBufferedAmount(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; return uws->getBufferedAmount(); } @@ -1008,14 +1011,14 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; std::string_view value = uws->getRemoteAddress(); *dest = value.data(); return value.length(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; std::string_view value = uws->getRemoteAddress(); *dest = value.data(); @@ -1027,15 +1030,15 @@ extern "C" { if (ssl) { - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TLSWebSocket *uws = + (TLSWebSocket *)ws; std::string_view value = uws->getRemoteAddressAsText(); *dest = value.data(); return value.length(); } - uWS::WebSocket *uws = - (uWS::WebSocket *)ws; + TCPWebSocket *uws = + (TCPWebSocket *)ws; std::string_view value = uws->getRemoteAddressAsText(); *dest = value.data(); @@ -1714,6 +1717,14 @@ __attribute__((callback (corker, ctx))) } } + size_t uws_ws_memory_cost(int ssl, uws_websocket_t *ws) { + if (ssl) { + return ((TLSWebSocket*)ws)->memoryCost(); + } else { + return ((TCPWebSocket*)ws)->memoryCost(); + } + } + void us_socket_sendfile_needs_more(us_socket_r s) { s->context->loop->data.last_write_failed = 1; us_poll_change(&s->p, s->context->loop, LIBUS_SOCKET_READABLE | LIBUS_SOCKET_WRITABLE); diff --git a/src/deps/lol-html.zig b/src/deps/lol-html.zig index d5e747c773..303588edc0 100644 --- a/src/deps/lol-html.zig +++ b/src/deps/lol-html.zig @@ -33,7 +33,9 @@ pub const HTMLRewriter = opaque { pub fn write(rewriter: *HTMLRewriter, chunk: []const u8) Error!void { auto_disable(); - if (rewriter.lol_html_rewriter_write(ptrWithoutPanic(chunk), chunk.len) < 0) + const ptr = ptrWithoutPanic(chunk); + const rc = rewriter.lol_html_rewriter_write(ptr, chunk.len); + if (rc < 0) return error.Fail; } @@ -207,17 +209,17 @@ pub const HTMLRewriter = opaque { auto_disable(); return switch (builder.lol_html_rewriter_builder_add_element_content_handlers( selector, - if (element_handler_data != null) + if (element_handler != null and element_handler_data != null) DirectiveHandler(Element, ElementHandler, element_handler.?) else null, element_handler_data, - if (comment_handler_data != null) + if (comment_handler != null and comment_handler_data != null) DirectiveHandler(Comment, CommentHandler, comment_handler.?) else null, comment_handler_data, - if (text_chunk_handler_data != null) + if (text_chunk_handler != null and text_chunk_handler_data != null) DirectiveHandler(TextChunk, TextChunkHandler, text_chunk_handler.?) else null, @@ -794,6 +796,8 @@ pub const DocType = opaque { extern fn lol_html_doctype_system_id_get(doctype: *const DocType) HTMLString; extern fn lol_html_doctype_user_data_set(doctype: *const DocType, user_data: ?*anyopaque) void; extern fn lol_html_doctype_user_data_get(doctype: *const DocType) ?*anyopaque; + extern fn lol_html_doctype_remove(doctype: *DocType) void; + extern fn lol_html_doctype_is_removed(doctype: *const DocType) bool; pub const Callback = *const fn (*DocType, ?*anyopaque) callconv(.C) Directive; @@ -809,6 +813,14 @@ pub const DocType = opaque { auto_disable(); return this.lol_html_doctype_system_id_get(); } + pub fn remove(this: *DocType) void { + auto_disable(); + return this.lol_html_doctype_remove(); + } + pub fn isRemoved(this: *const DocType) bool { + auto_disable(); + return this.lol_html_doctype_is_removed(); + } }; pub const Encoding = enum { diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 183bbacfb1..3555261098 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -62,7 +62,7 @@ pub const InternalLoopData = extern struct { low_prio_budget: i32, dns_ready_head: *ConnectingSocket, closed_connecting_head: *ConnectingSocket, - mutex: u32, // this is actually a bun.Lock + mutex: bun.Mutex.ReleaseImpl.Type, parent_ptr: ?*anyopaque, parent_tag: c_char, iteration_nr: usize, @@ -2884,6 +2884,13 @@ pub const AnyWebSocket = union(enum) { }; } + pub fn memoryCost(this: AnyWebSocket) usize { + return switch (this) { + .ssl => this.ssl.memoryCost(), + .tcp => this.tcp.memoryCost(), + }; + } + pub fn close(this: AnyWebSocket) void { const ssl_flag = @intFromBool(this == .ssl); return uws_ws_close(ssl_flag, this.raw()); @@ -2974,7 +2981,13 @@ pub const AnyWebSocket = union(enum) { } }; -pub const RawWebSocket = opaque {}; +pub const RawWebSocket = opaque { + pub fn memoryCost(this: *RawWebSocket, ssl_flag: i32) usize { + return uws_ws_memory_cost(ssl_flag, this); + } + + extern fn uws_ws_memory_cost(ssl: i32, ws: *RawWebSocket) usize; +}; pub const uws_websocket_handler = ?*const fn (*RawWebSocket) callconv(.C) void; pub const uws_websocket_message_handler = ?*const fn (*RawWebSocket, [*c]const u8, usize, Opcode) callconv(.C) void; @@ -3960,6 +3973,11 @@ pub fn NewApp(comptime ssl: bool) type { pub fn sendWithOptions(this: *WebSocket, message: []const u8, opcode: Opcode, compress: bool, fin: bool) SendStatus { return uws_ws_send_with_options(ssl_flag, this.raw(), message.ptr, message.len, opcode, compress, fin); } + + pub fn memoryCost(this: *WebSocket) usize { + return this.raw().memoryCost(ssl_flag); + } + // pub fn sendFragment(this: *WebSocket, message: []const u8) SendStatus { // return uws_ws_send_fragment(ssl_flag, this.raw(), message: [*c]const u8, length: usize, compress: bool); // } diff --git a/src/dns.zig b/src/dns.zig index 6601ee8eb3..95d9d74635 100644 --- a/src/dns.zig +++ b/src/dns.zig @@ -3,6 +3,12 @@ const std = @import("std"); const JSC = bun.JSC; const JSValue = JSC.JSValue; +const netdb = if (bun.Environment.isWindows) .{ + .AI_V4MAPPED = @as(c_int, 2048), + .AI_ADDRCONFIG = @as(c_int, 1024), + .AI_ALL = @as(c_int, 256), +} else @cImport(@cInclude("netdb.h")); + pub const GetAddrInfo = struct { name: []const u8 = "", port: u16 = 0, @@ -95,6 +101,9 @@ pub const GetAddrInfo = struct { return error.InvalidFlags; options.flags = flags.coerce(i32, globalObject); + + if (options.flags & ~(netdb.AI_ALL | netdb.AI_ADDRCONFIG | netdb.AI_V4MAPPED) != 0) + return error.InvalidFlags; } return options; diff --git a/src/env.zig b/src/env.zig index bbc36aba6f..482d7d5058 100644 --- a/src/env.zig +++ b/src/env.zig @@ -19,7 +19,6 @@ pub const isBrowser = !isWasi and isWasm; pub const isWindows = @import("builtin").target.os.tag == .windows; pub const isPosix = !isWindows and !isWasm; pub const isDebug = std.builtin.Mode.Debug == @import("builtin").mode; -pub const isRelease = std.builtin.Mode.Debug != @import("builtin").mode and !isTest; pub const isTest = @import("builtin").is_test; pub const isLinux = @import("builtin").target.os.tag == .linux; pub const isAarch64 = @import("builtin").target.cpu.arch.isAARCH64(); @@ -28,6 +27,11 @@ pub const isX64 = @import("builtin").target.cpu.arch == .x86_64; pub const isMusl = builtin.target.abi.isMusl(); pub const allow_assert = isDebug or isTest or std.builtin.Mode.ReleaseSafe == @import("builtin").mode; +/// All calls to `@export` should be gated behind this check, so that code +/// generators that compile Zig code know not to reference and compile a ton of +/// unused code. +pub const export_cpp_apis = @import("builtin").output_mode == .Obj; + pub const build_options = @import("build_options"); pub const reported_nodejs_version = build_options.reported_nodejs_version; diff --git a/src/env_loader.zig b/src/env_loader.zig index 77cc2aa7ec..6364684527 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -17,6 +17,7 @@ const Fs = @import("./fs.zig"); const URL = @import("./url.zig").URL; const Api = @import("./api/schema.zig").Api; const which = @import("./which.zig").which; +const s3 = bun.S3; const DotEnvFileSuffix = enum { development, @@ -45,6 +46,8 @@ pub const Loader = struct { did_load_process: bool = false, reject_unauthorized: ?bool = null, + aws_credentials: ?s3.S3Credentials = null, + pub fn iterator(this: *const Loader) Map.HashTable.Iterator { return this.map.iterator(); } @@ -112,6 +115,60 @@ pub const Loader = struct { } } + pub fn getS3Credentials(this: *Loader) s3.S3Credentials { + if (this.aws_credentials) |credentials| { + return credentials; + } + + var accessKeyId: []const u8 = ""; + var secretAccessKey: []const u8 = ""; + var region: []const u8 = ""; + var endpoint: []const u8 = ""; + var bucket: []const u8 = ""; + var session_token: []const u8 = ""; + + if (this.get("S3_ACCESS_KEY_ID")) |access_key| { + accessKeyId = access_key; + } else if (this.get("AWS_ACCESS_KEY_ID")) |access_key| { + accessKeyId = access_key; + } + if (this.get("S3_SECRET_ACCESS_KEY")) |access_key| { + secretAccessKey = access_key; + } else if (this.get("AWS_SECRET_ACCESS_KEY")) |access_key| { + secretAccessKey = access_key; + } + + if (this.get("S3_REGION")) |region_| { + region = region_; + } else if (this.get("AWS_REGION")) |region_| { + region = region_; + } + if (this.get("S3_ENDPOINT")) |endpoint_| { + endpoint = bun.URL.parse(endpoint_).host; + } else if (this.get("AWS_ENDPOINT")) |endpoint_| { + endpoint = bun.URL.parse(endpoint_).host; + } + if (this.get("S3_BUCKET")) |bucket_| { + bucket = bucket_; + } else if (this.get("AWS_BUCKET")) |bucket_| { + bucket = bucket_; + } + if (this.get("S3_SESSION_TOKEN")) |token| { + session_token = token; + } else if (this.get("AWS_SESSION_TOKEN")) |token| { + session_token = token; + } + this.aws_credentials = .{ + .accessKeyId = accessKeyId, + .secretAccessKey = secretAccessKey, + .region = region, + .endpoint = endpoint, + .bucket = bucket, + .sessionToken = session_token, + }; + + return this.aws_credentials.?; + } /// Checks whether `NODE_TLS_REJECT_UNAUTHORIZED` is set to `0` or `false`. /// /// **Prefer VirtualMachine.getTLSRejectUnauthorized()** for JavaScript, as individual workers could have different settings. @@ -134,11 +191,15 @@ pub const Loader = struct { return true; } - pub fn getHttpProxy(this: *Loader, url: URL) ?URL { + pub fn getHttpProxyFor(this: *Loader, url: URL) ?URL { + return this.getHttpProxy(url.isHTTP(), url.hostname); + } + + pub fn getHttpProxy(this: *Loader, is_http: bool, hostname: ?[]const u8) ?URL { // TODO: When Web Worker support is added, make sure to intern these strings var http_proxy: ?URL = null; - if (url.isHTTP()) { + if (is_http) { if (this.get("http_proxy") orelse this.get("HTTP_PROXY")) |proxy| { if (proxy.len > 0 and !strings.eqlComptime(proxy, "\"\"") and !strings.eqlComptime(proxy, "''")) { http_proxy = URL.parse(proxy); @@ -154,7 +215,7 @@ pub const Loader = struct { // NO_PROXY filter // See the syntax at https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/ - if (http_proxy != null) { + if (http_proxy != null and hostname != null) { if (this.get("no_proxy") orelse this.get("NO_PROXY")) |no_proxy_text| { if (no_proxy_text.len == 0 or strings.eqlComptime(no_proxy_text, "\"\"") or strings.eqlComptime(no_proxy_text, "''")) { return http_proxy; @@ -172,7 +233,7 @@ pub const Loader = struct { host = host[1..]; } //hostname ends with suffix - if (strings.endsWith(url.hostname, host)) { + if (strings.endsWith(hostname.?, host)) { return null; } next = no_proxy_list.next(); diff --git a/src/fd.zig b/src/fd.zig index 4a51880541..24c36a965f 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -263,7 +263,7 @@ pub const FDImpl = packed struct { defer req.deinit(); const rc = libuv.uv_fs_close(libuv.Loop.get(), &req, this.value.as_uv, null); break :result if (rc.errno()) |errno| - .{ .errno = errno, .syscall = .close, .fd = this.encode() } + .{ .errno = errno, .syscall = .close, .fd = this.encode(), .from_libuv = true } else null; }, @@ -321,7 +321,12 @@ pub const FDImpl = packed struct { // If a non-number is given, returns null. // If the given number is not an fd (negative), an error is thrown and error.JSException is returned. pub fn fromJSValidated(value: JSValue, global: *JSC.JSGlobalObject) bun.JSError!?FDImpl { - if (!value.isAnyInt()) return null; + if (!value.isNumber()) { + return null; + } + if (!value.isAnyInt()) { + return global.ERR_OUT_OF_RANGE("The value of \"fd\" is out of range. It must be an integer. Received {}", .{bun.fmt.double(value.asNumber())}).throw(); + } const fd64 = value.toInt64(); try JSC.Node.Valid.fileDescriptor(fd64, global); const fd: i32 = @intCast(fd64); @@ -362,7 +367,7 @@ pub const FDImpl = packed struct { // ambiguous and almost certainly a mistake. You probably meant to format fd.cast(). // // Remember this formatter will - // - on posix, print the numebr + // - on posix, print the number // - on windows, print if it is a handle or a libuv file descriptor // - in debug on all platforms, print the path of the file descriptor // diff --git a/src/fmt.bind.ts b/src/fmt.bind.ts new file mode 100644 index 0000000000..cae52da42c --- /dev/null +++ b/src/fmt.bind.ts @@ -0,0 +1,15 @@ +import { fn, t } from "bindgen"; + +const implNamespace = "js_bindings"; + +export const Formatter = t.stringEnum("highlight-javascript", "escape-powershell"); + +export const fmtString = fn({ + implNamespace, + args: { + global: t.globalObject, + code: t.UTF8String, + formatter: Formatter, + }, + ret: t.DOMString, +}); diff --git a/src/fmt.zig b/src/fmt.zig index c42dc178b1..e946f2ae56 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -234,19 +234,28 @@ const JSONFormatter = struct { const JSONFormatterUTF8 = struct { input: []const u8, + opts: Options, + + pub const Options = struct { + quote: bool = true, + }; pub fn format(self: JSONFormatterUTF8, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try bun.js_printer.writeJSONString(self.input, @TypeOf(writer), writer, .utf8); + if (self.opts.quote) { + try bun.js_printer.writeJSONString(self.input, @TypeOf(writer), writer, .utf8); + } else { + try bun.js_printer.writePreQuotedString(self.input, @TypeOf(writer), writer, '"', false, true, .utf8); + } } }; /// Expects latin1 -pub fn formatJSONString(text: []const u8) JSONFormatter { +pub fn formatJSONStringLatin1(text: []const u8) JSONFormatter { return .{ .input = text }; } -pub fn formatJSONStringUTF8(text: []const u8) JSONFormatterUTF8 { - return .{ .input = text }; +pub fn formatJSONStringUTF8(text: []const u8, opts: JSONFormatterUTF8.Options) JSONFormatterUTF8 { + return .{ .input = text, .opts = opts }; } const SharedTempBuffer = [32 * 1024]u8; @@ -1717,86 +1726,79 @@ fn escapePowershellImpl(str: []const u8, comptime f: []const u8, _: std.fmt.Form try writer.writeAll(remain); } -pub const fmt_js_test_bindings = struct { - const Formatter = enum { - fmtJavaScript, - escapePowershell, - }; +pub const js_bindings = struct { + const gen = bun.gen.fmt; /// Internal function for testing in highlighter.test.ts - pub fn jsFunctionStringFormatter(globalThis: *bun.JSC.JSGlobalObject, callframe: *bun.JSC.CallFrame) bun.JSError!bun.JSC.JSValue { - const args = callframe.arguments_old(2); - if (args.len < 2) { - return globalThis.throwNotEnoughArguments("code", 1, 0); - } - - const code = try args.ptr[0].toSliceOrNull(globalThis); - defer code.deinit(); - + pub fn fmtString(global: *bun.JSC.JSGlobalObject, code: []const u8, formatter_id: gen.Formatter) bun.JSError!bun.String { var buffer = bun.MutableString.initEmpty(bun.default_allocator); defer buffer.deinit(); var writer = buffer.bufferedWriter(); - const formatter_id: Formatter = @enumFromInt(args.ptr[1].toInt32()); switch (formatter_id) { - .fmtJavaScript => { - const formatter = bun.fmt.fmtJavaScript(code.slice(), .{ + .highlight_javascript => { + const formatter = bun.fmt.fmtJavaScript(code, .{ .enable_colors = true, .check_for_unhighlighted_write = false, }); std.fmt.format(writer.writer(), "{}", .{formatter}) catch |err| { - return globalThis.throwError(err, "Error formatting"); + return global.throwError(err, "while formatting"); }; }, - .escapePowershell => { - std.fmt.format(writer.writer(), "{}", .{escapePowershell(code.slice())}) catch |err| { - return globalThis.throwError(err, "Error formatting"); + .escape_powershell => { + std.fmt.format(writer.writer(), "{}", .{escapePowershell(code)}) catch |err| { + return global.throwError(err, "while formatting"); }; }, } writer.flush() catch |err| { - return globalThis.throwError(err, "Error formatting"); + return global.throwError(err, "while formatting"); }; - var str = bun.String.createUTF8(buffer.list.items); - defer str.deref(); - return str.toJS(globalThis); + return bun.String.createUTF8(buffer.list.items); } }; +// Equivalent to ERR_OUT_OF_RANGE from fn NewOutOfRangeFormatter(comptime T: type) type { return struct { value: T, min: i64 = std.math.maxInt(i64), max: i64 = std.math.maxInt(i64), field_name: []const u8, + msg: []const u8 = "", pub fn format(self: @This(), comptime _: []const u8, _: fmt.FormatOptions, writer: anytype) !void { try writer.writeAll("The value of \""); try writer.writeAll(self.field_name); - try writer.writeAll("\" "); + try writer.writeAll("\" is out of range. It must be "); + const min = self.min; const max = self.max; + const msg = self.msg; if (min != std.math.maxInt(i64) and max != std.math.maxInt(i64)) { - try std.fmt.format(writer, "must be >= {d} and <= {d}.", .{ min, max }); + try std.fmt.format(writer, ">= {d} and <= {d}.", .{ min, max }); } else if (min != std.math.maxInt(i64)) { - try std.fmt.format(writer, "must be >= {d}.", .{min}); + try std.fmt.format(writer, ">= {d}.", .{min}); } else if (max != std.math.maxInt(i64)) { - try std.fmt.format(writer, "must be <= {d}.", .{max}); + try std.fmt.format(writer, "<= {d}.", .{max}); + } else if (msg.len > 0) { + try writer.writeAll(msg); + try writer.writeByte('.'); } else { - try writer.writeAll("must be within the range of values for type "); + try writer.writeAll("within the range of values for type "); try writer.writeAll(comptime @typeName(T)); try writer.writeAll("."); } if (comptime T == f64 or T == f32) { - try std.fmt.format(writer, " Received: {}", .{double(self.value)}); + try std.fmt.format(writer, " Received {}", .{double(self.value)}); } else if (comptime T == []const u8) { - try std.fmt.format(writer, " Received: {s}", .{self.value}); + try std.fmt.format(writer, " Received {s}", .{self.value}); } else { - try std.fmt.format(writer, " Received: {d}", .{self.value}); + try std.fmt.format(writer, " Received {d}", .{self.value}); } } }; @@ -1820,10 +1822,11 @@ pub const OutOfRangeOptions = struct { min: i64 = std.math.maxInt(i64), max: i64 = std.math.maxInt(i64), field_name: []const u8, + msg: []const u8 = "", }; pub fn outOfRange(value: anytype, options: OutOfRangeOptions) OutOfRangeFormatter(@TypeOf(value)) { - return .{ .value = value, .min = options.min, .max = options.max, .field_name = options.field_name }; + return .{ .value = value, .min = options.min, .max = options.max, .field_name = options.field_name, .msg = options.msg }; } /// esbuild has an 8 character truncation of a base32 encoded bytes. this diff --git a/src/fs.zig b/src/fs.zig index eec9ef34a8..fa20e43a52 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -13,7 +13,7 @@ const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const C = bun.C; const sync = @import("sync.zig"); -const Mutex = @import("./lock.zig").Lock; +const Mutex = bun.Mutex; const Semaphore = sync.Semaphore; const Fs = @This(); const path_handler = @import("./resolver/resolve_path.zig"); @@ -37,7 +37,7 @@ pub const Preallocate = struct { }; pub const FileSystem = struct { - top_level_dir: string, + top_level_dir: stringZ, // used on subsequent updates top_level_dir_buf: bun.PathBuffer = undefined, @@ -108,22 +108,14 @@ pub const FileSystem = struct { ENOTDIR, }; - pub fn init(top_level_dir: ?string) !*FileSystem { + pub fn init(top_level_dir: ?stringZ) !*FileSystem { return initWithForce(top_level_dir, false); } - pub fn initWithForce(top_level_dir_: ?string, comptime force: bool) !*FileSystem { + pub fn initWithForce(top_level_dir_: ?stringZ, comptime force: bool) !*FileSystem { const allocator = bun.fs_allocator; var top_level_dir = top_level_dir_ orelse (if (Environment.isBrowser) "/project/" else try bun.getcwdAlloc(allocator)); - - // Ensure there's a trailing separator in the top level directory - // This makes path resolution more reliable - if (!bun.path.isSepAny(top_level_dir[top_level_dir.len - 1])) { - const tld = try allocator.alloc(u8, top_level_dir.len + 1); - bun.copy(u8, tld, top_level_dir); - tld[tld.len - 1] = std.fs.path.sep; - top_level_dir = tld; - } + _ = &top_level_dir; if (!instance_loaded or force) { instance = FileSystem{ @@ -819,10 +811,19 @@ pub const FileSystem = struct { Limit.handles_before = limit; file_limit = limit.max; Limit.handles = file_limit; - if (limit.cur < limit.max or limit.max < 163840) { + const max_to_use: @TypeOf(limit.max) = if (Environment.isMusl) + // musl has extremely low defaults here, so we really want + // to enable this on musl or tests will start failing. + @max(limit.max, 163840) + else + // apparently, requesting too high of a number can cause other processes to not start. + // https://discord.com/channels/876711213126520882/1316342194176790609/1318175562367242271 + // https://github.com/postgres/postgres/blob/fee2b3ea2ecd0da0c88832b37ac0d9f6b3bfb9a9/src/backend/storage/file/fd.c#L1072 + limit.max; + if (limit.cur < max_to_use) { var new_limit = std.mem.zeroes(std.posix.rlimit); - new_limit.cur = @max(limit.max, 163840); - new_limit.max = @max(limit.max, 163840); + new_limit.cur = max_to_use; + new_limit.max = max_to_use; std.posix.setrlimit(resource, new_limit) catch break :blk; file_limit = new_limit.max; @@ -1712,15 +1713,20 @@ pub const Path = struct { pub fn isJSONCFile(this: *const Path) bool { const str = this.name.filename; - if (strings.eqlComptime(str, "package.json")) { + + if (strings.eqlComptime(str, "package.json") or strings.eqlComptime(str, "bun.lock")) { return true; } - if (!(strings.hasPrefixComptime(str, "tsconfig.") or strings.hasPrefixComptime(str, "jsconfig."))) { - return false; + if (strings.hasSuffixComptime(str, ".jsonc")) { + return true; } - return strings.hasSuffixComptime(str, ".json"); + if (strings.hasPrefixComptime(str, "tsconfig.") or strings.hasPrefixComptime(str, "jsconfig.")) { + return strings.hasSuffixComptime(str, ".json"); + } + + return false; } pub const PackageRelative = struct { diff --git a/src/futex.zig b/src/futex.zig index 49f4e83e77..24d66b056e 100644 --- a/src/futex.zig +++ b/src/futex.zig @@ -1,182 +1,142 @@ -//! Futex is a mechanism used to block (`wait`) and unblock (`wake`) threads using a 32bit memory address as hints. -//! Blocking a thread is acknowledged only if the 32bit memory address is equal to a given value. -//! This check helps avoid block/unblock deadlocks which occur if a `wake()` happens before a `wait()`. -//! Using Futex, other Thread synchronization primitives can be built which efficiently wait for cross-thread events or signals. - -// This is copy-pasted from Zig's source code to fix an issue with linking on macOS Catalina and earlier. +//! This is a copy-pasta of std.Thread.Futex, except without `unreachable` +//! A mechanism used to block (`wait`) and unblock (`wake`) threads using a +//! 32bit memory address as hints. +//! +//! Blocking a thread is acknowledged only if the 32bit memory address is equal +//! to a given value. This check helps avoid block/unblock deadlocks which +//! occur if a `wake()` happens before a `wait()`. +//! +//! Using Futex, other Thread synchronization primitives can be built which +//! efficiently wait for cross-thread events or signals. const std = @import("std"); -const bun = @import("root").bun; const builtin = @import("builtin"); const Futex = @This(); - -const target = builtin.target; -const single_threaded = builtin.single_threaded; - +const windows = std.os.windows; +const linux = std.os.linux; +const c = std.c; +const bun = @import("root").bun; const assert = bun.assert; -const testing = std.testing; - -const Atomic = std.atomic.Value; -const spinLoopHint = std.atomic.spinLoopHint; +const atomic = std.atomic; /// Checks if `ptr` still contains the value `expect` and, if so, blocks the caller until either: /// - The value at `ptr` is no longer equal to `expect`. /// - The caller is unblocked by a matching `wake()`. -/// - The caller is unblocked spuriously by an arbitrary internal signal. -/// -/// If `timeout` is provided, and the caller is blocked for longer than `timeout` nanoseconds`, `error.TimedOut` is returned. +/// - The caller is unblocked spuriously ("at random"). +/// - The caller blocks for longer than the given timeout. In which case, `error.Timeout` is returned. /// /// The checking of `ptr` and `expect`, along with blocking the caller, is done atomically /// and totally ordered (sequentially consistent) with respect to other wait()/wake() calls on the same `ptr`. -pub fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{TimedOut}!void { - if (single_threaded) { - // check whether the caller should block - if (ptr.raw != expect) { - return; - } +pub fn wait(ptr: *const atomic.Value(u32), expect: u32, timeout_ns: ?u64) error{Timeout}!void { + @setCold(true); - // There are no other threads which could notify the caller on single_threaded. - // Therefore a wait() without a timeout would block indefinitely. - const timeout_ns = timeout orelse { - @panic("deadlock"); - }; - - // Simulate blocking with the timeout knowing that: - // - no other thread can change the ptr value - // - no other thread could unblock us if we waiting on the ptr - std.time.sleep(timeout_ns); - return error.TimedOut; - } - - // Avoid calling into the OS for no-op waits() - if (timeout) |timeout_ns| { - if (timeout_ns == 0) { + // Avoid calling into the OS for no-op timeouts. + if (timeout_ns) |t| { + if (t == 0) { if (ptr.load(.seq_cst) != expect) return; - return error.TimedOut; + return error.Timeout; } } - return OsFutex.wait(ptr, expect, timeout); + return Impl.wait(ptr, expect, timeout_ns); } -/// Unblocks at most `num_waiters` callers blocked in a `wait()` call on `ptr`. -/// `num_waiters` of 1 unblocks at most one `wait(ptr, ...)` and `maxInt(u32)` unblocks effectively all `wait(ptr, ...)`. -pub fn wake(ptr: *const Atomic(u32), num_waiters: u32) void { - if (single_threaded) return; - if (num_waiters == 0) return; +pub fn waitForever(ptr: *const atomic.Value(u32), expect: u32) void { + @setCold(true); - return OsFutex.wake(ptr, num_waiters); + while (true) { + Impl.wait(ptr, expect, null) catch |err| switch (err) { + // Shouldn't happen, but people can override system calls sometimes. + error.Timeout => continue, + }; + break; + } } -const OsFutex = if (target.os.tag == .windows) - WindowsFutex -else if (target.os.tag == .linux) - LinuxFutex -else if (target.isDarwin()) - DarwinFutex -else if (builtin.link_libc) - PosixFutex +/// Unblocks at most `max_waiters` callers blocked in a `wait()` call on `ptr`. +pub fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void { + @setCold(true); + + // Avoid calling into the OS if there's nothing to wake up. + if (max_waiters == 0) { + return; + } + + Impl.wake(ptr, max_waiters); +} + +const Impl = if (builtin.os.tag == .windows) + WindowsImpl +else if (builtin.os.tag.isDarwin()) + DarwinImpl +else if (builtin.os.tag == .linux) + LinuxImpl +else if (builtin.target.isWasm()) + WasmImpl else - UnsupportedFutex; + UnsupportedImpl; -const UnsupportedFutex = struct { - fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{TimedOut}!void { +/// We can't do @compileError() in the `Impl` switch statement above as its eagerly evaluated. +/// So instead, we @compileError() on the methods themselves for platforms which don't support futex. +const UnsupportedImpl = struct { + fn wait(ptr: *const atomic.Value(u32), expect: u32, timeout: ?u64) error{Timeout}!void { return unsupported(.{ ptr, expect, timeout }); } - fn wake(ptr: *const Atomic(u32), num_waiters: u32) void { - return unsupported(.{ ptr, num_waiters }); + fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void { + return unsupported(.{ ptr, max_waiters }); } - fn unsupported(unused: anytype) noreturn { - _ = unused; - @compileError("Unsupported operating system: " ++ @tagName(target.os.tag)); + fn unsupported(_: anytype) noreturn { + @compileError("Unsupported operating system " ++ @tagName(builtin.target.os.tag)); } }; -const WindowsFutex = struct { - const windows = std.os.windows; - - fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{TimedOut}!void { +// We use WaitOnAddress through NtDll instead of API-MS-Win-Core-Synch-l1-2-0.dll +// as it's generally already a linked target and is autoloaded into all processes anyway. +const WindowsImpl = struct { + fn wait(ptr: *const atomic.Value(u32), expect: u32, timeout: ?u64) error{Timeout}!void { var timeout_value: windows.LARGE_INTEGER = undefined; var timeout_ptr: ?*const windows.LARGE_INTEGER = null; // NTDLL functions work with time in units of 100 nanoseconds. - // Positive values for timeouts are absolute time while negative is relative. - if (timeout) |timeout_ns| { + // Positive values are absolute deadlines while negative values are relative durations. + if (timeout) |delay| { + timeout_value = @as(windows.LARGE_INTEGER, @intCast(delay / 100)); + timeout_value = -timeout_value; timeout_ptr = &timeout_value; - timeout_value = -@as(windows.LARGE_INTEGER, @intCast(timeout_ns / 100)); } - switch (windows.ntdll.RtlWaitOnAddress( - @as(?*const anyopaque, @ptrCast(ptr)), - @as(?*const anyopaque, @ptrCast(&expect)), + const rc = windows.ntdll.RtlWaitOnAddress( + ptr, + &expect, @sizeOf(@TypeOf(expect)), timeout_ptr, - )) { + ); + + switch (rc) { .SUCCESS => {}, - .TIMEOUT => return error.TimedOut, - else => unreachable, + .TIMEOUT => { + assert(timeout != null); + return error.Timeout; + }, + else => @panic("Unexpected RtlWaitOnAddress() return code"), } } - fn wake(ptr: *const Atomic(u32), num_waiters: u32) void { - const address = @as(?*const anyopaque, @ptrCast(ptr)); - switch (num_waiters) { + fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void { + const address: ?*const anyopaque = ptr; + assert(max_waiters != 0); + + switch (max_waiters) { 1 => windows.ntdll.RtlWakeAddressSingle(address), else => windows.ntdll.RtlWakeAddressAll(address), } } }; -const LinuxFutex = struct { - const linux = std.os.linux; - - fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{TimedOut}!void { - var ts: std.posix.timespec = undefined; - var ts_ptr: ?*std.posix.timespec = null; - - // Futex timespec timeout is already in relative time. - if (timeout) |timeout_ns| { - ts_ptr = &ts; - ts.tv_sec = @as(@TypeOf(ts.tv_sec), @intCast(timeout_ns / std.time.ns_per_s)); - ts.tv_nsec = @as(@TypeOf(ts.tv_nsec), @intCast(timeout_ns % std.time.ns_per_s)); - } - - switch (bun.C.getErrno(linux.futex_wait( - @as(*const i32, @ptrCast(ptr)), - linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAIT, - @as(i32, @bitCast(expect)), - ts_ptr, - ))) { - .SUCCESS => {}, // notified by `wake()` - .INTR => {}, // spurious wakeup - .AGAIN => {}, // ptr.* != expect - .TIMEDOUT => return error.TimedOut, - .INVAL => {}, // possibly timeout overflow - .FAULT => unreachable, - else => unreachable, - } - } - - fn wake(ptr: *const Atomic(u32), num_waiters: u32) void { - switch (bun.C.getErrno(linux.futex_wake( - @as(*const i32, @ptrCast(ptr)), - linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAKE, - std.math.cast(i32, num_waiters) orelse std.math.maxInt(i32), - ))) { - .SUCCESS => {}, // successful wake up - .INVAL => {}, // invalid futex_wait() on ptr done elsewhere - .FAULT => {}, // pointer became invalid while doing the wake - else => unreachable, - } - } -}; - -const DarwinFutex = struct { - const darwin = std.c; - - fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{TimedOut}!void { +const DarwinImpl = struct { + fn wait(ptr: *const atomic.Value(u32), expect: u32, timeout: ?u64) error{Timeout}!void { // Darwin XNU 7195.50.7.100.1 introduced __ulock_wait2 and migrated code paths (notably pthread_cond_t) towards it: // https://github.com/apple/darwin-xnu/commit/d4061fb0260b3ed486147341b72468f836ed6c8f#diff-08f993cc40af475663274687b7c326cc6c3031e0db3ac8de7b24624610616be6 // @@ -185,218 +145,160 @@ const DarwinFutex = struct { // // ulock_wait() uses 32-bit micro-second timeouts where 0 = INFINITE or no-timeout // ulock_wait2() uses 64-bit nano-second timeouts (with the same convention) + const supports_ulock_wait2 = builtin.target.os.version_range.semver.min.major >= 11; + var timeout_ns: u64 = 0; - if (timeout) |timeout_value| { - // This should be checked by the caller. - assert(timeout_value != 0); - timeout_ns = timeout_value; + if (timeout) |delay| { + assert(delay != 0); // handled by timedWait() + timeout_ns = delay; } - const addr = @as(*const anyopaque, @ptrCast(ptr)); - const flags = darwin.UL_COMPARE_AND_WAIT | darwin.ULF_NO_ERRNO; + // If we're using `__ulock_wait` and `timeout` is too big to fit inside a `u32` count of // micro-seconds (around 70min), we'll request a shorter timeout. This is fine (users // should handle spurious wakeups), but we need to remember that we did so, so that - // we don't return `TimedOut` incorrectly. If that happens, we set this variable to + // we don't return `Timeout` incorrectly. If that happens, we set this variable to // true so that we we know to ignore the ETIMEDOUT result. var timeout_overflowed = false; + + const addr: *const anyopaque = ptr; + const flags = c.UL_COMPARE_AND_WAIT | c.ULF_NO_ERRNO; const status = blk: { - const timeout_us = cast: { - const timeout_u32 = std.math.cast(u32, timeout_ns / std.time.ns_per_us); - timeout_overflowed = timeout_u32 == null; - break :cast timeout_u32 orelse std.math.maxInt(u32); + if (supports_ulock_wait2) { + break :blk c.__ulock_wait2(flags, addr, expect, timeout_ns, 0); + } + + const timeout_us = std.math.cast(u32, timeout_ns / std.time.ns_per_us) orelse overflow: { + timeout_overflowed = true; + break :overflow std.math.maxInt(u32); }; - break :blk darwin.__ulock_wait(flags, addr, expect, timeout_us); + + break :blk c.__ulock_wait(flags, addr, expect, timeout_us); }; if (status >= 0) return; - switch (@as(std.posix.E, @enumFromInt(-status))) { + switch (@as(c.E, @enumFromInt(-status))) { + // Wait was interrupted by the OS or other spurious signalling. .INTR => {}, - // Address of the futex is paged out. This is unlikely, but possible in theory, and + // Address of the futex was paged out. This is unlikely, but possible in theory, and // pthread/libdispatch on darwin bother to handle it. In this case we'll return // without waiting, but the caller should retry anyway. .FAULT => {}, - .TIMEDOUT => if (!timeout_overflowed) return error.TimedOut, - else => unreachable, + // Only report Timeout if we didn't have to cap the timeout + .TIMEDOUT => { + assert(timeout != null); + if (!timeout_overflowed) return error.Timeout; + }, + else => @panic("Unexpected __ulock_wait() return code"), } } - fn wake(ptr: *const Atomic(u32), num_waiters: u32) void { - var flags: u32 = darwin.UL_COMPARE_AND_WAIT | darwin.ULF_NO_ERRNO; - if (num_waiters > 1) { - flags |= darwin.ULF_WAKE_ALL; - } + fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void { + const default_flags: u32 = c.UL_COMPARE_AND_WAIT | c.ULF_NO_ERRNO; + const flags: u32 = default_flags | (if (max_waiters > 1) c.ULF_WAKE_ALL else @as(u32, 0)); while (true) { - const addr = @as(*const anyopaque, @ptrCast(ptr)); - const status = darwin.__ulock_wake(flags, addr, 0); + const addr: *const anyopaque = ptr; + const status = c.__ulock_wake(flags, addr, 0); if (status >= 0) return; - switch (@as(std.posix.E, @enumFromInt(-status))) { + switch (@as(c.E, @enumFromInt(-status))) { .INTR => continue, // spurious wake() - .FAULT => continue, // address of the lock was paged out + .FAULT => @panic("__ulock_wake() returned EFAULT unexpectedly"), // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t .NOENT => return, // nothing was woken up - .ALREADY => unreachable, // only for ULF_WAKE_THREAD - else => unreachable, + .ALREADY => @panic("__ulock_wake() returned EALREADY unexpectedly"), // only for ULF_WAKE_THREAD + else => @panic("Unexpected __ulock_wake() return code"), } } } }; -const PosixFutex = struct { - fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{TimedOut}!void { - const address = @intFromPtr(ptr); - const bucket = Bucket.from(address); - var waiter: List.Node = undefined; - - { - assert(std.c.pthread_mutex_lock(&bucket.mutex) == .SUCCESS); - defer assert(std.c.pthread_mutex_unlock(&bucket.mutex) == .SUCCESS); - - if (ptr.load(.seq_cst) != expect) { - return; +// https://man7.org/linux/man-pages/man2/futex.2.html +const LinuxImpl = struct { + fn wait(ptr: *const atomic.Value(u32), expect: u32, timeout: ?u64) error{Timeout}!void { + const ts: linux.timespec = if (timeout) |timeout_ns| + .{ + .tv_sec = @intCast(timeout_ns / std.time.ns_per_s), + .tv_nsec = @intCast(timeout_ns % std.time.ns_per_s), } + else + undefined; - waiter.data = .{ .address = address }; - bucket.list.prepend(&waiter); - } + const rc = linux.futex_wait( + @as(*const i32, @ptrCast(&ptr.raw)), + linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAIT, + @as(i32, @bitCast(expect)), + if (timeout != null) &ts else null, + ); - var timed_out = false; - waiter.data.wait(timeout) catch { - defer if (!timed_out) { - waiter.data.wait(null) catch unreachable; - }; - - assert(std.c.pthread_mutex_lock(&bucket.mutex) == .SUCCESS); - defer assert(std.c.pthread_mutex_unlock(&bucket.mutex) == .SUCCESS); - - if (waiter.data.address == address) { - timed_out = true; - bucket.list.remove(&waiter); - } - }; - - waiter.data.deinit(); - if (timed_out) { - return error.TimedOut; + switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // spurious wakeup + .AGAIN => {}, // ptr.* != expect + .TIMEDOUT => { + assert(timeout != null); + return error.Timeout; + }, + .INVAL => {}, // possibly timeout overflow + .FAULT => @panic("futex_wait() returned EFAULT unexpectedly"), // ptr was invalid + else => |err| bun.Output.panic("Unexpected futex_wait() return code: {d} - {s}", .{ rc, std.enums.tagName(linux.E, err) orelse "unknown" }), } } - fn wake(ptr: *const Atomic(u32), num_waiters: u32) void { - const address = @intFromPtr(ptr); - const bucket = Bucket.from(address); - var can_notify = num_waiters; + fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void { + const rc = linux.futex_wake( + @as(*const i32, @ptrCast(&ptr.raw)), + linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAKE, + std.math.cast(i32, max_waiters) orelse std.math.maxInt(i32), + ); - var notified = List{}; - defer while (notified.popFirst()) |waiter| { - waiter.data.notify(); - }; - - assert(std.c.pthread_mutex_lock(&bucket.mutex) == .SUCCESS); - defer assert(std.c.pthread_mutex_unlock(&bucket.mutex) == .SUCCESS); - - var waiters = bucket.list.first; - while (waiters) |waiter| { - assert(waiter.data.address != null); - waiters = waiter.next; - - if (waiter.data.address != address) continue; - if (can_notify == 0) break; - can_notify -= 1; - - bucket.list.remove(waiter); - waiter.data.address = null; - notified.prepend(waiter); + switch (linux.E.init(rc)) { + .SUCCESS => {}, // successful wake up + .INVAL => {}, // invalid futex_wait() on ptr done elsewhere + .FAULT => @panic("futex_wake() returned EFAULT unexpectedly"), // pointer became invalid while doing the wake + else => @panic("Unexpected futex_wake() return code"), } } - - const Bucket = struct { - mutex: std.c.pthread_mutex_t = .{}, - list: List = .{}, - - var buckets = [_]Bucket{.{}} ** 64; - - fn from(address: usize) *Bucket { - return &buckets[address % buckets.len]; - } - }; - - const List = std.TailQueue(struct { - address: ?usize, - state: State = .empty, - cond: std.c.pthread_cond_t = .{}, - mutex: std.c.pthread_mutex_t = .{}, - - const Self = @This(); - const State = enum { - empty, - waiting, - notified, - }; - - fn deinit(self: *Self) void { - _ = std.c.pthread_cond_destroy(&self.cond); - _ = std.c.pthread_mutex_destroy(&self.mutex); - } - - fn wait(self: *Self, timeout: ?u64) error{TimedOut}!void { - assert(std.c.pthread_mutex_lock(&self.mutex) == .SUCCESS); - defer assert(std.c.pthread_mutex_unlock(&self.mutex) == .SUCCESS); - - switch (self.state) { - .empty => self.state = .waiting, - .waiting => unreachable, - .notified => return, - } - - var ts: std.posix.timespec = undefined; - var ts_ptr: ?*const std.posix.timespec = null; - if (timeout) |timeout_ns| { - ts_ptr = &ts; - std.posix.clock_gettime(std.posix.CLOCK_REALTIME, &ts) catch unreachable; - ts.tv_sec += @as(@TypeOf(ts.tv_sec), @intCast(timeout_ns / std.time.ns_per_s)); - ts.tv_nsec += @as(@TypeOf(ts.tv_nsec), @intCast(timeout_ns % std.time.ns_per_s)); - if (ts.tv_nsec >= std.time.ns_per_s) { - ts.tv_sec += 1; - ts.tv_nsec -= std.time.ns_per_s; - } - } - - while (true) { - switch (self.state) { - .empty => unreachable, - .waiting => {}, - .notified => return, - } - - const ts_ref = ts_ptr orelse { - assert(std.c.pthread_cond_wait(&self.cond, &self.mutex) == .SUCCESS); - continue; - }; - - const rc = std.c.pthread_cond_timedwait(&self.cond, &self.mutex, ts_ref); - switch (rc) { - .SUCCESS => {}, - .TIMEDOUT => { - self.state = .empty; - return error.TimedOut; - }, - else => unreachable, - } - } - } - - fn notify(self: *Self) void { - assert(std.c.pthread_mutex_lock(&self.mutex) == .SUCCESS); - defer assert(std.c.pthread_mutex_unlock(&self.mutex) == .SUCCESS); - - switch (self.state) { - .empty => self.state = .notified, - .waiting => { - self.state = .notified; - assert(std.c.pthread_cond_signal(&self.cond) == .SUCCESS); - }, - .notified => unreachable, - } - } - }); +}; + +const WasmImpl = struct { + fn wait(ptr: *const atomic.Value(u32), expect: u32, timeout: ?u64) error{Timeout}!void { + if (!comptime std.Target.wasm.featureSetHas(builtin.target.cpu.features, .atomics)) { + @compileError("WASI target missing cpu feature 'atomics'"); + } + const to: i64 = if (timeout) |to| @intCast(to) else -1; + const result = asm ( + \\local.get %[ptr] + \\local.get %[expected] + \\local.get %[timeout] + \\memory.atomic.wait32 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [expected] "r" (@as(i32, @bitCast(expect))), + [timeout] "r" (to), + ); + switch (result) { + 0 => {}, // ok + 1 => {}, // expected =! loaded + 2 => return error.Timeout, + else => @panic("Unexpected memory.atomic.wait32() return code"), + } + } + + fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void { + if (!comptime std.Target.wasm.featureSetHas(builtin.target.cpu.features, .atomics)) { + @compileError("WASI target missing cpu feature 'atomics'"); + } + assert(max_waiters != 0); + const woken_count = asm ( + \\local.get %[ptr] + \\local.get %[waiters] + \\memory.atomic.notify 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [waiters] "r" (max_waiters), + ); + _ = woken_count; // can be 0 when linker flag 'shared-memory' is not enabled + } }; diff --git a/src/glob.zig b/src/glob.zig index 3b97954620..09248b37ae 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -1,2200 +1,6 @@ -// Portions of this file are derived from works under the MIT License: -// -// Copyright (c) 2023 Devon Govett -// Copyright (c) 2023 Stephen Gregoratto -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -const std = @import("std"); -const bun = @import("root").bun; +pub const walk = @import("./glob/GlobWalker.zig"); +pub const Ascii = @import("./glob/ascii.zig"); -const eqlComptime = @import("./string_immutable.zig").eqlComptime; -const expect = std.testing.expect; -const isAllAscii = @import("./string_immutable.zig").isAllASCII; -const math = std.math; -const mem = std.mem; -const isWindows = @import("builtin").os.tag == .windows; - -const Allocator = std.mem.Allocator; -const Arena = std.heap.ArenaAllocator; -const ArrayList = std.ArrayListUnmanaged; -const ArrayListManaged = std.ArrayList; -const BunString = bun.String; -const C = @import("./c.zig"); -const CodepointIterator = @import("./string_immutable.zig").PackedCodepointIterator; -const Codepoint = CodepointIterator.Cursor.CodePointType; -const Dirent = @import("./bun.js/node/types.zig").Dirent; -const DirIterator = @import("./bun.js/node/dir_iterator.zig"); -const EntryKind = @import("./bun.js/node/types.zig").Dirent.Kind; -const GlobAscii = @import("./glob_ascii.zig"); -const JSC = bun.JSC; -const Maybe = JSC.Maybe; -const PathLike = @import("./bun.js/node/types.zig").PathLike; -const PathString = @import("./string_types.zig").PathString; -const ResolvePath = @import("./resolver/resolve_path.zig"); -const Syscall = bun.sys; -const ZigString = @import("./bun.js/bindings/bindings.zig").ZigString; - -// const Codepoint = u32; -const Cursor = CodepointIterator.Cursor; - -const log = bun.Output.scoped(.Glob, false); - -const CursorState = struct { - cursor: CodepointIterator.Cursor = .{}, - /// The index in terms of codepoints - // cp_idx: usize, - - fn init(iterator: *const CodepointIterator) CursorState { - var this_cursor: CodepointIterator.Cursor = .{}; - _ = iterator.next(&this_cursor); - return .{ - // .cp_idx = 0, - .cursor = this_cursor, - }; - } - - /// Return cursor pos of next codepoint without modifying the current. - /// - /// NOTE: If there is no next codepoint (cursor is at the last one), then - /// the returned cursor will have `c` as zero value and `i` will be >= - /// sourceBytes.len - fn peek(this: *const CursorState, iterator: *const CodepointIterator) CursorState { - var cpy = this.*; - // If outside of bounds - if (!iterator.next(&cpy.cursor)) { - // This will make `i >= sourceBytes.len` - cpy.cursor.i += cpy.cursor.width; - cpy.cursor.width = 1; - cpy.cursor.c = CodepointIterator.ZeroValue; - } - // cpy.cp_idx += 1; - return cpy; - } - - fn bump(this: *CursorState, iterator: *const CodepointIterator) void { - if (!iterator.next(&this.cursor)) { - this.cursor.i += this.cursor.width; - this.cursor.width = 1; - this.cursor.c = CodepointIterator.ZeroValue; - } - // this.cp_idx += 1; - } - - inline fn manualBumpAscii(this: *CursorState, i: u32, nextCp: Codepoint) void { - this.cursor.i += i; - this.cursor.c = nextCp; - this.cursor.width = 1; - } - - inline fn manualPeekAscii(this: *CursorState, i: u32, nextCp: Codepoint) CursorState { - return .{ - .cursor = CodepointIterator.Cursor{ - .i = this.cursor.i + i, - .c = @truncate(nextCp), - .width = 1, - }, - }; - } -}; - -pub const BunGlobWalker = GlobWalker_(null, SyscallAccessor, false); - -fn dummyFilterTrue(val: []const u8) bool { - _ = val; - return true; -} - -fn dummyFilterFalse(val: []const u8) bool { - _ = val; - return false; -} - -pub fn statatWindows(fd: bun.FileDescriptor, path: [:0]const u8) Maybe(bun.Stat) { - if (comptime !bun.Environment.isWindows) @compileError("oi don't use this"); - var buf: bun.PathBuffer = undefined; - const dir = switch (Syscall.getFdPath(fd, &buf)) { - .err => |e| return .{ .err = e }, - .result => |s| s, - }; - const parts: []const []const u8 = &.{ - dir[0..dir.len], - path, - }; - const statpath = ResolvePath.joinZBuf(&buf, parts, .auto); - return Syscall.stat(statpath); -} - -pub const SyscallAccessor = struct { - const count_fds = true; - - const Handle = struct { - value: bun.FileDescriptor, - - const zero = Handle{ .value = bun.FileDescriptor.zero }; - - pub fn isZero(this: Handle) bool { - return this.value == bun.FileDescriptor.zero; - } - - pub fn eql(this: Handle, other: Handle) bool { - return this.value == other.value; - } - }; - - const DirIter = struct { - value: DirIterator.WrappedIterator, - - pub inline fn next(self: *DirIter) Maybe(?DirIterator.IteratorResult) { - return self.value.next(); - } - - pub inline fn iterate(dir: Handle) DirIter { - return .{ .value = DirIterator.WrappedIterator.init(dir.value.asDir()) }; - } - }; - - pub fn open(path: [:0]const u8) !Maybe(Handle) { - return switch (Syscall.open(path, bun.O.DIRECTORY | bun.O.RDONLY, 0)) { - .err => |err| .{ .err = err }, - .result => |fd| .{ .result = Handle{ .value = fd } }, - }; - } - - pub fn statat(handle: Handle, path: [:0]const u8) Maybe(bun.Stat) { - if (comptime bun.Environment.isWindows) return statatWindows(handle.value, path); - return switch (Syscall.fstatat(handle.value, path)) { - .err => |err| .{ .err = err }, - .result => |s| .{ .result = s }, - }; - } - - pub fn openat(handle: Handle, path: [:0]const u8) !Maybe(Handle) { - return switch (Syscall.openat(handle.value, path, bun.O.DIRECTORY | bun.O.RDONLY, 0)) { - .err => |err| .{ .err = err }, - .result => |fd| .{ .result = Handle{ .value = fd } }, - }; - } - - pub fn close(handle: Handle) ?Syscall.Error { - return Syscall.close(handle.value); - } - - pub fn getcwd(path_buf: *bun.PathBuffer) Maybe([]const u8) { - return Syscall.getcwd(path_buf); - } -}; - -pub const DirEntryAccessor = struct { - const FS = bun.fs.FileSystem; - - const count_fds = false; - - const Handle = struct { - value: ?*FS.DirEntry, - - const zero = Handle{ .value = null }; - - pub fn isZero(this: Handle) bool { - return this.value == null; - } - - pub fn eql(this: Handle, other: Handle) bool { - // TODO this might not be quite right, we're comparing pointers, not the underlying directory - // On the other hand, DirEntries are only ever created once (per generation), so this should be fine? - // Realistically, as closing the handle is a no-op, this should be fine either way. - return this.value == other.value; - } - }; - - const DirIter = struct { - value: ?FS.DirEntry.EntryMap.Iterator, - - const IterResult = struct { - name: NameWrapper, - kind: std.fs.File.Kind, - - const NameWrapper = struct { - value: []const u8, - - pub fn slice(this: NameWrapper) []const u8 { - return this.value; - } - }; - }; - - pub inline fn next(self: *DirIter) Maybe(?IterResult) { - if (self.value) |*value| { - const nextval = value.next() orelse return .{ .result = null }; - const name = nextval.key_ptr.*; - const kind = nextval.value_ptr.*.kind(&FS.instance.fs, true); - const fskind = switch (kind) { - .file => std.fs.File.Kind.file, - .dir => std.fs.File.Kind.directory, - }; - return .{ - .result = .{ - .name = IterResult.NameWrapper{ .value = name }, - .kind = fskind, - }, - }; - } else { - return .{ .result = null }; - } - } - - pub inline fn iterate(dir: Handle) DirIter { - const entry = dir.value orelse return DirIter{ .value = null }; - return .{ .value = entry.data.iterator() }; - } - }; - - pub fn statat(handle: Handle, path_: [:0]const u8) Maybe(bun.Stat) { - var path: [:0]const u8 = path_; - var buf: bun.PathBuffer = undefined; - if (!bun.path.Platform.auto.isAbsolute(path)) { - if (handle.value) |entry| { - const slice = bun.path.joinStringBuf(&buf, [_][]const u8{ entry.dir, path }, .auto); - buf[slice.len] = 0; - path = buf[0..slice.len :0]; - } - } - return Syscall.stat(path); - } - - pub fn open(path: [:0]const u8) !Maybe(Handle) { - return openat(Handle.zero, path); - } - - pub fn openat(handle: Handle, path_: [:0]const u8) !Maybe(Handle) { - var path: []const u8 = path_; - var buf: bun.PathBuffer = undefined; - - if (!bun.path.Platform.auto.isAbsolute(path)) { - if (handle.value) |entry| { - path = bun.path.joinStringBuf(&buf, [_][]const u8{ entry.dir, path }, .auto); - } - } - // TODO do we want to propagate ENOTDIR through the 'Maybe' to match the SyscallAccessor? - // The glob implementation specifically checks for this error when dealing with symlinks - // return .{ .err = Syscall.Error.fromCode(bun.C.E.NOTDIR, Syscall.Tag.open) }; - const res = FS.instance.fs.readDirectory(path, null, 0, false) catch |err| { - return err; - }; - switch (res.*) { - .entries => |entry| { - return .{ .result = Handle{ .value = entry } }; - }, - .err => |err| { - return err.original_err; - }, - } - } - - pub inline fn close(handle: Handle) ?Syscall.Error { - // TODO is this a noop? - _ = handle; - return null; - } - - pub fn getcwd(path_buf: *bun.PathBuffer) Maybe([]const u8) { - @memcpy(path_buf, bun.fs.FileSystem.instance.fs.cwd); - } -}; - -pub fn GlobWalker_( - comptime ignore_filter_fn: ?*const fn ([]const u8) bool, - comptime Accessor: type, - comptime sentinel: bool, -) type { - const is_ignored: *const fn ([]const u8) bool = if (comptime ignore_filter_fn) |func| func else dummyFilterFalse; - - const count_fds = Accessor.count_fds and bun.Environment.isDebug; - - const stdJoin = comptime if (!sentinel) std.fs.path.join else std.fs.path.joinZ; - const bunJoin = comptime if (!sentinel) ResolvePath.join else ResolvePath.joinZ; - const MatchedPath = comptime if (!sentinel) []const u8 else [:0]const u8; - - return struct { - const GlobWalker = @This(); - pub const Result = Maybe(void); - - arena: Arena = undefined, - - /// not owned by this struct - pattern: []const u8 = "", - - pattern_codepoints: []u32 = &[_]u32{}, - cp_len: u32 = 0, - - /// If the pattern contains "./" or "../" - has_relative_components: bool = false, - - end_byte_of_basename_excluding_special_syntax: u32 = 0, - basename_excluding_special_syntax_component_idx: u32 = 0, - - patternComponents: ArrayList(Component) = .{}, - matchedPaths: MatchedMap = .{}, - i: u32 = 0, - - dot: bool = false, - absolute: bool = false, - - cwd: []const u8 = "", - follow_symlinks: bool = false, - error_on_broken_symlinks: bool = false, - only_files: bool = true, - - pathBuf: bun.PathBuffer = undefined, - // iteration state - workbuf: ArrayList(WorkItem) = ArrayList(WorkItem){}, - - /// Array hashmap used as a set (values are the keys) - /// to store matched paths and prevent duplicates - /// - /// BunString is used so that we can call BunString.toJSArray() - /// on the result of `.keys()` to give the result back to JS - /// - /// The only type of string impl we use is ZigString since - /// all matched paths are UTF-8 (DirIterator converts them on - /// windows) and allocated on the arnea - /// - /// Multiple patterns are not supported so right now this is - /// only possible when running a pattern like: - /// - /// `foo/**/*` - /// - /// Use `.keys()` to get the matched paths - const MatchedMap = std.ArrayHashMapUnmanaged(BunString, void, struct { - pub fn hash(_: @This(), this: BunString) u32 { - bun.assert(this.tag == .ZigString); - const slice = this.byteSlice(); - if (comptime sentinel) { - const slicez = slice[0 .. slice.len - 1 :0]; - return std.array_hash_map.hashString(slicez); - } - - return std.array_hash_map.hashString(slice); - } - - pub fn eql(_: @This(), this: BunString, other: BunString, _: usize) bool { - return this.eql(other); - } - }, true); - - /// The glob walker references the .directory.path so its not safe to - /// copy/move this - const IterState = union(enum) { - /// Pops the next item off the work stack - get_next, - - /// Currently iterating over a directory - directory: Directory, - - /// Two particular cases where this is used: - /// - /// 1. A pattern with no special glob syntax was supplied, for example: `/Users/zackradisic/foo/bar` - /// - /// In that case, the mere existence of the file/dir counts as a match, so we can eschew directory - /// iterating and walking for a simple stat call to the path. - /// - /// 2. Pattern ending in literal optimization - /// - /// With a pattern like: `packages/**/package.json`, once the iteration component index reaches - /// the final component, which is a literal string ("package.json"), we can similarly make a - /// single stat call to complete the pattern. - matched: MatchedPath, - - const Directory = struct { - fd: Accessor.Handle, - iter: Accessor.DirIter, - path: bun.PathBuffer, - dir_path: [:0]const u8, - - component_idx: u32, - pattern: *Component, - next_pattern: ?*Component, - is_last: bool, - - iter_closed: bool = false, - at_cwd: bool = false, - }; - }; - - pub const Iterator = struct { - walker: *GlobWalker, - iter_state: IterState = .get_next, - cwd_fd: Accessor.Handle = Accessor.Handle.zero, - empty_dir_path: [0:0]u8 = [0:0]u8{}, - /// This is to make sure in debug/tests that we are closing file descriptors - /// We should only have max 2 open at a time. One for the cwd, and one for the - /// directory being iterated on. - fds_open: if (count_fds) usize else u0 = 0, - - pub fn init(this: *Iterator) !Maybe(void) { - log("Iterator init pattern={s}", .{this.walker.pattern}); - var was_absolute = false; - const root_work_item = brk: { - var use_posix = bun.Environment.isPosix; - const is_absolute = if (bun.Environment.isPosix) std.fs.path.isAbsolute(this.walker.pattern) else std.fs.path.isAbsolute(this.walker.pattern) or is_absolute: { - use_posix = true; - break :is_absolute std.fs.path.isAbsolutePosix(this.walker.pattern); - }; - - if (!is_absolute) break :brk WorkItem.new(this.walker.cwd, 0, .directory); - - was_absolute = true; - - var path_without_special_syntax = this.walker.pattern[0..this.walker.end_byte_of_basename_excluding_special_syntax]; - var starting_component_idx = this.walker.basename_excluding_special_syntax_component_idx; - - if (path_without_special_syntax.len == 0) { - path_without_special_syntax = if (!bun.Environment.isWindows) "/" else ResolvePath.windowsFilesystemRoot(this.walker.cwd); - } else { - // Skip the components associated with the literal path - starting_component_idx += 1; - - // This means we got a pattern without any special glob syntax, for example: - // `/Users/zackradisic/foo/bar` - // - // In that case we don't need to do any walking and can just open up the FS entry - if (starting_component_idx >= this.walker.patternComponents.items.len) { - const path = try this.walker.arena.allocator().dupeZ(u8, path_without_special_syntax); - const fd = switch (try Accessor.open(path)) { - .err => |e| { - if (e.getErrno() == bun.C.E.NOTDIR) { - this.iter_state = .{ .matched = path }; - return Maybe(void).success; - } - // Doesn't exist - if (e.getErrno() == bun.C.E.NOENT) { - this.iter_state = .get_next; - return Maybe(void).success; - } - const errpath = try this.walker.arena.allocator().dupeZ(u8, path); - return .{ .err = e.withPath(errpath) }; - }, - .result => |fd| fd, - }; - _ = Accessor.close(fd); - this.iter_state = .{ .matched = path }; - return Maybe(void).success; - } - - // In the above branch, if `starting_compoennt_dix >= pattern_components.len` then - // it should also mean that `end_byte_of_basename_excluding_special_syntax >= pattern.len` - // - // So if we see that `end_byte_of_basename_excluding_special_syntax < this.walker.pattern.len` we - // miscalculated the values - bun.assert(this.walker.end_byte_of_basename_excluding_special_syntax < this.walker.pattern.len); - } - - break :brk WorkItem.new( - path_without_special_syntax, - starting_component_idx, - .directory, - ); - }; - - var path_buf: *bun.PathBuffer = &this.walker.pathBuf; - const root_path = root_work_item.path; - @memcpy(path_buf[0..root_path.len], root_path[0..root_path.len]); - path_buf[root_path.len] = 0; - const cwd_fd = switch (try Accessor.open(path_buf[0..root_path.len :0])) { - .err => |err| return .{ .err = this.walker.handleSysErrWithPath(err, @ptrCast(path_buf[0 .. root_path.len + 1])) }, - .result => |fd| fd, - }; - - if (comptime count_fds) { - this.fds_open += 1; - } - - this.cwd_fd = cwd_fd; - - switch (if (was_absolute) try this.transitionToDirIterState( - root_work_item, - false, - ) else try this.transitionToDirIterState( - root_work_item, - true, - )) { - .err => |err| return .{ .err = err }, - else => {}, - } - - return Maybe(void).success; - } - - pub fn deinit(this: *Iterator) void { - defer { - bun.debugAssert(this.fds_open == 0); - } - this.closeCwdFd(); - switch (this.iter_state) { - .directory => |dir| { - if (!dir.iter_closed) { - this.closeDisallowingCwd(dir.fd); - } - }, - else => {}, - } - - while (this.walker.workbuf.popOrNull()) |work_item| { - if (work_item.fd) |fd| { - this.closeDisallowingCwd(fd); - } - } - - if (comptime count_fds) { - bun.debugAssert(this.fds_open == 0); - } - } - - pub fn closeCwdFd(this: *Iterator) void { - if (this.cwd_fd.isZero()) return; - _ = Accessor.close(this.cwd_fd); - if (comptime count_fds) this.fds_open -= 1; - } - - pub fn closeDisallowingCwd(this: *Iterator, fd: Accessor.Handle) void { - if (fd.isZero() or fd.eql(this.cwd_fd)) return; - _ = Accessor.close(fd); - if (comptime count_fds) this.fds_open -= 1; - } - - pub fn bumpOpenFds(this: *Iterator) void { - if (comptime count_fds) { - this.fds_open += 1; - // If this is over 2 then this means that there is a bug in the iterator code - bun.debugAssert(this.fds_open <= 2); - } - } - - fn transitionToDirIterState( - this: *Iterator, - work_item: WorkItem, - comptime root: bool, - ) !Maybe(void) { - log("transition => {s}", .{work_item.path}); - this.iter_state = .{ .directory = .{ - .fd = Accessor.Handle.zero, - .iter = undefined, - .path = undefined, - .dir_path = undefined, - .component_idx = 0, - .pattern = undefined, - .next_pattern = null, - .is_last = false, - .iter_closed = false, - .at_cwd = false, - } }; - - var dir_path: [:0]u8 = dir_path: { - if (comptime root) { - if (!this.walker.absolute) { - this.iter_state.directory.path[0] = 0; - break :dir_path this.iter_state.directory.path[0..0 :0]; - } - } - // TODO Optimization: On posix systems filepaths are already null byte terminated so we can skip this if thats the case - @memcpy(this.iter_state.directory.path[0..work_item.path.len], work_item.path); - this.iter_state.directory.path[work_item.path.len] = 0; - break :dir_path this.iter_state.directory.path[0..work_item.path.len :0]; - }; - - var had_dot_dot = false; - const component_idx = this.walker.skipSpecialComponents(work_item.idx, &dir_path, &this.iter_state.directory.path, &had_dot_dot); - - const fd: Accessor.Handle = fd: { - if (work_item.fd) |fd| break :fd fd; - if (comptime root) { - if (had_dot_dot) break :fd switch (try Accessor.openat(this.cwd_fd, dir_path)) { - .err => |err| return .{ - .err = this.walker.handleSysErrWithPath(err, dir_path), - }, - .result => |fd_| brk: { - this.bumpOpenFds(); - break :brk fd_; - }, - }; - - this.iter_state.directory.at_cwd = true; - break :fd this.cwd_fd; - } - - break :fd switch (try Accessor.openat(this.cwd_fd, dir_path)) { - .err => |err| return .{ - .err = this.walker.handleSysErrWithPath(err, dir_path), - }, - .result => |fd_| brk: { - this.bumpOpenFds(); - break :brk fd_; - }, - }; - }; - - // Optimization: - // If we have a pattern like: - // `packages/*/package.json` - // ^ and we are at this component, with let's say - // a directory named: `packages/frontend/` - // - // Then we can just open `packages/frontend/package.json` without - // doing any iteration on the current directory. - // - // More generally, we can apply this optimization if we are on the - // last component and it is a literal with no special syntax. - if (component_idx == this.walker.patternComponents.items.len -| 1 and - this.walker.patternComponents.items[component_idx].syntax_hint == .Literal) - { - defer { - this.closeDisallowingCwd(fd); - } - const stackbuf_size = 256; - var stfb = std.heap.stackFallback(stackbuf_size, this.walker.arena.allocator()); - const pathz = try stfb.get().dupeZ(u8, this.walker.patternComponents.items[component_idx].patternSlice(this.walker.pattern)); - const stat_result: bun.Stat = switch (Accessor.statat(fd, pathz)) { - .err => |e_| { - var e: bun.sys.Error = e_; - if (e.getErrno() == bun.C.E.NOENT) { - this.iter_state = .get_next; - return Maybe(void).success; - } - return .{ .err = e.withPath(this.walker.patternComponents.items[component_idx].patternSlice(this.walker.pattern)) }; - }, - .result => |stat| stat, - }; - const matches = (bun.S.ISDIR(@intCast(stat_result.mode)) and !this.walker.only_files) or bun.S.ISREG(@intCast(stat_result.mode)) or !this.walker.only_files; - if (matches) { - if (try this.walker.prepareMatchedPath(pathz, dir_path)) |path| { - this.iter_state = .{ .matched = path }; - } else { - this.iter_state = .get_next; - } - } else { - this.iter_state = .get_next; - } - return Maybe(void).success; - } - - this.iter_state.directory.dir_path = dir_path; - this.iter_state.directory.component_idx = component_idx; - this.iter_state.directory.pattern = &this.walker.patternComponents.items[component_idx]; - this.iter_state.directory.next_pattern = if (component_idx + 1 < this.walker.patternComponents.items.len) &this.walker.patternComponents.items[component_idx + 1] else null; - this.iter_state.directory.is_last = component_idx == this.walker.patternComponents.items.len - 1; - this.iter_state.directory.at_cwd = false; - this.iter_state.directory.fd = Accessor.Handle.zero; - - log("Transition(dirpath={s}, fd={}, component_idx={d})", .{ dir_path, fd, component_idx }); - - this.iter_state.directory.fd = fd; - const iterator = Accessor.DirIter.iterate(fd); - this.iter_state.directory.iter = iterator; - this.iter_state.directory.iter_closed = false; - - return Maybe(void).success; - } - - pub fn next(this: *Iterator) !Maybe(?MatchedPath) { - while (true) { - switch (this.iter_state) { - .matched => |path| { - this.iter_state = .get_next; - return .{ .result = path }; - }, - .get_next => { - // Done - if (this.walker.workbuf.items.len == 0) return .{ .result = null }; - const work_item = this.walker.workbuf.pop(); - switch (work_item.kind) { - .directory => { - switch (try this.transitionToDirIterState(work_item, false)) { - .err => |err| return .{ .err = err }, - else => {}, - } - continue; - }, - .symlink => { - var scratch_path_buf: *bun.PathBuffer = &this.walker.pathBuf; - @memcpy(scratch_path_buf[0..work_item.path.len], work_item.path); - scratch_path_buf[work_item.path.len] = 0; - var symlink_full_path_z: [:0]u8 = scratch_path_buf[0..work_item.path.len :0]; - const entry_name = symlink_full_path_z[work_item.entry_start..symlink_full_path_z.len]; - - var has_dot_dot = false; - const component_idx = this.walker.skipSpecialComponents(work_item.idx, &symlink_full_path_z, scratch_path_buf, &has_dot_dot); - var pattern = this.walker.patternComponents.items[component_idx]; - const next_pattern = if (component_idx + 1 < this.walker.patternComponents.items.len) &this.walker.patternComponents.items[component_idx + 1] else null; - const is_last = component_idx == this.walker.patternComponents.items.len - 1; - - this.iter_state = .get_next; - const maybe_dir_fd: ?Accessor.Handle = switch (try Accessor.openat(this.cwd_fd, symlink_full_path_z)) { - .err => |err| brk: { - if (@as(usize, @intCast(err.errno)) == @as(usize, @intFromEnum(bun.C.E.NOTDIR))) { - break :brk null; - } - if (this.walker.error_on_broken_symlinks) return .{ .err = this.walker.handleSysErrWithPath(err, symlink_full_path_z) }; - // Broken symlink, but if `only_files` is false we still want to append - // it to the matched paths - if (!this.walker.only_files) { - // (See case A and B in the comment for `matchPatternFile()`) - // When we encounter a symlink we call the catch all - // matching function: `matchPatternImpl()` to see if we can avoid following the symlink. - // So for case A, we just need to check if the pattern is the last pattern. - if (is_last or - (pattern.syntax_hint == .Double and - component_idx + 1 == this.walker.patternComponents.items.len -| 1 and - next_pattern.?.syntax_hint != .Double and - this.walker.matchPatternImpl(next_pattern.?, entry_name))) - { - return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; - } - } - continue; - }, - .result => |fd| brk: { - this.bumpOpenFds(); - break :brk fd; - }, - }; - - const dir_fd = maybe_dir_fd orelse { - // No directory file descriptor, it's a file - if (is_last) - return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; - - if (pattern.syntax_hint == .Double and - component_idx + 1 == this.walker.patternComponents.items.len -| 1 and - next_pattern.?.syntax_hint != .Double and - this.walker.matchPatternImpl(next_pattern.?, entry_name)) - { - return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; - } - - continue; - }; - - var add_dir: bool = false; - // TODO this function calls `matchPatternImpl(pattern, - // entry_name)` which is redundant because we already called - // that when we first encountered the symlink - const recursion_idx_bump_ = this.walker.matchPatternDir(&pattern, next_pattern, entry_name, component_idx, is_last, &add_dir); - - if (recursion_idx_bump_) |recursion_idx_bump| { - if (recursion_idx_bump == 2) { - try this.walker.workbuf.append( - this.walker.arena.allocator(), - WorkItem.newWithFd(work_item.path, component_idx + recursion_idx_bump, .directory, dir_fd), - ); - try this.walker.workbuf.append( - this.walker.arena.allocator(), - WorkItem.newWithFd(work_item.path, component_idx, .directory, dir_fd), - ); - } else { - try this.walker.workbuf.append( - this.walker.arena.allocator(), - WorkItem.newWithFd(work_item.path, component_idx + recursion_idx_bump, .directory, dir_fd), - ); - } - } - - if (add_dir and !this.walker.only_files) { - return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; - } - - continue; - }, - } - }, - .directory => |*dir| { - const entry = switch (dir.iter.next()) { - .err => |err| { - if (!dir.at_cwd) this.closeDisallowingCwd(dir.fd); - dir.iter_closed = true; - return .{ .err = this.walker.handleSysErrWithPath(err, dir.dir_path) }; - }, - .result => |ent| ent, - } orelse { - if (!dir.at_cwd) this.closeDisallowingCwd(dir.fd); - dir.iter_closed = true; - this.iter_state = .get_next; - continue; - }; - log("dir: {s} entry: {s}", .{ dir.dir_path, entry.name.slice() }); - - const dir_iter_state: *const IterState.Directory = &this.iter_state.directory; - - const entry_name = entry.name.slice(); - switch (entry.kind) { - .file => { - const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern); - if (matches) { - const prepared = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue; - return .{ .result = prepared }; - } - continue; - }, - .directory => { - var add_dir: bool = false; - const recursion_idx_bump_ = this.walker.matchPatternDir(dir_iter_state.pattern, dir_iter_state.next_pattern, entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, &add_dir); - - if (recursion_idx_bump_) |recursion_idx_bump| { - const subdir_parts: []const []const u8 = &[_][]const u8{ - dir.dir_path[0..dir.dir_path.len], - entry_name, - }; - - const subdir_entry_name = try this.walker.join(subdir_parts); - - if (recursion_idx_bump == 2) { - try this.walker.workbuf.append( - this.walker.arena.allocator(), - WorkItem.new(subdir_entry_name, dir_iter_state.component_idx + recursion_idx_bump, .directory), - ); - try this.walker.workbuf.append( - this.walker.arena.allocator(), - WorkItem.new(subdir_entry_name, dir_iter_state.component_idx, .directory), - ); - } else { - try this.walker.workbuf.append( - this.walker.arena.allocator(), - WorkItem.new(subdir_entry_name, dir_iter_state.component_idx + recursion_idx_bump, .directory), - ); - } - } - - if (add_dir and !this.walker.only_files) { - const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue; - return .{ .result = prepared_path }; - } - - continue; - }, - .sym_link => { - if (this.walker.follow_symlinks) { - // Following a symlink requires additional syscalls, so - // we first try it against our "catch-all" pattern match - // function - const matches = this.walker.matchPatternImpl(dir_iter_state.pattern, entry_name); - if (!matches) continue; - - const subdir_parts: []const []const u8 = &[_][]const u8{ - dir.dir_path[0..dir.dir_path.len], - entry_name, - }; - const entry_start: u32 = @intCast(if (dir.dir_path.len == 0) 0 else dir.dir_path.len + 1); - - // const subdir_entry_name = try this.arena.allocator().dupe(u8, ResolvePath.join(subdir_parts, .auto)); - const subdir_entry_name = try this.walker.join(subdir_parts); - - try this.walker.workbuf.append( - this.walker.arena.allocator(), - WorkItem.newSymlink(subdir_entry_name, dir_iter_state.component_idx, entry_start), - ); - - continue; - } - - if (this.walker.only_files) continue; - - const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern); - if (matches) { - const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue; - return .{ .result = prepared_path }; - } - - continue; - }, - else => continue, - } - }, - } - } - } - }; - - const WorkItem = struct { - path: []const u8, - idx: u32, - kind: Kind, - entry_start: u32 = 0, - fd: ?Accessor.Handle = null, - - const Kind = enum { - directory, - symlink, - }; - - fn new(path: []const u8, idx: u32, kind: Kind) WorkItem { - return .{ - .path = path, - .idx = idx, - .kind = kind, - }; - } - - fn newWithFd(path: []const u8, idx: u32, kind: Kind, fd: Accessor.Handle) WorkItem { - return .{ - .path = path, - .idx = idx, - .kind = kind, - .fd = fd, - }; - } - - fn newSymlink(path: []const u8, idx: u32, entry_start: u32) WorkItem { - return .{ - .path = path, - .idx = idx, - .kind = .symlink, - .entry_start = entry_start, - }; - } - }; - - /// A component is each part of a glob pattern, separated by directory - /// separator: - /// `src/**/*.ts` -> `src`, `**`, `*.ts` - const Component = struct { - start: u32, - len: u32, - - syntax_hint: SyntaxHint = .None, - trailing_sep: bool = false, - is_ascii: bool = false, - - /// Only used when component is not ascii - unicode_set: bool = false, - start_cp: u32 = 0, - end_cp: u32 = 0, - - pub fn patternSlice(this: *const Component, pattern: []const u8) []const u8 { - return pattern[this.start .. this.start + this.len - @as(u1, @bitCast(this.trailing_sep))]; - } - - pub fn patternSliceCp(this: *const Component, pattern: []u32) []u32 { - return pattern[this.start_cp .. this.end_cp - @as(u1, @bitCast(this.trailing_sep))]; - } - - const SyntaxHint = enum { - None, - Single, - Double, - /// Uses special fast-path matching for components like: `*.ts` - WildcardFilepath, - /// Uses special fast-patch matching for literal components e.g. - /// "node_modules", becomes memcmp - Literal, - /// ./fixtures/*.ts - /// ^ - Dot, - /// ../ - DotBack, - - fn isSpecialSyntax(this: SyntaxHint) bool { - return switch (this) { - .Literal => false, - else => true, - }; - } - }; - }; - - /// The arena parameter is dereferenced and copied if all allocations go well and nothing goes wrong - pub fn init( - this: *GlobWalker, - arena: *Arena, - pattern: []const u8, - dot: bool, - absolute: bool, - follow_symlinks: bool, - error_on_broken_symlinks: bool, - only_files: bool, - ) !Maybe(void) { - return try this.initWithCwd( - arena, - pattern, - bun.fs.FileSystem.instance.top_level_dir, - dot, - absolute, - follow_symlinks, - error_on_broken_symlinks, - only_files, - ); - } - - pub fn convertUtf8ToCodepoints(codepoints: []u32, pattern: []const u8) void { - _ = bun.simdutf.convert.utf8.to.utf32.le(pattern, codepoints); - } - - pub fn debugPatternComopnents(this: *GlobWalker) void { - const pattern = this.pattern; - const components = &this.patternComponents; - const ptr = @intFromPtr(this); - log("GlobWalker(0x{x}) components:", .{ptr}); - for (components.items) |cmp| { - switch (cmp.syntax_hint) { - .Single => log(" *", .{}), - .Double => log(" **", .{}), - .Dot => log(" .", .{}), - .DotBack => log(" ../", .{}), - .Literal, .WildcardFilepath, .None => log(" hint={s} component_str={s}", .{ @tagName(cmp.syntax_hint), cmp.patternSlice(pattern) }), - } - } - } - - /// `cwd` should be allocated with the arena - /// The arena parameter is dereferenced and copied if all allocations go well and nothing goes wrong - pub fn initWithCwd( - this: *GlobWalker, - arena: *Arena, - pattern: []const u8, - cwd: []const u8, - dot: bool, - absolute: bool, - follow_symlinks: bool, - error_on_broken_symlinks: bool, - only_files: bool, - ) !Maybe(void) { - log("initWithCwd(cwd={s})", .{cwd}); - this.* = .{ - .cwd = cwd, - .pattern = pattern, - .dot = dot, - .absolute = absolute, - .follow_symlinks = follow_symlinks, - .error_on_broken_symlinks = error_on_broken_symlinks, - .only_files = only_files, - .basename_excluding_special_syntax_component_idx = 0, - .end_byte_of_basename_excluding_special_syntax = 0, - }; - - try GlobWalker.buildPatternComponents( - arena, - &this.patternComponents, - pattern, - &this.cp_len, - &this.pattern_codepoints, - &this.has_relative_components, - &this.end_byte_of_basename_excluding_special_syntax, - &this.basename_excluding_special_syntax_component_idx, - ); - - // copy arena after all allocations are successful - this.arena = arena.*; - - if (bun.Environment.allow_assert) { - this.debugPatternComopnents(); - } - - return Maybe(void).success; - } - - /// NOTE This also calls deinit on the arena, if you don't want to do that then - pub fn deinit(this: *GlobWalker, comptime clear_arena: bool) void { - log("GlobWalker.deinit", .{}); - if (comptime clear_arena) { - this.arena.deinit(); - } - } - - pub fn handleSysErrWithPath( - this: *GlobWalker, - err: Syscall.Error, - path_buf: [:0]const u8, - ) Syscall.Error { - std.mem.copyForwards(u8, this.pathBuf[0 .. path_buf.len + 1], @as([]const u8, @ptrCast(path_buf[0 .. path_buf.len + 1]))); - return err.withPath(this.pathBuf[0 .. path_buf.len + 1]); - } - - pub fn walk(this: *GlobWalker) !Maybe(void) { - if (this.patternComponents.items.len == 0) return Maybe(void).success; - - var iter = GlobWalker.Iterator{ .walker = this }; - defer iter.deinit(); - switch (try iter.init()) { - .err => |err| return .{ .err = err }, - else => {}, - } - - while (switch (try iter.next()) { - .err => |err| return .{ .err = err }, - .result => |matched_path| matched_path, - }) |path| { - log("walker: matched path: {s}", .{path}); - // The paths are already put into this.matchedPaths, which we use for the output, - // so we don't need to do anything here - } - - return Maybe(void).success; - } - - // NOTE you must check that the pattern at `idx` has `syntax_hint == .Dot` or - // `syntax_hint == .DotBack` first - fn collapseDots( - this: *GlobWalker, - idx: u32, - dir_path: *[:0]u8, - path_buf: *bun.PathBuffer, - encountered_dot_dot: *bool, - ) u32 { - var component_idx = idx; - var len = dir_path.len; - while (component_idx < this.patternComponents.items.len) { - switch (this.patternComponents.items[component_idx].syntax_hint) { - .Dot => { - defer component_idx += 1; - if (len + 2 >= bun.MAX_PATH_BYTES) @panic("Invalid path"); - if (len == 0) { - path_buf[len] = '.'; - path_buf[len + 1] = 0; - len += 1; - } else { - path_buf[len] = '/'; - path_buf[len + 1] = '.'; - path_buf[len + 2] = 0; - len += 2; - } - }, - .DotBack => { - defer component_idx += 1; - encountered_dot_dot.* = true; - if (dir_path.len + 3 >= bun.MAX_PATH_BYTES) @panic("Invalid path"); - if (len == 0) { - path_buf[len] = '.'; - path_buf[len + 1] = '.'; - path_buf[len + 2] = 0; - len += 2; - } else { - path_buf[len] = '/'; - path_buf[len + 1] = '.'; - path_buf[len + 2] = '.'; - path_buf[len + 3] = 0; - len += 3; - } - }, - else => break, - } - } - - dir_path.len = len; - - return component_idx; - } - - // NOTE you must check that the pattern at `idx` has `syntax_hint == .Double` first - fn collapseSuccessiveDoubleWildcards(this: *GlobWalker, idx: u32) u32 { - var component_idx = idx; - const pattern = this.patternComponents.items[idx]; - _ = pattern; - // Collapse successive double wildcards - while (component_idx + 1 < this.patternComponents.items.len and - this.patternComponents.items[component_idx + 1].syntax_hint == .Double) : (component_idx += 1) - {} - return component_idx; - } - - pub fn skipSpecialComponents( - this: *GlobWalker, - work_item_idx: u32, - dir_path: *[:0]u8, - scratch_path_buf: *bun.PathBuffer, - encountered_dot_dot: *bool, - ) u32 { - var component_idx = work_item_idx; - - // Skip `.` and `..` while also appending them to `dir_path` - component_idx = switch (this.patternComponents.items[component_idx].syntax_hint) { - .Dot => this.collapseDots( - component_idx, - dir_path, - scratch_path_buf, - encountered_dot_dot, - ), - .DotBack => this.collapseDots( - component_idx, - dir_path, - scratch_path_buf, - encountered_dot_dot, - ), - else => component_idx, - }; - - // Skip to the last `**` if there is a chain of them - component_idx = switch (this.patternComponents.items[component_idx].syntax_hint) { - .Double => this.collapseSuccessiveDoubleWildcards(component_idx), - else => component_idx, - }; - - return component_idx; - } - - fn matchPatternDir( - this: *GlobWalker, - pattern: *Component, - next_pattern: ?*Component, - entry_name: []const u8, - component_idx: u32, - is_last: bool, - add: *bool, - ) ?u32 { - if (!this.dot and GlobWalker.startsWithDot(entry_name)) return null; - if (is_ignored(entry_name)) return null; - - // Handle double wildcard `**`, this could possibly - // propagate the `**` to the directory's children - if (pattern.syntax_hint == .Double) { - // Stop the double wildcard if it matches the pattern afer it - // Example: src/**/*.js - // - Matches: src/bun.js/ - // src/bun.js/foo/bar/baz.js - if (!is_last and this.matchPatternImpl(next_pattern.?, entry_name)) { - // But if the next pattern is the last - // component, it should match and propagate the - // double wildcard recursion to the directory's - // children - if (component_idx + 1 == this.patternComponents.items.len - 1) { - add.* = true; - return 0; - } - - // In the normal case skip over the next pattern - // since we matched it, example: - // BEFORE: src/**/node_modules/**/*.js - // ^ - // AFTER: src/**/node_modules/**/*.js - // ^ - return 2; - } - - if (is_last) { - add.* = true; - } - - return 0; - } - - const matches = this.matchPatternImpl(pattern, entry_name); - if (matches) { - if (is_last) { - add.* = true; - return null; - } - return 1; - } - - return null; - } - - /// A file can only match if: - /// a) it matches against the last pattern, or - /// b) it matches the next pattern, provided the current - /// pattern is a double wildcard and the next pattern is - /// not a double wildcard - /// - /// Examples: - /// a -> `src/foo/index.ts` matches - /// b -> `src/**/*.ts` (on 2nd pattern) matches - fn matchPatternFile( - this: *GlobWalker, - entry_name: []const u8, - component_idx: u32, - is_last: bool, - pattern: *Component, - next_pattern: ?*Component, - ) bool { - if (pattern.trailing_sep) return false; - - // Handle case b) - if (!is_last) return pattern.syntax_hint == .Double and - component_idx + 1 == this.patternComponents.items.len -| 1 and - next_pattern.?.syntax_hint != .Double and - this.matchPatternImpl(next_pattern.?, entry_name); - - // Handle case a) - return this.matchPatternImpl(pattern, entry_name); - } - - fn matchPatternImpl( - this: *GlobWalker, - pattern_component: *Component, - filepath: []const u8, - ) bool { - log("matchPatternImpl: {s}", .{filepath}); - if (!this.dot and GlobWalker.startsWithDot(filepath)) return false; - if (is_ignored(filepath)) return false; - - return switch (pattern_component.syntax_hint) { - .Double, .Single => true, - .WildcardFilepath => if (comptime !isWindows) - matchWildcardFilepath(pattern_component.patternSlice(this.pattern), filepath) - else - this.matchPatternSlow(pattern_component, filepath), - .Literal => if (comptime !isWindows) - matchWildcardLiteral(pattern_component.patternSlice(this.pattern), filepath) - else - this.matchPatternSlow(pattern_component, filepath), - else => this.matchPatternSlow(pattern_component, filepath), - }; - } - - fn matchPatternSlow(this: *GlobWalker, pattern_component: *Component, filepath: []const u8) bool { - // windows filepaths are utf-16 so GlobAscii.match will never work - if (comptime !isWindows) { - if (pattern_component.is_ascii and isAllAscii(filepath)) - return GlobAscii.match( - pattern_component.patternSlice(this.pattern), - filepath, - ); - } - const codepoints = this.componentStringUnicode(pattern_component); - return matchImpl( - codepoints, - filepath, - ).matches(); - } - - fn componentStringUnicode(this: *GlobWalker, pattern_component: *Component) []const u32 { - if (comptime isWindows) { - return this.componentStringUnicodeWindows(pattern_component); - } else { - return this.componentStringUnicodePosix(pattern_component); - } - } - - fn componentStringUnicodeWindows(this: *GlobWalker, pattern_component: *Component) []const u32 { - return pattern_component.patternSliceCp(this.pattern_codepoints); - } - - fn componentStringUnicodePosix(this: *GlobWalker, pattern_component: *Component) []const u32 { - if (pattern_component.unicode_set) return pattern_component.patternSliceCp(this.pattern_codepoints); - - const codepoints = pattern_component.patternSliceCp(this.pattern_codepoints); - GlobWalker.convertUtf8ToCodepoints( - codepoints, - pattern_component.patternSlice(this.pattern), - ); - pattern_component.unicode_set = true; - return codepoints; - } - - inline fn matchedPathToBunString(matched_path: MatchedPath) BunString { - if (comptime sentinel) { - return BunString.fromBytes(matched_path[0 .. matched_path.len + 1]); - } - return BunString.fromBytes(matched_path); - } - - fn prepareMatchedPathSymlink(this: *GlobWalker, symlink_full_path: []const u8) !?MatchedPath { - const result = try this.matchedPaths.getOrPut(this.arena.allocator(), BunString.fromBytes(symlink_full_path)); - if (result.found_existing) { - log("(dupe) prepared match: {s}", .{symlink_full_path}); - return null; - } - if (comptime !sentinel) { - const slice = try this.arena.allocator().dupe(u8, symlink_full_path); - result.key_ptr.* = matchedPathToBunString(slice); - return slice; - } - const slicez = try this.arena.allocator().dupeZ(u8, symlink_full_path); - result.key_ptr.* = matchedPathToBunString(slicez); - return slicez; - } - - fn prepareMatchedPath(this: *GlobWalker, entry_name: []const u8, dir_name: []const u8) !?MatchedPath { - const subdir_parts: []const []const u8 = &[_][]const u8{ - dir_name[0..dir_name.len], - entry_name, - }; - const name_matched_path = try this.join(subdir_parts); - const name = matchedPathToBunString(name_matched_path); - const result = try this.matchedPaths.getOrPutValue(this.arena.allocator(), name, {}); - if (result.found_existing) { - log("(dupe) prepared match: {s}", .{name_matched_path}); - this.arena.allocator().free(name_matched_path); - return null; - } - result.key_ptr.* = name; - // if (comptime sentinel) return name[0 .. name.len - 1 :0]; - log("prepared match: {s}", .{name_matched_path}); - return name_matched_path; - } - - fn appendMatchedPath( - this: *GlobWalker, - entry_name: []const u8, - dir_name: [:0]const u8, - ) !void { - const subdir_parts: []const []const u8 = &[_][]const u8{ - dir_name[0..dir_name.len], - entry_name, - }; - const name_matched_path = try this.join(subdir_parts); - const name = matchedPathToBunString(name_matched_path); - const result = try this.matchedPaths.getOrPut(this.arena.allocator(), name); - if (result.found_existing) { - this.arena.allocator().free(name_matched_path); - log("(dupe) prepared match: {s}", .{name_matched_path}); - return; - } - result.key_ptr.* = name; - } - - fn appendMatchedPathSymlink(this: *GlobWalker, symlink_full_path: []const u8) !void { - const name = try this.arena.allocator().dupe(u8, symlink_full_path); - try this.matchedPaths.put(this.arena.allocator(), BunString.fromBytes(name), {}); - } - - inline fn join(this: *GlobWalker, subdir_parts: []const []const u8) !MatchedPath { - if (!this.absolute) { - // If relative paths enabled, stdlib join is preferred over - // ResolvePath.joinBuf because it doesn't try to normalize the path - return try stdJoin(this.arena.allocator(), subdir_parts); - } - - const out = try this.arena.allocator().dupe(u8, bunJoin(subdir_parts, .auto)); - if (comptime sentinel) return out[0 .. out.len - 1 :0]; - - return out; - } - - inline fn startsWithDot(filepath: []const u8) bool { - return filepath.len > 0 and filepath[0] == '.'; - } - - fn checkSpecialSyntax(pattern: []const u8) bool { - if (pattern.len < 16) { - for (pattern[0..]) |c| { - switch (c) { - '*', '[', '{', '?', '!' => return true, - else => {}, - } - } - return false; - } - - const syntax_tokens = comptime [_]u8{ '*', '[', '{', '?', '!' }; - const needles: [syntax_tokens.len]@Vector(16, u8) = comptime needles: { - var needles: [syntax_tokens.len]@Vector(16, u8) = undefined; - for (syntax_tokens, 0..) |tok, i| { - needles[i] = @splat(tok); - } - break :needles needles; - }; - - var i: usize = 0; - while (i + 16 <= pattern.len) : (i += 16) { - const haystack: @Vector(16, u8) = pattern[i..][0..16].*; - inline for (needles) |needle| { - if (std.simd.firstTrue(needle == haystack) != null) return true; - } - } - - if (i < pattern.len) { - for (pattern[i..]) |c| { - inline for (syntax_tokens) |tok| { - if (c == tok) return true; - } - } - } - - return false; - } - - fn makeComponent( - pattern: []const u8, - start_cp: u32, - end_cp: u32, - start_byte: u32, - end_byte: u32, - has_relative_patterns: *bool, - ) ?Component { - var component: Component = .{ - .start = start_byte, - .len = end_byte - start_byte, - .start_cp = start_cp, - .end_cp = end_cp, - }; - if (component.len == 0) return null; - - out: { - if (component.len == 1 and pattern[component.start] == '.') { - component.syntax_hint = .Dot; - has_relative_patterns.* = true; - break :out; - } - if (component.len == 2 and pattern[component.start] == '.' and pattern[component.start] == '.') { - component.syntax_hint = .DotBack; - has_relative_patterns.* = true; - break :out; - } - - if (!GlobWalker.checkSpecialSyntax(pattern[component.start .. component.start + component.len])) { - component.syntax_hint = .Literal; - break :out; - } - - switch (component.len) { - 1 => { - if (pattern[component.start] == '*') { - component.syntax_hint = .Single; - } - break :out; - }, - 2 => { - if (pattern[component.start] == '*' and pattern[component.start + 1] == '*') { - component.syntax_hint = .Double; - break :out; - } - }, - else => {}, - } - - out_of_check_wildcard_filepath: { - if (component.len > 1 and - pattern[component.start] == '*' and - pattern[component.start + 1] == '.' and - component.start + 2 < pattern.len) - { - for (pattern[component.start + 2 ..]) |c| { - switch (c) { - // The fast path checks that path[1..] == pattern[1..], - // this will obviously not work if additional - // glob syntax is present in the pattern, so we - // must not apply this optimization if we see - // special glob syntax. - // - // This is not a complete check, there can be - // false negatives, but that's okay, it just - // means we don't apply the optimization. - // - // We also don't need to look for the `!` token, - // because that only applies negation if at the - // beginning of the string. - '[', '{', '?', '*' => break :out_of_check_wildcard_filepath, - else => {}, - } - } - component.syntax_hint = .WildcardFilepath; - break :out; - } - } - } - - if (component.syntax_hint != .Single and component.syntax_hint != .Double) { - if (isAllAscii(pattern[component.start .. component.start + component.len])) { - component.is_ascii = true; - } - } else { - component.is_ascii = true; - } - - if (pattern[component.start + component.len -| 1] == '/') { - component.trailing_sep = true; - } else if (comptime bun.Environment.isWindows) { - component.trailing_sep = pattern[component.start + component.len -| 1] == '\\'; - } - - return component; - } - - fn buildPatternComponents( - arena: *Arena, - patternComponents: *ArrayList(Component), - pattern: []const u8, - out_cp_len: *u32, - out_pattern_cp: *[]u32, - has_relative_patterns: *bool, - end_byte_of_basename_excluding_special_syntax: *u32, - basename_excluding_special_syntax_component_idx: *u32, - ) !void { - var start_cp: u32 = 0; - var start_byte: u32 = 0; - - const iter = CodepointIterator.init(pattern); - var cursor = CodepointIterator.Cursor{}; - - var cp_len: u32 = 0; - var prevIsBackslash = false; - var saw_special = false; - while (iter.next(&cursor)) : (cp_len += 1) { - const c = cursor.c; - - switch (c) { - '\\' => { - if (comptime isWindows) { - var end_cp = cp_len; - var end_byte = cursor.i; - // is last char - if (cursor.i + cursor.width == pattern.len) { - end_cp += 1; - end_byte += cursor.width; - } - if (makeComponent( - pattern, - start_cp, - end_cp, - start_byte, - end_byte, - has_relative_patterns, - )) |component| { - saw_special = saw_special or component.syntax_hint.isSpecialSyntax(); - if (!saw_special) { - basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); - end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; - } - try patternComponents.append(arena.allocator(), component); - } - start_cp = cp_len + 1; - start_byte = cursor.i + cursor.width; - continue; - } - - if (prevIsBackslash) { - prevIsBackslash = false; - continue; - } - - prevIsBackslash = true; - }, - '/' => { - var end_cp = cp_len; - var end_byte = cursor.i; - // is last char - if (cursor.i + cursor.width == pattern.len) { - end_cp += 1; - end_byte += cursor.width; - } - if (makeComponent( - pattern, - start_cp, - end_cp, - start_byte, - end_byte, - has_relative_patterns, - )) |component| { - saw_special = saw_special or component.syntax_hint.isSpecialSyntax(); - if (!saw_special) { - basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); - end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; - } - try patternComponents.append(arena.allocator(), component); - } - start_cp = cp_len + 1; - start_byte = cursor.i + cursor.width; - }, - // TODO: Support other escaping glob syntax - else => {}, - } - } - - out_cp_len.* = cp_len; - - const codepoints = try arena.allocator().alloc(u32, cp_len); - // On Windows filepaths are UTF-16 so its better to fill the codepoints buffer upfront - if (comptime isWindows) { - GlobWalker.convertUtf8ToCodepoints(codepoints, pattern); - } - out_pattern_cp.* = codepoints; - - const end_cp = cp_len; - if (makeComponent( - pattern, - start_cp, - end_cp, - start_byte, - @intCast(pattern.len), - has_relative_patterns, - )) |component| { - saw_special = saw_special or component.syntax_hint.isSpecialSyntax(); - if (!saw_special) { - basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); - end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; - } - try patternComponents.append(arena.allocator(), component); - } else if (!saw_special) { - basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); - end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; - } - } - }; -} - -// From: https://github.com/The-King-of-Toasters/globlin -/// State for matching a glob against a string -pub const GlobState = struct { - // These store character indices into the glob and path strings. - path_index: CursorState = .{}, - glob_index: u32 = 0, - // When we hit a * or **, we store the state for backtracking. - wildcard: Wildcard = .{}, - globstar: Wildcard = .{}, - - fn init(path_iter: *const CodepointIterator) GlobState { - var this = GlobState{}; - // this.glob_index = CursorState.init(glob_iter); - this.path_index = CursorState.init(path_iter); - return this; - } - - fn skipBraces(self: *GlobState, glob: []const u32, stop_on_comma: bool) BraceState { - var braces: u32 = 1; - var in_brackets = false; - while (self.glob_index < glob.len and braces > 0) : (self.glob_index += 1) { - switch (glob[self.glob_index]) { - // Skip nested braces - '{' => if (!in_brackets) { - braces += 1; - }, - '}' => if (!in_brackets) { - braces -= 1; - }, - ',' => if (stop_on_comma and braces == 1 and !in_brackets) { - self.glob_index += 1; - return .Comma; - }, - '*', '?', '[' => |c| if (!in_brackets) { - if (c == '[') - in_brackets = true; - }, - ']' => in_brackets = false, - '\\' => self.glob_index += 1, - else => {}, - } - } - - if (braces != 0) - return .Invalid; - return .EndBrace; - } - - inline fn backtrack(self: *GlobState) void { - self.glob_index = self.wildcard.glob_index; - self.path_index = self.wildcard.path_index; - } -}; - -const Wildcard = struct { - // Using u32 rather than usize for these results in 10% faster performance. - // glob_index: CursorState = .{}, - glob_index: u32 = 0, - path_index: CursorState = .{}, -}; - -const BraceState = enum { Invalid, Comma, EndBrace }; - -const BraceStack = struct { - stack: [10]GlobState = undefined, - len: u32 = 0, - longest_brace_match: CursorState = .{}, - - inline fn push(self: *BraceStack, state: *const GlobState) GlobState { - self.stack[self.len] = state.*; - self.len += 1; - return GlobState{ - .path_index = state.path_index, - .glob_index = state.glob_index + 1, - }; - } - - inline fn pop(self: *BraceStack, state: *const GlobState) GlobState { - self.len -= 1; - const s = GlobState{ - .glob_index = state.glob_index, - .path_index = self.longest_brace_match, - // Restore star state if needed later. - .wildcard = self.stack[self.len].wildcard, - .globstar = self.stack[self.len].globstar, - }; - if (self.len == 0) - self.longest_brace_match = .{}; - return s; - } - - inline fn last(self: *const BraceStack) *const GlobState { - return &self.stack[self.len - 1]; - } -}; - -pub const MatchResult = enum { - no_match, - match, - - negate_no_match, - negate_match, - - pub fn matches(this: MatchResult) bool { - return this == .match or this == .negate_match; - } -}; - -/// This function checks returns a boolean value if the pathname `path` matches -/// the pattern `glob`. -/// -/// The supported pattern syntax for `glob` is: -/// -/// "?" -/// Matches any single character. -/// "*" -/// Matches zero or more characters, except for path separators ('/' or '\'). -/// "**" -/// Matches zero or more characters, including path separators. -/// Must match a complete path segment, i.e. followed by a path separator or -/// at the end of the pattern. -/// "[ab]" -/// Matches one of the characters contained in the brackets. -/// Character ranges (e.g. "[a-z]") are also supported. -/// Use "[!ab]" or "[^ab]" to match any character *except* those contained -/// in the brackets. -/// "{a,b}" -/// Match one of the patterns contained in the braces. -/// Any of the wildcards listed above can be used in the sub patterns. -/// Braces may be nested up to 10 levels deep. -/// "!" -/// Negates the result when at the start of the pattern. -/// Multiple "!" characters negate the pattern multiple times. -/// "\" -/// Used to escape any of the special characters above. -pub fn matchImpl(glob: []const u32, path: []const u8) MatchResult { - const path_iter = CodepointIterator.init(path); - - // This algorithm is based on https://research.swtch.com/glob - var state = GlobState.init(&path_iter); - // Store the state when we see an opening '{' brace in a stack. - // Up to 10 nested braces are supported. - var brace_stack = BraceStack{}; - - // First, check if the pattern is negated with a leading '!' character. - // Multiple negations can occur. - var negated = false; - while (state.glob_index < glob.len and glob[state.glob_index] == '!') { - negated = !negated; - state.glob_index += 1; - } - - while (state.glob_index < glob.len or state.path_index.cursor.i < path.len) { - if (state.glob_index < glob.len) { - switch (glob[state.glob_index]) { - '*' => { - const is_globstar = state.glob_index + 1 < glob.len and glob[state.glob_index + 1] == '*'; - // const is_globstar = state.glob_index.cursor.i + state.glob_index.cursor.width < glob.len and - // state.glob_index.peek(&glob_iter).cursor.c == '*'; - if (is_globstar) { - // Coalesce multiple ** segments into one. - var index = state.glob_index + 2; - state.glob_index = skipGlobstars(glob, &index) - 2; - } - - state.wildcard.glob_index = state.glob_index; - state.wildcard.path_index = state.path_index.peek(&path_iter); - - // ** allows path separators, whereas * does not. - // However, ** must be a full path component, i.e. a/**/b not a**b. - if (is_globstar) { - // Skip wildcards - state.glob_index += 2; - - if (glob.len == state.glob_index) { - // A trailing ** segment without a following separator. - state.globstar = state.wildcard; - } else if (glob[state.glob_index] == '/' and - (state.glob_index < 3 or glob[state.glob_index - 3] == '/')) - { - // Matched a full /**/ segment. If the last character in the path was a separator, - // skip the separator in the glob so we search for the next character. - // In effect, this makes the whole segment optional so that a/**/b matches a/b. - if (state.path_index.cursor.i == 0 or - (state.path_index.cursor.i < path.len and - isSeparator(path[state.path_index.cursor.i - 1]))) - { - state.glob_index += 1; - } - - // The allows_sep flag allows separator characters in ** matches. - // one is a '/', which prevents a/**/b from matching a/bb. - state.globstar = state.wildcard; - } - } else { - state.glob_index += 1; - } - - // If we are in a * segment and hit a separator, - // either jump back to a previous ** or end the wildcard. - if (state.globstar.path_index.cursor.i != state.wildcard.path_index.cursor.i and - state.path_index.cursor.i < path.len and - isSeparator(state.path_index.cursor.c)) - { - // Special case: don't jump back for a / at the end of the glob. - if (state.globstar.path_index.cursor.i > 0 and state.path_index.cursor.i + state.path_index.cursor.width < path.len) { - state.glob_index = state.globstar.glob_index; - state.wildcard.glob_index = state.globstar.glob_index; - } else { - state.wildcard.path_index.cursor.i = 0; - } - } - - // If the next char is a special brace separator, - // skip to the end of the braces so we don't try to match it. - if (brace_stack.len > 0 and - state.glob_index < glob.len and - (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) - { - if (state.skipBraces(glob, false) == .Invalid) - return .no_match; // invalid pattern! - } - - continue; - }, - '?' => if (state.path_index.cursor.i < path.len) { - if (!isSeparator(state.path_index.cursor.c)) { - state.glob_index += 1; - state.path_index.bump(&path_iter); - continue; - } - }, - '[' => if (state.path_index.cursor.i < path.len) { - state.glob_index += 1; - const c = state.path_index.cursor.c; - - // Check if the character class is negated. - var class_negated = false; - if (state.glob_index < glob.len and - (glob[state.glob_index] == '^' or glob[state.glob_index] == '!')) - { - class_negated = true; - state.glob_index += 1; - } - - // Try each range. - var first = true; - var is_match = false; - while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { - var low = glob[state.glob_index]; - if (!unescape(&low, glob, &state.glob_index)) - return .no_match; // Invalid pattern - state.glob_index += 1; - - // If there is a - and the following character is not ], - // read the range end character. - const high = if (state.glob_index + 1 < glob.len and - glob[state.glob_index] == '-' and glob[state.glob_index + 1] != ']') - blk: { - state.glob_index += 1; - var h = glob[state.glob_index]; - if (!unescape(&h, glob, &state.glob_index)) - return .no_match; // Invalid pattern! - state.glob_index += 1; - break :blk h; - } else low; - - if (low <= c and c <= high) - is_match = true; - first = false; - } - if (state.glob_index >= glob.len) - return .no_match; // Invalid pattern! - state.glob_index += 1; - if (is_match != class_negated) { - state.path_index.bump(&path_iter); - continue; - } - }, - '{' => if (state.path_index.cursor.i < path.len) { - if (brace_stack.len >= brace_stack.stack.len) - return .no_match; // Invalid pattern! Too many nested braces. - - // Push old state to the stack, and reset current state. - state = brace_stack.push(&state); - continue; - }, - '}' => if (brace_stack.len > 0) { - // If we hit the end of the braces, we matched the last option. - brace_stack.longest_brace_match = if (state.path_index.cursor.i >= brace_stack.longest_brace_match.cursor.i) - state.path_index - else - brace_stack.longest_brace_match; - state.glob_index += 1; - state = brace_stack.pop(&state); - continue; - }, - ',' => if (brace_stack.len > 0) { - // If we hit a comma, we matched one of the options! - // But we still need to check the others in case there is a longer match. - brace_stack.longest_brace_match = if (state.path_index.cursor.i >= brace_stack.longest_brace_match.cursor.i) - state.path_index - else - brace_stack.longest_brace_match; - state.path_index = brace_stack.last().path_index; - state.glob_index += 1; - state.wildcard = Wildcard{}; - state.globstar = Wildcard{}; - continue; - }, - else => |c| if (state.path_index.cursor.i < path.len) { - var cc = c; - // Match escaped characters as literals. - if (!unescape(&cc, glob, &state.glob_index)) - return .no_match; // Invalid pattern; - - const is_match = if (cc == '/') - isSeparator(state.path_index.cursor.c) - else - state.path_index.cursor.c == cc; - - if (is_match) { - if (brace_stack.len > 0 and - state.glob_index > 0 and - glob[state.glob_index - 1] == '}') - { - brace_stack.longest_brace_match = state.path_index; - state = brace_stack.pop(&state); - } - state.glob_index += 1; - state.path_index.bump(&path_iter); - - // If this is not a separator, lock in the previous globstar. - if (cc != '/') - state.globstar.path_index.cursor.i = 0; - - continue; - } - }, - } - } - // If we didn't match, restore state to the previous star pattern. - if (state.wildcard.path_index.cursor.i > 0 and state.wildcard.path_index.cursor.i <= path.len) { - state.backtrack(); - continue; - } - - if (brace_stack.len > 0) { - // If in braces, find next option and reset path to index where we saw the '{' - switch (state.skipBraces(glob, true)) { - .Invalid => return .no_match, - .Comma => { - state.path_index = brace_stack.last().path_index; - continue; - }, - .EndBrace => {}, - } - - // Hit the end. Pop the stack. - // If we matched a previous option, use that. - if (brace_stack.longest_brace_match.cursor.i > 0) { - state = brace_stack.pop(&state); - continue; - } else { - // Didn't match. Restore state, and check if we need to jump back to a star pattern. - state = brace_stack.last().*; - brace_stack.len -= 1; - if (state.wildcard.path_index.cursor.i > 0 and state.wildcard.path_index.cursor.i <= path.len) { - state.backtrack(); - continue; - } - } - } - - return if (negated) .negate_match else .no_match; - } - - return if (!negated) .match else .negate_no_match; -} - -pub inline fn isSeparator(c: Codepoint) bool { - if (comptime @import("builtin").os.tag == .windows) return c == '/' or c == '\\'; - return c == '/'; -} - -inline fn unescape(c: *u32, glob: []const u32, glob_index: *u32) bool { - if (c.* == '\\') { - glob_index.* += 1; - if (glob_index.* >= glob.len) - return false; // Invalid pattern! - - c.* = switch (glob[glob_index.*]) { - 'a' => '\x61', - 'b' => '\x08', - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - else => |cc| cc, - }; - } - - return true; -} - -const GLOB_STAR_MATCH_STR: []const u32 = &[_]u32{ '/', '*', '*' }; -// src/**/**/foo.ts -inline fn skipGlobstars(glob: []const u32, glob_index: *u32) u32 { - // Coalesce multiple ** segments into one. - while (glob_index.* + 3 <= glob.len and - // std.mem.eql(u8, glob[glob_index.*..][0..3], "/**")) - std.mem.eql(u32, glob[glob_index.*..][0..3], GLOB_STAR_MATCH_STR)) - { - glob_index.* += 3; - } - - return glob_index.*; -} - -const MatchAscii = struct {}; - -pub fn matchWildcardFilepath(glob: []const u8, path: []const u8) bool { - const needle = glob[1..]; - const needle_len: u32 = @intCast(needle.len); - if (path.len < needle_len) return false; - return std.mem.eql(u8, needle, path[path.len - needle_len ..]); -} - -pub fn matchWildcardLiteral(literal: []const u8, path: []const u8) bool { - return std.mem.eql(u8, literal, path); -} - -/// Returns true if the given string contains glob syntax, -/// excluding those escaped with backslashes -/// TODO: this doesn't play nicely with Windows directory separator and -/// backslashing, should we just require the user to supply posix filepaths? -pub fn detectGlobSyntax(potential_pattern: []const u8) bool { - // Negation only allowed in the beginning of the pattern - if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; - - // In descending order of how popular the token is - const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; - - inline for (SPECIAL_SYNTAX) |token| { - var slice = potential_pattern[0..]; - while (slice.len > 0) { - if (std.mem.indexOfScalar(u8, slice, token)) |idx| { - // Check for even number of backslashes preceding the - // token to know that it's not escaped - var i = idx; - var backslash_count: u16 = 0; - - while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { - backslash_count += 1; - } - - if (backslash_count % 2 == 0) return true; - slice = slice[idx + 1 ..]; - } else break; - } - } - - return false; -} +pub const GlobWalker = walk.GlobWalker_; +pub const BunGlobWalker = GlobWalker(null, walk.SyscallAccessor, false); +pub const BunGlobWalkerZ = GlobWalker(null, walk.SyscallAccessor, true); diff --git a/src/glob/GlobWalker.zig b/src/glob/GlobWalker.zig new file mode 100644 index 0000000000..6498fbb7d4 --- /dev/null +++ b/src/glob/GlobWalker.zig @@ -0,0 +1,2191 @@ +// Portions of this file are derived from works under the MIT License: +// +// Copyright (c) 2023 Devon Govett +// Copyright (c) 2023 Stephen Gregoratto +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +const std = @import("std"); +const bun = @import("root").bun; + +const eqlComptime = @import("../string_immutable.zig").eqlComptime; +const expect = std.testing.expect; +const isAllAscii = @import("../string_immutable.zig").isAllASCII; +const math = std.math; +const mem = std.mem; +const isWindows = @import("builtin").os.tag == .windows; + +const Allocator = std.mem.Allocator; +const Arena = std.heap.ArenaAllocator; +const ArrayList = std.ArrayListUnmanaged; +const ArrayListManaged = std.ArrayList; +const BunString = bun.String; +const C = @import("../c.zig"); +const CodepointIterator = @import("../string_immutable.zig").PackedCodepointIterator; +const Codepoint = CodepointIterator.Cursor.CodePointType; +const Dirent = @import("../bun.js/node/types.zig").Dirent; +const DirIterator = @import("../bun.js/node/dir_iterator.zig"); +const EntryKind = @import("../bun.js/node/types.zig").Dirent.Kind; +const GlobAscii = @import("./ascii.zig"); +const JSC = bun.JSC; +const Maybe = JSC.Maybe; +const PathLike = @import("../bun.js/node/types.zig").PathLike; +const PathString = @import("../string_types.zig").PathString; +const ResolvePath = @import("../resolver/resolve_path.zig"); +const Syscall = bun.sys; +const ZigString = @import("../bun.js/bindings/bindings.zig").ZigString; + +// const Codepoint = u32; +const Cursor = CodepointIterator.Cursor; + +const log = bun.Output.scoped(.Glob, false); + +const CursorState = struct { + cursor: CodepointIterator.Cursor = .{}, + /// The index in terms of codepoints + // cp_idx: usize, + + fn init(iterator: *const CodepointIterator) CursorState { + var this_cursor: CodepointIterator.Cursor = .{}; + _ = iterator.next(&this_cursor); + return .{ + // .cp_idx = 0, + .cursor = this_cursor, + }; + } + + /// Return cursor pos of next codepoint without modifying the current. + /// + /// NOTE: If there is no next codepoint (cursor is at the last one), then + /// the returned cursor will have `c` as zero value and `i` will be >= + /// sourceBytes.len + fn peek(this: *const CursorState, iterator: *const CodepointIterator) CursorState { + var cpy = this.*; + // If outside of bounds + if (!iterator.next(&cpy.cursor)) { + // This will make `i >= sourceBytes.len` + cpy.cursor.i += cpy.cursor.width; + cpy.cursor.width = 1; + cpy.cursor.c = CodepointIterator.ZeroValue; + } + // cpy.cp_idx += 1; + return cpy; + } + + fn bump(this: *CursorState, iterator: *const CodepointIterator) void { + if (!iterator.next(&this.cursor)) { + this.cursor.i += this.cursor.width; + this.cursor.width = 1; + this.cursor.c = CodepointIterator.ZeroValue; + } + // this.cp_idx += 1; + } + + inline fn manualBumpAscii(this: *CursorState, i: u32, nextCp: Codepoint) void { + this.cursor.i += i; + this.cursor.c = nextCp; + this.cursor.width = 1; + } + + inline fn manualPeekAscii(this: *CursorState, i: u32, nextCp: Codepoint) CursorState { + return .{ + .cursor = CodepointIterator.Cursor{ + .i = this.cursor.i + i, + .c = @truncate(nextCp), + .width = 1, + }, + }; + } +}; + +fn dummyFilterTrue(val: []const u8) bool { + _ = val; + return true; +} + +fn dummyFilterFalse(val: []const u8) bool { + _ = val; + return false; +} + +pub fn statatWindows(fd: bun.FileDescriptor, path: [:0]const u8) Maybe(bun.Stat) { + if (comptime !bun.Environment.isWindows) @compileError("oi don't use this"); + var buf: bun.PathBuffer = undefined; + const dir = switch (Syscall.getFdPath(fd, &buf)) { + .err => |e| return .{ .err = e }, + .result => |s| s, + }; + const parts: []const []const u8 = &.{ + dir[0..dir.len], + path, + }; + const statpath = ResolvePath.joinZBuf(&buf, parts, .auto); + return Syscall.stat(statpath); +} + +pub const SyscallAccessor = struct { + const count_fds = true; + + const Handle = struct { + value: bun.FileDescriptor, + + const zero = Handle{ .value = bun.FileDescriptor.zero }; + + pub fn isZero(this: Handle) bool { + return this.value == bun.FileDescriptor.zero; + } + + pub fn eql(this: Handle, other: Handle) bool { + return this.value == other.value; + } + }; + + const DirIter = struct { + value: DirIterator.WrappedIterator, + + pub inline fn next(self: *DirIter) Maybe(?DirIterator.IteratorResult) { + return self.value.next(); + } + + pub inline fn iterate(dir: Handle) DirIter { + return .{ .value = DirIterator.WrappedIterator.init(dir.value.asDir()) }; + } + }; + + pub fn open(path: [:0]const u8) !Maybe(Handle) { + return switch (Syscall.open(path, bun.O.DIRECTORY | bun.O.RDONLY, 0)) { + .err => |err| .{ .err = err }, + .result => |fd| .{ .result = Handle{ .value = fd } }, + }; + } + + pub fn statat(handle: Handle, path: [:0]const u8) Maybe(bun.Stat) { + if (comptime bun.Environment.isWindows) return statatWindows(handle.value, path); + return switch (Syscall.fstatat(handle.value, path)) { + .err => |err| .{ .err = err }, + .result => |s| .{ .result = s }, + }; + } + + pub fn openat(handle: Handle, path: [:0]const u8) !Maybe(Handle) { + return switch (Syscall.openat(handle.value, path, bun.O.DIRECTORY | bun.O.RDONLY, 0)) { + .err => |err| .{ .err = err }, + .result => |fd| .{ .result = Handle{ .value = fd } }, + }; + } + + pub fn close(handle: Handle) ?Syscall.Error { + return Syscall.close(handle.value); + } + + pub fn getcwd(path_buf: *bun.PathBuffer) Maybe([]const u8) { + return Syscall.getcwd(path_buf); + } +}; + +pub const DirEntryAccessor = struct { + const FS = bun.fs.FileSystem; + + const count_fds = false; + + const Handle = struct { + value: ?*FS.DirEntry, + + const zero = Handle{ .value = null }; + + pub fn isZero(this: Handle) bool { + return this.value == null; + } + + pub fn eql(this: Handle, other: Handle) bool { + // TODO this might not be quite right, we're comparing pointers, not the underlying directory + // On the other hand, DirEntries are only ever created once (per generation), so this should be fine? + // Realistically, as closing the handle is a no-op, this should be fine either way. + return this.value == other.value; + } + }; + + const DirIter = struct { + value: ?FS.DirEntry.EntryMap.Iterator, + + const IterResult = struct { + name: NameWrapper, + kind: std.fs.File.Kind, + + const NameWrapper = struct { + value: []const u8, + + pub fn slice(this: NameWrapper) []const u8 { + return this.value; + } + }; + }; + + pub inline fn next(self: *DirIter) Maybe(?IterResult) { + if (self.value) |*value| { + const nextval = value.next() orelse return .{ .result = null }; + const name = nextval.key_ptr.*; + const kind = nextval.value_ptr.*.kind(&FS.instance.fs, true); + const fskind = switch (kind) { + .file => std.fs.File.Kind.file, + .dir => std.fs.File.Kind.directory, + }; + return .{ + .result = .{ + .name = IterResult.NameWrapper{ .value = name }, + .kind = fskind, + }, + }; + } else { + return .{ .result = null }; + } + } + + pub inline fn iterate(dir: Handle) DirIter { + const entry = dir.value orelse return DirIter{ .value = null }; + return .{ .value = entry.data.iterator() }; + } + }; + + pub fn statat(handle: Handle, path_: [:0]const u8) Maybe(bun.Stat) { + var path: [:0]const u8 = path_; + var buf: bun.PathBuffer = undefined; + if (!bun.path.Platform.auto.isAbsolute(path)) { + if (handle.value) |entry| { + const slice = bun.path.joinStringBuf(&buf, [_][]const u8{ entry.dir, path }, .auto); + buf[slice.len] = 0; + path = buf[0..slice.len :0]; + } + } + return Syscall.stat(path); + } + + pub fn open(path: [:0]const u8) !Maybe(Handle) { + return openat(Handle.zero, path); + } + + pub fn openat(handle: Handle, path_: [:0]const u8) !Maybe(Handle) { + var path: []const u8 = path_; + var buf: bun.PathBuffer = undefined; + + if (!bun.path.Platform.auto.isAbsolute(path)) { + if (handle.value) |entry| { + path = bun.path.joinStringBuf(&buf, [_][]const u8{ entry.dir, path }, .auto); + } + } + // TODO do we want to propagate ENOTDIR through the 'Maybe' to match the SyscallAccessor? + // The glob implementation specifically checks for this error when dealing with symlinks + // return .{ .err = Syscall.Error.fromCode(bun.C.E.NOTDIR, Syscall.Tag.open) }; + const res = FS.instance.fs.readDirectory(path, null, 0, false) catch |err| { + return err; + }; + switch (res.*) { + .entries => |entry| { + return .{ .result = Handle{ .value = entry } }; + }, + .err => |err| { + return err.original_err; + }, + } + } + + pub inline fn close(handle: Handle) ?Syscall.Error { + // TODO is this a noop? + _ = handle; + return null; + } + + pub fn getcwd(path_buf: *bun.PathBuffer) Maybe([]const u8) { + @memcpy(path_buf, bun.fs.FileSystem.instance.fs.cwd); + } +}; + +pub fn GlobWalker_( + comptime ignore_filter_fn: ?*const fn ([]const u8) bool, + comptime Accessor: type, + comptime sentinel: bool, +) type { + const is_ignored: *const fn ([]const u8) bool = if (comptime ignore_filter_fn) |func| func else dummyFilterFalse; + + const count_fds = Accessor.count_fds and bun.Environment.isDebug; + + const stdJoin = comptime if (!sentinel) std.fs.path.join else std.fs.path.joinZ; + const bunJoin = comptime if (!sentinel) ResolvePath.join else ResolvePath.joinZ; + const MatchedPath = comptime if (!sentinel) []const u8 else [:0]const u8; + + return struct { + const GlobWalker = @This(); + pub const Result = Maybe(void); + + arena: Arena = undefined, + + /// not owned by this struct + pattern: []const u8 = "", + + pattern_codepoints: []u32 = &[_]u32{}, + cp_len: u32 = 0, + + /// If the pattern contains "./" or "../" + has_relative_components: bool = false, + + end_byte_of_basename_excluding_special_syntax: u32 = 0, + basename_excluding_special_syntax_component_idx: u32 = 0, + + patternComponents: ArrayList(Component) = .{}, + matchedPaths: MatchedMap = .{}, + i: u32 = 0, + + dot: bool = false, + absolute: bool = false, + + cwd: []const u8 = "", + follow_symlinks: bool = false, + error_on_broken_symlinks: bool = false, + only_files: bool = true, + + pathBuf: bun.PathBuffer = undefined, + // iteration state + workbuf: ArrayList(WorkItem) = ArrayList(WorkItem){}, + + /// Array hashmap used as a set (values are the keys) + /// to store matched paths and prevent duplicates + /// + /// BunString is used so that we can call BunString.toJSArray() + /// on the result of `.keys()` to give the result back to JS + /// + /// The only type of string impl we use is ZigString since + /// all matched paths are UTF-8 (DirIterator converts them on + /// windows) and allocated on the arnea + /// + /// Multiple patterns are not supported so right now this is + /// only possible when running a pattern like: + /// + /// `foo/**/*` + /// + /// Use `.keys()` to get the matched paths + const MatchedMap = std.ArrayHashMapUnmanaged(BunString, void, struct { + pub fn hash(_: @This(), this: BunString) u32 { + bun.assert(this.tag == .ZigString); + const slice = this.byteSlice(); + if (comptime sentinel) { + const slicez = slice[0 .. slice.len - 1 :0]; + return std.array_hash_map.hashString(slicez); + } + + return std.array_hash_map.hashString(slice); + } + + pub fn eql(_: @This(), this: BunString, other: BunString, _: usize) bool { + return this.eql(other); + } + }, true); + + /// The glob walker references the .directory.path so its not safe to + /// copy/move this + const IterState = union(enum) { + /// Pops the next item off the work stack + get_next, + + /// Currently iterating over a directory + directory: Directory, + + /// Two particular cases where this is used: + /// + /// 1. A pattern with no special glob syntax was supplied, for example: `/Users/zackradisic/foo/bar` + /// + /// In that case, the mere existence of the file/dir counts as a match, so we can eschew directory + /// iterating and walking for a simple stat call to the path. + /// + /// 2. Pattern ending in literal optimization + /// + /// With a pattern like: `packages/**/package.json`, once the iteration component index reaches + /// the final component, which is a literal string ("package.json"), we can similarly make a + /// single stat call to complete the pattern. + matched: MatchedPath, + + const Directory = struct { + fd: Accessor.Handle, + iter: Accessor.DirIter, + path: bun.PathBuffer, + dir_path: [:0]const u8, + + component_idx: u32, + pattern: *Component, + next_pattern: ?*Component, + is_last: bool, + + iter_closed: bool = false, + at_cwd: bool = false, + }; + }; + + pub const Iterator = struct { + walker: *GlobWalker, + iter_state: IterState = .get_next, + cwd_fd: Accessor.Handle = Accessor.Handle.zero, + empty_dir_path: [0:0]u8 = [0:0]u8{}, + /// This is to make sure in debug/tests that we are closing file descriptors + /// We should only have max 2 open at a time. One for the cwd, and one for the + /// directory being iterated on. + fds_open: if (count_fds) usize else u0 = 0, + + pub fn init(this: *Iterator) !Maybe(void) { + log("Iterator init pattern={s}", .{this.walker.pattern}); + var was_absolute = false; + const root_work_item = brk: { + var use_posix = bun.Environment.isPosix; + const is_absolute = if (bun.Environment.isPosix) std.fs.path.isAbsolute(this.walker.pattern) else std.fs.path.isAbsolute(this.walker.pattern) or is_absolute: { + use_posix = true; + break :is_absolute std.fs.path.isAbsolutePosix(this.walker.pattern); + }; + + if (!is_absolute) break :brk WorkItem.new(this.walker.cwd, 0, .directory); + + was_absolute = true; + + var path_without_special_syntax = this.walker.pattern[0..this.walker.end_byte_of_basename_excluding_special_syntax]; + var starting_component_idx = this.walker.basename_excluding_special_syntax_component_idx; + + if (path_without_special_syntax.len == 0) { + path_without_special_syntax = if (!bun.Environment.isWindows) "/" else ResolvePath.windowsFilesystemRoot(this.walker.cwd); + } else { + // Skip the components associated with the literal path + starting_component_idx += 1; + + // This means we got a pattern without any special glob syntax, for example: + // `/Users/zackradisic/foo/bar` + // + // In that case we don't need to do any walking and can just open up the FS entry + if (starting_component_idx >= this.walker.patternComponents.items.len) { + const path = try this.walker.arena.allocator().dupeZ(u8, path_without_special_syntax); + const fd = switch (try Accessor.open(path)) { + .err => |e| { + if (e.getErrno() == bun.C.E.NOTDIR) { + this.iter_state = .{ .matched = path }; + return Maybe(void).success; + } + // Doesn't exist + if (e.getErrno() == bun.C.E.NOENT) { + this.iter_state = .get_next; + return Maybe(void).success; + } + const errpath = try this.walker.arena.allocator().dupeZ(u8, path); + return .{ .err = e.withPath(errpath) }; + }, + .result => |fd| fd, + }; + _ = Accessor.close(fd); + this.iter_state = .{ .matched = path }; + return Maybe(void).success; + } + + // In the above branch, if `starting_compoennt_dix >= pattern_components.len` then + // it should also mean that `end_byte_of_basename_excluding_special_syntax >= pattern.len` + // + // So if we see that `end_byte_of_basename_excluding_special_syntax < this.walker.pattern.len` we + // miscalculated the values + bun.assert(this.walker.end_byte_of_basename_excluding_special_syntax < this.walker.pattern.len); + } + + break :brk WorkItem.new( + path_without_special_syntax, + starting_component_idx, + .directory, + ); + }; + + var path_buf: *bun.PathBuffer = &this.walker.pathBuf; + const root_path = root_work_item.path; + @memcpy(path_buf[0..root_path.len], root_path[0..root_path.len]); + path_buf[root_path.len] = 0; + const cwd_fd = switch (try Accessor.open(path_buf[0..root_path.len :0])) { + .err => |err| return .{ .err = this.walker.handleSysErrWithPath(err, @ptrCast(path_buf[0 .. root_path.len + 1])) }, + .result => |fd| fd, + }; + + if (comptime count_fds) { + this.fds_open += 1; + } + + this.cwd_fd = cwd_fd; + + switch (if (was_absolute) try this.transitionToDirIterState( + root_work_item, + false, + ) else try this.transitionToDirIterState( + root_work_item, + true, + )) { + .err => |err| return .{ .err = err }, + else => {}, + } + + return Maybe(void).success; + } + + pub fn deinit(this: *Iterator) void { + defer { + bun.debugAssert(this.fds_open == 0); + } + this.closeCwdFd(); + switch (this.iter_state) { + .directory => |dir| { + if (!dir.iter_closed) { + this.closeDisallowingCwd(dir.fd); + } + }, + else => {}, + } + + while (this.walker.workbuf.popOrNull()) |work_item| { + if (work_item.fd) |fd| { + this.closeDisallowingCwd(fd); + } + } + + if (comptime count_fds) { + bun.debugAssert(this.fds_open == 0); + } + } + + pub fn closeCwdFd(this: *Iterator) void { + if (this.cwd_fd.isZero()) return; + _ = Accessor.close(this.cwd_fd); + if (comptime count_fds) this.fds_open -= 1; + } + + pub fn closeDisallowingCwd(this: *Iterator, fd: Accessor.Handle) void { + if (fd.isZero() or fd.eql(this.cwd_fd)) return; + _ = Accessor.close(fd); + if (comptime count_fds) this.fds_open -= 1; + } + + pub fn bumpOpenFds(this: *Iterator) void { + if (comptime count_fds) { + this.fds_open += 1; + // If this is over 2 then this means that there is a bug in the iterator code + bun.debugAssert(this.fds_open <= 2); + } + } + + fn transitionToDirIterState( + this: *Iterator, + work_item: WorkItem, + comptime root: bool, + ) !Maybe(void) { + log("transition => {s}", .{work_item.path}); + this.iter_state = .{ .directory = .{ + .fd = Accessor.Handle.zero, + .iter = undefined, + .path = undefined, + .dir_path = undefined, + .component_idx = 0, + .pattern = undefined, + .next_pattern = null, + .is_last = false, + .iter_closed = false, + .at_cwd = false, + } }; + + var dir_path: [:0]u8 = dir_path: { + if (comptime root) { + if (!this.walker.absolute) { + this.iter_state.directory.path[0] = 0; + break :dir_path this.iter_state.directory.path[0..0 :0]; + } + } + // TODO Optimization: On posix systems filepaths are already null byte terminated so we can skip this if thats the case + @memcpy(this.iter_state.directory.path[0..work_item.path.len], work_item.path); + this.iter_state.directory.path[work_item.path.len] = 0; + break :dir_path this.iter_state.directory.path[0..work_item.path.len :0]; + }; + + var had_dot_dot = false; + const component_idx = this.walker.skipSpecialComponents(work_item.idx, &dir_path, &this.iter_state.directory.path, &had_dot_dot); + + const fd: Accessor.Handle = fd: { + if (work_item.fd) |fd| break :fd fd; + if (comptime root) { + if (had_dot_dot) break :fd switch (try Accessor.openat(this.cwd_fd, dir_path)) { + .err => |err| return .{ + .err = this.walker.handleSysErrWithPath(err, dir_path), + }, + .result => |fd_| brk: { + this.bumpOpenFds(); + break :brk fd_; + }, + }; + + this.iter_state.directory.at_cwd = true; + break :fd this.cwd_fd; + } + + break :fd switch (try Accessor.openat(this.cwd_fd, dir_path)) { + .err => |err| return .{ + .err = this.walker.handleSysErrWithPath(err, dir_path), + }, + .result => |fd_| brk: { + this.bumpOpenFds(); + break :brk fd_; + }, + }; + }; + + // Optimization: + // If we have a pattern like: + // `packages/*/package.json` + // ^ and we are at this component, with let's say + // a directory named: `packages/frontend/` + // + // Then we can just open `packages/frontend/package.json` without + // doing any iteration on the current directory. + // + // More generally, we can apply this optimization if we are on the + // last component and it is a literal with no special syntax. + if (component_idx == this.walker.patternComponents.items.len -| 1 and + this.walker.patternComponents.items[component_idx].syntax_hint == .Literal) + { + defer { + this.closeDisallowingCwd(fd); + } + const stackbuf_size = 256; + var stfb = std.heap.stackFallback(stackbuf_size, this.walker.arena.allocator()); + const pathz = try stfb.get().dupeZ(u8, this.walker.patternComponents.items[component_idx].patternSlice(this.walker.pattern)); + const stat_result: bun.Stat = switch (Accessor.statat(fd, pathz)) { + .err => |e_| { + var e: bun.sys.Error = e_; + if (e.getErrno() == bun.C.E.NOENT) { + this.iter_state = .get_next; + return Maybe(void).success; + } + return .{ .err = e.withPath(this.walker.patternComponents.items[component_idx].patternSlice(this.walker.pattern)) }; + }, + .result => |stat| stat, + }; + const matches = (bun.S.ISDIR(@intCast(stat_result.mode)) and !this.walker.only_files) or bun.S.ISREG(@intCast(stat_result.mode)) or !this.walker.only_files; + if (matches) { + if (try this.walker.prepareMatchedPath(pathz, dir_path)) |path| { + this.iter_state = .{ .matched = path }; + } else { + this.iter_state = .get_next; + } + } else { + this.iter_state = .get_next; + } + return Maybe(void).success; + } + + this.iter_state.directory.dir_path = dir_path; + this.iter_state.directory.component_idx = component_idx; + this.iter_state.directory.pattern = &this.walker.patternComponents.items[component_idx]; + this.iter_state.directory.next_pattern = if (component_idx + 1 < this.walker.patternComponents.items.len) &this.walker.patternComponents.items[component_idx + 1] else null; + this.iter_state.directory.is_last = component_idx == this.walker.patternComponents.items.len - 1; + this.iter_state.directory.at_cwd = false; + this.iter_state.directory.fd = Accessor.Handle.zero; + + log("Transition(dirpath={s}, fd={}, component_idx={d})", .{ dir_path, fd, component_idx }); + + this.iter_state.directory.fd = fd; + const iterator = Accessor.DirIter.iterate(fd); + this.iter_state.directory.iter = iterator; + this.iter_state.directory.iter_closed = false; + + return Maybe(void).success; + } + + pub fn next(this: *Iterator) !Maybe(?MatchedPath) { + while (true) { + switch (this.iter_state) { + .matched => |path| { + this.iter_state = .get_next; + return .{ .result = path }; + }, + .get_next => { + // Done + if (this.walker.workbuf.items.len == 0) return .{ .result = null }; + const work_item = this.walker.workbuf.pop(); + switch (work_item.kind) { + .directory => { + switch (try this.transitionToDirIterState(work_item, false)) { + .err => |err| return .{ .err = err }, + else => {}, + } + continue; + }, + .symlink => { + var scratch_path_buf: *bun.PathBuffer = &this.walker.pathBuf; + @memcpy(scratch_path_buf[0..work_item.path.len], work_item.path); + scratch_path_buf[work_item.path.len] = 0; + var symlink_full_path_z: [:0]u8 = scratch_path_buf[0..work_item.path.len :0]; + const entry_name = symlink_full_path_z[work_item.entry_start..symlink_full_path_z.len]; + + var has_dot_dot = false; + const component_idx = this.walker.skipSpecialComponents(work_item.idx, &symlink_full_path_z, scratch_path_buf, &has_dot_dot); + var pattern = this.walker.patternComponents.items[component_idx]; + const next_pattern = if (component_idx + 1 < this.walker.patternComponents.items.len) &this.walker.patternComponents.items[component_idx + 1] else null; + const is_last = component_idx == this.walker.patternComponents.items.len - 1; + + this.iter_state = .get_next; + const maybe_dir_fd: ?Accessor.Handle = switch (try Accessor.openat(this.cwd_fd, symlink_full_path_z)) { + .err => |err| brk: { + if (@as(usize, @intCast(err.errno)) == @as(usize, @intFromEnum(bun.C.E.NOTDIR))) { + break :brk null; + } + if (this.walker.error_on_broken_symlinks) return .{ .err = this.walker.handleSysErrWithPath(err, symlink_full_path_z) }; + // Broken symlink, but if `only_files` is false we still want to append + // it to the matched paths + if (!this.walker.only_files) { + // (See case A and B in the comment for `matchPatternFile()`) + // When we encounter a symlink we call the catch all + // matching function: `matchPatternImpl()` to see if we can avoid following the symlink. + // So for case A, we just need to check if the pattern is the last pattern. + if (is_last or + (pattern.syntax_hint == .Double and + component_idx + 1 == this.walker.patternComponents.items.len -| 1 and + next_pattern.?.syntax_hint != .Double and + this.walker.matchPatternImpl(next_pattern.?, entry_name))) + { + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; + } + } + continue; + }, + .result => |fd| brk: { + this.bumpOpenFds(); + break :brk fd; + }, + }; + + const dir_fd = maybe_dir_fd orelse { + // No directory file descriptor, it's a file + if (is_last) + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; + + if (pattern.syntax_hint == .Double and + component_idx + 1 == this.walker.patternComponents.items.len -| 1 and + next_pattern.?.syntax_hint != .Double and + this.walker.matchPatternImpl(next_pattern.?, entry_name)) + { + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; + } + + continue; + }; + + var add_dir: bool = false; + // TODO this function calls `matchPatternImpl(pattern, + // entry_name)` which is redundant because we already called + // that when we first encountered the symlink + const recursion_idx_bump_ = this.walker.matchPatternDir(&pattern, next_pattern, entry_name, component_idx, is_last, &add_dir); + + if (recursion_idx_bump_) |recursion_idx_bump| { + if (recursion_idx_bump == 2) { + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.newWithFd(work_item.path, component_idx + recursion_idx_bump, .directory, dir_fd), + ); + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.newWithFd(work_item.path, component_idx, .directory, dir_fd), + ); + } else { + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.newWithFd(work_item.path, component_idx + recursion_idx_bump, .directory, dir_fd), + ); + } + } + + if (add_dir and !this.walker.only_files) { + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) orelse continue }; + } + + continue; + }, + } + }, + .directory => |*dir| { + const entry = switch (dir.iter.next()) { + .err => |err| { + if (!dir.at_cwd) this.closeDisallowingCwd(dir.fd); + dir.iter_closed = true; + return .{ .err = this.walker.handleSysErrWithPath(err, dir.dir_path) }; + }, + .result => |ent| ent, + } orelse { + if (!dir.at_cwd) this.closeDisallowingCwd(dir.fd); + dir.iter_closed = true; + this.iter_state = .get_next; + continue; + }; + log("dir: {s} entry: {s}", .{ dir.dir_path, entry.name.slice() }); + + const dir_iter_state: *const IterState.Directory = &this.iter_state.directory; + + const entry_name = entry.name.slice(); + switch (entry.kind) { + .file => { + const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern); + if (matches) { + const prepared = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue; + return .{ .result = prepared }; + } + continue; + }, + .directory => { + var add_dir: bool = false; + const recursion_idx_bump_ = this.walker.matchPatternDir(dir_iter_state.pattern, dir_iter_state.next_pattern, entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, &add_dir); + + if (recursion_idx_bump_) |recursion_idx_bump| { + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir.dir_path[0..dir.dir_path.len], + entry_name, + }; + + const subdir_entry_name = try this.walker.join(subdir_parts); + + if (recursion_idx_bump == 2) { + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.new(subdir_entry_name, dir_iter_state.component_idx + recursion_idx_bump, .directory), + ); + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.new(subdir_entry_name, dir_iter_state.component_idx, .directory), + ); + } else { + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.new(subdir_entry_name, dir_iter_state.component_idx + recursion_idx_bump, .directory), + ); + } + } + + if (add_dir and !this.walker.only_files) { + const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue; + return .{ .result = prepared_path }; + } + + continue; + }, + .sym_link => { + if (this.walker.follow_symlinks) { + // Following a symlink requires additional syscalls, so + // we first try it against our "catch-all" pattern match + // function + const matches = this.walker.matchPatternImpl(dir_iter_state.pattern, entry_name); + if (!matches) continue; + + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir.dir_path[0..dir.dir_path.len], + entry_name, + }; + const entry_start: u32 = @intCast(if (dir.dir_path.len == 0) 0 else dir.dir_path.len + 1); + + // const subdir_entry_name = try this.arena.allocator().dupe(u8, ResolvePath.join(subdir_parts, .auto)); + const subdir_entry_name = try this.walker.join(subdir_parts); + + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.newSymlink(subdir_entry_name, dir_iter_state.component_idx, entry_start), + ); + + continue; + } + + if (this.walker.only_files) continue; + + const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern); + if (matches) { + const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue; + return .{ .result = prepared_path }; + } + + continue; + }, + else => continue, + } + }, + } + } + } + }; + + const WorkItem = struct { + path: []const u8, + idx: u32, + kind: Kind, + entry_start: u32 = 0, + fd: ?Accessor.Handle = null, + + const Kind = enum { + directory, + symlink, + }; + + fn new(path: []const u8, idx: u32, kind: Kind) WorkItem { + return .{ + .path = path, + .idx = idx, + .kind = kind, + }; + } + + fn newWithFd(path: []const u8, idx: u32, kind: Kind, fd: Accessor.Handle) WorkItem { + return .{ + .path = path, + .idx = idx, + .kind = kind, + .fd = fd, + }; + } + + fn newSymlink(path: []const u8, idx: u32, entry_start: u32) WorkItem { + return .{ + .path = path, + .idx = idx, + .kind = .symlink, + .entry_start = entry_start, + }; + } + }; + + /// A component is each part of a glob pattern, separated by directory + /// separator: + /// `src/**/*.ts` -> `src`, `**`, `*.ts` + const Component = struct { + start: u32, + len: u32, + + syntax_hint: SyntaxHint = .None, + trailing_sep: bool = false, + is_ascii: bool = false, + + /// Only used when component is not ascii + unicode_set: bool = false, + start_cp: u32 = 0, + end_cp: u32 = 0, + + pub fn patternSlice(this: *const Component, pattern: []const u8) []const u8 { + return pattern[this.start .. this.start + this.len - @as(u1, @bitCast(this.trailing_sep))]; + } + + pub fn patternSliceCp(this: *const Component, pattern: []u32) []u32 { + return pattern[this.start_cp .. this.end_cp - @as(u1, @bitCast(this.trailing_sep))]; + } + + const SyntaxHint = enum { + None, + Single, + Double, + /// Uses special fast-path matching for components like: `*.ts` + WildcardFilepath, + /// Uses special fast-patch matching for literal components e.g. + /// "node_modules", becomes memcmp + Literal, + /// ./fixtures/*.ts + /// ^ + Dot, + /// ../ + DotBack, + + fn isSpecialSyntax(this: SyntaxHint) bool { + return switch (this) { + .Literal => false, + else => true, + }; + } + }; + }; + + /// The arena parameter is dereferenced and copied if all allocations go well and nothing goes wrong + pub fn init( + this: *GlobWalker, + arena: *Arena, + pattern: []const u8, + dot: bool, + absolute: bool, + follow_symlinks: bool, + error_on_broken_symlinks: bool, + only_files: bool, + ) !Maybe(void) { + return try this.initWithCwd( + arena, + pattern, + bun.fs.FileSystem.instance.top_level_dir, + dot, + absolute, + follow_symlinks, + error_on_broken_symlinks, + only_files, + ); + } + + pub fn convertUtf8ToCodepoints(codepoints: []u32, pattern: []const u8) void { + _ = bun.simdutf.convert.utf8.to.utf32.le(pattern, codepoints); + } + + pub fn debugPatternComopnents(this: *GlobWalker) void { + const pattern = this.pattern; + const components = &this.patternComponents; + const ptr = @intFromPtr(this); + log("GlobWalker(0x{x}) components:", .{ptr}); + for (components.items) |cmp| { + switch (cmp.syntax_hint) { + .Single => log(" *", .{}), + .Double => log(" **", .{}), + .Dot => log(" .", .{}), + .DotBack => log(" ../", .{}), + .Literal, .WildcardFilepath, .None => log(" hint={s} component_str={s}", .{ @tagName(cmp.syntax_hint), cmp.patternSlice(pattern) }), + } + } + } + + /// `cwd` should be allocated with the arena + /// The arena parameter is dereferenced and copied if all allocations go well and nothing goes wrong + pub fn initWithCwd( + this: *GlobWalker, + arena: *Arena, + pattern: []const u8, + cwd: []const u8, + dot: bool, + absolute: bool, + follow_symlinks: bool, + error_on_broken_symlinks: bool, + only_files: bool, + ) !Maybe(void) { + log("initWithCwd(cwd={s})", .{cwd}); + this.* = .{ + .cwd = cwd, + .pattern = pattern, + .dot = dot, + .absolute = absolute, + .follow_symlinks = follow_symlinks, + .error_on_broken_symlinks = error_on_broken_symlinks, + .only_files = only_files, + .basename_excluding_special_syntax_component_idx = 0, + .end_byte_of_basename_excluding_special_syntax = 0, + }; + + try GlobWalker.buildPatternComponents( + arena, + &this.patternComponents, + pattern, + &this.cp_len, + &this.pattern_codepoints, + &this.has_relative_components, + &this.end_byte_of_basename_excluding_special_syntax, + &this.basename_excluding_special_syntax_component_idx, + ); + + // copy arena after all allocations are successful + this.arena = arena.*; + + if (bun.Environment.allow_assert) { + this.debugPatternComopnents(); + } + + return Maybe(void).success; + } + + /// NOTE This also calls deinit on the arena, if you don't want to do that then + pub fn deinit(this: *GlobWalker, comptime clear_arena: bool) void { + log("GlobWalker.deinit", .{}); + if (comptime clear_arena) { + this.arena.deinit(); + } + } + + pub fn handleSysErrWithPath( + this: *GlobWalker, + err: Syscall.Error, + path_buf: [:0]const u8, + ) Syscall.Error { + std.mem.copyForwards(u8, this.pathBuf[0 .. path_buf.len + 1], @as([]const u8, @ptrCast(path_buf[0 .. path_buf.len + 1]))); + return err.withPath(this.pathBuf[0 .. path_buf.len + 1]); + } + + pub fn walk(this: *GlobWalker) !Maybe(void) { + if (this.patternComponents.items.len == 0) return Maybe(void).success; + + var iter = GlobWalker.Iterator{ .walker = this }; + defer iter.deinit(); + switch (try iter.init()) { + .err => |err| return .{ .err = err }, + else => {}, + } + + while (switch (try iter.next()) { + .err => |err| return .{ .err = err }, + .result => |matched_path| matched_path, + }) |path| { + log("walker: matched path: {s}", .{path}); + // The paths are already put into this.matchedPaths, which we use for the output, + // so we don't need to do anything here + } + + return Maybe(void).success; + } + + // NOTE you must check that the pattern at `idx` has `syntax_hint == .Dot` or + // `syntax_hint == .DotBack` first + fn collapseDots( + this: *GlobWalker, + idx: u32, + dir_path: *[:0]u8, + path_buf: *bun.PathBuffer, + encountered_dot_dot: *bool, + ) u32 { + var component_idx = idx; + var len = dir_path.len; + while (component_idx < this.patternComponents.items.len) { + switch (this.patternComponents.items[component_idx].syntax_hint) { + .Dot => { + defer component_idx += 1; + if (len + 2 >= bun.MAX_PATH_BYTES) @panic("Invalid path"); + if (len == 0) { + path_buf[len] = '.'; + path_buf[len + 1] = 0; + len += 1; + } else { + path_buf[len] = '/'; + path_buf[len + 1] = '.'; + path_buf[len + 2] = 0; + len += 2; + } + }, + .DotBack => { + defer component_idx += 1; + encountered_dot_dot.* = true; + if (dir_path.len + 3 >= bun.MAX_PATH_BYTES) @panic("Invalid path"); + if (len == 0) { + path_buf[len] = '.'; + path_buf[len + 1] = '.'; + path_buf[len + 2] = 0; + len += 2; + } else { + path_buf[len] = '/'; + path_buf[len + 1] = '.'; + path_buf[len + 2] = '.'; + path_buf[len + 3] = 0; + len += 3; + } + }, + else => break, + } + } + + dir_path.len = len; + + return component_idx; + } + + // NOTE you must check that the pattern at `idx` has `syntax_hint == .Double` first + fn collapseSuccessiveDoubleWildcards(this: *GlobWalker, idx: u32) u32 { + var component_idx = idx; + const pattern = this.patternComponents.items[idx]; + _ = pattern; + // Collapse successive double wildcards + while (component_idx + 1 < this.patternComponents.items.len and + this.patternComponents.items[component_idx + 1].syntax_hint == .Double) : (component_idx += 1) + {} + return component_idx; + } + + pub fn skipSpecialComponents( + this: *GlobWalker, + work_item_idx: u32, + dir_path: *[:0]u8, + scratch_path_buf: *bun.PathBuffer, + encountered_dot_dot: *bool, + ) u32 { + var component_idx = work_item_idx; + + // Skip `.` and `..` while also appending them to `dir_path` + component_idx = switch (this.patternComponents.items[component_idx].syntax_hint) { + .Dot => this.collapseDots( + component_idx, + dir_path, + scratch_path_buf, + encountered_dot_dot, + ), + .DotBack => this.collapseDots( + component_idx, + dir_path, + scratch_path_buf, + encountered_dot_dot, + ), + else => component_idx, + }; + + // Skip to the last `**` if there is a chain of them + component_idx = switch (this.patternComponents.items[component_idx].syntax_hint) { + .Double => this.collapseSuccessiveDoubleWildcards(component_idx), + else => component_idx, + }; + + return component_idx; + } + + fn matchPatternDir( + this: *GlobWalker, + pattern: *Component, + next_pattern: ?*Component, + entry_name: []const u8, + component_idx: u32, + is_last: bool, + add: *bool, + ) ?u32 { + if (!this.dot and GlobWalker.startsWithDot(entry_name)) return null; + if (is_ignored(entry_name)) return null; + + // Handle double wildcard `**`, this could possibly + // propagate the `**` to the directory's children + if (pattern.syntax_hint == .Double) { + // Stop the double wildcard if it matches the pattern afer it + // Example: src/**/*.js + // - Matches: src/bun.js/ + // src/bun.js/foo/bar/baz.js + if (!is_last and this.matchPatternImpl(next_pattern.?, entry_name)) { + // But if the next pattern is the last + // component, it should match and propagate the + // double wildcard recursion to the directory's + // children + if (component_idx + 1 == this.patternComponents.items.len - 1) { + add.* = true; + return 0; + } + + // In the normal case skip over the next pattern + // since we matched it, example: + // BEFORE: src/**/node_modules/**/*.js + // ^ + // AFTER: src/**/node_modules/**/*.js + // ^ + return 2; + } + + if (is_last) { + add.* = true; + } + + return 0; + } + + const matches = this.matchPatternImpl(pattern, entry_name); + if (matches) { + if (is_last) { + add.* = true; + return null; + } + return 1; + } + + return null; + } + + /// A file can only match if: + /// a) it matches against the last pattern, or + /// b) it matches the next pattern, provided the current + /// pattern is a double wildcard and the next pattern is + /// not a double wildcard + /// + /// Examples: + /// a -> `src/foo/index.ts` matches + /// b -> `src/**/*.ts` (on 2nd pattern) matches + fn matchPatternFile( + this: *GlobWalker, + entry_name: []const u8, + component_idx: u32, + is_last: bool, + pattern: *Component, + next_pattern: ?*Component, + ) bool { + if (pattern.trailing_sep) return false; + + // Handle case b) + if (!is_last) return pattern.syntax_hint == .Double and + component_idx + 1 == this.patternComponents.items.len -| 1 and + next_pattern.?.syntax_hint != .Double and + this.matchPatternImpl(next_pattern.?, entry_name); + + // Handle case a) + return this.matchPatternImpl(pattern, entry_name); + } + + fn matchPatternImpl( + this: *GlobWalker, + pattern_component: *Component, + filepath: []const u8, + ) bool { + log("matchPatternImpl: {s}", .{filepath}); + if (!this.dot and GlobWalker.startsWithDot(filepath)) return false; + if (is_ignored(filepath)) return false; + + return switch (pattern_component.syntax_hint) { + .Double, .Single => true, + .WildcardFilepath => if (comptime !isWindows) + matchWildcardFilepath(pattern_component.patternSlice(this.pattern), filepath) + else + this.matchPatternSlow(pattern_component, filepath), + .Literal => if (comptime !isWindows) + matchWildcardLiteral(pattern_component.patternSlice(this.pattern), filepath) + else + this.matchPatternSlow(pattern_component, filepath), + else => this.matchPatternSlow(pattern_component, filepath), + }; + } + + fn matchPatternSlow(this: *GlobWalker, pattern_component: *Component, filepath: []const u8) bool { + // windows filepaths are utf-16 so GlobAscii.match will never work + if (comptime !isWindows) { + if (pattern_component.is_ascii and isAllAscii(filepath)) + return GlobAscii.match( + pattern_component.patternSlice(this.pattern), + filepath, + ).matches(); + } + const codepoints = this.componentStringUnicode(pattern_component); + return matchImpl( + codepoints, + filepath, + ).matches(); + } + + fn componentStringUnicode(this: *GlobWalker, pattern_component: *Component) []const u32 { + if (comptime isWindows) { + return this.componentStringUnicodeWindows(pattern_component); + } else { + return this.componentStringUnicodePosix(pattern_component); + } + } + + fn componentStringUnicodeWindows(this: *GlobWalker, pattern_component: *Component) []const u32 { + return pattern_component.patternSliceCp(this.pattern_codepoints); + } + + fn componentStringUnicodePosix(this: *GlobWalker, pattern_component: *Component) []const u32 { + if (pattern_component.unicode_set) return pattern_component.patternSliceCp(this.pattern_codepoints); + + const codepoints = pattern_component.patternSliceCp(this.pattern_codepoints); + GlobWalker.convertUtf8ToCodepoints( + codepoints, + pattern_component.patternSlice(this.pattern), + ); + pattern_component.unicode_set = true; + return codepoints; + } + + inline fn matchedPathToBunString(matched_path: MatchedPath) BunString { + if (comptime sentinel) { + return BunString.fromBytes(matched_path[0 .. matched_path.len + 1]); + } + return BunString.fromBytes(matched_path); + } + + fn prepareMatchedPathSymlink(this: *GlobWalker, symlink_full_path: []const u8) !?MatchedPath { + const result = try this.matchedPaths.getOrPut(this.arena.allocator(), BunString.fromBytes(symlink_full_path)); + if (result.found_existing) { + log("(dupe) prepared match: {s}", .{symlink_full_path}); + return null; + } + if (comptime !sentinel) { + const slice = try this.arena.allocator().dupe(u8, symlink_full_path); + result.key_ptr.* = matchedPathToBunString(slice); + return slice; + } + const slicez = try this.arena.allocator().dupeZ(u8, symlink_full_path); + result.key_ptr.* = matchedPathToBunString(slicez); + return slicez; + } + + fn prepareMatchedPath(this: *GlobWalker, entry_name: []const u8, dir_name: []const u8) !?MatchedPath { + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir_name[0..dir_name.len], + entry_name, + }; + const name_matched_path = try this.join(subdir_parts); + const name = matchedPathToBunString(name_matched_path); + const result = try this.matchedPaths.getOrPutValue(this.arena.allocator(), name, {}); + if (result.found_existing) { + log("(dupe) prepared match: {s}", .{name_matched_path}); + this.arena.allocator().free(name_matched_path); + return null; + } + result.key_ptr.* = name; + // if (comptime sentinel) return name[0 .. name.len - 1 :0]; + log("prepared match: {s}", .{name_matched_path}); + return name_matched_path; + } + + fn appendMatchedPath( + this: *GlobWalker, + entry_name: []const u8, + dir_name: [:0]const u8, + ) !void { + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir_name[0..dir_name.len], + entry_name, + }; + const name_matched_path = try this.join(subdir_parts); + const name = matchedPathToBunString(name_matched_path); + const result = try this.matchedPaths.getOrPut(this.arena.allocator(), name); + if (result.found_existing) { + this.arena.allocator().free(name_matched_path); + log("(dupe) prepared match: {s}", .{name_matched_path}); + return; + } + result.key_ptr.* = name; + } + + fn appendMatchedPathSymlink(this: *GlobWalker, symlink_full_path: []const u8) !void { + const name = try this.arena.allocator().dupe(u8, symlink_full_path); + try this.matchedPaths.put(this.arena.allocator(), BunString.fromBytes(name), {}); + } + + inline fn join(this: *GlobWalker, subdir_parts: []const []const u8) !MatchedPath { + if (!this.absolute) { + // If relative paths enabled, stdlib join is preferred over + // ResolvePath.joinBuf because it doesn't try to normalize the path + return try stdJoin(this.arena.allocator(), subdir_parts); + } + + const out = try this.arena.allocator().dupe(u8, bunJoin(subdir_parts, .auto)); + if (comptime sentinel) return out[0 .. out.len - 1 :0]; + + return out; + } + + inline fn startsWithDot(filepath: []const u8) bool { + return filepath.len > 0 and filepath[0] == '.'; + } + + fn checkSpecialSyntax(pattern: []const u8) bool { + if (pattern.len < 16) { + for (pattern[0..]) |c| { + switch (c) { + '*', '[', '{', '?', '!' => return true, + else => {}, + } + } + return false; + } + + const syntax_tokens = comptime [_]u8{ '*', '[', '{', '?', '!' }; + const needles: [syntax_tokens.len]@Vector(16, u8) = comptime needles: { + var needles: [syntax_tokens.len]@Vector(16, u8) = undefined; + for (syntax_tokens, 0..) |tok, i| { + needles[i] = @splat(tok); + } + break :needles needles; + }; + + var i: usize = 0; + while (i + 16 <= pattern.len) : (i += 16) { + const haystack: @Vector(16, u8) = pattern[i..][0..16].*; + inline for (needles) |needle| { + if (std.simd.firstTrue(needle == haystack) != null) return true; + } + } + + if (i < pattern.len) { + for (pattern[i..]) |c| { + inline for (syntax_tokens) |tok| { + if (c == tok) return true; + } + } + } + + return false; + } + + fn makeComponent( + pattern: []const u8, + start_cp: u32, + end_cp: u32, + start_byte: u32, + end_byte: u32, + has_relative_patterns: *bool, + ) ?Component { + var component: Component = .{ + .start = start_byte, + .len = end_byte - start_byte, + .start_cp = start_cp, + .end_cp = end_cp, + }; + if (component.len == 0) return null; + + out: { + if (component.len == 1 and pattern[component.start] == '.') { + component.syntax_hint = .Dot; + has_relative_patterns.* = true; + break :out; + } + if (component.len == 2 and pattern[component.start] == '.' and pattern[component.start] == '.') { + component.syntax_hint = .DotBack; + has_relative_patterns.* = true; + break :out; + } + + if (!GlobWalker.checkSpecialSyntax(pattern[component.start .. component.start + component.len])) { + component.syntax_hint = .Literal; + break :out; + } + + switch (component.len) { + 1 => { + if (pattern[component.start] == '*') { + component.syntax_hint = .Single; + } + break :out; + }, + 2 => { + if (pattern[component.start] == '*' and pattern[component.start + 1] == '*') { + component.syntax_hint = .Double; + break :out; + } + }, + else => {}, + } + + out_of_check_wildcard_filepath: { + if (component.len > 1 and + pattern[component.start] == '*' and + pattern[component.start + 1] == '.' and + component.start + 2 < pattern.len) + { + for (pattern[component.start + 2 ..]) |c| { + switch (c) { + // The fast path checks that path[1..] == pattern[1..], + // this will obviously not work if additional + // glob syntax is present in the pattern, so we + // must not apply this optimization if we see + // special glob syntax. + // + // This is not a complete check, there can be + // false negatives, but that's okay, it just + // means we don't apply the optimization. + // + // We also don't need to look for the `!` token, + // because that only applies negation if at the + // beginning of the string. + '[', '{', '?', '*' => break :out_of_check_wildcard_filepath, + else => {}, + } + } + component.syntax_hint = .WildcardFilepath; + break :out; + } + } + } + + if (component.syntax_hint != .Single and component.syntax_hint != .Double) { + if (isAllAscii(pattern[component.start .. component.start + component.len])) { + component.is_ascii = true; + } + } else { + component.is_ascii = true; + } + + if (pattern[component.start + component.len -| 1] == '/') { + component.trailing_sep = true; + } else if (comptime bun.Environment.isWindows) { + component.trailing_sep = pattern[component.start + component.len -| 1] == '\\'; + } + + return component; + } + + /// Build an ad-hoc glob pattern. Useful when you don't need to traverse + /// a directory. + pub fn buildPattern( + arena: *Arena, + patternComponents: *ArrayList(Component), + pattern: []const u8, + out_cp_len: ?*u32, + out_pattern_cp: *[]u32, + has_relative_patterns: *bool, + end_byte_of_basename_excluding_special_syntax: ?*u32, + basename_excluding_special_syntax_component_idx: ?*u32, + ) !void { + // in case the consumer doesn't care about some outputs. + const scratchpad: [3]u32 = .{0} ** 3; + return buildPatternComponents( + arena, + patternComponents, + pattern, + out_cp_len orelse &scratchpad[0], + out_pattern_cp, + has_relative_patterns, + end_byte_of_basename_excluding_special_syntax orelse scratchpad[1], + basename_excluding_special_syntax_component_idx orelse scratchpad[2], + ); + } + + fn buildPatternComponents( + arena: *Arena, + patternComponents: *ArrayList(Component), + pattern: []const u8, + out_cp_len: *u32, + out_pattern_cp: *[]u32, + has_relative_patterns: *bool, + end_byte_of_basename_excluding_special_syntax: *u32, + basename_excluding_special_syntax_component_idx: *u32, + ) !void { + var start_cp: u32 = 0; + var start_byte: u32 = 0; + + const iter = CodepointIterator.init(pattern); + var cursor = CodepointIterator.Cursor{}; + + var cp_len: u32 = 0; + var prevIsBackslash = false; + var saw_special = false; + while (iter.next(&cursor)) : (cp_len += 1) { + const c = cursor.c; + + switch (c) { + '\\' => { + if (comptime isWindows) { + var end_cp = cp_len; + var end_byte = cursor.i; + // is last char + if (cursor.i + cursor.width == pattern.len) { + end_cp += 1; + end_byte += cursor.width; + } + if (makeComponent( + pattern, + start_cp, + end_cp, + start_byte, + end_byte, + has_relative_patterns, + )) |component| { + saw_special = saw_special or component.syntax_hint.isSpecialSyntax(); + if (!saw_special) { + basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); + end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; + } + try patternComponents.append(arena.allocator(), component); + } + start_cp = cp_len + 1; + start_byte = cursor.i + cursor.width; + continue; + } + + if (prevIsBackslash) { + prevIsBackslash = false; + continue; + } + + prevIsBackslash = true; + }, + '/' => { + var end_cp = cp_len; + var end_byte = cursor.i; + // is last char + if (cursor.i + cursor.width == pattern.len) { + end_cp += 1; + end_byte += cursor.width; + } + if (makeComponent( + pattern, + start_cp, + end_cp, + start_byte, + end_byte, + has_relative_patterns, + )) |component| { + saw_special = saw_special or component.syntax_hint.isSpecialSyntax(); + if (!saw_special) { + basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); + end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; + } + try patternComponents.append(arena.allocator(), component); + } + start_cp = cp_len + 1; + start_byte = cursor.i + cursor.width; + }, + // TODO: Support other escaping glob syntax + else => {}, + } + } + + out_cp_len.* = cp_len; + + const codepoints = try arena.allocator().alloc(u32, cp_len); + // On Windows filepaths are UTF-16 so its better to fill the codepoints buffer upfront + if (comptime isWindows) { + GlobWalker.convertUtf8ToCodepoints(codepoints, pattern); + } + out_pattern_cp.* = codepoints; + + const end_cp = cp_len; + if (makeComponent( + pattern, + start_cp, + end_cp, + start_byte, + @intCast(pattern.len), + has_relative_patterns, + )) |component| { + saw_special = saw_special or component.syntax_hint.isSpecialSyntax(); + if (!saw_special) { + basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); + end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; + } + try patternComponents.append(arena.allocator(), component); + } else if (!saw_special) { + basename_excluding_special_syntax_component_idx.* = @intCast(patternComponents.items.len); + end_byte_of_basename_excluding_special_syntax.* = cursor.i + cursor.width; + } + } + }; +} + +// From: https://github.com/The-King-of-Toasters/globlin +/// State for matching a glob against a string +pub const GlobState = struct { + // These store character indices into the glob and path strings. + path_index: CursorState = .{}, + glob_index: u32 = 0, + // When we hit a * or **, we store the state for backtracking. + wildcard: Wildcard = .{}, + globstar: Wildcard = .{}, + + fn init(path_iter: *const CodepointIterator) GlobState { + var this = GlobState{}; + // this.glob_index = CursorState.init(glob_iter); + this.path_index = CursorState.init(path_iter); + return this; + } + + fn skipBraces(self: *GlobState, glob: []const u32, stop_on_comma: bool) BraceState { + var braces: u32 = 1; + var in_brackets = false; + while (self.glob_index < glob.len and braces > 0) : (self.glob_index += 1) { + switch (glob[self.glob_index]) { + // Skip nested braces + '{' => if (!in_brackets) { + braces += 1; + }, + '}' => if (!in_brackets) { + braces -= 1; + }, + ',' => if (stop_on_comma and braces == 1 and !in_brackets) { + self.glob_index += 1; + return .Comma; + }, + '*', '?', '[' => |c| if (!in_brackets) { + if (c == '[') + in_brackets = true; + }, + ']' => in_brackets = false, + '\\' => self.glob_index += 1, + else => {}, + } + } + + if (braces != 0) + return .Invalid; + return .EndBrace; + } + + inline fn backtrack(self: *GlobState) void { + self.glob_index = self.wildcard.glob_index; + self.path_index = self.wildcard.path_index; + } +}; + +const Wildcard = struct { + // Using u32 rather than usize for these results in 10% faster performance. + // glob_index: CursorState = .{}, + glob_index: u32 = 0, + path_index: CursorState = .{}, +}; + +const BraceState = enum { Invalid, Comma, EndBrace }; + +const BraceStack = struct { + stack: [10]GlobState = undefined, + len: u32 = 0, + longest_brace_match: CursorState = .{}, + + inline fn push(self: *BraceStack, state: *const GlobState) GlobState { + self.stack[self.len] = state.*; + self.len += 1; + return GlobState{ + .path_index = state.path_index, + .glob_index = state.glob_index + 1, + }; + } + + inline fn pop(self: *BraceStack, state: *const GlobState) GlobState { + self.len -= 1; + const s = GlobState{ + .glob_index = state.glob_index, + .path_index = self.longest_brace_match, + // Restore star state if needed later. + .wildcard = self.stack[self.len].wildcard, + .globstar = self.stack[self.len].globstar, + }; + if (self.len == 0) + self.longest_brace_match = .{}; + return s; + } + + inline fn last(self: *const BraceStack) *const GlobState { + return &self.stack[self.len - 1]; + } +}; + +pub const MatchResult = enum { + no_match, + match, + + negate_no_match, + negate_match, + + pub fn matches(this: MatchResult) bool { + return this == .match or this == .negate_match; + } +}; + +/// This function checks returns a boolean value if the pathname `path` matches +/// the pattern `glob`. +/// +/// The supported pattern syntax for `glob` is: +/// +/// "?" +/// Matches any single character. +/// "*" +/// Matches zero or more characters, except for path separators ('/' or '\'). +/// "**" +/// Matches zero or more characters, including path separators. +/// Must match a complete path segment, i.e. followed by a path separator or +/// at the end of the pattern. +/// "[ab]" +/// Matches one of the characters contained in the brackets. +/// Character ranges (e.g. "[a-z]") are also supported. +/// Use "[!ab]" or "[^ab]" to match any character *except* those contained +/// in the brackets. +/// "{a,b}" +/// Match one of the patterns contained in the braces. +/// Any of the wildcards listed above can be used in the sub patterns. +/// Braces may be nested up to 10 levels deep. +/// "!" +/// Negates the result when at the start of the pattern. +/// Multiple "!" characters negate the pattern multiple times. +/// "\" +/// Used to escape any of the special characters above. +pub fn matchImpl(glob: []const u32, path: []const u8) MatchResult { + const path_iter = CodepointIterator.init(path); + + // This algorithm is based on https://research.swtch.com/glob + var state = GlobState.init(&path_iter); + // Store the state when we see an opening '{' brace in a stack. + // Up to 10 nested braces are supported. + var brace_stack = BraceStack{}; + + // First, check if the pattern is negated with a leading '!' character. + // Multiple negations can occur. + var negated = false; + while (state.glob_index < glob.len and glob[state.glob_index] == '!') { + negated = !negated; + state.glob_index += 1; + } + + while (state.glob_index < glob.len or state.path_index.cursor.i < path.len) { + if (state.glob_index < glob.len) { + switch (glob[state.glob_index]) { + '*' => { + const is_globstar = state.glob_index + 1 < glob.len and glob[state.glob_index + 1] == '*'; + // const is_globstar = state.glob_index.cursor.i + state.glob_index.cursor.width < glob.len and + // state.glob_index.peek(&glob_iter).cursor.c == '*'; + if (is_globstar) { + // Coalesce multiple ** segments into one. + var index = state.glob_index + 2; + state.glob_index = skipGlobstars(glob, &index) - 2; + } + + state.wildcard.glob_index = state.glob_index; + state.wildcard.path_index = state.path_index.peek(&path_iter); + + // ** allows path separators, whereas * does not. + // However, ** must be a full path component, i.e. a/**/b not a**b. + if (is_globstar) { + // Skip wildcards + state.glob_index += 2; + + if (glob.len == state.glob_index) { + // A trailing ** segment without a following separator. + state.globstar = state.wildcard; + } else if (glob[state.glob_index] == '/' and + (state.glob_index < 3 or glob[state.glob_index - 3] == '/')) + { + // Matched a full /**/ segment. If the last character in the path was a separator, + // skip the separator in the glob so we search for the next character. + // In effect, this makes the whole segment optional so that a/**/b matches a/b. + if (state.path_index.cursor.i == 0 or + (state.path_index.cursor.i < path.len and + isSeparator(path[state.path_index.cursor.i - 1]))) + { + state.glob_index += 1; + } + + // The allows_sep flag allows separator characters in ** matches. + // one is a '/', which prevents a/**/b from matching a/bb. + state.globstar = state.wildcard; + } + } else { + state.glob_index += 1; + } + + // If we are in a * segment and hit a separator, + // either jump back to a previous ** or end the wildcard. + if (state.globstar.path_index.cursor.i != state.wildcard.path_index.cursor.i and + state.path_index.cursor.i < path.len and + isSeparator(state.path_index.cursor.c)) + { + // Special case: don't jump back for a / at the end of the glob. + if (state.globstar.path_index.cursor.i > 0 and state.path_index.cursor.i + state.path_index.cursor.width < path.len) { + state.glob_index = state.globstar.glob_index; + state.wildcard.glob_index = state.globstar.glob_index; + } else { + state.wildcard.path_index.cursor.i = 0; + } + } + + // If the next char is a special brace separator, + // skip to the end of the braces so we don't try to match it. + if (brace_stack.len > 0 and + state.glob_index < glob.len and + (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) + { + if (state.skipBraces(glob, false) == .Invalid) + return .no_match; // invalid pattern! + } + + continue; + }, + '?' => if (state.path_index.cursor.i < path.len) { + if (!isSeparator(state.path_index.cursor.c)) { + state.glob_index += 1; + state.path_index.bump(&path_iter); + continue; + } + }, + '[' => if (state.path_index.cursor.i < path.len) { + state.glob_index += 1; + const c = state.path_index.cursor.c; + + // Check if the character class is negated. + var class_negated = false; + if (state.glob_index < glob.len and + (glob[state.glob_index] == '^' or glob[state.glob_index] == '!')) + { + class_negated = true; + state.glob_index += 1; + } + + // Try each range. + var first = true; + var is_match = false; + while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { + var low = glob[state.glob_index]; + if (!unescape(&low, glob, &state.glob_index)) + return .no_match; // Invalid pattern + state.glob_index += 1; + + // If there is a - and the following character is not ], + // read the range end character. + const high = if (state.glob_index + 1 < glob.len and + glob[state.glob_index] == '-' and glob[state.glob_index + 1] != ']') + blk: { + state.glob_index += 1; + var h = glob[state.glob_index]; + if (!unescape(&h, glob, &state.glob_index)) + return .no_match; // Invalid pattern! + state.glob_index += 1; + break :blk h; + } else low; + + if (low <= c and c <= high) + is_match = true; + first = false; + } + if (state.glob_index >= glob.len) + return .no_match; // Invalid pattern! + state.glob_index += 1; + if (is_match != class_negated) { + state.path_index.bump(&path_iter); + continue; + } + }, + '{' => if (state.path_index.cursor.i < path.len) { + if (brace_stack.len >= brace_stack.stack.len) + return .no_match; // Invalid pattern! Too many nested braces. + + // Push old state to the stack, and reset current state. + state = brace_stack.push(&state); + continue; + }, + '}' => if (brace_stack.len > 0) { + // If we hit the end of the braces, we matched the last option. + brace_stack.longest_brace_match = if (state.path_index.cursor.i >= brace_stack.longest_brace_match.cursor.i) + state.path_index + else + brace_stack.longest_brace_match; + state.glob_index += 1; + state = brace_stack.pop(&state); + continue; + }, + ',' => if (brace_stack.len > 0) { + // If we hit a comma, we matched one of the options! + // But we still need to check the others in case there is a longer match. + brace_stack.longest_brace_match = if (state.path_index.cursor.i >= brace_stack.longest_brace_match.cursor.i) + state.path_index + else + brace_stack.longest_brace_match; + state.path_index = brace_stack.last().path_index; + state.glob_index += 1; + state.wildcard = Wildcard{}; + state.globstar = Wildcard{}; + continue; + }, + else => |c| if (state.path_index.cursor.i < path.len) { + var cc = c; + // Match escaped characters as literals. + if (!unescape(&cc, glob, &state.glob_index)) + return .no_match; // Invalid pattern; + + const is_match = if (cc == '/') + isSeparator(state.path_index.cursor.c) + else + state.path_index.cursor.c == cc; + + if (is_match) { + if (brace_stack.len > 0 and + state.glob_index > 0 and + glob[state.glob_index - 1] == '}') + { + brace_stack.longest_brace_match = state.path_index; + state = brace_stack.pop(&state); + } + state.glob_index += 1; + state.path_index.bump(&path_iter); + + // If this is not a separator, lock in the previous globstar. + if (cc != '/') + state.globstar.path_index.cursor.i = 0; + + continue; + } + }, + } + } + // If we didn't match, restore state to the previous star pattern. + if (state.wildcard.path_index.cursor.i > 0 and state.wildcard.path_index.cursor.i <= path.len) { + state.backtrack(); + continue; + } + + if (brace_stack.len > 0) { + // If in braces, find next option and reset path to index where we saw the '{' + switch (state.skipBraces(glob, true)) { + .Invalid => return .no_match, + .Comma => { + state.path_index = brace_stack.last().path_index; + continue; + }, + .EndBrace => {}, + } + + // Hit the end. Pop the stack. + // If we matched a previous option, use that. + if (brace_stack.longest_brace_match.cursor.i > 0) { + state = brace_stack.pop(&state); + continue; + } else { + // Didn't match. Restore state, and check if we need to jump back to a star pattern. + state = brace_stack.last().*; + brace_stack.len -= 1; + if (state.wildcard.path_index.cursor.i > 0 and state.wildcard.path_index.cursor.i <= path.len) { + state.backtrack(); + continue; + } + } + } + + return if (negated) .negate_match else .no_match; + } + + return if (!negated) .match else .negate_no_match; +} + +pub inline fn isSeparator(c: Codepoint) bool { + if (comptime @import("builtin").os.tag == .windows) return c == '/' or c == '\\'; + return c == '/'; +} + +inline fn unescape(c: *u32, glob: []const u32, glob_index: *u32) bool { + if (c.* == '\\') { + glob_index.* += 1; + if (glob_index.* >= glob.len) + return false; // Invalid pattern! + + c.* = switch (glob[glob_index.*]) { + 'a' => '\x61', + 'b' => '\x08', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + else => |cc| cc, + }; + } + + return true; +} + +const GLOB_STAR_MATCH_STR: []const u32 = &[_]u32{ '/', '*', '*' }; +// src/**/**/foo.ts +inline fn skipGlobstars(glob: []const u32, glob_index: *u32) u32 { + // Coalesce multiple ** segments into one. + while (glob_index.* + 3 <= glob.len and + // std.mem.eql(u8, glob[glob_index.*..][0..3], "/**")) + std.mem.eql(u32, glob[glob_index.*..][0..3], GLOB_STAR_MATCH_STR)) + { + glob_index.* += 3; + } + + return glob_index.*; +} + +const MatchAscii = struct {}; + +pub fn matchWildcardFilepath(glob: []const u8, path: []const u8) bool { + const needle = glob[1..]; + const needle_len: u32 = @intCast(needle.len); + if (path.len < needle_len) return false; + return std.mem.eql(u8, needle, path[path.len - needle_len ..]); +} + +pub fn matchWildcardLiteral(literal: []const u8, path: []const u8) bool { + return std.mem.eql(u8, literal, path); +} diff --git a/src/glob/ascii.zig b/src/glob/ascii.zig new file mode 100644 index 0000000000..c2e4724cb1 --- /dev/null +++ b/src/glob/ascii.zig @@ -0,0 +1,527 @@ +// Portions of this file are derived from works under the MIT License: +// +// Copyright (c) 2023 Devon Govett +// Copyright (c) 2023 Stephen Gregoratto +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +const std = @import("std"); +const math = std.math; +const mem = std.mem; +const expect = std.testing.expect; + +// These store character indices into the glob and path strings. +path_index: usize = 0, +glob_index: usize = 0, +// When we hit a * or **, we store the state for backtracking. +wildcard: Wildcard = .{}, +globstar: Wildcard = .{}, + +const Wildcard = struct { + // Using u32 rather than usize for these results in 10% faster performance. + glob_index: u32 = 0, + path_index: u32 = 0, +}; + +const BraceState = enum { Invalid, Comma, EndBrace }; + +fn skipBraces(self: *State, glob: []const u8, stop_on_comma: bool) BraceState { + var braces: u32 = 1; + var in_brackets = false; + while (self.glob_index < glob.len and braces > 0) : (self.glob_index += 1) { + switch (glob[self.glob_index]) { + // Skip nested braces + '{' => if (!in_brackets) { + braces += 1; + }, + '}' => if (!in_brackets) { + braces -= 1; + }, + ',' => if (stop_on_comma and braces == 1 and !in_brackets) { + self.glob_index += 1; + return .Comma; + }, + '*', '?', '[' => |c| if (!in_brackets) { + if (c == '[') + in_brackets = true; + }, + ']' => in_brackets = false, + '\\' => self.glob_index += 1, + else => {}, + } + } + + if (braces != 0) + return .Invalid; + return .EndBrace; +} + +inline fn backtrack(self: *State) void { + self.glob_index = self.wildcard.glob_index; + self.path_index = self.wildcard.path_index; +} + +const State = @This(); +const BraceStack = struct { + stack: [10]State = undefined, + len: u32 = 0, + longest_brace_match: u32 = 0, + + inline fn push(self: *BraceStack, state: *const State) State { + self.stack[self.len] = state.*; + self.len += 1; + return State{ + .path_index = state.path_index, + .glob_index = state.glob_index + 1, + }; + } + + inline fn pop(self: *BraceStack, state: *const State) State { + self.len -= 1; + const s = State{ + .glob_index = state.glob_index, + .path_index = self.longest_brace_match, + // Restore star state if needed later. + .wildcard = self.stack[self.len].wildcard, + .globstar = self.stack[self.len].globstar, + }; + if (self.len == 0) + self.longest_brace_match = 0; + return s; + } + + inline fn last(self: *const BraceStack) *const State { + return &self.stack[self.len - 1]; + } +}; + +const BraceIndex = struct { + start: u32 = 0, + end: u32 = 0, +}; + +pub fn preprocess_glob(glob: []const u8, brace_indices: *[10]BraceIndex, brace_indices_len: *u8, search_count: *u8, i: *u32) ?u32 { + while (i.* < glob.len) { + const c = glob[i]; + switch (c) { + '{' => { + if (brace_indices_len.* == brace_indices.len) continue; + const stack_idx = brace_indices_len.*; + if (i == glob.len - 1) continue; + const matching_idx = preprocess_glob(glob[i + 1 ..], brace_indices, brace_indices_len, search_count + 1); + if (matching_idx) |idx| { + if (brace_indices_len.* == brace_indices.len) continue; + brace_indices[stack_idx].start = @intCast(i); + brace_indices[stack_idx].end = @as(u32, @intCast(i)) + idx + 1; + brace_indices_len.* += 1; + } + }, + '}' => { + if (search_count > 0) return @intCast(i); + }, + else => {}, + } + } + return null; +} + +// pub fn preprocess_glob(glob: []const u8, brace_indices: *[10]BraceIndex, brace_indices_len: *u8, search_count_: u8) ?u32 { +// if (glob.len == 0) return null; + +// var search_count = search_count_; +// var i: u32 = 0; +// while (i < glob.len): (i += 1) { +// const c = glob[i]; +// switch (c) { +// '{' => { +// if (brace_indices_len.* == brace_indices.len) continue; +// const stack_idx = brace_indices_len.*; +// if (i == glob.len - 1) continue; +// const matching_idx = preprocess_glob(glob[i + 1..], brace_indices, brace_indices_len, search_count + 1); +// if (matching_idx) |idx| { +// if (brace_indices_len.* == brace_indices.len) continue; +// brace_indices[stack_idx].start = @intCast(i); +// brace_indices[stack_idx].end = @as(u32, @intCast(i)) + idx + 1; +// brace_indices_len.* += 1; +// } +// }, +// '}' => { +// if (search_count > 0) return @intCast(i); +// }, +// else => {}, +// } + +// } + +// return null; +// } + +pub fn valid_glob_indices(glob: []const u8, indices: std.ArrayList(BraceIndex)) !void { + _ = indices; + // {a,b,c} + for (glob, 0..) |c, i| { + _ = i; + _ = c; + } +} + +pub const MatchResult = enum { + no_match, + match, + + negate_no_match, + negate_match, + + pub fn matches(this: MatchResult) bool { + return this == .match or this == .negate_match; + } +}; + +/// This function checks returns a boolean value if the pathname `path` matches +/// the pattern `glob`. +/// +/// The supported pattern syntax for `glob` is: +/// +/// "?" +/// Matches any single character. +/// "*" +/// Matches zero or more characters, except for path separators ('/' or '\'). +/// "**" +/// Matches zero or more characters, including path separators. +/// Must match a complete path segment, i.e. followed by a path separator or +/// at the end of the pattern. +/// "[ab]" +/// Matches one of the characters contained in the brackets. +/// Character ranges (e.g. "[a-z]") are also supported. +/// Use "[!ab]" or "[^ab]" to match any character *except* those contained +/// in the brackets. +/// "{a,b}" +/// Match one of the patterns contained in the braces. +/// Any of the wildcards listed above can be used in the sub patterns. +/// Braces may be nested up to 10 levels deep. +/// "!" +/// Negates the result when at the start of the pattern. +/// Multiple "!" characters negate the pattern multiple times. +/// "\" +/// Used to escape any of the special characters above. +pub fn match(glob: []const u8, path: []const u8) MatchResult { + // This algorithm is based on https://research.swtch.com/glob + var state = State{}; + // Store the state when we see an opening '{' brace in a stack. + // Up to 10 nested braces are supported. + var brace_stack = BraceStack{}; + + // First, check if the pattern is negated with a leading '!' character. + // Multiple negations can occur. + var negated = false; + while (state.glob_index < glob.len and glob[state.glob_index] == '!') { + negated = !negated; + state.glob_index += 1; + } + + while (state.glob_index < glob.len or state.path_index < path.len) { + if (state.glob_index < glob.len) { + const gc = glob[state.glob_index]; + switch (gc) { + '*' => { + const is_globstar = state.glob_index + 1 < glob.len and + glob[state.glob_index + 1] == '*'; + if (is_globstar) { + // Coalesce multiple ** segments into one. + var index = state.glob_index + 2; + state.glob_index = skipGlobstars(glob, &index) - 2; + } + + state.wildcard.glob_index = @intCast(state.glob_index); + state.wildcard.path_index = @intCast(state.path_index + 1); + + // ** allows path separators, whereas * does not. + // However, ** must be a full path component, i.e. a/**/b not a**b. + if (is_globstar) { + state.glob_index += 2; + + if (glob.len == state.glob_index) { + // A trailing ** segment without a following separator. + state.globstar = state.wildcard; + } else if (glob[state.glob_index] == '/' and + (state.glob_index < 3 or glob[state.glob_index - 3] == '/')) + { + // Matched a full /**/ segment. If the last character in the path was a separator, + // skip the separator in the glob so we search for the next character. + // In effect, this makes the whole segment optional so that a/**/b matches a/b. + if (state.path_index == 0 or + (state.path_index < path.len and + isSeparator(path[state.path_index - 1]))) + { + state.glob_index += 1; + } + + // The allows_sep flag allows separator characters in ** matches. + // one is a '/', which prevents a/**/b from matching a/bb. + state.globstar = state.wildcard; + } + } else { + state.glob_index += 1; + } + + // If we are in a * segment and hit a separator, + // either jump back to a previous ** or end the wildcard. + if (state.globstar.path_index != state.wildcard.path_index and + state.path_index < path.len and + isSeparator(path[state.path_index])) + { + // Special case: don't jump back for a / at the end of the glob. + if (state.globstar.path_index > 0 and state.path_index + 1 < path.len) { + state.glob_index = state.globstar.glob_index; + state.wildcard.glob_index = state.globstar.glob_index; + } else { + state.wildcard.path_index = 0; + } + } + + // If the next char is a special brace separator, + // skip to the end of the braces so we don't try to match it. + if (brace_stack.len > 0 and + state.glob_index < glob.len and + (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) + { + if (state.skipBraces(glob, false) == .Invalid) + return .no_match; // invalid pattern! + } + + continue; + }, + '?' => if (state.path_index < path.len) { + if (!isSeparator(path[state.path_index])) { + state.glob_index += 1; + state.path_index += 1; + continue; + } + }, + '[' => if (state.path_index < path.len) { + state.glob_index += 1; + const c = path[state.path_index]; + + // Check if the character class is negated. + var class_negated = false; + if (state.glob_index < glob.len and + (glob[state.glob_index] == '^' or glob[state.glob_index] == '!')) + { + class_negated = true; + state.glob_index += 1; + } + + // Try each range. + var first = true; + var is_match = false; + while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { + var low = glob[state.glob_index]; + if (!unescape(&low, glob, &state.glob_index)) + return .no_match; // Invalid pattern + state.glob_index += 1; + + // If there is a - and the following character is not ], + // read the range end character. + const high = if (state.glob_index + 1 < glob.len and + glob[state.glob_index] == '-' and glob[state.glob_index + 1] != ']') + blk: { + state.glob_index += 1; + var h = glob[state.glob_index]; + if (!unescape(&h, glob, &state.glob_index)) + return .no_match; // Invalid pattern! + state.glob_index += 1; + break :blk h; + } else low; + + if (low <= c and c <= high) + is_match = true; + first = false; + } + if (state.glob_index >= glob.len) + return .no_match; // Invalid pattern! + state.glob_index += 1; + if (is_match != class_negated) { + state.path_index += 1; + continue; + } + }, + '{' => if (state.path_index < path.len) { + if (brace_stack.len >= brace_stack.stack.len) + return .no_match; // Invalid pattern! Too many nested braces. + + // Push old state to the stack, and reset current state. + state = brace_stack.push(&state); + continue; + }, + '}' => if (brace_stack.len > 0) { + // If we hit the end of the braces, we matched the last option. + brace_stack.longest_brace_match = + @max(brace_stack.longest_brace_match, @as(u32, @intCast(state.path_index))); + state.glob_index += 1; + state = brace_stack.pop(&state); + continue; + }, + ',' => if (brace_stack.len > 0) { + // If we hit a comma, we matched one of the options! + // But we still need to check the others in case there is a longer match. + brace_stack.longest_brace_match = + @max(brace_stack.longest_brace_match, @as(u32, @intCast(state.path_index))); + state.path_index = brace_stack.last().path_index; + state.glob_index += 1; + state.wildcard = Wildcard{}; + state.globstar = Wildcard{}; + continue; + }, + else => |c| if (state.path_index < path.len) { + var cc = c; + // Match escaped characters as literals. + if (!unescape(&cc, glob, &state.glob_index)) + return .no_match; // Invalid pattern; + + const is_match = if (cc == '/') + isSeparator(path[state.path_index]) + else + path[state.path_index] == cc; + + if (is_match) { + if (brace_stack.len > 0 and + state.glob_index > 0 and + glob[state.glob_index - 1] == '}') + { + brace_stack.longest_brace_match = @intCast(state.path_index); + state = brace_stack.pop(&state); + } + state.glob_index += 1; + state.path_index += 1; + + // If this is not a separator, lock in the previous globstar. + if (cc != '/') + state.globstar.path_index = 0; + + continue; + } + }, + } + } + // If we didn't match, restore state to the previous star pattern. + if (state.wildcard.path_index > 0 and state.wildcard.path_index <= path.len) { + state.backtrack(); + continue; + } + + if (brace_stack.len > 0) { + // If in braces, find next option and reset path to index where we saw the '{' + switch (state.skipBraces(glob, true)) { + .Invalid => return .no_match, + .Comma => { + state.path_index = brace_stack.last().path_index; + continue; + }, + .EndBrace => {}, + } + + // Hit the end. Pop the stack. + // If we matched a previous option, use that. + if (brace_stack.longest_brace_match > 0) { + state = brace_stack.pop(&state); + continue; + } else { + // Didn't match. Restore state, and check if we need to jump back to a star pattern. + state = brace_stack.last().*; + brace_stack.len -= 1; + if (state.wildcard.path_index > 0 and state.wildcard.path_index <= path.len) { + state.backtrack(); + continue; + } + } + } + + return if (negated) .negate_match else .no_match; + } + + return if (!negated) .match else .negate_no_match; +} + +inline fn isSeparator(c: u8) bool { + if (comptime @import("builtin").os.tag == .windows) return c == '/' or c == '\\'; + return c == '/'; +} + +inline fn unescape(c: *u8, glob: []const u8, glob_index: *usize) bool { + if (c.* == '\\') { + glob_index.* += 1; + if (glob_index.* >= glob.len) + return false; // Invalid pattern! + + c.* = switch (glob[glob_index.*]) { + 'a' => '\x61', + 'b' => '\x08', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + else => |cc| cc, + }; + } + + return true; +} + +inline fn skipGlobstars(glob: []const u8, glob_index: *usize) usize { + // Coalesce multiple ** segments into one. + while (glob_index.* + 3 <= glob.len and + std.mem.eql(u8, glob[glob_index.*..][0..3], "/**")) + { + glob_index.* += 3; + } + + return glob_index.*; +} + +/// Returns true if the given string contains glob syntax, +/// excluding those escaped with backslashes +/// TODO: this doesn't play nicely with Windows directory separator and +/// backslashing, should we just require the user to supply posix filepaths? +pub fn detectGlobSyntax(potential_pattern: []const u8) bool { + // Negation only allowed in the beginning of the pattern + if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; + + // In descending order of how popular the token is + const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; + + inline for (SPECIAL_SYNTAX) |token| { + var slice = potential_pattern[0..]; + while (slice.len > 0) { + if (std.mem.indexOfScalar(u8, slice, token)) |idx| { + // Check for even number of backslashes preceding the + // token to know that it's not escaped + var i = idx; + var backslash_count: u16 = 0; + + while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { + backslash_count += 1; + } + + if (backslash_count % 2 == 0) return true; + slice = slice[idx + 1 ..]; + } else break; + } + } + + return false; +} diff --git a/src/glob_ascii.zig b/src/glob_ascii.zig deleted file mode 100644 index 38914eface..0000000000 --- a/src/glob_ascii.zig +++ /dev/null @@ -1,482 +0,0 @@ -// Portions of this file are derived from works under the MIT License: -// -// Copyright (c) 2023 Devon Govett -// Copyright (c) 2023 Stephen Gregoratto -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -const std = @import("std"); -const math = std.math; -const mem = std.mem; -const expect = std.testing.expect; - -// These store character indices into the glob and path strings. -path_index: usize = 0, -glob_index: usize = 0, -// When we hit a * or **, we store the state for backtracking. -wildcard: Wildcard = .{}, -globstar: Wildcard = .{}, - -const Wildcard = struct { - // Using u32 rather than usize for these results in 10% faster performance. - glob_index: u32 = 0, - path_index: u32 = 0, -}; - -const BraceState = enum { Invalid, Comma, EndBrace }; - -fn skipBraces(self: *State, glob: []const u8, stop_on_comma: bool) BraceState { - var braces: u32 = 1; - var in_brackets = false; - while (self.glob_index < glob.len and braces > 0) : (self.glob_index += 1) { - switch (glob[self.glob_index]) { - // Skip nested braces - '{' => if (!in_brackets) { - braces += 1; - }, - '}' => if (!in_brackets) { - braces -= 1; - }, - ',' => if (stop_on_comma and braces == 1 and !in_brackets) { - self.glob_index += 1; - return .Comma; - }, - '*', '?', '[' => |c| if (!in_brackets) { - if (c == '[') - in_brackets = true; - }, - ']' => in_brackets = false, - '\\' => self.glob_index += 1, - else => {}, - } - } - - if (braces != 0) - return .Invalid; - return .EndBrace; -} - -inline fn backtrack(self: *State) void { - self.glob_index = self.wildcard.glob_index; - self.path_index = self.wildcard.path_index; -} - -const State = @This(); -const BraceStack = struct { - stack: [10]State = undefined, - len: u32 = 0, - longest_brace_match: u32 = 0, - - inline fn push(self: *BraceStack, state: *const State) State { - self.stack[self.len] = state.*; - self.len += 1; - return State{ - .path_index = state.path_index, - .glob_index = state.glob_index + 1, - }; - } - - inline fn pop(self: *BraceStack, state: *const State) State { - self.len -= 1; - const s = State{ - .glob_index = state.glob_index, - .path_index = self.longest_brace_match, - // Restore star state if needed later. - .wildcard = self.stack[self.len].wildcard, - .globstar = self.stack[self.len].globstar, - }; - if (self.len == 0) - self.longest_brace_match = 0; - return s; - } - - inline fn last(self: *const BraceStack) *const State { - return &self.stack[self.len - 1]; - } -}; - -const BraceIndex = struct { - start: u32 = 0, - end: u32 = 0, -}; - -pub fn preprocess_glob(glob: []const u8, brace_indices: *[10]BraceIndex, brace_indices_len: *u8, search_count: *u8, i: *u32) ?u32 { - while (i.* < glob.len) { - const c = glob[i]; - switch (c) { - '{' => { - if (brace_indices_len.* == brace_indices.len) continue; - const stack_idx = brace_indices_len.*; - if (i == glob.len - 1) continue; - const matching_idx = preprocess_glob(glob[i + 1 ..], brace_indices, brace_indices_len, search_count + 1); - if (matching_idx) |idx| { - if (brace_indices_len.* == brace_indices.len) continue; - brace_indices[stack_idx].start = @intCast(i); - brace_indices[stack_idx].end = @as(u32, @intCast(i)) + idx + 1; - brace_indices_len.* += 1; - } - }, - '}' => { - if (search_count > 0) return @intCast(i); - }, - else => {}, - } - } - return null; -} - -// pub fn preprocess_glob(glob: []const u8, brace_indices: *[10]BraceIndex, brace_indices_len: *u8, search_count_: u8) ?u32 { -// if (glob.len == 0) return null; - -// var search_count = search_count_; -// var i: u32 = 0; -// while (i < glob.len): (i += 1) { -// const c = glob[i]; -// switch (c) { -// '{' => { -// if (brace_indices_len.* == brace_indices.len) continue; -// const stack_idx = brace_indices_len.*; -// if (i == glob.len - 1) continue; -// const matching_idx = preprocess_glob(glob[i + 1..], brace_indices, brace_indices_len, search_count + 1); -// if (matching_idx) |idx| { -// if (brace_indices_len.* == brace_indices.len) continue; -// brace_indices[stack_idx].start = @intCast(i); -// brace_indices[stack_idx].end = @as(u32, @intCast(i)) + idx + 1; -// brace_indices_len.* += 1; -// } -// }, -// '}' => { -// if (search_count > 0) return @intCast(i); -// }, -// else => {}, -// } - -// } - -// return null; -// } - -pub fn valid_glob_indices(glob: []const u8, indices: std.ArrayList(BraceIndex)) !void { - _ = indices; - // {a,b,c} - for (glob, 0..) |c, i| { - _ = i; - _ = c; - } -} - -/// This function checks returns a boolean value if the pathname `path` matches -/// the pattern `glob`. -/// -/// The supported pattern syntax for `glob` is: -/// -/// "?" -/// Matches any single character. -/// "*" -/// Matches zero or more characters, except for path separators ('/' or '\'). -/// "**" -/// Matches zero or more characters, including path separators. -/// Must match a complete path segment, i.e. followed by a path separator or -/// at the end of the pattern. -/// "[ab]" -/// Matches one of the characters contained in the brackets. -/// Character ranges (e.g. "[a-z]") are also supported. -/// Use "[!ab]" or "[^ab]" to match any character *except* those contained -/// in the brackets. -/// "{a,b}" -/// Match one of the patterns contained in the braces. -/// Any of the wildcards listed above can be used in the sub patterns. -/// Braces may be nested up to 10 levels deep. -/// "!" -/// Negates the result when at the start of the pattern. -/// Multiple "!" characters negate the pattern multiple times. -/// "\" -/// Used to escape any of the special characters above. -pub fn match(glob: []const u8, path: []const u8) bool { - // This algorithm is based on https://research.swtch.com/glob - var state = State{}; - // Store the state when we see an opening '{' brace in a stack. - // Up to 10 nested braces are supported. - var brace_stack = BraceStack{}; - - // First, check if the pattern is negated with a leading '!' character. - // Multiple negations can occur. - var negated = false; - while (state.glob_index < glob.len and glob[state.glob_index] == '!') { - negated = !negated; - state.glob_index += 1; - } - - while (state.glob_index < glob.len or state.path_index < path.len) { - if (state.glob_index < glob.len) { - const gc = glob[state.glob_index]; - switch (gc) { - '*' => { - const is_globstar = state.glob_index + 1 < glob.len and - glob[state.glob_index + 1] == '*'; - if (is_globstar) { - // Coalesce multiple ** segments into one. - var index = state.glob_index + 2; - state.glob_index = skipGlobstars(glob, &index) - 2; - } - - state.wildcard.glob_index = @intCast(state.glob_index); - state.wildcard.path_index = @intCast(state.path_index + 1); - - // ** allows path separators, whereas * does not. - // However, ** must be a full path component, i.e. a/**/b not a**b. - if (is_globstar) { - state.glob_index += 2; - - if (glob.len == state.glob_index) { - // A trailing ** segment without a following separator. - state.globstar = state.wildcard; - } else if (glob[state.glob_index] == '/' and - (state.glob_index < 3 or glob[state.glob_index - 3] == '/')) - { - // Matched a full /**/ segment. If the last character in the path was a separator, - // skip the separator in the glob so we search for the next character. - // In effect, this makes the whole segment optional so that a/**/b matches a/b. - if (state.path_index == 0 or - (state.path_index < path.len and - isSeparator(path[state.path_index - 1]))) - { - state.glob_index += 1; - } - - // The allows_sep flag allows separator characters in ** matches. - // one is a '/', which prevents a/**/b from matching a/bb. - state.globstar = state.wildcard; - } - } else { - state.glob_index += 1; - } - - // If we are in a * segment and hit a separator, - // either jump back to a previous ** or end the wildcard. - if (state.globstar.path_index != state.wildcard.path_index and - state.path_index < path.len and - isSeparator(path[state.path_index])) - { - // Special case: don't jump back for a / at the end of the glob. - if (state.globstar.path_index > 0 and state.path_index + 1 < path.len) { - state.glob_index = state.globstar.glob_index; - state.wildcard.glob_index = state.globstar.glob_index; - } else { - state.wildcard.path_index = 0; - } - } - - // If the next char is a special brace separator, - // skip to the end of the braces so we don't try to match it. - if (brace_stack.len > 0 and - state.glob_index < glob.len and - (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) - { - if (state.skipBraces(glob, false) == .Invalid) - return false; // invalid pattern! - } - - continue; - }, - '?' => if (state.path_index < path.len) { - if (!isSeparator(path[state.path_index])) { - state.glob_index += 1; - state.path_index += 1; - continue; - } - }, - '[' => if (state.path_index < path.len) { - state.glob_index += 1; - const c = path[state.path_index]; - - // Check if the character class is negated. - var class_negated = false; - if (state.glob_index < glob.len and - (glob[state.glob_index] == '^' or glob[state.glob_index] == '!')) - { - class_negated = true; - state.glob_index += 1; - } - - // Try each range. - var first = true; - var is_match = false; - while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { - var low = glob[state.glob_index]; - if (!unescape(&low, glob, &state.glob_index)) - return false; // Invalid pattern - state.glob_index += 1; - - // If there is a - and the following character is not ], - // read the range end character. - const high = if (state.glob_index + 1 < glob.len and - glob[state.glob_index] == '-' and glob[state.glob_index + 1] != ']') - blk: { - state.glob_index += 1; - var h = glob[state.glob_index]; - if (!unescape(&h, glob, &state.glob_index)) - return false; // Invalid pattern! - state.glob_index += 1; - break :blk h; - } else low; - - if (low <= c and c <= high) - is_match = true; - first = false; - } - if (state.glob_index >= glob.len) - return false; // Invalid pattern! - state.glob_index += 1; - if (is_match != class_negated) { - state.path_index += 1; - continue; - } - }, - '{' => if (state.path_index < path.len) { - if (brace_stack.len >= brace_stack.stack.len) - return false; // Invalid pattern! Too many nested braces. - - // Push old state to the stack, and reset current state. - state = brace_stack.push(&state); - continue; - }, - '}' => if (brace_stack.len > 0) { - // If we hit the end of the braces, we matched the last option. - brace_stack.longest_brace_match = - @max(brace_stack.longest_brace_match, @as(u32, @intCast(state.path_index))); - state.glob_index += 1; - state = brace_stack.pop(&state); - continue; - }, - ',' => if (brace_stack.len > 0) { - // If we hit a comma, we matched one of the options! - // But we still need to check the others in case there is a longer match. - brace_stack.longest_brace_match = - @max(brace_stack.longest_brace_match, @as(u32, @intCast(state.path_index))); - state.path_index = brace_stack.last().path_index; - state.glob_index += 1; - state.wildcard = Wildcard{}; - state.globstar = Wildcard{}; - continue; - }, - else => |c| if (state.path_index < path.len) { - var cc = c; - // Match escaped characters as literals. - if (!unescape(&cc, glob, &state.glob_index)) - return false; // Invalid pattern; - - const is_match = if (cc == '/') - isSeparator(path[state.path_index]) - else - path[state.path_index] == cc; - - if (is_match) { - if (brace_stack.len > 0 and - state.glob_index > 0 and - glob[state.glob_index - 1] == '}') - { - brace_stack.longest_brace_match = @intCast(state.path_index); - state = brace_stack.pop(&state); - } - state.glob_index += 1; - state.path_index += 1; - - // If this is not a separator, lock in the previous globstar. - if (cc != '/') - state.globstar.path_index = 0; - - continue; - } - }, - } - } - // If we didn't match, restore state to the previous star pattern. - if (state.wildcard.path_index > 0 and state.wildcard.path_index <= path.len) { - state.backtrack(); - continue; - } - - if (brace_stack.len > 0) { - // If in braces, find next option and reset path to index where we saw the '{' - switch (state.skipBraces(glob, true)) { - .Invalid => return false, - .Comma => { - state.path_index = brace_stack.last().path_index; - continue; - }, - .EndBrace => {}, - } - - // Hit the end. Pop the stack. - // If we matched a previous option, use that. - if (brace_stack.longest_brace_match > 0) { - state = brace_stack.pop(&state); - continue; - } else { - // Didn't match. Restore state, and check if we need to jump back to a star pattern. - state = brace_stack.last().*; - brace_stack.len -= 1; - if (state.wildcard.path_index > 0 and state.wildcard.path_index <= path.len) { - state.backtrack(); - continue; - } - } - } - - return negated; - } - - return !negated; -} - -inline fn isSeparator(c: u8) bool { - if (comptime @import("builtin").os.tag == .windows) return c == '/' or c == '\\'; - return c == '/'; -} - -inline fn unescape(c: *u8, glob: []const u8, glob_index: *usize) bool { - if (c.* == '\\') { - glob_index.* += 1; - if (glob_index.* >= glob.len) - return false; // Invalid pattern! - - c.* = switch (glob[glob_index.*]) { - 'a' => '\x61', - 'b' => '\x08', - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - else => |cc| cc, - }; - } - - return true; -} - -inline fn skipGlobstars(glob: []const u8, glob_index: *usize) usize { - // Coalesce multiple ** segments into one. - while (glob_index.* + 3 <= glob.len and - std.mem.eql(u8, glob[glob_index.*..][0..3], "/**")) - { - glob_index.* += 3; - } - - return glob_index.*; -} diff --git a/src/hive_array.zig b/src/hive_array.zig index c3479da816..375e7929c8 100644 --- a/src/hive_array.zig +++ b/src/hive_array.zig @@ -11,7 +11,7 @@ pub fn HiveArray(comptime T: type, comptime capacity: u16) type { return struct { const Self = @This(); buffer: [capacity]T = undefined, - available: std.bit_set.IntegerBitSet(capacity) = std.bit_set.IntegerBitSet(capacity).initFull(), + available: bun.bit_set.IntegerBitSet(capacity) = bun.bit_set.IntegerBitSet(capacity).initFull(), pub const size = capacity; pub fn init() Self { @@ -75,7 +75,7 @@ pub fn HiveArray(comptime T: type, comptime capacity: u16) type { pub fn init(allocator: std.mem.Allocator) This { return .{ .allocator = allocator, - .hive = if (capacity > 0) HiveArray(T, capacity).init() else {}, + .hive = if (capacity > 0) HiveArray(T, capacity).init(), }; } diff --git a/src/http.zig b/src/http.zig index 240c5c790a..3b4451c0cc 100644 --- a/src/http.zig +++ b/src/http.zig @@ -18,7 +18,7 @@ const URL = @import("./url.zig").URL; const PercentEncoding = @import("./url.zig").PercentEncoding; pub const Method = @import("./http/method.zig").Method; const Api = @import("./api/schema.zig").Api; -const Lock = @import("./lock.zig").Lock; +const Lock = bun.Mutex; const HTTPClient = @This(); const Zlib = @import("./zlib.zig"); const Brotli = bun.brotli; @@ -27,7 +27,7 @@ const ThreadPool = bun.ThreadPool; const ObjectPool = @import("./pool.zig").ObjectPool; const posix = std.posix; const SOCK = posix.SOCK; -const Arena = @import("./mimalloc_arena.zig").Arena; +const Arena = @import("./allocators/mimalloc_arena.zig").Arena; const ZlibPool = @import("./http/zlib.zig"); const BoringSSL = bun.BoringSSL; const Progress = bun.Progress; @@ -69,7 +69,7 @@ var shared_request_headers_buf: [256]picohttp.Header = undefined; // this doesn't need to be stack memory because it is immediately cloned after use var shared_response_headers_buf: [256]picohttp.Header = undefined; -const end_of_chunked_http1_1_encoding_response_body = "0\r\n\r\n"; +pub const end_of_chunked_http1_1_encoding_response_body = "0\r\n\r\n"; pub const Signals = struct { header_progress: ?*std.atomic.Value(bool) = null, @@ -118,11 +118,32 @@ pub const FetchRedirect = enum(u8) { pub const HTTPRequestBody = union(enum) { bytes: []const u8, sendfile: Sendfile, + stream: struct { + buffer: bun.io.StreamBuffer, + ended: bool, + has_backpressure: bool = false, + pub fn hasEnded(this: *@This()) bool { + return this.ended and this.buffer.isEmpty(); + } + }, + + pub fn isStream(this: *const HTTPRequestBody) bool { + return this.* == .stream; + } + + pub fn deinit(this: *HTTPRequestBody) void { + switch (this.*) { + .sendfile, .bytes => {}, + .stream => |*stream| stream.buffer.deinit(), + } + } pub fn len(this: *const HTTPRequestBody) usize { return switch (this.*) { .bytes => this.bytes.len, .sendfile => this.sendfile.content_size, + // unknow amounts + .stream => std.math.maxInt(usize), }; } }; @@ -555,6 +576,13 @@ fn NewHTTPContext(comptime ssl: bool) type { return ActiveSocket.from(bun.cast(**anyopaque, ptr).*); } + pub fn getTaggedFromSocket(socket: HTTPSocket) ActiveSocket { + if (socket.ext(anyopaque)) |ctx| { + return getTagged(ctx); + } + return ActiveSocket.init(&dead_socket); + } + pending_sockets: HiveArray(PooledSocket, pool_size) = HiveArray(PooledSocket, pool_size).init(), us_socket_context: *uws.SocketContext, @@ -1001,6 +1029,7 @@ fn NewHTTPContext(comptime ssl: bool) type { const UnboundedQueue = @import("./bun.js/unbounded_queue.zig").UnboundedQueue; const Queue = UnboundedQueue(AsyncHTTP, .next); const ShutdownQueue = UnboundedQueue(AsyncHTTP, .next); +const RequestWriteQueue = UnboundedQueue(AsyncHTTP, .next); pub const HTTPThread = struct { loop: *JSC.MiniEventLoop, @@ -1010,17 +1039,88 @@ pub const HTTPThread = struct { queued_tasks: Queue = Queue{}, queued_shutdowns: std.ArrayListUnmanaged(ShutdownMessage) = std.ArrayListUnmanaged(ShutdownMessage){}, - queued_shutdowns_lock: bun.Lock = .{}, + queued_writes: std.ArrayListUnmanaged(WriteMessage) = std.ArrayListUnmanaged(WriteMessage){}, + + queued_shutdowns_lock: bun.Mutex = .{}, + queued_writes_lock: bun.Mutex = .{}, queued_proxy_deref: std.ArrayListUnmanaged(*ProxyTunnel) = std.ArrayListUnmanaged(*ProxyTunnel){}, has_awoken: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), timer: std.time.Timer, - lazy_libdeflater: ?*LibdeflateState = null, + lazy_request_body_buffer: ?*HeapRequestBodyBuffer = null, + + pub const HeapRequestBodyBuffer = struct { + buffer: [512 * 1024]u8 = undefined, + fixed_buffer_allocator: std.heap.FixedBufferAllocator, + + pub usingnamespace bun.New(@This()); + + pub fn init() *@This() { + var this = HeapRequestBodyBuffer.new(.{ + .fixed_buffer_allocator = undefined, + }); + this.fixed_buffer_allocator = std.heap.FixedBufferAllocator.init(&this.buffer); + return this; + } + + pub fn put(this: *@This()) void { + if (http_thread.lazy_request_body_buffer == null) { + // This case hypothetically should never happen + this.fixed_buffer_allocator.reset(); + http_thread.lazy_request_body_buffer = this; + } else { + this.deinit(); + } + } + + pub fn deinit(this: *@This()) void { + this.destroy(); + } + }; + + pub const RequestBodyBuffer = union(enum) { + heap: *HeapRequestBodyBuffer, + stack: std.heap.StackFallbackAllocator(request_body_send_stack_buffer_size), + + pub fn deinit(this: *@This()) void { + switch (this.*) { + .heap => |heap| heap.put(), + .stack => {}, + } + } + + pub fn allocatedSlice(this: *@This()) []u8 { + return switch (this.*) { + .heap => |heap| &heap.buffer, + .stack => |*stack| &stack.buffer, + }; + } + + pub fn allocator(this: *@This()) std.mem.Allocator { + return switch (this.*) { + .heap => |heap| heap.fixed_buffer_allocator.allocator(), + .stack => |*stack| stack.get(), + }; + } + + pub fn toArrayList(this: *@This()) std.ArrayList(u8) { + var arraylist = std.ArrayList(u8).fromOwnedSlice(this.allocator(), this.allocatedSlice()); + arraylist.items.len = 0; + return arraylist; + } + }; const threadlog = Output.scoped(.HTTPThread, true); - + const WriteMessage = struct { + data: []const u8, + async_http_id: u32, + flags: packed struct { + is_tls: bool, + ended: bool, + }, + }; const ShutdownMessage = struct { async_http_id: u32, is_tls: bool, @@ -1033,6 +1133,24 @@ pub const HTTPThread = struct { pub usingnamespace bun.New(@This()); }; + const request_body_send_stack_buffer_size = 32 * 1024; + + pub inline fn getRequestBodySendBuffer(this: *@This(), estimated_size: usize) RequestBodyBuffer { + if (estimated_size >= request_body_send_stack_buffer_size) { + if (this.lazy_request_body_buffer == null) { + log("Allocating HeapRequestBodyBuffer due to {d} bytes request body", .{estimated_size}); + return .{ + .heap = HeapRequestBodyBuffer.init(), + }; + } + + return .{ .heap = bun.take(&this.lazy_request_body_buffer).? }; + } + return .{ + .stack = std.heap.stackFallback(request_body_send_stack_buffer_size, bun.default_allocator), + }; + } + pub fn deflater(this: *@This()) *LibdeflateState { if (this.lazy_libdeflater == null) { this.lazy_libdeflater = LibdeflateState.new(.{ @@ -1109,7 +1227,8 @@ pub const HTTPThread = struct { if (Environment.isWindows) { _ = std.process.getenvW(comptime bun.strings.w("SystemRoot")) orelse { - std.debug.panic("The %SystemRoot% environment variable is not set. Bun needs this set in order for network requests to work.", .{}); + bun.Output.errGeneric("The %SystemRoot% environment variable is not set. Bun needs this set in order for network requests to work.", .{}); + Global.crash(); }; } @@ -1176,11 +1295,13 @@ pub const HTTPThread = struct { } } if (client.http_proxy) |url| { - // https://github.com/oven-sh/bun/issues/11343 - if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http")) { - return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto()); + if (url.href.len > 0) { + // https://github.com/oven-sh/bun/issues/11343 + if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http")) { + return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto()); + } + return error.UnsupportedProxyProtocol; } - return error.UnsupportedProxyProtocol; } return try this.context(is_ssl).connect(client, client.url.hostname, client.url.getPortAuto()); } @@ -1207,6 +1328,65 @@ pub const HTTPThread = struct { } this.queued_shutdowns.clearRetainingCapacity(); } + { + this.queued_writes_lock.lock(); + defer this.queued_writes_lock.unlock(); + for (this.queued_writes.items) |write| { + const ended = write.flags.ended; + defer if (!strings.eqlComptime(write.data, end_of_chunked_http1_1_encoding_response_body) and write.data.len > 0) { + // "0\r\n\r\n" is always a static so no need to free + bun.default_allocator.free(write.data); + }; + if (socket_async_http_abort_tracker.get(write.async_http_id)) |socket_ptr| { + if (write.flags.is_tls) { + const socket = uws.SocketTLS.fromAny(socket_ptr); + if (socket.isClosed() or socket.isShutdown()) { + continue; + } + const tagged = NewHTTPContext(true).getTaggedFromSocket(socket); + if (tagged.get(HTTPClient)) |client| { + if (client.state.original_request_body == .stream) { + var stream = &client.state.original_request_body.stream; + if (write.data.len > 0) { + stream.buffer.write(write.data) catch {}; + } + stream.ended = ended; + if (!stream.has_backpressure) { + client.onWritable( + false, + true, + socket, + ); + } + } + } + } else { + const socket = uws.SocketTCP.fromAny(socket_ptr); + if (socket.isClosed() or socket.isShutdown()) { + continue; + } + const tagged = NewHTTPContext(false).getTaggedFromSocket(socket); + if (tagged.get(HTTPClient)) |client| { + if (client.state.original_request_body == .stream) { + var stream = &client.state.original_request_body.stream; + if (write.data.len > 0) { + stream.buffer.write(write.data) catch {}; + } + stream.ended = ended; + if (!stream.has_backpressure) { + client.onWritable( + false, + false, + socket, + ); + } + } + } + } + } + } + this.queued_writes.clearRetainingCapacity(); + } while (this.queued_proxy_deref.popOrNull()) |http| { http.deref(); @@ -1282,6 +1462,23 @@ pub const HTTPThread = struct { this.loop.loop.wakeup(); } + pub fn scheduleRequestWrite(this: *@This(), http: *AsyncHTTP, data: []const u8, ended: bool) void { + { + this.queued_writes_lock.lock(); + defer this.queued_writes_lock.unlock(); + this.queued_writes.append(bun.default_allocator, .{ + .async_http_id = http.async_http_id, + .data = data, + .flags = .{ + .is_tls = http.client.isHTTPS(), + .ended = ended, + }, + }) catch bun.outOfMemory(); + } + if (this.has_awoken.load(.monotonic)) + this.loop.loop.wakeup(); + } + pub fn scheduleProxyDeref(this: *@This(), proxy: *ProxyTunnel) void { // this is always called on the http thread { @@ -1488,7 +1685,12 @@ pub fn onClose( tunnel.detachAndDeref(); } const in_progress = client.state.stage != .done and client.state.stage != .fail and client.state.flags.is_redirect_pending == false; - + if (client.state.flags.is_redirect_pending) { + // if the connection is closed and we are pending redirect just do the redirect + // in this case we will re-connect or go to a different socket if needed + client.doRedirect(is_ssl, if (is_ssl) &http_thread.https_context else &http_thread.http_context, socket); + return; + } if (in_progress) { // if the peer closed after a full chunk, treat this // as if the transfer had complete, browsers appear to ignore @@ -1544,6 +1746,23 @@ pub inline fn getAllocator() std.mem.Allocator { return default_allocator; } +const max_tls_record_size = 16 * 1024; + +/// Get the buffer we use to write data to the network. +/// +/// For large files, we want to avoid extra network send overhead +/// So we do two things: +/// 1. Use a 32 KB stack buffer for small files +/// 2. Use a 512 KB heap buffer for large files +/// This only has an impact on http:// +/// +/// On https://, we are limited to a 16 KB TLS record size. +inline fn getRequestBodySendBuffer(this: *@This()) HTTPThread.RequestBodyBuffer { + const actual_estimated_size = this.state.request_body.len + this.estimatedRequestHeaderByteLength(); + const estimated_size = if (this.isHTTPS()) @min(actual_estimated_size, max_tls_record_size) else actual_estimated_size * 2; + return http_thread.getRequestBodySendBuffer(estimated_size); +} + pub inline fn cleanup(force: bool) void { default_arena.gc(force); } @@ -1847,6 +2066,7 @@ pub const InternalState = struct { info.deinit(bun.default_allocator); } + this.original_request_body.deinit(); this.* = .{ .body_out_str = body_msg, .compressed_body = MutableString{ .allocator = default_allocator, .list = .{} }, @@ -2000,6 +2220,7 @@ pub const Flags = packed struct { proxy_tunneling: bool = false, reject_unauthorized: bool = true, is_preconnect_only: bool = false, + is_streaming_request_body: bool = false, }; // TODO: reduce the size of this struct @@ -2131,6 +2352,7 @@ pub const Encoding = enum { const host_header_name = "Host"; const content_length_header_name = "Content-Length"; +const chunked_encoded_header = picohttp.Header{ .name = "Transfer-Encoding", .value = "chunked" }; const connection_header = picohttp.Header{ .name = "Connection", .value = "keep-alive" }; const connection_closing_header = picohttp.Header{ .name = "Connection", .value = "close" }; const accept_header = picohttp.Header{ .name = "Accept", .value = "*/*" }; @@ -2745,10 +2967,14 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request { } if (body_len > 0 or this.method.hasRequestBody()) { - request_headers_buf[header_count] = .{ - .name = content_length_header_name, - .value = std.fmt.bufPrint(&this.request_content_len_buf, "{d}", .{body_len}) catch "0", - }; + if (this.flags.is_streaming_request_body) { + request_headers_buf[header_count] = chunked_encoded_header; + } else { + request_headers_buf[header_count] = .{ + .name = content_length_header_name, + .value = std.fmt.bufPrint(&this.request_content_len_buf, "{d}", .{body_len}) catch "0", + }; + } header_count += 1; } @@ -2766,8 +2992,17 @@ pub fn doRedirect( ctx: *NewHTTPContext(is_ssl), socket: NewHTTPContext(is_ssl).HTTPSocket, ) void { + log("doRedirect", .{}); + if (this.state.original_request_body == .stream) { + // we cannot follow redirect from a stream right now + // NOTE: we can use .tee(), reset the readable stream and cancel/wait pending write requests before redirecting. node.js just errors here so we just closeAndFail too. + this.closeAndFail(error.UnexpectedRedirect, is_ssl, socket); + return; + } + this.unix_socket_path.deinit(); this.unix_socket_path = JSC.ZigString.Slice.empty; + // TODO: what we do with stream body? const request_body = if (this.state.flags.resend_request_body_on_redirect and this.state.original_request_body == .bytes) this.state.original_request_body.bytes else @@ -2802,6 +3037,7 @@ pub fn doRedirect( return; } this.state.reset(this.allocator); + log("doRedirect state reset", .{}); // also reset proxy to redirect this.flags.proxy_tunneling = false; if (this.proxy_tunnel) |tunnel| { @@ -2918,6 +3154,114 @@ pub fn onPreconnect(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPCon this.result_callback.run(@fieldParentPtr("client", this), HTTPClientResult{ .fail = null, .metadata = null, .has_more = false }); } +fn estimatedRequestHeaderByteLength(this: *const HTTPClient) usize { + const sliced = this.header_entries.slice(); + var count: usize = 0; + for (sliced.items(.name)) |head| { + count += @as(usize, head.length); + } + for (sliced.items(.value)) |value| { + count += @as(usize, value.length); + } + return count; +} + +const InitialRequestPayloadResult = struct { + has_sent_headers: bool, + has_sent_body: bool, + try_sending_more_data: bool, +}; + +// This exists as a separate function to reduce the amount of time the request body buffer is kept around. +noinline fn sendInitialRequestPayload(this: *HTTPClient, comptime is_first_call: bool, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) !InitialRequestPayloadResult { + var request_body_buffer = this.getRequestBodySendBuffer(); + defer request_body_buffer.deinit(); + var temporary_send_buffer = request_body_buffer.toArrayList(); + defer temporary_send_buffer.deinit(); + + const writer = &temporary_send_buffer.writer(); + + const request = this.buildRequest(this.state.original_request_body.len()); + + if (this.http_proxy) |_| { + if (this.url.isHTTPS()) { + //DO the tunneling! + this.flags.proxy_tunneling = true; + try writeProxyConnect(@TypeOf(writer), writer, this); + } else { + // HTTP do not need tunneling with CONNECT just a slightly different version of the request + try writeProxyRequest( + @TypeOf(writer), + writer, + request, + this, + ); + } + } else { + try writeRequest( + @TypeOf(writer), + writer, + request, + ); + } + + const headers_len = temporary_send_buffer.items.len; + assert(temporary_send_buffer.items.len == writer.context.items.len); + if (this.state.request_body.len > 0 and temporary_send_buffer.capacity - temporary_send_buffer.items.len > 0 and !this.flags.proxy_tunneling) { + var remain = temporary_send_buffer.items.ptr[temporary_send_buffer.items.len..temporary_send_buffer.capacity]; + const wrote = @min(remain.len, this.state.request_body.len); + assert(wrote > 0); + @memcpy(remain[0..wrote], this.state.request_body[0..wrote]); + temporary_send_buffer.items.len += wrote; + } + + const to_send = temporary_send_buffer.items[this.state.request_sent_len..]; + if (comptime Environment.allow_assert) { + assert(!socket.isShutdown()); + assert(!socket.isClosed()); + } + const amount = socket.write( + to_send, + false, + ); + if (comptime is_first_call) { + if (amount == 0) { + // don't worry about it + return .{ + .has_sent_headers = this.state.request_sent_len >= headers_len, + .has_sent_body = false, + .try_sending_more_data = false, + }; + } + } + + if (amount < 0) { + return error.WriteFailed; + } + + this.state.request_sent_len += @as(usize, @intCast(amount)); + const has_sent_headers = this.state.request_sent_len >= headers_len; + + if (has_sent_headers and this.verbose != .none) { + printRequest(request, this.url.href, !this.flags.reject_unauthorized, this.state.request_body, this.verbose == .curl); + } + + if (has_sent_headers and this.state.request_body.len > 0) { + this.state.request_body = this.state.request_body[this.state.request_sent_len - headers_len ..]; + } + + const has_sent_body = if (this.state.original_request_body == .bytes) + this.state.request_body.len == 0 + else + false; + + return .{ + .has_sent_headers = has_sent_headers, + .has_sent_body = has_sent_body, + .try_sending_more_data = amount == @as(c_int, @intCast(to_send.len)) and (!has_sent_body or !has_sent_headers), + }; +} + pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void { if (this.signals.get(.aborted)) { this.closeAndAbort(is_ssl, socket); @@ -2937,101 +3281,24 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s switch (this.state.request_stage) { .pending, .headers => { - var stack_fallback = std.heap.stackFallback(16384, default_allocator); - const allocator = stack_fallback.get(); - var list = std.ArrayList(u8).initCapacity(allocator, stack_fallback.buffer.len) catch unreachable; - defer if (list.capacity > stack_fallback.buffer.len) list.deinit(); - const writer = &list.writer(); - this.setTimeout(socket, 5); - - const request = this.buildRequest(this.state.original_request_body.len()); - - if (this.http_proxy) |_| { - if (this.url.isHTTPS()) { - - //DO the tunneling! - this.flags.proxy_tunneling = true; - writeProxyConnect(@TypeOf(writer), writer, this) catch { - this.closeAndFail(error.OutOfMemory, is_ssl, socket); - return; - }; - } else { - //HTTP do not need tunneling with CONNECT just a slightly different version of the request - - writeProxyRequest( - @TypeOf(writer), - writer, - request, - this, - ) catch { - this.closeAndFail(error.OutOfMemory, is_ssl, socket); - return; - }; - } - } else { - writeRequest( - @TypeOf(writer), - writer, - request, - ) catch { - this.closeAndFail(error.OutOfMemory, is_ssl, socket); - return; - }; - } - - const headers_len = list.items.len; - assert(list.items.len == writer.context.items.len); - if (this.state.request_body.len > 0 and list.capacity - list.items.len > 0 and !this.flags.proxy_tunneling) { - var remain = list.items.ptr[list.items.len..list.capacity]; - const wrote = @min(remain.len, this.state.request_body.len); - assert(wrote > 0); - @memcpy(remain[0..wrote], this.state.request_body[0..wrote]); - list.items.len += wrote; - } - - const to_send = list.items[this.state.request_sent_len..]; - if (comptime Environment.allow_assert) { - assert(!socket.isShutdown()); - assert(!socket.isClosed()); - } - const amount = socket.write( - to_send, - false, - ); - if (comptime is_first_call) { - if (amount == 0) { - // don't worry about it - return; - } - } - - if (amount < 0) { - this.closeAndFail(error.WriteFailed, is_ssl, socket); + const result = sendInitialRequestPayload(this, is_first_call, is_ssl, socket) catch |err| { + this.closeAndFail(err, is_ssl, socket); return; - } - - this.state.request_sent_len += @as(usize, @intCast(amount)); - const has_sent_headers = this.state.request_sent_len >= headers_len; - - if (has_sent_headers and this.verbose != .none) { - printRequest(request, this.url.href, !this.flags.reject_unauthorized, this.state.request_body, this.verbose == .curl); - } - - if (has_sent_headers and this.state.request_body.len > 0) { - this.state.request_body = this.state.request_body[this.state.request_sent_len - headers_len ..]; - } - - const has_sent_body = if (this.state.original_request_body == .bytes) - this.state.request_body.len == 0 - else - false; + }; + const has_sent_headers = result.has_sent_headers; + const has_sent_body = result.has_sent_body; + const try_sending_more_data = result.try_sending_more_data; if (has_sent_headers and has_sent_body) { if (this.flags.proxy_tunneling) { this.state.request_stage = .proxy_handshake; } else { this.state.request_stage = .body; + if (this.flags.is_streaming_request_body) { + // lets signal to start streaming the body + this.progressUpdate(is_ssl, if (is_ssl) &http_thread.https_context else &http_thread.http_context, socket); + } } return; } @@ -3041,15 +3308,19 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s this.state.request_stage = .proxy_handshake; } else { this.state.request_stage = .body; + if (this.flags.is_streaming_request_body) { + // lets signal to start streaming the body + this.progressUpdate(is_ssl, if (is_ssl) &http_thread.https_context else &http_thread.http_context, socket); + } } assert( - // we should have leftover data OR we use sendfile() + // we should have leftover data OR we use sendfile/stream (this.state.original_request_body == .bytes and this.state.request_body.len > 0) or - this.state.original_request_body == .sendfile, + this.state.original_request_body == .sendfile or this.state.original_request_body == .stream, ); // we sent everything, but there's some body leftover - if (amount == @as(c_int, @intCast(to_send.len))) { + if (try_sending_more_data) { this.onWritable(false, is_ssl, socket); } } else { @@ -3076,6 +3347,32 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s return; } }, + .stream => { + var stream = &this.state.original_request_body.stream; + stream.has_backpressure = false; + // to simplify things here the buffer contains the raw data we just need to flush to the socket it + if (stream.buffer.isNotEmpty()) { + const to_send = stream.buffer.slice(); + const amount = socket.write(to_send, true); + if (amount < 0) { + this.closeAndFail(error.WriteFailed, is_ssl, socket); + return; + } + this.state.request_sent_len += @as(usize, @intCast(amount)); + stream.buffer.cursor += @intCast(amount); + if (amount < to_send.len) { + stream.has_backpressure = true; + } + if (stream.buffer.isEmpty()) { + stream.buffer.reset(); + } + } + if (stream.hasEnded()) { + this.state.request_stage = .done; + stream.buffer.deinit(); + return; + } + }, .sendfile => |*sendfile| { if (comptime is_ssl) { @panic("sendfile is only supported without SSL. This code should never have been reached!"); @@ -3098,32 +3395,60 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s } }, .proxy_body => { - if (this.state.original_request_body != .bytes) { - @panic("sendfile is only supported without SSL. This code should never have been reached!"); - } if (this.proxy_tunnel) |proxy| { - this.setTimeout(socket, 5); + switch (this.state.original_request_body) { + .bytes => { + this.setTimeout(socket, 5); - const to_send = this.state.request_body; - const amount = proxy.writeData(to_send) catch return; // just wait and retry when onWritable! if closed internally will call proxy.onClose + const to_send = this.state.request_body; + const amount = proxy.writeData(to_send) catch return; // just wait and retry when onWritable! if closed internally will call proxy.onClose - this.state.request_sent_len += @as(usize, @intCast(amount)); - this.state.request_body = this.state.request_body[@as(usize, @intCast(amount))..]; + this.state.request_sent_len += @as(usize, @intCast(amount)); + this.state.request_body = this.state.request_body[@as(usize, @intCast(amount))..]; - if (this.state.request_body.len == 0) { - this.state.request_stage = .done; - return; + if (this.state.request_body.len == 0) { + this.state.request_stage = .done; + return; + } + }, + .stream => { + var stream = &this.state.original_request_body.stream; + stream.has_backpressure = false; + + // to simplify things here the buffer contains the raw data we just need to flush to the socket it + if (stream.buffer.isNotEmpty()) { + const to_send = stream.buffer.slice(); + const amount = proxy.writeData(to_send) catch return; // just wait and retry when onWritable! if closed internally will call proxy.onClose + this.state.request_sent_len += amount; + stream.buffer.cursor += @truncate(amount); + if (amount < to_send.len) { + stream.has_backpressure = true; + } + if (stream.buffer.isEmpty()) { + stream.buffer.reset(); + } + } + if (stream.hasEnded()) { + this.state.request_stage = .done; + stream.buffer.deinit(); + return; + } + }, + .sendfile => { + @panic("sendfile is only supported without SSL. This code should never have been reached!"); + }, } } }, .proxy_headers => { if (this.proxy_tunnel) |proxy| { this.setTimeout(socket, 5); - var stack_fallback = std.heap.stackFallback(16384, default_allocator); - const allocator = stack_fallback.get(); - var list = std.ArrayList(u8).initCapacity(allocator, stack_fallback.buffer.len) catch unreachable; - defer if (list.capacity > stack_fallback.buffer.len) list.deinit(); - const writer = &list.writer(); + var stack_buffer = std.heap.stackFallback(1024 * 16, bun.default_allocator); + const allocator = stack_buffer.get(); + var temporary_send_buffer = std.ArrayList(u8).fromOwnedSlice(allocator, &stack_buffer.buffer); + temporary_send_buffer.items.len = 0; + defer temporary_send_buffer.deinit(); + const writer = &temporary_send_buffer.writer(); const request = this.buildRequest(this.state.request_body.len); writeRequest( @@ -3135,17 +3460,17 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s return; }; - const headers_len = list.items.len; - assert(list.items.len == writer.context.items.len); - if (this.state.request_body.len > 0 and list.capacity - list.items.len > 0) { - var remain = list.items.ptr[list.items.len..list.capacity]; + const headers_len = temporary_send_buffer.items.len; + assert(temporary_send_buffer.items.len == writer.context.items.len); + if (this.state.request_body.len > 0 and temporary_send_buffer.capacity - temporary_send_buffer.items.len > 0) { + var remain = temporary_send_buffer.items.ptr[temporary_send_buffer.items.len..temporary_send_buffer.capacity]; const wrote = @min(remain.len, this.state.request_body.len); assert(wrote > 0); @memcpy(remain[0..wrote], this.state.request_body[0..wrote]); - list.items.len += wrote; + temporary_send_buffer.items.len += wrote; } - const to_send = list.items[this.state.request_sent_len..]; + const to_send = temporary_send_buffer.items[this.state.request_sent_len..]; if (comptime Environment.allow_assert) { assert(!socket.isShutdown()); assert(!socket.isClosed()); @@ -3175,6 +3500,10 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s if (has_sent_headers) { this.state.request_stage = .proxy_body; + if (this.flags.is_streaming_request_body) { + // lets signal to start streaming the body + this.progressUpdate(is_ssl, if (is_ssl) &http_thread.https_context else &http_thread.http_context, socket); + } assert(this.state.request_body.len > 0); // we sent everything, but there's some body leftover @@ -3558,6 +3887,7 @@ pub const HTTPClientResult = struct { body: ?*MutableString = null, has_more: bool = false, redirected: bool = false, + can_stream: bool = false, fail: ?anyerror = null, @@ -3665,6 +3995,8 @@ pub fn toResult(this: *HTTPClient) HTTPClientResult { .has_more = certificate_info != null or (this.state.fail == null and !this.state.isDone()), .body_size = body_size, .certificate_info = certificate_info, + // we can stream the request_body at this stage + .can_stream = (this.state.request_stage == .body or this.state.request_stage == .proxy_body) and this.flags.is_streaming_request_body, }; } diff --git a/src/http/headers.zig b/src/http/headers.zig index 4c76f3b4a5..fe1f08e98b 100644 --- a/src/http/headers.zig +++ b/src/http/headers.zig @@ -1,8 +1,8 @@ const Api = @import("../api/schema.zig").Api; const std = @import("std"); - +const bun = @import("root").bun; pub const Kv = struct { name: Api.StringPointer, value: Api.StringPointer, }; -pub const Entries = std.MultiArrayList(Kv); +pub const Entries = bun.MultiArrayList(Kv); diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index 5918f68078..734b795835 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -204,16 +204,10 @@ const CppWebSocket = opaque { } }; -const body_buf_len = 16384 - 16; -const BodyBufBytes = [body_buf_len]u8; - -const BodyBufPool = ObjectPool(BodyBufBytes, null, true, 4); -const BodyBuf = BodyBufPool.Node; - pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { return struct { pub const Socket = uws.NewSocketHandler(ssl); - tcp: ?Socket = null, + tcp: Socket, outgoing_websocket: ?*CppWebSocket, input_body_buf: []u8 = &[_]u8{}, client_protocol: []const u8 = "", @@ -225,13 +219,14 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { hostname: [:0]const u8 = "", poll_ref: Async.KeepAlive = Async.KeepAlive.init(), state: State = .initializing, + ref_count: u32 = 1, const State = enum { initializing, reading, failed }; pub const name = if (ssl) "WebSocketHTTPSClient" else "WebSocketHTTPClient"; pub const shim = JSC.Shimmer("Bun", name, @This()); - pub usingnamespace bun.New(@This()); + pub usingnamespace bun.NewRefCounted(@This(), deinit); const HTTPClient = @This(); pub fn register(_: *JSC.JSGlobalObject, _: *anyopaque, ctx: *uws.SocketContext) callconv(.C) void { @@ -253,6 +248,12 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { ); } + pub fn deinit(this: *HTTPClient) void { + this.clearData(); + bun.debugAssert(this.tcp.isDetached()); + this.destroy(); + } + /// On error, this returns null. /// Returning null signals to the parent function that the connection failed. pub fn connect( @@ -284,7 +285,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { ) catch return null; var client = HTTPClient.new(.{ - .tcp = null, + .tcp = .{ .socket = .{ .detached = {} } }, .outgoing_websocket = websocket, .input_body_buf = body, .websocket_protocol = client_protocol_hash, @@ -312,8 +313,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { )) |out| { // I don't think this case gets reached. if (out.state == .failed) { - client.clearData(); - client.destroy(); + client.deref(); return null; } bun.Analytics.Features.WebSocket += 1; @@ -324,12 +324,13 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { } } - out.tcp.?.timeout(120); + out.tcp.timeout(120); out.state = .reading; + // +1 for cpp_websocket + out.ref(); return out; } else |_| { - client.clearData(); - client.destroy(); + client.deref(); } return null; @@ -347,40 +348,58 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { } pub fn cancel(this: *HTTPClient) callconv(.C) void { this.clearData(); - this.outgoing_websocket = null; - const tcp = this.tcp orelse return; - this.tcp = null; + + // Either of the below two operations - closing the TCP socket or clearing the C++ reference could trigger a deref + // Therefore, we need to make sure the `this` pointer is valid until the end of the function. + this.ref(); + defer this.deref(); + + // The C++ end of the socket is no longer holding a reference to this, sowe must clear it. + if (this.outgoing_websocket != null) { + this.outgoing_websocket = null; + this.deref(); + } + // no need to be .failure we still wanna to send pending SSL buffer + close_notify if (comptime ssl) { - tcp.close(.normal); + this.tcp.close(.normal); } else { - tcp.close(.failure); + this.tcp.close(.failure); } } pub fn fail(this: *HTTPClient, code: ErrorCode) void { - log("onFail", .{}); + log("onFail: {s}", .{@tagName(code)}); JSC.markBinding(@src()); + + this.ref(); + defer this.deref(); + + this.dispatchAbruptClose(code); + + if (comptime ssl) { + this.tcp.close(.normal); + } else { + this.tcp.close(.failure); + } + } + + fn dispatchAbruptClose(this: *HTTPClient, code: ErrorCode) void { if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; ws.didAbruptClose(code); + this.deref(); } - - this.cancel(); } pub fn handleClose(this: *HTTPClient, _: Socket, _: c_int, _: ?*anyopaque) void { log("onClose", .{}); JSC.markBinding(@src()); this.clearData(); - this.tcp = null; + this.tcp.detach(); + this.dispatchAbruptClose(ErrorCode.ended); - if (this.outgoing_websocket) |ws| { - this.outgoing_websocket = null; - - ws.didAbruptClose(ErrorCode.ended); - } - this.destroy(); + this.deref(); } pub fn terminate(this: *HTTPClient, code: ErrorCode) void { @@ -447,18 +466,18 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { } pub fn isSameSocket(this: *HTTPClient, socket: Socket) bool { - if (this.tcp) |tcp| { - return socket.socket.eq(tcp.socket); - } - return false; + return socket.socket.eq(this.tcp.socket); } pub fn handleData(this: *HTTPClient, socket: Socket, data: []const u8) void { log("onData", .{}); if (this.outgoing_websocket == null) { this.clearData(); + socket.close(.failure); return; } + this.ref(); + defer this.deref(); bun.assert(this.isSameSocket(socket)); @@ -499,10 +518,8 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { this.processResponse(response, body[@as(usize, @intCast(response.bytes_read))..]); } - pub fn handleEnd(this: *HTTPClient, socket: Socket) void { + pub fn handleEnd(this: *HTTPClient, _: Socket) void { log("onEnd", .{}); - - bun.assert(this.isSameSocket(socket)); this.terminate(ErrorCode.ended); } @@ -618,14 +635,34 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { this.clearData(); JSC.markBinding(@src()); - if (this.tcp != null and this.outgoing_websocket != null) { - this.tcp.?.timeout(0); + if (!this.tcp.isClosed() and this.outgoing_websocket != null) { + this.tcp.timeout(0); log("onDidConnect", .{}); - this.outgoing_websocket.?.didConnect(this.tcp.?.socket.get().?, overflow.ptr, overflow.len); + // Once for the outgoing_websocket. + defer this.deref(); + const ws = bun.take(&this.outgoing_websocket).?; + const socket = this.tcp; + + this.tcp.detach(); + // Once again for the TCP socket. + defer this.deref(); + + ws.didConnect(socket.socket.get().?, overflow.ptr, overflow.len); + } else if (this.tcp.isClosed()) { + this.terminate(ErrorCode.cancel); + } else if (this.outgoing_websocket == null) { + this.tcp.close(.failure); } } + pub fn memoryCost(this: *HTTPClient) callconv(.C) usize { + var cost: usize = @sizeOf(HTTPClient); + cost += this.body.capacity; + cost += this.to_send.len; + return cost; + } + pub fn handleWritable( this: *HTTPClient, socket: Socket, @@ -635,6 +672,9 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { if (this.to_send.len == 0) return; + this.ref(); + defer this.deref(); + // Do not set MSG_MORE, see https://github.com/oven-sh/bun/issues/4010 const wrote = socket.write(this.to_send, false); if (wrote < 0) { @@ -653,13 +693,13 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { // In theory, this could be called immediately // In that case, we set `state` to `failed` and return, expecting the parent to call `destroy`. pub fn handleConnectError(this: *HTTPClient, _: Socket, _: c_int) void { - this.tcp = null; + this.tcp.detach(); - // the socket is freed by usockets when the connection fails + // For the TCP socket. + defer this.deref(); if (this.state == .reading) { this.terminate(ErrorCode.failed_to_connect); - this.destroy(); } else { this.state = .failed; } @@ -669,6 +709,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { .connect = connect, .cancel = cancel, .register = register, + .memoryCost = memoryCost, }); comptime { @@ -682,6 +723,9 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { @export(register, .{ .name = Export[2].symbol_name, }); + @export(memoryCost, .{ + .name = Export[3].symbol_name, + }); } } }; @@ -951,7 +995,7 @@ const Copy = union(enum) { pub fn NewWebSocketClient(comptime ssl: bool) type { return struct { pub const Socket = uws.NewSocketHandler(ssl); - tcp: ?Socket = null, + tcp: Socket, outgoing_websocket: ?*CppWebSocket = null, receive_state: ReceiveState = ReceiveState.need_header, @@ -981,6 +1025,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { initial_data_handler: ?*InitialDataHandler = null, event_loop: *JSC.EventLoop = undefined, + ref_count: u32 = 1, pub const name = if (ssl) "WebSocketClientTLS" else "WebSocketClient"; @@ -989,7 +1034,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { const WebSocket = @This(); - pub usingnamespace bun.New(@This()); + pub usingnamespace bun.NewRefCounted(@This(), deinit); pub fn register(global: *JSC.JSGlobalObject, loop_: *anyopaque, ctx_: *anyopaque) callconv(.C) void { const vm = global.bunVM(); const loop = @as(*uws.Loop, @ptrCast(@alignCast(loop_))); @@ -1032,13 +1077,12 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { pub fn cancel(this: *WebSocket) callconv(.C) void { log("cancel", .{}); this.clearData(); - const tcp = this.tcp orelse return; - this.tcp = null; - // no need to be .failure we still wanna to send pending SSL buffer + close_notify + if (comptime ssl) { - tcp.close(.normal); + // we still want to send pending SSL buffer + close_notify + this.tcp.close(.normal); } else { - tcp.close(.failure); + this.tcp.close(.failure); } } @@ -1047,8 +1091,8 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; log("fail ({s})", .{@tagName(code)}); - ws.didAbruptClose(code); + this.deref(); } this.cancel(); @@ -1087,10 +1131,12 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { log("onClose", .{}); JSC.markBinding(@src()); this.clearData(); - if (this.outgoing_websocket) |ws| { - this.outgoing_websocket = null; - ws.didAbruptClose(ErrorCode.ended); - } + this.tcp.detach(); + + this.dispatchAbruptClose(ErrorCode.ended); + + // For the socket. + this.deref(); } pub fn terminate(this: *WebSocket, code: ErrorCode) void { @@ -1131,7 +1177,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // this function encodes to UTF-16 if > 127 // so we don't need to worry about latin1 non-ascii code points // we avoid trim since we wanna keep the utf8 validation intact - const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true, false) catch { + const utf16_bytes_ = strings.toUTF16Alloc(bun.default_allocator, data_, true, false) catch { this.terminate(ErrorCode.invalid_utf8); return; }; @@ -1197,6 +1243,8 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // after receiving close we should ignore the data if (this.close_received) return; + this.ref(); + defer this.deref(); // Due to scheduling, it is possible for the websocket onData // handler to run with additional data before the microtask queue is @@ -1212,7 +1260,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { bun.assert(this.initial_data_handler == null); // If we disconnected for any reason in the re-entrant case, we should just ignore the data - if (this.outgoing_websocket == null or this.tcp == null or this.tcp.?.isShutdown() or this.tcp.?.isClosed()) + if (this.outgoing_websocket == null or !this.hasTCP()) return; } @@ -1523,9 +1571,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { } pub fn sendClose(this: *WebSocket) void { - if (this.tcp) |tcp| { - this.sendCloseWithBody(tcp, 1000, null, 0); - } + this.sendCloseWithBody(this.tcp, 1000, null, 0); } fn enqueueEncodedBytes( @@ -1569,11 +1615,9 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { if (do_write) { if (comptime Environment.allow_assert) { - if (this.tcp) |tcp| { - bun.assert(!tcp.isShutdown()); - bun.assert(!tcp.isClosed()); - bun.assert(tcp.isEstablished()); - } + bun.assert(!this.tcp.isShutdown()); + bun.assert(!this.tcp.isClosed()); + bun.assert(this.tcp.isEstablished()); } return this.sendBuffer(this.send_buffer.readableSlice(0)); } @@ -1587,8 +1631,10 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { ) bool { bun.assert(out_buf.len > 0); // Do not set MSG_MORE, see https://github.com/oven-sh/bun/issues/4010 - const tcp = this.tcp orelse return false; - const wrote = tcp.write(out_buf, false); + if (this.tcp.isClosed()) { + return false; + } + const wrote = this.tcp.write(out_buf, false); if (wrote < 0) { this.terminate(ErrorCode.failed_to_write); return false; @@ -1604,7 +1650,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { fn sendPong(this: *WebSocket, socket: Socket) bool { if (socket.isClosed() or socket.isShutdown()) { - this.dispatchAbruptClose(); + this.dispatchAbruptClose(ErrorCode.ended); return false; } @@ -1636,7 +1682,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { ) void { log("Sending close with code {d}", .{code}); if (socket.isClosed() or socket.isShutdown()) { - this.dispatchAbruptClose(); + this.dispatchAbruptClose(ErrorCode.ended); this.clearData(); return; } @@ -1673,15 +1719,12 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { Mask.fill(this.globalThis, mask_buf, slice[6..], slice[6..]); if (this.enqueueEncodedBytes(socket, slice)) { - this.dispatchClose(code, &reason); this.clearData(); + this.dispatchClose(code, &reason); } } pub fn isSameSocket(this: *WebSocket, socket: Socket) bool { - if (this.tcp) |tcp| { - return socket.socket.eq(tcp.socket); - } - return false; + return socket.socket.eq(this.tcp.socket); } pub fn handleEnd(this: *WebSocket, socket: Socket) void { @@ -1707,6 +1750,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { this.terminate(ErrorCode.timeout); } pub fn handleConnectError(this: *WebSocket, _: Socket, _: c_int) void { + this.tcp.detach(); this.terminate(ErrorCode.failed_to_connect); } @@ -1721,7 +1765,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { op: u8, ) callconv(.C) void { if (!this.hasTCP() or op > 0xF) { - this.dispatchAbruptClose(); + this.dispatchAbruptClose(ErrorCode.ended); return; } @@ -1733,17 +1777,14 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { if (!this.hasBackpressure() and frame_size < stack_frame_size) { var inline_buf: [stack_frame_size]u8 = undefined; bytes.copy(this.globalThis, inline_buf[0..frame_size], slice.len, opcode); - _ = this.enqueueEncodedBytes(this.tcp.?, inline_buf[0..frame_size]); + _ = this.enqueueEncodedBytes(this.tcp, inline_buf[0..frame_size]); return; } _ = this.sendData(bytes, !this.hasBackpressure(), opcode); } fn hasTCP(this: *WebSocket) bool { - if (this.tcp) |tcp| { - return !tcp.isClosed() and !tcp.isShutdown(); - } - return false; + return !this.tcp.isClosed() and !this.tcp.isShutdown(); } pub fn writeString( @@ -1753,10 +1794,10 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { ) callconv(.C) void { const str = str_.*; if (!this.hasTCP()) { - this.dispatchAbruptClose(); + this.dispatchAbruptClose(ErrorCode.ended); return; } - const tcp = this.tcp.?; + const tcp = this.tcp; // Note: 0 is valid @@ -1796,12 +1837,13 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { ); } - fn dispatchAbruptClose(this: *WebSocket) void { + fn dispatchAbruptClose(this: *WebSocket, code: ErrorCode) void { var out = this.outgoing_websocket orelse return; this.poll_ref.unref(this.globalThis.bunVM()); JSC.markBinding(@src()); this.outgoing_websocket = null; - out.didAbruptClose(ErrorCode.closed); + out.didAbruptClose(code); + this.deref(); } fn dispatchClose(this: *WebSocket, code: u16, reason: *const bun.String) void { @@ -1810,12 +1852,13 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { JSC.markBinding(@src()); this.outgoing_websocket = null; out.didClose(code, reason); + this.deref(); } pub fn close(this: *WebSocket, code: u16, reason: ?*const JSC.ZigString) callconv(.C) void { if (!this.hasTCP()) return; - const tcp = this.tcp.?; + const tcp = this.tcp; var close_reason_buf: [128]u8 = undefined; if (reason) |str| { inner: { @@ -1837,6 +1880,8 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { pub const Handle = JSC.AnyTask.New(@This(), handle); + pub usingnamespace bun.New(@This()); + pub fn handleWithoutDeinit(this: *@This()) void { var this_socket = this.adopted orelse return; this.adopted = null; @@ -1844,17 +1889,19 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { var ws = this.ws; defer ws.unref(); - if (this_socket.outgoing_websocket != null and this_socket.tcp != null) { - this_socket.handleData(this_socket.tcp.?, this.slice); + if (this_socket.outgoing_websocket != null and !this_socket.tcp.isClosed()) { + this_socket.handleData(this_socket.tcp, this.slice); } } pub fn handle(this: *@This()) void { - defer { - bun.default_allocator.free(this.slice); - bun.default_allocator.destroy(this); - } this.handleWithoutDeinit(); + this.deinit(); + } + + pub fn deinit(this: *@This()) void { + bun.default_allocator.free(this.slice); + this.destroy(); } }; @@ -1869,13 +1916,14 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { const tcp = @as(*uws.Socket, @ptrCast(input_socket)); const ctx = @as(*uws.SocketContext, @ptrCast(socket_ctx)); var ws = WebSocket.new(WebSocket{ - .tcp = undefined, + .tcp = .{ .socket = .{ .detached = {} } }, .outgoing_websocket = outgoing, .globalThis = globalThis, .send_buffer = bun.LinearFifo(u8, .Dynamic).init(bun.default_allocator), .receive_buffer = bun.LinearFifo(u8, .Dynamic).init(bun.default_allocator), .event_loop = globalThis.bunVM().eventLoop(), }); + if (!Socket.adoptPtr( tcp, ctx, @@ -1883,7 +1931,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { "tcp", ws, )) { - ws.destroy(); + ws.deref(); return null; } @@ -1893,12 +1941,11 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { const buffered_slice: []u8 = buffered_data[0..buffered_data_len]; if (buffered_slice.len > 0) { - const initial_data = bun.default_allocator.create(InitialDataHandler) catch unreachable; - initial_data.* = .{ + const initial_data = InitialDataHandler.new(.{ .adopted = ws, .slice = buffered_slice, .ws = outgoing, - }; + }); // Use a higher-priority callback for the initial onData handler globalThis.queueMicrotaskCallback(initial_data, InitialDataHandler.handle); @@ -1907,6 +1954,10 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // before the initial data handler is called outgoing.ref(); } + + // And lastly, ref the new websocket since C++ has a reference to it + ws.ref(); + return @as( *anyopaque, @ptrCast(ws), @@ -1917,15 +1968,33 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { log("finalize", .{}); this.clearData(); - this.outgoing_websocket = null; - const tcp = this.tcp orelse return; - this.tcp = null; - // no need to be .failure we still wanna to send pending SSL buffer + close_notify - if (comptime ssl) { - tcp.close(.normal); - } else { - tcp.close(.failure); + // This is only called by outgoing_websocket. + if (this.outgoing_websocket != null) { + this.outgoing_websocket = null; + this.deref(); } + + if (!this.tcp.isClosed()) { + // no need to be .failure we still wanna to send pending SSL buffer + close_notify + if (comptime ssl) { + this.tcp.close(.normal); + } else { + this.tcp.close(.failure); + } + } + } + + pub fn deinit(this: *WebSocket) void { + this.clearData(); + this.destroy(); + } + + pub fn memoryCost(this: *WebSocket) callconv(.C) usize { + var cost: usize = @sizeOf(WebSocket); + cost += this.send_buffer.buf.len; + cost += this.receive_buffer.buf.len; + // This is under-estimated a little, as we don't include usockets context. + return cost; } pub const Export = shim.exportFunctions(.{ @@ -1936,6 +2005,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { .register = register, .init = init, .finalize = finalize, + .memoryCost = memoryCost, }); comptime { @@ -1947,6 +2017,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { @export(register, .{ .name = Export[4].symbol_name }); @export(init, .{ .name = Export[5].symbol_name }); @export(finalize, .{ .name = Export[6].symbol_name }); + @export(memoryCost, .{ .name = Export[7].symbol_name }); } } }; diff --git a/src/http/zlib.zig b/src/http/zlib.zig index 3055ac99f1..c492ce743a 100644 --- a/src/http/zlib.zig +++ b/src/http/zlib.zig @@ -1,4 +1,4 @@ -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const std = @import("std"); const MutableString = bun.MutableString; const getAllocator = @import("../http.zig").getAllocator; diff --git a/src/import_record.zig b/src/import_record.zig index 87d1f93ac5..a81f5f180b 100644 --- a/src/import_record.zig +++ b/src/import_record.zig @@ -7,24 +7,26 @@ const Index = @import("ast/base.zig").Index; const Api = @import("./api/schema.zig").Api; pub const ImportKind = enum(u8) { - /// An entry point provided by the user - entry_point, + /// An entry point provided to `bun run` or `bun` + entry_point_run = 0, + /// An entry point provided to `bun build` or `Bun.build` + entry_point_build = 1, /// An ES6 import or re-export statement - stmt, + stmt = 2, /// A call to "require()" - require, + require = 3, /// An "import()" expression with a string argument - dynamic, + dynamic = 4, /// A call to "require.resolve()" - require_resolve, + require_resolve = 5, /// A CSS "@import" rule - at, + at = 6, /// A CSS "@import" rule with import conditions - at_conditional, + at_conditional = 7, /// A CSS "url(...)" token - url, + url = 8, - internal, + internal = 9, pub const Label = std.EnumArray(ImportKind, []const u8); pub const all_labels: Label = brk: { @@ -32,7 +34,8 @@ pub const ImportKind = enum(u8) { // - src/js/builtins/codegen/replacements.ts // - packages/bun-types/bun.d.ts var labels = Label.initFill(""); - labels.set(ImportKind.entry_point, "entry-point"); + labels.set(ImportKind.entry_point_run, "entry-point-run"); + labels.set(ImportKind.entry_point_build, "entry-point-build"); labels.set(ImportKind.stmt, "import-statement"); labels.set(ImportKind.require, "require-call"); labels.set(ImportKind.dynamic, "dynamic-import"); @@ -45,7 +48,8 @@ pub const ImportKind = enum(u8) { pub const error_labels: Label = brk: { var labels = Label.initFill(""); - labels.set(ImportKind.entry_point, "entry point"); + labels.set(ImportKind.entry_point_run, "entry point (run)"); + labels.set(ImportKind.entry_point_build, "entry point (build)"); labels.set(ImportKind.stmt, "import"); labels.set(ImportKind.require, "require()"); labels.set(ImportKind.dynamic, "import()"); diff --git a/src/ini.zig b/src/ini.zig index 92cefeae54..5fe5499c5b 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -528,9 +528,9 @@ pub const IniTestingAPIs = struct { defer arena.deinit(); const envjs = callframe.argument(1); - const env = if (envjs.isEmptyOrUndefinedOrNull()) globalThis.bunVM().bundler.env else brk: { + const env = if (envjs.isEmptyOrUndefinedOrNull()) globalThis.bunVM().transpiler.env else brk: { var envmap = bun.DotEnv.Map.HashTable.init(allocator); - var object_iter = JSC.JSPropertyIterator(.{ + var object_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, envjs); @@ -538,7 +538,7 @@ pub const IniTestingAPIs = struct { try envmap.ensureTotalCapacity(object_iter.len); - while (object_iter.next()) |key| { + while (try object_iter.next()) |key| { const keyslice = try key.toOwnedSlice(allocator); var value = object_iter.value; if (value == .undefined) continue; @@ -565,7 +565,9 @@ pub const IniTestingAPIs = struct { const install = try allocator.create(bun.Schema.Api.BunInstall); install.* = std.mem.zeroes(bun.Schema.Api.BunInstall); - loadNpmrc(allocator, install, env, ".npmrc", &log, &source) catch { + var configs = std.ArrayList(ConfigIterator.Item).init(allocator); + defer configs.deinit(); + loadNpmrc(allocator, install, env, ".npmrc", &log, &source, &configs) catch { return log.toJS(globalThis, allocator, "error"); }; @@ -591,12 +593,12 @@ pub const IniTestingAPIs = struct { default_registry_password.deref(); } - return globalThis.createObjectFromStruct(.{ - .default_registry_url = default_registry_url.toJS(globalThis), - .default_registry_token = default_registry_token.toJS(globalThis), - .default_registry_username = default_registry_username.toJS(globalThis), - .default_registry_password = default_registry_password.toJS(globalThis), - }).toJS(); + return JSC.JSObject.create(.{ + .default_registry_url = default_registry_url, + .default_registry_token = default_registry_token, + .default_registry_username = default_registry_username, + .default_registry_password = default_registry_password, + }, globalThis).toJS(); } pub fn parse(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { @@ -609,7 +611,7 @@ pub const IniTestingAPIs = struct { const utf8str = bunstr.toUTF8(bun.default_allocator); defer utf8str.deinit(); - var parser = Parser.init(bun.default_allocator, "", utf8str.slice(), globalThis.bunVM().bundler.env); + var parser = Parser.init(bun.default_allocator, "", utf8str.slice(), globalThis.bunVM().transpiler.env); defer parser.deinit(); try parser.parse(parser.arena.allocator()); @@ -701,6 +703,16 @@ pub const ConfigIterator = struct { } }; + /// Duplicate ConfigIterator.Item + pub fn dupe(this: *const Item, allocator: Allocator) OOM!?Item { + return .{ + .registry_url = try allocator.dupe(u8, this.registry_url), + .optname = this.optname, + .value = try allocator.dupe(u8, this.value), + .loc = this.loc, + }; + } + /// Duplicate the value, decoding it if it is base64 encoded. pub fn dupeValueDecoded( this: *const Item, @@ -733,6 +745,11 @@ pub const ConfigIterator = struct { pub fn format(this: *const @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { try writer.print("//{s}:{s}={s}", .{ this.registry_url, @tagName(this.optname), this.value }); } + + pub fn deinit(self: *const Item, allocator: Allocator) void { + allocator.free(self.registry_url); + allocator.free(self.value); + } }; pub fn next(this: *ConfigIterator) ?Option(Item) { @@ -838,36 +855,49 @@ pub const ScopeIterator = struct { } }; -pub fn loadNpmrcFromFile( +pub fn loadNpmrcConfig( allocator: std.mem.Allocator, install: *bun.Schema.Api.BunInstall, env: *bun.DotEnv.Loader, auto_loaded: bool, - npmrc_path: [:0]const u8, + npmrc_paths: []const [:0]const u8, ) void { var log = bun.logger.Log.init(allocator); defer log.deinit(); - const source = bun.sys.File.toSource(npmrc_path, allocator).unwrap() catch |err| { - if (auto_loaded) return; - Output.err(err, "failed to read .npmrc: \"{s}\"", .{npmrc_path}); - Global.crash(); - }; - defer allocator.free(source.contents); - - loadNpmrc(allocator, install, env, npmrc_path, &log, &source) catch |err| { - switch (err) { - error.OutOfMemory => bun.outOfMemory(), + // npmrc registry configurations are shared between all npmrc files + // so we need to collect them as we go for the final registry map + // to be created at the end. + var configs = std.ArrayList(ConfigIterator.Item).init(allocator); + defer { + for (configs.items) |item| { + item.deinit(allocator); } - }; - if (log.hasErrors()) { - if (log.errors == 1) - Output.warn("Encountered an error while reading .npmrc:\n\n", .{}) - else - Output.warn("Encountered errors while reading .npmrc:\n\n", .{}); - Output.flush(); + configs.deinit(); + } + + for (npmrc_paths) |npmrc_path| { + const source = bun.sys.File.toSource(npmrc_path, allocator).unwrap() catch |err| { + if (auto_loaded) continue; + Output.err(err, "failed to read .npmrc: \"{s}\"", .{npmrc_path}); + Global.crash(); + }; + defer allocator.free(source.contents); + + loadNpmrc(allocator, install, env, npmrc_path, &log, &source, &configs) catch |err| { + switch (err) { + error.OutOfMemory => bun.outOfMemory(), + } + }; + if (log.hasErrors()) { + if (log.errors == 1) + Output.warn("Encountered an error while reading {s}:\n\n", .{npmrc_path}) + else + Output.warn("Encountered errors while reading {s}:\n\n", .{npmrc_path}); + Output.flush(); + } + log.print(Output.errorWriter()) catch {}; } - log.print(Output.errorWriter()) catch {}; } pub fn loadNpmrc( @@ -877,11 +907,11 @@ pub fn loadNpmrc( npmrc_path: [:0]const u8, log: *bun.logger.Log, source: *const bun.logger.Source, + configs: *std.ArrayList(ConfigIterator.Item), ) OOM!void { var parser = bun.ini.Parser.init(allocator, npmrc_path, source.contents, env); defer parser.deinit(); try parser.parse(parser.arena.allocator()); - // Need to be very, very careful here with strings. // They are allocated in the Parser's arena, which of course gets // deinitialized at the end of the scope. @@ -941,6 +971,68 @@ pub fn loadNpmrc( } } + if (out.asProperty("omit")) |omit| { + switch (omit.expr.data) { + .e_string => |str| { + if (str.eqlComptime("dev")) { + install.save_dev = false; + } else if (str.eqlComptime("peer")) { + install.save_peer = false; + } else if (str.eqlComptime("optional")) { + install.save_optional = false; + } + }, + .e_array => |arr| { + for (arr.items.slice()) |item| { + switch (item.data) { + .e_string => |str| { + if (str.eqlComptime("dev")) { + install.save_dev = false; + } else if (str.eqlComptime("peer")) { + install.save_peer = false; + } else if (str.eqlComptime("optional")) { + install.save_optional = false; + } + }, + else => {}, + } + } + }, + else => {}, + } + } + + if (out.asProperty("include")) |omit| { + switch (omit.expr.data) { + .e_string => |str| { + if (str.eqlComptime("dev")) { + install.save_dev = true; + } else if (str.eqlComptime("peer")) { + install.save_peer = true; + } else if (str.eqlComptime("optional")) { + install.save_optional = true; + } + }, + .e_array => |arr| { + for (arr.items.slice()) |item| { + switch (item.data) { + .e_string => |str| { + if (str.eqlComptime("dev")) { + install.save_dev = true; + } else if (str.eqlComptime("peer")) { + install.save_peer = true; + } else if (str.eqlComptime("optional")) { + install.save_optional = true; + } + }, + else => {}, + } + } + }, + else => {}, + } + } + var registry_map = install.scoped orelse bun.Schema.Api.NpmRegistryMap{}; // Process scopes @@ -984,7 +1076,7 @@ pub fn loadNpmrc( // Process registry configuration out: { const count = brk: { - var count: usize = 0; + var count: usize = configs.items.len; for (parser.out.data.e_object.properties.slice()) |prop| { if (prop.key) |keyexpr| { if (keyexpr.asUtf8StringLiteral()) |key| { @@ -1068,20 +1160,53 @@ pub fn loadNpmrc( }, else => {}, } - const conf_item_url = bun.URL.parse(conf_item.registry_url); + if (try conf_item_.dupe(allocator)) |x| try configs.append(x); + } + } - if (std.mem.eql(u8, bun.strings.withoutTrailingSlash(default_registry_url.host), bun.strings.withoutTrailingSlash(conf_item_url.host))) { - const v: *bun.Schema.Api.NpmRegistry = brk: { - if (install.default_registry) |*r| break :brk r; - install.default_registry = bun.Schema.Api.NpmRegistry{ - .password = "", - .token = "", - .username = "", - .url = Registry.default_url, - }; - break :brk &install.default_registry.?; + for (configs.items) |conf_item| { + const conf_item_url = bun.URL.parse(conf_item.registry_url); + + if (std.mem.eql(u8, bun.strings.withoutTrailingSlash(default_registry_url.host), bun.strings.withoutTrailingSlash(conf_item_url.host))) { + // Apply config to default registry + const v: *bun.Schema.Api.NpmRegistry = brk: { + if (install.default_registry) |*r| break :brk r; + install.default_registry = bun.Schema.Api.NpmRegistry{ + .password = "", + .token = "", + .username = "", + .url = Registry.default_url, }; + break :brk &install.default_registry.?; + }; + switch (conf_item.optname) { + ._authToken => { + if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.token = x; + }, + .username => { + if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.username = x; + }, + ._password => { + if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.password = x; + }, + ._auth => { + try @"handle _auth"(allocator, v, &conf_item, log, source); + }, + .email, .certfile, .keyfile => unreachable, + } + } + + for (registry_map.scopes.keys(), registry_map.scopes.values()) |*k, *v| { + const url = url_map.get(k.*) orelse unreachable; + + if (std.mem.eql(u8, bun.strings.withoutTrailingSlash(url.host), bun.strings.withoutTrailingSlash(conf_item_url.host))) { + if (conf_item_url.hostname.len > 0) { + if (!std.mem.eql(u8, bun.strings.withoutTrailingSlash(url.hostname), bun.strings.withoutTrailingSlash(conf_item_url.hostname))) { + continue; + } + } + // Apply config to scoped registry switch (conf_item.optname) { ._authToken => { if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.token = x; @@ -1097,37 +1222,9 @@ pub fn loadNpmrc( }, .email, .certfile, .keyfile => unreachable, } + // We have to keep going as it could match multiple scopes continue; } - - for (registry_map.scopes.keys(), registry_map.scopes.values()) |*k, *v| { - const url = url_map.get(k.*) orelse unreachable; - - if (std.mem.eql(u8, bun.strings.withoutTrailingSlash(url.host), bun.strings.withoutTrailingSlash(conf_item_url.host))) { - if (conf_item_url.hostname.len > 0) { - if (!std.mem.eql(u8, bun.strings.withoutTrailingSlash(url.hostname), bun.strings.withoutTrailingSlash(conf_item_url.hostname))) { - continue; - } - } - switch (conf_item.optname) { - ._authToken => { - if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.token = x; - }, - .username => { - if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.username = x; - }, - ._password => { - if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.password = x; - }, - ._auth => { - try @"handle _auth"(allocator, v, &conf_item, log, source); - }, - .email, .certfile, .keyfile => unreachable, - } - // We have to keep going as it could match multiple scopes - continue; - } - } } } } diff --git a/src/install/bin.zig b/src/install/bin.zig index 1667455d26..d0162e6383 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -17,6 +17,9 @@ const string = bun.string; const Install = @import("./install.zig"); const PackageInstall = Install.PackageInstall; const Dependency = @import("./dependency.zig"); +const OOM = bun.OOM; +const JSON = bun.JSON; +const Lockfile = Install.Lockfile; /// Normalized `bin` field in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bin) /// Can be a: @@ -51,6 +54,50 @@ pub const Bin = extern struct { return 0; } + pub fn eql( + l: *const Bin, + r: *const Bin, + l_buf: string, + l_extern_strings: []const ExternalString, + r_buf: string, + r_extern_strings: []const ExternalString, + ) bool { + if (l.tag != r.tag) return false; + + return switch (l.tag) { + .none => true, + .file => l.value.file.eql(r.value.file, l_buf, r_buf), + .dir => l.value.dir.eql(r.value.dir, l_buf, r_buf), + .named_file => l.value.named_file[0].eql(r.value.named_file[0], l_buf, r_buf) and + l.value.named_file[1].eql(r.value.named_file[1], l_buf, r_buf), + .map => { + const l_list = l.value.map.get(l_extern_strings); + const r_list = r.value.map.get(r_extern_strings); + if (l_list.len != r_list.len) return false; + + // assuming these maps are small without duplicate keys + var i: usize = 0; + outer: while (i < l_list.len) : (i += 2) { + var j: usize = 0; + while (j < r_list.len) : (j += 2) { + if (l_list[i].hash == r_list[j].hash) { + if (l_list[i + 1].hash != r_list[j + 1].hash) { + return false; + } + + continue :outer; + } + } + + // not found + return false; + } + + return true; + }, + }; + } + pub fn clone(this: *const Bin, buf: []const u8, prev_external_strings: []const ExternalString, all_extern_strings: []ExternalString, extern_strings_slice: []ExternalString, comptime StringBuilder: type, builder: StringBuilder) Bin { switch (this.tag) { .none => { @@ -99,6 +146,169 @@ pub const Bin = extern struct { unreachable; } + pub fn cloneAppend(this: *const Bin, this_buf: string, this_extern_strings: []const ExternalString, lockfile: *Lockfile) OOM!Bin { + var string_buf = lockfile.stringBuf(); + defer string_buf.apply(lockfile); + + const cloned: Bin = .{ + .tag = this.tag, + + .value = switch (this.tag) { + .none => Value.init(.{ .none = {} }), + .file => Value.init(.{ + .file = try string_buf.append(this.value.file.slice(this_buf)), + }), + .named_file => Value.init(.{ .named_file = .{ + try string_buf.append(this.value.named_file[0].slice(this_buf)), + try string_buf.append(this.value.named_file[1].slice(this_buf)), + } }), + .dir => Value.init(.{ + .dir = try string_buf.append(this.value.dir.slice(this_buf)), + }), + .map => map: { + const off = lockfile.buffers.extern_strings.items.len; + for (this.value.map.get(this_extern_strings)) |extern_string| { + try lockfile.buffers.extern_strings.append( + lockfile.allocator, + try string_buf.appendExternal(extern_string.slice(this_buf)), + ); + } + const new = lockfile.buffers.extern_strings.items[off..]; + break :map Value.init(.{ + .map = ExternalStringList.init(lockfile.buffers.extern_strings.items, new), + }); + }, + }, + }; + + return cloned; + } + + /// Used for packages read from text lockfile. + pub fn parseAppend( + allocator: std.mem.Allocator, + bin_expr: JSON.Expr, + buf: *String.Buf, + extern_strings: *std.ArrayListUnmanaged(ExternalString), + ) OOM!Bin { + switch (bin_expr.data) { + .e_object => |obj| { + switch (obj.properties.len) { + 0 => {}, + 1 => { + const bin_name = obj.properties.ptr[0].key.?.asString(allocator) orelse return .{}; + const value = obj.properties.ptr[0].value.?.asString(allocator) orelse return .{}; + + return .{ + .tag = .named_file, + .value = .{ + .named_file = .{ + try buf.append(bin_name), + try buf.append(value), + }, + }, + }; + }, + else => { + const current_len = extern_strings.items.len; + const num_props: usize = obj.properties.len * 2; + try extern_strings.ensureTotalCapacityPrecise( + allocator, + current_len + num_props, + ); + var new = extern_strings.items.ptr[current_len .. current_len + num_props]; + extern_strings.items.len += num_props; + + var i: usize = 0; + for (obj.properties.slice()) |bin_prop| { + const key = bin_prop.key.?; + const value = bin_prop.value.?; + const key_str = key.asString(allocator) orelse return .{}; + const value_str = value.asString(allocator) orelse return .{}; + new[i] = try buf.appendExternal(key_str); + i += 1; + new[i] = try buf.appendExternal(value_str); + i += 1; + } + if (comptime Environment.allow_assert) { + bun.assert(i == new.len); + } + return .{ + .tag = .map, + .value = .{ + .map = ExternalStringList.init(extern_strings.items, new), + }, + }; + }, + } + }, + .e_string => |str| { + if (str.data.len > 0) { + return .{ + .tag = .file, + .value = .{ + .file = try buf.append(str.data), + }, + }; + } + }, + else => {}, + } + return .{}; + } + + pub fn parseAppendFromDirectories(allocator: std.mem.Allocator, bin_expr: JSON.Expr, buf: *String.Buf) OOM!Bin { + if (bin_expr.asString(allocator)) |bin_str| { + return .{ + .tag = .dir, + .value = .{ + .dir = try buf.append(bin_str), + }, + }; + } + return .{}; + } + + /// Writes value of bin to a single line, either as a string or object. Cannot be `.none` because a value is expected to be + /// written to the json, as a property value or array value. + pub fn toSingleLineJson(this: *const Bin, buf: string, extern_strings: []const ExternalString, writer: anytype) @TypeOf(writer).Error!void { + bun.debugAssert(this.tag != .none); + switch (this.tag) { + .none => {}, + .file => { + try writer.print("{}", .{this.value.file.fmtJson(buf, .{})}); + }, + .named_file => { + try writer.writeByte('{'); + try writer.print(" {}: {} ", .{ + this.value.named_file[0].fmtJson(buf, .{}), + this.value.named_file[1].fmtJson(buf, .{}), + }); + try writer.writeByte('}'); + }, + .dir => { + try writer.print("{}", .{this.value.dir.fmtJson(buf, .{})}); + }, + .map => { + try writer.writeByte('{'); + const list = this.value.map.get(extern_strings); + var first = true; + var i: usize = 0; + while (i < list.len) : (i += 2) { + if (!first) { + try writer.writeByte(','); + } + first = false; + try writer.print(" {}: {}", .{ + list[i].value.fmtJson(buf, .{}), + list[i + 1].value.fmtJson(buf, .{}), + }); + } + try writer.writeAll(" }"); + }, + } + } + pub fn init() Bin { return bun.serializable(.{ .tag = .none, .value = Value.init(.{ .none = {} }) }); } diff --git a/src/install/bun.lock.zig b/src/install/bun.lock.zig new file mode 100644 index 0000000000..b34d21e009 --- /dev/null +++ b/src/install/bun.lock.zig @@ -0,0 +1,1771 @@ +const std = @import("std"); +const bun = @import("root").bun; +const string = bun.string; +const stringZ = bun.stringZ; +const strings = bun.strings; +const URL = bun.URL; +const PackageManager = bun.install.PackageManager; +const OOM = bun.OOM; +const logger = bun.logger; +const BinaryLockfile = bun.install.Lockfile; +const JSON = bun.JSON; +const Output = bun.Output; +const Expr = bun.js_parser.Expr; +const MutableString = bun.MutableString; +const DependencySlice = BinaryLockfile.DependencySlice; +const Install = bun.install; +const Dependency = Install.Dependency; +const PackageID = Install.PackageID; +const Semver = bun.Semver; +const String = Semver.String; +const Resolution = Install.Resolution; +const PackageNameHash = Install.PackageNameHash; +const NameHashMap = BinaryLockfile.NameHashMap; +const Repository = Install.Repository; +const Progress = bun.Progress; +const Environment = bun.Environment; +const Global = bun.Global; +const LoadResult = BinaryLockfile.LoadResult; +const TruncatedPackageNameHash = Install.TruncatedPackageNameHash; +const invalid_package_id = Install.invalid_package_id; +const Npm = Install.Npm; +const ExtractTarball = @import("./extract_tarball.zig"); +const Integrity = @import("./integrity.zig").Integrity; +const Meta = BinaryLockfile.Package.Meta; +const Negatable = Npm.Negatable; +const DependencyID = Install.DependencyID; +const invalid_dependency_id = Install.invalid_dependency_id; +const DependencyIDSlice = BinaryLockfile.DependencyIDSlice; +const Bin = Install.Bin; +const ExternalString = Semver.ExternalString; + +/// A property key in the `packages` field of the lockfile +pub const PkgPath = struct { + raw: string, + depth: u8, + + /// raw must be valid + /// fills buf with the path to dependency in node_modules. + /// e.g. loose-envify/js-tokens@4.0.0 -> node_modules/loose-envify/node_modules/js-tokens + pub fn path(this: PkgPath, path_buf: []u8, comptime sep: u8) stringZ { + var buf = path_buf; + var remain = this.raw; + + const end = loop: while (true) { + @memcpy(buf[0.."node_modules/".len], "node_modules" ++ [1]u8{sep}); + buf = buf["node_modules/".len..]; + + var at = strings.indexOfChar(remain, '@') orelse unreachable; + var slash = strings.indexOfChar(remain, '/') orelse break :loop at; + + if (at == 0) { + // scoped package, find next '@' and '/' + at += 1 + (strings.indexOfChar(remain[1..], '@') orelse unreachable); + slash += 1 + (strings.indexOfChar(remain[slash + 1 ..], '/') orelse { + break :loop at; + }); + } + + if (at < slash) { + // slash is in the version + break :loop at; + } + + @memcpy(buf[0..slash], remain[0..slash]); + buf[slash] = sep; + buf = buf[slash + 1 ..]; + remain = remain[slash + 1 ..]; + }; + + @memcpy(buf[0..end], remain[0..end]); + buf = buf[end..]; + buf[0] = 0; + return path_buf[0 .. @intFromPtr(buf.ptr) - @intFromPtr(path_buf.ptr) :0]; + } + + pub fn reverseIterator(input: string) Iterator { + return .{ + .input = input, + .i = @intCast(input.len), + }; + } + + pub const ReverseIterator = struct { + input: string, + i: u32, + + pub fn next(this: *ReverseIterator) error{InvalidPackageKey}!?string { + if (this.i == 0) return null; + + const remain = this.input[0..this.i]; + if (remain.len == 0) return error.InvalidPackageKey; + + const slash = strings.indexOfCharNeg(remain, '/') orelse { + // the end + const name = remain; + this.i = 0; + return name; + }; + + // if this is the second component of a scoped package an '@' + // will begin the next + const at = strings.indexOfCharNeg(remain, '@') orelse { + const name = this.input[slash + 1 .. this.i]; + this.i = slash; + return name; + }; + + if (at < slash) { + return error.InvalidPackageKey; + } + + const next_slash = strings.indexOfCharNeg(remain[0..slash]) orelse { + // if `@` exists there must be another slash unless the first package + // is a scoped package + if (at != 0) { + return error.InvalidPackageKey; + } + + const name = remain; + this.i = 0; + return name; + }; + + if (next_slash + 1 != at) { + return error.InvalidPackageKey; + } + + const name = this.input[next_slash + 1 .. this.i]; + this.i = next_slash; + return name; + } + + pub fn first(this: *ReverseIterator) error{InvalidPackageKey}!string { + bun.debugAssert(this.i == this.input.len); + + return this.next() orelse return error.InvalidPackageKey; + } + }; + + pub fn iterator(input: string) Iterator { + return .{ + .input = input, + .i = 0, + }; + } + + pub const Iterator = struct { + input: string, + i: u32, + version_offset: ?u32 = null, + + pub fn next(this: *Iterator) error{InvalidPackageKey}!?string { + if (this.i == this.input.len) return null; + + var remain = this.input[this.i..]; + + var maybe_at = strings.indexOfChar(remain, '@'); + var slash = strings.indexOfChar(remain, '/') orelse { + // no slashes left, it's the last dependency name. + // '@' will only exist if '/' exists (scoped package) + if (maybe_at != null) return error.InvalidPackageKey; + this.i = @intCast(this.input.len); + return remain; + }; + + if (maybe_at == null) { + if (slash + 1 == this.input.len) return error.InvalidPackageKey; + this.i += slash + 1; + return remain[0..slash]; + } + + if (maybe_at.? == 0) { + // scoped package, find next '/' and '@' if it exists + maybe_at = strings.indexOfChar(remain[1..], '@'); + slash += 1 + (strings.indexOfChar(remain[slash + 1 ..], '/') orelse { + if (maybe_at != null) return error.InvalidPackageKey; + this.i = @intCast(this.input.len); + return remain; + }); + } + + if (maybe_at) |at| { + if (at + 1 < slash) { + // both '@' and '/' exist and it's not a scoped package, so + // '@' must be greater than '/' + return error.InvalidPackageKey; + } + } + + this.i += slash + 1; + return remain[0..slash]; + } + + /// There will always be at least one component to this path. Return + /// an error if none is found (empty string) + pub fn first(this: *Iterator) error{InvalidPackageKey}!string { + bun.assertWithLocation(this.i == 0, @src()); + return try this.next() orelse error.InvalidPackageKey; + } + }; + + pub fn fromLockfile(input: string) PkgPath { + return .{ + .raw = input, + .depth = 0, + }; + } +}; + +pub const Version = enum(u32) { + v0 = 0, + + // probably bump when we support nested resolutions + // v1, + + pub const current: Version = .v0; +}; + +// For sorting dependencies belonging to a node_modules folder. No duplicate names, so +// only string compare +const TreeDepsSortCtx = struct { + string_buf: string, + deps_buf: []const Dependency, + + pub fn isLessThan(this: @This(), lhs: DependencyID, rhs: DependencyID) bool { + const l = this.deps_buf[lhs]; + const r = this.deps_buf[rhs]; + return strings.cmpStringsAsc({}, l.name.slice(this.string_buf), r.name.slice(this.string_buf)); + } +}; + +pub const Stringifier = struct { + const indent_scalar = 2; + + // pub fn save(this: *const Lockfile) void { + // _ = this; + // } + + pub fn saveFromBinary(allocator: std.mem.Allocator, lockfile: *const BinaryLockfile, writer: anytype) @TypeOf(writer).Error!void { + const buf = lockfile.buffers.string_bytes.items; + const deps_buf = lockfile.buffers.dependencies.items; + const resolution_buf = lockfile.buffers.resolutions.items; + const pkgs = lockfile.packages.slice(); + const pkg_dep_lists: []DependencySlice = pkgs.items(.dependencies); + const pkg_resolutions: []Resolution = pkgs.items(.resolution); + const pkg_names: []String = pkgs.items(.name); + const pkg_name_hashes: []PackageNameHash = pkgs.items(.name_hash); + const pkg_metas: []BinaryLockfile.Package.Meta = pkgs.items(.meta); + const pkg_bins = pkgs.items(.bin); + + var temp_buf: std.ArrayListUnmanaged(u8) = .{}; + defer temp_buf.deinit(allocator); + const temp_writer = temp_buf.writer(allocator); + + var found_trusted_dependencies: std.AutoHashMapUnmanaged(u64, String) = .{}; + defer found_trusted_dependencies.deinit(allocator); + if (lockfile.trusted_dependencies) |trusted_dependencies| { + try found_trusted_dependencies.ensureTotalCapacity(allocator, @truncate(trusted_dependencies.count())); + } + + var found_patched_dependencies: std.AutoHashMapUnmanaged(u64, struct { string, String }) = .{}; + defer found_patched_dependencies.deinit(allocator); + try found_patched_dependencies.ensureTotalCapacity(allocator, @truncate(lockfile.patched_dependencies.count())); + + var found_overrides: std.AutoHashMapUnmanaged(u64, struct { String, Dependency.Version }) = .{}; + defer found_overrides.deinit(allocator); + try found_overrides.ensureTotalCapacity(allocator, @truncate(lockfile.overrides.map.count())); + + var optional_peers_buf = std.ArrayList(String).init(allocator); + defer optional_peers_buf.deinit(); + + var _indent: u32 = 0; + const indent = &_indent; + try writer.writeAll("{\n"); + try incIndent(writer, indent); + { + try writer.print("\"lockfileVersion\": {d},\n", .{@intFromEnum(Version.current)}); + try writeIndent(writer, indent); + + try writer.writeAll("\"workspaces\": {\n"); + try incIndent(writer, indent); + { + try writeWorkspaceDeps( + writer, + indent, + 0, + .{}, + pkg_names, + pkg_name_hashes, + pkg_dep_lists, + buf, + deps_buf, + lockfile.workspace_versions, + &optional_peers_buf, + ); + + var workspace_sort_buf: std.ArrayListUnmanaged(PackageID) = .{}; + defer workspace_sort_buf.deinit(allocator); + + for (0..pkgs.len) |_pkg_id| { + const pkg_id: PackageID = @intCast(_pkg_id); + const res = pkg_resolutions[pkg_id]; + if (res.tag != .workspace) continue; + try workspace_sort_buf.append(allocator, pkg_id); + } + + const Sorter = struct { + string_buf: string, + res_buf: []const Resolution, + + pub fn isLessThan(this: @This(), l: PackageID, r: PackageID) bool { + const l_res = this.res_buf[l]; + const r_res = this.res_buf[r]; + return l_res.value.workspace.order(&r_res.value.workspace, this.string_buf, this.string_buf) == .lt; + } + }; + + std.sort.pdq( + PackageID, + workspace_sort_buf.items, + Sorter{ .string_buf = buf, .res_buf = pkg_resolutions }, + Sorter.isLessThan, + ); + + for (workspace_sort_buf.items) |workspace_pkg_id| { + const res = pkg_resolutions[workspace_pkg_id]; + try writer.writeAll("\n"); + try writeIndent(writer, indent); + try writeWorkspaceDeps( + writer, + indent, + @intCast(workspace_pkg_id), + res.value.workspace, + pkg_names, + pkg_name_hashes, + pkg_dep_lists, + buf, + deps_buf, + lockfile.workspace_versions, + &optional_peers_buf, + ); + } + } + try writer.writeByte('\n'); + try decIndent(writer, indent); + try writer.writeAll("},\n"); + + const TreeSortCtx = struct { + pub const Item = struct { []const DependencyID, string, usize }; + + pub fn isLessThan(_: void, l: Item, r: Item) bool { + _, const l_rel_path, const l_depth = l; + _, const r_rel_path, const r_depth = r; + return switch (std.math.order(l_depth, r_depth)) { + .lt => true, + .gt => false, + .eq => strings.order(l_rel_path, r_rel_path) == .lt, + }; + } + }; + + var tree_sort_buf: std.ArrayListUnmanaged(TreeSortCtx.Item) = .{}; + defer tree_sort_buf.deinit(allocator); + + var pkgs_iter = BinaryLockfile.Tree.Iterator(.pkg_path).init(lockfile); + + // find trusted and patched dependencies. also overrides + while (pkgs_iter.next({})) |node| { + try tree_sort_buf.append(allocator, .{ + node.dependencies, + try allocator.dupe(u8, node.relative_path), + node.depth, + }); + + for (node.dependencies) |dep_id| { + const pkg_id = resolution_buf[dep_id]; + if (pkg_id == invalid_package_id) continue; + + const pkg_name = pkg_names[pkg_id]; + const pkg_name_hash = pkg_name_hashes[pkg_id]; + const res = pkg_resolutions[pkg_id]; + const dep = deps_buf[dep_id]; + + if (lockfile.patched_dependencies.count() > 0) { + try temp_writer.print("{s}@", .{pkg_name.slice(buf)}); + switch (res.tag) { + .workspace => { + if (lockfile.workspace_versions.get(pkg_name_hash)) |workspace_version| { + try temp_writer.print("{}", .{workspace_version.fmt(buf)}); + } + }, + else => { + try temp_writer.print("{}", .{res.fmt(buf, .posix)}); + }, + } + defer temp_buf.clearRetainingCapacity(); + + const name_and_version = temp_buf.items; + const name_and_version_hash = String.Builder.stringHash(name_and_version); + + if (lockfile.patched_dependencies.get(name_and_version_hash)) |patch| { + try found_patched_dependencies.put(allocator, name_and_version_hash, .{ + try allocator.dupe(u8, name_and_version), + patch.path, + }); + } + } + + // intentionally not checking default trusted dependencies + if (lockfile.trusted_dependencies) |trusted_dependencies| { + if (trusted_dependencies.contains(@truncate(dep.name_hash))) { + try found_trusted_dependencies.put(allocator, dep.name_hash, dep.name); + } + } + + if (lockfile.overrides.map.count() > 0) { + if (lockfile.overrides.get(dep.name_hash)) |version| { + try found_overrides.put(allocator, dep.name_hash, .{ dep.name, version }); + } + } + } + } + + pkgs_iter.reset(); + + std.sort.pdq( + TreeSortCtx.Item, + tree_sort_buf.items, + {}, + TreeSortCtx.isLessThan, + ); + + if (found_trusted_dependencies.count() > 0) { + try writeIndent(writer, indent); + try writer.writeAll( + \\"trustedDependencies": [ + \\ + ); + indent.* += 1; + var values_iter = found_trusted_dependencies.valueIterator(); + while (values_iter.next()) |dep_name| { + try writeIndent(writer, indent); + try writer.print( + \\"{s}", + \\ + , .{dep_name.slice(buf)}); + } + + try decIndent(writer, indent); + try writer.writeAll( + \\], + \\ + ); + } + + if (found_patched_dependencies.count() > 0) { + try writeIndent(writer, indent); + try writer.writeAll( + \\"patchedDependencies": { + \\ + ); + indent.* += 1; + var values_iter = found_patched_dependencies.valueIterator(); + while (values_iter.next()) |value| { + const name_and_version, const patch_path = value.*; + try writeIndent(writer, indent); + try writer.print( + \\"{s}": "{s}", + \\ + , .{ name_and_version, patch_path.slice(buf) }); + } + + try decIndent(writer, indent); + try writer.writeAll( + \\}, + \\ + ); + } + + if (found_overrides.count() > 0) { + try writeIndent(writer, indent); + try writer.writeAll( + \\"overrides": { + \\ + ); + indent.* += 1; + var values_iter = found_overrides.valueIterator(); + while (values_iter.next()) |value| { + const name, const version = value.*; + try writeIndent(writer, indent); + try writer.print( + \\"{s}": "{s}", + \\ + , .{ name.slice(buf), version.literal.slice(buf) }); + } + + try decIndent(writer, indent); + try writer.writeAll( + \\}, + \\ + ); + } + + var tree_deps_sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}; + defer tree_deps_sort_buf.deinit(allocator); + + var pkg_deps_sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}; + defer pkg_deps_sort_buf.deinit(allocator); + + try writeIndent(writer, indent); + try writer.writeAll("\"packages\": {"); + var first = true; + for (tree_sort_buf.items) |item| { + const dependencies, const relative_path, const depth = item; + tree_deps_sort_buf.clearRetainingCapacity(); + try tree_deps_sort_buf.appendSlice(allocator, dependencies); + + std.sort.pdq( + DependencyID, + tree_deps_sort_buf.items, + TreeDepsSortCtx{ .string_buf = buf, .deps_buf = deps_buf }, + TreeDepsSortCtx.isLessThan, + ); + + for (tree_deps_sort_buf.items) |dep_id| { + const pkg_id = resolution_buf[dep_id]; + if (pkg_id == invalid_package_id) continue; + + const res = pkg_resolutions[pkg_id]; + switch (res.tag) { + .root, .npm, .folder, .local_tarball, .github, .git, .symlink, .workspace, .remote_tarball => {}, + .uninitialized => continue, + // should not be possible, just being safe + .single_file_module => continue, + else => continue, + } + + if (first) { + first = false; + try writer.writeByte('\n'); + try incIndent(writer, indent); + } else { + try writer.writeAll(",\n\n"); + try writeIndent(writer, indent); + } + + try writer.writeByte('"'); + // relative_path is empty string for root resolutions + try writer.print("{}", .{ + bun.fmt.formatJSONStringUTF8(relative_path, .{ .quote = false }), + }); + + if (depth != 0) { + try writer.writeByte('/'); + } + + const dep = deps_buf[dep_id]; + const dep_name = dep.name.slice(buf); + + try writer.print("{}\": ", .{ + bun.fmt.formatJSONStringUTF8(dep_name, .{ .quote = false }), + }); + + const pkg_name = pkg_names[pkg_id]; + const pkg_meta = pkg_metas[pkg_id]; + const pkg_bin = pkg_bins[pkg_id]; + const pkg_deps_list = pkg_dep_lists[pkg_id]; + + pkg_deps_sort_buf.clearRetainingCapacity(); + try pkg_deps_sort_buf.ensureUnusedCapacity(allocator, pkg_deps_list.len); + for (pkg_deps_list.begin()..pkg_deps_list.end()) |pkg_dep_id| { + pkg_deps_sort_buf.appendAssumeCapacity(@intCast(pkg_dep_id)); + } + + // there might be duplicate names due to dependency behaviors, + // but we print behaviors in different groups so it won't affect + // the result + std.sort.pdq( + DependencyID, + pkg_deps_sort_buf.items, + TreeDepsSortCtx{ .string_buf = buf, .deps_buf = deps_buf }, + TreeDepsSortCtx.isLessThan, + ); + + // INFO = { prod/dev/optional/peer dependencies, os, cpu, libc (TODO), bin, binDir } + + // first index is resolution for each type of package + // npm -> [ "name@version", registry (TODO: remove if default), INFO, integrity] + // symlink -> [ "name@link:path", INFO ] + // folder -> [ "name@file:path", INFO ] + // workspace -> [ "name@workspace:path", INFO ] + // tarball -> [ "name@tarball", INFO ] + // root -> [ "name@root:", { bin, binDir } ] + // git -> [ "name@git+repo", INFO, .bun-tag string (TODO: remove this) ] + // github -> [ "name@github:user/repo", INFO, .bun-tag string (TODO: remove this) ] + + switch (res.tag) { + .root => { + try writer.print("[\"{}@root:\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + // we don't read the root package version into the binary lockfile + }); + + try writer.writeByte('{'); + if (pkg_bin.tag != .none) { + try writer.writeAll(if (pkg_bin.tag == .dir) " \"binDir\": " else " \"bin\": "); + try pkg_bin.toSingleLineJson(buf, lockfile.buffers.extern_strings.items, writer); + try writer.writeAll(" }]"); + } else { + try writer.writeAll("}]"); + } + }, + .folder => { + try writer.print("[\"{}@file:{}\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + res.value.folder.fmtJson(buf, .{ .quote = false }), + }); + + try writePackageInfoObject(writer, dep.behavior, deps_buf, pkg_deps_sort_buf.items, &pkg_meta, &pkg_bin, buf, &optional_peers_buf, lockfile.buffers.extern_strings.items); + + try writer.writeByte(']'); + }, + .local_tarball => { + try writer.print("[\"{}@{}\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + res.value.local_tarball.fmtJson(buf, .{ .quote = false }), + }); + + try writePackageInfoObject(writer, dep.behavior, deps_buf, pkg_deps_sort_buf.items, &pkg_meta, &pkg_bin, buf, &optional_peers_buf, lockfile.buffers.extern_strings.items); + + try writer.writeByte(']'); + }, + .remote_tarball => { + try writer.print("[\"{}@{}\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + res.value.remote_tarball.fmtJson(buf, .{ .quote = false }), + }); + + try writePackageInfoObject(writer, dep.behavior, deps_buf, pkg_deps_sort_buf.items, &pkg_meta, &pkg_bin, buf, &optional_peers_buf, lockfile.buffers.extern_strings.items); + + try writer.writeByte(']'); + }, + .symlink => { + try writer.print("[\"{}@link:{}\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + res.value.symlink.fmtJson(buf, .{ .quote = false }), + }); + + try writePackageInfoObject(writer, dep.behavior, deps_buf, pkg_deps_sort_buf.items, &pkg_meta, &pkg_bin, buf, &optional_peers_buf, lockfile.buffers.extern_strings.items); + + try writer.writeByte(']'); + }, + .npm => { + try writer.print("[\"{}@{}\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + res.value.npm.version.fmt(buf), + }); + + // only write the registry if it's not the default. empty string means default registry + try writer.print("\"{s}\", ", .{ + if (strings.hasPrefixComptime(res.value.npm.url.slice(buf), strings.withoutTrailingSlash(Npm.Registry.default_url))) + "" + else + res.value.npm.url.slice(buf), + }); + + try writePackageInfoObject(writer, dep.behavior, deps_buf, pkg_deps_sort_buf.items, &pkg_meta, &pkg_bin, buf, &optional_peers_buf, lockfile.buffers.extern_strings.items); + + try writer.print(", \"{}\"]", .{ + pkg_meta.integrity, + }); + }, + .workspace => { + try writer.print("[\"{}@workspace:{}\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + res.value.workspace.fmtJson(buf, .{ .quote = false }), + }); + + try writePackageInfoObject(writer, dep.behavior, deps_buf, pkg_deps_sort_buf.items, &pkg_meta, &pkg_bin, buf, &optional_peers_buf, lockfile.buffers.extern_strings.items); + + try writer.writeByte(']'); + }, + inline .git, .github => |tag| { + const repo: Repository = @field(res.value, @tagName(tag)); + try writer.print("[\"{}@{}\", ", .{ + pkg_name.fmtJson(buf, .{ .quote = false }), + repo.fmt(if (comptime tag == .git) "git+" else "github:", buf), + }); + + try writePackageInfoObject(writer, dep.behavior, deps_buf, pkg_deps_sort_buf.items, &pkg_meta, &pkg_bin, buf, &optional_peers_buf, lockfile.buffers.extern_strings.items); + + try writer.print(", {}]", .{ + repo.resolved.fmtJson(buf, .{}), + }); + }, + else => unreachable, + } + } + } + + if (!first) { + try writer.writeAll(",\n"); + try decIndent(writer, indent); + } + try writer.writeAll("}\n"); + } + try decIndent(writer, indent); + try writer.writeAll("}\n"); + } + + /// Writes a single line object. Contains dependencies, os, cpu, libc (soon), and bin + /// { "devDependencies": { "one": "1.1.1", "two": "2.2.2" }, "os": "none" } + fn writePackageInfoObject( + writer: anytype, + dep_behavior: Dependency.Behavior, + deps_buf: []const Dependency, + pkg_dep_ids: []const DependencyID, + meta: *const Meta, + bin: *const Install.Bin, + buf: string, + optional_peers_buf: *std.ArrayList(String), + extern_strings: []const ExternalString, + ) OOM!void { + defer optional_peers_buf.clearRetainingCapacity(); + + try writer.writeByte('{'); + + var any = false; + inline for (workspace_dependency_groups) |group| { + const group_name, const group_behavior = group; + + var first = true; + for (pkg_dep_ids) |dep_id| { + const dep = deps_buf[dep_id]; + if (!dep.behavior.includes(group_behavior)) continue; + + if (dep.behavior.isOptionalPeer()) { + // only write to "peerDependencies" + if (group_behavior.isOptional()) continue; + + try optional_peers_buf.append(dep.name); + } + + if (first) { + if (any) { + try writer.writeByte(','); + } + try writer.writeAll(" \"" ++ group_name ++ "\": { "); + first = false; + any = true; + } else { + try writer.writeAll(", "); + } + + try writer.print("{}: {}", .{ + bun.fmt.formatJSONStringUTF8(dep.name.slice(buf), .{}), + bun.fmt.formatJSONStringUTF8(dep.version.literal.slice(buf), .{}), + }); + } + + if (!first) { + try writer.writeAll(" }"); + } + } + + if (optional_peers_buf.items.len > 0) { + bun.debugAssert(any); + try writer.writeAll( + \\, "optionalPeers": [ + ); + + for (optional_peers_buf.items, 0..) |optional_peer, i| { + try writer.print( + \\{s}{}{s} + , .{ + if (i != 0) " " else "", + bun.fmt.formatJSONStringUTF8(optional_peer.slice(buf), .{}), + if (i != optional_peers_buf.items.len - 1) "," else "", + }); + } + + try writer.writeByte(']'); + } + + if (dep_behavior.isBundled()) { + if (any) { + try writer.writeByte(','); + } else { + any = true; + } + + try writer.writeAll( + \\ "bundled": true + ); + } + + // TODO(dylan-conway) + // if (meta.libc != .all) { + // try writer.writeAll( + // \\"libc": [ + // ); + // try Negatable(Npm.Libc).toJson(meta.libc, writer); + // try writer.writeAll("], "); + // } + + if (meta.os != .all) { + if (any) { + try writer.writeByte(','); + } else { + any = true; + } + try writer.writeAll( + \\ "os": + ); + try Negatable(Npm.OperatingSystem).toJson(meta.os, writer); + } + + if (meta.arch != .all) { + if (any) { + try writer.writeByte(','); + } else { + any = true; + } + try writer.writeAll( + \\ "cpu": + ); + try Negatable(Npm.Architecture).toJson(meta.arch, writer); + } + + if (bin.tag != .none) { + if (any) { + try writer.writeByte(','); + } else { + any = true; + } + try writer.writeAll(if (bin.tag == .dir) " \"binDir\": " else " \"bin\": "); + try bin.toSingleLineJson(buf, extern_strings, writer); + } + + if (any) { + try writer.writeAll(" }"); + } else { + try writer.writeByte('}'); + } + } + + fn writeWorkspaceDeps( + writer: anytype, + indent: *u32, + pkg_id: PackageID, + res: String, + pkg_names: []const String, + pkg_name_hashes: []const PackageNameHash, + pkg_deps: []const DependencySlice, + buf: string, + deps_buf: []const Dependency, + workspace_versions: BinaryLockfile.VersionHashMap, + optional_peers_buf: *std.ArrayList(String), + ) OOM!void { + defer optional_peers_buf.clearRetainingCapacity(); + // any - have any properties been written + var any = false; + + // always print the workspace key even if it doesn't have dependencies because we + // need a way to detect new/deleted workspaces + if (pkg_id == 0) { + try writer.writeAll("\"\": {"); + const root_name = pkg_names[0].slice(buf); + if (root_name.len > 0) { + try writer.writeByte('\n'); + try incIndent(writer, indent); + try writer.print("\"name\": {}", .{ + bun.fmt.formatJSONStringUTF8(root_name, .{}), + }); + + // TODO(dylan-conway) should we save version? + any = true; + } + } else { + try writer.print("{}: {{", .{ + bun.fmt.formatJSONStringUTF8(res.slice(buf), .{}), + }); + try writer.writeByte('\n'); + try incIndent(writer, indent); + try writer.print("\"name\": {}", .{ + bun.fmt.formatJSONStringUTF8(pkg_names[pkg_id].slice(buf), .{}), + }); + + if (workspace_versions.get(pkg_name_hashes[pkg_id])) |version| { + try writer.writeAll(",\n"); + try writeIndent(writer, indent); + try writer.print("\"version\": \"{}\"", .{ + version.fmt(buf), + }); + } + + any = true; + } + + inline for (workspace_dependency_groups) |group| { + const group_name, const group_behavior = group; + + var first = true; + for (pkg_deps[pkg_id].get(deps_buf)) |dep| { + if (!dep.behavior.includes(group_behavior)) continue; + + if (dep.behavior.isOptionalPeer()) { + if (group_behavior.isOptional()) continue; + + try optional_peers_buf.append(dep.name); + } + + if (first) { + if (any) { + try writer.writeByte(','); + } + try writer.writeByte('\n'); + if (any) { + try writeIndent(writer, indent); + } else { + try incIndent(writer, indent); + } + try writer.writeAll("\"" ++ group_name ++ "\": {\n"); + try incIndent(writer, indent); + any = true; + first = false; + } else { + try writer.writeAll(",\n"); + try writeIndent(writer, indent); + } + + const name = dep.name.slice(buf); + const version = dep.version.literal.slice(buf); + + try writer.print("{}: {}", .{ + bun.fmt.formatJSONStringUTF8(name, .{}), + bun.fmt.formatJSONStringUTF8(version, .{}), + }); + } + + if (!first) { + try writer.writeAll(",\n"); + try decIndent(writer, indent); + try writer.writeAll("}"); + } + } + if (optional_peers_buf.items.len > 0) { + bun.debugAssert(any); + try writer.writeAll( + \\, + \\ + ); + try writeIndent(writer, indent); + try writer.writeAll( + \\"optionalPeers": [ + \\ + ); + indent.* += 1; + for (optional_peers_buf.items) |optional_peer| { + try writeIndent(writer, indent); + try writer.print( + \\{}, + \\ + , .{ + bun.fmt.formatJSONStringUTF8(optional_peer.slice(buf), .{}), + }); + } + try decIndent(writer, indent); + try writer.writeByte(']'); + } + + if (any) { + try writer.writeAll(",\n"); + try decIndent(writer, indent); + } + try writer.writeAll("},"); + } + + fn writeIndent(writer: anytype, indent: *const u32) OOM!void { + for (0..indent.*) |_| { + try writer.writeAll(" " ** indent_scalar); + } + } + + fn incIndent(writer: anytype, indent: *u32) OOM!void { + indent.* += 1; + for (0..indent.*) |_| { + try writer.writeAll(" " ** indent_scalar); + } + } + + fn decIndent(writer: anytype, indent: *u32) OOM!void { + indent.* -= 1; + for (0..indent.*) |_| { + try writer.writeAll(" " ** indent_scalar); + } + } +}; + +const workspace_dependency_groups = [4]struct { []const u8, Dependency.Behavior }{ + .{ "dependencies", Dependency.Behavior.prod }, + .{ "devDependencies", Dependency.Behavior.dev }, + .{ "optionalDependencies", Dependency.Behavior.optional }, + .{ "peerDependencies", Dependency.Behavior.peer }, +}; + +const ParseError = OOM || error{ + InvalidLockfileVersion, + UnknownLockfileVersion, + InvalidOptionalValue, + InvalidPeerValue, + InvalidDefaultRegistry, + InvalidPatchedDependencies, + InvalidPatchedDependency, + InvalidWorkspaceObject, + InvalidPackagesObject, + InvalidPackagesProp, + InvalidPackageKey, + InvalidPackageInfo, + InvalidPackageSpecifier, + InvalidSemver, + InvalidPackagesTree, + InvalidTrustedDependenciesSet, + InvalidOverridesObject, + InvalidDependencyName, + InvalidDependencyVersion, + InvalidPackageResolution, + UnexpectedResolution, +}; + +pub fn parseIntoBinaryLockfile( + lockfile: *BinaryLockfile, + allocator: std.mem.Allocator, + root: JSON.Expr, + source: *const logger.Source, + log: *logger.Log, + manager: ?*PackageManager, +) ParseError!void { + lockfile.initEmpty(allocator); + + const lockfile_version_expr = root.get("lockfileVersion") orelse { + try log.addError(source, root.loc, "Missing lockfile version"); + return error.InvalidLockfileVersion; + }; + + const lockfile_version: u32 = lockfile_version: { + err: { + switch (lockfile_version_expr.data) { + .e_number => |num| { + if (num.value < 0 or num.value > std.math.maxInt(u32)) { + break :err; + } + + break :lockfile_version @intFromFloat(std.math.divExact(f64, num.value, 1) catch break :err); + }, + else => {}, + } + } + + try log.addError(source, lockfile_version_expr.loc, "Invalid lockfile version"); + return error.InvalidLockfileVersion; + }; + + lockfile.text_lockfile_version = std.meta.intToEnum(Version, lockfile_version) catch { + try log.addError(source, lockfile_version_expr.loc, "Unknown lockfile version"); + return error.InvalidLockfileVersion; + }; + + var string_buf = lockfile.stringBuf(); + + if (root.get("trustedDependencies")) |trusted_dependencies_expr| { + var trusted_dependencies: BinaryLockfile.TrustedDependenciesSet = .{}; + if (!trusted_dependencies_expr.isArray()) { + try log.addError(source, trusted_dependencies_expr.loc, "Expected an array"); + return error.InvalidTrustedDependenciesSet; + } + + for (trusted_dependencies_expr.data.e_array.items.slice()) |dep| { + if (!dep.isString()) { + try log.addError(source, dep.loc, "Expected a string"); + return error.InvalidTrustedDependenciesSet; + } + const name_hash: TruncatedPackageNameHash = @truncate((try dep.asStringHash(allocator, String.Builder.stringHash)).?); + try trusted_dependencies.put(allocator, name_hash, {}); + } + + lockfile.trusted_dependencies = trusted_dependencies; + } + + if (root.get("patchedDependencies")) |patched_dependencies_expr| { + if (!patched_dependencies_expr.isObject()) { + try log.addError(source, patched_dependencies_expr.loc, "Expected an object"); + return error.InvalidPatchedDependencies; + } + + for (patched_dependencies_expr.data.e_object.properties.slice()) |prop| { + const key = prop.key.?; + const value = prop.value.?; + if (!key.isString()) { + try log.addError(source, key.loc, "Expected a string"); + return error.InvalidPatchedDependencies; + } + + if (!value.isString()) { + try log.addError(source, value.loc, "Expected a string"); + return error.InvalidPatchedDependencies; + } + + const key_hash = (try key.asStringHash(allocator, String.Builder.stringHash)).?; + try lockfile.patched_dependencies.put( + allocator, + key_hash, + .{ .path = try string_buf.append(value.asString(allocator).?) }, + ); + } + } + + if (root.get("overrides")) |overrides_expr| { + if (!overrides_expr.isObject()) { + try log.addError(source, overrides_expr.loc, "Expected an object"); + return error.InvalidOverridesObject; + } + + for (overrides_expr.data.e_object.properties.slice()) |prop| { + const key = prop.key.?; + const value = prop.value.?; + + if (!key.isString() or key.data.e_string.len() == 0) { + try log.addError(source, key.loc, "Expected a non-empty string"); + return error.InvalidOverridesObject; + } + + const name_str = key.asString(allocator).?; + const name_hash = String.Builder.stringHash(name_str); + const name = try string_buf.appendWithHash(name_str, name_hash); + + // TODO(dylan-conway) also accept object when supported + if (!value.isString()) { + try log.addError(source, value.loc, "Expected a string"); + return error.InvalidOverridesObject; + } + + const version_str = value.asString(allocator).?; + const version_hash = String.Builder.stringHash(version_str); + const version = try string_buf.appendWithHash(version_str, version_hash); + const version_sliced = version.sliced(string_buf.bytes.items); + + const dep: Dependency = .{ + .name = name, + .name_hash = name_hash, + .version = Dependency.parse( + allocator, + name, + name_hash, + version_sliced.slice, + &version_sliced, + log, + manager, + ) orelse { + try log.addError(source, value.loc, "Invalid override version"); + return error.InvalidOverridesObject; + }, + }; + + try lockfile.overrides.map.put(allocator, name_hash, dep); + } + } + + const workspaces = root.getObject("workspaces") orelse { + try log.addError(source, root.loc, "Missing a workspaces object property"); + return error.InvalidWorkspaceObject; + }; + + var maybe_root_pkg: ?Expr = null; + + for (workspaces.data.e_object.properties.slice()) |prop| { + const key = prop.key.?; + const value: Expr = prop.value.?; + if (!key.isString()) { + try log.addError(source, key.loc, "Expected a string"); + return error.InvalidWorkspaceObject; + } + if (!value.isObject()) { + try log.addError(source, value.loc, "Expected an object"); + return error.InvalidWorkspaceObject; + } + + const path = key.asString(allocator).?; + + if (path.len == 0) { + if (maybe_root_pkg != null) { + try log.addError(source, key.loc, "Duplicate root package"); + return error.InvalidWorkspaceObject; + } + + maybe_root_pkg = value; + continue; + } + + const name_expr: Expr = value.get("name") orelse { + try log.addError(source, value.loc, "Expected a string name property"); + return error.InvalidWorkspaceObject; + }; + + const name_hash = try name_expr.asStringHash(allocator, String.Builder.stringHash) orelse { + try log.addError(source, name_expr.loc, "Expected a string name property"); + return error.InvalidWorkspaceObject; + }; + + try lockfile.workspace_paths.put(allocator, name_hash, try string_buf.append(path)); + + // versions are optional + if (value.get("version")) |version_expr| { + if (!version_expr.isString()) { + try log.addError(source, version_expr.loc, "Expected a string version property"); + return error.InvalidWorkspaceObject; + } + + const version_str = try string_buf.append(version_expr.asString(allocator).?); + + const parsed = Semver.Version.parse(version_str.sliced(string_buf.bytes.items)); + if (!parsed.valid) { + try log.addError(source, version_expr.loc, "Invalid semver version"); + return error.InvalidSemver; + } + + try lockfile.workspace_versions.put(allocator, name_hash, parsed.version.min()); + } + } + + var optional_peers_buf: std.AutoHashMapUnmanaged(u64, void) = .{}; + defer optional_peers_buf.deinit(allocator); + + const root_pkg_exr = maybe_root_pkg orelse { + try log.addError(source, workspaces.loc, "Expected root package"); + return error.InvalidWorkspaceObject; + }; + + { + const maybe_name = if (root_pkg_exr.get("name")) |name| name.asString(allocator) orelse { + try log.addError(source, name.loc, "Expected a string"); + return error.InvalidWorkspaceObject; + } else null; + + const off, var len = try parseAppendDependencies(lockfile, allocator, &root_pkg_exr, &string_buf, log, source, &optional_peers_buf); + + var root_pkg: BinaryLockfile.Package = .{}; + root_pkg.meta.id = 0; + + if (maybe_name) |name| { + const name_hash = String.Builder.stringHash(name); + root_pkg.name = try string_buf.appendWithHash(name, name_hash); + root_pkg.name_hash = name_hash; + } + + workspaces: for (lockfile.workspace_paths.values()) |workspace_path| { + for (workspaces.data.e_object.properties.slice()) |prop| { + const key = prop.key.?; + const value = prop.value.?; + const path = key.asString(allocator).?; + if (!strings.eqlLong(path, workspace_path.slice(string_buf.bytes.items), true)) continue; + + const name = value.get("name").?.asString(allocator).?; + const name_hash = String.Builder.stringHash(name); + + const dep: Dependency = .{ + .name = try string_buf.appendWithHash(name, name_hash), + .name_hash = name_hash, + .behavior = Dependency.Behavior.workspace, + .version = .{ + .tag = .workspace, + .value = .{ + .workspace = try string_buf.append(path), + }, + }, + }; + + try lockfile.buffers.dependencies.append(allocator, dep); + len += 1; + continue :workspaces; + } + } + + root_pkg.dependencies = .{ .off = off, .len = len }; + root_pkg.resolutions = .{ .off = off, .len = len }; + + root_pkg.meta.id = 0; + try lockfile.packages.append(allocator, root_pkg); + try lockfile.getOrPutID(0, root_pkg.name_hash); + } + + const PkgMapEntry = struct { + pkg_id: PackageID, + bundled: bool, + }; + var pkg_map = bun.StringArrayHashMap(PkgMapEntry).init(allocator); + defer pkg_map.deinit(); + + if (root.get("packages")) |pkgs_expr| { + if (!pkgs_expr.isObject()) { + try log.addError(source, pkgs_expr.loc, "Expected an object"); + return error.InvalidPackagesObject; + } + + for (pkgs_expr.data.e_object.properties.slice()) |prop| { + const key = prop.key.?; + const value = prop.value.?; + + const pkg_path = key.asString(allocator) orelse { + try log.addError(source, key.loc, "Expected a string"); + return error.InvalidPackageKey; + }; + + if (!value.isArray()) { + try log.addError(source, value.loc, "Expected an array"); + return error.InvalidPackageInfo; + } + + var i: usize = 0; + const pkg_info = value.data.e_array.items; + + if (pkg_info.len == 0) { + try log.addError(source, value.loc, "Missing package info"); + return error.InvalidPackageInfo; + } + + const res_info = pkg_info.at(i); + i += 1; + + const res_info_str = res_info.asString(allocator) orelse { + try log.addError(source, res_info.loc, "Expected a string"); + return error.InvalidPackageResolution; + }; + + const name_str, const res_str = Dependency.splitNameAndVersion(res_info_str) catch { + try log.addError(source, res_info.loc, "Invalid package resolution"); + return error.InvalidPackageResolution; + }; + + const name_hash = String.Builder.stringHash(name_str); + const name = try string_buf.append(name_str); + + var res = Resolution.fromTextLockfile(res_str, &string_buf) catch |err| switch (err) { + error.OutOfMemory => return err, + error.UnexpectedResolution => { + try log.addErrorFmt(source, res_info.loc, allocator, "Unexpected resolution: {s}", .{res_str}); + return err; + }, + error.InvalidSemver => { + try log.addErrorFmt(source, res_info.loc, allocator, "Invalid package version: {s}", .{res_str}); + return err; + }, + }; + + if (res.tag == .npm) { + if (i >= pkg_info.len) { + try log.addError(source, value.loc, "Missing npm registry"); + return error.InvalidPackageInfo; + } + const registry_expr = pkg_info.at(i); + i += 1; + + const registry_str = registry_expr.asString(allocator) orelse { + try log.addError(source, registry_expr.loc, "Expected a string"); + return error.InvalidPackageInfo; + }; + + if (registry_str.len == 0) { + const url = try ExtractTarball.buildURL( + Npm.Registry.default_url, + strings.StringOrTinyString.init(name.slice(string_buf.bytes.items)), + res.value.npm.version, + string_buf.bytes.items, + ); + + res.value.npm.url = try string_buf.append(url); + } else { + res.value.npm.url = try string_buf.append(registry_str); + } + } + + var pkg: BinaryLockfile.Package = .{}; + + var bundled = false; + + // dependencies, os, cpu, libc + switch (res.tag) { + .npm, .folder, .git, .github, .local_tarball, .remote_tarball, .symlink, .workspace => { + if (i >= pkg_info.len) { + try log.addError(source, value.loc, "Missing dependencies object"); + return error.InvalidPackageInfo; + } + + const deps_os_cpu_libc_bin_bundle_obj = pkg_info.at(i); + i += 1; + if (!deps_os_cpu_libc_bin_bundle_obj.isObject()) { + try log.addError(source, deps_os_cpu_libc_bin_bundle_obj.loc, "Expected an object"); + return error.InvalidPackageInfo; + } + + if (deps_os_cpu_libc_bin_bundle_obj.get("bundled")) |bundled_expr| { + if (!bundled_expr.isBoolean()) { + try log.addError(source, bundled_expr.loc, "Expected a boolean"); + return error.InvalidPackageInfo; + } + + bundled = bundled_expr.data.e_boolean.value; + } + + const off, const len = try parseAppendDependencies(lockfile, allocator, deps_os_cpu_libc_bin_bundle_obj, &string_buf, log, source, &optional_peers_buf); + + pkg.dependencies = .{ .off = off, .len = len }; + pkg.resolutions = .{ .off = off, .len = len }; + + if (deps_os_cpu_libc_bin_bundle_obj.get("bin")) |bin| { + pkg.bin = try Bin.parseAppend(allocator, bin, &string_buf, &lockfile.buffers.extern_strings); + } else if (deps_os_cpu_libc_bin_bundle_obj.get("binDir")) |bin_dir| { + pkg.bin = try Bin.parseAppendFromDirectories(allocator, bin_dir, &string_buf); + } + + if (res.tag != .workspace) { + if (deps_os_cpu_libc_bin_bundle_obj.get("os")) |os| { + pkg.meta.os = try Negatable(Npm.OperatingSystem).fromJson(allocator, os); + } + if (deps_os_cpu_libc_bin_bundle_obj.get("cpu")) |arch| { + pkg.meta.arch = try Negatable(Npm.Architecture).fromJson(allocator, arch); + } + // TODO(dylan-conway) + // if (os_cpu_libc_obj.get("libc")) |libc| { + // pkg.meta.libc = Negatable(Npm.Libc).fromJson(allocator, libc); + // } + } + }, + .root => { + if (i >= pkg_info.len) { + try log.addError(source, value.loc, "Missing package binaries object"); + return error.InvalidPackageInfo; + } + const bin_obj = pkg_info.at(i); + i += 1; + if (!bin_obj.isObject()) { + try log.addError(source, bin_obj.loc, "Expected an object"); + return error.InvalidPackageInfo; + } + + if (bin_obj.get("bin")) |bin| { + pkg.bin = try Bin.parseAppend(allocator, bin, &string_buf, &lockfile.buffers.extern_strings); + } else if (bin_obj.get("binDir")) |bin_dir| { + pkg.bin = try Bin.parseAppendFromDirectories(allocator, bin_dir, &string_buf); + } + }, + else => {}, + } + + // integrity + switch (res.tag) { + .npm => { + if (i >= pkg_info.len) { + try log.addError(source, value.loc, "Missing integrity"); + return error.InvalidPackageInfo; + } + const integrity_expr = pkg_info.at(i); + i += 1; + const integrity_str = integrity_expr.asString(allocator) orelse { + try log.addError(source, integrity_expr.loc, "Expected a string"); + return error.InvalidPackageInfo; + }; + + pkg.meta.integrity = Integrity.parse(integrity_str); + }, + inline .git, .github => |tag| { + // .bun-tag + if (i >= pkg_info.len) { + try log.addError(source, value.loc, "Missing git dependency tag"); + return error.InvalidPackageInfo; + } + + const bun_tag = pkg_info.at(i); + i += 1; + + const bun_tag_str = bun_tag.asString(allocator) orelse { + try log.addError(source, bun_tag.loc, "Expected a string"); + return error.InvalidPackageInfo; + }; + + @field(res.value, @tagName(tag)).resolved = try string_buf.append(bun_tag_str); + }, + else => {}, + } + + pkg.name = name; + pkg.name_hash = name_hash; + pkg.resolution = res; + pkg.scripts = .{}; + + const pkg_id = try lockfile.appendPackageDedupe(&pkg, string_buf.bytes.items); + + const entry = try pkg_map.getOrPut(pkg_path); + if (entry.found_existing) { + try log.addError(source, key.loc, "Duplicate package path"); + return error.InvalidPackageKey; + } + + entry.value_ptr.* = .{ + .pkg_id = pkg_id, + .bundled = bundled, + }; + } + + try lockfile.buffers.resolutions.ensureTotalCapacityPrecise(allocator, lockfile.buffers.dependencies.items.len); + lockfile.buffers.resolutions.expandToCapacity(); + @memset(lockfile.buffers.resolutions.items, invalid_package_id); + + const pkgs = lockfile.packages.slice(); + const pkg_deps = pkgs.items(.dependencies); + var pkg_metas = pkgs.items(.meta); + var pkg_resolutions = pkgs.items(.resolution); + + { + // first the root dependencies are resolved + pkg_resolutions[0] = Resolution.init(.{ .root = {} }); + pkg_metas[0].origin = .local; + + for (pkg_deps[0].begin()..pkg_deps[0].end()) |_dep_id| { + const dep_id: DependencyID = @intCast(_dep_id); + const dep = lockfile.buffers.dependencies.items[dep_id]; + + const entry = pkg_map.get(dep.name.slice(lockfile.buffers.string_bytes.items)) orelse { + if (dep.behavior.optional) { + continue; + } + try dependencyResolutionFailure(&dep, null, allocator, lockfile.buffers.string_bytes.items, source, log, root_pkg_exr.loc); + return error.InvalidPackageInfo; + }; + + lockfile.buffers.resolutions.items[dep_id] = entry.pkg_id; + lockfile.buffers.dependencies.items[dep_id].behavior.bundled = entry.bundled; + } + + // TODO(dylan-conway) should we handle workspaces separately here for custom hoisting + + } + + var path_buf: bun.PathBuffer = undefined; + + // then each package dependency + for (pkgs_expr.data.e_object.properties.slice()) |prop| { + const key = prop.key.?; + + const pkg_path = key.asString(allocator).?; + + const pkg_id = (pkg_map.get(pkg_path) orelse { + return error.InvalidPackagesObject; + }).pkg_id; + + // find resolutions. iterate up to root through the pkg path. + deps: for (pkg_deps[pkg_id].begin()..pkg_deps[pkg_id].end()) |_dep_id| { + const dep_id: DependencyID = @intCast(_dep_id); + var dep = &lockfile.buffers.dependencies.items[dep_id]; + const dep_name = dep.name.slice(lockfile.buffers.string_bytes.items); + + @memcpy(path_buf[0..pkg_path.len], pkg_path); + path_buf[pkg_path.len] = '/'; + var offset = pkg_path.len + 1; + + var valid = true; + while (valid) { + @memcpy(path_buf[offset..][0..dep_name.len], dep_name); + const res_path = path_buf[0 .. offset + dep_name.len]; + + if (pkg_map.get(res_path)) |entry| { + lockfile.buffers.resolutions.items[dep_id] = entry.pkg_id; + dep.behavior.bundled = entry.bundled; + continue :deps; + } + + if (offset == 0) { + if (dep.behavior.optional) { + continue :deps; + } + try dependencyResolutionFailure(dep, pkg_path, allocator, lockfile.buffers.string_bytes.items, source, log, key.loc); + return error.InvalidPackageInfo; + } + + const slash = strings.lastIndexOfChar(path_buf[0 .. offset - 1], '/') orelse { + offset = 0; + continue; + }; + + // might be a scoped package + const at = strings.lastIndexOfChar(path_buf[0 .. offset - 1], '@') orelse { + offset = slash + 1; + continue; + }; + + if (at > slash) { + valid = false; + continue; + } + + const next_slash = strings.lastIndexOfChar(path_buf[0..slash], '/') orelse { + if (at != 0) { + try log.addError(source, key.loc, "Invalid package path"); + return error.InvalidPackageKey; + } + offset = 0; + continue; + }; + + if (next_slash > at) { + // there's a scoped package but it exists farther up + offset = slash + 1; + continue; + } + + if (next_slash + 1 != at) { + valid = false; + continue; + } + + offset = at; + } + + try log.addError(source, key.loc, "Invalid package path"); + return error.InvalidPackageKey; + } + } + + lockfile.resolve(log) catch |err| { + switch (err) { + error.OutOfMemory => |oom| return oom, + else => { + return error.InvalidPackagesObject; + }, + } + }; + + return; + } + + lockfile.initEmpty(allocator); +} + +fn dependencyResolutionFailure(dep: *const Dependency, pkg_path: ?string, allocator: std.mem.Allocator, buf: string, source: *const logger.Source, log: *logger.Log, loc: logger.Loc) OOM!void { + const behavior_str = if (dep.behavior.dev) + "dev" + else if (dep.behavior.optional) + "optional" + else if (dep.behavior.peer) + "peer" + else if (dep.behavior.isWorkspaceOnly()) + "workspace" + else + "prod"; + + if (pkg_path) |path| { + try log.addErrorFmt(source, loc, allocator, "Failed to resolve {s} dependency '{s}' for package '{s}'", .{ + behavior_str, + dep.name.slice(buf), + path, + }); + } else { + try log.addErrorFmt(source, loc, allocator, "Failed to resolve root {s} dependency '{s}'", .{ + behavior_str, + dep.name.slice(buf), + }); + } +} + +fn parseAppendDependencies( + lockfile: *BinaryLockfile, + allocator: std.mem.Allocator, + obj: *const Expr, + buf: *String.Buf, + log: *logger.Log, + source: *const logger.Source, + optional_peers_buf: *std.AutoHashMapUnmanaged(u64, void), +) ParseError!struct { u32, u32 } { + defer optional_peers_buf.clearRetainingCapacity(); + + if (obj.get("optionalPeers")) |optional_peers| { + if (!optional_peers.isArray()) { + try log.addError(source, optional_peers.loc, "Expected an array"); + return error.InvalidPackageInfo; + } + + for (optional_peers.data.e_array.items.slice()) |item| { + const name_hash = try item.asStringHash(allocator, String.Builder.stringHash) orelse { + try log.addError(source, item.loc, "Expected a string"); + return error.InvalidPackageInfo; + }; + + try optional_peers_buf.put(allocator, name_hash, {}); + } + } + + const off = lockfile.buffers.dependencies.items.len; + inline for (workspace_dependency_groups) |dependency_group| { + const group_name, const group_behavior = dependency_group; + if (obj.get(group_name)) |deps| { + if (!deps.isObject()) { + try log.addError(source, deps.loc, "Expected an object"); + return error.InvalidPackagesTree; + } + + for (deps.data.e_object.properties.slice()) |prop| { + const key = prop.key.?; + const value = prop.value.?; + + const name_str = key.asString(allocator) orelse { + try log.addError(source, key.loc, "Expected a string"); + return error.InvalidDependencyName; + }; + + const name_hash = String.Builder.stringHash(name_str); + const name = try buf.appendExternalWithHash(name_str, name_hash); + + const version_str = value.asString(allocator) orelse { + try log.addError(source, value.loc, "Expected a string"); + return error.InvalidDependencyVersion; + }; + + const version = try buf.append(version_str); + const version_sliced = version.sliced(buf.bytes.items); + + const dep: Dependency = .{ + .name = name.value, + .name_hash = name.hash, + .behavior = if (group_behavior.peer and optional_peers_buf.contains(name.hash)) + group_behavior.add(.optional) + else + group_behavior, + .version = Dependency.parse( + allocator, + name.value, + name.hash, + version_sliced.slice, + &version_sliced, + log, + null, + ) orelse { + try log.addError(source, value.loc, "Invalid dependency version"); + return error.InvalidDependencyVersion; + }, + }; + + try lockfile.buffers.dependencies.append(allocator, dep); + } + } + } + const end = lockfile.buffers.dependencies.items.len; + + std.sort.pdq( + Dependency, + lockfile.buffers.dependencies.items[off..], + buf.bytes.items, + Dependency.isLessThan, + ); + + return .{ @intCast(off), @intCast(end - off) }; +} diff --git a/src/install/default-trusted-dependencies.txt b/src/install/default-trusted-dependencies.txt index 270a9eccf5..85b0876636 100644 --- a/src/install/default-trusted-dependencies.txt +++ b/src/install/default-trusted-dependencies.txt @@ -91,7 +91,6 @@ @splunk/otel @strapi/strapi @sveltejs/kit -@swc/core @syncfusion/ej2-angular-base @taquito/taquito @temporalio/core-bridge diff --git a/src/install/dependency.zig b/src/install/dependency.zig index 6f3667988e..4ded9d5682 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -51,7 +51,7 @@ version: Dependency.Version = .{}, /// - `peerDependencies` /// Technically, having the same package name specified under multiple fields is invalid /// But we don't want to allocate extra arrays for them. So we use a bitfield instead. -behavior: Behavior = Behavior.uninitialized, +behavior: Behavior = .{}, /// Sorting order for dependencies is: /// 1. [ `peerDependencies`, `optionalDependencies`, `devDependencies`, `dependencies` ] @@ -265,7 +265,7 @@ pub inline fn isRemoteTarball(dependency: string) bool { } /// Turns `foo@1.1.1` into `foo`, `1.1.1`, or `@foo/bar@1.1.1` into `@foo/bar`, `1.1.1`, or `foo` into `foo`, `null`. -pub fn splitNameAndVersion(str: string) struct { string, ?string } { +pub fn splitNameAndMaybeVersion(str: string) struct { string, ?string } { if (strings.indexOfChar(str, '@')) |at_index| { if (at_index != 0) { return .{ str[0..at_index], if (at_index + 1 < str.len) str[at_index + 1 ..] else null }; @@ -279,6 +279,14 @@ pub fn splitNameAndVersion(str: string) struct { string, ?string } { return .{ str, null }; } +pub fn splitNameAndVersion(str: string) error{MissingVersion}!struct { string, string } { + const name, const version = splitNameAndMaybeVersion(str); + return .{ + name, + version orelse return error.MissingVersion, + }; +} + pub fn unscopedPackageName(name: []const u8) []const u8 { if (name[0] != '@') return name; var name_ = name; @@ -1293,36 +1301,32 @@ pub fn fromJS(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS } pub const Behavior = packed struct(u8) { - pub const uninitialized: Behavior = .{}; - - // these padding fields are to have compatibility - // with older versions of lockfile v2 _unused_1: u1 = 0, - - normal: bool = false, + prod: bool = false, optional: bool = false, dev: bool = false, peer: bool = false, workspace: bool = false, + /// Is not set for transitive bundled dependencies + bundled: bool = false, + _unused_2: u1 = 0, - _unused_2: u2 = 0, - - pub const normal = Behavior{ .normal = true }; + pub const prod = Behavior{ .prod = true }; pub const optional = Behavior{ .optional = true }; pub const dev = Behavior{ .dev = true }; pub const peer = Behavior{ .peer = true }; pub const workspace = Behavior{ .workspace = true }; - pub inline fn isNormal(this: Behavior) bool { - return this.normal; + pub inline fn isProd(this: Behavior) bool { + return this.prod; } pub inline fn isOptional(this: Behavior) bool { - return this.optional and !this.isPeer(); + return this.optional and !this.peer; } pub inline fn isOptionalPeer(this: Behavior) bool { - return this.optional and this.isPeer(); + return this.optional and this.peer; } pub inline fn isDev(this: Behavior) bool { @@ -1337,51 +1341,41 @@ pub const Behavior = packed struct(u8) { return this.workspace; } + pub inline fn isBundled(this: Behavior) bool { + return this.bundled; + } + pub inline fn isWorkspaceOnly(this: Behavior) bool { - return this.workspace and !this.dev and !this.normal and !this.optional and !this.peer; - } - - pub inline fn setNormal(this: Behavior, value: bool) Behavior { - var b = this; - b.normal = value; - return b; - } - - pub inline fn setOptional(this: Behavior, value: bool) Behavior { - var b = this; - b.optional = value; - return b; - } - - pub inline fn setDev(this: Behavior, value: bool) Behavior { - var b = this; - b.dev = value; - return b; - } - - pub inline fn setPeer(this: Behavior, value: bool) Behavior { - var b = this; - b.peer = value; - return b; - } - - pub inline fn setWorkspace(this: Behavior, value: bool) Behavior { - var b = this; - b.workspace = value; - return b; + return this.workspace and !this.dev and !this.prod and !this.optional and !this.peer; } pub inline fn eq(lhs: Behavior, rhs: Behavior) bool { return @as(u8, @bitCast(lhs)) == @as(u8, @bitCast(rhs)); } + pub inline fn includes(lhs: Behavior, rhs: Behavior) bool { + return @as(u8, @bitCast(lhs)) & @as(u8, @bitCast(rhs)) != 0; + } + + pub inline fn add(this: Behavior, kind: @Type(.EnumLiteral)) Behavior { + var new = this; + @field(new, @tagName(kind)) = true; + return new; + } + + pub inline fn set(this: Behavior, kind: @Type(.EnumLiteral), value: bool) Behavior { + var new = this; + @field(new, @tagName(kind)) = value; + return new; + } + pub inline fn cmp(lhs: Behavior, rhs: Behavior) std.math.Order { if (eq(lhs, rhs)) { return .eq; } - if (lhs.isNormal() != rhs.isNormal()) { - return if (lhs.isNormal()) + if (lhs.isProd() != rhs.isProd()) { + return if (lhs.isProd()) .gt else .lt; @@ -1423,40 +1417,15 @@ pub const Behavior = packed struct(u8) { } pub fn isEnabled(this: Behavior, features: Features) bool { - return this.isNormal() or + return this.isProd() or (features.optional_dependencies and this.isOptional()) or (features.dev_dependencies and this.isDev()) or (features.peer_dependencies and this.isPeer()) or (features.workspaces and this.isWorkspaceOnly()); } - pub fn format(self: Behavior, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - const fields = .{ - "normal", - "optional", - "dev", - "peer", - "workspace", - }; - - var first = true; - inline for (fields) |field| { - if (@field(self, field)) { - if (!first) { - try writer.writeAll(" | "); - } - try writer.writeAll(field); - first = false; - } - } - - if (first) { - try writer.writeAll("-"); - } - } - comptime { - bun.assert(@as(u8, @bitCast(Behavior.normal)) == (1 << 1)); + bun.assert(@as(u8, @bitCast(Behavior.prod)) == (1 << 1)); bun.assert(@as(u8, @bitCast(Behavior.optional)) == (1 << 2)); bun.assert(@as(u8, @bitCast(Behavior.dev)) == (1 << 3)); bun.assert(@as(u8, @bitCast(Behavior.peer)) == (1 << 4)); diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 8ca72a1fc8..e8b8fb0c48 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -18,6 +18,7 @@ const strings = @import("../string_immutable.zig"); const Path = @import("../resolver/resolve_path.zig"); const Environment = bun.Environment; const w = std.os.windows; +const OOM = bun.OOM; const ExtractTarball = @This(); @@ -52,51 +53,20 @@ pub fn buildURL( full_name_: strings.StringOrTinyString, version: Semver.Version, string_buf: []const u8, -) !string { - return try buildURLWithPrinter( +) OOM!string { + return buildURLWithPrinter( registry_, full_name_, version, string_buf, @TypeOf(FileSystem.instance.dirname_store), string, - anyerror, + OOM, FileSystem.instance.dirname_store, FileSystem.DirnameStore.print, ); } -pub fn buildURLWithWriter( - comptime Writer: type, - writer: Writer, - registry_: string, - full_name_: strings.StringOrTinyString, - version: Semver.Version, - string_buf: []const u8, -) !void { - const Printer = struct { - writer: Writer, - - pub fn print(this: @This(), comptime fmt: string, args: anytype) Writer.Error!void { - return try std.fmt.format(this.writer, fmt, args); - } - }; - - return try buildURLWithPrinter( - registry_, - full_name_, - version, - string_buf, - Printer, - void, - Writer.Error, - Printer{ - .writer = writer, - }, - Printer.print, - ); -} - pub fn buildURLWithPrinter( registry_: string, full_name_: strings.StringOrTinyString, diff --git a/src/install/install.zig b/src/install/install.zig index 86c34c1490..e62b5e6fc0 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -24,6 +24,7 @@ const DirInfo = @import("../resolver/dir_info.zig"); const File = bun.sys.File; const JSLexer = bun.js_lexer; const logger = bun.logger; +const OOM = bun.OOM; const js_parser = bun.js_parser; const JSON = bun.JSON; @@ -36,14 +37,14 @@ const Path = bun.path; const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; const Command = @import("../cli.zig").Command; const BunArguments = @import("../cli.zig").Arguments; -const bundler = bun.bundler; +const transpiler = bun.transpiler; const DotEnv = @import("../env_loader.zig"); const which = @import("../which.zig").which; const Run = @import("../bun_js.zig").Run; const Fs = @import("../fs.zig"); const FileSystem = Fs.FileSystem; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const URL = @import("../url.zig").URL; const HTTP = bun.http; const AsyncHTTP = HTTP.AsyncHTTP; @@ -56,7 +57,7 @@ const clap = bun.clap; const ExtractTarball = @import("./extract_tarball.zig"); pub const Npm = @import("./npm.zig"); const Bitset = bun.bit_set.DynamicBitSetUnmanaged; -const z_allocator = @import("../memory_allocator.zig").z_allocator; +const z_allocator = @import("../allocators/memory_allocator.zig").z_allocator; const Syscall = bun.sys; const RunCommand = @import("../cli/run_command.zig").RunCommand; const PackageManagerCommand = @import("../cli/package_manager_command.zig").PackageManagerCommand; @@ -147,19 +148,14 @@ const ExternalString = Semver.ExternalString; const String = Semver.String; const GlobalStringBuilder = @import("../string_builder.zig"); const SlicedString = Semver.SlicedString; -const Repository = @import("./repository.zig").Repository; +pub const Repository = @import("./repository.zig").Repository; pub const Bin = @import("./bin.zig").Bin; pub const Dependency = @import("./dependency.zig"); const Behavior = @import("./dependency.zig").Behavior; const FolderResolution = @import("./resolvers/folder_resolver.zig").FolderResolution; pub fn ExternalSlice(comptime Type: type) type { - return ExternalSliceAligned(Type, null); -} - -pub fn ExternalSliceAligned(comptime Type: type, comptime alignment_: ?u29) type { return extern struct { - pub const alignment = alignment_ orelse @alignOf(*Type); pub const Slice = @This(); pub const Child: type = Type; @@ -167,6 +163,12 @@ pub fn ExternalSliceAligned(comptime Type: type, comptime alignment_: ?u29) type off: u32 = 0, len: u32 = 0, + pub const invalid: @This() = .{ .off = std.math.maxInt(u32), .len = std.math.maxInt(u32) }; + + pub inline fn isInvalid(this: Slice) bool { + return this.off == std.math.maxInt(u32) and this.len == std.math.maxInt(u32); + } + pub inline fn contains(this: Slice, id: u32) bool { return id >= this.off and id < (this.len + this.off); } @@ -210,9 +212,20 @@ pub fn ExternalSliceAligned(comptime Type: type, comptime alignment_: ?u29) type pub const PackageID = u32; pub const DependencyID = u32; + +// pub const DependencyID = enum(u32) { +// root = max - 1, +// invalid = max, +// _, + +// const max = std.math.maxInt(u32); +// }; + pub const invalid_package_id = std.math.maxInt(PackageID); +pub const invalid_dependency_id = std.math.maxInt(DependencyID); pub const ExternalStringList = ExternalSlice(ExternalString); +pub const ExternalPackageNameHashList = ExternalSlice(PackageNameHash); pub const VersionSlice = ExternalSlice(Semver.Version); pub const ExternalStringMap = extern struct { @@ -475,13 +488,17 @@ const NetworkTask = struct { this.http.schedule(this.allocator, batch); } + pub const ForTarballError = OOM || error{ + InvalidURL, + }; + pub fn forTarball( this: *NetworkTask, allocator: std.mem.Allocator, tarball_: *const ExtractTarball, scope: *const Npm.Registry.Scope, authorization: NetworkTask.Authorization, - ) !void { + ) ForTarballError!void { this.callback = .{ .extract = tarball_.* }; const tarball = &this.callback.extract; const tarball_url = tarball.url.slice(); @@ -502,11 +519,11 @@ const NetworkTask = struct { .args = .{ bun.fmt.QuotedFormatter{ .text = this.url_buf }, bun.fmt.QuotedFormatter{ .text = tarball.name.slice() } }, }; - this.package_manager.log.addErrorFmt(null, .{}, allocator, msg.fmt, msg.args) catch unreachable; + try this.package_manager.log.addErrorFmt(null, .{}, allocator, msg.fmt, msg.args); return error.InvalidURL; } - this.response_buffer = try MutableString.init(allocator, 0); + this.response_buffer = MutableString.initEmpty(allocator); this.allocator = allocator; var header_builder = HeaderBuilder{}; @@ -944,6 +961,8 @@ pub const Task = struct { name: strings.StringOrTinyString, url: strings.StringOrTinyString, env: DotEnv.Map, + dep_id: DependencyID, + res: Resolution, }, git_checkout: struct { repo_dir: bun.FileDescriptor, @@ -988,12 +1007,12 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { progress: ProgressT, - package_name: string, + package_name: String, package_version: string, patch: Patch = .{}, file_count: u32 = 0, node_modules: *const PackageManager.NodeModulesFolder, - lockfile: *const Lockfile, + lockfile: *Lockfile, const ThisPackageInstall = @This(); @@ -1020,10 +1039,6 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { skipped: u32 = 0, successfully_installed: ?Bitset = null, - /// The lockfile used by `installPackages`. Might be different from the lockfile - /// on disk if `--production` is used and dev dependencies are removed. - lockfile_used_for_install: *Lockfile, - /// Package name hash -> number of scripts skipped. /// Multiple versions of the same package might add to the count, and each version /// might have a different number of scripts @@ -1119,6 +1134,7 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { .result => |fd| _ = bun.sys.close(fd), } } + return true; } @@ -1127,7 +1143,6 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { fn verifyGitResolution( this: *@This(), repo: *const Repository, - buf: []const u8, root_node_modules_dir: std.fs.Dir, ) bool { bun.copy(u8, this.destination_dir_subpath_buf[this.destination_dir_subpath.len..], std.fs.path.sep_str ++ ".bun-tag"); @@ -1137,31 +1152,25 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { var git_tag_stack_fallback = std.heap.stackFallback(2048, bun.default_allocator); const allocator = git_tag_stack_fallback.get(); - var destination_dir = this.node_modules.openDir(root_node_modules_dir) catch return false; - defer { - if (std.fs.cwd().fd != destination_dir.fd) destination_dir.close(); - } - - const bun_tag_file = File.readFrom( - destination_dir, + var bun_tag_file = this.node_modules.readSmallFile( + root_node_modules_dir, bun_tag_path, allocator, - ).unwrap() catch return false; - defer allocator.free(bun_tag_file); + ) catch return false; + defer bun_tag_file.bytes.deinit(); - return strings.eqlLong(repo.resolved.slice(buf), bun_tag_file, true); + return strings.eqlLong(repo.resolved.slice(this.lockfile.buffers.string_bytes.items), bun_tag_file.bytes.items, true); } pub fn verify( this: *@This(), resolution: *const Resolution, - buf: []const u8, root_node_modules_dir: std.fs.Dir, ) bool { const verified = switch (resolution.tag) { - .git => this.verifyGitResolution(&resolution.value.git, buf, root_node_modules_dir), - .github => this.verifyGitResolution(&resolution.value.github, buf, root_node_modules_dir), + .git => this.verifyGitResolution(&resolution.value.git, root_node_modules_dir), + .github => this.verifyGitResolution(&resolution.value.github, root_node_modules_dir), .root => this.verifyTransitiveSymlinkedFolder(root_node_modules_dir), .folder => if (this.lockfile.isWorkspaceTreeId(this.node_modules.tree_id)) this.verifyPackageJSONNameAndVersion(root_node_modules_dir, resolution.tag) @@ -1178,18 +1187,62 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { // Only check for destination directory in node_modules. We can't use package.json because // it might not exist fn verifyTransitiveSymlinkedFolder(this: *@This(), root_node_modules_dir: std.fs.Dir) bool { - var destination_dir = this.node_modules.openDir(root_node_modules_dir) catch return false; - defer destination_dir.close(); + return this.node_modules.directoryExistsAt(root_node_modules_dir, this.destination_dir_subpath); + } - return bun.sys.directoryExistsAt(destination_dir.fd, this.destination_dir_subpath).unwrap() catch false; + fn getInstalledPackageJsonSource( + this: *PackageInstall, + root_node_modules_dir: std.fs.Dir, + mutable: *MutableString, + resolution_tag: Resolution.Tag, + ) ?logger.Source { + var total: usize = 0; + var read: usize = 0; + mutable.reset(); + mutable.list.expandToCapacity(); + bun.copy(u8, this.destination_dir_subpath_buf[this.destination_dir_subpath.len..], std.fs.path.sep_str ++ "package.json"); + this.destination_dir_subpath_buf[this.destination_dir_subpath.len + std.fs.path.sep_str.len + "package.json".len] = 0; + const package_json_path: [:0]u8 = this.destination_dir_subpath_buf[0 .. this.destination_dir_subpath.len + std.fs.path.sep_str.len + "package.json".len :0]; + defer this.destination_dir_subpath_buf[this.destination_dir_subpath.len] = 0; + + var package_json_file = this.node_modules.openFile(root_node_modules_dir, package_json_path) catch return null; + defer package_json_file.close(); + + // Heuristic: most package.jsons will be less than 2048 bytes. + read = package_json_file.read(mutable.list.items[total..]).unwrap() catch return null; + var remain = mutable.list.items[@min(total, read)..]; + if (read > 0 and remain.len < 1024) { + mutable.growBy(4096) catch return null; + mutable.list.expandToCapacity(); + } + + while (read > 0) : (read = package_json_file.read(remain).unwrap() catch return null) { + total += read; + + mutable.list.expandToCapacity(); + remain = mutable.list.items[total..]; + + if (remain.len < 1024) { + mutable.growBy(4096) catch return null; + } + mutable.list.expandToCapacity(); + remain = mutable.list.items[total..]; + } + + // If it's not long enough to have {"name": "foo", "version": "1.2.0"}, there's no way it's valid + const minimum = if (resolution_tag == .workspace and this.package_version.len == 0) + // workspaces aren't required to have a version + "{\"name\":\"\"}".len + this.package_name.len() + else + "{\"name\":\"\",\"version\":\"\"}".len + this.package_name.len() + this.package_version.len; + + if (total < minimum) return null; + + return logger.Source.initPathString(bun.span(package_json_path), mutable.list.items[0..total]); } fn verifyPackageJSONNameAndVersion(this: *PackageInstall, root_node_modules_dir: std.fs.Dir, resolution_tag: Resolution.Tag) bool { - const allocator = this.allocator; - var total: usize = 0; - var read: usize = 0; - - var body_pool = Npm.Registry.BodyPool.get(allocator); + var body_pool = Npm.Registry.BodyPool.get(this.allocator); var mutable: MutableString = body_pool.data; defer { body_pool.data = mutable; @@ -1201,61 +1254,18 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { // Don't keep it open while we're parsing the JSON. // The longer the file stays open, the more likely it causes issues for // other processes on Windows. - const source = brk: { - mutable.reset(); - mutable.list.expandToCapacity(); - bun.copy(u8, this.destination_dir_subpath_buf[this.destination_dir_subpath.len..], std.fs.path.sep_str ++ "package.json"); - this.destination_dir_subpath_buf[this.destination_dir_subpath.len + std.fs.path.sep_str.len + "package.json".len] = 0; - const package_json_path: [:0]u8 = this.destination_dir_subpath_buf[0 .. this.destination_dir_subpath.len + std.fs.path.sep_str.len + "package.json".len :0]; - defer this.destination_dir_subpath_buf[this.destination_dir_subpath.len] = 0; + const source = this.getInstalledPackageJsonSource(root_node_modules_dir, &mutable, resolution_tag) orelse return false; - var destination_dir = this.node_modules.openDir(root_node_modules_dir) catch return false; - defer { - if (std.fs.cwd().fd != destination_dir.fd) destination_dir.close(); - } - - var package_json_file = File.openat(destination_dir, package_json_path, bun.O.RDONLY, 0).unwrap() catch return false; - defer package_json_file.close(); - - // Heuristic: most package.jsons will be less than 2048 bytes. - read = package_json_file.read(mutable.list.items[total..]).unwrap() catch return false; - var remain = mutable.list.items[@min(total, read)..]; - if (read > 0 and remain.len < 1024) { - mutable.growBy(4096) catch return false; - mutable.list.expandToCapacity(); - } - - while (read > 0) : (read = package_json_file.read(remain).unwrap() catch return false) { - total += read; - - mutable.list.expandToCapacity(); - remain = mutable.list.items[total..]; - - if (remain.len < 1024) { - mutable.growBy(4096) catch return false; - } - mutable.list.expandToCapacity(); - remain = mutable.list.items[total..]; - } - - // If it's not long enough to have {"name": "foo", "version": "1.2.0"}, there's no way it's valid - const minimum = if (resolution_tag == .workspace and this.package_version.len == 0) - // workspaces aren't required to have a version - "{\"name\":\"\"}".len + this.package_name.len - else - "{\"name\":\"\",\"version\":\"\"}".len + this.package_name.len + this.package_version.len; - - if (total < minimum) return false; - - break :brk logger.Source.initPathString(bun.span(package_json_path), mutable.list.items[0..total]); - }; - - var log = logger.Log.init(allocator); + var log = logger.Log.init(this.allocator); defer log.deinit(); initializeStore(); - var package_json_checker = JSON.PackageJSONVersionChecker.init(allocator, &source, &log) catch return false; + var package_json_checker = JSON.PackageJSONVersionChecker.init( + this.allocator, + &source, + &log, + ) catch return false; _ = package_json_checker.parseExpr() catch return false; if (log.errors > 0 or !package_json_checker.has_found_name) return false; // workspaces aren't required to have a version @@ -1299,7 +1309,7 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { } // lastly, check the name. - return strings.eql(package_json_checker.found_name, this.package_name); + return strings.eql(package_json_checker.found_name, this.package_name.slice(this.lockfile.buffers.string_bytes.items)); } pub const Result = union(Tag) { @@ -1484,10 +1494,10 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { cached_package_dir: std.fs.Dir = undefined, walker: Walker = undefined, subdir: std.fs.Dir = if (Environment.isWindows) std.fs.Dir{ .fd = std.os.windows.INVALID_HANDLE_VALUE } else undefined, - buf: bun.windows.WPathBuffer = if (Environment.isWindows) undefined else {}, - buf2: bun.windows.WPathBuffer = if (Environment.isWindows) undefined else {}, - to_copy_buf: if (Environment.isWindows) []u16 else void = if (Environment.isWindows) undefined else {}, - to_copy_buf2: if (Environment.isWindows) []u16 else void = if (Environment.isWindows) undefined else {}, + buf: bun.windows.WPathBuffer = if (Environment.isWindows) undefined, + buf2: bun.windows.WPathBuffer = if (Environment.isWindows) undefined, + to_copy_buf: if (Environment.isWindows) []u16 else void = if (Environment.isWindows) undefined, + to_copy_buf2: if (Environment.isWindows) []u16 else void = if (Environment.isWindows) undefined, pub fn deinit(this: *@This()) void { if (!Environment.isWindows) { @@ -1878,7 +1888,7 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { head2: if (Environment.isWindows) []u16 else void, ) !u32 { var real_file_count: u32 = 0; - var queue = if (Environment.isWindows) HardLinkWindowsInstallTask.getQueue() else {}; + var queue = if (Environment.isWindows) HardLinkWindowsInstallTask.getQueue(); while (try walker.next()) |entry| { if (comptime Environment.isPosix) { @@ -2758,6 +2768,105 @@ pub const PackageManager = struct { patched_dependencies_to_remove: std.ArrayHashMapUnmanaged(PackageNameAndVersionHash, void, ArrayIdentityContext.U64, false) = .{}, + active_lifecycle_scripts: LifecycleScriptSubprocess.List, + last_reported_slow_lifecycle_script_at: u64 = 0, + cached_tick_for_slow_lifecycle_script_logging: u64 = 0, + + pub const WorkspaceFilter = union(enum) { + all, + name: []const u32, + path: []const u32, + + pub fn init(allocator: std.mem.Allocator, input: string, cwd: string, path_buf: []u8) OOM!WorkspaceFilter { + if ((input.len == 1 and input[0] == '*') or strings.eqlComptime(input, "**")) { + return .all; + } + + var remain = input; + + var prepend_negate = false; + while (remain.len > 0 and remain[0] == '!') { + prepend_negate = !prepend_negate; + remain = remain[1..]; + } + + const is_path = remain.len > 0 and remain[0] == '.'; + + const filter = if (is_path) + strings.withoutTrailingSlash(bun.path.joinAbsStringBuf(cwd, path_buf, &.{remain}, .posix)) + else + remain; + + if (filter.len == 0) { + // won't match anything + return .{ .path = &.{} }; + } + + // TODO(dylan-conway): finish encoding agnostic glob matcher so we don't + // need to convert + const len = bun.simdutf.length.utf32.from.utf8.le(filter) + @intFromBool(prepend_negate); + const buf = try allocator.alloc(u32, len); + + const result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(filter, buf[@intFromBool(prepend_negate)..]); + if (!result.isSuccessful()) { + // won't match anything + return .{ .path = &.{} }; + } + + if (prepend_negate) { + buf[0] = '!'; + } + + const pattern = buf[0..len]; + + return if (is_path) + .{ .path = pattern } + else + .{ .name = pattern }; + } + + pub fn deinit(this: WorkspaceFilter, allocator: std.mem.Allocator) void { + switch (this) { + .path, .name => |pattern| allocator.free(pattern), + .all => {}, + } + } + }; + + pub fn reportSlowLifecycleScripts(this: *PackageManager, log_level: Options.LogLevel) void { + if (log_level == .silent) return; + if (bun.getRuntimeFeatureFlag("BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING")) { + return; + } + + if (this.active_lifecycle_scripts.peek()) |active_lifecycle_script_running_for_the_longest_amount_of_time| { + if (this.cached_tick_for_slow_lifecycle_script_logging == this.event_loop.iterationNumber()) { + return; + } + this.cached_tick_for_slow_lifecycle_script_logging = this.event_loop.iterationNumber(); + const current_time = bun.timespec.now().ns(); + const time_running = current_time -| active_lifecycle_script_running_for_the_longest_amount_of_time.started_at; + const interval: u64 = if (log_level.isVerbose()) std.time.ns_per_s * 5 else std.time.ns_per_s * 30; + if (time_running > interval and current_time -| this.last_reported_slow_lifecycle_script_at > interval) { + this.last_reported_slow_lifecycle_script_at = current_time; + const package_name = active_lifecycle_script_running_for_the_longest_amount_of_time.package_name; + + if (!(package_name.len > 1 and package_name[package_name.len - 1] == 's')) { + Output.warn("{s}'s postinstall has costed you {}\n", .{ + package_name, + bun.fmt.fmtDurationOneDecimal(time_running), + }); + } else { + Output.warn("{s}' postinstall has costed you {}\n", .{ + package_name, + bun.fmt.fmtDurationOneDecimal(time_running), + }); + } + Output.flush(); + } + } + } + pub const PackageUpdateInfo = struct { original_version_literal: string, is_alias: bool, @@ -2842,8 +2951,13 @@ pub const PackageManager = struct { } const key = allocator.dupeZ(u8, path) catch bun.outOfMemory(); + entry.key_ptr.* = key; - const source = bun.sys.File.toSource(key, allocator).unwrap() catch |err| return .{ .read_err = err }; + const source = bun.sys.File.toSource(key, allocator).unwrap() catch |err| { + _ = this.map.remove(key); + allocator.free(key); + return .{ .read_err = err }; + }; if (comptime opts.init_reset_store) initializeStore(); @@ -2859,6 +2973,9 @@ pub const PackageManager = struct { .guess_indentation = opts.guess_indentation, }, ) catch |err| { + _ = this.map.remove(key); + allocator.free(source.contents); + allocator.free(key); bun.handleErrorReturnTrace(err, @errorReturnTrace()); return .{ .parse_err = err }; }; @@ -2869,8 +2986,6 @@ pub const PackageManager = struct { .indentation = json.indentation, }; - entry.key_ptr.* = key; - return .{ .entry = entry.value_ptr }; } @@ -2913,7 +3028,10 @@ pub const PackageManager = struct { }, ); - const json = json_result catch |err| return .{ .parse_err = err }; + const json = json_result catch |err| { + _ = this.map.remove(path); + return .{ .parse_err = err }; + }; entry.value_ptr.* = .{ .root = json.root.deepClone(allocator) catch bun.outOfMemory(), @@ -2934,7 +3052,7 @@ pub const PackageManager = struct { pub const ScriptRunEnvironment = struct { root_dir_info: *DirInfo, - bundler: bundler.Bundler, + transpiler: bun.Transpiler, }; const TimePasser = struct { @@ -2943,14 +3061,14 @@ pub const PackageManager = struct { pub const LifecycleScriptTimeLog = struct { const Entry = struct { - package_name: []const u8, + package_name: string, script_id: u8, // nanosecond duration duration: u64, }; - mutex: std.Thread.Mutex = .{}, + mutex: bun.Mutex = .{}, list: std.ArrayListUnmanaged(Entry) = .{}, pub fn appendConcurrent(log: *LifecycleScriptTimeLog, allocator: std.mem.Allocator, entry: Entry) void { @@ -3001,9 +3119,9 @@ pub const PackageManager = struct { return false; } - pub fn configureEnvForScripts(this: *PackageManager, ctx: Command.Context, log_level: Options.LogLevel) !*bundler.Bundler { + pub fn configureEnvForScripts(this: *PackageManager, ctx: Command.Context, log_level: Options.LogLevel) !*transpiler.Transpiler { if (this.env_configure) |*env_configure| { - return &env_configure.bundler; + return &env_configure.transpiler; } // We need to figure out the PATH and other environment variables @@ -3012,13 +3130,13 @@ pub const PackageManager = struct { // so we really only want to do it when strictly necessary this.env_configure = .{ .root_dir_info = undefined, - .bundler = undefined, + .transpiler = undefined, }; - const this_bundler: *bundler.Bundler = &this.env_configure.?.bundler; + const this_transpiler: *transpiler.Transpiler = &this.env_configure.?.transpiler; const root_dir_info = try RunCommand.configureEnvForRun( ctx, - this_bundler, + this_transpiler, this.env, log_level != .silent, false, @@ -3033,12 +3151,12 @@ pub const PackageManager = struct { }; } - this.env.loadCCachePath(this_bundler.fs); + this.env.loadCCachePath(this_transpiler.fs); { var node_path: bun.PathBuffer = undefined; - if (this.env.getNodePath(this_bundler.fs, &node_path)) |node_pathZ| { - _ = try this.env.loadNodeJSConfig(this_bundler.fs, bun.default_allocator.dupe(u8, node_pathZ) catch bun.outOfMemory()); + if (this.env.getNodePath(this_transpiler.fs, &node_path)) |node_pathZ| { + _ = try this.env.loadNodeJSConfig(this_transpiler.fs, bun.default_allocator.dupe(u8, node_pathZ) catch bun.outOfMemory()); } else brk: { const current_path = this.env.get("PATH") orelse ""; var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, current_path.len); @@ -3046,17 +3164,17 @@ pub const PackageManager = struct { var bun_path: string = ""; RunCommand.createFakeTemporaryNodeExecutable(&PATH, &bun_path) catch break :brk; try this.env.map.put("PATH", PATH.items); - _ = try this.env.loadNodeJSConfig(this_bundler.fs, bun.default_allocator.dupe(u8, bun_path) catch bun.outOfMemory()); + _ = try this.env.loadNodeJSConfig(this_transpiler.fs, bun.default_allocator.dupe(u8, bun_path) catch bun.outOfMemory()); } } this.env_configure.?.root_dir_info = root_dir_info; - return this_bundler; + return this_transpiler; } pub fn httpProxy(this: *PackageManager, url: URL) ?URL { - return this.env.getHttpProxy(url); + return this.env.getHttpProxyFor(url); } pub fn tlsRejectUnauthorized(this: *PackageManager) bool { @@ -3108,6 +3226,7 @@ pub const PackageManager = struct { } fn hasNoMorePendingLifecycleScripts(this: *PackageManager) bool { + this.reportSlowLifecycleScripts(this.options.log_level); return this.pending_lifecycle_script_tasks.load(.monotonic) == 0; } @@ -3121,6 +3240,7 @@ pub const PackageManager = struct { } pub fn sleep(this: *PackageManager) void { + this.reportSlowLifecycleScripts(this.options.log_level); Output.flush(); this.event_loop.tick(this, hasNoMorePendingLifecycleScripts); } @@ -3336,18 +3456,18 @@ pub const PackageManager = struct { pub fn determinePreinstallState( manager: *PackageManager, - this: Package, + pkg: Package, lockfile: *Lockfile, out_name_and_version_hash: *?u64, out_patchfile_hash: *?u64, ) PreinstallState { - switch (manager.getPreinstallState(this.meta.id)) { + switch (manager.getPreinstallState(pkg.meta.id)) { .unknown => { // Do not automatically start downloading packages which are disabled // i.e. don't download all of esbuild's versions or SWCs - if (this.isDisabled()) { - manager.setPreinstallState(this.meta.id, lockfile, .done); + if (pkg.isDisabled()) { + manager.setPreinstallState(pkg.meta.id, lockfile, .done); return .done; } @@ -3358,37 +3478,37 @@ pub const PackageManager = struct { sfb.get(), "{s}@{}", .{ - this.name.slice(manager.lockfile.buffers.string_bytes.items), - this.resolution.fmt(manager.lockfile.buffers.string_bytes.items, .posix), + pkg.name.slice(manager.lockfile.buffers.string_bytes.items), + pkg.resolution.fmt(manager.lockfile.buffers.string_bytes.items, .posix), }, ) catch unreachable; const name_and_version_hash = String.Builder.stringHash(name_and_version); const patched_dep = manager.lockfile.patched_dependencies.get(name_and_version_hash) orelse break :brk null; defer out_name_and_version_hash.* = name_and_version_hash; if (patched_dep.patchfile_hash_is_null) { - manager.setPreinstallState(this.meta.id, manager.lockfile, .calc_patch_hash); + manager.setPreinstallState(pkg.meta.id, manager.lockfile, .calc_patch_hash); return .calc_patch_hash; } out_patchfile_hash.* = patched_dep.patchfileHash().?; break :brk patched_dep.patchfileHash().?; }; - const folder_path = switch (this.resolution.tag) { - .git => manager.cachedGitFolderNamePrintAuto(&this.resolution.value.git, patch_hash), - .github => manager.cachedGitHubFolderNamePrintAuto(&this.resolution.value.github, patch_hash), - .npm => manager.cachedNPMPackageFolderName(lockfile.str(&this.name), this.resolution.value.npm.version, patch_hash), - .local_tarball => manager.cachedTarballFolderName(this.resolution.value.local_tarball, patch_hash), - .remote_tarball => manager.cachedTarballFolderName(this.resolution.value.remote_tarball, patch_hash), + const folder_path = switch (pkg.resolution.tag) { + .git => manager.cachedGitFolderNamePrintAuto(&pkg.resolution.value.git, patch_hash), + .github => manager.cachedGitHubFolderNamePrintAuto(&pkg.resolution.value.github, patch_hash), + .npm => manager.cachedNPMPackageFolderName(lockfile.str(&pkg.name), pkg.resolution.value.npm.version, patch_hash), + .local_tarball => manager.cachedTarballFolderName(pkg.resolution.value.local_tarball, patch_hash), + .remote_tarball => manager.cachedTarballFolderName(pkg.resolution.value.remote_tarball, patch_hash), else => "", }; if (folder_path.len == 0) { - manager.setPreinstallState(this.meta.id, lockfile, .extract); + manager.setPreinstallState(pkg.meta.id, lockfile, .extract); return .extract; } if (manager.isFolderInCache(folder_path)) { - manager.setPreinstallState(this.meta.id, lockfile, .done); + manager.setPreinstallState(pkg.meta.id, lockfile, .done); return .done; } @@ -3405,16 +3525,16 @@ pub const PackageManager = struct { const non_patched_path = manager.lockfile.allocator.dupeZ(u8, non_patched_path_) catch bun.outOfMemory(); defer manager.lockfile.allocator.free(non_patched_path); if (manager.isFolderInCache(non_patched_path)) { - manager.setPreinstallState(this.meta.id, manager.lockfile, .apply_patch); + manager.setPreinstallState(pkg.meta.id, manager.lockfile, .apply_patch); // yay step 1 is already done for us return .apply_patch; } // we need to extract non-patched pkg into the cache - manager.setPreinstallState(this.meta.id, lockfile, .extract); + manager.setPreinstallState(pkg.meta.id, lockfile, .extract); return .extract; } - manager.setPreinstallState(this.meta.id, lockfile, .extract); + manager.setPreinstallState(pkg.meta.id, lockfile, .extract); return .extract; }, else => |val| return val, @@ -3474,7 +3594,7 @@ pub const PackageManager = struct { noinline fn ensureCacheDirectory(this: *PackageManager) std.fs.Dir { loop: while (true) { if (this.options.enable.cache) { - const cache_dir = fetchCacheDirectoryPath(this.env); + const cache_dir = fetchCacheDirectoryPath(this.env, &this.options); this.cache_directory_path = this.allocator.dupeZ(u8, cache_dir.path) catch bun.outOfMemory(); return std.fs.cwd().makeOpenPath(cache_dir.path, .{}) catch { @@ -4237,8 +4357,6 @@ pub const PackageManager = struct { }; } else if (behavior.isPeer() and !install_peer) { return null; - } else if (behavior.isOptional() and !this.options.remote_package_features.optional_dependencies) { - return null; } // appendPackage sets the PackageID on the package @@ -4271,24 +4389,26 @@ pub const PackageManager = struct { // We don't need to download the tarball, but we should enqueue dependencies .done => .{ .package = package, .is_first_time = true }, // Do we need to download the tarball? - .extract => .{ - .package = package, - .is_first_time = true, - .task = .{ - .network_task = try this.generateNetworkTaskForTarball( - Task.Id.forNPMPackage( - this.lockfile.str(&name), - package.resolution.value.npm.version, - ), - manifest.str(&find_result.package.tarball_url), - dependency.behavior.isRequired(), - dependency_id, - package, - name_and_version_hash, - // its npm. - .allow_authorization, - ) orelse unreachable, - }, + .extract => extract: { + const task_id = Task.Id.forNPMPackage(this.lockfile.str(&name), package.resolution.value.npm.version); + bun.debugAssert(!this.network_dedupe_map.contains(task_id)); + + break :extract .{ + .package = package, + .is_first_time = true, + .task = .{ + .network_task = try this.generateNetworkTaskForTarball( + task_id, + manifest.str(&find_result.package.tarball_url), + dependency.behavior.isRequired(), + dependency_id, + package, + name_and_version_hash, + // its npm. + .allow_authorization, + ) orelse unreachable, + }, + }; }, .calc_patch_hash => .{ .package = package, @@ -4346,7 +4466,7 @@ pub const PackageManager = struct { package: Lockfile.Package, patch_name_and_version_hash: ?u64, authorization: NetworkTask.Authorization, - ) !?*NetworkTask { + ) NetworkTask.ForTarballError!?*NetworkTask { if (this.hasCreatedNetworkTask(task_id, is_required)) { return null; } @@ -4372,21 +4492,21 @@ pub const PackageManager = struct { this.allocator, &.{ .package_manager = this, - .name = try strings.StringOrTinyString.initAppendIfNeeded( + .name = strings.StringOrTinyString.initAppendIfNeeded( this.lockfile.str(&package.name), *FileSystem.FilenameStore, FileSystem.FilenameStore.instance, - ), + ) catch bun.outOfMemory(), .resolution = package.resolution, .cache_dir = this.getCacheDirectory(), .temp_dir = this.getTemporaryDirectory(), .dependency_id = dependency_id, .integrity = package.meta.integrity, - .url = try strings.StringOrTinyString.initAppendIfNeeded( + .url = strings.StringOrTinyString.initAppendIfNeeded( url, *FileSystem.FilenameStore, FileSystem.FilenameStore.instance, - ), + ) catch bun.outOfMemory(), }, scope, authorization, @@ -4491,7 +4611,7 @@ pub const PackageManager = struct { if (this.lockfile.package_index.get(name_hash)) |index| { const resolutions: []Resolution = this.lockfile.packages.items(.resolution); switch (index) { - .PackageID => |existing_id| { + .id => |existing_id| { if (existing_id < resolutions.len) { const existing_resolution = resolutions[existing_id]; if (this.resolutionSatisfiesDependency(existing_resolution, version)) { @@ -4524,7 +4644,7 @@ pub const PackageManager = struct { } } }, - .PackageIDMultiple => |list| { + .ids => |list| { for (list.items) |existing_id| { if (existing_id < resolutions.len) { const existing_resolution = resolutions[existing_id]; @@ -4832,7 +4952,9 @@ pub const PackageManager = struct { task_id: u64, name: string, repository: *const Repository, + dep_id: DependencyID, dependency: *const Dependency, + res: *const Resolution, /// if patched then we need to do apply step after network task is done patch_name_and_version_hash: ?u64, ) *ThreadPool.Task { @@ -4854,14 +4976,16 @@ pub const PackageManager = struct { FileSystem.FilenameStore.instance, ) catch unreachable, .env = Repository.shared_env.get(this.allocator, this.env), + .dep_id = dep_id, + .res = res.*, }, }, .id = task_id, .apply_patch_task = if (patch_name_and_version_hash) |h| brk: { const dep = dependency; const pkg_id = switch (this.lockfile.package_index.get(dep.name_hash) orelse @panic("Package not found")) { - .PackageID => |p| p, - .PackageIDMultiple => |ps| ps.items[0], // TODO is this correct + .id => |p| p, + .ids => |ps| ps.items[0], // TODO is this correct }; const patch_hash = this.lockfile.patched_dependencies.get(h).?.patchfileHash().?; const pt = PatchTask.newApplyPatchHash(this, pkg_id, patch_hash, h); @@ -4915,8 +5039,8 @@ pub const PackageManager = struct { .apply_patch_task = if (patch_name_and_version_hash) |h| brk: { const dep = this.lockfile.buffers.dependencies.items[dependency_id]; const pkg_id = switch (this.lockfile.package_index.get(dep.name_hash) orelse @panic("Package not found")) { - .PackageID => |p| p, - .PackageIDMultiple => |ps| ps.items[0], // TODO is this correct + .id => |p| p, + .ids => |ps| ps.items[0], // TODO is this correct }; const patch_hash = this.lockfile.patched_dependencies.get(h).?.patchfileHash().?; const pt = PatchTask.newApplyPatchHash(this, pkg_id, patch_hash, h); @@ -4971,9 +5095,9 @@ pub const PackageManager = struct { pub fn updateLockfileIfNeeded( manager: *PackageManager, - load_lockfile_result: Lockfile.LoadFromDiskResult, + load_result: Lockfile.LoadResult, ) !void { - if (load_lockfile_result == .ok and load_lockfile_result.ok.serializer_result.packages_need_update) { + if (load_result == .ok and load_result.ok.serializer_result.packages_need_update) { const slice = manager.lockfile.packages.slice(); for (slice.items(.meta)) |*meta| { // these are possibly updated later, but need to make sure non are zero @@ -5332,15 +5456,13 @@ pub const PackageManager = struct { this.allocator, this.scopeForPackageName(name_str), if (loaded_manifest) |*manifest| manifest else null, - dependency.behavior.isOptional() or !this.options.do.install_peer_dependencies, + dependency.behavior.isOptional(), ); this.enqueueNetworkTask(network_task); } } else { - if (this.options.do.install_peer_dependencies) { - try this.peer_dependencies.writeItem(id); - return; - } + try this.peer_dependencies.writeItem(id); + return; } var manifest_entry_parse = try this.task_queue.getOrPutContext(this.allocator, task_id, .{}); @@ -5412,9 +5534,7 @@ pub const PackageManager = struct { if (dependency.behavior.isPeer()) { if (!install_peer) { - if (this.options.do.install_peer_dependencies) { - try this.peer_dependencies.writeItem(id); - } + try this.peer_dependencies.writeItem(id); return; } } @@ -5437,16 +5557,14 @@ pub const PackageManager = struct { if (dependency.behavior.isPeer()) { if (!install_peer) { - if (this.options.do.install_peer_dependencies) { - try this.peer_dependencies.writeItem(id); - } + try this.peer_dependencies.writeItem(id); return; } } if (this.hasCreatedNetworkTask(clone_id, dependency.behavior.isRequired())) return; - this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone(clone_id, alias, dep, dependency, null))); + this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone(clone_id, alias, dep, id, dependency, &res, null))); } }, .github => { @@ -5489,9 +5607,7 @@ pub const PackageManager = struct { if (dependency.behavior.isPeer()) { if (!install_peer) { - if (this.options.do.install_peer_dependencies) { - try this.peer_dependencies.writeItem(id); - } + try this.peer_dependencies.writeItem(id); return; } } @@ -5678,9 +5794,7 @@ pub const PackageManager = struct { if (dependency.behavior.isPeer()) { if (!install_peer) { - if (this.options.do.install_peer_dependencies) { - try this.peer_dependencies.writeItem(id); - } + try this.peer_dependencies.writeItem(id); return; } } @@ -5962,6 +6076,10 @@ pub const PackageManager = struct { resolution.value.github.resolved = builder.append(String, this.resolved); return resolution; } + + pub fn checkBundledDependencies() bool { + return true; + } }; const TarballResolver = struct { @@ -5985,6 +6103,10 @@ pub const PackageManager = struct { } return resolution; } + + pub fn checkBundledDependencies() bool { + return true; + } }; /// Returns true if we need to drain dependencies @@ -6018,7 +6140,7 @@ pub const PackageManager = struct { manager.allocator, manager.log, package_json_source, - *GitResolver, + GitResolver, &resolver, Features.npm, ) catch |err| { @@ -6091,6 +6213,11 @@ pub const PackageManager = struct { ); var package = Lockfile.Package{}; + var resolver: TarballResolver = .{ + .url = data.url, + .resolution = resolution, + }; + package.parse( manager.lockfile, manager, @@ -6098,10 +6225,7 @@ pub const PackageManager = struct { manager.log, package_json_source, TarballResolver, - TarballResolver{ - .url = data.url, - .resolution = resolution, - }, + &resolver, Features.npm, ) catch |err| { if (comptime log_level != .silent) { @@ -6171,11 +6295,17 @@ pub const PackageManager = struct { } const CacheDir = struct { path: string, is_node_modules: bool }; - pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader) CacheDir { + pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader, options: ?*const Options) CacheDir { if (env.get("BUN_INSTALL_CACHE_DIR")) |dir| { return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{dir}), .is_node_modules = false }; } + if (options) |opts| { + if (opts.cache_directory.len > 0) { + return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{opts.cache_directory}), .is_node_modules = false }; + } + } + if (env.get("BUN_INSTALL")) |dir| { var parts = [_]string{ dir, "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; @@ -6208,6 +6338,24 @@ pub const PackageManager = struct { var timestamp_this_tick: ?u32 = null; + defer { + manager.drainDependencyList(); + + if (comptime log_level.showProgress()) { + manager.startProgressBarIfNone(); + + if (@hasField(@TypeOf(callbacks), "progress_bar") and callbacks.progress_bar == true) { + const completed_items = manager.total_tasks - manager.pendingTaskCount(); + if (completed_items != manager.downloads_node.?.unprotected_completed_items or has_updated_this_run) { + manager.downloads_node.?.setCompletedItems(completed_items); + manager.downloads_node.?.setEstimatedTotalItems(manager.total_tasks); + } + } + manager.downloads_node.?.activate(); + manager.progress.maybeRefresh(); + } + } + var patch_tasks_batch = manager.patch_task_queue.popBatch(); var patch_tasks_iter = patch_tasks_batch.iterator(); while (patch_tasks_iter.next()) |ptask| { @@ -6730,13 +6878,20 @@ pub const PackageManager = struct { manager.extracted_count += 1; bun.Analytics.Features.extracted_packages += 1; - // GitHub and tarball URL dependencies are not fully resolved until after the tarball is downloaded & extracted. - if (manager.processExtractedTarballPackage(&package_id, dependency_id, resolution, &task.data.extract, comptime log_level)) |pkg| brk: { + if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) { + extract_ctx.fixCachedLockfilePackageSlices(); + callbacks.onExtract( + extract_ctx, + dependency_id, + &task.data.extract, + log_level, + ); + } else if (manager.processExtractedTarballPackage(&package_id, dependency_id, resolution, &task.data.extract, log_level)) |pkg| handle_pkg: { // In the middle of an install, you could end up needing to downlaod the github tarball for a dependency // We need to make sure we resolve the dependencies first before calling the onExtract callback // TODO: move this into a separate function var any_root = false; - var dependency_list_entry = manager.task_queue.getEntry(task.id) orelse break :brk; + var dependency_list_entry = manager.task_queue.getEntry(task.id) orelse break :handle_pkg; var dependency_list = dependency_list_entry.value_ptr.*; dependency_list_entry.value_ptr.* = .{}; @@ -6788,13 +6943,8 @@ pub const PackageManager = struct { manager.setPreinstallState(package_id, manager.lockfile, .done); - // if (task.tag == .extract and task.request.extract.network.apply_patch_task != null) { - // manager.enqueuePatchTask(task.request.extract.network.apply_patch_task.?); - // } else - if (comptime @TypeOf(callbacks.onExtract) != void) { - if (ExtractCompletionContext == *PackageInstaller) { - extract_ctx.fixCachedLockfilePackageSlices(); - } + if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext != *PackageInstaller) { + // handled *PackageInstaller above callbacks.onExtract(extract_ctx, dependency_id, &task.data.extract, comptime log_level); } @@ -6807,10 +6957,11 @@ pub const PackageManager = struct { }, .git_clone => { const clone = &task.request.git_clone; + const repo_fd = task.data.git_clone; const name = clone.name.slice(); const url = clone.url.slice(); - manager.git_repositories.put(manager.allocator, task.id, task.data.git_clone) catch unreachable; + manager.git_repositories.put(manager.allocator, task.id, repo_fd) catch unreachable; if (task.status == .fail) { const err = task.err orelse error.Failed; @@ -6837,11 +6988,49 @@ pub const PackageManager = struct { continue; } - const dependency_list_entry = manager.task_queue.getEntry(task.id).?; - const dependency_list = dependency_list_entry.value_ptr.*; - dependency_list_entry.value_ptr.* = .{}; + if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) { + // Installing! + // this dependency might be something other than a git dependency! only need the name and + // behavior, use the resolution from the task. + const dep_id = clone.dep_id; + const dep = manager.lockfile.buffers.dependencies.items[dep_id]; + const dep_name = dep.name.slice(manager.lockfile.buffers.string_bytes.items); - try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks, install_peer); + const git = clone.res.value.git; + const committish = git.committish.slice(manager.lockfile.buffers.string_bytes.items); + const repo = git.repo.slice(manager.lockfile.buffers.string_bytes.items); + + const resolved = try Repository.findCommit( + manager.allocator, + manager.env, + manager.log, + task.data.git_clone.asDir(), + dep_name, + committish, + task.id, + ); + + const checkout_id = Task.Id.forGitCheckout(repo, resolved); + + if (manager.hasCreatedNetworkTask(checkout_id, dep.behavior.isRequired())) continue; + + manager.task_batch.push(ThreadPool.Batch.from(manager.enqueueGitCheckout( + checkout_id, + repo_fd, + dep_id, + dep_name, + clone.res, + resolved, + null, + ))); + } else { + // Resolving! + const dependency_list_entry = manager.task_queue.getEntry(task.id).?; + const dependency_list = dependency_list_entry.value_ptr.*; + dependency_list_entry.value_ptr.* = .{}; + + try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks, install_peer); + } if (comptime log_level.showProgress()) { if (!has_updated_this_run) { @@ -6873,15 +7062,29 @@ pub const PackageManager = struct { continue; } - if (manager.processExtractedTarballPackage( + if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) { + // We've populated the cache, package already exists in memory. Call the package installer callback + // and don't enqueue dependencies + + // TODO(dylan-conway) most likely don't need to call this now that the package isn't appended, but + // keeping just in case for now + extract_ctx.fixCachedLockfilePackageSlices(); + + callbacks.onExtract( + extract_ctx, + git_checkout.dependency_id, + &task.data.git_checkout, + log_level, + ); + } else if (manager.processExtractedTarballPackage( &package_id, git_checkout.dependency_id, resolution, &task.data.git_checkout, - comptime log_level, - )) |pkg| brk: { + log_level, + )) |pkg| handle_pkg: { var any_root = false; - var dependency_list_entry = manager.task_queue.getEntry(task.id) orelse break :brk; + var dependency_list_entry = manager.task_queue.getEntry(task.id) orelse break :handle_pkg; var dependency_list = dependency_list_entry.value_ptr.*; dependency_list_entry.value_ptr.* = .{}; @@ -6908,18 +7111,15 @@ pub const PackageManager = struct { }, } } - } - if (comptime @TypeOf(callbacks.onExtract) != void) { - if (ExtractCompletionContext == *PackageInstaller) { - extract_ctx.fixCachedLockfilePackageSlices(); + if (comptime @TypeOf(callbacks.onExtract) != void) { + callbacks.onExtract( + extract_ctx, + git_checkout.dependency_id, + &task.data.git_checkout, + comptime log_level, + ); } - callbacks.onExtract( - extract_ctx, - git_checkout.dependency_id, - &task.data.git_checkout, - comptime log_level, - ); } if (comptime log_level.showProgress()) { @@ -6931,20 +7131,6 @@ pub const PackageManager = struct { }, } } - - manager.drainDependencyList(); - - if (comptime log_level.showProgress()) { - if (@hasField(@TypeOf(callbacks), "progress_bar") and callbacks.progress_bar == true) { - const completed_items = manager.total_tasks - manager.pendingTaskCount(); - if (completed_items != manager.downloads_node.?.unprotected_completed_items or has_updated_this_run) { - manager.downloads_node.?.setCompletedItems(completed_items); - manager.downloads_node.?.setEstimatedTotalItems(manager.total_tasks); - } - } - manager.downloads_node.?.activate(); - manager.progress.maybeRefresh(); - } } pub const Options = struct { @@ -6957,7 +7143,6 @@ pub const PackageManager = struct { // must be a variable due to global installs and bunx bin_path: stringZ = bun.pathLiteral("node_modules/.bin"), - lockfile_path: stringZ = Lockfile.default_filename, did_override_default_scope: bool = false, scope: Npm.Registry.Scope = undefined, @@ -6972,6 +7157,7 @@ pub const PackageManager = struct { .optional_dependencies = true, }, local_package_features: Features = .{ + .optional_dependencies = true, .dev_dependencies = true, .workspaces = true, }, @@ -6998,6 +7184,10 @@ pub const PackageManager = struct { ca: []const string = &.{}, ca_file_name: string = &.{}, + save_text_lockfile: bool = false, + + lockfile_only: bool = false, + pub const PublishConfig = struct { access: ?Access = null, tag: string = "", @@ -7055,6 +7245,7 @@ pub const PackageManager = struct { pub const Update = struct { development: bool = false, optional: bool = false, + peer: bool = false, }; pub fn openGlobalDir(explicit_global_dir: string) !std.fs.Dir { @@ -7153,6 +7344,10 @@ pub const PackageManager = struct { this.did_override_default_scope = this.scope.url_hash != Npm.Registry.default_url_hash; } if (bun_install_) |config| { + if (config.cache_directory) |cache_directory| { + this.cache_directory = cache_directory; + } + if (config.scoped) |scoped| { for (scoped.scopes.keys(), scoped.scopes.values()) |name, *registry_| { var registry = registry_.*; @@ -7200,11 +7395,18 @@ pub const PackageManager = struct { if (config.save_dev) |save| { this.local_package_features.dev_dependencies = save; + // remote packages should never install dev dependencies + // (TODO: unless git dependency with postinstalls) + } + + if (config.save_optional) |save| { + this.remote_package_features.optional_dependencies = save; + this.local_package_features.optional_dependencies = save; } if (config.save_peer) |save| { - this.do.install_peer_dependencies = save; this.remote_package_features.peer_dependencies = save; + this.local_package_features.peer_dependencies = save; } if (config.exact) |exact| { @@ -7226,13 +7428,16 @@ pub const PackageManager = struct { } } + if (config.save_text_lockfile) |save_text_lockfile| { + this.save_text_lockfile = save_text_lockfile; + } + if (config.concurrent_scripts) |jobs| { this.max_concurrent_lifecycle_scripts = jobs; } - if (config.save_optional) |save| { - this.remote_package_features.optional_dependencies = save; - this.local_package_features.optional_dependencies = save; + if (config.cache_directory) |cache_dir| { + this.cache_directory = cache_dir; } this.explicit_global_directory = config.global_dir orelse this.explicit_global_directory; @@ -7338,6 +7543,10 @@ pub const PackageManager = struct { this.scope.url = URL.parse(cli.registry); } + if (cli.cache_dir) |cache_dir| { + this.cache_directory = cache_dir; + } + if (cli.exact) { this.enable.exact_versions = true; } @@ -7358,7 +7567,7 @@ pub const PackageManager = struct { this.do.save_lockfile = false; } - if (cli.no_summary) { + if (cli.no_summary or cli.silent) { this.do.summary = false; } @@ -7372,8 +7581,22 @@ pub const PackageManager = struct { this.enable.manifest_cache_control = false; } - if (cli.omit.dev) { - this.local_package_features.dev_dependencies = false; + if (cli.omit) |omit| { + if (omit.dev) { + this.local_package_features.dev_dependencies = false; + // remote packages should never install dev dependencies + // (TODO: unless git dependency with postinstalls) + } + + if (omit.optional) { + this.local_package_features.optional_dependencies = false; + this.remote_package_features.optional_dependencies = false; + } + + if (omit.peer) { + this.local_package_features.peer_dependencies = false; + this.remote_package_features.peer_dependencies = false; + } } if (cli.global or cli.ignore_scripts) { @@ -7384,7 +7607,11 @@ pub const PackageManager = struct { this.do.trust_dependencies_from_args = true; } - this.local_package_features.optional_dependencies = !cli.omit.optional; + if (cli.save_text_lockfile) |save_text_lockfile| { + this.save_text_lockfile = save_text_lockfile; + } + + this.lockfile_only = cli.lockfile_only; const disable_progress_bar = default_disable_progress_bar or cli.no_progress; @@ -7433,8 +7660,13 @@ pub const PackageManager = struct { this.enable.force_save_lockfile = true; } - this.update.development = cli.development; - if (!this.update.development) this.update.optional = cli.optional; + if (cli.development) { + this.update.development = cli.development; + } else if (cli.optional) { + this.update.optional = cli.optional; + } else if (cli.peer) { + this.update.peer = cli.peer; + } switch (cli.patch) { .nothing => {}, @@ -7491,7 +7723,6 @@ pub const PackageManager = struct { print_meta_hash_string: bool = false, verify_integrity: bool = true, summary: bool = true, - install_peer_dependencies: bool = true, trust_dependencies_from_args: bool = false, update_to_latest: bool = false, }; @@ -7570,7 +7801,7 @@ pub const PackageManager = struct { const dependency_groups = &.{ .{ "optionalDependencies", Dependency.Behavior.optional }, .{ "devDependencies", Dependency.Behavior.dev }, - .{ "dependencies", Dependency.Behavior.normal }, + .{ "dependencies", Dependency.Behavior.prod }, .{ "peerDependencies", Dependency.Behavior.peer }, }; @@ -8395,6 +8626,7 @@ pub const PackageManager = struct { pub fn supportsWorkspaceFiltering(this: Subcommand) bool { return switch (this) { .outdated => true, + .install => true, // .pack => true, else => false, }; @@ -8611,7 +8843,7 @@ pub const PackageManager = struct { } else child_path; if (strings.eqlLong(maybe_workspace_path, path, true)) { - fs.top_level_dir = parent; + fs.top_level_dir = try bun.default_allocator.dupeZ(u8, parent); found = true; child_json.close(); if (comptime Environment.isWindows) { @@ -8628,16 +8860,15 @@ pub const PackageManager = struct { } } - fs.top_level_dir = child_cwd; + fs.top_level_dir = try bun.default_allocator.dupeZ(u8, child_cwd); break :root_package_json_file child_json; }; - try bun.sys.chdir(fs.top_level_dir).unwrap(); + try bun.sys.chdir(fs.top_level_dir, fs.top_level_dir).unwrap(); try BunArguments.loadConfig(ctx.allocator, cli.config, ctx, .InstallCommand); bun.copy(u8, &cwd_buf, fs.top_level_dir); - cwd_buf[fs.top_level_dir.len] = std.fs.path.sep; - cwd_buf[fs.top_level_dir.len + 1] = 0; - fs.top_level_dir = cwd_buf[0 .. fs.top_level_dir.len + 1]; + cwd_buf[fs.top_level_dir.len] = 0; + fs.top_level_dir = cwd_buf[0..fs.top_level_dir.len :0]; package_json_cwd = try bun.getFdPath(root_package_json_file.handle, &package_json_cwd_buf); const entries_option = try fs.fs.readDirectory(fs.top_level_dir, null, 0, true); @@ -8661,38 +8892,25 @@ pub const PackageManager = struct { "./.npmrc", }; - bun.ini.loadNpmrcFromFile( - ctx.allocator, - ctx.install orelse brk: { - const install_ = ctx.allocator.create(Api.BunInstall) catch bun.outOfMemory(); - install_.* = std.mem.zeroes(Api.BunInstall); - ctx.install = install_; - break :brk install_; - }, - env, - true, - Path.joinAbsStringBufZ( - data_dir, - &buf, - &parts, - .auto, - ), - ); - } - - bun.ini.loadNpmrcFromFile( - ctx.allocator, - ctx.install orelse brk: { + bun.ini.loadNpmrcConfig(ctx.allocator, ctx.install orelse brk: { const install_ = ctx.allocator.create(Api.BunInstall) catch bun.outOfMemory(); install_.* = std.mem.zeroes(Api.BunInstall); ctx.install = install_; break :brk install_; - }, - env, - true, - ".npmrc", - ); - + }, env, true, &[_][:0]const u8{ Path.joinAbsStringBufZ( + data_dir, + &buf, + &parts, + .auto, + ), ".npmrc" }); + } else { + bun.ini.loadNpmrcConfig(ctx.allocator, ctx.install orelse brk: { + const install_ = ctx.allocator.create(Api.BunInstall) catch bun.outOfMemory(); + install_.* = std.mem.zeroes(Api.BunInstall); + ctx.install = install_; + break :brk install_; + }, env, true, &[_][:0]const u8{".npmrc"}); + } const cpu_count = bun.getThreadCount(); const options = Options{ @@ -8721,6 +8939,9 @@ pub const PackageManager = struct { // var node = progress.start(name: []const u8, estimated_total_items: usize) manager.* = PackageManager{ .options = options, + .active_lifecycle_scripts = .{ + .context = manager, + }, .network_task_fifo = NetworkQueue.init(), .patch_task_fifo = PatchTaskFifo.init(), .allocator = ctx.allocator, @@ -8887,6 +9108,9 @@ pub const PackageManager = struct { .options = .{ .max_concurrent_lifecycle_scripts = cli.concurrent_scripts orelse cpu_count * 2, }, + .active_lifecycle_scripts = .{ + .context = manager, + }, .network_task_fifo = NetworkQueue.init(), .allocator = allocator, .log = log, @@ -8959,24 +9183,10 @@ pub const PackageManager = struct { ) -| std.time.s_per_day; if (root_dir.entries.hasComptimeQuery("bun.lockb")) { - var buf: bun.PathBuffer = undefined; - var parts = [_]string{ - "./bun.lockb", - }; - const lockfile_path = Path.joinAbsStringBuf( - Fs.FileSystem.instance.top_level_dir, - &buf, - &parts, - .auto, - ); - buf[lockfile_path.len] = 0; - const lockfile_path_z = buf[0..lockfile_path.len :0]; - - switch (manager.lockfile.loadFromDisk( + switch (manager.lockfile.loadFromCwd( manager, allocator, log, - lockfile_path_z, true, )) { .ok => |load| manager.lockfile = load.lockfile, @@ -9077,7 +9287,8 @@ pub const PackageManager = struct { }; lockfile.initEmpty(ctx.allocator); - try package.parse(&lockfile, manager, ctx.allocator, manager.log, package_json_source, void, {}, Features.folder); + var resolver: void = {}; + try package.parse(&lockfile, manager, ctx.allocator, manager.log, package_json_source, void, &resolver, Features.folder); name = lockfile.str(&package.name); if (name.len == 0) { if (manager.options.log_level != .silent) { @@ -9222,7 +9433,7 @@ pub const PackageManager = struct { } else { // bun link lodash switch (manager.options.log_level) { - inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), } } } @@ -9259,7 +9470,8 @@ pub const PackageManager = struct { }; lockfile.initEmpty(ctx.allocator); - try package.parse(&lockfile, manager, ctx.allocator, manager.log, package_json_source, void, {}, Features.folder); + var resolver: void = {}; + try package.parse(&lockfile, manager, ctx.allocator, manager.log, package_json_source, void, &resolver, Features.folder); name = lockfile.str(&package.name); if (name.len == 0) { if (manager.options.log_level != .silent) { @@ -9383,6 +9595,9 @@ pub const PackageManager = struct { clap.parseParam("--registry Use a specific registry by default, overriding .npmrc, bunfig.toml and environment variables") catch unreachable, clap.parseParam("--concurrent-scripts Maximum number of concurrent jobs for lifecycle scripts (default 5)") catch unreachable, clap.parseParam("--network-concurrency Maximum number of concurrent network requests (default 48)") catch unreachable, + clap.parseParam("--save-text-lockfile Save a text-based lockfile") catch unreachable, + clap.parseParam("--omit ... Exclude 'dev', 'optional', or 'peer' dependencies from install") catch unreachable, + clap.parseParam("--lockfile-only Generate a lockfile without installing dependencies") catch unreachable, clap.parseParam("-h, --help Print this help menu") catch unreachable, }; @@ -9390,7 +9605,9 @@ pub const PackageManager = struct { clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable, clap.parseParam("-D, --development") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, + clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, + clap.parseParam("--filter ... Install packages for the matching workspaces") catch unreachable, clap.parseParam(" ... ") catch unreachable, }); @@ -9411,6 +9628,7 @@ pub const PackageManager = struct { clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable, clap.parseParam("-D, --development") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, + clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, clap.parseParam(" ... \"name\" or \"name@version\" of package(s) to install") catch unreachable, }); @@ -9440,7 +9658,7 @@ pub const PackageManager = struct { const outdated_params: []const ParamType = &(shared_params ++ [_]ParamType{ // clap.parseParam("--json Output outdated information in JSON format") catch unreachable, - clap.parseParam("--filter ... Display outdated dependencies for each matching workspace") catch unreachable, + clap.parseParam("-F, --filter ... Display outdated dependencies for each matching workspace") catch unreachable, clap.parseParam(" ... Package patterns to filter by") catch unreachable, }); @@ -9461,7 +9679,7 @@ pub const PackageManager = struct { }); pub const CommandLineArguments = struct { - cache_dir: string = "", + cache_dir: ?string = null, lockfile: string = "", token: string = "", global: bool = false, @@ -9494,9 +9712,9 @@ pub const PackageManager = struct { development: bool = false, optional: bool = false, + peer: bool = false, - no_optional: bool = false, - omit: Omit = Omit{}, + omit: ?Omit = null, exact: bool = false, @@ -9511,6 +9729,10 @@ pub const PackageManager = struct { ca: []const string = &.{}, ca_file_name: string = "", + save_text_lockfile: ?bool = null, + + lockfile_only: bool = false, + const PatchOpts = union(enum) { nothing: struct {}, patch: struct {}, @@ -9521,25 +9743,23 @@ pub const PackageManager = struct { const Omit = struct { dev: bool = false, - optional: bool = true, + optional: bool = false, peer: bool = false, - - pub inline fn toFeatures(this: Omit) Features { - return .{ - .dev_dependencies = this.dev, - .optional_dependencies = this.optional, - .peer_dependencies = this.peer, - }; - } }; pub fn printHelp(subcommand: Subcommand) void { + + // the output of --help uses the following syntax highlighting + // template: Usage: bun [flags] [arguments] + // use [foo] for multiple arguments or flags for foo. + // use to emphasize 'bar' + switch (subcommand) { // fall back to HelpCommand.printWithReason Subcommand.install => { const intro_text = - \\Usage: bun install [flags] [...\] - \\Alias: bun i + \\Usage: bun install [flags] \@\ + \\Alias: bun i \\ Install the dependencies listed in package.json ; const outro_text = @@ -9548,7 +9768,7 @@ pub const PackageManager = struct { \\ bun install \\ \\ Skip devDependencies - \\ bun install --production + \\ bun install --production \\ \\Full documentation is available at https://bun.sh/docs/cli/install ; @@ -9562,7 +9782,7 @@ pub const PackageManager = struct { }, Subcommand.update => { const intro_text = - \\Usage: bun update [flags] + \\Usage: bun update [flags] \@\ \\ Update all dependencies to most recent versions within the version range in package.json \\ ; @@ -9572,10 +9792,10 @@ pub const PackageManager = struct { \\ bun update \\ \\ Update all dependencies to latest: - \\ bun update --latest + \\ bun update --latest \\ \\ Update specific packages: - \\ bun update zod jquery@3 + \\ bun update zod jquery@3 \\ \\Full documentation is available at https://bun.sh/docs/cli/update ; @@ -9589,7 +9809,7 @@ pub const PackageManager = struct { }, Subcommand.patch => { const intro_text = - \\Usage: bun patch \@\ + \\Usage: bun patch [flags or options] \@\ \\ \\Prepare a package for patching. \\ @@ -9601,11 +9821,12 @@ pub const PackageManager = struct { Output.flush(); clap.simpleHelp(PackageManager.patch_params); // Output.pretty("\n\n" ++ outro_text ++ "\n", .{}); + Output.pretty("\n", .{}); Output.flush(); }, Subcommand.@"patch-commit" => { const intro_text = - \\Usage: bun patch-commit \ + \\Usage: bun patch-commit [flags or options] \ \\ \\Generate a patc out of a directory and save it. \\ @@ -9626,6 +9847,7 @@ pub const PackageManager = struct { Output.flush(); clap.simpleHelp(PackageManager.patch_params); // Output.pretty("\n\n" ++ outro_text ++ "\n", .{}); + Output.pretty("\n", .{}); Output.flush(); }, Subcommand.pm => { @@ -9633,20 +9855,20 @@ pub const PackageManager = struct { }, Subcommand.add => { const intro_text = - \\Usage: bun add [flags] \ [...\] - \\Alias: bun a + \\Usage: bun add [flags] \\<@version\> + \\Alias: bun a ; const outro_text = \\Examples: \\ Add a dependency from the npm registry - \\ bun add zod - \\ bun add zod@next - \\ bun add zod@3.0.0 + \\ bun add zod + \\ bun add zod@next + \\ bun add zod@3.0.0 \\ \\ Add a dev, optional, or peer dependency - \\ bun add -d typescript - \\ bun add --optional lodash - \\ bun add --peer esbuild + \\ bun add -d typescript + \\ bun add --optional lodash + \\ bun add --peer esbuild \\ \\Full documentation is available at https://bun.sh/docs/cli/add ; @@ -9660,7 +9882,7 @@ pub const PackageManager = struct { }, Subcommand.remove => { const intro_text = - \\Usage: bun remove [flags] \ [...\] + \\Usage: bun remove [flags] [\] \\Alias: bun r \\ Remove a package from package.json and uninstall from node_modules \\ @@ -9668,7 +9890,7 @@ pub const PackageManager = struct { const outro_text = \\Examples: \\ Remove a dependency - \\ bun remove ts-node + \\ bun remove ts-node \\ \\Full documentation is available at https://bun.sh/docs/cli/remove ; @@ -9682,7 +9904,7 @@ pub const PackageManager = struct { }, Subcommand.link => { const intro_text = - \\Usage: bun link [flags] [\] + \\Usage: bun link [flags] [\] \\ ; const outro_text = @@ -9692,7 +9914,7 @@ pub const PackageManager = struct { \\ bun link \\ \\ Add a previously-registered linkable package as a dependency of the current project. - \\ bun link \ + \\ bun link \ \\ \\Full documentation is available at https://bun.sh/docs/cli/link ; @@ -9727,7 +9949,7 @@ pub const PackageManager = struct { }, .outdated => { const intro_text = - \\Usage: bun outdated [flags] + \\Usage: bun outdated [flags] [filter] ; const outro_text = @@ -9736,14 +9958,14 @@ pub const PackageManager = struct { \\ bun outdated \\ \\ Use --filter to include more than one workspace. - \\ bun outdated --filter="*" - \\ bun outdated --filter="./app/*" - \\ bun outdated --filter="!frontend" + \\ bun outdated --filter="*" + \\ bun outdated --filter="./app/*" + \\ bun outdated --filter="!frontend" \\ \\ Filter dependencies with name patterns. - \\ bun outdated jquery - \\ bun outdated "is-*" - \\ bun outdated "!is-even" + \\ bun outdated jquery + \\ bun outdated "is-*" + \\ bun outdated "!is-even" \\ ; @@ -9774,19 +9996,19 @@ pub const PackageManager = struct { }, .publish => { const intro_text = - \\Usage: bun publish [flags] + \\Usage: bun publish [flags] [dist] ; const outro_text = \\Examples: \\ Display files that would be published, without publishing to the registry. - \\ bun publish --dry-run + \\ bun publish --dry-run \\ \\ Publish the current package with public access. - \\ bun publish --access public + \\ bun publish --access public \\ \\ Publish a pre-existing package tarball with tag 'next'. - \\ bun publish ./path/to/tarball.tgz --tag next + \\ bun publish --tag next ./path/to/tarball.tgz \\ ; @@ -9851,6 +10073,11 @@ pub const PackageManager = struct { cli.trusted = args.flag("--trust"); cli.no_summary = args.flag("--no-summary"); cli.ca = args.options("--ca"); + cli.lockfile_only = args.flag("--lockfile-only"); + + if (args.option("--cache-dir")) |cache_dir| { + cli.cache_dir = cache_dir; + } if (args.option("--cafile")) |ca_file_name| { cli.ca_file_name = ca_file_name; @@ -9863,6 +10090,29 @@ pub const PackageManager = struct { }; } + if (args.flag("--save-text-lockfile")) { + cli.save_text_lockfile = true; + } + + const omit_values = args.options("--omit"); + + if (omit_values.len > 0) { + var omit: Omit = .{}; + for (omit_values) |omit_value| { + if (strings.eqlComptime(omit_value, "dev")) { + omit.dev = true; + } else if (strings.eqlComptime(omit_value, "optional")) { + omit.optional = true; + } else if (strings.eqlComptime(omit_value, "peer")) { + omit.peer = true; + } else { + Output.errGeneric("invalid `omit` value: '{s}'", .{omit_value}); + Global.crash(); + } + } + cli.omit = omit; + } + // commands that support --filter if (comptime subcommand.supportsWorkspaceFiltering()) { cli.filters = args.options("--filter"); @@ -9947,6 +10197,7 @@ pub const PackageManager = struct { if (comptime subcommand == .add or subcommand == .install) { cli.development = args.flag("--development") or args.flag("--dev"); cli.optional = args.flag("--optional"); + cli.peer = args.flag("--peer"); cli.exact = args.flag("--exact"); } @@ -9969,7 +10220,7 @@ pub const PackageManager = struct { buf[cwd_.len] = 0; final_path = buf[0..cwd_.len :0]; } - bun.sys.chdir(final_path).unwrap() catch |err| { + bun.sys.chdir("", final_path).unwrap() catch |err| { Output.errGeneric("failed to change directory to \"{s}\": {s}\n", .{ final_path, @errorName(err) }); Global.crash(); }; @@ -10297,7 +10548,7 @@ pub const PackageManager = struct { } switch (manager.options.log_level) { - inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), } if (manager.options.patch_features == .patch) { @@ -10428,6 +10679,7 @@ pub const PackageManager = struct { fn updatePackageJSONAndInstallWithManager( manager: *PackageManager, ctx: Command.Context, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { var update_requests = UpdateRequest.Array.initCapacity(manager.allocator, 64) catch bun.outOfMemory(); @@ -10461,6 +10713,7 @@ pub const PackageManager = struct { ctx, updates, manager.subcommand, + original_cwd, log_level, ); } @@ -10470,6 +10723,7 @@ pub const PackageManager = struct { ctx: Command.Context, updates: []UpdateRequest, subcommand: Subcommand, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { if (manager.log.errors > 0) { @@ -10533,6 +10787,8 @@ pub const PackageManager = struct { "devDependencies" else if (manager.options.update.optional) "optionalDependencies" + else if (manager.options.update.peer) + "peerDependencies" else "dependencies"; var any_changes = false; @@ -10740,7 +10996,7 @@ pub const PackageManager = struct { break :brk .{ root_package_json.source.contents, root_package_json_path_buf[0..root_package_json_path.len :0] }; }; - try manager.installWithManager(ctx, root_package_json_source, log_level); + try manager.installWithManager(ctx, root_package_json_source, original_cwd, log_level); if (subcommand == .update or subcommand == .add or subcommand == .link) { for (updates) |request| { @@ -10879,8 +11135,8 @@ pub const PackageManager = struct { } } - fn nodeModulesFolderForDependencyIDs(iterator: *Lockfile.Tree.Iterator, ids: []const IdPair) !?Lockfile.Tree.NodeModulesFolder { - while (iterator.nextNodeModulesFolder(null)) |node_modules| { + fn nodeModulesFolderForDependencyIDs(iterator: *Lockfile.Tree.Iterator(.node_modules), ids: []const IdPair) !?Lockfile.Tree.Iterator(.node_modules).Next { + while (iterator.next(null)) |node_modules| { for (ids) |id| { _ = std.mem.indexOfScalar(DependencyID, node_modules.dependencies, id[0]) orelse continue; return node_modules; @@ -10889,8 +11145,8 @@ pub const PackageManager = struct { return null; } - fn nodeModulesFolderForDependencyID(iterator: *Lockfile.Tree.Iterator, dependency_id: DependencyID) !?Lockfile.Tree.NodeModulesFolder { - while (iterator.nextNodeModulesFolder(null)) |node_modules| { + fn nodeModulesFolderForDependencyID(iterator: *Lockfile.Tree.Iterator(.node_modules), dependency_id: DependencyID) !?Lockfile.Tree.Iterator(.node_modules).Next { + while (iterator.next(null)) |node_modules| { _ = std.mem.indexOfScalar(DependencyID, node_modules.dependencies, dependency_id) orelse continue; return node_modules; } @@ -10902,11 +11158,11 @@ pub const PackageManager = struct { fn pkgInfoForNameAndVersion( lockfile: *Lockfile, - iterator: *Lockfile.Tree.Iterator, + iterator: *Lockfile.Tree.Iterator(.node_modules), pkg_maybe_version_to_patch: []const u8, name: []const u8, version: ?[]const u8, - ) struct { PackageID, Lockfile.Tree.NodeModulesFolder } { + ) struct { PackageID, Lockfile.Tree.Iterator(.node_modules).Next } { var sfb = std.heap.stackFallback(@sizeOf(IdPair) * 4, lockfile.allocator); var pairs = std.ArrayList(IdPair).initCapacity(sfb.get(), 8) catch bun.outOfMemory(); defer pairs.deinit(); @@ -11080,7 +11336,7 @@ pub const PackageManager = struct { const arg_kind: PatchArgKind = PatchArgKind.fromArg(argument); var folder_path_buf: bun.PathBuffer = undefined; - var iterator = Lockfile.Tree.Iterator.init(manager.lockfile); + var iterator = Lockfile.Tree.Iterator(.node_modules).init(manager.lockfile); var resolution_buf: [1024]u8 = undefined; var win_normalizer: if (bun.Environment.isWindows) bun.PathBuffer else struct {} = undefined; @@ -11109,10 +11365,7 @@ pub const PackageManager = struct { switch (bun.sys.File.toSource(package_json_path, manager.allocator)) { .result => |s| break :src s, .err => |e| { - Output.prettyError( - "error: failed to read package.json: {}\n", - .{e.withPath(package_json_path).toSystemError()}, - ); + Output.err(e, "failed to read {s}", .{bun.fmt.quote(package_json_path)}); Global.crash(); }, } @@ -11120,7 +11373,7 @@ pub const PackageManager = struct { defer manager.allocator.free(package_json_source.contents); initializeStore(); - const json = JSON.parsePackageJSONUTF8AlwaysDecode(&package_json_source, manager.log, manager.allocator) catch |err| { + const json = JSON.parsePackageJSONUTF8(&package_json_source, manager.log, manager.allocator) catch |err| { manager.log.print(Output.errorWriter()) catch {}; Output.prettyErrorln("{s} parsing package.json in \"{s}\"", .{ @errorName(err), package_json_source.path.prettyDir() }); Global.crash(); @@ -11137,8 +11390,9 @@ pub const PackageManager = struct { Global.crash(); }; + var resolver: void = {}; var package = Lockfile.Package{}; - try package.parseWithJSON(lockfile, manager, manager.allocator, manager.log, package_json_source, json, void, {}, Features.folder); + try package.parseWithJSON(lockfile, manager, manager.allocator, manager.log, package_json_source, json, void, &resolver, Features.folder); const name = lockfile.str(&package.name); const actual_package = switch (lockfile.package_index.get(package.name_hash) orelse { @@ -11148,8 +11402,8 @@ pub const PackageManager = struct { ); Global.crash(); }) { - .PackageID => |id| lockfile.packages.get(id), - .PackageIDMultiple => |ids| id: { + .id => |id| lockfile.packages.get(id), + .ids => |ids| id: { for (ids.items) |id| { const pkg = lockfile.packages.get(id); const resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{pkg.resolution.fmt(lockfile.buffers.string_bytes.items, .posix)}) catch unreachable; @@ -11196,7 +11450,7 @@ pub const PackageManager = struct { }, .name_and_version => brk: { const pkg_maybe_version_to_patch = argument; - const name, const version = Dependency.splitNameAndVersion(pkg_maybe_version_to_patch); + const name, const version = Dependency.splitNameAndMaybeVersion(pkg_maybe_version_to_patch); const pkg_id, const folder = pkgInfoForNameAndVersion(manager.lockfile, &iterator, pkg_maybe_version_to_patch, name, version); const pkg = manager.lockfile.packages.get(pkg_id); @@ -11447,7 +11701,7 @@ pub const PackageManager = struct { var folder_path_buf: bun.PathBuffer = undefined; var lockfile: *Lockfile = try manager.allocator.create(Lockfile); defer lockfile.deinit(); - switch (lockfile.loadFromDisk(manager, manager.allocator, manager.log, manager.options.lockfile_path, true)) { + switch (lockfile.loadFromCwd(manager, manager.allocator, manager.log, true)) { .not_found => { Output.errGeneric("Cannot find lockfile. Install packages with `bun install` before patching them.", .{}); Global.crash(); @@ -11512,7 +11766,7 @@ pub const PackageManager = struct { }; defer root_node_modules.close(); - var iterator = Lockfile.Tree.Iterator.init(lockfile); + var iterator = Lockfile.Tree.Iterator(.node_modules).init(lockfile); var resolution_buf: [1024]u8 = undefined; const _cache_dir: std.fs.Dir, const _cache_dir_subpath: stringZ, const _changes_dir: []const u8, const _pkg: Package = switch (arg_kind) { .path => result: { @@ -11522,10 +11776,7 @@ pub const PackageManager = struct { switch (bun.sys.File.toSource(package_json_path, manager.allocator)) { .result => |s| break :brk s, .err => |e| { - Output.prettyError( - "error: failed to read package.json: {}\n", - .{e.withPath(package_json_path).toSystemError()}, - ); + Output.err(e, "failed to read {s}", .{bun.fmt.quote(package_json_path)}); Global.crash(); }, } @@ -11533,7 +11784,7 @@ pub const PackageManager = struct { defer manager.allocator.free(package_json_source.contents); initializeStore(); - const json = JSON.parsePackageJSONUTF8AlwaysDecode(&package_json_source, manager.log, manager.allocator) catch |err| { + const json = JSON.parsePackageJSONUTF8(&package_json_source, manager.log, manager.allocator) catch |err| { manager.log.print(Output.errorWriter()) catch {}; Output.prettyErrorln("{s} parsing package.json in \"{s}\"", .{ @errorName(err), package_json_source.path.prettyDir() }); Global.crash(); @@ -11550,8 +11801,9 @@ pub const PackageManager = struct { Global.crash(); }; + var resolver: void = {}; var package = Lockfile.Package{}; - try package.parseWithJSON(lockfile, manager, manager.allocator, manager.log, package_json_source, json, void, {}, Features.folder); + try package.parseWithJSON(lockfile, manager, manager.allocator, manager.log, package_json_source, json, void, &resolver, Features.folder); const name = lockfile.str(&package.name); const actual_package = switch (lockfile.package_index.get(package.name_hash) orelse { @@ -11561,8 +11813,8 @@ pub const PackageManager = struct { ); Global.crash(); }) { - .PackageID => |id| lockfile.packages.get(id), - .PackageIDMultiple => |ids| brk: { + .id => |id| lockfile.packages.get(id), + .ids => |ids| brk: { for (ids.items) |id| { const pkg = lockfile.packages.get(id); const resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{pkg.resolution.fmt(lockfile.buffers.string_bytes.items, .posix)}) catch unreachable; @@ -11591,7 +11843,7 @@ pub const PackageManager = struct { break :result .{ cache_dir, cache_dir_subpath, changes_dir, actual_package }; }, .name_and_version => brk: { - const name, const version = Dependency.splitNameAndVersion(argument); + const name, const version = Dependency.splitNameAndMaybeVersion(argument); const pkg_id, const node_modules = pkgInfoForNameAndVersion(lockfile, &iterator, argument, name, version); const changes_dir = bun.path.joinZBuf(pathbuf[0..], &[_][]const u8{ @@ -11629,10 +11881,7 @@ pub const PackageManager = struct { const cache_dir_path = switch (bun.sys.getFdPath(bun.toFD(cache_dir.fd), &buf2)) { .result => |s| s, .err => |e| { - Output.prettyError( - "error: failed to read from cache {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed to read from cache", .{}); Global.crash(); }, }; @@ -11643,10 +11892,7 @@ pub const PackageManager = struct { }; const random_tempdir = bun.span(bun.fs.FileSystem.instance.tmpname("node_modules_tmp", buf2[0..], bun.fastRandom()) catch |e| { - Output.prettyError( - "error: failed to make tempdir {s}\n", - .{@errorName(e)}, - ); + Output.err(e, "failed to make tempdir", .{}); Global.crash(); }); @@ -11657,10 +11903,7 @@ pub const PackageManager = struct { // will `rename()` it out and back again. const has_nested_node_modules = has_nested_node_modules: { var new_folder_handle = std.fs.cwd().openDir(new_folder, .{}) catch |e| { - Output.prettyError( - "error: failed to open directory {s} {s}\n", - .{ new_folder, @errorName(e) }, - ); + Output.err(e, "failed to open directory {s}", .{new_folder}); Global.crash(); }; defer new_folder_handle.close(); @@ -11677,10 +11920,7 @@ pub const PackageManager = struct { }; const patch_tag_tmpname = bun.span(bun.fs.FileSystem.instance.tmpname("patch_tmp", buf3[0..], bun.fastRandom()) catch |e| { - Output.prettyError( - "error: failed to make tempdir {s}\n", - .{@errorName(e)}, - ); + Output.err(e, "failed to make tempdir", .{}); Global.crash(); }); @@ -11698,10 +11938,7 @@ pub const PackageManager = struct { break :has_bun_patch_tag null; }; var new_folder_handle = std.fs.cwd().openDir(new_folder, .{}) catch |e| { - Output.prettyError( - "error: failed to open directory {s} {s}\n", - .{ new_folder, @errorName(e) }, - ); + Output.err(e, "failed to open directory {s}", .{new_folder}); Global.crash(); }; defer new_folder_handle.close(); @@ -11852,20 +12089,14 @@ pub const PackageManager = struct { )) { .result => |fd| fd, .err => |e| { - Output.prettyError( - "error: failed to open temp file {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed to open temp file", .{}); Global.crash(); }, }; defer _ = bun.sys.close(tmpfd); if (bun.sys.File.writeAll(.{ .handle = tmpfd }, patchfile_contents.items).asErr()) |e| { - Output.prettyError( - "error: failed to write patch to temp file {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed to write patch to temp file", .{}); Global.crash(); } @@ -11890,11 +12121,8 @@ pub const PackageManager = struct { const args = bun.JSC.Node.Arguments.Mkdir{ .path = .{ .string = bun.PathString.init(manager.options.patch_features.commit.patches_dir) }, }; - if (nodefs.mkdirRecursive(args, .sync).asErr()) |e| { - Output.prettyError( - "error: failed to make patches dir {}\n", - .{e.toSystemError()}, - ); + if (nodefs.mkdirRecursive(args).asErr()) |e| { + Output.err(e, "failed to make patches dir {}", .{bun.fmt.quote(args.path.slice())}); Global.crash(); } @@ -11906,10 +12134,7 @@ pub const PackageManager = struct { path_in_patches_dir, .{ .move_fallback = true }, ).asErr()) |e| { - Output.prettyError( - "error: failed renaming patch file to patches dir {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed renaming patch file to patches dir", .{}); Global.crash(); } @@ -12007,7 +12232,7 @@ pub const PackageManager = struct { // TODO(dylan-conway): print `bun install ` or `bun add ` before logs from `init`. // and cleanup install/add subcommand usage - var manager, _ = try init(ctx, cli, .install); + var manager, const original_cwd = try init(ctx, cli, .install); // switch to `bun add ` if (subcommand == .add) { @@ -12017,7 +12242,7 @@ pub const PackageManager = struct { Output.flush(); } return try switch (manager.options.log_level) { - inline else => |log_level| manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), }; } @@ -12035,7 +12260,7 @@ pub const PackageManager = struct { }; try switch (manager.options.log_level) { - inline else => |log_level| manager.installWithManager(ctx, package_json_contents, log_level), + inline else => |log_level| manager.installWithManager(ctx, package_json_contents, original_cwd, log_level), }; if (manager.any_failed_to_install) { @@ -12043,6 +12268,40 @@ pub const PackageManager = struct { } } + pub const LazyPackageDestinationDir = union(enum) { + dir: std.fs.Dir, + node_modules_path: struct { + node_modules: *NodeModulesFolder, + root_node_modules_dir: std.fs.Dir, + }, + closed: void, + + pub fn getDir(this: *LazyPackageDestinationDir) !std.fs.Dir { + return switch (this.*) { + .dir => |dir| dir, + .node_modules_path => |lazy| brk: { + const dir = try lazy.node_modules.openDir(lazy.root_node_modules_dir); + this.* = .{ .dir = dir }; + break :brk dir; + }, + .closed => @panic("LazyPackageDestinationDir is closed! This should never happen. Why did this happen?! It's not your fault. Its our fault. We're sorry."), + }; + } + + pub fn close(this: *LazyPackageDestinationDir) void { + switch (this.*) { + .dir => { + if (this.dir.fd != std.fs.cwd().fd) { + this.dir.close(); + } + }, + .node_modules_path, .closed => {}, + } + + this.* = .{ .closed = {} }; + } + }; + pub const NodeModulesFolder = struct { tree_id: Lockfile.Tree.Id = 0, path: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), @@ -12051,9 +12310,73 @@ pub const PackageManager = struct { this.path.clearAndFree(); } + // Since the stack size of these functions are rather large, let's not let them be inlined. + noinline fn directoryExistsAtWithoutOpeningDirectories(this: *const NodeModulesFolder, root_node_modules_dir: std.fs.Dir, file_path: [:0]const u8) bool { + var path_buf: bun.PathBuffer = undefined; + const parts: [2][]const u8 = .{ this.path.items, file_path }; + return bun.sys.directoryExistsAt(bun.toFD(root_node_modules_dir), bun.path.joinZBuf(&path_buf, &parts, .auto)).unwrapOr(false); + } + + pub fn directoryExistsAt(this: *const NodeModulesFolder, root_node_modules_dir: std.fs.Dir, file_path: [:0]const u8) bool { + if (file_path.len + this.path.items.len * 2 < bun.MAX_PATH_BYTES) { + return this.directoryExistsAtWithoutOpeningDirectories(root_node_modules_dir, file_path); + } + + const dir = this.openDir(root_node_modules_dir) catch return false; + defer { + _ = bun.sys.close(bun.toFD(dir)); + } + + return bun.sys.directoryExistsAt(bun.toFD(dir), file_path).unwrapOr(false); + } + + // Since the stack size of these functions are rather large, let's not let them be inlined. + noinline fn openFileWithoutOpeningDirectories(this: *const NodeModulesFolder, root_node_modules_dir: std.fs.Dir, file_path: [:0]const u8) bun.sys.Maybe(bun.sys.File) { + var path_buf: bun.PathBuffer = undefined; + const parts: [2][]const u8 = .{ this.path.items, file_path }; + return bun.sys.File.openat(bun.toFD(root_node_modules_dir), bun.path.joinZBuf(&path_buf, &parts, .auto), bun.O.RDONLY, 0); + } + + pub fn readFile(this: *const NodeModulesFolder, root_node_modules_dir: std.fs.Dir, file_path: [:0]const u8, allocator: std.mem.Allocator) !bun.sys.File.ReadToEndResult { + const file = try this.openFile(root_node_modules_dir, file_path); + defer file.close(); + return file.readToEnd(allocator); + } + + pub fn readSmallFile(this: *const NodeModulesFolder, root_node_modules_dir: std.fs.Dir, file_path: [:0]const u8, allocator: std.mem.Allocator) !bun.sys.File.ReadToEndResult { + const file = try this.openFile(root_node_modules_dir, file_path); + defer file.close(); + return file.readToEndSmall(allocator); + } + + pub fn openFile(this: *const NodeModulesFolder, root_node_modules_dir: std.fs.Dir, file_path: [:0]const u8) !bun.sys.File { + if (this.path.items.len + file_path.len * 2 < bun.MAX_PATH_BYTES) { + // If we do not run the risk of ENAMETOOLONG, then let's just avoid opening the extra directories altogether. + switch (this.openFileWithoutOpeningDirectories(root_node_modules_dir, file_path)) { + .err => |e| { + switch (e.getErrno()) { + // Just incase we're wrong, let's try the fallback + .PERM, .ACCES, .INVAL, .NAMETOOLONG => { + // Use fallback + }, + else => return e.toZigErr(), + } + }, + .result => |file| return file, + } + } + + const dir = try this.openDir(root_node_modules_dir); + defer { + _ = bun.sys.close(bun.toFD(dir)); + } + + return try bun.sys.File.openat(bun.toFD(dir), file_path, bun.O.RDONLY, 0).unwrap(); + } + pub fn openDir(this: *const NodeModulesFolder, root: std.fs.Dir) !std.fs.Dir { if (comptime Environment.isPosix) { - return root.openDir(this.path.items, .{ .iterate = true, .access_sub_paths = true }); + return (try bun.sys.openat(bun.toFD(root), &try std.posix.toPosixPath(this.path.items), bun.O.DIRECTORY, 0).unwrap()).asDir(); } return (try bun.sys.openDirAtWindowsA(bun.toFD(root), this.path.items, .{ @@ -12110,7 +12433,7 @@ pub const PackageManager = struct { lockfile: *Lockfile, progress: *Progress, - // relative paths from `nextNodeModulesFolder` will be copied into this list. + // relative paths from `next` will be copied into this list. node_modules: NodeModulesFolder, skip_verify_installed_version_number: bool, @@ -12121,13 +12444,15 @@ pub const PackageManager = struct { options: *const PackageManager.Options, metas: []const Lockfile.Package.Meta, names: []const String, + pkg_dependencies: []const Lockfile.DependencySlice, + pkg_name_hashes: []const PackageNameHash, bins: []const Bin, resolutions: []Resolution, node: *Progress.Node, destination_dir_subpath_buf: bun.PathBuffer = undefined, folder_path_buf: bun.PathBuffer = undefined, successfully_installed: Bitset, - tree_iterator: *Lockfile.Tree.Iterator, + tree_iterator: *Lockfile.Tree.Iterator(.node_modules), command_ctx: Command.Context, current_tree_id: Lockfile.Tree.Id = Lockfile.Tree.invalid_id, @@ -12155,7 +12480,7 @@ pub const PackageManager = struct { pub fn incrementTreeInstallCount( this: *PackageInstaller, tree_id: Lockfile.Tree.Id, - maybe_destination_dir: ?std.fs.Dir, + maybe_destination_dir: ?*LazyPackageDestinationDir, comptime should_install_packages: bool, comptime log_level: Options.LogLevel, ) void { @@ -12182,22 +12507,19 @@ pub const PackageManager = struct { this.completed_trees.set(tree_id); - if (maybe_destination_dir orelse (this.node_modules.makeAndOpenDir(this.root_node_modules_folder) catch null)) |_destination_dir| { - var destination_dir = _destination_dir; - defer { - if (maybe_destination_dir == null) { - destination_dir.close(); + // Avoid opening this directory if we don't need to. + if (tree.binaries.count() > 0) { + // Don't close this directory in here. It will be closed by the caller. + if (maybe_destination_dir) |maybe| { + if (maybe.getDir() catch null) |destination_dir| { + this.seen_bin_links.clearRetainingCapacity(); + + var link_target_buf: bun.PathBuffer = undefined; + var link_dest_buf: bun.PathBuffer = undefined; + var link_rel_buf: bun.PathBuffer = undefined; + this.linkTreeBins(tree, tree_id, destination_dir, &link_target_buf, &link_dest_buf, &link_rel_buf, log_level); } } - - this.seen_bin_links.clearRetainingCapacity(); - - if (tree.binaries.count() > 0) { - var link_target_buf: bun.PathBuffer = undefined; - var link_dest_buf: bun.PathBuffer = undefined; - var link_rel_buf: bun.PathBuffer = undefined; - this.linkTreeBins(tree, tree_id, destination_dir, &link_target_buf, &link_dest_buf, &link_rel_buf, log_level); - } } if (comptime should_install_packages) { @@ -12278,7 +12600,7 @@ pub const PackageManager = struct { } pub fn linkRemainingBins(this: *PackageInstaller, comptime log_level: Options.LogLevel) void { - var depth_buf: Lockfile.Tree.Iterator.DepthBuf = undefined; + var depth_buf: Lockfile.Tree.DepthBuf = undefined; var node_modules_rel_path_buf: bun.PathBuffer = undefined; @memcpy(node_modules_rel_path_buf[0.."node_modules".len], "node_modules"); @@ -12296,6 +12618,7 @@ pub const PackageManager = struct { @intCast(tree_id), &node_modules_rel_path_buf, &depth_buf, + .node_modules, ); this.node_modules.path.appendSlice(rel_path) catch bun.outOfMemory(); @@ -12379,7 +12702,7 @@ pub const PackageManager = struct { // packages upon completing the current tree for (tree.pending_installs.items) |context| { const package_id = resolutions[context.dependency_id]; - const name = lockfile.str(&this.names[package_id]); + const name = this.names[package_id]; const resolution = &this.resolutions[package_id]; this.node_modules.tree_id = context.tree_id; this.node_modules.path = context.path; @@ -12409,10 +12732,6 @@ pub const PackageManager = struct { for (this.pending_lifecycle_scripts.items) |entry| { const package_name = entry.list.package_name; while (LifecycleScriptSubprocess.alive_count.load(.monotonic) >= this.manager.options.max_concurrent_lifecycle_scripts) { - if (PackageManager.verbose_install) { - if (PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) Output.prettyErrorln("[PackageManager] waiting for {d} scripts\n", .{LifecycleScriptSubprocess.alive_count.load(.monotonic)}); - } - this.manager.sleep(); } @@ -12444,9 +12763,7 @@ pub const PackageManager = struct { } while (this.manager.pending_lifecycle_script_tasks.load(.monotonic) > 0) { - if (PackageManager.verbose_install) { - if (PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) Output.prettyErrorln("[PackageManager] waiting for {d} scripts\n", .{LifecycleScriptSubprocess.alive_count.load(.monotonic)}); - } + this.manager.reportSlowLifecycleScripts(log_level); if (comptime log_level.showProgress()) { if (this.manager.scripts_node) |scripts_node| { @@ -12497,8 +12814,10 @@ pub const PackageManager = struct { var packages = this.lockfile.packages.slice(); this.metas = packages.items(.meta); this.names = packages.items(.name); + this.pkg_name_hashes = packages.items(.name_hash); this.bins = packages.items(.bin); this.resolutions = packages.items(.resolution); + this.pkg_dependencies = packages.items(.dependencies); // fixes an assertion failure where a transitive dependency is a git dependency newly added to the lockfile after the list of dependencies has been resized // this assertion failure would also only happen after the lockfile has been written to disk and the summary is being printed. @@ -12519,21 +12838,21 @@ pub const PackageManager = struct { comptime log_level: Options.LogLevel, ) void { const package_id = this.lockfile.buffers.resolutions.items[dependency_id]; - const name = this.lockfile.str(&this.names[package_id]); + const name = this.names[package_id]; const resolution = &this.resolutions[package_id]; const task_id = switch (resolution.tag) { .git => Task.Id.forGitCheckout(data.url, data.resolved), .github => Task.Id.forTarball(data.url), .local_tarball => Task.Id.forTarball(this.lockfile.str(&resolution.value.local_tarball)), .remote_tarball => Task.Id.forTarball(this.lockfile.str(&resolution.value.remote_tarball)), - .npm => Task.Id.forNPMPackage(name, resolution.value.npm.version), + .npm => Task.Id.forNPMPackage(name.slice(this.lockfile.buffers.string_bytes.items), resolution.value.npm.version), else => unreachable, }; if (!this.installEnqueuedPackagesImpl(name, task_id, log_level)) { if (comptime Environment.allow_assert) { Output.panic("Ran callback to install enqueued packages, but there was no task associated with it. {}:{} (dependency_id: {d})", .{ - bun.fmt.quote(name), + bun.fmt.quote(name.slice(this.lockfile.buffers.string_bytes.items)), bun.fmt.quote(data.url), dependency_id, }); @@ -12543,7 +12862,7 @@ pub const PackageManager = struct { pub fn installEnqueuedPackagesImpl( this: *PackageInstaller, - name: []const u8, + name: String, task_id: Task.Id.Type, comptime log_level: Options.LogLevel, ) bool { @@ -12594,7 +12913,7 @@ pub const PackageManager = struct { alias: string, package_id: PackageID, resolution_tag: Resolution.Tag, - node_modules_folder: std.fs.Dir, + node_modules_folder: *LazyPackageDestinationDir, comptime log_level: Options.LogLevel, ) usize { if (comptime Environment.allow_assert) { @@ -12640,7 +12959,7 @@ pub const PackageManager = struct { } switch (resolution_tag) { - .git, .github, .gitlab, .root => { + .git, .github, .root => { inline for (Lockfile.Scripts.names) |script_name| { count += @intFromBool(!@field(scripts, script_name).isEmpty()); } @@ -12681,7 +13000,7 @@ pub const PackageManager = struct { dependency_id: DependencyID, package_id: PackageID, comptime log_level: Options.LogLevel, - name: string, + pkg_name: String, resolution: *const Resolution, // false when coming from download. if the package was downloaded @@ -12692,30 +13011,34 @@ pub const PackageManager = struct { // pending packages if we're already draining them. comptime is_pending_package_install: bool, ) void { - const buf = this.lockfile.buffers.string_bytes.items; - - const alias = this.lockfile.buffers.dependencies.items[dependency_id].name.slice(buf); + const alias = this.lockfile.buffers.dependencies.items[dependency_id].name; const destination_dir_subpath: [:0]u8 = brk: { - bun.copy(u8, &this.destination_dir_subpath_buf, alias); - this.destination_dir_subpath_buf[alias.len] = 0; - break :brk this.destination_dir_subpath_buf[0..alias.len :0]; + const alias_slice = alias.slice(this.lockfile.buffers.string_bytes.items); + bun.copy(u8, &this.destination_dir_subpath_buf, alias_slice); + this.destination_dir_subpath_buf[alias_slice.len] = 0; + break :brk this.destination_dir_subpath_buf[0..alias_slice.len :0]; }; + const pkg_name_hash = this.pkg_name_hashes[package_id]; + var resolution_buf: [512]u8 = undefined; const package_version = if (resolution.tag == .workspace) brk: { - if (this.manager.lockfile.workspace_versions.get(String.Builder.stringHash(name))) |workspace_version| { - break :brk std.fmt.bufPrint(&resolution_buf, "{}", .{workspace_version.fmt(buf)}) catch unreachable; + if (this.manager.lockfile.workspace_versions.get(pkg_name_hash)) |workspace_version| { + break :brk std.fmt.bufPrint(&resolution_buf, "{}", .{workspace_version.fmt(this.lockfile.buffers.string_bytes.items)}) catch unreachable; } // no version break :brk ""; - } else std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(buf, .posix)}) catch unreachable; + } else std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(this.lockfile.buffers.string_bytes.items, .posix)}) catch unreachable; const patch_patch, const patch_contents_hash, const patch_name_and_version_hash, const remove_patch = brk: { if (this.manager.lockfile.patched_dependencies.entries.len == 0 and this.manager.patched_dependencies_to_remove.entries.len == 0) break :brk .{ null, null, null, false }; var sfa = std.heap.stackFallback(1024, this.lockfile.allocator); const alloc = sfa.get(); - const name_and_version = std.fmt.allocPrint(alloc, "{s}@{s}", .{ name, package_version }) catch unreachable; + const name_and_version = std.fmt.allocPrint(alloc, "{s}@{s}", .{ + pkg_name.slice(this.lockfile.buffers.string_bytes.items), + package_version, + }) catch unreachable; defer alloc.free(name_and_version); const name_and_version_hash = String.Builder.stringHash(name_and_version); @@ -12750,7 +13073,7 @@ pub const PackageManager = struct { .destination_dir_subpath = destination_dir_subpath, .destination_dir_subpath_buf = &this.destination_dir_subpath_buf, .allocator = this.lockfile.allocator, - .package_name = name, + .package_name = pkg_name, .patch = if (patch_patch) |p| PackageInstall.Patch{ .patch_contents_hash = patch_contents_hash.?, .patch_path = p, @@ -12760,12 +13083,19 @@ pub const PackageManager = struct { .node_modules = &this.node_modules, .lockfile = this.lockfile, }; - debug("Installing {s}@{s}", .{ name, resolution.fmt(buf, .posix) }); + debug("Installing {s}@{s}", .{ + pkg_name.slice(this.lockfile.buffers.string_bytes.items), + resolution.fmt(this.lockfile.buffers.string_bytes.items, .posix), + }); const pkg_has_patch = !installer.patch.isNull(); switch (resolution.tag) { .npm => { - installer.cache_dir_subpath = this.manager.cachedNPMPackageFolderName(name, resolution.value.npm.version, patch_contents_hash); + installer.cache_dir_subpath = this.manager.cachedNPMPackageFolderName( + pkg_name.slice(this.lockfile.buffers.string_bytes.items), + resolution.value.npm.version, + patch_contents_hash, + ); installer.cache_dir = this.manager.getCacheDirectory(); }, .git => { @@ -12777,7 +13107,7 @@ pub const PackageManager = struct { installer.cache_dir = this.manager.getCacheDirectory(); }, .folder => { - const folder = resolution.value.folder.slice(buf); + const folder = resolution.value.folder.slice(this.lockfile.buffers.string_bytes.items); if (this.lockfile.isWorkspaceTreeId(this.current_tree_id)) { // Handle when a package depends on itself via file: @@ -12810,7 +13140,7 @@ pub const PackageManager = struct { installer.cache_dir = this.manager.getCacheDirectory(); }, .workspace => { - const folder = resolution.value.workspace.slice(buf); + const folder = resolution.value.workspace.slice(this.lockfile.buffers.string_bytes.items); // Handle when a package depends on itself if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) { installer.cache_dir_subpath = "."; @@ -12829,7 +13159,7 @@ pub const PackageManager = struct { const directory = this.manager.globalLinkDir() catch |err| { if (comptime log_level != .silent) { const fmt = "\nerror: unable to access global directory while installing {s}: {s}\n"; - const args = .{ name, @errorName(err) }; + const args = .{ pkg_name.slice(this.lockfile.buffers.string_bytes.items), @errorName(err) }; if (comptime log_level.showProgress()) { switch (Output.enable_ansi_colors) { @@ -12853,7 +13183,7 @@ pub const PackageManager = struct { return; }; - const folder = resolution.value.symlink.slice(buf); + const folder = resolution.value.symlink.slice(this.lockfile.buffers.string_bytes.items); if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) { installer.cache_dir_subpath = "."; @@ -12887,7 +13217,6 @@ pub const PackageManager = struct { const needs_install = this.force_install or this.skip_verify_installed_version_number or !needs_verify or remove_patch or !installer.verify( resolution, - buf, this.root_node_modules_folder, ); this.summary.skipped += @intFromBool(!needs_install); @@ -12909,7 +13238,7 @@ pub const PackageManager = struct { .git => { this.manager.enqueueGitForCheckout( dependency_id, - alias, + alias.slice(this.lockfile.buffers.string_bytes.items), resolution, context, patch_name_and_version_hash, @@ -12924,12 +13253,19 @@ pub const PackageManager = struct { url, context, patch_name_and_version_hash, - ); + ) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + error.InvalidURL => this.failWithInvalidUrl( + pkg_has_patch, + is_pending_package_install, + log_level, + ), + }; }, .local_tarball => { this.manager.enqueueTarballForReading( dependency_id, - alias, + alias.slice(this.lockfile.buffers.string_bytes.items), resolution, context, ); @@ -12938,29 +13274,46 @@ pub const PackageManager = struct { this.manager.enqueueTarballForDownload( dependency_id, package_id, - resolution.value.remote_tarball.slice(buf), + resolution.value.remote_tarball.slice(this.lockfile.buffers.string_bytes.items), context, patch_name_and_version_hash, - ); + ) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + error.InvalidURL => this.failWithInvalidUrl( + pkg_has_patch, + is_pending_package_install, + log_level, + ), + }; }, .npm => { if (comptime Environment.isDebug) { // Very old versions of Bun didn't store the tarball url when it didn't seem necessary // This caused bugs. We can't assert on it because they could come from old lockfiles if (resolution.value.npm.url.isEmpty()) { - Output.debugWarn("package {s}@{} missing tarball_url", .{ name, resolution.fmt(buf, .posix) }); + Output.debugWarn("package {s}@{} missing tarball_url", .{ + pkg_name.slice(this.lockfile.buffers.string_bytes.items), + resolution.fmt(this.lockfile.buffers.string_bytes.items, .posix), + }); } } this.manager.enqueuePackageForDownload( - name, + pkg_name.slice(this.lockfile.buffers.string_bytes.items), dependency_id, package_id, resolution.value.npm.version, - resolution.value.npm.url.slice(buf), + resolution.value.npm.url.slice(this.lockfile.buffers.string_bytes.items), context, patch_name_and_version_hash, - ); + ) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + error.InvalidURL => this.failWithInvalidUrl( + pkg_has_patch, + is_pending_package_install, + log_level, + ), + }; }, else => { if (comptime Environment.allow_assert) { @@ -13007,7 +13360,7 @@ pub const PackageManager = struct { var destination_dir = this.node_modules.makeAndOpenDir(this.root_node_modules_folder) catch |err| { if (log_level != .silent) { Output.err(err, "Failed to open node_modules folder for {s} in {s}", .{ - name, + pkg_name.slice(this.lockfile.buffers.string_bytes.items), bun.fmt.fmtPath(u8, this.node_modules.path.items, .{}), }); } @@ -13020,6 +13373,8 @@ pub const PackageManager = struct { if (std.fs.cwd().fd != destination_dir.fd) destination_dir.close(); } + var lazy_package_dir: LazyPackageDestinationDir = .{ .dir = destination_dir }; + const install_result = switch (resolution.tag) { .symlink, .workspace => installer.installFromLink(this.skip_delete, destination_dir), else => result: { @@ -13061,18 +13416,18 @@ pub const PackageManager = struct { } const dep = this.lockfile.buffers.dependencies.items[dependency_id]; - const name_hash: TruncatedPackageNameHash = @truncate(dep.name_hash); + const truncated_dep_name_hash: TruncatedPackageNameHash = @truncate(dep.name_hash); const is_trusted, const is_trusted_through_update_request = brk: { - if (this.trusted_dependencies_from_update_requests.contains(name_hash)) break :brk .{ true, true }; - if (this.lockfile.hasTrustedDependency(alias)) break :brk .{ true, false }; + if (this.trusted_dependencies_from_update_requests.contains(truncated_dep_name_hash)) break :brk .{ true, true }; + if (this.lockfile.hasTrustedDependency(alias.slice(this.lockfile.buffers.string_bytes.items))) break :brk .{ true, false }; break :brk .{ false, false }; }; if (resolution.tag != .root and (resolution.tag == .workspace or is_trusted)) { if (this.enqueueLifecycleScripts( - alias, + alias.slice(this.lockfile.buffers.string_bytes.items), log_level, - destination_dir, + &lazy_package_dir, package_id, dep.behavior.optional, resolution, @@ -13080,11 +13435,11 @@ pub const PackageManager = struct { if (is_trusted_through_update_request) { this.manager.trusted_deps_to_add_to_package_json.append( this.manager.allocator, - this.manager.allocator.dupe(u8, alias) catch bun.outOfMemory(), + this.manager.allocator.dupe(u8, alias.slice(this.lockfile.buffers.string_bytes.items)) catch bun.outOfMemory(), ) catch bun.outOfMemory(); if (this.lockfile.trusted_dependencies == null) this.lockfile.trusted_dependencies = .{}; - this.lockfile.trusted_dependencies.?.put(this.manager.allocator, name_hash, {}) catch bun.outOfMemory(); + this.lockfile.trusted_dependencies.?.put(this.manager.allocator, truncated_dep_name_hash, {}) catch bun.outOfMemory(); } } } @@ -13096,23 +13451,29 @@ pub const PackageManager = struct { else => if (!is_trusted and this.metas[package_id].hasInstallScript()) { // Check if the package actually has scripts. `hasInstallScript` can be false positive if a package is published with // an auto binding.gyp rebuild script but binding.gyp is excluded from the published files. - const count = this.getInstalledPackageScriptsCount(alias, package_id, resolution.tag, destination_dir, log_level); + const count = this.getInstalledPackageScriptsCount( + alias.slice(this.lockfile.buffers.string_bytes.items), + package_id, + resolution.tag, + &lazy_package_dir, + log_level, + ); if (count > 0) { if (comptime log_level.isVerbose()) { Output.prettyError("Blocked {d} scripts for: {s}@{}\n", .{ count, - alias, + alias.slice(this.lockfile.buffers.string_bytes.items), resolution.fmt(this.lockfile.buffers.string_bytes.items, .posix), }); } - const entry = this.summary.packages_with_blocked_scripts.getOrPut(this.manager.allocator, name_hash) catch bun.outOfMemory(); + const entry = this.summary.packages_with_blocked_scripts.getOrPut(this.manager.allocator, truncated_dep_name_hash) catch bun.outOfMemory(); if (!entry.found_existing) entry.value_ptr.* = 0; entry.value_ptr.* += count; } }, } - if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, destination_dir, !is_pending_package_install, log_level); + if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, &lazy_package_dir, !is_pending_package_install, log_level); }, .fail => |cause| { if (comptime Environment.allow_assert) { @@ -13121,12 +13482,12 @@ pub const PackageManager = struct { // even if the package failed to install, we still need to increment the install // counter for this tree - if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, destination_dir, !is_pending_package_install, log_level); + if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, &lazy_package_dir, !is_pending_package_install, log_level); if (cause.err == error.DanglingSymlink) { Output.prettyErrorln( "error: {s} \"link:{s}\" not found (try running 'bun link' in the intended package's folder)", - .{ @errorName(cause.err), this.names[package_id].slice(buf) }, + .{ @errorName(cause.err), this.names[package_id].slice(this.lockfile.buffers.string_bytes.items) }, ); this.summary.fail += 1; } else if (cause.err == error.AccessDenied) { @@ -13140,9 +13501,17 @@ pub const PackageManager = struct { }; if (!Singleton.node_modules_is_ok) { if (!Environment.isWindows) { - const stat = bun.sys.fstat(bun.toFD(destination_dir)).unwrap() catch |err| { + const stat = bun.sys.fstat(bun.toFD(lazy_package_dir.getDir() catch |err| { Output.err("EACCES", "Permission denied while installing {s}", .{ - this.names[package_id].slice(buf), + this.names[package_id].slice(this.lockfile.buffers.string_bytes.items), + }); + if (Environment.isDebug) { + Output.err(err, "Failed to stat node_modules", .{}); + } + Global.exit(1); + })).unwrap() catch |err| { + Output.err("EACCES", "Permission denied while installing {s}", .{ + this.names[package_id].slice(this.lockfile.buffers.string_bytes.items), }); if (Environment.isDebug) { Output.err(err, "Failed to stat node_modules", .{}); @@ -13166,14 +13535,14 @@ pub const PackageManager = struct { } Output.err("EACCES", "Permission denied while installing {s}", .{ - this.names[package_id].slice(buf), + this.names[package_id].slice(this.lockfile.buffers.string_bytes.items), }); this.summary.fail += 1; } else { Output.prettyErrorln( "error: {s} installing {s} ({s})", - .{ @errorName(cause.err), this.names[package_id].slice(buf), install_result.fail.step.name() }, + .{ @errorName(cause.err), this.names[package_id].slice(this.lockfile.buffers.string_bytes.items), install_result.fail.step.name() }, ); this.summary.fail += 1; } @@ -13184,31 +13553,26 @@ pub const PackageManager = struct { this.trees[this.current_tree_id].binaries.add(dependency_id) catch bun.outOfMemory(); } - var destination_dir = this.node_modules.makeAndOpenDir(this.root_node_modules_folder) catch |err| { - if (log_level != .silent) { - Output.err(err, "Failed to open node_modules folder for {s} in {s}", .{ - name, - bun.fmt.fmtPath(u8, this.node_modules.path.items, .{}), - }); - } - this.summary.fail += 1; - if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); - return; + var destination_dir: LazyPackageDestinationDir = .{ + .node_modules_path = .{ + .node_modules = &this.node_modules, + .root_node_modules_dir = this.root_node_modules_folder, + }, }; defer { - if (std.fs.cwd().fd != destination_dir.fd) destination_dir.close(); + destination_dir.close(); } - defer if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, destination_dir, !is_pending_package_install, log_level); + defer if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, &destination_dir, !is_pending_package_install, log_level); const dep = this.lockfile.buffers.dependencies.items[dependency_id]; - const name_hash: TruncatedPackageNameHash = @truncate(dep.name_hash); + const truncated_dep_name_hash: TruncatedPackageNameHash = @truncate(dep.name_hash); const is_trusted, const is_trusted_through_update_request, const add_to_lockfile = brk: { // trusted through a --trust dependency. need to enqueue scripts, write to package.json, and add to lockfile - if (this.trusted_dependencies_from_update_requests.contains(name_hash)) break :brk .{ true, true, true }; + if (this.trusted_dependencies_from_update_requests.contains(truncated_dep_name_hash)) break :brk .{ true, true, true }; - if (this.manager.summary.added_trusted_dependencies.get(name_hash)) |should_add_to_lockfile| { + if (this.manager.summary.added_trusted_dependencies.get(truncated_dep_name_hash)) |should_add_to_lockfile| { // is a new trusted dependency. need to enqueue scripts and maybe add to lockfile break :brk .{ true, false, should_add_to_lockfile }; } @@ -13217,9 +13581,9 @@ pub const PackageManager = struct { if (resolution.tag != .root and is_trusted) { if (this.enqueueLifecycleScripts( - alias, + alias.slice(this.lockfile.buffers.string_bytes.items), log_level, - destination_dir, + &destination_dir, package_id, dep.behavior.optional, resolution, @@ -13227,25 +13591,35 @@ pub const PackageManager = struct { if (is_trusted_through_update_request) { this.manager.trusted_deps_to_add_to_package_json.append( this.manager.allocator, - this.manager.allocator.dupe(u8, alias) catch bun.outOfMemory(), + this.manager.allocator.dupe(u8, alias.slice(this.lockfile.buffers.string_bytes.items)) catch bun.outOfMemory(), ) catch bun.outOfMemory(); } if (add_to_lockfile) { if (this.lockfile.trusted_dependencies == null) this.lockfile.trusted_dependencies = .{}; - this.lockfile.trusted_dependencies.?.put(this.manager.allocator, name_hash, {}) catch bun.outOfMemory(); + this.lockfile.trusted_dependencies.?.put(this.manager.allocator, truncated_dep_name_hash, {}) catch bun.outOfMemory(); } } } } } + fn failWithInvalidUrl( + this: *PackageInstaller, + pkg_has_patch: bool, + comptime is_pending_package_install: bool, + comptime log_level: Options.LogLevel, + ) void { + this.summary.fail += 1; + if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); + } + // returns true if scripts are enqueued fn enqueueLifecycleScripts( this: *PackageInstaller, folder_name: string, comptime log_level: Options.LogLevel, - node_modules_folder: std.fs.Dir, + node_modules_folder: *LazyPackageDestinationDir, package_id: PackageID, optional: bool, resolution: *const Resolution, @@ -13310,47 +13684,18 @@ pub const PackageManager = struct { pub fn installPackage( this: *PackageInstaller, - dependency_id: DependencyID, + dep_id: DependencyID, comptime log_level: Options.LogLevel, ) void { - this.installPackageImpl(dependency_id, log_level, true); - } + const package_id = this.lockfile.buffers.resolutions.items[dep_id]; - pub fn installPackageImpl( - this: *PackageInstaller, - dependency_id: DependencyID, - comptime log_level: Options.LogLevel, - comptime increment_tree_count: bool, - ) void { - const package_id = this.lockfile.buffers.resolutions.items[dependency_id]; - const meta = &this.metas[package_id]; - const is_pending_package_install = false; - - if (meta.isDisabled()) { - if (comptime log_level.showProgress()) { - this.node.completeOne(); - } - if (comptime log_level.isVerbose()) { - const name = this.lockfile.str(&this.names[package_id]); - if (!meta.os.isMatch() and !meta.arch.isMatch()) { - Output.prettyErrorln("Skip installing '{s}' cpu & os mismatch", .{name}); - } else if (!meta.os.isMatch()) { - Output.prettyErrorln("Skip installing '{s}' os mismatch", .{name}); - } else if (!meta.arch.isMatch()) { - Output.prettyErrorln("Skip installing '{s}' cpu mismatch", .{name}); - } - } - - if (comptime increment_tree_count) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); - return; - } - - const name = this.lockfile.str(&this.names[package_id]); + const name = this.names[package_id]; const resolution = &this.resolutions[package_id]; const needs_verify = true; + const is_pending_package_install = false; this.installPackageWithNameAndResolution( - dependency_id, + dep_id, package_id, log_level, name, @@ -13401,10 +13746,20 @@ pub const PackageManager = struct { if (clone_queue.found_existing) return; - this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone(clone_id, alias, repository, &this.lockfile.buffers.dependencies.items[dependency_id], null))); + this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone( + clone_id, + alias, + repository, + dependency_id, + &this.lockfile.buffers.dependencies.items[dependency_id], + resolution, + null, + ))); } } + const EnqueuePackageForDownloadError = NetworkTask.ForTarballError; + pub fn enqueuePackageForDownload( this: *PackageManager, name: []const u8, @@ -13414,23 +13769,23 @@ pub const PackageManager = struct { url: []const u8, task_context: TaskCallbackContext, patch_name_and_version_hash: ?u64, - ) void { + ) EnqueuePackageForDownloadError!void { const task_id = Task.Id.forNPMPackage(name, version); - var task_queue = this.task_queue.getOrPut(this.allocator, task_id) catch unreachable; + var task_queue = try this.task_queue.getOrPut(this.allocator, task_id); if (!task_queue.found_existing) { task_queue.value_ptr.* = .{}; } - task_queue.value_ptr.append( + try task_queue.value_ptr.append( this.allocator, task_context, - ) catch unreachable; + ); if (task_queue.found_existing) return; const is_required = this.lockfile.buffers.dependencies.items[dependency_id].behavior.isRequired(); - if (this.generateNetworkTaskForTarball( + if (try this.generateNetworkTaskForTarball( task_id, url, is_required, @@ -13438,7 +13793,7 @@ pub const PackageManager = struct { this.lockfile.packages.get(package_id), patch_name_and_version_hash, .allow_authorization, - ) catch unreachable) |task| { + )) |task| { task.schedule(&this.network_tarball_batch); if (this.network_tarball_batch.len > 0) { _ = this.scheduleTasks(); @@ -13446,6 +13801,8 @@ pub const PackageManager = struct { } } + const EnqueueTarballForDownloadError = NetworkTask.ForTarballError; + pub fn enqueueTarballForDownload( this: *PackageManager, dependency_id: DependencyID, @@ -13453,21 +13810,21 @@ pub const PackageManager = struct { url: string, task_context: TaskCallbackContext, patch_name_and_version_hash: ?u64, - ) void { + ) EnqueueTarballForDownloadError!void { const task_id = Task.Id.forTarball(url); - var task_queue = this.task_queue.getOrPut(this.allocator, task_id) catch unreachable; + var task_queue = try this.task_queue.getOrPut(this.allocator, task_id); if (!task_queue.found_existing) { task_queue.value_ptr.* = .{}; } - task_queue.value_ptr.append( + try task_queue.value_ptr.append( this.allocator, task_context, - ) catch unreachable; + ); if (task_queue.found_existing) return; - if (this.generateNetworkTaskForTarball( + if (try this.generateNetworkTaskForTarball( task_id, url, this.lockfile.buffers.dependencies.items[dependency_id].behavior.isRequired(), @@ -13475,7 +13832,7 @@ pub const PackageManager = struct { this.lockfile.packages.get(package_id), patch_name_and_version_hash, .no_authorization, - ) catch unreachable) |task| { + )) |task| { task.schedule(&this.network_tarball_batch); if (this.network_tarball_batch.len > 0) { _ = this.scheduleTasks(); @@ -13537,17 +13894,18 @@ pub const PackageManager = struct { pub fn installPackages( this: *PackageManager, ctx: Command.Context, + workspace_filters: []const WorkspaceFilter, + install_root_dependencies: bool, comptime log_level: PackageManager.Options.LogLevel, ) !PackageInstall.Summary { - const original_lockfile = this.lockfile; - defer this.lockfile = original_lockfile; - if (!this.options.local_package_features.dev_dependencies) { - this.lockfile = try this.lockfile.maybeCloneFilteringRootPackages( - this, - this.options.local_package_features, - this.options.enable.exact_versions, - log_level, - ); + const original_trees = this.lockfile.buffers.trees; + const original_tree_dep_ids = this.lockfile.buffers.hoisted_dependencies; + + try this.lockfile.filter(this.log, this, install_root_dependencies, workspace_filters); + + defer { + this.lockfile.buffers.trees = original_trees; + this.lockfile.buffers.hoisted_dependencies = original_tree_dep_ids; } var root_node: *Progress.Node = undefined; @@ -13562,7 +13920,7 @@ pub const PackageManager = struct { root_node = progress.start("", 0); download_node = root_node.start(ProgressStrings.download(), 0); - install_node = root_node.start(ProgressStrings.install(), this.lockfile.packages.len); + install_node = root_node.start(ProgressStrings.install(), this.lockfile.buffers.hoisted_dependencies.items.len); scripts_node = root_node.start(ProgressStrings.script(), 0); this.downloads_node = &download_node; this.scripts_node = &scripts_node; @@ -13613,12 +13971,10 @@ pub const PackageManager = struct { skip_delete = false; } - var summary = PackageInstall.Summary{ - .lockfile_used_for_install = this.lockfile, - }; + var summary = PackageInstall.Summary{}; { - var iterator = Lockfile.Tree.Iterator.init(this.lockfile); + var iterator = Lockfile.Tree.Iterator(.node_modules).init(this.lockfile); if (comptime Environment.isPosix) { Bin.Linker.ensureUmask(); } @@ -13711,7 +14067,9 @@ pub const PackageManager = struct { .bins = parts.items(.bin), .root_node_modules_folder = node_modules_folder, .names = parts.items(.name), + .pkg_name_hashes = parts.items(.name_hash), .resolutions = parts.items(.resolution), + .pkg_dependencies = parts.items(.dependencies), .lockfile = this.lockfile, .node = &install_node, .node_modules = .{ @@ -13758,7 +14116,7 @@ pub const PackageManager = struct { defer installer.deinit(); - while (iterator.nextNodeModulesFolder(&installer.completed_trees)) |node_modules| { + while (iterator.next(&installer.completed_trees)) |node_modules| { installer.node_modules.path.items.len = strings.withoutTrailingSlash(FileSystem.instance.top_level_dir).len + 1; try installer.node_modules.path.appendSlice(node_modules.relative_path); installer.node_modules.tree_id = node_modules.tree_id; @@ -13799,8 +14157,8 @@ pub const PackageManager = struct { ); if (!installer.options.do.install_packages) return error.InstallFailed; } - this.tickLifecycleScripts(); + this.reportSlowLifecycleScripts(log_level); } for (remaining) |dependency_id| { @@ -13823,6 +14181,7 @@ pub const PackageManager = struct { if (!installer.options.do.install_packages) return error.InstallFailed; this.tickLifecycleScripts(); + this.reportSlowLifecycleScripts(log_level); } while (this.pendingTaskCount() > 0 and installer.options.do.install_packages) { @@ -13852,6 +14211,8 @@ pub const PackageManager = struct { return true; } + closure.manager.reportSlowLifecycleScripts(log_level); + if (PackageManager.verbose_install and closure.manager.pendingTaskCount() > 0) { const pending_task_count = closure.manager.pendingTaskCount(); if (pending_task_count > 0 and PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) { @@ -13878,6 +14239,7 @@ pub const PackageManager = struct { } } else { this.tickLifecycleScripts(); + this.reportSlowLifecycleScripts(log_level); } for (installer.trees) |tree| { @@ -13903,9 +14265,7 @@ pub const PackageManager = struct { installer.completeRemainingScripts(log_level); while (this.pending_lifecycle_script_tasks.load(.monotonic) > 0) { - if (PackageManager.verbose_install) { - if (PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) Output.prettyErrorln("[PackageManager] waiting for {d} scripts\n", .{this.pending_lifecycle_script_tasks.load(.monotonic)}); - } + this.reportSlowLifecycleScripts(log_level); this.sleep(); } @@ -13972,7 +14332,7 @@ pub const PackageManager = struct { const buf = this.lockfile.buffers.string_bytes.items; // need to clone because this is a copy before Lockfile.cleanWithLogger - const name = this.allocator.dupe(u8, root_package.name.slice(buf)) catch bun.outOfMemory(); + const name = root_package.name.slice(buf); const top_level_dir_without_trailing_slash = strings.withoutTrailingSlash(FileSystem.instance.top_level_dir); if (root_package.scripts.hasAny()) { @@ -14005,6 +14365,7 @@ pub const PackageManager = struct { manager: *PackageManager, ctx: Command.Context, root_package_json_contents: string, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { @@ -14017,24 +14378,24 @@ pub const PackageManager = struct { bun.dns.internal.prefetch(manager.event_loop.loop(), hostname); } - var load_lockfile_result: Lockfile.LoadFromDiskResult = if (manager.options.do.load_lockfile) - manager.lockfile.loadFromDisk( + var load_result: Lockfile.LoadResult = if (manager.options.do.load_lockfile) + manager.lockfile.loadFromCwd( manager, manager.allocator, manager.log, - manager.options.lockfile_path, true, ) else .{ .not_found = {} }; - try manager.updateLockfileIfNeeded(load_lockfile_result); + try manager.updateLockfileIfNeeded(load_result); var root = Lockfile.Package{}; - var needs_new_lockfile = load_lockfile_result != .ok or - (load_lockfile_result.ok.lockfile.buffers.dependencies.items.len == 0 and manager.update_requests.len > 0); + var needs_new_lockfile = load_result != .ok or + (load_result.ok.lockfile.buffers.dependencies.items.len == 0 and manager.update_requests.len > 0); - manager.options.enable.force_save_lockfile = manager.options.enable.force_save_lockfile or (load_lockfile_result == .ok and load_lockfile_result.ok.was_migrated); + manager.options.enable.force_save_lockfile = manager.options.enable.force_save_lockfile or + (load_result == .ok and (load_result.ok.was_migrated or (load_result.ok.format == .binary and manager.options.save_text_lockfile))); // this defaults to false // but we force allowing updates to the lockfile when you do bun add @@ -14044,32 +14405,32 @@ pub const PackageManager = struct { // Step 2. Parse the package.json file const root_package_json_source = logger.Source.initPathString(package_json_cwd, root_package_json_contents); - switch (load_lockfile_result) { + switch (load_result) { .err => |cause| { if (log_level != .silent) { switch (cause.step) { - .open_file => Output.prettyError("error opening lockfile: {s}\n", .{ - @errorName(cause.value), + .open_file => Output.err(cause.value, "failed to open lockfile: '{s}'", .{ + cause.lockfile_path, }), - .parse_file => Output.prettyError("error parsing lockfile: {s}\n", .{ - @errorName(cause.value), + .parse_file => Output.err(cause.value, "failed to parse lockfile: '{s}'", .{ + cause.lockfile_path, }), - .read_file => Output.prettyError("error reading lockfile: {s}\n", .{ - @errorName(cause.value), + .read_file => Output.err(cause.value, "failed to read lockfile: '{s}'", .{ + cause.lockfile_path, }), - .migrating => Output.prettyError("error migrating lockfile: {s}\n", .{ - @errorName(cause.value), + .migrating => Output.err(cause.value, "failed to migrate lockfile: '{s}'", .{ + cause.lockfile_path, }), } - if (manager.options.enable.fail_early) { - Output.prettyError("failed to load lockfile\n", .{}); - } else { - Output.prettyError("ignoring lockfile\n", .{}); + if (!manager.options.enable.fail_early) { + Output.printErrorln("", .{}); + Output.warn("Ignoring lockfile", .{}); } if (ctx.log.errors > 0) { try manager.log.print(Output.errorWriter()); + manager.log.reset(); } Output.flush(); } @@ -14116,7 +14477,7 @@ pub const PackageManager = struct { } } differ: { - root = load_lockfile_result.ok.lockfile.rootPackage() orelse { + root = load_result.ok.lockfile.rootPackage() orelse { needs_new_lockfile = true; break :differ; }; @@ -14131,6 +14492,7 @@ pub const PackageManager = struct { lockfile.initEmpty(manager.allocator); var maybe_root = Lockfile.Package{}; + var resolver: void = {}; try maybe_root.parse( &lockfile, manager, @@ -14138,7 +14500,7 @@ pub const PackageManager = struct { manager.log, root_package_json_source, void, - {}, + &resolver, Features.main, ); const mapping = try manager.lockfile.allocator.alloc(PackageID, maybe_root.dependencies.len); @@ -14357,13 +14719,14 @@ pub const PackageManager = struct { root = .{}; manager.lockfile.initEmpty(manager.allocator); - if (manager.options.enable.frozen_lockfile and load_lockfile_result != .not_found) { + if (manager.options.enable.frozen_lockfile and load_result != .not_found) { if (comptime log_level != .silent) { Output.prettyErrorln("error: lockfile had changes, but lockfile is frozen", .{}); } Global.crash(); } + var resolver: void = {}; try root.parse( manager.lockfile, manager, @@ -14371,7 +14734,7 @@ pub const PackageManager = struct { manager.log, root_package_json_source, void, - {}, + &resolver, Features.main, ); @@ -14488,9 +14851,7 @@ pub const PackageManager = struct { try waitForEverythingExceptPeers(manager); } - if (manager.options.do.install_peer_dependencies) { - try waitForPeers(manager); - } + try waitForPeers(manager); if (comptime log_level.showProgress()) { manager.endProgressBar(); @@ -14505,6 +14866,8 @@ pub const PackageManager = struct { manager.log.reset(); // This operation doesn't perform any I/O, so it should be relatively cheap. + const lockfile_before_clean = manager.lockfile; + manager.lockfile = try manager.lockfile.cleanWithLogger( manager, manager.update_requests, @@ -14597,22 +14960,119 @@ pub const PackageManager = struct { const packages_len_before_install = manager.lockfile.packages.len; - if (manager.options.enable.frozen_lockfile and load_lockfile_result != .not_found) { - if (manager.lockfile.hasMetaHashChanged(PackageManager.verbose_install or manager.options.do.print_meta_hash_string, packages_len_before_install) catch false) { - if (comptime log_level != .silent) { - Output.prettyErrorln("error: lockfile had changes, but lockfile is frozen", .{}); - Output.note("try re-running without --frozen-lockfile and commit the updated lockfile", .{}); + if (manager.options.enable.frozen_lockfile and load_result != .not_found) frozen_lockfile: { + if (load_result.loadedFromTextLockfile()) { + if (manager.lockfile.eql(lockfile_before_clean, packages_len_before_install, manager.allocator) catch bun.outOfMemory()) { + break :frozen_lockfile; + } + } else { + if (!(manager.lockfile.hasMetaHashChanged(PackageManager.verbose_install or manager.options.do.print_meta_hash_string, packages_len_before_install) catch false)) { + break :frozen_lockfile; + } + } + + if (comptime log_level != .silent) { + Output.prettyErrorln("error: lockfile had changes, but lockfile is frozen", .{}); + Output.note("try re-running without --frozen-lockfile and commit the updated lockfile", .{}); + } + Global.crash(); + } + + const lockfile_before_install = manager.lockfile; + + const save_format: Lockfile.LoadResult.LockfileFormat = if (manager.options.save_text_lockfile) + .text + else switch (load_result) { + .not_found => .binary, + .err => |err| err.format, + .ok => |ok| ok.format, + }; + + if (manager.options.lockfile_only) { + // save the lockfile and exit. make sure metahash is generated for binary lockfile + + manager.lockfile.meta_hash = try manager.lockfile.generateMetaHash( + PackageManager.verbose_install or manager.options.do.print_meta_hash_string, + packages_len_before_install, + ); + + try manager.saveLockfile(&load_result, save_format, had_any_diffs, lockfile_before_install, packages_len_before_install, log_level); + + if (manager.options.do.summary) { + // TODO(dylan-conway): packages aren't installed but we can still print + // added/removed/updated direct dependencies. + Output.pretty( + \\ + \\Saved {s} ({d} package{s}) + , .{ + switch (save_format) { + .text => "bun.lock", + .binary => "bun.lockb", + }, + manager.lockfile.packages.len, + if (manager.lockfile.packages.len == 1) "" else "s", + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.pretty("\n", .{}); + } + Output.flush(); + return; + } + + var path_buf: bun.PathBuffer = undefined; + var workspace_filters: std.ArrayListUnmanaged(WorkspaceFilter) = .{}; + // only populated when subcommand is `.install` + if (manager.subcommand == .install and manager.options.filter_patterns.len > 0) { + try workspace_filters.ensureUnusedCapacity(manager.allocator, manager.options.filter_patterns.len); + for (manager.options.filter_patterns) |pattern| { + try workspace_filters.append(manager.allocator, try WorkspaceFilter.init(manager.allocator, pattern, original_cwd, &path_buf)); + } + } + defer workspace_filters.deinit(manager.allocator); + + var install_root_dependencies = workspace_filters.items.len == 0; + if (!install_root_dependencies) { + const pkg_names = manager.lockfile.packages.items(.name); + + const abs_root_path = abs_root_path: { + if (comptime !Environment.isWindows) { + break :abs_root_path strings.withoutTrailingSlash(FileSystem.instance.top_level_dir); + } + + var abs_path = Path.pathToPosixBuf(u8, FileSystem.instance.top_level_dir, &path_buf); + break :abs_root_path strings.withoutTrailingSlash(abs_path[Path.windowsVolumeNameLen(abs_path)[0]..]); + }; + + for (workspace_filters.items) |filter| { + const pattern, const path_or_name = switch (filter) { + .name => |pattern| .{ pattern, pkg_names[0].slice(manager.lockfile.buffers.string_bytes.items) }, + .path => |pattern| .{ pattern, abs_root_path }, + .all => { + install_root_dependencies = true; + continue; + }, + }; + + switch (bun.glob.walk.matchImpl(pattern, path_or_name)) { + .match, .negate_match => install_root_dependencies = true, + + .negate_no_match => { + // always skip if a pattern specifically says "!" + install_root_dependencies = false; + break; + }, + + .no_match => {}, } - Global.crash(); } } - var install_summary = PackageInstall.Summary{ - .lockfile_used_for_install = manager.lockfile, - }; + var install_summary = PackageInstall.Summary{}; if (manager.options.do.install_packages) { install_summary = try manager.installPackages( ctx, + workspace_filters.items, + install_root_dependencies, log_level, ); } @@ -14625,10 +15085,13 @@ pub const PackageManager = struct { const did_meta_hash_change = // If the lockfile was frozen, we already checked it !manager.options.enable.frozen_lockfile and + if (load_result.loadedFromTextLockfile()) + !try manager.lockfile.eql(lockfile_before_clean, packages_len_before_install, manager.allocator) + else try manager.lockfile.hasMetaHashChanged( - PackageManager.verbose_install or manager.options.do.print_meta_hash_string, - @min(packages_len_before_install, manager.lockfile.packages.len), - ); + PackageManager.verbose_install or manager.options.do.print_meta_hash_string, + @min(packages_len_before_install, manager.lockfile.packages.len), + ); const should_save_lockfile = did_meta_hash_change or had_any_diffs or @@ -14636,64 +15099,13 @@ pub const PackageManager = struct { // this will handle new trusted dependencies added through --trust manager.update_requests.len > 0 or - (load_lockfile_result == .ok and load_lockfile_result.ok.serializer_result.packages_need_update); + (load_result == .ok and load_result.ok.serializer_result.packages_need_update); // It's unnecessary work to re-save the lockfile if there are no changes if (manager.options.do.save_lockfile and (should_save_lockfile or manager.lockfile.isEmpty() or manager.options.enable.force_save_lockfile)) - save: { - if (manager.lockfile.isEmpty()) { - if (!manager.options.dry_run) { - std.fs.cwd().deleteFileZ(manager.options.lockfile_path) catch |err| brk: { - // we don't care - if (err == error.FileNotFound) { - if (had_any_diffs) break :save; - break :brk; - } - - if (log_level != .silent) Output.prettyErrorln("\nerror: {s} deleting empty lockfile", .{@errorName(err)}); - break :save; - }; - } - if (!manager.options.global) { - if (log_level != .silent) { - switch (manager.subcommand) { - .remove => Output.prettyErrorln("\npackage.json has no dependencies! Deleted empty lockfile", .{}), - else => Output.prettyErrorln("No packages! Deleted empty lockfile", .{}), - } - } - } - - break :save; - } - - var save_node: *Progress.Node = undefined; - - if (comptime log_level.showProgress()) { - manager.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; - save_node = manager.progress.start(ProgressStrings.save(), 0); - save_node.activate(); - - manager.progress.refresh(); - } - - manager.lockfile.saveToDisk(manager.options.lockfile_path, manager.options.log_level.isVerbose()); - - if (comptime Environment.allow_assert) { - if (manager.lockfile.hasMetaHashChanged(false, packages_len_before_install) catch false) { - Output.panic("Lockfile metahash non-deterministic after saving", .{}); - } - } - - if (comptime log_level.showProgress()) { - save_node.end(); - manager.progress.refresh(); - manager.progress.root.end(); - manager.progress = .{}; - } else if (comptime log_level != .silent) { - Output.prettyErrorln("Saved lockfile", .{}); - Output.flush(); - } + { + try manager.saveLockfile(&load_result, save_format, had_any_diffs, lockfile_before_install, packages_len_before_install, log_level); } if (needs_new_lockfile) { @@ -14720,7 +15132,7 @@ pub const PackageManager = struct { } } - if (manager.options.do.run_scripts) { + if (manager.options.do.run_scripts and install_root_dependencies) { if (manager.root_lifecycle_scripts) |scripts| { if (comptime Environment.allow_assert) { bun.assert(scripts.total > 0); @@ -14737,103 +15149,15 @@ pub const PackageManager = struct { try manager.spawnPackageLifecycleScripts(ctx, scripts, optional, log_level, output_in_foreground); while (manager.pending_lifecycle_script_tasks.load(.monotonic) > 0) { - if (PackageManager.verbose_install) { - if (PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) Output.prettyErrorln("[PackageManager] waiting for {d} scripts\n", .{manager.pending_lifecycle_script_tasks.load(.monotonic)}); - } + manager.reportSlowLifecycleScripts(log_level); manager.sleep(); } } } - var printed_timestamp = false; if (comptime log_level != .silent) { - if (manager.options.do.summary) { - var printer = Lockfile.Printer{ - .lockfile = install_summary.lockfile_used_for_install, - .options = manager.options, - .updates = manager.update_requests, - .successfully_installed = install_summary.successfully_installed, - }; - - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - try Lockfile.Printer.Tree.print(&printer, manager, Output.WriterType, Output.writer(), enable_ansi_colors, log_level); - }, - } - - if (!did_meta_hash_change) { - manager.summary.remove = 0; - manager.summary.add = 0; - manager.summary.update = 0; - } - - if (install_summary.success > 0) { - // it's confusing when it shows 3 packages and says it installed 1 - const pkgs_installed = @max( - install_summary.success, - @as( - u32, - @truncate(manager.update_requests.len), - ), - ); - Output.pretty("{d} package{s} installed ", .{ pkgs_installed, if (pkgs_installed == 1) "" else "s" }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - - if (manager.summary.remove > 0) { - Output.pretty("Removed: {d}\n", .{manager.summary.remove}); - } - } else if (manager.summary.remove > 0) { - if (manager.subcommand == .remove) { - for (manager.update_requests) |request| { - Output.prettyln("- {s}", .{request.name}); - } - } - - Output.pretty("{d} package{s} removed ", .{ manager.summary.remove, if (manager.summary.remove == 1) "" else "s" }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - } else if (install_summary.skipped > 0 and install_summary.fail == 0 and manager.update_requests.len == 0) { - const count = @as(PackageID, @truncate(manager.lockfile.packages.len)); - if (count != install_summary.skipped) { - Output.pretty("Checked {d} install{s} across {d} package{s} (no changes) ", .{ - install_summary.skipped, - if (install_summary.skipped == 1) "" else "s", - count, - if (count == 1) "" else "s", - }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - } else { - Output.pretty("Done! Checked {d} package{s} (no changes) ", .{ - install_summary.skipped, - if (install_summary.skipped == 1) "" else "s", - }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - } - } - - if (install_summary.fail > 0) { - Output.prettyln("Failed to install {d} package{s}\n", .{ install_summary.fail, if (install_summary.fail == 1) "" else "s" }); - Output.flush(); - } - } - } - - if (comptime log_level != .silent) { - if (manager.options.do.summary) { - if (!printed_timestamp) { - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - Output.prettyln(" done", .{}); - printed_timestamp = true; - } - } + try manager.printInstallSummary(ctx, &install_summary, did_meta_hash_change, log_level); } if (install_summary.fail > 0) { @@ -14843,7 +15167,182 @@ pub const PackageManager = struct { Output.flush(); } - fn printBlockedPackagesInfo(summary: PackageInstall.Summary, global: bool) void { + fn printInstallSummary( + this: *PackageManager, + ctx: Command.Context, + install_summary: *const PackageInstall.Summary, + did_meta_hash_change: bool, + comptime log_level: Options.LogLevel, + ) !void { + var printed_timestamp = false; + if (this.options.do.summary) { + var printer = Lockfile.Printer{ + .lockfile = this.lockfile, + .options = this.options, + .updates = this.update_requests, + .successfully_installed = install_summary.successfully_installed, + }; + + switch (Output.enable_ansi_colors) { + inline else => |enable_ansi_colors| { + try Lockfile.Printer.Tree.print(&printer, this, Output.WriterType, Output.writer(), enable_ansi_colors, log_level); + }, + } + + if (!did_meta_hash_change) { + this.summary.remove = 0; + this.summary.add = 0; + this.summary.update = 0; + } + + if (install_summary.success > 0) { + // it's confusing when it shows 3 packages and says it installed 1 + const pkgs_installed = @max( + install_summary.success, + @as( + u32, + @truncate(this.update_requests.len), + ), + ); + Output.pretty("{d} package{s} installed ", .{ pkgs_installed, if (pkgs_installed == 1) "" else "s" }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + + if (this.summary.remove > 0) { + Output.pretty("Removed: {d}\n", .{this.summary.remove}); + } + } else if (this.summary.remove > 0) { + if (this.subcommand == .remove) { + for (this.update_requests) |request| { + Output.prettyln("- {s}", .{request.name}); + } + } + + Output.pretty("{d} package{s} removed ", .{ this.summary.remove, if (this.summary.remove == 1) "" else "s" }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + } else if (install_summary.skipped > 0 and install_summary.fail == 0 and this.update_requests.len == 0) { + const count = @as(PackageID, @truncate(this.lockfile.packages.len)); + if (count != install_summary.skipped) { + Output.pretty("Checked {d} install{s} across {d} package{s} (no changes) ", .{ + install_summary.skipped, + if (install_summary.skipped == 1) "" else "s", + count, + if (count == 1) "" else "s", + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + } else { + Output.pretty("Done! Checked {d} package{s} (no changes) ", .{ + install_summary.skipped, + if (install_summary.skipped == 1) "" else "s", + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + } + } + + if (install_summary.fail > 0) { + Output.prettyln("Failed to install {d} package{s}\n", .{ install_summary.fail, if (install_summary.fail == 1) "" else "s" }); + Output.flush(); + } + } + + if (this.options.do.summary) { + if (!printed_timestamp) { + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.prettyln(" done", .{}); + printed_timestamp = true; + } + } + } + + fn saveLockfile( + this: *PackageManager, + load_result: *const Lockfile.LoadResult, + save_format: Lockfile.LoadResult.LockfileFormat, + had_any_diffs: bool, + // TODO(dylan-conway): this and `packages_len_before_install` can most likely be deleted + // now that git dependnecies don't append to lockfile during installation. + lockfile_before_install: *const Lockfile, + packages_len_before_install: usize, + log_level: Options.LogLevel, + ) OOM!void { + if (this.lockfile.isEmpty()) { + if (!this.options.dry_run) delete: { + const delete_format = switch (load_result.*) { + .not_found => break :delete, + .err => |err| err.format, + .ok => |ok| ok.format, + }; + + std.fs.cwd().deleteFileZ(if (delete_format == .text) "bun.lock" else "bun.lockb") catch |err| brk: { + // we don't care + if (err == error.FileNotFound) { + if (had_any_diffs) return; + break :brk; + } + + if (log_level != .silent) { + Output.err(err, "failed to delete empty lockfile", .{}); + } + return; + }; + } + if (!this.options.global) { + if (log_level != .silent) { + switch (this.subcommand) { + .remove => Output.prettyErrorln("\npackage.json has no dependencies! Deleted empty lockfile", .{}), + else => Output.prettyErrorln("No packages! Deleted empty lockfile", .{}), + } + } + } + + return; + } + + var save_node: *Progress.Node = undefined; + + if (log_level.showProgress()) { + this.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; + save_node = this.progress.start(ProgressStrings.save(), 0); + save_node.activate(); + + this.progress.refresh(); + } + + this.lockfile.saveToDisk(save_format, this.options.log_level.isVerbose()); + + if (comptime Environment.allow_assert) { + if (load_result.* != .not_found) { + if (load_result.loadedFromTextLockfile()) { + if (!try this.lockfile.eql(lockfile_before_install, packages_len_before_install, this.allocator)) { + Output.panic("Lockfile non-deterministic after saving", .{}); + } + } else { + if (this.lockfile.hasMetaHashChanged(false, packages_len_before_install) catch false) { + Output.panic("Lockfile metahash non-deterministic after saving", .{}); + } + } + } + } + + if (log_level.showProgress()) { + save_node.end(); + this.progress.refresh(); + this.progress.root.end(); + this.progress = .{}; + } else if (log_level != .silent) { + Output.prettyErrorln("Saved lockfile", .{}); + Output.flush(); + } + } + + fn printBlockedPackagesInfo(summary: *const PackageInstall.Summary, global: bool) void { const packages_count = summary.packages_with_blocked_scripts.count(); var scripts_count: usize = 0; for (summary.packages_with_blocked_scripts.values()) |count| scripts_count += count; @@ -14870,6 +15369,7 @@ pub const PackageManager = struct { const lockfile = this.lockfile; const resolutions_lists: []const Lockfile.DependencyIDSlice = lockfile.packages.items(.resolutions); const dependency_lists: []const Lockfile.DependencySlice = lockfile.packages.items(.dependencies); + const pkg_resolutions = lockfile.packages.items(.resolution); const dependencies_buffer = lockfile.buffers.dependencies.items; const resolutions_buffer = lockfile.buffers.resolutions.items; const end: PackageID = @truncate(lockfile.packages.len); @@ -14877,7 +15377,6 @@ pub const PackageManager = struct { var any_failed = false; const string_buf = lockfile.buffers.string_bytes.items; - const root_list = resolutions_lists[0]; for (resolutions_lists, dependency_lists, 0..) |resolution_list, dependency_list, parent_id| { for (resolution_list.get(resolutions_buffer), dependency_list.get(dependencies_buffer)) |package_id, failed_dep| { if (package_id < end) continue; @@ -14886,13 +15385,13 @@ pub const PackageManager = struct { // Need to keep this for now because old lockfiles might have a peer dependency without the optional flag set. if (failed_dep.behavior.isPeer()) continue; + const features = switch (pkg_resolutions[parent_id].tag) { + .root, .workspace, .folder => this.options.local_package_features, + else => this.options.remote_package_features, + }; // even if optional dependencies are enabled, it's still allowed to fail - if (failed_dep.behavior.optional or !failed_dep.behavior.isEnabled( - if (root_list.contains(@truncate(parent_id))) - this.options.local_package_features - else - this.options.remote_package_features, - )) continue; + if (failed_dep.behavior.optional or !failed_dep.behavior.isEnabled(features)) continue; + if (log_level != .silent) { if (failed_dep.name.isEmpty() or strings.eqlLong(failed_dep.name.slice(string_buf), failed_dep.version.literal.slice(string_buf), true)) { Output.errGeneric("{} failed to resolve", .{ @@ -14935,11 +15434,11 @@ pub const PackageManager = struct { try this.ensureTempNodeGypScript(); const cwd = list.cwd; - const this_bundler = try this.configureEnvForScripts(ctx, log_level); - const original_path = this_bundler.env.get("PATH") orelse ""; + const this_transpiler = try this.configureEnvForScripts(ctx, log_level); + const original_path = this_transpiler.env.get("PATH") orelse ""; var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, original_path.len + 1 + "node_modules/.bin".len + cwd.len + 1); - var current_dir: ?*DirInfo = this_bundler.resolver.readDirInfo(cwd) catch null; + var current_dir: ?*DirInfo = this_transpiler.resolver.readDirInfo(cwd) catch null; bun.assert(current_dir != null); while (current_dir) |dir| { if (PATH.items.len > 0 and PATH.items[PATH.items.len - 1] != std.fs.path.delimiter) { @@ -14961,10 +15460,10 @@ pub const PackageManager = struct { try PATH.appendSlice(original_path); } - this_bundler.env.map.put("PATH", PATH.items) catch unreachable; + this_transpiler.env.map.put("PATH", PATH.items) catch unreachable; - const envp = try this_bundler.env.map.createNullDelimitedEnvMap(this.allocator); - try this_bundler.env.map.put("PATH", original_path); + const envp = try this_transpiler.env.map.createNullDelimitedEnvMap(this.allocator); + try this_transpiler.env.map.put("PATH", original_path); PATH.deinit(); try LifecycleScriptSubprocess.spawnPackageScripts(this, list, envp, optional, log_level, foreground); @@ -15006,25 +15505,30 @@ pub const bun_install_js_bindings = struct { const cwd = try args[0].toSliceOrNull(globalObject); defer cwd.deinit(); + var dir = bun.openDirAbsoluteNotForDeletingOrRenaming(cwd.slice()) catch |err| { + return globalObject.throw("failed to open: {s}, '{s}'", .{ @errorName(err), cwd.slice() }); + }; + defer dir.close(); + const lockfile_path = Path.joinAbsStringZ(cwd.slice(), &[_]string{"bun.lockb"}, .auto); var lockfile: Lockfile = undefined; lockfile.initEmpty(allocator); - if (globalObject.bunVM().bundler.resolver.env_loader == null) { - globalObject.bunVM().bundler.resolver.env_loader = globalObject.bunVM().bundler.env; + if (globalObject.bunVM().transpiler.resolver.env_loader == null) { + globalObject.bunVM().transpiler.resolver.env_loader = globalObject.bunVM().transpiler.env; } // as long as we aren't migration from `package-lock.json`, leaving this undefined is okay - const manager = globalObject.bunVM().bundler.resolver.getPackageManager(); + const manager = globalObject.bunVM().transpiler.resolver.getPackageManager(); - const load_result: Lockfile.LoadFromDiskResult = lockfile.loadFromDisk(manager, allocator, &log, lockfile_path, true); + const load_result: Lockfile.LoadResult = lockfile.loadFromDir(bun.toFD(dir), manager, allocator, &log, true); switch (load_result) { .err => |err| { - return globalObject.throw("failed to load lockfile: {s}, \"{s}\"", .{ @errorName(err.value), lockfile_path }); + return globalObject.throw("failed to load lockfile: {s}, '{s}'", .{ @errorName(err.value), lockfile_path }); }, .not_found => { - return globalObject.throw("lockfile not found: \"{s}\"", .{lockfile_path}); + return globalObject.throw("lockfile not found: '{s}'", .{lockfile_path}); }, .ok => {}, } diff --git a/src/install/integrity.zig b/src/install/integrity.zig index 726f824ee4..4e8f47d90e 100644 --- a/src/install/integrity.zig +++ b/src/install/integrity.zig @@ -66,7 +66,7 @@ pub const Integrity = extern struct { return integrity; } - pub fn parse(buf: []const u8) !Integrity { + pub fn parse(buf: []const u8) Integrity { if (buf.len < "sha256-".len) { return Integrity{ .tag = Tag.unknown, diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index 3900e790e8..42c71494ea 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -10,11 +10,13 @@ const Global = bun.Global; const JSC = bun.JSC; const WaiterThread = bun.spawn.WaiterThread; const Timer = std.time.Timer; +const String = bun.Semver.String; +const string = bun.string; const Process = bun.spawn.Process; const log = Output.scoped(.Script, false); pub const LifecycleScriptSubprocess = struct { - package_name: []const u8, + package_name: string, scripts: Lockfile.Package.Scripts.List, current_script_index: u8 = 0, @@ -33,6 +35,15 @@ pub const LifecycleScriptSubprocess = struct { foreground: bool = false, optional: bool = false, + started_at: u64 = 0, + + heap: bun.io.heap.IntrusiveField(LifecycleScriptSubprocess) = .{}, + + pub const List = bun.io.heap.Intrusive(LifecycleScriptSubprocess, *PackageManager, sortByStartedAt); + + fn sortByStartedAt(_: *PackageManager, a: *LifecycleScriptSubprocess, b: *LifecycleScriptSubprocess) bool { + return a.started_at < b.started_at; + } pub usingnamespace bun.New(@This()); @@ -90,6 +101,12 @@ pub const LifecycleScriptSubprocess = struct { // This is only used on the main thread. var cwd_z_buf: bun.PathBuffer = undefined; + fn ensureNotInHeap(this: *LifecycleScriptSubprocess) void { + if (this.heap.child != null or this.heap.next != null or this.heap.prev != null or this.manager.active_lifecycle_scripts.root == this) { + this.manager.active_lifecycle_scripts.remove(this); + } + } + pub fn spawnNextScript(this: *LifecycleScriptSubprocess, next_script_index: u8) !void { bun.Analytics.Features.lifecycle_scripts += 1; @@ -103,6 +120,8 @@ pub const LifecycleScriptSubprocess = struct { this.has_incremented_alive_count = false; _ = alive_count.fetchSub(1, .monotonic); } + + this.ensureNotInHeap(); } const manager = this.manager; @@ -112,6 +131,8 @@ pub const LifecycleScriptSubprocess = struct { this.stdout.setParent(this); this.stderr.setParent(this); + this.ensureNotInHeap(); + this.current_script_index = next_script_index; this.has_called_process_exit = false; @@ -185,16 +206,16 @@ pub const LifecycleScriptSubprocess = struct { }, .cwd = cwd, - .windows = if (Environment.isWindows) - .{ - .loop = JSC.EventLoopHandle.init(&manager.event_loop), - } - else {}, + .windows = if (Environment.isWindows) .{ + .loop = JSC.EventLoopHandle.init(&manager.event_loop), + }, .stream = false, }; this.remaining_fds = 0; + this.started_at = bun.timespec.now().ns(); + this.manager.active_lifecycle_scripts.insert(this); var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(&argv), this.envp)).unwrap(); if (comptime Environment.isPosix) { @@ -297,6 +318,8 @@ pub const LifecycleScriptSubprocess = struct { _ = alive_count.fetchSub(1, .monotonic); } + this.ensureNotInHeap(); + switch (status) { .exited => |exit| { const maybe_duration = if (this.timer) |*t| t.read() else null; @@ -353,8 +376,15 @@ pub const LifecycleScriptSubprocess = struct { } } + if (PackageManager.verbose_install) { + Output.prettyErrorln("[Scripts] Finished scripts for {}", .{ + bun.fmt.quote(this.package_name), + }); + } + // the last script finished _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .monotonic); + this.deinit(); }, .signaled => |signal| { @@ -424,6 +454,7 @@ pub const LifecycleScriptSubprocess = struct { pub fn deinit(this: *LifecycleScriptSubprocess) void { this.resetPolls(); + this.ensureNotInHeap(); if (!this.manager.options.log_level.isVerbose()) { this.stdout.deinit(); diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index bf93c60932..25f96d6a2a 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -7,11 +7,15 @@ const Output = bun.Output; const Global = bun.Global; const Environment = bun.Environment; const strings = bun.strings; +const Glob = bun.glob; const MutableString = bun.MutableString; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const C = bun.C; const JSAst = bun.JSAst; +const TextLockfile = @import("./bun.lock.zig"); +const OOM = bun.OOM; +const WorkspaceFilter = PackageManager.WorkspaceFilter; const JSLexer = bun.js_lexer; const logger = bun.logger; @@ -30,7 +34,6 @@ const Path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; const Command = @import("../cli.zig").Command; const BunArguments = @import("../cli.zig").Arguments; -const bundler = bun.bundler; const DotEnv = @import("../env_loader.zig"); const which = @import("../which.zig").which; @@ -38,7 +41,7 @@ const Run = @import("../bun_js.zig").Run; const HeaderBuilder = bun.http.HeaderBuilder; const Fs = @import("../fs.zig"); const FileSystem = Fs.FileSystem; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const URL = @import("../url.zig").URL; const AsyncHTTP = bun.http.AsyncHTTP; const HTTPChannel = bun.http.HTTPChannel; @@ -48,7 +51,7 @@ const clap = bun.clap; const ExtractTarball = @import("./extract_tarball.zig"); const Npm = @import("./npm.zig"); const Bitset = bun.bit_set.DynamicBitSetUnmanaged; -const z_allocator = @import("../memory_allocator.zig").z_allocator; +const z_allocator = @import("../allocators/memory_allocator.zig").z_allocator; const Lockfile = @This(); const IdentityContext = @import("../identity_context.zig").IdentityContext; @@ -75,6 +78,7 @@ const ExternalStringMap = Install.ExternalStringMap; const Features = Install.Features; const initializeStore = Install.initializeStore; const invalid_package_id = Install.invalid_package_id; +const invalid_dependency_id = Install.invalid_dependency_id; const Origin = Install.Origin; const PackageID = Install.PackageID; const PackageInstall = Install.PackageInstall; @@ -123,12 +127,14 @@ fn ignoredWorkspacePaths(path: []const u8) bool { return false; } -const GlobWalker = bun.glob.GlobWalker_(ignoredWorkspacePaths, bun.glob.SyscallAccessor, false); +const GlobWalker = Glob.GlobWalker(ignoredWorkspacePaths, Glob.walk.SyscallAccessor, false); // Serialized data /// The version of the lockfile format, intended to prevent data corruption for format changes. format: FormatVersion = FormatVersion.current, +text_lockfile_version: TextLockfile.Version = .v0, + meta_hash: MetaHash = zero_hash, packages: Lockfile.Package.List = .{}, @@ -209,63 +215,202 @@ pub fn isEmpty(this: *const Lockfile) bool { return this.packages.len == 0 or (this.packages.len == 1 and this.packages.get(0).resolutions.len == 0); } -pub const LoadFromDiskResult = union(enum) { +pub const LoadResult = union(enum) { not_found: void, err: struct { step: Step, value: anyerror, + lockfile_path: stringZ, + format: LockfileFormat, }, ok: struct { lockfile: *Lockfile, + loaded_from_binary_lockfile: bool, was_migrated: bool = false, serializer_result: Serializer.SerializerLoadResult, + format: LockfileFormat, }, + pub const LockfileFormat = enum { + text, + binary, + + pub fn filename(this: LockfileFormat) stringZ { + return switch (this) { + .text => "bun.lock", + .binary => "bun.lockb", + }; + } + }; + + pub fn loadedFromTextLockfile(this: LoadResult) bool { + return switch (this) { + .not_found => false, + .err => |err| err.format == .text, + .ok => |ok| ok.format == .text, + }; + } + pub const Step = enum { open_file, read_file, parse_file, migrating }; }; -pub fn loadFromDisk( +pub fn loadFromCwd( this: *Lockfile, manager: ?*PackageManager, allocator: Allocator, log: *logger.Log, - filename: stringZ, comptime attempt_loading_from_other_lockfile: bool, -) LoadFromDiskResult { +) LoadResult { + return loadFromDir(this, bun.FD.cwd(), manager, allocator, log, attempt_loading_from_other_lockfile); +} + +pub fn loadFromDir( + this: *Lockfile, + dir: bun.FD, + manager: ?*PackageManager, + allocator: Allocator, + log: *logger.Log, + comptime attempt_loading_from_other_lockfile: bool, +) LoadResult { if (comptime Environment.allow_assert) assert(FileSystem.instance_loaded); - const buf = (if (filename.len > 0) - File.readFrom(std.fs.cwd(), filename, allocator).unwrap() - else - File.from(std.io.getStdIn()).readToEnd(allocator).unwrap()) catch |err| { - return switch (err) { - error.EACCESS, error.EPERM, error.ENOENT => { - if (comptime attempt_loading_from_other_lockfile) { - if (manager) |pm| { - // Attempt to load from "package-lock.json", "yarn.lock", etc. - return migration.detectAndLoadOtherLockfile( - this, - pm, - allocator, - log, - filename, - ); - } - } + var lockfile_format: LoadResult.LockfileFormat = .text; + const file = File.openat(dir, "bun.lock", bun.O.RDONLY, 0).unwrap() catch |text_open_err| file: { + if (text_open_err != error.ENOENT) { + return .{ .err = .{ + .step = .open_file, + .value = text_open_err, + .lockfile_path = "bun.lock", + .format = .text, + } }; + } - return LoadFromDiskResult{ - .err = .{ .step = .open_file, .value = err }, - }; - }, - error.EINVAL, error.ENOTDIR, error.EISDIR => LoadFromDiskResult{ .not_found = {} }, - else => LoadFromDiskResult{ .err = .{ .step = .open_file, .value = err } }, + lockfile_format = .binary; + + break :file File.openat(dir, "bun.lockb", bun.O.RDONLY, 0).unwrap() catch |binary_open_err| { + if (binary_open_err != error.ENOENT) { + return .{ .err = .{ + .step = .open_file, + .value = binary_open_err, + .lockfile_path = "bun.lockb", + .format = .binary, + } }; + } + + if (comptime attempt_loading_from_other_lockfile) { + if (manager) |pm| { + const migrate_result = migration.detectAndLoadOtherLockfile( + this, + dir, + pm, + allocator, + log, + ); + + if (migrate_result == .ok) { + lockfile_format = .text; + } + + return migrate_result; + } + } + + return .not_found; }; }; - return this.loadFromBytes(manager, buf, allocator, log); + const buf = file.readToEnd(allocator).unwrap() catch |err| { + return .{ .err = .{ + .step = .read_file, + .value = err, + .lockfile_path = if (lockfile_format == .text) "bun.lock" else "bun.lockb", + .format = lockfile_format, + } }; + }; + + if (lockfile_format == .text) { + const source = logger.Source.initPathString("bun.lock", buf); + initializeStore(); + const json = JSON.parsePackageJSONUTF8(&source, log, allocator) catch |err| { + return .{ + .err = .{ + .step = .parse_file, + .value = err, + .lockfile_path = "bun.lock", + .format = lockfile_format, + }, + }; + }; + + TextLockfile.parseIntoBinaryLockfile(this, allocator, json, &source, log, manager) catch |err| { + switch (err) { + error.OutOfMemory => bun.outOfMemory(), + else => { + return .{ + .err = .{ + .step = .parse_file, + .value = err, + .lockfile_path = "bun.lock", + .format = lockfile_format, + }, + }; + }, + } + }; + + bun.Analytics.Features.text_lockfile += 1; + + return .{ + .ok = .{ + .lockfile = this, + .serializer_result = .{}, + .loaded_from_binary_lockfile = false, + .format = lockfile_format, + }, + }; + } + + const result = this.loadFromBytes(manager, buf, allocator, log); + + switch (result) { + .ok => { + if (bun.getenvZ("BUN_DEBUG_TEST_TEXT_LOCKFILE") != null and manager != null) { + + // Convert the loaded binary lockfile into a text lockfile in memory, then + // parse it back into a binary lockfile. + + var writer_buf = MutableString.initEmpty(allocator); + var buffered_writer = writer_buf.bufferedWriter(); + const writer = buffered_writer.writer(); + + TextLockfile.Stringifier.saveFromBinary(allocator, result.ok.lockfile, writer) catch |err| { + Output.panic("failed to convert binary lockfile to text lockfile: {s}", .{@errorName(err)}); + }; + + buffered_writer.flush() catch bun.outOfMemory(); + + const text_lockfile_bytes = writer_buf.list.items; + + const source = logger.Source.initPathString("bun.lock", text_lockfile_bytes); + initializeStore(); + const json = JSON.parsePackageJSONUTF8(&source, log, allocator) catch |err| { + Output.panic("failed to print valid json from binary lockfile: {s}", .{@errorName(err)}); + }; + + TextLockfile.parseIntoBinaryLockfile(this, allocator, json, &source, log, manager) catch |err| { + Output.panic("failed to parse text lockfile converted from binary lockfile: {s}", .{@errorName(err)}); + }; + + bun.Analytics.Features.text_lockfile += 1; + } + }, + else => {}, + } + + return result; } -pub fn loadFromBytes(this: *Lockfile, pm: ?*PackageManager, buf: []u8, allocator: Allocator, log: *logger.Log) LoadFromDiskResult { +pub fn loadFromBytes(this: *Lockfile, pm: ?*PackageManager, buf: []u8, allocator: Allocator, log: *logger.Log) LoadResult { var stream = Stream{ .buffer = buf, .pos = 0 }; this.format = FormatVersion.current; @@ -277,17 +422,19 @@ pub fn loadFromBytes(this: *Lockfile, pm: ?*PackageManager, buf: []u8, allocator this.patched_dependencies = .{}; const load_result = Lockfile.Serializer.load(this, &stream, allocator, log, pm) catch |err| { - return LoadFromDiskResult{ .err = .{ .step = .parse_file, .value = err } }; + return LoadResult{ .err = .{ .step = .parse_file, .value = err, .lockfile_path = "bun.lockb", .format = .binary } }; }; if (Environment.allow_assert) { this.verifyData() catch @panic("lockfile data is corrupt"); } - return LoadFromDiskResult{ + return LoadResult{ .ok = .{ .lockfile = this, .serializer_result = load_result, + .loaded_from_binary_lockfile = true, + .format = .binary, }, }; } @@ -299,7 +446,13 @@ pub const InstallResult = struct { pub const Tree = struct { id: Id = invalid_id, - dependency_id: DependencyID = invalid_package_id, + + // Should not be used for anything other than name + // through `folderName()`. There is not guarantee a dependency + // id chosen for a tree node is the same behavior or has the + // same version literal for packages hoisted. + dependency_id: DependencyID = invalid_dependency_id, + parent: Id = invalid_id, dependencies: Lockfile.DependencyIDSlice = .{}, @@ -309,6 +462,12 @@ pub const Tree = struct { pub const List = std.ArrayListUnmanaged(Tree); pub const Id = u32; + pub fn folderName(this: *const Tree, deps: []const Dependency, buf: string) string { + const dep_id = this.dependency_id; + if (dep_id == invalid_dependency_id) return ""; + return deps[dep_id].name.slice(buf); + } + pub fn toExternal(this: Tree) External { var out = External{}; out[0..4].* = @as(Id, @bitCast(this.id)); @@ -334,95 +493,121 @@ pub const Tree = struct { pub const root_dep_id: DependencyID = invalid_package_id - 1; pub const invalid_id: Id = std.math.maxInt(Id); - const dependency_loop = invalid_id - 1; - const hoisted = invalid_id - 2; - const error_id = hoisted; - const SubtreeError = error{ OutOfMemory, DependencyLoop }; - - pub const NodeModulesFolder = struct { - relative_path: stringZ, - dependencies: []const DependencyID, - tree_id: Tree.Id, - - /// depth of the node_modules folder in the tree - /// - /// 0 (./node_modules) - /// / \ - /// 1 1 - /// / - /// 2 - depth: usize, + pub const HoistDependencyResult = union(enum) { + dependency_loop, + hoisted, + placement: struct { + id: Id, + bundled: bool = false, + }, + // replace: struct { + // dest_id: Id, + // dep_id: DependencyID, + // }, }; + pub const SubtreeError = OOM || error{DependencyLoop}; + // max number of node_modules folders pub const max_depth = (bun.MAX_PATH_BYTES / "node_modules".len) + 1; - pub const Iterator = struct { - tree_id: Id, - path_buf: bun.PathBuffer = undefined, - last_parent: Id = invalid_id, + pub const DepthBuf = [max_depth]Id; - lockfile: *const Lockfile, + const IteratorPathStyle = enum { + /// `relative_path` will have the form `node_modules/jquery/node_modules/zod`. + /// Path separators are platform. + node_modules, + /// `relative_path` will have the form `jquery/zod`. Path separators are always + /// posix separators. + pkg_path, + }; - depth_stack: DepthBuf = undefined, + pub fn Iterator(comptime path_style: IteratorPathStyle) type { + return struct { + tree_id: Id, + path_buf: bun.PathBuffer = undefined, - pub const DepthBuf = [max_depth]Id; + lockfile: *const Lockfile, - pub fn init(lockfile: *const Lockfile) Iterator { - var iter = Iterator{ - .tree_id = 0, - .lockfile = lockfile, - }; - @memcpy(iter.path_buf[0.."node_modules".len], "node_modules"); - return iter; - } + depth_stack: DepthBuf = undefined, - pub fn reset(this: *Iterator) void { - this.tree_id = 0; - } - - pub fn nextNodeModulesFolder(this: *Iterator, completed_trees: ?*Bitset) ?NodeModulesFolder { - const trees = this.lockfile.buffers.trees.items; - - if (this.tree_id >= trees.len) return null; - - while (trees[this.tree_id].dependencies.len == 0) { - if (completed_trees) |_completed_trees| { - _completed_trees.set(this.tree_id); + pub fn init(lockfile: *const Lockfile) @This() { + var iter: @This() = .{ + .tree_id = 0, + .lockfile = lockfile, + }; + if (comptime path_style == .node_modules) { + @memcpy(iter.path_buf[0.."node_modules".len], "node_modules"); } - this.tree_id += 1; - if (this.tree_id >= trees.len) return null; + return iter; } - const current_tree_id = this.tree_id; - const tree = trees[current_tree_id]; - const tree_dependencies = tree.dependencies.get(this.lockfile.buffers.hoisted_dependencies.items); + pub fn reset(this: *@This()) void { + this.tree_id = 0; + } - const relative_path, const depth = relativePathAndDepth( - this.lockfile, - current_tree_id, - &this.path_buf, - &this.depth_stack, - ); + pub const Next = struct { + relative_path: stringZ, + dependencies: []const DependencyID, + tree_id: Tree.Id, - this.tree_id += 1; - - return .{ - .relative_path = relative_path, - .dependencies = tree_dependencies, - .tree_id = current_tree_id, - .depth = depth, + /// depth of the node_modules folder in the tree + /// + /// 0 (./node_modules) + /// / \ + /// 1 1 + /// / + /// 2 + depth: usize, }; - } - }; + + pub fn next(this: *@This(), completed_trees: if (path_style == .node_modules) ?*Bitset else void) ?Next { + const trees = this.lockfile.buffers.trees.items; + + if (this.tree_id >= trees.len) return null; + + while (trees[this.tree_id].dependencies.len == 0) { + if (comptime path_style == .node_modules) { + if (completed_trees) |_completed_trees| { + _completed_trees.set(this.tree_id); + } + } + this.tree_id += 1; + if (this.tree_id >= trees.len) return null; + } + + const current_tree_id = this.tree_id; + const tree = trees[current_tree_id]; + const tree_dependencies = tree.dependencies.get(this.lockfile.buffers.hoisted_dependencies.items); + + const relative_path, const depth = relativePathAndDepth( + this.lockfile, + current_tree_id, + &this.path_buf, + &this.depth_stack, + path_style, + ); + + this.tree_id += 1; + + return .{ + .relative_path = relative_path, + .dependencies = tree_dependencies, + .tree_id = current_tree_id, + .depth = depth, + }; + } + }; + } /// Returns relative path and the depth of the tree pub fn relativePathAndDepth( lockfile: *const Lockfile, tree_id: Id, path_buf: *bun.PathBuffer, - depth_buf: *Iterator.DepthBuf, + depth_buf: *DepthBuf, + comptime path_style: IteratorPathStyle, ) struct { stringZ, usize } { const trees = lockfile.buffers.trees.items; var depth: usize = 0; @@ -430,7 +615,10 @@ pub const Tree = struct { const tree = trees[tree_id]; var parent_id = tree.id; - var path_written: usize = "node_modules".len; + var path_written: usize = switch (comptime path_style) { + .node_modules => "node_modules".len, + .pkg_path => 0, + }; depth_buf[0] = 0; @@ -449,16 +637,25 @@ pub const Tree = struct { depth = depth_buf_len; while (depth_buf_len > 0) : (depth_buf_len -= 1) { - path_buf[path_written] = std.fs.path.sep; - path_written += 1; + if (comptime path_style == .pkg_path) { + if (depth_buf_len != depth) { + path_buf[path_written] = '/'; + path_written += 1; + } + } else { + path_buf[path_written] = std.fs.path.sep; + path_written += 1; + } const id = depth_buf[depth_buf_len]; - const name = dependencies[trees[id].dependency_id].name.slice(buf); + const name = trees[id].folderName(dependencies, buf); @memcpy(path_buf[path_written..][0..name.len], name); path_written += name.len; - @memcpy(path_buf[path_written..][0.."/node_modules".len], std.fs.path.sep_str ++ "node_modules"); - path_written += "/node_modules".len; + if (comptime path_style == .node_modules) { + @memcpy(path_buf[path_written..][0.."/node_modules".len], std.fs.path.sep_str ++ "node_modules"); + path_written += "/node_modules".len; + } } } path_buf[path_written] = 0; @@ -467,80 +664,118 @@ pub const Tree = struct { return .{ rel, depth }; } - const Builder = struct { - allocator: Allocator, - name_hashes: []const PackageNameHash, - list: bun.MultiArrayList(Entry) = .{}, - resolutions: []const PackageID, - dependencies: []const Dependency, - resolution_lists: []const Lockfile.DependencyIDSlice, - queue: Lockfile.TreeFiller, - log: *logger.Log, - lockfile: *Lockfile, - prefer_dev_dependencies: bool = false, + const BuilderMethod = enum { + /// Hoist, but include every dependency so it's resolvable if configuration + /// changes. For saving to disk. + resolvable, - pub fn maybeReportError(this: *Builder, comptime fmt: string, args: anytype) void { - this.log.addErrorFmt(null, logger.Loc.Empty, this.allocator, fmt, args) catch {}; - } - - pub fn buf(this: *const Builder) []const u8 { - return this.lockfile.buffers.string_bytes.items; - } - - pub fn packageName(this: *Builder, id: PackageID) String.Formatter { - return this.lockfile.packages.items(.name)[id].fmt(this.lockfile.buffers.string_bytes.items); - } - - pub fn packageVersion(this: *Builder, id: PackageID) Resolution.Formatter { - return this.lockfile.packages.items(.resolution)[id].fmt(this.lockfile.buffers.string_bytes.items, .auto); - } - - pub const Entry = struct { - tree: Tree, - dependencies: Lockfile.DependencyIDList, - }; - - /// Flatten the multi-dimensional ArrayList of package IDs into a single easily serializable array - pub fn clean(this: *Builder) !DependencyIDList { - const end = @as(Id, @truncate(this.list.len)); - var i: Id = 0; - var total: u32 = 0; - const trees = this.list.items(.tree); - const dependencies = this.list.items(.dependencies); - - while (i < end) : (i += 1) { - total += trees[i].dependencies.len; - } - - var dependency_ids = try DependencyIDList.initCapacity(z_allocator, total); - var next = PackageIDSlice{}; - - for (trees, dependencies) |*tree, *child| { - if (tree.dependencies.len > 0) { - const len = @as(PackageID, @truncate(child.items.len)); - next.off += next.len; - next.len = len; - tree.dependencies = next; - dependency_ids.appendSliceAssumeCapacity(child.items); - child.deinit(this.allocator); - } - } - this.queue.deinit(); - - return dependency_ids; - } + /// This will filter out disabled dependencies, resulting in more aggresive + /// hoisting compared to `hoist()`. We skip dependencies based on 'os', 'cpu', + /// 'libc' (TODO), and omitted dependency types (`--omit=dev/peer/optional`). + /// Dependencies of a disabled package are not included in the output. + filter, }; + pub fn Builder(comptime method: BuilderMethod) type { + return struct { + allocator: Allocator, + name_hashes: []const PackageNameHash, + list: bun.MultiArrayList(Entry) = .{}, + resolutions: []const PackageID, + dependencies: []const Dependency, + resolution_lists: []const Lockfile.DependencyIDSlice, + queue: Lockfile.TreeFiller, + log: *logger.Log, + lockfile: *const Lockfile, + manager: if (method == .filter) *const PackageManager else void, + sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}, + workspace_filters: if (method == .filter) []const WorkspaceFilter else void = if (method == .filter) &.{}, + install_root_dependencies: if (method == .filter) bool else void, + path_buf: []u8, + + pub fn maybeReportError(this: *@This(), comptime fmt: string, args: anytype) void { + this.log.addErrorFmt(null, logger.Loc.Empty, this.allocator, fmt, args) catch {}; + } + + pub fn buf(this: *const @This()) []const u8 { + return this.lockfile.buffers.string_bytes.items; + } + + pub fn packageName(this: *@This(), id: PackageID) String.Formatter { + return this.lockfile.packages.items(.name)[id].fmt(this.lockfile.buffers.string_bytes.items); + } + + pub fn packageVersion(this: *@This(), id: PackageID) Resolution.Formatter { + return this.lockfile.packages.items(.resolution)[id].fmt(this.lockfile.buffers.string_bytes.items, .auto); + } + + pub const Entry = struct { + tree: Tree, + dependencies: Lockfile.DependencyIDList, + }; + + pub const CleanResult = struct { + trees: std.ArrayListUnmanaged(Tree), + dep_ids: std.ArrayListUnmanaged(DependencyID), + }; + + /// Flatten the multi-dimensional ArrayList of package IDs into a single easily serializable array + pub fn clean(this: *@This()) OOM!CleanResult { + var total: u32 = 0; + + const list_ptr = this.list.bytes; + const slice = this.list.toOwnedSlice(); + var trees = slice.items(.tree); + const dependencies = slice.items(.dependencies); + + for (trees) |*tree| { + total += tree.dependencies.len; + } + + var dependency_ids = try DependencyIDList.initCapacity(z_allocator, total); + var next = PackageIDSlice{}; + + for (trees, dependencies) |*tree, *child| { + if (tree.dependencies.len > 0) { + const len = @as(PackageID, @truncate(child.items.len)); + next.off += next.len; + next.len = len; + tree.dependencies = next; + dependency_ids.appendSliceAssumeCapacity(child.items); + child.deinit(this.allocator); + } + } + this.queue.deinit(); + this.sort_buf.deinit(this.allocator); + + // take over the `builder.list` pointer for only trees + if (@intFromPtr(trees.ptr) != @intFromPtr(list_ptr)) { + var new: [*]Tree = @ptrCast(list_ptr); + bun.copy(Tree, new[0..trees.len], trees); + trees = new[0..trees.len]; + } + + return .{ + .trees = std.ArrayListUnmanaged(Tree).fromOwnedSlice(trees), + .dep_ids = dependency_ids, + }; + } + }; + } + pub fn processSubtree( this: *const Tree, dependency_id: DependencyID, - builder: *Builder, + hoist_root_id: Tree.Id, + comptime method: BuilderMethod, + builder: *Builder(method), + log_level: if (method == .filter) PackageManager.Options.LogLevel else void, ) SubtreeError!void { - const package_id = switch (dependency_id) { + const parent_pkg_id = switch (dependency_id) { root_dep_id => 0, else => |id| builder.resolutions[id], }; - const resolution_list = builder.resolution_lists[package_id]; + const resolution_list = builder.resolution_lists[parent_pkg_id]; if (resolution_list.len == 0) return; @@ -559,40 +794,186 @@ pub const Tree = struct { const next: *Tree = &trees[builder.list.len - 1]; const name_hashes: []const PackageNameHash = builder.name_hashes; const max_package_id = @as(PackageID, @truncate(name_hashes.len)); - const resolutions = builder.lockfile.packages.items(.resolution); - var dep_id = resolution_list.off; - const end = dep_id + resolution_list.len; + const pkgs = builder.lockfile.packages.slice(); + const pkg_resolutions = pkgs.items(.resolution); + const pkg_metas = pkgs.items(.meta); + const pkg_names = pkgs.items(.name); - while (dep_id < end) : (dep_id += 1) { - const pid = builder.resolutions[dep_id]; + builder.sort_buf.clearRetainingCapacity(); + try builder.sort_buf.ensureUnusedCapacity(builder.allocator, resolution_list.len); + + for (resolution_list.begin()..resolution_list.end()) |dep_id| { + builder.sort_buf.appendAssumeCapacity(@intCast(dep_id)); + } + + const DepSorter = struct { + lockfile: *const Lockfile, + + pub fn isLessThan(sorter: @This(), l: DependencyID, r: DependencyID) bool { + const deps_buf = sorter.lockfile.buffers.dependencies.items; + const string_buf = sorter.lockfile.buffers.string_bytes.items; + + const l_dep = deps_buf[l]; + const r_dep = deps_buf[r]; + + return switch (l_dep.behavior.cmp(r_dep.behavior)) { + .lt => true, + .gt => false, + .eq => strings.order(l_dep.name.slice(string_buf), r_dep.name.slice(string_buf)) == .lt, + }; + } + }; + + std.sort.pdq( + DependencyID, + builder.sort_buf.items, + DepSorter{ + .lockfile = builder.lockfile, + }, + DepSorter.isLessThan, + ); + + for (builder.sort_buf.items) |dep_id| { + const pkg_id = builder.resolutions[dep_id]; // Skip unresolved packages, e.g. "peerDependencies" - if (pid >= max_package_id) continue; + if (pkg_id >= max_package_id) continue; - const dependency = builder.dependencies[dep_id]; - // Do not hoist folder dependencies - const destination = if (resolutions[pid].tag == .folder) - next.id - else - try next.hoistDependency( - true, - pid, + // filter out disabled dependencies + if (comptime method == .filter) { + if (builder.lockfile.isResolvedDependencyDisabled( dep_id, + switch (pkg_resolutions[parent_pkg_id].tag) { + .root, .workspace, .folder => builder.manager.options.local_package_features, + else => builder.manager.options.remote_package_features, + }, + &pkg_metas[pkg_id], + )) { + if (log_level.isVerbose()) { + const meta = &pkg_metas[pkg_id]; + const name = builder.lockfile.str(&pkg_names[pkg_id]); + if (!meta.os.isMatch() and !meta.arch.isMatch()) { + Output.prettyErrorln("Skip installing '{s}' cpu & os mismatch", .{name}); + } else if (!meta.os.isMatch()) { + Output.prettyErrorln("Skip installing '{s}' os mismatch", .{name}); + } else if (!meta.arch.isMatch()) { + Output.prettyErrorln("Skip installing '{s}' cpu mismatch", .{name}); + } + } + + continue; + } + + if (builder.manager.subcommand == .install) dont_skip: { + // only do this when parent is root. workspaces are always dependencies of the root + // package, and the root package is always called with `processSubtree` + if (parent_pkg_id == 0 and builder.workspace_filters.len > 0) { + if (!builder.dependencies[dep_id].behavior.isWorkspaceOnly()) { + if (builder.install_root_dependencies) { + break :dont_skip; + } + + continue; + } + + var match = false; + + for (builder.workspace_filters) |workspace_filter| { + const res_id = builder.resolutions[dep_id]; + + const pattern, const path_or_name = switch (workspace_filter) { + .name => |pattern| .{ pattern, pkg_names[res_id].slice(builder.buf()) }, + + .path => |pattern| path: { + const res = &pkg_resolutions[res_id]; + if (res.tag != .workspace) { + break :dont_skip; + } + const res_path = res.value.workspace.slice(builder.buf()); + + // occupy `builder.path_buf` + var abs_res_path = strings.withoutTrailingSlash(bun.path.joinAbsStringBuf( + FileSystem.instance.top_level_dir, + builder.path_buf, + &.{res_path}, + .auto, + )); + + if (comptime Environment.isWindows) { + abs_res_path = abs_res_path[Path.windowsVolumeNameLen(abs_res_path)[0]..]; + Path.dangerouslyConvertPathToPosixInPlace(u8, builder.path_buf[0..abs_res_path.len]); + } + + break :path .{ + pattern, + abs_res_path, + }; + }, + + .all => { + match = true; + continue; + }, + }; + + switch (bun.glob.walk.matchImpl(pattern, path_or_name)) { + .match, .negate_match => match = true, + + .negate_no_match => { + // always skip if a pattern specifically says "!" + match = false; + break; + }, + + .no_match => { + // keep current + }, + } + } + + if (!match) { + continue; + } + } + } + } + + const hoisted: HoistDependencyResult = hoisted: { + const dependency = builder.dependencies[dep_id]; + + // don't hoist if it's a folder dependency or a bundled dependency. + if (dependency.behavior.isBundled()) { + break :hoisted .{ .placement = .{ .id = next.id, .bundled = true } }; + } + + if (pkg_resolutions[pkg_id].tag == .folder) { + break :hoisted .{ .placement = .{ .id = next.id } }; + } + + break :hoisted try next.hoistDependency( + true, + hoist_root_id, + pkg_id, &dependency, dependency_lists, trees, + method, builder, ); + }; - switch (destination) { - Tree.dependency_loop, Tree.hoisted => continue, - else => { - dependency_lists[destination].append(builder.allocator, dep_id) catch unreachable; - trees[destination].dependencies.len += 1; - if (builder.resolution_lists[pid].len > 0) { + switch (hoisted) { + .dependency_loop, .hoisted => continue, + .placement => |dest| { + dependency_lists[dest.id].append(builder.allocator, dep_id) catch bun.outOfMemory(); + trees[dest.id].dependencies.len += 1; + if (builder.resolution_lists[pkg_id].len > 0) { try builder.queue.writeItem(.{ - .tree_id = destination, + .tree_id = dest.id, .dependency_id = dep_id, + + // if it's bundled, start a new hoist root + .hoist_root_id = if (dest.bundled) dest.id else hoist_root_id, }); } }, @@ -612,32 +993,31 @@ pub const Tree = struct { fn hoistDependency( this: *Tree, comptime as_defined: bool, + hoist_root_id: Id, package_id: PackageID, - dependency_id: DependencyID, dependency: *const Dependency, dependency_lists: []Lockfile.DependencyIDList, trees: []Tree, - builder: *Builder, - ) !Id { + comptime method: BuilderMethod, + builder: *Builder(method), + ) !HoistDependencyResult { const this_dependencies = this.dependencies.get(dependency_lists[this.id].items); - for (this_dependencies) |dep_id| { + for (0..this_dependencies.len) |i| { + const dep_id = this_dependencies[i]; const dep = builder.dependencies[dep_id]; if (dep.name_hash != dependency.name_hash) continue; if (builder.resolutions[dep_id] == package_id) { // this dependency is the same package as the other, hoist - return hoisted; // 1 + return .hoisted; // 1 } if (comptime as_defined) { - // same dev dependency as another package in the same package.json, but different version. - // choose dev dep over other if enabled if (dep.behavior.isDev() != dependency.behavior.isDev()) { - if (builder.prefer_dev_dependencies and dep.behavior.isDev()) { - return hoisted; // 1 - } - - return dependency_loop; // 3 + // will only happen in workspaces and root package because + // dev dependencies won't be included in other types of + // dependencies + return .hoisted; // 1 } } @@ -649,7 +1029,7 @@ pub const Tree = struct { const resolution: Resolution = builder.lockfile.packages.items(.resolution)[builder.resolutions[dep_id]]; const version = dependency.version.value.npm.version; if (resolution.tag == .npm and version.satisfies(resolution.value.npm.version, builder.buf(), builder.buf())) { - return hoisted; // 1 + return .hoisted; // 1 } } @@ -657,7 +1037,7 @@ pub const Tree = struct { // to hoist other peers even if they don't satisfy the version if (builder.lockfile.isWorkspaceRootDependency(dep_id)) { // TODO: warning about peer dependency version mismatch - return hoisted; // 1 + return .hoisted; // 1 } } @@ -673,29 +1053,43 @@ pub const Tree = struct { return error.DependencyLoop; } - return dependency_loop; // 3 + return .dependency_loop; // 3 } // this dependency was not found in this tree, try hoisting or placing in the next parent - if (this.parent < error_id) { + if (this.parent != invalid_id and this.id != hoist_root_id) { const id = trees[this.parent].hoistDependency( false, + hoist_root_id, package_id, - dependency_id, dependency, dependency_lists, trees, + method, builder, ) catch unreachable; - if (!as_defined or id != dependency_loop) return id; // 1 or 2 + if (!as_defined or id != .dependency_loop) return id; // 1 or 2 } // place the dependency in the current tree - return this.id; // 2 + return .{ .placement = .{ .id = this.id } }; // 2 } }; -/// This conditonally clones the lockfile with root packages marked as non-resolved +pub fn isResolvedDependencyDisabled( + lockfile: *const Lockfile, + dep_id: DependencyID, + features: Features, + meta: *const Package.Meta, +) bool { + if (meta.isDisabled()) return true; + + const dep = lockfile.buffers.dependencies.items[dep_id]; + + return dep.behavior.isBundled() or !dep.behavior.isEnabled(features); +} + +/// This conditionally clones the lockfile with root packages marked as non-resolved /// that do not satisfy `Features`. The package may still end up installed even /// if it was e.g. in "devDependencies" and its a production install. In that case, /// it would be installed because another dependency or transient dependency needed it. @@ -741,26 +1135,35 @@ pub fn maybeCloneFilteringRootPackages( } fn preprocessUpdateRequests(old: *Lockfile, manager: *PackageManager, updates: []PackageManager.UpdateRequest, exact_versions: bool) !void { - const root_deps_list: Lockfile.DependencySlice = old.packages.items(.dependencies)[0]; + const workspace_package_id = manager.root_package_id.get(old, manager.workspace_name_hash); + const root_deps_list: Lockfile.DependencySlice = old.packages.items(.dependencies)[workspace_package_id]; + if (@as(usize, root_deps_list.off) < old.buffers.dependencies.items.len) { var string_builder = old.stringBuilder(); { const root_deps: []const Dependency = root_deps_list.get(old.buffers.dependencies.items); - const old_resolutions_list = old.packages.items(.resolutions)[0]; + const old_resolutions_list = old.packages.items(.resolutions)[workspace_package_id]; const old_resolutions: []const PackageID = old_resolutions_list.get(old.buffers.resolutions.items); const resolutions_of_yore: []const Resolution = old.packages.items(.resolution); for (updates) |update| { - if (update.version.tag == .uninitialized) { + if (update.package_id == invalid_package_id) { for (root_deps, old_resolutions) |dep, old_resolution| { if (dep.name_hash == String.Builder.stringHash(update.name)) { if (old_resolution > old.packages.len) continue; const res = resolutions_of_yore[old_resolution]; + if (res.tag != .npm or update.version.tag != .dist_tag) continue; + + // TODO(dylan-conway): this will need to handle updating dependencies (exact, ^, or ~) and aliases + const len = switch (exact_versions) { - false => std.fmt.count("^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}), - true => std.fmt.count("{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}), + else => |exact| std.fmt.count("{s}{}", .{ + if (exact) "" else "^", + res.value.npm.version.fmt(old.buffers.string_bytes.items), + }), }; + if (len >= String.max_inline_len) { string_builder.cap += len; } @@ -778,20 +1181,27 @@ fn preprocessUpdateRequests(old: *Lockfile, manager: *PackageManager, updates: [ const root_deps: []Dependency = root_deps_list.mut(old.buffers.dependencies.items); const old_resolutions_list_lists = old.packages.items(.resolutions); - const old_resolutions_list = old_resolutions_list_lists[0]; + const old_resolutions_list = old_resolutions_list_lists[workspace_package_id]; const old_resolutions: []const PackageID = old_resolutions_list.get(old.buffers.resolutions.items); const resolutions_of_yore: []const Resolution = old.packages.items(.resolution); for (updates) |*update| { - if (update.version.tag == .uninitialized) { + if (update.package_id == invalid_package_id) { for (root_deps, old_resolutions) |*dep, old_resolution| { if (dep.name_hash == String.Builder.stringHash(update.name)) { if (old_resolution > old.packages.len) continue; const res = resolutions_of_yore[old_resolution]; + if (res.tag != .npm or update.version.tag != .dist_tag) continue; + + // TODO(dylan-conway): this will need to handle updating dependencies (exact, ^, or ~) and aliases + const buf = switch (exact_versions) { - false => std.fmt.bufPrint(&temp_buf, "^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}) catch break, - true => std.fmt.bufPrint(&temp_buf, "{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}) catch break, + else => |exact| std.fmt.bufPrint(&temp_buf, "{s}{}", .{ + if (exact) "" else "^", + res.value.npm.version.fmt(old.buffers.string_bytes.items), + }) catch break, }; + const external_version = string_builder.append(ExternalString, buf); const sliced = external_version.value.sliced(old.buffers.string_bytes.items); dep.version = Dependency.parse( @@ -860,6 +1270,7 @@ pub fn getWorkspacePkgIfWorkspaceDep(this: *const Lockfile, id: DependencyID) Pa } /// Does this tree id belong to a workspace (including workspace root)? +/// TODO(dylan-conway) fix! pub fn isWorkspaceTreeId(this: *const Lockfile, id: Tree.Id) bool { return id == 0 or this.buffers.dependencies.items[this.buffers.trees.items[id].dependency_id].behavior.isWorkspaceOnly(); } @@ -889,7 +1300,7 @@ pub fn cleanWithLogger( exact_versions: bool, comptime log_level: PackageManager.Options.LogLevel, ) !*Lockfile { - var timer: if (log_level.isVerbose()) std.time.Timer else void = if (comptime log_level.isVerbose()) try std.time.Timer.start() else {}; + var timer: if (log_level.isVerbose()) std.time.Timer else void = if (comptime log_level.isVerbose()) try std.time.Timer.start(); const old_trusted_dependencies = old.trusted_dependencies; const old_scripts = old.scripts; @@ -1101,6 +1512,10 @@ pub fn fmtMetaHash(this: *const Lockfile) MetaHashFormatter { pub const FillItem = struct { tree_id: Tree.Id, dependency_id: DependencyID, + + /// If valid, dependencies will not hoist + /// beyond this tree if they're in a subtree + hoist_root_id: Tree.Id, }; pub const TreeFiller = std.fifo.LinearFifo(FillItem, .Dynamic); @@ -1117,9 +1532,7 @@ const Cloner = struct { pub fn flush(this: *Cloner) anyerror!void { const max_package_id = this.old.packages.len; - while (this.clone_queue.popOrNull()) |to_clone_| { - const to_clone: PendingResolution = to_clone_; - + while (this.clone_queue.popOrNull()) |to_clone| { const mapping = this.mapping[to_clone.old_resolution]; if (mapping < max_package_id) { this.lockfile.buffers.resolutions.items[to_clone.resolve_id] = mapping; @@ -1142,7 +1555,7 @@ const Cloner = struct { this.manager.clearCachedItemsDependingOnLockfileBuffer(); if (this.lockfile.packages.len != 0) { - try this.hoist(this.lockfile); + try this.lockfile.resolve(this.log); } // capacity is used for calculating byte size @@ -1150,39 +1563,78 @@ const Cloner = struct { if (this.lockfile.packages.capacity != this.lockfile.packages.len and this.lockfile.packages.len > 0) this.lockfile.packages.shrinkAndFree(this.lockfile.allocator, this.lockfile.packages.len); } - - fn hoist(this: *Cloner, lockfile: *Lockfile) anyerror!void { - const allocator = lockfile.allocator; - var slice = lockfile.packages.slice(); - var builder = Tree.Builder{ - .name_hashes = slice.items(.name_hash), - .queue = TreeFiller.init(allocator), - .resolution_lists = slice.items(.resolutions), - .resolutions = lockfile.buffers.resolutions.items, - .allocator = allocator, - .dependencies = lockfile.buffers.dependencies.items, - .log = this.log, - .lockfile = lockfile, - .prefer_dev_dependencies = this.manager.options.local_package_features.dev_dependencies, - }; - - try (Tree{}).processSubtree(Tree.root_dep_id, &builder); - // This goes breadth-first - while (builder.queue.readItem()) |item| { - try builder.list.items(.tree)[item.tree_id].processSubtree(item.dependency_id, &builder); - } - - lockfile.buffers.hoisted_dependencies = try builder.clean(); - { - const final = builder.list.items(.tree); - lockfile.buffers.trees = .{ - .items = final, - .capacity = final.len, - }; - } - } }; +pub fn resolve( + lockfile: *Lockfile, + log: *logger.Log, +) Tree.SubtreeError!void { + return lockfile.hoist(log, .resolvable, {}, {}, {}); +} + +pub fn filter( + lockfile: *Lockfile, + log: *logger.Log, + manager: *PackageManager, + install_root_dependencies: bool, + workspace_filters: []const WorkspaceFilter, +) Tree.SubtreeError!void { + return lockfile.hoist(log, .filter, manager, install_root_dependencies, workspace_filters); +} + +/// Sets `buffers.trees` and `buffers.hoisted_dependencies` +pub fn hoist( + lockfile: *Lockfile, + log: *logger.Log, + comptime method: Tree.BuilderMethod, + manager: if (method == .filter) *PackageManager else void, + install_root_dependencies: if (method == .filter) bool else void, + workspace_filters: if (method == .filter) []const WorkspaceFilter else void, +) Tree.SubtreeError!void { + const allocator = lockfile.allocator; + var slice = lockfile.packages.slice(); + + var path_buf: bun.PathBuffer = undefined; + + var builder = Tree.Builder(method){ + .name_hashes = slice.items(.name_hash), + .queue = TreeFiller.init(allocator), + .resolution_lists = slice.items(.resolutions), + .resolutions = lockfile.buffers.resolutions.items, + .allocator = allocator, + .dependencies = lockfile.buffers.dependencies.items, + .log = log, + .lockfile = lockfile, + .manager = manager, + .path_buf = &path_buf, + .install_root_dependencies = install_root_dependencies, + .workspace_filters = workspace_filters, + }; + + try (Tree{}).processSubtree( + Tree.root_dep_id, + Tree.invalid_id, + method, + &builder, + if (method == .filter) manager.options.log_level, + ); + + // This goes breadth-first + while (builder.queue.readItem()) |item| { + try builder.list.items(.tree)[item.tree_id].processSubtree( + item.dependency_id, + item.hoist_root_id, + method, + &builder, + if (method == .filter) manager.options.log_level, + ); + } + + const cleaned = try builder.clean(); + lockfile.buffers.trees = cleaned.trees; + lockfile.buffers.hoisted_dependencies = cleaned.dep_ids; +} + const PendingResolution = struct { old_resolution: PackageID, resolve_id: PackageID, @@ -1229,13 +1681,13 @@ pub const Printer = struct { } if (lockfile_path.len > 0 and lockfile_path[0] == std.fs.path.sep) - _ = bun.sys.chdir(std.fs.path.dirname(lockfile_path) orelse std.fs.path.sep_str); + _ = bun.sys.chdir("", std.fs.path.dirname(lockfile_path) orelse std.fs.path.sep_str); _ = try FileSystem.init(null); var lockfile = try allocator.create(Lockfile); - const load_from_disk = lockfile.loadFromDisk(null, allocator, log, lockfile_path, false); + const load_from_disk = lockfile.loadFromCwd(null, allocator, log, false); switch (load_from_disk) { .err => |cause| { switch (cause.step) { @@ -1340,6 +1792,7 @@ pub const Printer = struct { const dependencies = lockfile.buffers.dependencies.items; const workspace_res = packages_slice.items(.resolution)[workspace_package_id]; const names = packages_slice.items(.name); + const pkg_metas = packages_slice.items(.meta); bun.assert(workspace_res.tag == .workspace or workspace_res.tag == .root); const resolutions_list = packages_slice.items(.resolutions); var printed_section_header = false; @@ -1347,7 +1800,7 @@ pub const Printer = struct { // find the updated packages for (resolutions_list[workspace_package_id].begin()..resolutions_list[workspace_package_id].end()) |dep_id| { - switch (shouldPrintPackageInstall(this, manager, @intCast(dep_id), installed, id_map)) { + switch (shouldPrintPackageInstall(this, manager, @intCast(dep_id), installed, id_map, pkg_metas)) { .yes, .no, .@"return" => {}, .update => |update_info| { printed_new_install.* = true; @@ -1369,7 +1822,7 @@ pub const Printer = struct { } for (resolutions_list[workspace_package_id].begin()..resolutions_list[workspace_package_id].end()) |dep_id| { - switch (shouldPrintPackageInstall(this, manager, @intCast(dep_id), installed, id_map)) { + switch (shouldPrintPackageInstall(this, manager, @intCast(dep_id), installed, id_map, pkg_metas)) { .@"return" => return, .yes => {}, .no, .update => continue, @@ -1418,6 +1871,7 @@ pub const Printer = struct { dep_id: DependencyID, installed: *const Bitset, id_map: ?[]DependencyID, + pkg_metas: []const Package.Meta, ) ShouldPrintPackageInstallResult { const dependencies = this.lockfile.buffers.dependencies.items; const resolutions = this.lockfile.buffers.resolutions.items; @@ -1441,6 +1895,17 @@ pub const Printer = struct { if (!installed.isSet(package_id)) return .no; + // It's possible this package was installed but the dependency is disabled. + // Have "zod@1.0.0" in dependencies and `zod2@npm:zod@1.0.0` in devDependencies + // and install with --omit=dev. + if (this.lockfile.isResolvedDependencyDisabled( + dep_id, + this.options.local_package_features, + &pkg_metas[package_id], + )) { + return .no; + } + const resolution = this.lockfile.packages.items(.resolution)[package_id]; if (resolution.tag == .npm) { const name = dependency.name.slice(this.lockfile.buffers.string_bytes.items); @@ -1560,6 +2025,7 @@ pub const Printer = struct { if (resolved.len == 0) return; const string_buf = this.lockfile.buffers.string_bytes.items; const resolutions_list = slice.items(.resolutions); + const pkg_metas = slice.items(.meta); const resolutions_buffer: []const PackageID = this.lockfile.buffers.resolutions.items; const dependencies_buffer: []const Dependency = this.lockfile.buffers.dependencies.items; if (dependencies_buffer.len == 0) return; @@ -1586,7 +2052,7 @@ pub const Printer = struct { for (workspaces_to_print.items) |workspace_dep_id| { const workspace_package_id = resolutions_buffer[workspace_dep_id]; for (resolutions_list[workspace_package_id].begin()..resolutions_list[workspace_package_id].end()) |dep_id| { - switch (shouldPrintPackageInstall(this, manager, @intCast(dep_id), installed, id_map)) { + switch (shouldPrintPackageInstall(this, manager, @intCast(dep_id), installed, id_map, pkg_metas)) { .yes => found_workspace_to_print = true, else => {}, } @@ -1928,14 +2394,14 @@ pub const Printer = struct { } if (dependencies.len > 0) { - var behavior = Behavior.uninitialized; + var behavior: Behavior = .{}; var dependency_behavior_change_count: u8 = 0; for (dependencies) |dep| { if (!dep.behavior.eq(behavior)) { if (dep.behavior.isOptional()) { try writer.writeAll(" optionalDependencies:\n"); if (comptime Environment.allow_assert) dependency_behavior_change_count += 1; - } else if (dep.behavior.isNormal()) { + } else if (dep.behavior.isProd()) { try writer.writeAll(" dependencies:\n"); if (comptime Environment.allow_assert) dependency_behavior_change_count += 1; } else if (dep.behavior.isDev()) { @@ -1992,7 +2458,7 @@ pub fn verifyData(this: *const Lockfile) !void { } } -pub fn saveToDisk(this: *Lockfile, filename: stringZ, verbose_log: bool) void { +pub fn saveToDisk(this: *Lockfile, save_format: LoadResult.LockfileFormat, verbose_log: bool) void { if (comptime Environment.allow_assert) { this.verifyData() catch |err| { Output.prettyErrorln("error: failed to verify lockfile: {s}", .{@errorName(err)}); @@ -2001,10 +2467,25 @@ pub fn saveToDisk(this: *Lockfile, filename: stringZ, verbose_log: bool) void { assert(FileSystem.instance_loaded); } - var bytes = std.ArrayList(u8).init(bun.default_allocator); - defer bytes.deinit(); + const bytes = bytes: { + if (save_format == .text) { + var writer_buf = MutableString.initEmpty(bun.default_allocator); + var buffered_writer = writer_buf.bufferedWriter(); + const writer = buffered_writer.writer(); + + TextLockfile.Stringifier.saveFromBinary(bun.default_allocator, this, writer) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + }; + + buffered_writer.flush() catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + }; + + break :bytes writer_buf.list.items; + } + + var bytes = std.ArrayList(u8).init(bun.default_allocator); - { var total_size: usize = 0; var end_pos: usize = 0; Lockfile.Serializer.save(this, verbose_log, &bytes, &total_size, &end_pos) catch |err| { @@ -2013,45 +2494,54 @@ pub fn saveToDisk(this: *Lockfile, filename: stringZ, verbose_log: bool) void { }; if (bytes.items.len >= end_pos) bytes.items[end_pos..][0..@sizeOf(usize)].* = @bitCast(total_size); - } + break :bytes bytes.items; + }; + defer bun.default_allocator.free(bytes); var tmpname_buf: [512]u8 = undefined; var base64_bytes: [8]u8 = undefined; bun.rand(&base64_bytes); - const tmpname = std.fmt.bufPrintZ(&tmpname_buf, ".lockb-{s}.tmp", .{bun.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable; + const tmpname = if (save_format == .text) + std.fmt.bufPrintZ(&tmpname_buf, ".lock-{s}.tmp", .{bun.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable + else + std.fmt.bufPrintZ(&tmpname_buf, ".lockb-{s}.tmp", .{bun.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable; const file = switch (File.openat(std.fs.cwd(), tmpname, bun.O.CREAT | bun.O.WRONLY, 0o777)) { .err => |err| { - Output.err(err, "failed to create temporary file to save lockfile\n{}", .{}); + Output.err(err, "failed to create temporary file to save lockfile", .{}); Global.crash(); }, .result => |f| f, }; - switch (file.writeAll(bytes.items)) { + switch (file.writeAll(bytes)) { .err => |e| { file.close(); _ = bun.sys.unlink(tmpname); - Output.err(e, "failed to write lockfile\n{}", .{}); + Output.err(e, "failed to write lockfile", .{}); Global.crash(); }, .result => {}, } if (comptime Environment.isPosix) { - // chmod 777 on posix - switch (bun.sys.fchmod(file.handle, 0o777)) { + // chmod 755 for binary, 644 for plaintext + var filemode: bun.Mode = 0o755; + if (save_format == .text) { + filemode = 0o644; + } + switch (bun.sys.fchmod(file.handle, filemode)) { .err => |err| { file.close(); _ = bun.sys.unlink(tmpname); - Output.err(err, "failed to change lockfile permissions\n{}", .{}); + Output.err(err, "failed to change lockfile permissions", .{}); Global.crash(); }, .result => {}, } } - file.closeAndMoveTo(tmpname, filename) catch |err| { + file.closeAndMoveTo(tmpname, save_format.filename()) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); // note: file is already closed here. @@ -2121,7 +2611,7 @@ pub fn getPackageID( const buf = this.buffers.string_bytes.items; switch (entry) { - .PackageID => |id| { + .id => |id| { if (comptime Environment.allow_assert) assert(id < resolutions.len); if (resolutions[id].eql(resolution, buf, buf)) { @@ -2132,7 +2622,7 @@ pub fn getPackageID( if (npm_version.?.satisfies(resolutions[id].value.npm.version, buf, buf)) return id; } }, - .PackageIDMultiple => |ids| { + .ids => |ids| { for (ids.items) |id| { if (comptime Environment.allow_assert) assert(id < resolutions.len); @@ -2150,14 +2640,83 @@ pub fn getPackageID( return null; } -pub fn getOrPutID(this: *Lockfile, id: PackageID, name_hash: PackageNameHash) !void { +/// Appends `pkg` to `this.packages` if a duplicate isn't found +pub fn appendPackageDedupe(this: *Lockfile, pkg: *Package, buf: string) OOM!PackageID { + const entry = try this.package_index.getOrPut(pkg.name_hash); + + if (!entry.found_existing) { + const new_id: PackageID = @intCast(this.packages.len); + pkg.meta.id = new_id; + try this.packages.append(this.allocator, pkg.*); + entry.value_ptr.* = .{ .id = new_id }; + return new_id; + } + + var resolutions = this.packages.items(.resolution); + + return switch (entry.value_ptr.*) { + .id => |existing_id| { + if (pkg.resolution.eql(&resolutions[existing_id], buf, buf)) { + pkg.meta.id = existing_id; + return existing_id; + } + + const new_id: PackageID = @intCast(this.packages.len); + pkg.meta.id = new_id; + try this.packages.append(this.allocator, pkg.*); + + resolutions = this.packages.items(.resolution); + + var ids = try PackageIDList.initCapacity(this.allocator, 8); + ids.items.len = 2; + + ids.items[0..2].* = if (pkg.resolution.order(&resolutions[existing_id], buf, buf) == .gt) + .{ new_id, existing_id } + else + .{ existing_id, new_id }; + + entry.value_ptr.* = .{ + .ids = ids, + }; + + return new_id; + }, + .ids => |*existing_ids| { + for (existing_ids.items) |existing_id| { + if (pkg.resolution.eql(&resolutions[existing_id], buf, buf)) { + pkg.meta.id = existing_id; + return existing_id; + } + } + + const new_id: PackageID = @intCast(this.packages.len); + pkg.meta.id = new_id; + try this.packages.append(this.allocator, pkg.*); + + resolutions = this.packages.items(.resolution); + + for (existing_ids.items, 0..) |existing_id, i| { + if (pkg.resolution.order(&resolutions[existing_id], buf, buf) == .gt) { + try existing_ids.insert(this.allocator, i, new_id); + return new_id; + } + } + + try existing_ids.append(this.allocator, new_id); + + return new_id; + }, + }; +} + +pub fn getOrPutID(this: *Lockfile, id: PackageID, name_hash: PackageNameHash) OOM!void { const gpe = try this.package_index.getOrPut(name_hash); if (gpe.found_existing) { const index: *PackageIndex.Entry = gpe.value_ptr; switch (index.*) { - .PackageID => |existing_id| { + .id => |existing_id| { var ids = try PackageIDList.initCapacity(this.allocator, 8); ids.items.len = 2; @@ -2170,10 +2729,10 @@ pub fn getOrPutID(this: *Lockfile, id: PackageID, name_hash: PackageNameHash) !v .{ existing_id, id }; index.* = .{ - .PackageIDMultiple = ids, + .ids = ids, }; }, - .PackageIDMultiple => |*existing_ids| { + .ids => |*existing_ids| { const resolutions = this.packages.items(.resolution); const buf = this.buffers.string_bytes.items; @@ -2189,16 +2748,16 @@ pub fn getOrPutID(this: *Lockfile, id: PackageID, name_hash: PackageNameHash) !v }, } } else { - gpe.value_ptr.* = .{ .PackageID = id }; + gpe.value_ptr.* = .{ .id = id }; } } -pub fn appendPackage(this: *Lockfile, package_: Lockfile.Package) !Lockfile.Package { +pub fn appendPackage(this: *Lockfile, package_: Lockfile.Package) OOM!Lockfile.Package { const id: PackageID = @truncate(this.packages.len); return try appendPackageWithID(this, package_, id); } -fn appendPackageWithID(this: *Lockfile, package_: Lockfile.Package, id: PackageID) !Lockfile.Package { +fn appendPackageWithID(this: *Lockfile, package_: Lockfile.Package, id: PackageID) OOM!Lockfile.Package { defer { if (comptime Environment.allow_assert) { assert(this.getPackageID(package_.name_hash, null, &package_.resolution) != null); @@ -2220,6 +2779,14 @@ pub inline fn stringBuilder(this: *Lockfile) Lockfile.StringBuilder { }; } +pub fn stringBuf(this: *Lockfile) String.Buf { + return .{ + .bytes = &this.buffers.string_bytes, + .allocator = this.allocator, + .pool = &this.string_pool, + }; +} + pub const Scratch = struct { pub const DuplicateCheckerMap = std.HashMap(PackageNameHash, logger.Loc, IdentityContext(PackageNameHash), 80); pub const DependencyQueue = std.fifo.LinearFifo(DependencySlice, .Dynamic); @@ -2369,12 +2936,12 @@ pub const StringBuilder = struct { pub const PackageIndex = struct { pub const Map = std.HashMap(PackageNameHash, PackageIndex.Entry, IdentityContext(PackageNameHash), 80); pub const Entry = union(Tag) { - PackageID: PackageID, - PackageIDMultiple: PackageIDList, + id: PackageID, + ids: PackageIDList, pub const Tag = enum(u8) { - PackageID = 0, - PackageIDMultiple = 1, + id = 0, + ids = 1, }; }; }; @@ -2389,7 +2956,7 @@ pub const OverrideMap = struct { map: std.ArrayHashMapUnmanaged(PackageNameHash, Dependency, ArrayIdentityContext.U64, false) = .{}, /// In the future, this `get` function should handle multi-level resolutions. This is difficult right - /// now because given a Dependency ID, there is no fast way to trace it to it's package. + /// now because given a Dependency ID, there is no fast way to trace it to its package. /// /// A potential approach is to add another buffer to the lockfile that maps Dependency ID to Package ID, /// and from there `OverrideMap.map` can have a union as the value, where the union is between "override all" @@ -2768,6 +3335,15 @@ pub const Package = extern struct { postprepare: String = .{}, filled: bool = false, + pub fn eql(l: *const Package.Scripts, r: *const Package.Scripts, l_buf: string, r_buf: string) bool { + return l.preinstall.eql(r.preinstall, l_buf, r_buf) and + l.install.eql(r.install, l_buf, r_buf) and + l.postinstall.eql(r.postinstall, l_buf, r_buf) and + l.preprepare.eql(r.preprepare, l_buf, r_buf) and + l.prepare.eql(r.prepare, l_buf, r_buf) and + l.postprepare.eql(r.postprepare, l_buf, r_buf); + } + pub const List = struct { items: [Lockfile.Scripts.names.len]?Lockfile.Scripts.Entry, first_index: u8, @@ -2918,7 +3494,7 @@ pub const Package = extern struct { } switch (resolution_tag) { - .git, .github, .gitlab, .root => { + .git, .github, .root => { const prepare_scripts = .{ "preprepare", "prepare", @@ -2988,7 +3564,7 @@ pub const Package = extern struct { .first_index = @intCast(first_index), .total = total, .cwd = allocator.dupeZ(u8, cwd) catch bun.outOfMemory(), - .package_name = package_name, + .package_name = lockfile.allocator.dupe(u8, package_name) catch bun.outOfMemory(), }; } @@ -3027,7 +3603,7 @@ pub const Package = extern struct { this: *Package.Scripts, log: *logger.Log, lockfile: *Lockfile, - node_modules: std.fs.Dir, + node_modules: *PackageManager.LazyPackageDestinationDir, abs_node_modules_path: string, folder_name: string, resolution: *const Resolution, @@ -3087,13 +3663,13 @@ pub const Package = extern struct { allocator: std.mem.Allocator, string_builder: *Lockfile.StringBuilder, log: *logger.Log, - node_modules: std.fs.Dir, + node_modules: *PackageManager.LazyPackageDestinationDir, folder_name: string, ) !void { const json = brk: { const json_src = brk2: { const json_path = bun.path.joinZ([_]string{ folder_name, "package.json" }, .auto); - const buf = try bun.sys.File.readFrom(node_modules, json_path, allocator).unwrap(); + const buf = try bun.sys.File.readFrom(try node_modules.getDir(), json_path, allocator).unwrap(); break :brk2 logger.Source.initPathString(json_path, buf); }; @@ -3115,7 +3691,7 @@ pub const Package = extern struct { this: *Package.Scripts, log: *logger.Log, lockfile: *Lockfile, - node_modules: std.fs.Dir, + node_modules: *PackageManager.LazyPackageDestinationDir, abs_folder_path: string, folder_name: string, resolution_tag: Resolution.Tag, @@ -3152,7 +3728,7 @@ pub const Package = extern struct { field: string, behavior: Behavior, - pub const dependencies = DependencyGroup{ .prop = "dependencies", .field = "dependencies", .behavior = Behavior.normal }; + pub const dependencies = DependencyGroup{ .prop = "dependencies", .field = "dependencies", .behavior = Behavior.prod }; pub const dev = DependencyGroup{ .prop = "devDependencies", .field = "dev_dependencies", .behavior = Behavior.dev }; pub const optional = DependencyGroup{ .prop = "optionalDependencies", .field = "optional_dependencies", .behavior = Behavior.optional }; pub const peer = DependencyGroup{ .prop = "peerDependencies", .field = "peer_dependencies", .behavior = Behavior.peer }; @@ -3542,13 +4118,23 @@ pub const Package = extern struct { const dep_version = string_builder.appendWithHash(String, version_string_.slice(string_buf), version_string_.hash); const sliced = dep_version.sliced(lockfile.buffers.string_bytes.items); + var behavior = group.behavior; + if (comptime is_peer) { + behavior.optional = i < package_version.non_optional_peer_dependencies_start; + } + if (package_version_ptr.allDependenciesBundled()) { + behavior.bundled = true; + } else for (package_version.bundled_dependencies.get(manifest.bundled_deps_buf)) |bundled_dep_name_hash| { + if (bundled_dep_name_hash == name.hash) { + behavior.bundled = true; + break; + } + } + const dependency = Dependency{ .name = name.value, .name_hash = name.hash, - .behavior = if (comptime is_peer) - group.behavior.setOptional(i < package_version.non_optional_peer_dependencies_start) - else - group.behavior, + .behavior = behavior, .version = Dependency.parse( allocator, name.value, @@ -3861,6 +4447,7 @@ pub const Package = extern struct { const json = pm.workspace_package_json_cache.getWithSource(bun.default_allocator, log, source, .{}).unwrap() catch break :brk false; + var resolver: void = {}; try workspace.parseWithJSON( to_lockfile, pm, @@ -3869,7 +4456,7 @@ pub const Package = extern struct { source, json.root, void, - {}, + &resolver, Features.workspace, ); @@ -3950,11 +4537,11 @@ pub const Package = extern struct { log: *logger.Log, source: logger.Source, comptime ResolverContext: type, - resolver: ResolverContext, + resolver: *ResolverContext, comptime features: Features, ) !void { initializeStore(); - const json = JSON.parsePackageJSONUTF8AlwaysDecode(&source, log, allocator) catch |err| { + const json = JSON.parsePackageJSONUTF8(&source, log, allocator) catch |err| { log.print(Output.errorWriter()) catch {}; Output.prettyErrorln("{s} parsing package.json in \"{s}\"", .{ @errorName(err), source.path.prettyDir() }); Global.crash(); @@ -4106,7 +4693,7 @@ pub const Package = extern struct { for (package_dependencies[0..dependencies_count]) |*dep| { if (dep.name_hash == name_hash and dep.version.tag == .workspace) { dep.* = .{ - .behavior = if (in_workspace) group.behavior.setWorkspace(true) else group.behavior, + .behavior = if (in_workspace) group.behavior.add(.workspace) else group.behavior, .name = external_alias.value, .name_hash = external_alias.hash, .version = dependency_version, @@ -4218,7 +4805,7 @@ pub const Package = extern struct { } const this_dep = Dependency{ - .behavior = if (in_workspace) group.behavior.setWorkspace(true) else group.behavior, + .behavior = if (in_workspace) group.behavior.add(.workspace) else group.behavior, .name = external_alias.value, .name_hash = external_alias.hash, .version = dependency_version, @@ -4397,7 +4984,7 @@ pub const Package = extern struct { if (input_path.len == 0 or input_path.len == 1 and input_path[0] == '.') continue; - if (bun.glob.detectGlobSyntax(input_path)) { + if (Glob.Ascii.detectGlobSyntax(input_path)) { workspace_globs.append(input_path) catch bun.outOfMemory(); continue; } @@ -4640,7 +5227,7 @@ pub const Package = extern struct { source: logger.Source, json: Expr, comptime ResolverContext: type, - resolver: ResolverContext, + resolver: *ResolverContext, comptime features: Features, ) !void { var string_builder = lockfile.stringBuilder(); @@ -4662,7 +5249,7 @@ pub const Package = extern struct { } // name is not validated by npm, so fallback to creating a new from the version literal - if (ResolverContext == *PackageManager.GitResolver) { + if (ResolverContext == PackageManager.GitResolver) { const resolution: *const Resolution = resolver.resolution; const repo = switch (resolution.tag) { .git => resolution.value.git, @@ -4952,7 +5539,7 @@ pub const Package = extern struct { const package_dependencies = lockfile.buffers.dependencies.items.ptr[off..total_len]; name: { - if (ResolverContext == *PackageManager.GitResolver) { + if (ResolverContext == PackageManager.GitResolver) { if (resolver.new_name.len != 0) { defer lockfile.allocator.free(resolver.new_name); const external_string = string_builder.append(ExternalString, resolver.new_name); @@ -5100,6 +5687,25 @@ pub const Package = extern struct { try lockfile.scratch.duplicate_checker_map.ensureTotalCapacity(total_dependencies_count); } + var bundled_deps = bun.StringSet.init(allocator); + defer bundled_deps.deinit(); + var bundle_all_deps = false; + if (comptime ResolverContext != void and ResolverContext.checkBundledDependencies()) { + if (json.get("bundleDependencies") orelse json.get("bundledDependencies")) |bundled_deps_expr| { + switch (bundled_deps_expr.data) { + .e_boolean => |boolean| { + bundle_all_deps = boolean.value; + }, + .e_array => |arr| { + for (arr.slice()) |item| { + try bundled_deps.insert(item.asString(allocator) orelse continue); + } + }, + else => {}, + } + } + } + total_dependencies_count = 0; const in_workspace = lockfile.workspace_paths.contains(package.name_hash); @@ -5200,7 +5806,7 @@ pub const Package = extern struct { )) |_dep| { var dep = _dep; if (group.behavior.isPeer() and optional_peer_dependencies.contains(external_name.hash)) { - dep.behavior = dep.behavior.setOptional(true); + dep.behavior = dep.behavior.add(.optional); } package_dependencies[total_dependencies_count] = dep; @@ -5243,7 +5849,11 @@ pub const Package = extern struct { )) |_dep| { var dep = _dep; if (group.behavior.isPeer() and optional_peer_dependencies.contains(external_name.hash)) { - dep.behavior = dep.behavior.setOptional(true); + dep.behavior.optional = true; + } + + if (bundle_all_deps or bundled_deps.contains(dep.name.slice(lockfile.buffers.string_bytes.items))) { + dep.behavior.bundled = true; } package_dependencies[total_dependencies_count] = dep; @@ -6324,7 +6934,7 @@ pub const Serializer = struct { lockfile.scratch = Lockfile.Scratch.init(allocator); lockfile.package_index = PackageIndex.Map.initContext(allocator, .{}); - lockfile.string_pool = StringPool.initContext(allocator, .{}); + lockfile.string_pool = StringPool.init(allocator); try lockfile.package_index.ensureTotalCapacity(@as(u32, @truncate(lockfile.packages.len))); if (!has_workspace_name_hashes) { @@ -6357,6 +6967,159 @@ pub const Serializer = struct { } }; +pub const EqlSorter = struct { + string_buf: string, + pkg_names: []const String, + + // Basically placement id + pub const PathToId = struct { + pkg_id: PackageID, + tree_path: string, + }; + + pub fn isLessThan(this: @This(), l: PathToId, r: PathToId) bool { + switch (strings.order(l.tree_path, r.tree_path)) { + .lt => return true, + .gt => return false, + .eq => {}, + } + + // they exist in the same tree, name can't be the same so string + // compare. + const l_name = this.pkg_names[l.pkg_id]; + const r_name = this.pkg_names[r.pkg_id]; + return l_name.order(&r_name, this.string_buf, this.string_buf) == .lt; + } +}; + +/// `cut_off_pkg_id` should be removed when we stop appending packages to lockfile during install step +pub fn eql(l: *const Lockfile, r: *const Lockfile, cut_off_pkg_id: usize, allocator: std.mem.Allocator) OOM!bool { + const l_hoisted_deps = l.buffers.hoisted_dependencies.items; + const r_hoisted_deps = r.buffers.hoisted_dependencies.items; + const l_string_buf = l.buffers.string_bytes.items; + const r_string_buf = r.buffers.string_bytes.items; + + const l_len = l_hoisted_deps.len; + const r_len = r_hoisted_deps.len; + + if (l_len != r_len) return false; + + const sort_buf = try allocator.alloc(EqlSorter.PathToId, l_len + r_len); + defer l.allocator.free(sort_buf); + var l_buf = sort_buf[0..l_len]; + var r_buf = sort_buf[r_len..]; + + var path_buf: bun.PathBuffer = undefined; + var depth_buf: Tree.DepthBuf = undefined; + + var i: usize = 0; + for (l.buffers.trees.items) |l_tree| { + const rel_path, _ = Tree.relativePathAndDepth(l, l_tree.id, &path_buf, &depth_buf, .pkg_path); + const tree_path = try allocator.dupe(u8, rel_path); + for (l_tree.dependencies.get(l_hoisted_deps)) |l_dep_id| { + if (l_dep_id == invalid_dependency_id) continue; + const l_pkg_id = l.buffers.resolutions.items[l_dep_id]; + if (l_pkg_id == invalid_package_id or l_pkg_id >= cut_off_pkg_id) continue; + l_buf[i] = .{ + .pkg_id = l_pkg_id, + .tree_path = tree_path, + }; + i += 1; + } + } + l_buf = l_buf[0..i]; + + i = 0; + for (r.buffers.trees.items) |r_tree| { + const rel_path, _ = Tree.relativePathAndDepth(r, r_tree.id, &path_buf, &depth_buf, .pkg_path); + const tree_path = try allocator.dupe(u8, rel_path); + for (r_tree.dependencies.get(r_hoisted_deps)) |r_dep_id| { + if (r_dep_id == invalid_dependency_id) continue; + const r_pkg_id = r.buffers.resolutions.items[r_dep_id]; + if (r_pkg_id == invalid_package_id or r_pkg_id >= cut_off_pkg_id) continue; + r_buf[i] = .{ + .pkg_id = r_pkg_id, + .tree_path = tree_path, + }; + i += 1; + } + } + r_buf = r_buf[0..i]; + + if (l_buf.len != r_buf.len) return false; + + const l_pkgs = l.packages.slice(); + const r_pkgs = r.packages.slice(); + const l_pkg_names = l_pkgs.items(.name); + const r_pkg_names = r_pkgs.items(.name); + + std.sort.pdq( + EqlSorter.PathToId, + l_buf, + EqlSorter{ + .pkg_names = l_pkg_names, + .string_buf = l_string_buf, + }, + EqlSorter.isLessThan, + ); + + std.sort.pdq( + EqlSorter.PathToId, + r_buf, + EqlSorter{ + .pkg_names = r_pkg_names, + .string_buf = r_string_buf, + }, + EqlSorter.isLessThan, + ); + + const l_pkg_name_hashes = l_pkgs.items(.name_hash); + const l_pkg_resolutions = l_pkgs.items(.resolution); + const l_pkg_bins = l_pkgs.items(.bin); + const l_pkg_scripts = l_pkgs.items(.scripts); + const r_pkg_name_hashes = r_pkgs.items(.name_hash); + const r_pkg_resolutions = r_pkgs.items(.resolution); + const r_pkg_bins = r_pkgs.items(.bin); + const r_pkg_scripts = r_pkgs.items(.scripts); + + const l_extern_strings = l.buffers.extern_strings.items; + const r_extern_strings = r.buffers.extern_strings.items; + + for (l_buf, r_buf) |l_ids, r_ids| { + const l_pkg_id = l_ids.pkg_id; + const r_pkg_id = r_ids.pkg_id; + if (l_pkg_name_hashes[l_pkg_id] != r_pkg_name_hashes[r_pkg_id]) { + return false; + } + const l_res = l_pkg_resolutions[l_pkg_id]; + const r_res = r_pkg_resolutions[r_pkg_id]; + + if (l_res.tag == .uninitialized or r_res.tag == .uninitialized) { + if (l_res.tag != r_res.tag) { + return false; + } + } else if (!l_res.eql(&r_res, l_string_buf, r_string_buf)) { + return false; + } + + if (!l_pkg_bins[l_pkg_id].eql( + &r_pkg_bins[r_pkg_id], + l_string_buf, + l_extern_strings, + r_string_buf, + r_extern_strings, + )) { + return false; + } + + if (!l_pkg_scripts[l_pkg_id].eql(&r_pkg_scripts[r_pkg_id], l_string_buf, r_string_buf)) { + return false; + } + } + + return true; +} + pub fn hasMetaHashChanged(this: *Lockfile, print_name_version_string: bool, packages_len: usize) !bool { const previous_meta_hash = this.meta_hash; this.meta_hash = try this.generateMetaHash(print_name_version_string, packages_len); @@ -6464,14 +7227,14 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool, packag return digest; } -pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Version) ?PackageID { +pub fn resolvePackageFromNameAndVersion(this: *Lockfile, package_name: []const u8, version: Dependency.Version) ?PackageID { const name_hash = String.Builder.stringHash(package_name); const entry = this.package_index.get(name_hash) orelse return null; const buf = this.buffers.string_bytes.items; switch (version.tag) { .npm => switch (entry) { - .PackageID => |id| { + .id => |id| { const resolutions = this.packages.items(.resolution); if (comptime Environment.allow_assert) assert(id < resolutions.len); @@ -6479,7 +7242,7 @@ pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Ve return id; } }, - .PackageIDMultiple => |ids| { + .ids => |ids| { const resolutions = this.packages.items(.resolution); for (ids.items) |id| { @@ -6569,7 +7332,6 @@ pub fn hasTrustedDependency(this: *Lockfile, name: []const u8) bool { pub fn jsonStringifyDependency(this: *const Lockfile, w: anytype, dep_id: DependencyID, dep: Dependency, res: PackageID) !void { const sb = this.buffers.string_bytes.items; - var buf: [2048]u8 = undefined; try w.beginObject(); defer w.endObject() catch {}; @@ -6598,7 +7360,7 @@ pub fn jsonStringifyDependency(this: *const Lockfile, w: anytype, dep_id: Depend try w.write(info.name.slice(sb)); try w.objectField("version"); - try w.write(try std.fmt.bufPrint(&buf, "{}", .{info.version.fmt(sb)})); + try w.print("\"{}\"", .{info.version.fmt(sb)}); }, .dist_tag => { try w.beginObject(); @@ -6692,7 +7454,6 @@ pub fn jsonStringifyDependency(this: *const Lockfile, w: anytype, dep_id: Depend } pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { - var buf: [2048]u8 = undefined; const sb = this.buffers.string_bytes.items; try w.beginObject(); defer w.endObject() catch {}; @@ -6711,14 +7472,14 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { while (iter.next()) |it| { const entry: PackageIndex.Entry = it.value_ptr.*; const first_id = switch (entry) { - .PackageID => |id| id, - .PackageIDMultiple => |ids| ids.items[0], + .id => |id| id, + .ids => |ids| ids.items[0], }; const name = this.packages.items(.name)[first_id].slice(sb); try w.objectField(name); switch (entry) { - .PackageID => |id| try w.write(id), - .PackageIDMultiple => |ids| { + .id => |id| try w.write(id), + .ids => |ids| { try w.beginArray(); for (ids.items) |id| { try w.write(id); @@ -6736,7 +7497,7 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { const dependencies = this.buffers.dependencies.items; const hoisted_deps = this.buffers.hoisted_dependencies.items; const resolutions = this.buffers.resolutions.items; - var depth_buf: Tree.Iterator.DepthBuf = undefined; + var depth_buf: Tree.DepthBuf = undefined; var path_buf: bun.PathBuffer = undefined; @memcpy(path_buf[0.."node_modules".len], "node_modules"); @@ -6754,11 +7515,11 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { @intCast(tree_id), &path_buf, &depth_buf, + .node_modules, ); try w.objectField("path"); - const formatted = try std.fmt.bufPrint(&buf, "{}", .{bun.fmt.fmtPath(u8, relative_path, .{ .path_sep = .posix })}); - try w.write(formatted); + try w.print("\"{}\"", .{bun.fmt.fmtPath(u8, relative_path, .{ .path_sep = .posix })}); try w.objectField("depth"); try w.write(depth); @@ -6832,12 +7593,10 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { try w.write(@tagName(res.tag)); try w.objectField("value"); - const formatted = try std.fmt.bufPrint(&buf, "{s}", .{res.fmt(sb, .posix)}); - try w.write(formatted); + try w.print("\"{s}\"", .{res.fmt(sb, .posix)}); try w.objectField("resolved"); - const formatted_url = try std.fmt.bufPrint(&buf, "{}", .{res.fmtURL(sb)}); - try w.write(formatted_url); + try w.print("\"{}\"", .{res.fmtURL(sb)}); } try w.objectField("dependencies"); @@ -6876,7 +7635,7 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { try w.objectField("integrity"); if (pkg.meta.integrity.tag != .unknown) { - try w.write(try std.fmt.bufPrint(&buf, "{}", .{pkg.meta.integrity})); + try w.print("\"{}\"", .{pkg.meta.integrity}); } else { try w.write(null); } @@ -6940,13 +7699,15 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { } } + var buf: [100]u8 = undefined; + try w.objectField("workspace_paths"); { try w.beginObject(); defer w.endObject() catch {}; for (this.workspace_paths.keys(), this.workspace_paths.values()) |k, v| { - try w.objectField(try std.fmt.bufPrint(&buf, "{d}", .{k})); + try w.objectField(std.fmt.bufPrintIntToSlice(&buf, k, 10, .lower, .{})); try w.write(v.slice(sb)); } } @@ -6956,8 +7717,8 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { defer w.endObject() catch {}; for (this.workspace_versions.keys(), this.workspace_versions.values()) |k, v| { - try w.objectField(try std.fmt.bufPrint(&buf, "{d}", .{k})); - try w.write(try std.fmt.bufPrint(&buf, "{}", .{v.fmt(sb)})); + try w.objectField(std.fmt.bufPrintIntToSlice(&buf, k, 10, .lower, .{})); + try w.print("\"{}\"", .{v.fmt(sb)}); } } } diff --git a/src/install/migration.zig b/src/install/migration.zig index 87dfc373ca..71ecc89e8f 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -10,6 +10,7 @@ const strings = bun.strings; const MutableString = bun.MutableString; const stringZ = bun.stringZ; const logger = bun.logger; +const File = bun.sys.File; const Install = @import("./install.zig"); const Resolution = @import("./resolution.zig").Resolution; @@ -25,7 +26,7 @@ const ExternalString = Semver.ExternalString; const stringHash = String.Builder.stringHash; const Lockfile = @import("./lockfile.zig"); -const LoadFromDiskResult = Lockfile.LoadFromDiskResult; +const LoadResult = Lockfile.LoadResult; const JSAst = bun.JSAst; const Expr = JSAst.Expr; @@ -38,32 +39,21 @@ const debug = Output.scoped(.migrate, false); pub fn detectAndLoadOtherLockfile( this: *Lockfile, + dir: bun.FD, manager: *Install.PackageManager, allocator: Allocator, log: *logger.Log, - bun_lockfile_path: stringZ, -) LoadFromDiskResult { - const dirname = bun_lockfile_path[0 .. strings.lastIndexOfChar(bun_lockfile_path, '/') orelse 0]; +) LoadResult { // check for package-lock.json, yarn.lock, etc... // if it exists, do an in-memory migration - var buf: bun.PathBuffer = undefined; - @memcpy(buf[0..dirname.len], dirname); npm: { - const npm_lockfile_name = "package-lock.json"; - @memcpy(buf[dirname.len .. dirname.len + npm_lockfile_name.len], npm_lockfile_name); - buf[dirname.len + npm_lockfile_name.len] = 0; var timer = std.time.Timer.start() catch unreachable; - const lockfile = bun.sys.openat( - bun.FD.cwd(), - buf[0 .. dirname.len + npm_lockfile_name.len :0], - bun.O.RDONLY, - 0, - ).unwrap() catch break :npm; - defer _ = bun.sys.close(lockfile); + const lockfile = File.openat(dir, "package-lock.json", bun.O.RDONLY, 0).unwrap() catch break :npm; + defer lockfile.close(); var lockfile_path_buf: bun.PathBuffer = undefined; - const lockfile_path = bun.getFdPathZ(lockfile, &lockfile_path_buf) catch break :npm; - const data = bun.sys.File.from(lockfile).readToEnd(allocator).unwrap() catch break :npm; + const lockfile_path = bun.getFdPathZ(lockfile.handle, &lockfile_path_buf) catch break :npm; + const data = lockfile.readToEnd(allocator).unwrap() catch break :npm; const migrate_result = migrateNPMLockfile(this, manager, allocator, log, data, lockfile_path) catch |err| { if (err == error.NPMLockfileVersionMismatch) { Output.prettyErrorln( @@ -73,7 +63,7 @@ pub fn detectAndLoadOtherLockfile( , .{}); Global.exit(1); } - if (Environment.allow_assert) { + if (Environment.isDebug) { bun.handleErrorReturnTrace(err, @errorReturnTrace()); Output.prettyErrorln("Error: {s}", .{@errorName(err)}); @@ -81,7 +71,12 @@ pub fn detectAndLoadOtherLockfile( Output.prettyErrorln("Invalid NPM package-lock.json\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); Global.exit(1); } - return LoadFromDiskResult{ .err = .{ .step = .migrating, .value = err } }; + return LoadResult{ .err = .{ + .step = .migrating, + .value = err, + .lockfile_path = "package-lock.json", + .format = .binary, + } }; }; if (migrate_result == .ok) { @@ -94,7 +89,7 @@ pub fn detectAndLoadOtherLockfile( return migrate_result; } - return LoadFromDiskResult{ .not_found = {} }; + return LoadResult{ .not_found = {} }; } const ResolvedURLsMap = bun.StringHashMapUnmanaged(string); @@ -130,7 +125,7 @@ pub fn migrateNPMLockfile( log: *logger.Log, data: string, abs_path: string, -) !LoadFromDiskResult { +) !LoadResult { debug("begin lockfile migration", .{}); this.initEmpty(allocator); @@ -156,10 +151,6 @@ pub fn migrateNPMLockfile( bun.Analytics.Features.lockfile_migration_from_package_lock += 1; // Count pass - var builder_ = this.stringBuilder(); - var builder = &builder_; - const name = (if (json.get("name")) |expr| expr.asString(allocator) else null) orelse ""; - builder.count(name); var root_package: *E.Object = undefined; var packages_properties = brk: { @@ -203,7 +194,7 @@ pub fn migrateNPMLockfile( json_array, &json_src, wksp.loc, - builder, + null, ); debug("found {d} workspace packages", .{workspace_packages_count}); num_deps += workspace_packages_count; @@ -275,20 +266,6 @@ pub fn migrateNPMLockfile( return error.InvalidNPMLockfile; } num_deps +|= @as(u32, deps.data.e_object.properties.len); - - for (deps.data.e_object.properties.slice()) |dep| { - const dep_name = dep.key.?.asString(allocator).?; - const version_string = dep.value.?.asString(allocator) orelse return error.InvalidNPMLockfile; - - builder.count(dep_name); - builder.count(version_string); - - // If it's a folder or workspace, pessimistically assume we will need a maximum path - switch (Dependency.Version.Tag.infer(version_string)) { - .folder, .workspace => builder.cap += bun.MAX_PATH_BYTES, - else => {}, - } - } } } @@ -296,51 +273,14 @@ pub fn migrateNPMLockfile( if (bin.data != .e_object) return error.InvalidNPMLockfile; switch (bin.data.e_object.properties.len) { 0 => return error.InvalidNPMLockfile, - 1 => { - const first_bin = bin.data.e_object.properties.at(0); - const key = first_bin.key.?.asString(allocator).?; - - const workspace_entry = if (workspace_map) |map| map.map.get(pkg_path) else null; - const is_workspace = workspace_entry != null; - - const pkg_name = if (is_workspace) - workspace_entry.?.name - else if (entry.value.?.get("name")) |set_name| - (set_name.asString(this.allocator) orelse return error.InvalidNPMLockfile) - else - packageNameFromPath(pkg_path); - - if (!strings.eql(key, pkg_name)) { - builder.count(key); - } - builder.count(first_bin.value.?.asString(allocator) orelse return error.InvalidNPMLockfile); - }, + 1 => {}, else => { - for (bin.data.e_object.properties.slice()) |bin_entry| { - builder.count(bin_entry.key.?.asString(allocator).?); - builder.count(bin_entry.value.?.asString(allocator) orelse return error.InvalidNPMLockfile); - } num_extern_strings += @truncate(bin.data.e_object.properties.len * 2); }, } } - if (pkg.get("resolved")) |resolved_expr| { - const resolved = resolved_expr.asString(allocator) orelse return error.InvalidNPMLockfile; - if (strings.hasPrefixComptime(resolved, "file:")) { - builder.count(resolved[5..]); - } else if (strings.hasPrefixComptime(resolved, "git+")) { - builder.count(resolved[4..]); - } else { - builder.count(resolved); - - // this is over-counting but whatever. it would be too hard to determine if the case here - // is an `npm`/`dist_tag` version (the only times this is actually used) - if (pkg.get("version")) |v| if (v.asString(allocator)) |s| { - builder.count(s); - }; - } - } else { + if (pkg.get("resolved") == null) { const version_prop = pkg.get("version"); const pkg_name = packageNameFromPath(pkg_path); if (version_prop != null and pkg_name.len > 0) { @@ -381,10 +321,7 @@ pub fn migrateNPMLockfile( remain = remain[version_str.len..]; remain[0..".tgz".len].* = ".tgz".*; - builder.count(resolved_url); try resolved_urls.put(allocator, pkg_path, resolved_url); - } else { - builder.count(pkg_path); } } } @@ -400,7 +337,10 @@ pub fn migrateNPMLockfile( try this.packages.ensureTotalCapacity(allocator, package_idx); // The package index is overallocated, but we know the upper bound try this.package_index.ensureTotalCapacity(package_idx); - try builder.allocate(); + + // dependency on `resolved`, a dependencies version tag might change, requiring + // new strings to be allocated. + var string_buf = this.stringBuf(); if (workspace_map) |wksp| { try this.workspace_paths.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); @@ -413,7 +353,7 @@ pub fn migrateNPMLockfile( bun.assert(!strings.containsChar(k, '\\')); } - this.workspace_paths.putAssumeCapacity(name_hash, builder.append(String, k)); + this.workspace_paths.putAssumeCapacity(name_hash, try string_buf.append(k)); if (v.version) |version_string| { const sliced_version = Semver.SlicedString.init(version_string, version_string); @@ -451,7 +391,7 @@ pub fn migrateNPMLockfile( // the package name is different. This package doesn't exist // in node_modules, but we still allow packages to resolve to it's // resolution. - path_entry.value_ptr.* = builder.append(String, resolved_str); + path_entry.value_ptr.* = try string_buf.append(resolved_str); if (wksp_entry.version) |version_string| { const sliced_version = Semver.SlicedString.init(version_string, version_string); @@ -494,15 +434,14 @@ pub fn migrateNPMLockfile( // Instead of calling this.appendPackage, manually append // the other function has some checks that will fail since we have not set resolution+dependencies yet. this.packages.appendAssumeCapacity(Lockfile.Package{ - .name = builder.appendWithHash(String, pkg_name, name_hash), + .name = try string_buf.appendWithHash(pkg_name, name_hash), .name_hash = name_hash, // For non workspace packages these are set to .uninitialized, then in the third phase // they are resolved. This is because the resolution uses the dependant's version // specifier as a "hint" to resolve the dependency. .resolution = if (is_workspace) Resolution.init(.{ - // This string is counted by `processWorkspaceNamesArray` - .workspace = builder.append(String, pkg_path), + .workspace = try string_buf.append(pkg_path), }) else Resolution{}, // we fill this data in later @@ -553,7 +492,7 @@ pub fn migrateNPMLockfile( } else .false, .integrity = if (pkg.get("integrity")) |integrity| - try Integrity.parse( + Integrity.parse( integrity.asString(this.allocator) orelse return error.InvalidNPMLockfile, ) @@ -576,7 +515,7 @@ pub fn migrateNPMLockfile( break :bin .{ .tag = .file, .value = Bin.Value.init(.{ - .file = builder.append(String, script_value), + .file = try string_buf.append(script_value), }), }; } @@ -585,8 +524,8 @@ pub fn migrateNPMLockfile( .tag = .named_file, .value = Bin.Value.init(.{ .named_file = .{ - builder.append(String, key), - builder.append(String, script_value), + try string_buf.append(key), + try string_buf.append(script_value), }, }), }; @@ -600,8 +539,8 @@ pub fn migrateNPMLockfile( for (bin.data.e_object.properties.slice()) |bin_entry| { const key = bin_entry.key.?.asString(this.allocator) orelse return error.InvalidNPMLockfile; const script_value = bin_entry.value.?.asString(this.allocator) orelse return error.InvalidNPMLockfile; - this.buffers.extern_strings.appendAssumeCapacity(builder.append(ExternalString, key)); - this.buffers.extern_strings.appendAssumeCapacity(builder.append(ExternalString, script_value)); + this.buffers.extern_strings.appendAssumeCapacity(try string_buf.appendExternal(key)); + this.buffers.extern_strings.appendAssumeCapacity(try string_buf.appendExternal(script_value)); } if (Environment.allow_assert) { @@ -724,8 +663,8 @@ pub fn migrateNPMLockfile( for (wksp.keys(), wksp.values()) |key, value| { const entry1 = id_map.get(key) orelse return error.InvalidNPMLockfile; const name_hash = stringHash(value.name); - const wksp_name = builder.append(String, value.name); - const wksp_path = builder.append(String, key); + const wksp_name = try string_buf.append(value.name); + const wksp_path = try string_buf.append(key); dependencies_buf[0] = Dependency{ .name = wksp_name, .name_hash = name_hash, @@ -736,9 +675,7 @@ pub fn migrateNPMLockfile( .workspace = wksp_path, }, }, - .behavior = .{ - .workspace = true, - }, + .behavior = Dependency.Behavior.workspace, }; resolutions_buf[0] = entry1.new_package_id; @@ -769,10 +706,10 @@ pub fn migrateNPMLockfile( const version_bytes = prop.value.?.asString(this.allocator) orelse return error.InvalidNPMLockfile; const name_hash = stringHash(name_bytes); - const dep_name = builder.appendWithHash(String, name_bytes, name_hash); + const dep_name = try string_buf.appendWithHash(name_bytes, name_hash); - const dep_version = builder.append(String, version_bytes); - const sliced = dep_version.sliced(this.buffers.string_bytes.items); + const dep_version = try string_buf.append(version_bytes); + const sliced = dep_version.sliced(string_buf.bytes.items); debug("parsing {s}, {s}\n", .{ name_bytes, version_bytes }); const version = Dependency.parse( @@ -829,7 +766,7 @@ pub fn migrateNPMLockfile( .name_hash = name_hash, .version = version, .behavior = .{ - .normal = dep_key == .dependencies, + .prod = dep_key == .dependencies, .optional = dep_key == .optionalDependencies, .dev = dep_key == .devDependencies, .peer = dep_key == .peerDependencies, @@ -846,11 +783,33 @@ pub fn migrateNPMLockfile( if (resolutions[id].tag == .uninitialized) { debug("resolving '{s}'", .{name_bytes}); + var res_version = version; + const res = resolved: { const dep_pkg = packages_properties.at(found.old_json_index).value.?.data.e_object; const dep_resolved: string = dep_resolved: { if (dep_pkg.get("resolved")) |resolved| { - break :dep_resolved resolved.asString(this.allocator) orelse return error.InvalidNPMLockfile; + const dep_resolved = resolved.asString(this.allocator) orelse return error.InvalidNPMLockfile; + switch (Dependency.Version.Tag.infer(dep_resolved)) { + .git, .github => |tag| { + const dep_resolved_str = try string_buf.append(dep_resolved); + const dep_resolved_sliced = dep_resolved_str.sliced(string_buf.bytes.items); + res_version = Dependency.parseWithTag( + this.allocator, + dep_name, + name_hash, + dep_resolved_sliced.slice, + tag, + &dep_resolved_sliced, + log, + manager, + ) orelse return error.InvalidNPMLockfile; + + break :dep_resolved dep_resolved; + }, + // TODO(dylan-conway): might need to handle more cases + else => break :dep_resolved dep_resolved, + } } if (version.tag == .npm) { @@ -860,14 +819,11 @@ pub fn migrateNPMLockfile( } break :resolved Resolution.init(.{ - .folder = builder.append( - String, - packages_properties.at(found.old_json_index).key.?.asString(allocator).?, - ), + .folder = try string_buf.append(packages_properties.at(found.old_json_index).key.?.asString(allocator).?), }); }; - break :resolved switch (version.tag) { + break :resolved switch (res_version.tag) { .uninitialized => std.debug.panic("Version string {s} resolved to `.uninitialized`", .{version_bytes}), .npm, .dist_tag => res: { // It is theoretically possible to hit this in a case where the resolved dependency is NOT @@ -880,25 +836,25 @@ pub fn migrateNPMLockfile( const dep_actual_version = (dep_pkg.get("version") orelse return error.InvalidNPMLockfile) .asString(this.allocator) orelse return error.InvalidNPMLockfile; - const dep_actual_version_str = builder.append(String, dep_actual_version); - const dep_actual_version_sliced = dep_actual_version_str.sliced(this.buffers.string_bytes.items); + const dep_actual_version_str = try string_buf.append(dep_actual_version); + const dep_actual_version_sliced = dep_actual_version_str.sliced(string_buf.bytes.items); break :res Resolution.init(.{ .npm = .{ - .url = builder.append(String, dep_resolved), + .url = try string_buf.append(dep_resolved), .version = Semver.Version.parse(dep_actual_version_sliced).version.min(), }, }); }, .tarball => if (strings.hasPrefixComptime(dep_resolved, "file:")) - Resolution.init(.{ .local_tarball = builder.append(String, dep_resolved[5..]) }) + Resolution.init(.{ .local_tarball = try string_buf.append(dep_resolved[5..]) }) else - Resolution.init(.{ .remote_tarball = builder.append(String, dep_resolved) }), - .folder => Resolution.init(.{ .folder = builder.append(String, dep_resolved) }), + Resolution.init(.{ .remote_tarball = try string_buf.append(dep_resolved) }), + .folder => Resolution.init(.{ .folder = try string_buf.append(dep_resolved) }), // not sure if this is possible to hit - .symlink => Resolution.init(.{ .folder = builder.append(String, dep_resolved) }), + .symlink => Resolution.init(.{ .folder = try string_buf.append(dep_resolved) }), .workspace => workspace: { - var input = builder.append(String, dep_resolved).sliced(this.buffers.string_bytes.items); + var input = (try string_buf.append(dep_resolved)).sliced(string_buf.bytes.items); if (strings.hasPrefixComptime(input.slice, "workspace:")) { input = input.sub(input.slice["workspace:".len..]); } @@ -908,17 +864,17 @@ pub fn migrateNPMLockfile( }, .git => res: { const str = (if (strings.hasPrefixComptime(dep_resolved, "git+")) - builder.append(String, dep_resolved[4..]) + try string_buf.append(dep_resolved[4..]) else - builder.append(String, dep_resolved)) - .sliced(this.buffers.string_bytes.items); + try string_buf.append(dep_resolved)) + .sliced(string_buf.bytes.items); const hash_index = strings.lastIndexOfChar(str.slice, '#') orelse return error.InvalidNPMLockfile; const commit = str.sub(str.slice[hash_index + 1 ..]).value(); break :res Resolution.init(.{ .git = .{ - .owner = version.value.git.owner, + .owner = res_version.value.git.owner, .repo = str.sub(str.slice[0..hash_index]).value(), .committish = commit, .resolved = commit, @@ -928,17 +884,17 @@ pub fn migrateNPMLockfile( }, .github => res: { const str = (if (strings.hasPrefixComptime(dep_resolved, "git+")) - builder.append(String, dep_resolved[4..]) + try string_buf.append(dep_resolved[4..]) else - builder.append(String, dep_resolved)) - .sliced(this.buffers.string_bytes.items); + try string_buf.append(dep_resolved)) + .sliced(string_buf.bytes.items); const hash_index = strings.lastIndexOfChar(str.slice, '#') orelse return error.InvalidNPMLockfile; const commit = str.sub(str.slice[hash_index + 1 ..]).value(); break :res Resolution.init(.{ .git = .{ - .owner = version.value.github.owner, + .owner = res_version.value.github.owner, .repo = str.sub(str.slice[0..hash_index]).value(), .committish = commit, .resolved = commit, @@ -948,7 +904,7 @@ pub fn migrateNPMLockfile( }, }; }; - debug("-> {}", .{res.fmtForDebug(this.buffers.string_bytes.items)}); + debug("-> {}", .{res.fmtForDebug(string_buf.bytes.items)}); resolutions[id] = res; metas[id].origin = switch (res.tag) { @@ -987,7 +943,7 @@ pub fn migrateNPMLockfile( .name_hash = name_hash, .version = version, .behavior = .{ - .normal = dep_key == .dependencies, + .prod = dep_key == .dependencies, .optional = true, .dev = dep_key == .devDependencies, .peer = dep_key == .peerDependencies, @@ -1061,16 +1017,7 @@ pub fn migrateNPMLockfile( return error.NotAllPackagesGotResolved; } - // if (Environment.isDebug) { - // const dump_file = try std.fs.cwd().createFileZ("before-clean.json", .{}); - // defer dump_file.close(); - // try std.json.stringify(this, .{ .whitespace = .indent_2 }, dump_file.writer()); - // } - - // This is definitely a memory leak, but it's fine because there is no install api, so this can only be leaked once per process. - // This operation is neccecary because callers of `loadFromDisk` assume the data is written into the passed `this`. - // You'll find that not cleaning the lockfile will cause `bun install` to not actually install anything since it doesnt have any hoisted trees. - this.* = (try this.cleanWithLogger(manager, &.{}, log, false, .silent)).*; + try this.resolve(log); // if (Environment.isDebug) { // const dump_file = try std.fs.cwd().createFileZ("after-clean.json", .{}); @@ -1084,11 +1031,13 @@ pub fn migrateNPMLockfile( this.meta_hash = try this.generateMetaHash(false, this.packages.len); - return LoadFromDiskResult{ + return LoadResult{ .ok = .{ .lockfile = this, .was_migrated = true, + .loaded_from_binary_lockfile = false, .serializer_result = .{}, + .format = .binary, }, }; } diff --git a/src/install/npm.zig b/src/install/npm.zig index 4c5210f86d..d344de30e5 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -9,6 +9,8 @@ const string = @import("../string_types.zig").string; const strings = @import("../string_immutable.zig"); const PackageManager = @import("./install.zig").PackageManager; const ExternalStringMap = @import("./install.zig").ExternalStringMap; +const ExternalPackageNameHashList = bun.install.ExternalPackageNameHashList; +const PackageNameHash = bun.install.PackageNameHash; const ExternalStringList = @import("./install.zig").ExternalStringList; const ExternalSlice = @import("./install.zig").ExternalSlice; const initializeStore = @import("./install.zig").initializeMiniStore; @@ -530,7 +532,7 @@ const ExternVersionMap = extern struct { } }; -fn Negatable(comptime T: type) type { +pub fn Negatable(comptime T: type) type { return struct { added: T = T.none, removed: T = T.none, @@ -578,6 +580,11 @@ fn Negatable(comptime T: type) type { return; } + if (strings.eqlComptime(str, "none")) { + this.had_unrecognized_values = true; + return; + } + const is_not = str[0] == '!'; const offset: usize = @intFromBool(is_not); @@ -593,6 +600,74 @@ fn Negatable(comptime T: type) type { this.* = .{ .added = @enumFromInt(@intFromEnum(this.added) | field), .removed = this.removed }; } } + + pub fn fromJson(allocator: std.mem.Allocator, expr: JSON.Expr) OOM!T { + var this = T.none.negatable(); + switch (expr.data) { + .e_array => |arr| { + const items = arr.slice(); + if (items.len > 0) { + for (items) |item| { + if (item.asString(allocator)) |value| { + this.apply(value); + } + } + } + }, + .e_string => |str| { + this.apply(str.data); + }, + else => {}, + } + + return this.combine(); + } + + /// writes to a one line json array with a trailing comma and space, or writes a string + pub fn toJson(field: T, writer: anytype) @TypeOf(writer).Error!void { + if (field == .none) { + // [] means everything, so unrecognized value + try writer.writeAll( + \\"none" + ); + return; + } + + const kvs = T.NameMap.kvs; + var removed: u8 = 0; + for (kvs) |kv| { + if (!field.has(kv.value)) { + removed += 1; + } + } + const included = kvs.len - removed; + const print_included = removed > kvs.len - removed; + + const one = (print_included and included == 1) or (!print_included and removed == 1); + + if (!one) { + try writer.writeAll("[ "); + } + + for (kvs) |kv| { + const has = field.has(kv.value); + if (has and print_included) { + try writer.print( + \\"{s}" + , .{kv.key}); + if (one) return; + try writer.writeAll(", "); + } else if (!has and !print_included) { + try writer.print( + \\"!{s}" + , .{kv.key}); + if (one) return; + try writer.writeAll(", "); + } + } + + try writer.writeByte(']'); + } }; } @@ -784,8 +859,6 @@ pub const Architecture = enum(u16) { } }; -const BigExternalString = Semver.BigExternalString; - pub const PackageVersion = extern struct { /// `"integrity"` field || `"shasum"` field /// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#dist @@ -807,6 +880,8 @@ pub const PackageVersion = extern struct { /// We keep it in the data layout so that if it turns out we do need it, we can add it without invalidating everyone's history. dev_dependencies: ExternalStringMap = ExternalStringMap{}, + bundled_dependencies: ExternalPackageNameHashList = .{}, + /// `"bin"` field in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bin) bin: Bin = Bin{}, @@ -836,10 +911,14 @@ pub const PackageVersion = extern struct { /// `hasInstallScript` field in registry API. has_install_script: bool = false, + + pub fn allDependenciesBundled(this: *const PackageVersion) bool { + return this.bundled_dependencies.isInvalid(); + } }; comptime { - if (@sizeOf(Npm.PackageVersion) != 224) { + if (@sizeOf(Npm.PackageVersion) != 232) { @compileError(std.fmt.comptimePrint("Npm.PackageVersion has unexpected size {d}", .{@sizeOf(Npm.PackageVersion)})); } } @@ -861,7 +940,6 @@ pub const NpmPackage = extern struct { versions_buf: VersionSlice = VersionSlice{}, string_lists_buf: ExternalStringList = ExternalStringList{}, - string_buf: BigExternalString = BigExternalString{}, }; pub const PackageManifest = struct { @@ -874,6 +952,7 @@ pub const PackageManifest = struct { external_strings_for_versions: []const ExternalString = &[_]ExternalString{}, package_versions: []const PackageVersion = &[_]PackageVersion{}, extern_strings_bin_entries: []const ExternalString = &[_]ExternalString{}, + bundled_deps_buf: []const PackageNameHash = &.{}, pub inline fn name(this: *const PackageManifest) string { return this.pkg.name.slice(this.string_buf); @@ -888,9 +967,10 @@ pub const PackageManifest = struct { } pub const Serializer = struct { - // - version 3: added serialization of registry url. it's used to invalidate when it changes - // - version 4: fixed bug with cpu & os tag not being added correctly - pub const version = "bun-npm-manifest-cache-v0.0.4\n"; + // - v0.0.3: added serialization of registry url. it's used to invalidate when it changes + // - v0.0.4: fixed bug with cpu & os tag not being added correctly + // - v0.0.5: added bundled dependencies + pub const version = "bun-npm-manifest-cache-v0.0.5\n"; const header_bytes: string = "#!/usr/bin/env bun\n" ++ version; pub const sizes = blk: { @@ -1035,7 +1115,7 @@ pub const PackageManifest = struct { else tmp_path; - var is_using_o_tmpfile = if (Environment.isLinux) false else {}; + var is_using_o_tmpfile = if (Environment.isLinux) false; const file = brk: { const flags = bun.O.WRONLY; const mask = if (Environment.isPosix) 0o664 else 0; @@ -1508,6 +1588,12 @@ pub const PackageManifest = struct { var optional_peer_dep_names = std.ArrayList(u64).init(default_allocator); defer optional_peer_dep_names.deinit(); + var bundled_deps_set = bun.StringSet.init(allocator); + defer bundled_deps_set.deinit(); + var bundle_all_deps = false; + + var bundled_deps_count: usize = 0; + var string_builder = String.Builder{ .string_pool = string_pool, }; @@ -1620,6 +1706,22 @@ pub const PackageManifest = struct { } } + bundled_deps_set.map.clearRetainingCapacity(); + bundle_all_deps = false; + if (prop.value.?.get("bundleDependencies") orelse prop.value.?.get("bundledDependencies")) |bundled_deps_expr| { + switch (bundled_deps_expr.data) { + .e_boolean => |boolean| { + bundle_all_deps = boolean.value; + }, + .e_array => |arr| { + for (arr.slice()) |bundled_dep| { + try bundled_deps_set.insert(bundled_dep.asString(allocator) orelse continue); + } + }, + else => {}, + } + } + inline for (dependency_groups) |pair| { if (prop.value.?.asProperty(pair.prop)) |versioned_deps| { if (versioned_deps.expr.data == .e_object) { @@ -1627,6 +1729,11 @@ pub const PackageManifest = struct { const properties = versioned_deps.expr.data.e_object.properties.slice(); for (properties) |property| { if (property.key.?.asString(allocator)) |key| { + if (!bundle_all_deps and bundled_deps_set.swapRemove(key)) { + // swap remove the dependency name because it could exist in + // multiple behavior groups. + bundled_deps_count += 1; + } string_builder.count(key); string_builder.count(property.value.?.asString(allocator) orelse ""); } @@ -1672,6 +1779,8 @@ pub const PackageManifest = struct { var all_extern_strings_bin_entries = extern_strings_bin_entries; var all_tarball_url_strings = try allocator.alloc(ExternalString, tarball_urls_count); var tarball_url_strings = all_tarball_url_strings; + const bundled_deps_buf = try allocator.alloc(PackageNameHash, bundled_deps_count); + var bundled_deps_offset: usize = 0; if (versioned_packages.len > 0) { const versioned_packages_bytes = std.mem.sliceAsBytes(versioned_packages); @@ -1756,72 +1865,34 @@ pub const PackageManifest = struct { } if (!parsed_version.valid) continue; + bundled_deps_set.map.clearRetainingCapacity(); + bundle_all_deps = false; + if (prop.value.?.get("bundleDependencies") orelse prop.value.?.get("bundledDependencies")) |bundled_deps_expr| { + switch (bundled_deps_expr.data) { + .e_boolean => |boolean| { + bundle_all_deps = boolean.value; + }, + .e_array => |arr| { + for (arr.slice()) |bundled_dep| { + try bundled_deps_set.insert(bundled_dep.asString(allocator) orelse continue); + } + }, + else => {}, + } + } + var package_version: PackageVersion = empty_version; if (prop.value.?.asProperty("cpu")) |cpu_q| { - var cpu = Architecture.none.negatable(); - - switch (cpu_q.expr.data) { - .e_array => |arr| { - const items = arr.slice(); - if (items.len > 0) { - for (items) |item| { - if (item.asString(allocator)) |cpu_str_| { - cpu.apply(cpu_str_); - } - } - } - }, - .e_string => |stri| { - cpu.apply(stri.data); - }, - else => {}, - } - package_version.cpu = cpu.combine(); + package_version.cpu = try Negatable(Architecture).fromJson(allocator, cpu_q.expr); } if (prop.value.?.asProperty("os")) |os_q| { - var os = OperatingSystem.none.negatable(); - - switch (os_q.expr.data) { - .e_array => |arr| { - const items = arr.slice(); - if (items.len > 0) { - for (items) |item| { - if (item.asString(allocator)) |cpu_str_| { - os.apply(cpu_str_); - } - } - } - }, - .e_string => |stri| { - os.apply(stri.data); - }, - else => {}, - } - package_version.os = os.combine(); + package_version.os = try Negatable(OperatingSystem).fromJson(allocator, os_q.expr); } if (prop.value.?.asProperty("libc")) |libc| { - var libc_ = Libc.none.negatable(); - - switch (libc.expr.data) { - .e_array => |arr| { - const items = arr.slice(); - if (items.len > 0) { - for (items) |item| { - if (item.asString(allocator)) |libc_str_| { - libc_.apply(libc_str_); - } - } - } - }, - .e_string => |stri| { - libc_.apply(stri.data); - }, - else => {}, - } - package_version.libc = libc_.combine(); + package_version.libc = try Negatable(Libc).fromJson(allocator, libc.expr); } if (prop.value.?.asProperty("hasInstallScript")) |has_install_script| { @@ -1973,7 +2044,7 @@ pub const PackageManifest = struct { if (dist.expr.asProperty("integrity")) |shasum| { if (shasum.expr.asString(allocator)) |shasum_str| { - package_version.integrity = Integrity.parse(shasum_str) catch Integrity{}; + package_version.integrity = Integrity.parse(shasum_str); if (package_version.integrity.tag.isSupported()) break :integrity; } } @@ -2023,6 +2094,8 @@ pub const PackageManifest = struct { } } + const bundled_deps_begin = bundled_deps_offset; + var i: usize = 0; for (items) |item| { @@ -2032,6 +2105,11 @@ pub const PackageManifest = struct { this_names[i] = string_builder.append(ExternalString, name_str); this_versions[i] = string_builder.append(ExternalString, version_str); + if (!bundle_all_deps and bundled_deps_set.swapRemove(name_str)) { + bundled_deps_buf[bundled_deps_offset] = this_names[i].hash; + bundled_deps_offset += 1; + } + if (comptime is_peer) { if (std.mem.indexOfScalar(u64, optional_peer_dep_names.items, this_names[i].hash) != null) { // For optional peer dependencies, we store a length instead of a whole separate array @@ -2068,6 +2146,15 @@ pub const PackageManifest = struct { count = i; + if (bundle_all_deps) { + package_version.bundled_dependencies = ExternalPackageNameHashList.invalid; + } else { + package_version.bundled_dependencies = ExternalPackageNameHashList.init( + bundled_deps_buf, + bundled_deps_buf[bundled_deps_begin..bundled_deps_offset], + ); + } + var name_list = ExternalStringList.init(all_extern_strings, this_names); var version_list = ExternalStringList.init(version_extern_strings, this_versions); @@ -2349,15 +2436,11 @@ pub const PackageManifest = struct { result.external_strings_for_versions = version_extern_strings; result.package_versions = versioned_packages; result.extern_strings_bin_entries = all_extern_strings_bin_entries[0 .. all_extern_strings_bin_entries.len - extern_strings_bin_entries.len]; + result.bundled_deps_buf = bundled_deps_buf; result.pkg.public_max_age = public_max_age; if (string_builder.ptr) |ptr| { result.string_buf = ptr[0..string_builder.len]; - result.pkg.string_buf = BigExternalString{ - .off = 0, - .len = @as(u32, @truncate(string_builder.len)), - .hash = 0, - }; } return result; diff --git a/src/install/patch_install.zig b/src/install/patch_install.zig index 3005a0c548..50e7eb49ec 100644 --- a/src/install/patch_install.zig +++ b/src/install/patch_install.zig @@ -9,6 +9,7 @@ const Environment = bun.Environment; const strings = bun.strings; const MutableString = bun.MutableString; const Progress = bun.Progress; +const String = bun.Semver.String; const logger = bun.logger; const Loc = logger.Loc; @@ -80,7 +81,7 @@ pub const PatchTask = struct { name_and_version_hash: u64, resolution: *const Resolution, patchfilepath: []const u8, - pkgname: []const u8, + pkgname: String, cache_dir: std.fs.Dir, cache_dir_subpath: stringZ, @@ -103,7 +104,6 @@ pub const PatchTask = struct { .apply => { this.manager.allocator.free(this.callback.apply.patchfilepath); this.manager.allocator.free(this.callback.apply.cache_dir_subpath); - this.manager.allocator.free(this.callback.apply.pkgname); if (this.callback.apply.install_context) |ictx| ictx.path.deinit(); this.callback.apply.logger.deinit(); }, @@ -211,12 +211,13 @@ pub const PatchTask = struct { }, .extract => { debug("pkg: {s} extract", .{pkg.name.slice(manager.lockfile.buffers.string_bytes.items)}); + + const task_id = Task.Id.forNPMPackage(manager.lockfile.str(&pkg.name), pkg.resolution.value.npm.version); + bun.debugAssert(!manager.network_dedupe_map.contains(task_id)); + const network_task = try manager.generateNetworkTaskForTarball( // TODO: not just npm package - Task.Id.forNPMPackage( - manager.lockfile.str(&pkg.name), - pkg.resolution.value.npm.version, - ), + task_id, url, manager.lockfile.buffers.dependencies.items[dep_id].behavior.isRequired(), dep_id, @@ -281,10 +282,10 @@ pub const PatchTask = struct { )) { .result => |txt| txt, .err => |e| { - try log.addErrorFmtOpts( + try log.addSysError( this.manager.allocator, - "failed to read patchfile: {}", - .{e.toSystemError()}, + e, + "failed to read patchfile", .{}, ); return; @@ -564,7 +565,7 @@ pub const PatchTask = struct { .name_and_version_hash = name_and_version_hash, .cache_dir = stuff.cache_dir, .patchfilepath = patchfilepath, - .pkgname = pkg_manager.allocator.dupe(u8, pkg_name.slice(pkg_manager.lockfile.buffers.string_bytes.items)) catch bun.outOfMemory(), + .pkgname = pkg_name, .logger = logger.Log.init(pkg_manager.allocator), // need to dupe this as it's calculated using // `PackageManager.cached_package_folder_name_buf` which may be diff --git a/src/install/repository.zig b/src/install/repository.zig index 848731b7cb..8e016bf965 100644 --- a/src/install/repository.zig +++ b/src/install/repository.zig @@ -17,6 +17,7 @@ const strings = @import("../string_immutable.zig"); const GitSHA = String; const Path = bun.path; const File = bun.sys.File; +const OOM = bun.OOM; threadlocal var final_path_buf: bun.PathBuffer = undefined; threadlocal var ssh_path_buf: bun.PathBuffer = undefined; @@ -181,6 +182,51 @@ pub const Repository = extern struct { .{ "gitlab", ".com" }, }); + pub fn parseAppendGit(input: string, buf: *String.Buf) OOM!Repository { + var remain = input; + if (strings.hasPrefixComptime(remain, "git+")) { + remain = remain["git+".len..]; + } + if (strings.lastIndexOfChar(remain, '#')) |hash| { + return .{ + .repo = try buf.append(remain[0..hash]), + .committish = try buf.append(remain[hash + 1 ..]), + }; + } + return .{ + .repo = try buf.append(remain), + }; + } + + pub fn parseAppendGithub(input: string, buf: *String.Buf) OOM!Repository { + var remain = input; + if (strings.hasPrefixComptime(remain, "github:")) { + remain = remain["github:".len..]; + } + var hash: usize = 0; + var slash: usize = 0; + for (remain, 0..) |c, i| { + switch (c) { + '/' => slash = i, + '#' => hash = i, + else => {}, + } + } + + const repo = if (hash == 0) remain[slash + 1 ..] else remain[slash + 1 .. hash]; + + var result: Repository = .{ + .owner = try buf.append(remain[0..slash]), + .repo = try buf.append(repo), + }; + + if (hash != 0) { + result.committish = try buf.append(remain[hash + 1 ..]); + } + + return result; + } + pub fn createDependencyNameFromVersionLiteral( allocator: std.mem.Allocator, repository: *const Repository, @@ -260,6 +306,14 @@ pub const Repository = extern struct { return try formatter.format(layout, opts, writer); } + pub fn fmt(this: *const Repository, label: string, buf: []const u8) Formatter { + return .{ + .repository = this, + .buf = buf, + .label = label, + }; + } + pub const Formatter = struct { label: []const u8 = "", buf: []const u8, diff --git a/src/install/resolution.zig b/src/install/resolution.zig index 2fecc21aff..d51829b1d4 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -10,6 +10,9 @@ const strings = @import("../string_immutable.zig"); const VersionedURL = @import("./versioned_url.zig").VersionedURL; const bun = @import("root").bun; const Path = bun.path; +const JSON = bun.JSON; +const OOM = bun.OOM; +const Dependency = bun.install.Dependency; pub const Resolution = extern struct { tag: Tag = .uninitialized, @@ -32,6 +35,74 @@ pub const Resolution = extern struct { return this.tag.canEnqueueInstallTask(); } + const FromTextLockfileError = OOM || error{ + UnexpectedResolution, + InvalidSemver, + }; + + pub fn fromTextLockfile(res_str: string, string_buf: *String.Buf) FromTextLockfileError!Resolution { + if (strings.hasPrefixComptime(res_str, "root:")) { + return Resolution.init(.{ .root = {} }); + } + + if (strings.withoutPrefixIfPossibleComptime(res_str, "link:")) |link| { + return Resolution.init(.{ .symlink = try string_buf.append(link) }); + } + + if (strings.withoutPrefixIfPossibleComptime(res_str, "workspace:")) |workspace| { + return Resolution.init(.{ .workspace = try string_buf.append(workspace) }); + } + + if (strings.withoutPrefixIfPossibleComptime(res_str, "file:")) |folder| { + return Resolution.init(.{ .folder = try string_buf.append(folder) }); + } + + return switch (Dependency.Version.Tag.infer(res_str)) { + .git => Resolution.init(.{ .git = try Repository.parseAppendGit(res_str, string_buf) }), + .github => Resolution.init(.{ .github = try Repository.parseAppendGithub(res_str, string_buf) }), + .tarball => { + if (Dependency.isRemoteTarball(res_str)) { + return Resolution.init(.{ .remote_tarball = try string_buf.append(res_str) }); + } + + return Resolution.init(.{ .local_tarball = try string_buf.append(res_str) }); + }, + .npm => { + const version_literal = try string_buf.append(res_str); + const parsed = Semver.Version.parse(version_literal.sliced(string_buf.bytes.items)); + + if (!parsed.valid) { + return error.UnexpectedResolution; + } + + if (parsed.version.major == null or parsed.version.minor == null or parsed.version.patch == null) { + return error.UnexpectedResolution; + } + + return .{ + .tag = .npm, + .value = .{ + .npm = .{ + .version = parsed.version.min(), + + // will fill this later + .url = .{}, + }, + }, + }; + }, + + // covered above + .workspace => error.UnexpectedResolution, + .symlink => error.UnexpectedResolution, + .folder => error.UnexpectedResolution, + + // should not happen + .dist_tag => error.UnexpectedResolution, + .uninitialized => error.UnexpectedResolution, + }; + } + pub fn order( lhs: *const Resolution, rhs: *const Resolution, @@ -52,7 +123,6 @@ pub const Resolution = extern struct { .single_file_module => lhs.value.single_file_module.order(&rhs.value.single_file_module, lhs_buf, rhs_buf), .git => lhs.value.git.order(&rhs.value.git, lhs_buf, rhs_buf), .github => lhs.value.github.order(&rhs.value.github, lhs_buf, rhs_buf), - .gitlab => lhs.value.gitlab.order(&rhs.value.gitlab, lhs_buf, rhs_buf), else => .eq, }; } @@ -68,7 +138,6 @@ pub const Resolution = extern struct { .single_file_module => builder.count(this.value.single_file_module.slice(buf)), .git => this.value.git.count(buf, Builder, builder), .github => this.value.github.count(buf, Builder, builder), - .gitlab => this.value.gitlab.count(buf, Builder, builder), else => {}, } } @@ -102,9 +171,6 @@ pub const Resolution = extern struct { .github => Value.init(.{ .github = this.value.github.clone(buf, Builder, builder), }), - .gitlab => Value.init(.{ - .gitlab = this.value.gitlab.clone(buf, Builder, builder), - }), .root => Value.init(.{ .root = {} }), else => { std.debug.panic("Internal error: unexpected resolution tag: {}", .{this.tag}); @@ -180,11 +246,6 @@ pub const Resolution = extern struct { lhs_string_buf, rhs_string_buf, ), - .gitlab => lhs.value.gitlab.eql( - &rhs.value.gitlab, - lhs_string_buf, - rhs_string_buf, - ), else => unreachable, }; } @@ -204,7 +265,6 @@ pub const Resolution = extern struct { .remote_tarball => try writer.writeAll(value.remote_tarball.slice(formatter.buf)), .git => try value.git.formatAs("git+", formatter.buf, layout, opts, writer), .github => try value.github.formatAs("github:", formatter.buf, layout, opts, writer), - .gitlab => try value.gitlab.formatAs("gitlab:", formatter.buf, layout, opts, writer), .workspace => try std.fmt.format(writer, "workspace:{s}", .{value.workspace.slice(formatter.buf)}), .symlink => try std.fmt.format(writer, "link:{s}", .{value.symlink.slice(formatter.buf)}), .single_file_module => try std.fmt.format(writer, "module:{s}", .{value.single_file_module.slice(formatter.buf)}), @@ -228,7 +288,6 @@ pub const Resolution = extern struct { .remote_tarball => try writer.writeAll(value.remote_tarball.slice(buf)), .git => try value.git.formatAs("git+", buf, layout, opts, writer), .github => try value.github.formatAs("github:", buf, layout, opts, writer), - .gitlab => try value.gitlab.formatAs("gitlab:", buf, layout, opts, writer), .workspace => try std.fmt.format(writer, "workspace:{s}", .{bun.fmt.fmtPath(u8, value.workspace.slice(buf), .{ .path_sep = formatter.path_sep, })}), @@ -256,7 +315,6 @@ pub const Resolution = extern struct { .remote_tarball => try writer.writeAll(formatter.resolution.value.remote_tarball.slice(formatter.buf)), .git => try formatter.resolution.value.git.formatAs("git+", formatter.buf, layout, opts, writer), .github => try formatter.resolution.value.github.formatAs("github:", formatter.buf, layout, opts, writer), - .gitlab => try formatter.resolution.value.gitlab.formatAs("gitlab:", formatter.buf, layout, opts, writer), .workspace => try std.fmt.format(writer, "workspace:{s}", .{formatter.resolution.value.workspace.slice(formatter.buf)}), .symlink => try std.fmt.format(writer, "link:{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}), .single_file_module => try std.fmt.format(writer, "module:{s}", .{formatter.resolution.value.single_file_module.slice(formatter.buf)}), @@ -282,7 +340,6 @@ pub const Resolution = extern struct { git: Repository, github: Repository, - gitlab: Repository, workspace: String, @@ -306,7 +363,6 @@ pub const Resolution = extern struct { local_tarball = 8, github = 16, - gitlab = 24, git = 32, @@ -338,7 +394,7 @@ pub const Resolution = extern struct { _, pub fn isGit(this: Tag) bool { - return this == .git or this == .github or this == .gitlab; + return this == .git or this == .github; } pub fn canEnqueueInstallTask(this: Tag) bool { diff --git a/src/install/resolvers/folder_resolver.zig b/src/install/resolvers/folder_resolver.zig index 30ff41f9c3..dceff73f4b 100644 --- a/src/install/resolvers/folder_resolver.zig +++ b/src/install/resolvers/folder_resolver.zig @@ -77,6 +77,10 @@ pub const FolderResolution = union(Tag) { pub fn count(this: @This(), comptime Builder: type, builder: Builder, _: JSAst.Expr) void { builder.count(this.folder_path); } + + pub fn checkBundledDependencies() bool { + return tag == .folder or tag == .symlink; + } }; } @@ -99,6 +103,10 @@ pub const FolderResolution = union(Tag) { } pub fn count(_: @This(), comptime Builder: type, _: Builder, _: JSAst.Expr) void {} + + pub fn checkBundledDependencies() bool { + return true; + } }; const Paths = struct { @@ -165,7 +173,7 @@ pub const FolderResolution = union(Tag) { version: Dependency.Version, comptime features: Features, comptime ResolverType: type, - resolver: ResolverType, + resolver: *ResolverType, ) !Lockfile.Package { var body = Npm.Registry.BodyPool.get(manager.allocator); defer Npm.Registry.BodyPool.release(body); @@ -200,7 +208,7 @@ pub const FolderResolution = union(Tag) { body.data.reset(); var man = body.data.list.toManaged(manager.allocator); defer body.data.list = man.moveToUnmanaged(); - _ = try file.readToEndWithArrayList(&man).unwrap(); + _ = try file.readToEndWithArrayList(&man, true).unwrap(); } break :brk logger.Source.initPathString(abs, body.data.list.items); @@ -262,45 +270,63 @@ pub const FolderResolution = union(Tag) { if (entry.found_existing) return entry.value_ptr.*; const package: Lockfile.Package = switch (global_or_relative) { - .global => brk: { + .global => global: { var path: bun.PathBuffer = undefined; std.mem.copyForwards(u8, &path, non_normalized_path); - break :brk readPackageJSONFromDisk( + var resolver: SymlinkResolver = .{ + .folder_path = path[0..non_normalized_path.len], + }; + break :global readPackageJSONFromDisk( manager, abs, version, Features.link, SymlinkResolver, - SymlinkResolver{ .folder_path = path[0..non_normalized_path.len] }, + &resolver, ); }, .relative => |tag| switch (tag) { - .folder => readPackageJSONFromDisk( - manager, - abs, - version, - Features.folder, - Resolver, - Resolver{ .folder_path = rel }, - ), - .workspace => readPackageJSONFromDisk( - manager, - abs, - version, - Features.workspace, - WorkspaceResolver, - WorkspaceResolver{ .folder_path = rel }, - ), + .folder => folder: { + var resolver: Resolver = .{ + .folder_path = rel, + }; + break :folder readPackageJSONFromDisk( + manager, + abs, + version, + Features.folder, + Resolver, + &resolver, + ); + }, + .workspace => workspace: { + var resolver: WorkspaceResolver = .{ + .folder_path = rel, + }; + break :workspace readPackageJSONFromDisk( + manager, + abs, + version, + Features.workspace, + WorkspaceResolver, + &resolver, + ); + }, else => unreachable, }, - .cache_folder => readPackageJSONFromDisk( - manager, - abs, - version, - Features.npm, - CacheFolderResolver, - CacheFolderResolver{ .version = version.value.npm.version.toVersion() }, - ), + .cache_folder => cache_folder: { + var resolver: CacheFolderResolver = .{ + .version = version.value.npm.version.toVersion(), + }; + break :cache_folder readPackageJSONFromDisk( + manager, + abs, + version, + Features.npm, + CacheFolderResolver, + &resolver, + ); + }, } catch |err| { if (err == error.FileNotFound or err == error.ENOENT) { entry.value_ptr.* = .{ .err = error.MissingPackageJSON }; diff --git a/src/install/semver.zig b/src/install/semver.zig index fc90129b67..a3f9ca2efd 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -12,6 +12,9 @@ const default_allocator = bun.default_allocator; const C = bun.C; const JSC = bun.JSC; const IdentityContext = @import("../identity_context.zig").IdentityContext; +const OOM = bun.OOM; +const TruncatedPackageNameHash = bun.install.TruncatedPackageNameHash; +const Lockfile = bun.install.Lockfile; /// String type that stores either an offset/length into an external buffer or a string inline directly pub const String = extern struct { @@ -35,6 +38,103 @@ pub const String = extern struct { return String.init(inlinable_buffer, inlinable_buffer); } + pub const Buf = struct { + bytes: *std.ArrayListUnmanaged(u8), + allocator: std.mem.Allocator, + pool: *Builder.StringPool, + + pub fn init(lockfile: *const Lockfile) Buf { + return .{ + .bytes = &lockfile.buffers.string_bytes, + .allocator = lockfile.allocator, + .pool = &lockfile.string_pool, + }; + } + + pub fn append(this: *Buf, str: string) OOM!String { + if (canInline(str)) { + return String.initInline(str); + } + + const hash = Builder.stringHash(str); + const entry = try this.pool.getOrPut(hash); + if (entry.found_existing) { + return entry.value_ptr.*; + } + + // new entry + const new = try String.initAppend(this.allocator, this.bytes, str); + entry.value_ptr.* = new; + return new; + } + + pub fn appendWithHash(this: *Buf, str: string, hash: u64) OOM!String { + if (canInline(str)) { + return initInline(str); + } + + const entry = try this.pool.getOrPut(hash); + if (entry.found_existing) { + return entry.value_ptr.*; + } + + // new entry + const new = try String.initAppend(this.allocator, this.bytes, str); + entry.value_ptr.* = new; + return new; + } + + pub fn appendExternal(this: *Buf, str: string) OOM!ExternalString { + const hash = Builder.stringHash(str); + + if (canInline(str)) { + return .{ + .value = String.initInline(str), + .hash = hash, + }; + } + + const entry = try this.pool.getOrPut(hash); + if (entry.found_existing) { + return .{ + .value = entry.value_ptr.*, + .hash = hash, + }; + } + + const new = try String.initAppend(this.allocator, this.bytes, str); + entry.value_ptr.* = new; + return .{ + .value = new, + .hash = hash, + }; + } + + pub fn appendExternalWithHash(this: *Buf, str: string, hash: u64) OOM!ExternalString { + if (canInline(str)) { + return .{ + .value = initInline(str), + .hash = hash, + }; + } + + const entry = try this.pool.getOrPut(hash); + if (entry.found_existing) { + return .{ + .value = entry.value_ptr.*, + .hash = hash, + }; + } + + const new = try String.initAppend(this.allocator, this.bytes, str); + entry.value_ptr.* = new; + return .{ + .value = new, + .hash = hash, + }; + } + }; + pub const Tag = enum { small, big, @@ -57,6 +157,29 @@ pub const String = extern struct { } }; + /// Escapes for json. Expects string to be prequoted + pub inline fn fmtJson(self: *const String, buf: []const u8, opts: JsonFormatter.Options) JsonFormatter { + return .{ + .buf = buf, + .str = self, + .opts = opts, + }; + } + + pub const JsonFormatter = struct { + str: *const String, + buf: string, + opts: Options, + + pub const Options = struct { + quote: bool = true, + }; + + pub fn format(formatter: JsonFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("{}", .{bun.fmt.formatJSONStringUTF8(formatter.str.slice(formatter.buf), .{ .quote = formatter.opts.quote })}); + } + }; + pub fn Sorter(comptime direction: enum { asc, desc }) type { return struct { lhs_buf: []const u8, @@ -187,6 +310,62 @@ pub const String = extern struct { }; } + pub fn initInline( + in: string, + ) String { + bun.assertWithLocation(canInline(in), @src()); + return switch (in.len) { + 0 => .{}, + 1 => .{ .bytes = .{ in[0], 0, 0, 0, 0, 0, 0, 0 } }, + 2 => .{ .bytes = .{ in[0], in[1], 0, 0, 0, 0, 0, 0 } }, + 3 => .{ .bytes = .{ in[0], in[1], in[2], 0, 0, 0, 0, 0 } }, + 4 => .{ .bytes = .{ in[0], in[1], in[2], in[3], 0, 0, 0, 0 } }, + 5 => .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], 0, 0, 0 } }, + 6 => .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], 0, 0 } }, + 7 => .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], in[6], 0 } }, + 8 => .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7] } }, + else => unreachable, + }; + } + + pub fn initAppendIfNeeded( + allocator: std.mem.Allocator, + buf: *std.ArrayListUnmanaged(u8), + in: string, + ) OOM!String { + return switch (in.len) { + 0 => .{}, + 1 => .{ .bytes = .{ in[0], 0, 0, 0, 0, 0, 0, 0 } }, + 2 => .{ .bytes = .{ in[0], in[1], 0, 0, 0, 0, 0, 0 } }, + 3 => .{ .bytes = .{ in[0], in[1], in[2], 0, 0, 0, 0, 0 } }, + 4 => .{ .bytes = .{ in[0], in[1], in[2], in[3], 0, 0, 0, 0 } }, + 5 => .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], 0, 0, 0 } }, + 6 => .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], 0, 0 } }, + 7 => .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], in[6], 0 } }, + + max_inline_len => + // If they use the final bit, then it's a big string. + // This should only happen for non-ascii strings that are exactly 8 bytes. + // so that's an edge-case + if ((in[max_inline_len - 1]) >= 128) + try initAppend(allocator, buf, in) + else + .{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7] } }, + + else => try initAppend(allocator, buf, in), + }; + } + + pub fn initAppend( + allocator: std.mem.Allocator, + buf: *std.ArrayListUnmanaged(u8), + in: string, + ) OOM!String { + try buf.appendSlice(allocator, in); + const in_buf = buf.items[buf.items.len - in.len ..]; + return @bitCast((@as(u64, 0) | @as(u64, @as(max_addressable_space, @truncate(@as(u64, @bitCast(Pointer.init(buf.items, in_buf))))))) | 1 << 63); + } + pub fn eql(this: String, that: String, this_buf: []const u8, that_buf: []const u8) bool { if (this.isInline() and that.isInline()) { return @as(u64, @bitCast(this.bytes)) == @as(u64, @bitCast(that.bytes)); @@ -525,34 +704,6 @@ pub const ExternalString = extern struct { } }; -pub const BigExternalString = extern struct { - off: u32 = 0, - len: u32 = 0, - hash: u64 = 0, - - pub fn from(in: string) BigExternalString { - return BigExternalString{ - .off = 0, - .len = @as(u32, @truncate(in.len)), - .hash = bun.Wyhash.hash(0, in), - }; - } - - pub inline fn init(buf: string, in: string, hash: u64) BigExternalString { - assert(@intFromPtr(buf.ptr) <= @intFromPtr(in.ptr) and ((@intFromPtr(in.ptr) + in.len) <= (@intFromPtr(buf.ptr) + buf.len))); - - return BigExternalString{ - .off = @as(u32, @truncate(@intFromPtr(in.ptr) - @intFromPtr(buf.ptr))), - .len = @as(u32, @truncate(in.len)), - .hash = hash, - }; - } - - pub fn slice(this: BigExternalString, buf: string) string { - return buf[this.off..][0..this.len]; - } -}; - pub const SlicedString = struct { buf: string, slice: string, @@ -592,14 +743,12 @@ pub const SlicedString = struct { } }; -const RawType = void; pub const Version = extern struct { major: u32 = 0, minor: u32 = 0, patch: u32 = 0, _tag_padding: [4]u8 = .{0} ** 4, // [see padding_checker.zig] tag: Tag = .{}, - // raw: RawType = RawType{}, /// Assumes that there is only one buffer for all the strings pub fn sortGt(ctx: []const u8, lhs: Version, rhs: Version) bool { @@ -1286,9 +1435,7 @@ pub const Version = extern struct { .none => {}, .pre => { result.tag.pre = sliced_string.sub(input[start..i]).external(); - if (comptime Environment.isDebug) { - assert(!strings.containsChar(result.tag.pre.slice(sliced_string.buf), '-')); - } + state = State.none; }, .build => { @@ -1540,10 +1687,6 @@ pub const Version = extern struct { result.len = @as(u32, @intCast(i)); - if (comptime RawType != void) { - result.version.raw = sliced_string.sub(input[0..i]).external(); - } - return result; } @@ -2064,7 +2207,7 @@ pub const Query = struct { }; } - pub const FlagsBitSet = std.bit_set.IntegerBitSet(3); + pub const FlagsBitSet = bun.bit_set.IntegerBitSet(3); pub fn isExact(this: *const Group) bool { return this.head.next == null and this.head.head.next == null and !this.head.head.range.hasRight() and this.head.head.range.left.op == .eql; diff --git a/src/install/versioned_url.zig b/src/install/versioned_url.zig index 0a06856d7c..ac31f98ece 100644 --- a/src/install/versioned_url.zig +++ b/src/install/versioned_url.zig @@ -13,10 +13,6 @@ pub const VersionedURL = extern struct { return this.version.order(other.version, lhs_buf, rhs_buf); } - pub fn fmt(this: VersionedURL, buf: []const u8) Semver.Version.Formatter { - return this.version.fmt(buf); - } - pub fn count(this: VersionedURL, buf: []const u8, comptime Builder: type, builder: Builder) void { this.version.count(buf, comptime Builder, builder); builder.count(this.url.slice(buf)); diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index c8cf07e09d..7f4716d661 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -37,7 +37,7 @@ //! The compiled binary is 13312 bytes and is `@embedFile`d into Bun itself. //! When this file is updated, the new binary should be compiled and BinLinkingShim.VersionFlag.current should be updated. //! -//! Questions about this file should be directed at @paperdave. +//! Questions about this file should be directed at @paperclover. const builtin = @import("builtin"); const dbg = builtin.mode == .Debug; diff --git a/src/io/PipeReader.zig b/src/io/PipeReader.zig index b7b8ebfbdc..668f7b55fe 100644 --- a/src/io/PipeReader.zig +++ b/src/io/PipeReader.zig @@ -704,6 +704,10 @@ const PosixBufferedReader = struct { return this.flags.is_done or this.flags.received_eof or this.flags.closed_without_reporting; } + pub fn memoryCost(this: *const PosixBufferedReader) usize { + return @sizeOf(@This()) + this._buffer.capacity; + } + pub fn from(to: *@This(), other: *PosixBufferedReader, parent_: *anyopaque) void { to.* = .{ .handle = other.handle, @@ -806,7 +810,7 @@ const PosixBufferedReader = struct { pub fn finalBuffer(this: *PosixBufferedReader) *std.ArrayList(u8) { if (this.flags.memfd and this.handle == .fd) { defer this.handle.close(null, {}); - _ = bun.sys.File.readToEndWithArrayList(.{ .handle = this.handle.fd }, this.buffer()).unwrap() catch |err| { + _ = bun.sys.File.readToEndWithArrayList(.{ .handle = this.handle.fd }, this.buffer(), false).unwrap() catch |err| { bun.Output.debugWarn("error reading from memfd\n{}", .{err}); return this.buffer(); }; @@ -972,6 +976,10 @@ pub const WindowsBufferedReader = struct { const WindowsOutputReader = @This(); + pub fn memoryCost(this: *const WindowsOutputReader) usize { + return @sizeOf(@This()) + this._buffer.capacity; + } + const Flags = packed struct { is_done: bool = false, pollable: bool = false, diff --git a/src/io/PipeWriter.zig b/src/io/PipeWriter.zig index fd41ae637c..622fda1527 100644 --- a/src/io/PipeWriter.zig +++ b/src/io/PipeWriter.zig @@ -199,6 +199,10 @@ pub fn PosixBufferedWriter( closed_without_reporting: bool = false, close_fd: bool = true, + pub fn memoryCost(_: *const @This()) usize { + return @sizeOf(@This()); + } + const PosixWriter = @This(); pub const auto_poll = if (@hasDecl(Parent, "auto_poll")) Parent.auto_poll else true; @@ -393,6 +397,10 @@ pub fn PosixStreamingWriter( // TODO: chunk_size: usize = 0, + pub fn memoryCost(this: *const @This()) usize { + return @sizeOf(@This()) + this.buffer.capacity; + } + pub fn getPoll(this: *@This()) ?*Async.FilePoll { return this.handle.getPoll(); } @@ -909,6 +917,10 @@ pub fn WindowsBufferedWriter( } } + pub fn memoryCost(this: *const WindowsWriter) usize { + return @sizeOf(@This()) + this.write_buffer.len; + } + pub fn startWithCurrentPipe(this: *WindowsWriter) bun.JSC.Maybe(void) { bun.assert(this.source != null); this.is_done = false; @@ -1025,6 +1037,10 @@ pub const StreamBuffer = struct { this.list.clearRetainingCapacity(); } + pub fn memoryCost(this: *const StreamBuffer) usize { + return this.list.capacity; + } + pub fn size(this: *const StreamBuffer) usize { return this.list.items.len - this.cursor; } @@ -1153,6 +1169,10 @@ pub fn WindowsStreamingWriter( pub usingnamespace BaseWindowsPipeWriter(WindowsWriter, Parent); + pub fn memoryCost(this: *const WindowsWriter) usize { + return @sizeOf(@This()) + this.current_payload.memoryCost() + this.outgoing.memoryCost(); + } + fn onCloseSource(this: *WindowsWriter) void { this.source = null; if (this.closed_without_reporting) { diff --git a/src/io/fifo.zig b/src/io/fifo.zig deleted file mode 100644 index e9a7ab1fab..0000000000 --- a/src/io/fifo.zig +++ /dev/null @@ -1,57 +0,0 @@ -const std = @import("std"); -const bun = @import("root").bun; -const assert = bun.assert; - -/// An intrusive first in/first out linked list. -/// The element type T must have a field called "next" of type ?*T -pub fn FIFO(comptime T: type) type { - return struct { - const Self = @This(); - - in: ?*T = null, - out: ?*T = null, - - pub fn push(self: *Self, elem: *T) void { - assert(elem.next == null); - if (self.in) |in| { - in.next = elem; - self.in = elem; - } else { - assert(self.out == null); - self.in = elem; - self.out = elem; - } - } - - pub fn pop(self: *Self) ?*T { - const ret = self.out orelse return null; - self.out = ret.next; - ret.next = null; - if (self.in == ret) self.in = null; - return ret; - } - - pub fn peek(self: Self) ?*T { - return self.out; - } - - /// Remove an element from the FIFO. Asserts that the element is - /// in the FIFO. This operation is O(N), if this is done often you - /// probably want a different data structure. - pub fn remove(self: *Self, to_remove: *T) void { - if (to_remove == self.out) { - _ = self.pop(); - return; - } - var it = self.out; - while (it) |elem| : (it = elem.next) { - if (to_remove == elem.next) { - if (to_remove == self.in) self.in = elem; - elem.next = to_remove.next; - to_remove.next = null; - break; - } - } else unreachable; - } - }; -} diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 2c07c8aa0e..65b425d4fd 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -10,8 +10,14 @@ type TODO = any; * This only works in debug builds, the log fn is completely removed in release builds. */ declare function $debug(...args: any[]): void; -/** $assert is a preprocessor macro that only runs in debug mode. it throws an error if the first argument is falsy. - * The source code passed to `check` is inlined in the message, but in addition you can pass additional messages. +/** + * Assert that a condition holds in debug builds. + * + * $assert is a preprocessor macro that only runs in debug mode. it throws an + * error if the first argument is falsy. The source code passed to `check` is + * inlined in the message, but in addition you can pass additional messages. + * + * @note gets removed in release builds. Do not put code with side effects in the `check`. */ declare function $assert(check: any, ...message: any[]): asserts check; @@ -67,11 +73,15 @@ declare function $getByIdDirectPrivate(obj: any, key: string): T; declare function $getByValWithThis(target: any, receiver: any, propertyKey: string): void; /** gets the prototype of an object */ declare function $getPrototypeOf(value: any): any; -/** gets an internal property on a promise +/** + * Gets an internal property on a promise * * You can pass - * - $promiseFieldFlags - get a number with flags - * - $promiseFieldReactionsOrResult - get the result (like Bun.peek) + * - {@link $promiseFieldFlags} - get a number with flags + * - {@link $promiseFieldReactionsOrResult} - get the result (like {@link Bun.peek}) + * + * @param promise the promise to get the field from + * @param key an internal field id. */ declare function $getPromiseInternalField( promise: Promise, @@ -93,6 +103,19 @@ declare function $getMapIteratorInternalField(): TODO; declare function $getSetIteratorInternalField(): TODO; declare function $getProxyInternalField(): TODO; declare function $idWithProfile(): TODO; +/** + * True for object-like `JSCell`s. That is, this is roughly equivalent to this + * JS code: + * ```js + * typeof obj === "object" && obj !== null + * ``` + * + * @param obj The object to check + * @returns `true` if `obj` is an object-like `JSCell` + * + * @see [JSCell.h](https://github.com/oven-sh/WebKit/blob/main/Source/JavaScriptCore/runtime/JSCell.h) + * @see [JIT implementation](https://github.com/oven-sh/WebKit/blob/433f7598bf3537a295d0af5ffd83b9a307abec4e/Source/JavaScriptCore/jit/JITOpcodes.cpp#L311) + */ declare function $isObject(obj: unknown): obj is object; declare function $isArray(obj: unknown): obj is any[]; declare function $isCallable(fn: unknown): fn is CallableFunction; @@ -161,8 +184,26 @@ declare function $toPropertyKey(x: any): PropertyKey; * `$toObject(this, "Class.prototype.method requires that |this| not be null or undefined");` */ declare function $toObject(object: any, errorMessage?: string): object; +/** + * ## References + * - [WebKit - `emit_intrinsic_newArrayWithSize`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2317) + */ declare function $newArrayWithSize(size: number): T[]; -declare function $newArrayWithSpecies(): TODO; +/** + * Optimized path for creating a new array storing objects with the same homogenous Structure + * as {@link array}. + * + * @param size the initial size of the new array + * @param array the array whose shape we want to copy + * + * @returns a new array + * + * ## References + * - [WebKit - `emit_intrinsic_newArrayWithSpecies`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2328) + * - [WebKit - #4909](https://github.com/WebKit/WebKit/pull/4909) + * - [WebKit Bugzilla - Related Issue/Ticket](https://bugs.webkit.org/show_bug.cgi?id=245797) + */ +declare function $newArrayWithSpecies(size: number, array: T[]): T[]; declare function $newPromise(): TODO; declare function $createPromise(): TODO; declare const $iterationKindKey: TODO; @@ -331,7 +372,6 @@ declare function $localStreams(): TODO; declare function $main(): TODO; declare function $makeDOMException(): TODO; declare function $makeGetterTypeError(className: string, prop: string): Error; -declare function $makeThisTypeError(className: string, method: string): Error; declare function $map(): TODO; declare function $method(): TODO; declare function $nextTick(): TODO; @@ -536,6 +576,11 @@ declare interface Function { path: string; } +interface String { + $charCodeAt: String["charCodeAt"]; + // add others as needed +} + declare var $Buffer: { new (a: any, b?: any, c?: any): Buffer; }; @@ -544,11 +589,44 @@ declare interface Error { code?: string; } +declare function $makeAbortError(message?: string, options?: { cause: Error }): Error; + /** * -- Error Codes with manual messages */ declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedType: string, actualValue: string): TypeError; declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedTypes: any[], actualValue: string): TypeError; +declare function $ERR_INVALID_ARG_VALUE(name: string, value: any, reason?: string): TypeError; +declare function $ERR_UNKNOWN_ENCODING(enc: string): TypeError; +declare function $ERR_STREAM_DESTROYED(method: string): Error; +declare function $ERR_METHOD_NOT_IMPLEMENTED(method: string): Error; +declare function $ERR_STREAM_ALREADY_FINISHED(method: string): Error; +declare function $ERR_MISSING_ARGS(a1: string, a2?: string): TypeError; +declare function $ERR_INVALID_RETURN_VALUE(expected_type: string, name: string, actual_value: any): TypeError; + +declare function $ERR_IPC_DISCONNECTED(): Error; +declare function $ERR_SERVER_NOT_RUNNING(): Error; +declare function $ERR_IPC_CHANNEL_CLOSED(): Error; +declare function $ERR_SOCKET_BAD_TYPE(): Error; +declare function $ERR_ZLIB_INITIALIZATION_FAILED(): Error; +declare function $ERR_BUFFER_OUT_OF_BOUNDS(): Error; +declare function $ERR_IPC_ONE_PIPE(): Error; +declare function $ERR_SOCKET_ALREADY_BOUND(): Error; +declare function $ERR_SOCKET_BAD_BUFFER_SIZE(): Error; +declare function $ERR_SOCKET_DGRAM_IS_CONNECTED(): Error; +declare function $ERR_SOCKET_DGRAM_NOT_CONNECTED(): Error; +declare function $ERR_SOCKET_DGRAM_NOT_RUNNING(): Error; +declare function $ERR_INVALID_CURSOR_POS(): Error; +declare function $ERR_MULTIPLE_CALLBACK(): Error; +declare function $ERR_STREAM_PREMATURE_CLOSE(): Error; +declare function $ERR_STREAM_NULL_VALUES(): TypeError; +declare function $ERR_STREAM_CANNOT_PIPE(): Error; +declare function $ERR_STREAM_WRITE_AFTER_END(): Error; +declare function $ERR_STREAM_UNSHIFT_AFTER_END_EVENT(): Error; +declare function $ERR_STREAM_PUSH_AFTER_EOF(): Error; +declare function $ERR_STREAM_UNABLE_TO_PIPE(): Error; +declare function $ERR_ILLEGAL_CONSTRUCTOR(): TypeError; + /** * Convert a function to a class-like object. * diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index ca85b8eadc..9faf064bf8 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -52,8 +52,9 @@ export function renderRoutesForProdStatic( // Call the framework's rendering function const callback = renderStatic[type]; $assert(callback != null && $isCallable(callback)); + let client = clientEntryUrl[type]; const results = await callback({ - modules: [clientEntryUrl[type]], + modules: client ? [client] : [], modulepreload: [], styles: styles[i], layouts, @@ -116,7 +117,7 @@ export function renderRoutesForProdStatic( [pageModule, ...layouts] = anyPromise ? await Promise.all(loaded) : loaded; } else { const id = fileList[0]; - pageModule = loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[fileList[0]])); + pageModule = loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id])); layouts = []; } diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 12fd5d6299..7fd2016a10 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -71,6 +71,7 @@ using namespace JSC; macro(dataView) \ macro(decode) \ macro(delimiter) \ + macro(dest) \ macro(destroy) \ macro(dir) \ macro(direct) \ @@ -114,6 +115,7 @@ using namespace JSC; macro(inFlightCloseRequest) \ macro(inFlightWriteRequest) \ macro(initializeWith) \ + macro(inherits) \ macro(internalModuleRegistry) \ macro(internalRequire) \ macro(internalStream) \ @@ -132,10 +134,10 @@ using namespace JSC; macro(Loader) \ macro(localStreams) \ macro(main) \ + macro(makeAbortError) \ macro(makeDOMException) \ macro(makeErrorWithCode) \ macro(makeGetterTypeError) \ - macro(makeThisTypeError) \ macro(method) \ macro(mockedFunction) \ macro(nextTick) \ @@ -244,6 +246,7 @@ using namespace JSC; macro(version) \ macro(versions) \ macro(view) \ + macro(warning) \ macro(writable) \ macro(WritableStream) \ macro(WritableStreamDefaultController) \ @@ -254,6 +257,8 @@ using namespace JSC; macro(writeRequests) \ macro(writing) \ macro(written) \ + macro(napiDlopenHandle) \ + macro(napiWrappedContents) \ BUN_ADDITIONAL_BUILTIN_NAMES(macro) // --- END of BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME --- diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts index 7b248081d6..a4ded7976c 100644 --- a/src/js/builtins/BundlerPlugin.ts +++ b/src/js/builtins/BundlerPlugin.ts @@ -1,11 +1,4 @@ -import type { - BuildConfig, - BunPlugin, - OnLoadCallback, - OnResolveCallback, - PluginBuilder, - PluginConstraints, -} from "bun"; +import type { BuildConfig, BunPlugin, OnLoadCallback, OnResolveCallback, PluginBuilder, PluginConstraints } from "bun"; type AnyFunction = (...args: any[]) => any; interface BundlerPlugin { @@ -46,6 +39,8 @@ interface PluginBuilderExt extends PluginBuilder { esbuild: any; } +type BeforeOnParseExternal = unknown; + export function runSetupFunction( this: BundlerPlugin, setup: Setup, @@ -57,14 +52,33 @@ export function runSetupFunction( this.promises = promises; var onLoadPlugins = new Map(); var onResolvePlugins = new Map(); + var onBeforeParsePlugins = new Map< + string, + [RegExp, napiModule: unknown, symbol: string, external?: undefined | unknown][] + >(); - function validate(filterObject: PluginConstraints, callback, map) { + function validate(filterObject: PluginConstraints, callback, map, symbol, external) { if (!filterObject || !$isObject(filterObject)) { throw new TypeError('Expected an object with "filter" RegExp'); } - if (!callback || !$isCallable(callback)) { - throw new TypeError("callback must be a function"); + let isOnBeforeParse = false; + if (map === onBeforeParsePlugins) { + isOnBeforeParse = true; + // TODO: how to check if it a napi module here? + if (!callback || !$isObject(callback) || !callback.$napiDlopenHandle) { + throw new TypeError( + "onBeforeParse `napiModule` must be a Napi module which exports the `BUN_PLUGIN_NAME` symbol.", + ); + } + + if (typeof symbol !== "string") { + throw new TypeError("onBeforeParse `symbol` must be a string"); + } + } else { + if (!callback || !$isCallable(callback)) { + throw new TypeError("lmao callback must be a function"); + } } var { filter, namespace = "file" } = filterObject; @@ -92,23 +106,30 @@ export function runSetupFunction( var callbacks = map.$get(namespace); if (!callbacks) { - map.$set(namespace, [[filter, callback]]); + map.$set(namespace, [isOnBeforeParse ? [filter, callback, symbol, external] : [filter, callback]]); } else { - $arrayPush(callbacks, [filter, callback]); + $arrayPush(callbacks, isOnBeforeParse ? [filter, callback, symbol, external] : [filter, callback]); } } function onLoad(filterObject, callback) { - validate(filterObject, callback, onLoadPlugins); + validate(filterObject, callback, onLoadPlugins, undefined, undefined); } function onResolve(filterObject, callback) { - validate(filterObject, callback, onResolvePlugins); + validate(filterObject, callback, onResolvePlugins, undefined, undefined); + } + + function onBeforeParse( + filterObject, + { napiModule, external, symbol }: { napiModule: unknown; symbol: string; external?: undefined | unknown }, + ) { + validate(filterObject, napiModule, onBeforeParsePlugins, symbol, external); } const self = this; function onStart(callback) { - if(isBake) { + if (isBake) { throw new TypeError("onStart() is not supported in Bake yet"); } if (!$isCallable(callback)) { @@ -126,7 +147,8 @@ export function runSetupFunction( const processSetupResult = () => { var anyOnLoad = false, - anyOnResolve = false; + anyOnResolve = false, + anyOnBeforeParse = false; for (var [namespace, callbacks] of onLoadPlugins.entries()) { for (var [filter] of callbacks) { @@ -142,6 +164,13 @@ export function runSetupFunction( } } + for (let [namespace, callbacks] of onBeforeParsePlugins.entries()) { + for (let [filter, addon, symbol, external] of callbacks) { + this.onBeforeParse(filter, namespace, addon, symbol, external); + anyOnBeforeParse = true; + } + } + if (anyOnResolve) { var onResolveObject = this.onResolve; if (!onResolveObject) { @@ -189,6 +218,7 @@ export function runSetupFunction( onEnd: notImplementedIssueFn(2771, "On-end callbacks"), onLoad, onResolve, + onBeforeParse, onStart, resolve: notImplementedIssueFn(2771, "build.resolve()"), module: () => { @@ -335,7 +365,14 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa } } -export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespace, defaultLoaderId, isServerSide: boolean) { +export function runOnLoadPlugins( + this: BundlerPlugin, + internalID, + path, + namespace, + defaultLoaderId, + isServerSide: boolean, +) { const LOADERS_MAP = $LoaderLabelToId; const loaderName = $LoaderIdToLabel[defaultLoaderId]; @@ -376,15 +413,15 @@ export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespac } var { contents, loader = defaultLoader } = result as any; - if ((loader as any) === 'object') { - if (!('exports' in result)) { + if ((loader as any) === "object") { + if (!("exports" in result)) { throw new TypeError('onLoad plugin returning loader: "object" must have "exports" property'); } try { contents = JSON.stringify(result.exports); - loader = 'json'; + loader = "json"; } catch (e) { - throw new TypeError('When using Bun.build, onLoad plugin must return a JSON-serializable object: ' + e) ; + throw new TypeError("When using Bun.build, onLoad plugin must return a JSON-serializable object: " + e); } } diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index bf1dabf1aa..7377e5710c 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -142,6 +142,9 @@ export function createConsoleConstructor(console: typeof globalThis.console) { const { inspect, formatWithOptions, stripVTControlCharacters } = require("node:util"); const { isBuffer } = require("node:buffer"); + const { validateObject, validateInteger, validateArray, validateOneOf } = require("internal/validators"); + const kMaxGroupIndentation = 1000; + const StringPrototypeIncludes = String.prototype.includes; const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace]; const ArrayPrototypeUnshift = Array.prototype.unshift; @@ -289,28 +292,25 @@ export function createConsoleConstructor(console: typeof globalThis.console) { } = options; if (!stdout || typeof stdout.write !== "function") { - // throw new ERR_CONSOLE_WRITABLE_STREAM("stdout"); - throw new TypeError("stdout is not a writable stream"); + throw $ERR_CONSOLE_WRITABLE_STREAM("stdout is not a writable stream"); } if (!stderr || typeof stderr.write !== "function") { - // throw new ERR_CONSOLE_WRITABLE_STREAM("stderr"); - throw new TypeError("stderr is not a writable stream"); + throw $ERR_CONSOLE_WRITABLE_STREAM("stderr is not a writable stream"); } - if (typeof colorMode !== "boolean" && colorMode !== "auto") { - // throw new ERR_INVALID_ARG_VALUE("colorMode", colorMode); - throw new TypeError("colorMode must be a boolean or 'auto'"); - } + validateOneOf(colorMode, "colorMode", ["auto", true, false]); if (groupIndentation !== undefined) { - // validateInteger(groupIndentation, "groupIndentation", 0, kMaxGroupIndentation); + validateInteger(groupIndentation, "groupIndentation", 0, kMaxGroupIndentation); } if (inspectOptions !== undefined) { - // validateObject(inspectOptions, "options.inspectOptions"); + validateObject(inspectOptions, "options.inspectOptions"); if (inspectOptions.colors !== undefined && options.colorMode !== undefined) { - // throw new ERR_INCOMPATIBLE_OPTION_PAIR("options.inspectOptions.color", "colorMode"); + throw $ERR_INCOMPATIBLE_OPTION_PAIR( + 'Option "options.inspectOptions.color" cannot be used in combination with option "colorMode"', + ); } optionsMap.set(this, inspectOptions); } @@ -455,12 +455,17 @@ export function createConsoleConstructor(console: typeof globalThis.console) { // Add and later remove a noop error handler to catch synchronous // errors. if (stream.listenerCount("error") === 0) stream.once("error", noop); - stream.write(string, errorHandler); } catch (e) { // Console is a debugging utility, so it swallowing errors is not // desirable even in edge cases such as low stack space. - // if (isStackOverflowError(e)) throw e; + if ( + e != null && + typeof e === "object" && + e.name === "RangeError" && + e.message === "Maximum call stack size exceeded." + ) + throw e; // Sorry, there's no proper way to pass along the error here. } finally { stream.removeListener("error", noop); @@ -472,7 +477,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { value: function (stream) { let color = this[kColorMode]; if (color === "auto") { - if (process.env.FORCE_COLOR !== undefined) { + if (Bun.env["FORCE_COLOR"] !== undefined) { color = Bun.enableANSIColors; } else { color = stream.isTTY && (typeof stream.getColorDepth === "function" ? stream.getColorDepth() > 2 : true); @@ -595,7 +600,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { clear() { // It only makes sense to clear if _stdout is a TTY. // Otherwise, do nothing. - if (this._stdout.isTTY && process.env.TERM !== "dumb") { + if (this._stdout.isTTY && Bun.env["TERM"] !== "dumb") { this._stdout.write("\x1B[2J\x1B[3J\x1B[H"); } }, @@ -643,7 +648,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { // https://console.spec.whatwg.org/#table table(tabularData, properties) { if (properties !== undefined) { - // validateArray(properties, "properties"); + validateArray(properties, "properties"); } if (tabularData === null || typeof tabularData !== "object") return this.log(tabularData); diff --git a/src/js/builtins/JSBufferPrototype.ts b/src/js/builtins/JSBufferPrototype.ts index 986e0992ba..99ca59405a 100644 --- a/src/js/builtins/JSBufferPrototype.ts +++ b/src/js/builtins/JSBufferPrototype.ts @@ -175,48 +175,87 @@ export function readBigUInt64BE(this: BufferExt, offset) { } export function writeInt8(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt8(offset, value); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt8( + offset === undefined ? (offset = 0) : offset, + value, + ); return offset + 1; } export function writeUInt8(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint8(offset, value); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint8( + offset === undefined ? (offset = 0) : offset, + value, + ); return offset + 1; } export function writeInt16LE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 2; } export function writeInt16BE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 2; } export function writeUInt16LE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 2; } export function writeUInt16BE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 2; } export function writeInt32LE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 4; } export function writeInt32BE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 4; } export function writeUInt32LE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 4; } export function writeUInt32BE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 4; } export function writeIntLE(this: BufferExt, value, offset, byteLength) { const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { case 1: { view.setInt8(offset, value); @@ -253,6 +292,7 @@ export function writeIntLE(this: BufferExt, value, offset, byteLength) { } export function writeIntBE(this: BufferExt, value, offset, byteLength) { const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { case 1: { view.setInt8(offset, value); @@ -289,6 +329,7 @@ export function writeIntBE(this: BufferExt, value, offset, byteLength) { } export function writeUIntLE(this: BufferExt, value, offset, byteLength) { const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { case 1: { view.setUint8(offset, value); @@ -325,6 +366,7 @@ export function writeUIntLE(this: BufferExt, value, offset, byteLength) { } export function writeUIntBE(this: BufferExt, value, offset, byteLength) { const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { case 1: { view.setUint8(offset, value); @@ -361,42 +403,74 @@ export function writeUIntBE(this: BufferExt, value, offset, byteLength) { } export function writeFloatLE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 4; } export function writeFloatBE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 4; } export function writeDoubleLE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 8; } export function writeDoubleBE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 8; } export function writeBigInt64LE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigInt64(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigInt64( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 8; } export function writeBigInt64BE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigInt64(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigInt64( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 8; } export function writeBigUInt64LE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64(offset, value, true); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64( + offset === undefined ? (offset = 0) : offset, + value, + true, + ); return offset + 8; } export function writeBigUInt64BE(this: BufferExt, value, offset) { - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64(offset, value, false); + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64( + offset === undefined ? (offset = 0) : offset, + value, + false, + ); return offset + 8; } diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index 506596a53f..1c7f2a0670 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -126,7 +126,7 @@ export function getStdinStream(fd) { let stream_destroyed = false; let stream_endEmitted = false; - stream.on = function (event, listener) { + stream.addListener = stream.on = function (event, listener) { // Streams don't generally required to present any data when only // `readable` events are present, i.e. `readableFlowing === false` // @@ -341,8 +341,13 @@ export function setMainModule(value) { } type InternalEnvMap = Record; +type EditWindowsEnvVarCb = (key: string, value: null | string) => void; -export function windowsEnv(internalEnv: InternalEnvMap, envMapList: Array) { +export function windowsEnv( + internalEnv: InternalEnvMap, + envMapList: Array, + editWindowsEnvVar: EditWindowsEnvVarCb, +) { // The use of String(key) here is intentional because Node.js as of v21.5.0 will throw // on symbol keys as it seems they assume the user uses string keys: // @@ -371,7 +376,10 @@ export function windowsEnv(internalEnv: InternalEnvMap, envMapList: Array +/** + * ## References + * - [ReadableStream - `ReadableByteStreamController`](https://streams.spec.whatwg.org/#rbs-controller-class) + */ /* * Copyright (C) 2016 Canon Inc. All rights reserved. * @@ -103,7 +108,7 @@ export function isReadableStreamBYOBReader(reader) { export function readableByteStreamControllerCancel(controller, reason) { var pendingPullIntos = $getByIdDirectPrivate(controller, "pendingPullIntos"); - var first = pendingPullIntos.peek(); + var first: PullIntoDescriptor | undefined = pendingPullIntos.peek(); if (first) first.bytesFilled = 0; $putByIdDirectPrivate(controller, "queue", $newQueue()); @@ -130,7 +135,7 @@ export function readableByteStreamControllerClose(controller) { return; } - var first = $getByIdDirectPrivate(controller, "pendingPullIntos")?.peek(); + var first: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos")?.peek(); if (first) { if (first.bytesFilled > 0) { const e = $makeTypeError("Close requested while there remain pending bytes"); @@ -144,7 +149,7 @@ export function readableByteStreamControllerClose(controller) { export function readableByteStreamControllerClearPendingPullIntos(controller) { $readableByteStreamControllerInvalidateBYOBRequest(controller); - var existing = $getByIdDirectPrivate(controller, "pendingPullIntos"); + var existing: Dequeue = $getByIdDirectPrivate(controller, "pendingPullIntos"); if (existing !== undefined) { existing.clear(); } else { @@ -204,7 +209,7 @@ export function readableByteStreamControllerPull(controller) { } catch (error) { return Promise.$reject(error); } - const pullIntoDescriptor = { + const pullIntoDescriptor: PullIntoDescriptor = { buffer, byteOffset: 0, byteLength: $getByIdDirectPrivate(controller, "autoAllocateChunkSize"), @@ -359,7 +364,7 @@ export function readableByteStreamControllerEnqueueChunk(controller, buffer, byt export function readableByteStreamControllerRespondWithNewView(controller, view) { $assert($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()); - let firstDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); + let firstDescriptor: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) throw new RangeError("Invalid value for view.byteOffset"); @@ -382,7 +387,7 @@ export function readableByteStreamControllerRespond(controller, bytesWritten) { } export function readableByteStreamControllerRespondInternal(controller, bytesWritten) { - let firstDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); + let firstDescriptor: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); let stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { @@ -449,7 +454,7 @@ export function readableByteStreamControllerProcessPullDescriptors(controller) { $assert(!$getByIdDirectPrivate(controller, "closeRequested")); while ($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()) { if ($getByIdDirectPrivate(controller, "queue").size === 0) return; - let pullIntoDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); + let pullIntoDescriptor: PullIntoDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); if ($readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)) { $readableByteStreamControllerShiftPendingDescriptor(controller); $readableByteStreamControllerCommitDescriptor( @@ -461,7 +466,10 @@ export function readableByteStreamControllerProcessPullDescriptors(controller) { } // Spec name: readableByteStreamControllerFillPullIntoDescriptorFromQueue (shortened for readability). -export function readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor) { +export function readableByteStreamControllerFillDescriptorFromQueue( + controller, + pullIntoDescriptor: PullIntoDescriptor, +) { const currentAlignedBytes = pullIntoDescriptor.bytesFilled - (pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize); const maxBytesToCopy = @@ -519,8 +527,8 @@ export function readableByteStreamControllerFillDescriptorFromQueue(controller, } // Spec name: readableByteStreamControllerShiftPendingPullInto (renamed for consistency). -export function readableByteStreamControllerShiftPendingDescriptor(controller) { - let descriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").shift(); +export function readableByteStreamControllerShiftPendingDescriptor(controller): PullIntoDescriptor | undefined { + let descriptor: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos").shift(); $readableByteStreamControllerInvalidateBYOBRequest(controller); return descriptor; } @@ -600,7 +608,7 @@ export function readableByteStreamControllerPullInto(controller, view) { // name has already been met before. const ctor = view.constructor; - const pullIntoDescriptor = { + const pullIntoDescriptor: PullIntoDescriptor = { buffer: view.buffer, byteOffset: view.byteOffset, byteLength: view.byteLength, @@ -654,3 +662,75 @@ export function readableStreamAddReadIntoRequest(stream) { return readRequest; } + +/** + * ## References + * - [Spec](https://streams.spec.whatwg.org/#pull-into-descriptor) + */ +interface PullIntoDescriptor { + /** + * An {@link ArrayBuffer} + */ + buffer: ArrayBuffer; + /** + * A positive integer representing the initial byte length of {@link buffer} + */ + bufferByteLength: number; + /** + * A nonnegative integer byte offset into the {@link buffer} where the + * underlying byte source will start writing + */ + byteOffset: number; + /** + * A positive integer number of bytes which can be written into the + * {@link buffer} + */ + byteLength: number; + /** + * A nonnegative integer number of bytes that have been written into the + * {@link buffer} so far + */ + bytesFilled: number; + /** + * A positive integer representing the minimum number of bytes that must be + * written into the {@link buffer} before the associated read() request may be + * fulfilled. By default, this equals the element size. + */ + minimumFill: number; + /** + * A positive integer representing the number of bytes that can be written + * into the {@link buffer} at a time, using views of the type described by the + * view constructor + */ + elementSize: number; + /** + * `view constructor` + * + * A {@link NodeJS.TypedArray typed array constructor} or + * {@link NodeJS.DataView `%DataView%`}, which will be used for constructing a + * view with which to write into the {@link buffer} + * + * ## References + * - [`TypedArray` Constructors](https://tc39.es/ecma262/#table-49) + */ + ctor: ArrayBufferViewConstructor; + /** + * Either "default" or "byob", indicating what type of readable stream reader + * initiated this request, or "none" if the initiating reader was released + */ + readerType: "default" | "byob" | "none"; +} + +type TypedArrayConstructor = + | Uint8ArrayConstructor + | Uint8ClampedArrayConstructor + | Uint16ArrayConstructor + | Uint32ArrayConstructor + | Int8ArrayConstructor + | Int16ArrayConstructor + | Int32ArrayConstructor + | BigUint64ArrayConstructor + | BigInt64ArrayConstructor + | Float32ArrayConstructor + | Float64ArrayConstructor; +type ArrayBufferViewConstructor = TypedArrayConstructor | DataViewConstructor; diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts index 6e7e2d5951..d8cf7ff8d5 100644 --- a/src/js/builtins/ReadableStream.ts +++ b/src/js/builtins/ReadableStream.ts @@ -114,7 +114,7 @@ export function readableStreamToArray(stream: ReadableStream): Promise { if (underlyingSource !== undefined) { return $readableStreamToTextDirect(stream, underlyingSource); } - if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); const result = $tryUseReadableStreamBufferedFastPath(stream, "text"); @@ -145,7 +145,7 @@ export function readableStreamToArrayBuffer(stream: ReadableStream) if (underlyingSource !== undefined) { return $readableStreamToArrayBufferDirect(stream, underlyingSource, false); } - if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); let result = $tryUseReadableStreamBufferedFastPath(stream, "arrayBuffer"); @@ -226,7 +226,7 @@ export function readableStreamToBytes(stream: ReadableStream): Prom if (underlyingSource !== undefined) { return $readableStreamToArrayBufferDirect(stream, underlyingSource, true); } - if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); let result = $tryUseReadableStreamBufferedFastPath(stream, "bytes"); @@ -302,7 +302,7 @@ export function readableStreamToFormData( contentType: string | ArrayBuffer | ArrayBufferView, ): Promise { if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); return Bun.readableStreamToBlob(stream).then(blob => { return FormData.from(blob, contentType); }); @@ -311,7 +311,7 @@ export function readableStreamToFormData( $linkTimeConstant; export function readableStreamToJSON(stream: ReadableStream): unknown { if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); let result = $tryUseReadableStreamBufferedFastPath(stream, "json"); if (result) { return result; @@ -333,7 +333,7 @@ export function readableStreamToJSON(stream: ReadableStream): unknown { $linkTimeConstant; export function readableStreamToBlob(stream: ReadableStream): Promise { if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); return ( $tryUseReadableStreamBufferedFastPath(stream, "blob") || @@ -370,15 +370,15 @@ export function createNativeReadableStream(nativePtr, autoAllocateChunkSize) { } export function cancel(this, reason) { - if (!$isReadableStream(this)) return Promise.$reject($makeThisTypeError("ReadableStream", "cancel")); + if (!$isReadableStream(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStream")); - if ($isReadableStreamLocked(this)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(this)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); return $readableStreamCancel(this, reason); } export function getReader(this, options) { - if (!$isReadableStream(this)) throw $makeThisTypeError("ReadableStream", "getReader"); + if (!$isReadableStream(this)) throw $ERR_INVALID_THIS("ReadableStream"); const mode = $toDictionary(options, {}, "ReadableStream.getReader takes an object as first argument").mode; if (mode === undefined) { @@ -423,9 +423,9 @@ export function pipeThrough(this, streams, options) { if (signal !== undefined && !$isAbortSignal(signal)) throw $makeTypeError("options.signal must be AbortSignal"); } - if (!$isReadableStream(this)) throw $makeThisTypeError("ReadableStream", "pipeThrough"); + if (!$isReadableStream(this)) throw $ERR_INVALID_THIS("ReadableStream"); - if ($isReadableStreamLocked(this)) throw $makeTypeError("ReadableStream is locked"); + if ($isReadableStreamLocked(this)) throw $ERR_INVALID_STATE_TypeError("ReadableStream is locked"); if ($isWritableStreamLocked(internalWritable)) throw $makeTypeError("WritableStream is locked"); @@ -443,9 +443,9 @@ export function pipeThrough(this, streams, options) { } export function pipeTo(this, destination) { - if (!$isReadableStream(this)) return Promise.$reject($makeThisTypeError("ReadableStream", "pipeTo")); + if (!$isReadableStream(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStream")); - if ($isReadableStreamLocked(this)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + if ($isReadableStreamLocked(this)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); // FIXME: https://bugs.webkit.org/show_bug.cgi?id=159869. // Built-in generator should be able to parse function signature to compute the function length correctly. @@ -489,7 +489,7 @@ export function pipeTo(this, destination) { } export function tee(this) { - if (!$isReadableStream(this)) throw $makeThisTypeError("ReadableStream", "tee"); + if (!$isReadableStream(this)) throw $ERR_INVALID_THIS("ReadableStream"); return $readableStreamTee(this, false); } diff --git a/src/js/builtins/ReadableStreamBYOBReader.ts b/src/js/builtins/ReadableStreamBYOBReader.ts index 62a04d8a3d..10481564e2 100644 --- a/src/js/builtins/ReadableStreamBYOBReader.ts +++ b/src/js/builtins/ReadableStreamBYOBReader.ts @@ -35,8 +35,7 @@ export function initializeReadableStreamBYOBReader(this, stream) { } export function cancel(this, reason) { - if (!$isReadableStreamBYOBReader(this)) - return Promise.$reject($makeThisTypeError("ReadableStreamBYOBReader", "cancel")); + if (!$isReadableStreamBYOBReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamBYOBReader")); if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return Promise.$reject($makeTypeError("cancel() called on a reader owned by no readable stream")); @@ -45,8 +44,7 @@ export function cancel(this, reason) { } export function read(this, view: DataView) { - if (!$isReadableStreamBYOBReader(this)) - return Promise.$reject($makeThisTypeError("ReadableStreamBYOBReader", "read")); + if (!$isReadableStreamBYOBReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamBYOBReader")); if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return Promise.$reject($makeTypeError("read() called on a reader owned by no readable stream")); @@ -61,7 +59,7 @@ export function read(this, view: DataView) { } export function releaseLock(this) { - if (!$isReadableStreamBYOBReader(this)) throw $makeThisTypeError("ReadableStreamBYOBReader", "releaseLock"); + if (!$isReadableStreamBYOBReader(this)) throw $ERR_INVALID_THIS("ReadableStreamBYOBReader"); if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return; diff --git a/src/js/builtins/ReadableStreamBYOBRequest.ts b/src/js/builtins/ReadableStreamBYOBRequest.ts index 1354f93499..aacf265771 100644 --- a/src/js/builtins/ReadableStreamBYOBRequest.ts +++ b/src/js/builtins/ReadableStreamBYOBRequest.ts @@ -31,7 +31,7 @@ export function initializeReadableStreamBYOBRequest(this, controller, view) { } export function respond(this, bytesWritten) { - if (!$isReadableStreamBYOBRequest(this)) throw $makeThisTypeError("ReadableStreamBYOBRequest", "respond"); + if (!$isReadableStreamBYOBRequest(this)) throw $ERR_INVALID_THIS("ReadableStreamBYOBRequest"); if ($getByIdDirectPrivate(this, "associatedReadableByteStreamController") === undefined) throw new TypeError("ReadableStreamBYOBRequest.associatedReadableByteStreamController is undefined"); @@ -43,7 +43,7 @@ export function respond(this, bytesWritten) { } export function respondWithNewView(this, view) { - if (!$isReadableStreamBYOBRequest(this)) throw $makeThisTypeError("ReadableStreamBYOBRequest", "respond"); + if (!$isReadableStreamBYOBRequest(this)) throw $ERR_INVALID_THIS("ReadableStreamBYOBRequest"); if ($getByIdDirectPrivate(this, "associatedReadableByteStreamController") === undefined) throw new TypeError("ReadableStreamBYOBRequest.associatedReadableByteStreamController is undefined"); diff --git a/src/js/builtins/ReadableStreamDefaultController.ts b/src/js/builtins/ReadableStreamDefaultController.ts index 6a04addc33..53d3fdd8e9 100644 --- a/src/js/builtins/ReadableStreamDefaultController.ts +++ b/src/js/builtins/ReadableStreamDefaultController.ts @@ -31,7 +31,7 @@ export function initializeReadableStreamDefaultController(this, stream, underlyi } export function enqueue(this, chunk) { - if (!$isReadableStreamDefaultController(this)) throw $makeThisTypeError("ReadableStreamDefaultController", "enqueue"); + if (!$isReadableStreamDefaultController(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultController"); if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) { throw $ERR_INVALID_STATE("ReadableStreamDefaultController is not in a state where chunk can be enqueued"); @@ -41,12 +41,12 @@ export function enqueue(this, chunk) { } export function error(this, err) { - if (!$isReadableStreamDefaultController(this)) throw $makeThisTypeError("ReadableStreamDefaultController", "error"); + if (!$isReadableStreamDefaultController(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultController"); $readableStreamDefaultControllerError(this, err); } export function close(this) { - if (!$isReadableStreamDefaultController(this)) throw $makeThisTypeError("ReadableStreamDefaultController", "close"); + if (!$isReadableStreamDefaultController(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultController"); if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) throw new TypeError("ReadableStreamDefaultController is not in a state where it can be closed"); diff --git a/src/js/builtins/ReadableStreamDefaultReader.ts b/src/js/builtins/ReadableStreamDefaultReader.ts index 9ddb3e3f38..c1004488a3 100644 --- a/src/js/builtins/ReadableStreamDefaultReader.ts +++ b/src/js/builtins/ReadableStreamDefaultReader.ts @@ -34,8 +34,7 @@ export function initializeReadableStreamDefaultReader(this, stream) { } export function cancel(this, reason) { - if (!$isReadableStreamDefaultReader(this)) - return Promise.$reject($makeThisTypeError("ReadableStreamDefaultReader", "cancel")); + if (!$isReadableStreamDefaultReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamDefaultReader")); if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return Promise.$reject(new TypeError("cancel() called on a reader owned by no readable stream")); @@ -159,8 +158,7 @@ export function readMany(this: ReadableStreamDefaultReader): ReadableStreamDefau } export function read(this) { - if (!$isReadableStreamDefaultReader(this)) - return Promise.$reject($makeThisTypeError("ReadableStreamDefaultReader", "read")); + if (!$isReadableStreamDefaultReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamDefaultReader")); if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return Promise.$reject(new TypeError("read() called on a reader owned by no readable stream")); @@ -168,7 +166,7 @@ export function read(this) { } export function releaseLock(this) { - if (!$isReadableStreamDefaultReader(this)) throw $makeThisTypeError("ReadableStreamDefaultReader", "releaseLock"); + if (!$isReadableStreamDefaultReader(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultReader"); if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return; diff --git a/src/js/builtins/StreamInternals.ts b/src/js/builtins/StreamInternals.ts index a81ff3ca6f..e36648c803 100644 --- a/src/js/builtins/StreamInternals.ts +++ b/src/js/builtins/StreamInternals.ts @@ -87,10 +87,11 @@ export function validateAndNormalizeQueuingStrategy(size, highWaterMark) { return { size: size, highWaterMark: newHighWaterMark }; } +import type Dequeue from "internal/fifo"; $linkTimeConstant; -export function createFIFO() { - const Denqueue = require("internal/fifo"); - return new Denqueue(); +export function createFIFO(): Dequeue { + const Dequeue = require("internal/fifo"); + return new Dequeue(); } export function newQueue() { diff --git a/src/js/builtins/TextDecoderStream.ts b/src/js/builtins/TextDecoderStream.ts index 2a5f1e528d..e64ad22f9f 100644 --- a/src/js/builtins/TextDecoderStream.ts +++ b/src/js/builtins/TextDecoderStream.ts @@ -78,24 +78,21 @@ export function initializeTextDecoderStream() { $getter; export function encoding() { - if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) - throw $makeThisTypeError("TextDecoderStream", "encoding"); + if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) throw $ERR_INVALID_THIS("TextDecoderStream"); return $getByIdDirectPrivate(this, "encoding"); } $getter; export function fatal() { - if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) - throw $makeThisTypeError("TextDecoderStream", "fatal"); + if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) throw $ERR_INVALID_THIS("TextDecoderStream"); return $getByIdDirectPrivate(this, "fatal"); } $getter; export function ignoreBOM() { - if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) - throw $makeThisTypeError("TextDecoderStream", "ignoreBOM"); + if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) throw $ERR_INVALID_THIS("TextDecoderStream"); return $getByIdDirectPrivate(this, "ignoreBOM"); } @@ -103,7 +100,7 @@ export function ignoreBOM() { $getter; export function readable() { const transform = $getByIdDirectPrivate(this, "textDecoderStreamTransform"); - if (!transform) throw $makeThisTypeError("TextDecoderStream", "readable"); + if (!transform) throw $ERR_INVALID_THIS("TextDecoderStream"); return $getByIdDirectPrivate(transform, "readable"); } @@ -111,7 +108,7 @@ export function readable() { $getter; export function writable() { const transform = $getByIdDirectPrivate(this, "textDecoderStreamTransform"); - if (!transform) throw $makeThisTypeError("TextDecoderStream", "writable"); + if (!transform) throw $ERR_INVALID_THIS("TextDecoderStream"); return $getByIdDirectPrivate(transform, "writable"); } diff --git a/src/js/builtins/TextEncoderStream.ts b/src/js/builtins/TextEncoderStream.ts index 4aa0c895dd..c9dda44bea 100644 --- a/src/js/builtins/TextEncoderStream.ts +++ b/src/js/builtins/TextEncoderStream.ts @@ -61,8 +61,7 @@ export function initializeTextEncoderStream() { $getter; export function encoding() { - if (!$getByIdDirectPrivate(this, "textEncoderStreamTransform")) - throw $makeThisTypeError("TextEncoderStream", "encoding"); + if (!$getByIdDirectPrivate(this, "textEncoderStreamTransform")) throw $ERR_INVALID_THIS("TextEncoderStream"); return "utf-8"; } @@ -70,7 +69,7 @@ export function encoding() { $getter; export function readable() { const transform = $getByIdDirectPrivate(this, "textEncoderStreamTransform"); - if (!transform) throw $makeThisTypeError("TextEncoderStream", "readable"); + if (!transform) throw $ERR_INVALID_THIS("TextEncoderStream"); return $getByIdDirectPrivate(transform, "readable"); } @@ -78,7 +77,7 @@ export function readable() { $getter; export function writable() { const transform = $getByIdDirectPrivate(this, "textEncoderStreamTransform"); - if (!transform) throw $makeThisTypeError("TextEncoderStream", "writable"); + if (!transform) throw $ERR_INVALID_THIS("TextEncoderStream"); return $getByIdDirectPrivate(transform, "writable"); } diff --git a/src/js/builtins/TransformStream.ts b/src/js/builtins/TransformStream.ts index f9d80b7cbd..f8bb7d3438 100644 --- a/src/js/builtins/TransformStream.ts +++ b/src/js/builtins/TransformStream.ts @@ -95,13 +95,13 @@ export function initializeTransformStream(this) { $getter; export function readable() { - if (!$isTransformStream(this)) throw $makeThisTypeError("TransformStream", "readable"); + if (!$isTransformStream(this)) throw $ERR_INVALID_THIS("TransformStream"); return $getByIdDirectPrivate(this, "readable"); } export function writable() { - if (!$isTransformStream(this)) throw $makeThisTypeError("TransformStream", "writable"); + if (!$isTransformStream(this)) throw $ERR_INVALID_THIS("TransformStream"); return $getByIdDirectPrivate(this, "writable"); } diff --git a/src/js/builtins/TransformStreamDefaultController.ts b/src/js/builtins/TransformStreamDefaultController.ts index 1045498b8d..84eb6ff6e9 100644 --- a/src/js/builtins/TransformStreamDefaultController.ts +++ b/src/js/builtins/TransformStreamDefaultController.ts @@ -29,8 +29,7 @@ export function initializeTransformStreamDefaultController(this) { $getter; export function desiredSize(this) { - if (!$isTransformStreamDefaultController(this)) - throw $makeThisTypeError("TransformStreamDefaultController", "enqueue"); + if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); const stream = $getByIdDirectPrivate(this, "stream"); const readable = $getByIdDirectPrivate(stream, "readable"); @@ -40,21 +39,19 @@ export function desiredSize(this) { } export function enqueue(this, chunk) { - if (!$isTransformStreamDefaultController(this)) - throw $makeThisTypeError("TransformStreamDefaultController", "enqueue"); + if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); $transformStreamDefaultControllerEnqueue(this, chunk); } export function error(this, e) { - if (!$isTransformStreamDefaultController(this)) throw $makeThisTypeError("TransformStreamDefaultController", "error"); + if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); $transformStreamDefaultControllerError(this, e); } export function terminate(this) { - if (!$isTransformStreamDefaultController(this)) - throw $makeThisTypeError("TransformStreamDefaultController", "terminate"); + if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); $transformStreamDefaultControllerTerminate(this); } diff --git a/src/js/builtins/WritableStreamDefaultController.ts b/src/js/builtins/WritableStreamDefaultController.ts index 1a3ddc2904..05cf16ba06 100644 --- a/src/js/builtins/WritableStreamDefaultController.ts +++ b/src/js/builtins/WritableStreamDefaultController.ts @@ -40,7 +40,7 @@ export function initializeWritableStreamDefaultController(this) { export function error(this, e) { if ($getByIdDirectPrivate(this, "abortSteps") === undefined) - throw $makeThisTypeError("WritableStreamDefaultController", "error"); + throw $ERR_INVALID_THIS("WritableStreamDefaultController"); const stream = $getByIdDirectPrivate(this, "stream"); if ($getByIdDirectPrivate(stream, "state") !== "writable") return; diff --git a/src/js/builtins/WritableStreamDefaultWriter.ts b/src/js/builtins/WritableStreamDefaultWriter.ts index ff3f38f006..87de5aa8e2 100644 --- a/src/js/builtins/WritableStreamDefaultWriter.ts +++ b/src/js/builtins/WritableStreamDefaultWriter.ts @@ -46,7 +46,7 @@ export function closed() { $getter; export function desiredSize() { - if (!$isWritableStreamDefaultWriter(this)) throw $makeThisTypeError("WritableStreamDefaultWriter", "desiredSize"); + if (!$isWritableStreamDefaultWriter(this)) throw $ERR_INVALID_THIS("WritableStreamDefaultWriter"); if ($getByIdDirectPrivate(this, "stream") === undefined) $throwTypeError("WritableStreamDefaultWriter has no stream"); @@ -55,15 +55,13 @@ export function desiredSize() { $getter; export function ready() { - if (!$isWritableStreamDefaultWriter(this)) - return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "ready")); + if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); return $getByIdDirectPrivate(this, "readyPromise").promise; } export function abort(reason) { - if (!$isWritableStreamDefaultWriter(this)) - return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "abort")); + if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); if ($getByIdDirectPrivate(this, "stream") === undefined) return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); @@ -72,8 +70,7 @@ export function abort(reason) { } export function close() { - if (!$isWritableStreamDefaultWriter(this)) - return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "close")); + if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); const stream = $getByIdDirectPrivate(this, "stream"); if (stream === undefined) return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); @@ -85,7 +82,7 @@ export function close() { } export function releaseLock() { - if (!$isWritableStreamDefaultWriter(this)) throw $makeThisTypeError("WritableStreamDefaultWriter", "releaseLock"); + if (!$isWritableStreamDefaultWriter(this)) throw $ERR_INVALID_THIS("WritableStreamDefaultWriter"); const stream = $getByIdDirectPrivate(this, "stream"); if (stream === undefined) return; @@ -95,8 +92,7 @@ export function releaseLock() { } export function write(chunk) { - if (!$isWritableStreamDefaultWriter(this)) - return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "write")); + if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); if ($getByIdDirectPrivate(this, "stream") === undefined) return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); diff --git a/src/js/builtins/shell.ts b/src/js/builtins/shell.ts index 28774036ac..2c3a1db28e 100644 --- a/src/js/builtins/shell.ts +++ b/src/js/builtins/shell.ts @@ -25,18 +25,25 @@ export function createBunShellTemplateFunction(createShellInterpreter, createPar this.#output = output; this.name = "ShellError"; - // Maybe we should just print all the properties on the Error instance - // instead of speical ones - this.info = { - exitCode: code, - stderr: output.stderr, - stdout: output.stdout, - }; + // We previously added this so that errors would display the "info" property + // We fixed that, but now it displays both. + Object.defineProperty(this, "info", { + value: { + exitCode: code, + stderr: output.stderr, + stdout: output.stdout, + }, + writable: true, + enumerable: false, + configurable: true, + }); this.info.stdout.toJSON = lazyBufferToHumanReadableString; this.info.stderr.toJSON = lazyBufferToHumanReadableString; - Object.assign(this, this.info); + this.stdout = output.stdout; + this.stderr = output.stderr; + this.exitCode = code; } text(encoding) { diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index d5e4d74da2..f7c3803758 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -433,7 +433,7 @@ function normalizePath(path) { // This is mostly for import.meta.resolve() // https://github.com/oven-sh/bun/issues/10304 path = Bun.fileURLToPath(path as URL); - } else if (path instanceof Blob) { + } else if ($inheritsBlob(path)) { // must be a Bun.file() blob // https://discord.com/channels/876711213126520882/1230114905898614794/1230114905898614794 path = path.name; @@ -447,7 +447,7 @@ function dlopen(path, options) { path = normalizePath(path); const result = nativeDLOpen(path, options); - if (result instanceof Error) throw result; + if (Error.isError(result)) throw result; for (let key in result.symbols) { var symbol = result.symbols[key]; @@ -495,7 +495,7 @@ function cc(options) { options.source = path; const result = ccFn(options); - if (result instanceof Error) throw result; + if (Error.isError(result)) throw result; for (let key in result.symbols) { var symbol = result.symbols[key]; diff --git a/src/js/bun/sql.ts b/src/js/bun/sql.ts index f4f29f431f..abe2a973cc 100644 --- a/src/js/bun/sql.ts +++ b/src/js/bun/sql.ts @@ -65,7 +65,7 @@ function normalizeSSLMode(value: string): SSLMode { } } - throw $ERR_INVALID_ARG_VALUE(`Invalid SSL mode: ${value}`); + throw $ERR_INVALID_ARG_VALUE("sslmode", value); } class Query extends PublicPromise { @@ -75,6 +75,15 @@ class Query extends PublicPromise { [_handler]; [_queryStatus] = 0; + [Symbol.for("nodejs.util.inspect.custom")]() { + const status = this[_queryStatus]; + const active = (status & QueryStatus.active) != 0; + const cancelled = (status & QueryStatus.cancelled) != 0; + const executed = (status & QueryStatus.executed) != 0; + const error = (status & QueryStatus.error) != 0; + return `PostgresQuery { ${active ? "active" : ""} ${cancelled ? "cancelled" : ""} ${executed ? "executed" : ""} ${error ? "error" : ""} }`; + } + constructor(handle, handler) { var resolve_, reject_; super((resolve, reject) => { @@ -182,7 +191,7 @@ class Query extends PublicPromise { Object.defineProperty(Query, Symbol.species, { value: PublicPromise }); Object.defineProperty(Query, Symbol.toStringTag, { value: "Query" }); init( - function (query, result, commandTag, count) { + function onResolvePostgresQuery(query, result, commandTag, count, queries) { $assert(result instanceof SQLResultArray, "Invalid result array"); if (typeof commandTag === "string") { if (commandTag.length > 0) { @@ -194,18 +203,48 @@ init( result.count = count || 0; + if (queries) { + const queriesIndex = queries.indexOf(query); + if (queriesIndex !== -1) { + queries.splice(queriesIndex, 1); + } + } + try { query.resolve(result); } catch (e) {} }, - function (query, reject) { + function onRejectPostgresQuery(query, reject, queries) { + if (queries) { + const queriesIndex = queries.indexOf(query); + if (queriesIndex !== -1) { + queries.splice(queriesIndex, 1); + } + } + try { query.reject(reject); } catch (e) {} }, ); -function createConnection({ hostname, port, username, password, tls, query, database, sslMode }, onConnected, onClose) { +function createConnection( + { + hostname, + port, + username, + password, + tls, + query, + database, + sslMode, + idleTimeout = 0, + connectionTimeout = 30 * 1000, + maxLifetime = 0, + }, + onConnected, + onClose, +) { return _createConnection( hostname, Number(port), @@ -221,6 +260,9 @@ function createConnection({ hostname, port, username, password, tls, query, data query || "", onConnected, onClose, + idleTimeout, + connectionTimeout, + maxLifetime, ); } @@ -312,7 +354,20 @@ class SQLArrayParameter { } function loadOptions(o) { - var hostname, port, username, password, database, tls, url, query, adapter; + var hostname, + port, + username, + password, + database, + tls, + url, + query, + adapter, + idleTimeout, + connectionTimeout, + maxLifetime, + onconnect, + onclose; const env = Bun.env; var sslMode: SSLMode = SSLMode.disable; @@ -375,6 +430,60 @@ function loadOptions(o) { tls ||= o.tls || o.ssl; adapter ||= o.adapter || "postgres"; + idleTimeout ??= o.idleTimeout; + idleTimeout ??= o.idle_timeout; + connectionTimeout ??= o.connectionTimeout; + connectionTimeout ??= o.connection_timeout; + maxLifetime ??= o.maxLifetime; + maxLifetime ??= o.max_lifetime; + + onconnect ??= o.onconnect; + onclose ??= o.onclose; + if (onconnect !== undefined) { + if (!$isCallable(onconnect)) { + throw $ERR_INVALID_ARG_TYPE("onconnect", "function", onconnect); + } + } + + if (onclose !== undefined) { + if (!$isCallable(onclose)) { + throw $ERR_INVALID_ARG_TYPE("onclose", "function", onclose); + } + } + + if (idleTimeout != null) { + idleTimeout = Number(idleTimeout); + if (idleTimeout > 2 ** 31 || idleTimeout < 0 || idleTimeout !== idleTimeout) { + throw $ERR_INVALID_ARG_VALUE( + "options.idle_timeout", + idleTimeout, + "must be a non-negative integer less than 2^31", + ); + } + } + + if (connectionTimeout != null) { + connectionTimeout = Number(connectionTimeout); + if (connectionTimeout > 2 ** 31 || connectionTimeout < 0 || connectionTimeout !== connectionTimeout) { + throw $ERR_INVALID_ARG_VALUE( + "options.connection_timeout", + connectionTimeout, + "must be a non-negative integer less than 2^31", + ); + } + } + + if (maxLifetime != null) { + maxLifetime = Number(maxLifetime); + if (maxLifetime > 2 ** 31 || maxLifetime < 0 || maxLifetime !== maxLifetime) { + throw $ERR_INVALID_ARG_VALUE( + "options.max_lifetime", + maxLifetime, + "must be a non-negative integer less than 2^31", + ); + } + } + if (sslMode !== SSLMode.disable && !tls?.serverName) { if (hostname) { tls = { @@ -398,7 +507,23 @@ function loadOptions(o) { throw new Error(`Unsupported adapter: ${adapter}. Only \"postgres\" is supported for now`); } - return { hostname, port, username, password, database, tls, query, sslMode }; + const ret: any = { hostname, port, username, password, database, tls, query, sslMode }; + if (idleTimeout != null) { + ret.idleTimeout = idleTimeout; + } + if (connectionTimeout != null) { + ret.connectionTimeout = connectionTimeout; + } + if (maxLifetime != null) { + ret.maxLifetime = maxLifetime; + } + if (onconnect !== undefined) { + ret.onconnect = onconnect; + } + if (onclose !== undefined) { + ret.onclose = onclose; + } + return ret; } function SQL(o) { @@ -407,6 +532,7 @@ function SQL(o) { connecting = false, closed = false, onConnect: any[] = [], + storedErrorForClosedConnection, connectionInfo = loadOptions(o); function connectedHandler(query, handle, err) { @@ -415,7 +541,7 @@ function SQL(o) { } if (!connected) { - return query.reject(new Error("Not connected")); + return query.reject(storedErrorForClosedConnection || new Error("Not connected")); } if (query.cancelled) { @@ -423,6 +549,10 @@ function SQL(o) { } handle.run(connection, query); + + // if the above throws, we don't want it to be in the array. + // This array exists mostly to keep the in-flight queries alive. + connection.queries.push(query); } function pendingConnectionHandler(query, handle) { @@ -434,7 +564,7 @@ function SQL(o) { } function closedConnectionHandler(query, handle) { - query.reject(new Error("Connection closed")); + query.reject(storedErrorForClosedConnection || new Error("Connection closed")); } function onConnected(err, result) { @@ -443,11 +573,31 @@ function SQL(o) { handler(err); } onConnect = []; + + if (connected && connectionInfo?.onconnect) { + connectionInfo.onconnect(err); + } } - function onClose(err) { + function onClose(err, queries) { closed = true; + storedErrorForClosedConnection = err; + if (sql === lazyDefaultSQL) { + resetDefaultSQL(initialDefaultSQL); + } + onConnected(err, undefined); + if (queries) { + const queriesCopy = queries.slice(); + queries.length = 0; + for (const handler of queriesCopy) { + handler.reject(err); + } + } + + if (connectionInfo?.onclose) { + connectionInfo.onclose(err); + } } function doCreateQuery(strings, values) { @@ -568,18 +718,23 @@ function SQL(o) { } var lazyDefaultSQL; -var defaultSQLObject = function sql(strings, ...values) { + +function resetDefaultSQL(sql) { + lazyDefaultSQL = sql; + Object.assign(defaultSQLObject, lazyDefaultSQL); + exportsObject.default = exportsObject.sql = lazyDefaultSQL; +} + +var initialDefaultSQL; +var defaultSQLObject = (initialDefaultSQL = function sql(strings, ...values) { if (new.target) { return SQL(strings); } - if (!lazyDefaultSQL) { - lazyDefaultSQL = SQL(undefined); - Object.assign(defaultSQLObject, lazyDefaultSQL); - exportsObject.default = exportsObject.sql = lazyDefaultSQL; + resetDefaultSQL(SQL(undefined)); } return lazyDefaultSQL(strings, ...values); -}; +}); var exportsObject = { sql: defaultSQLObject, diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index b21d2f330c..fb9d0391e1 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -5,15 +5,10 @@ // In a debug build, the import is always allowed. // It is disallowed in release builds unless run in Bun's CI. -/// +const fmtBinding = $bindgenFn("fmt.bind.ts", "fmtString"); -const fmtBinding = $newZigFunction("fmt.zig", "fmt_js_test_bindings.jsFunctionStringFormatter", 2) as ( - code: string, - id: number, -) => string; - -export const quickAndDirtyJavaScriptSyntaxHighlighter = (code: string) => fmtBinding(code, 0); -export const escapePowershell = (code: string) => fmtBinding(code, 1); +export const highlightJavaScript = (code: string) => fmtBinding(code, "highlight-javascript"); +export const escapePowershell = (code: string) => fmtBinding(code, "escape-powershell"); export const TLSBinding = $cpp("NodeTLS.cpp", "createNodeTLSBinding"); @@ -146,6 +141,14 @@ export const isModuleResolveFilenameSlowPathEnabled: () => boolean = $newCppFunc export const frameworkRouterInternals = $zig("FrameworkRouter.zig", "JSFrameworkRouter.getBindings") as { parseRoutePattern: (style: string, pattern: string) => null | { kind: string; pattern: string }; FrameworkRouter: { - new(opts: any): any; + new (opts: any): any; }; }; + +export const bindgen = $zig("bindgen_test.zig", "getBindgenTestFunctions") as { + add: (a: any, b: any) => number; + requiredAndOptionalArg: (a: any, b?: any, c?: any, d?: any) => number; +}; + +export const noOpForTesting = $cpp("NoOpForTesting.cpp", "createNoOpForTesting"); +export const Dequeue = require("internal/fifo"); diff --git a/src/js/internal/abort_listener.ts b/src/js/internal/abort_listener.ts new file mode 100644 index 0000000000..8a5ee71867 --- /dev/null +++ b/src/js/internal/abort_listener.ts @@ -0,0 +1,33 @@ +const { validateAbortSignal, validateFunction } = require("internal/validators"); +const { kResistStopPropagation } = require("internal/shared"); + +const SymbolDispose = Symbol.dispose; + +function addAbortListener(signal: AbortSignal, listener: EventListener): Disposable { + if (signal === undefined) { + throw $ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); + } + validateAbortSignal(signal, "signal"); + validateFunction(listener, "listener"); + + let removeEventListener; + if (signal.aborted) { + queueMicrotask(() => listener()); + } else { + // TODO(atlowChemi) add { subscription: true } and return directly + signal.addEventListener("abort", listener, { __proto__: null, once: true, [kResistStopPropagation]: true }); + removeEventListener = () => { + signal.removeEventListener("abort", listener); + }; + } + return { + __proto__: null, + [SymbolDispose]() { + removeEventListener?.(); + }, + }; +} + +export default { + addAbortListener, +}; diff --git a/src/js/internal/assert/assertion_error.ts b/src/js/internal/assert/assertion_error.ts new file mode 100644 index 0000000000..780e348915 --- /dev/null +++ b/src/js/internal/assert/assertion_error.ts @@ -0,0 +1,420 @@ +"use strict"; + +const { inspect } = require("internal/util/inspect"); +const colors = require("internal/util/colors"); +const { validateObject } = require("internal/validators"); + +const ErrorCaptureStackTrace = Error.captureStackTrace; +const ObjectAssign = Object.assign; +const ObjectDefineProperty = Object.defineProperty; +const ObjectGetPrototypeOf = Object.getPrototypeOf; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +const ArrayPrototypeJoin = Array.prototype.join; +const ArrayPrototypePop = Array.prototype.pop; +const ArrayPrototypeSlice = Array.prototype.slice; +const StringPrototypeRepeat = String.prototype.repeat; +const StringPrototypeSlice = String.prototype.slice; +const StringPrototypeSplit = String.prototype.split; + +declare namespace Internal { + const enum Operation { + Insert = 0, + Delete = 1, + Equal = 2, + } + interface Diff { + kind: Operation; + value: string; + } + + function myersDiff(actual: string, expected: string, checkCommaDisparity?: boolean, lines?: boolean): string; + // todo + + function printMyersDiff(...args: any[]): any; + function printSimpleMyersDiff(...args: any[]): any; +} + +const { myersDiff, printMyersDiff, printSimpleMyersDiff } = require("internal/assert/myers_diff") as typeof Internal; + +const kReadableOperator = { + deepStrictEqual: "Expected values to be strictly deep-equal:", + strictEqual: "Expected values to be strictly equal:", + strictEqualObject: 'Expected "actual" to be reference-equal to "expected":', + deepEqual: "Expected values to be loosely deep-equal:", + notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:', + notStrictEqual: 'Expected "actual" to be strictly unequal to:', + notStrictEqualObject: 'Expected "actual" not to be reference-equal to "expected":', + notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:', + notIdentical: "Values have same structure but are not reference-equal:", + notDeepEqualUnequal: "Expected values not to be loosely deep-equal:", +}; + +const kMaxShortStringLength = 12; +const kMaxLongStringLength = 512; + +function copyError(source) { + const target = ObjectAssign({ __proto__: ObjectGetPrototypeOf(source) }, source); + ObjectDefineProperty(target, "message", { + __proto__: null, + value: source.message, + }); + if (ObjectPrototypeHasOwnProperty.$call(source, "cause")) { + let { cause } = source; + + if (Error.isError(cause)) { + cause = copyError(cause); + } + + ObjectDefineProperty(target, "cause", { __proto__: null, value: cause }); + } + return target; +} + +function inspectValue(val) { + // The util.inspect default values could be changed. This makes sure the + // error messages contain the necessary information nevertheless. + return inspect(val, { + compact: false, + customInspect: false, + depth: 1000, + maxArrayLength: Infinity, + // Assert compares only enumerable properties (with a few exceptions). + showHidden: false, + // Assert does not detect proxies currently. + showProxy: false, + sorted: true, + // Inspect getters as we also check them when comparing entries. + getters: true, + }); +} + +function getErrorMessage(operator, message) { + return message || kReadableOperator[operator]; +} + +function checkOperator(actual, expected, operator) { + // In case both values are objects or functions explicitly mark them as not + // reference equal for the `strictEqual` operator. + if ( + operator === "strictEqual" && + ((typeof actual === "object" && actual !== null && typeof expected === "object" && expected !== null) || + (typeof actual === "function" && typeof expected === "function")) + ) { + operator = "strictEqualObject"; + } + + return operator; +} + +function getColoredMyersDiff(actual, expected) { + const header = `${colors.green}actual${colors.white} ${colors.red}expected${colors.white}`; + const skipped = false; + + // const diff = myersDiff(StringPrototypeSplit.$call(actual, ""), StringPrototypeSplit.$call(expected, "")); + const diff = myersDiff(actual, expected, false, false); + let message = printSimpleMyersDiff(diff); + + if (skipped) { + message += "..."; + } + + return { message, header, skipped }; +} + +function getStackedDiff(actual, expected) { + const isStringComparison = typeof actual === "string" && typeof expected === "string"; + + let message = `\n${colors.green}+${colors.white} ${actual}\n${colors.red}- ${colors.white}${expected}`; + const stringsLen = actual.length + expected.length; + const maxTerminalLength = process.stderr.isTTY ? process.stderr.columns : 80; + const showIndicator = isStringComparison && stringsLen <= maxTerminalLength; + + if (showIndicator) { + let indicatorIdx = -1; + + for (let i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) { + // Skip the indicator for the first 2 characters because the diff is immediately apparent + // It is 3 instead of 2 to account for the quotes + if (i >= 3) { + indicatorIdx = i; + } + break; + } + } + + if (indicatorIdx !== -1) { + message += `\n${StringPrototypeRepeat.$call(" ", indicatorIdx + 2)}^`; + } + } + + return { message }; +} + +function getSimpleDiff(originalActual, actual: string, originalExpected, expected: string) { + let stringsLen = actual.length + expected.length; + // Accounting for the quotes wrapping strings + if (typeof originalActual === "string") { + stringsLen -= 2; + } + if (typeof originalExpected === "string") { + stringsLen -= 2; + } + if (stringsLen <= kMaxShortStringLength && (originalActual !== 0 || originalExpected !== 0)) { + return { message: `${actual} !== ${expected}`, header: "" }; + } + + const isStringComparison = typeof originalActual === "string" && typeof originalExpected === "string"; + // colored myers diff + if (isStringComparison && colors.hasColors) { + return getColoredMyersDiff(actual, expected); + } + + return getStackedDiff(actual, expected); +} + +function isSimpleDiff(actual, inspectedActual, expected, inspectedExpected) { + if (inspectedActual.length > 1 || inspectedExpected.length > 1) { + return false; + } + + return typeof actual !== "object" || actual === null || typeof expected !== "object" || expected === null; +} + +function createErrDiff(actual, expected, operator, customMessage) { + operator = checkOperator(actual, expected, operator); + + let skipped = false; + let message = ""; + const inspectedActual = inspectValue(actual); + const inspectedExpected = inspectValue(expected); + const inspectedSplitActual = StringPrototypeSplit.$call(inspectedActual, "\n"); + const inspectedSplitExpected = StringPrototypeSplit.$call(inspectedExpected, "\n"); + const showSimpleDiff = isSimpleDiff(actual, inspectedSplitActual, expected, inspectedSplitExpected); + let header = `${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`; + + if (showSimpleDiff) { + const simpleDiff = getSimpleDiff(actual, inspectedSplitActual[0], expected, inspectedSplitExpected[0]); + message = simpleDiff.message; + if (typeof simpleDiff.header !== "undefined") { + header = simpleDiff.header; + } + if (simpleDiff.skipped) { + skipped = true; + } + } else if (inspectedActual === inspectedExpected) { + // Handles the case where the objects are structurally the same but different references + operator = "notIdentical"; + if (inspectedSplitActual.length > 50) { + message = `${ArrayPrototypeJoin.$call(ArrayPrototypeSlice.$call(inspectedSplitActual, 0, 50), "\n")}\n...}`; + skipped = true; + } else { + message = ArrayPrototypeJoin.$call(inspectedSplitActual, "\n"); + } + header = ""; + } else { + const checkCommaDisparity = actual != null && typeof actual === "object"; + const diff = myersDiff(inspectedActual, inspectedExpected, checkCommaDisparity, true); + + const myersDiffMessage = printMyersDiff(diff); + message = myersDiffMessage.message; + + if (myersDiffMessage.skipped) { + skipped = true; + } + } + + const headerMessage = `${getErrorMessage(operator, customMessage)}\n${header}`; + const skippedMessage = skipped ? "\n... Skipped lines" : ""; + + return `${headerMessage}${skippedMessage}\n${message}\n`; +} + +function addEllipsis(string) { + const lines = StringPrototypeSplit.$call(string, "\n", 11); + if (lines.length > 10) { + lines.length = 10; + return `${ArrayPrototypeJoin.$call(lines, "\n")}\n...`; + } else if (string.length > kMaxLongStringLength) { + return `${StringPrototypeSlice.$call(string, kMaxLongStringLength)}...`; + } + return string; +} + +class AssertionError extends Error { + constructor(options) { + validateObject(options, "options"); + const { + message, + operator, + stackStartFn, + details, + // Compatibility with older versions. + stackStartFunction, + } = options; + let { actual, expected } = options; + + // NOTE: stack trace is always writable. + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + + if (message != null) { + if (operator === "deepStrictEqual" || operator === "strictEqual") { + super(createErrDiff(actual, expected, operator, message)); + } else { + super(String(message)); + } + } else { + // Reset colors on each call to make sure we handle dynamically set environment + // variables correct. + colors.refresh(); + // Prevent the error stack from being visible by duplicating the error + // in a very close way to the original in case both sides are actually + // instances of Error. + if ( + typeof actual === "object" && + actual !== null && + typeof expected === "object" && + expected !== null && + "stack" in actual && + actual instanceof Error && + "stack" in expected && + expected instanceof Error + ) { + actual = copyError(actual); + expected = copyError(expected); + } + + if (operator === "deepStrictEqual" || operator === "strictEqual") { + super(createErrDiff(actual, expected, operator, message)); + } else if (operator === "notDeepStrictEqual" || operator === "notStrictEqual") { + // In case the objects are equal but the operator requires unequal, show + // the first object and say A equals B + let base = kReadableOperator[operator]; + const res = StringPrototypeSplit.$call(inspectValue(actual), "\n"); + + // In case "actual" is an object or a function, it should not be + // reference equal. + if ( + operator === "notStrictEqual" && + ((typeof actual === "object" && actual !== null) || typeof actual === "function") + ) { + base = kReadableOperator.notStrictEqualObject; + } + + // Only remove lines in case it makes sense to collapse those. + // TODO: Accept env to always show the full error. + if (res.length > 50) { + res[46] = `${colors.blue}...${colors.white}`; + while (res.length > 47) { + ArrayPrototypePop.$call(res); + } + } + + // Only print a single input. + if (res.length === 1) { + super(`${base}${res[0].length > 5 ? "\n\n" : " "}${res[0]}`); + } else { + super(`${base}\n\n${ArrayPrototypeJoin.$call(res, "\n")}\n`); + } + } else { + let res = inspectValue(actual); + let other = inspectValue(expected); + const knownOperator = kReadableOperator[operator]; + if (operator === "notDeepEqual" && res === other) { + res = `${knownOperator}\n\n${res}`; + if (res.length > 1024) { + res = `${StringPrototypeSlice.$call(res, 0, 1021)}...`; + } + super(res); + } else { + if (res.length > kMaxLongStringLength) { + res = `${StringPrototypeSlice.$call(res, 0, 509)}...`; + } + if (other.length > kMaxLongStringLength) { + other = `${StringPrototypeSlice.$call(other, 0, 509)}...`; + } + if (operator === "deepEqual") { + res = `${knownOperator}\n\n${res}\n\nshould loosely deep-equal\n\n`; + } else { + const newOp = kReadableOperator[`${operator}Unequal`]; + if (newOp) { + res = `${newOp}\n\n${res}\n\nshould not loosely deep-equal\n\n`; + } else { + other = ` ${operator} ${other}`; + } + } + super(`${res}${other}`); + } + } + } + + Error.stackTraceLimit = limit; + + this.generatedMessage = !message; + ObjectDefineProperty(this, "name", { + __proto__: null, + value: "AssertionError [ERR_ASSERTION]", + enumerable: false, + writable: true, + configurable: true, + }); + this.code = "ERR_ASSERTION"; + if (details) { + this.actual = undefined; + this.expected = undefined; + this.operator = undefined; + for (let i = 0; i < details.length; i++) { + this["message " + i] = details[i].message; + this["actual " + i] = details[i].actual; + this["expected " + i] = details[i].expected; + this["operator " + i] = details[i].operator; + this["stack trace " + i] = details[i].stack; + } + } else { + this.actual = actual; + this.expected = expected; + this.operator = operator; + } + ErrorCaptureStackTrace(this, stackStartFn || stackStartFunction); + // Create error message including the error code in the name. + this.stack; // eslint-disable-line no-unused-expressions + // Reset the name. + this.name = "AssertionError"; + } + + toString() { + return `${this.name} [${this.code}]: ${this.message}`; + } + + [inspect.custom](recurseTimes, ctx) { + // Long strings should not be fully inspected. + const tmpActual = this.actual; + const tmpExpected = this.expected; + + if (typeof this.actual === "string") { + this.actual = addEllipsis(this.actual); + } + if (typeof this.expected === "string") { + this.expected = addEllipsis(this.expected); + } + + // This limits the `actual` and `expected` property default inspection to + // the minimum depth. Otherwise those values would be too verbose compared + // to the actual error message which contains a combined view of these two + // input values. + const result = inspect(this, { + ...ctx, + customInspect: false, + depth: 0, + }); + + // Reset the properties after inspection. + this.actual = tmpActual; + this.expected = tmpExpected; + + return result; + } +} + +export default AssertionError; diff --git a/src/js/internal/assert/calltracker.ts b/src/js/internal/assert/calltracker.ts new file mode 100644 index 0000000000..57221f78cd --- /dev/null +++ b/src/js/internal/assert/calltracker.ts @@ -0,0 +1,136 @@ +"use strict"; + +const { SafeSet, SafeWeakMap } = require("internal/primordials"); + +const AssertionError = require("internal/assert/assertion_error"); +const { validateUint32 } = require("internal/validators"); + +const ObjectFreeze = Object.freeze; +const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeSlice = Array.prototype.slice; + +const noop = () => {}; + +class CallTrackerContext { + #expected; + #calls; + #name; + #stackTrace; + constructor({ expected, stackTrace, name }) { + this.#calls = []; + this.#expected = expected; + this.#stackTrace = stackTrace; + this.#name = name; + } + + track(thisArg, args) { + const argsClone = ObjectFreeze(ArrayPrototypeSlice.$call(args)); + ArrayPrototypePush.$call(this.#calls, ObjectFreeze({ thisArg, arguments: argsClone })); + } + + get delta() { + return this.#calls.length - this.#expected; + } + + reset() { + this.#calls = []; + } + getCalls() { + return ObjectFreeze(ArrayPrototypeSlice.$call(this.#calls)); + } + + report() { + if (this.delta !== 0) { + const message = + `Expected the ${this.#name} function to be ` + + `executed ${this.#expected} time(s) but was ` + + `executed ${this.#calls.length} time(s).`; + return { + message, + actual: this.#calls.length, + expected: this.#expected, + operator: this.#name, + stack: this.#stackTrace, + }; + } + } +} + +class CallTracker { + #callChecks = new SafeSet(); + #trackedFunctions = new SafeWeakMap(); + + #getTrackedFunction(tracked) { + if (!this.#trackedFunctions.has(tracked)) { + throw $ERR_INVALID_ARG_VALUE("tracked", tracked, "is not a tracked function"); + } + return this.#trackedFunctions.get(tracked); + } + + reset(tracked) { + if (tracked === undefined) { + this.#callChecks.forEach(check => check.reset()); + return; + } + + this.#getTrackedFunction(tracked).reset(); + } + + getCalls(tracked) { + return this.#getTrackedFunction(tracked).getCalls(); + } + + calls(fn, expected = 1) { + if (process._exiting) throw $ERR_UNAVAILABLE_DURING_EXIT("Cannot call function in process exit handler"); + if (typeof fn === "number") { + expected = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + validateUint32(expected, "expected", true); + + const context = new CallTrackerContext({ + expected, + // eslint-disable-next-line no-restricted-syntax + stackTrace: new Error(), + name: fn.name || "calls", + }); + const tracked = new Proxy(fn, { + __proto__: null, + apply(fn, thisArg, argList) { + context.track(thisArg, argList); + return fn.$apply(thisArg, argList); + }, + }); + this.#callChecks.add(context); + this.#trackedFunctions.set(tracked, context); + return tracked; + } + + report() { + const errors = []; + for (const context of this.#callChecks) { + const message = context.report(); + if (message !== undefined) { + ArrayPrototypePush.$call(errors, message); + } + } + return errors; + } + + verify() { + const errors = this.report(); + if (errors.length === 0) { + return; + } + const message = errors.length === 1 ? errors[0].message : "Functions were not called the expected number of times"; + throw new AssertionError({ + message, + details: errors, + }); + } +} + +export default CallTracker; diff --git a/src/js/internal/assert/myers_diff.ts b/src/js/internal/assert/myers_diff.ts new file mode 100644 index 0000000000..63d0fb45d8 --- /dev/null +++ b/src/js/internal/assert/myers_diff.ts @@ -0,0 +1,111 @@ +/// +"use strict"; + +const colors = require("internal/util/colors"); + +const enum Operation { + Insert = 0, + Delete = 1, + Equal = 2, +} +interface Diff { + kind: Operation; + /** + * When diffing chars (that is, `line == false`, this is a char code.) + */ + value: string | number; +} + +declare namespace Internal { + export function myersDiff( + actual: string[], + expected: string[], + checkCommaDisparity?: boolean, + lines?: boolean, + ): Diff[]; +} + +const kNopLinesToCollapse = 5; + +const { myersDiff } = $zig("node_assert_binding.zig", "generate") as typeof Internal; + +function printSimpleMyersDiff(diff: Diff[]) { + let message = ""; + + for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) { + let { kind, value } = diff[diffIdx]; + if (typeof value === "number") { + value = String.fromCharCode(value); + } + switch (kind) { + case Operation.Insert: + message += `${colors.green}${value}${colors.white}`; + break; + case Operation.Delete: + message += `${colors.red}${value}${colors.white}`; + break; + case Operation.Equal: + message += `${colors.white}${value}${colors.white}`; + break; + default: + throw new TypeError(`Invalid diff operation kind: ${kind}`); // should be unreachable + } + } + + return `\n${message}`; +} + +function printMyersDiff(diff: Diff[], simple = false) { + let message = ""; + let skipped = false; + let nopCount = 0; + + for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) { + const { kind, value } = diff[diffIdx]; + $assert( + typeof value !== "number", + "printMyersDiff is only called for line diffs, which never return numeric char code values.", + ); + const previousType = diffIdx < diff.length - 1 ? diff[diffIdx + 1].kind : null; + const typeChanged = previousType && kind !== previousType; + + if (typeChanged && previousType === Operation.Equal) { + // Avoid grouping if only one line would have been grouped otherwise + if (nopCount === kNopLinesToCollapse + 1) { + message += `${colors.white} ${diff[diffIdx + 1].value}\n`; + } else if (nopCount === kNopLinesToCollapse + 2) { + message += `${colors.white} ${diff[diffIdx + 2].value}\n`; + message += `${colors.white} ${diff[diffIdx + 1].value}\n`; + } + if (nopCount >= kNopLinesToCollapse + 3) { + message += `${colors.blue}...${colors.white}\n`; + message += `${colors.white} ${diff[diffIdx + 1].value}\n`; + skipped = true; + } + nopCount = 0; + } + + switch (kind) { + case Operation.Insert: + message += `${colors.green}+${colors.white} ${value}\n`; + break; + case Operation.Delete: + message += `${colors.red}-${colors.white} ${value}\n`; + break; + case Operation.Equal: + if (nopCount < kNopLinesToCollapse) { + message += `${colors.white} ${value}\n`; + } + nopCount++; + break; + default: + throw new TypeError(`Invalid diff operation kind: ${kind}`); // should be unreachable + } + } + + message = message.trimEnd(); + + return { message: `\n${message}`, skipped }; +} + +export default { myersDiff, printMyersDiff, printSimpleMyersDiff }; diff --git a/src/js/internal/assert/utils.ts b/src/js/internal/assert/utils.ts new file mode 100644 index 0000000000..f5dfa91263 --- /dev/null +++ b/src/js/internal/assert/utils.ts @@ -0,0 +1,272 @@ +/* prettier-ignore */ +'use strict'; + +var AssertionError; +function loadAssertionError() { + if (AssertionError === undefined) { + AssertionError = require("internal/assert/assertion_error"); + } +} + +// const { Buffer } = require('node:buffer'); +// const { +// isErrorStackTraceLimitWritable, +// overrideStackTrace, +// } = require('internal/errors'); +// const { openSync, closeSync, readSync } = require('node:fs'); +// // const { EOL } = require('internal/constants'); +// // const { BuiltinModule } = require('internal/bootstrap/realm'); +// // const { isError } = require('internal/util'); + +// const errorCache = new SafeMap(); +// // const { fileURLToPath } = require('internal/url'); + +// let parseExpressionAt; +// let findNodeAround; +// let tokenizer; +// let decoder; + +// // Escape control characters but not \n and \t to keep the line breaks and +// // indentation intact. +// // eslint-disable-next-line no-control-regex +// const escapeSequencesRegExp = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g; +// const meta = [ +// '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', +// '\\u0005', '\\u0006', '\\u0007', '\\b', '', +// '', '\\u000b', '\\f', '', '\\u000e', +// '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', +// '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', +// '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', +// '\\u001e', '\\u001f', +// ]; + +// const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)]; + +// function findColumn(fd, column: number, code: string) { +// if (code.length > column + 100) { +// try { +// return parseCode(code, column); +// } catch { +// // End recursion in case no code could be parsed. The expression should +// // have been found after 2500 characters, so stop trying. +// if (code.length - column > 2500) { +// // eslint-disable-next-line no-throw-literal +// throw null; +// } +// } +// } +// // Read up to 2500 bytes more than necessary in columns. That way we address +// // multi byte characters and read enough data to parse the code. +// const bytesToRead = column - code.length + 2500; +// const buffer = Buffer.allocUnsafe(bytesToRead); +// const bytesRead = readSync(fd, buffer, 0, bytesToRead); +// code += decoder.write(buffer.slice(0, bytesRead)); +// // EOF: fast path. +// if (bytesRead < bytesToRead) { +// return parseCode(code, column); +// } +// // Read potentially missing code. +// return findColumn(fd, column, code); +// } + +// function getCode(fd, line: number, column: number) { +// let bytesRead = 0; +// if (line === 0) { +// // Special handle line number one. This is more efficient and simplifies the +// // rest of the algorithm. Read more than the regular column number in bytes +// // to prevent multiple reads in case multi byte characters are used. +// return findColumn(fd, column, ''); +// } +// let lines = 0; +// // Prevent blocking the event loop by limiting the maximum amount of +// // data that may be read. +// let maxReads = 32; // bytesPerRead * maxReads = 512 KiB +// const bytesPerRead = 16384; +// // Use a single buffer up front that is reused until the call site is found. +// let buffer = Buffer.allocUnsafe(bytesPerRead); +// while (maxReads-- !== 0) { +// // Only allocate a new buffer in case the needed line is found. All data +// // before that can be discarded. +// buffer = lines < line ? buffer : Buffer.allocUnsafe(bytesPerRead); +// bytesRead = readSync(fd, buffer, 0, bytesPerRead); +// // Read the buffer until the required code line is found. +// for (let i = 0; i < bytesRead; i++) { +// if (buffer[i] === 10 && ++lines === line) { +// // If the end of file is reached, directly parse the code and return. +// if (bytesRead < bytesPerRead) { +// return parseCode(buffer.toString('utf8', i + 1, bytesRead), column); +// } +// // Check if the read code is sufficient or read more until the whole +// // expression is read. Make sure multi byte characters are preserved +// // properly by using the decoder. +// const code = decoder.write(buffer.slice(i + 1, bytesRead)); +// return findColumn(fd, column, code); +// } +// } +// } +// } + +// TODO: parse source to get assertion message +// function parseCode(code, offset) { +// // Lazy load acorn. +// if (parseExpressionAt === undefined) { +// const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; +// ({ findNodeAround } = require('internal/deps/acorn/acorn-walk/dist/walk')); + +// parseExpressionAt = FunctionPrototypeBind(Parser.parseExpressionAt, Parser); +// tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser); +// } +// let node; +// let start; +// // Parse the read code until the correct expression is found. +// for (const token of tokenizer(code, { ecmaVersion: 'latest' })) { +// start = token.start; +// if (start > offset) { +// // No matching expression found. This could happen if the assert +// // expression is bigger than the provided buffer. +// break; +// } +// try { +// node = parseExpressionAt(code, start, { ecmaVersion: 'latest' }); +// // Find the CallExpression in the tree. +// node = findNodeAround(node, offset, 'CallExpression'); +// if (node?.node.end >= offset) { +// return [ +// node.node.start, +// StringPrototypeReplace(StringPrototypeSlice(code, +// node.node.start, node.node.end), +// escapeSequencesRegExp, escapeFn), +// ]; +// } +// // eslint-disable-next-line no-unused-vars +// } catch (err) { +// continue; +// } +// } +// // eslint-disable-next-line no-throw-literal +// throw null; +// } + +function getErrMessage(message: string, value: unknown, fn: Function): string | undefined { + // const tmpLimit = Error.stackTraceLimit; + // const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable(); + // Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it + // does to much work. + // if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 1; + // We only need the stack trace. To minimize the overhead use an object + // instead of an error. + // const err = {}; + // ErrorCaptureStackTrace(err, fn); + // if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit; + // overrideStackTrace.set(err, (_, stack) => stack); + // const call = err.stack[0]; + // + // if (fn.name === "ok") { + // return `The expression evaluated to a falsy value:\n\n assert.ok(${value})\n`; + // } + // let filename = call.getFileName(); + // const line = call.getLineNumber() - 1; + // let column = call.getColumnNumber() - 1; + // let identifier; + // let code; + // if (filename) { + // identifier = `${filename}${line}${column}`; + // // Skip Node.js modules! + // if (StringPrototypeStartsWith(filename, 'node:') && + // BuiltinModule.exists(StringPrototypeSlice(filename, 5))) { + // errorCache.set(identifier, undefined); + // return; + // } + // } else { + // return message; + // } + // if (errorCache.has(identifier)) { + // return errorCache.get(identifier); + // } + // let fd; + // try { + // // Set the stack trace limit to zero. This makes sure unexpected token + // // errors are handled faster. + // if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 0; + // if (filename) { + // if (decoder === undefined) { + // const { StringDecoder } = require('string_decoder'); + // decoder = new StringDecoder('utf8'); + // } + // // ESM file prop is a file proto. Convert that to path. + // // This ensure opensync will not throw ENOENT for ESM files. + // const fileProtoPrefix = 'file://'; + // if (StringPrototypeStartsWith(filename, fileProtoPrefix)) { + // filename = Bun.fileURLToPath(filename); + // } + // fd = openSync(filename, 'r', 0o666); + // // Reset column and message. + // ({ 0: column, 1: message } = getCode(fd, line, column)); + // // Flush unfinished multi byte characters. + // decoder.end(); + // } else { + // for (let i = 0; i < line; i++) { + // code = StringPrototypeSlice(code, + // StringPrototypeIndexOf(code, '\n') + 1); + // } + // // ({ 0: column, 1: message } = parseCode(code, column)); + // throw new Error("todo: parseCode"); + // } + // // Always normalize indentation, otherwise the message could look weird. + // if (StringPrototypeIncludes(message, '\n')) { + // if (process.platform === 'win32') { + // message = RegExpPrototypeSymbolReplace(/\r\n/g, message, '\n'); + // } + // const frames = StringPrototypeSplit(message, '\n'); + // message = ArrayPrototypeShift(frames); + // for (const frame of frames) { + // let pos = 0; + // while (pos < column && (frame[pos] === ' ' || frame[pos] === '\t')) { + // pos++; + // } + // message += `\n ${StringPrototypeSlice(frame, pos)}`; + // } + // } + // message = `The expression evaluated to a falsy value:\n\n ${message}\n`; + // // Make sure to always set the cache! No matter if the message is + // // undefined or not + // errorCache.set(identifier, message); + // return message; + // } catch { + // // Invalidate cache to prevent trying to read this part again. + // errorCache.set(identifier, undefined); + // } finally { + // // Reset limit. + // if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit; + // if (fd !== undefined) + // closeSync(fd); + // } +} + +export function innerOk(fn, argLen, value, message) { + if (!value) { + let generatedMessage = false; + + if (argLen === 0) { + generatedMessage = true; + message = "No value argument passed to `assert.ok()`"; + } else if (message == null) { + generatedMessage = true; + message = getErrMessage(message, value, fn); + // TODO: message + } else if (Error.isError(message)) { + throw message; + } + + if (AssertionError === undefined) loadAssertionError(); + const err = new AssertionError({ + actual: value, + expected: true, + message, + operator: "==", + stackStartFn: fn, + }); + err.generatedMessage = generatedMessage; + throw err; + } +} diff --git a/src/js/internal/crypto/x509.ts b/src/js/internal/crypto/x509.ts new file mode 100644 index 0000000000..6a2f5c8cbd --- /dev/null +++ b/src/js/internal/crypto/x509.ts @@ -0,0 +1,3 @@ +const isX509Certificate = $newCppFunction("JSX509Certificate.cpp", "jsIsX509Certificate", 1); + +export { isX509Certificate }; diff --git a/src/js/internal/errors.ts b/src/js/internal/errors.ts index fd5036ec86..b0e84f460e 100644 --- a/src/js/internal/errors.ts +++ b/src/js/internal/errors.ts @@ -1,13 +1,22 @@ +const { SafeArrayIterator } = require("internal/primordials"); + +const ArrayIsArray = Array.isArray; +const ArrayPrototypePush = Array.prototype.push; + +function aggregateTwoErrors(innerError, outerError) { + if (innerError && outerError && innerError !== outerError) { + if (ArrayIsArray(outerError.errors)) { + // If `outerError` is already an `AggregateError`. + ArrayPrototypePush.$call(outerError.errors, innerError); + return outerError; + } + const err = new AggregateError(new SafeArrayIterator([outerError, innerError]), outerError.message); + err.code = outerError.code; + return err; + } + return innerError || outerError; +} + export default { - ERR_INVALID_ARG_TYPE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_INVALID_ARG_TYPE", 3), - ERR_OUT_OF_RANGE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_OUT_OF_RANGE", 3), - ERR_IPC_DISCONNECTED: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_IPC_DISCONNECTED", 0), - ERR_SERVER_NOT_RUNNING: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_SERVER_NOT_RUNNING", 0), - ERR_IPC_CHANNEL_CLOSED: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_IPC_CHANNEL_CLOSED", 0), - ERR_SOCKET_BAD_TYPE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_SOCKET_BAD_TYPE", 0), - ERR_INVALID_PROTOCOL: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_INVALID_PROTOCOL", 0), - ERR_BROTLI_INVALID_PARAM: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_BROTLI_INVALID_PARAM", 0), - ERR_BUFFER_TOO_LARGE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_BUFFER_TOO_LARGE", 0), - ERR_ZLIB_INITIALIZATION_FAILED: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_ZLIB_INITIALIZATION_FAILED", 0), - ERR_BUFFER_OUT_OF_BOUNDS: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_BUFFER_OUT_OF_BOUNDS", 0), + aggregateTwoErrors, }; diff --git a/src/js/internal/fifo.ts b/src/js/internal/fifo.ts index a7438aa3bd..c9ead66706 100644 --- a/src/js/internal/fifo.ts +++ b/src/js/internal/fifo.ts @@ -1,5 +1,10 @@ var slice = Array.prototype.slice; -class Denqueue { +class Dequeue { + _head: number; + _tail: number; + _capacityMask: number; + _list: (T | undefined)[]; + constructor() { this._head = 0; this._tail = 0; @@ -8,26 +13,21 @@ class Denqueue { this._list = $newArrayWithSize(4); } - _head; - _tail; - _capacityMask; - _list; - - size() { + size(): number { if (this._head === this._tail) return 0; if (this._head < this._tail) return this._tail - this._head; else return this._capacityMask + 1 - (this._head - this._tail); } - isEmpty() { + isEmpty(): boolean { return this.size() == 0; } - isNotEmpty() { + isNotEmpty(): boolean { return this.size() > 0; } - shift() { + shift(): T | undefined { var { _head: head, _tail, _list, _capacityMask } = this; if (head === _tail) return undefined; var item = _list[head]; @@ -37,24 +37,21 @@ class Denqueue { return item; } - peek() { + peek(): T | undefined { if (this._head === this._tail) return undefined; return this._list[this._head]; } - push(item) { + push(item: T): void { var tail = this._tail; $putByValDirect(this._list, tail, item); this._tail = (tail + 1) & this._capacityMask; if (this._tail === this._head) { this._growArray(); } - // if (this._capacity && this.size() > this._capacity) { - // this.shift(); - // } } - toArray(fullCopy) { + toArray(fullCopy: boolean): T[] { var list = this._list; var len = $toLength(list.length); @@ -66,19 +63,19 @@ class Denqueue { var j = 0; for (var i = _head; i < len; i++) $putByValDirect(array, j++, list[i]); for (var i = 0; i < _tail; i++) $putByValDirect(array, j++, list[i]); - return array; + return array as T[]; } else { return slice.$call(list, this._head, this._tail); } } - clear() { + clear(): void { this._head = 0; this._tail = 0; this._list.fill(undefined); } - _growArray() { + private _growArray(): void { if (this._head) { // copy existing data, head to end, then beginning to tail. this._list = this.toArray(true); @@ -92,10 +89,10 @@ class Denqueue { this._capacityMask = (this._capacityMask << 1) | 1; } - _shrinkArray() { + private _shrinkArray(): void { this._list.length >>>= 1; this._capacityMask >>>= 1; } } -export default Denqueue; +export default Dequeue; diff --git a/src/js/internal/primordials.js b/src/js/internal/primordials.js index e68d6d6fe3..046f44fa65 100644 --- a/src/js/internal/primordials.js +++ b/src/js/internal/primordials.js @@ -2,21 +2,24 @@ // Do not use this file for new code, many things here will be slow especailly when intrinsics for these operations is available. // It is primarily used for `internal/util` -const createSafeIterator = (factory, next) => { +const ObjectSetPrototypeOf = Object.setPrototypeOf; +const ObjectFreeze = Object.freeze; + +const createSafeIterator = (factory, next_) => { class SafeIterator { constructor(iterable) { this._iterator = factory(iterable); } next() { - return next(this._iterator); + return next_(this._iterator); } [Symbol.iterator]() { return this; } } - Object.setPrototypeOf(SafeIterator.prototype, null); - Object.freeze(SafeIterator.prototype); - Object.freeze(SafeIterator); + ObjectSetPrototypeOf(SafeIterator.prototype, null); + ObjectFreeze(SafeIterator.prototype); + ObjectFreeze(SafeIterator); return SafeIterator; }; @@ -75,86 +78,31 @@ const makeSafe = (unsafe, safe) => { const StringIterator = uncurryThis(String.prototype[Symbol.iterator]); const StringIteratorPrototype = Reflect.getPrototypeOf(StringIterator("")); const ArrayPrototypeForEach = uncurryThis(Array.prototype.forEach); - -function ErrorCaptureStackTrace(targetObject) { - const stack = new Error().stack; - // Remove the second line, which is this function - targetObject.stack = stack.replace(/.*\n.*/, "$1"); -} - -const arrayProtoPush = Array.prototype.push; const ArrayPrototypeSymbolIterator = uncurryThis(Array.prototype[Symbol.iterator]); -const ArrayIteratorPrototypeNext = uncurryThis(ArrayPrototypeSymbolIterator.next); +const ArrayIteratorPrototypeNext = uncurryThis(Array.prototype[Symbol.iterator]().next); +const SafeArrayIterator = createSafeIterator(ArrayPrototypeSymbolIterator, ArrayIteratorPrototypeNext); + +const ArrayPrototypeMap = Array.prototype.map; +const PromisePrototypeThen = Promise.prototype.then; + +const arrayToSafePromiseIterable = (promises, mapFn) => + new SafeArrayIterator( + ArrayPrototypeMap.$call( + promises, + (promise, i) => + new Promise((a, b) => PromisePrototypeThen.$call(mapFn == null ? promise : mapFn(promise, i), a, b)), + ), + ); +const PromiseAll = Promise.all; +const SafePromiseAll = (promises, mapFn) => PromiseAll(arrayToSafePromiseIterable(promises, mapFn)); + export default { - makeSafe, // exported for testing Array, - ArrayFrom: Array.from, - ArrayIsArray: Array.isArray, - SafeArrayIterator: createSafeIterator(ArrayPrototypeSymbolIterator, ArrayIteratorPrototypeNext), - ArrayPrototypeFlat: uncurryThis(Array.prototype.flat), - ArrayPrototypeFilter: uncurryThis(Array.prototype.filter), - ArrayPrototypeForEach, - ArrayPrototypeIncludes: uncurryThis(Array.prototype.includes), - ArrayPrototypeIndexOf: uncurryThis(Array.prototype.indexOf), - ArrayPrototypeJoin: uncurryThis(Array.prototype.join), - ArrayPrototypeMap: uncurryThis(Array.prototype.map), - ArrayPrototypePop: uncurryThis(Array.prototype.pop), - ArrayPrototypePush: uncurryThis(arrayProtoPush), - ArrayPrototypePushApply: (a, b) => arrayProtoPush.$apply(a, b), - ArrayPrototypeSlice: uncurryThis(Array.prototype.slice), - ArrayPrototypeSort: uncurryThis(Array.prototype.sort), - ArrayPrototypeSplice: uncurryThis(Array.prototype.splice), - ArrayPrototypeUnshift: uncurryThis(Array.prototype.unshift), - BigIntPrototypeValueOf: uncurryThis(BigInt.prototype.valueOf), - BooleanPrototypeValueOf: uncurryThis(Boolean.prototype.valueOf), - DatePrototypeGetTime: uncurryThis(Date.prototype.getTime), - DatePrototypeToISOString: uncurryThis(Date.prototype.toISOString), - DatePrototypeToString: uncurryThis(Date.prototype.toString), - ErrorCaptureStackTrace, - ErrorPrototypeToString: uncurryThis(Error.prototype.toString), - FunctionPrototypeToString: uncurryThis(Function.prototype.toString), - JSONStringify: JSON.stringify, + SafeArrayIterator, MapPrototypeGetSize: getGetter(Map, "size"), - MapPrototypeEntries: uncurryThis(Map.prototype.entries), - MapPrototypeValues: uncurryThis(Map.prototype.values), - MapPrototypeKeys: uncurryThis(Map.prototype.keys), - MathFloor: Math.floor, - MathMax: Math.max, - MathMin: Math.min, - MathRound: Math.round, - MathSqrt: Math.sqrt, - MathTrunc: Math.trunc, Number, - NumberIsFinite: Number.isFinite, - NumberIsNaN: Number.isNaN, - NumberParseFloat: Number.parseFloat, - NumberParseInt: Number.parseInt, - NumberPrototypeToString: uncurryThis(Number.prototype.toString), - NumberPrototypeValueOf: uncurryThis(Number.prototype.valueOf), Object, - ObjectAssign: Object.assign, - ObjectCreate: Object.create, - ObjectDefineProperty: Object.defineProperty, - ObjectEntries: Object.entries, - ObjectGetOwnPropertyDescriptor: Object.getOwnPropertyDescriptor, - ObjectGetOwnPropertyDescriptors: Object.getOwnPropertyDescriptors, - ObjectGetOwnPropertyNames: Object.getOwnPropertyNames, - ObjectGetOwnPropertySymbols: Object.getOwnPropertySymbols, - ObjectGetPrototypeOf: Object.getPrototypeOf, - ObjectIs: Object.is, - ObjectKeys: Object.keys, - ObjectPrototypeHasOwnProperty: uncurryThis(Object.prototype.hasOwnProperty), - ObjectPrototypePropertyIsEnumerable: uncurryThis(Object.prototype.propertyIsEnumerable), - ObjectPrototypeToString: uncurryThis(Object.prototype.toString), - ObjectSeal: Object.seal, - ObjectSetPrototypeOf: Object.setPrototypeOf, - ReflectOwnKeys: Reflect.ownKeys, RegExp, - RegExpPrototypeExec: uncurryThis(RegExp.prototype.exec), - RegExpPrototypeSymbolReplace: uncurryThis(RegExp.prototype[Symbol.replace]), - RegExpPrototypeSymbolSplit: uncurryThis(RegExp.prototype[Symbol.split]), - RegExpPrototypeTest: uncurryThis(RegExp.prototype.test), - RegExpPrototypeToString: uncurryThis(RegExp.prototype.toString), SafeStringIterator: createSafeIterator(StringIterator, uncurryThis(StringIteratorPrototype.next)), SafeMap: makeSafe( Map, @@ -164,6 +112,7 @@ export default { } }, ), + SafePromiseAll, SafeSet: makeSafe( Set, class SafeSet extends Set { @@ -172,41 +121,16 @@ export default { } }, ), - DatePrototypeGetMilliseconds: uncurryThis(Date.prototype.getMilliseconds), - DatePrototypeToUTCString: uncurryThis(Date.prototype.toUTCString), + SafeWeakSet: makeSafe( + WeakSet, + class SafeWeakSet extends WeakSet { + constructor(i) { + super(i); + } + }, + ), SetPrototypeGetSize: getGetter(Set, "size"), - SetPrototypeEntries: uncurryThis(Set.prototype.entries), - SetPrototypeValues: uncurryThis(Set.prototype.values), String, - StringPrototypeCharCodeAt: uncurryThis(String.prototype.charCodeAt), - StringPrototypeCodePointAt: uncurryThis(String.prototype.codePointAt), - StringPrototypeEndsWith: uncurryThis(String.prototype.endsWith), - StringPrototypeIncludes: uncurryThis(String.prototype.includes), - StringPrototypeIndexOf: uncurryThis(String.prototype.indexOf), - StringPrototypeLastIndexOf: uncurryThis(String.prototype.lastIndexOf), - StringPrototypeMatch: uncurryThis(String.prototype.match), - StringPrototypeNormalize: uncurryThis(String.prototype.normalize), - StringPrototypePadEnd: uncurryThis(String.prototype.padEnd), - StringPrototypePadStart: uncurryThis(String.prototype.padStart), - StringPrototypeRepeat: uncurryThis(String.prototype.repeat), - StringPrototypeReplace: uncurryThis(String.prototype.replace), - StringPrototypeReplaceAll: uncurryThis(String.prototype.replaceAll), - StringPrototypeSlice: uncurryThis(String.prototype.slice), - StringPrototypeSplit: uncurryThis(String.prototype.split), - StringPrototypeStartsWith: uncurryThis(String.prototype.startsWith), - StringPrototypeToLowerCase: uncurryThis(String.prototype.toLowerCase), - StringPrototypeTrim: uncurryThis(String.prototype.trim), - StringPrototypeValueOf: uncurryThis(String.prototype.valueOf), - SymbolPrototypeToString: uncurryThis(Symbol.prototype.toString), - SymbolPrototypeValueOf: uncurryThis(Symbol.prototype.valueOf), - FunctionPrototypeToString: uncurryThis(Function.prototype.toString), - FunctionPrototypeBind: uncurryThis(Function.prototype.bind), - SymbolDispose: Symbol.dispose, - SymbolAsyncDispose: Symbol.asyncDispose, - SymbolIterator: Symbol.iterator, - SymbolAsyncIterator: Symbol.asyncIterator, - SymbolFor: Symbol.for, - SymbolToStringTag: Symbol.toStringTag, TypedArrayPrototypeGetLength: getGetter(Uint8Array, "length"), TypedArrayPrototypeGetSymbolToStringTag: getGetter(Uint8Array, Symbol.toStringTag), Uint8ClampedArray, diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts index df0f652ee9..af82b5c0ba 100644 --- a/src/js/internal/shared.ts +++ b/src/js/internal/shared.ts @@ -1,10 +1,13 @@ +const ObjectFreeze = Object.freeze; + class NotImplementedError extends Error { code: string; - constructor(feature: string, issue?: number) { + constructor(feature: string, issue?: number, extra?: string) { super( feature + " is not yet implemented in Bun." + - (issue ? " Track the status & thumbs up the issue: https://github.com/oven-sh/bun/issues/" + issue : ""), + (issue ? " Track the status & thumbs up the issue: https://github.com/oven-sh/bun/issues/" + issue : "") + + (!!extra ? ". " + extra : ""), ); this.name = "NotImplementedError"; this.code = "ERR_NOT_IMPLEMENTED"; @@ -14,11 +17,11 @@ class NotImplementedError extends Error { } } -function throwNotImplemented(feature: string, issue?: number): never { +function throwNotImplemented(feature: string, issue?: number, extra?: string): never { // in the definition so that it isn't bundled unless used hideFromStack(throwNotImplemented); - throw new NotImplementedError(feature, issue); + throw new NotImplementedError(feature, issue, extra); } function hideFromStack(...fns) { @@ -46,11 +49,12 @@ const fileSinkSymbol = Symbol("fileSink"); // -let util; +let util: typeof import("node:util"); class ExceptionWithHostPort extends Error { errno: number; syscall: string; port?: number; + address; constructor(err, syscall, address, port) { // TODO(joyeecheung): We have to use the type-checked @@ -78,6 +82,20 @@ class ExceptionWithHostPort extends Error { } } +function once(callback, { preserveReturnValue = false } = kEmptyObject) { + let called = false; + let returnValue; + return function (...args) { + if (called) return returnValue; + called = true; + const result = callback.$apply(this, args); + returnValue = preserveReturnValue ? result : undefined; + return result; + }; +} + +const kEmptyObject = ObjectFreeze({ __proto__: null }); + // export default { @@ -87,6 +105,13 @@ export default { warnNotImplementedOnce, fileSinkSymbol, ExceptionWithHostPort, + once, + kHandle: Symbol("kHandle"), kAutoDestroyed: Symbol("kAutoDestroyed"), + kResistStopPropagation: Symbol("kResistStopPropagation"), + kWeakHandler: Symbol("kWeak"), + kEnsureConstructed: Symbol("kEnsureConstructed"), + kGetNativeReadableProto: Symbol("kGetNativeReadableProto"), + kEmptyObject, }; diff --git a/src/js/internal/stream.promises.ts b/src/js/internal/stream.promises.ts new file mode 100644 index 0000000000..c25b026f65 --- /dev/null +++ b/src/js/internal/stream.promises.ts @@ -0,0 +1,45 @@ +"use strict"; + +const ArrayPrototypePop = Array.prototype.pop; + +const { isIterable, isNodeStream, isWebStream } = require("internal/streams/utils"); +const { pipelineImpl: pl } = require("internal/streams/pipeline"); +const { finished } = require("internal/streams/end-of-stream"); + +// require("internal/stream"); + +function pipeline(...streams) { + return new Promise((resolve, reject) => { + let signal; + let end; + const lastArg = streams[streams.length - 1]; + if ( + lastArg && + typeof lastArg === "object" && + !isNodeStream(lastArg) && + !isIterable(lastArg) && + !isWebStream(lastArg) + ) { + const options = ArrayPrototypePop.$call(streams); + signal = options.signal; + end = options.end; + } + + pl( + streams, + (err, value) => { + if (err) { + reject(err); + } else { + resolve(value); + } + }, + { signal, end }, + ); + }); +} + +export default { + finished, + pipeline, +}; diff --git a/src/js/internal/stream.ts b/src/js/internal/stream.ts new file mode 100644 index 0000000000..41c2aae63a --- /dev/null +++ b/src/js/internal/stream.ts @@ -0,0 +1,113 @@ +"use strict"; + +const ObjectKeys = Object.keys; +const ObjectDefineProperty = Object.defineProperty; + +const customPromisify = Symbol.for("nodejs.util.promisify.custom"); +const { streamReturningOperators, promiseReturningOperators } = require("internal/streams/operators"); +const compose = require("internal/streams/compose"); +const { setDefaultHighWaterMark, getDefaultHighWaterMark } = require("internal/streams/state"); +const { pipeline } = require("internal/streams/pipeline"); +const { destroyer } = require("internal/streams/destroy"); +const eos = require("internal/streams/end-of-stream"); +const promises = require("internal/stream.promises"); +const utils = require("internal/streams/utils"); +const { isArrayBufferView, isUint8Array } = require("node:util/types"); +const Stream = require("internal/streams/legacy").Stream; + +Stream.isDestroyed = utils.isDestroyed; +Stream.isDisturbed = utils.isDisturbed; +Stream.isErrored = utils.isErrored; +Stream.isReadable = utils.isReadable; +Stream.isWritable = utils.isWritable; + +Stream.Readable = require("internal/streams/readable"); +const streamKeys = ObjectKeys(streamReturningOperators); +for (let i = 0; i < streamKeys.length; i++) { + const key = streamKeys[i]; + const op = streamReturningOperators[key]; + function fn(...args) { + if (new.target) { + throw $ERR_ILLEGAL_CONSTRUCTOR(); + } + return Stream.Readable.from(op.$apply(this, args)); + } + ObjectDefineProperty(fn, "name", { __proto__: null, value: op.name }); + ObjectDefineProperty(fn, "length", { __proto__: null, value: op.length }); + ObjectDefineProperty(Stream.Readable.prototype, key, { + __proto__: null, + value: fn, + enumerable: false, + configurable: true, + writable: true, + }); +} +const promiseKeys = ObjectKeys(promiseReturningOperators); +for (let i = 0; i < promiseKeys.length; i++) { + const key = promiseKeys[i]; + const op = promiseReturningOperators[key]; + function fn(...args) { + if (new.target) { + throw $ERR_ILLEGAL_CONSTRUCTOR(); + } + return Promise.resolve().then(() => op.$apply(this, args)); + } + ObjectDefineProperty(fn, "name", { __proto__: null, value: op.name }); + ObjectDefineProperty(fn, "length", { __proto__: null, value: op.length }); + ObjectDefineProperty(Stream.Readable.prototype, key, { + __proto__: null, + value: fn, + enumerable: false, + configurable: true, + writable: true, + }); +} +Stream.Writable = require("internal/streams/writable"); +Stream.Duplex = require("internal/streams/duplex"); +Stream.Transform = require("internal/streams/transform"); +Stream.PassThrough = require("internal/streams/passthrough"); +Stream.duplexPair = require("internal/streams/duplexpair"); +Stream.pipeline = pipeline; +const { addAbortSignal } = require("internal/streams/add-abort-signal"); +Stream.addAbortSignal = addAbortSignal; +Stream.finished = eos; +Stream.destroy = destroyer; +Stream.compose = compose; +Stream.setDefaultHighWaterMark = setDefaultHighWaterMark; +Stream.getDefaultHighWaterMark = getDefaultHighWaterMark; + +ObjectDefineProperty(Stream, "promises", { + __proto__: null, + configurable: true, + enumerable: true, + get() { + return promises; + }, +}); + +ObjectDefineProperty(pipeline, customPromisify, { + __proto__: null, + enumerable: true, + get() { + return promises.pipeline; + }, +}); + +ObjectDefineProperty(eos, customPromisify, { + __proto__: null, + enumerable: true, + get() { + return promises.finished; + }, +}); + +// Backwards-compat with node 0.4.x +Stream.Stream = Stream; + +Stream._isArrayBufferView = isArrayBufferView; +Stream._isUint8Array = isUint8Array; +Stream._uint8ArrayToBuffer = function _uint8ArrayToBuffer(chunk) { + return new $Buffer(chunk.buffer, chunk.byteOffset, chunk.byteLength); +}; + +export default Stream; diff --git a/src/js/internal/streams/add-abort-signal.ts b/src/js/internal/streams/add-abort-signal.ts new file mode 100644 index 0000000000..85007b81ce --- /dev/null +++ b/src/js/internal/streams/add-abort-signal.ts @@ -0,0 +1,51 @@ +"use strict"; + +const { isNodeStream, isWebStream, kControllerErrorFunction } = require("internal/streams/utils"); +const eos = require("internal/streams/end-of-stream"); + +const SymbolDispose = Symbol.dispose; + +let addAbortListener; + +// This method is inlined here for readable-stream +// It also does not allow for signal to not exist on the stream +// https://github.com/nodejs/node/pull/36061#discussion_r533718029 +const validateAbortSignal = (signal, name) => { + if (typeof signal !== "object" || !("aborted" in signal)) { + throw $ERR_INVALID_ARG_TYPE(name, "AbortSignal", signal); + } +}; + +function addAbortSignal(signal, stream) { + validateAbortSignal(signal, "signal"); + if (!isNodeStream(stream) && !isWebStream(stream)) { + throw $ERR_INVALID_ARG_TYPE("stream", ["ReadableStream", "WritableStream", "Stream"], stream); + } + return addAbortSignalNoValidate(signal, stream); +} + +function addAbortSignalNoValidate(signal, stream) { + if (typeof signal !== "object" || !("aborted" in signal)) { + return stream; + } + const onAbort = isNodeStream(stream) + ? () => { + stream.destroy($makeAbortError(undefined, { cause: signal.reason })); + } + : () => { + stream[kControllerErrorFunction]($makeAbortError(undefined, { cause: signal.reason })); + }; + if (signal.aborted) { + onAbort(); + } else { + addAbortListener ??= require("internal/abort_listener").addAbortListener; + const disposable = addAbortListener(signal, onAbort); + eos(stream, disposable[SymbolDispose]); + } + return stream; +} + +export default { + addAbortSignal, + addAbortSignalNoValidate, +}; diff --git a/src/js/internal/streams/compose.ts b/src/js/internal/streams/compose.ts new file mode 100644 index 0000000000..2a4df02339 --- /dev/null +++ b/src/js/internal/streams/compose.ts @@ -0,0 +1,221 @@ +"use strict"; + +const { pipeline } = require("internal/streams/pipeline"); +const Duplex = require("internal/streams/duplex"); +const { destroyer } = require("internal/streams/destroy"); +const { + isNodeStream, + isReadable, + isWritable, + isWebStream, + isTransformStream, + isWritableStream, + isReadableStream, +} = require("internal/streams/utils"); +const eos = require("internal/streams/end-of-stream"); + +const ArrayPrototypeSlice = Array.prototype.slice; + +export default function compose(...streams) { + if (streams.length === 0) { + throw $ERR_MISSING_ARGS("streams"); + } + + if (streams.length === 1) { + return Duplex.from(streams[0]); + } + + const orgStreams = ArrayPrototypeSlice.$call(streams); + + if (typeof streams[0] === "function") { + streams[0] = Duplex.from(streams[0]); + } + + if (typeof streams[streams.length - 1] === "function") { + const idx = streams.length - 1; + streams[idx] = Duplex.from(streams[idx]); + } + + for (let n = 0; n < streams.length; ++n) { + if (!isNodeStream(streams[n]) && !isWebStream(streams[n])) { + // TODO(ronag): Add checks for non streams. + continue; + } + if ( + n < streams.length - 1 && + !(isReadable(streams[n]) || isReadableStream(streams[n]) || isTransformStream(streams[n])) + ) { + throw $ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be readable"); + } + if (n > 0 && !(isWritable(streams[n]) || isWritableStream(streams[n]) || isTransformStream(streams[n]))) { + throw $ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be writable"); + } + } + + let ondrain; + let onfinish; + let onreadable; + let onclose; + let d; + + function onfinished(err) { + const cb = onclose; + onclose = null; + + if (cb) { + cb(err); + } else if (err) { + d.destroy(err); + } else if (!readable && !writable) { + d.destroy(); + } + } + + const head = streams[0]; + const tail = pipeline(streams, onfinished); + + const writable = !!(isWritable(head) || isWritableStream(head) || isTransformStream(head)); + const readable = !!(isReadable(tail) || isReadableStream(tail) || isTransformStream(tail)); + + // TODO(ronag): Avoid double buffering. + // Implement Writable/Readable/Duplex traits. + // See, https://github.com/nodejs/node/pull/33515. + d = new Duplex({ + // TODO (ronag): highWaterMark? + writableObjectMode: !!head?.writableObjectMode, + readableObjectMode: !!tail?.readableObjectMode, + writable, + readable, + }); + + if (writable) { + if (isNodeStream(head)) { + d._write = function (chunk, encoding, callback) { + if (head.write(chunk, encoding)) { + callback(); + } else { + ondrain = callback; + } + }; + + d._final = function (callback) { + head.end(); + onfinish = callback; + }; + + head.on("drain", function () { + if (ondrain) { + const cb = ondrain; + ondrain = null; + cb(); + } + }); + } else if (isWebStream(head)) { + const writable = isTransformStream(head) ? head.writable : head; + const writer = writable.getWriter(); + + d._write = async function (chunk, encoding, callback) { + try { + await writer.ready; + writer.write(chunk).catch(() => {}); + callback(); + } catch (err) { + callback(err); + } + }; + + d._final = async function (callback) { + try { + await writer.ready; + writer.close().catch(() => {}); + onfinish = callback; + } catch (err) { + callback(err); + } + }; + } + + const toRead = isTransformStream(tail) ? tail.readable : tail; + + eos(toRead, () => { + if (onfinish) { + const cb = onfinish; + onfinish = null; + cb(); + } + }); + } + + if (readable) { + if (isNodeStream(tail)) { + tail.on("readable", function () { + if (onreadable) { + const cb = onreadable; + onreadable = null; + cb(); + } + }); + + tail.on("end", function () { + d.push(null); + }); + + d._read = function () { + while (true) { + const buf = tail.read(); + if (buf === null) { + onreadable = d._read; + return; + } + + if (!d.push(buf)) { + return; + } + } + }; + } else if (isWebStream(tail)) { + const readable = isTransformStream(tail) ? tail.readable : tail; + const reader = readable.getReader(); + d._read = async function () { + while (true) { + try { + const { value, done } = await reader.read(); + + if (!d.push(value)) { + return; + } + + if (done) { + d.push(null); + return; + } + } catch { + return; + } + } + }; + } + } + + d._destroy = function (err, callback) { + if (!err && onclose !== null) { + err = $makeAbortError(); + } + + onreadable = null; + ondrain = null; + onfinish = null; + + if (isNodeStream(tail)) { + destroyer(tail, err); + } + + if (onclose === null) { + callback(err); + } else { + onclose = callback; + } + }; + + return d; +} diff --git a/src/js/internal/streams/destroy.ts b/src/js/internal/streams/destroy.ts new file mode 100644 index 0000000000..b11e41161b --- /dev/null +++ b/src/js/internal/streams/destroy.ts @@ -0,0 +1,340 @@ +"use strict"; + +const { aggregateTwoErrors } = require("internal/errors"); +const { + kIsDestroyed, + isDestroyed, + isFinished, + isServerRequest, + kState, + kErrorEmitted, + kEmitClose, + kClosed, + kCloseEmitted, + kConstructed, + kDestroyed, + kAutoDestroy, + kErrored, +} = require("internal/streams/utils"); + +const ProcessNextTick = process.nextTick; + +const kDestroy = Symbol("kDestroy"); +const kConstruct = Symbol("kConstruct"); + +function checkError(err, w, r) { + if (err) { + // Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364 + err.stack; // eslint-disable-line no-unused-expressions + + if (w && !w.errored) { + w.errored = err; + } + if (r && !r.errored) { + r.errored = err; + } + } +} + +// Backwards compat. cb() is undocumented and unused in core but +// unfortunately might be used by modules. +function destroy(err, cb) { + const r = this._readableState; + const w = this._writableState; + // With duplex streams we use the writable side for state. + const s = w || r; + + if ((w && (w[kState] & kDestroyed) !== 0) || (r && (r[kState] & kDestroyed) !== 0)) { + if (typeof cb === "function") { + cb(); + } + + return this; + } + + // We set destroyed to true before firing error callbacks in order + // to make it re-entrance safe in case destroy() is called within callbacks + checkError(err, w, r); + + if (w) { + w[kState] |= kDestroyed; + } + if (r) { + r[kState] |= kDestroyed; + } + + // If still constructing then defer calling _destroy. + if ((s[kState] & kConstructed) === 0) { + this.once(kDestroy, function (er) { + _destroy(this, aggregateTwoErrors(er, err), cb); + }); + } else { + _destroy(this, err, cb); + } + + return this; +} + +function _destroy(self, err, cb) { + let called = false; + + function onDestroy(err) { + if (called) { + return; + } + called = true; + + const r = self._readableState; + const w = self._writableState; + + checkError(err, w, r); + + if (w) { + w[kState] |= kClosed; + } + if (r) { + r[kState] |= kClosed; + } + + if (typeof cb === "function") { + cb(err); + } + + if (err) { + ProcessNextTick(emitErrorCloseNT, self, err); + } else { + ProcessNextTick(emitCloseNT, self); + } + } + try { + self._destroy(err || null, onDestroy); + } catch (err) { + onDestroy(err); + } +} + +function emitErrorCloseNT(self, err) { + emitErrorNT(self, err); + emitCloseNT(self); +} + +function emitCloseNT(self) { + const r = self._readableState; + const w = self._writableState; + + if (w) { + w[kState] |= kCloseEmitted; + } + if (r) { + r[kState] |= kCloseEmitted; + } + + if ((w && (w[kState] & kEmitClose) !== 0) || (r && (r[kState] & kEmitClose) !== 0)) { + self.emit("close"); + } +} + +function emitErrorNT(self, err) { + const r = self._readableState; + const w = self._writableState; + + if ((w && (w[kState] & kErrorEmitted) !== 0) || (r && (r[kState] & kErrorEmitted) !== 0)) { + return; + } + + if (w) { + w[kState] |= kErrorEmitted; + } + if (r) { + r[kState] |= kErrorEmitted; + } + + self.emit("error", err); +} + +function undestroy() { + const r = this._readableState; + const w = this._writableState; + + if (r) { + r.constructed = true; + r.closed = false; + r.closeEmitted = false; + r.destroyed = false; + r.errored = null; + r.errorEmitted = false; + r.reading = false; + r.ended = r.readable === false; + r.endEmitted = r.readable === false; + } + + if (w) { + w.constructed = true; + w.destroyed = false; + w.closed = false; + w.closeEmitted = false; + w.errored = null; + w.errorEmitted = false; + w.finalCalled = false; + w.prefinished = false; + w.ended = w.writable === false; + w.ending = w.writable === false; + w.finished = w.writable === false; + } +} + +function errorOrDestroy(stream, err, sync?) { + // We have tests that rely on errors being emitted + // in the same tick, so changing this is semver major. + // For now when you opt-in to autoDestroy we allow + // the error to be emitted nextTick. In a future + // semver major update we should change the default to this. + + const r = stream._readableState; + const w = stream._writableState; + + if ( + (w && (w[kState] ? (w[kState] & kDestroyed) !== 0 : w.destroyed)) || + (r && (r[kState] ? (r[kState] & kDestroyed) !== 0 : r.destroyed)) + ) { + return this; + } + + if ((r && (r[kState] & kAutoDestroy) !== 0) || (w && (w[kState] & kAutoDestroy) !== 0)) { + stream.destroy(err); + } else if (err) { + // Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364 + err.stack; // eslint-disable-line no-unused-expressions + + if (w && (w[kState] & kErrored) === 0) { + w.errored = err; + } + if (r && (r[kState] & kErrored) === 0) { + r.errored = err; + } + if (sync) { + ProcessNextTick(emitErrorNT, stream, err); + } else { + emitErrorNT(stream, err); + } + } +} + +function construct(stream, cb) { + if (typeof stream._construct !== "function") { + return; + } + + const r = stream._readableState; + const w = stream._writableState; + + if (r) { + r[kState] &= ~kConstructed; + } + if (w) { + w[kState] &= ~kConstructed; + } + + stream.once(kConstruct, cb); + + if (stream.listenerCount(kConstruct) > 1) { + // Duplex + return; + } + + ProcessNextTick(constructNT, stream); +} + +function constructNT(stream) { + let called = false; + + function onConstruct(err) { + if (called) { + errorOrDestroy(stream, err ?? $ERR_MULTIPLE_CALLBACK()); + return; + } + called = true; + + const r = stream._readableState; + const w = stream._writableState; + const s = w || r; + + if (r) { + r[kState] |= kConstructed; + } + if (w) { + w[kState] |= kConstructed; + } + + if (s.destroyed) { + stream.emit(kDestroy, err); + } else if (err) { + errorOrDestroy(stream, err, true); + } else { + stream.emit(kConstruct); + } + } + + try { + stream._construct(err => { + ProcessNextTick(onConstruct, err); + }); + } catch (err) { + ProcessNextTick(onConstruct, err); + } +} + +function isRequest(stream) { + return stream?.setHeader && typeof stream.abort === "function"; +} + +function emitCloseLegacy(stream) { + stream.emit("close"); +} + +function emitErrorCloseLegacy(stream, err) { + stream.emit("error", err); + ProcessNextTick(emitCloseLegacy, stream); +} + +// Normalize destroy for legacy. +function destroyer(stream, err) { + if (!stream || isDestroyed(stream)) { + return; + } + + if (!err && !isFinished(stream)) { + err = $makeAbortError(); + } + + // TODO: Remove isRequest branches. + if (isServerRequest(stream)) { + stream.socket = null; + stream.destroy(err); + } else if (isRequest(stream)) { + stream.abort(); + } else if (isRequest(stream.req)) { + stream.req.abort(); + } else if (typeof stream.destroy === "function") { + stream.destroy(err); + } else if (typeof stream.close === "function") { + // TODO: Don't lose err? + stream.close(); + } else if (err) { + ProcessNextTick(emitErrorCloseLegacy, stream, err); + } else { + ProcessNextTick(emitCloseLegacy, stream); + } + + if (!stream.destroyed) { + stream[kIsDestroyed] = true; + } +} + +export default { + construct, + destroyer, + destroy, + undestroy, + errorOrDestroy, +}; diff --git a/src/js/internal/streams/duplex.ts b/src/js/internal/streams/duplex.ts new file mode 100644 index 0000000000..c814b5b157 --- /dev/null +++ b/src/js/internal/streams/duplex.ts @@ -0,0 +1,153 @@ +// a duplex stream is just a stream that is both readable and writable. +// Since JS doesn't have multiple prototype inheritance, this class +// prototypically inherits from Readable, and then parasitically from +// Writable. + +"use strict"; + +const Stream = require("internal/streams/legacy").Stream; +const Readable = require("internal/streams/readable"); +const Writable = require("internal/streams/writable"); +const { addAbortSignal } = require("internal/streams/add-abort-signal"); +const destroyImpl = require("internal/streams/destroy"); +const { kOnConstructed } = require("internal/streams/utils"); + +const ObjectKeys = Object.keys; +const ObjectDefineProperties = Object.defineProperties; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + +function Duplex(options) { + if (!(this instanceof Duplex)) return Reflect.construct(Duplex, [options]); + + this._events ??= { + close: undefined, + error: undefined, + prefinish: undefined, + finish: undefined, + drain: undefined, + data: undefined, + end: undefined, + readable: undefined, + // Skip uncommon events... + // pause: undefined, + // resume: undefined, + // pipe: undefined, + // unpipe: undefined, + // [destroyImpl.kConstruct]: undefined, + // [destroyImpl.kDestroy]: undefined, + }; + + this._readableState = new Readable.ReadableState(options, this, true); + this._writableState = new Writable.WritableState(options, this, true); + + if (options) { + this.allowHalfOpen = options.allowHalfOpen !== false; + + if (options.readable === false) { + this._readableState.readable = false; + this._readableState.ended = true; + this._readableState.endEmitted = true; + } + + if (options.writable === false) { + this._writableState.writable = false; + this._writableState.ending = true; + this._writableState.ended = true; + this._writableState.finished = true; + } + + if (typeof options.read === "function") this._read = options.read; + + if (typeof options.write === "function") this._write = options.write; + + if (typeof options.writev === "function") this._writev = options.writev; + + if (typeof options.destroy === "function") this._destroy = options.destroy; + + if (typeof options.final === "function") this._final = options.final; + + if (typeof options.construct === "function") this._construct = options.construct; + + if (options.signal) addAbortSignal(options.signal, this); + } else { + this.allowHalfOpen = true; + } + + Stream.$call(this, options); + + if (this._construct != null) { + destroyImpl.construct(this, () => { + this._readableState[kOnConstructed](this); + this._writableState[kOnConstructed](this); + }); + } +} +$toClass(Duplex, "Duplex", Readable); + +// Use the `destroy` method of `Writable`. +Duplex.prototype.destroy = Writable.prototype.destroy; + +{ + const keys = ObjectKeys(Writable.prototype); + // Allow the keys array to be GC'ed. + for (let i = 0; i < keys.length; i++) { + const method = keys[i]; + Duplex.prototype[method] ||= Writable.prototype[method]; + } +} + +ObjectDefineProperties(Duplex.prototype, { + writable: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writable") }, + writableHighWaterMark: { + __proto__: null, + ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableHighWaterMark"), + }, + writableObjectMode: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableObjectMode") }, + writableBuffer: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableBuffer") }, + writableLength: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableLength") }, + writableFinished: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableFinished") }, + writableCorked: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableCorked") }, + writableEnded: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableEnded") }, + writableNeedDrain: { __proto__: null, ...ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableNeedDrain") }, + + destroyed: { + __proto__: null, + get() { + if (this._readableState === undefined || this._writableState === undefined) { + return false; + } + return this._readableState.destroyed && this._writableState.destroyed; + }, + set(value) { + // Backward compatibility, the user is explicitly + // managing destroyed. + if (this._readableState && this._writableState) { + this._readableState.destroyed = value; + this._writableState.destroyed = value; + } + }, + }, +}); + +// Lazy to avoid circular references +let webStreamsAdapters; +function lazyWebStreams() { + if (webStreamsAdapters === undefined) webStreamsAdapters = require("internal/webstreams_adapters"); + return webStreamsAdapters; +} + +Duplex.fromWeb = function (pair, options) { + return lazyWebStreams().newStreamDuplexFromReadableWritablePair(pair, options); +}; + +Duplex.toWeb = function (duplex) { + return lazyWebStreams().newReadableWritablePairFromDuplex(duplex); +}; + +let duplexify; +Duplex.from = function (body) { + duplexify ??= require("internal/streams/duplexify"); + return duplexify(body, "body"); +}; + +export default Duplex; diff --git a/src/js/internal/streams/duplexify.ts b/src/js/internal/streams/duplexify.ts new file mode 100644 index 0000000000..cb193e7895 --- /dev/null +++ b/src/js/internal/streams/duplexify.ts @@ -0,0 +1,369 @@ +"use strict"; + +const { + isReadable, + isWritable, + isIterable, + isNodeStream, + isReadableNodeStream, + isWritableNodeStream, + isDuplexNodeStream, + isReadableStream, + isWritableStream, +} = require("internal/streams/utils"); +const eos = require("internal/streams/end-of-stream"); +const { destroyer } = require("internal/streams/destroy"); +const Duplex = require("internal/streams/duplex"); +const Readable = require("internal/streams/readable"); +const Writable = require("internal/streams/writable"); +const from = require("internal/streams/from"); + +const PromiseWithResolvers = Promise.withResolvers.bind(Promise); + +class Duplexify extends Duplex { + constructor(options) { + super(options); + + // https://github.com/nodejs/node/pull/34385 + + if (options?.readable === false) { + this._readableState.readable = false; + this._readableState.ended = true; + this._readableState.endEmitted = true; + } + + if (options?.writable === false) { + this._writableState.writable = false; + this._writableState.ending = true; + this._writableState.ended = true; + this._writableState.finished = true; + } + } +} + +function duplexify(body, name?) { + if (isDuplexNodeStream(body)) { + return body; + } + + if (isReadableNodeStream(body)) { + return _duplexify({ readable: body }); + } + + if (isWritableNodeStream(body)) { + return _duplexify({ writable: body }); + } + + if (isNodeStream(body)) { + return _duplexify({ writable: false, readable: false }); + } + + if (isReadableStream(body)) { + return _duplexify({ readable: Readable.fromWeb(body) }); + } + + if (isWritableStream(body)) { + return _duplexify({ writable: Writable.fromWeb(body) }); + } + + if (typeof body === "function") { + const { value, write, final, destroy } = fromAsyncGen(body); + + // Body might be a constructor function instead of an async generator function. + if (isDuplexNodeStream(value)) { + return value; + } + + if (isIterable(value)) { + return from(Duplexify, value, { + // TODO (ronag): highWaterMark? + objectMode: true, + write, + final, + destroy, + }); + } + + const then = value?.then; + if (typeof then === "function") { + let d; + + const promise = then.$call( + value, + val => { + if (val != null) { + throw $ERR_INVALID_RETURN_VALUE("nully", "body", val); + } + }, + err => { + destroyer(d, err); + }, + ); + + return (d = new Duplexify({ + // TODO (ronag): highWaterMark? + objectMode: true, + readable: false, + write, + final(cb) { + final(async () => { + try { + await promise; + process.nextTick(cb, null); + } catch (err) { + process.nextTick(cb, err); + } + }); + }, + destroy, + })); + } + + throw $ERR_INVALID_RETURN_VALUE("Iterable, AsyncIterable or AsyncFunction", name, value); + } + + if ($inheritsBlob(body)) { + return duplexify(body.arrayBuffer()); + } + + if (isIterable(body)) { + return from(Duplexify, body, { + // TODO (ronag): highWaterMark? + objectMode: true, + writable: false, + }); + } + + if (isReadableStream(body?.readable) && isWritableStream(body?.writable)) { + return Duplexify.fromWeb(body); + } + + if (typeof body?.writable === "object" || typeof body?.readable === "object") { + const readable = body?.readable + ? isReadableNodeStream(body?.readable) + ? body?.readable + : duplexify(body.readable) + : undefined; + + const writable = body?.writable + ? isWritableNodeStream(body?.writable) + ? body?.writable + : duplexify(body.writable) + : undefined; + + return _duplexify({ readable, writable }); + } + + const then = body?.then; + if (typeof then === "function") { + let d; + + then.$call( + body, + val => { + if (val != null) { + d.push(val); + } + d.push(null); + }, + err => { + destroyer(d, err); + }, + ); + + return (d = new Duplexify({ + objectMode: true, + writable: false, + read() {}, + })); + } + + throw $ERR_INVALID_ARG_TYPE( + name, + [ + "Blob", + "ReadableStream", + "WritableStream", + "Stream", + "Iterable", + "AsyncIterable", + "Function", + "{ readable, writable } pair", + "Promise", + ], + body, + ); +} + +function fromAsyncGen(fn) { + let { promise, resolve } = PromiseWithResolvers(); + const ac = new AbortController(); + const signal = ac.signal; + const value = fn( + (async function* () { + while (true) { + const _promise = promise; + promise = null; + const { chunk, done, cb } = await _promise; + process.nextTick(cb); + if (done) return; + if (signal.aborted) throw $makeAbortError(undefined, { cause: signal.reason }); + ({ promise, resolve } = PromiseWithResolvers()); + yield chunk; + } + })(), + { signal }, + ); + + return { + value, + write(chunk, encoding, cb) { + const _resolve = resolve; + resolve = null; + _resolve({ chunk, done: false, cb }); + }, + final(cb) { + const _resolve = resolve; + resolve = null; + _resolve({ done: true, cb }); + }, + destroy(err, cb) { + ac.abort(); + cb(err); + }, + }; +} + +function _duplexify(pair) { + const r = pair.readable && typeof pair.readable.read !== "function" ? Readable.wrap(pair.readable) : pair.readable; + const w = pair.writable; + + let readable = !!isReadable(r); + let writable = !!isWritable(w); + + let ondrain; + let onfinish; + let onreadable; + let onclose; + let d; + + function onfinished(err) { + const cb = onclose; + onclose = null; + + if (cb) { + cb(err); + } else if (err) { + d.destroy(err); + } + } + + // TODO(ronag): Avoid double buffering. + // Implement Writable/Readable/Duplex traits. + // See, https://github.com/nodejs/node/pull/33515. + d = new Duplexify({ + // TODO (ronag): highWaterMark? + readableObjectMode: !!r?.readableObjectMode, + writableObjectMode: !!w?.writableObjectMode, + readable, + writable, + }); + + if (writable) { + eos(w, err => { + writable = false; + if (err) { + destroyer(r, err); + } + onfinished(err); + }); + + d._write = function (chunk, encoding, callback) { + if (w.write(chunk, encoding)) { + callback(); + } else { + ondrain = callback; + } + }; + + d._final = function (callback) { + w.end(); + onfinish = callback; + }; + + w.on("drain", function () { + if (ondrain) { + const cb = ondrain; + ondrain = null; + cb(); + } + }); + + w.on("finish", function () { + if (onfinish) { + const cb = onfinish; + onfinish = null; + cb(); + } + }); + } + + if (readable) { + eos(r, err => { + readable = false; + if (err) { + destroyer(r, err); + } + onfinished(err); + }); + + r.on("readable", function () { + if (onreadable) { + const cb = onreadable; + onreadable = null; + cb(); + } + }); + + r.on("end", function () { + d.push(null); + }); + + d._read = function () { + while (true) { + const buf = r.read(); + + if (buf === null) { + onreadable = d._read; + return; + } + + if (!d.push(buf)) { + return; + } + } + }; + } + + d._destroy = function (err, callback) { + if (!err && onclose !== null) { + err = $makeAbortError(); + } + + onreadable = null; + ondrain = null; + onfinish = null; + + if (onclose === null) { + callback(err); + } else { + onclose = callback; + destroyer(w, err); + destroyer(r, err); + } + }; + + return d; +} + +export default duplexify; diff --git a/src/js/internal/streams/duplexpair.ts b/src/js/internal/streams/duplexpair.ts new file mode 100644 index 0000000000..63211b8032 --- /dev/null +++ b/src/js/internal/streams/duplexpair.ts @@ -0,0 +1,59 @@ +"use strict"; + +const Duplex = require("internal/streams/duplex"); + +const kCallback = Symbol("Callback"); +const kInitOtherSide = Symbol("InitOtherSide"); + +class DuplexSide extends Duplex { + #otherSide = null; + + constructor(options) { + super(options); + this[kCallback] = null; + this.#otherSide = null; + } + + [kInitOtherSide](otherSide) { + // Ensure this can only be set once, to enforce encapsulation. + if (this.#otherSide === null) { + this.#otherSide = otherSide; + } else { + $assert(this.#otherSide === null); + } + } + + _read() { + const callback = this[kCallback]; + if (callback) { + this[kCallback] = null; + callback(); + } + } + + _write(chunk, encoding, callback) { + $assert(this.#otherSide !== null); + $assert(this.#otherSide[kCallback] === null); + if (chunk.length === 0) { + process.nextTick(callback); + } else { + this.#otherSide.push(chunk); + this.#otherSide[kCallback] = callback; + } + } + + _final(callback) { + this.#otherSide.on("end", callback); + this.#otherSide.push(null); + } +} + +function duplexPair(options) { + const side0 = new DuplexSide(options); + const side1 = new DuplexSide(options); + side0[kInitOtherSide](side1); + side1[kInitOtherSide](side0); + return [side0, side1]; +} + +export default duplexPair; diff --git a/src/js/internal/streams/end-of-stream.ts b/src/js/internal/streams/end-of-stream.ts new file mode 100644 index 0000000000..fdf0b2d236 --- /dev/null +++ b/src/js/internal/streams/end-of-stream.ts @@ -0,0 +1,297 @@ +// Ported from https://github.com/mafintosh/end-of-stream with +// permission from the author, Mathias Buus (@mafintosh). + +"use strict"; + +const { kEmptyObject, once } = require("internal/shared"); +const { validateAbortSignal, validateFunction, validateObject, validateBoolean } = require("internal/validators"); +const { + isClosed, + isReadable, + isReadableNodeStream, + isReadableStream, + isReadableFinished, + isReadableErrored, + isWritable, + isWritableNodeStream, + isWritableStream, + isWritableFinished, + isWritableErrored, + isNodeStream, + willEmitClose: _willEmitClose, + kIsClosedPromise, +} = require("internal/streams/utils"); + +const SymbolDispose = Symbol.dispose; +const PromisePrototypeThen = Promise.prototype.then; + +let addAbortListener; + +function isRequest(stream) { + return stream.setHeader && typeof stream.abort === "function"; +} + +const nop = () => {}; + +function eos(stream, options, callback) { + if (arguments.length === 2) { + callback = options; + options = kEmptyObject; + } else if (options == null) { + options = kEmptyObject; + } else { + validateObject(options, "options"); + } + validateFunction(callback, "callback"); + validateAbortSignal(options.signal, "options.signal"); + + callback = once(callback); + + if (isReadableStream(stream) || isWritableStream(stream)) { + return eosWeb(stream, options, callback); + } + + if (!isNodeStream(stream)) { + throw $ERR_INVALID_ARG_TYPE("stream", ["ReadableStream", "WritableStream", "Stream"], stream); + } + + const readable = options.readable ?? isReadableNodeStream(stream); + const writable = options.writable ?? isWritableNodeStream(stream); + + const wState = stream._writableState; + const rState = stream._readableState; + + const onlegacyfinish = () => { + if (!stream.writable) { + onfinish(); + } + }; + + // TODO (ronag): Improve soft detection to include core modules and + // common ecosystem modules that do properly emit 'close' but fail + // this generic check. + let willEmitClose = + _willEmitClose(stream) && isReadableNodeStream(stream) === readable && isWritableNodeStream(stream) === writable; + + let writableFinished = isWritableFinished(stream, false); + const onfinish = () => { + writableFinished = true; + // Stream should not be destroyed here. If it is that + // means that user space is doing something differently and + // we cannot trust willEmitClose. + if (stream.destroyed) { + willEmitClose = false; + } + + if (willEmitClose && (!stream.readable || readable)) { + return; + } + + if (!readable || readableFinished) { + callback.$call(stream); + } + }; + + let readableFinished = isReadableFinished(stream, false); + const onend = () => { + readableFinished = true; + // Stream should not be destroyed here. If it is that + // means that user space is doing something differently and + // we cannot trust willEmitClose. + if (stream.destroyed) { + willEmitClose = false; + } + + if (willEmitClose && (!stream.writable || writable)) { + return; + } + + if (!writable || writableFinished) { + callback.$call(stream); + } + }; + + const onerror = err => { + callback.$call(stream, err); + }; + + let closed = isClosed(stream); + + const onclose = () => { + closed = true; + + const errored = isWritableErrored(stream) || isReadableErrored(stream); + + if (errored && typeof errored !== "boolean") { + return callback.$call(stream, errored); + } + + if (readable && !readableFinished && isReadableNodeStream(stream, true)) { + if (!isReadableFinished(stream, false)) return callback.$call(stream, $ERR_STREAM_PREMATURE_CLOSE()); + } + if (writable && !writableFinished) { + if (!isWritableFinished(stream, false)) return callback.$call(stream, $ERR_STREAM_PREMATURE_CLOSE()); + } + + callback.$call(stream); + }; + + const onclosed = () => { + closed = true; + + const errored = isWritableErrored(stream) || isReadableErrored(stream); + + if (errored && typeof errored !== "boolean") { + return callback.$call(stream, errored); + } + + callback.$call(stream); + }; + + const onrequest = () => { + stream.req.on("finish", onfinish); + }; + + if (isRequest(stream)) { + stream.on("complete", onfinish); + if (!willEmitClose) { + stream.on("abort", onclose); + } + if (stream.req) { + onrequest(); + } else { + stream.on("request", onrequest); + } + } else if (writable && !wState) { + // legacy streams + stream.on("end", onlegacyfinish); + stream.on("close", onlegacyfinish); + } + + // Not all streams will emit 'close' after 'aborted'. + if (!willEmitClose && typeof stream.aborted === "boolean") { + stream.on("aborted", onclose); + } + + stream.on("end", onend); + stream.on("finish", onfinish); + if (options.error !== false) { + stream.on("error", onerror); + } + stream.on("close", onclose); + + if (closed) { + process.nextTick(onclose); + } else if (wState?.errorEmitted || rState?.errorEmitted) { + if (!willEmitClose) { + process.nextTick(onclosed); + } + } else if ( + !readable && + (!willEmitClose || isReadable(stream)) && + (writableFinished || isWritable(stream) === false) && + (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) + ) { + process.nextTick(onclosed); + } else if ( + !writable && + (!willEmitClose || isWritable(stream)) && + (readableFinished || isReadable(stream) === false) + ) { + process.nextTick(onclosed); + } else if (rState && stream.req && stream.aborted) { + process.nextTick(onclosed); + } + + const cleanup = () => { + callback = nop; + stream.removeListener("aborted", onclose); + stream.removeListener("complete", onfinish); + stream.removeListener("abort", onclose); + stream.removeListener("request", onrequest); + if (stream.req) stream.req.removeListener("finish", onfinish); + stream.removeListener("end", onlegacyfinish); + stream.removeListener("close", onlegacyfinish); + stream.removeListener("finish", onfinish); + stream.removeListener("end", onend); + stream.removeListener("error", onerror); + stream.removeListener("close", onclose); + }; + + if (options.signal && !closed) { + const abort = () => { + // Keep it because cleanup removes it. + const endCallback = callback; + cleanup(); + endCallback.$call(stream, $makeAbortError(undefined, { cause: options.signal.reason })); + }; + if (options.signal.aborted) { + process.nextTick(abort); + } else { + addAbortListener ??= require("internal/abort_listener").addAbortListener; + const disposable = addAbortListener(options.signal, abort); + const originalCallback = callback; + callback = once((...args) => { + disposable[SymbolDispose](); + originalCallback.$apply(stream, args); + }); + } + } + + return cleanup; +} + +function eosWeb(stream, options, callback) { + let isAborted = false; + let abort = nop; + if (options.signal) { + abort = () => { + isAborted = true; + callback.$call(stream, $makeAbortError(undefined, { cause: options.signal.reason })); + }; + if (options.signal.aborted) { + process.nextTick(abort); + } else { + addAbortListener ??= require("internal/abort_listener").addAbortListener; + const disposable = addAbortListener(options.signal, abort); + const originalCallback = callback; + callback = once((...args) => { + disposable[SymbolDispose](); + originalCallback.$apply(stream, args); + }); + } + } + const resolverFn = (...args) => { + if (!isAborted) { + process.nextTick(() => callback.$apply(stream, args)); + } + }; + PromisePrototypeThen.$call(stream[kIsClosedPromise].promise, resolverFn, resolverFn); + return nop; +} + +function finished(stream, opts) { + let autoCleanup = false; + if (opts === null) { + opts = kEmptyObject; + } + if (opts?.cleanup) { + validateBoolean(opts.cleanup, "cleanup"); + autoCleanup = opts.cleanup; + } + return new Promise((resolve, reject) => { + const cleanup = eos(stream, opts, err => { + if (autoCleanup) { + cleanup(); + } + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +eos.finished = finished; +export default eos; diff --git a/src/js/internal/streams/from.ts b/src/js/internal/streams/from.ts new file mode 100644 index 0000000000..46308d5d3d --- /dev/null +++ b/src/js/internal/streams/from.ts @@ -0,0 +1,197 @@ +"use strict"; + +const { Buffer } = require("node:buffer"); + +const SymbolIterator = Symbol.iterator; +const SymbolAsyncIterator = Symbol.asyncIterator; +const PromisePrototypeThen = Promise.prototype.then; + +function from(Readable, iterable, opts) { + let iterator; + if (typeof iterable === "string" || iterable instanceof Buffer) { + return new Readable({ + objectMode: true, + ...opts, + read() { + this.push(iterable); + this.push(null); + }, + }); + } + + let isAsync; + if (iterable?.[SymbolAsyncIterator]) { + isAsync = true; + iterator = iterable[SymbolAsyncIterator](); + } else if (iterable?.[SymbolIterator]) { + isAsync = false; + iterator = iterable[SymbolIterator](); + } else { + throw $ERR_INVALID_ARG_TYPE("iterable", ["Iterable"], iterable); + } + + const readable = new Readable({ + objectMode: true, + highWaterMark: 1, + // TODO(ronag): What options should be allowed? + ...opts, + }); + + // Flag to protect against _read + // being called before last iteration completion. + let reading = false; + let isAsyncValues = false; + + readable._read = function () { + if (!reading) { + reading = true; + + if (isAsync) { + nextAsync(); + } else if (isAsyncValues) { + nextSyncWithAsyncValues(); + } else { + nextSyncWithSyncValues(); + } + } + }; + + readable._destroy = function (error, cb) { + PromisePrototypeThen.$call( + close(error), + () => process.nextTick(cb, error), // nextTick is here in case cb throws + e => process.nextTick(cb, e || error), + ); + }; + + async function close(error) { + const hadError = error !== undefined && error !== null; + const hasThrow = typeof iterator.throw === "function"; + if (hadError && hasThrow) { + const { value, done } = await iterator.throw(error); + await value; + if (done) { + return; + } + } + if (typeof iterator.return === "function") { + const { value } = await iterator.return(); + await value; + } + } + + // There are a lot of duplication here, it's done on purpose for performance + // reasons - avoid await when not needed. + + function nextSyncWithSyncValues() { + for (;;) { + try { + const { value, done } = iterator.next(); + + if (done) { + readable.push(null); + return; + } + + if (value && typeof value.then === "function") { + return changeToAsyncValues(value); + } + + if (value === null) { + reading = false; + throw $ERR_STREAM_NULL_VALUES(); + } + + if (readable.push(value)) { + continue; + } + + reading = false; + } catch (err) { + readable.destroy(err); + } + break; + } + } + + async function changeToAsyncValues(value) { + isAsyncValues = true; + + try { + const res = await value; + + if (res === null) { + reading = false; + throw $ERR_STREAM_NULL_VALUES(); + } + + if (readable.push(res)) { + nextSyncWithAsyncValues(); + return; + } + + reading = false; + } catch (err) { + readable.destroy(err); + } + } + + async function nextSyncWithAsyncValues() { + for (;;) { + try { + const { value, done } = iterator.next(); + + if (done) { + readable.push(null); + return; + } + + const res = value && typeof value.then === "function" ? await value : value; + + if (res === null) { + reading = false; + throw $ERR_STREAM_NULL_VALUES(); + } + + if (readable.push(res)) { + continue; + } + + reading = false; + } catch (err) { + readable.destroy(err); + } + break; + } + } + + async function nextAsync() { + for (;;) { + try { + const { value, done } = await iterator.next(); + + if (done) { + readable.push(null); + return; + } + + if (value === null) { + reading = false; + throw $ERR_STREAM_NULL_VALUES(); + } + + if (readable.push(value)) { + continue; + } + + reading = false; + } catch (err) { + readable.destroy(err); + } + break; + } + } + return readable; +} + +export default from; diff --git a/src/js/internal/streams/lazy_transform.ts b/src/js/internal/streams/lazy_transform.ts new file mode 100644 index 0000000000..56bdb1f8d4 --- /dev/null +++ b/src/js/internal/streams/lazy_transform.ts @@ -0,0 +1,53 @@ +// LazyTransform is a special type of Transform stream that is lazily loaded. +// This is used for performance with bi-API-ship: when two APIs are available +// for the stream, one conventional and one non-conventional. +"use strict"; + +const Transform = require("internal/streams/transform"); + +const ObjectDefineProperty = Object.defineProperty; +const ObjectDefineProperties = Object.defineProperties; + +function LazyTransform(options) { + this._options = options; +} +$toClass(LazyTransform, "LazyTransform", Transform); + +function makeGetter(name) { + return function () { + Transform.$call(this, this._options); + this._writableState.decodeStrings = false; + return this[name]; + }; +} + +function makeSetter(name) { + return function (val) { + ObjectDefineProperty(this, name, { + __proto__: null, + value: val, + enumerable: true, + configurable: true, + writable: true, + }); + }; +} + +ObjectDefineProperties(LazyTransform.prototype, { + _readableState: { + __proto__: null, + get: makeGetter("_readableState"), + set: makeSetter("_readableState"), + configurable: true, + enumerable: true, + }, + _writableState: { + __proto__: null, + get: makeGetter("_writableState"), + set: makeSetter("_writableState"), + configurable: true, + enumerable: true, + }, +}); + +export default LazyTransform; diff --git a/src/js/internal/streams/legacy.ts b/src/js/internal/streams/legacy.ts new file mode 100644 index 0000000000..251cac6438 --- /dev/null +++ b/src/js/internal/streams/legacy.ts @@ -0,0 +1,116 @@ +"use strict"; + +const EE = require("node:events"); + +const ReflectOwnKeys = Reflect.ownKeys; +const ArrayIsArray = Array.isArray; + +function Stream(opts) { + EE.$call(this, opts); +} +$toClass(Stream, "Stream", EE); + +Stream.prototype.pipe = function (dest, options) { + const source = this; + + function ondata(chunk) { + if (dest.writable && dest.write(chunk) === false && source.pause) { + source.pause(); + } + } + + source.on("data", ondata); + + function ondrain() { + if (source.readable && source.resume) { + source.resume(); + } + } + + dest.on("drain", ondrain); + + // If the 'end' option is not supplied, dest.end() will be called when + // source gets the 'end' or 'close' events. Only dest.end() once. + if (!dest._isStdio && (!options || options.end !== false)) { + source.on("end", onend); + source.on("close", onclose); + } + + let didOnEnd = false; + function onend() { + if (didOnEnd) return; + didOnEnd = true; + + dest.end(); + } + + function onclose() { + if (didOnEnd) return; + didOnEnd = true; + + if (typeof dest.destroy === "function") dest.destroy(); + } + + // Don't leave dangling pipes when there are errors. + function onerror(er) { + cleanup(); + if (EE.listenerCount(this, "error") === 0) { + this.emit("error", er); + } + } + + prependListener(source, "error", onerror); + prependListener(dest, "error", onerror); + + // Remove all the event listeners that were added. + function cleanup() { + source.removeListener("data", ondata); + dest.removeListener("drain", ondrain); + + source.removeListener("end", onend); + source.removeListener("close", onclose); + + source.removeListener("error", onerror); + dest.removeListener("error", onerror); + + source.removeListener("end", cleanup); + source.removeListener("close", cleanup); + + dest.removeListener("close", cleanup); + } + + source.on("end", cleanup); + source.on("close", cleanup); + + dest.on("close", cleanup); + dest.emit("pipe", source); + + // Allow for unix-like usage: A.pipe(B).pipe(C) + return dest; +}; + +Stream.prototype.eventNames = function eventNames() { + const names = []; + for (const key of ReflectOwnKeys(this._events)) { + if (typeof this._events[key] === "function" || (ArrayIsArray(this._events[key]) && this._events[key].length > 0)) { + names.push(key); + } + } + return names; +}; + +function prependListener(emitter, event, fn) { + // Sadly this is not cacheable as some libraries bundle their own + // event emitter implementation with them. + if (typeof emitter.prependListener === "function") return emitter.prependListener(event, fn); + + // This is a hack to make sure that our error handler is attached before any + // userland ones. NEVER DO THIS. This is here only because this code needs + // to continue to work with older versions of Node.js that do not include + // the prependListener() method. The goal is to eventually remove this hack. + if (!emitter._events || !emitter._events[event]) emitter.on(event, fn); + else if (ArrayIsArray(emitter._events[event])) emitter._events[event].unshift(fn); + else emitter._events[event] = [fn, emitter._events[event]]; +} + +export default { Stream, prependListener }; diff --git a/src/js/internal/streams/nativereadable.ts b/src/js/internal/streams/nativereadable.ts new file mode 100644 index 0000000000..74f897c519 --- /dev/null +++ b/src/js/internal/streams/nativereadable.ts @@ -0,0 +1,246 @@ +const { kEnsureConstructed } = require("internal/shared"); +const { errorOrDestroy } = require("internal/streams/destroy"); + +const ProcessNextTick = process.nextTick; + +var DYNAMICALLY_ADJUST_CHUNK_SIZE = process.env.BUN_DISABLE_DYNAMIC_CHUNK_SIZE !== "1"; + +const MIN_BUFFER_SIZE = 512; + +const refCount = Symbol("refCount"); +const constructed = Symbol("constructed"); +const remainingChunk = Symbol("remainingChunk"); +const highWaterMark = Symbol("highWaterMark"); +const pendingRead = Symbol("pendingRead"); +const hasResized = Symbol("hasResized"); +const _onClose = Symbol("_onClose"); +const _onDrain = Symbol("_onDrain"); +const _internalConstruct = Symbol("_internalConstruct"); +const _getRemainingChunk = Symbol("_getRemainingChunk"); +const _adjustHighWaterMark = Symbol("_adjustHighWaterMark"); +const _handleResult = Symbol("_handleResult"); +const _internalRead = Symbol("_internalRead"); + +export default function () { + const Readable = require("internal/streams/readable"); + + var closer = [false]; + var handleNumberResult = function (nativeReadable, result, view, isClosed) { + if (result > 0) { + const slice = view.subarray(0, result); + view = slice.byteLength < view.byteLength ? view.subarray(result) : undefined; + if (slice.byteLength > 0) { + nativeReadable.push(slice); + } + } + + if (isClosed) { + ProcessNextTick(() => { + nativeReadable.push(null); + }); + } + + return view; + }; + + var handleArrayBufferViewResult = function (nativeReadable, result, view, isClosed) { + if (result.byteLength > 0) { + nativeReadable.push(result); + } + + if (isClosed) { + ProcessNextTick(() => { + nativeReadable.push(null); + }); + } + + return view; + }; + + function NativeReadable(ptr, options) { + if (!(this instanceof NativeReadable)) return Reflect.construct(NativeReadable, [ptr, options]); + + this[refCount] = 0; + this[constructed] = false; + this[remainingChunk] = undefined; + this[pendingRead] = false; + this[hasResized] = !DYNAMICALLY_ADJUST_CHUNK_SIZE; + + options ??= {}; + Readable.$apply(this, [options]); + + if (typeof options.highWaterMark === "number") { + this[highWaterMark] = options.highWaterMark; + } else { + this[highWaterMark] = 256 * 1024; + } + this.$bunNativePtr = ptr; + this[constructed] = false; + this[remainingChunk] = undefined; + this[pendingRead] = false; + ptr.onClose = this[_onClose].bind(this); + ptr.onDrain = this[_onDrain].bind(this); + } + $toClass(NativeReadable, "NativeReadable", Readable); + + NativeReadable.prototype[_onClose] = function () { + this.push(null); + }; + + NativeReadable.prototype[_onDrain] = function (chunk) { + this.push(chunk); + }; + + // maxToRead is by default the highWaterMark passed from the Readable.read call to this fn + // However, in the case of an fs.ReadStream, we can pass the number of bytes we want to read + // which may be significantly less than the actual highWaterMark + NativeReadable.prototype._read = function _read(maxToRead) { + $debug("NativeReadable._read", this.__id); + if (this[pendingRead]) { + $debug("pendingRead is true", this.__id); + return; + } + var ptr = this.$bunNativePtr; + $debug("ptr @ NativeReadable._read", ptr, this.__id); + if (!ptr) { + this.push(null); + return; + } + if (!this[constructed]) { + $debug("NativeReadable not constructed yet", this.__id); + this[_internalConstruct](ptr); + } + return this[_internalRead](this[_getRemainingChunk](maxToRead), ptr); + }; + + NativeReadable.prototype[_internalConstruct] = function (ptr) { + $assert(this[constructed] === false); + this[constructed] = true; + + const result = ptr.start(this[highWaterMark]); + + $debug("NativeReadable internal `start` result", result, this.__id); + + if (typeof result === "number" && result > 1) { + this[hasResized] = true; + $debug("NativeReadable resized", this.__id); + + this[highWaterMark] = Math.min(this[highWaterMark], result); + } + + const drainResult = ptr.drain(); + $debug("NativeReadable drain result", drainResult, this.__id); + if ((drainResult?.byteLength ?? 0) > 0) { + this.push(drainResult); + } + }; + + // maxToRead can be the highWaterMark (by default) or the remaining amount of the stream to read + // This is so the consumer of the stream can terminate the stream early if they know + // how many bytes they want to read (ie. when reading only part of a file) + // ObjectDefinePrivateProperty(NativeReadable.prototype, "_getRemainingChunk", ); + NativeReadable.prototype[_getRemainingChunk] = function (maxToRead) { + maxToRead ??= this[highWaterMark]; + var chunk = this[remainingChunk]; + $debug("chunk @ #getRemainingChunk", chunk, this.__id); + if (chunk?.byteLength ?? 0 < MIN_BUFFER_SIZE) { + var size = maxToRead > MIN_BUFFER_SIZE ? maxToRead : MIN_BUFFER_SIZE; + this[remainingChunk] = chunk = new Buffer(size); + } + return chunk; + }; + + // ObjectDefinePrivateProperty(NativeReadable.prototype, "_adjustHighWaterMark", ); + NativeReadable.prototype[_adjustHighWaterMark] = function () { + this[highWaterMark] = Math.min(this[highWaterMark] * 2, 1024 * 1024 * 2); + this[hasResized] = true; + $debug("Resized", this.__id); + }; + + // ObjectDefinePrivateProperty(NativeReadable.prototype, "_handleResult", ); + NativeReadable.prototype[_handleResult] = function (result, view, isClosed) { + $debug("result, isClosed @ #handleResult", result, isClosed, this.__id); + + if (typeof result === "number") { + if (result >= this[highWaterMark] && !this[hasResized] && !isClosed) { + this[_adjustHighWaterMark](); + } + return handleNumberResult(this, result, view, isClosed); + } else if (typeof result === "boolean") { + ProcessNextTick(() => { + this.push(null); + }); + return (view?.byteLength ?? 0 > 0) ? view : undefined; + } else if ($isTypedArrayView(result)) { + if (result.byteLength >= this[highWaterMark] && !this[hasResized] && !isClosed) { + this[_adjustHighWaterMark](); + } + + return handleArrayBufferViewResult(this, result, view, isClosed); + } else { + $debug("Unknown result type", result, this.__id); + throw new Error("Invalid result from pull"); + } + }; + + NativeReadable.prototype[_internalRead] = function (view, ptr) { + $debug("#internalRead()", this.__id); + closer[0] = false; + var result = ptr.pull(view, closer); + if ($isPromise(result)) { + this[pendingRead] = true; + return result.then( + result => { + this[pendingRead] = false; + $debug("pending no longerrrrrrrr (result returned from pull)", this.__id); + const isClosed = closer[0]; + this[remainingChunk] = this[_handleResult](result, view, isClosed); + }, + reason => { + $debug("error from pull", reason, this.__id); + errorOrDestroy(this, reason); + }, + ); + } else { + this[remainingChunk] = this[_handleResult](result, view, closer[0]); + } + }; + + NativeReadable.prototype._destroy = function (error, callback) { + var ptr = this.$bunNativePtr; + if (!ptr) { + callback(error); + return; + } + + this.$bunNativePtr = undefined; + ptr.updateRef(false); + + $debug("NativeReadable destroyed", this.__id); + ptr.cancel(error); + callback(error); + }; + + NativeReadable.prototype.ref = function () { + var ptr = this.$bunNativePtr; + if (ptr === undefined) return; + if (this[refCount]++ === 0) { + ptr.updateRef(true); + } + }; + + NativeReadable.prototype.unref = function () { + var ptr = this.$bunNativePtr; + if (ptr === undefined) return; + if (this[refCount]-- === 1) { + ptr.updateRef(false); + } + }; + + NativeReadable.prototype[kEnsureConstructed] = function () { + if (this[constructed]) return; + this[_internalConstruct](this.$bunNativePtr); + }; + + return NativeReadable; +} diff --git a/src/js/internal/streams/nativewritable.ts b/src/js/internal/streams/nativewritable.ts new file mode 100644 index 0000000000..fcc371efd2 --- /dev/null +++ b/src/js/internal/streams/nativewritable.ts @@ -0,0 +1,135 @@ +const Writable = require("internal/streams/writable"); + +const ProcessNextTick = process.nextTick; + +const _native = Symbol("native"); +const _pathOrFdOrSink = Symbol("pathOrFdOrSink"); +const { fileSinkSymbol: _fileSink } = require("internal/shared"); + +function NativeWritable(pathOrFdOrSink, options = {}) { + Writable.$call(this, options); + + this[_native] = true; + + this._construct = NativeWritable_internalConstruct; + this._final = NativeWritable_internalFinal; + this._write = NativeWritablePrototypeWrite; + + this[_pathOrFdOrSink] = pathOrFdOrSink; +} +$toClass(NativeWritable, "NativeWritable", Writable); + +// These are confusingly two different fns for construct which initially were the same thing because +// `_construct` is part of the lifecycle of Writable and is not called lazily, +// so we need to separate our _construct for Writable state and actual construction of the write stream +function NativeWritable_internalConstruct(cb) { + this._writableState.constructed = true; + this.constructed = true; + if (typeof cb === "function") ProcessNextTick(cb); + ProcessNextTick(() => { + this.emit("open", this.fd); + this.emit("ready"); + }); +} + +function NativeWritable_internalFinal(cb) { + var sink = this[_fileSink]; + if (sink) { + const end = sink.end(true); + if ($isPromise(end) && cb) { + end.then(() => { + if (cb) cb(); + }, cb); + } + } + if (cb) cb(); +} + +function NativeWritablePrototypeWrite(chunk, encoding, cb) { + var fileSink = this[_fileSink] ?? NativeWritable_lazyConstruct(this); + var result = fileSink.write(chunk); + + if (typeof encoding === "function") { + cb = encoding; + } + + if ($isPromise(result)) { + // var writePromises = this.#writePromises; + // var i = writePromises.length; + // writePromises[i] = result; + result + .then(result => { + this.emit("drain"); + if (cb) { + cb(null, result); + } + }) + .catch( + cb + ? err => { + cb(err); + } + : err => { + this.emit("error", err); + }, + ); + return false; + } + + // TODO: Should we just have a calculation based on encoding and length of chunk? + if (cb) cb(null, chunk.byteLength); + return true; +} + +function NativeWritable_lazyConstruct(stream) { + // TODO: Turn this check into check for instanceof FileSink + var sink = stream[_pathOrFdOrSink]; + if (typeof sink === "object") { + if (typeof sink.write === "function") { + return (stream[_fileSink] = sink); + } else { + throw new Error("Invalid FileSink"); + } + } else { + return (stream[_fileSink] = Bun.file(sink).writer()); + } +} + +const WritablePrototypeEnd = Writable.prototype.end; +NativeWritable.prototype.end = function end(chunk, encoding, cb, native) { + return WritablePrototypeEnd.$call(this, chunk, encoding, cb, native ?? this[_native]); +}; + +NativeWritable.prototype._destroy = function (error, cb) { + const w = this._writableState; + const r = this._readableState; + + if (w) { + w.destroyed = true; + w.closeEmitted = true; + } + if (r) { + r.destroyed = true; + r.closeEmitted = true; + } + + if (typeof cb === "function") cb(error); + + if (w?.closeEmitted || r?.closeEmitted) { + this.emit("close"); + } +}; + +NativeWritable.prototype.ref = function ref() { + const sink = (this[_fileSink] ||= NativeWritable_lazyConstruct(this)); + sink.ref(); + return this; +}; + +NativeWritable.prototype.unref = function unref() { + const sink = (this[_fileSink] ||= NativeWritable_lazyConstruct(this)); + sink.unref(); + return this; +}; + +export default NativeWritable; diff --git a/src/js/internal/streams/operators.ts b/src/js/internal/streams/operators.ts new file mode 100644 index 0000000000..c2ae0b0744 --- /dev/null +++ b/src/js/internal/streams/operators.ts @@ -0,0 +1,410 @@ +"use strict"; + +const { validateAbortSignal, validateInteger, validateObject } = require("internal/validators"); +const { kWeakHandler, kResistStopPropagation } = require("internal/shared"); +const { finished } = require("internal/streams/end-of-stream"); +const staticCompose = require("internal/streams/compose"); +const { addAbortSignalNoValidate } = require("internal/streams/add-abort-signal"); +const { isWritable, isNodeStream } = require("internal/streams/utils"); + +const MathFloor = Math.floor; +const PromiseResolve = Promise.resolve.bind(Promise); +const PromiseReject = Promise.reject.bind(Promise); +const PromisePrototypeThen = Promise.prototype.then; +const ArrayPrototypePush = Array.prototype.push; +const NumberIsNaN = Number.isNaN; +const ObjectDefineProperty = Object.defineProperty; + +const kEmpty = Symbol("kEmpty"); +const kEof = Symbol("kEof"); + +function compose(stream, options) { + if (options != null) { + validateObject(options, "options"); + } + if (options?.signal != null) { + validateAbortSignal(options.signal, "options.signal"); + } + + if (isNodeStream(stream) && !isWritable(stream)) { + throw $ERR_INVALID_ARG_VALUE("stream", stream, "must be writable"); + } + + const composedStream = staticCompose(this, stream); + + if (options?.signal) { + // Not validating as we already validated before + addAbortSignalNoValidate(options.signal, composedStream); + } + + return composedStream; +} + +function map(fn, options) { + if (typeof fn !== "function") { + throw $ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); + } + if (options != null) { + validateObject(options, "options"); + } + if (options?.signal != null) { + validateAbortSignal(options.signal, "options.signal"); + } + + let concurrency = 1; + if (options?.concurrency != null) { + concurrency = MathFloor(options.concurrency); + } + + let highWaterMark = concurrency - 1; + if (options?.highWaterMark != null) { + highWaterMark = MathFloor(options.highWaterMark); + } + + validateInteger(concurrency, "options.concurrency", 1); + validateInteger(highWaterMark, "options.highWaterMark", 0); + + highWaterMark += concurrency; + + return async function* map() { + const signal = AbortSignal.any([options?.signal].filter(Boolean)); + const stream = this; + const queue: (Promise | typeof kEof)[] = []; + const signalOpt = { signal }; + + let next; + let resume; + let done = false; + let cnt = 0; + + function onCatch() { + done = true; + afterItemProcessed(); + } + + function afterItemProcessed() { + cnt -= 1; + maybeResume(); + } + + function maybeResume() { + if (resume && !done && cnt < concurrency && queue.length < highWaterMark) { + resume(); + resume = null; + } + } + + async function pump() { + try { + for await (let val of stream) { + if (done) { + return; + } + + if (signal.aborted) { + throw $makeAbortError(); + } + + try { + val = fn(val, signalOpt); + + if (val === kEmpty) { + continue; + } + + val = PromiseResolve(val); + } catch (err) { + val = PromiseReject(err); + } + + cnt += 1; + + PromisePrototypeThen.$call(val, afterItemProcessed, onCatch); + + queue.push(val); + if (next) { + next(); + next = null; + } + + if (!done && (queue.length >= highWaterMark || cnt >= concurrency)) { + await new Promise(resolve => { + resume = resolve; + }); + } + } + queue.push(kEof); + } catch (err) { + const val = PromiseReject(err); + PromisePrototypeThen.$call(val, afterItemProcessed, onCatch); + queue.push(val); + } finally { + done = true; + if (next) { + next(); + next = null; + } + } + } + + pump(); + + try { + while (true) { + while (queue.length > 0) { + const val = await queue[0]; + + if (val === kEof) { + return; + } + + if (signal.aborted) { + throw $makeAbortError(); + } + + if (val !== kEmpty) { + yield val; + } + + queue.shift(); + maybeResume(); + } + + await new Promise(resolve => { + next = resolve; + }); + } + } finally { + done = true; + if (resume) { + resume(); + resume = null; + } + } + }.$call(this); +} + +async function some(fn, options = undefined) { + for await (const unused of filter.$call(this, fn, options)) { + return true; + } + return false; +} + +async function every(fn, options = undefined) { + if (typeof fn !== "function") { + throw $ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); + } + // https://en.wikipedia.org/wiki/De_Morgan's_laws + return !(await some.$call( + this, + async (...args) => { + return !(await fn(...args)); + }, + options, + )); +} + +async function find(fn, options) { + for await (const result of filter.$call(this, fn, options)) { + return result; + } + return undefined; +} + +async function forEach(fn, options) { + if (typeof fn !== "function") { + throw $ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); + } + async function forEachFn(value, options) { + await fn(value, options); + return kEmpty; + } + // eslint-disable-next-line no-unused-vars + for await (const unused of map.$call(this, forEachFn, options)); +} + +function filter(fn, options) { + if (typeof fn !== "function") { + throw $ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); + } + async function filterFn(value, options) { + if (await fn(value, options)) { + return value; + } + return kEmpty; + } + return map.$call(this, filterFn, options); +} + +// Specific to provide better error to reduce since the argument is only +// missing if the stream has no items in it - but the code is still appropriate +class ReduceAwareErrMissingArgs extends TypeError { + constructor() { + super("reduce"); + this.code = "ERR_MISSING_ARGS"; + this.message = "Reduce of an empty stream requires an initial value"; + } +} + +async function reduce(reducer, initialValue, options) { + if (typeof reducer !== "function") { + throw $ERR_INVALID_ARG_TYPE("reducer", ["Function", "AsyncFunction"], reducer); + } + if (options != null) { + validateObject(options, "options"); + } + if (options?.signal != null) { + validateAbortSignal(options.signal, "options.signal"); + } + + let hasInitialValue = arguments.length > 1; + if (options?.signal?.aborted) { + const err = $makeAbortError(undefined, { cause: options.signal.reason }); + this.once("error", () => {}); // The error is already propagated + await finished(this.destroy(err)); + throw err; + } + const ac = new AbortController(); + const signal = ac.signal; + if (options?.signal) { + const opts = { once: true, [kWeakHandler]: this, [kResistStopPropagation]: true }; + options.signal.addEventListener("abort", () => ac.abort(), opts); + } + let gotAnyItemFromStream = false; + try { + for await (const value of this) { + gotAnyItemFromStream = true; + if (options?.signal?.aborted) { + throw $makeAbortError(); + } + if (!hasInitialValue) { + initialValue = value; + hasInitialValue = true; + } else { + initialValue = await reducer(initialValue, value, { signal }); + } + } + if (!gotAnyItemFromStream && !hasInitialValue) { + throw new ReduceAwareErrMissingArgs(); + } + } finally { + ac.abort(); + } + return initialValue; +} + +async function toArray(options) { + if (options != null) { + validateObject(options, "options"); + } + if (options?.signal != null) { + validateAbortSignal(options.signal, "options.signal"); + } + + const result = []; + for await (const val of this) { + if (options?.signal?.aborted) { + throw $makeAbortError(undefined, { cause: options.signal.reason }); + } + ArrayPrototypePush.$call(result, val); + } + return result; +} + +function flatMap(fn, options) { + const values = map.$call(this, fn, options); + async function* flatMapInner() { + for await (const val of values) { + yield* val; + } + } + return flatMapInner.$call(this); +} + +function toIntegerOrInfinity(number) { + // We coerce here to align with the spec + // https://github.com/tc39/proposal-iterator-helpers/issues/169 + number = Number(number); + if (NumberIsNaN(number)) { + return 0; + } + if (number < 0) { + throw $ERR_OUT_OF_RANGE("number", ">= 0", number); + } + return number; +} + +function drop(number, options = undefined) { + if (options != null) { + validateObject(options, "options"); + } + if (options?.signal != null) { + validateAbortSignal(options.signal, "options.signal"); + } + + number = toIntegerOrInfinity(number); + return async function* drop() { + if (options?.signal?.aborted) { + throw $makeAbortError(); + } + for await (const val of this) { + if (options?.signal?.aborted) { + throw $makeAbortError(); + } + if (number-- <= 0) { + yield val; + } + } + }.$call(this); +} +ObjectDefineProperty(drop, "length", { value: 1 }); + +function take(number, options?: { signal: AbortSignal }) { + if (options != null) { + validateObject(options, "options"); + } + if (options?.signal != null) { + validateAbortSignal(options.signal, "options.signal"); + } + + number = toIntegerOrInfinity(number); + return async function* take() { + if (options?.signal?.aborted) { + throw $makeAbortError(); + } + for await (const val of this) { + if (options?.signal?.aborted) { + throw $makeAbortError(); + } + if (number-- > 0) { + yield val; + } + + // Don't get another item from iterator in case we reached the end + if (number <= 0) { + return; + } + } + }.$call(this); +} +ObjectDefineProperty(take, "length", { value: 1 }); + +export default { + streamReturningOperators: { + drop, + filter, + flatMap, + map, + take, + compose, + }, + promiseReturningOperators: { + every, + forEach, + reduce, + toArray, + some, + find, + }, +}; diff --git a/src/js/internal/streams/passthrough.ts b/src/js/internal/streams/passthrough.ts new file mode 100644 index 0000000000..7fb5fd901d --- /dev/null +++ b/src/js/internal/streams/passthrough.ts @@ -0,0 +1,20 @@ +// a passthrough stream. +// basically just the most minimal sort of Transform stream. +// Every written chunk gets output as-is. + +"use strict"; + +const Transform = require("internal/streams/transform"); + +function PassThrough(options) { + if (!(this instanceof PassThrough)) return Reflect.construct(PassThrough, [options]); + + Transform.$call(this, options); +} +$toClass(PassThrough, "PassThrough", Transform); + +PassThrough.prototype._transform = function (chunk, encoding, cb) { + cb(null, chunk); +}; + +export default PassThrough; diff --git a/src/js/internal/streams/pipeline.ts b/src/js/internal/streams/pipeline.ts new file mode 100644 index 0000000000..c771436558 --- /dev/null +++ b/src/js/internal/streams/pipeline.ts @@ -0,0 +1,448 @@ +// Ported from https://github.com/mafintosh/pump with +// permission from the author, Mathias Buus (@mafintosh). + +"use strict"; + +const eos = require("internal/streams/end-of-stream"); +const { once } = require("internal/shared"); +const destroyImpl = require("internal/streams/destroy"); +const Duplex = require("internal/streams/duplex"); +const { aggregateTwoErrors } = require("internal/errors"); +const { validateFunction, validateAbortSignal } = require("internal/validators"); +const { + isIterable, + isReadable, + isReadableNodeStream, + isNodeStream, + isTransformStream, + isWebStream, + isReadableStream, + isReadableFinished, +} = require("internal/streams/utils"); + +const SymbolAsyncIterator = Symbol.asyncIterator; +const ArrayIsArray = Array.isArray; +const SymbolDispose = Symbol.dispose; + +let PassThrough; +let Readable; +let addAbortListener; + +function destroyer(stream, reading, writing) { + let finished = false; + stream.on("close", () => { + finished = true; + }); + + const cleanup = eos(stream, { readable: reading, writable: writing }, err => { + finished = !err; + }); + + return { + destroy: err => { + if (finished) return; + finished = true; + destroyImpl.destroyer(stream, err || $ERR_STREAM_DESTROYED("pipe")); + }, + cleanup, + }; +} + +function popCallback(streams) { + // Streams should never be an empty array. It should always contain at least + // a single stream. Therefore optimize for the average case instead of + // checking for length === 0 as well. + validateFunction(streams[streams.length - 1], "streams[stream.length - 1]"); + return streams.pop(); +} + +function makeAsyncIterable(val) { + if (isIterable(val)) { + return val; + } else if (isReadableNodeStream(val)) { + // Legacy streams are not Iterable. + return fromReadable(val); + } + throw $ERR_INVALID_ARG_TYPE("val", ["Readable", "Iterable", "AsyncIterable"], val); +} + +async function* fromReadable(val) { + Readable ??= require("internal/streams/readable"); + yield* Readable.prototype[SymbolAsyncIterator].$call(val); +} + +async function pumpToNode(iterable, writable, finish, { end }) { + let error; + let onresolve: (() => void) | null = null; + + const resume = err => { + if (err) { + error = err; + } + + if (onresolve) { + const callback = onresolve; + onresolve = null; + callback(); + } + }; + + const wait = () => + new Promise((resolve, reject) => { + if (error) { + reject(error); + } else { + onresolve = () => { + if (error) { + reject(error); + } else { + resolve(); + } + }; + } + }); + + writable.on("drain", resume); + const cleanup = eos(writable, { readable: false }, resume); + + try { + if (writable.writableNeedDrain) { + await wait(); + } + + for await (const chunk of iterable) { + if (!writable.write(chunk)) { + await wait(); + } + } + + if (end) { + writable.end(); + await wait(); + } + + finish(); + } catch (err) { + finish(error !== err ? aggregateTwoErrors(error, err) : err); + } finally { + cleanup(); + writable.off("drain", resume); + } +} + +async function pumpToWeb(readable, writable, finish, { end }) { + if (isTransformStream(writable)) { + writable = writable.writable; + } + // https://streams.spec.whatwg.org/#example-manual-write-with-backpressure + const writer = writable.getWriter(); + try { + for await (const chunk of readable) { + await writer.ready; + writer.write(chunk).catch(() => {}); + } + + await writer.ready; + + if (end) { + await writer.close(); + } + + finish(); + } catch (err) { + try { + await writer.abort(err); + finish(err); + } catch (err) { + finish(err); + } + } +} + +function pipeline(...streams) { + return pipelineImpl(streams, once(popCallback(streams))); +} + +function pipelineImpl(streams, callback, opts?) { + if (streams.length === 1 && ArrayIsArray(streams[0])) { + streams = streams[0]; + } + + if (streams.length < 2) { + throw $ERR_MISSING_ARGS("streams"); + } + + const ac = new AbortController(); + const signal = ac.signal; + const outerSignal = opts?.signal; + + // Need to cleanup event listeners if last stream is readable + // https://github.com/nodejs/node/issues/35452 + const lastStreamCleanup: (() => void)[] = []; + + validateAbortSignal(outerSignal, "options.signal"); + + function abort() { + finishImpl($makeAbortError(undefined, { cause: outerSignal?.reason })); + } + + addAbortListener ??= require("internal/abort_listener").addAbortListener; + let disposable; + if (outerSignal) { + disposable = addAbortListener(outerSignal, abort); + } + + let error; + let value; + const destroys: ((err: Error) => void)[] = []; + + let finishCount = 0; + + function finish(err) { + finishImpl(err, --finishCount === 0); + } + + function finishOnlyHandleError(err) { + finishImpl(err, false); + } + + function finishImpl(err, final?) { + if (err && (!error || error.code === "ERR_STREAM_PREMATURE_CLOSE")) { + error = err; + } + + if (!error && !final) { + return; + } + + while (destroys.length) { + destroys.shift()?.(error); + } + + disposable?.[SymbolDispose](); + ac.abort(); + + if (final) { + if (!error) { + lastStreamCleanup.forEach(fn => fn()); + } + process.nextTick(callback, error, value); + } + } + + let ret; + for (let i = 0; i < streams.length; i++) { + const stream = streams[i]; + const reading = i < streams.length - 1; + const writing = i > 0; + const next = i + 1 < streams.length ? streams[i + 1] : null; + const end = reading || opts?.end !== false; + const isLastStream = i === streams.length - 1; + + if (isNodeStream(stream)) { + if (next !== null && (next?.closed || next?.destroyed)) { + throw $ERR_STREAM_UNABLE_TO_PIPE(); + } + + if (end) { + const { destroy, cleanup } = destroyer(stream, reading, writing); + destroys.push(destroy); + + if (isReadable(stream) && isLastStream) { + lastStreamCleanup.push(cleanup); + } + } + + // Catch stream errors that occur after pipe/pump has completed. + function onError(err) { + if (err && err.name !== "AbortError" && err.code !== "ERR_STREAM_PREMATURE_CLOSE") { + finishOnlyHandleError(err); + } + } + stream.on("error", onError); + if (isReadable(stream) && isLastStream) { + lastStreamCleanup.push(() => { + stream.removeListener("error", onError); + }); + } + } + + if (i === 0) { + if (typeof stream === "function") { + ret = stream({ signal }); + if (!isIterable(ret)) { + throw $ERR_INVALID_RETURN_VALUE("Iterable, AsyncIterable or Stream", "source", ret); + } + } else if (isIterable(stream) || isReadableNodeStream(stream) || isTransformStream(stream)) { + ret = stream; + } else { + ret = Duplex.from(stream); + } + } else if (typeof stream === "function") { + if (isTransformStream(ret)) { + ret = makeAsyncIterable(ret?.readable); + } else { + ret = makeAsyncIterable(ret); + } + ret = stream(ret, { signal }); + + if (reading) { + if (!isIterable(ret, true)) { + throw $ERR_INVALID_RETURN_VALUE("AsyncIterable", `transform[${i - 1}]`, ret); + } + } else { + PassThrough ??= require("internal/streams/passthrough"); + + // If the last argument to pipeline is not a stream + // we must create a proxy stream so that pipeline(...) + // always returns a stream which can be further + // composed through `.pipe(stream)`. + + const pt = new PassThrough({ + objectMode: true, + }); + + // Handle Promises/A+ spec, `then` could be a getter that throws on + // second use. + const then = ret?.then; + if (typeof then === "function") { + finishCount++; + then.$call( + ret, + val => { + value = val; + if (val != null) { + pt.write(val); + } + if (end) { + pt.end(); + } + process.nextTick(finish); + }, + err => { + pt.destroy(err); + process.nextTick(finish, err); + }, + ); + } else if (isIterable(ret, true)) { + finishCount++; + pumpToNode(ret, pt, finish, { end }); + } else if (isReadableStream(ret) || isTransformStream(ret)) { + const toRead = ret.readable || ret; + finishCount++; + pumpToNode(toRead, pt, finish, { end }); + } else { + throw $ERR_INVALID_RETURN_VALUE("AsyncIterable or Promise", "destination", ret); + } + + ret = pt; + + const { destroy, cleanup } = destroyer(ret, false, true); + destroys.push(destroy); + if (isLastStream) { + lastStreamCleanup.push(cleanup); + } + } + } else if (isNodeStream(stream)) { + if (isReadableNodeStream(ret)) { + finishCount += 2; + const cleanup = pipe(ret, stream, finish, finishOnlyHandleError, { end }); + if (isReadable(stream) && isLastStream) { + lastStreamCleanup.push(cleanup); + } + } else if (isTransformStream(ret) || isReadableStream(ret)) { + const toRead = ret.readable || ret; + finishCount++; + pumpToNode(toRead, stream, finish, { end }); + } else if (isIterable(ret)) { + finishCount++; + pumpToNode(ret, stream, finish, { end }); + } else { + throw $ERR_INVALID_ARG_TYPE( + "val", + ["Readable", "Iterable", "AsyncIterable", "ReadableStream", "TransformStream"], + ret, + ); + } + ret = stream; + } else if (isWebStream(stream)) { + if (isReadableNodeStream(ret)) { + finishCount++; + pumpToWeb(makeAsyncIterable(ret), stream, finish, { end }); + } else if (isReadableStream(ret) || isIterable(ret)) { + finishCount++; + pumpToWeb(ret, stream, finish, { end }); + } else if (isTransformStream(ret)) { + finishCount++; + pumpToWeb(ret.readable, stream, finish, { end }); + } else { + throw $ERR_INVALID_ARG_TYPE( + "val", + ["Readable", "Iterable", "AsyncIterable", "ReadableStream", "TransformStream"], + ret, + ); + } + ret = stream; + } else { + ret = Duplex.from(stream); + } + } + + if (signal?.aborted || outerSignal?.aborted) { + process.nextTick(abort); + } + + return ret; +} + +function pipe(src, dst, finish, finishOnlyHandleError, { end }) { + let ended = false; + dst.on("close", () => { + if (!ended) { + // Finish if the destination closes before the source has completed. + finishOnlyHandleError($ERR_STREAM_PREMATURE_CLOSE()); + } + }); + + src.pipe(dst, { end: false }); // If end is true we already will have a listener to end dst. + + if (end) { + // Compat. Before node v10.12.0 stdio used to throw an error so + // pipe() did/does not end() stdio destinations. + // Now they allow it but "secretly" don't close the underlying fd. + + function endFn() { + ended = true; + dst.end(); + } + + if (isReadableFinished(src)) { + // End the destination if the source has already ended. + process.nextTick(endFn); + } else { + src.once("end", endFn); + } + } else { + finish(); + } + + eos(src, { readable: true, writable: false }, err => { + const rState = src._readableState; + if (err && err.code === "ERR_STREAM_PREMATURE_CLOSE" && rState?.ended && !rState.errored && !rState.errorEmitted) { + // Some readable streams will emit 'close' before 'end'. However, since + // this is on the readable side 'end' should still be emitted if the + // stream has been ended and no error emitted. This should be allowed in + // favor of backwards compatibility. Since the stream is piped to a + // destination this should not result in any observable difference. + // We don't need to check if this is a writable premature close since + // eos will only fail with premature close on the reading side for + // duplex streams. + src.once("end", finish).once("error", finish); + } else { + finish(err); + } + }); + return eos(dst, { readable: false, writable: true }, finish); +} + +export default { pipelineImpl, pipeline }; diff --git a/src/js/internal/streams/readable.ts b/src/js/internal/streams/readable.ts new file mode 100644 index 0000000000..c31c05fb9e --- /dev/null +++ b/src/js/internal/streams/readable.ts @@ -0,0 +1,1650 @@ +"use strict"; + +const EE = require("node:events"); +const { Stream, prependListener } = require("internal/streams/legacy"); +const { Buffer } = require("node:buffer"); +const { addAbortSignal } = require("internal/streams/add-abort-signal"); +const eos = require("internal/streams/end-of-stream"); +const destroyImpl = require("internal/streams/destroy"); +const { getHighWaterMark, getDefaultHighWaterMark } = require("internal/streams/state"); +const { + kOnConstructed, + kState, + // bitfields + kObjectMode, + kErrorEmitted, + kAutoDestroy, + kEmitClose, + kDestroyed, + kClosed, + kCloseEmitted, + kErrored, + kConstructed, +} = require("internal/streams/utils"); +const { aggregateTwoErrors } = require("internal/errors"); +const { validateObject } = require("internal/validators"); +const { StringDecoder } = require("node:string_decoder"); +const from = require("internal/streams/from"); +const { SafeSet } = require("internal/primordials"); +const { kAutoDestroyed } = require("internal/shared"); + +const ObjectDefineProperties = Object.defineProperties; +const SymbolAsyncDispose = Symbol.asyncDispose; +const NumberIsNaN = Number.isNaN; +const NumberIsInteger = Number.isInteger; +const NumberParseInt = Number.parseInt; +const ArrayPrototypeIndexOf = Array.prototype.indexOf; +const ObjectKeys = Object.keys; +const SymbolAsyncIterator = Symbol.asyncIterator; +const TypedArrayPrototypeSet = Uint8Array.prototype.set; + +const { errorOrDestroy } = destroyImpl; +const nop = () => {}; + +const kErroredValue = Symbol("kErroredValue"); +const kDefaultEncodingValue = Symbol("kDefaultEncodingValue"); +const kDecoderValue = Symbol("kDecoderValue"); +const kEncodingValue = Symbol("kEncodingValue"); + +const kEnded = 1 << 9; +const kEndEmitted = 1 << 10; +const kReading = 1 << 11; +const kSync = 1 << 12; +const kNeedReadable = 1 << 13; +const kEmittedReadable = 1 << 14; +const kReadableListening = 1 << 15; +const kResumeScheduled = 1 << 16; +const kMultiAwaitDrain = 1 << 17; +const kReadingMore = 1 << 18; +const kDataEmitted = 1 << 19; +const kDefaultUTF8Encoding = 1 << 20; +const kDecoder = 1 << 21; +const kEncoding = 1 << 22; +const kHasFlowing = 1 << 23; +const kFlowing = 1 << 24; +const kHasPaused = 1 << 25; +const kPaused = 1 << 26; +const kDataListening = 1 << 27; + +// TODO(benjamingr) it is likely slower to do it this way than with free functions +function makeBitMapDescriptor(bit) { + return { + enumerable: false, + get() { + return (this[kState] & bit) !== 0; + }, + set(value) { + if (value) this[kState] |= bit; + else this[kState] &= ~bit; + }, + }; +} + +function ReadableState(options, stream, isDuplex) { + // Bit map field to store ReadableState more efficiently with 1 bit per field + // instead of a V8 slot per field. + this[kState] = kEmitClose | kAutoDestroy | kConstructed | kSync; + + // Object stream flag. Used to make read(n) ignore n and to + // make all the buffer merging and length checks go away. + if (options?.objectMode) this[kState] |= kObjectMode; + + if (isDuplex && options?.readableObjectMode) this[kState] |= kObjectMode; + + // The point at which it stops calling _read() to fill the buffer + // Note: 0 is a valid value, means "don't call _read preemptively ever" + this.highWaterMark = options + ? getHighWaterMark(this, options, "readableHighWaterMark", isDuplex) + : getDefaultHighWaterMark(false); + + this.buffer = []; + this.bufferIndex = 0; + this.length = 0; + this.pipes = []; + + // Should close be emitted on destroy. Defaults to true. + if (options && options.emitClose === false) this[kState] &= ~kEmitClose; + + // Should .destroy() be called after 'end' (and potentially 'finish'). + if (options && options.autoDestroy === false) this[kState] &= ~kAutoDestroy; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + const defaultEncoding = options?.defaultEncoding; + if (defaultEncoding == null || defaultEncoding === "utf8" || defaultEncoding === "utf-8") { + this[kState] |= kDefaultUTF8Encoding; + } else if (Buffer.isEncoding(defaultEncoding)) { + this.defaultEncoding = defaultEncoding; + } else { + throw $ERR_UNKNOWN_ENCODING(defaultEncoding); + } + + // Ref the piped dest which we need a drain event on it + // type: null | Writable | Set. + this.awaitDrainWriters = null; + + if (options?.encoding) { + this.decoder = new StringDecoder(options.encoding); + this.encoding = options.encoding; + } +} +ReadableState.prototype = {}; +ObjectDefineProperties(ReadableState.prototype, { + objectMode: makeBitMapDescriptor(kObjectMode), + ended: makeBitMapDescriptor(kEnded), + endEmitted: makeBitMapDescriptor(kEndEmitted), + reading: makeBitMapDescriptor(kReading), + // Stream is still being constructed and cannot be + // destroyed until construction finished or failed. + // Async construction is opt in, therefore we start as + // constructed. + constructed: makeBitMapDescriptor(kConstructed), + // A flag to be able to tell if the event 'readable'/'data' is emitted + // immediately, or on a later tick. We set this to true at first, because + // any actions that shouldn't happen until "later" should generally also + // not happen before the first read call. + sync: makeBitMapDescriptor(kSync), + // Whenever we return null, then we set a flag to say + // that we're awaiting a 'readable' event emission. + needReadable: makeBitMapDescriptor(kNeedReadable), + emittedReadable: makeBitMapDescriptor(kEmittedReadable), + readableListening: makeBitMapDescriptor(kReadableListening), + resumeScheduled: makeBitMapDescriptor(kResumeScheduled), + // True if the error was already emitted and should not be thrown again. + errorEmitted: makeBitMapDescriptor(kErrorEmitted), + emitClose: makeBitMapDescriptor(kEmitClose), + autoDestroy: makeBitMapDescriptor(kAutoDestroy), + // Has it been destroyed. + destroyed: makeBitMapDescriptor(kDestroyed), + // Indicates whether the stream has finished destroying. + closed: makeBitMapDescriptor(kClosed), + // True if close has been emitted or would have been emitted + // depending on emitClose. + closeEmitted: makeBitMapDescriptor(kCloseEmitted), + multiAwaitDrain: makeBitMapDescriptor(kMultiAwaitDrain), + // If true, a maybeReadMore has been scheduled. + readingMore: makeBitMapDescriptor(kReadingMore), + dataEmitted: makeBitMapDescriptor(kDataEmitted), + + // Indicates whether the stream has errored. When true no further + // _read calls, 'data' or 'readable' events should occur. This is needed + // since when autoDestroy is disabled we need a way to tell whether the + // stream has failed. + errored: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kErrored) !== 0 ? this[kErroredValue] : null; + }, + set(value) { + if (value) { + this[kErroredValue] = value; + this[kState] |= kErrored; + } else { + this[kState] &= ~kErrored; + } + }, + }, + + defaultEncoding: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kDefaultUTF8Encoding) !== 0 ? "utf8" : this[kDefaultEncodingValue]; + }, + set(value) { + if (value === "utf8" || value === "utf-8") { + this[kState] |= kDefaultUTF8Encoding; + } else { + this[kState] &= ~kDefaultUTF8Encoding; + this[kDefaultEncodingValue] = value; + } + }, + }, + + decoder: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kDecoder) !== 0 ? this[kDecoderValue] : null; + }, + set(value) { + if (value) { + this[kDecoderValue] = value; + this[kState] |= kDecoder; + } else { + this[kState] &= ~kDecoder; + } + }, + }, + + encoding: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kEncoding) !== 0 ? this[kEncodingValue] : null; + }, + set(value) { + if (value) { + this[kEncodingValue] = value; + this[kState] |= kEncoding; + } else { + this[kState] &= ~kEncoding; + } + }, + }, + + flowing: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kHasFlowing) !== 0 ? (this[kState] & kFlowing) !== 0 : null; + }, + set(value) { + if (value == null) { + this[kState] &= ~(kHasFlowing | kFlowing); + } else if (value) { + this[kState] |= kHasFlowing | kFlowing; + } else { + this[kState] |= kHasFlowing; + this[kState] &= ~kFlowing; + } + }, + }, +}); + +ReadableState.prototype[kOnConstructed] = function onConstructed(stream) { + if ((this[kState] & kNeedReadable) !== 0) { + maybeReadMore(stream, this); + } +}; + +function Readable(options) { + if (!(this instanceof Readable)) return Reflect.construct(Readable, [options]); + + this._events ??= { + close: undefined, + error: undefined, + data: undefined, + end: undefined, + readable: undefined, + // Skip uncommon events... + // pause: undefined, + // resume: undefined, + // pipe: undefined, + // unpipe: undefined, + // [destroyImpl.kConstruct]: undefined, + // [destroyImpl.kDestroy]: undefined, + }; + + this._readableState = new ReadableState(options, this, false); + + if (options) { + if (typeof options.read === "function") this._read = options.read; + + if (typeof options.destroy === "function") this._destroy = options.destroy; + + if (typeof options.construct === "function") this._construct = options.construct; + + if (options.signal) addAbortSignal(options.signal, this); + } + + Stream.$call(this, options); + + if (this._construct != null) { + destroyImpl.construct(this, () => { + this._readableState[kOnConstructed](this); + }); + } +} +$toClass(Readable, "Readable", Stream); + +Readable.ReadableState = ReadableState; + +Readable.prototype.destroy = destroyImpl.destroy; +Readable.prototype._undestroy = destroyImpl.undestroy; +Readable.prototype._destroy = function (err, cb) { + cb(err); +}; + +Readable.prototype[EE.captureRejectionSymbol] = function (err) { + this.destroy(err); +}; + +Readable.prototype[SymbolAsyncDispose] = function () { + let error; + if (!this.destroyed) { + error = this.readableEnded ? null : $makeAbortError(); + this.destroy(error); + } + return new Promise((resolve, reject) => eos(this, err => (err && err !== error ? reject(err) : resolve(null)))); +}; + +// Manually shove something into the read() buffer. +// This returns true if the highWaterMark has not been hit yet, +// similar to how Writable.write() returns true if you should +// write() some more. +Readable.prototype.push = function (chunk, encoding) { + $debug("push", chunk); + + const state = this._readableState; + return (state[kState] & kObjectMode) === 0 + ? readableAddChunkPushByteMode(this, state, chunk, encoding) + : readableAddChunkPushObjectMode(this, state, chunk, encoding); +}; + +// Unshift should *always* be something directly out of read(). +Readable.prototype.unshift = function (chunk, encoding) { + $debug("unshift", chunk); + const state = this._readableState; + return (state[kState] & kObjectMode) === 0 + ? readableAddChunkUnshiftByteMode(this, state, chunk, encoding) + : readableAddChunkUnshiftObjectMode(this, state, chunk); +}; + +function readableAddChunkUnshiftByteMode(stream, state, chunk, encoding) { + if (chunk === null) { + state[kState] &= ~kReading; + onEofChunk(stream, state); + + return false; + } + + if (typeof chunk === "string") { + encoding ||= state.defaultEncoding; + if (state.encoding !== encoding) { + if (state.encoding) { + // When unshifting, if state.encoding is set, we have to save + // the string in the BufferList with the state encoding. + chunk = Buffer.from(chunk, encoding).toString(state.encoding); + } else { + chunk = Buffer.from(chunk, encoding); + } + } + } else if (Stream._isArrayBufferView(chunk)) { + chunk = Stream._uint8ArrayToBuffer(chunk); + } else if (chunk !== undefined && !(chunk instanceof Buffer)) { + errorOrDestroy(stream, $ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "TypedArray", "DataView"], chunk)); + return false; + } + + if (!(chunk && chunk.length > 0)) { + return canPushMore(state); + } + + return readableAddChunkUnshiftValue(stream, state, chunk); +} + +function readableAddChunkUnshiftObjectMode(stream, state, chunk) { + if (chunk === null) { + state[kState] &= ~kReading; + onEofChunk(stream, state); + + return false; + } + + return readableAddChunkUnshiftValue(stream, state, chunk); +} + +function readableAddChunkUnshiftValue(stream, state, chunk) { + if ((state[kState] & kEndEmitted) !== 0) errorOrDestroy(stream, $ERR_STREAM_UNSHIFT_AFTER_END_EVENT()); + else if ((state[kState] & (kDestroyed | kErrored)) !== 0) return false; + else addChunk(stream, state, chunk, true); + + return canPushMore(state); +} + +function readableAddChunkPushByteMode(stream, state, chunk, encoding) { + if (chunk === null) { + state[kState] &= ~kReading; + onEofChunk(stream, state); + return false; + } + + if (typeof chunk === "string") { + encoding ||= state.defaultEncoding; + if (state.encoding !== encoding) { + chunk = Buffer.from(chunk, encoding); + encoding = ""; + } + } else if (chunk instanceof Buffer) { + encoding = ""; + } else if (Stream._isArrayBufferView(chunk)) { + chunk = Stream._uint8ArrayToBuffer(chunk); + encoding = ""; + } else if (chunk !== undefined) { + errorOrDestroy(stream, $ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "TypedArray", "DataView"], chunk)); + return false; + } + + if (!chunk || chunk.length <= 0) { + state[kState] &= ~kReading; + maybeReadMore(stream, state); + + return canPushMore(state); + } + + if ((state[kState] & kEnded) !== 0) { + errorOrDestroy(stream, $ERR_STREAM_PUSH_AFTER_EOF()); + return false; + } + + if ((state[kState] & (kDestroyed | kErrored)) !== 0) { + return false; + } + + state[kState] &= ~kReading; + if ((state[kState] & kDecoder) !== 0 && !encoding) { + chunk = state[kDecoderValue].write(chunk); + if (chunk.length === 0) { + maybeReadMore(stream, state); + return canPushMore(state); + } + } + + addChunk(stream, state, chunk, false); + return canPushMore(state); +} + +function readableAddChunkPushObjectMode(stream, state, chunk, encoding) { + if (chunk === null) { + state[kState] &= ~kReading; + onEofChunk(stream, state); + return false; + } + + if ((state[kState] & kEnded) !== 0) { + errorOrDestroy(stream, $ERR_STREAM_PUSH_AFTER_EOF()); + return false; + } + + if ((state[kState] & (kDestroyed | kErrored)) !== 0) { + return false; + } + + state[kState] &= ~kReading; + + if ((state[kState] & kDecoder) !== 0 && !encoding) { + chunk = state[kDecoderValue].write(chunk); + } + + addChunk(stream, state, chunk, false); + return canPushMore(state); +} + +function canPushMore(state) { + // We can push more data if we are below the highWaterMark. + // Also, if we have no data yet, we can stand some more bytes. + // This is to work around cases where hwm=0, such as the repl. + return (state[kState] & kEnded) === 0 && (state.length < state.highWaterMark || state.length === 0); +} + +function addChunk(stream, state, chunk, addToFront) { + if ((state[kState] & (kFlowing | kSync | kDataListening)) === (kFlowing | kDataListening) && state.length === 0) { + // Use the guard to avoid creating `Set()` repeatedly + // when we have multiple pipes. + if ((state[kState] & kMultiAwaitDrain) !== 0) { + state.awaitDrainWriters.clear(); + } else { + state.awaitDrainWriters = null; + } + + state[kState] |= kDataEmitted; + stream.emit("data", chunk); + } else { + // Update the buffer info. + state.length += (state[kState] & kObjectMode) !== 0 ? 1 : chunk.length; + if (addToFront) { + if (state.bufferIndex > 0) { + state.buffer[--state.bufferIndex] = chunk; + } else { + state.buffer.unshift(chunk); // Slow path + } + } else { + state.buffer.push(chunk); + } + + if ((state[kState] & kNeedReadable) !== 0) emitReadable(stream); + } + maybeReadMore(stream, state); +} + +Readable.prototype.isPaused = function () { + const state = this._readableState; + return (state[kState] & kPaused) !== 0 || (state[kState] & (kHasFlowing | kFlowing)) === kHasFlowing; +}; + +// Backwards compatibility. +Readable.prototype.setEncoding = function (enc) { + const state = this._readableState; + + const decoder = new StringDecoder(enc); + state.decoder = decoder; + // If setEncoding(null), decoder.encoding equals utf8. + state.encoding = state.decoder.encoding; + + // Iterate over current buffer to convert already stored Buffers: + let content = ""; + for (const data of state.buffer.slice(state.bufferIndex)) { + content += decoder.write(data); + } + state.buffer.length = 0; + state.bufferIndex = 0; + + if (content !== "") state.buffer.push(content); + state.length = content.length; + return this; +}; + +// Don't raise the hwm > 1GB. +const MAX_HWM = 0x40000000; +function computeNewHighWaterMark(n) { + if (n > MAX_HWM) { + throw $ERR_OUT_OF_RANGE("size", "<= 1GiB", n); + } else { + // Get the next highest power of 2 to prevent increasing hwm excessively in + // tiny amounts. + n--; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + n++; + } + return n; +} + +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function howMuchToRead(n, state) { + if (n <= 0 || (state.length === 0 && (state[kState] & kEnded) !== 0)) return 0; + if ((state[kState] & kObjectMode) !== 0) return 1; + if (NumberIsNaN(n)) { + // Only flow one buffer at a time. + if ((state[kState] & kFlowing) !== 0 && state.length) return state.buffer[state.bufferIndex].length; + return state.length; + } + if (n <= state.length) return n; + return (state[kState] & kEnded) !== 0 ? state.length : 0; +} + +// You can override either this method, or the async _read(n) below. +Readable.prototype.read = function (n) { + $debug("read", n); + // Same as parseInt(undefined, 10), however V8 7.3 performance regressed + // in this scenario, so we are doing it manually. + if (n === undefined) { + n = NaN; + } else if (!NumberIsInteger(n)) { + n = NumberParseInt(n, 10); + } + const state = this._readableState; + const nOrig = n; + + // If we're asking for more than the current hwm, then raise the hwm. + if (n > state.highWaterMark) state.highWaterMark = computeNewHighWaterMark(n); + + if (n !== 0) state[kState] &= ~kEmittedReadable; + + // If we're doing read(0) to trigger a readable event, but we + // already have a bunch of data in the buffer, then just trigger + // the 'readable' event and move on. + if ( + n === 0 && + (state[kState] & kNeedReadable) !== 0 && + ((state.highWaterMark !== 0 ? state.length >= state.highWaterMark : state.length > 0) || + (state[kState] & kEnded) !== 0) + ) { + $debug("read: emitReadable"); + if (state.length === 0 && (state[kState] & kEnded) !== 0) endReadable(this); + else emitReadable(this); + return null; + } + + n = howMuchToRead(n, state); + + // If we've ended, and we're now clear, then finish it up. + if (n === 0 && (state[kState] & kEnded) !== 0) { + if (state.length === 0) endReadable(this); + return null; + } + + // All the actual chunk generation logic needs to be + // *below* the call to _read. The reason is that in certain + // synthetic stream cases, such as passthrough streams, _read + // may be a completely synchronous operation which may change + // the state of the read buffer, providing enough data when + // before there was *not* enough. + // + // So, the steps are: + // 1. Figure out what the state of things will be after we do + // a read from the buffer. + // + // 2. If that resulting state will trigger a _read, then call _read. + // Note that this may be asynchronous, or synchronous. Yes, it is + // deeply ugly to write APIs this way, but that still doesn't mean + // that the Readable class should behave improperly, as streams are + // designed to be sync/async agnostic. + // Take note if the _read call is sync or async (ie, if the read call + // has returned yet), so that we know whether or not it's safe to emit + // 'readable' etc. + // + // 3. Actually pull the requested chunks out of the buffer and return. + + // if we need a readable event, then we need to do some reading. + let doRead = (state[kState] & kNeedReadable) !== 0; + $debug("need readable", doRead); + + // If we currently have less than the highWaterMark, then also read some. + if (state.length === 0 || state.length - n < state.highWaterMark) { + doRead = true; + $debug("length less than watermark", doRead); + } + + // However, if we've ended, then there's no point, if we're already + // reading, then it's unnecessary, if we're constructing we have to wait, + // and if we're destroyed or errored, then it's not allowed, + if ((state[kState] & (kReading | kEnded | kDestroyed | kErrored | kConstructed)) !== kConstructed) { + doRead = false; + $debug("reading, ended or constructing", doRead); + } else if (doRead) { + $debug("do read"); + state[kState] |= kReading | kSync; + // If the length is currently zero, then we *need* a readable event. + if (state.length === 0) state[kState] |= kNeedReadable; + + // Call internal read method + try { + this._read(state.highWaterMark); + } catch (err) { + errorOrDestroy(this, err); + } + state[kState] &= ~kSync; + + // If _read pushed data synchronously, then `reading` will be false, + // and we need to re-evaluate how much data we can return to the user. + if ((state[kState] & kReading) === 0) n = howMuchToRead(nOrig, state); + } + + let ret; + if (n > 0) ret = fromList(n, state); + else ret = null; + + if (ret === null) { + state[kState] |= state.length <= state.highWaterMark ? kNeedReadable : 0; + n = 0; + } else { + state.length -= n; + if ((state[kState] & kMultiAwaitDrain) !== 0) { + state.awaitDrainWriters.clear(); + } else { + state.awaitDrainWriters = null; + } + } + + if (state.length === 0) { + // If we have nothing in the buffer, then we want to know + // as soon as we *do* get something into the buffer. + if ((state[kState] & kEnded) === 0) state[kState] |= kNeedReadable; + + // If we tried to read() past the EOF, then emit end on the next tick. + if (nOrig !== n && (state[kState] & kEnded) !== 0) endReadable(this); + } + + if (ret !== null && (state[kState] & (kErrorEmitted | kCloseEmitted)) === 0) { + state[kState] |= kDataEmitted; + this.emit("data", ret); + } + + return ret; +}; + +function onEofChunk(stream, state) { + $debug("onEofChunk"); + if ((state[kState] & kEnded) !== 0) return; + const decoder = (state[kState] & kDecoder) !== 0 ? state[kDecoderValue] : null; + if (decoder) { + const chunk = decoder.end(); + if (chunk?.length) { + state.buffer.push(chunk); + state.length += (state[kState] & kObjectMode) !== 0 ? 1 : chunk.length; + } + } + state[kState] |= kEnded; + + if ((state[kState] & kSync) !== 0) { + // If we are sync, wait until next tick to emit the data. + // Otherwise we risk emitting data in the flow() + // the readable code triggers during a read() call. + emitReadable(stream); + } else { + // Emit 'readable' now to make sure it gets picked up. + state[kState] &= ~kNeedReadable; + state[kState] |= kEmittedReadable; + // We have to emit readable now that we are EOF. Modules + // in the ecosystem (e.g. dicer) rely on this event being sync. + emitReadable_(stream); + } +} + +// Don't emit readable right away in sync mode, because this can trigger +// another read() call => stack overflow. This way, it might trigger +// a nextTick recursion warning, but that's not so bad. +function emitReadable(stream) { + const state = stream._readableState; + $debug("emitReadable"); + state[kState] &= ~kNeedReadable; + if ((state[kState] & kEmittedReadable) === 0) { + $debug("emitReadable", (state[kState] & kFlowing) !== 0); + state[kState] |= kEmittedReadable; + process.nextTick(emitReadable_, stream); + } +} + +function emitReadable_(stream) { + const state = stream._readableState; + $debug("emitReadable_"); + if ((state[kState] & (kDestroyed | kErrored)) === 0 && (state.length || (state[kState] & kEnded) !== 0)) { + stream.emit("readable"); + state[kState] &= ~kEmittedReadable; + } + + // The stream needs another readable event if: + // 1. It is not flowing, as the flow mechanism will take + // care of it. + // 2. It is not ended. + // 3. It is below the highWaterMark, so we can schedule + // another readable later. + state[kState] |= + (state[kState] & (kFlowing | kEnded)) === 0 && state.length <= state.highWaterMark ? kNeedReadable : 0; + flow(stream); +} + +// At this point, the user has presumably seen the 'readable' event, +// and called read() to consume some data. that may have triggered +// in turn another _read(n) call, in which case reading = true if +// it's in progress. +// However, if we're not ended, or reading, and the length < hwm, +// then go ahead and try to read some more preemptively. +function maybeReadMore(stream, state) { + if ((state[kState] & (kReadingMore | kConstructed)) === kConstructed) { + state[kState] |= kReadingMore; + process.nextTick(maybeReadMore_, stream, state); + } +} + +function maybeReadMore_(stream, state) { + // Attempt to read more data if we should. + // + // The conditions for reading more data are (one of): + // - Not enough data buffered (state.length < state.highWaterMark). The loop + // is responsible for filling the buffer with enough data if such data + // is available. If highWaterMark is 0 and we are not in the flowing mode + // we should _not_ attempt to buffer any extra data. We'll get more data + // when the stream consumer calls read() instead. + // - No data in the buffer, and the stream is in flowing mode. In this mode + // the loop below is responsible for ensuring read() is called. Failing to + // call read here would abort the flow and there's no other mechanism for + // continuing the flow if the stream consumer has just subscribed to the + // 'data' event. + // + // In addition to the above conditions to keep reading data, the following + // conditions prevent the data from being read: + // - The stream has ended (state.ended). + // - There is already a pending 'read' operation (state.reading). This is a + // case where the stream has called the implementation defined _read() + // method, but they are processing the call asynchronously and have _not_ + // called push() with new data. In this case we skip performing more + // read()s. The execution ends in this method again after the _read() ends + // up calling push() with more data. + while ( + (state[kState] & (kReading | kEnded)) === 0 && + (state.length < state.highWaterMark || ((state[kState] & kFlowing) !== 0 && state.length === 0)) + ) { + const len = state.length; + $debug("maybeReadMore read 0"); + stream.read(0); + if (len === state.length) + // Didn't get any data, stop spinning. + break; + } + state[kState] &= ~kReadingMore; +} + +// Abstract method. to be overridden in specific implementation classes. +// call cb(er, data) where data is <= n in length. +// for virtual (non-string, non-buffer) streams, "length" is somewhat +// arbitrary, and perhaps not very meaningful. +Readable.prototype._read = function (n) { + throw $ERR_METHOD_NOT_IMPLEMENTED("_read()"); +}; + +Readable.prototype.pipe = function (dest, pipeOpts) { + const src = this; + const state = this._readableState; + + if (state.pipes.length === 1) { + if ((state[kState] & kMultiAwaitDrain) === 0) { + state[kState] |= kMultiAwaitDrain; + state.awaitDrainWriters = new SafeSet(state.awaitDrainWriters ? [state.awaitDrainWriters] : []); + } + } + + state.pipes.push(dest); + $debug("pipe count=%d opts=%j", state.pipes.length, pipeOpts); + + const doEnd = (!pipeOpts || pipeOpts.end !== false) && dest !== process.stdout && dest !== process.stderr; + + const endFn = doEnd ? onend : unpipe; + if ((state[kState] & kEndEmitted) !== 0) process.nextTick(endFn); + else src.once("end", endFn); + + dest.on("unpipe", onunpipe); + function onunpipe(readable, unpipeInfo) { + $debug("onunpipe"); + if (readable === src) { + if (unpipeInfo && unpipeInfo.hasUnpiped === false) { + unpipeInfo.hasUnpiped = true; + cleanup(); + } + } + } + + function onend() { + $debug("onend"); + dest.end(); + } + + let ondrain; + + let cleanedUp = false; + function cleanup() { + $debug("cleanup"); + // Cleanup event handlers once the pipe is broken. + dest.removeListener("close", onclose); + dest.removeListener("finish", onfinish); + if (ondrain) { + dest.removeListener("drain", ondrain); + } + dest.removeListener("error", onerror); + dest.removeListener("unpipe", onunpipe); + src.removeListener("end", onend); + src.removeListener("end", unpipe); + src.removeListener("data", ondata); + + cleanedUp = true; + + // If the reader is waiting for a drain event from this + // specific writer, then it would cause it to never start + // flowing again. + // So, if this is awaiting a drain, then we just call it now. + // If we don't know, then assume that we are waiting for one. + if (ondrain && state.awaitDrainWriters && (!dest._writableState || dest._writableState.needDrain)) ondrain(); + } + + function pause() { + // If the user unpiped during `dest.write()`, it is possible + // to get stuck in a permanently paused state if that write + // also returned false. + // => Check whether `dest` is still a piping destination. + if (!cleanedUp) { + if (state.pipes.length === 1 && state.pipes[0] === dest) { + $debug("false write response, pause", 0); + state.awaitDrainWriters = dest; + state[kState] &= ~kMultiAwaitDrain; + } else if (state.pipes.length > 1 && state.pipes.includes(dest)) { + $debug("false write response, pause", state.awaitDrainWriters.size); + state.awaitDrainWriters.add(dest); + } + src.pause(); + } + if (!ondrain) { + // When the dest drains, it reduces the awaitDrain counter + // on the source. This would be more elegant with a .once() + // handler in flow(), but adding and removing repeatedly is + // too slow. + ondrain = pipeOnDrain(src, dest); + dest.on("drain", ondrain); + } + } + + src.on("data", ondata); + function ondata(chunk) { + $debug("ondata"); + const ret = dest.write(chunk); + $debug("dest.write", ret); + if (ret === false) { + pause(); + } + } + + // If the dest has an error, then stop piping into it. + // However, don't suppress the throwing behavior for this. + function onerror(er) { + $debug("onerror", er); + unpipe(); + dest.removeListener("error", onerror); + if (dest.listenerCount("error") === 0) { + const s = dest._writableState || dest._readableState; + if (s && !s.errorEmitted) { + // User incorrectly emitted 'error' directly on the stream. + errorOrDestroy(dest, er); + } else { + dest.emit("error", er); + } + } + } + + // Make sure our error handler is attached before userland ones. + prependListener(dest, "error", onerror); + + // Both close and finish should trigger unpipe, but only once. + function onclose() { + dest.removeListener("finish", onfinish); + unpipe(); + } + dest.once("close", onclose); + function onfinish() { + $debug("onfinish"); + dest.removeListener("close", onclose); + unpipe(); + } + dest.once("finish", onfinish); + + function unpipe() { + $debug("unpipe"); + src.unpipe(dest); + } + + // Tell the dest that it's being piped to. + dest.emit("pipe", src); + + // Start the flow if it hasn't been started already. + + if (dest.writableNeedDrain === true) { + pause(); + } else if ((state[kState] & kFlowing) === 0) { + $debug("pipe resume"); + src.resume(); + } + + return dest; +}; + +function pipeOnDrain(src, dest) { + return function pipeOnDrainFunctionResult() { + const state = src._readableState; + + // `ondrain` will call directly, + // `this` maybe not a reference to dest, + // so we use the real dest here. + if (state.awaitDrainWriters === dest) { + $debug("pipeOnDrain", 1); + state.awaitDrainWriters = null; + } else if ((state[kState] & kMultiAwaitDrain) !== 0) { + $debug("pipeOnDrain", state.awaitDrainWriters.size); + state.awaitDrainWriters.delete(dest); + } + + if ((!state.awaitDrainWriters || state.awaitDrainWriters.size === 0) && (state[kState] & kDataListening) !== 0) { + src.resume(); + } + }; +} + +Readable.prototype.unpipe = function (dest) { + const state = this._readableState; + const unpipeInfo = { hasUnpiped: false }; + + // If we're not piping anywhere, then do nothing. + if (state.pipes.length === 0) return this; + + if (!dest) { + // remove all. + const dests = state.pipes; + state.pipes = []; + this.pause(); + + for (let i = 0; i < dests.length; i++) dests[i].emit("unpipe", this, { hasUnpiped: false }); + return this; + } + + // Try to find the right one. + const index = ArrayPrototypeIndexOf.$call(state.pipes, dest); + if (index === -1) return this; + + state.pipes.splice(index, 1); + if (state.pipes.length === 0) this.pause(); + + dest.emit("unpipe", this, unpipeInfo); + + return this; +}; + +// Set up data events if they are asked for +// Ensure readable listeners eventually get something. +Readable.prototype.on = function (ev, fn) { + const res = Stream.prototype.on.$call(this, ev, fn); + const state = this._readableState; + + if (ev === "data") { + state[kState] |= kDataListening; + + // Update readableListening so that resume() may be a no-op + // a few lines down. This is needed to support once('readable'). + state[kState] |= this.listenerCount("readable") > 0 ? kReadableListening : 0; + + // Try start flowing on next tick if stream isn't explicitly paused. + if ((state[kState] & (kHasFlowing | kFlowing)) !== kHasFlowing) { + this.resume(); + } + } else if (ev === "readable") { + if ((state[kState] & (kEndEmitted | kReadableListening)) === 0) { + state[kState] |= kReadableListening | kNeedReadable | kHasFlowing; + state[kState] &= ~(kFlowing | kEmittedReadable); + $debug("on readable"); + if (state.length) { + emitReadable(this); + } else if ((state[kState] & kReading) === 0) { + process.nextTick(nReadingNextTick, this); + } + } + } + + return res; +}; +Readable.prototype.addListener = Readable.prototype.on; + +Readable.prototype.removeListener = function (ev, fn) { + const state = this._readableState; + + const res = Stream.prototype.removeListener.$call(this, ev, fn); + + if (ev === "readable") { + // We need to check if there is someone still listening to + // readable and reset the state. However this needs to happen + // after readable has been emitted but before I/O (nextTick) to + // support once('readable', fn) cycles. This means that calling + // resume within the same tick will have no + // effect. + process.nextTick(updateReadableListening, this); + } else if (ev === "data" && this.listenerCount("data") === 0) { + state[kState] &= ~kDataListening; + } + + return res; +}; +Readable.prototype.off = Readable.prototype.removeListener; + +Readable.prototype.removeAllListeners = function (ev) { + const res = Stream.prototype.removeAllListeners.$apply(this, arguments); + + if (ev === "readable" || ev === undefined) { + // We need to check if there is someone still listening to + // readable and reset the state. However this needs to happen + // after readable has been emitted but before I/O (nextTick) to + // support once('readable', fn) cycles. This means that calling + // resume within the same tick will have no + // effect. + process.nextTick(updateReadableListening, this); + } + + return res; +}; + +function updateReadableListening(self) { + const state = self._readableState; + + if (self.listenerCount("readable") > 0) { + state[kState] |= kReadableListening; + } else { + state[kState] &= ~kReadableListening; + } + + if ((state[kState] & (kHasPaused | kPaused | kResumeScheduled)) === (kHasPaused | kResumeScheduled)) { + // Flowing needs to be set to true now, otherwise + // the upcoming resume will not flow. + state[kState] |= kHasFlowing | kFlowing; + + // Crude way to check if we should resume. + } else if ((state[kState] & kDataListening) !== 0) { + self.resume(); + } else if ((state[kState] & kReadableListening) === 0) { + state[kState] &= ~(kHasFlowing | kFlowing); + } +} + +function nReadingNextTick(self) { + $debug("readable nexttick read 0"); + self.read(0); +} + +// pause() and resume() are remnants of the legacy readable stream API +// If the user uses them, then switch into old mode. +Readable.prototype.resume = function () { + const state = this._readableState; + if ((state[kState] & kFlowing) === 0) { + $debug("resume"); + // We flow only if there is no one listening + // for readable, but we still have to call + // resume(). + state[kState] |= kHasFlowing; + if ((state[kState] & kReadableListening) === 0) { + state[kState] |= kFlowing; + } else { + state[kState] &= ~kFlowing; + } + resume(this, state); + } + state[kState] |= kHasPaused; + state[kState] &= ~kPaused; + return this; +}; + +function resume(stream, state) { + if ((state[kState] & kResumeScheduled) === 0) { + state[kState] |= kResumeScheduled; + process.nextTick(resume_, stream, state); + } +} + +function resume_(stream, state) { + $debug("resume", (state[kState] & kReading) !== 0); + if ((state[kState] & kReading) === 0) { + stream.read(0); + } + + state[kState] &= ~kResumeScheduled; + stream.emit("resume"); + flow(stream); + if ((state[kState] & (kFlowing | kReading)) === kFlowing) stream.read(0); +} + +Readable.prototype.pause = function () { + const state = this._readableState; + $debug("call pause"); + if ((state[kState] & (kHasFlowing | kFlowing)) !== kHasFlowing) { + $debug("pause"); + state[kState] |= kHasFlowing; + state[kState] &= ~kFlowing; + this.emit("pause"); + } + state[kState] |= kHasPaused | kPaused; + return this; +}; + +function flow(stream) { + const state = stream._readableState; + $debug("flow"); + while ((state[kState] & kFlowing) !== 0 && stream.read() !== null); +} + +// Wrap an old-style stream as the async data source. +// This is *not* part of the readable stream interface. +// It is an ugly unfortunate mess of history. +Readable.prototype.wrap = function (stream) { + let paused = false; + + // TODO (ronag): Should this.destroy(err) emit + // 'error' on the wrapped stream? Would require + // a static factory method, e.g. Readable.wrap(stream). + + stream.on("data", chunk => { + if (!this.push(chunk) && stream.pause) { + paused = true; + stream.pause(); + } + }); + + stream.on("end", () => { + this.push(null); + }); + + stream.on("error", err => { + errorOrDestroy(this, err); + }); + + stream.on("close", () => { + this.destroy(); + }); + + stream.on("destroy", () => { + this.destroy(); + }); + + this._read = () => { + if (paused && stream.resume) { + paused = false; + stream.resume(); + } + }; + + // Proxy all the other methods. Important when wrapping filters and duplexes. + const streamKeys = ObjectKeys(stream); + for (let j = 1; j < streamKeys.length; j++) { + const i = streamKeys[j]; + if (this[i] === undefined && typeof stream[i] === "function") { + this[i] = stream[i].bind(stream); + } + } + + return this; +}; + +Readable.prototype[SymbolAsyncIterator] = function () { + return streamToAsyncIterator(this); +}; + +Readable.prototype.iterator = function (options) { + if (options !== undefined) { + validateObject(options, "options"); + } + return streamToAsyncIterator(this, options); +}; + +function streamToAsyncIterator(stream, options?) { + if (typeof stream.read !== "function") { + stream = Readable.wrap(stream, { objectMode: true }); + } + + const iter = createAsyncIterator(stream, options); + iter.stream = stream; + return iter; +} + +async function* createAsyncIterator(stream, options) { + let callback = nop; + + function next(resolve) { + if (this === stream) { + callback(); + callback = nop; + } else { + callback = resolve; + } + } + + stream.on("readable", next); + + let error; + const cleanup = eos(stream, { writable: false }, err => { + error = err ? aggregateTwoErrors(error, err) : null; + callback(); + callback = nop; + }); + + try { + while (true) { + const chunk = stream.destroyed ? null : stream.read(); + if (chunk !== null) { + yield chunk; + } else if (error) { + throw error; + } else if (error === null) { + return; + } else { + await new Promise(next); + } + } + } catch (err) { + error = aggregateTwoErrors(error, err); + throw error; + } finally { + if ((error || options?.destroyOnReturn !== false) && (error === undefined || stream._readableState.autoDestroy)) { + destroyImpl.destroyer(stream, null); + } else { + stream.off("readable", next); + cleanup(); + } + } +} + +// Making it explicit these properties are not enumerable +// because otherwise some prototype manipulation in +// userland will fail. +ObjectDefineProperties(Readable.prototype, { + readable: { + __proto__: null, + get() { + const r = this._readableState; + // r.readable === false means that this is part of a Duplex stream + // where the readable side was disabled upon construction. + // Compat. The user might manually disable readable side through + // deprecated setter. + return !!r && r.readable !== false && !r.destroyed && !r.errorEmitted && !r.endEmitted; + }, + set(val) { + // Backwards compat. + if (this._readableState) { + this._readableState.readable = !!val; + } + }, + }, + + readableDidRead: { + __proto__: null, + enumerable: false, + get: function () { + return this._readableState.dataEmitted; + }, + }, + + readableAborted: { + __proto__: null, + enumerable: false, + get: function () { + return !!( + this._readableState.readable !== false && + (this._readableState.destroyed || this._readableState.errored) && + !this._readableState.endEmitted + ); + }, + }, + + readableHighWaterMark: { + __proto__: null, + enumerable: false, + get: function () { + return this._readableState.highWaterMark; + }, + }, + + readableBuffer: { + __proto__: null, + enumerable: false, + get: function () { + return this._readableState?.buffer; + }, + }, + + readableFlowing: { + __proto__: null, + enumerable: false, + get: function () { + return this._readableState.flowing; + }, + set: function (state) { + if (this._readableState) { + this._readableState.flowing = state; + } + }, + }, + + readableLength: { + __proto__: null, + enumerable: false, + get() { + return this._readableState.length; + }, + }, + + readableObjectMode: { + __proto__: null, + enumerable: false, + get() { + return this._readableState ? this._readableState.objectMode : false; + }, + }, + + readableEncoding: { + __proto__: null, + enumerable: false, + get() { + return this._readableState ? this._readableState.encoding : null; + }, + }, + + errored: { + __proto__: null, + enumerable: false, + get() { + return this._readableState ? this._readableState.errored : null; + }, + }, + + closed: { + __proto__: null, + get() { + return this._readableState ? this._readableState.closed : false; + }, + }, + + destroyed: { + __proto__: null, + enumerable: false, + get() { + return this._readableState ? this._readableState.destroyed : false; + }, + set(value) { + // We ignore the value if the stream + // has not been initialized yet. + if (!this._readableState) { + return; + } + + // Backward compatibility, the user is explicitly + // managing destroyed. + this._readableState.destroyed = value; + }, + }, + + readableEnded: { + __proto__: null, + enumerable: false, + get() { + return this._readableState ? this._readableState.endEmitted : false; + }, + }, +}); + +ObjectDefineProperties(ReadableState.prototype, { + // Legacy getter for `pipesCount`. + pipesCount: { + __proto__: null, + get() { + return this.pipes.length; + }, + }, + + // Legacy property for `paused`. + paused: { + __proto__: null, + get() { + return (this[kState] & kPaused) !== 0; + }, + set(value) { + this[kState] |= kHasPaused; + if (value) { + this[kState] |= kPaused; + } else { + this[kState] &= ~kPaused; + } + }, + }, +}); + +// Exposed for testing purposes only. +Readable._fromList = fromList; + +// Pluck off n bytes from an array of buffers. +// Length is the combined lengths of all the buffers in the list. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function fromList(n, state) { + // nothing buffered. + if (state.length === 0) return null; + + let idx = state.bufferIndex; + let ret; + + const buf = state.buffer; + const len = buf.length; + + if ((state[kState] & kObjectMode) !== 0) { + ret = buf[idx]; + buf[idx++] = null; + } else if (!n || n >= state.length) { + // Read it all, truncate the list. + if ((state[kState] & kDecoder) !== 0) { + ret = ""; + while (idx < len) { + ret += buf[idx]; + buf[idx++] = null; + } + } else if (len - idx === 0) { + ret = Buffer.alloc(0); + } else if (len - idx === 1) { + ret = buf[idx]; + buf[idx++] = null; + } else { + ret = Buffer.allocUnsafe(state.length); + + let i = 0; + while (idx < len) { + TypedArrayPrototypeSet.$call(ret, buf[idx], i); + i += buf[idx].length; + buf[idx++] = null; + } + } + } else if (n < buf[idx].length) { + // `slice` is the same for buffers and strings. + ret = buf[idx].slice(0, n); + buf[idx] = buf[idx].slice(n); + } else if (n === buf[idx].length) { + // First chunk is a perfect match. + ret = buf[idx]; + buf[idx++] = null; + } else if ((state[kState] & kDecoder) !== 0) { + ret = ""; + while (idx < len) { + const str = buf[idx]; + if (n > str.length) { + ret += str; + n -= str.length; + buf[idx++] = null; + } else { + if (n === buf.length) { + ret += str; + buf[idx++] = null; + } else { + ret += str.slice(0, n); + buf[idx] = str.slice(n); + } + break; + } + } + } else { + ret = Buffer.allocUnsafe(n); + + const retLen = n; + while (idx < len) { + const data = buf[idx]; + if (n > data.length) { + TypedArrayPrototypeSet.$call(ret, data, retLen - n); + n -= data.length; + buf[idx++] = null; + } else { + if (n === data.length) { + TypedArrayPrototypeSet.$call(ret, data, retLen - n); + buf[idx++] = null; + } else { + TypedArrayPrototypeSet.$call(ret, new $Buffer(data.buffer, data.byteOffset, n), retLen - n); + buf[idx] = new $Buffer(data.buffer, data.byteOffset + n, data.length - n); + } + break; + } + } + } + + if (idx === len) { + state.buffer.length = 0; + state.bufferIndex = 0; + } else if (idx > 1024) { + state.buffer.splice(0, idx); + state.bufferIndex = 0; + } else { + state.bufferIndex = idx; + } + + return ret; +} + +function endReadable(stream) { + const state = stream._readableState; + + $debug("endReadable"); + if ((state[kState] & kEndEmitted) === 0) { + state[kState] |= kEnded; + process.nextTick(endReadableNT, state, stream); + } +} + +function endReadableNT(state, stream) { + $debug("endReadableNT"); + + // Check that we didn't get one last unshift. + if ((state[kState] & (kErrored | kCloseEmitted | kEndEmitted)) === 0 && state.length === 0) { + state[kState] |= kEndEmitted; + stream.emit("end"); + + if (stream.writable && stream.allowHalfOpen === false) { + process.nextTick(endWritableNT, stream); + } else if (state.autoDestroy) { + // In case of duplex streams we need a way to detect + // if the writable side is ready for autoDestroy as well. + const wState = stream._writableState; + const autoDestroy = + !wState || + (wState.autoDestroy && + // We don't expect the writable to ever 'finish' + // if writable is explicitly set to false. + (wState.finished || wState.writable === false)); + + if (autoDestroy) { + stream[kAutoDestroyed] = true; // workaround for node:http Server not using node:net Server + stream.destroy(); + } + } + } +} + +function endWritableNT(stream) { + const writable = stream.writable && !stream.writableEnded && !stream.destroyed; + if (writable) { + stream.end(); + } +} + +Readable.from = function (iterable, opts) { + return from(Readable, iterable, opts); +}; + +// Lazy to avoid circular references +let webStreamsAdapters; +function lazyWebStreams() { + if (webStreamsAdapters === undefined) webStreamsAdapters = require("internal/webstreams_adapters"); + return webStreamsAdapters; +} + +Readable.fromWeb = function (readableStream, options) { + return lazyWebStreams().newStreamReadableFromReadableStream(readableStream, options); +}; + +Readable.toWeb = function (streamReadable, options) { + return lazyWebStreams().newReadableStreamFromStreamReadable(streamReadable, options); +}; + +Readable.wrap = function (src, options) { + return new Readable({ + objectMode: src.readableObjectMode ?? src.objectMode ?? true, + ...options, + destroy(err, callback) { + destroyImpl.destroyer(src, err); + callback(err); + }, + }).wrap(src); +}; + +export default Readable; diff --git a/src/js/internal/streams/state.ts b/src/js/internal/streams/state.ts new file mode 100644 index 0000000000..2fb3379b5a --- /dev/null +++ b/src/js/internal/streams/state.ts @@ -0,0 +1,47 @@ +"use strict"; + +const { validateInteger } = require("internal/validators"); + +const NumberIsInteger = Number.isInteger; +const MathFloor = Math.floor; + +// TODO (fix): For some reason Windows CI fails with bigger hwm. +let defaultHighWaterMarkBytes = process.platform === "win32" ? 16 * 1024 : 64 * 1024; +let defaultHighWaterMarkObjectMode = 16; + +function highWaterMarkFrom(options, isDuplex, duplexKey) { + return options.highWaterMark != null ? options.highWaterMark : isDuplex ? options[duplexKey] : null; +} + +function getDefaultHighWaterMark(objectMode) { + return objectMode ? defaultHighWaterMarkObjectMode : defaultHighWaterMarkBytes; +} + +function setDefaultHighWaterMark(objectMode, value) { + validateInteger(value, "value", 0); + if (objectMode) { + defaultHighWaterMarkObjectMode = value; + } else { + defaultHighWaterMarkBytes = value; + } +} + +function getHighWaterMark(state, options, duplexKey, isDuplex) { + const hwm = highWaterMarkFrom(options, isDuplex, duplexKey); + if (hwm != null) { + if (!NumberIsInteger(hwm) || hwm < 0) { + const name = isDuplex ? `options.${duplexKey}` : "options.highWaterMark"; + throw $ERR_INVALID_ARG_VALUE(name, hwm); + } + return MathFloor(hwm); + } + + // Default value + return getDefaultHighWaterMark(state.objectMode); +} + +export default { + getHighWaterMark, + getDefaultHighWaterMark, + setDefaultHighWaterMark, +}; diff --git a/src/js/internal/streams/transform.ts b/src/js/internal/streams/transform.ts new file mode 100644 index 0000000000..620b523e76 --- /dev/null +++ b/src/js/internal/streams/transform.ts @@ -0,0 +1,172 @@ +// a transform stream is a readable/writable stream where you do +// something with the data. Sometimes it's called a "filter", +// but that's not a great name for it, since that implies a thing where +// some bits pass through, and others are simply ignored. (That would +// be a valid example of a transform, of course.) +// +// While the output is causally related to the input, it's not a +// necessarily symmetric or synchronous transformation. For example, +// a zlib stream might take multiple plain-text writes(), and then +// emit a single compressed chunk some time in the future. +// +// Here's how this works: +// +// The Transform stream has all the aspects of the readable and writable +// stream classes. When you write(chunk), that calls _write(chunk,cb) +// internally, and returns false if there's a lot of pending writes +// buffered up. When you call read(), that calls _read(n) until +// there's enough pending readable data buffered up. +// +// In a transform stream, the written data is placed in a buffer. When +// _read(n) is called, it transforms the queued up data, calling the +// buffered _write cb's as it consumes chunks. If consuming a single +// written chunk would result in multiple output chunks, then the first +// outputted bit calls the readcb, and subsequent chunks just go into +// the read buffer, and will cause it to emit 'readable' if necessary. +// +// This way, back-pressure is actually determined by the reading side, +// since _read has to be called to start processing a new chunk. However, +// a pathological inflate type of transform can cause excessive buffering +// here. For example, imagine a stream where every byte of input is +// interpreted as an integer from 0-255, and then results in that many +// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in +// 1kb of data being output. In this case, you could write a very small +// amount of input, and end up with a very large amount of output. In +// such a pathological inflating mechanism, there'd be no way to tell +// the system to stop doing the transform. A single 4MB write could +// cause the system to run out of memory. +// +// However, even in such a pathological case, only a single written chunk +// would be consumed, and then the rest would wait (un-transformed) until +// the results of the previous transformed chunk were consumed. + +"use strict"; + +const Duplex = require("internal/streams/duplex"); +const { getHighWaterMark } = require("internal/streams/state"); + +const kCallback = Symbol("kCallback"); + +function Transform(options) { + if (!(this instanceof Transform)) return Reflect.construct(Transform, [options]); + + // TODO (ronag): This should preferably always be + // applied but would be semver-major. Or even better; + // make Transform a Readable with the Writable interface. + const readableHighWaterMark = options ? getHighWaterMark(this, options, "readableHighWaterMark", true) : null; + if (readableHighWaterMark === 0) { + // A Duplex will buffer both on the writable and readable side while + // a Transform just wants to buffer hwm number of elements. To avoid + // buffering twice we disable buffering on the writable side. + options = { + ...options, + highWaterMark: null, + readableHighWaterMark, + writableHighWaterMark: options.writableHighWaterMark || 0, + }; + } + + Duplex.$call(this, options); + + // We have implemented the _read method, and done the other things + // that Readable wants before the first _read call, so unset the + // sync guard flag. + this._readableState.sync = false; + + this[kCallback] = null; + + if (options) { + if (typeof options.transform === "function") this._transform = options.transform; + + if (typeof options.flush === "function") this._flush = options.flush; + } + + // When the writable side finishes, then flush out anything remaining. + // Backwards compat. Some Transform streams incorrectly implement _final + // instead of or in addition to _flush. By using 'prefinish' instead of + // implementing _final we continue supporting this unfortunate use case. + this.on("prefinish", prefinish); +} +$toClass(Transform, "Transform", Duplex); + +function final(cb?) { + if (typeof this._flush === "function" && !this.destroyed) { + this._flush((er, data) => { + if (er) { + if (cb) { + cb(er); + } else { + this.destroy(er); + } + return; + } + + if (data != null) { + this.push(data); + } + this.push(null); + if (cb) { + cb(); + } + }); + } else { + this.push(null); + if (cb) { + cb(); + } + } +} + +function prefinish() { + if (this._final !== final) { + final.$call(this); + } +} + +Transform.prototype._final = final; + +Transform.prototype._transform = function (chunk, encoding, callback) { + throw $ERR_METHOD_NOT_IMPLEMENTED("_transform()"); +}; + +Transform.prototype._write = function (chunk, encoding, callback) { + const rState = this._readableState; + const wState = this._writableState; + const length = rState.length; + + this._transform(chunk, encoding, (err, val) => { + if (err) { + callback(err); + return; + } + + if (val != null) { + this.push(val); + } + + if (rState.ended) { + // If user has called this.push(null) we have to + // delay the callback to properly propagate the new + // state. + process.nextTick(callback); + } else if ( + wState.ended || // Backwards compat. + length === rState.length || // Backwards compat. + rState.length < rState.highWaterMark + ) { + callback(); + } else { + this[kCallback] = callback; + } + }); +}; + +Transform.prototype._read = function () { + if (this[kCallback]) { + const callback = this[kCallback]; + this[kCallback] = null; + callback(); + } +}; + +export default Transform; diff --git a/src/js/internal/streams/utils.ts b/src/js/internal/streams/utils.ts new file mode 100644 index 0000000000..12f0e9ff3d --- /dev/null +++ b/src/js/internal/streams/utils.ts @@ -0,0 +1,321 @@ +"use strict"; + +const SymbolFor = Symbol.for; +const SymbolIterator = Symbol.iterator; +const SymbolAsyncIterator = Symbol.asyncIterator; + +// We need to use SymbolFor to make these globally available +// for interoperability with readable-stream, i.e. readable-stream +// and node core needs to be able to read/write private state +// from each other for proper interoperability. +const kIsDestroyed = SymbolFor("nodejs.stream.destroyed"); +const kIsErrored = SymbolFor("nodejs.stream.errored"); +const kIsReadable = SymbolFor("nodejs.stream.readable"); +const kIsWritable = SymbolFor("nodejs.stream.writable"); +const kIsDisturbed = SymbolFor("nodejs.stream.disturbed"); + +const kOnConstructed = Symbol("kOnConstructed"); + +const kIsClosedPromise = SymbolFor("nodejs.webstream.isClosedPromise"); +const kControllerErrorFunction = SymbolFor("nodejs.webstream.controllerErrorFunction"); + +const kState = Symbol("kState"); +const kObjectMode = 1 << 0; +const kErrorEmitted = 1 << 1; +const kAutoDestroy = 1 << 2; +const kEmitClose = 1 << 3; +const kDestroyed = 1 << 4; +const kClosed = 1 << 5; +const kCloseEmitted = 1 << 6; +const kErrored = 1 << 7; +const kConstructed = 1 << 8; + +function isReadableNodeStream(obj, strict = false) { + return !!( + ( + obj && + typeof obj.pipe === "function" && + typeof obj.on === "function" && + (!strict || (typeof obj.pause === "function" && typeof obj.resume === "function")) && + (!obj._writableState || obj._readableState?.readable !== false) && // Duplex + (!obj._writableState || obj._readableState) + ) // Writable has .pipe. + ); +} + +function isWritableNodeStream(obj) { + return !!( + ( + obj && + typeof obj.write === "function" && + typeof obj.on === "function" && + (!obj._readableState || obj._writableState?.writable !== false) + ) // Duplex + ); +} + +function isDuplexNodeStream(obj) { + return !!( + obj && + typeof obj.pipe === "function" && + obj._readableState && + typeof obj.on === "function" && + typeof obj.write === "function" + ); +} + +function isNodeStream(obj) { + return ( + obj && + (obj._readableState || + obj._writableState || + (typeof obj.write === "function" && typeof obj.on === "function") || + (typeof obj.pipe === "function" && typeof obj.on === "function")) + ); +} + +function isReadableStream(obj) { + return $inheritsReadableStream(obj); +} + +function isWritableStream(obj) { + return $inheritsWritableStream(obj); +} + +function isTransformStream(obj) { + return $inheritsTransformStream(obj); +} + +function isWebStream(obj) { + return isReadableStream(obj) || isWritableStream(obj) || isTransformStream(obj); +} + +function isIterable(obj, isAsync) { + if (obj == null) return false; + if (isAsync === true) return typeof obj[SymbolAsyncIterator] === "function"; + if (isAsync === false) return typeof obj[SymbolIterator] === "function"; + return typeof obj[SymbolAsyncIterator] === "function" || typeof obj[SymbolIterator] === "function"; +} + +function isDestroyed(stream) { + if (!isNodeStream(stream)) return null; + const wState = stream._writableState; + const rState = stream._readableState; + const state = wState || rState; + return !!(stream.destroyed || stream[kIsDestroyed] || state?.destroyed); +} + +// Have been end():d. +function isWritableEnded(stream) { + if (!isWritableNodeStream(stream)) return null; + if (stream.writableEnded === true) return true; + const wState = stream._writableState; + if (wState?.errored) return false; + if (typeof wState?.ended !== "boolean") return null; + return wState.ended; +} + +// Have emitted 'finish'. +function isWritableFinished(stream, strict) { + if (!isWritableNodeStream(stream)) return null; + if (stream.writableFinished === true) return true; + const wState = stream._writableState; + if (wState?.errored) return false; + if (typeof wState?.finished !== "boolean") return null; + return !!(wState.finished || (strict === false && wState.ended === true && wState.length === 0)); +} + +// Have been push(null):d. +function isReadableEnded(stream) { + if (!isReadableNodeStream(stream)) return null; + if (stream.readableEnded === true) return true; + const rState = stream._readableState; + if (!rState || rState.errored) return false; + if (typeof rState?.ended !== "boolean") return null; + return rState.ended; +} + +// Have emitted 'end'. +function isReadableFinished(stream, strict?) { + if (!isReadableNodeStream(stream)) return null; + const rState = stream._readableState; + if (rState?.errored) return false; + if (typeof rState?.endEmitted !== "boolean") return null; + return !!(rState.endEmitted || (strict === false && rState.ended === true && rState.length === 0)); +} + +function isReadable(stream) { + if (stream && stream[kIsReadable] != null) return stream[kIsReadable]; + if (typeof stream?.readable !== "boolean") return null; + if (isDestroyed(stream)) return false; + return isReadableNodeStream(stream) && stream.readable && !isReadableFinished(stream); +} + +function isWritable(stream) { + if (stream && stream[kIsWritable] != null) return stream[kIsWritable]; + if (typeof stream?.writable !== "boolean") return null; + if (isDestroyed(stream)) return false; + return isWritableNodeStream(stream) && stream.writable && !isWritableEnded(stream); +} + +function isFinished(stream, opts) { + if (!isNodeStream(stream)) { + return null; + } + + if (isDestroyed(stream)) { + return true; + } + + if (opts?.readable !== false && isReadable(stream)) { + return false; + } + + if (opts?.writable !== false && isWritable(stream)) { + return false; + } + + return true; +} + +function isWritableErrored(stream) { + if (!isNodeStream(stream)) { + return null; + } + + if (stream.writableErrored) { + return stream.writableErrored; + } + + return stream._writableState?.errored ?? null; +} + +function isReadableErrored(stream) { + if (!isNodeStream(stream)) { + return null; + } + + if (stream.readableErrored) { + return stream.readableErrored; + } + + return stream._readableState?.errored ?? null; +} + +function isClosed(stream) { + if (!isNodeStream(stream)) { + return null; + } + + if (typeof stream.closed === "boolean") { + return stream.closed; + } + + const wState = stream._writableState; + const rState = stream._readableState; + + if (typeof wState?.closed === "boolean" || typeof rState?.closed === "boolean") { + return wState?.closed || rState?.closed; + } + + if (typeof stream._closed === "boolean" && isOutgoingMessage(stream)) { + return stream._closed; + } + + return null; +} + +function isOutgoingMessage(stream) { + return ( + typeof stream._closed === "boolean" && + typeof stream._defaultKeepAlive === "boolean" && + typeof stream._removedConnection === "boolean" && + typeof stream._removedContLen === "boolean" + ); +} + +function isServerResponse(stream) { + return typeof stream._sent100 === "boolean" && isOutgoingMessage(stream); +} + +function isServerRequest(stream) { + return ( + typeof stream._consuming === "boolean" && + typeof stream._dumped === "boolean" && + stream.req?.upgradeOrConnect === undefined + ); +} + +function willEmitClose(stream) { + if (!isNodeStream(stream)) return null; + + const wState = stream._writableState; + const rState = stream._readableState; + const state = wState || rState; + + return (!state && isServerResponse(stream)) || !!(state?.autoDestroy && state.emitClose && state.closed === false); +} + +function isDisturbed(stream) { + return !!(stream && (stream[kIsDisturbed] ?? (stream.readableDidRead || stream.readableAborted))); +} + +function isErrored(stream) { + return !!( + stream && + (stream[kIsErrored] ?? + stream.readableErrored ?? + stream.writableErrored ?? + stream._readableState?.errorEmitted ?? + stream._writableState?.errorEmitted ?? + stream._readableState?.errored ?? + stream._writableState?.errored) + ); +} + +export default { + kOnConstructed, + isDestroyed, + kIsDestroyed, + isDisturbed, + kIsDisturbed, + isErrored, + kIsErrored, + isReadable, + kIsReadable, + kIsClosedPromise, + kControllerErrorFunction, + kIsWritable, + isClosed, + isDuplexNodeStream, + isFinished, + isIterable, + isReadableNodeStream, + isReadableStream, + isReadableEnded, + isReadableFinished, + isReadableErrored, + isNodeStream, + isWebStream, + isWritable, + isWritableNodeStream, + isWritableStream, + isWritableEnded, + isWritableFinished, + isWritableErrored, + isServerRequest, + isServerResponse, + willEmitClose, + isTransformStream, + kState, + // bitfields + kObjectMode, + kErrorEmitted, + kAutoDestroy, + kEmitClose, + kDestroyed, + kClosed, + kCloseEmitted, + kErrored, + kConstructed, +}; diff --git a/src/js/internal/streams/writable.ts b/src/js/internal/streams/writable.ts new file mode 100644 index 0000000000..3d86b0771b --- /dev/null +++ b/src/js/internal/streams/writable.ts @@ -0,0 +1,1123 @@ +// A bit simpler than readable streams. +// Implement an async ._write(chunk, encoding, cb), and it'll handle all +// the drain event emission and buffering. + +"use strict"; + +const EE = require("node:events"); +const { Stream } = require("internal/streams/legacy"); +const { Buffer } = require("node:buffer"); +const destroyImpl = require("internal/streams/destroy"); +const eos = require("internal/streams/end-of-stream"); +const { addAbortSignal } = require("internal/streams/add-abort-signal"); +const { getHighWaterMark, getDefaultHighWaterMark } = require("internal/streams/state"); +const { + kOnConstructed, + kState, + kObjectMode, + kErrorEmitted, + kAutoDestroy, + kEmitClose, + kDestroyed, + kClosed, + kCloseEmitted, + kErrored, + kConstructed, +}: { + readonly kState: unique symbol; + readonly kOnConstructed: unique symbol; + kObjectMode: number; + kErrorEmitted: number; + kAutoDestroy: number; + kEmitClose: number; + kDestroyed: number; + kClosed: number; + kCloseEmitted: number; + kErrored: number; + kConstructed: number; +} = require("internal/streams/utils"); + +const ObjectDefineProperties = Object.defineProperties; +const ArrayPrototypeSlice = Array.prototype.slice; +const ObjectDefineProperty = Object.defineProperty; +const SymbolHasInstance = Symbol.hasInstance; +const FunctionPrototypeSymbolHasInstance = Function.prototype[Symbol.hasInstance]; +const StringPrototypeToLowerCase = String.prototype.toLowerCase; +const SymbolAsyncDispose = Symbol.asyncDispose; + +const { errorOrDestroy } = destroyImpl; + +function nop() {} + +const kOnFinishedValue = Symbol("kOnFinishedValue"); +const kErroredValue = Symbol("kErroredValue"); +const kDefaultEncodingValue = Symbol("kDefaultEncodingValue"); +const kWriteCbValue = Symbol("kWriteCbValue"); +const kAfterWriteTickInfoValue = Symbol("kAfterWriteTickInfoValue"); +const kBufferedValue = Symbol("kBufferedValue"); + +const kSync = 1 << 9; +const kFinalCalled = 1 << 10; +const kNeedDrain = 1 << 11; +const kEnding = 1 << 12; +const kFinished = 1 << 13; +const kDecodeStrings = 1 << 14; +const kWriting = 1 << 15; +const kBufferProcessing = 1 << 16; +const kPrefinished = 1 << 17; +const kAllBuffers = 1 << 18; +const kAllNoop = 1 << 19; +const kOnFinished = 1 << 20; +const kHasWritable = 1 << 21; +const kWritable = 1 << 22; +const kCorked = 1 << 23; +const kDefaultUTF8Encoding = 1 << 24; +const kWriteCb = 1 << 25; +const kExpectWriteCb = 1 << 26; +const kAfterWriteTickInfo = 1 << 27; +const kAfterWritePending = 1 << 28; +const kBuffered = 1 << 29; +const kEnded = 1 << 30; + +// TODO(benjamingr) it is likely slower to do it this way than with free functions +function makeBitMapDescriptor(bit) { + return { + enumerable: false, + get() { + return (this[kState] & bit) !== 0; + }, + set(value) { + if (value) this[kState] |= bit; + else this[kState] &= ~bit; + }, + }; +} +WritableState.prototype = {}; +ObjectDefineProperties(WritableState.prototype, { + // Object stream flag to indicate whether or not this stream + // contains buffers or objects. + objectMode: makeBitMapDescriptor(kObjectMode), + + // if _final has been called. + finalCalled: makeBitMapDescriptor(kFinalCalled), + + // drain event flag. + needDrain: makeBitMapDescriptor(kNeedDrain), + + // At the start of calling end() + ending: makeBitMapDescriptor(kEnding), + + // When end() has been called, and returned. + ended: makeBitMapDescriptor(kEnded), + + // When 'finish' is emitted. + finished: makeBitMapDescriptor(kFinished), + + // Has it been destroyed. + destroyed: makeBitMapDescriptor(kDestroyed), + + // Should we decode strings into buffers before passing to _write? + // this is here so that some node-core streams can optimize string + // handling at a lower level. + decodeStrings: makeBitMapDescriptor(kDecodeStrings), + + // A flag to see when we're in the middle of a write. + writing: makeBitMapDescriptor(kWriting), + + // A flag to be able to tell if the onwrite cb is called immediately, + // or on a later tick. We set this to true at first, because any + // actions that shouldn't happen until "later" should generally also + // not happen before the first write call. + sync: makeBitMapDescriptor(kSync), + + // A flag to know if we're processing previously buffered items, which + // may call the _write() callback in the same tick, so that we don't + // end up in an overlapped onwrite situation. + bufferProcessing: makeBitMapDescriptor(kBufferProcessing), + + // Stream is still being constructed and cannot be + // destroyed until construction finished or failed. + // Async construction is opt in, therefore we start as + // constructed. + constructed: makeBitMapDescriptor(kConstructed), + + // Emit prefinish if the only thing we're waiting for is _write cbs + // This is relevant for synchronous Transform streams. + prefinished: makeBitMapDescriptor(kPrefinished), + + // True if the error was already emitted and should not be thrown again. + errorEmitted: makeBitMapDescriptor(kErrorEmitted), + + // Should close be emitted on destroy. Defaults to true. + emitClose: makeBitMapDescriptor(kEmitClose), + + // Should .destroy() be called after 'finish' (and potentially 'end'). + autoDestroy: makeBitMapDescriptor(kAutoDestroy), + + // Indicates whether the stream has finished destroying. + closed: makeBitMapDescriptor(kClosed), + + // True if close has been emitted or would have been emitted + // depending on emitClose. + closeEmitted: makeBitMapDescriptor(kCloseEmitted), + + allBuffers: makeBitMapDescriptor(kAllBuffers), + allNoop: makeBitMapDescriptor(kAllNoop), + + // Indicates whether the stream has errored. When true all write() calls + // should return false. This is needed since when autoDestroy + // is disabled we need a way to tell whether the stream has failed. + // This is/should be a cold path. + errored: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kErrored) !== 0 ? this[kErroredValue] : null; + }, + set(value) { + if (value) { + this[kErroredValue] = value; + this[kState] |= kErrored; + } else { + this[kState] &= ~kErrored; + } + }, + }, + + writable: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kHasWritable) !== 0 ? (this[kState] & kWritable) !== 0 : undefined; + }, + set(value) { + if (value == null) { + this[kState] &= ~(kHasWritable | kWritable); + } else if (value) { + this[kState] |= kHasWritable | kWritable; + } else { + this[kState] |= kHasWritable; + this[kState] &= ~kWritable; + } + }, + }, + + defaultEncoding: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kDefaultUTF8Encoding) !== 0 ? "utf8" : this[kDefaultEncodingValue]; + }, + set(value) { + if (value === "utf8" || value === "utf-8") { + this[kState] |= kDefaultUTF8Encoding; + } else { + this[kState] &= ~kDefaultUTF8Encoding; + this[kDefaultEncodingValue] = value; + } + }, + }, + + // The callback that the user supplies to write(chunk, encoding, cb). + writecb: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kWriteCb) !== 0 ? this[kWriteCbValue] : nop; + }, + set(value) { + this[kWriteCbValue] = value; + if (value) { + this[kState] |= kWriteCb; + } else { + this[kState] &= ~kWriteCb; + } + }, + }, + + // Storage for data passed to the afterWrite() callback in case of + // synchronous _write() completion. + afterWriteTickInfo: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kAfterWriteTickInfo) !== 0 ? this[kAfterWriteTickInfoValue] : null; + }, + set(value) { + this[kAfterWriteTickInfoValue] = value; + if (value) { + this[kState] |= kAfterWriteTickInfo; + } else { + this[kState] &= ~kAfterWriteTickInfo; + } + }, + }, + + buffered: { + __proto__: null, + enumerable: false, + get() { + return (this[kState] & kBuffered) !== 0 ? this[kBufferedValue] : []; + }, + set(value) { + this[kBufferedValue] = value; + if (value) { + this[kState] |= kBuffered; + } else { + this[kState] &= ~kBuffered; + } + }, + }, +}); + +function WritableState(options, stream, isDuplex) { + // Bit map field to store WritableState more efficiently with 1 bit per field + // instead of a V8 slot per field. + this[kState] = kSync | kConstructed | kEmitClose | kAutoDestroy; + + if (options?.objectMode) this[kState] |= kObjectMode; + + if (isDuplex && options?.writableObjectMode) this[kState] |= kObjectMode; + + // The point at which write() starts returning false + // Note: 0 is a valid value, means that we always return false if + // the entire buffer is not flushed immediately on write(). + this.highWaterMark = options + ? getHighWaterMark(this, options, "writableHighWaterMark", isDuplex) + : getDefaultHighWaterMark(false); + + if (!options || options.decodeStrings !== false) this[kState] |= kDecodeStrings; + + // Should close be emitted on destroy. Defaults to true. + if (options && options.emitClose === false) this[kState] &= ~kEmitClose; + + // Should .destroy() be called after 'end' (and potentially 'finish'). + if (options && options.autoDestroy === false) this[kState] &= ~kAutoDestroy; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + const defaultEncoding = options ? options.defaultEncoding : null; + if (defaultEncoding == null || defaultEncoding === "utf8" || defaultEncoding === "utf-8") { + this[kState] |= kDefaultUTF8Encoding; + } else if (Buffer.isEncoding(defaultEncoding)) { + this[kState] &= ~kDefaultUTF8Encoding; + this[kDefaultEncodingValue] = defaultEncoding; + } else { + throw $ERR_UNKNOWN_ENCODING(defaultEncoding); + } + + // Not an actual buffer we keep track of, but a measurement + // of how much we're waiting to get pushed to some underlying + // socket or file. + this.length = 0; + + // When true all writes will be buffered until .uncork() call. + this.corked = 0; + + // The callback that's passed to _write(chunk, cb). + this.onwrite = onwrite.bind(undefined, stream); + + // The amount that is being written when _write is called. + this.writelen = 0; + + resetBuffer(this); + + // Number of pending user-supplied write callbacks + // this must be 0 before 'finish' can be emitted. + this.pendingcb = 0; +} + +function resetBuffer(state) { + state[kBufferedValue] = null; + state.bufferedIndex = 0; + state[kState] |= kAllBuffers | kAllNoop; + state[kState] &= ~kBuffered; +} + +WritableState.prototype.getBuffer = function getBuffer() { + return (this[kState] & kBuffered) === 0 ? [] : ArrayPrototypeSlice.$call(this.buffered, this.bufferedIndex); +}; + +ObjectDefineProperty(WritableState.prototype, "bufferedRequestCount", { + __proto__: null, + get() { + return (this[kState] & kBuffered) === 0 ? 0 : this[kBufferedValue].length - this.bufferedIndex; + }, +}); + +WritableState.prototype[kOnConstructed] = function onConstructed(stream) { + if ((this[kState] & kWriting) === 0) { + clearBuffer(stream, this); + } + + if ((this[kState] & kEnding) !== 0) { + finishMaybe(stream, this); + } +}; + +function Writable(options) { + if (!(this instanceof Writable)) return Reflect.construct(Writable, [options]); + + this._events ??= { + close: undefined, + error: undefined, + prefinish: undefined, + finish: undefined, + drain: undefined, + // Skip uncommon events... + // [destroyImpl.kConstruct]: undefined, + // [destroyImpl.kDestroy]: undefined, + }; + + this._writableState = new WritableState(options, this, false); + + if (options) { + if (typeof options.write === "function") this._write = options.write; + + if (typeof options.writev === "function") this._writev = options.writev; + + if (typeof options.destroy === "function") this._destroy = options.destroy; + + if (typeof options.final === "function") this._final = options.final; + + if (typeof options.construct === "function") this._construct = options.construct; + + if (options.signal) addAbortSignal(options.signal, this); + } + + Stream.$call(this, options); + + if (this._construct != null) { + destroyImpl.construct(this, () => { + this._writableState[kOnConstructed](this); + }); + } +} +$toClass(Writable, "Writable", Stream); + +Writable.WritableState = WritableState; + +ObjectDefineProperty(Writable, SymbolHasInstance, { + __proto__: null, + value: function (object) { + if (FunctionPrototypeSymbolHasInstance.$call(this, object)) return true; + if (this !== Writable) return false; + + return object && object._writableState instanceof WritableState; + }, +}); + +// Otherwise people can pipe Writable streams, which is just wrong. +Writable.prototype.pipe = function () { + errorOrDestroy(this, $ERR_STREAM_CANNOT_PIPE()); +}; + +function _write(stream, chunk, encoding, cb?) { + const state = stream._writableState; + + if (cb == null || typeof cb !== "function") { + cb = nop; + } + + if (chunk === null) { + throw $ERR_STREAM_NULL_VALUES(); + } + + if ((state[kState] & kObjectMode) === 0) { + if (!encoding) { + encoding = (state[kState] & kDefaultUTF8Encoding) !== 0 ? "utf8" : state.defaultEncoding; + } else if (encoding !== "buffer" && !Buffer.isEncoding(encoding)) { + throw $ERR_UNKNOWN_ENCODING(encoding); + } + + if (typeof chunk === "string") { + if ((state[kState] & kDecodeStrings) !== 0) { + chunk = Buffer.from(chunk, encoding); + encoding = "buffer"; + } + } else if (chunk instanceof Buffer) { + encoding = "buffer"; + } else if (Stream._isArrayBufferView(chunk)) { + chunk = Stream._uint8ArrayToBuffer(chunk); + encoding = "buffer"; + } else { + throw $ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "TypedArray", "DataView"], chunk); + } + } + + let err; + if ((state[kState] & kEnding) !== 0) { + err = $ERR_STREAM_WRITE_AFTER_END(); + } else if ((state[kState] & kDestroyed) !== 0) { + err = $ERR_STREAM_DESTROYED("write"); + } + + if (err) { + process.nextTick(cb, err); + errorOrDestroy(stream, err, true); + return err; + } + + state.pendingcb++; + return writeOrBuffer(stream, state, chunk, encoding, cb); +} + +Writable.prototype.write = function (chunk, encoding, cb) { + if (encoding != null && typeof encoding === "function") { + cb = encoding; + encoding = null; + } + + return _write(this, chunk, encoding, cb) === true; +}; + +Writable.prototype.cork = function () { + const state = this._writableState; + + state[kState] |= kCorked; + state.corked++; +}; + +Writable.prototype.uncork = function () { + const state = this._writableState; + + if (state.corked) { + state.corked--; + + if (!state.corked) { + state[kState] &= ~kCorked; + } + + if ((state[kState] & kWriting) === 0) clearBuffer(this, state); + } +}; + +Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { + // node::ParseEncoding() requires lower case. + if (typeof encoding === "string") encoding = StringPrototypeToLowerCase.$call(encoding); + if (!Buffer.isEncoding(encoding)) throw $ERR_UNKNOWN_ENCODING(encoding); + this._writableState.defaultEncoding = encoding; + return this; +}; + +// If we're already writing something, then just put this +// in the queue, and wait our turn. Otherwise, call _write +// If we return false, then we need a drain event, so set that flag. +function writeOrBuffer(stream, state, chunk, encoding, callback) { + const len = (state[kState] & kObjectMode) !== 0 ? 1 : chunk.length; + + state.length += len; + + if ((state[kState] & (kWriting | kErrored | kCorked | kConstructed)) !== kConstructed) { + if ((state[kState] & kBuffered) === 0) { + state[kState] |= kBuffered; + state[kBufferedValue] = []; + } + + state[kBufferedValue].push({ chunk, encoding, callback }); + if ((state[kState] & kAllBuffers) !== 0 && encoding !== "buffer") { + state[kState] &= ~kAllBuffers; + } + if ((state[kState] & kAllNoop) !== 0 && callback !== nop) { + state[kState] &= ~kAllNoop; + } + } else { + state.writelen = len; + if (callback !== nop) { + state.writecb = callback; + } + state[kState] |= kWriting | kSync | kExpectWriteCb; + stream._write(chunk, encoding, state.onwrite); + state[kState] &= ~kSync; + } + + const ret = state.length < state.highWaterMark || state.length === 0; + + if (!ret) { + state[kState] |= kNeedDrain; + } + + // Return false if errored or destroyed in order to break + // any synchronous while(stream.write(data)) loops. + return ret && (state[kState] & (kDestroyed | kErrored)) === 0; +} + +function doWrite(stream, state, writev, len, chunk, encoding, cb) { + state.writelen = len; + if (cb !== nop) { + state.writecb = cb; + } + state[kState] |= kWriting | kSync | kExpectWriteCb; + if ((state[kState] & kDestroyed) !== 0) state.onwrite($ERR_STREAM_DESTROYED("write")); + else if (writev) stream._writev(chunk, state.onwrite); + else stream._write(chunk, encoding, state.onwrite); + state[kState] &= ~kSync; +} + +function onwriteError(stream, state, er, cb) { + --state.pendingcb; + + cb(er); + // Ensure callbacks are invoked even when autoDestroy is + // not enabled. Passing `er` here doesn't make sense since + // it's related to one specific write, not to the buffered + // writes. + errorBuffer(state); + // This can emit error, but error must always follow cb. + errorOrDestroy(stream, er); +} + +function onwrite(stream, er) { + const state = stream._writableState; + + if ((state[kState] & kExpectWriteCb) === 0) { + errorOrDestroy(stream, $ERR_MULTIPLE_CALLBACK()); + return; + } + + const sync = (state[kState] & kSync) !== 0; + const cb = (state[kState] & kWriteCb) !== 0 ? state[kWriteCbValue] : nop; + + state.writecb = null; + state[kState] &= ~(kWriting | kExpectWriteCb); + state.length -= state.writelen; + state.writelen = 0; + + if (er) { + // Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364 + er.stack; // eslint-disable-line no-unused-expressions + + if ((state[kState] & kErrored) === 0) { + state[kErroredValue] = er; + state[kState] |= kErrored; + } + + // In case of duplex streams we need to notify the readable side of the + // error. + if (stream._readableState && !stream._readableState.errored) { + stream._readableState.errored = er; + } + + if (sync) { + process.nextTick(onwriteError, stream, state, er, cb); + } else { + onwriteError(stream, state, er, cb); + } + } else { + if ((state[kState] & kBuffered) !== 0) { + clearBuffer(stream, state); + } + + if (sync) { + const needDrain = (state[kState] & kNeedDrain) !== 0 && state.length === 0; + const needTick = needDrain || state[kState] & Number(kDestroyed !== 0) || cb !== nop; + + // It is a common case that the callback passed to .write() is always + // the same. In that case, we do not schedule a new nextTick(), but + // rather just increase a counter, to improve performance and avoid + // memory allocations. + if (cb === nop) { + if ((state[kState] & kAfterWritePending) === 0 && needTick) { + process.nextTick(afterWrite, stream, state, 1, cb); + state[kState] |= kAfterWritePending; + } else { + state.pendingcb--; + if ((state[kState] & kEnding) !== 0) { + finishMaybe(stream, state, true); + } + } + } else if ((state[kState] & kAfterWriteTickInfo) !== 0 && state[kAfterWriteTickInfoValue].cb === cb) { + state[kAfterWriteTickInfoValue].count++; + } else if (needTick) { + state[kAfterWriteTickInfoValue] = { count: 1, cb, stream, state }; + process.nextTick(afterWriteTick, state[kAfterWriteTickInfoValue]); + state[kState] |= kAfterWritePending | kAfterWriteTickInfo; + } else { + state.pendingcb--; + if ((state[kState] & kEnding) !== 0) { + finishMaybe(stream, state, true); + } + } + } else { + afterWrite(stream, state, 1, cb); + } + } +} + +function afterWriteTick({ stream, state, count, cb }) { + state[kState] &= ~kAfterWriteTickInfo; + state[kAfterWriteTickInfoValue] = null; + return afterWrite(stream, state, count, cb); +} + +function afterWrite(stream, state, count, cb) { + state[kState] &= ~kAfterWritePending; + + const needDrain = (state[kState] & (kEnding | kNeedDrain | kDestroyed)) === kNeedDrain && state.length === 0; + if (needDrain) { + state[kState] &= ~kNeedDrain; + stream.emit("drain"); + } + + while (count-- > 0) { + state.pendingcb--; + cb(null); + } + + if ((state[kState] & kDestroyed) !== 0) { + errorBuffer(state); + } + + if ((state[kState] & kEnding) !== 0) { + finishMaybe(stream, state, true); + } +} + +// If there's something in the buffer waiting, then invoke callbacks. +function errorBuffer(state) { + if ((state[kState] & kWriting) !== 0) { + return; + } + + if ((state[kState] & kBuffered) !== 0) { + for (let n = state.bufferedIndex; n < state.buffered.length; ++n) { + const { chunk, callback } = state[kBufferedValue][n]; + const len = (state[kState] & kObjectMode) !== 0 ? 1 : chunk.length; + state.length -= len; + callback(state.errored ?? $ERR_STREAM_DESTROYED("write")); + } + } + + callFinishedCallbacks(state, state.errored ?? $ERR_STREAM_DESTROYED("end")); + + resetBuffer(state); +} + +// If there's something in the buffer waiting, then process it. +function clearBuffer(stream, state) { + if ( + (state[kState] & (kDestroyed | kBufferProcessing | kCorked | kBuffered | kConstructed)) !== + (kBuffered | kConstructed) + ) { + return; + } + + const objectMode = (state[kState] & kObjectMode) !== 0; + const { [kBufferedValue]: buffered, bufferedIndex } = state; + const bufferedLength = buffered.length - bufferedIndex; + + if (!bufferedLength) { + return; + } + + let i = bufferedIndex; + + state[kState] |= kBufferProcessing; + if (bufferedLength > 1 && stream._writev) { + state.pendingcb -= bufferedLength - 1; + + const callback = + (state[kState] & kAllNoop) !== 0 + ? nop + : err => { + for (let n = i; n < buffered.length; ++n) { + buffered[n].callback(err); + } + }; + // Make a copy of `buffered` if it's going to be used by `callback` above, + // since `doWrite` will mutate the array. + const chunks = (state[kState] & kAllNoop) !== 0 && i === 0 ? buffered : ArrayPrototypeSlice.$call(buffered, i); + chunks.allBuffers = (state[kState] & kAllBuffers) !== 0; + + doWrite(stream, state, true, state.length, chunks, "", callback); + + resetBuffer(state); + } else { + do { + const { chunk, encoding, callback } = buffered[i]; + buffered[i++] = null; + const len = objectMode ? 1 : chunk.length; + doWrite(stream, state, false, len, chunk, encoding, callback); + } while (i < buffered.length && (state[kState] & kWriting) === 0); + + if (i === buffered.length) { + resetBuffer(state); + } else if (i > 256) { + buffered.splice(0, i); + state.bufferedIndex = 0; + } else { + state.bufferedIndex = i; + } + } + state[kState] &= ~kBufferProcessing; +} + +Writable.prototype._write = function (chunk, encoding, cb) { + if (this._writev) { + this._writev([{ chunk, encoding }], cb); + } else { + throw $ERR_METHOD_NOT_IMPLEMENTED("_write()"); + } +}; + +Writable.prototype._writev = null; + +Writable.prototype.end = function (chunk, encoding, cb) { + const state = this._writableState; + + if (typeof chunk === "function") { + cb = chunk; + chunk = null; + encoding = null; + } else if (typeof encoding === "function") { + cb = encoding; + encoding = null; + } + + let err; + + if (chunk != null) { + const ret = _write(this, chunk, encoding); + if (Error.isError(ret)) { + err = ret; + } + } + + // .end() fully uncorks. + if ((state[kState] & kCorked) !== 0) { + state.corked = 1; + this.uncork(); + } + + if (err) { + // Do nothing... + } else if ((state[kState] & (kEnding | kErrored)) === 0) { + // This is forgiving in terms of unnecessary calls to end() and can hide + // logic errors. However, usually such errors are harmless and causing a + // hard error can be disproportionately destructive. It is not always + // trivial for the user to determine whether end() needs to be called + // or not. + + state[kState] |= kEnding; + finishMaybe(this, state, true); + state[kState] |= kEnded; + } else if ((state[kState] & kFinished) !== 0) { + err = $ERR_STREAM_ALREADY_FINISHED("end"); + } else if ((state[kState] & kDestroyed) !== 0) { + err = $ERR_STREAM_DESTROYED("end"); + } + + if (typeof cb === "function") { + if (err) { + process.nextTick(cb, err); + } else if ((state[kState] & kErrored) !== 0) { + process.nextTick(cb, state[kErroredValue]); + } else if ((state[kState] & kFinished) !== 0) { + process.nextTick(cb, null); + } else { + state[kState] |= kOnFinished; + state[kOnFinishedValue] ??= []; + state[kOnFinishedValue].push(cb); + } + } + + return this; +}; + +function needFinish(state) { + return ( + // State is ended && constructed but not destroyed, finished, writing, errorEmitted or closedEmitted + (state[kState] & + (kEnding | + kDestroyed | + kConstructed | + kFinished | + kWriting | + kErrorEmitted | + kCloseEmitted | + kErrored | + kBuffered)) === + (kEnding | kConstructed) && state.length === 0 + ); +} + +function onFinish(stream, state, err) { + if ((state[kState] & kPrefinished) !== 0) { + errorOrDestroy(stream, err ?? $ERR_MULTIPLE_CALLBACK()); + return; + } + state.pendingcb--; + if (err) { + callFinishedCallbacks(state, err); + errorOrDestroy(stream, err, (state[kState] & kSync) !== 0); + } else if (needFinish(state)) { + state[kState] |= kPrefinished; + stream.emit("prefinish"); + // Backwards compat. Don't check state.sync here. + // Some streams assume 'finish' will be emitted + // asynchronously relative to _final callback. + state.pendingcb++; + process.nextTick(finish, stream, state); + } +} + +function prefinish(stream, state) { + if ((state[kState] & (kPrefinished | kFinalCalled)) !== 0) { + return; + } + + if (typeof stream._final === "function" && (state[kState] & kDestroyed) === 0) { + state[kState] |= kFinalCalled | kSync; + state.pendingcb++; + + try { + stream._final(err => onFinish(stream, state, err)); + } catch (err) { + onFinish(stream, state, err); + } + + state[kState] &= ~kSync; + } else { + state[kState] |= kFinalCalled | kPrefinished; + stream.emit("prefinish"); + } +} + +function finishMaybe(stream, state, sync?) { + if (needFinish(state)) { + prefinish(stream, state); + if (state.pendingcb === 0) { + if (sync) { + state.pendingcb++; + process.nextTick( + (stream, state) => { + if (needFinish(state)) { + finish(stream, state); + } else { + state.pendingcb--; + } + }, + stream, + state, + ); + } else if (needFinish(state)) { + state.pendingcb++; + finish(stream, state); + } + } + } +} + +function finish(stream, state) { + state.pendingcb--; + state[kState] |= kFinished; + + callFinishedCallbacks(state, null); + + stream.emit("finish"); + + if ((state[kState] & kAutoDestroy) !== 0) { + // In case of duplex streams we need a way to detect + // if the readable side is ready for autoDestroy as well. + const rState = stream._readableState; + const autoDestroy = + !rState || + (rState.autoDestroy && + // We don't expect the readable to ever 'end' + // if readable is explicitly set to false. + (rState.endEmitted || rState.readable === false)); + if (autoDestroy) { + stream.destroy(); + } + } +} + +function callFinishedCallbacks(state, err) { + if ((state[kState] & kOnFinished) === 0) { + return; + } + + const onfinishCallbacks = state[kOnFinishedValue]; + state[kOnFinishedValue] = null; + state[kState] &= ~kOnFinished; + for (let i = 0; i < onfinishCallbacks.length; i++) { + onfinishCallbacks[i](err); + } +} + +ObjectDefineProperties(Writable.prototype, { + closed: { + __proto__: null, + get() { + return this._writableState ? (this._writableState[kState] & kClosed) !== 0 : false; + }, + }, + + destroyed: { + __proto__: null, + get() { + return this._writableState ? (this._writableState[kState] & kDestroyed) !== 0 : false; + }, + set(value) { + // Backward compatibility, the user is explicitly managing destroyed. + if (!this._writableState) return; + + if (value) this._writableState[kState] |= kDestroyed; + else this._writableState[kState] &= ~kDestroyed; + }, + }, + + writable: { + __proto__: null, + get() { + const w = this._writableState; + // w.writable === false means that this is part of a Duplex stream + // where the writable side was disabled upon construction. + // Compat. The user might manually disable writable side through + // deprecated setter. + return !!w && w.writable !== false && (w[kState] & (kEnding | kEnded | kDestroyed | kErrored)) === 0; + }, + set(val) { + // Backwards compatible. + if (this._writableState) { + this._writableState.writable = !!val; + } + }, + }, + + writableFinished: { + __proto__: null, + get() { + const state = this._writableState; + return state ? (state[kState] & kFinished) !== 0 : false; + }, + }, + + writableObjectMode: { + __proto__: null, + get() { + const state = this._writableState; + return state ? (state[kState] & kObjectMode) !== 0 : false; + }, + }, + + writableBuffer: { + __proto__: null, + get() { + const state = this._writableState; + return state && state.getBuffer(); + }, + }, + + writableEnded: { + __proto__: null, + get() { + const state = this._writableState; + return state ? (state[kState] & kEnding) !== 0 : false; + }, + }, + + writableNeedDrain: { + __proto__: null, + get() { + const state = this._writableState; + return state ? (state[kState] & (kDestroyed | kEnding | kNeedDrain)) === kNeedDrain : false; + }, + }, + + writableHighWaterMark: { + __proto__: null, + get() { + const state = this._writableState; + return state?.highWaterMark; + }, + }, + + writableCorked: { + __proto__: null, + get() { + const state = this._writableState; + return state ? state.corked : 0; + }, + }, + + writableLength: { + __proto__: null, + get() { + const state = this._writableState; + return state?.length; + }, + }, + + errored: { + __proto__: null, + enumerable: false, + get() { + const state = this._writableState; + return state ? state.errored : null; + }, + }, + + writableAborted: { + __proto__: null, + get: function () { + const state = this._writableState; + return ( + (state[kState] & (kHasWritable | kWritable)) !== kHasWritable && + (state[kState] & (kDestroyed | kErrored)) !== 0 && + (state[kState] & kFinished) === 0 + ); + }, + }, +}); + +const destroy = destroyImpl.destroy; +Writable.prototype.destroy = function (err, cb) { + const state = this._writableState; + + // Invoke pending callbacks. + if ((state[kState] & (kBuffered | kOnFinished)) !== 0 && (state[kState] & kDestroyed) === 0) { + process.nextTick(errorBuffer, state); + } + + destroy.$call(this, err, cb); + return this; +}; + +Writable.prototype._undestroy = destroyImpl.undestroy; +Writable.prototype._destroy = function (err, cb) { + cb(err); +}; + +Writable.prototype[EE.captureRejectionSymbol] = function (err) { + this.destroy(err); +}; + +// Lazy to avoid circular references +let webStreamsAdapters; +function lazyWebStreams() { + if (webStreamsAdapters === undefined) webStreamsAdapters = require("internal/webstreams_adapters"); + return webStreamsAdapters; +} + +Writable.fromWeb = function (writableStream, options) { + return lazyWebStreams().newStreamWritableFromWritableStream(writableStream, options); +}; + +Writable.toWeb = function (streamWritable) { + return lazyWebStreams().newWritableStreamFromStreamWritable(streamWritable); +}; + +Writable.prototype[SymbolAsyncDispose] = function () { + let error; + if (!this.destroyed) { + error = this.writableFinished ? null : $makeAbortError(); + this.destroy(error); + } + return new Promise((resolve, reject) => + eos(this, err => (err && err.name !== "AbortError" ? reject(err) : resolve(null))), + ); +}; + +export default Writable; diff --git a/src/js/internal/tty.ts b/src/js/internal/tty.ts new file mode 100644 index 0000000000..a0adc4414a --- /dev/null +++ b/src/js/internal/tty.ts @@ -0,0 +1,174 @@ +let OSRelease; + +const COLORS_2 = 1; +const COLORS_16 = 4; +const COLORS_256 = 8; +const COLORS_16m = 24; + +// Some entries were taken from `dircolors` +// (https://linux.die.net/man/1/dircolors). The corresponding terminals might +// support more than 16 colors, but this was not tested for. +// +// Copyright (C) 1996-2016 Free Software Foundation, Inc. Copying and +// distribution of this file, with or without modification, are permitted +// provided the copyright notice and this notice are preserved. +const TERM_ENVS = { + "eterm": COLORS_16, + "cons25": COLORS_16, + "console": COLORS_16, + "cygwin": COLORS_16, + "dtterm": COLORS_16, + "gnome": COLORS_16, + "hurd": COLORS_16, + "jfbterm": COLORS_16, + "konsole": COLORS_16, + "kterm": COLORS_16, + "mlterm": COLORS_16, + "mosh": COLORS_16m, + "putty": COLORS_16, + "st": COLORS_16, + // https://github.com/da-x/rxvt-unicode/tree/v9.22-with-24bit-color + "rxvt-unicode-24bit": COLORS_16m, + // https://gist.github.com/XVilka/8346728#gistcomment-2823421 + "terminator": COLORS_16m, +}; + +const TERM_ENVS_REG_EXP = [/ansi/, /color/, /linux/, /^con[0-9]*x[0-9]/, /^rxvt/, /^screen/, /^xterm/, /^vt100/]; + +let warned = false; +function warnOnDeactivatedColors(env) { + if (warned) return; + let name = ""; + if (env.NODE_DISABLE_COLORS !== undefined) name = "NODE_DISABLE_COLORS"; + if (env.NO_COLOR !== undefined) { + if (name !== "") { + name += "' and '"; + } + name += "NO_COLOR"; + } + + if (name !== "") { + process.emitWarning(`The '${name}' env is ignored due to the 'FORCE_COLOR' env being set.`, "Warning"); + warned = true; + } +} + +function getColorDepth(env: NodeJS.ProcessEnv) { + const FORCE_COLOR = env.FORCE_COLOR; + // Use level 0-3 to support the same levels as `chalk` does. This is done for + // consistency throughout the ecosystem. + if (FORCE_COLOR !== undefined) { + switch (FORCE_COLOR) { + case "": + case "1": + case "true": + warnOnDeactivatedColors(env); + return COLORS_16; + case "2": + warnOnDeactivatedColors(env); + return COLORS_256; + case "3": + warnOnDeactivatedColors(env); + return COLORS_16m; + default: + return COLORS_2; + } + } + + if ( + env.NODE_DISABLE_COLORS !== undefined || + // See https://no-color.org/ + env.NO_COLOR !== undefined || + // The "dumb" special terminal, as defined by terminfo, doesn't support + // ANSI color control codes. + // See https://invisible-island.net/ncurses/terminfo.ti.html#toc-_Specials + env.TERM === "dumb" + ) { + return COLORS_2; + } + + if (process.platform === "win32") { + // Lazy load for startup performance. + if (OSRelease === undefined) { + const { release } = require("node:os"); + OSRelease = release().split("."); + } + // Windows 10 build 10586 is the first Windows release that supports 256 + // colors. Windows 10 build 14931 is the first release that supports + // 16m/TrueColor. + if (+OSRelease[0] >= 10) { + const build = +OSRelease[2]; + if (build >= 14931) return COLORS_16m; + if (build >= 10586) return COLORS_256; + } + + return COLORS_16; + } + + if (env.TMUX) { + return COLORS_256; + } + + if (env.CI) { + if ( + ["APPVEYOR", "BUILDKITE", "CIRCLECI", "DRONE", "GITHUB_ACTIONS", "GITLAB_CI", "TRAVIS"].some( + sign => sign in env, + ) || + env.CI_NAME === "codeship" + ) { + return COLORS_256; + } + return COLORS_2; + } + + const TEAMCITY_VERSION = env.TEAMCITY_VERSION; + if (TEAMCITY_VERSION) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(TEAMCITY_VERSION) ? COLORS_16 : COLORS_2; + } + + switch (env.TERM_PROGRAM) { + case "iTerm.app": + if (!env.TERM_PROGRAM_VERSION || /^[0-2]\./.test(env.TERM_PROGRAM_VERSION)) { + return COLORS_256; + } + return COLORS_16m; + case "HyperTerm": + case "ghostty": + case "WezTerm": + case "MacTerm": + return COLORS_16m; + case "Apple_Terminal": + return COLORS_256; + } + + const COLORTERM = env.COLORTERM; + + if (COLORTERM === "truecolor" || COLORTERM === "24bit") { + return COLORS_16m; + } + + const TERM = env.TERM; + + if (TERM) { + if (/^xterm-256/.test(TERM) !== null) { + return COLORS_256; + } + + const termEnv = TERM.toLowerCase(); + + if (TERM_ENVS[termEnv]) { + return TERM_ENVS[termEnv]; + } + if (TERM_ENVS_REG_EXP.some(term => term.test(termEnv))) { + return COLORS_16; + } + } + + // Move 16 color COLORTERM below 16m and 256 + if (env.COLORTERM) { + return COLORS_16; + } + return COLORS_2; +} + +export default { getColorDepth }; diff --git a/src/js/internal/util.ts b/src/js/internal/util.ts new file mode 100644 index 0000000000..32db0807c3 --- /dev/null +++ b/src/js/internal/util.ts @@ -0,0 +1,5 @@ +const isInsideNodeModules: () => boolean = $newZigFunction("node_util_binding.zig", "isInsideNodeModules", 0); + +export default { + isInsideNodeModules, +}; diff --git a/src/js/internal/util/colors.ts b/src/js/internal/util/colors.ts new file mode 100644 index 0000000000..c58c802c1b --- /dev/null +++ b/src/js/internal/util/colors.ts @@ -0,0 +1,50 @@ +// Taken from Node - lib/internal/util/colors.js +"use strict"; + +type WriteStream = import("node:tty").WriteStream; + +let exports = { + blue: "", + green: "", + white: "", + yellow: "", + red: "", + gray: "", + clear: "", + reset: "", + hasColors: false, + shouldColorize(stream: WriteStream) { + if (process.env.FORCE_COLOR !== undefined) { + return require("internal/tty").getColorDepth(process.env) > 2; + } + + return stream?.isTTY && (typeof stream.getColorDepth === "function" ? stream.getColorDepth() > 2 : true); + }, + refresh(): void { + if (exports.shouldColorize(process.stderr)) { + exports.blue = "\u001b[34m"; + exports.green = "\u001b[32m"; + exports.white = "\u001b[39m"; + exports.yellow = "\u001b[33m"; + exports.red = "\u001b[31m"; + exports.gray = "\u001b[90m"; + exports.clear = "\u001bc"; + exports.reset = "\u001b[0m"; + exports.hasColors = true; + } else { + exports.blue = ""; + exports.green = ""; + exports.white = ""; + exports.yellow = ""; + exports.red = ""; + exports.gray = ""; + exports.clear = ""; + exports.reset = ""; + exports.hasColors = false; + } + }, +}; + +exports.refresh(); + +export default exports; diff --git a/src/js/internal/util/inspect.js b/src/js/internal/util/inspect.js index 5cdb40af5b..38d8f38326 100644 --- a/src/js/internal/util/inspect.js +++ b/src/js/internal/util/inspect.js @@ -34,106 +34,100 @@ const { pathToFileURL } = require("node:url"); let BufferModule; const primordials = require("internal/primordials"); +const { uncurryThis } = primordials; const { - Array, - ArrayFrom, - ArrayPrototypeFilter, - ArrayPrototypeFlat, - ArrayPrototypeForEach, - ArrayPrototypeIncludes, - ArrayPrototypeIndexOf, - ArrayPrototypeJoin, - ArrayPrototypeMap, - ArrayPrototypePop, - ArrayPrototypePush, - ArrayPrototypePushApply, - ArrayPrototypeSlice, - ArrayPrototypeSplice, - ArrayPrototypeSort, - ArrayPrototypeUnshift, - BigIntPrototypeValueOf, - BooleanPrototypeValueOf, - DatePrototypeGetTime, - DatePrototypeToISOString, - DatePrototypeToString, - ErrorCaptureStackTrace, - ErrorPrototypeToString, - FunctionPrototypeBind, - FunctionPrototypeToString, - JSONStringify, MapPrototypeGetSize, - MapPrototypeEntries, - MapPrototypeValues, - MapPrototypeKeys, - MathFloor, - MathMax, - MathMin, - MathRound, - MathSqrt, - MathTrunc, - Number, - NumberIsFinite, - NumberIsNaN, - NumberParseFloat, - NumberParseInt, - NumberPrototypeToString, - NumberPrototypeValueOf, - Object, - ObjectAssign, - ObjectDefineProperty, - ObjectEntries, - ObjectGetOwnPropertyDescriptor, - ObjectGetOwnPropertyDescriptors, - ObjectGetOwnPropertyNames, - ObjectGetOwnPropertySymbols, - ObjectGetPrototypeOf, - ObjectIs, - ObjectKeys, - ObjectPrototypeHasOwnProperty, - ObjectPrototypePropertyIsEnumerable, - ObjectPrototypeToString, - ObjectSeal, - ObjectSetPrototypeOf, - ReflectOwnKeys, - RegExp, - RegExpPrototypeExec, - RegExpPrototypeSymbolReplace, - RegExpPrototypeSymbolSplit, - RegExpPrototypeTest, - RegExpPrototypeToString, SafeMap, SafeSet, - SetPrototypeEntries, SetPrototypeGetSize, - SetPrototypeValues, - String, - StringPrototypeCharCodeAt, - StringPrototypeCodePointAt, - StringPrototypeIncludes, - StringPrototypeIndexOf, - StringPrototypeLastIndexOf, - StringPrototypeMatch, - StringPrototypeNormalize, - StringPrototypePadEnd, - StringPrototypePadStart, - StringPrototypeRepeat, - StringPrototypeReplaceAll, - StringPrototypeSlice, - StringPrototypeSplit, - StringPrototypeEndsWith, - StringPrototypeStartsWith, - StringPrototypeToLowerCase, - StringPrototypeTrim, - StringPrototypeValueOf, - SymbolPrototypeToString, - SymbolPrototypeValueOf, - SymbolIterator, - SymbolToStringTag, TypedArrayPrototypeGetLength, TypedArrayPrototypeGetSymbolToStringTag, - Uint8Array, } = primordials; +const ArrayFrom = Array.from; +const ArrayPrototypeFilter = uncurryThis(Array.prototype.filter); +const ArrayPrototypeFlat = uncurryThis(Array.prototype.flat); +const ArrayPrototypeForEach = uncurryThis(Array.prototype.forEach); +const ArrayPrototypeIncludes = uncurryThis(Array.prototype.includes); +const ArrayPrototypeIndexOf = uncurryThis(Array.prototype.indexOf); +const ArrayPrototypeJoin = uncurryThis(Array.prototype.join); +const ArrayPrototypeMap = uncurryThis(Array.prototype.map); +const ArrayPrototypePop = uncurryThis(Array.prototype.pop); +const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeSlice = uncurryThis(Array.prototype.slice); +const ArrayPrototypeSplice = uncurryThis(Array.prototype.splice); +const ArrayPrototypeSort = uncurryThis(Array.prototype.sort); +const ArrayPrototypeUnshift = uncurryThis(Array.prototype.unshift); +const BigIntPrototypeValueOf = uncurryThis(BigInt.prototype.valueOf); +const BooleanPrototypeValueOf = uncurryThis(Boolean.prototype.valueOf); +const DatePrototypeGetTime = uncurryThis(Date.prototype.getTime); +const DatePrototypeToISOString = uncurryThis(Date.prototype.toISOString); +const DatePrototypeToString = uncurryThis(Date.prototype.toString); +const ErrorCaptureStackTrace = Error.captureStackTrace; +const ErrorPrototypeToString = uncurryThis(Error.prototype.toString); +const FunctionPrototypeBind = uncurryThis(Function.prototype.bind); +const FunctionPrototypeToString = uncurryThis(Function.prototype.toString); +const JSONStringify = JSON.stringify; +const MapPrototypeEntries = uncurryThis(Map.prototype.entries); +const MapPrototypeValues = uncurryThis(Map.prototype.values); +const MapPrototypeKeys = uncurryThis(Map.prototype.keys); +const MathFloor = Math.floor; +const MathMax = Math.max; +const MathMin = Math.min; +const MathRound = Math.round; +const MathSqrt = Math.sqrt; +const MathTrunc = Math.trunc; +const NumberIsFinite = Number.isFinite; +const NumberIsNaN = Number.isNaN; +const NumberParseFloat = Number.parseFloat; +const NumberParseInt = Number.parseInt; +const NumberPrototypeToString = uncurryThis(Number.prototype.toString); +const NumberPrototypeValueOf = uncurryThis(Number.prototype.valueOf); +const ObjectAssign = Object.assign; +const ObjectDefineProperty = Object.defineProperty; +const ObjectEntries = Object.entries; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors; +const ObjectGetOwnPropertyNames = Object.getOwnPropertyNames; +const ObjectGetOwnPropertySymbols = Object.getOwnPropertySymbols; +const ObjectGetPrototypeOf = Object.getPrototypeOf; +const ObjectIs = Object.is; +const ObjectKeys = Object.keys; +const ObjectPrototypeHasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty); +const ObjectPrototypePropertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable); +const ObjectPrototypeToString = uncurryThis(Object.prototype.toString); +const ObjectSeal = Object.seal; +const ObjectSetPrototypeOf = Object.setPrototypeOf; +const ReflectOwnKeys = Reflect.ownKeys; +const RegExpPrototypeExec = uncurryThis(RegExp.prototype.exec); +const RegExpPrototypeSymbolReplace = uncurryThis(RegExp.prototype[Symbol.replace]); +const RegExpPrototypeSymbolSplit = uncurryThis(RegExp.prototype[Symbol.split]); +const RegExpPrototypeTest = uncurryThis(RegExp.prototype.test); +const RegExpPrototypeToString = uncurryThis(RegExp.prototype.toString); +const SetPrototypeEntries = uncurryThis(Set.prototype.entries); +const SetPrototypeValues = uncurryThis(Set.prototype.values); +const StringPrototypeCharCodeAt = uncurryThis(String.prototype.charCodeAt); +const StringPrototypeIncludes = uncurryThis(String.prototype.includes); +const StringPrototypeIndexOf = uncurryThis(String.prototype.indexOf); +const StringPrototypeLastIndexOf = uncurryThis(String.prototype.lastIndexOf); +const StringPrototypeMatch = uncurryThis(String.prototype.match); +const StringPrototypeNormalize = uncurryThis(String.prototype.normalize); +const StringPrototypePadEnd = uncurryThis(String.prototype.padEnd); +const StringPrototypePadStart = uncurryThis(String.prototype.padStart); +const StringPrototypeRepeat = uncurryThis(String.prototype.repeat); +const StringPrototypeReplaceAll = uncurryThis(String.prototype.replaceAll); +const StringPrototypeSlice = uncurryThis(String.prototype.slice); +const StringPrototypeSplit = uncurryThis(String.prototype.split); +const StringPrototypeEndsWith = uncurryThis(String.prototype.endsWith); +const StringPrototypeStartsWith = uncurryThis(String.prototype.startsWith); +const StringPrototypeToLowerCase = uncurryThis(String.prototype.toLowerCase); +const StringPrototypeTrim = uncurryThis(String.prototype.trim); +const StringPrototypeValueOf = uncurryThis(String.prototype.valueOf); +const SymbolPrototypeToString = uncurryThis(Symbol.prototype.toString); +const SymbolPrototypeValueOf = uncurryThis(Symbol.prototype.valueOf); +const SymbolIterator = Symbol.iterator; +const SymbolToStringTag = Symbol.toStringTag; + const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom"); const kPending = Symbol("kPending"); // state ID 0 const kFulfilled = Symbol("kFulfilled"); // state ID 1 @@ -141,6 +135,21 @@ const kRejected = Symbol("kRejected"); // state ID 2 const ALL_PROPERTIES = 0; const ONLY_ENUMERABLE = 2; +/** + * Fast path for {@link extractedSplitNewLines} for ASCII/Latin1 strings. + * @returns `value` split on newlines (newline included at end), or `undefined` + * if non-ascii UTF8/UTF16. + * + * Passing this a non-string will cause a panic. + * + * @type {(value: string) => string[] | undefined} + */ +const extractedSplitNewLinesFastPathStringsOnly = $newZigFunction( + "node_util_binding.zig", + "extractedSplitNewLinesFastPathStringsOnly", + 1, +); + const isAsyncFunction = v => typeof v === "function" && StringPrototypeStartsWith(FunctionPrototypeToString(v), "async"); const isGeneratorFunction = v => @@ -251,11 +260,11 @@ const codes = {}; // exported from errors.js const other = []; for (const value of expected) { assert(typeof value === "string", "All expected entries have to be of type string"); - if (ArrayPrototypeIncludes(kTypes, value)) ArrayPrototypePush(types, StringPrototypeToLowerCase(value)); - else if (RegExpPrototypeTest(classRegExp, value)) ArrayPrototypePush(instances, value); + if (ArrayPrototypeIncludes(kTypes, value)) ArrayPrototypePush.$call(types, StringPrototypeToLowerCase(value)); + else if (RegExpPrototypeTest(classRegExp, value)) ArrayPrototypePush.$call(instances, value); else { assert(value !== "object", 'The value "object" should be written as "Object"'); - ArrayPrototypePush(other, value); + ArrayPrototypePush.$call(other, value); } } // Special handle `object` in case other instances are allowed to outline the differences between each other. @@ -263,7 +272,7 @@ const codes = {}; // exported from errors.js const pos = ArrayPrototypeIndexOf(types, "object"); if (pos !== -1) { ArrayPrototypeSplice(types, pos, 1); - ArrayPrototypePush(instances, "Object"); + ArrayPrototypePush.$call(instances, "Object"); } } if (types.length > 0) { @@ -397,7 +406,7 @@ let strEscapeSequencesRegExp, strEscapeSequencesReplacer, strEscapeSequencesRegExpSingle, strEscapeSequencesReplacerSingle, - extractedSplitNewLines; + extractedSplitNewLinesSlow; try { // Change from regex literals to RegExp constructors to avoid unrecoverable // syntax error at load time. @@ -416,7 +425,7 @@ try { "g", ); const extractedNewLineRe = new RegExp("(?<=\\n)"); - extractedSplitNewLines = value => RegExpPrototypeSymbolSplit(extractedNewLineRe, value); + extractedSplitNewLinesSlow = value => RegExpPrototypeSymbolSplit(extractedNewLineRe, value); // CI doesn't run in an elderly runtime } catch { // These are from a previous version of node, @@ -426,7 +435,7 @@ try { strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c\x7f-\x9f]/g; strEscapeSequencesRegExpSingle = /[\x00-\x1f\x5c\x7f-\x9f]/; strEscapeSequencesReplacerSingle = /[\x00-\x1f\x5c\x7f-\x9f]/g; - extractedSplitNewLines = value => { + extractedSplitNewLinesSlow = value => { const lines = RegExpPrototypeSymbolSplit(/\n/, value); const last = ArrayPrototypePop(lines); const nlLines = ArrayPrototypeMap(lines, line => line + "\n"); @@ -437,6 +446,13 @@ try { }; } +const extractedSplitNewLines = value => { + if (typeof value === "string") { + return extractedSplitNewLinesFastPathStringsOnly(value) || extractedSplitNewLinesSlow(value); + } + return extractedSplitNewLinesSlow(value); +}; + const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; const numberRegExp = /^(0|[1-9][0-9]*)$/; @@ -1025,7 +1041,7 @@ function addPrototypeProperties(ctx, main, obj, recurseTimes, output) { } // Get all own property names and symbols. keys = ReflectOwnKeys(obj); - ArrayPrototypePush(ctx.seen, main); + ArrayPrototypePush.$call(ctx.seen, main); for (const key of keys) { // Ignore the `constructor` property and keys that exist on layers above. if (key === "constructor" || ObjectPrototypeHasOwnProperty(main, key) || (depth !== 0 && keySet.has(key))) { @@ -1038,9 +1054,9 @@ function addPrototypeProperties(ctx, main, obj, recurseTimes, output) { const value = formatProperty(ctx, obj, recurseTimes, key, kObjectType, desc, main); if (ctx.colors) { // Faint! - ArrayPrototypePush(output, `\u001b[2m${value}\u001b[22m`); + ArrayPrototypePush.$call(output, `\u001b[2m${value}\u001b[22m`); } else { - ArrayPrototypePush(output, value); + ArrayPrototypePush.$call(output, value); } } ArrayPrototypePop(ctx.seen); @@ -1070,7 +1086,7 @@ function getKeys(value, showHidden) { const symbols = ObjectGetOwnPropertySymbols(value); if (showHidden) { keys = ObjectGetOwnPropertyNames(value); - if (symbols.length !== 0) ArrayPrototypePushApply(keys, symbols); + if (symbols.length !== 0) ArrayPrototypePush.$apply(keys, symbols); } else { // This might throw if `value` is a Module Namespace Object from an // unevaluated module, but we don't want to perform the actual type @@ -1085,7 +1101,7 @@ function getKeys(value, showHidden) { } if (symbols.length !== 0) { const filter = key => ObjectPrototypePropertyIsEnumerable(value, key); - ArrayPrototypePushApply(keys, ArrayPrototypeFilter(symbols, filter)); + ArrayPrototypePush.$apply(keys, ArrayPrototypeFilter(symbols, filter)); } } return keys; @@ -1377,10 +1393,10 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { if (ctx.currentDepth > 1000) throw new RangeError(ERROR_STACK_OVERFLOW_MSG); output = formatter(ctx, value, recurseTimes); for (i = 0; i < keys.length; i++) { - ArrayPrototypePush(output, formatProperty(ctx, value, recurseTimes, keys[i], extrasType)); + ArrayPrototypePush.$call(output, formatProperty(ctx, value, recurseTimes, keys[i], extrasType)); } if (protoProps !== undefined) { - ArrayPrototypePushApply(output, protoProps); + ArrayPrototypePush.$apply(output, protoProps); } } catch (err) { if (err instanceof RangeError && err.message === ERROR_STACK_OVERFLOW_MSG) { @@ -1720,12 +1736,12 @@ function formatError(err, constructor, tag, ctx, keys) { removeDuplicateErrorKeys(ctx, keys, err, stack); if ("cause" in err && (keys.length === 0 || !ArrayPrototypeIncludes(keys, "cause"))) { - ArrayPrototypePush(keys, "cause"); + ArrayPrototypePush.$call(keys, "cause"); } // Print errors aggregated into AggregateError if ($isJSArray(err.errors) && (keys.length === 0 || !ArrayPrototypeIncludes(keys, "errors"))) { - ArrayPrototypePush(keys, "errors"); + ArrayPrototypePush.$call(keys, "errors"); } stack = improveStack(stack, constructor, name, tag); @@ -1871,10 +1887,10 @@ function groupArrayElements(ctx, output, value) { } else { str += output[j]; } - ArrayPrototypePush(tmp, str); + ArrayPrototypePush.$call(tmp, str); } if (ctx.maxArrayLength < output.length) { - ArrayPrototypePush(tmp, output[outputLength]); + ArrayPrototypePush.$call(tmp, output[outputLength]); } output = tmp; } @@ -2008,13 +2024,13 @@ function formatSpecialArray(ctx, value, recurseTimes, maxLength, output, i) { const emptyItems = tmp - index; const ending = emptyItems > 1 ? "s" : ""; const message = `<${emptyItems} empty item${ending}>`; - ArrayPrototypePush(output, ctx.stylize(message, "undefined")); + ArrayPrototypePush.$call(output, ctx.stylize(message, "undefined")); index = tmp; if (output.length === maxLength) { break; } } - ArrayPrototypePush(output, formatProperty(ctx, value, recurseTimes, key, kArrayType)); + ArrayPrototypePush.$call(output, formatProperty(ctx, value, recurseTimes, key, kArrayType)); index++; } const remaining = value.length - index; @@ -2022,10 +2038,10 @@ function formatSpecialArray(ctx, value, recurseTimes, maxLength, output, i) { if (remaining > 0) { const ending = remaining > 1 ? "s" : ""; const message = `<${remaining} empty item${ending}>`; - ArrayPrototypePush(output, ctx.stylize(message, "undefined")); + ArrayPrototypePush.$call(output, ctx.stylize(message, "undefined")); } } else if (remaining > 0) { - ArrayPrototypePush(output, remainingText(remaining)); + ArrayPrototypePush.$call(output, remainingText(remaining)); } return output; } @@ -2063,10 +2079,10 @@ function formatArray(ctx, value, recurseTimes) { if (!ObjectPrototypeHasOwnProperty(value, i)) { return formatSpecialArray(ctx, value, recurseTimes, len, output, i); } - ArrayPrototypePush(output, formatProperty(ctx, value, recurseTimes, i, kArrayType)); + ArrayPrototypePush.$call(output, formatProperty(ctx, value, recurseTimes, i, kArrayType)); } if (remaining > 0) { - ArrayPrototypePush(output, remainingText(remaining)); + ArrayPrototypePush.$call(output, remainingText(remaining)); } return output; } @@ -2093,7 +2109,7 @@ function formatTypedArray(value, length, ctx, ignored, recurseTimes) { ctx.indentationLvl += 2; for (const key of ["BYTES_PER_ELEMENT", "length", "byteLength", "byteOffset", "buffer"]) { const str = formatValue(ctx, value[key], recurseTimes, true); - ArrayPrototypePush(output, `[${key}]: ${str}`); + ArrayPrototypePush.$call(output, `[${key}]: ${str}`); } ctx.indentationLvl -= 2; } @@ -2109,11 +2125,11 @@ function formatSet(value, ctx, ignored, recurseTimes) { let i = 0; for (const v of value) { if (i >= maxLength) break; - ArrayPrototypePush(output, formatValue(ctx, v, recurseTimes)); + ArrayPrototypePush.$call(output, formatValue(ctx, v, recurseTimes)); i++; } if (remaining > 0) { - ArrayPrototypePush(output, remainingText(remaining)); + ArrayPrototypePush.$call(output, remainingText(remaining)); } ctx.indentationLvl -= 2; return output; @@ -2128,11 +2144,11 @@ function formatMap(value, ctx, ignored, recurseTimes) { let i = 0; for (const { 0: k, 1: v } of value) { if (i >= maxLength) break; - ArrayPrototypePush(output, `${formatValue(ctx, k, recurseTimes)} => ${formatValue(ctx, v, recurseTimes)}`); + ArrayPrototypePush.$call(output, `${formatValue(ctx, k, recurseTimes)} => ${formatValue(ctx, v, recurseTimes)}`); i++; } if (remaining > 0) { - ArrayPrototypePush(output, remainingText(remaining)); + ArrayPrototypePush.$call(output, remainingText(remaining)); } ctx.indentationLvl -= 2; return output; @@ -2155,7 +2171,7 @@ function formatSetIterInner(ctx, recurseTimes, entries, state) { } const remaining = entries.length - maxLength; if (remaining > 0) { - ArrayPrototypePush(output, remainingText(remaining)); + ArrayPrototypePush.$call(output, remainingText(remaining)); } return output; } @@ -2191,7 +2207,7 @@ function formatMapIterInner(ctx, recurseTimes, entries, state) { } ctx.indentationLvl -= 2; if (remaining > 0) { - ArrayPrototypePush(output, remainingText(remaining)); + ArrayPrototypePush.$call(output, remainingText(remaining)); } return output; } @@ -2585,14 +2601,18 @@ function getStringWidth(str, removeControlChars = true) { } // Regex used for ansi escape code splitting -// Adopted from https://github.com/chalk/ansi-regex/blob/HEAD/index.js -// License: MIT, authors: @sindresorhus, Qix-, arjunmehta and LitoMore +// Ref: https://github.com/chalk/ansi-regex/blob/f338e1814144efb950276aac84135ff86b72dc8e/index.js +// License: MIT by Sindre Sorhus // Matches all ansi escape code sequences in a string -const ansiPattern = +const ansiPattern = new RegExp( "[\\u001B\\u009B][[\\]()#;?]*" + - "(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*" + - "|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)" + - "|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"; + "(?:(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*" + + "|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]*)*)?" + + "(?:\\u0007|\\u001B\\u005C|\\u009C))" + + "|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?" + + "[\\dA-PR-TZcf-nq-uy=><~]))", + "g", +); const ansi = new RegExp(ansiPattern, "g"); /** Remove all VT control characters. Use to estimate displayed string width. */ function stripVTControlCharacters(str) { @@ -2608,13 +2628,13 @@ function getOwnNonIndexProperties(a, filter = ONLY_ENUMERABLE) { if (!RegExpPrototypeTest(/^(0|[1-9][0-9]*)$/, k) || NumberParseInt(k, 10) >= 2 ** 32 - 1) { // Arrays are limited in size if (filter === ONLY_ENUMERABLE && !v.enumerable) continue; - else ArrayPrototypePush(ret, k); + else ArrayPrototypePush.$call(ret, k); } } for (const s of ObjectGetOwnPropertySymbols(a)) { const v = ObjectGetOwnPropertyDescriptor(a, s); if (filter === ONLY_ENUMERABLE && !v.enumerable) continue; - ArrayPrototypePush(ret, s); + ArrayPrototypePush.$call(ret, s); } return ret; } diff --git a/src/js/internal/validators.ts b/src/js/internal/validators.ts index a6612d6db0..2df37ba6ea 100644 --- a/src/js/internal/validators.ts +++ b/src/js/internal/validators.ts @@ -1,6 +1,10 @@ const { hideFromStack } = require("internal/shared"); -const { ArrayIsArray } = require("internal/primordials"); + const RegExpPrototypeExec = RegExp.prototype.exec; +const ArrayPrototypeIncludes = Array.prototype.includes; +const ArrayPrototypeJoin = Array.prototype.join; +const ArrayPrototypeMap = Array.prototype.map; +const ArrayIsArray = Array.isArray; const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; /** @@ -24,7 +28,9 @@ const linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/; function validateLinkHeaderFormat(value, name) { if (typeof value === "undefined" || !RegExpPrototypeExec.$call(linkValueRegExp, value)) { throw $ERR_INVALID_ARG_VALUE( - `The arguments ${name} is invalid must be an array or string of format "; rel=preload; as=style"`, + name, + value, + `must be an array or string of format "; rel=preload; as=style"`, ); } } @@ -55,7 +61,9 @@ function validateLinkHeaderValue(hints) { } throw $ERR_INVALID_ARG_VALUE( - `The arguments hints is invalid must be an array or string of format "; rel=preload; as=style"`, + "hints", + hints, + `must be an array or string of format "; rel=preload; as=style"`, ); } hideFromStack(validateLinkHeaderValue); @@ -65,6 +73,18 @@ function validateObject(value, name) { } hideFromStack(validateObject); +function validateOneOf(value, name, oneOf) { + if (!ArrayPrototypeIncludes.$call(oneOf, value)) { + const allowed = ArrayPrototypeJoin.$call( + ArrayPrototypeMap.$call(oneOf, v => (typeof v === "string" ? `'${v}'` : String(v))), + ", ", + ); + const reason = "must be one of: " + allowed; + throw $ERR_INVALID_ARG_VALUE(name, value, reason); + } +} +hideFromStack(validateOneOf); + export default { validateObject: validateObject, validateLinkHeaderValue: validateLinkHeaderValue, @@ -103,4 +123,6 @@ export default { validateUndefined: $newCppFunction("NodeValidator.cpp", "jsFunction_validateUndefined", 0), /** `(buffer, name = 'buffer')` */ validateBuffer: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0), + /** `(value, name, oneOf)` */ + validateOneOf, }; diff --git a/src/js/internal/webstreams_adapters.ts b/src/js/internal/webstreams_adapters.ts new file mode 100644 index 0000000000..e8931e1ccb --- /dev/null +++ b/src/js/internal/webstreams_adapters.ts @@ -0,0 +1,785 @@ +"use strict"; + +const { + SafePromiseAll, + SafeSet, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteOffset, + TypedArrayPrototypeGetByteLength, +} = require("internal/primordials"); + +const Writable = require("internal/streams/writable"); +const Readable = require("internal/streams/readable"); +const Duplex = require("internal/streams/duplex"); +const { destroyer } = require("internal/streams/destroy"); +const { isDestroyed, isReadable, isWritable, isWritableEnded } = require("internal/streams/utils"); +const { Buffer } = require("node:buffer"); +const { kEmptyObject, kGetNativeReadableProto } = require("internal/shared"); +const { validateBoolean, validateObject } = require("internal/validators"); +const finished = require("internal/streams/end-of-stream"); + +const normalizeEncoding = $newZigFunction("node_util_binding.zig", "normalizeEncoding", 1); + +const ArrayPrototypeFilter = Array.prototype.filter; +const ArrayPrototypeMap = Array.prototype.map; +const ObjectEntries = Object.entries; +const PromiseWithResolvers = Promise.withResolvers.bind(Promise); +const PromiseResolve = Promise.resolve.bind(Promise); +const PromisePrototypeThen = Promise.prototype.then; +const SafePromisePrototypeFinally = Promise.prototype.finally; + +const constants_zlib = process.binding("constants").zlib; + +// +// +const transferToNativeReadable = $newCppFunction("ReadableStream.cpp", "jsFunctionTransferToNativeReadableStream", 1); + +function getNativeReadableStream(Readable, stream, options) { + const ptr = stream.$bunNativePtr; + if (!ptr || ptr === -1) { + $debug("no native readable stream"); + return undefined; + } + const type = stream.$bunNativeType; + $assert(typeof type === "number", "Invalid native type"); + $assert(typeof ptr === "object", "Invalid native ptr"); + + const NativeReadable = require("node:stream")[kGetNativeReadableProto](type); + // https://github.com/oven-sh/bun/pull/12801 + // https://github.com/oven-sh/bun/issues/9555 + // There may be a ReadableStream.Strong handle to the ReadableStream. + // We can't update those handles to point to the NativeReadable from JS + // So we instead mark it as no longer usable, and create a new NativeReadable + transferToNativeReadable(stream); + + return new NativeReadable(ptr, options); +} + +class ReadableFromWeb extends Readable { + #reader; + #closed; + #pendingChunks; + #stream; + + constructor(options, stream) { + const { objectMode, highWaterMark, encoding, signal } = options; + super({ + objectMode, + highWaterMark, + encoding, + signal, + }); + this.#pendingChunks = []; + this.#reader = undefined; + this.#stream = stream; + this.#closed = false; + } + + #drainPending() { + var pendingChunks = this.#pendingChunks, + pendingChunksI = 0, + pendingChunksCount = pendingChunks.length; + + for (; pendingChunksI < pendingChunksCount; pendingChunksI++) { + const chunk = pendingChunks[pendingChunksI]; + pendingChunks[pendingChunksI] = undefined; + if (!this.push(chunk, undefined)) { + this.#pendingChunks = pendingChunks.slice(pendingChunksI + 1); + return true; + } + } + + if (pendingChunksCount > 0) { + this.#pendingChunks = []; + } + + return false; + } + + #handleDone(reader) { + reader.releaseLock(); + this.#reader = undefined; + this.#closed = true; + this.push(null); + return; + } + + async _read() { + $debug("ReadableFromWeb _read()", this.__id); + var stream = this.#stream, + reader = this.#reader; + if (stream) { + reader = this.#reader = stream.getReader(); + this.#stream = undefined; + } else if (this.#drainPending()) { + return; + } + + var deferredError; + try { + do { + var done = false, + value; + const firstResult = reader.readMany(); + + if ($isPromise(firstResult)) { + ({ done, value } = await firstResult); + + if (this.#closed) { + this.#pendingChunks.push(...value); + return; + } + } else { + ({ done, value } = firstResult); + } + + if (done) { + this.#handleDone(reader); + return; + } + + if (!this.push(value[0])) { + this.#pendingChunks = value.slice(1); + return; + } + + for (let i = 1, count = value.length; i < count; i++) { + if (!this.push(value[i])) { + this.#pendingChunks = value.slice(i + 1); + return; + } + } + } while (!this.#closed); + } catch (e) { + deferredError = e; + } finally { + if (deferredError) throw deferredError; + } + } + + _destroy(error, callback) { + if (!this.#closed) { + var reader = this.#reader; + if (reader) { + this.#reader = undefined; + reader.cancel(error).finally(() => { + this.#closed = true; + callback(error); + }); + } + + return; + } + try { + callback(error); + } catch (error) { + globalThis.reportError(error); + } + } +} +// +// + +const encoder = new TextEncoder(); + +// Collect all negative (error) ZLIB codes and Z_NEED_DICT +const ZLIB_FAILURES = new SafeSet([ + ...ArrayPrototypeFilter.$call( + ArrayPrototypeMap.$call(ObjectEntries(constants_zlib), ({ 0: code, 1: value }) => (value < 0 ? code : null)), + Boolean, + ), + "Z_NEED_DICT", +]); + +function handleKnownInternalErrors(cause: Error | null): Error | null { + switch (true) { + case cause?.code === "ERR_STREAM_PREMATURE_CLOSE": { + return $makeAbortError(undefined, { cause }); + } + case ZLIB_FAILURES.has(cause?.code): { + const error = new TypeError(undefined, { cause }); + error.code = cause.code; + return error; + } + default: + return cause; + } +} + +function newWritableStreamFromStreamWritable(streamWritable) { + // Not using the internal/streams/utils isWritableNodeStream utility + // here because it will return false if streamWritable is a Duplex + // whose writable option is false. For a Duplex that is not writable, + // we want it to pass this check but return a closed WritableStream. + // We check if the given stream is a stream.Writable or http.OutgoingMessage + const checkIfWritableOrOutgoingMessage = + streamWritable && typeof streamWritable?.write === "function" && typeof streamWritable?.on === "function"; + if (!checkIfWritableOrOutgoingMessage) { + throw $ERR_INVALID_ARG_TYPE("streamWritable", "stream.Writable", streamWritable); + } + + if (isDestroyed(streamWritable) || !isWritable(streamWritable)) { + const writable = new WritableStream(); + writable.close(); + return writable; + } + + const highWaterMark = streamWritable.writableHighWaterMark; + const strategy = streamWritable.writableObjectMode ? new CountQueuingStrategy({ highWaterMark }) : { highWaterMark }; + + let controller; + let backpressurePromise; + let closed; + + function onDrain() { + if (backpressurePromise !== undefined) backpressurePromise.resolve(); + } + + const cleanup = finished(streamWritable, error => { + error = handleKnownInternalErrors(error); + + cleanup(); + // This is a protection against non-standard, legacy streams + // that happen to emit an error event again after finished is called. + streamWritable.on("error", () => {}); + if (error != null) { + if (backpressurePromise !== undefined) backpressurePromise.reject(error); + // If closed is not undefined, the error is happening + // after the WritableStream close has already started. + // We need to reject it here. + if (closed !== undefined) { + closed.reject(error); + closed = undefined; + } + controller.error(error); + controller = undefined; + return; + } + + if (closed !== undefined) { + closed.resolve(); + closed = undefined; + return; + } + controller.error($makeAbortError()); + controller = undefined; + }); + + streamWritable.on("drain", onDrain); + + return new WritableStream( + { + start(c) { + controller = c; + }, + + write(chunk) { + if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) { + backpressurePromise = PromiseWithResolvers(); + return SafePromisePrototypeFinally.$call(backpressurePromise.promise, () => { + backpressurePromise = undefined; + }); + } + }, + + abort(reason) { + destroyer(streamWritable, reason); + }, + + close() { + if (closed === undefined && !isWritableEnded(streamWritable)) { + closed = PromiseWithResolvers(); + streamWritable.end(); + return closed.promise; + } + + controller = undefined; + return PromiseResolve(); + }, + }, + strategy, + ); +} + +function newStreamWritableFromWritableStream(writableStream, options = kEmptyObject) { + if (!$inheritsWritableStream(writableStream)) { + throw $ERR_INVALID_ARG_TYPE("writableStream", "WritableStream", writableStream); + } + + validateObject(options, "options"); + const { highWaterMark, decodeStrings = true, objectMode = false, signal } = options; + + validateBoolean(objectMode, "options.objectMode"); + validateBoolean(decodeStrings, "options.decodeStrings"); + + const writer = writableStream.getWriter(); + let closed = false; + + const writable = new Writable({ + highWaterMark, + objectMode, + decodeStrings, + signal, + + writev(chunks, callback) { + function done(error) { + error = error.filter(e => e); + try { + callback(error.length === 0 ? undefined : error); + } catch (error) { + // In a next tick because this is happening within + // a promise context, and if there are any errors + // thrown we don't want those to cause an unhandled + // rejection. Let's just escape the promise and + // handle it separately. + process.nextTick(() => destroyer(writable, error)); + } + } + + PromisePrototypeThen.$call( + writer.ready, + () => { + return PromisePrototypeThen.$call( + SafePromiseAll(chunks, data => writer.write(data.chunk)), + done, + done, + ); + }, + done, + ); + }, + + write(chunk, encoding, callback) { + if (typeof chunk === "string" && decodeStrings && !objectMode) { + const enc = normalizeEncoding(encoding); + + if (enc === "utf8") { + chunk = encoder.encode(chunk); + } else { + chunk = Buffer.from(chunk, encoding); + chunk = new Uint8Array( + TypedArrayPrototypeGetBuffer(chunk), + TypedArrayPrototypeGetByteOffset(chunk), + TypedArrayPrototypeGetByteLength(chunk), + ); + } + } + + function done(error) { + try { + callback(error); + } catch (error) { + destroyer(writable, error); + } + } + + PromisePrototypeThen.$call( + writer.ready, + () => { + return PromisePrototypeThen.$call(writer.write(chunk), done, done); + }, + done, + ); + }, + + destroy(error, callback) { + function done() { + try { + callback(error); + } catch (error) { + // In a next tick because this is happening within + // a promise context, and if there are any errors + // thrown we don't want those to cause an unhandled + // rejection. Let's just escape the promise and + // handle it separately. + process.nextTick(() => { + throw error; + }); + } + } + + if (!closed) { + if (error != null) { + PromisePrototypeThen.$call(writer.abort(error), done, done); + } else { + PromisePrototypeThen.$call(writer.close(), done, done); + } + return; + } + + done(); + }, + + final(callback) { + function done(error) { + try { + callback(error); + } catch (error) { + // In a next tick because this is happening within + // a promise context, and if there are any errors + // thrown we don't want those to cause an unhandled + // rejection. Let's just escape the promise and + // handle it separately. + process.nextTick(() => destroyer(writable, error)); + } + } + + if (!closed) { + PromisePrototypeThen.$call(writer.close(), done, done); + } + }, + }); + + PromisePrototypeThen.$call( + writer.closed, + () => { + // If the WritableStream closes before the stream.Writable has been + // ended, we signal an error on the stream.Writable. + closed = true; + if (!isWritableEnded(writable)) destroyer(writable, $ERR_STREAM_PREMATURE_CLOSE()); + }, + error => { + // If the WritableStream errors before the stream.Writable has been + // destroyed, signal an error on the stream.Writable. + closed = true; + destroyer(writable, error); + }, + ); + + return writable; +} + +function newReadableStreamFromStreamReadable(streamReadable, options = kEmptyObject) { + // Not using the internal/streams/utils isReadableNodeStream utility + // here because it will return false if streamReadable is a Duplex + // whose readable option is false. For a Duplex that is not readable, + // we want it to pass this check but return a closed ReadableStream. + if (typeof streamReadable?._readableState !== "object") { + throw $ERR_INVALID_ARG_TYPE("streamReadable", "stream.Readable", streamReadable); + } + + if (isDestroyed(streamReadable) || !isReadable(streamReadable)) { + const readable = new ReadableStream(); + readable.cancel(); + return readable; + } + + const objectMode = streamReadable.readableObjectMode; + const highWaterMark = streamReadable.readableHighWaterMark; + + const evaluateStrategyOrFallback = strategy => { + // If there is a strategy available, use it + if (strategy) return strategy; + + if (objectMode) { + // When running in objectMode explicitly but no strategy, we just fall + // back to CountQueuingStrategy + return new CountQueuingStrategy({ highWaterMark }); + } + + return new ByteLengthQueuingStrategy({ highWaterMark }); + }; + + const strategy = evaluateStrategyOrFallback(options?.strategy); + + let controller; + let wasCanceled = false; + + function onData(chunk) { + // Copy the Buffer to detach it from the pool. + if (Buffer.isBuffer(chunk) && !objectMode) chunk = new Uint8Array(chunk); + controller.enqueue(chunk); + if (controller.desiredSize <= 0) streamReadable.pause(); + } + + streamReadable.pause(); + + const cleanup = finished(streamReadable, error => { + error = handleKnownInternalErrors(error); + + cleanup(); + // This is a protection against non-standard, legacy streams + // that happen to emit an error event again after finished is called. + streamReadable.on("error", () => {}); + if (error) return controller.error(error); + // Was already canceled + if (wasCanceled) { + return; + } + controller.close(); + }); + + streamReadable.on("data", onData); + + return new ReadableStream( + { + start(c) { + controller = c; + }, + + pull() { + streamReadable.resume(); + }, + + cancel(reason) { + wasCanceled = true; + destroyer(streamReadable, reason); + }, + }, + strategy, + ); +} + +function newStreamReadableFromReadableStream(readableStream, options = kEmptyObject) { + if (!$inheritsReadableStream(readableStream)) { + throw $ERR_INVALID_ARG_TYPE("readableStream", "ReadableStream", readableStream); + } + + validateObject(options, "options"); + const { highWaterMark, encoding, objectMode = false, signal } = options; + + if (encoding !== undefined && !Buffer.isEncoding(encoding)) + throw $ERR_INVALID_ARG_VALUE("options.encoding", encoding); + validateBoolean(objectMode, "options.objectMode"); + + const nativeStream = getNativeReadableStream(Readable, readableStream, options); + + return ( + nativeStream || + new ReadableFromWeb( + { + highWaterMark, + encoding, + objectMode, + signal, + }, + readableStream, + ) + ); +} + +function newReadableWritablePairFromDuplex(duplex) { + // Not using the internal/streams/utils isWritableNodeStream and + // isReadableNodeStream utilities here because they will return false + // if the duplex was created with writable or readable options set to + // false. Instead, we'll check the readable and writable state after + // and return closed WritableStream or closed ReadableStream as + // necessary. + if (typeof duplex?._writableState !== "object" || typeof duplex?._readableState !== "object") { + throw $ERR_INVALID_ARG_TYPE("duplex", "stream.Duplex", duplex); + } + + if (isDestroyed(duplex)) { + const writable = new WritableStream(); + const readable = new ReadableStream(); + writable.close(); + readable.cancel(); + return { readable, writable }; + } + + const writable = isWritable(duplex) ? newWritableStreamFromStreamWritable(duplex) : new WritableStream(); + + if (!isWritable(duplex)) writable.close(); + + const readable = isReadable(duplex) ? newReadableStreamFromStreamReadable(duplex) : new ReadableStream(); + + if (!isReadable(duplex)) readable.cancel(); + + return { writable, readable }; +} + +function newStreamDuplexFromReadableWritablePair(pair = kEmptyObject, options = kEmptyObject) { + validateObject(pair, "pair"); + const { readable: readableStream, writable: writableStream } = pair; + + if (!$inheritsReadableStream(readableStream)) { + throw $ERR_INVALID_ARG_TYPE("pair.readable", "ReadableStream", readableStream); + } + if (!$inheritsWritableStream(writableStream)) { + throw $ERR_INVALID_ARG_TYPE("pair.writable", "WritableStream", writableStream); + } + + validateObject(options, "options"); + const { allowHalfOpen = false, objectMode = false, encoding, decodeStrings = true, highWaterMark, signal } = options; + + validateBoolean(objectMode, "options.objectMode"); + if (encoding !== undefined && !Buffer.isEncoding(encoding)) + throw $ERR_INVALID_ARG_VALUE(encoding, "options.encoding"); + + const writer = writableStream.getWriter(); + const reader = readableStream.getReader(); + let writableClosed = false; + let readableClosed = false; + + const duplex = new Duplex({ + allowHalfOpen, + highWaterMark, + objectMode, + encoding, + decodeStrings, + signal, + + writev(chunks, callback) { + function done(error) { + error = error.filter(e => e); + try { + callback(error.length === 0 ? undefined : error); + } catch (error) { + // In a next tick because this is happening within + // a promise context, and if there are any errors + // thrown we don't want those to cause an unhandled + // rejection. Let's just escape the promise and + // handle it separately. + process.nextTick(() => destroyer(duplex, error)); + } + } + + PromisePrototypeThen.$call( + writer.ready, + () => { + return PromisePrototypeThen.$call( + SafePromiseAll(chunks, data => writer.write(data.chunk)), + done, + done, + ); + }, + done, + ); + }, + + write(chunk, encoding, callback) { + if (typeof chunk === "string" && decodeStrings && !objectMode) { + const enc = normalizeEncoding(encoding); + + if (enc === "utf8") { + chunk = encoder.encode(chunk); + } else { + chunk = Buffer.from(chunk, encoding); + chunk = new Uint8Array( + TypedArrayPrototypeGetBuffer(chunk), + TypedArrayPrototypeGetByteOffset(chunk), + TypedArrayPrototypeGetByteLength(chunk), + ); + } + } + + function done(error) { + try { + callback(error); + } catch (error) { + destroyer(duplex, error); + } + } + + PromisePrototypeThen.$call( + writer.ready, + () => { + return PromisePrototypeThen.$call(writer.write(chunk), done, done); + }, + done, + ); + }, + + final(callback) { + function done(error) { + try { + callback(error); + } catch (error) { + // In a next tick because this is happening within + // a promise context, and if there are any errors + // thrown we don't want those to cause an unhandled + // rejection. Let's just escape the promise and + // handle it separately. + process.nextTick(() => destroyer(duplex, error)); + } + } + + if (!writableClosed) { + PromisePrototypeThen.$call(writer.close(), done, done); + } + }, + + read() { + PromisePrototypeThen.$call( + reader.read(), + chunk => { + if (chunk.done) { + duplex.push(null); + } else { + duplex.push(chunk.value); + } + }, + error => destroyer(duplex, error), + ); + }, + + destroy(error, callback) { + function done() { + try { + callback(error); + } catch (error) { + // In a next tick because this is happening within + // a promise context, and if there are any errors + // thrown we don't want those to cause an unhandled + // rejection. Let's just escape the promise and + // handle it separately. + process.nextTick(() => { + throw error; + }); + } + } + + async function closeWriter() { + if (!writableClosed) await writer.abort(error); + } + + async function closeReader() { + if (!readableClosed) await reader.cancel(error); + } + + if (!writableClosed || !readableClosed) { + PromisePrototypeThen.$call(SafePromiseAll([closeWriter(), closeReader()]), done, done); + return; + } + + done(); + }, + }); + + PromisePrototypeThen.$call( + writer.closed, + () => { + writableClosed = true; + if (!isWritableEnded(duplex)) destroyer(duplex, $ERR_STREAM_PREMATURE_CLOSE()); + }, + error => { + writableClosed = true; + readableClosed = true; + destroyer(duplex, error); + }, + ); + + PromisePrototypeThen.$call( + reader.closed, + () => { + readableClosed = true; + }, + error => { + writableClosed = true; + readableClosed = true; + destroyer(duplex, error); + }, + ); + + return duplex; +} + +export default { + newWritableStreamFromStreamWritable, + newReadableStreamFromStreamReadable, + newStreamWritableFromWritableStream, + newStreamReadableFromReadableStream, + newReadableWritablePairFromDuplex, + newStreamDuplexFromReadableWritablePair, + _ReadableFromWeb: ReadableFromWeb, +}; diff --git a/src/js/node/_stream_duplex.ts b/src/js/node/_stream_duplex.ts new file mode 100644 index 0000000000..ab61146db8 --- /dev/null +++ b/src/js/node/_stream_duplex.ts @@ -0,0 +1,3 @@ +"use strict"; + +export default require("internal/streams/duplex"); diff --git a/src/js/node/_stream_passthrough.ts b/src/js/node/_stream_passthrough.ts new file mode 100644 index 0000000000..357e647150 --- /dev/null +++ b/src/js/node/_stream_passthrough.ts @@ -0,0 +1,3 @@ +"use strict"; + +export default require("internal/streams/passthrough"); diff --git a/src/js/node/_stream_readable.ts b/src/js/node/_stream_readable.ts new file mode 100644 index 0000000000..b429f60158 --- /dev/null +++ b/src/js/node/_stream_readable.ts @@ -0,0 +1,3 @@ +"use strict"; + +export default require("internal/streams/readable"); diff --git a/src/js/node/_stream_transform.ts b/src/js/node/_stream_transform.ts new file mode 100644 index 0000000000..3ae986e613 --- /dev/null +++ b/src/js/node/_stream_transform.ts @@ -0,0 +1,3 @@ +"use strict"; + +export default require("internal/streams/transform"); diff --git a/src/js/node/_stream_wrap.ts b/src/js/node/_stream_wrap.ts new file mode 100644 index 0000000000..c754d0c71c --- /dev/null +++ b/src/js/node/_stream_wrap.ts @@ -0,0 +1,5 @@ +"use strict"; + +process.emitWarning("The _stream_wrap module is deprecated.", "DeprecationWarning", "DEP0125"); + +export default require("node:stream"); diff --git a/src/js/node/_stream_writable.ts b/src/js/node/_stream_writable.ts new file mode 100644 index 0000000000..7101582faa --- /dev/null +++ b/src/js/node/_stream_writable.ts @@ -0,0 +1,3 @@ +"use strict"; + +export default require("internal/streams/writable"); diff --git a/src/js/node/assert.ts b/src/js/node/assert.ts index e5343bd18e..4c12a84626 100644 --- a/src/js/node/assert.ts +++ b/src/js/node/assert.ts @@ -1,1271 +1,991 @@ -// Hardcoded module "node:assert" -const util = require("node:util"); +// Copied from Node.js (src/lib/assert.js) +// Originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the 'Software'), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -var isDeepEqual = Bun.deepEquals; -var __commonJS = (cb, mod: typeof module | undefined = undefined) => - function () { - return mod || (0, cb[Object.keys(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; - }; +"use strict"; -// assert/build/internal/errors.js -var require_errors = __commonJS({ - "assert/build/internal/errors.js"(exports, module2) { - "use strict"; - function _typeof(obj) { - return ( - typeof Symbol == "function" && typeof Symbol.iterator == "symbol" - ? (_typeof = function (obj2) { - return typeof obj2; - }) - : (_typeof = function (obj2) { - return obj2 && typeof Symbol == "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype - ? "symbol" - : typeof obj2; - }), - _typeof(obj) - ); - } - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); - } - function _possibleConstructorReturn(self, call) { - return call && (_typeof(call) === "object" || typeof call == "function") ? call : _assertThisInitialized(self); - } - function _assertThisInitialized(self) { - if (self === void 0) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - return self; - } - function _getPrototypeOf(o) { - return ( - (_getPrototypeOf = Object.setPrototypeOf - ? Object.getPrototypeOf - : function (o2) { - return o2.__proto__ || Object.getPrototypeOf(o2); - }), - _getPrototypeOf(o) - ); - } - function _inherits(subClass, superClass) { - if (typeof superClass != "function" && superClass !== null) - throw new TypeError("Super expression must either be null or a function"); - (subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { value: subClass, writable: !0, configurable: !0 }, - })), - superClass && _setPrototypeOf(subClass, superClass); - } - function _setPrototypeOf(o, p) { - return ( - (_setPrototypeOf = - Object.setPrototypeOf || - function (o2, p2) { - return (o2.__proto__ = p2), o2; - }), - _setPrototypeOf(o, p) - ); - } - var codes = {}, - assert, - util; - function createErrorType(code, message, Base) { - Base || (Base = Error); - function getMessage(arg1, arg2, arg3) { - return typeof message == "string" ? message : message(arg1, arg2, arg3); - } - var NodeError = /* @__PURE__ */ (function (_Base) { - _inherits(NodeError2, _Base); - function NodeError2(arg1, arg2, arg3) { - var _this; - return ( - _classCallCheck(this, NodeError2), - (_this = _possibleConstructorReturn( - this, - _getPrototypeOf(NodeError2).$call(this, getMessage(arg1, arg2, arg3)), - )), - (_this.code = code), - _this - ); - } - return NodeError2; - })(Base); - codes[code] = NodeError; - } - function oneOf(expected, thing) { - if (Array.isArray(expected)) { - var len = expected.length; - return ( - (expected = expected.map(function (i) { - return String(i); - })), - len > 2 - ? "one of ".concat(thing, " ").concat(expected.slice(0, len - 1).join(", "), ", or ") + expected[len - 1] - : len === 2 - ? "one of ".concat(thing, " ").concat(expected[0], " or ").concat(expected[1]) - : "of ".concat(thing, " ").concat(expected[0]) - ); - } else return "of ".concat(thing, " ").concat(String(expected)); - } - function startsWith(str, search, pos) { - return str.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; - } - function endsWith(str, search, this_len) { - return ( - (this_len === void 0 || this_len > str.length) && (this_len = str.length), - str.substring(this_len - search.length, this_len) === search - ); - } - function includes(str, search, start) { - return ( - typeof start != "number" && (start = 0), - start + search.length > str.length ? !1 : str.indexOf(search, start) !== -1 - ); - } - createErrorType("ERR_AMBIGUOUS_ARGUMENT", 'The "%s" argument is ambiguous. %s', TypeError); - createErrorType( - "ERR_INVALID_ARG_TYPE", - function (name, expected, actual) { - assert === void 0 && (assert = require_assert()), assert(typeof name == "string", "'name' must be a string"); - var determiner; - typeof expected == "string" && startsWith(expected, "not ") - ? ((determiner = "must not be"), (expected = expected.replace(/^not /, ""))) - : (determiner = "must be"); - var msg; - if (endsWith(name, " argument")) - msg = "The ".concat(name, " ").concat(determiner, " ").concat(oneOf(expected, "type")); - else { - var type = includes(name, ".") ? "property" : "argument"; - msg = 'The "'.concat(name, '" ').concat(type, " ").concat(determiner, " ").concat(oneOf(expected, "type")); - } - return (msg += ". Received type ".concat(_typeof(actual))), msg; - }, - TypeError, - ); - createErrorType( - "ERR_INVALID_ARG_VALUE", - function (name, value) { - var reason = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : "is invalid"; - var inspected = util.inspect(value); - return ( - inspected.length > 128 && (inspected = "".concat(inspected.slice(0, 128), "...")), - "The argument '".concat(name, "' ").concat(reason, ". Received ").concat(inspected) - ); - }, - TypeError, - RangeError, - ); - createErrorType( - "ERR_INVALID_RETURN_VALUE", - function (input, name, value) { - var type; - return ( - value && value.constructor && value.constructor.name - ? (type = "instance of ".concat(value.constructor.name)) - : (type = "type ".concat(_typeof(value))), - "Expected ".concat(input, ' to be returned from the "').concat(name, '"') + - " function but got ".concat(type, ".") - ); - }, - TypeError, - ); - createErrorType( - "ERR_MISSING_ARGS", - function () { - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) - args[_key] = arguments[_key]; - assert === void 0 && (assert = require_assert()), - assert(args.length > 0, "At least one arg needs to be specified"); - var msg = "The ", - len = args.length; - switch ( - ((args = args.map(function (a) { - return '"'.concat(a, '"'); - })), - len) - ) { - case 1: - msg += "".concat(args[0], " argument"); - break; - case 2: - msg += "".concat(args[0], " and ").concat(args[1], " arguments"); - break; - default: - (msg += args.slice(0, len - 1).join(", ")), (msg += ", and ".concat(args[len - 1], " arguments")); - break; - } - return "".concat(msg, " must be specified"); - }, - TypeError, - ); - module2.exports.codes = codes; - }, -}); +const { SafeMap, SafeSet, SafeWeakSet } = require("internal/primordials"); +const { Buffer } = require("node:buffer"); +const { isKeyObject, isPromise, isRegExp, isMap, isSet, isDate, isWeakSet, isWeakMap } = require("node:util/types"); +const { innerOk } = require("internal/assert/utils"); +const { validateFunction } = require("internal/validators"); -// assert/build/internal/assert/assertion_error.js -var require_assertion_error = __commonJS({ - "assert/build/internal/assert/assertion_error.js"(exports, module2) { - "use strict"; - function _objectSpread(target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i] != null ? arguments[i] : {}, - ownKeys = Object.keys(source); - typeof Object.getOwnPropertySymbols == "function" && - (ownKeys = ownKeys.concat( - Object.getOwnPropertySymbols(source).filter(function (sym) { - return Object.getOwnPropertyDescriptor(source, sym).enumerable; - }), - )), - ownKeys.forEach(function (key) { - _defineProperty(target, key, source[key]); - }); - } - return target; - } - function _defineProperty(obj, key, value) { - return ( - key in obj - ? Object.defineProperty(obj, key, { - value, - enumerable: !0, - configurable: !0, - writable: !0, - }) - : (obj[key] = value), - obj - ); - } - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); - } - function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - (descriptor.enumerable = descriptor.enumerable || !1), - (descriptor.configurable = !0), - "value" in descriptor && (descriptor.writable = !0), - Object.defineProperty(target, descriptor.key, descriptor); - } - } - function _createClass(Constructor, protoProps, staticProps) { - return ( - protoProps && _defineProperties(Constructor.prototype, protoProps), - staticProps && _defineProperties(Constructor, staticProps), - Constructor - ); - } - function _possibleConstructorReturn(self, call) { - return call && (_typeof(call) === "object" || typeof call == "function") ? call : _assertThisInitialized(self); - } - function _assertThisInitialized(self) { - if (self === void 0) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - return self; - } - function _inherits(subClass, superClass) { - if (typeof superClass != "function" && superClass !== null) - throw new TypeError("Super expression must either be null or a function"); - (subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { value: subClass, writable: !0, configurable: !0 }, - })), - superClass && _setPrototypeOf(subClass, superClass); - } - function _wrapNativeSuper(Class) { - var _cache = typeof Map == "function" ? new Map() : void 0; - return ( - (_wrapNativeSuper = function (Class2) { - if (Class2 === null || !_isNativeFunction(Class2)) return Class2; - if (typeof Class2 != "function") throw new TypeError("Super expression must either be null or a function"); - if (typeof _cache != "undefined") { - if (_cache.has(Class2)) return _cache.get(Class2); - _cache.set(Class2, Wrapper); - } - function Wrapper() { - return _construct(Class2, arguments, _getPrototypeOf(this).constructor); - } - return ( - (Wrapper.prototype = Object.create(Class2.prototype, { - constructor: { - value: Wrapper, - enumerable: !1, - writable: !0, - configurable: !0, - }, - })), - _setPrototypeOf(Wrapper, Class2) - ); - }), - _wrapNativeSuper(Class) - ); - } - function isNativeReflectConstruct() { - if (typeof Reflect == "undefined" || !Reflect.construct || Reflect.construct.sham) return !1; - if (typeof Proxy == "function") return !0; - try { - return Date.prototype.toString.$call(Reflect.construct(Date, [], function () {})), !0; - } catch { - return !1; - } - } - function _construct(Parent, args, Class) { - return ( - isNativeReflectConstruct() - ? (_construct = Reflect.construct) - : (_construct = function (Parent2, args2, Class2) { - var a = [null]; - a.push.$apply(a, args2); - var Constructor = Function.bind.$apply(Parent2, a), - instance = new Constructor(); - return Class2 && _setPrototypeOf(instance, Class2.prototype), instance; - }), - _construct.$apply(null, arguments) - ); - } - function _isNativeFunction(fn) { - return Function.toString.$call(fn).indexOf("[native code]") !== -1; - } - function _setPrototypeOf(o, p) { - return ( - (_setPrototypeOf = - Object.setPrototypeOf || - function (o2, p2) { - return (o2.__proto__ = p2), o2; - }), - _setPrototypeOf(o, p) - ); - } - function _getPrototypeOf(o) { - return ( - (_getPrototypeOf = Object.setPrototypeOf - ? Object.getPrototypeOf - : function (o2) { - return o2.__proto__ || Object.getPrototypeOf(o2); - }), - _getPrototypeOf(o) - ); - } - function _typeof(obj) { - return ( - typeof Symbol == "function" && typeof Symbol.iterator == "symbol" - ? (_typeof = function (obj2) { - return typeof obj2; - }) - : (_typeof = function (obj2) { - return obj2 && typeof Symbol == "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype - ? "symbol" - : typeof obj2; - }), - _typeof(obj) - ); - } - var inspect = util.inspect, - _require2 = require_errors(), - ERR_INVALID_ARG_TYPE = _require2.codes.ERR_INVALID_ARG_TYPE; - function endsWith(str, search, this_len) { - return ( - (this_len === void 0 || this_len > str.length) && (this_len = str.length), - str.substring(this_len - search.length, this_len) === search - ); - } - function repeat(str, count) { - if (((count = Math.floor(count)), str.length == 0 || count == 0)) return ""; - var maxCount = str.length * count; - for (count = Math.floor(Math.log(count) / Math.log(2)); count; ) (str += str), count--; - return (str += str.substring(0, maxCount - str.length)), str; - } - var blue = "", - green = "", - red = "", - white = "", - kReadableOperator = { - deepStrictEqual: "Expected values to be strictly deep-equal:", - strictEqual: "Expected values to be strictly equal:", - strictEqualObject: 'Expected "actual" to be reference-equal to "expected":', - deepEqual: "Expected values to be loosely deep-equal:", - equal: "Expected values to be loosely equal:", - notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:', - notStrictEqual: 'Expected "actual" to be strictly unequal to:', - notStrictEqualObject: 'Expected "actual" not to be reference-equal to "expected":', - notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:', - notEqual: 'Expected "actual" to be loosely unequal to:', - notIdentical: "Values identical but not reference-equal:", - }, - kMaxShortLength = 10; - function copyError(source) { - var keys = Object.keys(source), - target = Object.create(Object.getPrototypeOf(source)); - return ( - keys.forEach(function (key) { - target[key] = source[key]; - }), - Object.defineProperty(target, "message", { - value: source.message, - }), - target - ); - } - function inspectValue(val) { - return inspect(val, { - compact: !1, - customInspect: !1, - depth: 1e3, - maxArrayLength: 1 / 0, - showHidden: !1, - breakLength: 1 / 0, - showProxy: !1, - sorted: !0, - getters: !0, - }); - } - function createErrDiff(actual, expected, operator) { - var other = "", - res = "", - lastPos = 0, - end = "", - skipped = !1, - actualInspected = inspectValue(actual), - actualLines = actualInspected.split(` -`), - expectedLines = inspectValue(expected).split(` -`), - i = 0, - indicator = ""; - if ( - (operator === "strictEqual" && - _typeof(actual) === "object" && - _typeof(expected) === "object" && - actual !== null && - expected !== null && - (operator = "strictEqualObject"), - actualLines.length === 1 && expectedLines.length === 1 && actualLines[0] !== expectedLines[0]) - ) { - var inputLength = actualLines[0].length + expectedLines[0].length; - if (inputLength <= kMaxShortLength) { - if ( - (_typeof(actual) !== "object" || actual === null) && - (_typeof(expected) !== "object" || expected === null) && - (actual !== 0 || expected !== 0) - ) - return ( - "".concat( - kReadableOperator[operator], - ` +const ArrayFrom = Array.from; +const ArrayPrototypeIndexOf = Array.prototype.indexOf; +const ArrayPrototypeJoin = Array.prototype.join; +const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeSlice = Array.prototype.slice; +const NumberIsNaN = Number.isNaN; +const ObjectAssign = Object.assign; +const ObjectIs = Object.is; +const ObjectKeys = Object.keys; +const ObjectPrototypeIsPrototypeOf = Object.prototype.isPrototypeOf; +const ReflectHas = Reflect.has; +const ReflectOwnKeys = Reflect.ownKeys; +const RegExpPrototypeExec = RegExp.prototype.exec; +const StringPrototypeIndexOf = String.prototype.indexOf; +const StringPrototypeSlice = String.prototype.slice; +const StringPrototypeSplit = String.prototype.split; +const SymbolIterator = Symbol.iterator; -`, - ) + - "".concat(actualLines[0], " !== ").concat( - expectedLines[0], - ` -`, - ) - ); - } else if (operator !== "strictEqualObject") { - var maxLength = process.stderr && process.stderr.isTTY ? process.stderr.columns : 80; - if (inputLength < maxLength) { - for (; actualLines[0][i] === expectedLines[0][i]; ) i++; - i > 2 && - ((indicator = ` - `.concat(repeat(" ", i), "^")), - (i = 0)); - } - } - } - for ( - var a = actualLines[actualLines.length - 1], b = expectedLines[expectedLines.length - 1]; - a === b && - (i++ < 2 - ? (end = ` - ` - .concat(a) - .concat(end)) - : (other = a), - actualLines.pop(), - expectedLines.pop(), - !(actualLines.length === 0 || expectedLines.length === 0)); +type nodeAssert = typeof import("node:assert"); - ) - (a = actualLines[actualLines.length - 1]), (b = expectedLines[expectedLines.length - 1]); - var maxLines = Math.max(actualLines.length, expectedLines.length); - if (maxLines === 0) { - var _actualLines = actualInspected.split(` -`); - if (_actualLines.length > 30) - for (_actualLines[26] = "".concat(blue, "...").concat(white); _actualLines.length > 27; ) _actualLines.pop(); - return "" - .concat( - kReadableOperator.notIdentical, - ` - -`, - ) - .concat( - _actualLines.join(` -`), - ` -`, - ); - } - i > 3 && - ((end = ` -` - .concat(blue, "...") - .concat(white) - .concat(end)), - (skipped = !0)), - other !== "" && - ((end = ` - ` - .concat(other) - .concat(end)), - (other = "")); - var printedLines = 0, - msg = - kReadableOperator[operator] + - ` -` - .concat(green, "+ actual") - .concat(white, " ") - .concat(red, "- expected") - .concat(white), - skippedMsg = " ".concat(blue, "...").concat(white, " Lines skipped"); - for (i = 0; i < maxLines; i++) { - var cur = i - lastPos; - if (actualLines.length < i + 1) - cur > 1 && - i > 2 && - (cur > 4 - ? ((res += ` -` - .concat(blue, "...") - .concat(white)), - (skipped = !0)) - : cur > 3 && - ((res += ` - `.concat(expectedLines[i - 2])), - printedLines++), - (res += ` - `.concat(expectedLines[i - 1])), - printedLines++), - (lastPos = i), - (other += ` -` - .concat(red, "-") - .concat(white, " ") - .concat(expectedLines[i])), - printedLines++; - else if (expectedLines.length < i + 1) - cur > 1 && - i > 2 && - (cur > 4 - ? ((res += ` -` - .concat(blue, "...") - .concat(white)), - (skipped = !0)) - : cur > 3 && - ((res += ` - `.concat(actualLines[i - 2])), - printedLines++), - (res += ` - `.concat(actualLines[i - 1])), - printedLines++), - (lastPos = i), - (res += ` -` - .concat(green, "+") - .concat(white, " ") - .concat(actualLines[i])), - printedLines++; - else { - var expectedLine = expectedLines[i], - actualLine = actualLines[i], - divergingLines = - actualLine !== expectedLine && (!endsWith(actualLine, ",") || actualLine.slice(0, -1) !== expectedLine); - divergingLines && - endsWith(expectedLine, ",") && - expectedLine.slice(0, -1) === actualLine && - ((divergingLines = !1), (actualLine += ",")), - divergingLines - ? (cur > 1 && - i > 2 && - (cur > 4 - ? ((res += ` -` - .concat(blue, "...") - .concat(white)), - (skipped = !0)) - : cur > 3 && - ((res += ` - `.concat(actualLines[i - 2])), - printedLines++), - (res += ` - `.concat(actualLines[i - 1])), - printedLines++), - (lastPos = i), - (res += ` -` - .concat(green, "+") - .concat(white, " ") - .concat(actualLine)), - (other += ` -` - .concat(red, "-") - .concat(white, " ") - .concat(expectedLine)), - (printedLines += 2)) - : ((res += other), - (other = ""), - (cur === 1 || i === 0) && - ((res += ` - `.concat(actualLine)), - printedLines++)); - } - if (printedLines > 20 && i < maxLines - 2) - return ( - "" - .concat(msg) - .concat( - skippedMsg, - ` -`, - ) - .concat( - res, - ` -`, - ) - .concat(blue, "...") - .concat(white) - .concat( - other, - ` -`, - ) + "".concat(blue, "...").concat(white) - ); - } - return "" - .concat(msg) - .concat( - skipped ? skippedMsg : "", - ` -`, - ) - .concat(res) - .concat(other) - .concat(end) - .concat(indicator); - } - var AssertionError = /* @__PURE__ */ (function (_Error) { - function AssertionError2(options) { - var _this; - if ((_classCallCheck(this, AssertionError2), _typeof(options) !== "object" || options === null)) - throw new ERR_INVALID_ARG_TYPE("options", "object", options); - var message = options.message, - operator = options.operator, - stackStartFn = options.stackStartFn, - actual = options.actual, - expected = options.expected, - limit = Error.stackTraceLimit; - if (((Error.stackTraceLimit = 0), message != null)) - _this = _possibleConstructorReturn(this, _getPrototypeOf(AssertionError2).$call(this, String(message))); - else if ( - (process.stderr && - process.stderr.isTTY && - (process.stderr && process.stderr.getColorDepth && process.stderr.getColorDepth() !== 1 - ? ((blue = ""), (green = ""), (white = ""), (red = "")) - : ((blue = ""), (green = ""), (white = ""), (red = ""))), - _typeof(actual) === "object" && - actual !== null && - _typeof(expected) === "object" && - expected !== null && - "stack" in actual && - actual instanceof Error && - "stack" in expected && - expected instanceof Error && - ((actual = copyError(actual)), (expected = copyError(expected))), - operator === "deepStrictEqual" || operator === "strictEqual") - ) - _this = _possibleConstructorReturn( - this, - _getPrototypeOf(AssertionError2).$call(this, createErrDiff(actual, expected, operator)), - ); - else if (operator === "notDeepStrictEqual" || operator === "notStrictEqual") { - var base = kReadableOperator[operator], - res = inspectValue(actual).split(` -`); - if ( - (operator === "notStrictEqual" && - _typeof(actual) === "object" && - actual !== null && - (base = kReadableOperator.notStrictEqualObject), - res.length > 30) - ) - for (res[26] = "".concat(blue, "...").concat(white); res.length > 27; ) res.pop(); - res.length === 1 - ? (_this = _possibleConstructorReturn( - this, - _getPrototypeOf(AssertionError2).$call(this, "".concat(base, " ").concat(res[0])), - )) - : (_this = _possibleConstructorReturn( - this, - _getPrototypeOf(AssertionError2).$call( - this, - "" - .concat( - base, - ` - -`, - ) - .concat( - res.join(` -`), - ` -`, - ), - ), - )); - } else { - var _res = inspectValue(actual), - other = "", - knownOperators = kReadableOperator[operator]; - operator === "notDeepEqual" || operator === "notEqual" - ? ((_res = "" - .concat( - kReadableOperator[operator], - ` - -`, - ) - .concat(_res)), - _res.length > 1024 && (_res = "".concat(_res.slice(0, 1021), "..."))) - : ((other = "".concat(inspectValue(expected))), - _res.length > 512 && (_res = "".concat(_res.slice(0, 509), "...")), - other.length > 512 && (other = "".concat(other.slice(0, 509), "...")), - operator === "deepEqual" || operator === "equal" - ? (_res = "" - .concat( - knownOperators, - ` - -`, - ) - .concat( - _res, - ` - -should equal - -`, - )) - : (other = " ".concat(operator, " ").concat(other))), - (_this = _possibleConstructorReturn( - this, - _getPrototypeOf(AssertionError2).$call(this, "".concat(_res).concat(other)), - )); - } - return ( - (Error.stackTraceLimit = limit), - (_this.generatedMessage = !message), - Object.defineProperty(_assertThisInitialized(_this), "name", { - value: "AssertionError [ERR_ASSERTION]", - enumerable: !1, - writable: !0, - configurable: !0, - }), - (_this.code = "ERR_ASSERTION"), - (_this.actual = actual), - (_this.expected = expected), - (_this.operator = operator), - Error.captureStackTrace && Error.captureStackTrace(_assertThisInitialized(_this), stackStartFn), - _this.stack, - (_this.name = "AssertionError"), - _possibleConstructorReturn(_this) - ); - } - AssertionError2.prototype = {}; - _inherits(AssertionError2, _Error); - return ( - _createClass(AssertionError2, [ - { - key: "toString", - value: function () { - return "".concat(this.name, " [").concat(this.code, "]: ").concat(this.message); - }, - }, - { - key: inspect.custom, - value: function (recurseTimes, ctx) { - return inspect( - this, - _objectSpread({}, ctx, { - customInspect: !1, - depth: 0, - }), - ); - }, - }, - ]), - AssertionError2 - ); - })(_wrapNativeSuper(Error)); - module2.exports = AssertionError; - }, -}); - -// assert/build/assert.js -var require_assert = __commonJS({ - "assert/build/assert.js"(exports, module2) { - "use strict"; - function _typeof(obj) { - return ( - typeof Symbol == "function" && typeof Symbol.iterator == "symbol" - ? (_typeof = function (obj2) { - return typeof obj2; - }) - : (_typeof = function (obj2) { - return obj2 && typeof Symbol == "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype - ? "symbol" - : typeof obj2; - }), - _typeof(obj) - ); - } - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); - } - - var _require = require_errors(), - _require$codes = _require.codes, - ERR_AMBIGUOUS_ARGUMENT = _require$codes.ERR_AMBIGUOUS_ARGUMENT, - ERR_INVALID_ARG_TYPE = _require$codes.ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE = _require$codes.ERR_INVALID_ARG_VALUE, - ERR_INVALID_RETURN_VALUE = _require$codes.ERR_INVALID_RETURN_VALUE, - ERR_MISSING_ARGS = _require$codes.ERR_MISSING_ARGS, - AssertionError = require_assertion_error(), - _require2 = util, - inspect = _require2.inspect, - _require$types = util.types, - isPromise = _require$types.isPromise, - isRegExp = _require$types.isRegExp, - objectAssign = Object.assign, - objectIs = Object.is, - errorCache = new Map(); - - var warned = !1, - assert = (module2.exports = ok), - NO_EXCEPTION_SENTINEL = {}; - function innerFail(obj) { - throw obj.message instanceof Error ? obj.message : new AssertionError(obj); - } - function fail(actual, expected, message, operator, stackStartFn) { - var argsLen = arguments.length, - internalMessage; - if (argsLen === 0) internalMessage = "Failed"; - else if (argsLen === 1) (message = actual), (actual = void 0); - else { - if (warned === !1) { - warned = !0; - var warn = process.emitWarning ? process.emitWarning : console.warn.bind(console); - warn( - "assert.fail() with more than one argument is deprecated. Please use assert.strictEqual() instead or only pass a message.", - "DeprecationWarning", - "DEP0094", - ); - } - argsLen === 2 && (operator = "!="); - } - if (message instanceof Error) throw message; - var errArgs = { - actual, - expected, - operator: operator === void 0 ? "fail" : operator, - stackStartFn: stackStartFn || fail, - }; - message !== void 0 && (errArgs.message = message); - var err = new AssertionError(errArgs); - throw (internalMessage && ((err.message = internalMessage), (err.generatedMessage = !0)), err); - } - assert.fail = fail; - assert.AssertionError = AssertionError; - function innerOk(fn, argLen, value, message) { - if (!value) { - var generatedMessage = !1; - if (argLen === 0) (generatedMessage = !0), (message = "No value argument passed to `assert.ok()`"); - else if (message instanceof Error) throw message; - var err = new AssertionError({ - actual: value, - expected: !0, - message, - operator: "==", - stackStartFn: fn, - }); - throw ((err.generatedMessage = generatedMessage), err); - } - } - function ok() { - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) - args[_key] = arguments[_key]; - innerOk.$apply(void 0, [ok, args.length].concat(args)); - } - assert.ok = ok; - assert.equal = function equal(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - actual != expected && - innerFail({ - actual, - expected, - message, - operator: "==", - stackStartFn: equal, - }); - }; - assert.notEqual = function notEqual(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - actual == expected && - innerFail({ - actual, - expected, - message, - operator: "!=", - stackStartFn: notEqual, - }); - }; - assert.deepEqual = function deepEqual(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - isDeepEqual(actual, expected, false) || - innerFail({ - actual, - expected, - message, - operator: "deepEqual", - stackStartFn: deepEqual, - }); - }; - assert.notDeepEqual = function notDeepEqual(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - isDeepEqual(actual, expected, false) && - innerFail({ - actual, - expected, - message, - operator: "notDeepEqual", - stackStartFn: notDeepEqual, - }); - }; - assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - - isDeepEqual(actual, expected, true) || - innerFail({ - actual, - expected, - message, - operator: "deepStrictEqual", - stackStartFn: deepStrictEqual, - }); - }; - assert.notDeepStrictEqual = notDeepStrictEqual; - function notDeepStrictEqual(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - - isDeepEqual(actual, expected, true) && - innerFail({ - actual, - expected, - message, - operator: "notDeepStrictEqual", - stackStartFn: notDeepStrictEqual, - }); - } - assert.strictEqual = function strictEqual(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - objectIs(actual, expected) || - innerFail({ - actual, - expected, - message, - operator: "strictEqual", - stackStartFn: strictEqual, - }); - }; - assert.notStrictEqual = function notStrictEqual(actual, expected, message) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - objectIs(actual, expected) && - innerFail({ - actual, - expected, - message, - operator: "notStrictEqual", - stackStartFn: notStrictEqual, - }); - }; - var internalMatch = function (actual, expected, message, fn) { - if (arguments.length < 2) throw new ERR_MISSING_ARGS("actual", "expected"); - if (typeof actual !== "string") throw new ERR_INVALID_ARG_TYPE("actual", "string", actual); - if (!isRegExp(expected)) throw new ERR_INVALID_ARG_TYPE("expected", "RegExp", expected); - var match = fn === assert.match; - expected.test(actual) === match || - innerFail({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); - }; - assert.doesNotMatch = function doesNotMatch(actual, expected, message) { - internalMatch(actual, expected, message, doesNotMatch); - }; - assert.match = function match(actual, expected, message) { - internalMatch(actual, expected, message, match); - }; - var Comparison = function Comparison2(obj, keys, actual) { - var _this = this; - _classCallCheck(this, Comparison2), - keys.forEach(function (key) { - key in obj && - (actual !== void 0 && typeof actual[key] == "string" && isRegExp(obj[key]) && obj[key].test(actual[key]) - ? (_this[key] = actual[key]) - : (_this[key] = obj[key])); - }); - }; - Comparison.prototype = {}; - function compareExceptionKey(actual, expected, key, message, keys, fn) { - if (!(key in actual) || !isDeepEqual(actual[key], expected[key], true)) { - if (!message) { - var a = new Comparison(actual, keys), - b = new Comparison(expected, keys, actual), - err = new AssertionError({ - actual: a, - expected: b, - operator: "deepStrictEqual", - stackStartFn: fn, - }); - throw ((err.actual = actual), (err.expected = expected), (err.operator = fn.name), err); - } - innerFail({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); - } - } - function expectedException(actual, expected, msg, fn) { - if (typeof expected != "function") { - if (isRegExp(expected)) return expected.test(actual); - if (arguments.length === 2) throw new ERR_INVALID_ARG_TYPE("expected", ["Function", "RegExp"], expected); - if (_typeof(actual) !== "object" || actual === null) { - var err = new AssertionError({ - actual, - expected, - message: msg, - operator: "deepStrictEqual", - stackStartFn: fn, - }); - throw ((err.operator = fn.name), err); - } - var keys = Object.keys(expected); - if (expected instanceof Error) keys.push("name", "message"); - else if (keys.length === 0) throw new ERR_INVALID_ARG_VALUE("error", expected, "may not be an empty object"); - return ( - keys.forEach(function (key) { - return ( - (typeof actual[key] == "string" && isRegExp(expected[key]) && expected[key].test(actual[key])) || - compareExceptionKey(actual, expected, key, msg, keys, fn) - ); - }), - !0 - ); - } - return expected.prototype !== void 0 && actual instanceof expected - ? !0 - : Error.isPrototypeOf(expected) - ? !1 - : expected.$call({}, actual) === !0; - } - function getActual(fn) { - if (typeof fn != "function") throw new ERR_INVALID_ARG_TYPE("fn", "function", fn); - try { - fn(); - } catch (e) { - return e; - } - return NO_EXCEPTION_SENTINEL; - } - function checkIsPromise(obj) { - return ( - isPromise(obj) || - (obj !== null && _typeof(obj) === "object" && typeof obj.then == "function" && typeof obj.catch == "function") - ); - } - function waitForActual(promiseFn) { - return Promise.resolve().then(function () { - var resultPromise; - if (typeof promiseFn == "function") { - if (((resultPromise = promiseFn()), !checkIsPromise(resultPromise))) - throw new ERR_INVALID_RETURN_VALUE("instance of Promise", "promiseFn", resultPromise); - } else if (checkIsPromise(promiseFn)) resultPromise = promiseFn; - else throw new ERR_INVALID_ARG_TYPE("promiseFn", ["Function", "Promise"], promiseFn); - return Promise.resolve() - .then(function () { - return resultPromise; - }) - .then(function () { - return NO_EXCEPTION_SENTINEL; - }) - .catch(function (e) { - return e; - }); - }); - } - function expectsError(stackStartFn, actual, error, message) { - if (typeof error == "string") { - if (arguments.length === 4) - throw new ERR_INVALID_ARG_TYPE("error", ["Object", "Error", "Function", "RegExp"], error); - if (_typeof(actual) === "object" && actual !== null) { - if (actual.message === error) - throw new ERR_AMBIGUOUS_ARGUMENT( - "error/message", - 'The error message "'.concat(actual.message, '" is identical to the message.'), - ); - } else if (actual === error) - throw new ERR_AMBIGUOUS_ARGUMENT( - "error/message", - 'The error "'.concat(actual, '" is identical to the message.'), - ); - (message = error), (error = void 0); - } else if (error != null && _typeof(error) !== "object" && typeof error != "function") - throw new ERR_INVALID_ARG_TYPE("error", ["Object", "Error", "Function", "RegExp"], error); - if (actual === NO_EXCEPTION_SENTINEL) { - var details = ""; - error && error.name && (details += " (".concat(error.name, ")")), - (details += message ? ": ".concat(message) : "."); - var fnType = stackStartFn.name === "rejects" ? "rejection" : "exception"; - innerFail({ - actual: void 0, - expected: error, - operator: stackStartFn.name, - message: "Missing expected ".concat(fnType).concat(details), - stackStartFn, - }); - } - if (error && !expectedException(actual, error, message, stackStartFn)) throw actual; - } - function expectsNoError(stackStartFn, actual, error, message) { - if (actual !== NO_EXCEPTION_SENTINEL) { - if ( - (typeof error == "string" && ((message = error), (error = void 0)), - !error || expectedException(actual, error)) - ) { - var details = message ? ": ".concat(message) : ".", - fnType = stackStartFn.name === "doesNotReject" ? "rejection" : "exception"; - innerFail({ - actual, - expected: error, - operator: stackStartFn.name, - message: - "Got unwanted ".concat(fnType).concat( - details, - ` -`, - ) + 'Actual message: "'.concat(actual && actual.message, '"'), - stackStartFn, - }); - } - throw actual; - } - } - assert.throws = function throws(promiseFn) { - for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) - args[_key2 - 1] = arguments[_key2]; - expectsError.$apply(void 0, [throws, getActual(promiseFn)].concat(args)); - }; - assert.rejects = function rejects(promiseFn) { - for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) - args[_key3 - 1] = arguments[_key3]; - return waitForActual(promiseFn).then(function (result) { - return expectsError.$apply(void 0, [rejects, result].concat(args)); - }); - }; - assert.doesNotThrow = function doesNotThrow(fn) { - for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) - args[_key4 - 1] = arguments[_key4]; - expectsNoError.$apply(void 0, [doesNotThrow, getActual(fn)].concat(args)); - }; - assert.doesNotReject = function doesNotReject(fn) { - for (var _len5 = arguments.length, args = new Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) - args[_key5 - 1] = arguments[_key5]; - return waitForActual(fn).then(function (result) { - return expectsNoError.$apply(void 0, [doesNotReject, result].concat(args)); - }); - }; - assert.ifError = function ifError(err) { - if (err != null) { - var message = "ifError got unwanted exception: "; - _typeof(err) === "object" && typeof err.message == "string" - ? err.message.length === 0 && err.constructor - ? (message += err.constructor.name) - : (message += err.message) - : (message += inspect(err)); - var newErr = new AssertionError({ - actual: err, - expected: null, - operator: "ifError", - message, - stackStartFn: ifError, - }), - origStack = err.stack; - if (typeof origStack == "string") { - var tmp2 = origStack.split(` -`); - tmp2.shift(); - for ( - var tmp1 = newErr.stack.split(` -`), - i = 0; - i < tmp2.length; - i++ - ) { - var pos = tmp1.indexOf(tmp2[i]); - if (pos !== -1) { - tmp1 = tmp1.slice(0, pos); - break; - } - } - newErr.stack = "" - .concat( - tmp1.join(` -`), - ` -`, - ) - .concat( - tmp2.join(` -`), - ); - } - throw newErr; - } - }; - function strict() { - for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) - args[_key6] = arguments[_key6]; - innerOk.$apply(void 0, [strict, args.length].concat(args)); - } - assert.strict = objectAssign(strict, assert, { - equal: assert.strictEqual, - deepEqual: assert.deepStrictEqual, - notEqual: assert.notStrictEqual, - notDeepEqual: assert.notDeepStrictEqual, - }); - assert.strict.strict = assert.strict; - }, -}); -var assert_module = require_assert(); - -function CallTracker() { - throw new Error("CallTracker is not supported yet"); +function isDeepEqual(a, b) { + return Bun.deepEquals(a, b, false); +} +function isDeepStrictEqual(a, b) { + return Bun.deepEquals(a, b, true); } -assert_module["CallTracker"] = CallTracker; +var _inspect; +function lazyInspect() { + if (_inspect === undefined) { + _inspect = require("internal/util/inspect").inspect; + } + return _inspect; +} -export default assert_module; +var AssertionError; +function loadAssertionError() { + if (AssertionError === undefined) { + AssertionError = require("internal/assert/assertion_error"); + } +} + +let warned = false; + +// The assert module provides functions that throw +// AssertionError's when particular conditions are not met. The +// assert module must conform to the following interface. + +const assert: nodeAssert = ok as any; +export default assert; + +const NO_EXCEPTION_SENTINEL = {}; + +// All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. + +function innerFail(obj) { + if (obj.message instanceof Error) throw obj.message; + + throw new AssertionError(obj); +} + +function fail(message?: string | Error): never; +/** @deprecated since v10.0.0 - use fail([message]) or other assert functions instead. */ +function fail( + actual: unknown, + expected: unknown, + message?: string | Error, + operator?: string, + // eslint-disable-next-line @typescript-eslint/ban-types + stackStartFn?: Function, +): never; +function fail( + actual: unknown, + expected: unknown, + message?: string | Error, + operator?: string, + stackStartFn?: Function, +) { + const argsLen = arguments.length; + + let internalMessage = false; + if (actual == null && argsLen <= 1) { + internalMessage = true; + message = "Failed"; + } else if (argsLen === 1) { + message = actual; + actual = undefined; + } else { + if (warned === false) { + warned = true; + process.emitWarning( + "assert.fail() with more than one argument is deprecated. " + + "Please use assert.strictEqual() instead or only pass a message.", + "DeprecationWarning", + "DEP0094", + ); + } + if (argsLen === 2) operator = "!="; + } + + if (message instanceof Error) throw message; + + const errArgs = { + actual, + expected, + operator: operator === undefined ? "fail" : operator, + stackStartFn: stackStartFn || fail, + message, + }; + if (AssertionError === undefined) loadAssertionError(); + const err = new AssertionError(errArgs); + if (internalMessage) { + err.generatedMessage = true; + } + throw err; +} + +assert.fail = fail; + +// The AssertionError is defined in internal/error. +assert.AssertionError = AssertionError; +Object.defineProperty(assert, "AssertionError", { + get() { + loadAssertionError(); + return AssertionError; + }, + set(value) { + AssertionError = value; + }, + configurable: true, + enumerable: true, +}); + +/** + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args + * @returns {void} + */ + +function ok(value: unknown, message?: string | Error): asserts value; +function ok(...args: unknown[]): void { + innerOk(ok, args.length, ...args); +} +assert.ok = ok; + +/** + * The equality assertion tests shallow, coercive equality with ==. + * @param actual + * @param expected + * @param message + * @returns {void} + */ +/* eslint-disable no-restricted-properties */ +assert.equal = function equal(actual: unknown, expected: unknown, message?: string | Error) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + // eslint-disable-next-line eqeqeq + // if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { + if (actual != expected && !(isNaN(actual) && isNaN(expected))) { + innerFail({ + actual, + expected, + message, + operator: "==", + stackStartFn: equal, + }); + } +}; + +/** + * The non-equality assertion tests for whether two objects are not + * equal with !=. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.notEqual = function notEqual(actual, expected, message) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + // eslint-disable-next-line eqeqeq + if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { + innerFail({ + actual, + expected, + message, + operator: "!=", + stackStartFn: notEqual, + }); + } +}; + +/** + * The deep equivalence assertion tests a deep equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.deepEqual = function deepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + if (!isDeepEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: "deepEqual", + stackStartFn: deepEqual, + }); + } +}; + +/** + * The deep non-equivalence assertion tests for any deep inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + if (isDeepEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: "notDeepEqual", + stackStartFn: notDeepEqual, + }); + } +}; +/* eslint-enable */ + +/** + * The deep strict equivalence assertion tests a deep strict equality + * relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + if (!isDeepStrictEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: "deepStrictEqual", + stackStartFn: deepStrictEqual, + }); + } +}; + +/** + * The deep strict non-equivalence assertion tests for any deep strict + * inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.notDeepStrictEqual = notDeepStrictEqual; +function notDeepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + if (isDeepStrictEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: "notDeepStrictEqual", + stackStartFn: notDeepStrictEqual, + }); + } +} + +/** + * The strict equivalence assertion tests a strict equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.strictEqual = function strictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + if (!ObjectIs(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: "strictEqual", + stackStartFn: strictEqual, + }); + } +}; + +/** + * The strict non-equivalence assertion tests for any strict inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + if (ObjectIs(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: "notStrictEqual", + stackStartFn: notStrictEqual, + }); + } +}; + +function isSpecial(obj) { + return obj == null || typeof obj !== "object" || Error.isError(obj) || isRegExp(obj) || isDate(obj); +} + +const typesToCallDeepStrictEqualWith = [isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer]; +const SafeSetPrototypeIterator = SafeSet.prototype[SymbolIterator]; + +/** + * Compares two objects or values recursively to check if they are equal. + * @param {any} actual - The actual value to compare. + * @param {any} expected - The expected value to compare. + * @param {Set} [comparedObjects=new Set()] - Set to track compared objects for handling circular references. + * @returns {boolean} - Returns `true` if the actual value matches the expected value, otherwise `false`. + * @example + * compareBranch({a: 1, b: 2, c: 3}, {a: 1, b: 2}); // true + */ +function compareBranch(actual, expected, comparedObjects) { + // Check for Map object equality + if (isMap(actual) && isMap(expected)) { + return Bun.deepEquals(actual, expected, true); + } + + for (const type of typesToCallDeepStrictEqualWith) { + if (type(actual) || type(expected)) { + return isDeepStrictEqual(actual, expected); + } + } + + // Check for Set object equality + if (isSet(actual) && isSet(expected)) { + if (expected.size > actual.size) { + return false; // `expected` can't be a subset if it has more elements + } + + const actualArray = ArrayFrom(SafeSetPrototypeIterator.$call(actual)); + const expectedIterator = SafeSetPrototypeIterator.$call(expected); + const usedIndices = new SafeSet(); + + expectedIteration: for (const expectedItem of expectedIterator) { + for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) { + if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) { + usedIndices.add(actualIdx); + continue expectedIteration; + } + } + return false; + } + + return true; + } + + // Check if expected array is a subset of actual array + if ($isArray(actual) && $isArray(expected)) { + if (expected.length > actual.length) { + return false; + } + + // Create a map to count occurrences of each element in the expected array + const expectedCounts = new SafeMap(); + for (const expectedItem of expected) { + let found = false; + for (const { 0: key, 1: count } of expectedCounts) { + if (isDeepStrictEqual(key, expectedItem)) { + expectedCounts.$set(key, count + 1); + found = true; + break; + } + } + if (!found) { + expectedCounts.$set(expectedItem, 1); + } + } + + // Create a map to count occurrences of relevant elements in the actual array + for (const actualItem of actual) { + for (const { 0: key, 1: count } of expectedCounts) { + if (isDeepStrictEqual(key, actualItem)) { + if (count === 1) { + expectedCounts.$delete(key); + } else { + expectedCounts.$set(key, count - 1); + } + break; + } + } + } + + return !expectedCounts.size; + } + + // Comparison done when at least one of the values is not an object + if (isSpecial(actual) || isSpecial(expected)) { + return isDeepStrictEqual(actual, expected); + } + + // Use Reflect.ownKeys() instead of Object.keys() to include symbol properties + const keysExpected = ReflectOwnKeys(expected); + + comparedObjects ??= new SafeWeakSet(); + + // Handle circular references + if (comparedObjects.has(actual)) { + return true; + } + comparedObjects.add(actual); + + if (AssertionError === undefined) loadAssertionError(); + // Check if all expected keys and values match + for (let i = 0; i < keysExpected.length; i++) { + const key = keysExpected[i]; + assert( + ReflectHas(actual, key), + new AssertionError({ message: `Expected key ${String(key)} not found in actual object` }), + ); + if (!compareBranch(actual[key], expected[key], comparedObjects)) { + return false; + } + } + + return true; +} + +/** + * The strict equivalence assertion test between two objects + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.partialDeepStrictEqual = function partialDeepStrictEqual(actual, expected, message) { + // emitExperimentalWarning("assert.partialDeepStrictEqual"); + if (arguments.length < 2) { + throw $ERR_MISSING_ARGS("actual", "expected"); + } + + if (!compareBranch(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: "partialDeepStrictEqual", + stackStartFn: partialDeepStrictEqual, + }); + } +}; + +class Comparison { + constructor(obj, keys, actual) { + for (const key of keys) { + if (key in obj) { + if ( + actual !== undefined && + typeof actual[key] === "string" && + isRegExp(obj[key]) && + RegExpPrototypeExec.$call(obj[key], actual[key]) !== null + ) { + this[key] = actual[key]; + } else { + this[key] = obj[key]; + } + } + } + } +} + +function compareExceptionKey(actual, expected, key, message, keys, fn) { + if (!(key in actual) || !isDeepStrictEqual(actual[key], expected[key])) { + if (!message) { + // Create placeholder objects to create a nice output. + const a = new Comparison(actual, keys); + const b = new Comparison(expected, keys, actual); + + if (AssertionError === undefined) loadAssertionError(); + const err = new AssertionError({ + actual: a, + expected: b, + operator: "deepStrictEqual", + stackStartFn: fn, + }); + err.actual = actual; + err.expected = expected; + err.operator = fn.name; + throw err; + } + innerFail({ + actual, + expected, + message, + operator: fn.name, + stackStartFn: fn, + }); + } +} + +function expectedException(actual, expected, message, fn) { + let generatedMessage = false; + let throwError = false; + + if (typeof expected !== "function") { + // Handle regular expressions. + if (isRegExp(expected)) { + const str = String(actual); + if (RegExpPrototypeExec.$call(expected, str) !== null) return; + const inspect = lazyInspect(); + + if (!message) { + generatedMessage = true; + message = + "The input did not match the regular expression " + `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; + } + throwError = true; + // Handle primitives properly. + } else if (typeof actual !== "object" || actual === null) { + if (AssertionError === undefined) loadAssertionError(); + const err = new AssertionError({ + actual, + expected, + message, + operator: "deepStrictEqual", + stackStartFn: fn, + }); + err.operator = fn.name; + throw err; + } else { + // Handle validation objects. + const keys = ObjectKeys(expected); + // Special handle errors to make sure the name and the message are + // compared as well. + if (expected instanceof Error) { + ArrayPrototypePush.$call(keys, "name", "message"); + } else if (keys.length === 0) { + throw $ERR_INVALID_ARG_VALUE("error", expected, "may not be an empty object"); + } + for (const key of keys) { + if ( + typeof actual[key] === "string" && + isRegExp(expected[key]) && + RegExpPrototypeExec.$call(expected[key], actual[key]) !== null + ) { + continue; + } + compareExceptionKey(actual, expected, key, message, keys, fn); + } + return; + } + // Guard instanceof against arrow functions as they don't have a prototype. + // Check for matching Error classes. + } else if (expected.prototype !== undefined && actual instanceof expected) { + return; + } else if (ObjectPrototypeIsPrototypeOf.$call(Error, expected)) { + if (!message) { + generatedMessage = true; + message = "The error is expected to be an instance of " + `"${expected.name}". Received `; + if (Error.isError(actual)) { + const name = actual.constructor?.name || actual.name; + if (expected.name === name) { + message += "an error with identical name but a different prototype."; + } else { + message += `"${name}"`; + } + if (actual.message) { + message += `\n\nError message:\n\n${actual.message}`; + } + } else { + message += `"${lazyInspect()(actual, { depth: -1 })}"`; + } + } + throwError = true; + } else { + // Check validation functions return value. + const res = expected.$apply({}, [actual]); + if (res !== true) { + if (!message) { + generatedMessage = true; + const name = expected.name ? `"${expected.name}" ` : ""; + const inspect = lazyInspect(); + message = `The ${name}validation function is expected to return` + ` "true". Received ${inspect(res)}`; + + if (Error.isError(actual)) { + message += `\n\nCaught error:\n\n${actual}`; + } + } + throwError = true; + } + } + + if (throwError) { + if (AssertionError === undefined) loadAssertionError(); + const err = new AssertionError({ + actual, + expected, + message, + operator: fn.name, + stackStartFn: fn, + }); + err.generatedMessage = generatedMessage; + throw err; + } +} + +function getActual(fn) { + validateFunction(fn, "fn"); + try { + fn(); + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; +} + +function checkIsPromise(obj) { + // Accept native ES6 promises and promises that are implemented in a similar + // way. Do not accept thenables that use a function as `obj` and that have no + // `catch` handler. + return ( + isPromise(obj) || + (obj !== null && typeof obj === "object" && typeof obj.then === "function" && typeof obj.catch === "function") + ); +} + +async function waitForActual(promiseFn) { + let resultPromise; + if (typeof promiseFn === "function") { + // Return a rejected promise if `promiseFn` throws synchronously. + resultPromise = promiseFn(); + // Fail in case no promise is returned. + if (!checkIsPromise(resultPromise)) { + throw $ERR_INVALID_RETURN_VALUE("instance of Promise", "promiseFn", resultPromise); + } + } else if (checkIsPromise(promiseFn)) { + resultPromise = promiseFn; + } else { + throw $ERR_INVALID_ARG_TYPE("promiseFn", ["Function", "Promise"], promiseFn); + } + + try { + await resultPromise; + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; +} + +function expectsError(stackStartFn: Function, actual: unknown, error: unknown, message?: string | Error) { + if (typeof error === "string") { + if (arguments.length === 4) { + throw $ERR_INVALID_ARG_TYPE("error", ["Object", "Error", "Function", "RegExp"], error); + } + if (typeof actual === "object" && actual !== null) { + if ((actual as { message?: unknown }).message === error) { + throw $ERR_AMBIGUOUS_ARGUMENT( + `The "error/message" argument is ambiguous. The error message "${(actual as { message?: unknown }).message}" is identical to the message.`, + ); + } + if (Object.keys(error).length === 0) { + throw $ERR_INVALID_ARG_VALUE("error", error, "may not be an empty object"); + } + } else if (actual === error) { + throw $ERR_AMBIGUOUS_ARGUMENT( + `The "error/message" argument is ambiguous. The error "${actual}" is identical to the message.`, + ); + } + message = error; + error = undefined; + } else if (error != null && typeof error !== "object" && typeof error !== "function") { + throw $ERR_INVALID_ARG_TYPE("error", ["Object", "Error", "Function", "RegExp"], error); + } + + if (actual === NO_EXCEPTION_SENTINEL) { + let details = ""; + if ((error as Error | undefined)?.name) { + details += ` (${(error as Error).name})`; + } + details += message ? `: ${message}` : "."; + const fnType = stackStartFn === assert.rejects ? "rejection" : "exception"; + innerFail({ + actual: undefined, + expected: error, + operator: stackStartFn.name, + message: `Missing expected ${fnType}${details}`, + stackStartFn, + }); + } + + if (!error) return; + + expectedException(actual, error, message, stackStartFn); +} + +function hasMatchingError(actual, expected) { + if (typeof expected !== "function") { + if (isRegExp(expected)) { + const str = String(actual); + return RegExpPrototypeExec.$call(expected, str) !== null; + } + throw $ERR_INVALID_ARG_TYPE("expected", ["Function", "RegExp"], expected); + } + // Guard instanceof against arrow functions as they don't have a prototype. + if (expected.prototype !== undefined && actual instanceof expected) { + return true; + } + if (ObjectPrototypeIsPrototypeOf.$call(Error, expected)) { + return false; + } + return expected.$apply({}, [actual]) === true; +} + +function expectsNoError(stackStartFn, actual, error, message) { + if (actual === NO_EXCEPTION_SENTINEL) return; + + if (typeof error === "string") { + message = error; + error = undefined; + } + + if (!error || hasMatchingError(actual, error)) { + const details = message ? `: ${message}` : "."; + const fnType = stackStartFn === assert.doesNotReject ? "rejection" : "exception"; + innerFail({ + actual, + expected: error, + operator: stackStartFn.name, + message: `Got unwanted ${fnType}${details}\n` + `Actual message: "${actual?.message}"`, + stackStartFn, + }); + } + throw actual; +} + +/** + * Expects the function `promiseFn` to throw an error. + * @param {() => any} promiseFn + * @param {...any} [args] + * @returns {void} + */ +assert.throws = function throws(promiseFn: () => Promise | Promise, ...args: unknown[]): void { + expectsError(throws, getActual(promiseFn), ...args); +}; + +/** + * Expects `promiseFn` function or its value to reject. + * @param {() => Promise} promiseFn + * @param {...any} [args] + * @returns {Promise} + */ +function rejects(block: (() => Promise) | Promise, message?: string | Error): Promise; +function rejects( + block: (() => Promise) | Promise, + error: nodeAssert.AssertPredicate, + message?: string | Error, +): Promise; +assert.rejects = async function rejects(promiseFn: () => Promise, ...args: any[]): Promise { + expectsError(rejects, await waitForActual(promiseFn), ...args); +}; + +/** + * Asserts that the function `fn` does not throw an error. + * @param {() => any} fn + * @param {...any} [args] + * @returns {void} + */ +assert.doesNotThrow = function doesNotThrow(fn: () => Promise, ...args: unknown[]): void { + expectsNoError(doesNotThrow, getActual(fn), ...args); +}; + +/** + * Expects `fn` or its value to not reject. + * @param {() => Promise} fn + * @param {...any} [args] + * @returns {Promise} + */ +assert.doesNotReject = async function doesNotReject(fn: () => Promise, ...args: unknown[]): Promise { + expectsNoError(doesNotReject, await waitForActual(fn), ...args); +}; + +/** + * Throws `value` if the value is not `null` or `undefined`. + * @param {any} err + * @returns {void} + */ +assert.ifError = function ifError(err: unknown): void { + if (err !== null && err !== undefined) { + let message = "ifError got unwanted exception: "; + if (typeof err === "object" && typeof err.message === "string") { + if (err.message.length === 0 && err.constructor) { + message += err.constructor.name; + } else { + message += err.message; + } + } else { + const inspect = lazyInspect(); + message += inspect(err); + } + + if (AssertionError === undefined) loadAssertionError(); + const newErr = new AssertionError({ + actual: err, + expected: null, + operator: "ifError", + message, + stackStartFn: ifError, + }); + + // Make sure we actually have a stack trace! + const origStack = err.stack; + + if (typeof origStack === "string") { + // This will remove any duplicated frames from the error frames taken + // from within `ifError` and add the original error frames to the newly + // created ones. + const origStackStart = StringPrototypeIndexOf.$call(origStack, "\n at"); + if (origStackStart !== -1) { + const originalFrames = StringPrototypeSplit.$call( + StringPrototypeSlice.$call(origStack, origStackStart + 1), + "\n", + ); + // Filter all frames existing in err.stack. + let newFrames = StringPrototypeSplit.$call(newErr.stack, "\n"); + for (const errFrame of originalFrames) { + // Find the first occurrence of the frame. + const pos = ArrayPrototypeIndexOf.$call(newFrames, errFrame); + if (pos !== -1) { + // Only keep new frames. + newFrames = ArrayPrototypeSlice.$call(newFrames, 0, pos); + break; + } + } + const stackStart = ArrayPrototypeJoin.$call(newFrames, "\n"); + const stackEnd = ArrayPrototypeJoin.$call(originalFrames, "\n"); + newErr.stack = `${stackStart}\n${stackEnd}`; + } + } + + throw newErr; + } +}; + +function internalMatch(string, regexp, message, fn) { + if (!isRegExp(regexp)) { + throw $ERR_INVALID_ARG_TYPE("regexp", "RegExp", regexp); + } + const match = fn === assert.match; + if (typeof string !== "string" || (RegExpPrototypeExec.$call(regexp, string) !== null) !== match) { + if (message instanceof Error) { + throw message; + } + + const generatedMessage = !message; + const inspect = lazyInspect(); + + // 'The input was expected to not match the regular expression ' + + message ||= + typeof string !== "string" + ? 'The "string" argument must be of type string. Received type ' + `${typeof string} (${inspect(string)})` + : (match + ? "The input did not match the regular expression " + : "The input was expected to not match the regular expression ") + + `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`; + if (AssertionError === undefined) loadAssertionError(); + const err = new AssertionError({ + actual: string, + expected: regexp, + message, + operator: fn.name, + stackStartFn: fn, + }); + err.generatedMessage = generatedMessage; + throw err; + } +} + +/** + * Expects the `string` input to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] + * @returns {void} + */ +assert.match = function match(string, regexp, message) { + internalMatch(string, regexp, message, match); +}; + +/** + * Expects the `string` input not to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] + * @returns {void} + */ +assert.doesNotMatch = function doesNotMatch(string, regexp, message) { + internalMatch(string, regexp, message, doesNotMatch); +}; + +var CallTracker; +Object.defineProperty(assert, "CallTracker", { + get() { + if (CallTracker === undefined) { + const { deprecate } = require("node:util"); + CallTracker = deprecate(require("internal/assert/calltracker"), "assert.CallTracker is deprecated.", "DEP0173"); + } + return CallTracker; + }, + set(value) { + CallTracker = value; + }, + configurable: true, + enumerable: true, +}); +// assert.CallTracker = CallTracker + +/** + * Expose a strict only variant of assert. + * @param {...any} args + * @returns {void} + */ +function strict(...args) { + innerOk(strict, args.length, ...args); +} + +assert.strict = ObjectAssign(strict, assert, { + equal: assert.strictEqual, + deepEqual: assert.deepStrictEqual, + notEqual: assert.notStrictEqual, + notDeepEqual: assert.notDeepStrictEqual, +}); + +assert.strict.strict = assert.strict; diff --git a/src/js/node/async_hooks.ts b/src/js/node/async_hooks.ts index db0f0b8272..840afac3b9 100644 --- a/src/js/node/async_hooks.ts +++ b/src/js/node/async_hooks.ts @@ -23,6 +23,7 @@ // calls to $assert which will verify this invariant (only during bun-debug) // const [setAsyncHooksEnabled, cleanupLater] = $cpp("NodeAsyncHooks.cpp", "createAsyncHooksBinding"); +const { validateFunction, validateString, validateObject } = require("internal/validators"); // Only run during debug function assertValidAsyncContextArray(array: unknown): array is ReadonlyArray | undefined { @@ -89,6 +90,7 @@ class AsyncLocalStorage { } static bind(fn, ...args: any) { + validateFunction(fn); return this.snapshot().bind(null, fn, ...args); } @@ -234,6 +236,14 @@ class AsyncLocalStorage { if (context[i] === this) return context[i + 1]; } } + + // Node.js internal function. In Bun's implementation, calling this is not + // observable from outside the AsyncLocalStorage implementation. + _enable() {} + + // Node.js internal function. In Bun's implementation, calling this is not + // observable from outside the AsyncLocalStorage implementation. + _propagate(resource, triggerResource, type) {} } if (IS_BUN_DEVELOPMENT) { @@ -250,10 +260,22 @@ class AsyncResource { type; #snapshot; - constructor(type, options?) { - if (typeof type !== "string") { - throw new TypeError('The "type" argument must be of type string. Received type ' + typeof type); + constructor(type, opts?) { + validateString(type, "type"); + + let triggerAsyncId = opts; + if (opts != null) { + if (typeof opts !== "number") { + triggerAsyncId = opts.triggerAsyncId === undefined ? 1 : opts.triggerAsyncId; + } + if (!Number.isSafeInteger(triggerAsyncId) || triggerAsyncId < -1) { + throw $ERR_INVALID_ASYNC_ID(`Invalid triggerAsyncId value: ${triggerAsyncId}`); + } } + if (hasEnabledCreateHook && type.length === 0) { + throw $ERR_ASYNC_TYPE(`Invalid name for async "type": ${type}`); + } + setAsyncHooksEnabled(true); this.type = type; this.#snapshot = get(); @@ -292,6 +314,7 @@ class AsyncResource { } bind(fn, thisArg) { + validateFunction(fn, "fn"); return this.runInAsyncScope.bind(this, fn, thisArg ?? this); } @@ -320,11 +343,10 @@ function createWarning(message, isCreateHook?: boolean) { // times bundled into a framework or application. Their use defines three // handlers which are all TODO stubs. for more info see this comment: // https://github.com/oven-sh/bun/issues/13866#issuecomment-2397896065 - if (typeof arg1 === 'object') { + if (typeof arg1 === "object") { const { init, promiseResolve, destroy } = arg1; if (init && promiseResolve && destroy) { - if (isEmptyFunction(init) && isEmptyFunction(destroy)) - return; + if (isEmptyFunction(init) && isEmptyFunction(destroy)) return; } } } @@ -337,8 +359,8 @@ function createWarning(message, isCreateHook?: boolean) { function isEmptyFunction(f: Function) { let str = f.toString(); - if(!str.startsWith('function()'))return false; - str = str.slice('function()'.length).trim(); + if (!str.startsWith("function()")) return false; + str = str.slice("function()".length).trim(); return /^{\s*}$/.test(str); } @@ -347,10 +369,28 @@ const createHookNotImpl = createWarning( true, ); -function createHook(callbacks) { +let hasEnabledCreateHook = false; +function createHook(hook) { + validateObject(hook, "hook"); + const { init, before, after, destroy, promiseResolve } = hook; + if (init !== undefined && typeof init !== "function") throw $ERR_ASYNC_CALLBACK("hook.init must be a function"); + if (before !== undefined && typeof before !== "function") throw $ERR_ASYNC_CALLBACK("hook.before must be a function"); + if (after !== undefined && typeof after !== "function") throw $ERR_ASYNC_CALLBACK("hook.after must be a function"); + if (destroy !== undefined && typeof destroy !== "function") + throw $ERR_ASYNC_CALLBACK("hook.destroy must be a function"); + if (promiseResolve !== undefined && typeof promiseResolve !== "function") + throw $ERR_ASYNC_CALLBACK("hook.promiseResolve must be a function"); + return { - enable: () => createHookNotImpl(callbacks), - disable: createHookNotImpl, + enable() { + createHookNotImpl(hook); + hasEnabledCreateHook = true; + return this; + }, + disable() { + createHookNotImpl(); + return this; + }, }; } diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index 6767c6206b..096ee95907 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -2,7 +2,6 @@ const EventEmitter = require("node:events"); const StreamModule = require("node:stream"); const OsModule = require("node:os"); -const { ERR_INVALID_ARG_TYPE, ERR_IPC_DISCONNECTED } = require("internal/errors"); const { kHandle } = require("internal/shared"); const { validateBoolean, @@ -10,6 +9,8 @@ const { validateString, validateAbortSignal, validateArray, + validateObject, + validateOneOf, } = require("internal/validators"); var NetModule; @@ -22,18 +23,20 @@ var BufferIsEncoding = Buffer.isEncoding; var kEmptyObject = ObjectCreate(null); var signals = OsModule.constants.signals; -var ArrayPrototypePush = Array.prototype.push; var ArrayPrototypeJoin = Array.prototype.join; var ArrayPrototypeMap = Array.prototype.map; var ArrayPrototypeIncludes = Array.prototype.includes; var ArrayPrototypeSlice = Array.prototype.slice; var ArrayPrototypeUnshift = Array.prototype.unshift; +const ArrayPrototypeFilter = Array.prototype.filter; +const ArrayPrototypeSort = Array.prototype.sort; +const StringPrototypeToUpperCase = String.prototype.toUpperCase; +const ArrayPrototypePush = Array.prototype.push; var ArrayBufferIsView = ArrayBuffer.isView; var NumberIsInteger = Number.isInteger; -var StringPrototypeToUpperCase = String.prototype.toUpperCase; var StringPrototypeIncludes = String.prototype.includes; var StringPrototypeSlice = String.prototype.slice; var Uint8ArrayPrototypeIncludes = Uint8Array.prototype.includes; @@ -74,9 +77,7 @@ var ReadableFromWeb; // TODO: Add these params after support added in Bun.spawn // uid Sets the user identity of the process (see setuid(2)). // gid Sets the group identity of the process (see setgid(2)). -// detached Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). -// TODO: Add support for ipc option, verify only one IPC channel in array // stdio | Child's stdio configuration (see options.stdio). // Support wrapped ipc types (e.g. net.Socket, dgram.Socket, TTY, etc.) // IPC FD passing support @@ -526,6 +527,7 @@ execFile[kCustomPromisifySymbol][kCustomPromisifySymbol] = execFile[kCustomPromi */ function spawnSync(file, args, options) { options = { + __proto__: null, maxBuffer: MAX_BUFFER, ...normalizeSpawnArguments(file, args, options), }; @@ -554,24 +556,31 @@ function spawnSync(file, args, options) { } else if (typeof input === "string") { bunStdio[0] = Buffer.from(input, encoding || "utf8"); } else { - throw ERR_INVALID_ARG_TYPE(`options.stdio[0]`, ["Buffer", "TypedArray", "DataView", "string"], input); + throw $ERR_INVALID_ARG_TYPE(`options.stdio[0]`, ["Buffer", "TypedArray", "DataView", "string"], input); } } - const { - stdout = null, - stderr = null, - success, - exitCode, - signalCode, - } = Bun.spawnSync({ - cmd: options.args, - env: options.env || undefined, - cwd: options.cwd || undefined, - stdio: bunStdio, - windowsVerbatimArguments: options.windowsVerbatimArguments, - windowsHide: options.windowsHide, - }); + var error; + try { + var { + stdout = null, + stderr = null, + success, + exitCode, + signalCode, + } = Bun.spawnSync({ + cmd: options.args, + env: options.env || undefined, + cwd: options.cwd || undefined, + stdio: bunStdio, + windowsVerbatimArguments: options.windowsVerbatimArguments, + windowsHide: options.windowsHide, + }); + } catch (err) { + error = err; + stdout = null; + stderr = null; + } const result = { signal: signalCode ?? null, @@ -580,6 +589,10 @@ function spawnSync(file, args, options) { output: [null, stdout, stderr], }; + if (error) { + result.error = error; + } + if (stdout && encoding && encoding !== "buffer") { result.output[1] = result.output[1]?.toString(encoding); } @@ -591,8 +604,11 @@ function spawnSync(file, args, options) { result.stdout = result.output[1]; result.stderr = result.output[2]; - if (!success) { + if (!success && error == null) { result.error = new SystemError(result.output[2], options.file, "spawnSync", -1, result.status); + } + + if (result.error) { result.error.spawnargs = ArrayPrototypeSlice.$call(options.args, 1); } @@ -683,7 +699,7 @@ function stdioStringToArray(stdio, channel) { options = [0, 1, 2]; break; default: - throw ERR_INVALID_ARG_VALUE("stdio", stdio); + throw $ERR_INVALID_ARG_VALUE("stdio", stdio); } if (channel) $arrayPush(options, channel); @@ -781,7 +797,7 @@ function sanitizeKillSignal(killSignal) { if (typeof killSignal === "string" || typeof killSignal === "number") { return convertToValidSignal(killSignal); } else if (killSignal != null) { - throw ERR_INVALID_ARG_TYPE("options.killSignal", ["string", "number"], killSignal); + throw $ERR_INVALID_ARG_TYPE("options.killSignal", ["string", "number"], killSignal); } } @@ -861,14 +877,14 @@ function normalizeSpawnArguments(file, args, options) { validateString(file, "file"); validateArgumentNullCheck(file, "file"); - if (file.length === 0) throw ERR_INVALID_ARG_VALUE("file", file, "cannot be empty"); + if (file.length === 0) throw $ERR_INVALID_ARG_VALUE("file", file, "cannot be empty"); if ($isJSArray(args)) { args = ArrayPrototypeSlice.$call(args); } else if (args == null) { args = []; } else if (typeof args !== "object") { - throw ERR_INVALID_ARG_TYPE("args", "object", args); + throw $ERR_INVALID_ARG_TYPE("args", "object", args); } else { options = args; args = []; @@ -893,17 +909,17 @@ function normalizeSpawnArguments(file, args, options) { // Validate the uid, if present. if (options.uid != null && !isInt32(options.uid)) { - throw ERR_INVALID_ARG_TYPE("options.uid", "int32", options.uid); + throw $ERR_INVALID_ARG_TYPE("options.uid", "int32", options.uid); } // Validate the gid, if present. if (options.gid != null && !isInt32(options.gid)) { - throw ERR_INVALID_ARG_TYPE("options.gid", "int32", options.gid); + throw $ERR_INVALID_ARG_TYPE("options.gid", "int32", options.gid); } // Validate the shell, if present. if (options.shell != null && typeof options.shell !== "boolean" && typeof options.shell !== "string") { - throw ERR_INVALID_ARG_TYPE("options.shell", ["boolean", "string"], options.shell); + throw $ERR_INVALID_ARG_TYPE("options.shell", ["boolean", "string"], options.shell); } // Validate argv0, if present. @@ -953,13 +969,38 @@ function normalizeSpawnArguments(file, args, options) { } const env = options.env || process.env; - const envPairs = env; + const envPairs = {}; // // process.env.NODE_V8_COVERAGE always propagates, making it possible to // // collect coverage for programs that spawn with white-listed environment. // copyProcessEnvToEnv(env, "NODE_V8_COVERAGE", options.env); - // TODO: Windows env support here... + let envKeys: string[] = []; + for (const key in env) { + ArrayPrototypePush.$call(envKeys, key); + } + + if (process.platform === "win32") { + // On Windows env keys are case insensitive. Filter out duplicates, keeping only the first one (in lexicographic order) + const sawKey = new Set(); + envKeys = ArrayPrototypeFilter.$call(ArrayPrototypeSort.$call(envKeys), key => { + const uppercaseKey = StringPrototypeToUpperCase.$call(key); + if (sawKey.has(uppercaseKey)) { + return false; + } + sawKey.add(uppercaseKey); + return true; + }); + } + + for (const key of envKeys) { + const value = env[key]; + if (value !== undefined) { + validateArgumentNullCheck(key, `options.env['${key}']`); + validateArgumentNullCheck(value, `options.env['${key}']`); + envPairs[key] = value; + } + } return { // Make a shallow copy so we don't clobber the user's options object. @@ -1299,7 +1340,7 @@ class ChildProcess extends EventEmitter { options = undefined; } else if (options !== undefined) { if (typeof options !== "object" || options === null) { - throw ERR_INVALID_ARG_TYPE("options", "object", options); + throw $ERR_INVALID_ARG_TYPE("options", "object", options); } } @@ -1332,7 +1373,7 @@ class ChildProcess extends EventEmitter { $assert(this.connected); this.#handle.disconnect(); } else if (!ok) { - this.emit("error", ERR_IPC_DISCONNECTED()); + this.emit("error", $ERR_IPC_DISCONNECTED()); return; } this.#handle.disconnect(); @@ -1391,7 +1432,7 @@ const nodeToBunLookup = { ipc: "ipc", }; -function nodeToBun(item, index) { +function nodeToBun(item: string, index: number): string | number | null { // If not defined, use the default. // For stdin/stdout/stderr, it's pipe. For others, it's ignore. if (item == null) { @@ -1460,6 +1501,7 @@ function fdToStdioName(fd) { function getBunStdioFromOptions(stdio) { const normalizedStdio = normalizeStdio(stdio); + if (normalizedStdio.filter(v => v === "ipc").length > 1) throw $ERR_IPC_ONE_PIPE(); // Node options: // pipe: just a pipe // ipc = can only be one in array @@ -1486,7 +1528,7 @@ function getBunStdioFromOptions(stdio) { return bunStdio; } -function normalizeStdio(stdio) { +function normalizeStdio(stdio): string[] { if (typeof stdio === "string") { switch (stdio) { case "ignore": @@ -1522,7 +1564,7 @@ function abortChildProcess(child, killSignal, reason) { if (!child) return; try { if (child.kill(killSignal)) { - child.emit("error", new AbortError(undefined, { cause: reason })); + child.emit("error", $makeAbortError(undefined, { cause: reason })); } } catch (err) { child.emit("error", err); @@ -1569,13 +1611,13 @@ class ShimmedStdioOutStream extends EventEmitter { function validateMaxBuffer(maxBuffer) { if (maxBuffer != null && !(typeof maxBuffer === "number" && maxBuffer >= 0)) { - throw ERR_OUT_OF_RANGE("options.maxBuffer", "a positive number", maxBuffer); + throw $ERR_OUT_OF_RANGE("options.maxBuffer", "a positive number", maxBuffer); } } function validateArgumentNullCheck(arg, propName) { if (typeof arg === "string" && StringPrototypeIncludes.$call(arg, "\u0000")) { - throw ERR_INVALID_ARG_VALUE(propName, arg, "must be a string without null bytes"); + throw $ERR_INVALID_ARG_VALUE(propName, arg, "must be a string without null bytes"); } } @@ -1587,57 +1629,10 @@ function validateArgumentsNullCheck(args, propName) { function validateTimeout(timeout) { if (timeout != null && !(NumberIsInteger(timeout) && timeout >= 0)) { - throw ERR_OUT_OF_RANGE("timeout", "an unsigned integer", timeout); + throw $ERR_OUT_OF_RANGE("timeout", "an unsigned integer", timeout); } } -/** - * @callback validateOneOf - * @template T - * @param {T} value - * @param {string} name - * @param {T[]} oneOf - */ - -/** @type {validateOneOf} */ -const validateOneOf = (value, name, oneOf) => { - // const validateOneOf = hideStackFrames((value, name, oneOf) => { - if (!ArrayPrototypeIncludes.$call(oneOf, value)) { - const allowed = ArrayPrototypeJoin.$call( - ArrayPrototypeMap.$call(oneOf, v => (typeof v === "string" ? `'${v}'` : String(v))), - ", ", - ); - const reason = "must be one of: " + allowed; - throw ERR_INVALID_ARG_VALUE(name, value, reason); - } -}; - -/** - * @callback validateObject - * @param {*} value - * @param {string} name - * @param {{ - * allowArray?: boolean, - * allowFunction?: boolean, - * nullable?: boolean - * }} [options] - */ - -/** @type {validateObject} */ -const validateObject = (value, name, options = null) => { - // const validateObject = hideStackFrames((value, name, options = null) => { - const allowArray = options?.allowArray ?? false; - const allowFunction = options?.allowFunction ?? false; - const nullable = options?.nullable ?? false; - if ( - (!nullable && value === null) || - (!allowArray && $isJSArray(value)) || - (typeof value !== "object" && (!allowFunction || typeof value !== "function")) - ) { - throw ERR_INVALID_ARG_TYPE(name, "object", value); - } -}; - function isInt32(value) { return value === (value | 0); } @@ -1655,7 +1650,7 @@ function nullCheck(path, propName, throwError = true) { return; } - const err = ERR_INVALID_ARG_VALUE(propName, path, "must be a string or Uint8Array without null bytes"); + const err = $ERR_INVALID_ARG_VALUE(propName, path, "must be a string or Uint8Array without null bytes"); if (throwError) { throw err; } @@ -1664,7 +1659,7 @@ function nullCheck(path, propName, throwError = true) { function validatePath(path, propName = "path") { if (typeof path !== "string" && !isUint8Array(path)) { - throw ERR_INVALID_ARG_TYPE(propName, ["string", "Buffer", "URL"], path); + throw $ERR_INVALID_ARG_TYPE(propName, ["string", "Buffer", "URL"], path); } const err = nullCheck(path, propName, false); @@ -1704,18 +1699,6 @@ var Error = globalThis.Error; var TypeError = globalThis.TypeError; var RangeError = globalThis.RangeError; -// Node uses a slightly different abort error than standard DOM. See: https://github.com/nodejs/node/blob/main/lib/internal/errors.js -class AbortError extends Error { - code = "ABORT_ERR"; - name = "AbortError"; - constructor(message = "The operation was aborted", options = undefined) { - if (options !== undefined && typeof options !== "object") { - throw ERR_INVALID_ARG_TYPE("options", "object", options); - } - super(message, options); - } -} - function genericNodeError(message, options) { const err = new Error(message); err.code = options.code; @@ -1855,29 +1838,6 @@ function genericNodeError(message, options) { // TypeError // ); -function ERR_OUT_OF_RANGE(str, range, input, replaceDefaultBoolean = false) { - // Node implementation: - // assert(range, 'Missing "range" argument'); - // let msg = replaceDefaultBoolean - // ? str - // : `The value of "${str}" is out of range.`; - // let received; - // if (NumberIsInteger(input) && MathAbs(input) > 2 ** 32) { - // received = addNumericalSeparator(String(input)); - // } else if (typeof input === "bigint") { - // received = String(input); - // if (input > 2n ** 32n || input < -(2n ** 32n)) { - // received = addNumericalSeparator(received); - // } - // received += "n"; - // } else { - // received = lazyInternalUtilInspect().inspect(input); - // } - // msg += ` It must be ${range}. Received ${received}`; - // return new RangeError(msg); - return new RangeError(`The value of ${str} is out of range. It must be ${range}. Received ${input}`); -} - function ERR_CHILD_PROCESS_STDIO_MAXBUFFER(stdio) { const err = Error(`${stdio} maxBuffer length exceeded`); err.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"; @@ -1896,12 +1856,6 @@ function ERR_INVALID_OPT_VALUE(name, value) { return err; } -function ERR_INVALID_ARG_VALUE(name, value, reason) { - const err = new Error(`The value "${value}" is invalid for argument '${name}'. Reason: ${reason}`); - err.code = "ERR_INVALID_ARG_VALUE"; - return err; -} - function ERR_CHILD_PROCESS_IPC_REQUIRED(name) { const err = new TypeError(`Forked processes must have an IPC channel, missing value 'ipc' in ${name}`); err.code = "ERR_CHILD_PROCESS_IPC_REQUIRED"; diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 32909a2004..5198bd1239 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -4,6 +4,7 @@ var __getOwnPropNames = Object.getOwnPropertyNames; const StreamModule = require("node:stream"); const BufferModule = require("node:buffer"); const StringDecoder = require("node:string_decoder").StringDecoder; +const StringPrototypeToLowerCase = String.prototype.toLowerCase; const { CryptoHasher } = Bun; const { symmetricKeySize, @@ -22,7 +23,22 @@ const { privateDecrypt, privateEncrypt, publicDecrypt, -} = $cpp("KeyObject.cpp", "createNodeCryptoBinding"); + X509Certificate, +} = $cpp("KeyObject.cpp", "createKeyObjectBinding"); + +const { + statelessDH, + ecdhConvertKey, + getCurves, + certVerifySpkac, + certExportPublicKey, + certExportChallenge, + getCiphers, + _getCipherInfo, +} = $cpp("NodeCrypto.cpp", "createNodeCryptoBinding"); + +const { POINT_CONVERSION_COMPRESSED, POINT_CONVERSION_HYBRID, POINT_CONVERSION_UNCOMPRESSED } = + $processBindingConstants.crypto; const { randomInt: _randomInt, @@ -30,6 +46,53 @@ const { pbkdf2Sync: pbkdf2Sync_, } = $zig("node_crypto_binding.zig", "createNodeCryptoBindingZig"); +const { validateObject, validateString, validateInt32 } = require("internal/validators"); + +function verifySpkac(spkac, encoding) { + return certVerifySpkac(getArrayBufferOrView(spkac, "spkac", encoding)); +} +function exportPublicKey(spkac, encoding) { + return certExportPublicKey(getArrayBufferOrView(spkac, "spkac", encoding)); +} +function exportChallenge(spkac, encoding) { + return certExportChallenge(getArrayBufferOrView(spkac, "spkac", encoding)); +} + +function Certificate(): void { + if (!(this instanceof Certificate)) { + return new Certificate(); + } + + this.verifySpkac = verifySpkac; + this.exportPublicKey = exportPublicKey; + this.exportChallenge = exportChallenge; +} +Certificate.prototype = {}; +Certificate.verifySpkac = verifySpkac; +Certificate.exportPublicKey = exportPublicKey; +Certificate.exportChallenge = exportChallenge; + +function getCipherInfo(nameOrNid, options) { + if (typeof nameOrNid !== "string" && typeof nameOrNid !== "number") { + throw $ERR_INVALID_ARG_TYPE("nameOrNid", ["string", "number"], nameOrNid); + } + if (typeof nameOrNid === "number") validateInt32(nameOrNid, "nameOrNid"); + let keyLength, ivLength; + if (options !== undefined) { + validateObject(options, "options"); + ({ keyLength, ivLength } = options); + if (keyLength !== undefined) validateInt32(keyLength, "options.keyLength"); + if (ivLength !== undefined) validateInt32(ivLength, "options.ivLength"); + } + + const ret = _getCipherInfo({}, nameOrNid, keyLength, ivLength); + if (ret !== undefined) { + ret.name &&= ret.name; + ret.type &&= StringPrototypeToLowerCase.$call(ret.type); + } + return ret; +} + function randomInt(min, max, callback) { if (max == null) { max = min; @@ -1620,7 +1683,12 @@ var require_algos = __commonJS({ }, }); function pbkdf2(password, salt, iterations, keylen, digest, callback) { - const promise = pbkdf2_(password, salt, iterations, keylen, digest); + if (typeof digest === "function") { + callback = digest; + digest = undefined; + } + + const promise = pbkdf2_(password, salt, iterations, keylen, digest, callback); if (callback) { promise.then( result => callback(null, result), @@ -3081,11 +3149,7 @@ var require_decrypter = __commonJS({ var require_browser5 = __commonJS({ "node_modules/browserify-aes/browser.js"(exports) { var ciphers = require_encrypter(), - deciphers = require_decrypter(), - modes = require_list(); - function getCiphers() { - return Object.keys(modes); - } + deciphers = require_decrypter(); exports.createCipher = exports.Cipher = ciphers.createCipher; exports.createCipheriv = exports.Cipheriv = ciphers.createCipheriv; exports.createDecipher = exports.Decipher = deciphers.createDecipher; @@ -3160,9 +3224,6 @@ var require_browser6 = __commonJS({ if (desModes[suite]) return new DES({ key, iv, mode: suite, decrypt: !0 }); throw new TypeError("invalid suite type"); } - function getCiphers() { - return Object.keys(desModes).concat(aes.getCiphers()); - } exports.createCipher = exports.Cipher = createCipher; exports.createCipheriv = exports.Cipheriv = createCipheriv; exports.createDecipher = exports.Decipher = createDecipher; @@ -5514,6 +5575,39 @@ var require_browser7 = __commonJS({ } exports.DiffieHellmanGroup = exports.createDiffieHellmanGroup = exports.getDiffieHellman = getDiffieHellman; exports.createDiffieHellman = exports.DiffieHellman = createDiffieHellman; + + exports.diffieHellman = function diffieHellman(options) { + validateObject(options); + + const { privateKey, publicKey } = options; + + if (!(privateKey instanceof KeyObject)) { + throw $ERR_INVALID_ARG_VALUE("options.privateKey", privateKey); + } + + if (!(publicKey instanceof KeyObject)) { + throw $ERR_INVALID_ARG_VALUE("options.publicKey", publicKey); + } + + if (privateKey.type !== "private") { + throw $ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, "private"); + } + + const publicKeyType = publicKey.type; + if (publicKeyType !== "public" && publicKeyType !== "private") { + throw $ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKeyType, "private or public"); + } + + const privateType = privateKey.asymmetricKeyType; + const publicType = publicKey.asymmetricKeyType; + if (privateType !== publicType || !["dh", "ec", "x448", "x25519"].includes(privateType)) { + throw $ERR_CRYPTO_INCOMPATIBLE_KEY( + `Incompatible key types for Diffie-Hellman: ${privateType} and ${publicType}`, + ); + } + + return statelessDH(privateKey.$bunNativePtr, publicKey.$bunNativePtr); + }; }, }); @@ -5896,7 +5990,7 @@ var require_base = __commonJS({ return res; } else if ((bytes[0] === 2 || bytes[0] === 3) && bytes.length - 1 === len) return this.pointFromX(bytes.slice(1, 1 + len), bytes[0] === 3); - throw new Error("Unknown point format"); + throw $ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY("Public key is not valid for specified curve"); }; BasePoint.prototype.encodeCompressed = function (enc) { return this.encode(enc, !0); @@ -8914,8 +9008,7 @@ var require_signature = __commonJS({ function Signature(options, enc) { if (options instanceof Signature) return options; this._importDER(options, enc) || - (assert(options.r && options.s, "Signature without r or s"), - (this.r = new BN(options.r, 16)), + ((this.r = new BN(options.r, 16)), (this.s = new BN(options.s, 16)), options.recoveryParam === void 0 ? (this.recoveryParam = null) : (this.recoveryParam = options.recoveryParam)); } @@ -9212,7 +9305,7 @@ var require_signature2 = __commonJS({ R: sig.slice(0, eddsa.encodingLength), S: sig.slice(eddsa.encodingLength), }), - assert(sig.R && sig.S, "Signature without R or S"), + // assert(sig.R && sig.S, "Signature without R or S"), eddsa.isPoint(sig.R) && (this._R = sig.R), sig.S instanceof BN && (this._S = sig.S), (this._Rencoded = Array.isArray(sig.R) ? sig.R : sig.Rencoded), @@ -11355,9 +11448,6 @@ var require_browser9 = __commonJS({ "node_modules/create-ecdh/browser.js"(exports, module) { var elliptic = require_elliptic(), BN = require_bn6(); - module.exports = function (curve) { - return new ECDH(curve); - }; var aliases = { secp256k1: { name: "secp256k1", @@ -11431,6 +11521,29 @@ var require_browser9 = __commonJS({ var _priv = new BN(priv); return (_priv = _priv.toString(16)), (this.keys = this.curve.genKeyPair()), this.keys._importPrivate(_priv), this; }; + function getFormat(format) { + if (format) { + if (format === "compressed") return POINT_CONVERSION_COMPRESSED; + if (format === "hybrid") return POINT_CONVERSION_HYBRID; + if (format !== "uncompressed") throw $ERR_CRYPTO_ECDH_INVALID_FORMAT("Invalid ECDH format: " + format); + } + return POINT_CONVERSION_UNCOMPRESSED; + } + function encode(buffer, encoding) { + if (encoding && encoding !== "buffer") buffer = buffer.toString(encoding); + return buffer; + } + ECDH.convertKey = function convertKey(key, curve, inEnc, outEnc, format) { + validateString(curve, "curve"); + key = getArrayBufferOrView(key, "key", inEnc); + const f = getFormat(format); + const convertedKey = ecdhConvertKey(key, curve, f); + return encode(convertedKey, outEnc); + }; + module.exports.ECDH = ECDH; + module.exports.createECDH = function (curve) { + return new ECDH(curve); + }; function formatReturnValue(bn, enc, len) { Array.isArray(bn) || (bn = bn.toArray()); var buf = new Buffer(bn); @@ -11523,7 +11636,7 @@ var require_crypto_browserify2 = __commonJS({ exports.createDecipher = aes.createDecipher; exports.Decipheriv = aes.Decipheriv; exports.createDecipheriv = aes.createDecipheriv; - exports.getCiphers = aes.getCiphers; + exports.getCiphers = getCiphers; exports.listCiphers = aes.listCiphers; var dh = require_browser7(); exports.DiffieHellmanGroup = dh.DiffieHellmanGroup; @@ -11531,12 +11644,15 @@ var require_crypto_browserify2 = __commonJS({ exports.getDiffieHellman = dh.getDiffieHellman; exports.createDiffieHellman = dh.createDiffieHellman; exports.DiffieHellman = dh.DiffieHellman; + exports.diffieHellman = dh.diffieHellman; var sign = require_browser8(); exports.createSign = sign.createSign; exports.Sign = sign.Sign; exports.createVerify = sign.createVerify; exports.Verify = sign.Verify; - exports.createECDH = require_browser9(); + const ecdh = require_browser9(); + exports.ECDH = ecdh.ECDH; + exports.createECDH = ecdh.createECDH; exports.getRandomValues = values => crypto.getRandomValues(values); var rf = require_browser11(); exports.randomFill = rf.randomFill; @@ -11606,26 +11722,6 @@ timingSafeEqual && value: "::bunternal::", })); -const harcoded_curves = [ - "p192", - "p224", - "p256", - "p384", - "p521", - "curve25519", - "ed25519", - "secp256k1", - "secp224r1", - "prime256v1", - "prime192v1", - "secp384r1", - "secp521r1", -]; - -function getCurves() { - return harcoded_curves; -} - class KeyObject { // we use $bunNativePtr so that util.types.isKeyObject can detect it $bunNativePtr = undefined; @@ -12037,15 +12133,21 @@ crypto_exports.hash = function hash(algorithm, input, outputEncoding = "hex") { return CryptoHasher.hash(algorithm, input, outputEncoding); }; +crypto_exports.getFips = function getFips() { + return 0; +}; + crypto_exports.getRandomValues = getRandomValues; crypto_exports.randomUUID = randomUUID; crypto_exports.randomInt = randomInt; crypto_exports.getCurves = getCurves; +crypto_exports.getCipherInfo = getCipherInfo; crypto_exports.scrypt = scrypt; crypto_exports.scryptSync = scryptSync; crypto_exports.timingSafeEqual = timingSafeEqual; crypto_exports.webcrypto = webcrypto; crypto_exports.subtle = _subtle; - +crypto_exports.X509Certificate = X509Certificate; +crypto_exports.Certificate = Certificate; export default crypto_exports; /*! safe-buffer. MIT License. Feross Aboukhadijeh */ diff --git a/src/js/node/dgram.ts b/src/js/node/dgram.ts index b83c64bafd..0ed940d47f 100644 --- a/src/js/node/dgram.ts +++ b/src/js/node/dgram.ts @@ -34,7 +34,6 @@ const kStateSymbol = Symbol("state symbol"); const async_id_symbol = Symbol("async_id_symbol"); const { hideFromStack, throwNotImplemented } = require("internal/shared"); -const { ERR_SOCKET_BAD_TYPE } = require("internal/errors"); const { validateString, validateNumber, @@ -43,58 +42,12 @@ const { validateAbortSignal, } = require("internal/validators"); -const { - FunctionPrototypeBind, - ObjectSetPrototypeOf, - SymbolAsyncDispose, - SymbolDispose, - StringPrototypeTrim, - NumberIsNaN, -} = require("internal/primordials"); - const EventEmitter = require("node:events"); -class ERR_OUT_OF_RANGE extends Error { - constructor(argumentName, range, received) { - super(`The value of "${argumentName}" is out of range. It must be ${range}. Received ${received}`); - this.code = "ERR_OUT_OF_RANGE"; - } -} - -class ERR_BUFFER_OUT_OF_BOUNDS extends Error { - constructor() { - super("Buffer offset or length is out of bounds"); - this.code = "ERR_BUFFER_OUT_OF_BOUNDS"; - } -} - -class ERR_INVALID_ARG_TYPE extends Error { - constructor(argName, expected, actual) { - super(`The "${argName}" argument must be of type ${expected}. Received type ${typeof actual}`); - this.code = "ERR_INVALID_ARG_TYPE"; - } -} - -class ERR_MISSING_ARGS extends Error { - constructor(argName) { - super(`The "${argName}" argument is required`); - this.code = "ERR_MISSING_ARGS"; - } -} - -class ERR_SOCKET_ALREADY_BOUND extends Error { - constructor() { - super("Socket is already bound"); - this.code = "ERR_SOCKET_ALREADY_BOUND"; - } -} - -class ERR_SOCKET_BAD_BUFFER_SIZE extends Error { - constructor() { - super("Buffer size must be a number"); - this.code = "ERR_SOCKET_BAD_BUFFER_SIZE"; - } -} +const SymbolDispose = Symbol.dispose; +const SymbolAsyncDispose = Symbol.asyncDispose; +const ObjectSetPrototypeOf = Object.setPrototypeOf; +const FunctionPrototypeBind = Function.prototype.bind; class ERR_SOCKET_BUFFER_SIZE extends Error { constructor(ctx) { @@ -103,34 +56,6 @@ class ERR_SOCKET_BUFFER_SIZE extends Error { } } -class ERR_SOCKET_DGRAM_IS_CONNECTED extends Error { - constructor() { - super("Socket is connected"); - this.code = "ERR_SOCKET_DGRAM_IS_CONNECTED"; - } -} - -class ERR_SOCKET_DGRAM_NOT_CONNECTED extends Error { - constructor() { - super("Socket is not connected"); - this.code = "ERR_SOCKET_DGRAM_NOT_CONNECTED"; - } -} - -class ERR_SOCKET_BAD_PORT extends Error { - constructor(name, port, allowZero) { - super(`Invalid ${name}: ${port}. Ports must be >= 0 and <= 65535. ${allowZero ? "0" : ""}`); - this.code = "ERR_SOCKET_BAD_PORT"; - } -} - -class ERR_SOCKET_DGRAM_NOT_RUNNING extends Error { - constructor() { - super("Socket is not running"); - this.code = "ERR_SOCKET_DGRAM_NOT_RUNNING"; - } -} - function isInt32(value) { return value === (value | 0); } @@ -163,11 +88,11 @@ function newHandle(type, lookup) { const handle = {}; if (type === "udp4") { - handle.lookup = FunctionPrototypeBind(lookup4, handle, lookup); + handle.lookup = FunctionPrototypeBind.$call(lookup4, handle, lookup); } else if (type === "udp6") { - handle.lookup = FunctionPrototypeBind(lookup6, handle, lookup); + handle.lookup = FunctionPrototypeBind.$call(lookup6, handle, lookup); } else { - throw new ERR_SOCKET_BAD_TYPE(); + throw $ERR_SOCKET_BAD_TYPE(); } return handle; @@ -241,7 +166,7 @@ function createSocket(type, listener) { } function bufferSize(self, size, buffer) { - if (size >>> 0 !== size) throw new ERR_SOCKET_BAD_BUFFER_SIZE(); + if (size >>> 0 !== size) throw $ERR_SOCKET_BAD_BUFFER_SIZE(); const ctx = {}; // const ret = self[kStateSymbol].handle.bufferSize(size, buffer, ctx); @@ -257,7 +182,7 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { const state = this[kStateSymbol]; - if (state.bindState !== BIND_STATE_UNBOUND) throw new ERR_SOCKET_ALREADY_BOUND(); + if (state.bindState !== BIND_STATE_UNBOUND) throw $ERR_SOCKET_ALREADY_BOUND(); state.bindState = BIND_STATE_BINDING; @@ -354,7 +279,7 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { family, }); }, - error: (_socket, error) => { + error: error => { this.emit("error", error); }, }, @@ -393,13 +318,13 @@ Socket.prototype.connect = function (port, address, callback) { const state = this[kStateSymbol]; - if (state.connectState !== CONNECT_STATE_DISCONNECTED) throw new ERR_SOCKET_DGRAM_IS_CONNECTED(); + if (state.connectState !== CONNECT_STATE_DISCONNECTED) throw $ERR_SOCKET_DGRAM_IS_CONNECTED(); state.connectState = CONNECT_STATE_CONNECTING; if (state.bindState === BIND_STATE_UNBOUND) this.bind({ port: 0, exclusive: true }, null); if (state.bindState !== BIND_STATE_BOUND) { - enqueue(this, FunctionPrototypeBind(_connect, this, port, address, callback)); + enqueue(this, FunctionPrototypeBind.$call(_connect, this, port, address, callback)); return; } @@ -451,7 +376,7 @@ const disconnectFn = $newZigFunction("udp_socket.zig", "UDPSocket.jsDisconnect", Socket.prototype.disconnect = function () { const state = this[kStateSymbol]; - if (state.connectState !== CONNECT_STATE_CONNECTED) throw new ERR_SOCKET_DGRAM_NOT_CONNECTED(); + if (state.connectState !== CONNECT_STATE_CONNECTED) throw $ERR_SOCKET_DGRAM_NOT_CONNECTED(); disconnectFn.$call(state.handle.socket); state.connectState = CONNECT_STATE_DISCONNECTED; @@ -471,17 +396,17 @@ function sliceBuffer(buffer, offset, length) { if (typeof buffer === "string") { buffer = Buffer.from(buffer); } else if (!ArrayBuffer.isView(buffer)) { - throw new ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); + throw $ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); } offset = offset >>> 0; length = length >>> 0; if (offset > buffer.byteLength) { - throw new ERR_BUFFER_OUT_OF_BOUNDS("offset"); + throw $ERR_BUFFER_OUT_OF_BOUNDS("offset"); } if (offset + length > buffer.byteLength) { - throw new ERR_BUFFER_OUT_OF_BOUNDS("length"); + throw $ERR_BUFFER_OUT_OF_BOUNDS("length"); } return Buffer.from(buffer.buffer, buffer.byteOffset + offset, length); @@ -570,19 +495,19 @@ Socket.prototype.send = function (buffer, offset, length, port, address, callbac callback = offset; } - if (port || address) throw new ERR_SOCKET_DGRAM_IS_CONNECTED(); + if (port || address) throw $ERR_SOCKET_DGRAM_IS_CONNECTED(); } if (!Array.isArray(buffer)) { if (typeof buffer === "string") { list = [Buffer.from(buffer)]; } else if (!ArrayBuffer.isView(buffer)) { - throw new ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); + throw $ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); } else { list = [buffer]; } } else if (!(list = fixBufferList(buffer))) { - throw new ERR_INVALID_ARG_TYPE("buffer list arguments", ["Buffer", "TypedArray", "DataView", "string"], buffer); + throw $ERR_INVALID_ARG_TYPE("buffer list arguments", ["Buffer", "TypedArray", "DataView", "string"], buffer); } if (!connected) port = validatePort(port, "Port", false); @@ -605,7 +530,7 @@ Socket.prototype.send = function (buffer, offset, length, port, address, callbac // If the socket hasn't been bound yet, push the outbound packet onto the // send queue and send after binding is complete. if (state.bindState !== BIND_STATE_BOUND) { - enqueue(this, FunctionPrototypeBind(this.send, this, list, port, address, callback)); + enqueue(this, FunctionPrototypeBind.$call(this.send, this, list, port, address, callback)); return; } @@ -713,7 +638,7 @@ Socket.prototype.close = function (callback) { if (typeof callback === "function") this.on("close", callback); if (queue !== undefined) { - queue.push(FunctionPrototypeBind(this.close, this)); + queue.push(FunctionPrototypeBind.$call(this.close, this)); return this; } @@ -747,7 +672,7 @@ function socketCloseNT(self) { Socket.prototype.address = function () { const addr = this[kStateSymbol].handle.socket?.address; - if (!addr) throw new ERR_SOCKET_DGRAM_NOT_RUNNING(); + if (!addr) throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); return addr; }; @@ -755,11 +680,11 @@ Socket.prototype.remoteAddress = function () { const state = this[kStateSymbol]; const socket = state.handle.socket; - if (!socket) throw new ERR_SOCKET_DGRAM_NOT_RUNNING(); + if (!socket) throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); - if (state.connectState !== CONNECT_STATE_CONNECTED) throw new ERR_SOCKET_DGRAM_NOT_CONNECTED(); + if (state.connectState !== CONNECT_STATE_CONNECTED) throw $ERR_SOCKET_DGRAM_NOT_CONNECTED(); - if (!socket.remoteAddress) throw new ERR_SOCKET_DGRAM_NOT_CONNECTED(); + if (!socket.remoteAddress) throw $ERR_SOCKET_DGRAM_NOT_CONNECTED(); return socket.remoteAddress; }; @@ -830,7 +755,7 @@ Socket.prototype.addMembership = function (multicastAddress, interfaceAddress) { throwNotImplemented("addMembership", 10381); /* if (!multicastAddress) { - throw new ERR_MISSING_ARGS('multicastAddress'); + throw $ERR_MISSING_ARGS('multicastAddress'); } const { handle } = this[kStateSymbol]; @@ -845,7 +770,7 @@ Socket.prototype.dropMembership = function (multicastAddress, interfaceAddress) throwNotImplemented("dropMembership", 10381); /* if (!multicastAddress) { - throw new ERR_MISSING_ARGS('multicastAddress'); + throw $ERR_MISSING_ARGS('multicastAddress'); } const { handle } = this[kStateSymbol]; diff --git a/src/js/node/diagnostics_channel.ts b/src/js/node/diagnostics_channel.ts index 57722725e7..dfdca8b13d 100644 --- a/src/js/node/diagnostics_channel.ts +++ b/src/js/node/diagnostics_channel.ts @@ -2,7 +2,6 @@ // Reference: https://github.com/nodejs/node/blob/fb47afc335ef78a8cef7eac52b8ee7f045300696/lib/diagnostics_channel.js const { validateFunction } = require("internal/validators"); -const { ERR_INVALID_ARG_TYPE } = require("internal/errors"); const SafeMap = Map; const SafeFinalizationRegistry = FinalizationRegistry; @@ -13,8 +12,8 @@ const ArrayPrototypeSplice = Array.prototype.splice; const ObjectGetPrototypeOf = Object.getPrototypeOf; const ObjectSetPrototypeOf = Object.setPrototypeOf; const SymbolHasInstance = Symbol.hasInstance; -const PromiseResolve = Promise.resolve; -const PromiseReject = Promise.reject; +const PromiseResolve = Promise.resolve.bind(Promise); +const PromiseReject = Promise.reject.bind(Promise); const PromisePrototypeThen = (promise, onFulfilled, onRejected) => promise.then(onFulfilled, onRejected); // TODO: https://github.com/nodejs/node/blob/fb47afc335ef78a8cef7eac52b8ee7f045300696/src/node_util.h#L13 @@ -212,7 +211,7 @@ function channel(name) { if (channel) return channel; if (typeof name !== "string" && typeof name !== "symbol") { - throw ERR_INVALID_ARG_TYPE("channel", "string or symbol", name); + throw $ERR_INVALID_ARG_TYPE("channel", "string or symbol", name); } return new Channel(name); @@ -237,7 +236,7 @@ const traceEvents = ["start", "end", "asyncStart", "asyncEnd", "error"]; function assertChannel(value, name) { if (!(value instanceof Channel)) { - throw ERR_INVALID_ARG_TYPE(name, ["Channel"], value); + throw $ERR_INVALID_ARG_TYPE(name, ["Channel"], value); } } @@ -264,7 +263,7 @@ class TracingChannel { this.asyncEnd = asyncEnd; this.error = error; } else { - throw ERR_INVALID_ARG_TYPE("nameOrChannels", ["string, object, or Channel"], nameOrChannels); + throw $ERR_INVALID_ARG_TYPE("nameOrChannels", ["string, object, or Channel"], nameOrChannels); } } diff --git a/src/js/node/dns.ts b/src/js/node/dns.ts index cfde60acd3..de7af10956 100644 --- a/src/js/node/dns.ts +++ b/src/js/node/dns.ts @@ -1,7 +1,45 @@ // Hardcoded module "node:dns" -// only resolve4, resolve, lookup, resolve6, resolveSrv, and reverse are implemented. const dns = Bun.dns; const utilPromisifyCustomSymbol = Symbol.for("nodejs.util.promisify.custom"); +const { isIP } = require("./net"); +const { + validateFunction, + validateArray, + validateString, + validateBoolean, + validateNumber, +} = require("internal/validators"); + +const errorCodes = { + NODATA: "ENODATA", + FORMERR: "EFORMERR", + SERVFAIL: "ESERVFAIL", + NOTFOUND: "ENOTFOUND", + NOTIMP: "ENOTIMP", + REFUSED: "EREFUSED", + BADQUERY: "EBADQUERY", + BADNAME: "EBADNAME", + BADFAMILY: "EBADFAMILY", + BADRESP: "EBADRESP", + CONNREFUSED: "ECONNREFUSED", + TIMEOUT: "ETIMEOUT", + EOF: "EOF", + FILE: "EFILE", + NOMEM: "ENOMEM", + DESTRUCTION: "EDESTRUCTION", + BADSTR: "EBADSTR", + BADFLAGS: "EBADFLAGS", + NONAME: "ENONAME", + BADHINTS: "EBADHINTS", + NOTINITIALIZED: "ENOTINITIALIZED", + LOADIPHLPAPI: "ELOADIPHLPAPI", + ADDRGETNETWORKPARAMS: "EADDRGETNETWORKPARAMS", + CANCELLED: "ECANCELLED", +}; + +const IANA_DNS_PORT = 53; +const IPv6RE = /^\[([^[\]]*)\]/; +const addrSplitRE = /(^.+?)(?::(\d+))?$/; function translateErrorCode(promise: Promise) { return promise.catch(error => { @@ -23,32 +61,255 @@ function getServers() { return dns.getServers(); } -function lookup(domain, options, callback) { - if (typeof options == "function") { - callback = options; +function setServers(servers) { + return setServersOn(servers, dns); +} + +const getRuntimeDefaultResultOrderOption = $newZigFunction( + "dns_resolver.zig", + "DNSResolver.getRuntimeDefaultResultOrderOption", + 0, +); + +function newResolver(options) { + if (!newResolver.zig) { + newResolver.zig = $newZigFunction("dns_resolver.zig", "DNSResolver.newResolver", 1); + } + return newResolver.zig(options); +} + +function defaultResultOrder() { + if (typeof defaultResultOrder.value === "undefined") { + defaultResultOrder.value = getRuntimeDefaultResultOrderOption(); } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + return defaultResultOrder.value; +} - if (typeof options == "number") { - options = { family: options }; - } +function setDefaultResultOrder(order) { + validateOrder(order); + defaultResultOrder.value = order; +} - if (domain !== domain || (typeof domain !== "number" && !domain)) { - console.warn( - `DeprecationWarning: The provided hostname "${String( - domain, - )}" is not a valid hostname, and is supported in the dns module solely for compatibility.`, - ); - callback(null, null, 4); +function getDefaultResultOrder() { + return defaultResultOrder; +} + +function setServersOn(servers, object) { + validateArray(servers, "servers"); + + const triples = []; + + servers.forEach((server, i) => { + validateString(server, `servers[${i}]`); + let ipVersion = isIP(server); + + if (ipVersion !== 0) { + triples.push([ipVersion, server, IANA_DNS_PORT]); + return; + } + + const match = IPv6RE.exec(server); + + // Check for an IPv6 in brackets. + if (match) { + ipVersion = isIP(match[1]); + if (ipVersion !== 0) { + const port = parseInt(addrSplitRE[Symbol.replace](server, "$2")) || IANA_DNS_PORT; + triples.push([ipVersion, match[1], port]); + return; + } + } + + // addr:port + const addrSplitMatch = addrSplitRE.exec(server); + + if (addrSplitMatch) { + const hostIP = addrSplitMatch[1]; + const port = addrSplitMatch[2] || IANA_DNS_PORT; + + ipVersion = isIP(hostIP); + + if (ipVersion !== 0) { + triples.push([ipVersion, hostIP, parseInt(port)]); + return; + } + } + + throw $ERR_INVALID_IP_ADDRESS(server); + }); + + object.setServers(triples); +} + +function validateFlagsOption(options) { + if (options.flags === undefined) { return; } - dns.lookup(domain, options).then(res => { + validateNumber(options.flags); + + if ((options.flags & ~(dns.ALL | dns.ADDRCONFIG | dns.V4MAPPED)) != 0) { + throw $ERR_INVALID_ARG_VALUE("hints", options.flags, "is invalid"); + } +} + +function validateFamily(family) { + if (family !== 6 && family !== 4 && family !== 0) { + throw $ERR_INVALID_ARG_VALUE("family", family, "must be one of 0, 4 or 6"); + } +} + +function validateFamilyOption(options) { + if (options.family != null) { + switch (options.family) { + case "IPv4": + options.family = 4; + break; + case "IPv6": + options.family = 6; + break; + default: + validateFamily(options.family); + break; + } + } +} + +function validateAllOption(options) { + if (options.all !== undefined) { + validateBoolean(options.all); + } +} + +function validateVerbatimOption(options) { + if (options.verbatim !== undefined) { + validateBoolean(options.verbatim); + } +} + +function validateOrder(order) { + if (!["ipv4first", "ipv6first", "verbatim"].includes(order)) { + throw $ERR_INVALID_ARG_VALUE("order", order, "is invalid"); + } +} + +function validateOrderOption(options) { + if (options.order !== undefined) { + validateOrder(options.order); + } +} + +function validateResolve(hostname, callback) { + if (typeof hostname !== "string") { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); + } + + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); + } +} + +function validateLocalAddresses(first, second) { + validateString(first); + if (typeof second !== "undefined") { + validateString(second); + } +} + +function invalidHostname(hostname) { + if (invalidHostname.warned) { + return; + } + + invalidHostname.warned = true; + process.emitWarning( + `The provided hostname "${String(hostname)}" is not a valid hostname, and is supported in the dns module solely for compatibility.`, + "DeprecationWarning", + "DEP0118", + ); +} + +function translateLookupOptions(options) { + if (!options || typeof options !== "object") { + options = { family: options }; + } + + let { family, order, verbatim, hints: flags, all } = options; + + if (order === undefined && typeof verbatim === "boolean") { + order = verbatim ? "verbatim" : "ipv4first"; + } + + order ??= defaultResultOrder(); + + return { + family, + flags, + all, + order, + verbatim, + }; +} + +function validateLookupOptions(options) { + validateFlagsOption(options); + validateFamilyOption(options); + validateAllOption(options); + validateVerbatimOption(options); + validateOrderOption(options); +} + +function lookup(hostname, options, callback) { + if (typeof hostname !== "string" && hostname) { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); + } + + if (typeof options === "function") { + callback = options; + options = { family: 0 }; + } else if (typeof options === "number") { + validateFunction(callback, "callback"); + validateFamily(options); + options = { family: options }; + } else if (options !== undefined && typeof options !== "object") { + validateFunction(arguments.length === 2 ? options : callback, "callback"); + throw $ERR_INVALID_ARG_TYPE("options", ["integer", "object"], options); + } + + validateFunction(callback, "callback"); + + options = translateLookupOptions(options); + validateLookupOptions(options); + + if (!hostname) { + invalidHostname(hostname); + if (options.all) { + callback(null, []); + } else { + callback(null, null, 4); + } + return; + } + + const family = isIP(hostname); + if (family) { + if (options.all) { + process.nextTick(callback, null, [{ address: hostname, family }]); + } else { + process.nextTick(callback, null, hostname, family); + } + return; + } + + dns.lookup(hostname, options).then(res => { throwIfEmpty(res); - res.sort((a, b) => a.family - b.family); + + if (options.order == "ipv4first") { + res.sort((a, b) => a.family - b.family); + } else if (options.order == "ipv6first") { + res.sort((a, b) => b.family - a.family); + } if (options?.all) { callback(null, res.map(mapLookupAll)); @@ -60,11 +321,17 @@ function lookup(domain, options, callback) { } function lookupService(address, port, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (arguments.length < 3) { + throw $ERR_MISSING_ARGS("address", "port", "callback"); } - dns.lookupService(address, port, callback).then( + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); + } + + validateString(address); + + dns.lookupService(address, port).then( results => { callback(null, ...results); }, @@ -74,41 +341,77 @@ function lookupService(address, port, callback) { ); } -var InternalResolver = class Resolver { - constructor(options) {} +function validateResolverOptions(options) { + if (options === undefined) { + return; + } - cancel() {} + for (const key of ["timeout", "tries"]) { + if (key in options) { + if (typeof options[key] !== "number") { + throw $ERR_INVALID_ARG_TYPE(key, "number", options[key]); + } + } + } + + if ("timeout" in options) { + const timeout = options.timeout; + if ((timeout < 0 && timeout != -1) || Math.floor(timeout) != timeout || timeout >= 2 ** 31) { + throw $ERR_OUT_OF_RANGE("Invalid timeout", timeout); + } + } +} + +var InternalResolver = class Resolver { + #resolver; + + constructor(options) { + validateResolverOptions(options); + this.#resolver = this._handle = newResolver(options); + } + + cancel() { + this.#resolver.cancel(); + } + + static #getResolver(object) { + return typeof object !== "undefined" && #resolver in object ? object.#resolver : dns; + } getServers() { - return []; + return Resolver.#getResolver(this).getServers() || []; } resolve(hostname, rrtype, callback) { - if (typeof rrtype == "function") { + if (typeof rrtype === "function") { callback = rrtype; - rrtype = null; + rrtype = "A"; + } else if (typeof rrtype === "undefined") { + rrtype = "A"; + } else if (typeof rrtype !== "string") { + throw $ERR_INVALID_ARG_TYPE("rrtype", "string", rrtype); } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolve(hostname).then( - results => { - switch (rrtype?.toLowerCase()) { - case "a": - case "aaaa": - callback(null, hostname, results.map(mapResolveX)); - break; - default: - callback(null, results); - break; - } - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolve(hostname) + .then( + results => { + switch (rrtype?.toLowerCase()) { + case "a": + case "aaaa": + callback(null, hostname, results.map(mapResolveX)); + break; + default: + callback(null, results); + break; + } + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolve4(hostname, options, callback) { @@ -117,18 +420,18 @@ var InternalResolver = class Resolver { options = null; } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.lookup(hostname, { family: 4 }).then( - addresses => { - callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolve(hostname, "A") + .then( + addresses => { + callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolve6(hostname, options, callback) { @@ -137,174 +440,200 @@ var InternalResolver = class Resolver { options = null; } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.lookup(hostname, { family: 6 }).then( - addresses => { - callback(null, options?.ttl ? addresses : addresses.map(({ address }) => address)); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolve(hostname, "AAAA") + .then( + addresses => { + callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveAny(hostname, callback) { - callback(null, []); + validateResolve(hostname, callback); + + Resolver.#getResolver(this) + .resolveAny(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveCname(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveCname(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveCname(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveMx(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveMx(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveMx(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveNaptr(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveNaptr(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveNaptr(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveNs(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveNs(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveNs(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolvePtr(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolvePtr(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolvePtr(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveSrv(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveSrv(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveSrv(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveCaa(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.resolveCaa(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveCaa(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveTxt(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.resolveTxt(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveTxt(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveSoa(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.resolveSoa(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveSoa(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } reverse(ip, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.reverse(ip, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .reverse(ip) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } - setServers(servers) {} + setLocalAddress(first, second) { + validateLocalAddresses(first, second); + Resolver.#getResolver(this).setLocalAddress(first, second); + } + + setServers(servers) { + return setServersOn(servers, Resolver.#getResolver(this)); + } }; function Resolver(options) { @@ -331,9 +660,6 @@ var { resolveTxt, } = InternalResolver.prototype; -function setDefaultResultOrder() {} -function setServers() {} - const mapLookupAll = res => { const { address, family } = res; return { address, family }; @@ -352,65 +678,133 @@ function throwIfEmpty(res) { } Object.defineProperty(throwIfEmpty, "name", { value: "::bunternal::" }); -const promisifyLookup = res => { +const promisifyLookup = order => res => { throwIfEmpty(res); - res.sort((a, b) => a.family - b.family); + if (order == "ipv4first") { + res.sort((a, b) => a.family - b.family); + } else if (order == "ipv6first") { + res.sort((a, b) => b.family - a.family); + } const [{ address, family }] = res; return { address, family }; }; -const promisifyLookupAll = res => { +const promisifyLookupAll = order => res => { throwIfEmpty(res); - res.sort((a, b) => a.family - b.family); + if (order == "ipv4first") { + res.sort((a, b) => a.family - b.family); + } else if (order == "ipv6first") { + res.sort((a, b) => b.family - a.family); + } return res.map(mapLookupAll); }; const mapResolveX = a => a.address; -const promisifyResolveX = res => { - return res?.map(mapResolveX); +const promisifyResolveX = ttl => { + if (ttl) { + return res => res; + } else { + return res => { + return res?.map(mapResolveX); + }; + } }; // promisified versions const promises = { - lookup(domain, options) { - if (options?.all) { - return translateErrorCode(dns.lookup(domain, options).then(promisifyLookupAll)); + ...errorCodes, + + lookup(hostname, options) { + if (typeof hostname !== "string" && hostname) { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); } - return translateErrorCode(dns.lookup(domain, options).then(promisifyLookup)); + + if (typeof options === "number") { + validateFamily(options); + options = { family: options }; + } else if (options !== undefined && typeof options !== "object") { + throw $ERR_INVALID_ARG_TYPE("options", ["integer", "object"], options); + } + + options = translateLookupOptions(options); + validateLookupOptions(options); + + if (!hostname) { + invalidHostname(hostname); + return Promise.resolve( + options.all + ? [] + : { + address: null, + family: 4, + }, + ); + } + + const family = isIP(hostname); + if (family) { + const obj = { address: hostname, family }; + return Promise.resolve(options.all ? [obj] : obj); + } + + if (options.all) { + return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookupAll(options.order))); + } + return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookup(options.order))); }, lookupService(address, port) { - return translateErrorCode(dns.lookupService(address, port)); + if (arguments.length !== 2) { + throw $ERR_MISSING_ARGS("address", "port"); + } + + validateString(address); + + try { + return translateErrorCode(dns.lookupService(address, port)).then(([hostname, service]) => ({ + hostname, + service, + })); + } catch (err) { + if (err.name === "TypeError" || err.name === "RangeError") { + throw err; + } + return Promise.reject(withTranslatedError(err)); + } }, resolve(hostname, rrtype) { - if (typeof rrtype !== "string") { - rrtype = null; + if (typeof hostname !== "string") { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); } + + if (typeof rrtype === "undefined") { + rrtype = "A"; + } else if (typeof rrtype !== "string") { + throw $ERR_INVALID_ARG_TYPE("rrtype", "string", rrtype); + } + switch (rrtype?.toLowerCase()) { case "a": case "aaaa": - return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyLookup)); + return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyLookup(defaultResultOrder()))); default: return translateErrorCode(dns.resolve(hostname, rrtype)); } }, resolve4(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 4 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 4 }).then(promisifyResolveX)); + return translateErrorCode(dns.resolve(hostname, "A").then(promisifyResolveX(options?.ttl))); }, resolve6(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 6 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 6 }).then(promisifyResolveX)); + return translateErrorCode(dns.resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl))); }, + resolveAny(hostname) { + return translateErrorCode(dns.resolveAny(hostname)); + }, resolveSrv(hostname) { return translateErrorCode(dns.resolveSrv(hostname)); }, @@ -423,7 +817,6 @@ const promises = { resolveNaptr(hostname) { return translateErrorCode(dns.resolveNaptr(hostname)); }, - resolveMx(hostname) { return translateErrorCode(dns.resolveMx(hostname)); }, @@ -444,91 +837,111 @@ const promises = { }, Resolver: class Resolver { - constructor(options) {} + #resolver; - cancel() {} + constructor(options) { + validateResolverOptions(options); + this.#resolver = this._handle = newResolver(options); + } + + cancel() { + this.#resolver.cancel(); + } + + static #getResolver(object) { + return typeof object !== "undefined" && #resolver in object ? object.#resolver : dns; + } getServers() { - return []; + return Resolver.#getResolver(this).getServers() || []; } resolve(hostname, rrtype) { - if (typeof rrtype !== "string") { + if (typeof rrtype === "undefined") { + rrtype = "A"; + } else if (typeof rrtype !== "string") { rrtype = null; } switch (rrtype?.toLowerCase()) { case "a": case "aaaa": - return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyLookup)); + return translateErrorCode( + Resolver.#getResolver(this).resolve(hostname, rrtype).then(promisifyLookup(defaultResultOrder())), + ); default: - return translateErrorCode(dns.resolve(hostname, rrtype)); + return translateErrorCode(Resolver.#getResolver(this).resolve(hostname, rrtype)); } } resolve4(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 4 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 4 }).then(promisifyResolveX)); + return translateErrorCode( + Resolver.#getResolver(this).resolve(hostname, "A").then(promisifyResolveX(options?.ttl)), + ); } resolve6(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 6 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 6 }).then(promisifyResolveX)); + return translateErrorCode( + Resolver.#getResolver(this).resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl)), + ); } resolveAny(hostname) { - return Promise.resolve([]); + return translateErrorCode(Resolver.#getResolver(this).resolveAny(hostname)); } resolveCname(hostname) { - return translateErrorCode(dns.resolveCname(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveCname(hostname)); } resolveMx(hostname) { - return translateErrorCode(dns.resolveMx(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveMx(hostname)); } resolveNaptr(hostname) { - return translateErrorCode(dns.resolveNaptr(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveNaptr(hostname)); } resolveNs(hostname) { - return translateErrorCode(dns.resolveNs(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveNs(hostname)); } resolvePtr(hostname) { - return translateErrorCode(dns.resolvePtr(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolvePtr(hostname)); } resolveSoa(hostname) { - return translateErrorCode(dns.resolveSoa(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveSoa(hostname)); } resolveSrv(hostname) { - return translateErrorCode(dns.resolveSrv(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveSrv(hostname)); } resolveCaa(hostname) { - return translateErrorCode(dns.resolveCaa(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveCaa(hostname)); } resolveTxt(hostname) { - return translateErrorCode(dns.resolveTxt(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveTxt(hostname)); } reverse(ip) { - return translateErrorCode(dns.reverse(ip)); + return translateErrorCode(Resolver.#getResolver(this).reverse(ip)); } - setServers(servers) {} + setLocalAddress(first, second) { + validateLocalAddresses(first, second); + Resolver.#getResolver(this).setLocalAddress(first, second); + } + + setServers(servers) { + return setServersOn(servers, Resolver.#getResolver(this)); + } }, + + setDefaultResultOrder, + setServers, }; -for (const key of ["resolveAny"]) { - promises[key] = () => Promise.resolve(undefined); -} // Compatibility with util.promisify(dns[method]) for (const [method, pMethod] of [ @@ -553,42 +966,19 @@ for (const [method, pMethod] of [ } export default { - // these are wrong - ADDRCONFIG: 0, - ALL: 1, - V4MAPPED: 2, + ADDRCONFIG: dns.ADDRCONFIG, + ALL: dns.ALL, + V4MAPPED: dns.V4MAPPED, // ERROR CODES - NODATA: "DNS_ENODATA", - FORMERR: "DNS_EFORMERR", - SERVFAIL: "DNS_ESERVFAIL", - NOTFOUND: "DNS_ENOTFOUND", - NOTIMP: "DNS_ENOTIMP", - REFUSED: "DNS_EREFUSED", - BADQUERY: "DNS_EBADQUERY", - BADNAME: "DNS_EBADNAME", - BADFAMILY: "DNS_EBADFAMILY", - BADRESP: "DNS_EBADRESP", - CONNREFUSED: "DNS_ECONNREFUSED", - TIMEOUT: "DNS_ETIMEOUT", - EOF: "DNS_EOF", - FILE: "DNS_EFILE", - NOMEM: "DNS_ENOMEM", - DESTRUCTION: "DNS_EDESTRUCTION", - BADSTR: "DNS_EBADSTR", - BADFLAGS: "DNS_EBADFLAGS", - NONAME: "DNS_ENONAME", - BADHINTS: "DNS_EBADHINTS", - NOTINITIALIZED: "DNS_ENOTINITIALIZED", - LOADIPHLPAPI: "DNS_ELOADIPHLPAPI", - ADDRGETNETWORKPARAMS: "DNS_EADDRGETNETWORKPARAMS", - CANCELLED: "DNS_ECANCELLED", + ...errorCodes, lookup, lookupService, Resolver, setServers, setDefaultResultOrder, + getDefaultResultOrder, resolve, reverse, resolve4, diff --git a/src/js/node/domain.ts b/src/js/node/domain.ts index 7bed189e53..23198f15f7 100644 --- a/src/js/node/domain.ts +++ b/src/js/node/domain.ts @@ -1,12 +1,29 @@ // Import Events -var EventEmitter = require("node:events"); +let EventEmitter; + +const ObjectDefineProperty = Object.defineProperty; // Export Domain var domain: any = {}; domain.createDomain = domain.create = function () { + if (!EventEmitter) { + EventEmitter = require("node:events"); + } var d = new EventEmitter(); function emitError(e) { + e ||= $ERR_UNHANDLED_ERROR(); + if (typeof e === "object") { + e.domainEmitter = this; + ObjectDefineProperty(e, "domain", { + __proto__: null, + configurable: true, + enumerable: false, + value: domain, + writable: true, + }); + e.domainThrown = false; + } d.emit("error", e); } diff --git a/src/js/node/events.ts b/src/js/node/events.ts index 2bdb3a4bd5..268b198413 100644 --- a/src/js/node/events.ts +++ b/src/js/node/events.ts @@ -23,16 +23,21 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -const { ERR_INVALID_ARG_TYPE } = require("internal/errors"); const { validateObject, validateInteger, validateAbortSignal, validateNumber, validateBoolean, + validateFunction, } = require("internal/validators"); +const { inspect, types } = require("node:util"); + const SymbolFor = Symbol.for; +const ArrayPrototypeSlice = Array.prototype.slice; +const ArrayPrototypeSplice = Array.prototype.splice; +const ReflectOwnKeys = Reflect.ownKeys; const kCapture = Symbol("kCapture"); const kErrorMonitor = SymbolFor("events.errorMonitor"); @@ -42,7 +47,6 @@ const kWatermarkData = SymbolFor("nodejs.watermarkData"); const kRejection = SymbolFor("nodejs.rejection"); const kFirstEventParam = SymbolFor("nodejs.kFirstEventParam"); const captureRejectionSymbol = SymbolFor("nodejs.rejection"); -const ArrayPrototypeSlice = Array.prototype.slice; let FixedQueue; const kEmptyObject = Object.freeze({ __proto__: null }); @@ -50,7 +54,7 @@ const kEmptyObject = Object.freeze({ __proto__: null }); var defaultMaxListeners = 10; // EventEmitter must be a standard function because some old code will do weird tricks like `EventEmitter.$apply(this)`. -const EventEmitter = function EventEmitter(opts) { +function EventEmitter(opts) { if (this._events === undefined || this._events === this.__proto__._events) { this._events = { __proto__: null }; this._eventsCount = 0; @@ -60,41 +64,62 @@ const EventEmitter = function EventEmitter(opts) { if ((this[kCapture] = opts?.captureRejections ? Boolean(opts?.captureRejections) : EventEmitterPrototype[kCapture])) { this.emit = emitWithRejectionCapture; } -}; +} Object.defineProperty(EventEmitter, "name", { value: "EventEmitter", configurable: true }); const EventEmitterPrototype = (EventEmitter.prototype = {}); -EventEmitterPrototype._events = undefined; -EventEmitterPrototype._eventsCount = 0; -EventEmitterPrototype._maxListeners = undefined; EventEmitterPrototype.setMaxListeners = function setMaxListeners(n) { validateNumber(n, "setMaxListeners", 0); this._maxListeners = n; return this; }; +Object.defineProperty(EventEmitterPrototype.setMaxListeners, "name", { value: "setMaxListeners" }); EventEmitterPrototype.constructor = EventEmitter; EventEmitterPrototype.getMaxListeners = function getMaxListeners() { - return this?._maxListeners ?? defaultMaxListeners; + return _getMaxListeners(this); }; +Object.defineProperty(EventEmitterPrototype.getMaxListeners, "name", { value: "getMaxListeners" }); function emitError(emitter, args) { var { _events: events } = emitter; - args[0] ??= new Error("Unhandled error."); - if (!events) throw args[0]; - var errorMonitor = events[kErrorMonitor]; - if (errorMonitor) { - for (var handler of ArrayPrototypeSlice.$call(errorMonitor)) { - handler.$apply(emitter, args); + + if (events !== undefined) { + const errorMonitor = events[kErrorMonitor]; + if (errorMonitor) { + for (const handler of ArrayPrototypeSlice.$call(errorMonitor)) { + handler.$apply(emitter, args); + } + } + + const handlers = events.error; + if (handlers) { + for (var handler of ArrayPrototypeSlice.$call(handlers)) { + handler.$apply(emitter, args); + } + return true; } } - var handlers = events.error; - if (!handlers) throw args[0]; - for (var handler of ArrayPrototypeSlice.$call(handlers)) { - handler.$apply(emitter, args); + + let er; + if (args.length > 0) er = args[0]; + + if (er instanceof Error) { + throw er; // Unhandled 'error' event } - return true; + + let stringifiedEr; + try { + stringifiedEr = inspect(er); + } catch { + stringifiedEr = er; + } + + // At least give some kind of context to the user + const err = $ERR_UNHANDLED_ERROR(stringifiedEr); + err.context = er; + throw err; // Unhandled 'error' event } function addCatch(emitter, promise, type, args) { @@ -213,7 +238,7 @@ EventEmitterPrototype.addListener = function addListener(type, fn) { this._eventsCount++; } else { handlers.push(fn); - var m = this._maxListeners ?? defaultMaxListeners; + var m = _getMaxListeners(this); if (m > 0 && handlers.length > m && !handlers.warned) { overflowWarning(this, type, handlers); } @@ -238,7 +263,7 @@ EventEmitterPrototype.prependListener = function prependListener(type, fn) { this._eventsCount++; } else { handlers.unshift(fn); - var m = this._maxListeners ?? defaultMaxListeners; + var m = _getMaxListeners(this); if (m > 0 && handlers.length > m && !handlers.warned) { overflowWarning(this, type, handlers); } @@ -249,8 +274,7 @@ EventEmitterPrototype.prependListener = function prependListener(type, fn) { function overflowWarning(emitter, type, handlers) { handlers.warned = true; const warn = new Error( - `Possible EventEmitter memory leak detected. ${handlers.length} ${String(type)} listeners ` + - `added to [${emitter.constructor.name}]. Use emitter.setMaxListeners() to increase limit`, + `Possible EventTarget memory leak detected. ${handlers.length} ${String(type)} listeners added to ${inspect(emitter, { depth: -1 })}. MaxListeners is ${emitter._maxListeners}. Use events.setMaxListeners() to increase limit`, ); warn.name = "MaxListenersExceededWarning"; warn.emitter = emitter; @@ -259,66 +283,100 @@ function overflowWarning(emitter, type, handlers) { process.emitWarning(warn); } -function onceWrapper(type, listener, ...args) { - this.removeListener(type, listener); - listener.$apply(this, args); +function _onceWrap(target, type, listener) { + const state = { fired: false, wrapFn: undefined, target, type, listener }; + const wrapped = onceWrapper.bind(state); + wrapped.listener = listener; + state.wrapFn = wrapped; + return wrapped; +} + +function onceWrapper() { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + if (arguments.length === 0) return this.listener.$call(this.target); + return this.listener.$apply(this.target, arguments); + } } EventEmitterPrototype.once = function once(type, fn) { checkListener(fn); - const bound = onceWrapper.bind(this, type, fn); - bound.listener = fn; - this.addListener(type, bound); + this.on(type, _onceWrap(this, type, fn)); return this; }; +Object.defineProperty(EventEmitterPrototype.once, "name", { value: "once" }); EventEmitterPrototype.prependOnceListener = function prependOnceListener(type, fn) { checkListener(fn); - const bound = onceWrapper.bind(this, type, fn); - bound.listener = fn; - this.prependListener(type, bound); + this.prependListener(type, _onceWrap(this, type, fn)); return this; }; -EventEmitterPrototype.removeListener = function removeListener(type, fn) { - checkListener(fn); - var { _events: events } = this; - if (!events) return this; - var handlers = events[type]; - if (!handlers) return this; - var length = handlers.length; +EventEmitterPrototype.removeListener = function removeListener(type, listener) { + checkListener(listener); + + const events = this._events; + if (events === undefined) return this; + + const list = events[type]; + if (list === undefined) return this; + let position = -1; - for (let i = length - 1; i >= 0; i--) { - if (handlers[i] === fn || handlers[i].listener === fn) { + for (let i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { position = i; break; } } if (position < 0) return this; - if (position === 0) { - handlers.shift(); - } else { - handlers.splice(position, 1); - } - if (handlers.length === 0) { + + if (position === 0) list.shift(); + else ArrayPrototypeSplice.$call(list, position, 1); + + if (list.length === 0) { delete events[type]; this._eventsCount--; } + + if (events.removeListener !== undefined) this.emit("removeListener", type, listener.listener || listener); + return this; }; EventEmitterPrototype.off = EventEmitterPrototype.removeListener; EventEmitterPrototype.removeAllListeners = function removeAllListeners(type) { - var { _events: events } = this; - if (type && events) { - if (events[type]) { - delete events[type]; - this._eventsCount--; + const events = this._events; + if (events === undefined) return this; + + if (events.removeListener === undefined) { + if (type) { + if (events[type]) { + delete events[type]; + this._eventsCount--; + } + } else { + this._events = { __proto__: null }; } - } else { - this._events = { __proto__: null }; + return this; } + + // Emit removeListener for all listeners on all events + if (!type) { + for (const key of ReflectOwnKeys(events)) { + if (key === "removeListener") continue; + this.removeAllListeners(key); + } + this.removeAllListeners("removeListener"); + this._events = { __proto__: null }; + this._eventsCount = 0; + return this; + } + + // emit in LIFO order + const listeners = events[type]; + for (let i = listeners.length - 1; i >= 0; i--) this.removeListener(type, listeners[i]); return this; }; @@ -338,11 +396,21 @@ EventEmitterPrototype.rawListeners = function rawListeners(type) { return handlers.slice(); }; -EventEmitterPrototype.listenerCount = function listenerCount(type) { +EventEmitterPrototype.listenerCount = function listenerCount(type, method) { var { _events: events } = this; if (!events) return 0; + if (method != null) { + var length = 0; + for (const handler of events[type] ?? []) { + if (handler === method || handler.listener === method) { + length++; + } + } + return length; + } return events[type]?.length ?? 0; }; +Object.defineProperty(EventEmitterPrototype.listenerCount, "name", { value: "listenerCount" }); EventEmitterPrototype.eventNames = function eventNames() { return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; @@ -355,7 +423,7 @@ function once(emitter, type, options = kEmptyObject) { var signal = options?.signal; validateAbortSignal(signal, "options.signal"); if (signal?.aborted) { - throw new AbortError(undefined, { cause: signal?.reason }); + throw $makeAbortError(undefined, { cause: signal?.reason }); } const { resolve, reject, promise } = $newPromiseCapability(Promise); const errorListener = err => { @@ -383,7 +451,7 @@ function once(emitter, type, options = kEmptyObject) { function abortListener() { eventTargetAgnosticRemoveListener(emitter, type, resolver); eventTargetAgnosticRemoveListener(emitter, "error", errorListener); - reject(new AbortError(undefined, { cause: signal?.reason })); + reject($makeAbortError(undefined, { cause: signal?.reason })); } if (signal != null) { eventTargetAgnosticAddListener(signal, "abort", abortListener, { once: true }); @@ -391,6 +459,7 @@ function once(emitter, type, options = kEmptyObject) { return promise; } +Object.defineProperty(once, "name", { value: "once" }); const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); function createIterResult(value, done) { @@ -401,7 +470,7 @@ function on(emitter, event, options = kEmptyObject) { validateObject(options, "options"); const signal = options.signal; validateAbortSignal(signal, "options.signal"); - if (signal?.aborted) throw new AbortError(undefined, { cause: signal?.reason }); + if (signal?.aborted) throw $makeAbortError(undefined, { cause: signal?.reason }); // Support both highWaterMark and highWatermark for backward compatibility const highWatermark = options.highWaterMark ?? options.highWatermark ?? Number.MAX_SAFE_INTEGER; validateInteger(highWatermark, "options.highWaterMark", 1); @@ -457,7 +526,7 @@ function on(emitter, event, options = kEmptyObject) { throw(err) { if (!err || !(err instanceof Error)) { - throw ERR_INVALID_ARG_TYPE("EventEmitter.AsyncIterator", "Error", err); + throw $ERR_INVALID_ARG_TYPE("EventEmitter.AsyncIterator", "Error", err); } errorHandler(err); }, @@ -508,7 +577,7 @@ function on(emitter, event, options = kEmptyObject) { return iterator; function abortListener() { - errorHandler(new AbortError(undefined, { cause: signal?.reason })); + errorHandler($makeAbortError(undefined, { cause: signal?.reason })); } function eventHandler(value) { @@ -541,6 +610,8 @@ function on(emitter, event, options = kEmptyObject) { return Promise.resolve(doneResult); } } +Object.defineProperty(on, "name", { value: "on" }); + function listenersController() { const listeners = []; @@ -575,22 +646,23 @@ function getEventListeners(emitter, type) { // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/events.js#L315-L339 function setMaxListeners(n = defaultMaxListeners, ...eventTargets) { validateNumber(n, "setMaxListeners", 0); - const length = eventTargets?.length; - if (length) { - for (let eventTargetOrEmitter of eventTargets) { - // TODO: EventTarget setMaxListeners is not implemented yet. - // Only EventEmitter has it. - if ($isCallable(eventTargetOrEmitter?.setMaxListeners)) { - eventTargetOrEmitter.setMaxListeners(n); - } else if ($isObject(eventTargetOrEmitter) && eventTargetOrEmitter instanceof EventTarget) { - // This is a fake number so that the number can be checked against with getMaxListeners() - eventTargetOrEmitter[eventTargetMaxListenersSymbol] = n; + if (eventTargets.length === 0) { + defaultMaxListeners = n; + } else { + for (let i = 0; i < eventTargets.length; i++) { + const target = eventTargets[i]; + if (types.isEventTarget(target)) { + target[kMaxEventTargetListeners] = n; + target[kMaxEventTargetListenersWarned] = false; + } else if (typeof target.setMaxListeners === "function") { + target.setMaxListeners(n); + } else { + throw $ERR_INVALID_ARG_TYPE("eventTargets", ["EventEmitter", "EventTarget"], target); } } - } else { - defaultMaxListeners = n; } } +Object.defineProperty(setMaxListeners, "name", { value: "setMaxListeners" }); const jsEventTargetGetEventListenersCount = $newCppFunction( "JSEventTarget.cpp", @@ -603,7 +675,26 @@ function listenerCount(emitter, type) { return emitter.listenerCount(type); } - return jsEventTargetGetEventListenersCount(emitter, type); + // EventTarget + const evt_count = jsEventTargetGetEventListenersCount(emitter, type); + if (evt_count !== undefined) return evt_count; + + // EventEmitter's with no `.listenerCount` + return listenerCountSlow(emitter, type); +} +Object.defineProperty(listenerCount, "name", { value: "listenerCount" }); + +function listenerCountSlow(emitter, type) { + const events = emitter._events; + if (events !== undefined) { + const evlistener = events[type]; + if (typeof evlistener === "function") { + return 1; + } else if (evlistener !== undefined) { + return evlistener.length; + } + } + return 0; } function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) { @@ -612,7 +703,7 @@ function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) { } else if (typeof emitter.removeEventListener === "function") { emitter.removeEventListener(name, listener, flags); } else { - throw ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); + throw $ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); } } @@ -626,49 +717,40 @@ function eventTargetAgnosticAddListener(emitter, name, listener, flags) { } else if (typeof emitter.addEventListener === "function") { emitter.addEventListener(name, listener, flags); } else { - throw ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); + throw $ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); } } -class AbortError extends Error { - constructor(message = "The operation was aborted", options = undefined) { - if (options !== undefined && typeof options !== "object") { - throw ERR_INVALID_ARG_TYPE("options", "object", options); - } - super(message, options); - this.code = "ABORT_ERR"; - this.name = "AbortError"; - } -} - -function ERR_OUT_OF_RANGE(name, range, value) { - const err = new RangeError(`The "${name}" argument is out of range. It must be ${range}. Received ${value}`); - err.code = "ERR_OUT_OF_RANGE"; - return err; -} - function checkListener(listener) { - if (typeof listener !== "function") { - throw new TypeError("The listener must be a function"); - } + validateFunction(listener, "listener"); +} + +function _getMaxListeners(emitter) { + return emitter?._maxListeners ?? defaultMaxListeners; } let AsyncResource = null; -const eventTargetMaxListenersSymbol = Symbol("EventTarget.maxListeners"); function getMaxListeners(emitterOrTarget) { - return emitterOrTarget?.[eventTargetMaxListenersSymbol] ?? emitterOrTarget?._maxListeners ?? defaultMaxListeners; + if (typeof emitterOrTarget?.getMaxListeners === "function") { + return _getMaxListeners(emitterOrTarget); + } else if (types.isEventTarget(emitterOrTarget)) { + emitterOrTarget[kMaxEventTargetListeners] ??= defaultMaxListeners; + return emitterOrTarget[kMaxEventTargetListeners]; + } + throw $ERR_INVALID_ARG_TYPE("emitter", ["EventEmitter", "EventTarget"], emitterOrTarget); } +Object.defineProperty(getMaxListeners, "name", { value: "getMaxListeners" }); // Copy-pasta from Node.js source code function addAbortListener(signal, listener) { if (signal === undefined) { - throw ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); + throw $ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); } validateAbortSignal(signal, "signal"); if (typeof listener !== "function") { - throw ERR_INVALID_ARG_TYPE("listener", "function", listener); + throw $ERR_INVALID_ARG_TYPE("listener", "function", listener); } let removeEventListener; diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index 5960cc01bf..98efb00193 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -21,6 +21,8 @@ const kDeserialize = Symbol("kDeserialize"); const kEmptyObject = ObjectFreeze({ __proto__: null }); const kFlag = Symbol("kFlag"); +const { validateObject } = require("internal/validators"); + function watch( filename: string | Buffer | URL, options: { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal } = {}, @@ -35,7 +37,7 @@ function watch( } else if (Buffer.isBuffer(filename)) { filename = filename.toString(); } else if (typeof filename !== "string") { - throw new TypeError("Expected path to be a string or Buffer"); + throw $ERR_INVALID_ARG_TYPE("filename", ["string", "Buffer", "URL"], filename); } let nextEventResolve: Function | null = null; if (typeof options === "string") { @@ -149,7 +151,7 @@ const private_symbols = { kRef, kUnref, kFd, - FileHandle: null, + FileHandle: null as any, fs, }; @@ -158,13 +160,13 @@ const _writeFile = fs.writeFile.bind(fs); const _appendFile = fs.appendFile.bind(fs); const exports = { - access: fs.access.bind(fs), - appendFile: function (fileHandleOrFdOrPath, ...args) { + access: asyncWrap(fs.access, "access"), + appendFile: async function (fileHandleOrFdOrPath, ...args) { fileHandleOrFdOrPath = fileHandleOrFdOrPath?.[kFd] ?? fileHandleOrFdOrPath; return _appendFile(fileHandleOrFdOrPath, ...args); }, - close: fs.close.bind(fs), - copyFile: fs.copyFile.bind(fs), + close: asyncWrap(fs.close, "close"), + copyFile: asyncWrap(fs.copyFile, "copyFile"), cp, exists: async function exists() { try { @@ -173,32 +175,32 @@ const exports = { return false; } }, - chown: fs.chown.bind(fs), - chmod: fs.chmod.bind(fs), - fchmod: fs.fchmod.bind(fs), - fchown: fs.fchown.bind(fs), - fstat: fs.fstat.bind(fs), - fsync: fs.fsync.bind(fs), - fdatasync: fs.fdatasync.bind(fs), - ftruncate: fs.ftruncate.bind(fs), - futimes: fs.futimes.bind(fs), - lchmod: fs.lchmod.bind(fs), - lchown: fs.lchown.bind(fs), - link: fs.link.bind(fs), - lstat: fs.lstat.bind(fs), - mkdir: fs.mkdir.bind(fs), - mkdtemp: fs.mkdtemp.bind(fs), + chown: asyncWrap(fs.chown, "chown"), + chmod: asyncWrap(fs.chmod, "chmod"), + fchmod: asyncWrap(fs.fchmod, "fchmod"), + fchown: asyncWrap(fs.fchown, "fchown"), + fstat: asyncWrap(fs.fstat, "fstat"), + fsync: asyncWrap(fs.fsync, "fsync"), + fdatasync: asyncWrap(fs.fdatasync, "fdatasync"), + ftruncate: asyncWrap(fs.ftruncate, "ftruncate"), + futimes: asyncWrap(fs.futimes, "futimes"), + lchmod: asyncWrap(fs.lchmod, "lchmod"), + lchown: asyncWrap(fs.lchown, "lchown"), + link: asyncWrap(fs.link, "link"), + lstat: asyncWrap(fs.lstat, "lstat"), + mkdir: asyncWrap(fs.mkdir, "mkdir"), + mkdtemp: asyncWrap(fs.mkdtemp, "mkdtemp"), open: async (path, flags = "r", mode = 0o666) => { return new FileHandle(await fs.open(path, flags, mode), flags); }, - read: fs.read.bind(fs), - write: fs.write.bind(fs), - readdir: fs.readdir.bind(fs), + read: asyncWrap(fs.read, "read"), + write: asyncWrap(fs.write, "write"), + readdir: asyncWrap(fs.readdir, "readdir"), readFile: function (fileHandleOrFdOrPath, ...args) { fileHandleOrFdOrPath = fileHandleOrFdOrPath?.[kFd] ?? fileHandleOrFdOrPath; return _readFile(fileHandleOrFdOrPath, ...args); }, - writeFile: function (fileHandleOrFdOrPath, ...args) { + writeFile: function (fileHandleOrFdOrPath, ...args: any[]) { fileHandleOrFdOrPath = fileHandleOrFdOrPath?.[kFd] ?? fileHandleOrFdOrPath; if ( !$isTypedArrayView(args[0]) && @@ -207,21 +209,22 @@ const exports = { ) { $debug("fs.promises.writeFile async iterator slow path!"); // Node accepts an arbitrary async iterator here + // @ts-expect-error TODO return writeFileAsyncIterator(fileHandleOrFdOrPath, ...args); } return _writeFile(fileHandleOrFdOrPath, ...args); }, - readlink: fs.readlink.bind(fs), - realpath: fs.realpath.bind(fs), - rename: fs.rename.bind(fs), - stat: fs.stat.bind(fs), - symlink: fs.symlink.bind(fs), - truncate: fs.truncate.bind(fs), - unlink: fs.unlink.bind(fs), - utimes: fs.utimes.bind(fs), - lutimes: fs.lutimes.bind(fs), - rm: fs.rm.bind(fs), - rmdir: fs.rmdir.bind(fs), + readlink: asyncWrap(fs.readlink, "readlink"), + realpath: asyncWrap(fs.realpath, "realpath"), + rename: asyncWrap(fs.rename, "rename"), + stat: asyncWrap(fs.stat, "stat"), + symlink: asyncWrap(fs.symlink, "symlink"), + truncate: asyncWrap(fs.truncate, "truncate"), + unlink: asyncWrap(fs.unlink, "unlink"), + utimes: asyncWrap(fs.utimes, "utimes"), + lutimes: asyncWrap(fs.lutimes, "lutimes"), + rm: asyncWrap(fs.rm, "rm"), + rmdir: asyncWrap(fs.rmdir, "rmdir"), writev: async (fd, buffers, position) => { var bytesWritten = await fs.writev(fd, buffers, position); return { @@ -247,6 +250,15 @@ const exports = { }; export default exports; +function asyncWrap(fn: any, name: string) { + const wrapped = async function (...args) { + return fn.$apply(fs, args); + }; + Object.defineProperty(wrapped, "name", { value: name }); + Object.defineProperty(wrapped, "length", { value: fn.length }); + return wrapped; +} + { const { writeFile, @@ -264,6 +276,7 @@ export default exports; writev, close, } = exports; + let isArrayBufferView; // Partially taken from https://github.com/nodejs/node/blob/c25878d370/lib/internal/fs/promises.js#L148 // These functions await the result so that errors propagate correctly with @@ -291,9 +304,9 @@ export default exports; [kClosePromise]; [kRefs]; - async appendFile(data, options: object | string | undefined) { + async appendFile(data, options) { const fd = this[kFd]; - throwEBADFIfNecessary(writeFile, fd); + throwEBADFIfNecessary("writeFile", fd); let encoding = "utf8"; let flush = false; @@ -315,7 +328,7 @@ export default exports; async chmod(mode) { const fd = this[kFd]; - throwEBADFIfNecessary(fchmod, fd); + throwEBADFIfNecessary("fchmod", fd); try { this[kRef](); @@ -327,7 +340,7 @@ export default exports; async chown(uid, gid) { const fd = this[kFd]; - throwEBADFIfNecessary(fchown, fd); + throwEBADFIfNecessary("fchown", fd); try { this[kRef](); @@ -339,7 +352,7 @@ export default exports; async datasync() { const fd = this[kFd]; - throwEBADFIfNecessary(fdatasync, fd); + throwEBADFIfNecessary("fdatasync", fd); try { this[kRef](); @@ -351,7 +364,7 @@ export default exports; async sync() { const fd = this[kFd]; - throwEBADFIfNecessary(fsync, fd); + throwEBADFIfNecessary("fsync", fd); try { this[kRef](); @@ -363,7 +376,21 @@ export default exports; async read(buffer, offset, length, position) { const fd = this[kFd]; - throwEBADFIfNecessary(read, fd); + throwEBADFIfNecessary("read", fd); + + isArrayBufferView ??= require("node:util/types").isArrayBufferView; + if (!isArrayBufferView(buffer)) { + // This is fh.read(params) + if (buffer != undefined) { + validateObject(buffer, "options"); + } + ({ buffer = Buffer.alloc(16384), offset = 0, length, position = null } = buffer ?? {}); + } + length = length ?? buffer?.byteLength - offset; + + if (length === 0) { + return { buffer, bytesRead: 0 }; + } try { this[kRef](); @@ -375,7 +402,7 @@ export default exports; async readv(buffers, position) { const fd = this[kFd]; - throwEBADFIfNecessary(readv, fd); + throwEBADFIfNecessary("readv", fd); try { this[kRef](); @@ -387,7 +414,7 @@ export default exports; async readFile(options) { const fd = this[kFd]; - throwEBADFIfNecessary(readFile, fd); + throwEBADFIfNecessary("readFile", fd); try { this[kRef](); @@ -403,7 +430,7 @@ export default exports; async stat(options) { const fd = this[kFd]; - throwEBADFIfNecessary(fstat, fd); + throwEBADFIfNecessary("fstat", fd); try { this[kRef](); @@ -415,7 +442,7 @@ export default exports; async truncate(len = 0) { const fd = this[kFd]; - throwEBADFIfNecessary(ftruncate, fd); + throwEBADFIfNecessary("ftruncate", fd); try { this[kRef](); @@ -427,7 +454,7 @@ export default exports; async utimes(atime, mtime) { const fd = this[kFd]; - throwEBADFIfNecessary(futimes, fd); + throwEBADFIfNecessary("futimes", fd); try { this[kRef](); @@ -439,8 +466,22 @@ export default exports; async write(buffer, offset, length, position) { const fd = this[kFd]; - throwEBADFIfNecessary(write, fd); + throwEBADFIfNecessary("write", fd); + if (buffer?.byteLength === 0) return { __proto__: null, bytesWritten: 0, buffer }; + + isArrayBufferView ??= require("node:util/types").isArrayBufferView; + if (isArrayBufferView(buffer)) { + if (typeof offset === "object") { + ({ offset = 0, length = buffer.byteLength - offset, position = null } = offset ?? kEmptyObject); + } + + if (offset == null) { + offset = 0; + } + if (typeof length !== "number") length = buffer.byteLength - offset; + if (typeof position !== "number") position = null; + } try { this[kRef](); return { buffer, bytesWritten: await write(fd, buffer, offset, length, position) }; @@ -451,7 +492,7 @@ export default exports; async writev(buffers, position) { const fd = this[kFd]; - throwEBADFIfNecessary(writev, fd); + throwEBADFIfNecessary("writev", fd); try { this[kRef](); @@ -461,9 +502,9 @@ export default exports; } } - async writeFile(data: string, options: object | string | undefined = "utf8") { + async writeFile(data: string, options: any = "utf8") { const fd = this[kFd]; - throwEBADFIfNecessary(writeFile, fd); + throwEBADFIfNecessary("writeFile", fd); let encoding: string = "utf8"; if (options == null || typeof options === "function") { @@ -520,14 +561,14 @@ export default exports; readableWebStream(options = kEmptyObject) { const fd = this[kFd]; - throwEBADFIfNecessary(fs.createReadStream, fd); + throwEBADFIfNecessary("fs".createReadStream, fd); return Bun.file(fd).stream(); } createReadStream(options = kEmptyObject) { const fd = this[kFd]; - throwEBADFIfNecessary(fs.createReadStream, fd); + throwEBADFIfNecessary("fs".createReadStream, fd); return require("node:fs").createReadStream("", { fd: this, highWaterMark: 64 * 1024, @@ -537,7 +578,7 @@ export default exports; createWriteStream(options = kEmptyObject) { const fd = this[kFd]; - throwEBADFIfNecessary(fs.createWriteStream, fd); + throwEBADFIfNecessary("fs".createWriteStream, fd); return require("node:fs").createWriteStream("", { fd: this, ...options, @@ -569,13 +610,12 @@ export default exports; }); } -function throwEBADFIfNecessary(fn, fd) { +function throwEBADFIfNecessary(fn: string, fd) { if (fd === -1) { - // eslint-disable-next-line no-restricted-syntax - const err = new Error("Bad file descriptor"); + const err: any = new Error("Bad file descriptor"); err.code = "EBADF"; err.name = "SystemError"; - err.syscall = fn.name; + err.syscall = fn; throw err; } } diff --git a/src/js/node/fs.ts b/src/js/node/fs.ts index 88b1b7aab3..4116d1efdc 100644 --- a/src/js/node/fs.ts +++ b/src/js/node/fs.ts @@ -5,20 +5,18 @@ const promises = require("node:fs/promises"); const Stream = require("node:stream"); const types = require("node:util/types"); -const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } = require("internal/errors"); const { validateInteger } = require("internal/validators"); +const { kGetNativeReadableProto } = require("internal/shared"); const NumberIsFinite = Number.isFinite; -const DateNow = Date.now; const DatePrototypeGetTime = Date.prototype.getTime; const isDate = types.isDate; const ObjectSetPrototypeOf = Object.setPrototypeOf; // Private exports +// `fs` points to the return value of `node_fs_binding.zig`'s `createBinding` function. const { FileHandle, kRef, kUnref, kFd, fs } = promises.$data; -// reusing a different private symbol -// this points to `node_fs_binding.zig`'s `createBinding` function. const constants = $processBindingConstants.fs; var _writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"); @@ -26,9 +24,7 @@ var _fs = Symbol.for("#fs"); function ensureCallback(callback) { if (!$isCallable(callback)) { - const err = new TypeError('The "cb" argument must be of type function. Received ' + typeof callback); - err.code = "ERR_INVALID_ARG_TYPE"; - throw err; + throw $ERR_INVALID_ARG_TYPE("cb", "function", callback); } return callback; @@ -112,18 +108,18 @@ class FSWatcher extends EventEmitter { } /** Implemented in `node_fs_stat_watcher.zig` */ -// interface StatWatcherHandle { -// ref(); -// unref(); -// close(); -// } +interface StatWatcherHandle { + ref(); + unref(); + close(); +} function openAsBlob(path, options) { return Promise.$resolve(Bun.file(path, options)); } class StatWatcher extends EventEmitter { - // _handle: StatWatcherHandle; + _handle: StatWatcherHandle | null; constructor(path, options) { super(); @@ -158,8 +154,7 @@ var access = function access(path, mode, callback) { } ensureCallback(callback); - - fs.access(path, mode).then(nullcallback(callback), callback); + fs.access(path, mode).then(callback, callback); }, appendFile = function appendFile(path, data, options, callback) { if (!$isCallable(callback)) { @@ -173,8 +168,8 @@ var access = function access(path, mode, callback) { }, close = function close(fd, callback) { if ($isCallable(callback)) { - fs.close(fd).then(() => callback(), callback); - } else if (callback == undefined) { + fs.close(fd).then(() => callback(null), callback); + } else if (callback === undefined) { fs.close(fd).then(() => {}); } else { callback = ensureCallback(callback); @@ -269,11 +264,11 @@ var access = function access(path, mode, callback) { fs.futimes(fd, atime, mtime).then(nullcallback(callback), callback); }, - lchmod = function lchmod(path, mode, callback) { + lchmod = constants.O_SYMLINK !== undefined ? function lchmod(path, mode, callback) { ensureCallback(callback); fs.lchmod(path, mode).then(nullcallback(callback), callback); - }, + } : undefined, // lchmod is only available on macOS lchown = function lchown(path, uid, gid, callback) { ensureCallback(callback); @@ -430,7 +425,7 @@ var access = function access(path, mode, callback) { ensureCallback(callback); - fs.realpath(p, options).then(function (resolvedPath) { + fs.realpath(p, options, false).then(function (resolvedPath) { callback(null, resolvedPath); }, callback); }, @@ -523,7 +518,7 @@ var access = function access(path, mode, callback) { fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), - lchmodSync = fs.lchmodSync.bind(fs), + lchmodSync = constants.O_SYMLINK !== undefined ? fs.lchmodSync.bind(fs) : undefined, // lchmod is only available on macOS lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), @@ -587,11 +582,8 @@ var access = function access(path, mode, callback) { }, callback); }; -// TODO: make symbols a separate export somewhere var kCustomPromisifiedSymbol = Symbol.for("nodejs.util.promisify.custom"); - exists[kCustomPromisifiedSymbol] = path => new Promise(resolve => exists(path, resolve)); - read[kCustomPromisifiedSymbol] = async function (fd, bufferOrOptions, ...rest) { const { isArrayBufferView } = require("node:util/types"); let buffer; @@ -610,11 +602,12 @@ read[kCustomPromisifiedSymbol] = async function (fd, bufferOrOptions, ...rest) { return { bytesRead, buffer }; }; - write[kCustomPromisifiedSymbol] = async function (fd, stringOrBuffer, ...rest) { const bytesWritten = await fs.write(fd, stringOrBuffer, ...rest); return { bytesWritten, buffer: stringOrBuffer }; }; +writev[kCustomPromisifiedSymbol] = promises.writev; +readv[kCustomPromisifiedSymbol] = promises.readv; // TODO: move this entire thing into native code. // the reason it's not done right now is because there isnt a great way to have multiple @@ -651,7 +644,7 @@ function unwatchFile(filename, listener) { filename = getValidatedPath(filename); var stat = statWatchers.get(filename); - if (!stat) return; + if (!stat) return throwIfNullBytesInFileName(filename); if (listener) { stat.removeListener("change", listener); if (stat.listenerCount("change") !== 0) { @@ -664,20 +657,9 @@ function unwatchFile(filename, listener) { statWatchers.delete(filename); } -function callbackify(fsFunction, args) { - const callback = args[args.length - 1]; - try { - var result = fsFunction.$apply(fs, args.slice(0, args.length - 1)); - result.then( - (...args) => callback(null, ...args), - err => callback(err), - ); - } catch (e) { - if (typeof callback === "function") { - callback(e); - } else { - throw e; - } +function throwIfNullBytesInFileName(filename: string) { + if (filename.indexOf("\u0000") !== -1) { + throw $ERR_INVALID_ARG_VALUE("path", "string without null bytes", filename); } } @@ -738,7 +720,7 @@ function createReadStream(path, options) { return new ReadStream(path, options); } -const NativeReadable = Stream._getNativeReadableStreamPrototype(2, Stream.Readable); +const NativeReadable = Stream[kGetNativeReadableProto](2); const NativeReadablePrototype = NativeReadable.prototype; const kFs = Symbol("kFs"); const kHandle = Symbol("kHandle"); @@ -781,6 +763,11 @@ function ReadStream(this: typeof ReadStream, pathOrFd, options) { fd = defaultReadStreamOptions.fd, }: Partial = options; + if (encoding && !Buffer.isEncoding(encoding)) { + const reason = "is invalid encoding"; + throw $ERR_INVALID_ARG_VALUE("encoding", encoding, reason); + } + if (pathOrFd?.constructor?.name === "URL") { pathOrFd = Bun.fileURLToPath(pathOrFd); } @@ -841,7 +828,7 @@ function ReadStream(this: typeof ReadStream, pathOrFd, options) { } else if (end !== Infinity) { validateInteger(end, "end", 0); if (start !== undefined && start > end) { - throw new ERR_OUT_OF_RANGE("start", `<= "end" (here: ${end})`, start); + throw $ERR_OUT_OF_RANGE("start", `<= "end" (here: ${end})`, start); } } @@ -1061,9 +1048,13 @@ var defaultWriteStreamOptions = { }, }; -var WriteStreamClass = (WriteStream = function WriteStream(path, options = defaultWriteStreamOptions) { +var WriteStreamClass = (WriteStream = function WriteStream(path, options: any = defaultWriteStreamOptions) { if (!(this instanceof WriteStream)) { - return new WriteStream(path, options); + return new (WriteStream as any)(path, options); + } + + if (typeof options === "string") { + options = { encoding: options }; } if (!options) { @@ -1088,6 +1079,11 @@ var WriteStreamClass = (WriteStream = function WriteStream(path, options = defau options.pos = start; } + if (encoding && !Buffer.isEncoding(encoding)) { + const reason = "is invalid encoding"; + throw $ERR_INVALID_ARG_VALUE("encoding", encoding, reason); + } + var tempThis = {}; var handle = null; if (fd != null) { @@ -1386,9 +1382,20 @@ Object.defineProperties(fs, { }, }); -// lol -realpath.native = realpath; -realpathSync.native = realpathSync; +// @ts-ignore +realpath.native = function realpath(p, options, callback) { + if ($isCallable(options)) { + callback = options; + options = undefined; + } + + ensureCallback(callback); + + fs.realpathNative(p, options).then(function (resolvedPath) { + callback(null, resolvedPath); + }, callback); +}; +realpathSync.native = fs.realpathNativeSync.bind(fs); // attempt to use the native code version if possible // and on MacOS, simple cases of recursive directory trees can be done in a single `clonefile()` @@ -1415,19 +1422,21 @@ function cp(src, dest, options, callback) { promises.cp(src, dest, options).then(() => callback(), callback); } -function _toUnixTimestamp(time, name = "time") { +function _toUnixTimestamp(time: any, name = "time") { + // @ts-ignore if (typeof time === "string" && +time == time) { return +time; } - if (NumberIsFinite(time)) { + // @ts-ignore + if ($isFinite(time)) { if (time < 0) { - return DateNow() / 1000; + return Date.now() / 1000; } return time; } if (isDate(time)) { // Convert to 123.456 UNIX timestamp - return DatePrototypeGetTime(time) / 1000; + return time.getTime() / 1000; } throw new TypeError(`Expected ${name} to be a number or Date`); } @@ -1583,8 +1592,8 @@ setName(ftruncate, "ftruncate"); setName(ftruncateSync, "ftruncateSync"); setName(futimes, "futimes"); setName(futimesSync, "futimesSync"); -setName(lchmod, "lchmod"); -setName(lchmodSync, "lchmodSync"); +if (lchmod) setName(lchmod, "lchmod"); +if (lchmodSync) setName(lchmodSync, "lchmodSync"); setName(lchown, "lchown"); setName(lchownSync, "lchownSync"); setName(link, "link"); diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 6c15bf58c4..2068f73ecb 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1,8 +1,7 @@ // Hardcoded module "node:http" const EventEmitter = require("node:events"); -const { isTypedArray } = require("node:util/types"); +const { isTypedArray, isArrayBuffer } = require("node:util/types"); const { Duplex, Readable, Writable } = require("node:stream"); -const { ERR_INVALID_ARG_TYPE, ERR_INVALID_PROTOCOL } = require("internal/errors"); const { isPrimary } = require("internal/cluster/isPrimary"); const { kAutoDestroyed } = require("internal/shared"); const { urlToHttpOptions } = require("internal/url"); @@ -112,12 +111,11 @@ const kfakeSocket = Symbol("kfakeSocket"); const kEmptyBuffer = Buffer.alloc(0); function isValidTLSArray(obj) { - if (typeof obj === "string" || isTypedArray(obj) || obj instanceof ArrayBuffer || obj instanceof Blob) return true; + if (typeof obj === "string" || isTypedArray(obj) || isArrayBuffer(obj) || $inheritsBlob(obj)) return true; if (Array.isArray(obj)) { for (var i = 0; i < obj.length; i++) { const item = obj[i]; - if (typeof item !== "string" && !isTypedArray(item) && !(item instanceof ArrayBuffer) && !(item instanceof Blob)) - return false; + if (typeof item !== "string" && !isTypedArray(item) && !isArrayBuffer(item) && !$inheritsBlob(item)) return false; } return true; } @@ -126,7 +124,7 @@ function isValidTLSArray(obj) { function validateMsecs(numberlike: any, field: string) { if (typeof numberlike !== "number" || numberlike < 0) { - throw ERR_INVALID_ARG_TYPE(field, "number", numberlike); + throw $ERR_INVALID_ARG_TYPE(field, "number", numberlike); } return numberlike; @@ -777,6 +775,9 @@ function requestHasNoBody(method, req) { if ("GET" === method || "HEAD" === method || "TRACE" === method || "CONNECT" === method || "OPTIONS" === method) return true; const headers = req?.headers; + const encoding = headers?.["transfer-encoding"]; + if (encoding?.indexOf?.("chunked") !== -1) return false; + const contentLength = headers?.["content-length"]; if (!parseInt(contentLength, 10)) return true; @@ -838,12 +839,15 @@ IncomingMessage.prototype = { return; } - const contentLength = this.headers["content-length"]; - const length = contentLength ? parseInt(contentLength, 10) : 0; - if (length === 0) { - this[noBodySymbol] = true; - callback(); - return; + const encoding = this.headers["transfer-encoding"]; + if (encoding?.indexOf?.("chunked") === -1) { + const contentLength = this.headers["content-length"]; + const length = contentLength ? parseInt(contentLength, 10) : 0; + if (length === 0) { + this[noBodySymbol] = true; + callback(); + return; + } } callback(); @@ -975,7 +979,6 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) { } else { ({ done, value } = result); } - if (self.destroyed || (aborted = self[abortedSymbol])) { break; } @@ -1479,6 +1482,9 @@ class ClientRequest extends OutgoingMessage { #socketPath; #bodyChunks: Buffer[] | null = null; + #stream: ReadableStream | null = null; + #controller: ReadableStream | null = null; + #fetchRequest; #signal: AbortSignal | null = null; [kAbortController]: AbortController | null = null; @@ -1512,24 +1518,97 @@ class ClientRequest extends OutgoingMessage { return this.#agent; } + set agent(value) { + this.#agent = value; + } + + #createStream() { + if (!this.#stream) { + var self = this; + + this.#stream = new ReadableStream({ + type: "direct", + pull(controller) { + self.#controller = controller; + for (let chunk of self.#bodyChunks) { + if (chunk === null) { + controller.close(); + } else { + controller.write(chunk); + } + } + self.#bodyChunks = null; + }, + }); + this.#startStream(); + } + } + _write(chunk, encoding, callback) { - if (!this.#bodyChunks) { - this.#bodyChunks = [chunk]; - callback(); + if (this.#controller) { + if (typeof chunk === "string") { + this.#controller.write(Buffer.from(chunk, encoding)); + } else { + this.#controller.write(chunk); + } + process.nextTick(callback); return; } + if (!this.#bodyChunks) { + this.#bodyChunks = [chunk]; + process.nextTick(callback); + return; + } + this.#bodyChunks.push(chunk); - callback(); + this.#createStream(); + process.nextTick(callback); } _writev(chunks, callback) { - if (!this.#bodyChunks) { - this.#bodyChunks = chunks; - callback(); + if (this.#controller) { + const allBuffers = chunks.allBuffers; + + if (allBuffers) { + for (let i = 0; i < chunks.length; i++) { + this.#controller.write(chunks[i].chunk); + } + } else { + for (let i = 0; i < chunks.length; i++) { + this.#controller.write(Buffer.from(chunks[i].chunk, chunks[i].encoding)); + } + } + process.nextTick(callback); return; } - this.#bodyChunks.push(...chunks); - callback(); + const allBuffers = chunks.allBuffers; + if (this.#bodyChunks) { + if (allBuffers) { + for (let i = 0; i < chunks.length; i++) { + this.#bodyChunks.push(chunks[i].chunk); + } + } else { + for (let i = 0; i < chunks.length; i++) { + this.#bodyChunks.push(Buffer.from(chunks[i].chunk, chunks[i].encoding)); + } + } + } else { + this.#bodyChunks = new Array(chunks.length); + + if (allBuffers) { + for (let i = 0; i < chunks.length; i++) { + this.#bodyChunks[i] = chunks[i].chunk; + } + } else { + for (let i = 0; i < chunks.length; i++) { + this.#bodyChunks[i] = Buffer.from(chunks[i].chunk, chunks[i].encoding); + } + } + } + if (this.#bodyChunks.length > 1) { + this.#createStream(); + } + process.nextTick(callback); } _destroy(err, callback) { @@ -1545,26 +1624,12 @@ class ClientRequest extends OutgoingMessage { return this.#tls; } - _final(callback) { - this.#finished = true; - this[kAbortController] = new AbortController(); - this[kAbortController].signal.addEventListener( - "abort", - () => { - this[kClearTimeout]?.(); - if (this.destroyed) return; - this.emit("abort"); - this.destroy(); - }, - { once: true }, - ); - if (this.#signal?.aborted) { - this[kAbortController].abort(); - } + #startStream() { + if (this.#fetchRequest) return; var method = this.#method, - body = this.#bodyChunks?.length === 1 ? this.#bodyChunks[0] : Buffer.concat(this.#bodyChunks || []); - + body = + this.#stream || (this.#bodyChunks?.length === 1 ? this.#bodyChunks[0] : Buffer.concat(this.#bodyChunks || [])); let url: string; let proxy: string | undefined; const protocol = this.#protocol; @@ -1594,7 +1659,7 @@ class ClientRequest extends OutgoingMessage { method, headers: this.getHeaders(), redirect: "manual", - signal: this[kAbortController].signal, + signal: this[kAbortController]?.signal, // Timeouts are handled via this.setTimeout. timeout: false, // Disable auto gzip/deflate @@ -1660,11 +1725,40 @@ class ClientRequest extends OutgoingMessage { } catch (err) { if (!!$debug) globalReportError(err); this.emit("error", err); - } finally { - callback(); } } + _final(callback) { + this.#finished = true; + this[kAbortController] = new AbortController(); + this[kAbortController].signal.addEventListener( + "abort", + () => { + this[kClearTimeout]?.(); + if (this.destroyed) return; + this.emit("abort"); + this.destroy(); + }, + { once: true }, + ); + if (this.#signal?.aborted) { + this[kAbortController].abort(); + } + + if (this.#controller) { + this.#controller.close(); + callback(); + return; + } + if (this.#bodyChunks?.length > 1) { + this.#bodyChunks?.push(null); + } + + this.#startStream(); + + callback(); + } + get aborted() { return this[abortedSymbol] || this.#signal?.aborted || !!this[kAbortController]?.signal.aborted; } @@ -1714,7 +1808,7 @@ class ClientRequest extends OutgoingMessage { } else if (agent == null) { agent = defaultAgent; } else if (typeof agent.addRequest !== "function") { - throw ERR_INVALID_ARG_TYPE("options.agent", "Agent-like Object, undefined, or false", agent); + throw $ERR_INVALID_ARG_TYPE("options.agent", "Agent-like Object, undefined, or false", agent); } this.#agent = agent; @@ -1724,7 +1818,7 @@ class ClientRequest extends OutgoingMessage { expectedProtocol = this.agent.protocol; } if (protocol !== expectedProtocol) { - throw ERR_INVALID_PROTOCOL(protocol, expectedProtocol); + throw $ERR_INVALID_PROTOCOL(protocol, expectedProtocol); } this.#protocol = protocol; @@ -1760,8 +1854,7 @@ class ClientRequest extends OutgoingMessage { let method = options.method; const methodIsString = typeof method === "string"; if (method !== null && method !== undefined && !methodIsString) { - // throw ERR_INVALID_ARG_TYPE("options.method", "string", method); - throw new Error("ERR_INVALID_ARG_TYPE: options.method"); + throw $ERR_INVALID_ARG_TYPE("options.method", "string", method); } if (methodIsString && method) { @@ -1996,12 +2089,7 @@ class ClientRequest extends OutgoingMessage { function validateHost(host, name) { if (host !== null && host !== undefined && typeof host !== "string") { - // throw ERR_INVALID_ARG_TYPE( - // `options.${name}`, - // ["string", "undefined", "null"], - // host, - // ); - throw new Error("Invalid arg type in options"); + throw $ERR_INVALID_ARG_TYPE(`options.${name}`, ["string", "undefined", "null"], host); } return host; } diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index bb7544bdbc..5ce20ef257 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -1,8 +1,6 @@ // Hardcoded module "node:http2" const { isTypedArray } = require("node:util/types"); - -// This is a stub! None of this is actually implemented yet. const { hideFromStack, throwNotImplemented } = require("internal/shared"); const tls = require("node:tls"); @@ -24,22 +22,20 @@ const Socket = net.Socket; const EventEmitter = require("node:events"); const { Duplex } = require("node:stream"); -const { - FunctionPrototypeBind, - StringPrototypeTrim, - ArrayPrototypePush, - ObjectAssign, - ArrayIsArray, - SafeArrayIterator, - StringPrototypeToLowerCase, - StringPrototypeIncludes, - ObjectKeys, - ObjectPrototypeHasOwnProperty, - SafeSet, - DatePrototypeToUTCString, - DatePrototypeGetMilliseconds, -} = require("internal/primordials"); +const { SafeArrayIterator, SafeSet } = require("internal/primordials"); + const RegExpPrototypeExec = RegExp.prototype.exec; +const ObjectAssign = Object.assign; +const ArrayIsArray = Array.isArray; +const ObjectKeys = Object.keys; +const FunctionPrototypeBind = Function.prototype.bind; +const StringPrototypeTrim = String.prototype.trim; +const ArrayPrototypePush = Array.prototype.push; +const StringPrototypeToLowerCase = String.prototype.toLocaleLowerCase; +const StringPrototypeIncludes = String.prototype.includes; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +const DatePrototypeToUTCString = Date.prototype.toUTCString; +const DatePrototypeGetMilliseconds = Date.prototype.getMilliseconds; const [H2FrameParser, assertSettings, getPackedSettings, getUnpackedSettings] = $zig( "h2_frame_parser.zig", @@ -91,8 +87,8 @@ function utcDate() { function cache() { const d = new Date(); - utcCache = DatePrototypeToUTCString(d); - setTimeout(resetCache, 1000 - DatePrototypeGetMilliseconds(d)).unref(); + utcCache = d.toUTCString(); + setTimeout(resetCache, 1000 - d.getMilliseconds()).unref(); } function resetCache() { @@ -116,7 +112,7 @@ function onStreamTrailers(trailers, flags, rawTrailers) { const request = this[kRequest]; if (request !== undefined) { ObjectAssign(request[kTrailers], trailers); - ArrayPrototypePush(request[kRawTrailers], ...new SafeArrayIterator(rawTrailers)); + ArrayPrototypePush.$call(request[kRawTrailers], ...new SafeArrayIterator(rawTrailers)); } } @@ -147,6 +143,10 @@ function onStreamDrain() { if (response !== undefined) response.emit("drain"); } +function onStreamAbortedResponse() { + // no-op for now +} + function onStreamAbortedRequest() { const request = this[kRequest]; if (request !== undefined && request[kState].closed === false) { @@ -240,7 +240,7 @@ function connectionHeaderMessageWarn() { } function assertValidHeader(name, value) { - if (name === "" || typeof name !== "string" || StringPrototypeIncludes(name, " ")) { + if (name === "" || typeof name !== "string" || StringPrototypeIncludes.$call(name, " ")) { throw $ERR_INVALID_HTTP_TOKEN(`The arguments Header name is invalid. Received ${name}`); } if (isPseudoHeader(name)) { @@ -349,9 +349,7 @@ class Http2ServerRequest extends Readable { set method(method) { validateString(method, "method"); - if (StringPrototypeTrim(method) === "") - throw $ERR_INVALID_ARG_VALUE(`The arguments method is invalid. Received ${method}`); - + if (StringPrototypeTrim.$call(method) === "") throw $ERR_INVALID_ARG_VALUE("method", method); this[kHeaders][HTTP2_HEADER_METHOD] = method; } @@ -394,6 +392,7 @@ class Http2ServerResponse extends Stream { this.writable = true; this.req = stream[kRequest]; stream.on("drain", onStreamDrain); + stream.on("aborted", onStreamAbortedResponse); stream.on("close", onStreamCloseResponse); stream.on("wantTrailers", onStreamTrailersReady); stream.on("timeout", onStreamTimeout); @@ -474,7 +473,7 @@ class Http2ServerResponse extends Stream { setTrailer(name, value) { validateString(name, "name"); - name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + name = StringPrototypeToLowerCase.$call(StringPrototypeTrim.$call(name)); assertValidHeader(name, value); this[kTrailers][name] = value; } @@ -490,7 +489,7 @@ class Http2ServerResponse extends Stream { getHeader(name) { validateString(name, "name"); - name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + name = StringPrototypeToLowerCase.$call(StringPrototypeTrim.$call(name)); return this[kHeaders][name]; } @@ -505,15 +504,15 @@ class Http2ServerResponse extends Stream { hasHeader(name) { validateString(name, "name"); - name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); - return ObjectPrototypeHasOwnProperty(this[kHeaders], name); + name = StringPrototypeToLowerCase.$call(StringPrototypeTrim.$call(name)); + return ObjectPrototypeHasOwnProperty.$call(this[kHeaders], name); } removeHeader(name) { validateString(name, "name"); if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated"); - name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + name = StringPrototypeToLowerCase.$call(StringPrototypeTrim.$call(name)); if (name === "date") { this[kState].sendDate = false; @@ -532,7 +531,7 @@ class Http2ServerResponse extends Stream { } [kSetHeader](name, value) { - name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + name = StringPrototypeToLowerCase.$call(StringPrototypeTrim.$call(name)); assertValidHeader(name, value); if (!isConnectionHeaderAllowed(name, value)) { @@ -554,7 +553,7 @@ class Http2ServerResponse extends Stream { } [kAppendHeader](name, value) { - name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + name = StringPrototypeToLowerCase.$call(StringPrototypeTrim.$call(name)); assertValidHeader(name, value); if (!isConnectionHeaderAllowed(name, value)) { @@ -642,7 +641,7 @@ class Http2ServerResponse extends Stream { } } else { if (headers.length % 2 !== 0) { - throw $ERR_INVALID_ARG_VALUE(`The arguments headers is invalid.`); + throw $ERR_INVALID_ARG_VALUE("headers", headers); } for (i = 0; i < headers.length; i += 2) { @@ -852,7 +851,7 @@ const proxySocketHandler = { case "setTimeout": case "ref": case "unref": - return FunctionPrototypeBind(session[prop], session); + return FunctionPrototypeBind.$call(session[prop], session); case "destroy": case "emit": case "end": @@ -872,7 +871,7 @@ const proxySocketHandler = { throw $ERR_HTTP2_SOCKET_UNBOUND("The socket has been disconnected from the Http2Session"); } const value = socket[prop]; - return typeof value === "function" ? FunctionPrototypeBind(value, socket) : value; + return typeof value === "function" ? FunctionPrototypeBind.$call(value, socket) : value; } } }, @@ -1635,9 +1634,10 @@ class Http2Stream extends Duplex { } const sensitives = headers[sensitiveHeaders]; const sensitiveNames = {}; + delete headers[sensitiveHeaders]; if (sensitives) { if (!$isJSArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid"); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; @@ -2045,10 +2045,11 @@ class ServerHttp2Stream extends Http2Stream { } const sensitives = headers[sensitiveHeaders]; + delete headers[sensitiveHeaders]; const sensitiveNames = {}; if (sensitives) { if (!$isArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid."); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; @@ -2073,7 +2074,7 @@ class ServerHttp2Stream extends Http2Stream { if (!this[kInfoHeaders]) { this[kInfoHeaders] = [headers]; } else { - ArrayPrototypePush(this[kInfoHeaders], headers); + ArrayPrototypePush.$call(this[kInfoHeaders], headers); } session[bunHTTP2Native]?.request(this.id, undefined, headers, sensitiveNames); @@ -2096,10 +2097,11 @@ class ServerHttp2Stream extends Http2Stream { } const sensitives = headers[sensitiveHeaders]; + delete headers[sensitiveHeaders]; const sensitiveNames = {}; if (sensitives) { if (!$isArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid."); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; @@ -2191,7 +2193,7 @@ function toHeaderObject(headers, sensitiveHeadersValue) { // fields with the same name. Since it cannot be combined into a // single field-value, recipients ought to handle "Set-Cookie" as a // special case while processing header fields." - ArrayPrototypePush(existing, value); + ArrayPrototypePush.$call(existing, value); break; default: // https://tools.ietf.org/html/rfc7230#section-3.2.2 @@ -2420,7 +2422,7 @@ class ServerHttp2Session extends Http2Session { // throwNotImplemented("ServerHttp2Stream.prototype.origin()"); } - constructor(socket: TLSSocket | Socket, options?: Http2ConnectOptions, server: Http2Server) { + constructor(socket: TLSSocket | Socket, options?: Http2ConnectOptions, server?: Http2Server) { super(); this[kServer] = server; this.#connected = true; @@ -3088,10 +3090,11 @@ class ClientHttp2Session extends Http2Session { } const sensitives = headers[sensitiveHeaders]; + delete headers[sensitiveHeaders]; const sensitiveNames = {}; if (sensitives) { if (!$isArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid."); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; @@ -3171,7 +3174,7 @@ function setupCompat(ev) { const options = this[bunSocketServerOptions]; const ServerRequest = options?.Http2ServerRequest || Http2ServerRequest; const ServerResponse = options?.Http2ServerResponse || Http2ServerResponse; - this.on("stream", FunctionPrototypeBind(onServerStream, this, ServerRequest, ServerResponse)); + this.on("stream", FunctionPrototypeBind.$call(onServerStream, this, ServerRequest, ServerResponse)); } } diff --git a/src/js/node/net.ts b/src/js/node/net.ts index c06c476816..927a2dab05 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -24,7 +24,6 @@ const { Duplex } = require("node:stream"); const EventEmitter = require("node:events"); const { addServerName, upgradeDuplexToTLS, isNamedPipeSocket } = require("../internal/net"); const { ExceptionWithHostPort } = require("internal/shared"); -const { ERR_SERVER_NOT_RUNNING } = require("internal/errors"); // IPv4 Segment const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"; @@ -99,6 +98,15 @@ function finishSocket(hasError) { detachSocket(this); this.emit("close", hasError); } + +function destroyNT(self, err) { + self.destroy(err); +} +function destroyWhenAborted(err) { + if (!this.destroyed) { + this.destroy(err.target.reason); + } +} // Provide a better error message when we call end() as a result // of the other side sending a FIN. The standard 'write after end' // is overly vague, and makes it seem like the user's code is to blame. @@ -479,9 +487,12 @@ const Socket = (function (InternalSocket) { }, }; } - if (signal) { - signal.addEventListener("abort", () => this.destroy()); + if (signal.aborted) { + process.nextTick(destroyNT, this, signal.reason); + } else { + signal.addEventListener("abort", destroyWhenAborted.bind(this)); + } } } @@ -1068,305 +1079,320 @@ function createConnection(port, host, connectListener) { const connect = createConnection; -class Server extends EventEmitter { - [bunSocketServerConnections] = 0; - [bunSocketServerOptions]; - maxConnections = 0; - _handle = null; - - constructor(options, connectionListener) { - super(); - - if (typeof options === "function") { - connectionListener = options; - options = {}; - } else if (options == null || typeof options === "object") { - options = { ...options }; - } else { - throw new Error("bun-net-polyfill: invalid arguments"); - } - const { maxConnections } = options; - this.maxConnections = Number.isSafeInteger(maxConnections) && maxConnections > 0 ? maxConnections : 0; - - options.connectionListener = connectionListener; - this[bunSocketServerOptions] = options; +function Server(options, connectionListener): void { + if (!(this instanceof Server)) { + return new Server(options, connectionListener); } - get listening() { + EventEmitter.$apply(this, []); + + this[bunSocketServerConnections] = 0; + this[bunSocketServerOptions] = undefined; + this.maxConnections = 0; + this._handle = null; + + if (typeof options === "function") { + connectionListener = options; + options = {}; + } else if (options == null || typeof options === "object") { + options = { ...options }; + } else { + throw new Error("bun-net-polyfill: invalid arguments"); + } + const { maxConnections } = options; + this.maxConnections = Number.isSafeInteger(maxConnections) && maxConnections > 0 ? maxConnections : 0; + + options.connectionListener = connectionListener; + this[bunSocketServerOptions] = options; +} +$toClass(Server, "Server", EventEmitter); + +Object.defineProperty(Server.prototype, "listening", { + get() { return !!this._handle; - } + }, +}); - ref() { - this._handle?.ref(); - return this; - } +Server.prototype.ref = function () { + this._handle?.ref(); + return this; +}; - unref() { - this._handle?.unref(); - return this; - } +Server.prototype.unref = function () { + this._handle?.unref(); + return this; +}; - close(callback) { - if (typeof callback === "function") { - if (!this._handle) { - this.once("close", function close() { - callback(ERR_SERVER_NOT_RUNNING()); - }); - } else { - this.once("close", callback); - } - } - - if (this._handle) { - this._handle.stop(false); - this._handle = null; - } - - this._emitCloseIfDrained(); - - return this; - } - - [Symbol.asyncDispose]() { - const { resolve, reject, promise } = Promise.withResolvers(); - this.close(function (err, ...args) { - if (err) reject(err); - else resolve(...args); - }); - return promise; - } - - _emitCloseIfDrained() { - if (this._handle || this[bunSocketServerConnections] > 0) { - return; - } - process.nextTick(() => { - this.emit("close"); - }); - } - - address() { - const server = this._handle; - if (server) { - const unix = server.unix; - if (unix) { - return unix; - } - - //TODO: fix adress when host is passed - let address = server.hostname; - const type = isIP(address); - const port = server.port; - if (typeof port === "number") { - return { - port, - address, - family: type ? `IPv${type}` : undefined, - }; - } - if (type) { - return { - address, - family: type ? `IPv${type}` : undefined, - }; - } - - return address; - } - return null; - } - - getConnections(callback) { - if (typeof callback === "function") { - //in Bun case we will never error on getConnections - //node only errors if in the middle of the couting the server got disconnected, what never happens in Bun - //if disconnected will only pass null as well and 0 connected - callback(null, this._handle ? this[bunSocketServerConnections] : 0); - } - return this; - } - - listen(port, hostname, onListen) { - let backlog; - let path; - let exclusive = false; - let allowHalfOpen = false; - let reusePort = false; - let ipv6Only = false; - //port is actually path - if (typeof port === "string") { - if (Number.isSafeInteger(hostname)) { - if (hostname > 0) { - //hostname is backlog - backlog = hostname; - } - } else if (typeof hostname === "function") { - //hostname is callback - onListen = hostname; - } - - path = port; - hostname = undefined; - port = undefined; +Server.prototype.close = function (callback) { + if (typeof callback === "function") { + if (!this._handle) { + this.once("close", function close() { + callback($ERR_SERVER_NOT_RUNNING()); + }); } else { - if (typeof hostname === "function") { - onListen = hostname; - hostname = undefined; + this.once("close", callback); + } + } + + if (this._handle) { + this._handle.stop(false); + this._handle = null; + } + + this._emitCloseIfDrained(); + + return this; +}; + +Server.prototype[Symbol.asyncDispose] = function () { + const { resolve, reject, promise } = Promise.withResolvers(); + this.close(function (err, ...args) { + if (err) reject(err); + else resolve(...args); + }); + return promise; +}; + +Server.prototype._emitCloseIfDrained = function () { + if (this._handle || this[bunSocketServerConnections] > 0) { + return; + } + process.nextTick(() => { + this.emit("close"); + }); +}; + +Server.prototype.address = function () { + const server = this._handle; + if (server) { + const unix = server.unix; + if (unix) { + return unix; + } + + //TODO: fix adress when host is passed + let address = server.hostname; + const type = isIP(address); + const port = server.port; + if (typeof port === "number") { + return { + port, + address, + family: type ? `IPv${type}` : undefined, + }; + } + if (type) { + return { + address, + family: type ? `IPv${type}` : undefined, + }; + } + + return address; + } + return null; +}; + +Server.prototype.getConnections = function (callback) { + if (typeof callback === "function") { + //in Bun case we will never error on getConnections + //node only errors if in the middle of the couting the server got disconnected, what never happens in Bun + //if disconnected will only pass null as well and 0 connected + callback(null, this._handle ? this[bunSocketServerConnections] : 0); + } + return this; +}; + +Server.prototype.listen = function (port, hostname, onListen) { + let backlog; + let path; + let exclusive = false; + let allowHalfOpen = false; + let reusePort = false; + let ipv6Only = false; + //port is actually path + if (typeof port === "string") { + if (Number.isSafeInteger(hostname)) { + if (hostname > 0) { + //hostname is backlog + backlog = hostname; } + } else if (typeof hostname === "function") { + //hostname is callback + onListen = hostname; + } - if (typeof port === "function") { - onListen = port; - port = 0; - } else if (typeof port === "object") { - const options = port; - options.signal?.addEventListener("abort", () => this.close()); + path = port; + hostname = undefined; + port = undefined; + } else { + if (typeof hostname === "function") { + onListen = hostname; + hostname = undefined; + } - hostname = options.host; - exclusive = options.exclusive; - path = options.path; - port = options.port; - ipv6Only = options.ipv6Only; - allowHalfOpen = options.allowHalfOpen; - reusePort = options.reusePort; + if (typeof port === "function") { + onListen = port; + port = 0; + } else if (typeof port === "object") { + const options = port; + options.signal?.addEventListener("abort", () => this.close()); - const isLinux = process.platform === "linux"; + hostname = options.host; + exclusive = options.exclusive; + path = options.path; + port = options.port; + ipv6Only = options.ipv6Only; + allowHalfOpen = options.allowHalfOpen; + reusePort = options.reusePort; - if (!Number.isSafeInteger(port) || port < 0) { - if (path) { - const isAbstractPath = path.startsWith("\0"); - if (isLinux && isAbstractPath && (options.writableAll || options.readableAll)) { - const message = `The argument 'options' can not set readableAll or writableAll to true when path is abstract unix socket. Received ${JSON.stringify(options)}`; + const isLinux = process.platform === "linux"; - const error = new TypeError(message); - error.code = "ERR_INVALID_ARG_VALUE"; - throw error; - } - - hostname = path; - port = undefined; - } else { - let message = 'The argument \'options\' must have the property "port" or "path"'; - try { - message = `${message}. Received ${JSON.stringify(options)}`; - } catch {} + if (!Number.isSafeInteger(port) || port < 0) { + if (path) { + const isAbstractPath = path.startsWith("\0"); + if (isLinux && isAbstractPath && (options.writableAll || options.readableAll)) { + const message = `The argument 'options' can not set readableAll or writableAll to true when path is abstract unix socket. Received ${JSON.stringify(options)}`; const error = new TypeError(message); error.code = "ERR_INVALID_ARG_VALUE"; throw error; } - } else if (!Number.isSafeInteger(port) || port < 0) { - port = 0; + + hostname = path; + port = undefined; + } else { + let message = 'The argument \'options\' must have the property "port" or "path"'; + try { + message = `${message}. Received ${JSON.stringify(options)}`; + } catch {} + + const error = new TypeError(message); + error.code = "ERR_INVALID_ARG_VALUE"; + throw error; } - - // port - // host - // path Will be ignored if port is specified. See Identifying paths for IPC connections. - // backlog Common parameter of server.listen() functions. - // exclusive Default: false - // readableAll For IPC servers makes the pipe readable for all users. Default: false. - // writableAll For IPC servers makes the pipe writable for all users. Default: false. - // ipv6Only For TCP servers, setting ipv6Only to true will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound. Default: false. - // signal An AbortSignal that may be used to close a listening server. - - if (typeof options.callback === "function") onListen = options?.callback; } else if (!Number.isSafeInteger(port) || port < 0) { port = 0; } - hostname = hostname || "::"; - } - try { - var tls = undefined; - var TLSSocketClass = undefined; - const bunTLS = this[bunTlsSymbol]; - const options = this[bunSocketServerOptions]; - let contexts: Map | null = null; - if (typeof bunTLS === "function") { - [tls, TLSSocketClass] = bunTLS.$call(this, port, hostname, false); - options.servername = tls.serverName; - options.InternalSocketClass = TLSSocketClass; - contexts = tls.contexts; - if (!tls.requestCert) { - tls.rejectUnauthorized = false; - } - } else { - options.InternalSocketClass = SocketClass; - } + // port + // host + // path Will be ignored if port is specified. See Identifying paths for IPC connections. + // backlog Common parameter of server.listen() functions. + // exclusive Default: false + // readableAll For IPC servers makes the pipe readable for all users. Default: false. + // writableAll For IPC servers makes the pipe writable for all users. Default: false. + // ipv6Only For TCP servers, setting ipv6Only to true will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound. Default: false. + // signal An AbortSignal that may be used to close a listening server. - listenInCluster( - this, - null, - port, - 4, - backlog, - undefined, - exclusive, - ipv6Only, - allowHalfOpen, - reusePort, - undefined, - undefined, - path, - hostname, - tls, - contexts, - onListen, - ); - } catch (err) { - setTimeout(emitErrorNextTick, 1, this, err); + if (typeof options.callback === "function") onListen = options?.callback; + } else if (!Number.isSafeInteger(port) || port < 0) { + port = 0; } - return this; + hostname = hostname || "::"; } - [kRealListen](path, port, hostname, exclusive, ipv6Only, allowHalfOpen, reusePort, tls, contexts, onListen) { - if (path) { - this._handle = Bun.listen({ - unix: path, - tls, - allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, - reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, - ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, - exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, - socket: SocketClass[bunSocketServerHandlers], - }); + try { + var tls = undefined; + var TLSSocketClass = undefined; + const bunTLS = this[bunTlsSymbol]; + const options = this[bunSocketServerOptions]; + let contexts: Map | null = null; + if (typeof bunTLS === "function") { + [tls, TLSSocketClass] = bunTLS.$call(this, port, hostname, false); + options.servername = tls.serverName; + options.InternalSocketClass = TLSSocketClass; + contexts = tls.contexts; + if (!tls.requestCert) { + tls.rejectUnauthorized = false; + } } else { - this._handle = Bun.listen({ - exclusive, - port, - hostname, - tls, - allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, - reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, - ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, - exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, - socket: SocketClass[bunSocketServerHandlers], - }); + options.InternalSocketClass = SocketClass; } - //make this instance available on handlers - this._handle.data = this; + listenInCluster( + this, + null, + port, + 4, + backlog, + undefined, + exclusive, + ipv6Only, + allowHalfOpen, + reusePort, + undefined, + undefined, + path, + hostname, + tls, + contexts, + onListen, + ); + } catch (err) { + setTimeout(emitErrorNextTick, 1, this, err); + } + return this; +}; - if (contexts) { - for (const [name, context] of contexts) { - addServerName(this._handle, name, context); - } +Server.prototype[kRealListen] = function ( + path, + port, + hostname, + exclusive, + ipv6Only, + allowHalfOpen, + reusePort, + tls, + contexts, + onListen, +) { + if (path) { + this._handle = Bun.listen({ + unix: path, + tls, + allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, + reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, + ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, + exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, + socket: SocketClass[bunSocketServerHandlers], + }); + } else { + this._handle = Bun.listen({ + port, + hostname, + tls, + allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, + reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, + ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, + exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, + socket: SocketClass[bunSocketServerHandlers], + }); + } + + //make this instance available on handlers + this._handle.data = this; + + if (contexts) { + for (const [name, context] of contexts) { + addServerName(this._handle, name, context); } - - // We must schedule the emitListeningNextTick() only after the next run of - // the event loop's IO queue. Otherwise, the server may not actually be listening - // when the 'listening' event is emitted. - // - // That leads to all sorts of confusion. - // - // process.nextTick() is not sufficient because it will run before the IO queue. - setTimeout(emitListeningNextTick, 1, this, onListen?.bind(this)); } - getsockname(out) { - out.port = this.address().port; - return out; - } -} + // We must schedule the emitListeningNextTick() only after the next run of + // the event loop's IO queue. Otherwise, the server may not actually be listening + // when the 'listening' event is emitted. + // + // That leads to all sorts of confusion. + // + // process.nextTick() is not sufficient because it will run before the IO queue. + setTimeout(emitListeningNextTick, 1, this, onListen?.bind(this)); +}; + +Server.prototype.getsockname = function (out) { + out.port = this.address().port; + return out; +}; function emitErrorNextTick(self, error) { self.emit("error", error); diff --git a/src/js/node/os.ts b/src/js/node/os.ts index f962ed31e2..53d6fd5b8f 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -1,5 +1,4 @@ // Hardcoded module "node:os" - var tmpdir = function () { var env = Bun.env; @@ -19,6 +18,8 @@ var tmpdir = function () { return path; }; + tmpdir[Symbol.toPrimitive] = tmpdir; + return tmpdir(); }; @@ -85,7 +86,7 @@ function lazyCpus({ cpus }) { } // all logic based on `process.platform` and `process.arch` is inlined at bundle time -function bound(obj) { +function bound(binding) { return { availableParallelism: function () { return navigator.hardwareConcurrency; @@ -93,25 +94,27 @@ function bound(obj) { arch: function () { return process.arch; }, - cpus: lazyCpus(obj), + cpus: lazyCpus(binding), endianness: function () { - return process.arch === "arm64" || process.arch === "x64" ? "LE" : $bundleError("TODO: endianness"); + return process.arch === "arm64" || process.arch === "x64" // + ? "LE" + : $bundleError("TODO: endianness"); }, - freemem: obj.freemem.bind(obj), - getPriority: obj.getPriority.bind(obj), - homedir: obj.homedir.bind(obj), - hostname: obj.hostname.bind(obj), - loadavg: obj.loadavg.bind(obj), - networkInterfaces: obj.networkInterfaces.bind(obj), + freemem: binding.freemem, + getPriority: binding.getPriority, + homedir: binding.homedir, + hostname: binding.hostname, + loadavg: binding.loadavg, + networkInterfaces: binding.networkInterfaces, platform: function () { return process.platform; }, - release: obj.release.bind(obj), - setPriority: obj.setPriority.bind(obj), + release: binding.release, + setPriority: binding.setPriority, get tmpdir() { return tmpdir; }, - totalmem: obj.totalmem.bind(obj), + totalmem: binding.totalmem, type: function () { return process.platform === "win32" ? "Windows_NT" @@ -121,17 +124,25 @@ function bound(obj) { ? "Linux" : $bundleError("TODO: type"); }, - uptime: obj.uptime.bind(obj), - userInfo: obj.userInfo.bind(obj), - version: obj.version.bind(obj), - machine: obj.machine.bind(obj), + uptime: binding.uptime, + userInfo: binding.userInfo, + version: binding.version, + machine: function () { + return process.arch === "arm64" // + ? "arm64" + : process.arch === "x64" + ? "x86_64" + : $bundleError("TODO: machine"); + }, devNull: process.platform === "win32" ? "\\\\.\\nul" : "/dev/null", - EOL: process.platform === "win32" ? "\r\n" : "\n", + get EOL() { + return process.platform === "win32" ? "\r\n" : "\n"; + }, constants: $processBindingConstants.os, }; } -const out = bound($zig("node_os.zig", "OS.create")); +const out = bound($zig("node_os.zig", "createNodeOsBinding")); symbolToStringify(out, "arch"); symbolToStringify(out, "availableParallelism"); @@ -147,8 +158,10 @@ symbolToStringify(out, "type"); symbolToStringify(out, "uptime"); symbolToStringify(out, "version"); symbolToStringify(out, "machine"); + function symbolToStringify(obj, key) { - obj[key][Symbol.toPrimitive] = function (hint) { + $assert(obj[key] !== undefined, `Missing ${key}`); + obj[key][Symbol.toPrimitive] = function (hint: string) { return obj[key](); }; } diff --git a/src/js/node/path.ts b/src/js/node/path.ts index f7364a82bb..8129e60bbf 100644 --- a/src/js/node/path.ts +++ b/src/js/node/path.ts @@ -1,4 +1,6 @@ // Hardcoded module "node:path" +const { validateString } = require("internal/validators"); + const [bindingPosix, bindingWin32] = $cpp("Path.cpp", "createNodePathBinding"); const toNamespacedPathPosix = bindingPosix.toNamespacedPath.bind(bindingPosix); const toNamespacedPathWin32 = bindingWin32.toNamespacedPath.bind(bindingWin32); @@ -40,4 +42,48 @@ const win32 = { }; posix.win32 = win32.win32 = win32; posix.posix = posix; + +type Glob = import("bun").Glob; + +let LazyGlob: Glob | undefined; +function loadGlob(): LazyGlob { + LazyGlob = require("bun").Glob; +} + +// the most-recently used glob is memoized in case `matchesGlob` is called in a +// loop with the same pattern +let prevGlob: Glob | undefined; +let prevPattern: string | undefined; +function matchesGlob(isWindows, path, pattern) { + let glob: Glob; + + validateString(path, "path"); + if (isWindows) path = path.replaceAll("\\", "/"); + + if (prevGlob) { + $assert(prevPattern !== undefined); + if (prevPattern === pattern) { + glob = prevGlob; + } else { + if (LazyGlob === undefined) loadGlob(); + validateString(pattern, "pattern"); + if (isWindows) pattern = pattern.replaceAll("\\", "/"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } + } else { + loadGlob(); // no prevGlob implies LazyGlob isn't loaded + validateString(pattern, "pattern"); + if (isWindows) pattern = pattern.replaceAll("\\", "/"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } + + return glob.match(path); +} + +// posix.matchesGlob = win32.matchesGlob = matchesGlob; +posix.matchesGlob = matchesGlob.bind(null, false); +win32.matchesGlob = matchesGlob.bind(null, true); + export default process.platform === "win32" ? win32 : posix; diff --git a/src/js/node/querystring.ts b/src/js/node/querystring.ts index 73a9a0ac15..d91e69f0a4 100644 --- a/src/js/node/querystring.ts +++ b/src/js/node/querystring.ts @@ -1,398 +1,540 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +const { Buffer } = require("node:buffer"); + +const ArrayIsArray = Array.isArray; +const MathAbs = Math.abs; +const NumberIsFinite = Number.isFinite; +const ObjectKeys = Object.keys; +const StringPrototypeCharCodeAt = String.prototype.charCodeAt; +const StringPrototypeSlice = String.prototype.slice; +const StringPrototypeToUpperCase = String.prototype.toUpperCase; +const NumberPrototypeToString = Number.prototype.toString; + var __commonJS = (cb, mod: typeof module | undefined = undefined) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); -var Buffer = require("node:buffer").Buffer; - -// src/node-fallbacks/node_modules/querystring-es3/src/object-keys.js -var require_object_keys = __commonJS((exports, module) => { - var objectKeys = - Object.keys || - (function () { - var hasOwnProperty = Object.prototype.hasOwnProperty; - var hasDontEnumBug = !{ toString: null }.propertyIsEnumerable("toString"); - var dontEnums = [ - "toString", - "toLocaleString", - "valueOf", - "hasOwnProperty", - "isPrototypeOf", - "propertyIsEnumerable", - "constructor", - ]; - var dontEnumsLength = dontEnums.length; - return function (obj) { - if (typeof obj !== "function" && (typeof obj !== "object" || obj === null)) { - throw new TypeError("Object.keys called on non-object"); - } - var result = []; - var prop; - var i; - for (prop in obj) { - if (hasOwnProperty.$call(obj, prop)) { - result.push(prop); - } - } - if (hasDontEnumBug) { - for (i = 0; i < dontEnumsLength; i++) { - if (hasOwnProperty.$call(obj, dontEnums[i])) { - result.push(dontEnums[i]); - } - } - } - return result; - }; - })(); - module.exports = objectKeys; -}); - -// src/node-fallbacks/node_modules/querystring-es3/src/index.js var require_src = __commonJS((exports, module) => { - var ParsedQueryString = function () {}; - var unescapeBuffer = function (s, decodeSpaces) { - var out = Buffer.allocUnsafe(s.length); - var state = 0; - var n, m, hexchar, c; - for (var inIndex = 0, outIndex = 0; ; inIndex++) { - if (inIndex < s.length) { - c = s.charCodeAt(inIndex); - } else { - if (state > 0) { - out[outIndex++] = 37; - if (state === 2) out[outIndex++] = hexchar; + /** + * @param {string} str + * @param {Int8Array} noEscapeTable + * @param {string[]} hexTable + * @returns {string} + */ + function encodeStr(str, noEscapeTable, hexTable) { + const len = str.length; + if (len === 0) return ""; + + let out = ""; + let lastPos = 0; + let i = 0; + + outer: for (; i < len; i++) { + let c = StringPrototypeCharCodeAt.$call(str, i); + + // ASCII + while (c < 0x80) { + if (noEscapeTable[c] !== 1) { + if (lastPos < i) out += StringPrototypeSlice.$call(str, lastPos, i); + lastPos = i + 1; + out += hexTable[c]; } - break; + + if (++i === len) break outer; + + c = StringPrototypeCharCodeAt.$call(str, i); } - switch (state) { - case 0: - switch (c) { - case 37: - n = 0; - m = 0; - state = 1; - break; - case 43: - if (decodeSpaces) c = 32; - default: - out[outIndex++] = c; - break; - } - break; - case 1: - hexchar = c; - n = unhexTable[c]; - if (!(n >= 0)) { - out[outIndex++] = 37; - out[outIndex++] = c; - state = 0; - break; - } - state = 2; - break; - case 2: - state = 0; - m = unhexTable[c]; - if (!(m >= 0)) { - out[outIndex++] = 37; - out[outIndex++] = hexchar; - out[outIndex++] = c; - break; - } - out[outIndex++] = 16 * n + m; - break; + + if (lastPos < i) out += StringPrototypeSlice.$call(str, lastPos, i); + + // Multi-byte characters ... + if (c < 0x800) { + lastPos = i + 1; + out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)]; + continue; } + if (c < 0xd800 || c >= 0xe000) { + lastPos = i + 1; + out += hexTable[0xe0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3f)] + hexTable[0x80 | (c & 0x3f)]; + continue; + } + // Surrogate pair + ++i; + + // This branch should never happen because all URLSearchParams entries + // should already be converted to USVString. But, included for + // completion's sake anyway. + if (i >= len) throw $ERR_INVALID_URI("URI malformed"); + + const c2 = StringPrototypeCharCodeAt.$call(str, i) & 0x3ff; + + lastPos = i + 1; + c = 0x10000 + (((c & 0x3ff) << 10) | c2); + out += + hexTable[0xf0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3f)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)]; } - return out.slice(0, outIndex); - }; - var qsUnescape = function (s, decodeSpaces) { + if (lastPos === 0) return str; + if (lastPos < len) return out + StringPrototypeSlice.$call(str, lastPos); + return out; + } + + const hexTable = new Array(256); + for (let i = 0; i < 256; ++i) + hexTable[i] = "%" + StringPrototypeToUpperCase.$call((i < 16 ? "0" : "") + NumberPrototypeToString.$call(i, 16)); + // prettier-ignore + const isHexTable = new Int8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95 + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ... + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ... 256 + ]); + const QueryString = (module.exports = { + unescapeBuffer, + // `unescape()` is a JS global, so we need to use a different local name + unescape: qsUnescape, + + // `escape()` is a JS global, so we need to use a different local name + escape: qsEscape, + + stringify, + encode: stringify, + + parse, + decode: parse, + }); + + // prettier-ignore + const unhexTable = new Int8Array([ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47 + +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63 + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95 + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ... + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255 + ]); + /** + * A safe fast alternative to decodeURIComponent + * @param {string} s + * @param {boolean} decodeSpaces + * @returns {string} + */ + function unescapeBuffer(s, decodeSpaces) { + const out = Buffer.allocUnsafe(s.length); + let index = 0; + let outIndex = 0; + let currentChar; + let nextChar; + let hexHigh; + let hexLow; + const maxLength = s.length - 2; + // Flag to know if some hex chars have been decoded + let hasHex = false; + while (index < s.length) { + currentChar = StringPrototypeCharCodeAt.$call(s, index); + if (currentChar === 43 /* '+' */ && decodeSpaces) { + out[outIndex++] = 32; // ' ' + index++; + continue; + } + if (currentChar === 37 /* '%' */ && index < maxLength) { + currentChar = StringPrototypeCharCodeAt.$call(s, ++index); + hexHigh = unhexTable[currentChar]; + if (!(hexHigh >= 0)) { + out[outIndex++] = 37; // '%' + continue; + } else { + nextChar = StringPrototypeCharCodeAt.$call(s, ++index); + hexLow = unhexTable[nextChar]; + if (!(hexLow >= 0)) { + out[outIndex++] = 37; // '%' + index--; + } else { + hasHex = true; + currentChar = hexHigh * 16 + hexLow; + } + } + } + out[outIndex++] = currentChar; + index++; + } + return hasHex ? out.slice(0, outIndex) : out; + } + + /** + * @param {string} s + * @param {boolean} decodeSpaces + * @returns {string} + */ + function qsUnescape(s, decodeSpaces) { try { return decodeURIComponent(s); - } catch (e) { + } catch { return QueryString.unescapeBuffer(s, decodeSpaces).toString(); } - }; - var qsEscape = function (str) { + } + + // These characters do not need escaping when generating query strings: + // ! - . _ ~ + // ' ( ) * + // digits + // alpha (uppercase) + // alpha (lowercase) + // prettier-ignore + const noEscape = new Int8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127 +]); + + /** + * QueryString.escape() replaces encodeURIComponent() + * @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 + * @param {any} str + * @returns {string} + */ + function qsEscape(str) { if (typeof str !== "string") { if (typeof str === "object") str = String(str); else str += ""; } - var out = ""; - var lastPos = 0; - for (var i2 = 0; i2 < str.length; ++i2) { - var c = str.charCodeAt(i2); - if (c < 128) { - if (noEscape[c] === 1) continue; - if (lastPos < i2) out += str.slice(lastPos, i2); - lastPos = i2 + 1; - out += hexTable[c]; - continue; - } - if (lastPos < i2) out += str.slice(lastPos, i2); - if (c < 2048) { - lastPos = i2 + 1; - out += hexTable[192 | (c >> 6)] + hexTable[128 | (c & 63)]; - continue; - } - if (c < 55296 || c >= 57344) { - lastPos = i2 + 1; - out += hexTable[224 | (c >> 12)] + hexTable[128 | ((c >> 6) & 63)] + hexTable[128 | (c & 63)]; - continue; - } - ++i2; - var c2; - if (i2 < str.length) c2 = str.charCodeAt(i2) & 1023; - else throw new URIError("URI malformed"); - lastPos = i2 + 1; - c = 65536 + (((c & 1023) << 10) | c2); - out += - hexTable[240 | (c >> 18)] + - hexTable[128 | ((c >> 12) & 63)] + - hexTable[128 | ((c >> 6) & 63)] + - hexTable[128 | (c & 63)]; - } - if (lastPos === 0) return str; - if (lastPos < str.length) return out + str.slice(lastPos); - return out; - }; - var stringifyPrimitive = function (v) { + + return encodeStr(str, noEscape, hexTable); + } + + /** + * @param {string | number | bigint | boolean | symbol | undefined | null} v + * @returns {string} + */ + function stringifyPrimitive(v) { if (typeof v === "string") return v; - if (typeof v === "number" && isFinite(v)) return "" + v; + if (typeof v === "number" && NumberIsFinite(v)) return "" + v; + if (typeof v === "bigint") return "" + v; if (typeof v === "boolean") return v ? "true" : "false"; return ""; - }; - var stringify = function (obj, sep, eq, options) { - sep = sep || "&"; - eq = eq || "="; - var encode = QueryString.escape; + } + + /** + * @param {string | number | bigint | boolean} v + * @param {(v: string) => string} encode + * @returns {string} + */ + function encodeStringified(v, encode) { + if (typeof v === "string") return v.length ? encode(v) : ""; + if (typeof v === "number" && NumberIsFinite(v)) { + // Values >= 1e21 automatically switch to scientific notation which requires + // escaping due to the inclusion of a '+' in the output + return MathAbs(v) < 1e21 ? "" + v : encode("" + v); + } + if (typeof v === "bigint") return "" + v; + if (typeof v === "boolean") return v ? "true" : "false"; + return ""; + } + + /** + * @param {string | number | boolean | null} v + * @param {(v: string) => string} encode + * @returns {string} + */ + function encodeStringifiedCustom(v, encode) { + return encode(stringifyPrimitive(v)); + } + + /** + * @param {Record | null>} obj + * @param {string} [sep] + * @param {string} [eq] + * @param {{ encodeURIComponent?: (v: string) => string }} [options] + * @returns {string} + */ + function stringify(obj, sep, eq, options) { + sep ||= "&"; + eq ||= "="; + + let encode = QueryString.escape; if (options && typeof options.encodeURIComponent === "function") { encode = options.encodeURIComponent; } + const convert = encode === qsEscape ? encodeStringified : encodeStringifiedCustom; + if (obj !== null && typeof obj === "object") { - var keys = objectKeys(obj); - var len = keys.length; - var flast = len - 1; - var fields = ""; - for (var i2 = 0; i2 < len; ++i2) { - var k = keys[i2]; - var v = obj[k]; - var ks = encode(stringifyPrimitive(k)) + eq; - if (isArray(v)) { - var vlen = v.length; - var vlast = vlen - 1; - for (var j = 0; j < vlen; ++j) { - fields += ks + encode(stringifyPrimitive(v[j])); - if (j < vlast) fields += sep; + const keys = ObjectKeys(obj); + const len = keys.length; + let fields = ""; + for (let i = 0; i < len; ++i) { + const k = keys[i]; + const v = obj[k]; + let ks = convert(k, encode); + ks += eq; + + if (ArrayIsArray(v)) { + const vlen = v.length; + if (vlen === 0) continue; + if (fields) fields += sep; + for (let j = 0; j < vlen; ++j) { + if (j) fields += sep; + fields += ks; + fields += convert(v[j], encode); } - if (vlen && i2 < flast) fields += sep; } else { - fields += ks + encode(stringifyPrimitive(v)); - if (i2 < flast) fields += sep; + if (fields) fields += sep; + fields += ks; + fields += convert(v, encode); } } return fields; } return ""; - }; - var charCodes = function (str) { + } + + /** + * @param {string} str + * @returns {number[]} + */ + function charCodes(str) { if (str.length === 0) return []; - if (str.length === 1) return [str.charCodeAt(0)]; - const ret = []; - for (var i2 = 0; i2 < str.length; ++i2) ret[ret.length] = str.charCodeAt(i2); + if (str.length === 1) return [StringPrototypeCharCodeAt.$call(str, 0)]; + const ret = new Array(str.length); + for (let i = 0; i < str.length; ++i) ret[i] = StringPrototypeCharCodeAt.$call(str, i); return ret; - }; - var parse = function (qs, sep, eq, options) { - const obj = new ParsedQueryString(); + } + const defSepCodes = [38]; // & + const defEqCodes = [61]; // = + + function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) { + if (key.length > 0 && keyEncoded) key = decodeStr(key, decode); + if (value.length > 0 && valEncoded) value = decodeStr(value, decode); + + if (obj[key] === undefined) { + obj[key] = value; + } else { + const curValue = obj[key]; + // A simple Array-specific property check is enough here to + // distinguish from a string value and is faster and still safe + // since we are generating all of the values being assigned. + if (curValue.pop) curValue[curValue.length] = value; + else obj[key] = [curValue, value]; + } + } + + /** + * Parse a key/val string. + * @param {string} qs + * @param {string} sep + * @param {string} eq + * @param {{ + * maxKeys?: number; + * decodeURIComponent?(v: string): string; + * }} [options] + * @returns {Record} + */ + function parse(qs, sep, eq, options) { + const obj = { __proto__: null }; + if (typeof qs !== "string" || qs.length === 0) { return obj; } - var sepCodes = !sep ? defSepCodes : charCodes(sep + ""); - var eqCodes = !eq ? defEqCodes : charCodes(eq + ""); + + const sepCodes = !sep ? defSepCodes : charCodes(String(sep)); + const eqCodes = !eq ? defEqCodes : charCodes(String(eq)); const sepLen = sepCodes.length; const eqLen = eqCodes.length; - var pairs = 1000; + + let pairs = 1000; if (options && typeof options.maxKeys === "number") { + // -1 is used in place of a value like Infinity for meaning + // "unlimited pairs" because of additional checks V8 (at least as of v5.4) + // has to do when using variables that contain values like Infinity. Since + // `pairs` is always decremented and checked explicitly for 0, -1 works + // effectively the same as Infinity, while providing a significant + // performance boost. pairs = options.maxKeys > 0 ? options.maxKeys : -1; } - var decode = QueryString.unescape; + + let decode = QueryString.unescape; if (options && typeof options.decodeURIComponent === "function") { decode = options.decodeURIComponent; } const customDecode = decode !== qsUnescape; - const keys = []; - var posIdx = 0; - var lastPos = 0; - var sepIdx = 0; - var eqIdx = 0; - var key = ""; - var value = ""; - var keyEncoded = customDecode; - var valEncoded = customDecode; - var encodeCheck = 0; - for (var i2 = 0; i2 < qs.length; ++i2) { - const code = qs.charCodeAt(i2); + + let lastPos = 0; + let sepIdx = 0; + let eqIdx = 0; + let key = ""; + let value = ""; + let keyEncoded = customDecode; + let valEncoded = customDecode; + const plusChar = customDecode ? "%20" : " "; + let encodeCheck = 0; + for (let i = 0; i < qs.length; ++i) { + const code = StringPrototypeCharCodeAt.$call(qs, i); + + // Try matching key/value pair separator (e.g. '&') if (code === sepCodes[sepIdx]) { if (++sepIdx === sepLen) { - const end = i2 - sepIdx + 1; + // Key/value pair separator match! + const end = i - sepIdx + 1; if (eqIdx < eqLen) { - if (lastPos < end) key += qs.slice(lastPos, end); - } else if (lastPos < end) value += qs.slice(lastPos, end); - if (keyEncoded) key = decodeStr(key, decode); - if (valEncoded) value = decodeStr(value, decode); - if (key || value || lastPos - posIdx > sepLen || i2 === 0) { - if (indexOf(keys, key) === -1) { - obj[key] = value; - keys[keys.length] = key; - } else { - const curValue = obj[key] || ""; - if (curValue.pop) curValue[curValue.length] = value; - else if (curValue) obj[key] = [curValue, value]; + // We didn't find the (entire) key/value separator + if (lastPos < end) { + // Treat the substring as part of the key instead of the value + key += StringPrototypeSlice.$call(qs, lastPos, end); + } else if (key.length === 0) { + // We saw an empty substring between separators + if (--pairs === 0) return obj; + lastPos = i + 1; + sepIdx = eqIdx = 0; + continue; } - } else if (i2 === 1) { - delete obj[key]; + } else if (lastPos < end) { + value += StringPrototypeSlice.$call(qs, lastPos, end); } - if (--pairs === 0) break; + + addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); + + if (--pairs === 0) return obj; keyEncoded = valEncoded = customDecode; - encodeCheck = 0; key = value = ""; - posIdx = lastPos; - lastPos = i2 + 1; + encodeCheck = 0; + lastPos = i + 1; sepIdx = eqIdx = 0; } - continue; } else { sepIdx = 0; - if (!valEncoded) { - if (code === 37) { - encodeCheck = 1; - } else if ( - encodeCheck > 0 && - ((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102)) - ) { - if (++encodeCheck === 3) valEncoded = true; + // Try matching key/value separator (e.g. '=') if we haven't already + if (eqIdx < eqLen) { + if (code === eqCodes[eqIdx]) { + if (++eqIdx === eqLen) { + // Key/value separator match! + const end = i - eqIdx + 1; + if (lastPos < end) key += StringPrototypeSlice.$call(qs, lastPos, end); + encodeCheck = 0; + lastPos = i + 1; + } + continue; } else { - encodeCheck = 0; + eqIdx = 0; + if (!keyEncoded) { + // Try to match an (valid) encoded byte once to minimize unnecessary + // calls to string decoding functions + if (code === 37 /* % */) { + encodeCheck = 1; + continue; + } else if (encodeCheck > 0) { + if (isHexTable[code] === 1) { + if (++encodeCheck === 3) keyEncoded = true; + continue; + } else { + encodeCheck = 0; + } + } + } + } + if (code === 43 /* + */) { + if (lastPos < i) key += StringPrototypeSlice.$call(qs, lastPos, i); + key += plusChar; + lastPos = i + 1; + continue; } } - } - if (eqIdx < eqLen) { - if (code === eqCodes[eqIdx]) { - if (++eqIdx === eqLen) { - const end = i2 - eqIdx + 1; - if (lastPos < end) key += qs.slice(lastPos, end); - encodeCheck = 0; - lastPos = i2 + 1; - } - continue; - } else { - eqIdx = 0; - if (!keyEncoded) { - if (code === 37) { - encodeCheck = 1; - } else if ( - encodeCheck > 0 && - ((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102)) - ) { - if (++encodeCheck === 3) keyEncoded = true; + if (code === 43 /* + */) { + if (lastPos < i) value += StringPrototypeSlice.$call(qs, lastPos, i); + value += plusChar; + lastPos = i + 1; + } else if (!valEncoded) { + // Try to match an (valid) encoded byte (once) to minimize unnecessary + // calls to string decoding functions + if (code === 37 /* % */) { + encodeCheck = 1; + } else if (encodeCheck > 0) { + if (isHexTable[code] === 1) { + if (++encodeCheck === 3) valEncoded = true; } else { encodeCheck = 0; } } } } - if (code === 43) { - if (eqIdx < eqLen) { - if (lastPos < i2) key += qs.slice(lastPos, i2); - key += "%20"; - keyEncoded = true; - } else { - if (lastPos < i2) value += qs.slice(lastPos, i2); - value += "%20"; - valEncoded = true; - } - lastPos = i2 + 1; - } } - if (pairs !== 0 && (lastPos < qs.length || eqIdx > 0)) { - if (lastPos < qs.length) { - if (eqIdx < eqLen) key += qs.slice(lastPos); - else if (sepIdx < sepLen) value += qs.slice(lastPos); - } - if (keyEncoded) key = decodeStr(key, decode); - if (valEncoded) value = decodeStr(value, decode); - if (indexOf(keys, key) === -1) { - obj[key] = value; - keys[keys.length] = key; - } else { - const curValue = obj[key]; - if (curValue.pop) curValue[curValue.length] = value; - else obj[key] = [curValue, value]; - } + + // Deal with any leftover key or value data + if (lastPos < qs.length) { + if (eqIdx < eqLen) key += StringPrototypeSlice.$call(qs, lastPos); + else if (sepIdx < sepLen) value += StringPrototypeSlice.$call(qs, lastPos); + } else if (eqIdx === 0 && key.length === 0) { + // We ended on an empty substring + return obj; } + + addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); + return obj; - }; - var decodeStr = function (s, decoder) { + } + + /** + * V8 does not optimize functions with try-catch blocks, so we isolate them here + * to minimize the damage (Note: no longer true as of V8 5.4 -- but still will + * not be inlined). + * @param {string} s + * @param {(v: string) => string} decoder + * @returns {string} + */ + function decodeStr(s, decoder) { try { return decoder(s); - } catch (e) { + } catch { return QueryString.unescape(s, true); } - }; - var QueryString = (module.exports = { - unescapeBuffer, - unescape: qsUnescape, - escape: qsEscape, - stringify, - encode: stringify, - parse, - decode: parse, - }); - var objectKeys = require_object_keys(); - var isArray = arg => Object.prototype.toString.$call(arg) === "[object Array]"; - var indexOf = (arr, searchElement, fromIndex) => { - var k; - if (arr == null) { - throw new TypeError('"arr" is null or not defined'); - } - var o = Object(arr); - var len = o.length >>> 0; - if (len === 0) { - return -1; - } - var n = fromIndex | 0; - if (n >= len) { - return -1; - } - k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); - while (k < len) { - if (k in o && o[k] === searchElement) { - return k; - } - k++; - } - return -1; - }; - ParsedQueryString.prototype = Object.create ? Object.create(null) : {}; - var unhexTable = [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, - -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - ]; - var hexTable = []; - for (i = 0; i < 256; ++i) hexTable[i] = "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase(); - var i; - var noEscape = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, - 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, - ]; - var defSepCodes = [38]; - var defEqCodes = [61]; + } }); export default require_src(); diff --git a/src/js/node/readline.ts b/src/js/node/readline.ts index 9db2708f43..07bb433a90 100644 --- a/src/js/node/readline.ts +++ b/src/js/node/readline.ts @@ -27,6 +27,7 @@ // ---------------------------------------------------------------------------- const EventEmitter = require("node:events"); const { StringDecoder } = require("node:string_decoder"); +const { promisify } = require("internal/promisify"); const { validateFunction, @@ -40,9 +41,6 @@ const { const internalGetStringWidth = $newZigFunction("string.zig", "String.jsGetStringWidth", 1); -const ObjectGetPrototypeOf = Object.getPrototypeOf; -const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors; -const ObjectValues = Object.values; const PromiseReject = Promise.reject; var isWritable; @@ -158,72 +156,6 @@ function stripVTControlCharacters(str) { return RegExpPrototypeSymbolReplace.$call(ansi, str, ""); } -// Promisify - -var kCustomPromisifiedSymbol = SymbolFor("nodejs.util.promisify.custom"); -var kCustomPromisifyArgsSymbol = Symbol("customPromisifyArgs"); - -function promisify(original) { - validateFunction(original, "original"); - - if (original[kCustomPromisifiedSymbol]) { - let fn = original[kCustomPromisifiedSymbol]; - - validateFunction(fn, "util.promisify.custom"); - - return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, { - __proto__: null, - value: fn, - enumerable: false, - writable: false, - configurable: true, - }); - } - - // Names to create an object from in case the callback receives multiple - // arguments, e.g. ['bytesRead', 'buffer'] for fs.read. - var argumentNames = original[kCustomPromisifyArgsSymbol]; - - function fn(...args) { - return new Promise((resolve, reject) => { - ArrayPrototypePush.$call(args, (err, ...values) => { - if (err) { - return reject(err); - } - if (argumentNames !== undefined && values.length > 1) { - var obj = {}; - for (var i = 0; i < argumentNames.length; i++) obj[argumentNames[i]] = values[i]; - resolve(obj); - } else { - resolve(values[0]); - } - }); - original.$apply(this, args); - }); - } - - ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original)); - - ObjectDefineProperty(fn, kCustomPromisifiedSymbol, { - __proto__: null, - value: fn, - enumerable: false, - writable: false, - configurable: true, - }); - - var descriptors = ObjectGetOwnPropertyDescriptors(original); - var propertiesValues = ObjectValues(descriptors); - for (var i = 0; i < propertiesValues.length; i++) { - // We want to use null-prototype objects to not rely on globally mutable - // %Object.prototype%. - ObjectSetPrototypeOf(propertiesValues[i], null); - } - return ObjectDefineProperties(fn, descriptors); -} - -promisify.custom = kCustomPromisifiedSymbol; - // Constants const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 @@ -270,30 +202,6 @@ var NodeError = getNodeErrorByName("Error"); var NodeTypeError = getNodeErrorByName("TypeError"); var NodeRangeError = getNodeErrorByName("RangeError"); -class ERR_INVALID_ARG_VALUE extends NodeTypeError { - constructor(name, value, reason = "not specified") { - super(`The value "${String(value)}" is invalid for argument '${name}'. Reason: ${reason}`, { - code: "ERR_INVALID_ARG_VALUE", - }); - } -} - -class ERR_INVALID_CURSOR_POS extends NodeTypeError { - constructor() { - super("Cannot set cursor row without setting its column", { - code: "ERR_INVALID_CURSOR_POS", - }); - } -} - -class ERR_OUT_OF_RANGE extends NodeRangeError { - constructor(name, range, received) { - super(`The value of "${name}" is out of range. It must be ${range}. Received ${received}`, { - code: "ERR_OUT_OF_RANGE", - }); - } -} - class ERR_USE_AFTER_CLOSE extends NodeError { constructor() { super("This socket has been ended by the other party", { @@ -302,14 +210,6 @@ class ERR_USE_AFTER_CLOSE extends NodeError { } } -class AbortError extends Error { - code; - constructor() { - super("The operation was aborted"); - this.code = "ABORT_ERR"; - } -} - // ---------------------------------------------------------------------------- // Section: Utils // ---------------------------------------------------------------------------- @@ -881,15 +781,15 @@ function cursorTo(stream, x, y, callback) { y = undefined; } - if (NumberIsNaN(x)) throw new ERR_INVALID_ARG_VALUE("x", x); - if (NumberIsNaN(y)) throw new ERR_INVALID_ARG_VALUE("y", y); + if (NumberIsNaN(x)) throw $ERR_INVALID_ARG_VALUE("x", x); + if (NumberIsNaN(y)) throw $ERR_INVALID_ARG_VALUE("y", y); if (stream == null || (typeof x !== "number" && typeof y !== "number")) { if (typeof callback === "function") process.nextTick(callback, null); return true; } - if (typeof x !== "number") throw new ERR_INVALID_CURSOR_POS(); + if (typeof x !== "number") throw $ERR_INVALID_CURSOR_POS(); var data = typeof y !== "number" ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; return stream.write(data, callback); @@ -1286,7 +1186,7 @@ function InterfaceConstructor(input, output, completer, terminal) { if (NumberIsFinite(inputEscapeCodeTimeout)) { this.escapeCodeTimeout = inputEscapeCodeTimeout; } else { - throw new ERR_INVALID_ARG_VALUE("input.escapeCodeTimeout", this.escapeCodeTimeout); + throw $ERR_INVALID_ARG_VALUE("input.escapeCodeTimeout", this.escapeCodeTimeout); } } @@ -1299,7 +1199,7 @@ function InterfaceConstructor(input, output, completer, terminal) { } if (completer !== undefined && typeof completer !== "function") { - throw new ERR_INVALID_ARG_VALUE("completer", completer); + throw $ERR_INVALID_ARG_VALUE("completer", completer); } if (history === undefined) { @@ -1313,7 +1213,7 @@ function InterfaceConstructor(input, output, completer, terminal) { } if (typeof historySize !== "number" || NumberIsNaN(historySize) || historySize < 0) { - throw new ERR_INVALID_ARG_VALUE("historySize", historySize); + throw $ERR_INVALID_ARG_VALUE("historySize", historySize); } // Backwards compat; check the isTTY prop of the output stream @@ -2429,14 +2329,14 @@ Interface.prototype.question[promisify.custom] = function question(query, option var signal = options?.signal; if (signal && signal.aborted) { - return PromiseReject(new AbortError(undefined, { cause: signal.reason })); + return PromiseReject($makeAbortError(undefined, { cause: signal.reason })); } return new Promise((resolve, reject) => { var cb = resolve; if (signal) { var onAbort = () => { - reject(new AbortError(undefined, { cause: signal.reason })); + reject($makeAbortError(undefined, { cause: signal.reason })); }; signal.addEventListener("abort", onAbort, { once: true }); cb = answer => { @@ -2898,7 +2798,7 @@ var PromisesInterface = class Interface extends _Interface { if (signal) { validateAbortSignal(signal, "options.signal"); if (signal.aborted) { - return PromiseReject(new AbortError(undefined, { cause: signal.reason })); + return PromiseReject($makeAbortError(undefined, { cause: signal.reason })); } } const { promise, resolve, reject } = $newPromiseCapability(Promise); @@ -2906,7 +2806,7 @@ var PromisesInterface = class Interface extends _Interface { if (options?.signal) { var onAbort = () => { this[kQuestionCancel](); - reject(new AbortError(undefined, { cause: signal.reason })); + reject($makeAbortError(undefined, { cause: signal.reason })); }; signal.addEventListener("abort", onAbort, { once: true }); cb = answer => { diff --git a/src/js/node/stream.consumers.ts b/src/js/node/stream.consumers.ts index d56c456bb4..84f3b0d03c 100644 --- a/src/js/node/stream.consumers.ts +++ b/src/js/node/stream.consumers.ts @@ -1,14 +1,52 @@ // Hardcoded module "node:stream/consumers" / "readable-stream/consumer" -const arrayBuffer = Bun.readableStreamToArrayBuffer; -const bytes = Bun.readableStreamToBytes; -const text = Bun.readableStreamToText; -const json = stream => Bun.readableStreamToText(stream).then(JSON.parse); +"use strict"; -const buffer = async readableStream => { - return new Buffer(await arrayBuffer(readableStream)); -}; +const { Buffer } = require("node:buffer"); -const blob = Bun.readableStreamToBlob; +const JSONParse = JSON.parse; + +async function blob(stream): Promise { + if ($inheritsReadableStream(stream)) return Bun.readableStreamToBlob(stream); + const chunks: (Blob | ArrayBuffer | string | NodeJS.ArrayBufferView)[] = []; + for await (const chunk of stream) chunks.push(chunk); + return new Blob(chunks); +} + +async function arrayBuffer(stream): Promise { + if ($inheritsReadableStream(stream)) return Bun.readableStreamToArrayBuffer(stream); + const ret = await blob(stream); + return ret.arrayBuffer(); +} + +async function bytes(stream): Promise { + if ($inheritsReadableStream(stream)) return Bun.readableStreamToBytes(stream); + const ret = await blob(stream); + return ret.bytes(); +} + +async function buffer(stream): Promise { + return Buffer.from(await arrayBuffer(stream)); +} + +async function text(stream): Promise { + if ($inheritsReadableStream(stream)) return Bun.readableStreamToText(stream); + const dec = new TextDecoder(); + let str = ""; + for await (const chunk of stream) { + if (typeof chunk === "string") str += chunk; + else str += dec.decode(chunk, { stream: true }); + } + // Flush the streaming TextDecoder so that any pending + // incomplete multibyte characters are handled. + str += dec.decode(undefined, { stream: false }); + return str; +} + +async function json(stream): Promise { + if ($inheritsReadableStream(stream)) return Bun.readableStreamToJSON(stream); + const str = await text(stream); + return JSONParse(str); +} export default { arrayBuffer, diff --git a/src/js/node/stream.ts b/src/js/node/stream.ts index 874031f49e..9d261544c1 100644 --- a/src/js/node/stream.ts +++ b/src/js/node/stream.ts @@ -1,5610 +1,11 @@ // Hardcoded module "node:stream" / "readable-stream" -// "readable-stream" npm package -// just transpiled and debug logs added. - -// BEGIN moved from require_readable -// when we split this stuff up again, we can move this back -const kObjectMode = 1 << 0; -const kEnded = 1 << 1; -const kEndEmitted = 1 << 2; -const kReading = 1 << 3; -const kConstructed = 1 << 4; -const kSync = 1 << 5; -const kNeedReadable = 1 << 6; -const kEmittedReadable = 1 << 7; -const kReadableListening = 1 << 8; -const kResumeScheduled = 1 << 9; -const kErrorEmitted = 1 << 10; -const kEmitClose = 1 << 11; -const kAutoDestroy = 1 << 12; -const kDestroyed = 1 << 13; -const kClosed = 1 << 14; -const kCloseEmitted = 1 << 15; -const kMultiAwaitDrain = 1 << 16; -const kReadingMore = 1 << 17; -const kDataEmitted = 1 << 18; -const kPaused = Symbol("kPaused"); -// END moved from require_readable - -const StringDecoder = require("node:string_decoder").StringDecoder; -const transferToNativeReadable = $newCppFunction("ReadableStream.cpp", "jsFunctionTransferToNativeReadableStream", 1); -const { kAutoDestroyed } = require("internal/shared"); -const { - validateBoolean, - validateString, - validateNumber, - validateSignalName, - validateEncoding, - validatePort, - validateInteger, - validateInt32, - validateUint32, - validateArray, - validateBuffer, - validateAbortSignal, - validateFunction, - validatePlainFunction, - validateUndefined, -} = require("internal/validators"); - -const ObjectSetPrototypeOf = Object.setPrototypeOf; - -const ProcessNextTick = process.nextTick; +const { kEnsureConstructed, kGetNativeReadableProto } = require("internal/shared"); const EE = require("node:events").EventEmitter; - -var __getOwnPropNames = Object.getOwnPropertyNames; - -var __commonJS = (cb, mod: typeof module | undefined = undefined) => - function __require2() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; - }; - -function isReadableStream(value) { - return typeof value === "object" && value !== null && value instanceof ReadableStream; -} +const exports = require("internal/stream"); $debug("node:stream loaded"); -//------------------------------------------------------------------------------ -// Node error polyfills -//------------------------------------------------------------------------------ - -function ERR_INVALID_ARG_TYPE(name, type, value) { - return new Error(`The argument '${name}' is invalid. Received '${value}' for type '${type}'`); -} - -function ERR_INVALID_ARG_VALUE(name, value, reason) { - return new Error(`The value '${value}' is invalid for argument '${name}'. Reason: ${reason}`); -} - -// node_modules/readable-stream/lib/ours/primordials.js -var require_primordials = __commonJS({ - "node_modules/readable-stream/lib/ours/primordials.js"(exports, module) { - "use strict"; - module.exports = { - ArrayPrototypeIncludes(self, el) { - return self.includes(el); - }, - ArrayPrototypeIndexOf(self, el) { - return self.indexOf(el); - }, - ArrayPrototypeJoin(self, sep) { - return self.join(sep); - }, - ArrayPrototypeMap(self, fn) { - return self.map(fn); - }, - ArrayPrototypePop(self, el) { - return self.pop(el); - }, - ArrayPrototypePush(self, el) { - return self.push(el); - }, - ArrayPrototypeSlice(self, start, end) { - return self.slice(start, end); - }, - Error, - FunctionPrototypeCall(fn, thisArgs, ...args) { - return fn.$call(thisArgs, ...args); - }, - FunctionPrototypeSymbolHasInstance(self, instance) { - return Function.prototype[Symbol.hasInstance].$call(self, instance); - }, - MathFloor: Math.floor, - Number, - NumberIsInteger: Number.isInteger, - NumberIsNaN: Number.isNaN, - NumberMAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER, - NumberMIN_SAFE_INTEGER: Number.MIN_SAFE_INTEGER, - NumberParseInt: Number.parseInt, - ObjectDefineProperties(self, props) { - return Object.defineProperties(self, props); - }, - ObjectDefineProperty(self, name, prop) { - return Object.defineProperty(self, name, prop); - }, - ObjectGetOwnPropertyDescriptor(self, name) { - return Object.getOwnPropertyDescriptor(self, name); - }, - ObjectKeys(obj) { - return Object.keys(obj); - }, - ObjectSetPrototypeOf(target, proto) { - return Object.setPrototypeOf(target, proto); - }, - Promise, - PromisePrototypeCatch(self, fn) { - return self.catch(fn); - }, - PromisePrototypeThen(self, thenFn, catchFn) { - return self.then(thenFn, catchFn); - }, - PromiseReject(err) { - return Promise.reject(err); - }, - RegExpPrototypeTest(self, value) { - return self.test(value); - }, - SafeSet: Set, - String, - StringPrototypeSlice(self, start, end) { - return self.slice(start, end); - }, - StringPrototypeToLowerCase(self) { - return self.toLowerCase(); - }, - StringPrototypeToUpperCase(self) { - return self.toUpperCase(); - }, - StringPrototypeTrim(self) { - return self.trim(); - }, - Symbol, - SymbolAsyncIterator: Symbol.asyncIterator, - SymbolHasInstance: Symbol.hasInstance, - SymbolIterator: Symbol.iterator, - TypedArrayPrototypeSet(self, buf, len) { - return self.set(buf, len); - }, - Uint8Array, - }; - }, -}); -// node_modules/readable-stream/lib/ours/util.js -var require_util = __commonJS({ - "node_modules/readable-stream/lib/ours/util.js"(exports, module) { - "use strict"; - - var AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; - var isBlob = - typeof Blob !== "undefined" - ? function isBlob2(b) { - return b instanceof Blob; - } - : function isBlob2(b) { - return false; - }; - var AggregateError = class extends Error { - constructor(errors) { - if (!Array.isArray(errors)) { - throw new TypeError(`Expected input to be an Array, got ${typeof errors}`); - } - let message = ""; - for (let i = 0; i < errors.length; i++) { - message += ` ${errors[i].stack} -`; - } - super(message); - this.name = "AggregateError"; - this.errors = errors; - } - }; - module.exports = { - AggregateError, - once(callback) { - let called = false; - return function (...args) { - if (called) { - return; - } - called = true; - callback.$apply(this, args); - }; - }, - createDeferredPromise: function () { - let resolve; - let reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { - promise, - resolve, - reject, - }; - }, - promisify(fn) { - return new Promise((resolve, reject) => { - fn((err, ...args) => { - if (err) { - return reject(err); - } - return resolve(...args); - }); - }); - }, - debuglog() { - return function () {}; - }, - format(format, ...args) { - return format.replace(/%([sdifj])/g, function (...[_unused, type]) { - const replacement = args.shift(); - if (type === "f") { - return replacement.toFixed(6); - } else if (type === "j") { - return JSON.stringify(replacement); - } else if (type === "s" && typeof replacement === "object") { - const ctor = replacement.constructor !== Object ? replacement.constructor.name : ""; - return `${ctor} {}`.trim(); - } else { - return replacement.toString(); - } - }); - }, - inspect(value) { - switch (typeof value) { - case "string": - if (value.includes("'")) { - if (!value.includes('"')) { - return `"${value}"`; - } else if (!value.includes("`") && !value.includes("${")) { - return `\`${value}\``; - } - } - return `'${value}'`; - case "number": - if (isNaN(value)) { - return "NaN"; - } else if (Object.is(value, -0)) { - return String(value); - } - return value; - case "bigint": - return `${String(value)}n`; - case "boolean": - case "undefined": - return String(value); - case "object": - return "{}"; - } - }, - types: { - isAsyncFunction(fn) { - return fn instanceof AsyncFunction; - }, - isArrayBufferView(arr) { - return ArrayBuffer.isView(arr); - }, - }, - isBlob, - }; - module.exports.promisify.custom = Symbol.for("nodejs.util.promisify.custom"); - }, -}); - -// node_modules/readable-stream/lib/ours/errors.js -var require_errors = __commonJS({ - "node_modules/readable-stream/lib/ours/errors.js"(exports, module) { - "use strict"; - var { format, inspect, AggregateError: CustomAggregateError } = require_util(); - var AggregateError = globalThis.AggregateError || CustomAggregateError; - var kIsNodeError = Symbol("kIsNodeError"); - var kTypes = ["string", "function", "number", "object", "Function", "Object", "boolean", "bigint", "symbol"]; - var classRegExp = /^([A-Z][a-z0-9]*)+$/; - var nodeInternalPrefix = "__node_internal_"; - var codes = {}; - function assert(value, message) { - if (!value) { - throw new codes.ERR_INTERNAL_ASSERTION(message); - } - } - function addNumericalSeparator(val) { - let res = ""; - let i = val.length; - const start = val[0] === "-" ? 1 : 0; - for (; i >= start + 4; i -= 3) { - res = `_${val.slice(i - 3, i)}${res}`; - } - return `${val.slice(0, i)}${res}`; - } - function getMessage(key, msg, args) { - if (typeof msg === "function") { - assert( - msg.length <= args.length, - `Code: ${key}; The provided arguments length (${args.length}) does not match the required ones (${msg.length}).`, - ); - return msg(...args); - } - const expectedLength = (msg.match(/%[dfijoOs]/g) || []).length; - assert( - expectedLength === args.length, - `Code: ${key}; The provided arguments length (${args.length}) does not match the required ones (${expectedLength}).`, - ); - if (args.length === 0) { - return msg; - } - return format(msg, ...args); - } - function E(code, message, Base) { - if (!Base) { - Base = Error; - } - class NodeError extends Base { - constructor(...args) { - super(getMessage(code, message, args)); - } - toString() { - return `${this.name} [${code}]: ${this.message}`; - } - } - Object.defineProperties(NodeError.prototype, { - name: { - value: Base.name, - writable: true, - enumerable: false, - configurable: true, - }, - toString: { - value() { - return `${this.name} [${code}]: ${this.message}`; - }, - writable: true, - enumerable: false, - configurable: true, - }, - }); - NodeError.prototype.code = code; - NodeError.prototype[kIsNodeError] = true; - codes[code] = NodeError; - } - function hideStackFrames(fn) { - const hidden = nodeInternalPrefix + fn.name; - Object.defineProperty(fn, "name", { - value: hidden, - }); - return fn; - } - function aggregateTwoErrors(innerError, outerError) { - if (innerError && outerError && innerError !== outerError) { - if (Array.isArray(outerError.errors)) { - outerError.errors.push(innerError); - return outerError; - } - const err = new AggregateError([outerError, innerError], outerError.message); - err.code = outerError.code; - return err; - } - return innerError || outerError; - } - var AbortError = class extends Error { - constructor(message = "The operation was aborted", options = void 0) { - if (options !== void 0 && typeof options !== "object") { - throw new codes.ERR_INVALID_ARG_TYPE("options", "Object", options); - } - super(message, options); - this.code = "ABORT_ERR"; - this.name = "AbortError"; - } - }; - E("ERR_ASSERTION", "%s", Error); - E( - "ERR_INVALID_ARG_TYPE", - (name, expected, actual) => { - assert(typeof name === "string", "'name' must be a string"); - if (!Array.isArray(expected)) { - expected = [expected]; - } - let msg = "The "; - if (name.endsWith(" argument")) { - msg += `${name} `; - } else { - msg += `"${name}" ${name.includes(".") ? "property" : "argument"} `; - } - msg += "must be "; - const types = []; - const instances = []; - const other = []; - for (const value of expected) { - assert(typeof value === "string", "All expected entries have to be of type string"); - if (kTypes.includes(value)) { - types.push(value.toLowerCase()); - } else if (classRegExp.test(value)) { - instances.push(value); - } else { - assert(value !== "object", 'The value "object" should be written as "Object"'); - other.push(value); - } - } - if (instances.length > 0) { - const pos = types.indexOf("object"); - if (pos !== -1) { - types.splice(types, pos, 1); - instances.push("Object"); - } - } - if (types.length > 0) { - switch (types.length) { - case 1: - msg += `of type ${types[0]}`; - break; - case 2: - msg += `one of type ${types[0]} or ${types[1]}`; - break; - default: { - const last = types.pop(); - msg += `one of type ${types.join(", ")}, or ${last}`; - } - } - if (instances.length > 0 || other.length > 0) { - msg += " or "; - } - } - if (instances.length > 0) { - switch (instances.length) { - case 1: - msg += `an instance of ${instances[0]}`; - break; - case 2: - msg += `an instance of ${instances[0]} or ${instances[1]}`; - break; - default: { - const last = instances.pop(); - msg += `an instance of ${instances.join(", ")}, or ${last}`; - } - } - if (other.length > 0) { - msg += " or "; - } - } - switch (other.length) { - case 0: - break; - case 1: - if (other[0].toLowerCase() !== other[0]) { - msg += "an "; - } - msg += `${other[0]}`; - break; - case 2: - msg += `one of ${other[0]} or ${other[1]}`; - break; - default: { - const last = other.pop(); - msg += `one of ${other.join(", ")}, or ${last}`; - } - } - if (actual == null) { - msg += `. Received ${actual}`; - } else if (typeof actual === "function" && actual.name) { - msg += `. Received function ${actual.name}`; - } else if (typeof actual === "object") { - var _actual$constructor; - if ( - (_actual$constructor = actual.constructor) !== null && - _actual$constructor !== void 0 && - _actual$constructor.name - ) { - msg += `. Received an instance of ${actual.constructor.name}`; - } else { - const inspected = inspect(actual, { - depth: -1, - }); - msg += `. Received ${inspected}`; - } - } else { - let inspected = inspect(actual, { - colors: false, - }); - if (inspected.length > 25) { - inspected = `${inspected.slice(0, 25)}...`; - } - msg += `. Received type ${typeof actual} (${inspected})`; - } - return msg; - }, - TypeError, - ); - E( - "ERR_INVALID_ARG_VALUE", - (name, value, reason = "is invalid") => { - let inspected = inspect(value); - if (inspected.length > 128) { - inspected = inspected.slice(0, 128) + "..."; - } - const type = name.includes(".") ? "property" : "argument"; - return `The ${type} '${name}' ${reason}. Received ${inspected}`; - }, - TypeError, - ); - E( - "ERR_INVALID_RETURN_VALUE", - (input, name, value) => { - var _value$constructor; - const type = - value !== null && - value !== void 0 && - (_value$constructor = value.constructor) !== null && - _value$constructor !== void 0 && - _value$constructor.name - ? `instance of ${value.constructor.name}` - : `type ${typeof value}`; - return `Expected ${input} to be returned from the "${name}" function but got ${type}.`; - }, - TypeError, - ); - E( - "ERR_MISSING_ARGS", - (...args) => { - assert(args.length > 0, "At least one arg needs to be specified"); - let msg; - const len = args.length; - args = (Array.isArray(args) ? args : [args]).map(a => `"${a}"`).join(" or "); - switch (len) { - case 1: - msg += `The ${args[0]} argument`; - break; - case 2: - msg += `The ${args[0]} and ${args[1]} arguments`; - break; - default: - { - const last = args.pop(); - msg += `The ${args.join(", ")}, and ${last} arguments`; - } - break; - } - return `${msg} must be specified`; - }, - TypeError, - ); - E( - "ERR_OUT_OF_RANGE", - (str, range, input) => { - assert(range, 'Missing "range" argument'); - let received; - if (Number.isInteger(input) && Math.abs(input) > 2 ** 32) { - received = addNumericalSeparator(String(input)); - } else if (typeof input === "bigint") { - received = String(input); - if (input > 2n ** 32n || input < -(2n ** 32n)) { - received = addNumericalSeparator(received); - } - received += "n"; - } else { - received = inspect(input); - } - return `The value of "${str}" is out of range. It must be ${range}. Received ${received}`; - }, - RangeError, - ); - E("ERR_MULTIPLE_CALLBACK", "Callback called multiple times", Error); - E("ERR_METHOD_NOT_IMPLEMENTED", "The %s method is not implemented", Error); - E("ERR_STREAM_ALREADY_FINISHED", "Cannot call %s after a stream was finished", Error); - E("ERR_STREAM_CANNOT_PIPE", "Cannot pipe, not readable", Error); - E("ERR_STREAM_DESTROYED", "Cannot call %s after a stream was destroyed", Error); - E("ERR_STREAM_NULL_VALUES", "May not write null values to stream", TypeError); - E("ERR_STREAM_PREMATURE_CLOSE", "Premature close", Error); - E("ERR_STREAM_PUSH_AFTER_EOF", "stream.push() after EOF", Error); - E("ERR_STREAM_UNSHIFT_AFTER_END_EVENT", "stream.unshift() after end event", Error); - E("ERR_STREAM_WRITE_AFTER_END", "write after end", Error); - E("ERR_UNKNOWN_ENCODING", "Unknown encoding: %s", TypeError); - module.exports = { - AbortError, - aggregateTwoErrors: hideStackFrames(aggregateTwoErrors), - hideStackFrames, - codes, - }; - }, -}); - -// node_modules/readable-stream/lib/internal/validators.js -var require_validators = __commonJS({ - "node_modules/readable-stream/lib/internal/validators.js"(exports, module) { - "use strict"; - var { - ArrayPrototypeIncludes, - ArrayPrototypeJoin, - ArrayPrototypeMap, - NumberParseInt, - RegExpPrototypeTest, - String: String2, - } = require_primordials(); - var { - hideStackFrames, - codes: { ERR_SOCKET_BAD_PORT, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE, ERR_UNKNOWN_SIGNAL }, - } = require_errors(); - var { normalizeEncoding } = require_util(); - var { isAsyncFunction, isArrayBufferView } = require_util().types; - var signals = {}; - function isInt32(value) { - return value === (value | 0); - } - function isUint32(value) { - return value === value >>> 0; - } - var octalReg = /^[0-7]+$/; - var modeDesc = "must be a 32-bit unsigned integer or an octal string"; - function parseFileMode(value, name, def) { - if (typeof value === "undefined") { - value = def; - } - if (typeof value === "string") { - if (!RegExpPrototypeTest(octalReg, value)) { - throw new ERR_INVALID_ARG_VALUE(name, value, modeDesc); - } - value = NumberParseInt(value, 8); - } - validateInt32(value, name, 0, 2 ** 32 - 1); - return value; - } - var validateOneOf = hideStackFrames((value, name, oneOf) => { - if (!ArrayPrototypeIncludes(oneOf, value)) { - const allowed = ArrayPrototypeJoin( - ArrayPrototypeMap(oneOf, v => (typeof v === "string" ? `'${v}'` : String2(v))), - ", ", - ); - const reason = "must be one of: " + allowed; - throw new ERR_INVALID_ARG_VALUE(name, value, reason); - } - }); - var validateObject = hideStackFrames((value, name, options) => { - const useDefaultOptions = options == null; - const allowArray = useDefaultOptions ? false : options.allowArray; - const allowFunction = useDefaultOptions ? false : options.allowFunction; - const nullable = useDefaultOptions ? false : options.nullable; - if ( - (!nullable && value === null) || - (!allowArray && $isJSArray(value)) || - (typeof value !== "object" && (!allowFunction || typeof value !== "function")) - ) { - throw new ERR_INVALID_ARG_TYPE(name, "Object", value); - } - }); - module.exports = { - isInt32, - isUint32, - parseFileMode, - validateObject, - validateOneOf, - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/utils.js -var require_utils = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/utils.js"(exports, module) { - "use strict"; - var { Symbol: Symbol2, SymbolAsyncIterator, SymbolIterator } = require_primordials(); - var kDestroyed = Symbol2("kDestroyed"); - var kIsErrored = Symbol2("kIsErrored"); - var kIsReadable = Symbol2("kIsReadable"); - var kIsDisturbed = Symbol2("kIsDisturbed"); - function isReadableNodeStream(obj, strict = false) { - var _obj$_readableState; - return !!( - obj && - typeof obj.pipe === "function" && - typeof obj.on === "function" && - (!strict || (typeof obj.pause === "function" && typeof obj.resume === "function")) && - (!obj._writableState || - ((_obj$_readableState = obj._readableState) === null || _obj$_readableState === void 0 - ? void 0 - : _obj$_readableState.readable) !== false) && - (!obj._writableState || obj._readableState) - ); - } - function isWritableNodeStream(obj) { - var _obj$_writableState; - return !!( - obj && - typeof obj.write === "function" && - typeof obj.on === "function" && - (!obj._readableState || - ((_obj$_writableState = obj._writableState) === null || _obj$_writableState === void 0 - ? void 0 - : _obj$_writableState.writable) !== false) - ); - } - function isDuplexNodeStream(obj) { - return !!( - obj && - typeof obj.pipe === "function" && - obj._readableState && - typeof obj.on === "function" && - typeof obj.write === "function" - ); - } - function isNodeStream(obj) { - return ( - obj && - (obj._readableState || - obj._writableState || - (typeof obj.write === "function" && typeof obj.on === "function") || - (typeof obj.pipe === "function" && typeof obj.on === "function")) - ); - } - function isIterable(obj, isAsync) { - if (obj == null) return false; - if (isAsync === true) return typeof obj[SymbolAsyncIterator] === "function"; - if (isAsync === false) return typeof obj[SymbolIterator] === "function"; - return typeof obj[SymbolAsyncIterator] === "function" || typeof obj[SymbolIterator] === "function"; - } - function isDestroyed(stream) { - if (!isNodeStream(stream)) return null; - const wState = stream._writableState; - const rState = stream._readableState; - const state = wState || rState; - return !!(stream.destroyed || stream[kDestroyed] || (state !== null && state !== void 0 && state.destroyed)); - } - function isWritableEnded(stream) { - if (!isWritableNodeStream(stream)) return null; - if (stream.writableEnded === true) return true; - const wState = stream._writableState; - if (wState !== null && wState !== void 0 && wState.errored) return false; - if (typeof (wState === null || wState === void 0 ? void 0 : wState.ended) !== "boolean") return null; - return wState.ended; - } - function isWritableFinished(stream, strict) { - if (!isWritableNodeStream(stream)) return null; - if (stream.writableFinished === true) return true; - const wState = stream._writableState; - if (wState !== null && wState !== void 0 && wState.errored) return false; - if (typeof (wState === null || wState === void 0 ? void 0 : wState.finished) !== "boolean") return null; - return !!(wState.finished || (strict === false && wState.ended === true && wState.length === 0)); - } - function isReadableEnded(stream) { - if (!isReadableNodeStream(stream)) return null; - if (stream.readableEnded === true) return true; - const rState = stream._readableState; - if (!rState || rState.errored) return false; - if (typeof (rState === null || rState === void 0 ? void 0 : rState.ended) !== "boolean") return null; - return rState.ended; - } - function isReadableFinished(stream, strict?: boolean) { - if (!isReadableNodeStream(stream)) return null; - const rState = stream._readableState; - if (rState !== null && rState !== void 0 && rState.errored) return false; - if (typeof (rState === null || rState === void 0 ? void 0 : rState.endEmitted) !== "boolean") return null; - return !!(rState.endEmitted || (strict === false && rState.ended === true && rState.length === 0)); - } - function isReadable(stream) { - if (stream && stream[kIsReadable] != null) return stream[kIsReadable]; - if (typeof (stream === null || stream === void 0 ? void 0 : stream.readable) !== "boolean") return null; - if (isDestroyed(stream)) return false; - return isReadableNodeStream(stream) && stream.readable && !isReadableFinished(stream); - } - function isWritable(stream) { - if (typeof (stream === null || stream === void 0 ? void 0 : stream.writable) !== "boolean") return null; - if (isDestroyed(stream)) return false; - return isWritableNodeStream(stream) && stream.writable && !isWritableEnded(stream); - } - function isFinished(stream, opts) { - if (!isNodeStream(stream)) { - return null; - } - if (isDestroyed(stream)) { - return true; - } - if ((opts === null || opts === void 0 ? void 0 : opts.readable) !== false && isReadable(stream)) { - return false; - } - if ((opts === null || opts === void 0 ? void 0 : opts.writable) !== false && isWritable(stream)) { - return false; - } - return true; - } - function isWritableErrored(stream) { - var _stream$_writableStat, _stream$_writableStat2; - if (!isNodeStream(stream)) { - return null; - } - if (stream.writableErrored) { - return stream.writableErrored; - } - return (_stream$_writableStat = - (_stream$_writableStat2 = stream._writableState) === null || _stream$_writableStat2 === void 0 - ? void 0 - : _stream$_writableStat2.errored) !== null && _stream$_writableStat !== void 0 - ? _stream$_writableStat - : null; - } - function isReadableErrored(stream) { - var _stream$_readableStat, _stream$_readableStat2; - if (!isNodeStream(stream)) { - return null; - } - if (stream.readableErrored) { - return stream.readableErrored; - } - return (_stream$_readableStat = - (_stream$_readableStat2 = stream._readableState) === null || _stream$_readableStat2 === void 0 - ? void 0 - : _stream$_readableStat2.errored) !== null && _stream$_readableStat !== void 0 - ? _stream$_readableStat - : null; - } - function isClosed(stream) { - if (!isNodeStream(stream)) { - return null; - } - if (typeof stream.closed === "boolean") { - return stream.closed; - } - const wState = stream._writableState; - const rState = stream._readableState; - if ( - typeof (wState === null || wState === void 0 ? void 0 : wState.closed) === "boolean" || - typeof (rState === null || rState === void 0 ? void 0 : rState.closed) === "boolean" - ) { - return ( - (wState === null || wState === void 0 ? void 0 : wState.closed) || - (rState === null || rState === void 0 ? void 0 : rState.closed) - ); - } - if (typeof stream._closed === "boolean" && isOutgoingMessage(stream)) { - return stream._closed; - } - return null; - } - function isOutgoingMessage(stream) { - return ( - typeof stream._closed === "boolean" && - typeof stream._defaultKeepAlive === "boolean" && - typeof stream._removedConnection === "boolean" && - typeof stream._removedContLen === "boolean" - ); - } - function isServerResponse(stream) { - return typeof stream._sent100 === "boolean" && isOutgoingMessage(stream); - } - function isServerRequest(stream) { - var _stream$req; - return ( - typeof stream._consuming === "boolean" && - typeof stream._dumped === "boolean" && - ((_stream$req = stream.req) === null || _stream$req === void 0 ? void 0 : _stream$req.upgradeOrConnect) === - void 0 - ); - } - function willEmitClose(stream) { - if (!isNodeStream(stream)) return null; - const wState = stream._writableState; - const rState = stream._readableState; - const state = wState || rState; - return ( - (!state && isServerResponse(stream)) || - !!(state && state.autoDestroy && state.emitClose && state.closed === false) - ); - } - function isDisturbed(stream) { - var _stream$kIsDisturbed; - return !!( - stream && - ((_stream$kIsDisturbed = stream[kIsDisturbed]) !== null && _stream$kIsDisturbed !== void 0 - ? _stream$kIsDisturbed - : stream.readableDidRead || stream.readableAborted) - ); - } - function isErrored(stream) { - var _ref, - _ref2, - _ref3, - _ref4, - _ref5, - _stream$kIsErrored, - _stream$_readableStat3, - _stream$_writableStat3, - _stream$_readableStat4, - _stream$_writableStat4; - return !!( - stream && - ((_ref = - (_ref2 = - (_ref3 = - (_ref4 = - (_ref5 = - (_stream$kIsErrored = stream[kIsErrored]) !== null && _stream$kIsErrored !== void 0 - ? _stream$kIsErrored - : stream.readableErrored) !== null && _ref5 !== void 0 - ? _ref5 - : stream.writableErrored) !== null && _ref4 !== void 0 - ? _ref4 - : (_stream$_readableStat3 = stream._readableState) === null || _stream$_readableStat3 === void 0 - ? void 0 - : _stream$_readableStat3.errorEmitted) !== null && _ref3 !== void 0 - ? _ref3 - : (_stream$_writableStat3 = stream._writableState) === null || _stream$_writableStat3 === void 0 - ? void 0 - : _stream$_writableStat3.errorEmitted) !== null && _ref2 !== void 0 - ? _ref2 - : (_stream$_readableStat4 = stream._readableState) === null || _stream$_readableStat4 === void 0 - ? void 0 - : _stream$_readableStat4.errored) !== null && _ref !== void 0 - ? _ref - : (_stream$_writableStat4 = stream._writableState) === null || _stream$_writableStat4 === void 0 - ? void 0 - : _stream$_writableStat4.errored) - ); - } - module.exports = { - kDestroyed, - isDisturbed, - kIsDisturbed, - isErrored, - kIsErrored, - isReadable, - kIsReadable, - isClosed, - isDestroyed, - isDuplexNodeStream, - isFinished, - isIterable, - isReadableNodeStream, - isReadableEnded, - isReadableFinished, - isReadableErrored, - isNodeStream, - isWritable, - isWritableNodeStream, - isWritableEnded, - isWritableFinished, - isWritableErrored, - isServerRequest, - isServerResponse, - willEmitClose, - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/end-of-stream.js -var require_end_of_stream = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/end-of-stream.js"(exports, module) { - "use strict"; - var { AbortError, codes } = require_errors(); - var { ERR_INVALID_ARG_TYPE, ERR_STREAM_PREMATURE_CLOSE } = codes; - var { once } = require_util(); - var { validateObject } = require_validators(); - var { Promise: Promise2 } = require_primordials(); - var { - isClosed, - isReadable, - isReadableNodeStream, - isReadableFinished, - isReadableErrored, - isWritable, - isWritableNodeStream, - isWritableFinished, - isWritableErrored, - isNodeStream, - willEmitClose: _willEmitClose, - } = require_utils(); - function isRequest(stream) { - return stream.setHeader && typeof stream.abort === "function"; - } - var nop = () => {}; - function eos(stream, options, callback) { - var _options$readable, _options$writable; - if (arguments.length === 2) { - callback = options; - options = {}; - } else if (options == null) { - options = {}; - } else { - validateObject(options, "options"); - } - validateFunction(callback, "callback"); - validateAbortSignal(options.signal, "options.signal"); - callback = once(callback); - const readable = - (_options$readable = options.readable) !== null && _options$readable !== void 0 - ? _options$readable - : isReadableNodeStream(stream); - const writable = - (_options$writable = options.writable) !== null && _options$writable !== void 0 - ? _options$writable - : isWritableNodeStream(stream); - if (!isNodeStream(stream)) { - throw new ERR_INVALID_ARG_TYPE("stream", "Stream", stream); - } - const wState = stream._writableState; - const rState = stream._readableState; - const onlegacyfinish = () => { - if (!stream.writable) { - onfinish(); - } - }; - let willEmitClose = - _willEmitClose(stream) && - isReadableNodeStream(stream) === readable && - isWritableNodeStream(stream) === writable; - let writableFinished = isWritableFinished(stream, false); - const onfinish = () => { - writableFinished = true; - if (stream.destroyed) { - willEmitClose = false; - } - if (willEmitClose && (!stream.readable || readable)) { - return; - } - if (!readable || readableFinished) { - callback.$call(stream); - } - }; - let readableFinished = isReadableFinished(stream, false); - const onend = () => { - readableFinished = true; - if (stream.destroyed) { - willEmitClose = false; - } - if (willEmitClose && (!stream.writable || writable)) { - return; - } - if (!writable || writableFinished) { - callback.$call(stream); - } - }; - const onerror = err => { - callback.$call(stream, err); - }; - let closed = isClosed(stream); - const onclose = () => { - closed = true; - const errored = isWritableErrored(stream) || isReadableErrored(stream); - if (errored && typeof errored !== "boolean") { - return callback.$call(stream, errored); - } - if (readable && !readableFinished && isReadableNodeStream(stream, true)) { - if (!isReadableFinished(stream, false)) return callback.$call(stream, new ERR_STREAM_PREMATURE_CLOSE()); - } - if (writable && !writableFinished) { - if (!isWritableFinished(stream, false)) return callback.$call(stream, new ERR_STREAM_PREMATURE_CLOSE()); - } - callback.$call(stream); - }; - const onrequest = () => { - stream.req.on("finish", onfinish); - }; - if (isRequest(stream)) { - stream.on("complete", onfinish); - if (!willEmitClose) { - stream.on("abort", onclose); - } - if (stream.req) { - onrequest(); - } else { - stream.on("request", onrequest); - } - } else if (writable && !wState) { - stream.on("end", onlegacyfinish); - stream.on("close", onlegacyfinish); - } - if (!willEmitClose && typeof stream.aborted === "boolean") { - stream.on("aborted", onclose); - } - stream.on("end", onend); - stream.on("finish", onfinish); - if (options.error !== false) { - stream.on("error", onerror); - } - stream.on("close", onclose); - if (closed) { - ProcessNextTick(onclose); - } else if ( - (wState !== null && wState !== void 0 && wState.errorEmitted) || - (rState !== null && rState !== void 0 && rState.errorEmitted) - ) { - if (!willEmitClose) { - ProcessNextTick(onclose); - } - } else if ( - !readable && - (!willEmitClose || isReadable(stream)) && - (writableFinished || isWritable(stream) === false) - ) { - ProcessNextTick(onclose); - } else if ( - !writable && - (!willEmitClose || isWritable(stream)) && - (readableFinished || isReadable(stream) === false) - ) { - ProcessNextTick(onclose); - } else if (rState && stream.req && stream.aborted) { - ProcessNextTick(onclose); - } - const cleanup = () => { - callback = nop; - stream.removeListener("aborted", onclose); - stream.removeListener("complete", onfinish); - stream.removeListener("abort", onclose); - stream.removeListener("request", onrequest); - if (stream.req) stream.req.removeListener("finish", onfinish); - stream.removeListener("end", onlegacyfinish); - stream.removeListener("close", onlegacyfinish); - stream.removeListener("finish", onfinish); - stream.removeListener("end", onend); - stream.removeListener("error", onerror); - stream.removeListener("close", onclose); - }; - if (options.signal && !closed) { - const abort = () => { - const endCallback = callback; - cleanup(); - endCallback.$call( - stream, - new AbortError(void 0, { - cause: options.signal.reason, - }), - ); - }; - if (options.signal.aborted) { - ProcessNextTick(abort); - } else { - const originalCallback = callback; - callback = once((...args) => { - options.signal.removeEventListener("abort", abort); - originalCallback.$apply(stream, args); - }); - options.signal.addEventListener("abort", abort); - } - } - return cleanup; - } - function finished(stream, opts) { - const { promise, resolve, reject } = $newPromiseCapability(Promise); - eos(stream, opts, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - return promise; - } - module.exports = eos; - module.exports.finished = finished; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/operators.js -var require_operators = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/operators.js"(exports, module) { - "use strict"; - var { - codes: { ERR_INVALID_ARG_TYPE, ERR_MISSING_ARGS, ERR_OUT_OF_RANGE }, - AbortError, - } = require_errors(); - var { validateObject } = require_validators(); - var kWeakHandler = require_primordials().Symbol("kWeak"); - var { finished } = require_end_of_stream(); - var { - ArrayPrototypePush, - MathFloor, - Number: Number2, - NumberIsNaN, - Promise: Promise2, - PromiseReject, - PromisePrototypeCatch, - Symbol: Symbol2, - } = require_primordials(); - var kEmpty = Symbol2("kEmpty"); - var kEof = Symbol2("kEof"); - function map(fn, options) { - if (typeof fn !== "function") { - throw new ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); - } - if (options != null) { - validateObject(options, "options"); - } - if ((options === null || options === void 0 ? void 0 : options.signal) != null) { - validateAbortSignal(options.signal, "options.signal"); - } - let concurrency = 1; - if ((options === null || options === void 0 ? void 0 : options.concurrency) != null) { - concurrency = MathFloor(options.concurrency); - } - validateInteger(concurrency, "concurrency", 1); - return async function* map2() { - var _options$signal, _options$signal2; - const ac = new AbortController(); - const stream = this; - const queue = []; - const signal = ac.signal; - const signalOpt = { - signal, - }; - const abort = () => ac.abort(); - if ( - options !== null && - options !== void 0 && - (_options$signal = options.signal) !== null && - _options$signal !== void 0 && - _options$signal.aborted - ) { - abort(); - } - options === null || options === void 0 - ? void 0 - : (_options$signal2 = options.signal) === null || _options$signal2 === void 0 - ? void 0 - : _options$signal2.addEventListener("abort", abort); - let next; - let resume; - let done = false; - function onDone() { - done = true; - } - async function pump() { - try { - for await (let val of stream) { - var _val; - if (done) { - return; - } - if (signal.aborted) { - throw new AbortError(); - } - try { - val = fn(val, signalOpt); - } catch (err) { - val = PromiseReject(err); - } - if (val === kEmpty) { - continue; - } - if (typeof ((_val = val) === null || _val === void 0 ? void 0 : _val.catch) === "function") { - val.catch(onDone); - } - queue.push(val); - if (next) { - next(); - next = null; - } - if (!done && queue.length && queue.length >= concurrency) { - await new Promise2(resolve => { - resume = resolve; - }); - } - } - queue.push(kEof); - } catch (err) { - const val = PromiseReject(err); - PromisePrototypeCatch(val, onDone); - queue.push(val); - } finally { - var _options$signal3; - done = true; - if (next) { - next(); - next = null; - } - options === null || options === void 0 - ? void 0 - : (_options$signal3 = options.signal) === null || _options$signal3 === void 0 - ? void 0 - : _options$signal3.removeEventListener("abort", abort); - } - } - pump(); - try { - while (true) { - while (queue.length > 0) { - const val = await queue[0]; - if (val === kEof) { - return; - } - if (signal.aborted) { - throw new AbortError(); - } - if (val !== kEmpty) { - yield val; - } - queue.shift(); - if (resume) { - resume(); - resume = null; - } - } - await new Promise2(resolve => { - next = resolve; - }); - } - } finally { - ac.abort(); - done = true; - if (resume) { - resume(); - resume = null; - } - } - }.$call(this); - } - function asIndexedPairs(options = void 0) { - if (options != null) { - validateObject(options, "options"); - } - if ((options === null || options === void 0 ? void 0 : options.signal) != null) { - validateAbortSignal(options.signal, "options.signal"); - } - return async function* asIndexedPairs2() { - let index = 0; - for await (const val of this) { - var _options$signal4; - if ( - options !== null && - options !== void 0 && - (_options$signal4 = options.signal) !== null && - _options$signal4 !== void 0 && - _options$signal4.aborted - ) { - throw new AbortError({ - cause: options.signal.reason, - }); - } - yield [index++, val]; - } - }.$call(this); - } - async function some(fn, options = void 0) { - for await (const unused of filter.$call(this, fn, options)) { - return true; - } - return false; - } - async function every(fn, options = void 0) { - if (typeof fn !== "function") { - throw new ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); - } - return !(await some.$call( - this, - async (...args) => { - return !(await fn(...args)); - }, - options, - )); - } - async function find(fn, options) { - for await (const result of filter.$call(this, fn, options)) { - return result; - } - return void 0; - } - async function forEach(fn, options) { - if (typeof fn !== "function") { - throw new ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); - } - async function forEachFn(value, options2) { - await fn(value, options2); - return kEmpty; - } - for await (const unused of map.$call(this, forEachFn, options)); - } - function filter(fn, options) { - if (typeof fn !== "function") { - throw new ERR_INVALID_ARG_TYPE("fn", ["Function", "AsyncFunction"], fn); - } - async function filterFn(value, options2) { - if (await fn(value, options2)) { - return value; - } - return kEmpty; - } - return map.$call(this, filterFn, options); - } - var ReduceAwareErrMissingArgs = class extends ERR_MISSING_ARGS { - constructor() { - super("reduce"); - this.message = "Reduce of an empty stream requires an initial value"; - } - }; - async function reduce(reducer, initialValue, options) { - var _options$signal5; - if (typeof reducer !== "function") { - throw new ERR_INVALID_ARG_TYPE("reducer", ["Function", "AsyncFunction"], reducer); - } - if (options != null) { - validateObject(options, "options"); - } - if ((options === null || options === void 0 ? void 0 : options.signal) != null) { - validateAbortSignal(options.signal, "options.signal"); - } - let hasInitialValue = arguments.length > 1; - if ( - options !== null && - options !== void 0 && - (_options$signal5 = options.signal) !== null && - _options$signal5 !== void 0 && - _options$signal5.aborted - ) { - const err = new AbortError(void 0, { - cause: options.signal.reason, - }); - this.once("error", () => {}); - await finished(this.destroy(err)); - throw err; - } - const ac = new AbortController(); - const signal = ac.signal; - if (options !== null && options !== void 0 && options.signal) { - const opts = { - once: true, - [kWeakHandler]: this, - }; - options.signal.addEventListener("abort", () => ac.abort(), opts); - } - let gotAnyItemFromStream = false; - try { - for await (const value of this) { - var _options$signal6; - gotAnyItemFromStream = true; - if ( - options !== null && - options !== void 0 && - (_options$signal6 = options.signal) !== null && - _options$signal6 !== void 0 && - _options$signal6.aborted - ) { - throw new AbortError(); - } - if (!hasInitialValue) { - initialValue = value; - hasInitialValue = true; - } else { - initialValue = await reducer(initialValue, value, { - signal, - }); - } - } - if (!gotAnyItemFromStream && !hasInitialValue) { - throw new ReduceAwareErrMissingArgs(); - } - } finally { - ac.abort(); - } - return initialValue; - } - async function toArray(options) { - if (options != null) { - validateObject(options, "options"); - } - if ((options === null || options === void 0 ? void 0 : options.signal) != null) { - validateAbortSignal(options.signal, "options.signal"); - } - const result = []; - for await (const val of this) { - var _options$signal7; - if ( - options !== null && - options !== void 0 && - (_options$signal7 = options.signal) !== null && - _options$signal7 !== void 0 && - _options$signal7.aborted - ) { - throw new AbortError(void 0, { - cause: options.signal.reason, - }); - } - ArrayPrototypePush(result, val); - } - return result; - } - function flatMap(fn, options) { - const values = map.$call(this, fn, options); - return async function* flatMap2() { - for await (const val of values) { - yield* val; - } - }.$call(this); - } - function toIntegerOrInfinity(number) { - number = Number2(number); - if (NumberIsNaN(number)) { - return 0; - } - if (number < 0) { - throw new ERR_OUT_OF_RANGE("number", ">= 0", number); - } - return number; - } - function drop(number, options = void 0) { - if (options != null) { - validateObject(options, "options"); - } - if ((options === null || options === void 0 ? void 0 : options.signal) != null) { - validateAbortSignal(options.signal, "options.signal"); - } - number = toIntegerOrInfinity(number); - return async function* drop2() { - var _options$signal8; - if ( - options !== null && - options !== void 0 && - (_options$signal8 = options.signal) !== null && - _options$signal8 !== void 0 && - _options$signal8.aborted - ) { - throw new AbortError(); - } - for await (const val of this) { - var _options$signal9; - if ( - options !== null && - options !== void 0 && - (_options$signal9 = options.signal) !== null && - _options$signal9 !== void 0 && - _options$signal9.aborted - ) { - throw new AbortError(); - } - if (number-- <= 0) { - yield val; - } - } - }.$call(this); - } - function take(number, options = void 0) { - if (options != null) { - validateObject(options, "options"); - } - if ((options === null || options === void 0 ? void 0 : options.signal) != null) { - validateAbortSignal(options.signal, "options.signal"); - } - number = toIntegerOrInfinity(number); - return async function* take2() { - var _options$signal10; - if ( - options !== null && - options !== void 0 && - (_options$signal10 = options.signal) !== null && - _options$signal10 !== void 0 && - _options$signal10.aborted - ) { - throw new AbortError(); - } - for await (const val of this) { - var _options$signal11; - if ( - options !== null && - options !== void 0 && - (_options$signal11 = options.signal) !== null && - _options$signal11 !== void 0 && - _options$signal11.aborted - ) { - throw new AbortError(); - } - if (number-- > 0) { - yield val; - } else { - return; - } - } - }.$call(this); - } - module.exports.streamReturningOperators = { - asIndexedPairs, - drop, - filter, - flatMap, - map, - take, - }; - module.exports.promiseReturningOperators = { - every, - forEach, - reduce, - toArray, - some, - find, - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/destroy.js -var require_destroy = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/destroy.js"(exports, module) { - "use strict"; - var { - aggregateTwoErrors, - codes: { ERR_MULTIPLE_CALLBACK }, - AbortError, - } = require_errors(); - var { Symbol: Symbol2 } = require_primordials(); - var { kDestroyed, isDestroyed, isFinished, isServerRequest } = require_utils(); - var kDestroy = Symbol.for("kDestroy"); - var kConstruct = Symbol.for("kConstruct"); - function checkError(err, w, r) { - if (err) { - err.stack; - if (w && !w.errored) { - w.errored = err; - } - if (r && !r.errored) { - r.errored = err; - } - } - } - function destroy(err, cb) { - const r = this._readableState; - const w = this._writableState; - const s = w || r; - if ((w && w.destroyed) || (r && r.destroyed)) { - if (typeof cb === "function") { - cb(); - } - return this; - } - checkError(err, w, r); - if (w) { - w.destroyed = true; - } - if (r) { - r.destroyed = true; - } - if (!s.constructed) { - this.once(kDestroy, er => { - _destroy(this, aggregateTwoErrors(er, err), cb); - }); - } else { - _destroy(this, err, cb); - } - return this; - } - function _destroy(self, err, cb) { - let called = false; - function onDestroy(err2) { - if (called) { - return; - } - called = true; - const r = self._readableState; - const w = self._writableState; - checkError(err2, w, r); - if (w) { - w.closed = true; - } - if (r) { - r.closed = true; - } - if (typeof cb === "function") { - cb(err2); - } - if (err2) { - ProcessNextTick(emitErrorCloseNT, self, err2); - } else { - ProcessNextTick(emitCloseNT, self); - } - } - try { - self._destroy(err || null, onDestroy); - } catch (err2) { - onDestroy(err2); - } - } - function emitErrorCloseNT(self, err) { - emitErrorNT(self, err); - emitCloseNT(self); - } - function emitCloseNT(self) { - const r = self._readableState; - const w = self._writableState; - if (w) { - w.closeEmitted = true; - } - if (r) { - r.closeEmitted = true; - } - if ((w && w.emitClose) || (r && r.emitClose)) { - self.emit("close"); - } - } - function emitErrorNT(self, err) { - const r = self?._readableState; - const w = self?._writableState; - if (w?.errorEmitted || r?.errorEmitted) { - return; - } - if (w) { - w.errorEmitted = true; - } - if (r) { - r.errorEmitted = true; - } - self?.emit?.("error", err); - } - function undestroy() { - const r = this._readableState; - const w = this._writableState; - if (r) { - r.constructed = true; - r.closed = false; - r.closeEmitted = false; - r.destroyed = false; - r.errored = null; - r.errorEmitted = false; - r.reading = false; - r.ended = r.readable === false; - r.endEmitted = r.readable === false; - } - if (w) { - w.constructed = true; - w.destroyed = false; - w.closed = false; - w.closeEmitted = false; - w.errored = null; - w.errorEmitted = false; - w.finalCalled = false; - w.prefinished = false; - w.ended = w.writable === false; - w.ending = w.writable === false; - w.finished = w.writable === false; - } - } - function errorOrDestroy(stream, err, sync?: boolean) { - const r = stream?._readableState; - const w = stream?._writableState; - if ((w && w.destroyed) || (r && r.destroyed)) { - return this; - } - if ((r && r.autoDestroy) || (w && w.autoDestroy)) stream.destroy(err); - else if (err) { - Error.captureStackTrace(err); - if (w && !w.errored) { - w.errored = err; - } - if (r && !r.errored) { - r.errored = err; - } - if (sync) { - ProcessNextTick(emitErrorNT, stream, err); - } else { - emitErrorNT(stream, err); - } - } - } - function construct(stream, cb) { - if (typeof stream._construct !== "function") { - return; - } - const r = stream._readableState; - const w = stream._writableState; - if (r) { - r.constructed = false; - } - if (w) { - w.constructed = false; - } - stream.once(kConstruct, cb); - if (stream.listenerCount(kConstruct) > 1) { - return; - } - ProcessNextTick(constructNT, stream); - } - function constructNT(stream) { - let called = false; - function onConstruct(err) { - if (called) { - errorOrDestroy(stream, err !== null && err !== void 0 ? err : new ERR_MULTIPLE_CALLBACK()); - return; - } - called = true; - const r = stream._readableState; - const w = stream._writableState; - const s = w || r; - if (r) { - r.constructed = true; - } - if (w) { - w.constructed = true; - } - if (s.destroyed) { - stream.emit(kDestroy, err); - } else if (err) { - errorOrDestroy(stream, err, true); - } else { - ProcessNextTick(emitConstructNT, stream); - } - } - try { - stream._construct(onConstruct); - } catch (err) { - onConstruct(err); - } - } - function emitConstructNT(stream) { - stream.emit(kConstruct); - } - function isRequest(stream) { - return stream && stream.setHeader && typeof stream.abort === "function"; - } - function emitCloseLegacy(stream) { - stream.emit("close"); - } - function emitErrorCloseLegacy(stream, err) { - stream.emit("error", err); - ProcessNextTick(emitCloseLegacy, stream); - } - function destroyer(stream, err) { - if (!stream || isDestroyed(stream)) { - return; - } - if (!err && !isFinished(stream)) { - err = new AbortError(); - } - if (isServerRequest(stream)) { - stream.socket = null; - stream.destroy(err); - } else if (isRequest(stream)) { - stream.abort(); - } else if (isRequest(stream.req)) { - stream.req.abort(); - } else if (typeof stream.destroy === "function") { - stream.destroy(err); - } else if (typeof stream.close === "function") { - stream.close(); - } else if (err) { - ProcessNextTick(emitErrorCloseLegacy, stream); - } else { - ProcessNextTick(emitCloseLegacy, stream); - } - if (!stream.destroyed) { - stream[kDestroyed] = true; - } - } - module.exports = { - construct, - destroyer, - destroy, - undestroy, - errorOrDestroy, - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/legacy.js -var require_legacy = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/legacy.js"(exports, module) { - "use strict"; - var { ObjectSetPrototypeOf } = require_primordials(); - - function Stream(options) { - if (!(this instanceof Stream)) return new Stream(options); - EE.$call(this, options); - } - Stream.prototype = {}; - ObjectSetPrototypeOf(Stream.prototype, EE.prototype); - Stream.prototype.constructor = Stream; // Re-add constructor which got lost when setting prototype - ObjectSetPrototypeOf(Stream, EE); - - Stream.prototype.pipe = function (dest, options) { - const source = this; - function ondata(chunk) { - if (dest.writable && dest.write(chunk) === false && source.pause) { - source.pause(); - } - } - source.on("data", ondata); - function ondrain() { - if (source.readable && source.resume) { - source.resume(); - } - } - dest.on("drain", ondrain); - if (!dest._isStdio && (!options || options.end !== false)) { - source.on("end", onend); - source.on("close", onclose); - } - let didOnEnd = false; - function onend() { - if (didOnEnd) return; - didOnEnd = true; - dest.end(); - } - function onclose() { - if (didOnEnd) return; - didOnEnd = true; - if (typeof dest.destroy === "function") dest.destroy(); - } - function onerror(er) { - cleanup(); - if (EE.listenerCount(this, "error") === 0) { - this.emit("error", er); - } - } - prependListener(source, "error", onerror); - prependListener(dest, "error", onerror); - function cleanup() { - source.removeListener("data", ondata); - dest.removeListener("drain", ondrain); - source.removeListener("end", onend); - source.removeListener("close", onclose); - source.removeListener("error", onerror); - dest.removeListener("error", onerror); - source.removeListener("end", cleanup); - source.removeListener("close", cleanup); - dest.removeListener("close", cleanup); - } - source.on("end", cleanup); - source.on("close", cleanup); - dest.on("close", cleanup); - dest.emit("pipe", source); - return dest; - }; - function prependListener(emitter, event, fn) { - if (typeof emitter.prependListener === "function") return emitter.prependListener(event, fn); - if (!emitter._events || !emitter._events[event]) emitter.on(event, fn); - else if ($isJSArray(emitter._events[event])) emitter._events[event].unshift(fn); - else emitter._events[event] = [fn, emitter._events[event]]; - } - module.exports = { - Stream, - prependListener, - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/add-abort-signal.js -var require_add_abort_signal = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/add-abort-signal.js"(exports, module) { - "use strict"; - var { AbortError, codes } = require_errors(); - var eos = require_end_of_stream(); - var { ERR_INVALID_ARG_TYPE } = codes; - function isNodeStream(obj) { - return !!(obj && typeof obj.pipe === "function"); - } - module.exports.addAbortSignal = function addAbortSignal(signal, stream) { - validateAbortSignal(signal, "signal"); - if (!isNodeStream(stream)) { - throw new ERR_INVALID_ARG_TYPE("stream", "stream.Stream", stream); - } - return module.exports.addAbortSignalNoValidate(signal, stream); - }; - module.exports.addAbortSignalNoValidate = function (signal, stream) { - if (typeof signal !== "object" || !("aborted" in signal)) { - return stream; - } - const onAbort = () => { - stream.destroy( - new AbortError(void 0, { - cause: signal.reason, - }), - ); - }; - if (signal.aborted) { - onAbort(); - } else { - signal.addEventListener("abort", onAbort); - eos(stream, () => signal.removeEventListener("abort", onAbort)); - } - return stream; - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/state.js -var { MathFloor, NumberIsInteger } = require_primordials(); -function highWaterMarkFrom(options, isDuplex, duplexKey) { - return options.highWaterMark != null ? options.highWaterMark : isDuplex ? options[duplexKey] : null; -} - -let hwm_object = 16; -let hwm_bytes = 16 * 1024; - -function getDefaultHighWaterMark(objectMode) { - return objectMode ? hwm_object : hwm_bytes; -} - -function setDefaultHighWaterMark(objectMode, value) { - if (objectMode) { - hwm_object = value; - } else { - hwm_bytes = value; - } -} - -function getHighWaterMark(state, options, duplexKey, isDuplex) { - const hwm = highWaterMarkFrom(options, isDuplex, duplexKey); - if (hwm != null) { - if (!NumberIsInteger(hwm) || hwm < 0) { - const name = isDuplex ? `options.${duplexKey}` : "options.highWaterMark"; - throw new ERR_INVALID_ARG_VALUE(name, hwm); - } - return MathFloor(hwm); - } - return getDefaultHighWaterMark(state.objectMode); -} - -// node_modules/readable-stream/lib/internal/streams/from.js -var require_from = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/from.js"(exports, module) { - "use strict"; - var { PromisePrototypeThen, SymbolAsyncIterator, SymbolIterator } = require_primordials(); - var { ERR_INVALID_ARG_TYPE, ERR_STREAM_NULL_VALUES } = require_errors().codes; - function from(Readable, iterable, opts) { - let iterator; - if (typeof iterable === "string" || iterable instanceof Buffer) { - return new Readable({ - objectMode: true, - ...opts, - read() { - this.push(iterable); - this.push(null); - }, - }); - } - let isAsync; - if (iterable && iterable[SymbolAsyncIterator]) { - isAsync = true; - iterator = iterable[SymbolAsyncIterator](); - } else if (iterable && iterable[SymbolIterator]) { - isAsync = false; - iterator = iterable[SymbolIterator](); - } else { - throw new ERR_INVALID_ARG_TYPE("iterable", ["Iterable"], iterable); - } - const readable = new Readable({ - objectMode: true, - highWaterMark: 1, - ...opts, - }); - let reading = false; - readable._read = function () { - if (!reading) { - reading = true; - next(); - } - }; - readable._destroy = function (error, cb) { - PromisePrototypeThen( - close(error), - () => ProcessNextTick(cb, error), - e => ProcessNextTick(cb, e || error), - ); - }; - async function close(error) { - const hadError = error !== void 0 && error !== null; - const hasThrow = typeof iterator.throw === "function"; - if (hadError && hasThrow) { - const { value, done } = await iterator.throw(error); - await value; - if (done) { - return; - } - } - if (typeof iterator.return === "function") { - const { value } = await iterator.return(); - await value; - } - } - async function next() { - for (;;) { - try { - const { value, done } = isAsync ? await iterator.next() : iterator.next(); - if (done) { - readable.push(null); - } else { - const res = value && typeof value.then === "function" ? await value : value; - if (res === null) { - reading = false; - throw new ERR_STREAM_NULL_VALUES(); - } else if (readable.push(res)) { - continue; - } else { - reading = false; - } - } - } catch (err) { - readable.destroy(err); - } - break; - } - } - return readable; - } - module.exports = from; - }, -}); - -var _ReadableFromWeb; -var _ReadableFromWebForUndici; - -// node_modules/readable-stream/lib/internal/streams/readable.js -var require_readable = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/readable.js"(exports, module) { - "use strict"; - var { - ArrayPrototypeIndexOf, - NumberIsInteger, - NumberIsNaN, - NumberParseInt, - ObjectDefineProperties, - ObjectKeys, - ObjectSetPrototypeOf, - Promise: Promise2, - SafeSet, - SymbolAsyncIterator, - Promise, - SymbolAsyncDispose, - Symbol, - } = require_primordials(); - - var { Stream, prependListener } = require_legacy(); - - const BufferList = $cpp("JSBufferList.cpp", "getBufferList"); - - const { AbortError } = require_errors(); - - // TODO(benjamingr) it is likely slower to do it this way than with free functions - function makeBitMapDescriptor(bit) { - return { - enumerable: false, - get() { - return (this.state & bit) !== 0; - }, - set(value) { - if (value) this.state |= bit; - else this.state &= ~bit; - }, - }; - } - function ReadableState(options, stream, isDuplex) { - // Duplex streams are both readable and writable, but share - // the same options object. - // However, some cases require setting options to different - // values for the readable and the writable sides of the duplex stream. - // These options can be provided separately as readableXXX and writableXXX. - if (typeof isDuplex !== "boolean") isDuplex = stream instanceof require_duplex(); - - // Bit map field to store ReadableState more effciently with 1 bit per field - // instead of a V8 slot per field. - this.state = kEmitClose | kAutoDestroy | kConstructed | kSync; - // Object stream flag. Used to make read(n) ignore n and to - // make all the buffer merging and length checks go away. - if (options && options.objectMode) this.state |= kObjectMode; - if (isDuplex && options && options.readableObjectMode) this.state |= kObjectMode; - - // The point at which it stops calling _read() to fill the buffer - // Note: 0 is a valid value, means "don't call _read preemptively ever" - this.highWaterMark = options - ? getHighWaterMark(this, options, "readableHighWaterMark", isDuplex) - : getDefaultHighWaterMark(false); - - // A linked list is used to store data chunks instead of an array because the - // linked list can remove elements from the beginning faster than - // array.shift(). - this.buffer = new BufferList(); - this.length = 0; - this.pipes = []; - this.flowing = null; - this[kPaused] = null; - - // Should close be emitted on destroy. Defaults to true. - if (options && options.emitClose === false) this.state &= ~kEmitClose; - - // Should .destroy() be called after 'end' (and potentially 'finish'). - if (options && options.autoDestroy === false) this.state &= ~kAutoDestroy; - - // Indicates whether the stream has errored. When true no further - // _read calls, 'data' or 'readable' events should occur. This is needed - // since when autoDestroy is disabled we need a way to tell whether the - // stream has failed. - this.errored = null; - - // Crypto is kind of old and crusty. Historically, its default string - // encoding is 'binary' so we have to make this configurable. - // Everything else in the universe uses 'utf8', though. - this.defaultEncoding = (options && options.defaultEncoding) || "utf8"; - - // Ref the piped dest which we need a drain event on it - // type: null | Writable | Set. - this.awaitDrainWriters = null; - this.decoder = null; - this.encoding = null; - if (options && options.encoding) { - this.decoder = new StringDecoder(options.encoding); - this.encoding = options.encoding; - } - } - ReadableState.prototype = {}; - ObjectDefineProperties(ReadableState.prototype, { - objectMode: makeBitMapDescriptor(kObjectMode), - ended: makeBitMapDescriptor(kEnded), - endEmitted: makeBitMapDescriptor(kEndEmitted), - reading: makeBitMapDescriptor(kReading), - // Stream is still being constructed and cannot be - // destroyed until construction finished or failed. - // Async construction is opt in, therefore we start as - // constructed. - constructed: makeBitMapDescriptor(kConstructed), - // A flag to be able to tell if the event 'readable'/'data' is emitted - // immediately, or on a later tick. We set this to true at first, because - // any actions that shouldn't happen until "later" should generally also - // not happen before the first read call. - sync: makeBitMapDescriptor(kSync), - // Whenever we return null, then we set a flag to say - // that we're awaiting a 'readable' event emission. - needReadable: makeBitMapDescriptor(kNeedReadable), - emittedReadable: makeBitMapDescriptor(kEmittedReadable), - readableListening: makeBitMapDescriptor(kReadableListening), - resumeScheduled: makeBitMapDescriptor(kResumeScheduled), - // True if the error was already emitted and should not be thrown again. - errorEmitted: makeBitMapDescriptor(kErrorEmitted), - emitClose: makeBitMapDescriptor(kEmitClose), - autoDestroy: makeBitMapDescriptor(kAutoDestroy), - // Has it been destroyed. - destroyed: makeBitMapDescriptor(kDestroyed), - // Indicates whether the stream has finished destroying. - closed: makeBitMapDescriptor(kClosed), - // True if close has been emitted or would have been emitted - // depending on emitClose. - closeEmitted: makeBitMapDescriptor(kCloseEmitted), - multiAwaitDrain: makeBitMapDescriptor(kMultiAwaitDrain), - // If true, a maybeReadMore has been scheduled. - readingMore: makeBitMapDescriptor(kReadingMore), - dataEmitted: makeBitMapDescriptor(kDataEmitted), - }); - - function Readable(options) { - if (!(this instanceof Readable)) return new Readable(options); - const isDuplex = this instanceof require_duplex(); - - this._readableState = new ReadableState(options, this, isDuplex); - if (options) { - const { read, destroy, construct, signal } = options; - if (typeof read === "function") this._read = read; - if (typeof destroy === "function") this._destroy = destroy; - if (typeof construct === "function") this._construct = construct; - if (signal && !isDuplex) addAbortSignal(signal, this); - } - Stream.$call(this, options); - - destroyImpl.construct(this, () => { - if (this._readableState.needReadable) { - maybeReadMore(this, this._readableState); - } - }); - } - Readable.prototype = {}; - ObjectSetPrototypeOf(Readable.prototype, Stream.prototype); - Readable.prototype.constructor = Readable; // Re-add constructor which got lost when setting prototype - ObjectSetPrototypeOf(Readable, Stream); - Readable.ReadableState = ReadableState; - - Readable.prototype.on = function (ev, fn) { - const res = Stream.prototype.on.$call(this, ev, fn); - const state = this._readableState; - if (ev === "data") { - state.readableListening = this.listenerCount("readable") > 0; - if (state.flowing !== false) { - $debug("in flowing mode!", this.__id); - this.resume(); - } else { - $debug("in readable mode!", this.__id); - } - } else if (ev === "readable") { - $debug("readable listener added!", this.__id); - if (!state.endEmitted && !state.readableListening) { - state.readableListening = state.needReadable = true; - state.flowing = false; - state.emittedReadable = false; - $debug( - "on readable - state.length, reading, emittedReadable", - state.length, - state.reading, - state.emittedReadable, - this.__id, - ); - if (state.length) { - emitReadable(this); - } else if (!state.reading) { - ProcessNextTick(nReadingNextTick, this); - } - } else if (state.endEmitted) { - $debug("end already emitted...", this.__id); - } - } - return res; - }; - - class ReadableFromWeb extends Readable { - #reader; - #closed; - #pendingChunks; - #stream; - - constructor(options, stream) { - const { objectMode, highWaterMark, encoding, signal } = options; - super({ - objectMode, - highWaterMark, - encoding, - signal, - }); - this.#pendingChunks = []; - this.#reader = undefined; - this.#stream = stream; - this.#closed = false; - } - - #drainPending() { - var pendingChunks = this.#pendingChunks, - pendingChunksI = 0, - pendingChunksCount = pendingChunks.length; - - for (; pendingChunksI < pendingChunksCount; pendingChunksI++) { - const chunk = pendingChunks[pendingChunksI]; - pendingChunks[pendingChunksI] = undefined; - if (!this.push(chunk, undefined)) { - this.#pendingChunks = pendingChunks.slice(pendingChunksI + 1); - return true; - } - } - - if (pendingChunksCount > 0) { - this.#pendingChunks = []; - } - - return false; - } - - #handleDone(reader) { - reader.releaseLock(); - this.#reader = undefined; - this.#closed = true; - this.push(null); - return; - } - - async _read() { - $debug("ReadableFromWeb _read()", this.__id); - var stream = this.#stream, - reader = this.#reader; - if (stream) { - reader = this.#reader = stream.getReader(); - this.#stream = undefined; - } else if (this.#drainPending()) { - return; - } - - var deferredError; - try { - do { - var done = false, - value; - const firstResult = reader.readMany(); - - if ($isPromise(firstResult)) { - ({ done, value } = await firstResult); - - if (this.#closed) { - this.#pendingChunks.push(...value); - return; - } - } else { - ({ done, value } = firstResult); - } - - if (done) { - this.#handleDone(reader); - return; - } - - if (!this.push(value[0])) { - this.#pendingChunks = value.slice(1); - return; - } - - for (let i = 1, count = value.length; i < count; i++) { - if (!this.push(value[i])) { - this.#pendingChunks = value.slice(i + 1); - return; - } - } - } while (!this.#closed); - } catch (e) { - deferredError = e; - } finally { - if (deferredError) throw deferredError; - } - } - - _destroy(error, callback) { - if (!this.#closed) { - var reader = this.#reader; - if (reader) { - this.#reader = undefined; - reader.cancel(error).finally(() => { - this.#closed = true; - callback(error); - }); - } - - return; - } - try { - callback(error); - } catch (error) { - globalThis.reportError(error); - } - } - } - - _ReadableFromWebForUndici = ReadableFromWeb; - - /** - * @param {ReadableStream} readableStream - * @param {{ - * highWaterMark? : number, - * encoding? : string, - * objectMode? : boolean, - * signal? : AbortSignal, - * }} [options] - * @returns {Readable} - */ - function newStreamReadableFromReadableStream(readableStream, options = {}) { - if (!isReadableStream(readableStream)) { - throw new ERR_INVALID_ARG_TYPE("readableStream", "ReadableStream", readableStream); - } - - validateObject(options, "options"); - const { - highWaterMark, - encoding, - objectMode = false, - signal, - // native = true, - } = options; - - if (encoding !== undefined && !Buffer.isEncoding(encoding)) - throw new ERR_INVALID_ARG_VALUE(encoding, "options.encoding"); - validateBoolean(objectMode, "options.objectMode"); - - // validateBoolean(native, "options.native"); - - // if (!native) { - // return new ReadableFromWeb( - // { - // highWaterMark, - // encoding, - // objectMode, - // signal, - // }, - // readableStream, - // ); - // } - - const nativeStream = getNativeReadableStream(Readable, readableStream, options); - - return ( - nativeStream || - new ReadableFromWeb( - { - highWaterMark, - encoding, - objectMode, - signal, - }, - readableStream, - ) - ); - } - - module.exports = Readable; - _ReadableFromWeb = newStreamReadableFromReadableStream; - - var { addAbortSignal } = require_add_abort_signal(); - var eos = require_end_of_stream(); - // function maybeReadMore(stream, state) { - // ProcessNextTick(_maybeReadMore, stream, state); - // } - - function maybeReadMore(stream, state) { - if (!state.readingMore && state.constructed) { - state.readingMore = true; - process.nextTick(maybeReadMore_, stream, state); - } - } - function maybeReadMore_(stream, state) { - // Attempt to read more data if we should. - // - // The conditions for reading more data are (one of): - // - Not enough data buffered (state.length < state.highWaterMark). The loop - // is responsible for filling the buffer with enough data if such data - // is available. If highWaterMark is 0 and we are not in the flowing mode - // we should _not_ attempt to buffer any extra data. We'll get more data - // when the stream consumer calls read() instead. - // - No data in the buffer, and the stream is in flowing mode. In this mode - // the loop below is responsible for ensuring read() is called. Failing to - // call read here would abort the flow and there's no other mechanism for - // continuing the flow if the stream consumer has just subscribed to the - // 'data' event. - // - // In addition to the above conditions to keep reading data, the following - // conditions prevent the data from being read: - // - The stream has ended (state.ended). - // - There is already a pending 'read' operation (state.reading). This is a - // case where the stream has called the implementation defined _read() - // method, but they are processing the call asynchronously and have _not_ - // called push() with new data. In this case we skip performing more - // read()s. The execution ends in this method again after the _read() ends - // up calling push() with more data. - while ( - !state.reading && - !state.ended && - (state.length < state.highWaterMark || (state.flowing && state.length === 0)) - ) { - const len = state.length; - stream.read(0); - if (len === state.length) - // Didn't get any data, stop spinning. - break; - } - state.readingMore = false; - } - - function emitReadable(stream) { - const state = stream._readableState; - $debug("emitReadable", state.needReadable, state.emittedReadable); - state.needReadable = false; - if (!state.emittedReadable) { - $debug("emitReadable", state.flowing); - state.emittedReadable = true; - process.nextTick(emitReadable_, stream); - } - } - function emitReadable_(stream) { - const state = stream._readableState; - $debug("emitReadable_", state.destroyed, state.length, state.ended); - if (!state.destroyed && !state.errored && (state.length || state.ended)) { - stream.emit("readable"); - state.emittedReadable = false; - } - - // The stream needs another readable event if: - // 1. It is not flowing, as the flow mechanism will take - // care of it. - // 2. It is not ended. - // 3. It is below the highWaterMark, so we can schedule - // another readable later. - state.needReadable = !state.flowing && !state.ended && state.length <= state.highWaterMark; - flow(stream); - } - - var destroyImpl = require_destroy(); - var { - aggregateTwoErrors, - codes: { - ERR_INVALID_ARG_TYPE, - ERR_METHOD_NOT_IMPLEMENTED, - ERR_OUT_OF_RANGE, - ERR_STREAM_PUSH_AFTER_EOF, - ERR_STREAM_UNSHIFT_AFTER_END_EVENT, - }, - } = require_errors(); - var { validateObject } = require_validators(); - var from = require_from(); - var nop = () => {}; - var { errorOrDestroy } = destroyImpl; - - Readable.prototype.destroy = destroyImpl.destroy; - Readable.prototype._undestroy = destroyImpl.undestroy; - Readable.prototype._destroy = function (err, cb) { - cb(err); - }; - Readable.prototype[EE.captureRejectionSymbol] = function (err) { - this.destroy(err); - }; - Readable.prototype.push = function (chunk, encoding) { - return readableAddChunk(this, chunk, encoding, false); - }; - Readable.prototype.unshift = function (chunk, encoding) { - return readableAddChunk(this, chunk, encoding, true); - }; - function readableAddChunk(stream, chunk, encoding, addToFront) { - $debug("readableAddChunk", chunk, stream.__id); - const state = stream._readableState; - let err; - if (!state.objectMode) { - if (typeof chunk === "string") { - encoding = encoding || state.defaultEncoding; - if (state.encoding !== encoding) { - if (addToFront && state.encoding) { - chunk = Buffer.from(chunk, encoding).toString(state.encoding); - } else { - chunk = Buffer.from(chunk, encoding); - encoding = ""; - } - } - } else if (chunk instanceof Buffer) { - encoding = ""; - } else if (Stream._isUint8Array(chunk)) { - if (addToFront || !state.decoder) { - chunk = Stream._uint8ArrayToBuffer(chunk); - } - encoding = ""; - } else if (chunk != null) { - err = new ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "Uint8Array"], chunk); - } - } - if (err) { - errorOrDestroy(stream, err); - } else if (chunk === null) { - state.reading = false; - onEofChunk(stream, state); - } else if (state.objectMode || (chunk && chunk.length > 0)) { - if (addToFront) { - if (state.endEmitted) errorOrDestroy(stream, new ERR_STREAM_UNSHIFT_AFTER_END_EVENT()); - else if (state.destroyed || state.errored) return false; - else addChunk(stream, state, chunk, true); - } else if (state.ended) { - errorOrDestroy(stream, new ERR_STREAM_PUSH_AFTER_EOF()); - } else if (state.destroyed || state.errored) { - return false; - } else { - state.reading = false; - if (state.decoder && !encoding) { - chunk = state.decoder.write(chunk); - if (state.objectMode || chunk.length !== 0) addChunk(stream, state, chunk, false); - else maybeReadMore(stream, state); - } else { - addChunk(stream, state, chunk, false); - } - } - } else if (!addToFront) { - state.reading = false; - maybeReadMore(stream, state); - } - return !state.ended && (state.length < state.highWaterMark || state.length === 0); - } - function addChunk(stream, state, chunk, addToFront) { - $debug("adding chunk", stream.__id); - $debug("chunk", chunk.toString(), stream.__id); - if (state.flowing && state.length === 0 && !state.sync && stream.listenerCount("data") > 0) { - if (state.multiAwaitDrain) { - state.awaitDrainWriters.clear(); - } else { - state.awaitDrainWriters = null; - } - state.dataEmitted = true; - stream.emit("data", chunk); - } else { - state.length += state.objectMode ? 1 : chunk.length; - if (addToFront) state.buffer.unshift(chunk); - else state.buffer.push(chunk); - $debug("needReadable @ addChunk", state.needReadable, stream.__id); - if (state.needReadable) emitReadable(stream); - } - $debug("about to maybereadmore"); - maybeReadMore(stream, state); - } - function onEofChunk(stream, state) { - if (state.ended) return; - - const decoder = state.decoder; - if (decoder) { - const chunk = decoder.end(); - const chunkLength = chunk?.length; - if (chunkLength) { - state.buffer.push(chunk); - state.length += state.objectMode ? 1 : chunkLength; - } - } - state.ended = true; - if (state.sync) { - emitReadable(stream); - } else { - state.needReadable = false; - state.emittedReadable = true; - emitReadable_(stream); - } - } - Readable.prototype.isPaused = function () { - const state = this._readableState; - return state.paused === true || state.flowing === false; - }; - Readable.prototype.setEncoding = function (enc) { - const decoder = new StringDecoder(enc); - this._readableState.decoder = decoder; - this._readableState.encoding = this._readableState.decoder.encoding; - const buffer = this._readableState.buffer; - let content = ""; - // BufferList does not support iterator now, and iterator is slow in JSC. - // for (const data of buffer) { - // content += decoder.write(data); - // } - // buffer.clear(); - for (let i = buffer.length; i > 0; i--) { - content += decoder.write(buffer.shift()); - } - if (content !== "") buffer.push(content); - this._readableState.length = content.length; - return this; - }; - var MAX_HWM = 1073741824; - function computeNewHighWaterMark(n) { - if (n > MAX_HWM) { - throw new ERR_OUT_OF_RANGE("size", "<= 1GiB", n); - } else { - n--; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - n++; - } - return n; - } - function howMuchToRead(n, state) { - if (n <= 0 || (state.length === 0 && state.ended)) return 0; - if (state.objectMode) return 1; - if (NumberIsNaN(n)) { - if (state.flowing && state.length) return state.buffer.first().length; - return state.length; - } - if (n <= state.length) return n; - return state.ended ? state.length : 0; - } - // You can override either this method, or the async _read(n) below. - Readable.prototype.read = function (n) { - $debug("read - n =", n, this.__id); - if (!NumberIsInteger(n)) { - n = NumberParseInt(n, 10); - } - const state = this._readableState; - const nOrig = n; - - // If we're asking for more than the current hwm, then raise the hwm. - if (n > state.highWaterMark) state.highWaterMark = computeNewHighWaterMark(n); - - if (n !== 0) state.emittedReadable = false; - - // If we're doing read(0) to trigger a readable event, but we - // already have a bunch of data in the buffer, then just trigger - // the 'readable' event and move on. - if ( - n === 0 && - state.needReadable && - ((state.highWaterMark !== 0 ? state.length >= state.highWaterMark : state.length > 0) || state.ended) - ) { - $debug("read: emitReadable or endReadable", state.length, state.ended, this.__id); - if (state.length === 0 && state.ended) endReadable(this); - else emitReadable(this); - return null; - } - - n = howMuchToRead(n, state); - - // If we've ended, and we're now clear, then finish it up. - if (n === 0 && state.ended) { - $debug("read: calling endReadable if length 0 -- length, state.ended", state.length, state.ended, this.__id); - if (state.length === 0) endReadable(this); - return null; - } - - // All the actual chunk generation logic needs to be - // *below* the call to _read. The reason is that in certain - // synthetic stream cases, such as passthrough streams, _read - // may be a completely synchronous operation which may change - // the state of the read buffer, providing enough data when - // before there was *not* enough. - // - // So, the steps are: - // 1. Figure out what the state of things will be after we do - // a read from the buffer. - // - // 2. If that resulting state will trigger a _read, then call _read. - // Note that this may be asynchronous, or synchronous. Yes, it is - // deeply ugly to write APIs this way, but that still doesn't mean - // that the Readable class should behave improperly, as streams are - // designed to be sync/async agnostic. - // Take note if the _read call is sync or async (ie, if the read call - // has returned yet), so that we know whether or not it's safe to emit - // 'readable' etc. - // - // 3. Actually pull the requested chunks out of the buffer and return. - - // if we need a readable event, then we need to do some reading. - let doRead = state.needReadable; - $debug("need readable", doRead, this.__id); - - // If we currently have less than the highWaterMark, then also read some. - if (state.length === 0 || state.length - n < state.highWaterMark) { - doRead = true; - $debug("length less than watermark", doRead, this.__id); - } - - // However, if we've ended, then there's no point, if we're already - // reading, then it's unnecessary, if we're constructing we have to wait, - // and if we're destroyed or errored, then it's not allowed, - if (state.ended || state.reading || state.destroyed || state.errored || !state.constructed) { - $debug("state.constructed?", state.constructed, this.__id); - doRead = false; - $debug("reading, ended or constructing", doRead, this.__id); - } else if (doRead) { - $debug("do read", this.__id); - state.reading = true; - state.sync = true; - // If the length is currently zero, then we *need* a readable event. - if (state.length === 0) state.needReadable = true; - - // Call internal read method - try { - var result = this._read(state.highWaterMark); - if ($isPromise(result)) { - $debug("async _read", this.__id); - const peeked = Bun.peek(result); - $debug("peeked promise", peeked, this.__id); - if (peeked !== result) { - result = peeked; - } - } - - if ($isPromise(result) && result?.then && $isCallable(result.then)) { - $debug("async _read result.then setup", this.__id); - result.then(nop, function (err) { - errorOrDestroy(this, err); - }); - } - } catch (err) { - errorOrDestroy(this, err); - } - - state.sync = false; - // If _read pushed data synchronously, then `reading` will be false, - // and we need to re-evaluate how much data we can return to the user. - if (!state.reading) n = howMuchToRead(nOrig, state); - } - - $debug("n @ fromList", n, this.__id); - let ret; - if (n > 0) ret = fromList(n, state); - else ret = null; - - $debug("ret @ read", ret, this.__id); - - if (ret === null) { - state.needReadable = state.length <= state.highWaterMark; - $debug("state.length while ret = null", state.length, this.__id); - n = 0; - } else { - state.length -= n; - if (state.multiAwaitDrain) { - state.awaitDrainWriters.clear(); - } else { - state.awaitDrainWriters = null; - } - } - - $debug("length", state.length, state.ended, nOrig, n); - if (state.length === 0) { - // If we have nothing in the buffer, then we want to know - // as soon as we *do* get something into the buffer. - if (!state.ended) state.needReadable = true; - - // If we tried to read() past the EOF, then emit end on the next tick. - if (nOrig !== n && state.ended) endReadable(this); - } - - if (ret !== null && !state.errorEmitted && !state.closeEmitted) { - state.dataEmitted = true; - this.emit("data", ret); - } - - return ret; - }; - Readable.prototype._read = function (n) { - throw new ERR_METHOD_NOT_IMPLEMENTED("_read()"); - }; - Readable.prototype.pipe = function (dest, pipeOpts) { - const src = this; - const state = this._readableState; - if (state.pipes.length === 1) { - if (!state.multiAwaitDrain) { - state.multiAwaitDrain = true; - state.awaitDrainWriters = new SafeSet(state.awaitDrainWriters ? [state.awaitDrainWriters] : []); - } - } - state.pipes.push(dest); - $debug("pipe count=%d opts=%j", state.pipes.length, pipeOpts, src.__id); - const doEnd = (!pipeOpts || pipeOpts.end !== false) && dest !== process.stdout && dest !== process.stderr; - const endFn = doEnd ? onend : unpipe; - if (state.endEmitted) ProcessNextTick(endFn); - else src.once("end", endFn); - dest.on("unpipe", onunpipe); - function onunpipe(readable, unpipeInfo) { - $debug("onunpipe", src.__id); - if (readable === src) { - if (unpipeInfo && unpipeInfo.hasUnpiped === false) { - unpipeInfo.hasUnpiped = true; - cleanup(); - } - } - } - function onend() { - $debug("onend", src.__id); - dest.end(); - } - let ondrain; - let cleanedUp = false; - function cleanup() { - $debug("cleanup", src.__id); - dest.removeListener("close", onclose); - dest.removeListener("finish", onfinish); - if (ondrain) { - dest.removeListener("drain", ondrain); - } - dest.removeListener("error", onerror); - dest.removeListener("unpipe", onunpipe); - src.removeListener("end", onend); - src.removeListener("end", unpipe); - src.removeListener("data", ondata); - cleanedUp = true; - if (ondrain && state.awaitDrainWriters && (!dest._writableState || dest._writableState.needDrain)) ondrain(); - } - function pause() { - if (!cleanedUp) { - if (state.pipes.length === 1 && state.pipes[0] === dest) { - $debug("false write response, pause", 0, src.__id); - state.awaitDrainWriters = dest; - state.multiAwaitDrain = false; - } else if (state.pipes.length > 1 && state.pipes.includes(dest)) { - $debug("false write response, pause", state.awaitDrainWriters.size, src.__id); - state.awaitDrainWriters.add(dest); - } - src.pause(); - } - if (!ondrain) { - ondrain = pipeOnDrain(src, dest); - dest.on("drain", ondrain); - } - } - src.on("data", ondata); - function ondata(chunk) { - $debug("ondata", src.__id); - const ret = dest.write(chunk); - $debug("dest.write", ret, src.__id); - if (ret === false) { - pause(); - } - } - function onerror(er) { - $debug("onerror", er); - unpipe(); - dest.removeListener("error", onerror); - if (dest.listenerCount("error") === 0) { - const s = dest._writableState || dest._readableState; - if (s && !s.errorEmitted) { - errorOrDestroy(dest, er); - } else { - dest.emit("error", er); - } - } - } - prependListener(dest, "error", onerror); - function onclose() { - dest.removeListener("finish", onfinish); - unpipe(); - } - dest.once("close", onclose); - function onfinish() { - $debug("onfinish"); - dest.removeListener("close", onclose); - unpipe(); - } - dest.once("finish", onfinish); - function unpipe() { - $debug("unpipe"); - src.unpipe(dest); - } - dest.emit("pipe", src); - if (dest.writableNeedDrain === true) { - if (state.flowing) { - pause(); - } - } else if (!state.flowing) { - $debug("pipe resume"); - src.resume(); - } - return dest; - }; - function pipeOnDrain(src, dest) { - return function pipeOnDrainFunctionResult() { - const state = src._readableState; - if (state.awaitDrainWriters === dest) { - $debug("pipeOnDrain", 1); - state.awaitDrainWriters = null; - } else if (state.multiAwaitDrain) { - $debug("pipeOnDrain", state.awaitDrainWriters.size); - state.awaitDrainWriters.delete(dest); - } - if ((!state.awaitDrainWriters || state.awaitDrainWriters.size === 0) && src.listenerCount("data")) { - src.resume(); - } - }; - } - Readable.prototype.unpipe = function (dest) { - const state = this._readableState; - const unpipeInfo = { - hasUnpiped: false, - }; - if (state.pipes.length === 0) return this; - if (!dest) { - const dests = state.pipes; - state.pipes = []; - this.pause(); - for (let i = 0; i < dests.length; i++) - dests[i].emit("unpipe", this, { - hasUnpiped: false, - }); - return this; - } - const index = ArrayPrototypeIndexOf(state.pipes, dest); - if (index === -1) return this; - state.pipes.splice(index, 1); - if (state.pipes.length === 0) this.pause(); - dest.emit("unpipe", this, unpipeInfo); - return this; - }; - Readable.prototype.addListener = Readable.prototype.on; - Readable.prototype.removeListener = function (ev, fn) { - const res = Stream.prototype.removeListener.$call(this, ev, fn); - if (ev === "readable") { - ProcessNextTick(updateReadableListening, this); - } - return res; - }; - Readable.prototype.off = Readable.prototype.removeListener; - Readable.prototype.removeAllListeners = function (ev) { - const res = Stream.prototype.removeAllListeners.$apply(this, arguments); - if (ev === "readable" || ev === void 0) { - ProcessNextTick(updateReadableListening, this); - } - return res; - }; - function updateReadableListening(self) { - const state = self._readableState; - state.readableListening = self.listenerCount("readable") > 0; - if (state.resumeScheduled && state.paused === false) { - state.flowing = true; - } else if (self.listenerCount("data") > 0) { - self.resume(); - } else if (!state.readableListening) { - state.flowing = null; - } - } - function nReadingNextTick(self) { - $debug("on readable nextTick, calling read(0)", self.__id); - self.read(0); - } - // Readable.prototype.resume = function () { - // const state = this._readableState; - // if (!state.flowing) { - // $debug("resume", this.__id); - // state.flowing = !state.readableListening; - // resume(this, state); - // } - // state.paused = false; - // return this; - // }; - Readable.prototype.resume = function () { - const state = this._readableState; - if (!state.flowing) { - $debug("resume"); - // We flow only if there is no one listening - // for readable, but we still have to call - // resume(). - state.flowing = !state.readableListening; - resume(this, state); - } - state[kPaused] = false; - return this; - }; - function resume(stream, state) { - if (!state.resumeScheduled) { - state.resumeScheduled = true; - process.nextTick(resume_, stream, state); - } - } - function resume_(stream, state) { - $debug("resume", state.reading); - if (!state.reading) { - stream.read(0); - } - state.resumeScheduled = false; - stream.emit("resume"); - flow(stream); - if (state.flowing && !state.reading) stream.read(0); - } - Readable.prototype.pause = function () { - $debug("call pause flowing=%j", this._readableState.flowing, this.__id); - if (this._readableState.flowing !== false) { - $debug("pause", this.__id); - this._readableState.flowing = false; - this.emit("pause"); - } - this._readableState.paused = true; - return this; - }; - function flow(stream) { - const state = stream._readableState; - while (state.flowing && stream.read() !== null); - } - Readable.prototype.wrap = function (stream) { - let paused = false; - stream.on("data", chunk => { - if (!this.push(chunk) && stream.pause) { - paused = true; - stream.pause(); - } - }); - stream.on("end", () => { - this.push(null); - }); - stream.on("error", err => { - errorOrDestroy(this, err); - }); - stream.on("close", () => { - this.destroy(); - }); - stream.on("destroy", () => { - this.destroy(); - }); - this._read = () => { - if (paused && stream.resume) { - paused = false; - stream.resume(); - } - }; - const streamKeys = ObjectKeys(stream); - for (let j = 1; j < streamKeys.length; j++) { - const i = streamKeys[j]; - if (this[i] === void 0 && typeof stream[i] === "function") { - this[i] = stream[i].bind(stream); - } - } - return this; - }; - Readable.prototype[SymbolAsyncIterator] = function () { - return streamToAsyncIterator(this); - }; - Readable.prototype.iterator = function (options) { - if (options !== void 0) { - validateObject(options, "options"); - } - return streamToAsyncIterator(this, options); - }; - function streamToAsyncIterator(stream, options) { - if (typeof stream.read !== "function") { - stream = Readable.wrap(stream, { - objectMode: true, - }); - } - const iter = createAsyncIterator(stream, options); - iter.stream = stream; - return iter; - } - async function* createAsyncIterator(stream, options) { - let callback = nop; - function next(resolve) { - if (this === stream) { - callback(); - callback = nop; - } else { - callback = resolve; - } - } - stream.on("readable", next); - let error; - const cleanup = eos( - stream, - { - writable: false, - }, - err => { - error = err ? aggregateTwoErrors(error, err) : null; - callback(); - callback = nop; - }, - ); - try { - while (true) { - const chunk = stream.destroyed ? null : stream.read(); - if (chunk !== null) { - yield chunk; - } else if (error) { - throw error; - } else if (error === null) { - return; - } else { - await new Promise2(next); - } - } - } catch (err) { - error = aggregateTwoErrors(error, err); - throw error; - } finally { - if ( - (error || (options === null || options === void 0 ? void 0 : options.destroyOnReturn) !== false) && - (error === void 0 || stream._readableState.autoDestroy) - ) { - destroyImpl.destroyer(stream, null); - } else { - stream.off("readable", next); - cleanup(); - } - } - } - ObjectDefineProperties(Readable.prototype, { - readable: { - get() { - const r = this._readableState; - return !!r && r.readable !== false && !r.destroyed && !r.errorEmitted && !r.endEmitted; - }, - set(val) { - if (this._readableState) { - this._readableState.readable = !!val; - } - }, - }, - readableDidRead: { - enumerable: false, - get: function () { - return this._readableState.dataEmitted; - }, - }, - readableAborted: { - enumerable: false, - get: function () { - return !!( - this._readableState.readable !== false && - (this._readableState.destroyed || this._readableState.errored) && - !this._readableState.endEmitted - ); - }, - }, - readableHighWaterMark: { - enumerable: false, - get: function () { - return this._readableState.highWaterMark; - }, - }, - readableBuffer: { - enumerable: false, - get: function () { - return this._readableState && this._readableState.buffer; - }, - }, - readableFlowing: { - enumerable: false, - get: function () { - return this._readableState.flowing; - }, - set: function (state) { - if (this._readableState) { - this._readableState.flowing = state; - } - }, - }, - readableLength: { - enumerable: false, - get() { - return this._readableState.length; - }, - }, - readableObjectMode: { - enumerable: false, - get() { - return this._readableState ? this._readableState.objectMode : false; - }, - }, - readableEncoding: { - enumerable: false, - get() { - return this._readableState ? this._readableState.encoding : null; - }, - }, - errored: { - enumerable: false, - get() { - return this._readableState ? this._readableState.errored : null; - }, - }, - closed: { - get() { - return this._readableState ? this._readableState.closed : false; - }, - }, - destroyed: { - enumerable: false, - get() { - return this._readableState ? this._readableState.destroyed : false; - }, - set(value) { - if (!this._readableState) { - return; - } - this._readableState.destroyed = value; - }, - }, - readableEnded: { - enumerable: false, - get() { - return this._readableState ? this._readableState.endEmitted : false; - }, - }, - }); - Readable._fromList = fromList; - function fromList(n, state) { - if (state.length === 0) return null; - let ret; - if (state.objectMode) ret = state.buffer.shift(); - else if (!n || n >= state.length) { - if (state.decoder) ret = state.buffer.join(""); - else if (state.buffer.length === 1) ret = state.buffer.first(); - else ret = state.buffer.concat(state.length); - state.buffer.clear(); - } else { - ret = state.buffer.consume(n, state.decoder); - } - return ret; - } - function endReadable(stream) { - const state = stream._readableState; - $debug("endEmitted @ endReadable", state.endEmitted, stream.__id); - if (!state.endEmitted) { - state.ended = true; - ProcessNextTick(endReadableNT, state, stream); - } - } - function endReadableNT(state, stream) { - $debug("endReadableNT -- endEmitted, state.length", state.endEmitted, state.length, stream.__id); - if (!state.errored && !state.closeEmitted && !state.endEmitted && state.length === 0) { - state.endEmitted = true; - stream.emit("end"); - $debug("end emitted @ endReadableNT", stream.__id); - if (stream.writable && stream.allowHalfOpen === false) { - ProcessNextTick(endWritableNT, stream); - } else if (state.autoDestroy) { - const wState = stream._writableState; - const autoDestroy = !wState || (wState.autoDestroy && (wState.finished || wState.writable === false)); - if (autoDestroy) { - stream[kAutoDestroyed] = true; // workaround for node:http Server not using node:net Server - stream.destroy(); - } - } - } - } - function endWritableNT(stream) { - const writable = stream.writable && !stream.writableEnded && !stream.destroyed; - if (writable) { - stream.end(); - } - } - Readable.from = function (iterable, opts) { - return from(Readable, iterable, opts); - }; - var webStreamsAdapters = { - newStreamReadableFromReadableStream, - - newReadableStreamFromStreamReadable(streamReadable, options = {}) { - // Not using the internal/streams/utils isReadableNodeStream utility - // here because it will return false if streamReadable is a Duplex - // whose readable option is false. For a Duplex that is not readable, - // we want it to pass this check but return a closed ReadableStream. - if (typeof streamReadable?._readableState !== "object") { - throw new ERR_INVALID_ARG_TYPE("streamReadable", "stream.Readable", streamReadable); - } - var { isDestroyed, isReadable } = require_utils(); - - if (isDestroyed(streamReadable) || !isReadable(streamReadable)) { - const readable = new ReadableStream(); - readable.cancel(); - return readable; - } - - const objectMode = streamReadable.readableObjectMode; - const highWaterMark = streamReadable.readableHighWaterMark; - - const evaluateStrategyOrFallback = strategy => { - // If there is a strategy available, use it - if (strategy) return strategy; - - if (objectMode) { - // When running in objectMode explicitly but no strategy, we just fall - // back to CountQueuingStrategy - return new CountQueuingStrategy({ highWaterMark }); - } - - // When not running in objectMode explicitly, we just fall - // back to a minimal strategy that just specifies the highWaterMark - // and no size algorithm. Using a ByteLengthQueuingStrategy here - // is unnecessary. - return { highWaterMark }; - }; - - const strategy = evaluateStrategyOrFallback(options?.strategy); - - let controller; - - function onData(chunk) { - controller.enqueue(chunk); - if (controller.desiredSize <= 0) streamReadable.pause(); - } - - streamReadable.pause(); - - const cleanup = eos(streamReadable, error => { - if (error?.code === "ERR_STREAM_PREMATURE_CLOSE") { - const err = new AbortError(undefined, { cause: error }); - error = err; - } - - cleanup(); - // This is a protection against non-standard, legacy streams - // that happen to emit an error event again after finished is called. - streamReadable.on("error", () => {}); - if (error) return controller.error(error); - controller.close(); - }); - - streamReadable.on("data", onData); - - return new ReadableStream( - { - start(c) { - controller = c; - }, - - pull() { - streamReadable.resume(); - }, - - cancel(reason) { - destroy(streamReadable, reason); - }, - }, - strategy, - ); - }, - }; - - Readable.fromWeb = function (readableStream, options) { - // We cache .stream() calls for file descriptors - // This won't create a new ReadableStream each time. - let bunStdinStream = Bun.stdin.stream(); - if (readableStream === bunStdinStream) { - return bunStdinStream; - } - - return webStreamsAdapters.newStreamReadableFromReadableStream(readableStream, options); - }; - Readable.toWeb = function (streamReadable, options) { - // Workaround for https://github.com/oven-sh/bun/issues/9041 - if (streamReadable === process.stdin) { - return Bun.stdin.stream(); - } - - return webStreamsAdapters.newReadableStreamFromStreamReadable(streamReadable, options); - }; - Readable.wrap = function (src, options) { - var _ref, _src$readableObjectMo; - return new Readable({ - objectMode: - (_ref = - (_src$readableObjectMo = src.readableObjectMode) !== null && _src$readableObjectMo !== void 0 - ? _src$readableObjectMo - : src.objectMode) !== null && _ref !== void 0 - ? _ref - : true, - ...options, - destroy(err, callback) { - destroyImpl.destroyer(src, err); - callback(err); - }, - }).wrap(src); - }; - }, -}); -const Readable = require_readable(); - -// node_modules/readable-stream/lib/internal/streams/writable.js -var errorOrDestroy; -var require_writable = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/writable.js"(exports, module) { - "use strict"; - var { - ArrayPrototypeSlice, - Error: Error2, - FunctionPrototypeSymbolHasInstance, - ObjectDefineProperty, - ObjectDefineProperties, - ObjectSetPrototypeOf, - StringPrototypeToLowerCase, - Symbol: Symbol2, - SymbolHasInstance, - } = require_primordials(); - - var Stream = require_legacy().Stream; - var destroyImpl = require_destroy(); - var { addAbortSignal } = require_add_abort_signal(); - var { - ERR_INVALID_ARG_TYPE, - ERR_METHOD_NOT_IMPLEMENTED, - ERR_MULTIPLE_CALLBACK, - ERR_STREAM_CANNOT_PIPE, - ERR_STREAM_DESTROYED, - ERR_STREAM_ALREADY_FINISHED, - ERR_STREAM_NULL_VALUES, - ERR_STREAM_WRITE_AFTER_END, - ERR_UNKNOWN_ENCODING, - } = require_errors().codes; - ({ errorOrDestroy } = destroyImpl); - - function Writable(options = {}) { - const isDuplex = this instanceof require_duplex(); - if (!isDuplex && !FunctionPrototypeSymbolHasInstance(Writable, this)) return new Writable(options); - - // this._events ??= { - // close: undefined, - // error: undefined, - // prefinish: undefined, - // finish: undefined, - // drain: undefined, - // }; - - this._writableState = new WritableState(options, this, isDuplex); - if (options) { - if (typeof options.write === "function") this._write = options.write; - if (typeof options.writev === "function") this._writev = options.writev; - if (typeof options.destroy === "function") this._destroy = options.destroy; - if (typeof options.final === "function") this._final = options.final; - if (typeof options.construct === "function") this._construct = options.construct; - if (options.signal) addAbortSignal(options.signal, this); - } - Stream.$call(this, options); - - destroyImpl.construct(this, () => { - const state = this._writableState; - if (!state.writing) { - clearBuffer(this, state); - } - finishMaybe(this, state); - }); - } - Writable.prototype = {}; - ObjectSetPrototypeOf(Writable.prototype, Stream.prototype); - Writable.prototype.constructor = Writable; // Re-add constructor which got lost when setting prototype - ObjectSetPrototypeOf(Writable, Stream); - module.exports = Writable; - - function nop() {} - var kOnFinished = Symbol2("kOnFinished"); - function WritableState(options, stream, isDuplex) { - if (typeof isDuplex !== "boolean") isDuplex = stream instanceof require_duplex(); - this.objectMode = !!(options && options.objectMode); - if (isDuplex) this.objectMode = this.objectMode || !!(options && options.writableObjectMode); - this.highWaterMark = options - ? getHighWaterMark(this, options, "writableHighWaterMark", isDuplex) - : getDefaultHighWaterMark(false); - this.finalCalled = false; - this.needDrain = false; - this.ending = false; - this.ended = false; - this.finished = false; - this.destroyed = false; - const noDecode = !!(options && options.decodeStrings === false); - this.decodeStrings = !noDecode; - this.defaultEncoding = (options && options.defaultEncoding) || "utf8"; - this.length = 0; - this.writing = false; - this.corked = 0; - this.sync = true; - this.bufferProcessing = false; - this.onwrite = onwrite.bind(void 0, stream); - this.writecb = null; - this.writelen = 0; - this.afterWriteTickInfo = null; - resetBuffer(this); - this.pendingcb = 0; - this.constructed = true; - this.prefinished = false; - this.errorEmitted = false; - this.emitClose = !options || options.emitClose !== false; - this.autoDestroy = !options || options.autoDestroy !== false; - this.errored = null; - this.closed = false; - this.closeEmitted = false; - this[kOnFinished] = []; - } - WritableState.prototype = {}; - function resetBuffer(state) { - state.buffered = []; - state.bufferedIndex = 0; - state.allBuffers = true; - state.allNoop = true; - } - WritableState.prototype.getBuffer = function getBuffer() { - return ArrayPrototypeSlice(this.buffered, this.bufferedIndex); - }; - ObjectDefineProperty(WritableState.prototype, "bufferedRequestCount", { - get() { - return this.buffered.length - this.bufferedIndex; - }, - }); - - ObjectDefineProperty(Writable, SymbolHasInstance, { - value: function (object) { - if (FunctionPrototypeSymbolHasInstance(this, object)) return true; - if (this !== Writable) return false; - return object && object._writableState instanceof WritableState; - }, - }); - Writable.prototype.pipe = function () { - errorOrDestroy(this, new ERR_STREAM_CANNOT_PIPE()); - }; - function _write(stream, chunk, encoding, cb) { - const state = stream._writableState; - if (typeof encoding === "function") { - cb = encoding; - encoding = state.defaultEncoding; - } else { - if (!encoding) encoding = state.defaultEncoding; - else if (encoding !== "buffer" && !Buffer.isEncoding(encoding)) throw new ERR_UNKNOWN_ENCODING(encoding); - if (typeof cb !== "function") cb = nop; - } - if (chunk === null) { - throw new ERR_STREAM_NULL_VALUES(); - } else if (!state.objectMode) { - if (typeof chunk === "string") { - if (state.decodeStrings !== false) { - chunk = Buffer.from(chunk, encoding); - encoding = "buffer"; - } - } else if (chunk instanceof Buffer) { - encoding = "buffer"; - } else if (Stream._isUint8Array(chunk)) { - chunk = Stream._uint8ArrayToBuffer(chunk); - encoding = "buffer"; - } else { - throw new ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "Uint8Array"], chunk); - } - } - let err; - if (state.ending) { - err = new ERR_STREAM_WRITE_AFTER_END(); - } else if (state.destroyed) { - err = new ERR_STREAM_DESTROYED("write"); - } - if (err) { - ProcessNextTick(cb, err); - errorOrDestroy(stream, err, true); - return err; - } - state.pendingcb++; - return writeOrBuffer(stream, state, chunk, encoding, cb); - } - Writable.prototype.write = function (chunk, encoding, cb) { - if ($isCallable(encoding)) { - cb = encoding; - encoding = null; - } - return _write(this, chunk, encoding, cb) === true; - }; - Writable.prototype.cork = function () { - this._writableState.corked++; - }; - Writable.prototype.uncork = function () { - const state = this._writableState; - if (state.corked) { - state.corked--; - if (!state.writing) clearBuffer(this, state); - } - }; - Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { - if (typeof encoding === "string") encoding = StringPrototypeToLowerCase(encoding); - if (!Buffer.isEncoding(encoding)) throw new ERR_UNKNOWN_ENCODING(encoding); - this._writableState.defaultEncoding = encoding; - return this; - }; - function writeOrBuffer(stream, state, chunk, encoding, callback) { - const len = state.objectMode ? 1 : chunk.length; - state.length += len; - const ret = state.length < state.highWaterMark; - if (!ret) state.needDrain = true; - if (state.writing || state.corked || state.errored || !state.constructed) { - state.buffered.push({ - chunk, - encoding, - callback, - }); - if (state.allBuffers && encoding !== "buffer") { - state.allBuffers = false; - } - if (state.allNoop && callback !== nop) { - state.allNoop = false; - } - } else { - state.writelen = len; - state.writecb = callback; - state.writing = true; - state.sync = true; - stream._write(chunk, encoding, state.onwrite); - state.sync = false; - } - return ret && !state.errored && !state.destroyed; - } - function doWrite(stream, state, writev, len, chunk, encoding, cb) { - state.writelen = len; - state.writecb = cb; - state.writing = true; - state.sync = true; - if (state.destroyed) state.onwrite(new ERR_STREAM_DESTROYED("write")); - else if (writev) stream._writev(chunk, state.onwrite); - else stream._write(chunk, encoding, state.onwrite); - state.sync = false; - } - function onwriteError(stream, state, er, cb) { - --state.pendingcb; - cb(er); - errorBuffer(state); - errorOrDestroy(stream, er); - } - function onwrite(stream, er) { - const state = stream._writableState; - const sync = state.sync; - const cb = state.writecb; - if (typeof cb !== "function") { - errorOrDestroy(stream, new ERR_MULTIPLE_CALLBACK()); - return; - } - state.writing = false; - state.writecb = null; - state.length -= state.writelen; - state.writelen = 0; - if (er) { - Error.captureStackTrace(er); - if (!state.errored) { - state.errored = er; - } - if (stream._readableState && !stream._readableState.errored) { - stream._readableState.errored = er; - } - if (sync) { - ProcessNextTick(onwriteError, stream, state, er, cb); - } else { - onwriteError(stream, state, er, cb); - } - } else { - if (state.buffered.length > state.bufferedIndex) { - clearBuffer(stream, state); - } - if (sync) { - if (state.afterWriteTickInfo !== null && state.afterWriteTickInfo.cb === cb) { - state.afterWriteTickInfo.count++; - } else { - state.afterWriteTickInfo = { - count: 1, - cb, - stream, - state, - }; - ProcessNextTick(afterWriteTick, state.afterWriteTickInfo); - } - } else { - afterWrite(stream, state, 1, cb); - } - } - } - function afterWriteTick({ stream, state, count, cb }) { - state.afterWriteTickInfo = null; - return afterWrite(stream, state, count, cb); - } - function afterWrite(stream, state, count, cb) { - const needDrain = !state.ending && !stream.destroyed && state.length === 0 && state.needDrain; - if (needDrain) { - state.needDrain = false; - stream.emit("drain"); - } - while (count-- > 0) { - state.pendingcb--; - cb(); - } - if (state.destroyed) { - errorBuffer(state); - } - finishMaybe(stream, state); - } - function errorBuffer(state) { - if (state.writing) { - return; - } - for (let n = state.bufferedIndex; n < state.buffered.length; ++n) { - var _state$errored; - const { chunk, callback } = state.buffered[n]; - const len = state.objectMode ? 1 : chunk.length; - state.length -= len; - callback( - (_state$errored = state.errored) !== null && _state$errored !== void 0 - ? _state$errored - : new ERR_STREAM_DESTROYED("write"), - ); - } - const onfinishCallbacks = state[kOnFinished].splice(0); - for (let i = 0; i < onfinishCallbacks.length; i++) { - var _state$errored2; - onfinishCallbacks[i]( - (_state$errored2 = state.errored) !== null && _state$errored2 !== void 0 - ? _state$errored2 - : new ERR_STREAM_DESTROYED("end"), - ); - } - resetBuffer(state); - } - function clearBuffer(stream, state) { - if (state.corked || state.bufferProcessing || state.destroyed || !state.constructed) { - return; - } - const { buffered, bufferedIndex, objectMode } = state; - const bufferedLength = buffered.length - bufferedIndex; - if (!bufferedLength) { - return; - } - let i = bufferedIndex; - state.bufferProcessing = true; - if (bufferedLength > 1 && stream._writev) { - state.pendingcb -= bufferedLength - 1; - const callback = state.allNoop - ? nop - : err => { - for (let n = i; n < buffered.length; ++n) { - buffered[n].callback(err); - } - }; - const chunks = state.allNoop && i === 0 ? buffered : ArrayPrototypeSlice(buffered, i); - chunks.allBuffers = state.allBuffers; - doWrite(stream, state, true, state.length, chunks, "", callback); - resetBuffer(state); - } else { - do { - const { chunk, encoding, callback } = buffered[i]; - buffered[i++] = null; - const len = objectMode ? 1 : chunk.length; - doWrite(stream, state, false, len, chunk, encoding, callback); - } while (i < buffered.length && !state.writing); - if (i === buffered.length) { - resetBuffer(state); - } else if (i > 256) { - buffered.splice(0, i); - state.bufferedIndex = 0; - } else { - state.bufferedIndex = i; - } - } - state.bufferProcessing = false; - } - Writable.prototype._write = function (chunk, encoding, cb) { - if (this._writev) { - this._writev( - [ - { - chunk, - encoding, - }, - ], - cb, - ); - } else { - throw new ERR_METHOD_NOT_IMPLEMENTED("_write()"); - } - }; - Writable.prototype._writev = null; - Writable.prototype.end = function (chunk, encoding, cb, native = false) { - const state = this._writableState; - $debug("end", state, this.__id); - if (typeof chunk === "function") { - cb = chunk; - chunk = null; - encoding = null; - } else if (typeof encoding === "function") { - cb = encoding; - encoding = null; - } - let err; - if (chunk !== null && chunk !== void 0) { - let ret; - if (!native) { - ret = _write(this, chunk, encoding); - } else { - ret = this.write(chunk, encoding); - } - if (ret instanceof Error2) { - err = ret; - } - } - if (state.corked) { - state.corked = 1; - this.uncork(); - } - if (err) { - this.emit("error", err); - } else if (!state.errored && !state.ending) { - state.ending = true; - finishMaybe(this, state, true); - state.ended = true; - } else if (state.finished) { - err = new ERR_STREAM_ALREADY_FINISHED("end"); - } else if (state.destroyed) { - err = new ERR_STREAM_DESTROYED("end"); - } - if (typeof cb === "function") { - if (err || state.finished) { - ProcessNextTick(cb, err); - } else { - state[kOnFinished].push(cb); - } - } - return this; - }; - function needFinish(state, tag?) { - var needFinish = - state.ending && - !state.destroyed && - state.constructed && - state.length === 0 && - !state.errored && - state.buffered.length === 0 && - !state.finished && - !state.writing && - !state.errorEmitted && - !state.closeEmitted; - $debug("needFinish", needFinish, tag); - return needFinish; - } - function callFinal(stream, state) { - let called = false; - function onFinish(err) { - if (called) { - errorOrDestroy(stream, err !== null && err !== void 0 ? err : new ERR_MULTIPLE_CALLBACK()); - return; - } - called = true; - state.pendingcb--; - if (err) { - const onfinishCallbacks = state[kOnFinished].splice(0); - for (let i = 0; i < onfinishCallbacks.length; i++) { - onfinishCallbacks[i](err); - } - errorOrDestroy(stream, err, state.sync); - } else if (needFinish(state)) { - state.prefinished = true; - stream.emit("prefinish"); - state.pendingcb++; - ProcessNextTick(finish, stream, state); - } - } - state.sync = true; - state.pendingcb++; - try { - stream._final(onFinish); - } catch (err) { - onFinish(err); - } - state.sync = false; - } - function prefinish(stream, state) { - if (!state.prefinished && !state.finalCalled) { - if (typeof stream._final === "function" && !state.destroyed) { - state.finalCalled = true; - callFinal(stream, state); - } else { - state.prefinished = true; - stream.emit("prefinish"); - } - } - } - function finishMaybe(stream, state, sync) { - $debug("finishMaybe -- state, sync", state, sync, stream.__id); - - if (!needFinish(state, stream.__id)) return; - - prefinish(stream, state); - if (state.pendingcb === 0) { - if (sync) { - state.pendingcb++; - ProcessNextTick.$call( - null, - (stream2, state2) => { - if (needFinish(state2)) { - finish(stream2, state2); - } else { - state2.pendingcb--; - } - }, - stream, - state, - ); - } else if (needFinish(state)) { - state.pendingcb++; - finish(stream, state); - } - } - } - function finish(stream, state) { - state.pendingcb--; - state.finished = true; - const onfinishCallbacks = state[kOnFinished].splice(0); - for (let i = 0; i < onfinishCallbacks.length; i++) { - onfinishCallbacks[i](); - } - stream.emit("finish"); - if (state.autoDestroy) { - const rState = stream._readableState; - const autoDestroy = !rState || (rState.autoDestroy && (rState.endEmitted || rState.readable === false)); - if (autoDestroy) { - stream.destroy(); - } - } - } - ObjectDefineProperties(Writable.prototype, { - closed: { - get() { - return this._writableState ? this._writableState.closed : false; - }, - }, - destroyed: { - get() { - return this._writableState ? this._writableState.destroyed : false; - }, - set(value) { - if (this._writableState) { - this._writableState.destroyed = value; - } - }, - }, - writable: { - get() { - const w = this._writableState; - return !!w && w.writable !== false && !w.destroyed && !w.errored && !w.ending && !w.ended; - }, - set(val) { - if (this._writableState) { - this._writableState.writable = !!val; - } - }, - }, - writableFinished: { - get() { - return this._writableState ? this._writableState.finished : false; - }, - }, - writableObjectMode: { - get() { - return this._writableState ? this._writableState.objectMode : false; - }, - }, - writableBuffer: { - get() { - return this._writableState && this._writableState.getBuffer(); - }, - }, - writableEnded: { - get() { - return this._writableState ? this._writableState.ending : false; - }, - }, - writableNeedDrain: { - get() { - const wState = this._writableState; - if (!wState) return false; - return !wState.destroyed && !wState.ending && wState.needDrain; - }, - }, - writableHighWaterMark: { - get() { - return this._writableState && this._writableState.highWaterMark; - }, - }, - writableCorked: { - get() { - return this._writableState ? this._writableState.corked : 0; - }, - }, - writableLength: { - get() { - return this._writableState && this._writableState.length; - }, - }, - errored: { - enumerable: false, - get() { - return this._writableState ? this._writableState.errored : null; - }, - }, - writableAborted: { - enumerable: false, - get: function () { - return !!( - this._writableState.writable !== false && - (this._writableState.destroyed || this._writableState.errored) && - !this._writableState.finished - ); - }, - }, - }); - var destroy = destroyImpl.destroy; - Writable.prototype.destroy = function (err, cb) { - const state = this._writableState; - if (!state.destroyed && (state.bufferedIndex < state.buffered.length || state[kOnFinished].length)) { - ProcessNextTick(errorBuffer, state); - } - destroy.$call(this, err, cb); - return this; - }; - Writable.prototype._undestroy = destroyImpl.undestroy; - Writable.prototype._destroy = function (err, cb) { - cb(err); - }; - Writable.prototype[EE.captureRejectionSymbol] = function (err) { - this.destroy(err); - }; - var webStreamsAdapters; - function lazyWebStreams() { - if (webStreamsAdapters === void 0) webStreamsAdapters = {}; - return webStreamsAdapters; - } - Writable.fromWeb = function (writableStream, options) { - return lazyWebStreams().newStreamWritableFromWritableStream(writableStream, options); - }; - Writable.toWeb = function (streamWritable) { - return lazyWebStreams().newWritableStreamFromStreamWritable(streamWritable); - }; - }, -}); -const Writable = require_writable(); - -// node_modules/readable-stream/lib/internal/streams/duplexify.js -var require_duplexify = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/duplexify.js"(exports, module) { - "use strict"; - var { - isReadable, - isWritable, - isIterable, - isNodeStream, - isReadableNodeStream, - isWritableNodeStream, - isDuplexNodeStream, - } = require_utils(); - var eos = require_end_of_stream(); - var { - AbortError, - codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_RETURN_VALUE }, - } = require_errors(); - var { destroyer } = require_destroy(); - var Duplex = require_duplex(); - var { createDeferredPromise } = require_util(); - var from = require_from(); - var isBlob = - typeof Blob !== "undefined" - ? function isBlob2(b) { - return b instanceof Blob; - } - : function isBlob2(b) { - return false; - }; - var { FunctionPrototypeCall } = require_primordials(); - class Duplexify extends Duplex { - constructor(options) { - super(options); - - // https://github.com/nodejs/node/pull/34385 - - if ((options === null || options === undefined ? undefined : options.readable) === false) { - this._readableState.readable = false; - this._readableState.ended = true; - this._readableState.endEmitted = true; - } - if ((options === null || options === undefined ? undefined : options.writable) === false) { - this._writableState.writable = false; - this._writableState.ending = true; - this._writableState.ended = true; - this._writableState.finished = true; - } - } - } - module.exports = function duplexify(body, name) { - if (isDuplexNodeStream(body)) { - return body; - } - if (isReadableNodeStream(body)) { - return _duplexify({ - readable: body, - }); - } - if (isWritableNodeStream(body)) { - return _duplexify({ - writable: body, - }); - } - if (isNodeStream(body)) { - return _duplexify({ - writable: false, - readable: false, - }); - } - if (typeof body === "function") { - const { value, write, final, destroy } = fromAsyncGen(body); - if (isIterable(value)) { - return from(Duplexify, value, { - objectMode: true, - write, - final, - destroy, - }); - } - const then2 = value === null || value === void 0 ? void 0 : value.then; - if (typeof then2 === "function") { - let d; - const promise = FunctionPrototypeCall( - then2, - value, - val => { - if (val != null) { - throw new ERR_INVALID_RETURN_VALUE("nully", "body", val); - } - }, - err => { - destroyer(d, err); - }, - ); - return (d = new Duplexify({ - objectMode: true, - readable: false, - write, - final(cb) { - final(async () => { - try { - await promise; - ProcessNextTick(cb, null); - } catch (err) { - ProcessNextTick(cb, err); - } - }); - }, - destroy, - })); - } - throw new ERR_INVALID_RETURN_VALUE("Iterable, AsyncIterable or AsyncFunction", name, value); - } - if (isBlob(body)) { - return duplexify(body.arrayBuffer()); - } - if (isIterable(body)) { - return from(Duplexify, body, { - objectMode: true, - writable: false, - }); - } - if ( - typeof (body === null || body === void 0 ? void 0 : body.writable) === "object" || - typeof (body === null || body === void 0 ? void 0 : body.readable) === "object" - ) { - const readable = - body !== null && body !== void 0 && body.readable - ? isReadableNodeStream(body === null || body === void 0 ? void 0 : body.readable) - ? body === null || body === void 0 - ? void 0 - : body.readable - : duplexify(body.readable) - : void 0; - const writable = - body !== null && body !== void 0 && body.writable - ? isWritableNodeStream(body === null || body === void 0 ? void 0 : body.writable) - ? body === null || body === void 0 - ? void 0 - : body.writable - : duplexify(body.writable) - : void 0; - return _duplexify({ - readable, - writable, - }); - } - const then = body === null || body === void 0 ? void 0 : body.then; - if (typeof then === "function") { - let d; - FunctionPrototypeCall( - then, - body, - val => { - if (val != null) { - d.push(val); - } - d.push(null); - }, - err => { - destroyer(d, err); - }, - ); - return (d = new Duplexify({ - objectMode: true, - writable: false, - read() {}, - })); - } - throw new ERR_INVALID_ARG_TYPE( - name, - [ - "Blob", - "ReadableStream", - "WritableStream", - "Stream", - "Iterable", - "AsyncIterable", - "Function", - "{ readable, writable } pair", - "Promise", - ], - body, - ); - }; - function fromAsyncGen(fn) { - let { promise, resolve } = createDeferredPromise(); - const ac = new AbortController(); - const signal = ac.signal; - const value = fn( - (async function* () { - while (true) { - const _promise = promise; - promise = null; - const { chunk, done, cb } = await _promise; - ProcessNextTick(cb); - if (done) return; - if (signal.aborted) - throw new AbortError(void 0, { - cause: signal.reason, - }); - ({ promise, resolve } = createDeferredPromise()); - yield chunk; - } - })(), - { - signal, - }, - ); - return { - value, - write(chunk, encoding, cb) { - const _resolve = resolve; - resolve = null; - _resolve({ - chunk, - done: false, - cb, - }); - }, - final(cb) { - const _resolve = resolve; - resolve = null; - _resolve({ - done: true, - cb, - }); - }, - destroy(err, cb) { - ac.abort(); - cb(err); - }, - }; - } - function _duplexify(pair) { - const r = - pair.readable && typeof pair.readable.read !== "function" ? Readable.wrap(pair.readable) : pair.readable; - const w = pair.writable; - let readable = !!isReadable(r); - let writable = !!isWritable(w); - let ondrain; - let onfinish; - let onreadable; - let onclose; - let d; - function onfinished(err) { - const cb = onclose; - onclose = null; - if (cb) { - cb(err); - } else if (err) { - d.destroy(err); - } else if (!readable && !writable) { - d.destroy(); - } - } - d = new Duplexify({ - readableObjectMode: !!(r !== null && r !== void 0 && r.readableObjectMode), - writableObjectMode: !!(w !== null && w !== void 0 && w.writableObjectMode), - readable, - writable, - }); - if (writable) { - eos(w, err => { - writable = false; - if (err) { - destroyer(r, err); - } - onfinished(err); - }); - d._write = function (chunk, encoding, callback) { - if (w.write(chunk, encoding)) { - callback(); - } else { - ondrain = callback; - } - }; - d._final = function (callback) { - w.end(); - onfinish = callback; - }; - w.on("drain", function () { - if (ondrain) { - const cb = ondrain; - ondrain = null; - cb(); - } - }); - w.on("finish", function () { - if (onfinish) { - const cb = onfinish; - onfinish = null; - cb(); - } - }); - } - if (readable) { - eos(r, err => { - readable = false; - if (err) { - destroyer(r, err); - } - onfinished(err); - }); - r.on("readable", function () { - if (onreadable) { - const cb = onreadable; - onreadable = null; - cb(); - } - }); - r.on("end", function () { - d.push(null); - }); - d._read = function () { - while (true) { - const buf = r.read(); - if (buf === null) { - onreadable = d._read; - return; - } - if (!d.push(buf)) { - return; - } - } - }; - } - d._destroy = function (err, callback) { - if (!err && onclose !== null) { - err = new AbortError(); - } - onreadable = null; - ondrain = null; - onfinish = null; - if (onclose === null) { - callback(err); - } else { - onclose = callback; - destroyer(w, err); - destroyer(r, err); - } - }; - return d; - } - }, -}); - -// node_modules/readable-stream/lib/internal/streams/duplex.js -var require_duplex = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/duplex.js"(exports, module) { - "use strict"; - var { ObjectDefineProperties, ObjectGetOwnPropertyDescriptor, ObjectKeys, ObjectSetPrototypeOf } = - require_primordials(); - - function Duplex(options) { - if (!(this instanceof Duplex)) return new Duplex(options); - - // this._events ??= { - // close: undefined, - // error: undefined, - // prefinish: undefined, - // finish: undefined, - // drain: undefined, - // data: undefined, - // end: undefined, - // readable: undefined, - // }; - - Readable.$call(this, options); - Writable.$call(this, options); - - if (options) { - this.allowHalfOpen = options.allowHalfOpen !== false; - if (options.readable === false) { - this._readableState.readable = false; - this._readableState.ended = true; - this._readableState.endEmitted = true; - } - if (options.writable === false) { - this._writableState.writable = false; - this._writableState.ending = true; - this._writableState.ended = true; - this._writableState.finished = true; - } - } else { - this.allowHalfOpen = true; - } - } - Duplex.prototype = {}; - module.exports = Duplex; - ObjectSetPrototypeOf(Duplex.prototype, Readable.prototype); - Duplex.prototype.constructor = Duplex; // Re-add constructor which got lost when setting prototype - ObjectSetPrototypeOf(Duplex, Readable); - - { - for (var method in Writable.prototype) { - if (!Duplex.prototype[method]) Duplex.prototype[method] = Writable.prototype[method]; - } - } - - ObjectDefineProperties(Duplex.prototype, { - writable: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writable"), - writableHighWaterMark: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableHighWaterMark"), - writableObjectMode: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableObjectMode"), - writableBuffer: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableBuffer"), - writableLength: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableLength"), - writableFinished: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableFinished"), - writableCorked: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableCorked"), - writableEnded: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableEnded"), - writableNeedDrain: ObjectGetOwnPropertyDescriptor(Writable.prototype, "writableNeedDrain"), - destroyed: { - get() { - if (this._readableState === void 0 || this._writableState === void 0) { - return false; - } - return this._readableState.destroyed && this._writableState.destroyed; - }, - set(value) { - if (this._readableState && this._writableState) { - this._readableState.destroyed = value; - this._writableState.destroyed = value; - } - }, - }, - }); - var webStreamsAdapters; - function lazyWebStreams() { - if (webStreamsAdapters === void 0) webStreamsAdapters = {}; - return webStreamsAdapters; - } - Duplex.fromWeb = function (pair, options) { - return lazyWebStreams().newStreamDuplexFromReadableWritablePair(pair, options); - }; - Duplex.toWeb = function (duplex) { - return lazyWebStreams().newReadableWritablePairFromDuplex(duplex); - }; - var duplexify; - Duplex.from = function (body) { - if (!duplexify) { - duplexify = require_duplexify(); - } - return duplexify(body, "body"); - }; - }, -}); -const Duplex = require_duplex(); - -// node_modules/readable-stream/lib/internal/streams/transform.js -var require_transform = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/transform.js"(exports, module) { - "use strict"; - var { ObjectSetPrototypeOf, Symbol: Symbol2 } = require_primordials(); - var { ERR_METHOD_NOT_IMPLEMENTED } = require_errors().codes; - function Transform(options) { - if (!(this instanceof Transform)) return new Transform(options); - - Duplex.$call(this, options); - - this._readableState.sync = false; - this[kCallback] = null; - - if (options) { - if (typeof options.transform === "function") this._transform = options.transform; - if (typeof options.flush === "function") this._flush = options.flush; - } else { - this.allowHalfOpen = true; - } - - this.on("prefinish", prefinish.bind(this)); - } - Transform.prototype = {}; - ObjectSetPrototypeOf(Transform.prototype, Duplex.prototype); - Transform.prototype.constructor = Transform; // Re-add constructor which got lost when setting prototype - ObjectSetPrototypeOf(Transform, Duplex); - - module.exports = Transform; - var kCallback = Symbol2("kCallback"); - function final(cb) { - if (typeof this._flush === "function" && !this.destroyed) { - this._flush((er, data) => { - if (er) { - if (cb) { - cb(er); - } else { - this.destroy(er); - } - return; - } - if (data != null) { - this.push(data); - } - this.push(null); - if (cb) { - cb(); - } - }); - } else { - this.push(null); - if (cb) { - cb(); - } - } - } - function prefinish() { - if (this._final !== final) { - final.$call(this); - } - } - Transform.prototype._final = final; - Transform.prototype._transform = function (chunk, encoding, callback) { - throw new ERR_METHOD_NOT_IMPLEMENTED("_transform()"); - }; - Transform.prototype._write = function (chunk, encoding, callback) { - const rState = this._readableState; - const wState = this._writableState; - const length = rState.length; - this._transform(chunk, encoding, (err, val) => { - if (err) { - callback(err); - return; - } - if (val != null) { - this.push(val); - } - if ( - wState.ended || - length === rState.length || - rState.length < rState.highWaterMark || - rState.highWaterMark === 0 || - rState.length === 0 - ) { - callback(); - } else { - this[kCallback] = callback; - } - }); - }; - Transform.prototype._read = function () { - if (this[kCallback]) { - const callback = this[kCallback]; - this[kCallback] = null; - callback(); - } - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/passthrough.js -var require_passthrough = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/passthrough.js"(exports, module) { - "use strict"; - var { ObjectSetPrototypeOf } = require_primordials(); - var Transform = require_transform(); - - function PassThrough(options) { - if (!(this instanceof PassThrough)) return new PassThrough(options); - Transform.$call(this, options); - } - PassThrough.prototype = {}; - - ObjectSetPrototypeOf(PassThrough.prototype, Transform.prototype); - PassThrough.prototype.constructor = PassThrough; // Re-add constructor which got lost when setting prototype - ObjectSetPrototypeOf(PassThrough, Transform); - - PassThrough.prototype._transform = function (chunk, encoding, cb) { - cb(null, chunk); - }; - - module.exports = PassThrough; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/pipeline.js -var require_pipeline = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/pipeline.js"(exports, module) { - "use strict"; - var { Promise: Promise2, SymbolAsyncIterator } = require_primordials(); - var eos = require_end_of_stream(); - var { once } = require_util(); - var destroyImpl = require_destroy(); - var { - aggregateTwoErrors, - codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_RETURN_VALUE, ERR_MISSING_ARGS, ERR_STREAM_DESTROYED }, - AbortError, - } = require_errors(); - var { isIterable, isReadable, isReadableNodeStream, isNodeStream } = require_utils(); - var PassThrough; - function destroyer(stream, reading, writing) { - let finished = false; - stream.on("close", () => { - finished = true; - }); - const cleanup = eos( - stream, - { - readable: reading, - writable: writing, - }, - err => { - finished = !err; - }, - ); - return { - destroy: err => { - if (finished) return; - finished = true; - destroyImpl.destroyer(stream, err || new ERR_STREAM_DESTROYED("pipe")); - }, - cleanup, - }; - } - function popCallback(streams) { - validateFunction(streams[streams.length - 1], "streams[stream.length - 1]"); - return streams.pop(); - } - function makeAsyncIterable(val) { - if (isIterable(val)) { - return val; - } else if (isReadableNodeStream(val)) { - return fromReadable(val); - } - throw new ERR_INVALID_ARG_TYPE("val", ["Readable", "Iterable", "AsyncIterable"], val); - } - async function* fromReadable(val) { - yield* Readable.prototype[SymbolAsyncIterator].$call(val); - } - async function pump(iterable, writable, finish, { end }) { - let error; - let onresolve = null; - const resume = err => { - if (err) { - error = err; - } - if (onresolve) { - const callback = onresolve; - onresolve = null; - callback(); - } - }; - const wait = () => - new Promise2((resolve, reject) => { - if (error) { - reject(error); - } else { - onresolve = () => { - if (error) { - reject(error); - } else { - resolve(); - } - }; - } - }); - writable.on("drain", resume); - const cleanup = eos( - writable, - { - readable: false, - }, - resume, - ); - try { - if (writable.writableNeedDrain) { - await wait(); - } - for await (const chunk of iterable) { - if (!writable.write(chunk)) { - await wait(); - } - } - if (end) { - writable.end(); - } - await wait(); - finish(); - } catch (err) { - finish(error !== err ? aggregateTwoErrors(error, err) : err); - } finally { - cleanup(); - writable.off("drain", resume); - } - } - function pipeline(...streams) { - return pipelineImpl(streams, once(popCallback(streams))); - } - function pipelineImpl(streams, callback, opts) { - if (streams.length === 1 && $isJSArray(streams[0])) { - streams = streams[0]; - } - if (streams.length < 2) { - throw new ERR_MISSING_ARGS("streams"); - } - const ac = new AbortController(); - const signal = ac.signal; - const outerSignal = opts === null || opts === void 0 ? void 0 : opts.signal; - const lastStreamCleanup = []; - validateAbortSignal(outerSignal, "options.signal"); - function abort() { - finishImpl(new AbortError()); - } - outerSignal === null || outerSignal === void 0 ? void 0 : outerSignal.addEventListener("abort", abort); - let error; - let value; - const destroys = []; - let finishCount = 0; - function finish(err) { - finishImpl(err, --finishCount === 0); - } - function finishImpl(err, final) { - if (err && (!error || error.code === "ERR_STREAM_PREMATURE_CLOSE")) { - error = err; - } - if (!error && !final) { - return; - } - while (destroys.length) { - destroys.shift()(error); - } - outerSignal === null || outerSignal === void 0 ? void 0 : outerSignal.removeEventListener("abort", abort); - ac.abort(); - if (final) { - if (!error) { - lastStreamCleanup.forEach(fn => fn()); - } - ProcessNextTick(callback, error, value); - } - } - let ret; - for (let i = 0; i < streams.length; i++) { - const stream = streams[i]; - const reading = i < streams.length - 1; - const writing = i > 0; - const end = reading || (opts === null || opts === void 0 ? void 0 : opts.end) !== false; - const isLastStream = i === streams.length - 1; - if (isNodeStream(stream)) { - let onError = function (err) { - if (err && err.name !== "AbortError" && err.code !== "ERR_STREAM_PREMATURE_CLOSE") { - finish(err); - } - }; - if (end) { - const { destroy, cleanup } = destroyer(stream, reading, writing); - destroys.push(destroy); - if (isReadable(stream) && isLastStream) { - lastStreamCleanup.push(cleanup); - } - } - stream.on("error", onError); - if (isReadable(stream) && isLastStream) { - lastStreamCleanup.push(() => { - stream.removeListener("error", onError); - }); - } - } - if (i === 0) { - if (typeof stream === "function") { - ret = stream({ - signal, - }); - if (!isIterable(ret)) { - throw new ERR_INVALID_RETURN_VALUE("Iterable, AsyncIterable or Stream", "source", ret); - } - } else if (isIterable(stream) || isReadableNodeStream(stream)) { - ret = stream; - } else { - ret = Duplex.from(stream); - } - } else if (typeof stream === "function") { - ret = makeAsyncIterable(ret); - ret = stream(ret, { - signal, - }); - if (reading) { - if (!isIterable(ret, true)) { - throw new ERR_INVALID_RETURN_VALUE("AsyncIterable", `transform[${i - 1}]`, ret); - } - } else { - var _ret; - if (!PassThrough) { - PassThrough = require_passthrough(); - } - const pt = new PassThrough({ - objectMode: true, - }); - const then = (_ret = ret) === null || _ret === void 0 ? void 0 : _ret.then; - if (typeof then === "function") { - finishCount++; - then.$call( - ret, - val => { - value = val; - if (val != null) { - pt.write(val); - } - if (end) { - pt.end(); - } - ProcessNextTick(finish); - }, - err => { - pt.destroy(err); - ProcessNextTick(finish, err); - }, - ); - } else if (isIterable(ret, true)) { - finishCount++; - pump(ret, pt, finish, { - end, - }); - } else { - throw new ERR_INVALID_RETURN_VALUE("AsyncIterable or Promise", "destination", ret); - } - ret = pt; - const { destroy, cleanup } = destroyer(ret, false, true); - destroys.push(destroy); - if (isLastStream) { - lastStreamCleanup.push(cleanup); - } - } - } else if (isNodeStream(stream)) { - if (isReadableNodeStream(ret)) { - finishCount += 2; - const cleanup = pipe(ret, stream, finish, { - end, - }); - if (isReadable(stream) && isLastStream) { - lastStreamCleanup.push(cleanup); - } - } else if (isIterable(ret)) { - finishCount++; - pump(ret, stream, finish, { - end, - }); - } else { - throw new ERR_INVALID_ARG_TYPE("val", ["Readable", "Iterable", "AsyncIterable"], ret); - } - ret = stream; - } else { - ret = Duplex.from(stream); - } - } - if ( - (signal !== null && signal !== void 0 && signal.aborted) || - (outerSignal !== null && outerSignal !== void 0 && outerSignal.aborted) - ) { - ProcessNextTick(abort); - } - return ret; - } - function pipe(src, dst, finish, { end }) { - src.pipe(dst, { - end, - }); - if (end) { - src.once("end", () => dst.end()); - } else { - finish(); - } - eos( - src, - { - readable: true, - writable: false, - }, - err => { - const rState = src._readableState; - if ( - err && - err.code === "ERR_STREAM_PREMATURE_CLOSE" && - rState && - rState.ended && - !rState.errored && - !rState.errorEmitted - ) { - src.once("end", finish).once("error", finish); - } else { - finish(err); - } - }, - ); - return eos( - dst, - { - readable: false, - writable: true, - }, - finish, - ); - } - module.exports = { - pipelineImpl, - pipeline, - }; - }, -}); - -// node_modules/readable-stream/lib/internal/streams/compose.js -var require_compose = __commonJS({ - "node_modules/readable-stream/lib/internal/streams/compose.js"(exports, module) { - "use strict"; - var { pipeline } = require_pipeline(); - var Duplex = require_duplex(); - var { destroyer } = require_destroy(); - var { isNodeStream, isReadable, isWritable } = require_utils(); - var { - AbortError, - codes: { ERR_INVALID_ARG_VALUE, ERR_MISSING_ARGS }, - } = require_errors(); - module.exports = function compose(...streams) { - if (streams.length === 0) { - throw new ERR_MISSING_ARGS("streams"); - } - if (streams.length === 1) { - return Duplex.from(streams[0]); - } - const orgStreams = [...streams]; - if (typeof streams[0] === "function") { - streams[0] = Duplex.from(streams[0]); - } - if (typeof streams[streams.length - 1] === "function") { - const idx = streams.length - 1; - streams[idx] = Duplex.from(streams[idx]); - } - for (let n = 0; n < streams.length; ++n) { - if (!isNodeStream(streams[n])) { - continue; - } - if (n < streams.length - 1 && !isReadable(streams[n])) { - throw new ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be readable"); - } - if (n > 0 && !isWritable(streams[n])) { - throw new ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be writable"); - } - } - let ondrain; - let onfinish; - let onreadable; - let onclose; - let d; - function onfinished(err) { - const cb = onclose; - onclose = null; - if (cb) { - cb(err); - } else if (err) { - d.destroy(err); - } else if (!readable && !writable) { - d.destroy(); - } - } - const head = streams[0]; - const tail = pipeline(streams, onfinished); - const writable = !!isWritable(head); - const readable = !!isReadable(tail); - d = new Duplex({ - writableObjectMode: !!(head !== null && head !== void 0 && head.writableObjectMode), - readableObjectMode: !!(tail !== null && tail !== void 0 && tail.writableObjectMode), - writable, - readable, - }); - if (writable) { - d._write = function (chunk, encoding, callback) { - if (head.write(chunk, encoding)) { - callback(); - } else { - ondrain = callback; - } - }; - d._final = function (callback) { - head.end(); - onfinish = callback; - }; - head.on("drain", function () { - if (ondrain) { - const cb = ondrain; - ondrain = null; - cb(); - } - }); - tail.on("finish", function () { - if (onfinish) { - const cb = onfinish; - onfinish = null; - cb(); - } - }); - } - if (readable) { - tail.on("readable", function () { - if (onreadable) { - const cb = onreadable; - onreadable = null; - cb(); - } - }); - tail.on("end", function () { - d.push(null); - }); - d._read = function () { - while (true) { - const buf = tail.read(); - if (buf === null) { - onreadable = d._read; - return; - } - if (!d.push(buf)) { - return; - } - } - }; - } - d._destroy = function (err, callback) { - if (!err && onclose !== null) { - err = new AbortError(); - } - onreadable = null; - ondrain = null; - onfinish = null; - if (onclose === null) { - callback(err); - } else { - onclose = callback; - destroyer(tail, err); - } - }; - return d; - }; - }, -}); - -// node_modules/readable-stream/lib/stream/promises.js -var require_promises = __commonJS({ - "node_modules/readable-stream/lib/stream/promises.js"(exports, module) { - "use strict"; - var { ArrayPrototypePop, Promise: Promise2 } = require_primordials(); - var { isIterable, isNodeStream } = require_utils(); - var { pipelineImpl: pl } = require_pipeline(); - var { finished } = require_end_of_stream(); - function pipeline(...streams) { - const { promise, resolve, reject } = $newPromiseCapability(Promise); - let signal; - let end; - const lastArg = streams[streams.length - 1]; - if (lastArg && typeof lastArg === "object" && !isNodeStream(lastArg) && !isIterable(lastArg)) { - const options = ArrayPrototypePop(streams); - signal = options.signal; - end = options.end; - } - pl( - streams, - (err, value) => { - if (err) { - reject(err); - } else { - resolve(value); - } - }, - { - signal, - end, - }, - ); - return promise; - } - module.exports = { - finished, - pipeline, - }; - }, -}); -// node_modules/readable-stream/lib/stream.js -var require_stream = __commonJS({ - "node_modules/readable-stream/lib/stream.js"(exports, module) { - "use strict"; - var { ObjectDefineProperty, ObjectKeys } = require_primordials(); - var { - promisify: { custom: customPromisify }, - } = require_util(); - - var { streamReturningOperators, promiseReturningOperators } = require_operators(); - var { - codes: { ERR_ILLEGAL_CONSTRUCTOR }, - } = require_errors(); - var compose = require_compose(); - var { pipeline } = require_pipeline(); - var { destroyer } = require_destroy(); - var eos = require_end_of_stream(); - var promises = require_promises(); - var utils = require_utils(); - var Stream = (module.exports = require_legacy().Stream); - Stream.isDisturbed = utils.isDisturbed; - Stream.isErrored = utils.isErrored; - Stream.isWritable = utils.isWritable; - Stream.isReadable = utils.isReadable; - Stream.Readable = require_readable(); - for (const key of ObjectKeys(streamReturningOperators)) { - let fn = function (...args) { - if (new.target) { - throw ERR_ILLEGAL_CONSTRUCTOR(); - } - return Stream.Readable.from(op.$apply(this, args)); - }; - const op = streamReturningOperators[key]; - ObjectDefineProperty(fn, "name", { - value: op.name, - }); - ObjectDefineProperty(fn, "length", { - value: op.length, - }); - ObjectDefineProperty(Stream.Readable.prototype, key, { - value: fn, - enumerable: false, - configurable: true, - writable: true, - }); - } - for (const key of ObjectKeys(promiseReturningOperators)) { - let fn = function (...args) { - if (new.target) { - throw ERR_ILLEGAL_CONSTRUCTOR(); - } - return op.$apply(this, args); - }; - const op = promiseReturningOperators[key]; - ObjectDefineProperty(fn, "name", { - value: op.name, - }); - ObjectDefineProperty(fn, "length", { - value: op.length, - }); - ObjectDefineProperty(Stream.Readable.prototype, key, { - value: fn, - enumerable: false, - configurable: true, - writable: true, - }); - } - Stream.Writable = require_writable(); - Stream.Duplex = require_duplex(); - Stream.Transform = require_transform(); - Stream.PassThrough = require_passthrough(); - Stream.pipeline = pipeline; - var { addAbortSignal } = require_add_abort_signal(); - Stream.addAbortSignal = addAbortSignal; - Stream.finished = eos; - Stream.destroy = destroyer; - Stream.compose = compose; - ObjectDefineProperty(Stream, "promises", { - configurable: true, - enumerable: true, - get() { - return promises; - }, - }); - ObjectDefineProperty(pipeline, customPromisify, { - enumerable: true, - get() { - return promises.pipeline; - }, - }); - ObjectDefineProperty(eos, customPromisify, { - enumerable: true, - get() { - return promises.finished; - }, - }); - Stream.Stream = Stream; - Stream._isUint8Array = function isUint8Array(value) { - return value instanceof Uint8Array; - }; - Stream._uint8ArrayToBuffer = function _uint8ArrayToBuffer(chunk) { - return new Buffer(chunk.buffer, chunk.byteOffset, chunk.byteLength); - }; - Stream.setDefaultHighWaterMark = setDefaultHighWaterMark; - Stream.getDefaultHighWaterMark = getDefaultHighWaterMark; - }, -}); - -var kEnsureConstructed = Symbol("kEnsureConstructed"); - -/** - * Bun native stream wrapper - * - * This glue code lets us avoid using ReadableStreams to wrap Bun internal streams - */ -function createNativeStreamReadable(Readable) { - var closer = [false]; - var handleNumberResult = function (nativeReadable, result, view, isClosed) { - if (result > 0) { - const slice = view.subarray(0, result); - const remainder = view.subarray(result); - if (slice.byteLength > 0) { - nativeReadable.push(slice); - } - - if (isClosed) { - nativeReadable.push(null); - } - - return remainder.byteLength > 0 ? remainder : undefined; - } - - if (isClosed) { - nativeReadable.push(null); - } - - return view; - }; - - var handleArrayBufferViewResult = function (nativeReadable, result, view, isClosed) { - if (result.byteLength > 0) { - nativeReadable.push(result); - } - - if (isClosed) { - nativeReadable.push(null); - } - - return view; - }; - - var DYNAMICALLY_ADJUST_CHUNK_SIZE = process.env.BUN_DISABLE_DYNAMIC_CHUNK_SIZE !== "1"; - - const MIN_BUFFER_SIZE = 512; - - const refCount = Symbol("refCount"); - const constructed = Symbol("constructed"); - const remainingChunk = Symbol("remainingChunk"); - const highWaterMark = Symbol("highWaterMark"); - const pendingRead = Symbol("pendingRead"); - const hasResized = Symbol("hasResized"); - - const _onClose = Symbol("_onClose"); - const _onDrain = Symbol("_onDrain"); - const _internalConstruct = Symbol("_internalConstruct"); - const _getRemainingChunk = Symbol("_getRemainingChunk"); - const _adjustHighWaterMark = Symbol("_adjustHighWaterMark"); - const _handleResult = Symbol("_handleResult"); - const _internalRead = Symbol("_internalRead"); - - function NativeReadable(this: typeof NativeReadable, ptr, options) { - if (!(this instanceof NativeReadable)) { - return new NativeReadable(path, options); - } - - this[refCount] = 0; - this[constructed] = false; - this[remainingChunk] = undefined; - this[pendingRead] = false; - this[hasResized] = !DYNAMICALLY_ADJUST_CHUNK_SIZE; - - options ??= {}; - Readable.$apply(this, [options]); - - if (typeof options.highWaterMark === "number") { - this[highWaterMark] = options.highWaterMark; - } else { - this[highWaterMark] = 256 * 1024; - } - this.$bunNativePtr = ptr; - this[constructed] = false; - this[remainingChunk] = undefined; - this[pendingRead] = false; - ptr.onClose = this[_onClose].bind(this); - ptr.onDrain = this[_onDrain].bind(this); - } - $toClass(NativeReadable, "NativeReadable", Readable); - - NativeReadable.prototype[_onClose] = function () { - this.push(null); - }; - - NativeReadable.prototype[_onDrain] = function (chunk) { - this.push(chunk); - }; - - // maxToRead is by default the highWaterMark passed from the Readable.read call to this fn - // However, in the case of an fs.ReadStream, we can pass the number of bytes we want to read - // which may be significantly less than the actual highWaterMark - NativeReadable.prototype._read = function _read(maxToRead) { - $debug("NativeReadable._read", this.__id); - if (this[pendingRead]) { - $debug("pendingRead is true", this.__id); - return; - } - var ptr = this.$bunNativePtr; - $debug("ptr @ NativeReadable._read", ptr, this.__id); - if (!ptr) { - this.push(null); - return; - } - if (!this[constructed]) { - $debug("NativeReadable not constructed yet", this.__id); - this[_internalConstruct](ptr); - } - return this[_internalRead](this[_getRemainingChunk](maxToRead), ptr); - }; - - NativeReadable.prototype[_internalConstruct] = function (ptr) { - $assert(this[constructed] === false); - this[constructed] = true; - - const result = ptr.start(this[highWaterMark]); - - $debug("NativeReadable internal `start` result", result, this.__id); - - if (typeof result === "number" && result > 1) { - this[hasResized] = true; - $debug("NativeReadable resized", this.__id); - - this[highWaterMark] = Math.min(this[highWaterMark], result); - } - - const drainResult = ptr.drain(); - $debug("NativeReadable drain result", drainResult, this.__id); - if ((drainResult?.byteLength ?? 0) > 0) { - this.push(drainResult); - } - }; - - // maxToRead can be the highWaterMark (by default) or the remaining amount of the stream to read - // This is so the consumer of the stream can terminate the stream early if they know - // how many bytes they want to read (ie. when reading only part of a file) - // ObjectDefinePrivateProperty(NativeReadable.prototype, "_getRemainingChunk", ); - NativeReadable.prototype[_getRemainingChunk] = function (maxToRead) { - maxToRead ??= this[highWaterMark]; - var chunk = this[remainingChunk]; - $debug("chunk @ #getRemainingChunk", chunk, this.__id); - if (chunk?.byteLength ?? 0 < MIN_BUFFER_SIZE) { - var size = maxToRead > MIN_BUFFER_SIZE ? maxToRead : MIN_BUFFER_SIZE; - this[remainingChunk] = chunk = new Buffer(size); - } - return chunk; - }; - - // ObjectDefinePrivateProperty(NativeReadable.prototype, "_adjustHighWaterMark", ); - NativeReadable.prototype[_adjustHighWaterMark] = function () { - this[highWaterMark] = Math.min(this[highWaterMark] * 2, 1024 * 1024 * 2); - this[hasResized] = true; - $debug("Resized", this.__id); - }; - - // ObjectDefinePrivateProperty(NativeReadable.prototype, "_handleResult", ); - NativeReadable.prototype[_handleResult] = function (result, view, isClosed) { - $debug("result, isClosed @ #handleResult", result, isClosed, this.__id); - - if (typeof result === "number") { - if (result >= this[highWaterMark] && !this[hasResized] && !isClosed) { - this[_adjustHighWaterMark](); - } - return handleNumberResult(this, result, view, isClosed); - } else if (typeof result === "boolean") { - ProcessNextTick(() => { - this.push(null); - }); - return (view?.byteLength ?? 0 > 0) ? view : undefined; - } else if ($isTypedArrayView(result)) { - if (result.byteLength >= this[highWaterMark] && !this[hasResized] && !isClosed) { - this[_adjustHighWaterMark](); - } - - return handleArrayBufferViewResult(this, result, view, isClosed); - } else { - $debug("Unknown result type", result, this.__id); - throw new Error("Invalid result from pull"); - } - }; - - NativeReadable.prototype[_internalRead] = function (view, ptr) { - $debug("#internalRead()", this.__id); - closer[0] = false; - var result = ptr.pull(view, closer); - if ($isPromise(result)) { - this[pendingRead] = true; - return result.then( - result => { - this[pendingRead] = false; - $debug("pending no longerrrrrrrr (result returned from pull)", this.__id); - const isClosed = closer[0]; - this[remainingChunk] = this[_handleResult](result, view, isClosed); - }, - reason => { - $debug("error from pull", reason, this.__id); - errorOrDestroy(this, reason); - }, - ); - } else { - this[remainingChunk] = this[_handleResult](result, view, closer[0]); - } - }; - - NativeReadable.prototype._destroy = function (error, callback) { - var ptr = this.$bunNativePtr; - if (!ptr) { - callback(error); - return; - } - - this.$bunNativePtr = undefined; - ptr.updateRef(false); - - $debug("NativeReadable destroyed", this.__id); - ptr.cancel(error); - callback(error); - }; - - NativeReadable.prototype.ref = function () { - var ptr = this.$bunNativePtr; - if (ptr === undefined) return; - if (this[refCount]++ === 0) { - ptr.updateRef(true); - } - }; - - NativeReadable.prototype.unref = function () { - var ptr = this.$bunNativePtr; - if (ptr === undefined) return; - if (this[refCount]-- === 1) { - ptr.updateRef(false); - } - }; - - NativeReadable.prototype[kEnsureConstructed] = function () { - if (this[constructed]) return; - this[_internalConstruct](this.$bunNativePtr); - }; - - return NativeReadable; -} - var nativeReadableStreamPrototypes = { 0: undefined, 1: undefined, @@ -5615,176 +16,21 @@ var nativeReadableStreamPrototypes = { }; function getNativeReadableStreamPrototype(nativeType, Readable) { - return (nativeReadableStreamPrototypes[nativeType] ??= createNativeStreamReadable(Readable)); -} - -function getNativeReadableStream(Readable, stream, options) { - const ptr = stream.$bunNativePtr; - if (!ptr || ptr === -1) { - $debug("no native readable stream"); - return undefined; - } - const type = stream.$bunNativeType; - $assert(typeof type === "number", "Invalid native type"); - $assert(typeof ptr === "object", "Invalid native ptr"); - - const NativeReadable = getNativeReadableStreamPrototype(type, Readable); - // https://github.com/oven-sh/bun/pull/12801 - // https://github.com/oven-sh/bun/issues/9555 - // There may be a ReadableStream.Strong handle to the ReadableStream. - // We can't update those handles to point to the NativeReadable from JS - // So we instead mark it as no longer usable, and create a new NativeReadable - transferToNativeReadable(stream); - - return new NativeReadable(ptr, options); + return (nativeReadableStreamPrototypes[nativeType] ??= require("internal/streams/nativereadable")()); } /** --- Bun native stream wrapper --- */ -const _pathOrFdOrSink = Symbol("pathOrFdOrSink"); -const { fileSinkSymbol: _fileSink } = require("internal/shared"); -const _native = Symbol("native"); +exports[kGetNativeReadableProto] = getNativeReadableStreamPrototype; +exports.NativeWritable = require("internal/streams/nativewritable"); -function NativeWritable(pathOrFdOrSink, options = {}) { - Writable.$call(this, options); - - this[_native] = true; - - this._construct = NativeWritable_internalConstruct; - this._final = NativeWritable_internalFinal; - this._write = NativeWritablePrototypeWrite; - - this[_pathOrFdOrSink] = pathOrFdOrSink; -} -$toClass(NativeWritable, "NativeWritable", Writable); - -// These are confusingly two different fns for construct which initially were the same thing because -// `_construct` is part of the lifecycle of Writable and is not called lazily, -// so we need to separate our _construct for Writable state and actual construction of the write stream -function NativeWritable_internalConstruct(cb) { - this._writableState.constructed = true; - this.constructed = true; - if (typeof cb === "function") ProcessNextTick(cb); - ProcessNextTick(() => { - this.emit("open", this.fd); - this.emit("ready"); - }); -} - -function NativeWritable_lazyConstruct(stream) { - // TODO: Turn this check into check for instanceof FileSink - var sink = stream[_pathOrFdOrSink]; - if (typeof sink === "object") { - if (typeof sink.write === "function") { - return (stream[_fileSink] = sink); - } else { - throw new Error("Invalid FileSink"); - } - } else { - return (stream[_fileSink] = Bun.file(sink).writer()); - } -} - -function NativeWritablePrototypeWrite(chunk, encoding, cb) { - var fileSink = this[_fileSink] ?? NativeWritable_lazyConstruct(this); - var result = fileSink.write(chunk); - - if (typeof encoding === "function") { - cb = encoding; - } - - if ($isPromise(result)) { - // var writePromises = this.#writePromises; - // var i = writePromises.length; - // writePromises[i] = result; - result - .then(result => { - this.emit("drain"); - if (cb) { - cb(null, result); - } - }) - .catch( - cb - ? err => { - cb(err); - } - : err => { - this.emit("error", err); - }, - ); - return false; - } - - // TODO: Should we just have a calculation based on encoding and length of chunk? - if (cb) cb(null, chunk.byteLength); - return true; -} - -const WritablePrototypeEnd = Writable.prototype.end; -NativeWritable.prototype.end = function end(chunk, encoding, cb, native) { - return WritablePrototypeEnd.$call(this, chunk, encoding, cb, native ?? this[_native]); -}; - -NativeWritable.prototype._destroy = function (error, cb) { - const w = this._writableState; - const r = this._readableState; - - if (w) { - w.destroyed = true; - w.closeEmitted = true; - } - if (r) { - r.destroyed = true; - r.closeEmitted = true; - } - - if (typeof cb === "function") cb(error); - - if (w?.closeEmitted || r?.closeEmitted) { - this.emit("close"); - } -}; - -function NativeWritable_internalFinal(cb) { - var sink = this[_fileSink]; - if (sink) { - const end = sink.end(true); - if ($isPromise(end) && cb) { - end.then(() => { - if (cb) cb(); - }, cb); - } - } - if (cb) cb(); -} - -NativeWritable.prototype.ref = function ref() { - const sink = (this[_fileSink] ||= NativeWritable_lazyConstruct(this)); - sink.ref(); - return this; -}; - -NativeWritable.prototype.unref = function unref() { - const sink = (this[_fileSink] ||= NativeWritable_lazyConstruct(this)); - sink.unref(); - return this; -}; - -const exports = require_stream(); -const promises = require_promises(); -exports._getNativeReadableStreamPrototype = getNativeReadableStreamPrototype; -exports.NativeWritable = NativeWritable; -Object.defineProperty(exports, "promises", { - configurable: true, - enumerable: true, - get() { - return promises; - }, -}); +const { + newStreamReadableFromReadableStream: _ReadableFromWeb, + _ReadableFromWeb: _ReadableFromWebForUndici, +} = require("internal/webstreams_adapters"); exports[Symbol.for("::bunternal::")] = { _ReadableFromWeb, _ReadableFromWebForUndici, kEnsureConstructed }; -exports.eos = require_end_of_stream(); +exports.eos = require("internal/streams/end-of-stream"); exports.EventEmitter = EE; export default exports; diff --git a/src/js/node/test.ts b/src/js/node/test.ts new file mode 100644 index 0000000000..01470f3c9c --- /dev/null +++ b/src/js/node/test.ts @@ -0,0 +1,38 @@ +// Hardcoded module "node:test" + +const { throwNotImplemented } = require("internal/shared"); + +function suite() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function test() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function before() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function after() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function beforeEach() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function afterEach() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +export default { + suite, + test, + describe: suite, + it: test, + before, + after, + beforeEach, + afterEach, +}; diff --git a/src/js/node/timers.promises.ts b/src/js/node/timers.promises.ts index 68ac1fa3f6..97c302d1ca 100644 --- a/src/js/node/timers.promises.ts +++ b/src/js/node/timers.promises.ts @@ -1,30 +1,10 @@ // Hardcoded module "node:timers/promises" // https://github.com/niksy/isomorphic-timers-promises/blob/master/index.js -const { validateBoolean, validateAbortSignal } = require("internal/validators"); +const { validateBoolean, validateAbortSignal, validateObject } = require("internal/validators"); const symbolAsyncIterator = Symbol.asyncIterator; -class ERR_INVALID_ARG_TYPE extends Error { - constructor(name, expected, actual) { - super(`${name} must be ${expected}, ${typeof actual} given`); - this.code = "ERR_INVALID_ARG_TYPE"; - } -} - -class AbortError extends Error { - constructor() { - super("The operation was aborted"); - this.code = "ABORT_ERR"; - } -} - -function validateObject(object, name) { - if (object === null || typeof object !== "object") { - throw new ERR_INVALID_ARG_TYPE(name, "Object", object); - } -} - function asyncIterator({ next: nextFunction, return: returnFunction }) { const result = {}; if (typeof nextFunction === "function") { @@ -59,7 +39,7 @@ function setTimeoutPromise(after = 1, value, options = {}) { return Promise.reject(error); } if (signal?.aborted) { - return Promise.reject(new AbortError()); + return Promise.reject($makeAbortError()); } let onCancel; const returnValue = new Promise((resolve, reject) => { @@ -70,7 +50,7 @@ function setTimeoutPromise(after = 1, value, options = {}) { if (signal) { onCancel = () => { clearTimeout(timeout); - reject(new AbortError()); + reject($makeAbortError()); }; signal.addEventListener("abort", onCancel); } @@ -98,7 +78,7 @@ function setImmediatePromise(value, options = {}) { return Promise.reject(error); } if (signal?.aborted) { - return Promise.reject(new AbortError()); + return Promise.reject($makeAbortError()); } let onCancel; const returnValue = new Promise((resolve, reject) => { @@ -109,7 +89,7 @@ function setImmediatePromise(value, options = {}) { if (signal) { onCancel = () => { clearImmediate(immediate); - reject(new AbortError()); + reject($makeAbortError()); }; signal.addEventListener("abort", onCancel); } @@ -152,7 +132,7 @@ function setIntervalPromise(after = 1, value, options = {}) { if (signal?.aborted) { return asyncIterator({ next: function () { - return Promise.reject(new AbortError()); + return Promise.reject($makeAbortError()); }, }); } @@ -193,7 +173,7 @@ function setIntervalPromise(after = 1, value, options = {}) { resolve(); } } else if (notYielded === 0) { - reject(new AbortError()); + reject($makeAbortError()); } else { resolve(); } diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index 46d0fa1113..5dae2b5cd0 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -1,5 +1,5 @@ // Hardcoded module "node:tls" -const { isArrayBufferView, isTypedArray } = require("node:util/types"); +const { isArrayBufferView, isArrayBuffer, isTypedArray } = require("node:util/types"); const { addServerName } = require("../internal/net"); const net = require("node:net"); const { Duplex } = require("node:stream"); @@ -8,6 +8,12 @@ const { Server: NetServer, [Symbol.for("::bunternal::")]: InternalTCPSocket } = const { rootCertificates, canonicalizeIP } = $cpp("NodeTLS.cpp", "createNodeTLSBinding"); +const { + ERR_TLS_CERT_ALTNAME_INVALID, + ERR_TLS_CERT_ALTNAME_FORMAT, + ERR_TLS_SNI_FROM_SERVER, +} = require("internal/errors"); + const SymbolReplace = Symbol.replace; const RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace]; const RegExpPrototypeExec = RegExp.prototype.exec; @@ -38,12 +44,11 @@ function parseCertString() { const rejectUnauthorizedDefault = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0" && process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "false"; function isValidTLSArray(obj) { - if (typeof obj === "string" || isTypedArray(obj) || obj instanceof ArrayBuffer || obj instanceof Blob) return true; + if (typeof obj === "string" || isTypedArray(obj) || isArrayBuffer(obj) || $inheritsBlob(obj)) return true; if (Array.isArray(obj)) { for (var i = 0; i < obj.length; i++) { const item = obj[i]; - if (typeof item !== "string" && !isTypedArray(item) && !(item instanceof ArrayBuffer) && !(item instanceof Blob)) - return false; + if (typeof item !== "string" && !isTypedArray(item) && !isArrayBuffer(item) && !$inheritsBlob(item)) return false; } return true; } @@ -134,9 +139,7 @@ function splitEscapedAltNames(altNames) { currentToken += StringPrototypeSubstring.$call(altNames, offset, nextQuote); const match = RegExpPrototypeExec.$call(jsonStringPattern, StringPrototypeSubstring.$call(altNames, nextQuote)); if (!match) { - let error = new SyntaxError("ERR_TLS_CERT_ALTNAME_FORMAT: Invalid subject alternative name string"); - error.code = "ERR_TLS_CERT_ALTNAME_FORMAT"; - throw error; + throw $ERR_TLS_CERT_ALTNAME_FORMAT("Invalid subject alternative name string"); } currentToken += JSON.parse(match[0]); offset = nextQuote + match[0].length; @@ -203,8 +206,7 @@ function checkServerIdentity(hostname, cert) { reason = "Cert does not contain a DNS name"; } if (!valid) { - let error = new Error(`ERR_TLS_CERT_ALTNAME_INVALID: Hostname/IP does not match certificate's altnames: ${reason}`); - error.name = "ERR_TLS_CERT_ALTNAME_INVALID"; + let error = $ERR_TLS_CERT_ALTNAME_INVALID(`Hostname/IP does not match certificate's altnames: ${reason}`); error.reason = reason; error.host = hostname; error.cert = cert; @@ -281,28 +283,6 @@ function createSecureContext(options) { // javascript object representations before passing them back to the user. Can // be used on any cert object, but changing the name would be semver-major. function translatePeerCertificate(c) { - if (!c) return null; - - if (c.issuerCertificate != null && c.issuerCertificate !== c) { - c.issuerCertificate = translatePeerCertificate(c.issuerCertificate); - } - if (c.infoAccess != null) { - const info = c.infoAccess; - c.infoAccess = { __proto__: null }; - // XXX: More key validation? - RegExpPrototypeSymbolReplace.$call(/([^\n:]*):([^\n]*)(?:\n|$)/g, info, (all, key, val) => { - if (val.charCodeAt(0) === 0x22) { - // The translatePeerCertificate function is only - // used on internally created legacy certificate - // objects, and any value that contains a quote - // will always be a valid JSON string literal, - // so this should never throw. - val = JSONParse(val); - } - if (key in c.infoAccess) ArrayPrototypePush.$call(c.infoAccess[key], val); - else c.infoAccess[key] = [val]; - }); - } return c; } @@ -469,9 +449,7 @@ const TLSSocket = (function (InternalTLSSocket) { setServername(name) { if (this.isServer) { - let error = new Error("ERR_TLS_SNI_FROM_SERVER: Cannot issue SNI from a TLS server-side socket"); - error.name = "ERR_TLS_SNI_FROM_SERVER"; - throw error; + throw $ERR_TLS_SNI_FROM_SERVER("Cannot issue SNI from a TLS server-side socket"); } // if the socket is detached we can't set the servername but we set this property so when open will auto set to it this.servername = name; @@ -498,10 +476,10 @@ const TLSSocket = (function (InternalTLSSocket) { } } getPeerX509Certificate() { - throw Error("Not implented in Bun yet"); + return this._handle?.getPeerX509Certificate?.(); } getX509Certificate() { - throw Error("Not implented in Bun yet"); + return this._handle?.getX509Certificate?.(); } [buntls](port, host) { @@ -520,23 +498,27 @@ const TLSSocket = (function (InternalTLSSocket) { ); let CLIENT_RENEG_LIMIT = 3, CLIENT_RENEG_WINDOW = 600; -class Server extends NetServer { - key; - cert; - ca; - passphrase; - secureOptions; - _rejectUnauthorized = rejectUnauthorizedDefault; - _requestCert; - servername; - ALPNProtocols; - #contexts: Map | null = null; - constructor(options, secureConnectionListener) { - super(options, secureConnectionListener); - this.setSecureContext(options); +function Server(options, secureConnectionListener): void { + if (!(this instanceof Server)) { + return new Server(options, secureConnectionListener); } - addContext(hostname: string, context: typeof InternalSecureContext | object) { + + NetServer.$apply(this, [options, secureConnectionListener]); + + this.key = undefined; + this.cert = undefined; + this.ca = undefined; + this.passphrase = undefined; + this.secureOptions = undefined; + this._rejectUnauthorized = rejectUnauthorizedDefault; + this._requestCert = undefined; + this.servername = undefined; + this.ALPNProtocols = undefined; + + let contexts: Map | null = null; + + this.addContext = function (hostname, context) { if (typeof hostname !== "string") { throw new TypeError("hostname must be a string"); } @@ -546,11 +528,12 @@ class Server extends NetServer { if (this._handle) { addServerName(this._handle, hostname, context); } else { - if (!this.#contexts) this.#contexts = new Map(); - this.#contexts.set(hostname, context as typeof InternalSecureContext); + if (!contexts) contexts = new Map(); + contexts.set(hostname, context); } - } - setSecureContext(options) { + }; + + this.setSecureContext = function (options) { if (options instanceof InternalSecureContext) { options = options.context; } @@ -619,17 +602,17 @@ class Server extends NetServer { this._rejectUnauthorized = rejectUnauthorized; } else this._rejectUnauthorized = rejectUnauthorizedDefault; } - } + }; - getTicketKeys() { + Server.prototype.getTicketKeys = function () { throw Error("Not implented in Bun yet"); - } + }; - setTicketKeys() { + Server.prototype.setTicketKeys = function () { throw Error("Not implented in Bun yet"); - } + }; - [buntls](port, host, isClient) { + this[buntls] = function (port, host, isClient) { return [ { serverName: this.servername || host || "localhost", @@ -643,12 +626,15 @@ class Server extends NetServer { ALPNProtocols: this.ALPNProtocols, clientRenegotiationLimit: CLIENT_RENEG_LIMIT, clientRenegotiationWindow: CLIENT_RENEG_WINDOW, - contexts: this.#contexts, + contexts: contexts, }, SocketClass, ]; - } + }; + + this.setSecureContext(options); } +$toClass(Server, "Server", NetServer); function createServer(options, connectionListener) { return new Server(options, connectionListener); diff --git a/src/js/node/trace_events.ts b/src/js/node/trace_events.ts index 762a565b78..37fe030ae5 100644 --- a/src/js/node/trace_events.ts +++ b/src/js/node/trace_events.ts @@ -5,16 +5,10 @@ class Tracing { categories = ""; } -function ERR_INVALID_ARG_TYPE(name, type, value) { - const err = new TypeError(`The "${name}" argument must be of type ${type}. Received ${value}`); - err.code = "ERR_INVALID_ARG_TYPE"; - return err; -} - function createTracing(opts) { if (typeof opts !== "object" || opts == null) { // @ts-ignore - throw new ERR_INVALID_ARG_TYPE("options", "object", opts); + throw $ERR_INVALID_ARG_TYPE("options", "object", opts); } // TODO: validate categories diff --git a/src/js/node/tty.ts b/src/js/node/tty.ts index 6bd0611a32..1daa2eaa39 100644 --- a/src/js/node/tty.ts +++ b/src/js/node/tty.ts @@ -6,9 +6,6 @@ const { const { validateInteger } = require("internal/validators"); -// primordials -const NumberIsInteger = Number.isInteger; - function ReadStream(fd) { if (!(this instanceof ReadStream)) { return new ReadStream(fd); @@ -80,61 +77,6 @@ Object.defineProperty(ReadStream, "prototype", { configurable: true, }); -let OSRelease; - -const COLORS_2 = 1; -const COLORS_16 = 4; -const COLORS_256 = 8; -const COLORS_16m = 24; - -// Some entries were taken from `dircolors` -// (https://linux.die.net/man/1/dircolors). The corresponding terminals might -// support more than 16 colors, but this was not tested for. -// -// Copyright (C) 1996-2016 Free Software Foundation, Inc. Copying and -// distribution of this file, with or without modification, are permitted -// provided the copyright notice and this notice are preserved. -const TERM_ENVS = { - "eterm": COLORS_16, - "cons25": COLORS_16, - "console": COLORS_16, - "cygwin": COLORS_16, - "dtterm": COLORS_16, - "gnome": COLORS_16, - "hurd": COLORS_16, - "jfbterm": COLORS_16, - "konsole": COLORS_16, - "kterm": COLORS_16, - "mlterm": COLORS_16, - "mosh": COLORS_16m, - "putty": COLORS_16, - "st": COLORS_16, - // https://github.com/da-x/rxvt-unicode/tree/v9.22-with-24bit-color - "rxvt-unicode-24bit": COLORS_16m, - // https://gist.github.com/XVilka/8346728#gistcomment-2823421 - "terminator": COLORS_16m, -}; - -const TERM_ENVS_REG_EXP = [/ansi/, /color/, /linux/, /^con[0-9]*x[0-9]/, /^rxvt/, /^screen/, /^xterm/, /^vt100/]; - -let warned = false; -function warnOnDeactivatedColors(env) { - if (warned) return; - let name = ""; - if (env.NODE_DISABLE_COLORS !== undefined) name = "NODE_DISABLE_COLORS"; - if (env.NO_COLOR !== undefined) { - if (name !== "") { - name += "' and '"; - } - name += "NO_COLOR"; - } - - if (name !== "") { - process.emitWarning(`The '${name}' env is ignored due to the 'FORCE_COLOR' env being set.`, "Warning"); - warned = true; - } -} - function WriteStream(fd) { if (!(this instanceof WriteStream)) return new WriteStream(fd); if (fd >> 0 !== fd || fd < 0) throw new RangeError("fd must be a positive integer"); @@ -190,112 +132,7 @@ Object.defineProperty(WriteStream, "prototype", { // https://github.com/chalk/supports-color, // https://github.com/isaacs/color-support. WriteStream.prototype.getColorDepth = function (env = process.env) { - // Use level 0-3 to support the same levels as `chalk` does. This is done for - // consistency throughout the ecosystem. - if (env.FORCE_COLOR !== undefined) { - switch (env.FORCE_COLOR) { - case "": - case "1": - case "true": - warnOnDeactivatedColors(env); - return COLORS_16; - case "2": - warnOnDeactivatedColors(env); - return COLORS_256; - case "3": - warnOnDeactivatedColors(env); - return COLORS_16m; - default: - return COLORS_2; - } - } - - if ( - env.NODE_DISABLE_COLORS !== undefined || - // See https://no-color.org/ - env.NO_COLOR !== undefined || - // The "dumb" special terminal, as defined by terminfo, doesn't support - // ANSI color control codes. - // See https://invisible-island.net/ncurses/terminfo.ti.html#toc-_Specials - env.TERM === "dumb" - ) { - return COLORS_2; - } - - if (process.platform === "win32") { - // Lazy load for startup performance. - if (OSRelease === undefined) { - const { release } = require("node:os"); - OSRelease = release().split("."); - } - // Windows 10 build 10586 is the first Windows release that supports 256 - // colors. Windows 10 build 14931 is the first release that supports - // 16m/TrueColor. - if (+OSRelease[0] >= 10) { - const build = +OSRelease[2]; - if (build >= 14931) return COLORS_16m; - if (build >= 10586) return COLORS_256; - } - - return COLORS_16; - } - - if (env.TMUX) { - return COLORS_256; - } - - if (env.CI) { - if ( - ["APPVEYOR", "BUILDKITE", "CIRCLECI", "DRONE", "GITHUB_ACTIONS", "GITLAB_CI", "TRAVIS"].some( - sign => sign in env, - ) || - env.CI_NAME === "codeship" - ) { - return COLORS_256; - } - return COLORS_2; - } - - if ("TEAMCITY_VERSION" in env) { - return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? COLORS_16 : COLORS_2; - } - - switch (env.TERM_PROGRAM) { - case "iTerm.app": - if (!env.TERM_PROGRAM_VERSION || /^[0-2]\./.test(env.TERM_PROGRAM_VERSION)) { - return COLORS_256; - } - return COLORS_16m; - case "HyperTerm": - case "MacTerm": - return COLORS_16m; - case "Apple_Terminal": - return COLORS_256; - } - - if (env.COLORTERM === "truecolor" || env.COLORTERM === "24bit") { - return COLORS_16m; - } - - if (env.TERM) { - if (/^xterm-256/.test(env.TERM) !== null) { - return COLORS_256; - } - - const termEnv = env.TERM.toLowerCase(); - - if (TERM_ENVS[termEnv]) { - return TERM_ENVS[termEnv]; - } - if (TERM_ENVS_REG_EXP.some(term => term.test(termEnv))) { - return COLORS_16; - } - } - // Move 16 color COLORTERM below 16m and 256 - if (env.COLORTERM) { - return COLORS_16; - } - return COLORS_2; + return require("internal/tty").getColorDepth(env); }; WriteStream.prototype.getWindowSize = function () { @@ -324,15 +161,3 @@ Object.defineProperty(WriteStream, "prototype", { }); export default { ReadStream, WriteStream, isatty }; - -function ERR_INVALID_ARG_TYPE(name, type, value) { - const err = new TypeError(`The "${name}" argument must be of type ${type}. Received ${value?.toString()}`); - err.code = "ERR_INVALID_ARG_TYPE"; - return err; -} - -function ERR_OUT_OF_RANGE(name, range, value) { - const err = new RangeError(`The "${name}" argument is out of range. It must be ${range}. Received ${value}`); - err.code = "ERR_OUT_OF_RANGE"; - return err; -} diff --git a/src/js/node/url.ts b/src/js/node/url.ts index d969ab08ff..bef41376f3 100644 --- a/src/js/node/url.ts +++ b/src/js/node/url.ts @@ -1,3 +1,5 @@ +/// + /* * Copyright Joyent, Inc. and other Node contributors. * @@ -26,6 +28,12 @@ const { URL, URLSearchParams } = globalThis; const [domainToASCII, domainToUnicode] = $cpp("NodeURL.cpp", "Bun::createNodeURLBinding"); const { urlToHttpOptions } = require("internal/url"); +const { validateString } = require("internal/validators"); + +var _lazyUtil; +function lazyUtil(): (typeof import("internal/util"))["default"] { + return (_lazyUtil ||= require("internal/util")); +} function Url() { this.protocol = null; @@ -97,44 +105,113 @@ var protocolPattern = /^([a-z0-9.+-]+:)/i, "file:": true, }; -function urlParse(url, parseQueryString, slashesDenoteHost) { - if (url && typeof url === "object" && url instanceof Url) { - return url; +let urlParseWarned = false; +function urlParse( + url: string | URL | Url, // really has unknown type but intellisense is nice + parseQueryString?: boolean, + slashesDenoteHost?: boolean, +) { + if (!urlParseWarned && !lazyUtil().isInsideNodeModules()) { + urlParseWarned = true; + process.emitWarning( + "`url.parse()` behavior is not standardized and prone to " + + "errors that have security implications. Use the WHATWG URL API " + + "instead. CVEs are not issued for `url.parse()` vulnerabilities.", + "DeprecationWarning", + "DEP0169", + ); } + if ($isObject(url) && url instanceof Url) return url; + var u = new Url(); - u.parse(url, parseQueryString, slashesDenoteHost); + try { + u.parse(url, parseQueryString, slashesDenoteHost); + } catch (e) { + $putByIdDirect(e, "input", url); + throw e; + } return u; } -Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { - if (typeof url !== "string") { - throw new TypeError("Parameter 'url' must be a string, not " + typeof url); - } +Url.prototype.parse = function parse(url: string, parseQueryString?: boolean, slashesDenoteHost?: boolean) { + validateString(url, "url"); /* * Copy chrome, IE, opera backslash-handling behavior. * Back slashes before the query string get converted to forward slashes * See: https://code.google.com/p/chromium/issues/detail?id=25916 */ - var queryIndex = url.indexOf("?"), - splitter = queryIndex !== -1 && queryIndex < url.indexOf("#") ? "?" : "#", - uSplit = url.split(splitter), - slashRegex = /\\/g; - uSplit[0] = uSplit[0].replace(slashRegex, "/"); - url = uSplit.join(splitter); + let hasHash = false; + let hasAt = false; + let start = -1; + let end = -1; + let rest = ""; + let lastPos = 0; + for (let i = 0, inWs = false, split = false; i < url.length; ++i) { + const code = url.$charCodeAt(i); - var rest = url; + // Find first and last non-whitespace characters for trimming + const isWs = code < 33 || code === Char.NO_BREAK_SPACE || code === Char.ZERO_WIDTH_NOBREAK_SPACE; + if (start === -1) { + if (isWs) continue; + lastPos = start = i; + } else if (inWs) { + if (!isWs) { + end = -1; + inWs = false; + } + } else if (isWs) { + end = i; + inWs = true; + } - /* - * trim before proceeding. - * This is to support parse stuff like " http://foo.com \n" - */ - rest = rest.trim(); + // Only convert backslashes while we haven't seen a split character + if (!split) { + switch (code) { + case Char.AT: + hasAt = true; + break; + case Char.HASH: + hasHash = true; + // Fall through + case Char.QUESTION_MARK: + split = true; + break; + case Char.BACKWARD_SLASH: + if (i - lastPos > 0) rest += url.slice(lastPos, i); + rest += "/"; + lastPos = i + 1; + break; + } + } else if (!hasHash && code === Char.HASH) { + hasHash = true; + } + } - if (!slashesDenoteHost && url.split("#").length === 1) { + // Check if string was non-empty (including strings with only whitespace) + if (start !== -1) { + if (lastPos === start) { + // We didn't convert any backslashes + + if (end === -1) { + if (start === 0) rest = url; + else rest = url.slice(start); + } else { + rest = url.slice(start, end); + } + } else if (end === -1 && lastPos < url.length) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos); + } else if (end !== -1 && lastPos < end) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos, end); + } + } + + if (!slashesDenoteHost && !hasHash && !hasAt) { // Try fast path regexp - var simplePath = simplePathPattern.exec(rest); + const simplePath = simplePathPattern.exec(rest); if (simplePath) { this.path = rest; this.href = rest; @@ -142,24 +219,24 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { if (simplePath[2]) { this.search = simplePath[2]; if (parseQueryString) { - this.query = new URLSearchParams(this.search.substr(1)).toJSON(); + this.query = new URLSearchParams(this.search.slice(1)).toJSON(); } else { - this.query = this.search.substr(1); + this.query = this.search.slice(1); } } else if (parseQueryString) { - this.search = ""; - this.query = {}; + this.search = null; + this.query = { __proto__: null }; } return this; } } - var proto = protocolPattern.exec(rest); + var proto: any = protocolPattern.exec(rest); if (proto) { proto = proto[0]; var lowerProto = proto.toLowerCase(); this.protocol = lowerProto; - rest = rest.substr(proto.length); + rest = rest.substring(proto.length); } /* @@ -169,9 +246,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { * how the browser resolves relative URLs. */ if (slashesDenoteHost || proto || rest.match(/^\/\/[^@/]+@[^@/]+/)) { - var slashes = rest.substr(0, 2) === "//"; + var slashes = rest.substring(0, 2) === "//"; if (slashes && !(proto && hostlessProtocol[proto])) { - rest = rest.substr(2); + rest = rest.substring(2); this.slashes = true; } } @@ -209,7 +286,7 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { * at this point, either we have an explicit point where the * auth portion cannot go past, or the last @ char is the decider. */ - var auth, atSign; + var auth: string | undefined, atSign: number; if (hostEnd === -1) { // atSign can be anywhere. atSign = rest.lastIndexOf("@"); @@ -254,53 +331,20 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { * we've indicated that there is a hostname, * so even if it's empty, it has to be present. */ - this.hostname = this.hostname || ""; + if (typeof this.hostname !== "string") { + this.hostname = ""; + } + const hostname = this.hostname; /* * if hostname begins with [ and ends with ] * assume that it's an IPv6 address. */ - var ipv6Hostname = this.hostname[0] === "[" && this.hostname[this.hostname.length - 1] === "]"; + var ipv6Hostname = isIpv6Hostname(this.hostname); // validate a little. if (!ipv6Hostname) { - var hostparts = this.hostname.split(/\./); - for (var i = 0, l = hostparts.length; i < l; i++) { - var part = hostparts[i]; - if (!part) { - continue; - } - if (!part.match(hostnamePartPattern)) { - var newpart = ""; - for (var j = 0, k = part.length; j < k; j++) { - if (part.charCodeAt(j) > 127) { - /* - * we replace non-ASCII char with a temporary placeholder - * we need this to make sure size of hostname is not - * broken by replacing non-ASCII by nothing - */ - newpart += "x"; - } else { - newpart += part[j]; - } - } - // we test again with ASCII char only - if (!newpart.match(hostnamePartPattern)) { - var validParts = hostparts.slice(0, i); - var notHost = hostparts.slice(i + 1); - var bit = part.match(hostnamePartStart); - if (bit) { - validParts.push(bit[1]); - notHost.unshift(bit[2]); - } - if (notHost.length) { - rest = "/" + notHost.join(".") + rest; - } - this.hostname = validParts.join("."); - break; - } - } - } + rest = getHostname(this, rest, hostname, url); } if (this.hostname.length > hostnameMaxLen) { @@ -330,7 +374,7 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { * the host field still retains them, though */ if (ipv6Hostname) { - this.hostname = this.hostname.substr(1, this.hostname.length - 2); + this.hostname = this.hostname.slice(1, -1); if (rest[0] !== "/") { rest = "/" + rest; } @@ -364,13 +408,13 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { var hash = rest.indexOf("#"); if (hash !== -1) { // got a fragment string. - this.hash = rest.substr(hash); + this.hash = rest.substring(hash); rest = rest.slice(0, hash); } var qm = rest.indexOf("?"); if (qm !== -1) { - this.search = rest.substr(qm); - this.query = rest.substr(qm + 1); + this.search = rest.substring(qm); + this.query = rest.substring(qm + 1); if (parseQueryString) { const query = this.query; this.query = new URLSearchParams(query).toJSON(); @@ -378,7 +422,7 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { rest = rest.slice(0, qm); } else if (parseQueryString) { // no query string, but parseQueryString still requested - this.search = ""; + this.search = null; this.query = {}; } if (rest) { @@ -400,34 +444,73 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { return this; }; +function isIpv6Hostname(hostname: string) { + return ( + hostname.$charCodeAt(0) === Char.LEFT_SQUARE_BRACKET && + hostname.$charCodeAt($toLength(hostname.length - 1)) === Char.RIGHT_SQUARE_BRACKET + ); +} + +let warnInvalidPort = true; +function getHostname(self, rest, hostname: string, url) { + for (let i = 0; i < hostname.length; ++i) { + const code = hostname.$charCodeAt(i); + const isValid = + code !== Char.FORWARD_SLASH && + code !== Char.BACKWARD_SLASH && + code !== Char.HASH && + code !== Char.QUESTION_MARK && + code !== Char.COLON; + + if (!isValid) { + // If leftover starts with :, then it represents an invalid port. + // But url.parse() is lenient about it for now. + // Issue a warning and continue. + if (warnInvalidPort && code === Char.COLON) { + const detail = `The URL ${url} is invalid. Future versions of Node.js will throw an error.`; + process.emitWarning(detail, "DeprecationWarning", "DEP0170"); + warnInvalidPort = false; + } + self.hostname = hostname.slice(0, i); + return `/${hostname.slice(i)}${rest}`; + } + } + return rest; +} + // format a parsed object into a url string -function urlFormat(obj) { +declare function urlFormat(urlObject: string | URL | Url): string; +function urlFormat(urlObject: unknown) { /* * ensure it's an object, and not a string url. * If it's an obj, this is a no-op. * this way, you can call url_format() on strings * to clean up potentially wonky urls. */ - if (typeof obj === "string") { - obj = urlParse(obj); + if (typeof urlObject === "string") { + urlObject = urlParse(urlObject); + // NOTE: $isObject returns true for functions + } else if (typeof urlObject !== "object" || urlObject === null) { + throw $ERR_INVALID_ARG_TYPE("urlObject", ["Object", "string"], urlObject); } - if (!(obj instanceof Url)) { - return Url.prototype.format.$call(obj); + + if (!(urlObject instanceof Url)) { + return Url.prototype.format.$call(urlObject); } - return obj.format(); + return urlObject.format(); } -Url.prototype.format = function () { - var auth = this.auth || ""; +Url.prototype.format = function format() { + var auth: string = this.auth || ""; if (auth) { auth = encodeURIComponent(auth); auth = auth.replace(/%3A/i, ":"); auth += "@"; } - var protocol = this.protocol || "", - pathname = this.pathname || "", - hash = this.hash || "", + var protocol: string = this.protocol || "", + pathname: string = this.pathname || "", + hash: string = this.hash || "", host = "", query = ""; @@ -478,11 +561,11 @@ Url.prototype.format = function () { return protocol + host + pathname + search + hash; }; -function urlResolve(source, relative) { +function urlResolve(source: string | URL | Url, relative: string | URL | Url) { return urlParse(source, false, true).resolve(relative); } -Url.prototype.resolve = function (relative) { +Url.prototype.resolve = function resolve(relative: string | URL | Url) { return this.resolveObject(urlParse(relative, false, true)).format(); }; @@ -493,7 +576,7 @@ function urlResolveObject(source, relative) { return urlParse(source, false, true).resolveObject(relative); } -Url.prototype.resolveObject = function (relative) { +Url.prototype.resolveObject = function resolveObject(relative) { if (typeof relative === "string") { var rel = new Url(); rel.parse(relative, false, true); @@ -562,21 +645,18 @@ Url.prototype.resolveObject = function (relative) { } result.protocol = relative.protocol; - if (!relative.host && !hostlessProtocol[relative.protocol]) { - var relPath = (relative.pathname || "").split("/"); + if ( + !relative.host && + !(relative.protocol === "file" || relative.protocol === "file:") && + !hostlessProtocol[relative.protocol] + ) { + let relPath = (relative.pathname || "").split("/"); while (relPath.length && !(relative.host = relPath.shift())) {} - if (!relative.host) { - relative.host = ""; - } - if (!relative.hostname) { - relative.hostname = ""; - } - if (relPath[0] !== "") { - relPath.unshift(""); - } - if (relPath.length < 2) { - relPath.unshift(""); - } + relative.host ||= ""; + relative.hostname ||= ""; + if (relPath[0] !== "") relPath.unshift(""); + if (relPath.length < 2) relPath.unshift(""); + result.pathname = relPath.join("/"); } else { result.pathname = relative.pathname; @@ -598,13 +678,13 @@ Url.prototype.resolveObject = function (relative) { return result; } - var isSourceAbs = result.pathname && result.pathname.charAt(0) === "/", - isRelAbs = relative.host || (relative.pathname && relative.pathname.charAt(0) === "/"), - mustEndAbs = isRelAbs || isSourceAbs || (result.host && relative.pathname), - removeAllDots = mustEndAbs, - srcPath = (result.pathname && result.pathname.split("/")) || [], - relPath = (relative.pathname && relative.pathname.split("/")) || [], - psychotic = result.protocol && !slashedProtocol[result.protocol]; + const isSourceAbs = result.pathname && result.pathname.charAt(0) === "/"; + const isRelAbs = relative.host || (relative.pathname && relative.pathname.charAt(0) === "/"); + let mustEndAbs = isRelAbs || isSourceAbs || (result.host && relative.pathname); + const removeAllDots = mustEndAbs; + let srcPath = (result.pathname && result.pathname.split("/")) || []; + const relPath = (relative.pathname && relative.pathname.split("/")) || []; + const psychotic = result.protocol && !slashedProtocol[result.protocol]; /* * if the url is a non-slashed url, then relative @@ -617,16 +697,14 @@ Url.prototype.resolveObject = function (relative) { result.hostname = ""; result.port = null; if (result.host) { - if (srcPath[0] === "") { - srcPath[0] = result.host; - } else { - srcPath.unshift(result.host); - } + if (srcPath[0] === "") srcPath[0] = result.host; + else srcPath.unshift(result.host); } result.host = ""; if (relative.protocol) { relative.hostname = null; relative.port = null; + result.auth = null; if (relative.host) { if (relPath[0] === "") { relPath[0] = relative.host; @@ -636,13 +714,20 @@ Url.prototype.resolveObject = function (relative) { } relative.host = null; } - mustEndAbs = mustEndAbs && (relPath[0] === "" || srcPath[0] === ""); + mustEndAbs &&= relPath[0] === "" || srcPath[0] === ""; } if (isRelAbs) { // it's absolute. - result.host = relative.host || relative.host === "" ? relative.host : result.host; - result.hostname = relative.hostname || relative.hostname === "" ? relative.hostname : result.hostname; + if (relative.host || relative.host === "") { + if (result.host !== relative.host) result.auth = null; + result.host = relative.host; + result.port = relative.port; + } + if (relative.hostname || relative.hostname === "") { + if (result.hostname !== relative.hostname) result.auth = null; + result.hostname = relative.hostname; + } result.search = relative.search; result.query = relative.query; srcPath = relPath; @@ -652,22 +737,19 @@ Url.prototype.resolveObject = function (relative) { * it's relative * throw away the existing file, and take the new path instead. */ - if (!srcPath) { - srcPath = []; - } + srcPath ||= []; srcPath.pop(); srcPath = srcPath.concat(relPath); result.search = relative.search; result.query = relative.query; - } else if (relative.search != null) { + } else if (relative.search != null && relative.search !== undefined) { /* * just pull out the search. * like href='?foo'. * Put this after the other two cases because it simplifies the booleans */ if (psychotic) { - result.host = srcPath.shift(); - result.hostname = result.host; + result.hostname = result.host = srcPath.shift(); /* * occationaly the auth can get stuck only in host * this especially happens in cases like @@ -676,15 +758,16 @@ Url.prototype.resolveObject = function (relative) { var authInHost = result.host && result.host.indexOf("@") > 0 ? result.host.split("@") : false; if (authInHost) { result.auth = authInHost.shift(); - result.hostname = authInHost.shift(); - result.host = result.hostname; + result.hostname = result.host = authInHost.shift(); } } result.search = relative.search; result.query = relative.query; // to support http.request if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : "") + (result.search ? result.search : ""); + result.path = + (result.pathname ? result.pathname : "") + // force line break + (result.search ? result.search : ""); } result.href = result.format(); return result; @@ -712,8 +795,10 @@ Url.prototype.resolveObject = function (relative) { * then it must NOT get a trailing slash. */ var last = srcPath.slice(-1)[0]; - var hasTrailingSlash = - ((result.host || relative.host || srcPath.length > 1) && (last === "." || last === "..")) || last === ""; + // prettier-ignore + var hasTrailingSlash = ( + ((result.host || relative.host || srcPath.length > 1) && + (last === "." || last === "..")) || last === ""); /* * strip single dots, resolve double dots to parent dir @@ -762,12 +847,11 @@ Url.prototype.resolveObject = function (relative) { var authInHost = result.host && result.host.indexOf("@") > 0 ? result.host.split("@") : false; if (authInHost) { result.auth = authInHost.shift(); - result.hostname = authInHost.shift(); - result.host = result.hostname; + result.hostname = result.host = authInHost.shift(); } } - mustEndAbs = mustEndAbs || (result.host && srcPath.length); + mustEndAbs ||= result.host && srcPath.length; if (mustEndAbs && !isAbsolute) { srcPath.unshift(""); @@ -782,7 +866,9 @@ Url.prototype.resolveObject = function (relative) { // to support request.http if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : "") + (result.search ? result.search : ""); + // prettier-ignore + result.path = (result.pathname ? result.pathname : "") + + (result.search ? result.search : ""); } result.auth = relative.auth || result.auth; result.slashes = result.slashes || relative.slashes; @@ -790,21 +876,69 @@ Url.prototype.resolveObject = function (relative) { return result; }; -Url.prototype.parseHost = function () { +Url.prototype.parseHost = function parseHost() { var host = this.host; var port = portPattern.exec(host); if (port) { port = port[0]; if (port !== ":") { - this.port = port.substr(1); + this.port = port.slice(1); } - host = host.substr(0, host.length - port.length); - } - if (host) { - this.hostname = host; + host = host.slice(0, host.length - port.length); } + if (host) this.hostname = host; }; +"".charCodeAt; +// function fileURLToPath(...args) { +// // Since we use WTF::URL::fileSystemPath directly in Bun.fileURLToPath, we don't get invalid windows +// // path checking. We patch this in to `node:url` for compatibility. Note that +// // this behavior is missing from WATWG URL. +// if (process.platform === "win32") { +// var url: string; +// if ($isObject(args[0]) && args[0] instanceof Url) { +// url = (args[0] as { href: string }).href; +// } else if (typeof args[0] === "string") { +// url = args[0]; +// } else { +// throw $ERR_INVALID_ARG_TYPE("url", ["string", "URL"], args[0]); +// } + +// for (var i = 0; i < url.length; i++) { +// if (url.charCodeAt(i) === Char.PERCENT && (i + 1) < url.length) { +// switch (url.charCodeAt(i + 1)) { +// break; +// } +// } +// } +// return Bun.fileURLToPath.$call(args); +// } + +/** + * Add new characters as needed from + * [here](https://github.com/nodejs/node/blob/main/lib/internal/constants.js). + * + * @note Do not move to another file, otherwise const enums will be imported as an object + * instead of being inlined. + */ +// prettier-ignore +const enum Char { + // non-alphabetic characters + AT = 64, // @ + COLON = 58, // : + BACKWARD_SLASH = 92, // \ + FORWARD_SLASH = 47, // / + HASH = 35, // # + QUESTION_MARK = 63, // ? + PERCENT = 37, // % + LEFT_SQUARE_BRACKET = 91, // [ + RIGHT_SQUARE_BRACKET = 93, // ] + + // whitespace + NO_BREAK_SPACE = 160, // \u00A0 + ZERO_WIDTH_NOBREAK_SPACE = 65279, // \uFEFF +} + export default { parse: urlParse, resolve: urlResolve, diff --git a/src/js/node/util.ts b/src/js/node/util.ts index 3541d1b806..e5f67d73e4 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -2,12 +2,13 @@ const types = require("node:util/types"); /** @type {import('node-inspect-extracted')} */ const utl = require("internal/util/inspect"); -const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } = require("internal/errors"); const { promisify } = require("internal/promisify"); +const { validateString, validateOneOf } = require("internal/validators"); const internalErrorName = $newZigFunction("node_util_binding.zig", "internalErrorName", 1); const NumberIsSafeInteger = Number.isSafeInteger; +const ObjectKeys = Object.keys; var cjs_exports; @@ -29,10 +30,12 @@ const formatWithOptions = utl.formatWithOptions; const format = utl.format; const stripVTControlCharacters = utl.stripVTControlCharacters; +const codesWarned = new Set(); function deprecate(fn, msg, code) { if (process.noDeprecation === true) { return fn; } + if (code !== undefined) validateString(code, "code"); var warned = false; function deprecated() { @@ -44,7 +47,15 @@ function deprecate(fn, msg, code) { } else if (process.traceDeprecation) { console.trace(msg); } else { - console.error(msg); + if (code !== undefined) { + // only warn for each code once + if (codesWarned.has(code)) { + process.emitWarning(msg, "DeprecationWarning", code); + } + codesWarned.add(code); + } else { + process.emitWarning(msg, "DeprecationWarning"); + } } warned = true; } @@ -137,17 +148,23 @@ var log = function log() { }; var inherits = function inherits(ctor, superCtor) { if (ctor === undefined || ctor === null) { - throw ERR_INVALID_ARG_TYPE("ctor", "function", ctor); + throw $ERR_INVALID_ARG_TYPE("ctor", "function", ctor); } if (superCtor === undefined || superCtor === null) { - throw ERR_INVALID_ARG_TYPE("superCtor", "function", superCtor); + throw $ERR_INVALID_ARG_TYPE("superCtor", "function", superCtor); } if (superCtor.prototype === undefined) { - throw ERR_INVALID_ARG_TYPE("superCtor.prototype", "object", superCtor.prototype); + throw $ERR_INVALID_ARG_TYPE("superCtor.prototype", "object", superCtor.prototype); } - ctor.super_ = superCtor; + Object.defineProperty(ctor, "super_", { + // @ts-ignore + __proto__: null, + value: superCtor, + writable: true, + configurable: true, + }); Object.setPrototypeOf(ctor.prototype, superCtor.prototype); }; var _extend = function (origin, add) { @@ -170,30 +187,40 @@ function callbackifyOnRejected(reason, cb) { return cb(reason); } function callbackify(original) { - if (typeof original !== "function") { - throw new TypeError('The "original" argument must be of type Function'); - } - function callbackified() { - var args = Array.prototype.slice.$call(arguments); - var maybeCb = args.pop(); - if (typeof maybeCb !== "function") { - throw new TypeError("The last argument must be of type Function"); - } - var self = this; - var cb = function () { - return maybeCb.$apply(self, arguments); - }; + const { validateFunction } = require("internal/validators"); + validateFunction(original, "original"); + + // We DO NOT return the promise as it gives the user a false sense that + // the promise is actually somehow related to the callback's execution + // and that the callback throwing will reject the promise. + function callbackified(...args) { + const maybeCb = Array.prototype.pop.$call(args); + validateFunction(maybeCb, "last argument"); + const cb = Function.prototype.bind.$call(maybeCb, this); + // In true node style we process the callback on `nextTick` with all the + // implications (stack, `uncaughtException`, `async_hooks`) original.$apply(this, args).then( - function (ret) { - process.nextTick(cb, null, ret); - }, - function (rej) { - process.nextTick(callbackifyOnRejected, rej, cb); - }, + ret => process.nextTick(cb, null, ret), + rej => process.nextTick(callbackifyOnRejected, rej, cb), ); } - Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original)); - Object.defineProperties(callbackified, getOwnPropertyDescriptors(original)); + + const descriptors = Object.getOwnPropertyDescriptors(original); + // It is possible to manipulate a functions `length` or `name` property. This + // guards against the manipulation. + if (typeof descriptors.length.value === "number") { + descriptors.length.value++; + } + if (typeof descriptors.name.value === "string") { + descriptors.name.value += "Callbackified"; + } + const propertiesValues = Object.values(descriptors); + for (let i = 0; i < propertiesValues.length; i++) { + // We want to use null-prototype objects to not rely on globally mutable + // %Object.prototype%. + Object.setPrototypeOf(propertiesValues[i], null); + } + Object.defineProperties(callbackified, descriptors); return callbackified; } var toUSVString = input => { @@ -201,25 +228,34 @@ var toUSVString = input => { }; function styleText(format, text) { - if (typeof text !== "string") { - const e = new Error(`The text argument must be of type string. Received type ${typeof text}`); - e.code = "ERR_INVALID_ARG_TYPE"; - throw e; + validateString(text, "text"); + + if ($isJSArray(format)) { + let left = ""; + let right = ""; + for (const key of format) { + const formatCodes = inspect.colors[key]; + if (formatCodes == null) { + validateOneOf(key, "format", ObjectKeys(inspect.colors)); + } + left += `\u001b[${formatCodes[0]}m`; + right = `\u001b[${formatCodes[1]}m${right}`; + } + + return `${left}${text}${right}`; } - const formatCodes = inspect.colors[format]; + + let formatCodes = inspect.colors[format]; + if (formatCodes == null) { - const e = new Error( - `The value "${typeof format === "symbol" ? format.description : format}" is invalid for argument 'format'. Reason: must be one of: ${Object.keys(inspect.colors).join(", ")}`, - ); - e.code = "ERR_INVALID_ARG_VALUE"; - throw e; + validateOneOf(format, "format", ObjectKeys(inspect.colors)); } return `\u001b[${formatCodes[0]}m${text}\u001b[${formatCodes[1]}m`; } function getSystemErrorName(err: any) { - if (typeof err !== "number") throw ERR_INVALID_ARG_TYPE("err", "number", err); - if (err >= 0 || !NumberIsSafeInteger(err)) throw ERR_OUT_OF_RANGE("err", "a negative integer", err); + if (typeof err !== "number") throw $ERR_INVALID_ARG_TYPE("err", "number", err); + if (err >= 0 || !NumberIsSafeInteger(err)) throw $ERR_OUT_OF_RANGE("err", "a negative integer", err); return internalErrorName(err); } @@ -235,11 +271,11 @@ function onAbortedCallback(resolveFn: Function) { function aborted(signal: AbortSignal, resource: object) { if (!$isObject(signal) || !(signal instanceof AbortSignal)) { - throw ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); + throw $ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); } if (!$isObject(resource)) { - throw ERR_INVALID_ARG_TYPE("resource", "object", resource); + throw $ERR_INVALID_ARG_TYPE("resource", "object", resource); } if (signal.aborted) { @@ -279,17 +315,43 @@ function aborted(signal: AbortSignal, resource: object) { } cjs_exports = { - format, - formatWithOptions, - stripVTControlCharacters, - deprecate, + // This is in order of `node --print 'Object.keys(util)'` + // _errnoException, + // _exceptionWithHostPort, + _extend, + callbackify, debug: debuglog, debuglog, - _extend, + deprecate, + format, + styleText, + formatWithOptions, + // getCallSite, + // getCallSites, + // getSystemErrorMap, + getSystemErrorName, + // getSystemErrorMessage, + inherits, inspect, + isDeepStrictEqual, + promisify, + stripVTControlCharacters, + toUSVString, + // transferableAbortSignal, + // transferableAbortController, + aborted, types, + // parseEnv, + parseArgs, + TextDecoder, + TextEncoder, + // MIMEType, + // MIMEParams, + + // Deprecated in Node.js 22, removed in 23 isArray: $isArray, isBoolean, + isBuffer, isNull, isNullOrUndefined, isNumber, @@ -299,22 +361,10 @@ cjs_exports = { isRegExp, isObject, isDate, - isFunction, isError, + isFunction, isPrimitive, - isBuffer, log, - inherits, - toUSVString, - promisify, - callbackify, - isDeepStrictEqual, - TextDecoder, - TextEncoder, - parseArgs, - styleText, - getSystemErrorName, - aborted, }; export default cjs_exports; diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts index ada998d747..082fbfe1dc 100644 --- a/src/js/node/v8.ts +++ b/src/js/node/v8.ts @@ -1,4 +1,5 @@ // Hardcoded module "node:v8" + // This is a stub! None of this is actually implemented yet. const { hideFromStack, throwNotImplemented } = require("internal/shared"); const jsc: typeof import("bun:jsc") = require("bun:jsc"); @@ -28,8 +29,21 @@ class GCProfiler { function cachedDataVersionTag() { notimpl("cachedDataVersionTag"); } +var HeapSnapshotReadable_; function getHeapSnapshot() { - notimpl("getHeapSnapshot"); + if (!HeapSnapshotReadable_) { + const Readable = require("node:stream").Readable; + class HeapSnapshotReadable extends Readable { + constructor() { + super(); + this.push(Bun.generateHeapSnapshot("v8")); + this.push(null); + } + } + HeapSnapshotReadable_ = HeapSnapshotReadable; + } + + return new HeapSnapshotReadable_(); } let totalmem_ = -1; @@ -92,8 +106,45 @@ function stopCoverage() { function serialize(arg1) { return jsc.serialize(arg1, { binaryType: "nodebuffer" }); } -function writeHeapSnapshot() { - notimpl("writeHeapSnapshot"); + +function getDefaultHeapSnapshotPath() { + const date = new Date(); + + const worker_threads = require("node:worker_threads"); + const thread_id = worker_threads.threadId; + + const yyyy = date.getFullYear(); + const mm = date.getMonth().toString().padStart(2, "0"); + const dd = date.getDate().toString().padStart(2, "0"); + const hh = date.getHours().toString().padStart(2, "0"); + const MM = date.getMinutes().toString().padStart(2, "0"); + const ss = date.getSeconds().toString().padStart(2, "0"); + + // 'Heap-${yyyymmdd}-${hhmmss}-${pid}-${thread_id}.heapsnapshot' + return `Heap-${yyyy}${mm}${dd}-${hh}${MM}${ss}-${process.pid}-${thread_id}.heapsnapshot`; +} + +let fs; + +function writeHeapSnapshot(path, options) { + if (path !== undefined) { + if (typeof path !== "string") { + throw $ERR_INVALID_ARG_TYPE("path", "string", path); + } + + if (!path) { + throw $ERR_INVALID_ARG_VALUE("path", path, "must be a non-empty string"); + } + } else { + path = getDefaultHeapSnapshotPath(); + } + + if (!fs) { + fs = require("node:fs"); + } + fs.writeFileSync(path, Bun.generateHeapSnapshot("v8"), "utf-8"); + + return path; } function setHeapSnapshotNearHeapLimit() { notimpl("setHeapSnapshotNearHeapLimit"); diff --git a/src/js/node/zlib.ts b/src/js/node/zlib.ts index 77651013eb..34235ac71c 100644 --- a/src/js/node/zlib.ts +++ b/src/js/node/zlib.ts @@ -24,13 +24,6 @@ const isArrayBufferView = ArrayBufferIsView; const isAnyArrayBuffer = b => b instanceof ArrayBuffer || b instanceof SharedArrayBuffer; const kMaxLength = $requireMap.$get("buffer")?.exports.kMaxLength ?? BufferModule.kMaxLength; -const { - ERR_BROTLI_INVALID_PARAM, - ERR_BUFFER_TOO_LARGE, - ERR_INVALID_ARG_TYPE, - ERR_OUT_OF_RANGE, - ERR_ZLIB_INITIALIZATION_FAILED, -} = require("internal/errors"); const { Transform, finished } = require("node:stream"); const owner_symbol = Symbol("owner_symbol"); const { @@ -97,7 +90,7 @@ function zlibBufferOnData(chunk) { if (this.nread > this._maxOutputLength) { this.close(); this.removeAllListeners("end"); - this.cb(ERR_BUFFER_TOO_LARGE(this._maxOutputLength)); + this.cb($ERR_BUFFER_TOO_LARGE(this._maxOutputLength)); } } @@ -126,7 +119,7 @@ function zlibBufferSync(engine, buffer) { if (isAnyArrayBuffer(buffer)) { buffer = Buffer.from(buffer); } else { - throw ERR_INVALID_ARG_TYPE("buffer", "string, Buffer, TypedArray, DataView, or ArrayBuffer", buffer); + throw $ERR_INVALID_ARG_TYPE("buffer", "string, Buffer, TypedArray, DataView, or ArrayBuffer", buffer); } } buffer = processChunkSync(engine, buffer, engine._finishFlushFlag); @@ -171,7 +164,7 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) { if (!validateFiniteNumber(chunkSize, "options.chunkSize")) { chunkSize = Z_DEFAULT_CHUNK; } else if (chunkSize < Z_MIN_CHUNK) { - throw ERR_OUT_OF_RANGE("options.chunkSize", `>= ${Z_MIN_CHUNK}`, chunkSize); + throw $ERR_OUT_OF_RANGE("options.chunkSize", `>= ${Z_MIN_CHUNK}`, chunkSize); } // prettier-ignore @@ -364,7 +357,7 @@ function processChunkSync(self, chunk, flushFlag) { if (nread > self._maxOutputLength) { _close(self); - throw ERR_BUFFER_TOO_LARGE(self._maxOutputLength); + throw $ERR_BUFFER_TOO_LARGE(self._maxOutputLength); } } else { assert(have === 0, "have should not go down"); @@ -562,7 +555,7 @@ function Zlib(opts, mode) { if (isAnyArrayBuffer(dictionary)) { dictionary = Buffer.from(dictionary); } else { - throw ERR_INVALID_ARG_TYPE("options.dictionary", "Buffer, TypedArray, DataView, or ArrayBuffer", dictionary); + throw $ERR_INVALID_ARG_TYPE("options.dictionary", "Buffer, TypedArray, DataView, or ArrayBuffer", dictionary); } } } @@ -681,12 +674,12 @@ function Brotli(opts, mode) { ArrayPrototypeForEach.$call(ObjectKeys(opts.params), origKey => { const key = +origKey; if (NumberIsNaN(key) || key < 0 || key > kMaxBrotliParam || (brotliInitParamsArray[key] | 0) !== -1) { - throw ERR_BROTLI_INVALID_PARAM(origKey); + throw $ERR_BROTLI_INVALID_PARAM(origKey); } const value = opts.params[origKey]; if (typeof value !== "number" && typeof value !== "boolean") { - throw ERR_INVALID_ARG_TYPE("options.params[key]", "number", opts.params[origKey]); + throw $ERR_INVALID_ARG_TYPE("options.params[key]", "number", opts.params[origKey]); } brotliInitParamsArray[key] = value; }); @@ -696,7 +689,7 @@ function Brotli(opts, mode) { this._writeState = new Uint32Array(2); if (!handle.init(brotliInitParamsArray, this._writeState, processCallback)) { - throw ERR_ZLIB_INITIALIZATION_FAILED(); + throw $ERR_ZLIB_INITIALIZATION_FAILED(); } ZlibBase.$apply(this, [opts, mode, handle, brotliDefaultOpts]); diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 2b8d7d5bf6..b058cc40c5 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -161,7 +161,43 @@ interface CommonJSModuleRecord { require: typeof require; } +/** + * Call a native c++ binding, getting whatever it returns. + * + * This is more like a macro; it is replaced with a WebKit intrisic during + * codegen. Passing a template parameter will break codegen. Prefer `$cpp(...) + * as Foo` instead. + * + * Binding files are located in `src/bun.js/bindings` + * + * @see {@link $zig} for native zig bindings. + * @see `src/codegen/replacements.ts` for the script that performs replacement of this funciton. + * + * @param filename name of the c++ file containing the function. Do not pass a path. + * @param symbol The name of the binding function to call. Use `dot.notation` to access + * member symbols. + * + * @returns whatever the binding function returns. + */ declare function $cpp(filename: NativeFilenameCPP, symbol: string): T; +/** + * Call a native zig binding function, getting whatever it returns. + * + * This is more like a macro; it is replaced with a WebKit intrisic during + * codegen. Passing a template parameter will break codegen. Prefer `$zig(...) + * as Foo` instead. + * + * Binding files are located in `src/bun.js/bindings` + * + * @see {@link $cpp} for native c++ bindings. + * @see `src/codegen/replacements.ts` for the script that performs replacement of this funciton. + * + * @param filename name of the zig file containing the function. Do not pass a path. + * @param symbol The name of the binding function. Use `dot.notation` to access + * member symbols. + * + * @returns whatever the binding function returns. + */ declare function $zig(filename: NativeFilenameZig, symbol: string): T; declare function $newCppFunction any>( filename: NativeFilenameCPP, @@ -173,3 +209,12 @@ declare function $newZigFunction any>( symbol: string, argCount: number, ): T; +/** + * Retrieves a handle to a function defined in Zig or C++, defined in a + * `.bind.ts` file. For more information on how to define bindgen functions, see + * [bindgen's documentation](https://bun.sh/docs/project/bindgen). + * @param filename - The basename of the `.bind.ts` file. + * @param symbol - The name of the function to call. + */ +declare function $bindgenFn any>(filename: string, symbol: string): T; +// NOTE: $debug, $assert, and $isPromiseResolved omitted diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 9726e991c7..279f79762a 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,25 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": ["ESNext", "DOM"], - "module": "ESNext", - "isolatedModules": true, - "noEmit": true, - "emitDeclarationOnly": false, + "esModuleInterop": true, + // Path remapping + "baseUrl": ".", "paths": { "internal/*": ["./internal/*"] //deprecated } }, - "include": [ - // - "./node", - "./bun", - "./builtins", - "./functions", - "./internal", - "./thirdparty", - "./builtins.d.ts", - "./private.d.ts", - "../../build/codegen/WebCoreJSBuiltins.d.ts" - ] + "include": ["**/*.ts", "**/*.tsx", "./builtins.d.ts", "./private.d.ts"] } diff --git a/src/js_ast.zig b/src/js_ast.zig index c7713a3dde..6ce22d81da 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -26,10 +26,10 @@ const ComptimeStringMap = bun.ComptimeStringMap; const JSPrinter = @import("./js_printer.zig"); const js_lexer = @import("./js_lexer.zig"); const TypeScript = @import("./js_parser.zig").TypeScript; -const ThreadlocalArena = @import("./mimalloc_arena.zig").Arena; +const ThreadlocalArena = @import("./allocators/mimalloc_arena.zig").Arena; const MimeType = bun.http.MimeType; const OOM = bun.OOM; - +const Loader = bun.options.Loader; /// This is the index to the automatically-generated part containing code that /// calls "__export(exports, { ... getters ... })". This is used to generate /// getters on an exports object for ES6 export statements, and is both for @@ -132,7 +132,6 @@ pub fn NewStore(comptime types: []const type, comptime count: usize) type { } pub fn reset(store: *Store) void { - store.debug_lock.assertUnlocked(); log("reset", .{}); if (Environment.isDebug) { @@ -153,8 +152,6 @@ pub fn NewStore(comptime types: []const type, comptime count: usize) type { @compileError("Store does not know about type: " ++ @typeName(T)); }; - store.debug_lock.assertUnlocked(); - if (store.current.tryAlloc(T)) |ptr| return ptr; @@ -2507,20 +2504,17 @@ pub const E = struct { if (s.isUTF8()) { if (try strings.toUTF16Alloc(allocator, s.slice8(), false, false)) |utf16| { var out, const chars = bun.String.createUninitialized(.utf16, utf16.len); - defer out.deref(); @memcpy(chars, utf16); - return out.toJS(globalObject); + return out.transferToJS(globalObject); } else { var out, const chars = bun.String.createUninitialized(.latin1, s.slice8().len); - defer out.deref(); @memcpy(chars, s.slice8()); - return out.toJS(globalObject); + return out.transferToJS(globalObject); } } else { var out, const chars = bun.String.createUninitialized(.utf16, s.slice16().len); - defer out.deref(); @memcpy(chars, s.slice16()); - return out.toJS(globalObject); + return out.transferToJS(globalObject); } } @@ -3044,7 +3038,7 @@ pub const Stmt = struct { return Stmt.allocate(allocator, S.SExpr, S.SExpr{ .value = expr }, expr.loc); } - pub const Tag = enum(u6) { + pub const Tag = enum { s_block, s_break, s_class, @@ -3126,7 +3120,13 @@ pub const Stmt = struct { s_empty: S.Empty, // special case, its a zero value type s_debugger: S.Debugger, - s_lazy_export: Expr.Data, + s_lazy_export: *Expr.Data, + + comptime { + if (@sizeOf(Stmt) > 24) { + @compileLog("Expected Stmt to be <= 24 bytes, but it is", @sizeOf(Stmt), " bytes"); + } + } pub const Store = struct { const StoreType = NewStore(&.{ @@ -4564,7 +4564,7 @@ pub const Expr = struct { }; } - pub const Tag = enum(u6) { + pub const Tag = enum { e_array, e_unary, e_binary, @@ -6911,8 +6911,6 @@ pub const Ast = struct { wrapper_ref: Ref = Ref.None, require_ref: Ref = Ref.None, - prepend_part: ?Part = null, - // These are used when bundling. They are filled in during the parser pass // since we already have to traverse the AST then anyway and the parser pass // is conveniently fully parallelized. @@ -7008,7 +7006,7 @@ pub const BundledAst = struct { hashbang: string = "", parts: Part.List = .{}, css: ?*bun.css.BundlerStyleSheet = null, - url_for_css: ?[]const u8 = null, + url_for_css: []const u8 = "", symbols: Symbol.List = .{}, module_scope: Scope = .{}, char_freq: CharFreq = undefined, @@ -7125,7 +7123,6 @@ pub const BundledAst = struct { .import_records = ast.import_records, .hashbang = ast.hashbang, - // .url_for_css = ast.url_for_css orelse "", .parts = ast.parts, // This list may be mutated later, so we should store the capacity .symbols = ast.symbols, @@ -7173,12 +7170,12 @@ pub const BundledAst = struct { pub fn addUrlForCss( this: *BundledAst, allocator: std.mem.Allocator, - css_enabled: bool, + experimental: Loader.Experimental, source: *const logger.Source, mime_type_: ?[]const u8, unique_key: ?[]const u8, ) void { - if (css_enabled) { + if (experimental.css) { const mime_type = if (mime_type_) |m| m else MimeType.byExtension(bun.strings.trimLeadingChar(std.fs.path.extension(source.path.text), '.')).value; const contents = source.contents; // TODO: make this configurable @@ -7902,8 +7899,8 @@ pub const Macro = struct { const DotEnv = @import("./env_loader.zig"); const js = @import("./bun.js/javascript_core_c_api.zig"); const Zig = @import("./bun.js/bindings/exports.zig"); - const Bundler = bun.Bundler; - const MacroEntryPoint = bun.bundler.MacroEntryPoint; + const Transpiler = bun.Transpiler; + const MacroEntryPoint = bun.transpiler.MacroEntryPoint; const MacroRemap = @import("./resolver/package_json.zig").MacroMap; pub const MacroRemapEntry = @import("./resolver/package_json.zig").MacroImportReplacementMap; @@ -7928,12 +7925,12 @@ pub const Macro = struct { return this.remap.get(path); } - pub fn init(bundler: *Bundler) MacroContext { + pub fn init(transpiler: *Transpiler) MacroContext { return MacroContext{ .macros = MacroMap.init(default_allocator), - .resolver = &bundler.resolver, - .env = bundler.env, - .remap = bundler.options.macro_remap, + .resolver = &transpiler.resolver, + .env = transpiler.env, + .remap = transpiler.options.macro_remap, }; } @@ -8090,13 +8087,14 @@ pub const Macro = struct { .allocator = default_allocator, .args = resolver.opts.transform_options, .log = log, + .is_main_thread = false, .env_loader = env, }); _vm.enableMacroMode(); _vm.eventLoop().ensureWaker(); - try _vm.bundler.configureDefines(); + try _vm.transpiler.configureDefines(); break :brk _vm; }; @@ -8124,7 +8122,7 @@ pub const Macro = struct { threadlocal var args_buf: [3]js.JSObjectRef = undefined; threadlocal var exception_holder: Zig.ZigException.Holder = undefined; - pub const MacroError = error{ MacroFailed, OutOfMemory } || ToJSError; + pub const MacroError = error{ MacroFailed, OutOfMemory } || ToJSError || bun.JSError; pub const Run = struct { caller: Expr, @@ -8196,14 +8194,13 @@ pub const Macro = struct { .String => this.coerce(value, .String), .Promise => this.coerce(value, .Promise), else => brk: { - var name = value.getClassInfoName() orelse bun.String.init("unknown"); - defer name.deref(); + const name = value.getClassInfoName() orelse "unknown"; this.log.addErrorFmt( this.source, this.caller.loc, this.allocator, - "cannot coerce {} ({s}) to Bun's AST. Please return a simpler type", + "cannot coerce {s} ({s}) to Bun's AST. Please return a simpler type", .{ name, @tagName(value.jsType()) }, ) catch unreachable; break :brk error.MacroFailed; @@ -8340,7 +8337,7 @@ pub const Macro = struct { return _entry.value_ptr.*; } - var object_iter = JSC.JSPropertyIterator(.{ + var object_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(this.global, value); @@ -8357,7 +8354,7 @@ pub const Macro = struct { ); _entry.value_ptr.* = out; - while (object_iter.next()) |prop| { + while (try object_iter.next()) |prop| { properties[object_iter.i] = G.Property{ .key = Expr.init(E.String, E.String.init(prop.toOwnedSlice(this.allocator) catch unreachable), this.caller.loc), .value = try this.run(object_iter.value), diff --git a/src/js_lexer.zig b/src/js_lexer.zig index bcb354401f..251cd7c08a 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -117,7 +117,7 @@ fn NewLexer_( const is_json = json_options.is_json; const json = json_options; const JSONBool = if (is_json) bool else void; - const JSONBoolDefault: JSONBool = if (is_json) true else {}; + const JSONBoolDefault: JSONBool = if (is_json) true; pub const Error = error{ UTF8Fail, @@ -153,13 +153,6 @@ fn NewLexer_( code_point: CodePoint = -1, identifier: []const u8 = "", jsx_pragma: JSXPragma = .{}, - bun_pragma: enum { - none, - bun, - bun_cjs, - bytecode, - bytecode_cjs, - } = .none, source_mapping_url: ?js_ast.Span = null, number: f64 = 0.0, rescan_close_brace_as_template_token: bool = false, @@ -187,8 +180,7 @@ fn NewLexer_( } else void = if (json_options.guess_indentation) - .{} - else {}, + .{}, pub inline fn loc(self: *const LexerType) logger.Loc { return logger.usize2Loc(self.start); @@ -830,7 +822,7 @@ fn NewLexer_( return code_point; } - fn step(lexer: *LexerType) void { + pub fn step(lexer: *LexerType) void { lexer.code_point = lexer.nextCodepoint(); // Track the approximate number of newlines in the file so we can preallocate @@ -1957,9 +1949,7 @@ fn NewLexer_( // } } - if (lexer.bun_pragma == .none and strings.hasPrefixWithWordBoundary(chunk, "bun")) { - lexer.bun_pragma = .bun; - } else if (strings.hasPrefixWithWordBoundary(chunk, "jsx")) { + if (strings.hasPrefixWithWordBoundary(chunk, "jsx")) { if (PragmaArg.scan(.skip_space_first, lexer.start + i + 1, "jsx", chunk)) |span| { lexer.jsx_pragma._jsx = span; } @@ -1979,10 +1969,6 @@ fn NewLexer_( if (PragmaArg.scan(.no_space_first, lexer.start + i + 1, " sourceMappingURL=", chunk)) |span| { lexer.source_mapping_url = span; } - } else if ((lexer.bun_pragma == .bun or lexer.bun_pragma == .bun_cjs) and strings.hasPrefixWithWordBoundary(chunk, "bytecode")) { - lexer.bun_pragma = if (lexer.bun_pragma == .bun) .bytecode else .bytecode_cjs; - } else if ((lexer.bun_pragma == .bytecode or lexer.bun_pragma == .bun) and strings.hasPrefixWithWordBoundary(chunk, "bun-cjs")) { - lexer.bun_pragma = if (lexer.bun_pragma == .bytecode) .bytecode_cjs else .bun_cjs; } }, else => {}, @@ -2012,9 +1998,7 @@ fn NewLexer_( } } - if (lexer.bun_pragma == .none and strings.hasPrefixWithWordBoundary(chunk, "bun")) { - lexer.bun_pragma = .bun; - } else if (strings.hasPrefixWithWordBoundary(chunk, "jsx")) { + if (strings.hasPrefixWithWordBoundary(chunk, "jsx")) { if (PragmaArg.scan(.skip_space_first, lexer.start + i + 1, "jsx", chunk)) |span| { lexer.jsx_pragma._jsx = span; } @@ -2034,10 +2018,6 @@ fn NewLexer_( if (PragmaArg.scan(.no_space_first, lexer.start + i + 1, " sourceMappingURL=", chunk)) |span| { lexer.source_mapping_url = span; } - } else if ((lexer.bun_pragma == .bun or lexer.bun_pragma == .bun_cjs) and strings.hasPrefixWithWordBoundary(chunk, "bytecode")) { - lexer.bun_pragma = if (lexer.bun_pragma == .bun) .bytecode else .bytecode_cjs; - } else if ((lexer.bun_pragma == .bytecode or lexer.bun_pragma == .bun) and strings.hasPrefixWithWordBoundary(chunk, "bun-cjs")) { - lexer.bun_pragma = if (lexer.bun_pragma == .bytecode) .bytecode_cjs else .bun_cjs; } }, else => {}, @@ -2162,7 +2142,7 @@ fn NewLexer_( const flag_characters = "dgimsuvy"; const min_flag = comptime std.mem.min(u8, flag_characters); const max_flag = comptime std.mem.max(u8, flag_characters); - const RegexpFlags = std.bit_set.IntegerBitSet((max_flag - min_flag) + 1); + const RegexpFlags = bun.bit_set.IntegerBitSet((max_flag - min_flag) + 1); var flags = RegexpFlags.initEmpty(); while (isIdentifierContinue(lexer.code_point)) { switch (lexer.code_point) { @@ -3062,18 +3042,10 @@ pub const Lexer = NewLexer(.{}); const JSIdentifier = @import("./js_lexer/identifier.zig"); pub inline fn isIdentifierStart(codepoint: i32) bool { - if (comptime Environment.isWasm) { - return JSIdentifier.JumpTable.isIdentifierStart(codepoint); - } - - return JSIdentifier.Bitset.isIdentifierStart(codepoint); + return JSIdentifier.isIdentifierStart(codepoint); } pub inline fn isIdentifierContinue(codepoint: i32) bool { - if (comptime Environment.isWasm) { - return JSIdentifier.JumpTable.isIdentifierPart(codepoint); - } - - return JSIdentifier.Bitset.isIdentifierPart(codepoint); + return JSIdentifier.isIdentifierPart(codepoint); } pub fn isWhitespace(codepoint: CodePoint) bool { diff --git a/src/js_lexer/identifier.zig b/src/js_lexer/identifier.zig index b8da1da65f..6df764bb72 100644 --- a/src/js_lexer/identifier.zig +++ b/src/js_lexer/identifier.zig @@ -1,2024 +1,78 @@ -// This file benchmarks different approaches for determinig whether or not a unicode codepoint is possibly a JS identifier -// these values are copy-pasted from "typescript/lib/typescriptServices.js" const std = @import("std"); -pub const SerializedBitset = extern struct {}; -pub const Bitset = struct { - const Cache = @import("identifier_cache.zig"); - const id_start_range: [2]i32 = Cache.id_start_meta.range; - const id_end_range: [2]i32 = Cache.id_continue_meta.range; - // this is a pointer because otherwise it may be copied onto the stack - // and it's a huge bitset - const id_start = &Cache.id_start; - // this is a pointer because otherwise it may be copied onto the stack - // and it's a huge bitset - const id_continue = &Cache.id_continue; +pub fn isIdentifierStart(codepoint: i32) bool { + return switch (codepoint) { + 'a'...'z', 'A'...'Z', '_', '$' => true, + std.math.minInt(i32)...0, 0x10FFFF...std.math.maxInt(i32) => false, + else => isIDStartESNext(@intCast(codepoint)), + }; +} - pub fn init() void {} +pub fn isIdentifierPart(codepoint: i32) bool { + return switch (codepoint) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '$' => true, + std.math.minInt(i32)...0, 0x10FFFF...std.math.maxInt(i32) => false, + else => isIDContinueESNext(@intCast(codepoint)), + }; +} - pub fn isIdentifierStart(codepoint: i32) bool { - return codepoint >= (comptime id_start_range[0]) and - codepoint <= (comptime id_start_range[1]) and - id_start.isSet((comptime @as(usize, @intCast(id_start_range[1]))) - @as( - usize, - @intCast(codepoint), - )); - } - - pub fn isIdentifierPart(codepoint: i32) bool { - return codepoint >= (comptime id_end_range[0]) and - codepoint <= (comptime id_end_range[1]) and - id_continue.isSet( - (comptime @as(usize, @intCast(id_end_range[1]))) - @as( - usize, - @intCast(codepoint), - ), - ); - } +/// This file is auto-generated. Do not edit. +/// isIDStartES5 checks if a codepoint is valid in the isIDStartES5 category +pub fn isIDStartES5(cp: u21) bool { + const high = cp >> 8; + const low = cp & 0xFF; + const stage2_idx = idStartES5.stage1[high]; + const bit_pos = stage2_idx + low; + const u64_idx = bit_pos >> 6; + const bit_idx = @as(u6, @intCast(bit_pos & 63)); + return (idStartES5.stage2[u64_idx] & (@as(u64, 1) << bit_idx)) != 0; +} +const idStartES5 = struct { + pub const stage1 = [_]u16{ 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 256, 5376, 5632, 5888, 2048, 2048, 2048, 2048, 2048, 6144, 6400, 6656, 6912, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 7168, 7424, 2048, 2048, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 7680, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 7936, 256, 256, 256, 256, 8192, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 8448, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 256, 8704, 8960, 256, 9216, 9472, 9728, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048 }; + pub const stage2 = [_]u64{ 0, 576460743847706622, 297241973452963840, 18410715276682199039, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 4503586742468607, 18446744073709486080, 18014187403249451007, 70501888360451, 0, 288230376151711744, 18446744056529672000, 4503599577006079, 18446744073709551615, 18446744073709551615, 18446744073709547523, 234187180623206815, 18446181123756130304, 18446744065161560063, 255, 1979120929931264, 576460743713488896, 18446181123756132351, 18446744073709551615, 2017613045381988351, 35184371892224, 0, 274877906943, 0, 0, 0, 0, 0, 2594073385365405664, 17163157504, 271902628478820320, 844440767823872, 247132830528276448, 7881300924956672, 2589004636761075680, 4295032832, 2579997437506199520, 15837691904, 270153412153034720, 0, 283724577500946400, 12884901888, 283724577500946400, 13958643712, 288228177128316896, 12884901888, 3457638613854978016, 127, 3940649673949182, 127, 2309762420256548246, 805306463, 1, 8796093021951, 3840, 0, 7679401525247, 4128768, 18446744069414584320, 36028797018898495, 18446744073709551615, 18446744071629176831, 18446743008557662207, 288230376151711743, 18446744073709551487, 18446744070446333311, 9168625153884503423, 18446603336212774717, 18446744071549321215, 134217599, 18446744069414584320, 9007199254740991, 18446744073709551614, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 35923243902697471, 18446744069548802046, 8796093022207, 0, 0, 4503599627370495, 0, 18446744069414584320, 72057594037927935, 2199023255551, 0, 18446744073709551615, 18446744073709551615, 18446744069683019775, 288230376151711743, 18446744070475743231, 4611686017001275199, 6908521828386340863, 2295745090394464220, 0, 9223372036854775808, 0, 0, 287031153606524036, 0, 0, 0, 17451448556060768, 18446744073709551614, 18446744066732326911, 8646911284551352319, 18446216308128219104, 18446744073709551615, 72057589742993407, 0, 18446744073709551615, 18446744073709551615, 18014398509481983, 0, 18446744073709551615, 18446744073709551615, 274877906943, 0, 18446744073709551615, 18446744073709551615, 8191, 0, 18446744073709551615, 18446744073709551615, 68719476735, 0, 70368744177663, 0, 0, 0, 6881498030004502655, 18446744073709551579, 1125899906842623, 18446744073709027328, 4611686018427387903, 18446744073709486080, 18446744073709355007, 1152640029630136575, 0, 18435203599664414720, 18446744073709551615, 2305843009213693951, 576460743713488896, 18446743798965862398, 9223372036854775807, 486341884 }; }; -/// In WASM, we use the JumpTable version -pub const JumpTable = struct { - const minInt = @import("std").math.minInt; - const maxInt = @import("std").math.maxInt; - const max_codepoint = 0x10FFFF; - noinline fn isIdentifierPartSlow(codepoint: i32) bool { - @setCold(true); - return switch (codepoint) { - // explicitly tell LLVM's optimizer about values we know will not be in the range of this switch statement - 0xaa...0xffd7 => isIdentifierPartSlow16(@as(u16, @intCast(codepoint))), - (0xffd7 + 1)...0xe01ef => isIdentifierPartSlow32(codepoint), - - else => false, - }; - } - - fn isIdentifierPartSlow16(codepoint: u16) bool { - return switch (codepoint) { - minInt(u16)...(0xaa - 1) => unreachable, - 0xaa...0xaa, 0xb5...0xb5, 0xb7...0xb7, 0xba...0xba, 0xc0...0xd6, 0xd8...0xf6, 0xf8...0x2c1, 0x2c6...0x2d1, 0x2e0...0x2e4, 0x2ec...0x2ec, 0x2ee...0x2ee, 0x300...0x374, 0x376...0x377, 0x37a...0x37d, 0x37f...0x37f, 0x386...0x38a, 0x38c...0x38c, 0x38e...0x3a1, 0x3a3...0x3f5, 0x3f7...0x481, 0x483...0x487, 0x48a...0x52f, 0x531...0x556, 0x559...0x559, 0x560...0x588, 0x591...0x5bd, 0x5bf...0x5bf, 0x5c1...0x5c2, 0x5c4...0x5c5, 0x5c7...0x5c7, 0x5d0...0x5ea, 0x5ef...0x5f2, 0x610...0x61a, 0x620...0x669, 0x66e...0x6d3, 0x6d5...0x6dc, 0x6df...0x6e8, 0x6ea...0x6fc, 0x6ff...0x6ff, 0x710...0x74a, 0x74d...0x7b1, 0x7c0...0x7f5, 0x7fa...0x7fa, 0x7fd...0x7fd, 0x800...0x82d, 0x840...0x85b, 0x860...0x86a, 0x8a0...0x8b4, 0x8b6...0x8c7, 0x8d3...0x8e1, 0x8e3...0x963, 0x966...0x96f, 0x971...0x983, 0x985...0x98c, 0x98f...0x990, 0x993...0x9a8, 0x9aa...0x9b0, 0x9b2...0x9b2, 0x9b6...0x9b9, 0x9bc...0x9c4, 0x9c7...0x9c8, 0x9cb...0x9ce, 0x9d7...0x9d7, 0x9dc...0x9dd, 0x9df...0x9e3, 0x9e6...0x9f1, 0x9fc...0x9fc, 0x9fe...0x9fe, 0xa01...0xa03, 0xa05...0xa0a, 0xa0f...0xa10, 0xa13...0xa28, 0xa2a...0xa30, 0xa32...0xa33, 0xa35...0xa36, 0xa38...0xa39, 0xa3c...0xa3c, 0xa3e...0xa42, 0xa47...0xa48, 0xa4b...0xa4d, 0xa51...0xa51, 0xa59...0xa5c, 0xa5e...0xa5e, 0xa66...0xa75, 0xa81...0xa83, 0xa85...0xa8d, 0xa8f...0xa91, 0xa93...0xaa8, 0xaaa...0xab0, 0xab2...0xab3, 0xab5...0xab9, 0xabc...0xac5, 0xac7...0xac9, 0xacb...0xacd, 0xad0...0xad0, 0xae0...0xae3, 0xae6...0xaef, 0xaf9...0xaff, 0xb01...0xb03, 0xb05...0xb0c, 0xb0f...0xb10, 0xb13...0xb28, 0xb2a...0xb30, 0xb32...0xb33, 0xb35...0xb39, 0xb3c...0xb44, 0xb47...0xb48, 0xb4b...0xb4d, 0xb55...0xb57, 0xb5c...0xb5d, 0xb5f...0xb63, 0xb66...0xb6f, 0xb71...0xb71, 0xb82...0xb83, 0xb85...0xb8a, 0xb8e...0xb90, 0xb92...0xb95, 0xb99...0xb9a, 0xb9c...0xb9c, 0xb9e...0xb9f, 0xba3...0xba4, 0xba8...0xbaa, 0xbae...0xbb9, 0xbbe...0xbc2, 0xbc6...0xbc8, 0xbca...0xbcd, 0xbd0...0xbd0, 0xbd7...0xbd7, 0xbe6...0xbef, 0xc00...0xc0c, 0xc0e...0xc10, 0xc12...0xc28, 0xc2a...0xc39, 0xc3d...0xc44, 0xc46...0xc48, 0xc4a...0xc4d, 0xc55...0xc56, 0xc58...0xc5a, 0xc60...0xc63, 0xc66...0xc6f, 0xc80...0xc83, 0xc85...0xc8c, 0xc8e...0xc90, 0xc92...0xca8, 0xcaa...0xcb3, 0xcb5...0xcb9, 0xcbc...0xcc4, 0xcc6...0xcc8, 0xcca...0xccd, 0xcd5...0xcd6, 0xcde...0xcde, 0xce0...0xce3, 0xce6...0xcef, 0xcf1...0xcf2, 0xd00...0xd0c, 0xd0e...0xd10, 0xd12...0xd44, 0xd46...0xd48, 0xd4a...0xd4e, 0xd54...0xd57, 0xd5f...0xd63, 0xd66...0xd6f, 0xd7a...0xd7f, 0xd81...0xd83, 0xd85...0xd96, 0xd9a...0xdb1, 0xdb3...0xdbb, 0xdbd...0xdbd, 0xdc0...0xdc6, 0xdca...0xdca, 0xdcf...0xdd4, 0xdd6...0xdd6, 0xdd8...0xddf, 0xde6...0xdef, 0xdf2...0xdf3, 0xe01...0xe3a, 0xe40...0xe4e, 0xe50...0xe59, 0xe81...0xe82, 0xe84...0xe84, 0xe86...0xe8a, 0xe8c...0xea3, 0xea5...0xea5, 0xea7...0xebd, 0xec0...0xec4, 0xec6...0xec6, 0xec8...0xecd, 0xed0...0xed9, 0xedc...0xedf, 0xf00...0xf00, 0xf18...0xf19, 0xf20...0xf29, 0xf35...0xf35, 0xf37...0xf37, 0xf39...0xf39, 0xf3e...0xf47, 0xf49...0xf6c, 0xf71...0xf84, 0xf86...0xf97, 0xf99...0xfbc, 0xfc6...0xfc6, 0x1000...0x1049, 0x1050...0x109d, 0x10a0...0x10c5, 0x10c7...0x10c7, 0x10cd...0x10cd, 0x10d0...0x10fa, 0x10fc...0x1248, 0x124a...0x124d, 0x1250...0x1256, 0x1258...0x1258, 0x125a...0x125d, 0x1260...0x1288, 0x128a...0x128d, 0x1290...0x12b0, 0x12b2...0x12b5, 0x12b8...0x12be, 0x12c0...0x12c0, 0x12c2...0x12c5, 0x12c8...0x12d6, 0x12d8...0x1310, 0x1312...0x1315, 0x1318...0x135a, 0x135d...0x135f, 0x1369...0x1371, 0x1380...0x138f, 0x13a0...0x13f5, 0x13f8...0x13fd, 0x1401...0x166c, 0x166f...0x167f, 0x1681...0x169a, 0x16a0...0x16ea, 0x16ee...0x16f8, 0x1700...0x170c, 0x170e...0x1714, 0x1720...0x1734, 0x1740...0x1753, 0x1760...0x176c, 0x176e...0x1770, 0x1772...0x1773, 0x1780...0x17d3, 0x17d7...0x17d7, 0x17dc...0x17dd, 0x17e0...0x17e9, 0x180b...0x180d, 0x1810...0x1819, 0x1820...0x1878, 0x1880...0x18aa, 0x18b0...0x18f5, 0x1900...0x191e, 0x1920...0x192b, 0x1930...0x193b, 0x1946...0x196d, 0x1970...0x1974, 0x1980...0x19ab, 0x19b0...0x19c9, 0x19d0...0x19da, 0x1a00...0x1a1b, 0x1a20...0x1a5e, 0x1a60...0x1a7c, 0x1a7f...0x1a89, 0x1a90...0x1a99, 0x1aa7...0x1aa7, 0x1ab0...0x1abd, 0x1abf...0x1ac0, 0x1b00...0x1b4b, 0x1b50...0x1b59, 0x1b6b...0x1b73, 0x1b80...0x1bf3, 0x1c00...0x1c37, 0x1c40...0x1c49, 0x1c4d...0x1c7d, 0x1c80...0x1c88, 0x1c90...0x1cba, 0x1cbd...0x1cbf, 0x1cd0...0x1cd2, 0x1cd4...0x1cfa, 0x1d00...0x1df9, 0x1dfb...0x1f15, 0x1f18...0x1f1d, 0x1f20...0x1f45, 0x1f48...0x1f4d, 0x1f50...0x1f57, 0x1f59...0x1f59, 0x1f5b...0x1f5b, 0x1f5d...0x1f5d, 0x1f5f...0x1f7d, 0x1f80...0x1fb4, 0x1fb6...0x1fbc, 0x1fbe...0x1fbe, 0x1fc2...0x1fc4, 0x1fc6...0x1fcc, 0x1fd0...0x1fd3, 0x1fd6...0x1fdb, 0x1fe0...0x1fec, 0x1ff2...0x1ff4, 0x1ff6...0x1ffc, 0x203f...0x2040, 0x2054...0x2054, 0x2071...0x2071, 0x207f...0x207f, 0x2090...0x209c, 0x20d0...0x20dc, 0x20e1...0x20e1, 0x20e5...0x20f0, 0x2102...0x2102, 0x2107...0x2107, 0x210a...0x2113, 0x2115...0x2115, 0x2118...0x211d, 0x2124...0x2124, 0x2126...0x2126, 0x2128...0x2128, 0x212a...0x2139, 0x213c...0x213f, 0x2145...0x2149, 0x214e...0x214e, 0x2160...0x2188, 0x2c00...0x2c2e, 0x2c30...0x2c5e, 0x2c60...0x2ce4, 0x2ceb...0x2cf3, 0x2d00...0x2d25, 0x2d27...0x2d27, 0x2d2d...0x2d2d, 0x2d30...0x2d67, 0x2d6f...0x2d6f, 0x2d7f...0x2d96, 0x2da0...0x2da6, 0x2da8...0x2dae, 0x2db0...0x2db6, 0x2db8...0x2dbe, 0x2dc0...0x2dc6, 0x2dc8...0x2dce, 0x2dd0...0x2dd6, 0x2dd8...0x2dde, 0x2de0...0x2dff, 0x3005...0x3007, 0x3021...0x302f, 0x3031...0x3035, 0x3038...0x303c, 0x3041...0x3096, 0x3099...0x309f, 0x30a1...0x30ff, 0x3105...0x312f, 0x3131...0x318e, 0x31a0...0x31bf, 0x31f0...0x31ff, 0x3400...0x4dbf, 0x4e00...0x9ffc, 0xa000...0xa48c, 0xa4d0...0xa4fd, 0xa500...0xa60c, 0xa610...0xa62b, 0xa640...0xa66f, 0xa674...0xa67d, 0xa67f...0xa6f1, 0xa717...0xa71f, 0xa722...0xa788, 0xa78b...0xa7bf, 0xa7c2...0xa7ca, 0xa7f5...0xa827, 0xa82c...0xa82c, 0xa840...0xa873, 0xa880...0xa8c5, 0xa8d0...0xa8d9, 0xa8e0...0xa8f7, 0xa8fb...0xa8fb, 0xa8fd...0xa92d, 0xa930...0xa953, 0xa960...0xa97c, 0xa980...0xa9c0, 0xa9cf...0xa9d9, 0xa9e0...0xa9fe, 0xaa00...0xaa36, 0xaa40...0xaa4d, 0xaa50...0xaa59, 0xaa60...0xaa76, 0xaa7a...0xaac2, 0xaadb...0xaadd, 0xaae0...0xaaef, 0xaaf2...0xaaf6, 0xab01...0xab06, 0xab09...0xab0e, 0xab11...0xab16, 0xab20...0xab26, 0xab28...0xab2e, 0xab30...0xab5a, 0xab5c...0xab69, 0xab70...0xabea, 0xabec...0xabed, 0xabf0...0xabf9, 0xac00...0xd7a3, 0xd7b0...0xd7c6, 0xd7cb...0xd7fb, 0xf900...0xfa6d, 0xfa70...0xfad9, 0xfb00...0xfb06, 0xfb13...0xfb17, 0xfb1d...0xfb28, 0xfb2a...0xfb36, 0xfb38...0xfb3c, 0xfb3e...0xfb3e, 0xfb40...0xfb41, 0xfb43...0xfb44, 0xfb46...0xfbb1, 0xfbd3...0xfd3d, 0xfd50...0xfd8f, 0xfd92...0xfdc7, 0xfdf0...0xfdfb, 0xfe00...0xfe0f, 0xfe20...0xfe2f, 0xfe33...0xfe34, 0xfe4d...0xfe4f, 0xfe70...0xfe74, 0xfe76...0xfefc, 0xff10...0xff19, 0xff21...0xff3a, 0xff3f...0xff3f, 0xff41...0xff5a, 0xff65...0xffbe, 0xffc2...0xffc7, 0xffca...0xffcf, 0xffd2...0xffd7 => true, - else => false, - }; - } - - fn isIdentifierPartSlow32(codepoint: i32) bool { - return switch (codepoint) { - 0xffda...0xffdc, 0x10000...0x1000b, 0x1000d...0x10026, 0x10028...0x1003a, 0x1003c...0x1003d, 0x1003f...0x1004d, 0x10050...0x1005d, 0x10080...0x100fa, 0x10140...0x10174, 0x101fd...0x101fd, 0x10280...0x1029c, 0x102a0...0x102d0, 0x102e0...0x102e0, 0x10300...0x1031f, 0x1032d...0x1034a, 0x10350...0x1037a, 0x10380...0x1039d, 0x103a0...0x103c3, 0x103c8...0x103cf, 0x103d1...0x103d5, 0x10400...0x1049d, 0x104a0...0x104a9, 0x104b0...0x104d3, 0x104d8...0x104fb, 0x10500...0x10527, 0x10530...0x10563, 0x10600...0x10736, 0x10740...0x10755, 0x10760...0x10767, 0x10800...0x10805, 0x10808...0x10808, 0x1080a...0x10835, 0x10837...0x10838, 0x1083c...0x1083c, 0x1083f...0x10855, 0x10860...0x10876, 0x10880...0x1089e, 0x108e0...0x108f2, 0x108f4...0x108f5, 0x10900...0x10915, 0x10920...0x10939, 0x10980...0x109b7, 0x109be...0x109bf, 0x10a00...0x10a03, 0x10a05...0x10a06, 0x10a0c...0x10a13, 0x10a15...0x10a17, 0x10a19...0x10a35, 0x10a38...0x10a3a, 0x10a3f...0x10a3f, 0x10a60...0x10a7c, 0x10a80...0x10a9c, 0x10ac0...0x10ac7, 0x10ac9...0x10ae6, 0x10b00...0x10b35, 0x10b40...0x10b55, 0x10b60...0x10b72, 0x10b80...0x10b91, 0x10c00...0x10c48, 0x10c80...0x10cb2, 0x10cc0...0x10cf2, 0x10d00...0x10d27, 0x10d30...0x10d39, 0x10e80...0x10ea9, 0x10eab...0x10eac, 0x10eb0...0x10eb1, 0x10f00...0x10f1c, 0x10f27...0x10f27, 0x10f30...0x10f50, 0x10fb0...0x10fc4, 0x10fe0...0x10ff6, 0x11000...0x11046, 0x11066...0x1106f, 0x1107f...0x110ba, 0x110d0...0x110e8, 0x110f0...0x110f9, 0x11100...0x11134, 0x11136...0x1113f, 0x11144...0x11147, 0x11150...0x11173, 0x11176...0x11176, 0x11180...0x111c4, 0x111c9...0x111cc, 0x111ce...0x111da, 0x111dc...0x111dc, 0x11200...0x11211, 0x11213...0x11237, 0x1123e...0x1123e, 0x11280...0x11286, 0x11288...0x11288, 0x1128a...0x1128d, 0x1128f...0x1129d, 0x1129f...0x112a8, 0x112b0...0x112ea, 0x112f0...0x112f9, 0x11300...0x11303, 0x11305...0x1130c, 0x1130f...0x11310, 0x11313...0x11328, 0x1132a...0x11330, 0x11332...0x11333, 0x11335...0x11339, 0x1133b...0x11344, 0x11347...0x11348, 0x1134b...0x1134d, 0x11350...0x11350, 0x11357...0x11357, 0x1135d...0x11363, 0x11366...0x1136c, 0x11370...0x11374, 0x11400...0x1144a, 0x11450...0x11459, 0x1145e...0x11461, 0x11480...0x114c5, 0x114c7...0x114c7, 0x114d0...0x114d9, 0x11580...0x115b5, 0x115b8...0x115c0, 0x115d8...0x115dd, 0x11600...0x11640, 0x11644...0x11644, 0x11650...0x11659, 0x11680...0x116b8, 0x116c0...0x116c9, 0x11700...0x1171a, 0x1171d...0x1172b, 0x11730...0x11739, 0x11800...0x1183a, 0x118a0...0x118e9, 0x118ff...0x11906, 0x11909...0x11909, 0x1190c...0x11913, 0x11915...0x11916, 0x11918...0x11935, 0x11937...0x11938, 0x1193b...0x11943, 0x11950...0x11959, 0x119a0...0x119a7, 0x119aa...0x119d7, 0x119da...0x119e1, 0x119e3...0x119e4, 0x11a00...0x11a3e, 0x11a47...0x11a47, 0x11a50...0x11a99, 0x11a9d...0x11a9d, 0x11ac0...0x11af8, 0x11c00...0x11c08, 0x11c0a...0x11c36, 0x11c38...0x11c40, 0x11c50...0x11c59, 0x11c72...0x11c8f, 0x11c92...0x11ca7, 0x11ca9...0x11cb6, 0x11d00...0x11d06, 0x11d08...0x11d09, 0x11d0b...0x11d36, 0x11d3a...0x11d3a, 0x11d3c...0x11d3d, 0x11d3f...0x11d47, 0x11d50...0x11d59, 0x11d60...0x11d65, 0x11d67...0x11d68, 0x11d6a...0x11d8e, 0x11d90...0x11d91, 0x11d93...0x11d98, 0x11da0...0x11da9, 0x11ee0...0x11ef6, 0x11fb0...0x11fb0, 0x12000...0x12399, 0x12400...0x1246e, 0x12480...0x12543, 0x13000...0x1342e, 0x14400...0x14646, 0x16800...0x16a38, 0x16a40...0x16a5e, 0x16a60...0x16a69, 0x16ad0...0x16aed, 0x16af0...0x16af4, 0x16b00...0x16b36, 0x16b40...0x16b43, 0x16b50...0x16b59, 0x16b63...0x16b77, 0x16b7d...0x16b8f, 0x16e40...0x16e7f, 0x16f00...0x16f4a, 0x16f4f...0x16f87, 0x16f8f...0x16f9f, 0x16fe0...0x16fe1, 0x16fe3...0x16fe4, 0x16ff0...0x16ff1, 0x17000...0x187f7, 0x18800...0x18cd5, 0x18d00...0x18d08, 0x1b000...0x1b11e, 0x1b150...0x1b152, 0x1b164...0x1b167, 0x1b170...0x1b2fb, 0x1bc00...0x1bc6a, 0x1bc70...0x1bc7c, 0x1bc80...0x1bc88, 0x1bc90...0x1bc99, 0x1bc9d...0x1bc9e, 0x1d165...0x1d169, 0x1d16d...0x1d172, 0x1d17b...0x1d182, 0x1d185...0x1d18b, 0x1d1aa...0x1d1ad, 0x1d242...0x1d244, 0x1d400...0x1d454, 0x1d456...0x1d49c, 0x1d49e...0x1d49f, 0x1d4a2...0x1d4a2, 0x1d4a5...0x1d4a6, 0x1d4a9...0x1d4ac, 0x1d4ae...0x1d4b9, 0x1d4bb...0x1d4bb, 0x1d4bd...0x1d4c3, 0x1d4c5...0x1d505, 0x1d507...0x1d50a, 0x1d50d...0x1d514, 0x1d516...0x1d51c, 0x1d51e...0x1d539, 0x1d53b...0x1d53e, 0x1d540...0x1d544, 0x1d546...0x1d546, 0x1d54a...0x1d550, 0x1d552...0x1d6a5, 0x1d6a8...0x1d6c0, 0x1d6c2...0x1d6da, 0x1d6dc...0x1d6fa, 0x1d6fc...0x1d714, 0x1d716...0x1d734, 0x1d736...0x1d74e, 0x1d750...0x1d76e, 0x1d770...0x1d788, 0x1d78a...0x1d7a8, 0x1d7aa...0x1d7c2, 0x1d7c4...0x1d7cb, 0x1d7ce...0x1d7ff, 0x1da00...0x1da36, 0x1da3b...0x1da6c, 0x1da75...0x1da75, 0x1da84...0x1da84, 0x1da9b...0x1da9f, 0x1daa1...0x1daaf, 0x1e000...0x1e006, 0x1e008...0x1e018, 0x1e01b...0x1e021, 0x1e023...0x1e024, 0x1e026...0x1e02a, 0x1e100...0x1e12c, 0x1e130...0x1e13d, 0x1e140...0x1e149, 0x1e14e...0x1e14e, 0x1e2c0...0x1e2f9, 0x1e800...0x1e8c4, 0x1e8d0...0x1e8d6, 0x1e900...0x1e94b, 0x1e950...0x1e959, 0x1ee00...0x1ee03, 0x1ee05...0x1ee1f, 0x1ee21...0x1ee22, 0x1ee24...0x1ee24, 0x1ee27...0x1ee27, 0x1ee29...0x1ee32, 0x1ee34...0x1ee37, 0x1ee39...0x1ee39, 0x1ee3b...0x1ee3b, 0x1ee42...0x1ee42, 0x1ee47...0x1ee47, 0x1ee49...0x1ee49, 0x1ee4b...0x1ee4b, 0x1ee4d...0x1ee4f, 0x1ee51...0x1ee52, 0x1ee54...0x1ee54, 0x1ee57...0x1ee57, 0x1ee59...0x1ee59, 0x1ee5b...0x1ee5b, 0x1ee5d...0x1ee5d, 0x1ee5f...0x1ee5f, 0x1ee61...0x1ee62, 0x1ee64...0x1ee64, 0x1ee67...0x1ee6a, 0x1ee6c...0x1ee72, 0x1ee74...0x1ee77, 0x1ee79...0x1ee7c, 0x1ee7e...0x1ee7e, 0x1ee80...0x1ee89, 0x1ee8b...0x1ee9b, 0x1eea1...0x1eea3, 0x1eea5...0x1eea9, 0x1eeab...0x1eebb, 0x1fbf0...0x1fbf9, 0x20000...0x2a6dd, 0x2a700...0x2b734, 0x2b740...0x2b81d, 0x2b820...0x2cea1, 0x2ceb0...0x2ebe0, 0x2f800...0x2fa1d, 0x30000...0x3134a, 0xe0100...0xe01ef => true, - else => false, - }; - } - - fn isIdentifierStartSlow16(codepoint: u16) bool { - return switch (codepoint) { - 0xaa...0xaa, 0xb5...0xb5, 0xba...0xba, 0xc0...0xd6, 0xd8...0xf6, 0xf8...0x2c1, 0x2c6...0x2d1, 0x2e0...0x2e4, 0x2ec...0x2ec, 0x2ee...0x2ee, 0x370...0x374, 0x376...0x377, 0x37a...0x37d, 0x37f...0x37f, 0x386...0x386, 0x388...0x38a, 0x38c...0x38c, 0x38e...0x3a1, 0x3a3...0x3f5, 0x3f7...0x481, 0x48a...0x52f, 0x531...0x556, 0x559...0x559, 0x560...0x588, 0x5d0...0x5ea, 0x5ef...0x5f2, 0x620...0x64a, 0x66e...0x66f, 0x671...0x6d3, 0x6d5...0x6d5, 0x6e5...0x6e6, 0x6ee...0x6ef, 0x6fa...0x6fc, 0x6ff...0x6ff, 0x710...0x710, 0x712...0x72f, 0x74d...0x7a5, 0x7b1...0x7b1, 0x7ca...0x7ea, 0x7f4...0x7f5, 0x7fa...0x7fa, 0x800...0x815, 0x81a...0x81a, 0x824...0x824, 0x828...0x828, 0x840...0x858, 0x860...0x86a, 0x8a0...0x8b4, 0x8b6...0x8c7, 0x904...0x939, 0x93d...0x93d, 0x950...0x950, 0x958...0x961, 0x971...0x980, 0x985...0x98c, 0x98f...0x990, 0x993...0x9a8, 0x9aa...0x9b0, 0x9b2...0x9b2, 0x9b6...0x9b9, 0x9bd...0x9bd, 0x9ce...0x9ce, 0x9dc...0x9dd, 0x9df...0x9e1, 0x9f0...0x9f1, 0x9fc...0x9fc, 0xa05...0xa0a, 0xa0f...0xa10, 0xa13...0xa28, 0xa2a...0xa30, 0xa32...0xa33, 0xa35...0xa36, 0xa38...0xa39, 0xa59...0xa5c, 0xa5e...0xa5e, 0xa72...0xa74, 0xa85...0xa8d, 0xa8f...0xa91, 0xa93...0xaa8, 0xaaa...0xab0, 0xab2...0xab3, 0xab5...0xab9, 0xabd...0xabd, 0xad0...0xad0, 0xae0...0xae1, 0xaf9...0xaf9, 0xb05...0xb0c, 0xb0f...0xb10, 0xb13...0xb28, 0xb2a...0xb30, 0xb32...0xb33, 0xb35...0xb39, 0xb3d...0xb3d, 0xb5c...0xb5d, 0xb5f...0xb61, 0xb71...0xb71, 0xb83...0xb83, 0xb85...0xb8a, 0xb8e...0xb90, 0xb92...0xb95, 0xb99...0xb9a, 0xb9c...0xb9c, 0xb9e...0xb9f, 0xba3...0xba4, 0xba8...0xbaa, 0xbae...0xbb9, 0xbd0...0xbd0, 0xc05...0xc0c, 0xc0e...0xc10, 0xc12...0xc28, 0xc2a...0xc39, 0xc3d...0xc3d, 0xc58...0xc5a, 0xc60...0xc61, 0xc80...0xc80, 0xc85...0xc8c, 0xc8e...0xc90, 0xc92...0xca8, 0xcaa...0xcb3, 0xcb5...0xcb9, 0xcbd...0xcbd, 0xcde...0xcde, 0xce0...0xce1, 0xcf1...0xcf2, 0xd04...0xd0c, 0xd0e...0xd10, 0xd12...0xd3a, 0xd3d...0xd3d, 0xd4e...0xd4e, 0xd54...0xd56, 0xd5f...0xd61, 0xd7a...0xd7f, 0xd85...0xd96, 0xd9a...0xdb1, 0xdb3...0xdbb, 0xdbd...0xdbd, 0xdc0...0xdc6, 0xe01...0xe30, 0xe32...0xe33, 0xe40...0xe46, 0xe81...0xe82, 0xe84...0xe84, 0xe86...0xe8a, 0xe8c...0xea3, 0xea5...0xea5, 0xea7...0xeb0, 0xeb2...0xeb3, 0xebd...0xebd, 0xec0...0xec4, 0xec6...0xec6, 0xedc...0xedf, 0xf00...0xf00, 0xf40...0xf47, 0xf49...0xf6c, 0xf88...0xf8c, 0x1000...0x102a, 0x103f...0x103f, 0x1050...0x1055, 0x105a...0x105d, 0x1061...0x1061, 0x1065...0x1066, 0x106e...0x1070, 0x1075...0x1081, 0x108e...0x108e, 0x10a0...0x10c5, 0x10c7...0x10c7, 0x10cd...0x10cd, 0x10d0...0x10fa, 0x10fc...0x1248, 0x124a...0x124d, 0x1250...0x1256, 0x1258...0x1258, 0x125a...0x125d, 0x1260...0x1288, 0x128a...0x128d, 0x1290...0x12b0, 0x12b2...0x12b5, 0x12b8...0x12be, 0x12c0...0x12c0, 0x12c2...0x12c5, 0x12c8...0x12d6, 0x12d8...0x1310, 0x1312...0x1315, 0x1318...0x135a, 0x1380...0x138f, 0x13a0...0x13f5, 0x13f8...0x13fd, 0x1401...0x166c, 0x166f...0x167f, 0x1681...0x169a, 0x16a0...0x16ea, 0x16ee...0x16f8, 0x1700...0x170c, 0x170e...0x1711, 0x1720...0x1731, 0x1740...0x1751, 0x1760...0x176c, 0x176e...0x1770, 0x1780...0x17b3, 0x17d7...0x17d7, 0x17dc...0x17dc, 0x1820...0x1878, 0x1880...0x18a8, 0x18aa...0x18aa, 0x18b0...0x18f5, 0x1900...0x191e, 0x1950...0x196d, 0x1970...0x1974, 0x1980...0x19ab, 0x19b0...0x19c9, 0x1a00...0x1a16, 0x1a20...0x1a54, 0x1aa7...0x1aa7, 0x1b05...0x1b33, 0x1b45...0x1b4b, 0x1b83...0x1ba0, 0x1bae...0x1baf, 0x1bba...0x1be5, 0x1c00...0x1c23, 0x1c4d...0x1c4f, 0x1c5a...0x1c7d, 0x1c80...0x1c88, 0x1c90...0x1cba, 0x1cbd...0x1cbf, 0x1ce9...0x1cec, 0x1cee...0x1cf3, 0x1cf5...0x1cf6, 0x1cfa...0x1cfa, 0x1d00...0x1dbf, 0x1e00...0x1f15, 0x1f18...0x1f1d, 0x1f20...0x1f45, 0x1f48...0x1f4d, 0x1f50...0x1f57, 0x1f59...0x1f59, 0x1f5b...0x1f5b, 0x1f5d...0x1f5d, 0x1f5f...0x1f7d, 0x1f80...0x1fb4, 0x1fb6...0x1fbc, 0x1fbe...0x1fbe, 0x1fc2...0x1fc4, 0x1fc6...0x1fcc, 0x1fd0...0x1fd3, 0x1fd6...0x1fdb, 0x1fe0...0x1fec, 0x1ff2...0x1ff4, 0x1ff6...0x1ffc, 0x2071...0x2071, 0x207f...0x207f, 0x2090...0x209c, 0x2102...0x2102, 0x2107...0x2107, 0x210a...0x2113, 0x2115...0x2115, 0x2118...0x211d, 0x2124...0x2124, 0x2126...0x2126, 0x2128...0x2128, 0x212a...0x2139, 0x213c...0x213f, 0x2145...0x2149, 0x214e...0x214e, 0x2160...0x2188, 0x2c00...0x2c2e, 0x2c30...0x2c5e, 0x2c60...0x2ce4, 0x2ceb...0x2cee, 0x2cf2...0x2cf3, 0x2d00...0x2d25, 0x2d27...0x2d27, 0x2d2d...0x2d2d, 0x2d30...0x2d67, 0x2d6f...0x2d6f, 0x2d80...0x2d96, 0x2da0...0x2da6, 0x2da8...0x2dae, 0x2db0...0x2db6, 0x2db8...0x2dbe, 0x2dc0...0x2dc6, 0x2dc8...0x2dce, 0x2dd0...0x2dd6, 0x2dd8...0x2dde, 0x3005...0x3007, 0x3021...0x3029, 0x3031...0x3035, 0x3038...0x303c, 0x3041...0x3096, 0x309b...0x309f, 0x30a1...0x30fa, 0x30fc...0x30ff, 0x3105...0x312f, 0x3131...0x318e, 0x31a0...0x31bf, 0x31f0...0x31ff, 0x3400...0x4dbf, 0x4e00...0x9ffc, 0xa000...0xa48c, 0xa4d0...0xa4fd, 0xa500...0xa60c, 0xa610...0xa61f, 0xa62a...0xa62b, 0xa640...0xa66e, 0xa67f...0xa69d, 0xa6a0...0xa6ef, 0xa717...0xa71f, 0xa722...0xa788, 0xa78b...0xa7bf, 0xa7c2...0xa7ca, 0xa7f5...0xa801, 0xa803...0xa805, 0xa807...0xa80a, 0xa80c...0xa822, 0xa840...0xa873, 0xa882...0xa8b3, 0xa8f2...0xa8f7, 0xa8fb...0xa8fb, 0xa8fd...0xa8fe, 0xa90a...0xa925, 0xa930...0xa946, 0xa960...0xa97c, 0xa984...0xa9b2, 0xa9cf...0xa9cf, 0xa9e0...0xa9e4, 0xa9e6...0xa9ef, 0xa9fa...0xa9fe, 0xaa00...0xaa28, 0xaa40...0xaa42, 0xaa44...0xaa4b, 0xaa60...0xaa76, 0xaa7a...0xaa7a, 0xaa7e...0xaaaf, 0xaab1...0xaab1, 0xaab5...0xaab6, 0xaab9...0xaabd, 0xaac0...0xaac0, 0xaac2...0xaac2, 0xaadb...0xaadd, 0xaae0...0xaaea, 0xaaf2...0xaaf4, 0xab01...0xab06, 0xab09...0xab0e, 0xab11...0xab16, 0xab20...0xab26, 0xab28...0xab2e, 0xab30...0xab5a, 0xab5c...0xab69, 0xab70...0xabe2, 0xac00...0xd7a3, 0xd7b0...0xd7c6, 0xd7cb...0xd7fb, 0xf900...0xfa6d, 0xfa70...0xfad9, 0xfb00...0xfb06, 0xfb13...0xfb17, 0xfb1d...0xfb1d, 0xfb1f...0xfb28, 0xfb2a...0xfb36, 0xfb38...0xfb3c, 0xfb3e...0xfb3e, 0xfb40...0xfb41, 0xfb43...0xfb44, 0xfb46...0xfbb1, 0xfbd3...0xfd3d, 0xfd50...0xfd8f, 0xfd92...0xfdc7 => true, - else => false, - }; - } - - fn isIdentifierStartSlow32(codepoint: i32) bool { - return switch (codepoint) { - 0xfdf0...0xfdfb, 0xfe70...0xfe74, 0xfe76...0xfefc, 0xff21...0xff3a, 0xff41...0xff5a, 0xff66...0xffbe, 0xffc2...0xffc7, 0xffca...0xffcf, 0xffd2...0xffd7, 0xffda...0xffdc, 0x10000...0x1000b, 0x1000d...0x10026, 0x10028...0x1003a, 0x1003c...0x1003d, 0x1003f...0x1004d, 0x10050...0x1005d, 0x10080...0x100fa, 0x10140...0x10174, 0x10280...0x1029c, 0x102a0...0x102d0, 0x10300...0x1031f, 0x1032d...0x1034a, 0x10350...0x10375, 0x10380...0x1039d, 0x103a0...0x103c3, 0x103c8...0x103cf, 0x103d1...0x103d5, 0x10400...0x1049d, 0x104b0...0x104d3, 0x104d8...0x104fb, 0x10500...0x10527, 0x10530...0x10563, 0x10600...0x10736, 0x10740...0x10755, 0x10760...0x10767, 0x10800...0x10805, 0x10808...0x10808, 0x1080a...0x10835, 0x10837...0x10838, 0x1083c...0x1083c, 0x1083f...0x10855, 0x10860...0x10876, 0x10880...0x1089e, 0x108e0...0x108f2, 0x108f4...0x108f5, 0x10900...0x10915, 0x10920...0x10939, 0x10980...0x109b7, 0x109be...0x109bf, 0x10a00...0x10a00, 0x10a10...0x10a13, 0x10a15...0x10a17, 0x10a19...0x10a35, 0x10a60...0x10a7c, 0x10a80...0x10a9c, 0x10ac0...0x10ac7, 0x10ac9...0x10ae4, 0x10b00...0x10b35, 0x10b40...0x10b55, 0x10b60...0x10b72, 0x10b80...0x10b91, 0x10c00...0x10c48, 0x10c80...0x10cb2, 0x10cc0...0x10cf2, 0x10d00...0x10d23, 0x10e80...0x10ea9, 0x10eb0...0x10eb1, 0x10f00...0x10f1c, 0x10f27...0x10f27, 0x10f30...0x10f45, 0x10fb0...0x10fc4, 0x10fe0...0x10ff6, 0x11003...0x11037, 0x11083...0x110af, 0x110d0...0x110e8, 0x11103...0x11126, 0x11144...0x11144, 0x11147...0x11147, 0x11150...0x11172, 0x11176...0x11176, 0x11183...0x111b2, 0x111c1...0x111c4, 0x111da...0x111da, 0x111dc...0x111dc, 0x11200...0x11211, 0x11213...0x1122b, 0x11280...0x11286, 0x11288...0x11288, 0x1128a...0x1128d, 0x1128f...0x1129d, 0x1129f...0x112a8, 0x112b0...0x112de, 0x11305...0x1130c, 0x1130f...0x11310, 0x11313...0x11328, 0x1132a...0x11330, 0x11332...0x11333, 0x11335...0x11339, 0x1133d...0x1133d, 0x11350...0x11350, 0x1135d...0x11361, 0x11400...0x11434, 0x11447...0x1144a, 0x1145f...0x11461, 0x11480...0x114af, 0x114c4...0x114c5, 0x114c7...0x114c7, 0x11580...0x115ae, 0x115d8...0x115db, 0x11600...0x1162f, 0x11644...0x11644, 0x11680...0x116aa, 0x116b8...0x116b8, 0x11700...0x1171a, 0x11800...0x1182b, 0x118a0...0x118df, 0x118ff...0x11906, 0x11909...0x11909, 0x1190c...0x11913, 0x11915...0x11916, 0x11918...0x1192f, 0x1193f...0x1193f, 0x11941...0x11941, 0x119a0...0x119a7, 0x119aa...0x119d0, 0x119e1...0x119e1, 0x119e3...0x119e3, 0x11a00...0x11a00, 0x11a0b...0x11a32, 0x11a3a...0x11a3a, 0x11a50...0x11a50, 0x11a5c...0x11a89, 0x11a9d...0x11a9d, 0x11ac0...0x11af8, 0x11c00...0x11c08, 0x11c0a...0x11c2e, 0x11c40...0x11c40, 0x11c72...0x11c8f, 0x11d00...0x11d06, 0x11d08...0x11d09, 0x11d0b...0x11d30, 0x11d46...0x11d46, 0x11d60...0x11d65, 0x11d67...0x11d68, 0x11d6a...0x11d89, 0x11d98...0x11d98, 0x11ee0...0x11ef2, 0x11fb0...0x11fb0, 0x12000...0x12399, 0x12400...0x1246e, 0x12480...0x12543, 0x13000...0x1342e, 0x14400...0x14646, 0x16800...0x16a38, 0x16a40...0x16a5e, 0x16ad0...0x16aed, 0x16b00...0x16b2f, 0x16b40...0x16b43, 0x16b63...0x16b77, 0x16b7d...0x16b8f, 0x16e40...0x16e7f, 0x16f00...0x16f4a, 0x16f50...0x16f50, 0x16f93...0x16f9f, 0x16fe0...0x16fe1, 0x16fe3...0x16fe3, 0x17000...0x187f7, 0x18800...0x18cd5, 0x18d00...0x18d08, 0x1b000...0x1b11e, 0x1b150...0x1b152, 0x1b164...0x1b167, 0x1b170...0x1b2fb, 0x1bc00...0x1bc6a, 0x1bc70...0x1bc7c, 0x1bc80...0x1bc88, 0x1bc90...0x1bc99, 0x1d400...0x1d454, 0x1d456...0x1d49c, 0x1d49e...0x1d49f, 0x1d4a2...0x1d4a2, 0x1d4a5...0x1d4a6, 0x1d4a9...0x1d4ac, 0x1d4ae...0x1d4b9, 0x1d4bb...0x1d4bb, 0x1d4bd...0x1d4c3, 0x1d4c5...0x1d505, 0x1d507...0x1d50a, 0x1d50d...0x1d514, 0x1d516...0x1d51c, 0x1d51e...0x1d539, 0x1d53b...0x1d53e, 0x1d540...0x1d544, 0x1d546...0x1d546, 0x1d54a...0x1d550, 0x1d552...0x1d6a5, 0x1d6a8...0x1d6c0, 0x1d6c2...0x1d6da, 0x1d6dc...0x1d6fa, 0x1d6fc...0x1d714, 0x1d716...0x1d734, 0x1d736...0x1d74e, 0x1d750...0x1d76e, 0x1d770...0x1d788, 0x1d78a...0x1d7a8, 0x1d7aa...0x1d7c2, 0x1d7c4...0x1d7cb, 0x1e100...0x1e12c, 0x1e137...0x1e13d, 0x1e14e...0x1e14e, 0x1e2c0...0x1e2eb, 0x1e800...0x1e8c4, 0x1e900...0x1e943, 0x1e94b...0x1e94b, 0x1ee00...0x1ee03, 0x1ee05...0x1ee1f, 0x1ee21...0x1ee22, 0x1ee24...0x1ee24, 0x1ee27...0x1ee27, 0x1ee29...0x1ee32, 0x1ee34...0x1ee37, 0x1ee39...0x1ee39, 0x1ee3b...0x1ee3b, 0x1ee42...0x1ee42, 0x1ee47...0x1ee47, 0x1ee49...0x1ee49, 0x1ee4b...0x1ee4b, 0x1ee4d...0x1ee4f, 0x1ee51...0x1ee52, 0x1ee54...0x1ee54, 0x1ee57...0x1ee57, 0x1ee59...0x1ee59, 0x1ee5b...0x1ee5b, 0x1ee5d...0x1ee5d, 0x1ee5f...0x1ee5f, 0x1ee61...0x1ee62, 0x1ee64...0x1ee64, 0x1ee67...0x1ee6a, 0x1ee6c...0x1ee72, 0x1ee74...0x1ee77, 0x1ee79...0x1ee7c, 0x1ee7e...0x1ee7e, 0x1ee80...0x1ee89, 0x1ee8b...0x1ee9b, 0x1eea1...0x1eea3, 0x1eea5...0x1eea9, 0x1eeab...0x1eebb, 0x20000...0x2a6dd, 0x2a700...0x2b734, 0x2b740...0x2b81d, 0x2b820...0x2cea1, 0x2ceb0...0x2ebe0, 0x2f800...0x2fa1d, 0x30000...0x3134a => true, - else => false, - }; - } - - noinline fn isIdentifierStartSlow(codepoint: i32) bool { - @setCold(true); - return switch (codepoint) { - // explicitly tell LLVM's optimizer about values we know will not be in the range of this switch statement - - (max_codepoint + 1)...maxInt(i32), minInt(i32)...127 => unreachable, - 128...0xfdc7 => isIdentifierStartSlow16(@as(u16, @intCast(codepoint))), - 0xfdf0...0x3134a => isIdentifierStartSlow32(codepoint), - else => false, - }; - } - - pub inline fn isIdentifierStart(codepoint: i32) bool { - return switch (codepoint) { - 'A'...'Z', 'a'...'z', '$', '_' => true, - else => if (codepoint < 128) - return false - else - return isIdentifierStartSlow(codepoint), - }; - } - - pub inline fn isIdentifierPart(codepoint: i32) bool { - return switch (codepoint) { - 'A'...'Z', 'a'...'z', '0'...'9', '$', '_' => true, - else => if (codepoint < 128) - return false - else if (codepoint == 0x200C or codepoint == 0x200D) - return true - else - return isIdentifierPartSlow(codepoint), - }; - } +/// isIDContinueES5 checks if a codepoint is valid in the isIDContinueES5 category +pub fn isIDContinueES5(cp: u21) bool { + const high = cp >> 8; + const low = cp & 0xFF; + const stage2_idx = idContinueES5.stage1[high]; + const bit_pos = stage2_idx + low; + const u64_idx = bit_pos >> 6; + const bit_idx = @as(u6, @intCast(bit_pos & 63)); + return (idContinueES5.stage2[u64_idx] & (@as(u64, 1) << bit_idx)) != 0; +} +const idContinueES5 = struct { + pub const stage1 = [_]u16{ 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 256, 5376, 5632, 5888, 2048, 2048, 2048, 2048, 2048, 6144, 6400, 6656, 6912, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 7168, 7424, 2048, 2048, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 7680, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 7936, 256, 256, 256, 256, 8192, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 8448, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 256, 8704, 8960, 256, 9216, 9472, 9728, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048 }; + pub const stage2 = [_]u64{ 287948901175001088, 576460745995190270, 297241973452963840, 18410715276682199039, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 4503586742468607, 18446744073709486080, 18014187403249451007, 70501888360451, 18446744073709551615, 288230406216515583, 18446744056529672000, 4503599577006079, 18446744073709551615, 18446744073709551615, 18446744073709547643, 234187180623206815, 18446181123756130304, 18446744065161560063, 13546827661950451967, 1979120929931286, 576460743713488896, 18446466992488579071, 18446744073709551615, 2305629702346244095, 18446497783104864256, 2047, 562949953421311, 0, 0, 0, 0, 0, 17582052945254416366, 281268803551231, 15259882188367831022, 1125692414638495, 15235112390417287140, 9006925953907079, 17576984196650086382, 281204393851839, 17567976997395210222, 281215949093263, 14105211467435198444, 280925229301191, 14118782632783110126, 281212990012895, 14118782632783110124, 281214063754719, 14123286232410480620, 281212992110031, 3457638613854978028, 3377704004977791, 576460752303423486, 67076095, 4323434403644581270, 872365919, 14024213633433600001, 18446189919849152255, 2305843009196855263, 64, 272457864671395839, 67044351, 18446744069414584320, 36028797018898495, 18446744073709551615, 18446744071629176831, 18446743008557662207, 288230376151711743, 18446744073709551487, 18446744070446333311, 9168625153884503423, 18446603336212774717, 18446744071549321215, 1123701017804671, 18446744069414584320, 9007199254740991, 18446744073709551614, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 35923243902697471, 18446744069548802046, 8796093022207, 0, 0, 18446744073709551615, 4393752592383, 18446744069481627648, 72057594037927935, 4398046511103, 0, 18446744073709551615, 18446744073709551615, 18446744069683019775, 288230376151711743, 18446744070475743231, 4611686017001275199, 6908521828386340863, 2295745090394464220, 9223372036854775808, 9223372036854775809, 0, 9126739968, 287031153606524036, 0, 0, 0, 17728525486260320, 18446744073709551614, 18446744066832990207, 9223372036854775807, 18446216308128219104, 18446744073709551615, 72057589742993407, 0, 18446744073709551615, 18446744073709551615, 18014398509481983, 0, 18446744073709551615, 18446744073709551615, 274877906943, 0, 18446744073709551615, 18446744073709551615, 8191, 0, 18446744073709551615, 18446744073709551615, 68719476735, 0, 70368744177663, 0, 0, 0, 6881498031078244479, 18446744073709551579, 1125899906842623, 18446744073709027328, 4611686018427387903, 18446744073709486080, 18446744073709355007, 1152640029630136575, 6755463865565184, 18435203599664472064, 18446744073709551615, 2305843009213693951, 9799832780635308032, 18446743936404815870, 9223372036854775807, 486341884 }; }; -pub const JumpTableInline = struct { - pub inline fn isIdentifierStart(codepoint: i32) bool { - return switch (codepoint) { - 'A'...'Z', 'a'...'z', '$', '_' => true, - - else => switch (codepoint) { - 0x41...0x5a, 0x61...0x7a, 0xaa...0xaa, 0xb5...0xb5, 0xba...0xba, 0xc0...0xd6, 0xd8...0xf6, 0xf8...0x2c1, 0x2c6...0x2d1, 0x2e0...0x2e4, 0x2ec...0x2ec, 0x2ee...0x2ee, 0x370...0x374, 0x376...0x377, 0x37a...0x37d, 0x37f...0x37f, 0x386...0x386, 0x388...0x38a, 0x38c...0x38c, 0x38e...0x3a1, 0x3a3...0x3f5, 0x3f7...0x481, 0x48a...0x52f, 0x531...0x556, 0x559...0x559, 0x560...0x588, 0x5d0...0x5ea, 0x5ef...0x5f2, 0x620...0x64a, 0x66e...0x66f, 0x671...0x6d3, 0x6d5...0x6d5, 0x6e5...0x6e6, 0x6ee...0x6ef, 0x6fa...0x6fc, 0x6ff...0x6ff, 0x710...0x710, 0x712...0x72f, 0x74d...0x7a5, 0x7b1...0x7b1, 0x7ca...0x7ea, 0x7f4...0x7f5, 0x7fa...0x7fa, 0x800...0x815, 0x81a...0x81a, 0x824...0x824, 0x828...0x828, 0x840...0x858, 0x860...0x86a, 0x8a0...0x8b4, 0x8b6...0x8c7, 0x904...0x939, 0x93d...0x93d, 0x950...0x950, 0x958...0x961, 0x971...0x980, 0x985...0x98c, 0x98f...0x990, 0x993...0x9a8, 0x9aa...0x9b0, 0x9b2...0x9b2, 0x9b6...0x9b9, 0x9bd...0x9bd, 0x9ce...0x9ce, 0x9dc...0x9dd, 0x9df...0x9e1, 0x9f0...0x9f1, 0x9fc...0x9fc, 0xa05...0xa0a, 0xa0f...0xa10, 0xa13...0xa28, 0xa2a...0xa30, 0xa32...0xa33, 0xa35...0xa36, 0xa38...0xa39, 0xa59...0xa5c, 0xa5e...0xa5e, 0xa72...0xa74, 0xa85...0xa8d, 0xa8f...0xa91, 0xa93...0xaa8, 0xaaa...0xab0, 0xab2...0xab3, 0xab5...0xab9, 0xabd...0xabd, 0xad0...0xad0, 0xae0...0xae1, 0xaf9...0xaf9, 0xb05...0xb0c, 0xb0f...0xb10, 0xb13...0xb28, 0xb2a...0xb30, 0xb32...0xb33, 0xb35...0xb39, 0xb3d...0xb3d, 0xb5c...0xb5d, 0xb5f...0xb61, 0xb71...0xb71, 0xb83...0xb83, 0xb85...0xb8a, 0xb8e...0xb90, 0xb92...0xb95, 0xb99...0xb9a, 0xb9c...0xb9c, 0xb9e...0xb9f, 0xba3...0xba4, 0xba8...0xbaa, 0xbae...0xbb9, 0xbd0...0xbd0, 0xc05...0xc0c, 0xc0e...0xc10, 0xc12...0xc28, 0xc2a...0xc39, 0xc3d...0xc3d, 0xc58...0xc5a, 0xc60...0xc61, 0xc80...0xc80, 0xc85...0xc8c, 0xc8e...0xc90, 0xc92...0xca8, 0xcaa...0xcb3, 0xcb5...0xcb9, 0xcbd...0xcbd, 0xcde...0xcde, 0xce0...0xce1, 0xcf1...0xcf2, 0xd04...0xd0c, 0xd0e...0xd10, 0xd12...0xd3a, 0xd3d...0xd3d, 0xd4e...0xd4e, 0xd54...0xd56, 0xd5f...0xd61, 0xd7a...0xd7f, 0xd85...0xd96, 0xd9a...0xdb1, 0xdb3...0xdbb, 0xdbd...0xdbd, 0xdc0...0xdc6, 0xe01...0xe30, 0xe32...0xe33, 0xe40...0xe46, 0xe81...0xe82, 0xe84...0xe84, 0xe86...0xe8a, 0xe8c...0xea3, 0xea5...0xea5, 0xea7...0xeb0, 0xeb2...0xeb3, 0xebd...0xebd, 0xec0...0xec4, 0xec6...0xec6, 0xedc...0xedf, 0xf00...0xf00, 0xf40...0xf47, 0xf49...0xf6c, 0xf88...0xf8c, 0x1000...0x102a, 0x103f...0x103f, 0x1050...0x1055, 0x105a...0x105d, 0x1061...0x1061, 0x1065...0x1066, 0x106e...0x1070, 0x1075...0x1081, 0x108e...0x108e, 0x10a0...0x10c5, 0x10c7...0x10c7, 0x10cd...0x10cd, 0x10d0...0x10fa, 0x10fc...0x1248, 0x124a...0x124d, 0x1250...0x1256, 0x1258...0x1258, 0x125a...0x125d, 0x1260...0x1288, 0x128a...0x128d, 0x1290...0x12b0, 0x12b2...0x12b5, 0x12b8...0x12be, 0x12c0...0x12c0, 0x12c2...0x12c5, 0x12c8...0x12d6, 0x12d8...0x1310, 0x1312...0x1315, 0x1318...0x135a, 0x1380...0x138f, 0x13a0...0x13f5, 0x13f8...0x13fd, 0x1401...0x166c, 0x166f...0x167f, 0x1681...0x169a, 0x16a0...0x16ea, 0x16ee...0x16f8, 0x1700...0x170c, 0x170e...0x1711, 0x1720...0x1731, 0x1740...0x1751, 0x1760...0x176c, 0x176e...0x1770, 0x1780...0x17b3, 0x17d7...0x17d7, 0x17dc...0x17dc, 0x1820...0x1878, 0x1880...0x18a8, 0x18aa...0x18aa, 0x18b0...0x18f5, 0x1900...0x191e, 0x1950...0x196d, 0x1970...0x1974, 0x1980...0x19ab, 0x19b0...0x19c9, 0x1a00...0x1a16, 0x1a20...0x1a54, 0x1aa7...0x1aa7, 0x1b05...0x1b33, 0x1b45...0x1b4b, 0x1b83...0x1ba0, 0x1bae...0x1baf, 0x1bba...0x1be5, 0x1c00...0x1c23, 0x1c4d...0x1c4f, 0x1c5a...0x1c7d, 0x1c80...0x1c88, 0x1c90...0x1cba, 0x1cbd...0x1cbf, 0x1ce9...0x1cec, 0x1cee...0x1cf3, 0x1cf5...0x1cf6, 0x1cfa...0x1cfa, 0x1d00...0x1dbf, 0x1e00...0x1f15, 0x1f18...0x1f1d, 0x1f20...0x1f45, 0x1f48...0x1f4d, 0x1f50...0x1f57, 0x1f59...0x1f59, 0x1f5b...0x1f5b, 0x1f5d...0x1f5d, 0x1f5f...0x1f7d, 0x1f80...0x1fb4, 0x1fb6...0x1fbc, 0x1fbe...0x1fbe, 0x1fc2...0x1fc4, 0x1fc6...0x1fcc, 0x1fd0...0x1fd3, 0x1fd6...0x1fdb, 0x1fe0...0x1fec, 0x1ff2...0x1ff4, 0x1ff6...0x1ffc, 0x2071...0x2071, 0x207f...0x207f, 0x2090...0x209c, 0x2102...0x2102, 0x2107...0x2107, 0x210a...0x2113, 0x2115...0x2115, 0x2118...0x211d, 0x2124...0x2124, 0x2126...0x2126, 0x2128...0x2128, 0x212a...0x2139, 0x213c...0x213f, 0x2145...0x2149, 0x214e...0x214e, 0x2160...0x2188, 0x2c00...0x2c2e, 0x2c30...0x2c5e, 0x2c60...0x2ce4, 0x2ceb...0x2cee, 0x2cf2...0x2cf3, 0x2d00...0x2d25, 0x2d27...0x2d27, 0x2d2d...0x2d2d, 0x2d30...0x2d67, 0x2d6f...0x2d6f, 0x2d80...0x2d96, 0x2da0...0x2da6, 0x2da8...0x2dae, 0x2db0...0x2db6, 0x2db8...0x2dbe, 0x2dc0...0x2dc6, 0x2dc8...0x2dce, 0x2dd0...0x2dd6, 0x2dd8...0x2dde, 0x3005...0x3007, 0x3021...0x3029, 0x3031...0x3035, 0x3038...0x303c, 0x3041...0x3096, 0x309b...0x309f, 0x30a1...0x30fa, 0x30fc...0x30ff, 0x3105...0x312f, 0x3131...0x318e, 0x31a0...0x31bf, 0x31f0...0x31ff, 0x3400...0x4dbf, 0x4e00...0x9ffc, 0xa000...0xa48c, 0xa4d0...0xa4fd, 0xa500...0xa60c, 0xa610...0xa61f, 0xa62a...0xa62b, 0xa640...0xa66e, 0xa67f...0xa69d, 0xa6a0...0xa6ef, 0xa717...0xa71f, 0xa722...0xa788, 0xa78b...0xa7bf, 0xa7c2...0xa7ca, 0xa7f5...0xa801, 0xa803...0xa805, 0xa807...0xa80a, 0xa80c...0xa822, 0xa840...0xa873, 0xa882...0xa8b3, 0xa8f2...0xa8f7, 0xa8fb...0xa8fb, 0xa8fd...0xa8fe, 0xa90a...0xa925, 0xa930...0xa946, 0xa960...0xa97c, 0xa984...0xa9b2, 0xa9cf...0xa9cf, 0xa9e0...0xa9e4, 0xa9e6...0xa9ef, 0xa9fa...0xa9fe, 0xaa00...0xaa28, 0xaa40...0xaa42, 0xaa44...0xaa4b, 0xaa60...0xaa76, 0xaa7a...0xaa7a, 0xaa7e...0xaaaf, 0xaab1...0xaab1, 0xaab5...0xaab6, 0xaab9...0xaabd, 0xaac0...0xaac0, 0xaac2...0xaac2, 0xaadb...0xaadd, 0xaae0...0xaaea, 0xaaf2...0xaaf4, 0xab01...0xab06, 0xab09...0xab0e, 0xab11...0xab16, 0xab20...0xab26, 0xab28...0xab2e, 0xab30...0xab5a, 0xab5c...0xab69, 0xab70...0xabe2, 0xac00...0xd7a3, 0xd7b0...0xd7c6, 0xd7cb...0xd7fb, 0xf900...0xfa6d, 0xfa70...0xfad9, 0xfb00...0xfb06, 0xfb13...0xfb17, 0xfb1d...0xfb1d, 0xfb1f...0xfb28, 0xfb2a...0xfb36, 0xfb38...0xfb3c, 0xfb3e...0xfb3e, 0xfb40...0xfb41, 0xfb43...0xfb44, 0xfb46...0xfbb1, 0xfbd3...0xfd3d, 0xfd50...0xfd8f, 0xfd92...0xfdc7, 0xfdf0...0xfdfb, 0xfe70...0xfe74, 0xfe76...0xfefc, 0xff21...0xff3a, 0xff41...0xff5a, 0xff66...0xffbe, 0xffc2...0xffc7, 0xffca...0xffcf, 0xffd2...0xffd7, 0xffda...0xffdc, 0x10000...0x1000b, 0x1000d...0x10026, 0x10028...0x1003a, 0x1003c...0x1003d, 0x1003f...0x1004d, 0x10050...0x1005d, 0x10080...0x100fa, 0x10140...0x10174, 0x10280...0x1029c, 0x102a0...0x102d0, 0x10300...0x1031f, 0x1032d...0x1034a, 0x10350...0x10375, 0x10380...0x1039d, 0x103a0...0x103c3, 0x103c8...0x103cf, 0x103d1...0x103d5, 0x10400...0x1049d, 0x104b0...0x104d3, 0x104d8...0x104fb, 0x10500...0x10527, 0x10530...0x10563, 0x10600...0x10736, 0x10740...0x10755, 0x10760...0x10767, 0x10800...0x10805, 0x10808...0x10808, 0x1080a...0x10835, 0x10837...0x10838, 0x1083c...0x1083c, 0x1083f...0x10855, 0x10860...0x10876, 0x10880...0x1089e, 0x108e0...0x108f2, 0x108f4...0x108f5, 0x10900...0x10915, 0x10920...0x10939, 0x10980...0x109b7, 0x109be...0x109bf, 0x10a00...0x10a00, 0x10a10...0x10a13, 0x10a15...0x10a17, 0x10a19...0x10a35, 0x10a60...0x10a7c, 0x10a80...0x10a9c, 0x10ac0...0x10ac7, 0x10ac9...0x10ae4, 0x10b00...0x10b35, 0x10b40...0x10b55, 0x10b60...0x10b72, 0x10b80...0x10b91, 0x10c00...0x10c48, 0x10c80...0x10cb2, 0x10cc0...0x10cf2, 0x10d00...0x10d23, 0x10e80...0x10ea9, 0x10eb0...0x10eb1, 0x10f00...0x10f1c, 0x10f27...0x10f27, 0x10f30...0x10f45, 0x10fb0...0x10fc4, 0x10fe0...0x10ff6, 0x11003...0x11037, 0x11083...0x110af, 0x110d0...0x110e8, 0x11103...0x11126, 0x11144...0x11144, 0x11147...0x11147, 0x11150...0x11172, 0x11176...0x11176, 0x11183...0x111b2, 0x111c1...0x111c4, 0x111da...0x111da, 0x111dc...0x111dc, 0x11200...0x11211, 0x11213...0x1122b, 0x11280...0x11286, 0x11288...0x11288, 0x1128a...0x1128d, 0x1128f...0x1129d, 0x1129f...0x112a8, 0x112b0...0x112de, 0x11305...0x1130c, 0x1130f...0x11310, 0x11313...0x11328, 0x1132a...0x11330, 0x11332...0x11333, 0x11335...0x11339, 0x1133d...0x1133d, 0x11350...0x11350, 0x1135d...0x11361, 0x11400...0x11434, 0x11447...0x1144a, 0x1145f...0x11461, 0x11480...0x114af, 0x114c4...0x114c5, 0x114c7...0x114c7, 0x11580...0x115ae, 0x115d8...0x115db, 0x11600...0x1162f, 0x11644...0x11644, 0x11680...0x116aa, 0x116b8...0x116b8, 0x11700...0x1171a, 0x11800...0x1182b, 0x118a0...0x118df, 0x118ff...0x11906, 0x11909...0x11909, 0x1190c...0x11913, 0x11915...0x11916, 0x11918...0x1192f, 0x1193f...0x1193f, 0x11941...0x11941, 0x119a0...0x119a7, 0x119aa...0x119d0, 0x119e1...0x119e1, 0x119e3...0x119e3, 0x11a00...0x11a00, 0x11a0b...0x11a32, 0x11a3a...0x11a3a, 0x11a50...0x11a50, 0x11a5c...0x11a89, 0x11a9d...0x11a9d, 0x11ac0...0x11af8, 0x11c00...0x11c08, 0x11c0a...0x11c2e, 0x11c40...0x11c40, 0x11c72...0x11c8f, 0x11d00...0x11d06, 0x11d08...0x11d09, 0x11d0b...0x11d30, 0x11d46...0x11d46, 0x11d60...0x11d65, 0x11d67...0x11d68, 0x11d6a...0x11d89, 0x11d98...0x11d98, 0x11ee0...0x11ef2, 0x11fb0...0x11fb0, 0x12000...0x12399, 0x12400...0x1246e, 0x12480...0x12543, 0x13000...0x1342e, 0x14400...0x14646, 0x16800...0x16a38, 0x16a40...0x16a5e, 0x16ad0...0x16aed, 0x16b00...0x16b2f, 0x16b40...0x16b43, 0x16b63...0x16b77, 0x16b7d...0x16b8f, 0x16e40...0x16e7f, 0x16f00...0x16f4a, 0x16f50...0x16f50, 0x16f93...0x16f9f, 0x16fe0...0x16fe1, 0x16fe3...0x16fe3, 0x17000...0x187f7, 0x18800...0x18cd5, 0x18d00...0x18d08, 0x1b000...0x1b11e, 0x1b150...0x1b152, 0x1b164...0x1b167, 0x1b170...0x1b2fb, 0x1bc00...0x1bc6a, 0x1bc70...0x1bc7c, 0x1bc80...0x1bc88, 0x1bc90...0x1bc99, 0x1d400...0x1d454, 0x1d456...0x1d49c, 0x1d49e...0x1d49f, 0x1d4a2...0x1d4a2, 0x1d4a5...0x1d4a6, 0x1d4a9...0x1d4ac, 0x1d4ae...0x1d4b9, 0x1d4bb...0x1d4bb, 0x1d4bd...0x1d4c3, 0x1d4c5...0x1d505, 0x1d507...0x1d50a, 0x1d50d...0x1d514, 0x1d516...0x1d51c, 0x1d51e...0x1d539, 0x1d53b...0x1d53e, 0x1d540...0x1d544, 0x1d546...0x1d546, 0x1d54a...0x1d550, 0x1d552...0x1d6a5, 0x1d6a8...0x1d6c0, 0x1d6c2...0x1d6da, 0x1d6dc...0x1d6fa, 0x1d6fc...0x1d714, 0x1d716...0x1d734, 0x1d736...0x1d74e, 0x1d750...0x1d76e, 0x1d770...0x1d788, 0x1d78a...0x1d7a8, 0x1d7aa...0x1d7c2, 0x1d7c4...0x1d7cb, 0x1e100...0x1e12c, 0x1e137...0x1e13d, 0x1e14e...0x1e14e, 0x1e2c0...0x1e2eb, 0x1e800...0x1e8c4, 0x1e900...0x1e943, 0x1e94b...0x1e94b, 0x1ee00...0x1ee03, 0x1ee05...0x1ee1f, 0x1ee21...0x1ee22, 0x1ee24...0x1ee24, 0x1ee27...0x1ee27, 0x1ee29...0x1ee32, 0x1ee34...0x1ee37, 0x1ee39...0x1ee39, 0x1ee3b...0x1ee3b, 0x1ee42...0x1ee42, 0x1ee47...0x1ee47, 0x1ee49...0x1ee49, 0x1ee4b...0x1ee4b, 0x1ee4d...0x1ee4f, 0x1ee51...0x1ee52, 0x1ee54...0x1ee54, 0x1ee57...0x1ee57, 0x1ee59...0x1ee59, 0x1ee5b...0x1ee5b, 0x1ee5d...0x1ee5d, 0x1ee5f...0x1ee5f, 0x1ee61...0x1ee62, 0x1ee64...0x1ee64, 0x1ee67...0x1ee6a, 0x1ee6c...0x1ee72, 0x1ee74...0x1ee77, 0x1ee79...0x1ee7c, 0x1ee7e...0x1ee7e, 0x1ee80...0x1ee89, 0x1ee8b...0x1ee9b, 0x1eea1...0x1eea3, 0x1eea5...0x1eea9, 0x1eeab...0x1eebb, 0x20000...0x2a6dd, 0x2a700...0x2b734, 0x2b740...0x2b81d, 0x2b820...0x2cea1, 0x2ceb0...0x2ebe0, 0x2f800...0x2fa1d, 0x30000...0x3134a => true, - else => false, - }, - }; - } - - pub inline fn isIdentifierPart(codepoint: i32) bool { - return switch (codepoint) { - 'A'...'Z', 'a'...'z', '0'...'9', '$', '_' => true, - else => switch (codepoint) { - 0x30...0x39, 0x41...0x5a, 0x5f...0x5f, 0x61...0x7a, 0xaa...0xaa, 0xb5...0xb5, 0xb7...0xb7, 0xba...0xba, 0xc0...0xd6, 0xd8...0xf6, 0xf8...0x2c1, 0x2c6...0x2d1, 0x2e0...0x2e4, 0x2ec...0x2ec, 0x2ee...0x2ee, 0x300...0x374, 0x376...0x377, 0x37a...0x37d, 0x37f...0x37f, 0x386...0x38a, 0x38c...0x38c, 0x38e...0x3a1, 0x3a3...0x3f5, 0x3f7...0x481, 0x483...0x487, 0x48a...0x52f, 0x531...0x556, 0x559...0x559, 0x560...0x588, 0x591...0x5bd, 0x5bf...0x5bf, 0x5c1...0x5c2, 0x5c4...0x5c5, 0x5c7...0x5c7, 0x5d0...0x5ea, 0x5ef...0x5f2, 0x610...0x61a, 0x620...0x669, 0x66e...0x6d3, 0x6d5...0x6dc, 0x6df...0x6e8, 0x6ea...0x6fc, 0x6ff...0x6ff, 0x710...0x74a, 0x74d...0x7b1, 0x7c0...0x7f5, 0x7fa...0x7fa, 0x7fd...0x7fd, 0x800...0x82d, 0x840...0x85b, 0x860...0x86a, 0x8a0...0x8b4, 0x8b6...0x8c7, 0x8d3...0x8e1, 0x8e3...0x963, 0x966...0x96f, 0x971...0x983, 0x985...0x98c, 0x98f...0x990, 0x993...0x9a8, 0x9aa...0x9b0, 0x9b2...0x9b2, 0x9b6...0x9b9, 0x9bc...0x9c4, 0x9c7...0x9c8, 0x9cb...0x9ce, 0x9d7...0x9d7, 0x9dc...0x9dd, 0x9df...0x9e3, 0x9e6...0x9f1, 0x9fc...0x9fc, 0x9fe...0x9fe, 0xa01...0xa03, 0xa05...0xa0a, 0xa0f...0xa10, 0xa13...0xa28, 0xa2a...0xa30, 0xa32...0xa33, 0xa35...0xa36, 0xa38...0xa39, 0xa3c...0xa3c, 0xa3e...0xa42, 0xa47...0xa48, 0xa4b...0xa4d, 0xa51...0xa51, 0xa59...0xa5c, 0xa5e...0xa5e, 0xa66...0xa75, 0xa81...0xa83, 0xa85...0xa8d, 0xa8f...0xa91, 0xa93...0xaa8, 0xaaa...0xab0, 0xab2...0xab3, 0xab5...0xab9, 0xabc...0xac5, 0xac7...0xac9, 0xacb...0xacd, 0xad0...0xad0, 0xae0...0xae3, 0xae6...0xaef, 0xaf9...0xaff, 0xb01...0xb03, 0xb05...0xb0c, 0xb0f...0xb10, 0xb13...0xb28, 0xb2a...0xb30, 0xb32...0xb33, 0xb35...0xb39, 0xb3c...0xb44, 0xb47...0xb48, 0xb4b...0xb4d, 0xb55...0xb57, 0xb5c...0xb5d, 0xb5f...0xb63, 0xb66...0xb6f, 0xb71...0xb71, 0xb82...0xb83, 0xb85...0xb8a, 0xb8e...0xb90, 0xb92...0xb95, 0xb99...0xb9a, 0xb9c...0xb9c, 0xb9e...0xb9f, 0xba3...0xba4, 0xba8...0xbaa, 0xbae...0xbb9, 0xbbe...0xbc2, 0xbc6...0xbc8, 0xbca...0xbcd, 0xbd0...0xbd0, 0xbd7...0xbd7, 0xbe6...0xbef, 0xc00...0xc0c, 0xc0e...0xc10, 0xc12...0xc28, 0xc2a...0xc39, 0xc3d...0xc44, 0xc46...0xc48, 0xc4a...0xc4d, 0xc55...0xc56, 0xc58...0xc5a, 0xc60...0xc63, 0xc66...0xc6f, 0xc80...0xc83, 0xc85...0xc8c, 0xc8e...0xc90, 0xc92...0xca8, 0xcaa...0xcb3, 0xcb5...0xcb9, 0xcbc...0xcc4, 0xcc6...0xcc8, 0xcca...0xccd, 0xcd5...0xcd6, 0xcde...0xcde, 0xce0...0xce3, 0xce6...0xcef, 0xcf1...0xcf2, 0xd00...0xd0c, 0xd0e...0xd10, 0xd12...0xd44, 0xd46...0xd48, 0xd4a...0xd4e, 0xd54...0xd57, 0xd5f...0xd63, 0xd66...0xd6f, 0xd7a...0xd7f, 0xd81...0xd83, 0xd85...0xd96, 0xd9a...0xdb1, 0xdb3...0xdbb, 0xdbd...0xdbd, 0xdc0...0xdc6, 0xdca...0xdca, 0xdcf...0xdd4, 0xdd6...0xdd6, 0xdd8...0xddf, 0xde6...0xdef, 0xdf2...0xdf3, 0xe01...0xe3a, 0xe40...0xe4e, 0xe50...0xe59, 0xe81...0xe82, 0xe84...0xe84, 0xe86...0xe8a, 0xe8c...0xea3, 0xea5...0xea5, 0xea7...0xebd, 0xec0...0xec4, 0xec6...0xec6, 0xec8...0xecd, 0xed0...0xed9, 0xedc...0xedf, 0xf00...0xf00, 0xf18...0xf19, 0xf20...0xf29, 0xf35...0xf35, 0xf37...0xf37, 0xf39...0xf39, 0xf3e...0xf47, 0xf49...0xf6c, 0xf71...0xf84, 0xf86...0xf97, 0xf99...0xfbc, 0xfc6...0xfc6, 0x1000...0x1049, 0x1050...0x109d, 0x10a0...0x10c5, 0x10c7...0x10c7, 0x10cd...0x10cd, 0x10d0...0x10fa, 0x10fc...0x1248, 0x124a...0x124d, 0x1250...0x1256, 0x1258...0x1258, 0x125a...0x125d, 0x1260...0x1288, 0x128a...0x128d, 0x1290...0x12b0, 0x12b2...0x12b5, 0x12b8...0x12be, 0x12c0...0x12c0, 0x12c2...0x12c5, 0x12c8...0x12d6, 0x12d8...0x1310, 0x1312...0x1315, 0x1318...0x135a, 0x135d...0x135f, 0x1369...0x1371, 0x1380...0x138f, 0x13a0...0x13f5, 0x13f8...0x13fd, 0x1401...0x166c, 0x166f...0x167f, 0x1681...0x169a, 0x16a0...0x16ea, 0x16ee...0x16f8, 0x1700...0x170c, 0x170e...0x1714, 0x1720...0x1734, 0x1740...0x1753, 0x1760...0x176c, 0x176e...0x1770, 0x1772...0x1773, 0x1780...0x17d3, 0x17d7...0x17d7, 0x17dc...0x17dd, 0x17e0...0x17e9, 0x180b...0x180d, 0x1810...0x1819, 0x1820...0x1878, 0x1880...0x18aa, 0x18b0...0x18f5, 0x1900...0x191e, 0x1920...0x192b, 0x1930...0x193b, 0x1946...0x196d, 0x1970...0x1974, 0x1980...0x19ab, 0x19b0...0x19c9, 0x19d0...0x19da, 0x1a00...0x1a1b, 0x1a20...0x1a5e, 0x1a60...0x1a7c, 0x1a7f...0x1a89, 0x1a90...0x1a99, 0x1aa7...0x1aa7, 0x1ab0...0x1abd, 0x1abf...0x1ac0, 0x1b00...0x1b4b, 0x1b50...0x1b59, 0x1b6b...0x1b73, 0x1b80...0x1bf3, 0x1c00...0x1c37, 0x1c40...0x1c49, 0x1c4d...0x1c7d, 0x1c80...0x1c88, 0x1c90...0x1cba, 0x1cbd...0x1cbf, 0x1cd0...0x1cd2, 0x1cd4...0x1cfa, 0x1d00...0x1df9, 0x1dfb...0x1f15, 0x1f18...0x1f1d, 0x1f20...0x1f45, 0x1f48...0x1f4d, 0x1f50...0x1f57, 0x1f59...0x1f59, 0x1f5b...0x1f5b, 0x1f5d...0x1f5d, 0x1f5f...0x1f7d, 0x1f80...0x1fb4, 0x1fb6...0x1fbc, 0x1fbe...0x1fbe, 0x1fc2...0x1fc4, 0x1fc6...0x1fcc, 0x1fd0...0x1fd3, 0x1fd6...0x1fdb, 0x1fe0...0x1fec, 0x1ff2...0x1ff4, 0x1ff6...0x1ffc, 0x203f...0x2040, 0x2054...0x2054, 0x2071...0x2071, 0x207f...0x207f, 0x2090...0x209c, 0x20d0...0x20dc, 0x20e1...0x20e1, 0x20e5...0x20f0, 0x2102...0x2102, 0x2107...0x2107, 0x210a...0x2113, 0x2115...0x2115, 0x2118...0x211d, 0x2124...0x2124, 0x2126...0x2126, 0x2128...0x2128, 0x212a...0x2139, 0x213c...0x213f, 0x2145...0x2149, 0x214e...0x214e, 0x2160...0x2188, 0x2c00...0x2c2e, 0x2c30...0x2c5e, 0x2c60...0x2ce4, 0x2ceb...0x2cf3, 0x2d00...0x2d25, 0x2d27...0x2d27, 0x2d2d...0x2d2d, 0x2d30...0x2d67, 0x2d6f...0x2d6f, 0x2d7f...0x2d96, 0x2da0...0x2da6, 0x2da8...0x2dae, 0x2db0...0x2db6, 0x2db8...0x2dbe, 0x2dc0...0x2dc6, 0x2dc8...0x2dce, 0x2dd0...0x2dd6, 0x2dd8...0x2dde, 0x2de0...0x2dff, 0x3005...0x3007, 0x3021...0x302f, 0x3031...0x3035, 0x3038...0x303c, 0x3041...0x3096, 0x3099...0x309f, 0x30a1...0x30ff, 0x3105...0x312f, 0x3131...0x318e, 0x31a0...0x31bf, 0x31f0...0x31ff, 0x3400...0x4dbf, 0x4e00...0x9ffc, 0xa000...0xa48c, 0xa4d0...0xa4fd, 0xa500...0xa60c, 0xa610...0xa62b, 0xa640...0xa66f, 0xa674...0xa67d, 0xa67f...0xa6f1, 0xa717...0xa71f, 0xa722...0xa788, 0xa78b...0xa7bf, 0xa7c2...0xa7ca, 0xa7f5...0xa827, 0xa82c...0xa82c, 0xa840...0xa873, 0xa880...0xa8c5, 0xa8d0...0xa8d9, 0xa8e0...0xa8f7, 0xa8fb...0xa8fb, 0xa8fd...0xa92d, 0xa930...0xa953, 0xa960...0xa97c, 0xa980...0xa9c0, 0xa9cf...0xa9d9, 0xa9e0...0xa9fe, 0xaa00...0xaa36, 0xaa40...0xaa4d, 0xaa50...0xaa59, 0xaa60...0xaa76, 0xaa7a...0xaac2, 0xaadb...0xaadd, 0xaae0...0xaaef, 0xaaf2...0xaaf6, 0xab01...0xab06, 0xab09...0xab0e, 0xab11...0xab16, 0xab20...0xab26, 0xab28...0xab2e, 0xab30...0xab5a, 0xab5c...0xab69, 0xab70...0xabea, 0xabec...0xabed, 0xabf0...0xabf9, 0xac00...0xd7a3, 0xd7b0...0xd7c6, 0xd7cb...0xd7fb, 0xf900...0xfa6d, 0xfa70...0xfad9, 0xfb00...0xfb06, 0xfb13...0xfb17, 0xfb1d...0xfb28, 0xfb2a...0xfb36, 0xfb38...0xfb3c, 0xfb3e...0xfb3e, 0xfb40...0xfb41, 0xfb43...0xfb44, 0xfb46...0xfbb1, 0xfbd3...0xfd3d, 0xfd50...0xfd8f, 0xfd92...0xfdc7, 0xfdf0...0xfdfb, 0xfe00...0xfe0f, 0xfe20...0xfe2f, 0xfe33...0xfe34, 0xfe4d...0xfe4f, 0xfe70...0xfe74, 0xfe76...0xfefc, 0xff10...0xff19, 0xff21...0xff3a, 0xff3f...0xff3f, 0xff41...0xff5a, 0xff65...0xffbe, 0xffc2...0xffc7, 0xffca...0xffcf, 0xffd2...0xffd7, 0xffda...0xffdc, 0x10000...0x1000b, 0x1000d...0x10026, 0x10028...0x1003a, 0x1003c...0x1003d, 0x1003f...0x1004d, 0x10050...0x1005d, 0x10080...0x100fa, 0x10140...0x10174, 0x101fd...0x101fd, 0x10280...0x1029c, 0x102a0...0x102d0, 0x102e0...0x102e0, 0x10300...0x1031f, 0x1032d...0x1034a, 0x10350...0x1037a, 0x10380...0x1039d, 0x103a0...0x103c3, 0x103c8...0x103cf, 0x103d1...0x103d5, 0x10400...0x1049d, 0x104a0...0x104a9, 0x104b0...0x104d3, 0x104d8...0x104fb, 0x10500...0x10527, 0x10530...0x10563, 0x10600...0x10736, 0x10740...0x10755, 0x10760...0x10767, 0x10800...0x10805, 0x10808...0x10808, 0x1080a...0x10835, 0x10837...0x10838, 0x1083c...0x1083c, 0x1083f...0x10855, 0x10860...0x10876, 0x10880...0x1089e, 0x108e0...0x108f2, 0x108f4...0x108f5, 0x10900...0x10915, 0x10920...0x10939, 0x10980...0x109b7, 0x109be...0x109bf, 0x10a00...0x10a03, 0x10a05...0x10a06, 0x10a0c...0x10a13, 0x10a15...0x10a17, 0x10a19...0x10a35, 0x10a38...0x10a3a, 0x10a3f...0x10a3f, 0x10a60...0x10a7c, 0x10a80...0x10a9c, 0x10ac0...0x10ac7, 0x10ac9...0x10ae6, 0x10b00...0x10b35, 0x10b40...0x10b55, 0x10b60...0x10b72, 0x10b80...0x10b91, 0x10c00...0x10c48, 0x10c80...0x10cb2, 0x10cc0...0x10cf2, 0x10d00...0x10d27, 0x10d30...0x10d39, 0x10e80...0x10ea9, 0x10eab...0x10eac, 0x10eb0...0x10eb1, 0x10f00...0x10f1c, 0x10f27...0x10f27, 0x10f30...0x10f50, 0x10fb0...0x10fc4, 0x10fe0...0x10ff6, 0x11000...0x11046, 0x11066...0x1106f, 0x1107f...0x110ba, 0x110d0...0x110e8, 0x110f0...0x110f9, 0x11100...0x11134, 0x11136...0x1113f, 0x11144...0x11147, 0x11150...0x11173, 0x11176...0x11176, 0x11180...0x111c4, 0x111c9...0x111cc, 0x111ce...0x111da, 0x111dc...0x111dc, 0x11200...0x11211, 0x11213...0x11237, 0x1123e...0x1123e, 0x11280...0x11286, 0x11288...0x11288, 0x1128a...0x1128d, 0x1128f...0x1129d, 0x1129f...0x112a8, 0x112b0...0x112ea, 0x112f0...0x112f9, 0x11300...0x11303, 0x11305...0x1130c, 0x1130f...0x11310, 0x11313...0x11328, 0x1132a...0x11330, 0x11332...0x11333, 0x11335...0x11339, 0x1133b...0x11344, 0x11347...0x11348, 0x1134b...0x1134d, 0x11350...0x11350, 0x11357...0x11357, 0x1135d...0x11363, 0x11366...0x1136c, 0x11370...0x11374, 0x11400...0x1144a, 0x11450...0x11459, 0x1145e...0x11461, 0x11480...0x114c5, 0x114c7...0x114c7, 0x114d0...0x114d9, 0x11580...0x115b5, 0x115b8...0x115c0, 0x115d8...0x115dd, 0x11600...0x11640, 0x11644...0x11644, 0x11650...0x11659, 0x11680...0x116b8, 0x116c0...0x116c9, 0x11700...0x1171a, 0x1171d...0x1172b, 0x11730...0x11739, 0x11800...0x1183a, 0x118a0...0x118e9, 0x118ff...0x11906, 0x11909...0x11909, 0x1190c...0x11913, 0x11915...0x11916, 0x11918...0x11935, 0x11937...0x11938, 0x1193b...0x11943, 0x11950...0x11959, 0x119a0...0x119a7, 0x119aa...0x119d7, 0x119da...0x119e1, 0x119e3...0x119e4, 0x11a00...0x11a3e, 0x11a47...0x11a47, 0x11a50...0x11a99, 0x11a9d...0x11a9d, 0x11ac0...0x11af8, 0x11c00...0x11c08, 0x11c0a...0x11c36, 0x11c38...0x11c40, 0x11c50...0x11c59, 0x11c72...0x11c8f, 0x11c92...0x11ca7, 0x11ca9...0x11cb6, 0x11d00...0x11d06, 0x11d08...0x11d09, 0x11d0b...0x11d36, 0x11d3a...0x11d3a, 0x11d3c...0x11d3d, 0x11d3f...0x11d47, 0x11d50...0x11d59, 0x11d60...0x11d65, 0x11d67...0x11d68, 0x11d6a...0x11d8e, 0x11d90...0x11d91, 0x11d93...0x11d98, 0x11da0...0x11da9, 0x11ee0...0x11ef6, 0x11fb0...0x11fb0, 0x12000...0x12399, 0x12400...0x1246e, 0x12480...0x12543, 0x13000...0x1342e, 0x14400...0x14646, 0x16800...0x16a38, 0x16a40...0x16a5e, 0x16a60...0x16a69, 0x16ad0...0x16aed, 0x16af0...0x16af4, 0x16b00...0x16b36, 0x16b40...0x16b43, 0x16b50...0x16b59, 0x16b63...0x16b77, 0x16b7d...0x16b8f, 0x16e40...0x16e7f, 0x16f00...0x16f4a, 0x16f4f...0x16f87, 0x16f8f...0x16f9f, 0x16fe0...0x16fe1, 0x16fe3...0x16fe4, 0x16ff0...0x16ff1, 0x17000...0x187f7, 0x18800...0x18cd5, 0x18d00...0x18d08, 0x1b000...0x1b11e, 0x1b150...0x1b152, 0x1b164...0x1b167, 0x1b170...0x1b2fb, 0x1bc00...0x1bc6a, 0x1bc70...0x1bc7c, 0x1bc80...0x1bc88, 0x1bc90...0x1bc99, 0x1bc9d...0x1bc9e, 0x1d165...0x1d169, 0x1d16d...0x1d172, 0x1d17b...0x1d182, 0x1d185...0x1d18b, 0x1d1aa...0x1d1ad, 0x1d242...0x1d244, 0x1d400...0x1d454, 0x1d456...0x1d49c, 0x1d49e...0x1d49f, 0x1d4a2...0x1d4a2, 0x1d4a5...0x1d4a6, 0x1d4a9...0x1d4ac, 0x1d4ae...0x1d4b9, 0x1d4bb...0x1d4bb, 0x1d4bd...0x1d4c3, 0x1d4c5...0x1d505, 0x1d507...0x1d50a, 0x1d50d...0x1d514, 0x1d516...0x1d51c, 0x1d51e...0x1d539, 0x1d53b...0x1d53e, 0x1d540...0x1d544, 0x1d546...0x1d546, 0x1d54a...0x1d550, 0x1d552...0x1d6a5, 0x1d6a8...0x1d6c0, 0x1d6c2...0x1d6da, 0x1d6dc...0x1d6fa, 0x1d6fc...0x1d714, 0x1d716...0x1d734, 0x1d736...0x1d74e, 0x1d750...0x1d76e, 0x1d770...0x1d788, 0x1d78a...0x1d7a8, 0x1d7aa...0x1d7c2, 0x1d7c4...0x1d7cb, 0x1d7ce...0x1d7ff, 0x1da00...0x1da36, 0x1da3b...0x1da6c, 0x1da75...0x1da75, 0x1da84...0x1da84, 0x1da9b...0x1da9f, 0x1daa1...0x1daaf, 0x1e000...0x1e006, 0x1e008...0x1e018, 0x1e01b...0x1e021, 0x1e023...0x1e024, 0x1e026...0x1e02a, 0x1e100...0x1e12c, 0x1e130...0x1e13d, 0x1e140...0x1e149, 0x1e14e...0x1e14e, 0x1e2c0...0x1e2f9, 0x1e800...0x1e8c4, 0x1e8d0...0x1e8d6, 0x1e900...0x1e94b, 0x1e950...0x1e959, 0x1ee00...0x1ee03, 0x1ee05...0x1ee1f, 0x1ee21...0x1ee22, 0x1ee24...0x1ee24, 0x1ee27...0x1ee27, 0x1ee29...0x1ee32, 0x1ee34...0x1ee37, 0x1ee39...0x1ee39, 0x1ee3b...0x1ee3b, 0x1ee42...0x1ee42, 0x1ee47...0x1ee47, 0x1ee49...0x1ee49, 0x1ee4b...0x1ee4b, 0x1ee4d...0x1ee4f, 0x1ee51...0x1ee52, 0x1ee54...0x1ee54, 0x1ee57...0x1ee57, 0x1ee59...0x1ee59, 0x1ee5b...0x1ee5b, 0x1ee5d...0x1ee5d, 0x1ee5f...0x1ee5f, 0x1ee61...0x1ee62, 0x1ee64...0x1ee64, 0x1ee67...0x1ee6a, 0x1ee6c...0x1ee72, 0x1ee74...0x1ee77, 0x1ee79...0x1ee7c, 0x1ee7e...0x1ee7e, 0x1ee80...0x1ee89, 0x1ee8b...0x1ee9b, 0x1eea1...0x1eea3, 0x1eea5...0x1eea9, 0x1eeab...0x1eebb, 0x1fbf0...0x1fbf9, 0x20000...0x2a6dd, 0x2a700...0x2b734, 0x2b740...0x2b81d, 0x2b820...0x2cea1, 0x2ceb0...0x2ebe0, 0x2f800...0x2fa1d, 0x30000...0x3134a, 0xe0100...0xe01ef => true, - else => false, - }, - }; - } +/// isIDStartESNext checks if a codepoint is valid in the isIDStartESNext category +pub fn isIDStartESNext(cp: u21) bool { + const high = cp >> 8; + const low = cp & 0xFF; + const stage2_idx = idStartESNext.stage1[high]; + const bit_pos = stage2_idx + low; + const u64_idx = bit_pos >> 6; + const bit_idx = @as(u6, @intCast(bit_pos & 63)); + return (idStartESNext.stage2[u64_idx] & (@as(u64, 1) << bit_idx)) != 0; +} +const idStartESNext = struct { + pub const stage1 = [_]u16{ 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 256, 4352, 4608, 4864, 256, 5120, 5376, 5632, 5888, 6144, 6400, 6656, 6912, 256, 7168, 7424, 7680, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 8192, 8448, 7936, 7936, 8704, 8960, 7936, 7936, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 6912, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 9216, 256, 9472, 9728, 9984, 10240, 10496, 10752, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 11008, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 256, 11264, 11520, 256, 11776, 12032, 12288, 12544, 12800, 13056, 13312, 13568, 13824, 256, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 15872, 16128, 16384, 16640, 16896, 17152, 17408, 17664, 17920, 18176, 18432, 18688, 18944, 7936, 19200, 19456, 19712, 19968, 256, 256, 256, 20224, 20480, 20736, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 20992, 256, 256, 256, 256, 21248, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 256, 256, 21504, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 256, 256, 21760, 22016, 7936, 7936, 22272, 22528, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 22784, 256, 256, 256, 256, 23040, 23296, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 23552, 256, 23808, 24064, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 24320, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 24576, 24832, 25088, 25344, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 25600, 25856, 26112, 26368, 7936, 26624, 7936, 7936, 26880, 27136, 27392, 7936, 7936, 7936, 7936, 27648, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 27904, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 28160, 28416, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 28672, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 28928, 256, 256, 29184, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 256, 256, 29440, 7936, 7936, 7936, 7936, 7936, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 29696, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 29952, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936, 7936 }; + pub const stage2 = [_]u64{ 0, 576460743847706622, 297241973452963840, 18410715276682199039, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 88094074470339, 0, 13609596598936928256, 18446744056529672000, 18428729675200069631, 18446744073709551615, 18446744073709551615, 18446744073709550595, 18446744073709551615, 18446462598732840959, 18446744069456527359, 511, 2119858418286592, 18446744069414584320, 18446392229988665343, 18446744073709551615, 11241196188469297151, 281474976514048, 18446744073709543424, 563224831328255, 301749971126844416, 1168302407679, 18446471390564450303, 18446744069414616831, 1023, 2594073385365405680, 18446181140919287808, 2577745637692514273, 1153765945374687232, 247132830528276448, 7881300924956672, 2589004636761079776, 144115200960823296, 2589004636760940512, 562965791113216, 288167810662516712, 65536, 2594071186342010848, 13539213312, 2589567586714640353, 1688864355778560, 2882303761516978160, 18158513712597581824, 3457638613854978016, 127, 3940649673949182, 127, 2309783315290257366, 4026531935, 1, 35184372088575, 7936, 0, 9223380832947798015, 18438229877581611008, 18446744069414600707, 17870283321406070975, 18446744073709551615, 18446744070446333439, 9168765891372858879, 18446744073701162813, 18446744073696837631, 134217727, 18446744069414649855, 4557642822898941951, 18446744073709551614, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446638520593285119, 18446744069548802046, 144053615424700415, 1125897759621119, 527761286627327, 4503599627370495, 276824064, 18446744069414584320, 144115188075855871, 18446469195802607615, 18014398509481983, 2147483647, 8796093022142464, 18446480190918885375, 1023, 18446744069422972927, 2097151, 549755813888, 0, 4503599627370464, 8160, 18158724812380307448, 274877906943, 68719476735, 4611686018360336384, 16717361816799216127, 319718190147960832, 18446744073709551615, 18446744073709551615, 18446744073709551615, 0, 18446744070475743231, 4611686017001275199, 6908521828386340863, 2295745090394464220, 0, 9223934986808197120, 536805376, 0, 17582049991377026180, 18446744069414601696, 511, 0, 0, 0, 0, 0, 18446744073709551615, 18446744073709551615, 18446744073709551615, 3509778554814463, 18446498607738650623, 141836999983103, 9187201948305063935, 2139062143, 2251241253188403424, 18446744073709551614, 18446744069288755199, 17870283321406128127, 18446462598732840928, 18446744073709551615, 18446744069414617087, 18446462598732840960, 18446744073709551615, 18446744073709551615, 8191, 4611686018427322368, 13198434443263, 9223512774343131135, 18446744070488326143, 281474976710655, 18446744060816261120, 18446744073709551615, 18446744073709550079, 18445618173868443647, 34359736251, 4503599627370495, 4503599627370492, 7564921474075590656, 18446462873610746880, 2305843004918726783, 2251799813685232, 8935422993945886720, 2199023255551, 14159317224157876215, 4495436853045886975, 7890092085477381, 18446602782178705022, 18446466996645134335, 18446744073709551615, 34359738367, 18446744073709551615, 18446744073709551615, 18446462667452317695, 1152921504606845055, 18446744073709551615, 18446532967477018623, 18446744073709551615, 67108863, 6881498030004502655, 18446744073709551579, 1125899906842623, 18446744073709027328, 4611686018427387903, 18446744073709486080, 18446744073709355007, 1152640029630136575, 0, 18437455399478099968, 18446744073709551615, 2305843009213693951, 576460743713488896, 18446743798965862398, 9223372036854775807, 486341884, 13258596753222922239, 1073692671, 18446744073709551615, 576460752303423487, 0, 9007199254740991, 0, 0, 0, 0, 18446744069951455231, 131071, 18446708893632430079, 18014398509418495, 18446744070488326143, 4128527, 18446744073709551615, 18446744073709551615, 18446462599806582783, 1152921504591118335, 18446463698244468735, 17870001915148894207, 2016486715970549759, 0, 36028797018963967, 1095220854783, 575897802350002111, 0, 10502394331027995967, 36028792728190975, 2147483647, 15762594400829440, 288230371860938751, 0, 13907115649320091647, 0, 18014398491590657, 2305843004918726656, 536870911, 137438953215, 18014398509481983, 2251795522912255, 262143, 0, 18446744073709551615, 511, 2251799813685247, 2251799813685247, 68719476735, 0, 0, 0, 0, 0, 848822976643071, 0, 18446463149025525759, 18446462598732841023, 18446462598732840963, 36028792723996703, 72057594037927928, 10696049115004928, 281474976710648, 2199023190016, 549755813880, 20266198323101840, 2251799813685240, 335544350, 9223389629040558079, 1, 18446464796682337663, 2147483647, 2589004636760940512, 16643063808, 0, 0, 9007199254740991, 15032387456, 281474976710655, 176, 0, 0, 140737488355327, 251658240, 281474976710655, 16, 72066390130950143, 0, 134217727, 127, 0, 0, 17592186044415, 0, 18446744069414584320, 9223372041149743103, 9223653511822045823, 2, 18446740770879700992, 42949804031, 290482175965394945, 18446744073441181696, 18446462599269712895, 144115188075855871, 140737488354815, 18445618173802708993, 65535, 0, 562949953420159, 18446741595513421888, 16778239, 0, 0, 0, 0, 2251795518717952, 4503599627239412, 0, 281474976710656, 0, 18446744073709551615, 18446744073709551615, 67108863, 0, 18446744073709551615, 140737488355327, 18446744073709551615, 18446744073709551615, 18446744073709551615, 15, 0, 0, 0, 0, 18446744073709486080, 562949953421311, 281474976710655, 126, 0, 0, 18446744073709551615, 127, 0, 0, 144115188075855871, 18446462600880324607, 9223372036854775807, 70368744112128, 281474976710655, 16212958624174047247, 65535, 0, 0, 18446744073709551615, 0, 0, 18446744073709551615, 67583, 4294443008, 47244640256, 18446744073709551615, 18446744073709551615, 18446744073709551615, 72057594037927935, 18446744073709551615, 18446744073709551615, 18446744073709551615, 4194303, 511, 0, 0, 0, 0, 0, 0, 8065665457643847680, 1125934266580991, 18446463629527547904, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 1152921504606846975, 18446744073709551615, 2305570330330005503, 67043839, 0, 18446744073709551615, 18446744073707454463, 17005555242810474495, 18446744073709551599, 8935141660164089791, 18446744073709419615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446743249075830783, 17870283321271910397, 18437736874452713471, 18446603336221163519, 18446741874686295551, 4087, 8660801552383, 0, 0, 0, 18446462598732840960, 70368744177663, 0, 0, 4575692405780512767, 16384, 0, 0, 0, 0, 70368744112128, 17592186044415, 0, 0, 0, 17592185978880, 0, 0, 0, 9223213153129594880, 18446744073709551615, 18446744073709551615, 18446744073709551615, 31, 18446744073709551615, 2063, 0, 0, 790380184120328175, 6843210385291930244, 1152917029519358975, 0, 18446744073709551615, 18446744073709551615, 18446744073709551615, 4294967295, 288230376151711743, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744070488326143, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446462615912710143, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446462607322775551, 18446744073709551615, 1073741823, 0, 0, 1073741823, 0, 0, 0, 18446744073709551615, 18446744073709488127, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 281474976710655, 0 }; }; -// // ----- The benchmark ------ - -// const std = @import("std"); - -// const part_codepoints_slice: []const i32 = &start_codepoints; -// const start_codepoints_slice: []const i32 = &part_codepoints; - -// pub const HashTable = struct { -// var starts: std.AutoHashMap(i32, void) = undefined; -// var parts: std.AutoHashMap(i32, void) = undefined; - -// pub fn isIdentifierStart(codepoint: i32) bool { -// if (codepoint > 255) return starts.contains(codepoint); -// return switch (codepoint) { -// 'A'...'Z', 'a'...'z', '$', '_' => true, -// else => false, -// }; -// } - -// pub fn isIdentifierPart(codepoint: i32) bool { -// if (codepoint > 255) return parts.contains(codepoint); -// return switch (codepoint) { -// 'A'...'Z', 'a'...'z', '0'...'9', '$', '_' => true, -// else => false, -// }; -// } - -// pub fn init(allocator: std.mem.Allocator) !void { -// starts = std.AutoHashMap(i32, void).init(allocator); -// parts = std.AutoHashMap(i32, void).init(allocator); - -// var i: i32 = 0; -// var j: i32 = 0; - -// while (i < start_codepoints.len) : (i += 2) { -// j = start_codepoints[i]; -// while (j <= start_codepoints[i + 1]) : (j += 1) { -// try starts.put(j, {}); -// } -// } -// i = 0; -// while (i < part_codepoints.len) : (i += 2) { -// j = part_codepoints[i]; -// while (j <= part_codepoints[i + 1]) : (j += 1) { -// try parts.put(j, {}); -// } -// } -// } -// }; - -// pub const BinarySearch = struct { - -// // "lookupInUnicodeMap" in TypeScript -// // esbuild does something similar -// fn search(comptime map: []const i32, code: i32) bool { -// // Bail out quickly if it couldn't possibly be in the map. -// if (code < map[0]) { -// return false; -// } - -// // Perform binary search in one of the Unicode range maps -// var lo: i32 = 0; -// var hi: i32 = map.len; -// var mid: i32 = undefined; - -// while (lo + 1 < hi) { -// mid = lo + (hi - lo) / 2; -// // mid has to be even to catch a range's beginning -// mid -= mid % 2; -// if (map[mid] <= code and code <= map[mid + 1]) { -// return true; -// } -// if (code < map[mid]) { -// hi = mid; -// } else { -// lo = mid + 2; -// } -// } - -// return false; -// } - -// // https://source.chromium.org/chromium/v8/v8.git/+/master:src/strings/char-predicates-inl.h;l=133 -// pub fn isIdentifierStart(codepoint: i32) bool { -// if (codepoint > 255) return search(start_codepoints_slice, codepoint); -// return switch (codepoint) { -// 'A'...'Z', 'a'...'z', '$', '_' => true, -// else => false, -// }; -// } - -// pub fn isIdentifierPart(codepoint: i32) bool { -// if (codepoint > 255) return search(part_codepoints_slice, codepoint); -// return switch (codepoint) { -// 'A'...'Z', 'a'...'z', '0'...'9', '$', '_' => true, -// else => false, -// }; -// } -// }; - -// const unicode_text: []const u8 = -// \\ -// \\_a["" + "constructor"] = 133 /* ConstructorKeyword */, -// \\_a.debugger = 87 /* DebuggerKeyword */, -// \\_a.declare = 134 /* DeclareKeyword */, -// \\_a.default = 88 /* DefaultKeyword */, -// \\_a.delete = 89 /* DeleteKeyword */, -// \\_a.do = 90 /* DoKeyword */, -// \\_a.else = 91 /* ElseKeyword */, -// \\_a.enum = 92 /* EnumKeyword */, -// \\_a.export = 93 /* ExportKeyword */, -// \\_a.extends = 94 /* ExtendsKeyword */, -// \\_a.false = 95 /* FalseKeyword */, -// \\_a.finally = 96 /* FinallyKeyword */, -// \\_a.for = 97 /* ForKeyword */, -// \\_a.from = 154 /* FromKeyword */, -// \\_a.function = 98 /* FunctionKeyword */, -// \\_a.get = 135 /* GetKeyword */, -// \\_a.if = 99 /* IfKeyword */, -// \\_a.implements = 117 /* ImplementsKeyword */, -// \\_a.import = 100 /* ImportKeyword */, -// \\_a.in = 101 /* InKeyword */, -// \\_a.infer = 136 /* InferKeyword */, -// \\_a.instanceof = 102 /* InstanceOfKeyword */, -// \\_a.interface = 118 /* InterfaceKeyword */, -// \\_a.intrinsic = 137 /* IntrinsicKeyword */, -// \\_a.is = 138 /* IsKeyword */, -// \\_a.keyof = 139 /* KeyOfKeyword */, -// \\_a.let = 119 /* LetKeyword */, -// \\_a.module = 140 /* ModuleKeyword */, -// \\_a.namespace = 141 /* NamespaceKeyword */, -// \\_a.never = 142 /* NeverKeyword */, -// \\_a.new = 103 /* NewKeyword */, -// \\_a.null = 104 /* NullKeyword */, -// \\_a.number = 145 /* NumberKeyword */, -// \\_a.object = 146 /* ObjectKeyword */, -// \\_a.package = 120 /* PackageKeyword */, -// \\_a.private = 121 /* PrivateKeyword */, -// \\_a.protected = 122 /* ProtectedKeyword */, -// \\_a.public = 123 /* PublicKeyword */, -// \\_a.override = 157 /* OverrideKeyword */, -// \\_a.readonly = 143 /* ReadonlyKeyword */, -// \\_a.require = 144 /* RequireKeyword */, -// \\_a.global = 155 /* GlobalKeyword */, -// \\_a.return = 105 /* ReturnKeyword */, -// \\_a.set = 147 /* SetKeyword */, -// \\_a.static = 124 /* StaticKeyword */, -// \\_a.string = 148 /* StringKeyword */, -// \\_a.super = 106 /* SuperKeyword */, -// \\_a.switch = 107 /* SwitchKeyword */, -// \\_a.symbol = 149 /* SymbolKeyword */, -// \\_a.this = 108 /* ThisKeyword */, -// \\_a.throw = 109 /* ThrowKeyword */, -// \\_a.true = 110 /* TrueKeyword */, -// \\_a.try = 111 /* TryKeyword */, -// \\_a.type = 150 /* TypeKeyword */, -// \\_a.typeof = 112 /* TypeOfKeyword */, -// \\_a.undefined = 151 /* UndefinedKeyword */, -// \\_a.unique = 152 /* UniqueKeyword */, -// \\_a.unknown = 153 /* UnknownKeyword */, -// \\_a.var = 113 /* VarKeyword */, -// \\_a.void = 114 /* VoidKeyword */, -// \\_a.while = 115 /* WhileKeyword */, -// \\_a.with = 116 /* WithKeyword */, -// \\_a.yield = 125 /* YieldKeyword */, -// \\_a.async = 130 /* AsyncKeyword */, -// \\_a.await = 131 /* AwaitKeyword */, -// \\_a.of = 158 /* OfKeyword */, -// \\_a); -// \\var textToKeyword = new ts.Map(ts.getEntries(ts.textToKeywordObj)); -// \\var textToToken = new ts.Map(ts.getEntries(__assign(__assign({}, ts.textToKeywordObj), { "{": 18 /* OpenBraceToken */, "}": 19 /* CloseBraceToken */, "(": 20 /* OpenParenToken */, ")": 21 /* CloseParenToken */, "[": 22 /* OpenBracketToken */, "]": 23 /* CloseBracketToken */, ".": 24 /* DotToken */, "...": 25 /* DotDotDotToken */, ";": 26 /* SemicolonToken */, ",": 27 /* CommaToken */, "<": 29 /* LessThanToken */, ">": 31 /* GreaterThanToken */, "<=": 32 /* LessThanEqualsToken */, ">=": 33 /* GreaterThanEqualsToken */, "==": 34 /* EqualsEqualsToken */, "!=": 35 /* ExclamationEqualsToken */, "===": 36 /* EqualsEqualsEqualsToken */, "!==": 37 /* ExclamationEqualsEqualsToken */, "=>": 38 /* EqualsGreaterThanToken */, "+": 39 /* PlusToken */, "-": 40 /* MinusToken */, "**": 42 /* AsteriskAsteriskToken */, "*": 41 /* AsteriskToken */, "/": 43 /* SlashToken */, "%": 44 /* PercentToken */, "++": 45 /* PlusPlusToken */, "--": 46 /* MinusMinusToken */, "<<": 47 /* LessThanLessThanToken */, ">": 48 /* GreaterThanGreaterThanToken */, ">>>": 49 /* GreaterThanGreaterThanGreaterThanToken */, "&": 50 /* AmpersandToken */, "|": 51 /* BarToken */, "^": 52 /* CaretToken */, "!": 53 /* ExclamationToken */, "~": 54 /* TildeToken */, "&&": 55 /* AmpersandAmpersandToken */, "||": 56 /* BarBarToken */, "?": 57 /* QuestionToken */, "??": 60 /* QuestionQuestionToken */, "?.": 28 /* QuestionDotToken */, ":": 58 /* ColonToken */, "=": 63 /* EqualsToken */, "+=": 64 /* PlusEqualsToken */, "-=": 65 /* MinusEqualsToken */, "*=": 66 /* AsteriskEqualsToken */, "**=": 67 /* AsteriskAsteriskEqualsToken */, "/=": 68 /* SlashEqualsToken */, "%=": 69 /* PercentEqualsToken */, "<<=": 70 /* LessThanLessThanEqualsToken */, ">>=": 71 /* GreaterThanGreaterThanEqualsToken */, ">>>=": 72 /* GreaterThanGreaterThanGreaterThanEqualsToken */, "&=": 73 /* AmpersandEqualsToken */, "|=": 74 /* BarEqualsToken */, "^=": 78 /* CaretEqualsToken */, "||=": 75 /* BarBarEqualsToken */, "&&=": 76 /* AmpersandAmpersandEqualsToken */, "??=": 77 /* QuestionQuestionEqualsToken */, "@": 59 /* AtToken */, "#": 62 /* HashToken */, "`": 61 /* BacktickToken */ }))); -// \\/* -// \\As per ECMAScript Language Specification 3th Edition, Section 7.6: Identifiers -// \\IdentifierStart :: -// \\Can contain Unicode 3.0.0 categories: -// \\Uppercase letter (Lu), -// \\Lowercase letter (Ll), -// \\Titlecase letter (Lt), -// \\Modifier letter (Lm), -// \\Other letter (Lo), or -// \\Letter number (Nl). -// \\IdentifierPart :: = -// \\Can contain IdentifierStart + Unicode 3.0.0 categories: -// \\Non-spacing mark (Mn), -// \\Combining spacing mark (Mc), -// \\Decimal number (Nd), or -// \\Connector punctuation (Pc). -// \\ -// \\Codepoint ranges for ES3 Identifiers are extracted from the Unicode 3.0.0 specification at: -// \\http://www.unicode.org/Public/3.0-Update/UnicodeData-3.0.0.txt -// \\*/ -// \\var unicodeES3IdentifierStart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 543, 546, 563, 592, 685, 688, 696, 699, 705, 720, 721, 736, 740, 750, 750, 890, 890, 902, 902, 904, 906, 908, 908, 910, 929, 931, 974, 976, 983, 986, 1011, 1024, 1153, 1164, 1220, 1223, 1224, 1227, 1228, 1232, 1269, 1272, 1273, 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1569, 1594, 1600, 1610, 1649, 1747, 1749, 1749, 1765, 1766, 1786, 1788, 1808, 1808, 1810, 1836, 1920, 1957, 2309, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2524, 2525, 2527, 2529, 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2699, 2701, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2784, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2870, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 2997, 2999, 3001, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3294, 3294, 3296, 3297, 3333, 3340, 3342, 3344, 3346, 3368, 3370, 3385, 3424, 3425, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, 3840, 3840, 3904, 3911, 3913, 3946, 3976, 3979, 4096, 4129, 4131, 4135, 4137, 4138, 4176, 4181, 4256, 4293, 4304, 4342, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4614, 4616, 4678, 4680, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4742, 4744, 4744, 4746, 4749, 4752, 4782, 4784, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4814, 4816, 4822, 4824, 4846, 4848, 4878, 4880, 4880, 4882, 4885, 4888, 4894, 4896, 4934, 4936, 4954, 5024, 5108, 5121, 5740, 5743, 5750, 5761, 5786, 5792, 5866, 6016, 6067, 6176, 6263, 6272, 6312, 7680, 7835, 7840, 7929, 7936, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8319, 8319, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8497, 8499, 8505, 8544, 8579, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12346, 12353, 12436, 12445, 12446, 12449, 12538, 12540, 12542, 12549, 12588, 12593, 12686, 12704, 12727, 13312, 19893, 19968, 40869, 40960, 42124, 44032, 55203, 63744, 64045, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65138, 65140, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; -// \\var unicodeES3IdentifierPart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 543, 546, 563, 592, 685, 688, 696, 699, 705, 720, 721, 736, 740, 750, 750, 768, 846, 864, 866, 890, 890, 902, 902, 904, 906, 908, 908, 910, 929, 931, 974, 976, 983, 986, 1011, 1024, 1153, 1155, 1158, 1164, 1220, 1223, 1224, 1227, 1228, 1232, 1269, 1272, 1273, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1441, 1443, 1465, 1467, 1469, 1471, 1471, 1473, 1474, 1476, 1476, 1488, 1514, 1520, 1522, 1569, 1594, 1600, 1621, 1632, 1641, 1648, 1747, 1749, 1756, 1759, 1768, 1770, 1773, 1776, 1788, 1808, 1836, 1840, 1866, 1920, 1968, 2305, 2307, 2309, 2361, 2364, 2381, 2384, 2388, 2392, 2403, 2406, 2415, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2492, 2494, 2500, 2503, 2504, 2507, 2509, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2562, 2562, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2649, 2652, 2654, 2654, 2662, 2676, 2689, 2691, 2693, 2699, 2701, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2784, 2790, 2799, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2870, 2873, 2876, 2883, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2913, 2918, 2927, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 2997, 2999, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3031, 3031, 3047, 3055, 3073, 3075, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3134, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3168, 3169, 3174, 3183, 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3262, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3297, 3302, 3311, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3368, 3370, 3385, 3390, 3395, 3398, 3400, 3402, 3405, 3415, 3415, 3424, 3425, 3430, 3439, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3805, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3946, 3953, 3972, 3974, 3979, 3984, 3991, 3993, 4028, 4038, 4038, 4096, 4129, 4131, 4135, 4137, 4138, 4140, 4146, 4150, 4153, 4160, 4169, 4176, 4185, 4256, 4293, 4304, 4342, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4614, 4616, 4678, 4680, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4742, 4744, 4744, 4746, 4749, 4752, 4782, 4784, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4814, 4816, 4822, 4824, 4846, 4848, 4878, 4880, 4880, 4882, 4885, 4888, 4894, 4896, 4934, 4936, 4954, 4969, 4977, 5024, 5108, 5121, 5740, 5743, 5750, 5761, 5786, 5792, 5866, 6016, 6099, 6112, 6121, 6160, 6169, 6176, 6263, 6272, 6313, 7680, 7835, 7840, 7929, 7936, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8319, 8319, 8400, 8412, 8417, 8417, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8497, 8499, 8505, 8544, 8579, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12346, 12353, 12436, 12441, 12442, 12445, 12446, 12449, 12542, 12549, 12588, 12593, 12686, 12704, 12727, 13312, 19893, 19968, 40869, 40960, 42124, 44032, 55203, 63744, 64045, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65056, 65059, 65075, 65076, 65101, 65103, 65136, 65138, 65140, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65381, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; -// \\/* -// \\As per ECMAScript Language Specification 5th Edition, Section 7.6: ISyntaxToken Names and Identifiers -// \\IdentifierStart :: -// \\Can contain Unicode 6.2 categories: -// \\Uppercase letter (Lu), -// \\Lowercase letter (Ll), -// \\Titlecase letter (Lt), -// \\Modifier letter (Lm), -// \\Other letter (Lo), or -// \\Letter number (Nl). -// \\IdentifierPart ::㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ -// \\Can contain IdentifierStart + Unicode 6.2 categories: -// \\Non-spacing mark (Mn), -// \\Combining spacing mark (Mc), -// \\Decimal number (Nd), -// \\Connector punctuation (Pc), -// \\, or -// \\. -// \\ -// \\Codepoint ranges for ES5 Identifiers are extracted from the Unicode 6.2 specification at: -// \\http://www.unicode.org/Public/6.2.0/ucd/UnicodeData.txt -// \\*/ -// \\var unicodeES5IdentifierStart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2208, 2208, 2210, 2220, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516, 6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409, 7413, 7414, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40908, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42894, 42896, 42899, 42912, 42922, 43000, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43968, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; -// \\var unicodeES5IdentifierPart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 887, 890, 893, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1319, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1520, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2048, 2093, 2112, 2139, 2208, 2208, 2210, 2220, 2276, 2302, 2304, 2403, 2406, 2415, 2417, 2423, 2425, 2431, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3073, 3075, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3161, 3168, 3171, 3174, 3183, 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3396, 3398, 3400, 3402, 3406, 3415, 3415, 3424, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4992, 5007, 5024, 5108, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6263, 6272, 6314, 6320, 6389, 6400, 6428, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6617, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7376, 7378, 7380, 7414, 7424, 7654, 7676, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8204, 8205, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 11823, 11823, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12442, 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40908, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42647, 42655, 42737, 42775, 42783, 42786, 42888, 42891, 42894, 42896, 42899, 42912, 42922, 43000, 43047, 43072, 43123, 43136, 43204, 43216, 43225, 43232, 43255, 43259, 43259, 43264, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43643, 43648, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43968, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65062, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; -// \\/** -// \\* Generated by scripts/regenerate-unicode-identifier-parts.js on node v12.4.0 with unicode 12.1 -// \\* based on http://www.unicode.org/reports/tr31/ and https://www.ecma-international.org/ecma-262/6.0/#sec-names-and-keywords -// \\* unicodeESNextIdentifierSt💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 art corresponds to the ID_Start and Other_ID_Start property, and -// \\* unicodeESNextIdentifierPart corresponds to ID_Continue, Other_ID_Continue, plus ID_Start and Other_ID_Start -// \\*/ -// \\var unicodeESNextIdentifierStart = [65, 90, 97, 122, 170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 895, 895, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1488, 1514, 1519, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2144, 2154, 2208, 2228, 2230, 2237, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2432, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2556, 2556, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2809, 2809, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3133, 3160, 3162, 3168, 3169, 3200, 3200, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3412, 3414, 3423, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6264, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6430, 6480, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7401, 7404, 7406, 7411, 7413, 7414, 7418, 7418, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42653, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43261, 43262, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43488, 43492, 43494, 43503, 43514, 43518, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43646, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66176, 66204, 66208, 66256, 66304, 66335, 66349, 66378, 66384, 66421, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68096, 68112, 68115, 68117, 68119, 68121, 68149, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68324, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68899, 69376, 69404, 69415, 69415, 69424, 69445, 69600, 69622, 69635, 69687, 69763, 69807, 69840, 69864, 69891, 69926, 69956, 69956, 69968, 70002, 70006, 70006, 70019, 70066, 70081, 70084, 70106, 70106, 70108, 70108, 70144, 70161, 70163, 70187, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70366, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70461, 70461, 70480, 70480, 70493, 70497, 70656, 70708, 70727, 70730, 70751, 70751, 70784, 70831, 70852, 70853, 70855, 70855, 71040, 71086, 71128, 71131, 71168, 71215, 71236, 71236, 71296, 71338, 71352, 71352, 71424, 71450, 71680, 71723, 71840, 71903, 71935, 71935, 72096, 72103, 72106, 72144, 72161, 72161, 72163, 72163, 72192, 72192, 72203, 72242, 72250, 72250, 72272, 72272, 72284, 72329, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72750, 72768, 72768, 72818, 72847, 72960, 72966, 72968, 72969, 72971, 73008, 73030, 73030, 73056, 73061, 73063, 73064, 73066, 73097, 73112, 73112, 73440, 73458, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92880, 92909, 92928, 92975, 92992, 92995, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94032, 94032, 94099, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 123136, 123180, 123191, 123197, 123214, 123214, 123584, 123627, 124928, 125124, 125184, 125251, 125259, 125259, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101]; -// \\var unicodeESNextIdentifierPart = [48, 57, 65, 90, 95, 95, 97, 122, 170, 170, 181, 181, 183, 183, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, 887, 890, 893, 895, 895, 902, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1519, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2045, 2045, 2048, 2093, 2112, 2139, 2144, 2154, 2208, 2228, 2230, 2237, 2259, 2273, 2275, 2403, 2406, 2415, 2417, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2556, 2556, 2558, 2558, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2809, 2815, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3072, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3162, 3168, 3171, 3174, 3183, 3200, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3328, 3331, 3333, 3340, 3342, 3344, 3346, 3396, 3398, 3400, 3402, 3406, 3412, 3415, 3423, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3558, 3567, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4969, 4977, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6264, 6272, 6314, 6320, 6389, 6400, 6430, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6618, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6832, 6845, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7376, 7378, 7380, 7418, 7424, 7673, 7675, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42737, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43047, 43072, 43123, 43136, 43205, 43216, 43225, 43232, 43255, 43259, 43259, 43261, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43488, 43518, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65071, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66045, 66045, 66176, 66204, 66208, 66256, 66272, 66272, 66304, 66335, 66349, 66378, 66384, 66426, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66720, 66729, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68099, 68101, 68102, 68108, 68115, 68117, 68119, 68121, 68149, 68152, 68154, 68159, 68159, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68326, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68903, 68912, 68921, 69376, 69404, 69415, 69415, 69424, 69456, 69600, 69622, 69632, 69702, 69734, 69743, 69759, 69818, 69840, 69864, 69872, 69881, 69888, 69940, 69942, 69951, 69956, 69958, 69968, 70003, 70006, 70006, 70016, 70084, 70089, 70092, 70096, 70106, 70108, 70108, 70144, 70161, 70163, 70199, 70206, 70206, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70378, 70384, 70393, 70400, 70403, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70459, 70468, 70471, 70472, 70475, 70477, 70480, 70480, 70487, 70487, 70493, 70499, 70502, 70508, 70512, 70516, 70656, 70730, 70736, 70745, 70750, 70751, 70784, 70853, 70855, 70855, 70864, 70873, 71040, 71093, 71096, 71104, 71128, 71133, 71168, 71232, 71236, 71236, 71248, 71257, 71296, 71352, 71360, 71369, 71424, 71450, 71453, 71467, 71472, 71481, 71680, 71738, 71840, 71913, 71935, 71935, 72096, 72103, 72106, 72151, 72154, 72161, 72163, 72164, 72192, 72254, 72263, 72263, 72272, 72345, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72758, 72760, 72768, 72784, 72793, 72818, 72847, 72850, 72871, 72873, 72886, 72960, 72966, 72968, 72969, 72971, 73014, 73018, 73018, 73020, 73021, 73023, 73031, 73040, 73049, 73056, 73061, 73063, 73064, 73066, 73102, 73104, 73105, 73107, 73112, 73120, 73129, 73440, 73462, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92768, 92777, 92880, 92909, 92912, 92916, 92928, 92982, 92992, 92995, 93008, 93017, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94031, 94087, 94095, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 113821, 113822, 119141, 119145, 119149, 119154, 119163, 119170, 119173, 119179, 119210, 119213, 119362, 119364, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 120782, 120831, 121344, 121398, 121403, 121452, 121461, 121461, 121476, 121476, 121499, 121503, 121505, 121519, 122880, 122886, 122888, 122904, 122907, 122913, 122915, 122916, 122918, 122922, 123136, 123180, 123184, 123197, 123200, 123209, 123214, 123214, 123584, 123641, 124928, 125124, 125136, 125142, 125184, 125259, 125264, 125273, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101, 917760, 917999]; -// \\/** -// \\* Test for whether a single line comment with leading whitespace trimmed's text contains a directive. -// \\*/ -// \\var commentDirectiveRegExSingleLine = /^\/\/\/?\s*@(ts-expect-error|ts-ignore)/; -// \\/** -// \\* Test for whether a multi-line comment with leading whitespace trimmed's last line contains a directive. -// \\*/ -// \\var commentDirectiveRegExMultiLine = /^(?:\/|\*)*\s*@(ts-expect-error|ts-ignore)/; -// \\ /** @deprecated Use `factory.updateTaggedTemplate` or the factory supplied by your transformation context instead. */ -// \\ ts.updateTaggedTemplate = ts.Debug.deprecate(function updateTaggedTemplate(node, tag, typeArgumentsOrTemplate, template) { -// \\ var typeArguments; -// \\ if (template) { -// \\ typeArguments = typeArgumentsOrTemplate; -// \\ } -// \\ else { -// \\ template = typeArgumentsOrTemplate; -// \\ } -// \\ return ts.factory.updateTaggedTemplateExpression(node, tag, typeArguments, template); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.updateBinary` or the factory supplied by your transformation context instead. */ -// \\ ts.updateBinary = ts.Debug.deprecate(function updateBinary(node, left, right, operator) { -// \\ if (operator === void 0) { operator = node.operatorToken; } -// \\ if (typeof operator === "number") { -// \\ operator = operator === node.operatorToken.kind ? node.operatorToken : ts.factory.createToken(operator); -// \\ } -// \\ return ts.factory.updateBinaryExpression(node, left, operator, right); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createConditional` or the factory supplied by your transformation context instead. */ -// \\ ts.createConditional = ts.Debug.deprecate(function createConditional(condition, questionTokenOrWhenTrue, whenTrueOrWhenFalse, colonToken, whenFalse) { -// \\ return arguments.length === 5 ? ts.factory.createCondit 🤎💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 ionalExpression(condition, questionTokenOrWhenTrue, whenTrueOrWhenFalse, colonToken, whenFalse) : -// \\ arguments.length === 3 ? ts.factory.createConditionalExpression(condition, ts.factory.createToken(57 /* QuestionToken */), questionTokenOrWhenTrue, ts.factory.createToken(58 /* ColonToken */), whenTrueOrWhenFalse) : -// \\ ts.Debug.fail("Argument count mismatch"); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createYield` or the factory supplied by your transformation context instead. */ -// \\ ts.createYield = ts.Debug.deprecate(function createYield(asteriskTokenOrExpression, expression) { -// \\ var asteriskToken; -// \\ if (expression) { -// \\ asteriskToken = asteriskTokenOrExpression; -// \\ } -// \\ else { -// \\ expression = asteriskTokenOrExpression; -// \\ } -// \\ return ts.factory.createYieldExpre 🤎💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 ssion(asteriskToken, expression); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createClassExpression` or the factory supplied by your transformation context instead. */ -// \\ ts.createClassExpression = ts.Debug.deprecate(function createClassExpression(modifiers, name, typeParameters, heritageClauses, members) { -// \\ return ts.factory.createClassExpression(/*decorators*/ undefined, modifiers, name, typeParameters, heritageClauses, members); -// \\ }, factoryDeprecation); 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ -// \\ /** @deprecated Use `factory.updateClassExpression` or the factory supplied by your transformation context instead. */ -// \\ ts.updateClassExpression = ts.Debug.deprecate(function updateClassExpression(node, modifiers, name, typeParameters, heritageClauses, members) { -// \\ return ts.factory.updateClassExpression(node, /*decorato 🤎💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 rs*/ undefined, modifiers, name, typeParameters, heritageClauses, members); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createPropertySignature` or the factory supplied by your transformation context instead. */ -// \\ ts.createPropertySignature = ts.Debug.deprecate(function createPropertySignature(modifiers, name, questionToken, type, initializer) { -// \\ var node = ts.factory.createPropertySignature(modifiers, name, questionToken, type); -// \\ node.initializer = initializer; -// \\ return node; -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.updatePropertySignature` or the factory supplied by your transformation context instead. */ -// \\ ts.updatePropertySignature = ts.Debug.deprecate(function updatePropertySignature(node, modifiers, name, questionToken, type, initializer) { -// \\ var updated = ts.factory.updatePropertySignature(node, modifiers, name, questionToken, type); -// \\ if (node.initia 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 lizer !== initializer) { -// \\ if (updated === node) { -// \\ updated = ts 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 .factory.cloneNode(node); -// \\ } -// \\ updated.ini ` 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 tializer = initializer; -// \\ } -// \\ return updated;⏏ ♀ 💚 💙 💜 🖤 🤍 🤎📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 ♂ ⚕ ♾️ -// \\ }, factoryDeprecation 💛 💚 💙 💜 🖤 🤍🕞 💛⏏ ♀ ♂ ⚕ ♾️ -// \\ /** @deprecated Use⏏ ♀ 💚 💙 💜 🖤 🤍 🤎📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 ♂ ⚕ ♾️ -// \\ ts.createExpression ` 💛 💚 💙 💜 🖤 🤍 🤎💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 Wi 💛⏏ ♀ ♂ ⚕ ♾️ -// \\ return ts.factory⏏ 💚 💙 💜 🖤 🤍 🤎 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 ♀ ♂ ⚕ ♾️ -// \\ }, factoryDeprecation 💛⏏ ♀ ♂ ⚕ ♾️ -// \\ /** @deprecated Use⏏ ♀ ♂ ⚕ ♾️ -// \\ ts.updateExpressionWi 💛⏏ ♀ ♂ ⚕ ♾️ -// \\ }, factoryDeprecation);👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ /** @deprecated Use `factory.createArrowFunction` or the factory supplied by your transformation context instead. */👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ ts.createArrowFunction = ts.Debug.deprecate(function createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanTokenOrBody, body) { -// \\ return arguments.length === 6 ? ts.factory.createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanTokenOrBody, body) : -// \\ arguments.length === 5 ? ts.factory.createA 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 -// \\💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 rrowFunction(modifiers, typeParamete👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ }, factoryDeprecation); 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 -// \\💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 -// \\ /** @deprecated Use `factory.updateArrowFunction` o 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍 ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 -// \\💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 r the factory supplied by your transformation context instead. */ -// \\ ts.updateArrowFunction = ts.Debug.deprecate(functio 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 -// \\💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 n updateArrowFunction(node, modifiers, typeParameters, parameters, type, equalsGreaterThanTokenOrBody, body) { -// \\ return arguments.length === 7 ? ts.factory.upda 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 -// \\💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 teArrowFunction(node, modifiers, typeParameters, parameters, type, equalsGreaterThanTokenOrBody, body) : -// \\ arguments.length === 6 ? ts.factory.updateA 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 -// \\💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 rrowFunction(node, modifiers, typeParameters, parameters, type, node.equalsGreaterThanToken, equalsGreaterThanTokenOrBody) : -// \\ ts.Debug.fail("Argument count mismatch" 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 -// \\ 💔 ❣️ 💕 💞 💓 -// \\ 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 -// \\💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 );👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽eDeclaration(name, exclamationTokenOrType, typeOrInitializer, initializer) { -// \\ return arguments.length === 4 ? ts.factory.createVariableDeclaration(name, exclamationTokenOrType, typeOrInitializer, initializer) :👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ argu💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 ments.length >= 1 && arguments.length <= 3 ? ts.factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, exclamationTokenOrType, typeOrInitializer) : -// \\ ts.Debug.fail("Argument count mismatch");👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.updateVariableDeclaration` or the factory supplied by your transformation context instead. */👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ return arguments.length === 5 ? ts.factory.updateVariableDeclaration(node, name, exclamationTokenOrType, typeOrInitializer, initializer) : -// \\ arguments.length === 4 ? ts.factory.updateVariableDeclaration(node, name, node.exclamationToken, exclamationTokenOrType, typeOrInitializer) :👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ ts.Debug.fail("Argument count mismatch"); -// \\ }, factoryDeprecation); -// \\ 😀 😃 😄 😁 😆 🤩 😅 😂 🤣 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 😥 🤤 😭 😓 😪 😴 🥱 🙄 🤨 🧐 🤔 🤫 🤭 🤥 😬 🤐 🤢 🤮 🤧 😷 🤒 🤕 😈 👿 👹 👺 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾 👐 🙌 👏 🙏 🤲 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤘 🤏 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🖕 🤟 ✍️ 🤳 💅 🖖 💄 💋 👄 👅 👂 🦻 👃 🦵 🦶 🦾 🦿 👣 👁 👀 🗣 👤 👥 👶 👦 👧 🧒 👨 👩 🧑 👱‍♀️ 👱 🧔 👴 👵 🧓 👲 👳‍♀️ 👳 🧕 👮‍♀️ 👮 👷‍♀️ 👷 💂‍♀️ 💂 🕵️‍♀️ 🕵️ 👩‍⚕️ 👨‍⚕️ 👩‍🌾 👨‍🌾 👩‍🍳 👨‍🍳 👩‍🎓 👨‍🎓 👩‍🎤 👨‍🎤 👩‍🏫 👨‍🏫 👩‍🏭 👨‍🏭 👩‍💻 👨‍💻 👩‍💼 👨‍💼 👩‍🔧 👨‍🔧 👩‍🔬 👨‍🔬 👩‍🎨 👨‍🎨 👩‍🚒 👨‍🚒 👩‍✈️ 👨‍✈️ 👩‍🚀 👨‍🚀 👩‍⚖️ 👨‍⚖️ 🤶 🎅 👸 🤴 👰 🤵 👼 🤰 🤱 🙇‍♀️ 🙇 💁 💁‍♂️ 🙅 🙅‍♂️ 🙆 🙆‍♂️ 🙋 🙋‍♂️ 🤦‍♀️ 🤦‍♂️ 🤷‍♀️ 🤷‍♂️ 🙎 🙎‍♂️ 🙍 🙍‍♂️ 💇 💇‍♂️ 💆 💆‍♂️ 🧖‍♀️ 🧖‍♂️ 🧏 🧏‍♂️ 🧏‍♀️ 🧙‍♀️ 🧙‍♂️ 🧛‍♀️ 🧛‍♂️ 🧟‍♀️ 🧟‍♂️ 🧚‍♀️ 🧚‍♂️ 🧜‍♀️ 🧜‍♂️ 🧝‍♀️ 🧝‍♂️ 🧞‍♀️ 🧞‍♂️ 🕴 💃 🕺 👯 👯‍♂️ 🚶‍♀️ 🚶 🏃‍♀️ 🏃 🧍 🧍‍♂️ 🧍‍♀️ 🧎 🧎‍♂️ 🧎‍♀️ 👨‍🦯 👩‍🦯 👨‍🦼 👩‍🦼 👨‍🦽 👩‍🦽 🧑‍🤝‍🧑 👫 👭 👬 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👦 👩‍👧 👩‍👧‍👦 👩‍👦‍👦 👩‍👧‍👧 👨‍👦 👨‍👧 👨‍👧‍👦 👨‍👦‍👦 👨‍👧‍👧 👚 👕 👖 👔 👗 👙 👘 👠 👡 👢 👞 👟 👒 🎩 🎓 👑 ⛑ 🎒 👝 👛 👜 💼 👓 🕶 🤿 🌂 ☂️ 🧣 🧤 🧥 🦺 🥻 🩱 🩲 🩳 🩰 🧦 🧢 ⛷ 🏂 🏋️‍♀️ 🏋️ 🤺 🤼‍♀️ 🤼‍♂️ 🤸‍♀️ 🤸‍♂️ ⛹️‍♀️ ⛹️ 🤾‍♀️ 🤾‍♂️ 🏌️‍♀️ 🏌️ 🏄‍♀️ 🏄 🏊‍♀️ 🏊 🤽‍♀️ 🤽‍♂️ 🚣‍♀️ 🚣 🏇 🚴‍♀️ 🚴 🚵‍♀️ 🚵 🤹‍♀️ 🤹‍♂️ 🧗‍♀️ 🧗‍♂️ 🧘‍♀️ 🧘‍♂️ 🥰 🥵 🥶 🥳 🥴 🥺 🦸 🦹 🧑‍🦰 🧑‍🦱 🧑‍🦳 🧑‍🦲 🧑‍⚕️ 🧑‍🎓 🧑‍🏫 🧑‍⚖️ 🧑‍🌾 🧑‍🍳 🧑‍🔧 🧑‍🏭 🧑‍💼 🧑‍🔬 🧑‍💻 🧑‍🎤 🧑‍🎨 🧑‍✈️ 🧑‍🚀 🧑‍🚒 🧑‍🦯 🧑‍🦼 🧑‍🦽 🦰 🦱 🦲 🦳 -// \\ /** @deprecated Use `factory.createImportClause` or the factory supplied by your transformation context instead. */ -// \\ ts.createImportClause = ts.Debug.deprecate(function createImportClause(name, namedBindings, isTypeOnly) {👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ return ts.factory.createImportClause(isTypeOnly, name, namedBindings); -// \\ }, factoryDeprecation);👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽ry supplied by your transformation context instead. */ -// \\ ts.updateImportClause = ts.Debug.deprecate(function updateImportClause(node, name, namedBindings, isTypeOnly) { -// \\ return ts.factory.updateImportClause(node, isTypeOnly, name, namedBindings); -// \\ }, factoryDeprecation);👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ /** @deprecated Use `factory.createExportDeclaration` or the factory supplied by your transformation context instead. */👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🏋🏿‍♀️ 🏋🏿 🤸🏿‍♀️ 🤸🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 🤾🏿‍♀️ 🤾🏿‍♂️ 🏌🏿‍♀️ 🏌🏿 🏄🏿‍♀️ 🏄🏿 🏊🏿‍♀️ 🏊🏿 🤽🏿‍♀️ 🤽🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🏇🏿 🚴🏿‍♀️ 🚴🏿 🚵🏿‍♀️ 🚵🏿 🤹🏿‍♀️ 🤹🏿‍♂️ 🛀🏿 🧒🏿 🧑🏿 🧓🏿 🧕🏿 🧔🏿 🤱🏿 🧙🏿‍♀️ 🧙🏿‍♂️ 🧚🏿‍♀️ 🧚🏿‍♂️ 🧛🏿‍♀️ 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿‍♂️ 🧝🏿‍♀️ 🧝🏿‍♂️ 🧖🏿‍♀️ 🧖🏿‍♂️ 🧗🏿‍♀️ 🧗🏿‍♂️ 🧘🏿‍♀️ 🧘🏿‍♂️ 🤟🏿 🤲🏿 💏🏿 💑🏿 🤏🏿 🦻🏿 🧏🏿 🧏🏿‍♂️ 🧏🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 🧍🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🧎🏿‍♀️ 👨🏿‍🦯 👩🏿‍🦯 👨🏿‍🦼 👩🏿‍🦼 👨🏿‍🦽 👩🏿‍🦽 🧑🏿‍🤝‍🧑🏿 🧑🏿‍🦰 🧑🏿‍🦱 🧑🏿‍🦳 🧑🏿‍🦲 🧑🏿‍⚕️ 🧑🏿‍🎓 🧑🏿‍🏫 🧑🏿‍⚖️ 🧑🏿‍🌾 🧑🏿‍🍳 🧑🏿‍🔧 🧑🏿‍🏭 🧑🏿‍💼 🧑🏿‍🔬 🧑🏿‍💻 🧑🏿‍🎤 🧑🏿‍🎨 🧑🏿‍✈️ 🧑🏿‍🚀 🧑🏿‍🚒 🧑🏿‍🦯 🧑🏿‍🦼 🧑🏿‍🦽 -// \\ ts.createExportDeclaration = ts.Debug.deprecate(function createExportDeclaration(decorators, modifiers, exportClause, moduleSpecifier, isTypeOnly) { -// \\ if (isTypeOnly === void 0) { isTypeOnly = false; } -// \\ return ts.factory.createExportDeclaration(decorators, modifiers, isTypeOnly, exportClause, moduleSpecifier);😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.updateExportDeclaration` or the factory supplied by your transformation context instead. */ -// \\ ts.updateEx 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍portDeclaration = ts.Debug.deprecate(function updateExportDeclaration(node, decorators, modifiers, exportClause, moduleSpecifier, isTypeOnly) { -// \\ return ts.factory.updateExportDeclaration(node, decorators, modifiers, isTypeOnly, exportClause, moduleSpecifier); -// \\ }, factory 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍Deprecation); -// \\ /** @deprecated Use `factory.createJSDocParameterTag` or the factory supplied by your transformation context instead. */ -// \\ ts.createJSDocPar 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍amTag = ts.Debug 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍.deprecate(function createJSDocParamTag(name, isBracketed, typeExpression, comment) { -// \\ return ts.factory.createJSDocParameterTag(/*tagName*/ undefined, name, isBracketed, typeExpression, /*isNameFirst*/ false, comment ? ts.factory.createNodeArray([ts.factory.createJSDocText(comment)]) : undefivned); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createComma` or the factory supplied by your transformation context instead. */ -// \\ ts.createComma = ts.Debug.deprecate(function createComma(left, right) { -// \\ return ts.factory.createComma(left, right); -// \\ 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍🐱‍🏍 }, factoryDeprecation); -// \\ /** @deprecated 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍Use `factory.createLessThan` or the factory supplied by your transformation context instead. */ -// \\ ts.createLessThan = ts.Debug.deprecate(function createLessThan(left, right) {😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ }, factoryDeprecation); -// \\ /** @ 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍deprecated Use `factory.createAssignment` or the factory supplied by your transformation context instead. */😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ /** @deprecated Use `factory.createStrictEquality` or the factory supplied by your transformation context instead. */ -// \\ ts.createStrictEquality = ts.Debug.dep 🐱‍👤 🐱‍🚀 🐱‍🐉 🐱‍💻 🐱‍🏍recate(function createStrictEquality(left, right) { -// \\ return ts.factory.createStrictEquality(left, right);😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ }, factoryDeprecation); -// \\ /** @deprecated 🐱‍🐉 🐱‍💻 🐱‍👤 🐱‍🚀Use `factory.createStrictInequality` or the factory supplied by your transformation context instead. */ -// \\ ts.createStrictInequality = ts.Debug.deprecate(function createStrictInequality(left, right) { -// \\ return ts.factory.createStrictInequality(left, right); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createAdd` or the factory supplied b😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ return ts.factory.createSubtract(left, right); -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createLogicalAnd` or the factory supplied by your transformation context instead. */ -// \\ ts.createLogicalAnd = ts.Debug.deprecate(function createLogicalAnd(left, right) { -// \\ return ts.factory.createLogicalAnd(left, right);😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use `factory.createLogicalOr` or the factory supplied by your transformation context instead. */ -// \\ ts.createLogicalOr = ts.Debug.deprecate(function createLogicalOr(left, right) {😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ return ts.factory.createLogicalOr(left, right);😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ }, factoryDeprecation);😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢supplied by your transformation context instead. */ -// \\ ts.createPostfixIncrement = ts.Debug.deprecate(function createPostfixIncrement(operand) {😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ return ts.factory.createPostfixIncrement(operand);😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ }, factoryDeprecation); -// \\ /** @deprecated Use an appropriate `factory` method instead. */ -// \\ ts.createNode = ts.Debug.deprecate(function createNode(kind, pos, end) { -// \\ if (pos === void 0) { pos = 0; }😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢NodeFactory.createBaseSourceFileNode(kind) : -// \\ kind === 79 /* Identifier */ ? ts.parseBaseNodeFactory.createBaseIdentifierNode(kind) : -// \\ kind === 80 /* PrivateIdentifier */ ? ts.parseBaseNodeFactory.createBasePrivateIdentifierNode(kind) : -// \\ !ts.isNodeKind(kind) ? ts.parseBaseNodeFactory.createBaseTokenNode(kind) :😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢a node ~for mutation~ with its `pos`, `end`, and `parent` set. -// \\ * -// \\ * NOTE: It is unsafe to change any properties of a `Node` that relate to its AST children, as those changes won't be -// \\ * captured with respect to transformations. -// \\ * -// \\ * @deprecated Use an appropriate `factory.update...` method instead, use `setCommentRange` or `setSourceMapRange`, and avoid setting `parent`.😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ ts.setTextRange(clone, node); -// \\ ts.setParent(clone, node.parent); -// \\ return clone;😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 🤪 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 🤬 😶 😐 😑 😯 😦 😧 😮 😲 😵 🤯 😳 😱 😨 😰 😢 -// \\ }, { since: "4.0", warnAfter: "4.1", message: "Use an appropriate `factory.update...` method instead, use `setCommentRange` or `setSourceMapRange`, and avoid setting `parent`." }); -// \\ // #endregion Node Factory top-level exports -// \\ // DEPRECATION: Renamed node tests -// \\ // DEPRECATION PLAN:🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 -// \\ // - soft: 4.0 -// \\ // - warn: 4.1 -// \\ // - error: TBD -// \\ // #region Renamed node Tests -// \\ /** @deprecated Use `isTypeAssertionExpression` instead. */ -// \\ ts.isTypeAssertion = ts.Debug.deprecate(function isTypeAssertion(node) { -// \\ return node.kind === 209 /* TypeAssertionExpression */; -// \\ }, { -// \\ since: "4.0", -// \\ warnAfter: "4.1", -// \\ message: "Use `isTypeAssertionExpression` instead." -// \\ }); -// \\ // #endregion -// \\ // DEPRECATION: Renamed node tests -// \\ // DEPRECATION PLAN: -// \\ // - soft: 4.2 -// \\ // - warn: 4.3 -// \\ // - error: TBD -// \\ // #region Renamed node Tests -// \\ /** -// \\ * @deprecated Use `isMemberName` instead. -// \\ */ -// \\ ts.isIdentifierOrPrivateIdentifier = ts.Debug.deprecate(function isIdentifierOrPrivateIdentifier(node) { -// \\ return ts.isMemberName(node); -// \\ }, { -// \\ since: "4.2", -// \\ warnAfter: "4.3", -// \\ message: "Use `isMemberName` instead." -// \\ }); -// \\ // #endregion Renamed node Tests -// \\})(ts || (ts = {})); -// ; -// const ascii_text: []const u8 = -// \\ -// \\package js_lexer -// \\ -// \\// The lexer converts a source file to a stream of tokens. Unlike many -// \\// compilers, esbuild does not run the lexer to completion before the parser is -// \\// started. Instead, the lexer is called repeatedly by the parser as the parser -// \\// parses the file. This is because many tokens are context-sensitive and need -// \\// high-level information from the parser. Examples are regular expression -// \\// literals and JSX elements. -// \\// -// \\// For efficiency, the text associated with textual tokens is stored in two -// \\// separate ways depending on the token. Identifiers use UTF-8 encoding which -// \\// allows them to be slices of the input file without allocating extra memory. -// \\// Strings use UTF-16 encoding so they can represent unicode surrogates -// \\// accurately. -// \\ -// \\import ( -// \\"fmt" -// \\"strconv" -// \\"strings" -// \\"unicode" -// \\"unicode/utf8" -// \\ -// \\"github.com/evanw/esbuild/internal/js_ast" -// \\"github.com/evanw/esbuild/internal/logger" -// \\) -// \\ -// \\type T uint -// \\ -// \\// If you add a new token, remember to add it to "tokenToString" too -// \\const ( -// \\TEndOfFile T = iota -// \\TSyntaxError -// \\ -// \\// "#!/usr/bin/env node" -// \\THashbang -// \\ -// \\// Literals -// \\TNoSubstitutionTemplateLiteral // Contents are in lexer.StringLiteral ([]uint16) -// \\TNumericLiteral // Contents are in lexer.Number (float64) -// \\TStringLiteral // Contents are in lexer.StringLiteral ([]uint16) -// \\TBigIntegerLiteral // Contents are in lexer.Identifier (string) -// \\ -// \\// Pseudo-literals -// \\TTemplateHead // Contents are in lexer.StringLiteral ([]uint16) -// \\TTemplateMiddle // Contents are in lexer.StringLiteral ([]uint16) -// \\TTemplateTail // Contents are in lexer.StringLiteral ([]uint16) -// \\ -// \\// Punctuation -// \\TAmpersand -// \\TAmpersandAmpersand -// \\TAsterisk -// \\TAsteriskAsterisk -// \\TAt -// \\TBar -// \\TBarBar -// \\TCaret -// \\TCloseBrace -// \\TCloseBracket -// \\TCloseParen -// \\TColon -// \\TComma -// \\TDot -// \\TDotDotDot -// \\TEqualsEquals -// \\TEqualsEqualsEquals -// \\TEqualsGreaterThan -// \\TExclamation -// \\TExclamationEquals -// \\TExclamationEqualsEquals -// \\TGreaterThan -// \\TGreaterThanEquals -// \\TGreaterThanGreaterThan -// \\TGreaterThanGreaterThanGreaterThan -// \\TLessThan -// \\TLessThanEquals -// \\TLessThanLessThan -// \\TMinus -// \\TMinusMinus -// \\TOpenBrace -// \\TOpenBracket -// \\TOpenParen -// \\TPercent -// \\TPlus -// \\TPlusPlus -// \\TQuestion -// \\TQuestionDot -// \\TQuestionQuestion -// \\TSemicolon -// \\TSlash -// \\TTilde -// \\ -// \\// Assignments (keep in sync with IsAssign() below) -// \\TAmpersandAmpersandEquals -// \\TAmpersandEquals -// \\TAsteriskAsteriskEquals -// \\TAsteriskEquals -// \\TBarBarEquals -// \\TBarEquals -// \\TCaretEquals -// \\TEquals -// \\TGreaterThanGreaterThanEquals -// \\TGreaterThanGreaterThanGreaterThanEquals -// \\TLessThanLessThanEquals -// \\TMinusEquals -// \\TPercentEquals -// \\TPlusEquals -// \\TQuestionQuestionEquals -// \\TSlashEquals -// \\ -// \\// Class-private fields and methods -// \\TPrivateIdentifier -// \\ -// \\// Identifiers -// \\TIdentifier // Contents are in lexer.Identifier (string) -// \\TEscapedKeyword // A keyword that has been escaped as an identifer -// \\ -// \\// Reserved words -// \\TBreak -// \\TCase -// \\TCatch -// \\TClass -// \\TConst -// \\TContinue -// \\TDebugger -// \\TDefault -// \\TDelete -// \\TDo -// \\TElse -// \\TEnum -// \\TExport -// \\TExtends -// \\TFalse -// \\TFinally -// \\TFor -// \\TFunction -// \\TIf -// \\TImport -// \\TIn -// \\TInstanceof -// \\TNew -// \\TNull -// \\TReturn -// \\TSuper -// \\TSwitch -// \\TThis -// \\TThrow -// \\TTrue -// \\TTry -// \\TTypeof -// \\TVar -// \\TVoid -// \\TWhile -// \\TWith -// \\) -// \\ -// \\func (t T) IsAssign() bool { -// \\return t >= TAmpersandAmpersandEquals && t <= TSlashEquals -// \\} -// \\ -// \\var Keywords = map[string]T{ -// \\// Reserved words -// \\"break": TBreak, -// \\"case": TCase, -// \\"catch": TCatch, -// \\"class": TClass, -// \\"const": TConst, -// \\"continue": TContinue, -// \\"debugger": TDebugger, -// \\"default": TDefault, -// \\"delete": TDelete, -// \\"do": TDo, -// \\"else": TElse, -// \\"enum": TEnum, -// \\"export": TExport, -// \\"extends": TExtends, -// \\"false": TFalse, -// \\"finally": TFinally, -// \\"for": TFor, -// \\"function": TFunction, -// \\"if": TIf, -// \\"import": TImport, -// \\"in": TIn, -// \\"instanceof": TInstanceof, -// \\"new": TNew, -// \\"null": TNull, -// \\"return": TReturn, -// \\"super": TSuper, -// \\"switch": TSwitch, -// \\"this": TThis, -// \\"throw": TThrow, -// \\"true": TTrue, -// \\"try": TTry, -// \\"typeof": TTypeof, -// \\"var": TVar, -// \\"void": TVoid, -// \\"while": TWhile, -// \\"with": TWith, -// \\} -// \\ -// \\var StrictModeReservedWords = map[string]bool{ -// \\"implements": true, -// \\"interface": true, -// \\"let": true, -// \\"package": true, -// \\"private": true, -// \\"protected": true, -// \\"public": true, -// \\"static": true, -// \\"yield": true, -// \\} -// \\ -// \\type json struct { -// \\parse bool -// \\allowComments bool -// \\} -// \\ -// \\type Lexer struct { -// \\log logger.Log -// \\source logger.Source -// \\tracker logger.LineColumnTracker -// \\current int -// \\start int -// \\end int -// \\ApproximateNewlineCount int -// \\LegacyOctalLoc logger.Loc -// \\AwaitKeywordLoc logger.Loc -// \\FnOrArrowStartLoc logger.Loc -// \\PreviousBackslashQuoteInJSX logger.Range -// \\LegacyHTMLCommentRange logger.Range -// \\Token T -// \\HasNewlineBefore bool -// \\HasPureCommentBefore bool -// \\PreserveAllCommentsBefore bool -// \\IsLegacyOctalLiteral bool -// \\PrevTokenWasAwaitKeyword bool -// \\CommentsToPreserveBefore []js_ast.Comment -// \\AllOriginalComments []js_ast.Comment -// \\codePoint rune -// \\Identifier string -// \\JSXFactoryPragmaComment logger.Span -// \\JSXFragmentPragmaComment logger.Span -// \\SourceMappingURL logger.Span -// \\Number float64 -// \\rescanCloseBraceAsTemplateToken bool -// \\forGlobalName bool -// \\json json -// \\prevErrorLoc logger.Loc -// \\ -// \\// Escape sequences in string literals are decoded lazily because they are -// \\// not interpreted inside tagged templates, and tagged templates can contain -// \\// invalid escape sequences. If the decoded array is nil, the encoded value -// \\// should be passed to "tryToDecodeEscapeSequences" first. -// \\decodedStringLiteralOrNil []uint16 -// \\encodedStringLiteralStart int -// \\encodedStringLiteralText string -// \\ -// \\// The log is disabled during speculative scans that may backtrack -// \\IsLogDisabled bool -// \\} -// \\ -// \\type LexerPanic struct{} -// \\ -// \\func NewLexer(log logger.Log, source logger.Source) Lexer { -// \\lexer := Lexer{ -// \\log: log, -// \\source: source, -// \\tracker: logger.MakeLineColumnTracker(&source), -// \\prevErrorLoc: logger.Loc{Start: -1}, -// \\FnOrArrowStartLoc: logger.Loc{Start: -1}, -// \\} -// \\lexer.step() -// \\lexer.Next() -// \\return lexer -// \\} -// \\ -// \\func NewLexerGlobalName(log logger.Log, source logger.Source) Lexer { -// \\lexer := Lexer{ -// \\log: log, -// \\source: source, -// \\tracker: logger.MakeLineColumnTracker(&source), -// \\prevErrorLoc: logger.Loc{Start: -1}, -// \\FnOrArrowStartLoc: logger.Loc{Start: -1}, -// \\forGlobalName: true, -// \\} -// \\lexer.step() -// \\lexer.Next() -// \\return lexer -// \\} -// \\ -// \\func NewLexerJSON(log logger.Log, source logger.Source, allowComments bool) Lexer { -// \\lexer := Lexer{ -// \\log: log, -// \\source: source, -// \\tracker: logger.MakeLineColumnTracker(&source), -// \\prevErrorLoc: logger.Loc{Start: -1}, -// \\FnOrArrowStartLoc: logger.Loc{Start: -1}, -// \\json: json{ -// \\parse: true, -// \\allowComments: allowComments, -// \\}, -// \\} -// \\lexer.step() -// \\lexer.Next() -// \\return lexer -// \\} -// \\ -// \\func (lexer *Lexer) Loc() logger.Loc { -// \\return logger.Loc{Start: int32(lexer.start)} -// \\} -// \\ -// \\func (lexer *Lexer) Range() logger.Range { -// \\return logger.Range{Loc: logger.Loc{Start: int32(lexer.start)}, Len: int32(lexer.end - lexer.start)} -// \\} -// \\ -// \\func (lexer *Lexer) Raw() string { -// \\return lexer.source.Contents[lexer.start:lexer.end] -// \\} -// \\ -// \\func (lexer *Lexer) StringLiteral() []uint16 { -// \\if lexer.decodedStringLiteralOrNil == nil { -// \\// Lazily decode escape sequences if needed -// \\if decoded, ok, end := lexer.tryToDecodeEscapeSequences(lexer.encodedStringLiteralStart, lexer.encodedStringLiteralText, true /* reportErrors */); !ok { -// \\lexer.end = end -// \\lexer.SyntaxError() -// \\} else { -// \\lexer.decodedStringLiteralOrNil = decoded -// \\} -// \\} -// \\return lexer.decodedStringLiteralOrNil -// \\} -// \\ -// \\func (lexer *Lexer) CookedAndRawTemplateContents() ([]uint16, string) { -// \\var raw string -// \\ -// \\switch lexer.Token { -// \\case TNoSubstitutionTemplateLiteral, TTemplateTail: -// \\// "`x`" or "}x`" -// \\raw = lexer.source.Contents[lexer.start+1 : lexer.end-1] -// \\ -// \\case TTemplateHead, TTemplateMiddle: -// \\// "`x${" or "}x${" -// \\raw = lexer.source.Contents[lexer.start+1 : lexer.end-2] -// \\} -// \\ -// \\if strings.IndexByte(raw, '\r') != -1 { -// \\// From the specification: -// \\// -// \\// 11.8.6.1 Static Semantics: TV and TRV -// \\// -// \\// TV excludes the code units of LineContinuation while TRV includes -// \\// them. and LineTerminatorSequences are normalized to -// \\// for both TV and TRV. An explicit EscapeSequence is needed to -// \\// include a or sequence. -// \\ -// \\bytes := []byte(raw) -// \\end := 0 -// \\i := 0 -// \\ -// \\for i < len(bytes) { -// \\c := bytes[i] -// \\i++ -// \\ -// \\if c == '\r' { -// \\// Convert '\r\n' into '\n' -// \\if i < len(bytes) && bytes[i] == '\n' { -// \\i++ -// \\} -// \\ -// \\// Convert '\r' into '\n' -// \\c = '\n' -// \\} -// \\ -// \\bytes[end] = c -// \\end++ -// \\} -// \\ -// \\raw = string(bytes[:end]) -// \\} -// \\ -// \\// This will return nil on failure, which will become "undefined" for the tag -// \\cooked, _, _ := lexer.tryToDecodeEscapeSequences(lexer.start+1, raw, false /* reportErrors */) -// \\return cooked, raw -// \\} -// \\ -// \\func (lexer *Lexer) IsIdentifierOrKeyword() bool { -// \\return lexer.Token >= TIdentifier -// \\} -// \\ -// \\func (lexer *Lexer) IsContextualKeyword(text string) bool { -// \\return lexer.Token == TIdentifier && lexer.Raw() == text -// \\} -// \\ -// \\func (lexer *Lexer) ExpectContextualKeyword(text string) { -// \\if !lexer.IsContextualKeyword(text) { -// \\lexer.ExpectedString(fmt.Sprintf("%q", text)) -// \\} -// \\lexer.Next() -// \\} -// \\ -// \\func (lexer *Lexer) SyntaxError() { -// \\loc := logger.Loc{Start: int32(lexer.end)} -// \\message := "Unexpected end of file" -// \\if lexer.end < len(lexer.source.Contents) { -// \\c, _ := utf8.DecodeRuneInString(lexer.source.Contents[lexer.end:]) -// \\if c < 0x20 { -// \\message = fmt.Sprintf("Syntax error \"\\x%02X\"", c) -// \\} else if c >= 0x80 { -// \\message = fmt.Sprintf("Syntax error \"\\u{%x}\"", c) -// \\} else if c != '"' { -// \\message = fmt.Sprintf("Syntax error \"%c\"", c) -// \\} else { -// \\message = "Syntax error '\"'" -// \\} -// \\} -// \\lexer.addError(loc, message) -// \\panic(LexerPanic{}) -// \\} -// \\ -// \\func (lexer *Lexer) ExpectedString(text string) { -// \\// Provide a friendly error message about "await" without "async" -// \\if lexer.PrevTokenWasAwaitKeyword { -// \\var notes []logger.MsgData -// \\if lexer.FnOrArrowStartLoc.Start != -1 { -// \\note := logger.RangeData(&lexer.tracker, logger.Range{Loc: lexer.FnOrArrowStartLoc}, -// \\"Consider adding the \"async\" keyword here") -// \\note.Location.Suggestion = "async" -// \\notes = []logger.MsgData{note} -// \\} -// \\lexer.addRangeErrorWithNotes(RangeOfIdentifier(lexer.source, lexer.AwaitKeywordLoc), -// \\"\"await\" can only be used inside an \"async\" function", -// \\notes) -// \\panic(LexerPanic{}) -// \\} -// \\ -// \\found := fmt.Sprintf("%q", lexer.Raw()) -// \\if lexer.start == len(lexer.source.Contents) { -// \\found = "end of file" -// \\} -// \\lexer.addRangeError(lexer.Range(), fmt.Sprintf("Expected %s but found %s", text, found)) -// \\panic(LexerPanic{}) -// \\} -// \\ -// \\func (lexer *Lexer) Expected(token T) { -// \\if text, ok := tokenToString[token]; ok { -// \\lexer.ExpectedString(text) -// \\} else { -// \\lexer.Unexpected() -// \\} -// \\} -// \\ -// \\func (lexer *Lexer) Unexpected() { -// \\found := fmt.Sprintf("%q", lexer.Raw()) -// \\if lexer.start == len(lexer.source.Contents) { -// \\found = "end of file" -// \\} -// \\lexer.addRangeError(lexer.Range(), fmt.Sprintf("Unexpected %s", found)) -// \\panic(LexerPanic{}) -// \\} -// \\ -// \\func (lexer *Lexer) Expect(token T) { -// \\if lexer.Token != token { -// \\lexer.Expected(token) -// \\} -// \\lexer.Next() -// \\} -// \\ -// \\func (lexer *Lexer) ExpectOrInsertSemicolon() { -// \\if lexer.Token == TSemicolon || (!lexer.HasNewlineBefore && -// \\lexer.Token != TCloseBrace && lexer.Token != TEndOfFile) { -// \\lexer.Expect(TSemicolon) -// \\} -// \\} -// \\func (lexer *Lexer) ExpectLessThan(isInsideJSXElement bool) { -// \\switch lexer.Token { -// \\case TLessThan: -// \\if isInsideJSXElement { -// \\lexer.NextInsideJSXElement() -// \\} else { -// \\lexer.Next() -// \\} -// \\ -// \\case TLessThanEquals: -// \\lexer.Token = TEquals -// \\lexer.start++ -// \\lexer.maybeExpandEquals() -// \\ -// \\case TLessThanLessThan: -// \\lexer.Token = TLessThan -// \\lexer.start++ -// \\ -// \\case TLessThanLessThanEquals: -// \\lexer.Token = TLessThanEquals -// \\lexer.start++ -// \\ -// \\default: -// \\lexer.Expected(TLessThan) -// \\} -// \\} -// \\ -// \\// This parses a single ">" token. If that is the first part of a longer token, -// \\// this function splits off the first ">" and leaves the remainder of the -// \\// current token as another, smaller token. For example, ">>=" becomes ">=". -// \\func (lexer *Lexer) ExpectGreaterThan(isInsideJSXElement bool) { -// \\switch lexer.Token { -// \\case TGreaterThan: -// \\if isInsideJSXElement { -// \\lexer.NextInsideJSXElement() -// \\} else { -// \\lexer.Next() -// \\} -// \\ -// \\case TGreaterThanEquals: -// \\lexer.Token = TEquals -// \\lexer.start++ -// \\lexer.maybeExpandEquals() -// \\ -// \\case TGreaterThanGreaterThan: -// \\lexer.Token = TGreaterThan -// \\lexer.start++ -// \\ -// \\case TGreaterThanGreaterThanEquals: -// \\lexer.Token = TGreaterThanEquals -// \\lexer.start++ -// \\ -// \\case TGreaterThanGreaterThanGreaterThan: -// \\lexer.Token = TGreaterThanGreaterThan -// \\lexer.start++ -// \\ -// \\case TGreaterThanGreaterThanGreaterThanEquals: -// \\lexer.Token = TGreaterThanGreaterThanEquals -// \\lexer.start++ -// \\ -// \\default: -// \\lexer.Expected(TGreaterThan) -// \\} -// \\} -// \\ -// \\func (lexer *Lexer) maybeExpandEquals() { -// \\switch lexer.codePoint { -// \\case '>': -// \\// "=" + ">" = "=>" -// \\lexer.Token = TEqualsGreaterThan -// \\lexer.step() -// \\ -// \\case '=': -// \\// "=" + "=" = "==" -// \\lexer.Token = TEqualsEquals -// \\lexer.step() -// \\ -// \\if lexer.Token == '=' { -// \\// "=" + "==" = "===" -// \\lexer.Token = TEqualsEqualsEquals -// \\lexer.step() -// \\} -// \\} -// \\} -// \\ -// \\func IsIdentifier(text string) bool { -// \\if len(text) == 0 { -// \\return false -// \\} -// \\for i, codePoint := range text { -// \\if i == 0 { -// \\if !IsIdentifierStart(codePoint) { -// \\return false -// \\} -// \\} else { -// \\if !IsIdentifierContinue(codePoint) { -// \\return false -// \\} -// \\} -// \\} -// \\return true -// \\} -// \\ -// \\func IsIdentifierES5AndESNext(text string) bool { -// \\if len(text) == 0 { -// \\return false -// \\} -// \\for i, codePoint := range text { -// \\if i == 0 { -// \\if !IsIdentifierStartES5AndESNext(codePoint) { -// \\return false -// \\} -// \\} else { -// \\if !IsIdentifierContinueES5AndESNext(codePoint) { -// \\return false -// \\} -// \\} -// \\} -// \\return true -// \\} -// \\ -// \\func ForceValidIdentifier(text string) string { -// \\if IsIdentifier(text) { -// \\return text -// \\} -// \\sb := strings.Builder{} -// \\ -// \\// Identifier start -// \\c, width := utf8.DecodeRuneInString(text) -// \\text = text[width:] -// \\if IsIdentifierStart(c) { -// \\sb.WriteRune(c) -// \\} else { -// \\sb.WriteRune('_') -// \\} -// \\ -// \\// Identifier continue -// \\for text != "" { -// \\c, width := utf8.DecodeRuneInString(text) -// \\text = text[width:] -// \\if IsIdentifierContinue(c) { -// \\sb.WriteRune(c) -// \\} else { -// \\sb.WriteRune('_') -// \\} -// \\} -// \\ -// \\return sb.String() -// \\} -// \\ -// \\// This does "IsIdentifier(UTF16ToString(text))" without any allocations -// \\func IsIdentifierUTF16(text []uint16) bool { -// \\n := len(text) -// \\if n == 0 { -// \\return false -// \\} -// \\for i := 0; i < n; i++ { -// \\isStart := i == 0 -// \\r1 := rune(text[i]) -// \\if r1 >= 0xD800 && r1 <= 0xDBFF && i+1 < n { -// \\if r2 := rune(text[i+1]); r2 >= 0xDC00 && r2 <= 0xDFFF { -// \\r1 = (r1 << 10) + r2 + (0x10000 - (0xD800 << 10) - 0xDC00) -// \\i++ -// \\} -// \\} -// \\if isStart { -// \\if !IsIdentifierStart(r1) { -// \\return false -// \\} -// \\} else { -// \\if !IsIdentifierContinue(r1) { -// \\return false -// \\} -// \\} -// \\} -// \\return true -// \\} -// \\ -// \\// This does "IsIdentifierES5AndESNext(UTF16ToString(text))" without any allocations -// \\func IsIdentifierES5AndESNextUTF16(text []uint16) bool { -// \\n := len(text) -// \\if n == 0 { -// \\return false -// \\} -// \\for i := 0; i < n; i++ { -// \\isStart := i == 0 -// \\r1 := rune(text[i]) -// \\if r1 >= 0xD800 && r1 <= 0xDBFF && i+1 < n { -// \\if r2 := rune(text[i+1]); r2 >= 0xDC00 && r2 <= 0xDFFF { -// \\r1 = (r1 << 10) + r2 + (0x10000 - (0xD800 << 10) - 0xDC00) -// \\i++ -// \\} -// \\} -// \\if isStart { -// \\if !IsIdentifierStartES5AndESNext(r1) { -// \\return false -// \\} -// \\} else { -// \\if !IsIdentifierContinueES5AndESNext(r1) { -// \\return false -// \\} -// \\} -// \\} -// \\return true -// \\} -// \\ -// \\func IsIdentifierStart(codePoint rune) bool { -// \\switch codePoint { -// \\case '_', '$', -// \\'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', -// \\'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', -// \\'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', -// \\'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': -// \\return true -// \\} -// \\ -// \\// All ASCII identifier start code points are listed above -// \\if codePoint < 0x7F { -// \\return false -// \\} -// \\ -// \\return unicode.Is(idStartES5OrESNext, codePoint) -// \\} -// \\ -// \\func IsIdentifierContinue(codePoint rune) bool { -// \\switch codePoint { -// \\case '_', '$', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', -// \\'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', -// \\'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', -// \\'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', -// \\'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': -// \\return true -// \\} -// \\ -// \\// All ASCII identifier start code points are listed above -// \\if codePoint < 0x7F { -// \\return false -// \\} -// \\ -// \\// ZWNJ and ZWJ are allowed in identifiers -// \\if codePoint == 0x200C || codePoint == 0x200D { -// \\return true -// \\} -// \\ -// \\return unicode.Is(idContinueES5OrESNext, codePoint) -// \\} -// \\ -// \\func IsIdentifierStartES5AndESNext(codePoint rune) bool { -// \\switch codePoint { -// \\case '_', '$', -// \\'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', -// \\'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', -// \\'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', -// \\'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': -// \\return true -// \\} -// \\ -// \\// All ASCII identifier start code points are listed above -// \\if codePoint < 0x7F { -// \\return false -// \\} -// \\ -// \\return unicode.Is(idStartES5AndESNext, codePoint) -// \\} -// \\ -// \\func IsIdentifierContinueES5AndESNext(codePoint rune) bool { -// \\switch codePoint { -// \\case '_', '$', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', -// \\'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', -// \\'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', -// \\'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', -// \\'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': -// \\return true -// \\} -// \\ -// \\// All ASCII identifier start code points are listed above -// \\if codePoint < 0x7F { -// \\return false -// \\} -// \\ -// \\// ZWNJ and ZWJ are allowed in identifiers -// \\if codePoint == 0x200C || codePoint == 0x200D { -// \\return true -// \\} -// \\ -// \\return unicode.Is(idContinueES5AndESNext, codePoint) -// \\} -// \\ -// \\// See the "White Space Code Points" table in the ECMAScript standard -// \\func IsWhitespace(codePoint rune) bool { -// \\switch codePoint { -// \\case -// \\'\u0009', // character tabulation -// \\'\u000B', // line tabulation -// \\'\u000C', // form feed -// \\'\u0020', // space -// \\'\u00A0', // no-break space -// \\ -// \\// Unicode "Space_Separator" code points -// \\'\u1680', // ogham space mark -// \\'\u2000', // en quad -// \\'\u2001', // em quad -// \\'\u2002', // en space -// \\'\u2003', // em space -// \\'\u2004', // three-per-em space -// \\'\u2005', // four-per-em space -// \\'\u2006', // six-per-em space -// \\'\u2007', // figure space -// \\'\u2008', // punctuation space -// \\'\u2009', // thin space -// \\'\u200A', // hair space -// \\'\u202F', // narrow no-break space -// \\'\u205F', // medium mathematical space -// \\'\u3000', // ideographic space -// \\ -// \\'\uFEFF': // zero width non-breaking space -// \\return true -// \\ -// \\default: -// \\return false -// \\} -// \\} -// \\ -// \\func RangeOfIdentifier(source logger.Source, loc logger.Loc) logger.Range { -// \\text := source.Contents[loc.Start:] -// \\if len(text) == 0 { -// \\return logger.Range{Loc: loc, Len: 0} -// \\} -// \\ -// \\i := 0 -// \\c, _ := utf8.DecodeRuneInString(text[i:]) -// \\ -// \\// Handle private names -// \\if c == '#' { -// \\i++ -// \\c, _ = utf8.DecodeRuneInString(text[i:]) -// \\} -// \\ -// \\if IsIdentifierStart(c) || c == '\\' { -// \\// Search for the end of the identifier -// \\for i < len(text) { -// \\c2, width2 := utf8.DecodeRuneInString(text[i:]) -// \\if c2 == '\\' { -// \\i += width2 -// \\ -// \\// Skip over bracketed unicode escapes such as "\u{10000}" -// \\if i+2 < len(text) && text[i] == 'u' && text[i+1] == '{' { -// \\i += 2 -// \\for i < len(text) { -// \\if text[i] == '}' { -// \\i++ -// \\break -// \\} -// \\i++ -// \\} -// \\} -// \\} else if !IsIdentifierContinue(c2) { -// \\return logger.Range{Loc: loc, Len: int32(i)} -// \\} else { -// \\i += width2 -// \\} -// \\} -// \\} -// \\ -// \\// When minifying, this identifier may have originally been a string -// \\return source.RangeOfString(loc) -// \\} -// \\ -// \\func (lexer *Lexer) ExpectJSXElementChild(token T) { -// \\if lexer.Token != token { -// \\lexer.Expected(token) -// \\} -// \\lexer.NextJSXElementChild() -// \\} -// \\ -// \\func (lexer *Lexer) NextJSXElementChild() { -// \\lexer.HasNewlineBefore = false -// \\originalStart := lexer.end -// \\ -// \\for { -// \\lexer.start = lexer.end -// \\lexer.Token = 0 -// \\ -// \\switch lexer.codePoint { -// \\case -1: // This indicates the end of the file -// \\lexer.Token = TEndOfFile -// \\ -// \\case '{': -// \\lexer.step() -// \\lexer.Token = TOpenBrace -// \\ -// \\case '<': -// \\lexer.step() -// \\lexer.Token = TLessThan -// \\ -// \\default: -// \\needsFixing := false -// \\ -// \\stringLiteral: -// \\for { -// \\switch lexer.codePoint { -// \\case -1: -// \\// Reaching the end of the file without a closing element is an error -// \\lexer.SyntaxError() -// \\ -// \\case '&', '\r', '\n', '\u2028', '\u2029': -// \\// This needs fixing if it has an entity or if it's a multi-line string -// \\needsFixing = true -// \\lexer.step() -// \\ -// \\case '{', '<': -// \\// Stop when the string ends -// \\break stringLiteral -// \\ -// \\default: -// \\// Non-ASCII strings need the slow path -// \\if lexer.codePoint >= 0x80 { -// \\needsFixing = true -// \\} -// \\lexer.step() -// \\} -// \\} -// \\ -// \\lexer.Token = TStringLiteral -// \\text := lexer.source.Contents[originalStart:lexer.end] -// \\ -// \\if needsFixing { -// \\// Slow path -// \\lexer.decodedStringLiteralOrNil = fixWhitespaceAndDecodeJSXEntities(text) -// \\ -// \\// Skip this token if it turned out to be empty after trimming -// \\if len(lexer.decodedStringLiteralOrNil) == 0 { -// \\lexer.HasNewlineBefore = true -// \\continue -// \\} -// \\} else { -// \\// Fast path -// \\n := len(text) -// \\copy := make([]uint16, n) -// \\for i := 0; i < n; i++ { -// \\copy[i] = uint16(text[i]) -// \\} -// \\lexer.decodedStringLiteralOrNil = copy -// \\} -// \\} -// \\ -// \\break -// \\} -// \\} -// \\ -// \\func (lexer *Lexer) ExpectInsideJSXElement(token T) { -// \\if lexer.Token != token { -// \\lexer.Expected(token) -// \\} -// \\lexer.NextInsideJSXElement() -// \\} -// \\ -// \\func (lexer *Lexer) NextInsideJSXElement() { -// \\lexer.HasNewlineBefore = false -// \\ -// \\for { -// \\lexer.start = lexer.end -// \\lexer.Token = 0 -// \\ -// \\switch lexer.codePoint { -// \\case -1: // This indicates the end of the file -// \\lexer.Token = TEndOfFile -// \\ -// \\case '\r', '\n', '\u2028', '\u2029': -// \\lexer.step() -// \\lexer.HasNewlineBefore = true -// \\continue -// \\ -// \\case '\t', ' ': -// \\lexer.step() -// \\continue -// \\ -// \\case '.': -// \\lexer.step() -// \\lexer.Token = TDot -// \\ -// \\case '=': -// \\lexer.step() -// \\lexer.Token = TEquals -// \\ -// \\case '{': -// \\lexer.step() -// \\lexer.Token = TOpenBrace -// \\ -// \\case '}': -// \\lexer.step() -// \\lexer.Token = TCloseBrace -// \\ -// \\case '<': -// \\lexer.step() -// \\lexer.Token = TLessThan -// \\ -// \\case '>': -// \\lexer.step() -// \\lexer.Token = TGreaterThan -// \\ -// \\case '/': -// \\// '/' or '//' or '/* ... */' -// \\lexer.step() -// \\switch lexer.codePoint { -// \\case '/': -// \\singleLineComment: -// \\for { -// \\lexer.step() -// \\switch lexer.codePoint { -// \\case '\r', '\n', '\u2028', '\u2029': -// \\break singleLineComment -// \\ -// \\case -1: // This indicates the end of the file -// \\break singleLineComment -// \\} -// \\} -// \\continue -// \\ -// \\case '*': -// \\lexer.step() -// \\startRange := lexer.Range() -// \\multiLineComment: -// \\for { -// \\switch lexer.codePoint { -// \\case '*': -// \\lexer.step() -// \\if lexer.codePoint == '/' { -// \\lexer.step() -// \\break multiLineComment -// \\} -// \\ -// \\case '\r', '\n', '\u2028', '\u2029': -// \\lexer.step() -// \\lexer.HasNewlineBefore = true -// \\ -// \\case -1: // This indicates the end of the file -// \\lexer.start = lexer.end -// \\lexer.addErrorWithNotes(lexer.Loc(), "Expected \"*/\" to terminate multi-line comment", -// \\[]logger.MsgData{logger.RangeData(&lexer.tracker, startRange, "The multi-line comment starts here")}) -// \\panic(LexerPanic{}) -// \\ -// \\default: -// \\lexer.step() -// \\} -// \\} -// \\continue -// \\ -// \\default: -// \\lexer.Token = TSlash -// \\} -// \\ -// \\case '\'', '"': -// \\var backslash logger.Range -// \\quote := lexer.codePoint -// \\needsDecode := false -// \\lexer.step() -// \\ -// \\stringLiteral: -// \\for { -// \\switch lexer.codePoint { -// \\case -1: // This indicates the end of the file -// \\lexer.SyntaxError() -// \\ -// \\case '&': -// \\needsDecode = true -// \\lexer.step() -// \\ -// \\case '\\': -// \\backslash = logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}, Len: 1} -// \\lexer.step() -// \\continue -// \\ -// \\case quote: -// \\if backslash.Len > 0 { -// \\backslash.Len++ -// \\lexer.PreviousBackslashQuoteInJSX = backslash -// \\} -// \\lexer.step() -// \\break stringLiteral -// \\ -// \\default: -// \\// Non-ASCII strings need the slow path -// \\if lexer.codePoint >= 0x80 { -// \\needsDecode = true -// \\} -// \\lexer.step() -// \\} -// \\backslash = logger.Range{} -// \\} -// \\ -// \\lexer.Token = TStringLiteral -// \\text := lexer.source.Contents[lexer.start+1 : lexer.end-1] -// \\ -// \\if needsDecode { -// \\// Slow path -// \\lexer.decodedStringLiteralOrNil = decodeJSXEntities([]uint16{}, text) -// \\} else { -// \\// Fast path -// \\n := len(text) -// \\copy := make([]uint16, n) -// \\for i := 0; i < n; i++ { -// \\copy[i] = uint16(text[i]) -// \\} -// \\lexer.decodedStringLiteralOrNil = copy -// \\} -// \\ -// \\default: -// \\// Check for unusual whitespace characters -// \\if IsWhitespace(lexer.codePoint) { -// \\lexer.step() -// \\continue -// \\} -// \\ -// \\if IsIdentifierStart(lexer.codePoint) { -// \\lexer.step() -// \\for IsIdentifierContinue(lexer.codePoint) || lexer.codePoint == '-' { -// \\lexer.step() -// \\} -// \\ -// \\// Parse JSX namespaces. These are not supported by React or TypeScript -// \\// but someone using JSX syntax in more obscure ways may find a use for -// \\// them. A namespaced name is just always turned into a string so you -// \\// can't use this feature to reference JavaScript identifiers. -// \\if lexer.codePoint == ':' { -// \\lexer.step() -// \\if IsIdentifierStart(lexer.codePoint) { -// \\lexer.step() -// \\for IsIdentifierContinue(lexer.codePoint) || lexer.codePoint == '-' { -// \\lexer.step() -// \\} -// \\} else { -// \\lexer.addError(logger.Loc{Start: lexer.Range().End()}, -// \\fmt.Sprintf("Expected identifier after %q in namespaced JSX name", lexer.Raw())) -// \\} -// \\} -// \\ -// \\lexer.Identifier = lexer.Raw() -// \\lexer.Token = TIdentifier -// \\break -// \\} -// \\ -// \\lexer.end = lexer.current -// \\lexer.Token = TSyntaxError -// \\} -// \\ -// \\return -// \\} -// \\} -// \\ -// \\func (lexer *Lexer) Next() { -// \\lexer.HasNewlineBefore = lexer.end == 0 -// \\lexer.HasPureCommentBefore = false -// \\lexer.PrevTokenWasAwaitKeyword = false -// \\lexer.CommentsToPreserveBefore = nil -// \\ -// \\for { -// \\lexer.start = lexer.end -// \\lexer.Token = 0 -// \\ -// \\switch lexer.codePoint { -// \\case -1: // This indicates the end of the file -// \\lexer.Token = TEndOfFile -// \\ -// \\case '#': -// \\if lexer.start == 0 && strings.HasPrefix(lexer.source.Contents, "#!") { -// \\// "#!/usr/bin/env node" -// \\lexer.Token = THashbang -// \\hashbang: -// \\for { -// \\lexer.step() -// \\switch lexer.codePoint { -// \\case '\r', '\n', '\u2028', '\u2029': -// \\break hashbang -// \\ -// \\case -1: // This indicates the end of the file -// \\break hashbang -// \\} -// \\} -// \\lexer.Identifier = lexer.Raw() -// \\} else { -// \\// "#foo" -// \\lexer.step() -// \\} -// ; - -// const repeat_count: usize = 1; -// const loop_count: usize = 1000; - -// pub fn main() anyerror!void { -// try HashTable.init(std.heap.c_allocator); -// Bitset.init(); -// { - -// // Ensure that the optimizer doesn't do something fancy with static memory addresses -// var code = try std.heap.c_allocator.dupe(u8, unicode_text); - -// var iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// var hash_table_count: usize = 0; -// var jump_table_count: usize = 0; -// var jump_table_elapsed: u64 = 0; -// var hash_table_elapsed: u64 = 0; -// var binary_search_elapsed: u64 = 0; -// var binary_search_count: usize = 0; -// var bitset_elapsed: u64 = 0; -// var bitset_count: usize = 0; - -// // change up the order these run in -// var loop_i: usize = 0; -// while (loop_i < loop_count) : (loop_i += 1) { -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// hash_table_count = 0; -// while (iter.nextCodepoint()) |cp| { -// hash_table_count += @as(usize, @intFromBool(HashTable.isIdentifierStart(cp) or HashTable.isIdentifierPart(cp))); -// } -// } -// hash_table_elapsed += timer.read(); -// } - -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// jump_table_count = 0; -// while (iter.nextCodepoint()) |cp| { -// jump_table_count += @as( -// usize, -// @intFromBool(JumpTable.isIdentifierStart(cp) or JumpTable.isIdentifierPart(cp)), -// ); -// } -// } -// jump_table_elapsed += timer.read(); -// } - -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// binary_search_count = 0; -// while (iter.nextCodepoint()) |cp| { -// binary_search_count += @as( -// usize, -// @intFromBool( -// BinarySearch.isIdentifierStart( -// cp, -// ) or BinarySearch.isIdentifierPart( -// cp, -// ), -// ), -// ); -// } -// } -// binary_search_elapsed += timer.read(); -// } - -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// bitset_count = 0; -// while (iter.nextCodepoint()) |cp| { -// bitset_count += @as( -// usize, -// @intFromBool( -// Bitset.isIdentifierStart( -// cp, -// ) or Bitset.isIdentifierPart( -// cp, -// ), -// ), -// ); -// } -// } -// bitset_elapsed += timer.read(); -// } -// } - -// .print( -// \\---- Unicode text ----- -// \\ -// \\Timings (sum of running {d} times each, lower is better): -// \\ -// \\ Binary Search : {d}ns -// \\ Hash Table : {d}ns -// \\ Switch statement : {d}ns -// \\ Bitset : {d}ns -// \\ -// \\Match count (these should be the same): -// \\ -// \\ Binary Search : {d} -// \\ Hash Table : {d} -// \\ Switch statement : {d} -// \\ Bitset : {d} -// \\ -// \\ -// , -// .{ -// repeat_count * loop_count, -// binary_search_elapsed, -// hash_table_elapsed, -// jump_table_elapsed, -// bitset_elapsed, - -// binary_search_count, -// hash_table_count, -// jump_table_count, -// bitset_count, -// }, -// ); -// } - -// { - -// // Ensure that the optimizer doesn't do something fancy with static memory addresses -// var code = try std.heap.c_allocator.dupe(u8, ascii_text); - -// var iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// var hash_table_count: usize = 0; -// var jump_table_count: usize = 0; -// var jump_table_elapsed: u64 = 0; -// var hash_table_elapsed: u64 = 0; -// var binary_search_elapsed: u64 = 0; -// var binary_search_count: usize = 0; -// var bitset_count: usize = 0; -// var bitset_elapsed: u64 = 0; - -// // change up the order these run in -// var loop_i: usize = 0; -// while (loop_i < loop_count) : (loop_i += 1) { -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// hash_table_count = 0; -// while (iter.nextCodepoint()) |cp| { -// hash_table_count += @as(usize, @intFromBool(HashTable.isIdentifierStart(cp) or HashTable.isIdentifierPart(cp))); -// } -// } -// hash_table_elapsed += timer.read(); -// } - -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// jump_table_count = 0; -// while (iter.nextCodepoint()) |cp| { -// jump_table_count += @as( -// usize, -// @intFromBool(JumpTable.isIdentifierStart(cp) or JumpTable.isIdentifierPart(cp)), -// ); -// } -// } -// jump_table_elapsed += timer.read(); -// } - -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// binary_search_count = 0; -// while (iter.nextCodepoint()) |cp| { -// binary_search_count += @as( -// usize, -// @intFromBool( -// BinarySearch.isIdentifierStart( -// cp, -// ) or BinarySearch.isIdentifierPart( -// cp, -// ), -// ), -// ); -// } -// } -// binary_search_elapsed += timer.read(); -// } - -// { -// var iteration_i: usize = 0; -// var timer = try std.time.Timer.start(); -// while (iteration_i < repeat_count) : (iteration_i += 1) { -// @setEvalBranchQuota(99999); -// iter = std.unicode.Utf8Iterator{ .bytes = code, .i = 0 }; -// bitset_count = 0; -// while (iter.nextCodepoint()) |cp| { -// bitset_count += @as( -// usize, -// @intFromBool( -// Bitset.isIdentifierStart( -// cp, -// ) or Bitset.isIdentifierPart( -// cp, -// ), -// ), -// ); -// } -// } -// bitset_elapsed += timer.read(); -// } -// } - -// { -// iter = std.unicode.Utf8Iterator{ .bytes = ascii_text, .i = 0 }; -// while (iter.nextCodepoint()) |cp| { -// if (cp > 127) std.debug.panic("This is not ASCII at {d}", .{iter.i}); -// } -// } - -// ( -// \\---- ASCII text ----- -// \\ -// \\Timings (sum of running {d} times each, lower is better): -// \\ -// \\ Binary Search : {d}ns -// \\ Hash Table : {d}ns -// \\ Switch statement : {d}ns -// \\ Bitset : {d}ns -// \\ -// \\Match count (these should be the same): -// \\ -// \\ Binary Search : {d} -// \\ Hash Table : {d} -// \\ Switch statement : {d} -// \\ Bitset : {d} -// \\ -// \\ -// , -// .{ -// repeat_count * loop_count, -// binary_search_elapsed, -// hash_table_elapsed, -// jump_table_elapsed, -// bitset_elapsed, - -// binary_search_count, -// hash_table_count, -// jump_table_count, -// bitset_count, -// }, -// ); -// } -// } +/// isIDContinueESNext checks if a codepoint is valid in the isIDContinueESNext category +pub fn isIDContinueESNext(cp: u21) bool { + const high = cp >> 8; + const low = cp & 0xFF; + const stage2_idx = idContinueESNext.stage1[high]; + const bit_pos = stage2_idx + low; + const u64_idx = bit_pos >> 6; + const bit_idx = @as(u6, @intCast(bit_pos & 63)); + return (idContinueESNext.stage2[u64_idx] & (@as(u64, 1) << bit_idx)) != 0; +} +const idContinueESNext = struct { + pub const stage1 = [_]u16{ 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 256, 4352, 4608, 4864, 256, 5120, 5376, 5632, 5888, 6144, 6400, 6656, 256, 256, 6912, 7168, 7424, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7936, 8192, 7680, 7680, 8448, 8704, 7680, 7680, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 8960, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 9216, 256, 9472, 9728, 9984, 10240, 10496, 10752, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 11008, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 256, 11264, 11520, 256, 11776, 12032, 12288, 12544, 12800, 13056, 13312, 13568, 13824, 256, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 15872, 16128, 16384, 16640, 16896, 17152, 17408, 17664, 17920, 18176, 18432, 18688, 18944, 7680, 19200, 19456, 19712, 19968, 256, 256, 256, 20224, 20480, 20736, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 20992, 256, 256, 256, 256, 21248, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 256, 256, 21504, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 256, 256, 21760, 22016, 7680, 7680, 22272, 22528, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 22784, 256, 256, 256, 256, 23040, 23296, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 23552, 256, 23808, 24064, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 24320, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 24576, 7680, 24832, 25088, 7680, 25344, 25600, 25856, 26112, 7680, 7680, 26368, 7680, 7680, 7680, 7680, 26624, 26880, 27136, 27392, 7680, 27648, 7680, 7680, 27904, 28160, 28416, 7680, 7680, 7680, 7680, 28672, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 28928, 7680, 7680, 7680, 7680, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 29184, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 29440, 29696, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 29952, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 30208, 256, 256, 30464, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 256, 256, 30720, 7680, 7680, 7680, 7680, 7680, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 30976, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 31232, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 31488, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680, 7680 }; + pub const stage2 = [_]u64{ 287948901175001088, 576460745995190270, 333270770471927808, 18410715276682199039, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 88094074470339, 18446744073709551615, 13609878073913638911, 18446744056529672128, 18428729675200069631, 18446744073709551615, 18446744073709551615, 18446744073709550843, 18446744073709551615, 18446462598732840959, 18446744069456527359, 13835058055282033151, 2119858418286774, 18446744069548736512, 18446678103011885055, 18446744073709551615, 11529212845433552895, 18446744073709486080, 18446744073709545471, 1125899906842623, 2612087783874887679, 70368744177663, 18446471390799331327, 18446744073692806911, 18446744056529682431, 18446744073709551615, 18446462392574410751, 17565725197581524975, 5765733215448889759, 15235112390417287150, 18014125208779143, 17576984196650090478, 18302910150157089727, 17576984196649951214, 844217444219295, 14123225865944680428, 281200107273671, 17582050746231021567, 281265183931871, 17577547146603651055, 4221915814182367, 18446744073709412351, 18158794964244397535, 3457638613854978030, 3658904103781503, 576460752303423486, 67076095, 4611685674830002134, 4093607775, 14024213633433600001, 18446216308128218879, 2305843009196916703, 64, 18446744073709551615, 18446744073709487103, 18446744070488326143, 17870283321406070975, 18446744073709551615, 18446744070446333439, 9168765891372858879, 18446744073701162813, 18446744073696837631, 1123704775901183, 18446744069414649855, 4557642822898941951, 18446744073709551614, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446638520593285119, 18446744069548802046, 144053615424700415, 9007197111451647, 3905461007941631, 18446744073709551615, 4394566287359, 18446744069481674752, 144115188075855871, 18446471394825863167, 18014398509481983, 1152657619668697087, 8796093022207936, 18446480190918885375, 134153215, 18446744069683019775, 11529215043920986111, 13834777130128311295, 32767, 18446744073709551615, 4494803601399807, 18446744073709551615, 4503599627370495, 72057594037927935, 4611686018427380735, 16717361816799216127, 576460752302833664, 18446744070475743231, 4611686017001275199, 6908521828386340863, 2295745090394464220, 9223372036854788096, 9223934986809245697, 536805376, 562821641207808, 17582049991377026180, 18446744069414601696, 511, 0, 0, 0, 0, 0, 18446744073709551615, 18446744073709551615, 18446744073709551615, 4494940973301759, 18446498607738650623, 9223513873854758911, 9187201948305063935, 18446744071553646463, 2251518330118602976, 18446744073709551614, 18446744069389418495, 17870283321406128127, 18446462598732840928, 18446744073709551615, 18446744069414617087, 18446462598732840960, 18446744073709551615, 18446744073709551615, 18446744073709551615, 0, 18446744073709551615, 18446744073709551615, 8191, 4611686018427322368, 17592185987071, 13830835930631503871, 18446744073709551615, 1125899906842623, 18446744060816261120, 18446744073709551615, 18446744073709550079, 18445618173868443647, 18691697672191, 4503599627370495, 18446744073709551615, 16789419406609285183, 18446532967477018623, 2305843004919775231, 18446744073709551615, 9223372032626884609, 36028797018963967, 18194542490348896255, 18446744073709551615, 35184368733388807, 18446602782178705022, 18446466996645134335, 18446744073709551615, 288010473826156543, 18446744073709551615, 18446744073709551615, 18446462667452317695, 1152921504606845055, 18446744073709551615, 18446532967477018623, 18446744073709551615, 67108863, 6881498031078244479, 18446744073709551579, 1125899906842623, 18446744073709027328, 4611686018427387903, 18446744073709486080, 18446744073709355007, 1152640029630136575, 7036870122864639, 18437455399478157312, 18446744073709551615, 2305843009213693951, 9799832780635308032, 18446743798965862398, 9223372036854775807, 486341884, 13258596753222922239, 1073692671, 18446744073709551615, 576460752303423487, 0, 9007199254740991, 0, 2305843009213693952, 0, 0, 18446744069951455231, 4295098367, 18446708893632430079, 576460752303359999, 18446744070488326143, 4128527, 18446744073709551615, 18446744073709551615, 18446466993558126591, 1152921504591118335, 18446463698244468735, 17870001915148894207, 2016486715970549759, 0, 36028797018963967, 1095220854783, 575897802350002111, 0, 10502394331027995967, 36028792728190975, 2147483647, 15762594400829440, 288230371860938751, 0, 13907115649320091647, 0, 9745789593611923567, 2305843004918726656, 536870911, 549755813631, 18014398509481983, 2251795522912255, 262143, 0, 18446744073709551615, 511, 2251799813685247, 2251799813685247, 287950000686628863, 0, 0, 0, 0, 0, 875211255709695, 16140901064495857664, 18446463149025525759, 18446462598732972031, 18446462598732841023, 36028792723996703, 18446744073709551615, 9241386160486350975, 576460752303423487, 287951100198191108, 18437736874454810623, 22517998136787184, 18446744073709551615, 402644511, 13907115649319829503, 3, 18446464796682337663, 287957697268023295, 18153444948953374703, 8760701963286943, 0, 0, 18446744073709551615, 16173172735, 18446744073709551615, 67043519, 0, 0, 18392700878181105663, 1056964609, 18446744073709551615, 67043345, 144115188075855871, 1023, 287966492958392319, 127, 0, 0, 576460752303423487, 0, 18446744069414584320, 9223376434901286911, 17996384110963061375, 67043343, 18446740770879700992, 120208752639, 9223372036854775807, 18446744073709486208, 18446462599336820735, 144115188075855871, 18410715276690587135, 18445618173869752321, 36027697507139583, 0, 13006395723845991295, 18446741595580465407, 4393784803327, 0, 0, 0, 0, 36028792723996672, 14411518807585456127, 67043335, 281474976710656, 0, 18446744073709551615, 18446744073709551615, 67108863, 0, 18446744073709551615, 140737488355327, 18446744073709551615, 18446744073709551615, 18446744073709551615, 15, 0, 0, 0, 0, 18446744073709486080, 562949953421311, 281474976710655, 4194303, 0, 0, 18446744073709551615, 127, 0, 0, 144115188075855871, 18446466994631868415, 9223372036854775807, 8796093022143487, 36028797018963967, 16212958624241090575, 65535, 0, 0, 18446744073709551615, 0, 0, 18446744073709551615, 18446744073709520895, 4294934783, 844540894248960, 18446744073709551615, 18446744073709551615, 18446744073709551615, 72057594037927935, 18446744073709551615, 18446744073709551615, 18446744073709551615, 4194303, 511, 0, 0, 0, 0, 0, 0, 8065665457643847680, 1125934266580991, 18446463629527547904, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 1152921504606846975, 18446744073709551615, 2305570330330005503, 1677656575, 0, 18446532967477018623, 127, 0, 0, 0, 17872504197455282176, 65970697670631, 0, 0, 28, 0, 0, 18446744073709551615, 18446744073707454463, 17005555242810474495, 18446744073709551599, 8935141660164089791, 18446744073709419615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446743249075830783, 17870283321271910397, 18437736874452713471, 18446603336221163519, 18446741874686295551, 18446744073709539319, 17906312118425092095, 9042383626829823, 281470547525648, 0, 8660801552383, 0, 0, 0, 18446471240106377087, 70368744177663, 32768, 0, 4611439727822766079, 17407, 0, 0, 0, 0, 140737488289792, 288230376151711743, 0, 0, 0, 288230376151646208, 0, 0, 0, 9223213153129594880, 18446744073709551615, 18446744073709551615, 18446744073709551615, 8323103, 18446744073709551615, 67047423, 0, 0, 790380184120328175, 6843210385291930244, 1152917029519358975, 0, 0, 0, 0, 287948901175001088, 18446744073709551615, 18446744073709551615, 18446744073709551615, 4294967295, 288230376151711743, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744070488326143, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446462615912710143, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446462607322775551, 18446744073709551615, 1073741823, 0, 0, 1073741823, 0, 0, 0, 18446744073709551615, 18446744073709488127, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 281474976710655, 0, 18446744073709551615, 18446744073709551615, 18446744073709551615, 281474976710655 }; +}; diff --git a/src/js_lexer/identifier_cache.zig b/src/js_lexer/identifier_cache.zig deleted file mode 100644 index de1ea0e771..0000000000 --- a/src/js_lexer/identifier_cache.zig +++ /dev/null @@ -1,22 +0,0 @@ -const std = @import("std"); -const bun = @import("root").bun; -const identifier_data = @import("./identifier_data.zig"); - -pub const CachedBitset = extern struct { - range: [2]i32, - len: u32, - - pub fn fromFile(comptime filename: anytype) CachedBitset { - return comptime @as(CachedBitset, @bitCast(bun.asByteSlice(@embedFile(filename)).ptr[0..@sizeOf(CachedBitset)].*)); - } -}; - -pub fn setMasks(masks: [*:0]const u8, comptime MaskType: type, masky: MaskType) void { - const FieldInfo: std.builtin.Type.StructField = std.meta.fieldInfo(MaskType, "masks"); - masky.masks = @as(masks, @bitCast(FieldInfo.type)); -} - -pub const id_start_meta = identifier_data.id_start_cached; -pub const id_continue_meta = identifier_data.id_continue_cached; -pub const id_start = identifier_data.id_start; -pub const id_continue = identifier_data.id_continue; diff --git a/src/js_lexer/identifier_data.zig b/src/js_lexer/identifier_data.zig deleted file mode 100644 index d82751e620..0000000000 --- a/src/js_lexer/identifier_data.zig +++ /dev/null @@ -1,177 +0,0 @@ -const std = @import("std"); - -const ZWJ = .{ 0x200C, 0x200D }; - -// "unicodeESNextIdentifierStart" -pub const start_codepoints = [_]i32{ 65, 90, 97, 122, 170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 895, 895, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1488, 1514, 1519, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2144, 2154, 2208, 2228, 2230, 2237, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2432, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2556, 2556, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2809, 2809, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3133, 3160, 3162, 3168, 3169, 3200, 3200, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3412, 3414, 3423, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6264, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6430, 6480, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7401, 7404, 7406, 7411, 7413, 7414, 7418, 7418, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42653, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43261, 43262, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43488, 43492, 43494, 43503, 43514, 43518, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43646, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66176, 66204, 66208, 66256, 66304, 66335, 66349, 66378, 66384, 66421, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68096, 68112, 68115, 68117, 68119, 68121, 68149, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68324, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68899, 69376, 69404, 69415, 69415, 69424, 69445, 69600, 69622, 69635, 69687, 69763, 69807, 69840, 69864, 69891, 69926, 69956, 69956, 69968, 70002, 70006, 70006, 70019, 70066, 70081, 70084, 70106, 70106, 70108, 70108, 70144, 70161, 70163, 70187, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70366, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70461, 70461, 70480, 70480, 70493, 70497, 70656, 70708, 70727, 70730, 70751, 70751, 70784, 70831, 70852, 70853, 70855, 70855, 71040, 71086, 71128, 71131, 71168, 71215, 71236, 71236, 71296, 71338, 71352, 71352, 71424, 71450, 71680, 71723, 71840, 71903, 71935, 71935, 72096, 72103, 72106, 72144, 72161, 72161, 72163, 72163, 72192, 72192, 72203, 72242, 72250, 72250, 72272, 72272, 72284, 72329, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72750, 72768, 72768, 72818, 72847, 72960, 72966, 72968, 72969, 72971, 73008, 73030, 73030, 73056, 73061, 73063, 73064, 73066, 73097, 73112, 73112, 73440, 73458, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92880, 92909, 92928, 92975, 92992, 92995, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94032, 94032, 94099, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 123136, 123180, 123191, 123197, 123214, 123214, 123584, 123627, 124928, 125124, 125184, 125251, 125259, 125259, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101 }; -// "unicodeESNextIdentifierPart" -pub const part_codepoints = ZWJ ++ [_]i32{ 48, 57, 65, 90, 95, 95, 97, 122, 170, 170, 181, 181, 183, 183, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, 887, 890, 893, 895, 895, 902, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1519, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2045, 2045, 2048, 2093, 2112, 2139, 2144, 2154, 2208, 2228, 2230, 2237, 2259, 2273, 2275, 2403, 2406, 2415, 2417, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2556, 2556, 2558, 2558, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2809, 2815, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3072, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3162, 3168, 3171, 3174, 3183, 3200, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3328, 3331, 3333, 3340, 3342, 3344, 3346, 3396, 3398, 3400, 3402, 3406, 3412, 3415, 3423, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3558, 3567, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4969, 4977, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6264, 6272, 6314, 6320, 6389, 6400, 6430, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6618, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6832, 6845, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7376, 7378, 7380, 7418, 7424, 7673, 7675, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42737, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43047, 43072, 43123, 43136, 43205, 43216, 43225, 43232, 43255, 43259, 43259, 43261, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43488, 43518, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65071, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66045, 66045, 66176, 66204, 66208, 66256, 66272, 66272, 66304, 66335, 66349, 66378, 66384, 66426, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66720, 66729, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68099, 68101, 68102, 68108, 68115, 68117, 68119, 68121, 68149, 68152, 68154, 68159, 68159, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68326, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68903, 68912, 68921, 69376, 69404, 69415, 69415, 69424, 69456, 69600, 69622, 69632, 69702, 69734, 69743, 69759, 69818, 69840, 69864, 69872, 69881, 69888, 69940, 69942, 69951, 69956, 69958, 69968, 70003, 70006, 70006, 70016, 70084, 70089, 70092, 70096, 70106, 70108, 70108, 70144, 70161, 70163, 70199, 70206, 70206, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70378, 70384, 70393, 70400, 70403, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70459, 70468, 70471, 70472, 70475, 70477, 70480, 70480, 70487, 70487, 70493, 70499, 70502, 70508, 70512, 70516, 70656, 70730, 70736, 70745, 70750, 70751, 70784, 70853, 70855, 70855, 70864, 70873, 71040, 71093, 71096, 71104, 71128, 71133, 71168, 71232, 71236, 71236, 71248, 71257, 71296, 71352, 71360, 71369, 71424, 71450, 71453, 71467, 71472, 71481, 71680, 71738, 71840, 71913, 71935, 71935, 72096, 72103, 72106, 72151, 72154, 72161, 72163, 72164, 72192, 72254, 72263, 72263, 72272, 72345, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72758, 72760, 72768, 72784, 72793, 72818, 72847, 72850, 72871, 72873, 72886, 72960, 72966, 72968, 72969, 72971, 73014, 73018, 73018, 73020, 73021, 73023, 73031, 73040, 73049, 73056, 73061, 73063, 73064, 73066, 73102, 73104, 73105, 73107, 73112, 73120, 73129, 73440, 73462, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92768, 92777, 92880, 92909, 92912, 92916, 92928, 92982, 92992, 92995, 93008, 93017, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94031, 94087, 94095, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 113821, 113822, 119141, 119145, 119149, 119154, 119163, 119170, 119173, 119179, 119210, 119213, 119362, 119364, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 120782, 120831, 121344, 121398, 121403, 121452, 121461, 121461, 121476, 121476, 121499, 121503, 121505, 121519, 122880, 122886, 122888, 122904, 122907, 122913, 122915, 122916, 122918, 122922, 123136, 123180, 123184, 123197, 123200, 123209, 123214, 123214, 123584, 123641, 124928, 125124, 125136, 125142, 125184, 125259, 125264, 125273, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101, 917760, 917999 }; - -const start_codepoints_including_ascii = [_]i32{ - 'a', - 'z', - 'A', - 'Z', - '_', - '_', - '$', - '$', -} ++ start_codepoints; -const part_codepoints_including_ascii = [_]i32{ - 'a', - 'z', - 'A', - 'Z', - '0', - '9', - '_', - '_', - '$', - '$', -} ++ part_codepoints; - -const id_start_range: [2]i32 = brk: { - var minmax = [2]i32{ std.math.maxInt(i32), 0 }; - - for (start_codepoints_including_ascii) |c| { - @setEvalBranchQuota(9999); - minmax[0] = if (c < minmax[0]) c else minmax[0]; - minmax[1] = if (c > minmax[1]) c else minmax[1]; - } - - break :brk minmax; -}; -const id_start_count = id_start_range[1] - id_start_range[0] + 1; - -const id_end_range: [2]i32 = brk: { - var minmax = [2]i32{ std.math.maxInt(i32), 0 }; - - for (part_codepoints_including_ascii) |c| { - @setEvalBranchQuota(9999); - minmax[0] = if (c < minmax[0]) c else minmax[0]; - minmax[1] = if (c > minmax[1]) c else minmax[1]; - } - - break :brk minmax; -}; - -const id_end_count = id_end_range[1] - id_end_range[0] + 1; - -pub const IDStartType = std.bit_set.StaticBitSet(id_start_count + 1); -pub const IDContinueType = std.bit_set.StaticBitSet(id_end_count + 1); - -pub const id_start: IDStartType = brk: { - var bits: IDStartType = IDStartType.initEmpty(); - var i: usize = 0; - - @setEvalBranchQuota(999999); - while (i < start_codepoints_including_ascii.len) : (i += 2) { - var min = start_codepoints_including_ascii[i]; - const max = start_codepoints_including_ascii[i + 1]; - while (min <= max) : (min += 1) { - @setEvalBranchQuota(999999); - bits.set(id_start_range[1] - min); - } - } - break :brk bits; -}; - -pub const id_continue: IDContinueType = brk: { - var bits: IDContinueType = IDContinueType.initEmpty(); - var i: usize = 0; - - while (i < part_codepoints_including_ascii.len) : (i += 2) { - var min = part_codepoints_including_ascii[i]; - const max = part_codepoints_including_ascii[i + 1]; - @setEvalBranchQuota(999999); - while (min <= max) : (min += 1) { - @setEvalBranchQuota(999999); - bits.set(id_end_range[1] - min); - } - } - break :brk bits; -}; - -const Cache = @import("./identifier_cache.zig"); - -pub const id_start_cached = Cache.CachedBitset{ .range = id_start_range, .len = id_start_count + 1 }; -pub const id_continue_cached = Cache.CachedBitset{ .range = id_end_range, .len = id_end_count + 1 }; - -fn main() anyerror!void { - const id_continue_data = std.mem.asBytes(&id_continue.masks); - const id_start_data = std.mem.asBytes(&id_start.masks); - - try std.posix.chdir(std.fs.path.dirname(@src().file).?); - var start = try std.fs.cwd().createFileZ("id_start_bitset.meta.blob", .{ .truncate = true }); - try start.writeAll(std.mem.asBytes(&id_start_cached)); - start.close(); - - var start_masks = try std.fs.cwd().createFileZ("id_start_bitset.blob", .{ .truncate = true }); - try start_masks.writeAll(id_start_data); - start_masks.close(); - - var continue_meta = try std.fs.cwd().createFileZ("id_continue_bitset.meta.blob", .{ .truncate = true }); - var continue_blob = try std.fs.cwd().createFileZ("id_continue_bitset.blob", .{ .truncate = true }); - - try continue_meta.writeAll(std.mem.asBytes(&id_continue_cached)); - continue_meta.close(); - try continue_blob.writeAll(std.mem.asBytes(id_continue_data)); - continue_blob.close(); -} - -test "Check" { - const id_start_cached_correct = Cache.CachedBitset{ .range = id_start_range, .len = id_start_count + 1 }; - const id_continue_cached_correct = Cache.CachedBitset{ .range = id_end_range, .len = id_end_count + 1 }; - try std.posix.chdir(std.fs.path.dirname(@src().file).?); - var start_cached = try std.fs.cwd().openFileZ("id_start_bitset.meta.blob", .{ .mode = .read_only }); - const start_cached_data = try start_cached.readToEndAlloc(std.heap.c_allocator, 4096); - - try std.testing.expectEqualSlices(u8, start_cached_data, std.mem.asBytes(&id_start_cached_correct)); - - var continue_cached = try std.fs.cwd().openFileZ("id_continue_bitset.meta.blob", .{ .mode = .read_only }); - const continue_cached_data = try continue_cached.readToEndAlloc(std.heap.c_allocator, 4096); - - try std.testing.expectEqualSlices(u8, continue_cached_data, std.mem.asBytes(&id_continue_cached_correct)); - - var start_blob_file = try std.fs.cwd().openFileZ("id_start_bitset.blob", .{ .mode = .read_only }); - const start_blob_data = try start_blob_file.readToEndAlloc(std.heap.c_allocator, try start_blob_file.getEndPos()); - var continue_blob_file = try std.fs.cwd().openFileZ("id_continue_bitset.blob", .{ .mode = .read_only }); - const continue_blob_data = try continue_blob_file.readToEndAlloc(std.heap.c_allocator, try continue_blob_file.getEndPos()); - - try std.testing.expectEqualSlices(u8, start_blob_data, std.mem.asBytes(&id_start.masks)); - try std.testing.expectEqualSlices(u8, continue_blob_data, std.mem.asBytes(&id_continue.masks)); -} - -test "Check #2" { - const id_start_cached_correct = Cache.CachedBitset{ .range = id_start_range, .len = id_start_count + 1 }; - const id_continue_cached_correct = Cache.CachedBitset{ .range = id_end_range, .len = id_end_count + 1 }; - try std.posix.chdir(std.fs.path.dirname(@src().file).?); - const start_cached_data = std.mem.asBytes(&Cache.id_start_meta); - - try std.testing.expectEqualSlices(u8, start_cached_data, std.mem.asBytes(&id_start_cached_correct)); - - const continue_cached_data = std.mem.asBytes(&Cache.id_continue_meta); - - try std.testing.expectEqualSlices(u8, continue_cached_data, std.mem.asBytes(&id_continue_cached_correct)); - - const start_blob_data = Cache.id_start_masks; - const continue_blob_data = Cache.id_continue_masks; - - try std.testing.expectEqualSlices(u8, start_blob_data, std.mem.asBytes(&id_start.masks)); - try std.testing.expectEqualSlices(u8, continue_blob_data, std.mem.asBytes(&id_continue.masks)); -} - -test "Check #3" { - try std.testing.expectEqualSlices(usize, &Cache.id_start.masks, &id_start.masks); - try std.testing.expectEqualSlices(usize, &Cache.id_continue.masks, &id_continue.masks); - - var i: i32 = id_end_range[0]; - while (i < id_end_range[1]) : (i += 1) { - try std.testing.expectEqual(id_continue.isSet(@as(usize, @intCast(id_end_range[1] - i))), Cache.id_continue.isSet(@as(usize, @intCast(id_end_range[1] - i)))); - } - - i = id_start_range[0]; - while (i < id_start_range[1]) : (i += 1) { - try std.testing.expectEqual(id_start.isSet(@as(usize, @intCast(id_start_range[1] - i))), Cache.id_start.isSet(@as(usize, @intCast(id_start_range[1] - i)))); - } -} diff --git a/src/js_parser.zig b/src/js_parser.zig index 812ff3580f..6de13bc2b2 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -3,6 +3,7 @@ /// ** you must also increment the `expected_version` in RuntimeTranspilerCache.zig ** /// ** IMPORTANT ** pub const std = @import("std"); +const bun = @import("root").bun; pub const logger = bun.logger; pub const js_lexer = bun.js_lexer; pub const importRecord = @import("./import_record.zig"); @@ -15,7 +16,6 @@ pub const RuntimeImports = _runtime.Runtime.Imports; pub const RuntimeFeatures = _runtime.Runtime.Features; pub const RuntimeNames = _runtime.Runtime.Names; pub const fs = @import("./fs.zig"); -const bun = @import("root").bun; const string = bun.string; const Output = bun.Output; const Global = bun.Global; @@ -187,18 +187,15 @@ const JSXFactoryName = "JSX"; const JSXAutomaticName = "jsx_module"; // kept as a static reference const exports_string_name: string = "exports"; -const MacroRefs = std.AutoArrayHashMap(Ref, u32); -pub const AllocatedNamesPool = ObjectPool( - std.ArrayList(string), - struct { - pub fn init(allocator: std.mem.Allocator) anyerror!std.ArrayList(string) { - return std.ArrayList(string).init(allocator); - } - }.init, - true, - 4, -); +const MacroRefData = struct { + import_record_id: u32, + // if name is null the macro is imported as a namespace import + // import * as macros from "./macros.js" with {type: "macro"}; + name: ?string = null, +}; + +const MacroRefs = std.AutoArrayHashMap(Ref, MacroRefData); const Substitution = union(enum) { success: Expr, @@ -1604,40 +1601,51 @@ pub const SideEffects = enum(u1) { pub fn simplifyBoolean(p: anytype, expr: Expr) Expr { if (!p.options.features.dead_code_elimination) return expr; - switch (expr.data) { - .e_unary => |e| { - if (e.op == .un_not) { - // "!!a" => "a" - if (e.value.data == .e_unary and e.value.data.e_unary.op == .un_not) { - return simplifyBoolean(p, e.value.data.e_unary.value); + + var result: Expr = expr; + _simplifyBoolean(p, &result); + return result; + } + + fn _simplifyBoolean(p: anytype, expr: *Expr) void { + while (true) { + switch (expr.data) { + .e_unary => |e| { + if (e.op == .un_not) { + // "!!a" => "a" + if (e.value.data == .e_unary and e.value.data.e_unary.op == .un_not) { + expr.* = e.value.data.e_unary.value; + continue; + } + + _simplifyBoolean(p, &e.value); } - - e.value = simplifyBoolean(p, e.value); - } - }, - .e_binary => |e| { - switch (e.op) { - .bin_logical_and => { - const effects = SideEffects.toBoolean(p, e.right.data); - if (effects.ok and effects.value and effects.side_effects == .no_side_effects) { - // "if (anything && truthyNoSideEffects)" => "if (anything)" - return e.left; - } - }, - .bin_logical_or => { - const effects = SideEffects.toBoolean(p, e.right.data); - if (effects.ok and !effects.value and effects.side_effects == .no_side_effects) { - // "if (anything || falsyNoSideEffects)" => "if (anything)" - return e.left; - } - }, - else => {}, - } - }, - else => {}, + }, + .e_binary => |e| { + switch (e.op) { + .bin_logical_and => { + const effects = SideEffects.toBoolean(p, e.right.data); + if (effects.ok and effects.value and effects.side_effects == .no_side_effects) { + // "if (anything && truthyNoSideEffects)" => "if (anything)" + expr.* = e.left; + continue; + } + }, + .bin_logical_or => { + const effects = SideEffects.toBoolean(p, e.right.data); + if (effects.ok and !effects.value and effects.side_effects == .no_side_effects) { + // "if (anything || falsyNoSideEffects)" => "if (anything)" + expr.* = e.left; + continue; + } + }, + else => {}, + } + }, + else => {}, + } + break; } - - return expr; } pub const toNumber = Expr.Data.toNumber; @@ -2266,10 +2274,18 @@ pub const SideEffects = enum(u1) { } pub fn toBoolean(p: anytype, exp: Expr.Data) Result { + // Only do this check once. if (!p.options.features.dead_code_elimination) { // value should not be read if ok is false, all existing calls to this function already adhere to this return Result{ .ok = false, .value = undefined, .side_effects = .could_have_side_effects }; } + + return toBooleanWithoutDCECheck(exp); + } + + // Avoid passing through *P + // This is a very recursive function. + fn toBooleanWithoutDCECheck(exp: Expr.Data) Result { switch (exp) { .e_null, .e_undefined => { return Result{ .ok = true, .value = false, .side_effects = .no_side_effects }; @@ -2303,10 +2319,9 @@ pub const SideEffects = enum(u1) { return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; }, .un_not => { - var result = toBoolean(p, e_.value.data); + const result = toBooleanWithoutDCECheck(e_.value.data); if (result.ok) { - result.value = !result.value; - return result; + return .{ .ok = true, .value = !result.value, .side_effects = result.side_effects }; } }, else => {}, @@ -2316,21 +2331,21 @@ pub const SideEffects = enum(u1) { switch (e_.op) { .bin_logical_or => { // "anything || truthy" is truthy - const result = toBoolean(p, e_.right.data); + const result = toBooleanWithoutDCECheck(e_.right.data); if (result.value and result.ok) { return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; } }, .bin_logical_and => { // "anything && falsy" is falsy - const result = toBoolean(p, e_.right.data); + const result = toBooleanWithoutDCECheck(e_.right.data); if (!result.value and result.ok) { return Result{ .ok = true, .value = false, .side_effects = .could_have_side_effects }; } }, .bin_comma => { // "anything, truthy/falsy" is truthy/falsy - var result = toBoolean(p, e_.right.data); + var result = toBooleanWithoutDCECheck(e_.right.data); if (result.ok) { result.side_effects = .could_have_side_effects; return result; @@ -2368,7 +2383,7 @@ pub const SideEffects = enum(u1) { } }, .e_inlined_enum => |inlined| { - return toBoolean(p, inlined.value.data); + return toBooleanWithoutDCECheck(inlined.value.data); }, else => {}, } @@ -2603,7 +2618,7 @@ const StmtList = ListManaged(Stmt); // This hash table is used every time we parse function args // Rather than allocating a new hash table each time, we can just reuse the previous allocation -const StringVoidMap = struct { +pub const StringVoidMap = struct { allocator: Allocator, map: bun.StringHashMapUnmanaged(void) = bun.StringHashMapUnmanaged(void){}, @@ -2972,7 +2987,14 @@ pub const Parser = struct { // Which makes sense. // June 4: "Parsing took: 18028000" // June 4: "Rest of this took: 8003000" - _ = try p.parseStmtsUpTo(js_lexer.T.t_end_of_file, &opts); + _ = p.parseStmtsUpTo(js_lexer.T.t_end_of_file, &opts) catch |err| { + if (err == error.StackOverflow) { + // The lexer location won't be totally accurate, but it's kind of helpful. + try p.log.addError(p.source, p.lexer.loc(), "Maximum call stack size exceeded"); + return; + } + return err; + }; // if (comptime ParserType.parser_features.typescript) { @@ -3053,7 +3075,11 @@ pub const Parser = struct { var stmts = try p.allocator.alloc(js_ast.Stmt, 1); stmts[0] = Stmt{ .data = .{ - .s_lazy_export = expr.data, + .s_lazy_export = brk: { + const data = try p.allocator.create(Expr.Data); + data.* = expr.data; + break :brk data; + }, }, .loc = expr.loc, }; @@ -3062,8 +3088,8 @@ pub const Parser = struct { .symbol_uses = p.symbol_uses, }; p.symbol_uses = .{}; - var parts = try p.allocator.alloc(js_ast.Part, 2); - parts[0..2].* = .{ ns_export_part, part }; + var parts = try ListManaged(js_ast.Part).initCapacity(p.allocator, 2); + parts.appendSliceAssumeCapacity(&.{ ns_export_part, part }); const exports_kind: js_ast.ExportsKind = brk: { if (expr.data == .e_undefined) { @@ -3072,7 +3098,7 @@ pub const Parser = struct { } break :brk .none; }; - return .{ .ast = try p.toAST(parts, exports_kind, .none, "") }; + return .{ .ast = try p.toAST(&parts, exports_kind, .none, "") }; } pub fn parse(self: *Parser) !js_ast.Result { @@ -3218,16 +3244,12 @@ pub const Parser = struct { } // Detect a leading "// @bun" pragma - if (p.lexer.bun_pragma != .none and p.options.features.dont_bundle_twice) { - return js_ast.Result{ - .already_bundled = switch (p.lexer.bun_pragma) { - .bun => .bun, - .bytecode => .bytecode, - .bytecode_cjs => .bytecode_cjs, - .bun_cjs => .bun_cjs, - else => unreachable, - }, - }; + if (self.options.features.dont_bundle_twice) { + if (self.hasBunPragma(hashbang.len > 0)) |pragma| { + return js_ast.Result{ + .already_bundled = pragma, + }; + } } // We must check the cache only after we've consumed the hashbang and leading // @bun pragma @@ -3251,7 +3273,18 @@ pub const Parser = struct { // Which makes sense. // June 4: "Parsing took: 18028000" // June 4: "Rest of this took: 8003000" - const stmts = try p.parseStmtsUpTo(js_lexer.T.t_end_of_file, &opts); + const stmts = p.parseStmtsUpTo(js_lexer.T.t_end_of_file, &opts) catch |err| { + parse_tracer.end(); + if (err == error.StackOverflow) { + // The lexer location won't be totally accurate, but it's kind of helpful. + try p.log.addError(p.source, p.lexer.loc(), "Maximum call stack size exceeded"); + + // Return a SyntaxError so that we reuse existing code for handling erorrs. + return error.SyntaxError; + } + + return err; + }; parse_tracer.end(); @@ -3952,9 +3985,10 @@ pub const Parser = struct { if (uses_any_import_statements) { exports_kind = .esm; - - // Otherwise, if they use CommonJS features its CommonJS - } else if (p.symbols.items[p.require_ref.innerIndex()].use_count_estimate > 0 or uses_dirname or uses_filename) { + } + // Otherwise, if they use CommonJS features its CommonJS. + // If you add a 'use strict'; at the top, you probably meant CommonJS because "use strict"; does nothing in ESM. + else if (p.symbols.items[p.require_ref.innerIndex()].use_count_estimate > 0 or uses_dirname or uses_filename or (!p.options.bundle and p.module_scope.strict_mode == .explicit_strict_mode)) { exports_kind = .cjs; } else { // If unknown, we default to ESM @@ -4207,41 +4241,21 @@ pub const Parser = struct { ); } - var parts_slice: []js_ast.Part = &([_]js_ast.Part{}); - if (before.items.len > 0 or after.items.len > 0) { - const before_len = before.items.len; - const after_len = after.items.len; + try parts.ensureUnusedCapacity(before.items.len + after.items.len); const parts_len = parts.items.len; + parts.items.len += before.items.len + after.items.len; - const _parts = try p.allocator.alloc( - js_ast.Part, - before_len + - after_len + - parts_len, - ); - - var remaining_parts = _parts; - if (before_len > 0) { - const parts_to_copy = before.items; - bun.copy(js_ast.Part, remaining_parts, parts_to_copy); - remaining_parts = remaining_parts[parts_to_copy.len..]; + if (before.items.len > 0) { + if (parts_len > 0) { + // first copy parts to the middle if before exists + bun.copy(js_ast.Part, parts.items[before.items.len..][0..parts_len], parts.items[0..parts_len]); + } + bun.copy(js_ast.Part, parts.items[0..before.items.len], before.items); } - - if (parts_len > 0) { - const parts_to_copy = parts.items; - bun.copy(js_ast.Part, remaining_parts, parts_to_copy); - remaining_parts = remaining_parts[parts_to_copy.len..]; + if (after.items.len > 0) { + bun.copy(js_ast.Part, parts.items[parts_len + before.items.len ..][0..after.items.len], after.items); } - - if (after_len > 0) { - const parts_to_copy = after.items; - bun.copy(js_ast.Part, remaining_parts, parts_to_copy); - } - - parts_slice = _parts; - } else { - parts_slice = parts.items; } // Pop the module scope to apply the "ContainsDirectEval" rules @@ -4260,7 +4274,7 @@ pub const Parser = struct { } } - return js_ast.Result{ .ast = try p.toAST(parts_slice, exports_kind, wrap_mode, hashbang) }; + return js_ast.Result{ .ast = try p.toAST(&parts, exports_kind, wrap_mode, hashbang) }; } pub fn init(_options: Options, log: *logger.Log, source: *const logger.Source, define: *Define, allocator: Allocator) !Parser { @@ -4273,6 +4287,64 @@ pub const Parser = struct { .log = log, }; } + + const PragmaState = packed struct { seen_cjs: bool = false, seen_bytecode: bool = false }; + + fn hasBunPragma(self: *const Parser, has_hashbang: bool) ?js_ast.Result.AlreadyBundled { + const BUN_PRAGMA = "// @bun"; + const contents = self.lexer.source.contents; + const end = contents.len; + + // pragmas may appear after a hashbang comment + // + // ```js + // #!/usr/bin/env bun + // // @bun + // const myCode = 1; + // ``` + var cursor: usize = 0; + if (has_hashbang) { + while (contents[cursor] != '\n') { + cursor += 1; + if (cursor >= end) return null; + } + + // eat the last newline + // NOTE: in windows, \n comes after \r so no extra work needs to be done + cursor += 1; + } + + if (!bun.strings.startsWith(contents[cursor..], BUN_PRAGMA)) return null; + cursor += BUN_PRAGMA.len; + + var state: PragmaState = .{}; + + while (cursor < self.lexer.end) : (cursor += 1) { + switch (contents[cursor]) { + '\n' => break, + '@' => { + cursor += 1; + if (cursor >= contents.len) break; + if (contents[cursor] != 'b') continue; + const slice = contents[cursor..]; + if (bun.strings.startsWith(slice, "bun-cjs")) { + state.seen_cjs = true; + cursor += "bun-cjs".len; + } else if (bun.strings.startsWith(slice, "bytecode")) { + state.seen_bytecode = true; + cursor += "bytecode".len; + } + }, + else => {}, + } + } + + if (state.seen_cjs) { + return if (state.seen_bytecode) .bytecode_cjs else .bun_cjs; + } else { + return if (state.seen_bytecode) .bytecode else .bun; + } + } }; const FindLabelSymbolResult = struct { ref: Ref, is_loop: bool, found: bool = false }; @@ -4769,6 +4841,8 @@ fn NewParser_( /// Used by commonjs_at_runtime has_commonjs_export_names: bool = false, + stack_check: bun.StackCheck, + /// When this flag is enabled, we attempt to fold all expressions that /// TypeScript would consider to be "constant expressions". This flag is /// enabled inside each enum body block since TypeScript requires numeric @@ -8782,12 +8856,12 @@ fn NewParser_( try p.lexer.next(); // The type following "extends" is not permitted to be another conditional type - var extends_type = if (get_metadata) TypeScript.Metadata.default else {}; + var extends_type = if (get_metadata) TypeScript.Metadata.default; try p.skipTypeScriptTypeWithOpts( .lowest, TypeScript.SkipTypeOptions.Bitset.initOne(.disallow_conditional_types), get_metadata, - if (get_metadata) &extends_type else {}, + if (get_metadata) &extends_type, ); if (comptime get_metadata) { @@ -8937,20 +9011,33 @@ fn NewParser_( const name = p.loadNameFromRef(name_loc.ref.?); const ref = try p.declareSymbol(.other, name_loc.loc, name); try p.is_import_item.put(p.allocator, ref, {}); - try p.macro.refs.put(ref, id); + try p.macro.refs.put(ref, .{ + .import_record_id = id, + .name = "default", + }); + } + + if (stmt.star_name_loc) |star| { + const name = p.loadNameFromRef(stmt.namespace_ref); + const ref = try p.declareSymbol(.other, star, name); + stmt.namespace_ref = ref; + try p.macro.refs.put(ref, .{ .import_record_id = id }); } for (stmt.items) |item| { const name = p.loadNameFromRef(item.name.ref.?); const ref = try p.declareSymbol(.other, item.name.loc, name); try p.is_import_item.put(p.allocator, ref, {}); - try p.macro.refs.put(ref, id); + try p.macro.refs.put(ref, .{ + .import_record_id = id, + .name = item.alias, + }); } return p.s(S.Empty{}, loc); } - const macro_remap = if ((comptime allow_macros) and !is_macro) + const macro_remap = if (comptime allow_macros) p.options.macro_context.getRemap(path.text) else null; @@ -8969,6 +9056,8 @@ fn NewParser_( .import_record_index = stmt.import_record_index, }) catch unreachable; } + + // TODO: not sure how to handle macro remappings for namespace imports } else { var path_name = fs.PathName.init(path.text); const name = try strings.append(p.allocator, "import_", try path_name.nonUniqueNameString(p.allocator)); @@ -9009,7 +9098,10 @@ fn NewParser_( if (macro_remap) |*remap| { if (remap.get("default")) |remapped_path| { const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path); - try p.macro.refs.put(ref, new_import_id); + try p.macro.refs.put(ref, .{ + .import_record_id = new_import_id, + .name = "default", + }); p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace; p.import_records.items[new_import_id].is_unused = true; @@ -9030,12 +9122,6 @@ fn NewParser_( }) catch unreachable; } - if (is_macro) { - try p.macro.refs.put(ref, stmt.import_record_index); - stmt.default_name = null; - break :outer; - } - if (comptime ParsePassSymbolUsageType != void) { p.parse_pass_symbol_uses.put(name, .{ .ref = ref, @@ -9062,7 +9148,7 @@ fn NewParser_( if (symbol.namespace_alias == null) { symbol.namespace_alias = .{ .namespace_ref = stmt.namespace_ref, - .alias = name, + .alias = item.alias, .import_record_index = stmt.import_record_index, }; } @@ -9071,7 +9157,10 @@ fn NewParser_( if (macro_remap) |*remap| { if (remap.get(item.alias)) |remapped_path| { const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path); - try p.macro.refs.put(ref, new_import_id); + try p.macro.refs.put(ref, .{ + .import_record_id = new_import_id, + .name = item.alias, + }); p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace; p.import_records.items[new_import_id].is_unused = true; @@ -9437,6 +9526,10 @@ fn NewParser_( } fn parseStmt(p: *P, opts: *ParseStatementOptions) anyerror!Stmt { + if (!p.stack_check.isSafeToRecurse()) { + try bun.throwStackOverflow(); + } + const loc = p.lexer.loc(); switch (p.lexer.token) { @@ -9946,28 +10039,60 @@ fn NewParser_( return p.s(S.Local{ .kind = .k_const, .decls = Decl.List.fromList(decls), .is_export = opts.is_export }, loc); }, .t_if => { - try p.lexer.next(); - try p.lexer.expect(.t_open_paren); - const test_ = try p.parseExpr(.lowest); - try p.lexer.expect(.t_close_paren); - var stmtOpts = ParseStatementOptions{ - .lexical_decl = .allow_fn_inside_if, - }; - const yes = try p.parseStmt(&stmtOpts); - var no: ?Stmt = null; - if (p.lexer.token == .t_else) { + var current_loc = loc; + var root_if: ?Stmt = null; + var current_if: ?*S.If = null; + + while (true) { try p.lexer.next(); - stmtOpts = ParseStatementOptions{ + try p.lexer.expect(.t_open_paren); + const test_ = try p.parseExpr(.lowest); + try p.lexer.expect(.t_close_paren); + var stmtOpts = ParseStatementOptions{ .lexical_decl = .allow_fn_inside_if, }; - no = try p.parseStmt(&stmtOpts); + const yes = try p.parseStmt(&stmtOpts); + + // Create the if node + const if_stmt = p.s(S.If{ + .test_ = test_, + .yes = yes, + .no = null, + }, current_loc); + + // First if statement becomes root + if (root_if == null) { + root_if = if_stmt; + } + + // Link to previous if statement's else branch + if (current_if) |prev_if| { + prev_if.no = if_stmt; + } + + // Set current if for next iteration + current_if = if_stmt.data.s_if; + + if (p.lexer.token != .t_else) { + return root_if.?; + } + + try p.lexer.next(); + + // Handle final else + if (p.lexer.token != .t_if) { + stmtOpts = ParseStatementOptions{ + .lexical_decl = .allow_fn_inside_if, + }; + current_if.?.no = try p.parseStmt(&stmtOpts); + return root_if.?; + } + + // Continue with else if + current_loc = p.lexer.loc(); } - return p.s(S.If{ - .test_ = test_, - .yes = yes, - .no = no, - }, loc); + unreachable; }, .t_do => { try p.lexer.next(); @@ -10087,7 +10212,6 @@ fn NewParser_( var binding: ?js_ast.Binding = null; // The catch binding is optional, and can be omitted - // jarred: TIL! if (p.lexer.token != .t_open_brace) { try p.lexer.expect(.t_open_paren); var value = try p.parseBinding(.{}); @@ -11135,8 +11259,7 @@ fn NewParser_( var is_single_line = !p.lexer.has_newline_before; // this variable should not exist if we're not in a typescript file var had_type_only_imports = if (comptime is_typescript_enabled) - false - else {}; + false; while (p.lexer.token != .t_close_brace) { // The alias may be a keyword; @@ -11793,6 +11916,8 @@ fn NewParser_( needs_symbol = true; } else { try p.lexer.expect(.t_identifier); + // error early, name is still `undefined` + return error.SyntaxError; } try p.lexer.next(); @@ -12977,7 +13102,11 @@ fn NewParser_( return try p.parseExprCommon(level, null, flags); } - pub fn parseExprCommon(p: *P, level: Level, errors: ?*DeferredErrors, flags: Expr.EFlags) anyerror!Expr { + fn parseExprCommon(p: *P, level: Level, errors: ?*DeferredErrors, flags: Expr.EFlags) anyerror!Expr { + if (!p.stack_check.isSafeToRecurse()) { + try bun.throwStackOverflow(); + } + const had_pure_comment_before = p.lexer.has_pure_comment_before and !p.options.ignore_dce_annotations; var expr = try p.parsePrefix(level, errors, flags); @@ -14630,48 +14759,6 @@ fn NewParser_( } } - pub const MacroVisitor = struct { - p: *P, - - loc: logger.Loc, - - pub fn visitImport(this: MacroVisitor, import_data: js_ast.Macro.JSNode.ImportData) void { - var p = this.p; - - const record_id = p.addImportRecord(.stmt, this.loc, import_data.path); - var record: *ImportRecord = &p.import_records.items[record_id]; - record.was_injected_by_macro = true; - p.macro.imports.ensureUnusedCapacity(import_data.import.items.len) catch unreachable; - var import = import_data.import; - import.import_record_index = record_id; - - p.is_import_item.ensureUnusedCapacity( - p.allocator, - @as(u32, @intCast(p.is_import_item.count() + import.items.len)), - ) catch unreachable; - - for (import.items) |*clause| { - const import_hash_name = clause.original_name; - - if (strings.eqlComptime(clause.alias, "default")) { - const non_unique_name = record.path.name.nonUniqueNameString(p.allocator) catch unreachable; - clause.original_name = std.fmt.allocPrint(p.allocator, "{s}_default", .{non_unique_name}) catch unreachable; - record.contains_default_alias = true; - } - const name_ref = p.declareSymbol(.import, this.loc, clause.original_name) catch unreachable; - clause.name = LocRef{ .loc = this.loc, .ref = name_ref }; - - p.is_import_item.putAssumeCapacity(name_ref, {}); - - p.macro.imports.putAssumeCapacity(js_ast.Macro.JSNode.SymbolMap.generateImportHash(import_hash_name, import_data.path), name_ref); - - // Ensure we don't accidentally think this is an export from - } - - p.macro.prepend_stmts.append(p.s(import, this.loc)) catch unreachable; - } - }; - pub fn panic(p: *P, comptime fmt: string, args: anytype) noreturn { p.panicLoc(fmt, args, null); @setCold(true); @@ -15861,15 +15948,19 @@ fn NewParser_( fn bindingCanBeRemovedIfUnused(p: *P, binding: Binding) bool { if (!p.options.features.dead_code_elimination) return false; + return bindingCanBeRemovedIfUnusedWithoutDCECheck(p, binding); + } + + fn bindingCanBeRemovedIfUnusedWithoutDCECheck(p: *P, binding: Binding) bool { switch (binding.data) { .b_array => |bi| { for (bi.items) |*item| { - if (!p.bindingCanBeRemovedIfUnused(item.binding)) { + if (!p.bindingCanBeRemovedIfUnusedWithoutDCECheck(item.binding)) { return false; } if (item.default_value) |*default| { - if (!p.exprCanBeRemovedIfUnused(default)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(default)) { return false; } } @@ -15877,16 +15968,16 @@ fn NewParser_( }, .b_object => |bi| { for (bi.properties) |*property| { - if (!property.flags.contains(.is_spread) and !p.exprCanBeRemovedIfUnused(&property.key)) { + if (!property.flags.contains(.is_spread) and !p.exprCanBeRemovedIfUnusedWithoutDCECheck(&property.key)) { return false; } - if (!p.bindingCanBeRemovedIfUnused(property.value)) { + if (!p.bindingCanBeRemovedIfUnusedWithoutDCECheck(property.value)) { return false; } if (property.default_value) |*default| { - if (!p.exprCanBeRemovedIfUnused(default)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(default)) { return false; } } @@ -15900,6 +15991,10 @@ fn NewParser_( fn stmtsCanBeRemovedIfUnused(p: *P, stmts: []Stmt) bool { if (!p.options.features.dead_code_elimination) return false; + return stmtsCanBeRemovedifUnusedWithoutDCECheck(p, stmts); + } + + fn stmtsCanBeRemovedifUnusedWithoutDCECheck(p: *P, stmts: []Stmt) bool { for (stmts) |stmt| { switch (stmt.data) { // These never have side effects @@ -15924,7 +16019,7 @@ fn NewParser_( continue; } - if (!p.exprCanBeRemovedIfUnused(&st.value)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&st.value)) { return false; } }, @@ -15934,12 +16029,12 @@ fn NewParser_( if (st.kind == .k_await_using) return false; for (st.decls.slice()) |*decl| { - if (!p.bindingCanBeRemovedIfUnused(decl.binding)) { + if (!p.bindingCanBeRemovedIfUnusedWithoutDCECheck(decl.binding)) { return false; } if (decl.value) |*decl_value| { - if (!p.exprCanBeRemovedIfUnused(decl_value)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(decl_value)) { return false; } else if (st.kind == .k_using) { // "using" declarations are only side-effect free if they are initialized to null or undefined @@ -15952,7 +16047,7 @@ fn NewParser_( }, .s_try => |try_| { - if (!p.stmtsCanBeRemovedIfUnused(try_.body) or (try_.finally != null and !p.stmtsCanBeRemovedIfUnused(try_.finally.?.stmts))) { + if (!p.stmtsCanBeRemovedifUnusedWithoutDCECheck(try_.body) or (try_.finally != null and !p.stmtsCanBeRemovedifUnusedWithoutDCECheck(try_.finally.?.stmts))) { return false; } }, @@ -15965,7 +16060,7 @@ fn NewParser_( .stmt => |s2| { switch (s2.data) { .s_expr => |s_expr| { - if (!p.exprCanBeRemovedIfUnused(&s_expr.value)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&s_expr.value)) { return false; } }, @@ -15984,7 +16079,7 @@ fn NewParser_( } }, .expr => |*exp| { - if (!p.exprCanBeRemovedIfUnused(exp)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(exp)) { return false; } }, @@ -16034,7 +16129,7 @@ fn NewParser_( } // public for JSNode.JSXWriter usage - pub fn visitExpr(p: *P, expr: Expr) Expr { + pub inline fn visitExpr(p: *P, expr: Expr) Expr { if (only_scan_imports_and_do_not_visit) { @compileError("only_scan_imports_and_do_not_visit must not run this."); } @@ -16499,12 +16594,15 @@ fn NewParser_( e_.tag = p.visitExpr(tag); if (comptime allow_macros) { - if (e_.tag.?.data == .e_import_identifier and !p.options.features.is_macro_runtime) { - const ref = e_.tag.?.data.e_import_identifier.ref; + const ref = switch (e_.tag.?.data) { + .e_import_identifier => |ident| ident.ref, + .e_dot => |dot| if (dot.target.data == .e_identifier) dot.target.data.e_identifier.ref else null, + else => null, + }; - if (p.macro.refs.get(ref)) |import_record_id| { - const name = p.symbols.items[ref.innerIndex()].original_name; - p.ignoreUsage(ref); + if (ref != null and !p.options.features.is_macro_runtime) { + if (p.macro.refs.get(ref.?)) |macro_ref_data| { + p.ignoreUsage(ref.?); if (p.is_control_flow_dead) { return p.newExpr(E.Undefined{}, e_.tag.?.loc); } @@ -16521,7 +16619,8 @@ fn NewParser_( } p.macro_call_count += 1; - const record = &p.import_records.items[import_record_id]; + const name = macro_ref_data.name orelse e_.tag.?.data.e_dot.name; + const record = &p.import_records.items[macro_ref_data.import_record_id]; // We must visit it to convert inline_identifiers and record usage const macro_result = (p.options.macro_context.call( record.path.text, @@ -17296,10 +17395,18 @@ fn NewParser_( else => {}, } - const is_macro_ref: bool = if (comptime FeatureFlags.is_macro_enabled) - e_.target.data == .e_import_identifier and p.macro.refs.contains(e_.target.data.e_import_identifier.ref) - else - false; + const is_macro_ref: bool = if (comptime allow_macros) brk: { + const possible_macro_ref = switch (e_.target.data) { + .e_import_identifier => |ident| ident.ref, + .e_dot => |dot| if (dot.target.data == .e_identifier) + dot.target.data.e_identifier.ref + else + null, + else => null, + }; + + break :brk possible_macro_ref != null and p.macro.refs.contains(possible_macro_ref.?); + } else false; { const old_ce = p.options.ignore_dce_annotations; @@ -17425,8 +17532,13 @@ fn NewParser_( if (comptime allow_macros) { if (is_macro_ref and !p.options.features.is_macro_runtime) { - const ref = e_.target.data.e_import_identifier.ref; - const import_record_id = p.macro.refs.get(ref).?; + const ref = switch (e_.target.data) { + .e_import_identifier => |ident| ident.ref, + .e_dot => |dot| dot.target.data.e_identifier.ref, + else => unreachable, + }; + + const macro_ref_data = p.macro.refs.get(ref).?; p.ignoreUsage(ref); if (p.is_control_flow_dead) { return p.newExpr(E.Undefined{}, e_.target.loc); @@ -17442,8 +17554,8 @@ fn NewParser_( return p.newExpr(E.Undefined{}, expr.loc); } - const name = p.symbols.items[ref.innerIndex()].original_name; - const record = &p.import_records.items[import_record_id]; + const name = macro_ref_data.name orelse e_.target.data.e_dot.name; + const record = &p.import_records.items[macro_ref_data.import_record_id]; const copied = Expr{ .loc = expr.loc, .data = .{ .e_call = e_ } }; const start_error_count = p.log.msgs.items.len; p.macro_call_count += 1; @@ -17778,34 +17890,34 @@ fn NewParser_( return true; } + // This one is never called in places that haven't already checked if DCE is enabled. pub fn classCanBeRemovedIfUnused(p: *P, class: *G.Class) bool { - if (!p.options.features.dead_code_elimination) return false; if (class.extends) |*extends| { - if (!p.exprCanBeRemovedIfUnused(extends)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(extends)) { return false; } } for (class.properties) |*property| { if (property.kind == .class_static_block) { - if (!p.stmtsCanBeRemovedIfUnused(property.class_static_block.?.stmts.slice())) { + if (!p.stmtsCanBeRemovedifUnusedWithoutDCECheck(property.class_static_block.?.stmts.slice())) { return false; } continue; } - if (!p.exprCanBeRemovedIfUnused(&(property.key orelse unreachable))) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&(property.key orelse unreachable))) { return false; } if (property.value) |*val| { - if (!p.exprCanBeRemovedIfUnused(val)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(val)) { return false; } } if (property.initializer) |*val| { - if (!p.exprCanBeRemovedIfUnused(val)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(val)) { return false; } } @@ -17819,6 +17931,11 @@ fn NewParser_( // This is to improve the reliability of fast refresh between page loads. pub fn exprCanBeRemovedIfUnused(p: *P, expr: *const Expr) bool { if (!p.options.features.dead_code_elimination) return false; + + return exprCanBeRemovedIfUnusedWithoutDCECheck(p, expr); + } + + fn exprCanBeRemovedIfUnusedWithoutDCECheck(p: *P, expr: *const Expr) bool { switch (expr.data) { .e_null, .e_undefined, @@ -17836,7 +17953,7 @@ fn NewParser_( return true; }, - .e_inlined_enum => |e| return p.exprCanBeRemovedIfUnused(&e.value), + .e_inlined_enum => |e| return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&e.value), .e_dot => |ex| { return ex.can_be_removed_if_unused; @@ -17895,24 +18012,24 @@ fn NewParser_( return true; }, .e_if => |ex| { - return p.exprCanBeRemovedIfUnused(&ex.test_) and + return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.test_) and (p.isSideEffectFreeUnboundIdentifierRef( ex.yes, ex.test_, true, ) or - p.exprCanBeRemovedIfUnused(&ex.yes)) and + p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.yes)) and (p.isSideEffectFreeUnboundIdentifierRef( ex.no, ex.test_, false, - ) or p.exprCanBeRemovedIfUnused( + ) or p.exprCanBeRemovedIfUnusedWithoutDCECheck( &ex.no, )); }, .e_array => |ex| { for (ex.items.slice()) |*item| { - if (!p.exprCanBeRemovedIfUnused(item)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(item)) { return false; } } @@ -17928,7 +18045,7 @@ fn NewParser_( } if (property.value) |*val| { - if (!p.exprCanBeRemovedIfUnused(val)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(val)) { return false; } } @@ -17940,7 +18057,7 @@ fn NewParser_( // can be removed. The annotation causes us to ignore the target. if (ex.can_be_unwrapped_if_unused) { for (ex.args.slice()) |*arg| { - if (!p.exprCanBeRemovedIfUnused(arg)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(arg)) { return false; } } @@ -17953,7 +18070,7 @@ fn NewParser_( // can be removed. The annotation causes us to ignore the target. if (ex.can_be_unwrapped_if_unused) { for (ex.args.slice()) |*arg| { - if (!p.exprCanBeRemovedIfUnused(arg)) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(arg)) { return false; } } @@ -17966,7 +18083,7 @@ fn NewParser_( // These operators must not have any type conversions that can execute code // such as "toString" or "valueOf". They must also never throw any exceptions. .un_void, .un_not => { - return p.exprCanBeRemovedIfUnused(&ex.value); + return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.value); }, // The "typeof" operator doesn't do any type conversions so it can be removed @@ -17984,7 +18101,7 @@ fn NewParser_( return true; } - return p.exprCanBeRemovedIfUnused(&ex.value); + return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.value); }, else => {}, @@ -17998,15 +18115,15 @@ fn NewParser_( .bin_strict_ne, .bin_comma, .bin_nullish_coalescing, - => return p.exprCanBeRemovedIfUnused(&ex.left) and p.exprCanBeRemovedIfUnused(&ex.right), + => return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right), // Special-case "||" to make sure "typeof x === 'undefined' || x" can be removed - .bin_logical_or => return p.exprCanBeRemovedIfUnused(&ex.left) and - (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, false) or p.exprCanBeRemovedIfUnused(&ex.right)), + .bin_logical_or => return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and + (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, false) or p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right)), // Special-case "&&" to make sure "typeof x !== 'undefined' && x" can be removed - .bin_logical_and => return p.exprCanBeRemovedIfUnused(&ex.left) and - (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, true) or p.exprCanBeRemovedIfUnused(&ex.right)), + .bin_logical_and => return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and + (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, true) or p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right)), // For "==" and "!=", pretend the operator was actually "===" or "!==". If // we know that we can convert it to "==" or "!=", then we can consider the @@ -18018,14 +18135,14 @@ fn NewParser_( ex.left.data, ex.right.data, ) and - p.exprCanBeRemovedIfUnused(&ex.left) and p.exprCanBeRemovedIfUnused(&ex.right), + p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right), else => {}, } }, .e_template => |templ| { if (templ.tag == null) { for (templ.parts) |part| { - if (!p.exprCanBeRemovedIfUnused(&part.value) or part.value.knownPrimitive() == .unknown) { + if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&part.value) or part.value.knownPrimitive() == .unknown) { return false; } } @@ -18039,7 +18156,7 @@ fn NewParser_( return false; } - // // This is based on exprCanBeRemovedIfUnused. + // // This is based on exprCanBeRemoved // // The main difference: identifiers, functions, arrow functions cause it to return false // pub fn exprCanBeHoistedForJSX(p: *P, expr: *const Expr) bool { // if (comptime jsx_transform_type != .react) { @@ -18745,22 +18862,20 @@ fn NewParser_( } }, .e_import_meta => { - // Make `import.meta.url` side effect free. - if (strings.eqlComptime(name, "url")) { - return p.newExpr( - E.Dot{ - .target = target, - .name = name, - .name_loc = name_loc, - .can_be_removed_if_unused = true, - }, - target.loc, - ); - } - if (strings.eqlComptime(name, "main")) { return p.valueForImportMetaMain(false, target.loc); } + + // Make all property accesses on `import.meta.url` side effect free. + return p.newExpr( + E.Dot{ + .target = target, + .name = name, + .name_loc = name_loc, + .can_be_removed_if_unused = true, + }, + target.loc, + ); }, .e_require_call_target => { if (strings.eqlComptime(name, "main")) { @@ -18951,809 +19066,985 @@ fn NewParser_( const was_after_after_const_local_prefix = p.current_scope.is_after_const_local_prefix; p.current_scope.is_after_const_local_prefix = true; - switch (stmt.data) { - // These don't contain anything to traverse + switch (@as(Stmt.Tag, stmt.data)) { + .s_directive, .s_comment, .s_empty => { + p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; + try stmts.append(stmt.*); + }, + .s_type_script => { + p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; + return; + }, .s_debugger => { p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; if (p.define.drop_debugger) { return; } + try stmts.append(stmt.*); }, - .s_empty, .s_comment => { - p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; - }, - .s_type_script => { - p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; - // Erase TypeScript constructs from the output completely + + inline .s_enum, .s_local => |tag| return @field(visitors, @tagName(tag))(p, stmts, stmt, @field(stmt.data, @tagName(tag)), was_after_after_const_local_prefix), + inline else => |tag| return @field(visitors, @tagName(tag))(p, stmts, stmt, @field(stmt.data, @tagName(tag))), + + // Only used by the bundler for lazy export ASTs. + .s_lazy_export => unreachable, + } + } + + const visitors = struct { + pub fn s_import(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Import) !void { + try p.recordDeclaredSymbol(data.namespace_ref); + + if (data.default_name) |default_name| { + try p.recordDeclaredSymbol(default_name.ref.?); + } + + if (data.items.len > 0) { + for (data.items) |*item| { + try p.recordDeclaredSymbol(item.name.ref.?); + } + } + + try stmts.append(stmt.*); + } + pub fn s_export_clause(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.ExportClause) !void { + // "export {foo}" + var end: usize = 0; + var any_replaced = false; + if (p.options.features.replace_exports.count() > 0) { + for (data.items) |*item| { + const name = p.loadNameFromRef(item.name.ref.?); + + const symbol = try p.findSymbol(item.alias_loc, name); + const ref = symbol.ref; + + if (p.options.features.replace_exports.getPtr(name)) |entry| { + if (entry.* != .replace) p.ignoreUsage(symbol.ref); + _ = p.injectReplacementExport(stmts, symbol.ref, stmt.loc, entry); + any_replaced = true; + continue; + } + + if (p.symbols.items[ref.innerIndex()].kind == .unbound) { + // Silently strip exports of non-local symbols in TypeScript, since + // those likely correspond to type-only exports. But report exports of + // non-local symbols as errors in JavaScript. + if (!is_typescript_enabled) { + const r = js_lexer.rangeOfIdentifier(p.source, item.name.loc); + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); + } + continue; + } + + item.name.ref = ref; + data.items[end] = item.*; + end += 1; + } + } else { + for (data.items) |*item| { + const name = p.loadNameFromRef(item.name.ref.?); + const symbol = try p.findSymbol(item.alias_loc, name); + const ref = symbol.ref; + + if (p.symbols.items[ref.innerIndex()].kind == .unbound) { + // Silently strip exports of non-local symbols in TypeScript, since + // those likely correspond to type-only exports. But report exports of + // non-local symbols as errors in JavaScript. + if (!is_typescript_enabled) { + const r = js_lexer.rangeOfIdentifier(p.source, item.name.loc); + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); + continue; + } + continue; + } + + item.name.ref = ref; + data.items[end] = item.*; + end += 1; + } + } + + const remove_for_tree_shaking = any_replaced and end == 0 and data.items.len > 0 and p.options.tree_shaking; + data.items.len = end; + + if (remove_for_tree_shaking) { return; - }, - .s_directive => { - p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; - }, - .s_import => |data| { - try p.recordDeclaredSymbol(data.namespace_ref); + } - if (data.default_name) |default_name| { - try p.recordDeclaredSymbol(default_name.ref.?); - } + try stmts.append(stmt.*); + } + pub fn s_export_from(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.ExportFrom) !void { - if (data.items.len > 0) { - for (data.items) |*item| { - try p.recordDeclaredSymbol(item.name.ref.?); - } - } - }, - .s_export_clause => |data| { - // "export {foo}" - var end: usize = 0; - var any_replaced = false; - if (p.options.features.replace_exports.count() > 0) { - for (data.items) |*item| { - const name = p.loadNameFromRef(item.name.ref.?); + // "export {foo} from 'path'" + const name = p.loadNameFromRef(data.namespace_ref); - const symbol = try p.findSymbol(item.alias_loc, name); - const ref = symbol.ref; + data.namespace_ref = try p.newSymbol(.other, name); + try p.current_scope.generated.push(p.allocator, data.namespace_ref); + try p.recordDeclaredSymbol(data.namespace_ref); + + if (p.options.features.replace_exports.count() > 0) { + var j: usize = 0; + // This is a re-export and the symbols created here are used to reference + for (data.items) |item| { + const old_ref = item.name.ref.?; + + if (p.options.features.replace_exports.count() > 0) { + if (p.options.features.replace_exports.getPtr(item.alias)) |entry| { + _ = p.injectReplacementExport(stmts, old_ref, logger.Loc.Empty, entry); - if (p.options.features.replace_exports.getPtr(name)) |entry| { - if (entry.* != .replace) p.ignoreUsage(symbol.ref); - _ = p.injectReplacementExport(stmts, symbol.ref, stmt.loc, entry); - any_replaced = true; continue; } - - if (p.symbols.items[ref.innerIndex()].kind == .unbound) { - // Silently strip exports of non-local symbols in TypeScript, since - // those likely correspond to type-only exports. But report exports of - // non-local symbols as errors in JavaScript. - if (!is_typescript_enabled) { - const r = js_lexer.rangeOfIdentifier(p.source, item.name.loc); - try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); - } - continue; - } - - item.name.ref = ref; - data.items[end] = item.*; - end += 1; } - } else { - for (data.items) |*item| { - const name = p.loadNameFromRef(item.name.ref.?); - const symbol = try p.findSymbol(item.alias_loc, name); - const ref = symbol.ref; - if (p.symbols.items[ref.innerIndex()].kind == .unbound) { - // Silently strip exports of non-local symbols in TypeScript, since - // those likely correspond to type-only exports. But report exports of - // non-local symbols as errors in JavaScript. - if (!is_typescript_enabled) { - const r = js_lexer.rangeOfIdentifier(p.source, item.name.loc); - try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); - continue; - } - continue; - } + const _name = p.loadNameFromRef(old_ref); - item.name.ref = ref; - data.items[end] = item.*; - end += 1; - } + const ref = try p.newSymbol(.import, _name); + try p.current_scope.generated.push(p.allocator, ref); + try p.recordDeclaredSymbol(ref); + data.items[j] = item; + data.items[j].name.ref = ref; + j += 1; } - const remove_for_tree_shaking = any_replaced and end == 0 and data.items.len > 0 and p.options.tree_shaking; - data.items.len = end; + data.items.len = j; - if (remove_for_tree_shaking) { + if (j == 0 and data.items.len > 0) { return; } - }, - .s_export_from => |data| { - // "export {foo} from 'path'" - const name = p.loadNameFromRef(data.namespace_ref); + } else { + // This is a re-export and the symbols created here are used to reference + for (data.items) |*item| { + const _name = p.loadNameFromRef(item.name.ref.?); + const ref = try p.newSymbol(.import, _name); + try p.current_scope.generated.push(p.allocator, ref); + try p.recordDeclaredSymbol(ref); + item.name.ref = ref; + } + } - data.namespace_ref = try p.newSymbol(.other, name); - try p.current_scope.generated.push(p.allocator, data.namespace_ref); - try p.recordDeclaredSymbol(data.namespace_ref); + try stmts.append(stmt.*); + } + pub fn s_export_star(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.ExportStar) !void { + // "export * from 'path'" + const name = p.loadNameFromRef(data.namespace_ref); + data.namespace_ref = try p.newSymbol(.other, name); + try p.current_scope.generated.push(p.allocator, data.namespace_ref); + try p.recordDeclaredSymbol(data.namespace_ref); + + // "export * as ns from 'path'" + if (data.alias) |alias| { if (p.options.features.replace_exports.count() > 0) { - var j: usize = 0; - // This is a re-export and the symbols created here are used to reference - for (data.items) |item| { - const old_ref = item.name.ref.?; - - if (p.options.features.replace_exports.count() > 0) { - if (p.options.features.replace_exports.getPtr(item.alias)) |entry| { - _ = p.injectReplacementExport(stmts, old_ref, logger.Loc.Empty, entry); - - continue; - } - } - - const _name = p.loadNameFromRef(old_ref); - - const ref = try p.newSymbol(.import, _name); - try p.current_scope.generated.push(p.allocator, ref); - try p.recordDeclaredSymbol(ref); - data.items[j] = item; - data.items[j].name.ref = ref; - j += 1; - } - - data.items.len = j; - - if (j == 0 and data.items.len > 0) { + if (p.options.features.replace_exports.getPtr(alias.original_name)) |entry| { + _ = p.injectReplacementExport(stmts, p.declareSymbol(.other, logger.Loc.Empty, alias.original_name) catch unreachable, logger.Loc.Empty, entry); return; } - } else { - // This is a re-export and the symbols created here are used to reference - for (data.items) |*item| { - const _name = p.loadNameFromRef(item.name.ref.?); - const ref = try p.newSymbol(.import, _name); - try p.current_scope.generated.push(p.allocator, ref); - try p.recordDeclaredSymbol(ref); - item.name.ref = ref; + } + } + + try stmts.append(stmt.*); + } + pub fn s_export_default(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.ExportDefault) !void { + defer { + if (data.default_name.ref) |ref| { + p.recordDeclaredSymbol(ref) catch unreachable; + } + } + + var mark_for_replace: bool = false; + + const orig_dead = p.is_control_flow_dead; + if (p.options.features.replace_exports.count() > 0) { + if (p.options.features.replace_exports.getPtr("default")) |entry| { + p.is_control_flow_dead = p.options.features.dead_code_elimination and (entry.* != .replace); + mark_for_replace = true; + } + } + + defer { + p.is_control_flow_dead = orig_dead; + } + + switch (data.value) { + .expr => |expr| { + const was_anonymous_named_expr = expr.isAnonymousNamed(); + + data.value.expr = p.visitExpr(expr); + + if (p.is_control_flow_dead) { + return; } - } - }, - .s_export_star => |data| { - // "export * from 'path'" - const name = p.loadNameFromRef(data.namespace_ref); - data.namespace_ref = try p.newSymbol(.other, name); - try p.current_scope.generated.push(p.allocator, data.namespace_ref); - try p.recordDeclaredSymbol(data.namespace_ref); - // "export * as ns from 'path'" - if (data.alias) |alias| { - if (p.options.features.replace_exports.count() > 0) { - if (p.options.features.replace_exports.getPtr(alias.original_name)) |entry| { - _ = p.injectReplacementExport(stmts, p.declareSymbol(.other, logger.Loc.Empty, alias.original_name) catch unreachable, logger.Loc.Empty, entry); - return; - } - } - } - }, - .s_export_default => |data| { - defer { - if (data.default_name.ref) |ref| { - p.recordDeclaredSymbol(ref) catch unreachable; - } - } + // Optionally preserve the name - var mark_for_replace: bool = false; + data.value.expr = p.maybeKeepExprSymbolName(data.value.expr, js_ast.ClauseItem.default_alias, was_anonymous_named_expr); - const orig_dead = p.is_control_flow_dead; - if (p.options.features.replace_exports.count() > 0) { - if (p.options.features.replace_exports.getPtr("default")) |entry| { - p.is_control_flow_dead = p.options.features.dead_code_elimination and (entry.* != .replace); - mark_for_replace = true; - } - } - - defer { - p.is_control_flow_dead = orig_dead; - } - - switch (data.value) { - .expr => |expr| { - const was_anonymous_named_expr = expr.isAnonymousNamed(); - - data.value.expr = p.visitExpr(expr); - - if (p.is_control_flow_dead) { - return; - } - - // Optionally preserve the name - - data.value.expr = p.maybeKeepExprSymbolName(data.value.expr, js_ast.ClauseItem.default_alias, was_anonymous_named_expr); - - // Discard type-only export default statements - if (is_typescript_enabled) { - switch (data.value.expr.data) { - .e_identifier => |ident| { - if (!ident.ref.isSourceContentsSlice()) { - const symbol = p.symbols.items[ident.ref.innerIndex()]; - if (symbol.kind == .unbound) { - if (p.local_type_names.get(symbol.original_name)) |local_type| { - if (local_type) { - // the name points to a type - // don't try to declare this symbol - data.default_name.ref = null; - return; - } + // Discard type-only export default statements + if (is_typescript_enabled) { + switch (data.value.expr.data) { + .e_identifier => |ident| { + if (!ident.ref.isSourceContentsSlice()) { + const symbol = p.symbols.items[ident.ref.innerIndex()]; + if (symbol.kind == .unbound) { + if (p.local_type_names.get(symbol.original_name)) |local_type| { + if (local_type) { + // the name points to a type + // don't try to declare this symbol + data.default_name.ref = null; + return; } } } - }, - else => {}, - } - } - - if (data.default_name.ref.?.isSourceContentsSlice()) { - data.default_name = createDefaultName(p, data.value.expr.loc) catch unreachable; - } - - if (p.options.features.server_components.wrapsExports()) { - data.value.expr = p.wrapValueForServerComponentReference(data.value.expr, "default"); - } - - // If there are lowered "using" declarations, change this into a "var" - if (p.current_scope.parent == null and p.will_wrap_module_in_try_catch_for_using) { - try stmts.ensureUnusedCapacity(2); - - const decls = p.allocator.alloc(G.Decl, 1) catch bun.outOfMemory(); - decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = data.default_name.ref.? }, data.default_name.loc), - .value = data.value.expr, - }; - stmts.appendAssumeCapacity(p.s(S.Local{ - .decls = G.Decl.List.init(decls), - }, stmt.loc)); - const items = p.allocator.alloc(js_ast.ClauseItem, 1) catch bun.outOfMemory(); - items[0] = js_ast.ClauseItem{ - .alias = "default", - .alias_loc = data.default_name.loc, - .name = data.default_name, - }; - stmts.appendAssumeCapacity(p.s(S.ExportClause{ - .items = items, - }, stmt.loc)); - } - - if (mark_for_replace) { - const entry = p.options.features.replace_exports.getPtr("default").?; - if (entry.* == .replace) { - data.value.expr = entry.replace; - } else { - _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); - return; - } - } - }, - - .stmt => |s2| { - switch (s2.data) { - .s_function => |func| { - var name: string = ""; - if (func.func.name) |func_loc| { - name = p.loadNameFromRef(func_loc.ref.?); - } else { - func.func.name = data.default_name; - name = js_ast.ClauseItem.default_alias; } - - var react_hook_data: ?ReactRefresh.HookContext = null; - const prev = p.react_refresh.hook_ctx_storage; - defer p.react_refresh.hook_ctx_storage = prev; - p.react_refresh.hook_ctx_storage = &react_hook_data; - - func.func = p.visitFunc(func.func, func.func.open_parens_loc); - - if (react_hook_data) |*hook| { - stmts.append(p.getReactRefreshHookSignalDecl(hook.signature_cb)) catch bun.outOfMemory(); - - data.value = .{ - .expr = p.getReactRefreshHookSignalInit(hook, p.newExpr( - E.Function{ .func = func.func }, - stmt.loc, - )), - }; - } - - if (p.is_control_flow_dead) { - return; - } - - if (mark_for_replace) { - const entry = p.options.features.replace_exports.getPtr("default").?; - if (entry.* == .replace) { - data.value = .{ .expr = entry.replace }; - } else { - _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); - return; - } - } - - if (data.default_name.ref.?.isSourceContentsSlice()) { - data.default_name = createDefaultName(p, stmt.loc) catch unreachable; - } - - if (p.options.features.server_components.wrapsExports()) { - data.value = .{ .expr = p.wrapValueForServerComponentReference(p.newExpr(E.Function{ .func = func.func }, stmt.loc), "default") }; - } - - stmts.append(stmt.*) catch unreachable; - - // if (func.func.name != null and func.func.name.?.ref != null) { - // stmts.append(p.keepStmtSymbolName(func.func.name.?.loc, func.func.name.?.ref.?, name)) catch unreachable; - // } - // prevent doubling export default function name - return; - }, - .s_class => |class| { - _ = p.visitClass(s2.loc, &class.class, data.default_name.ref.?); - - if (p.is_control_flow_dead) - return; - - if (mark_for_replace) { - const entry = p.options.features.replace_exports.getPtr("default").?; - if (entry.* == .replace) { - data.value = .{ .expr = entry.replace }; - } else { - _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); - return; - } - } - - if (data.default_name.ref.?.isSourceContentsSlice()) { - data.default_name = createDefaultName(p, stmt.loc) catch unreachable; - } - - // We only inject a name into classes when there is a decorator - if (class.class.has_decorators) { - if (class.class.class_name == null or - class.class.class_name.?.ref == null) - { - class.class.class_name = data.default_name; - } - } - - // This is to handle TS decorators, mostly. - var class_stmts = p.lowerClass(.{ .stmt = s2 }); - bun.assert(class_stmts[0].data == .s_class); - - if (class_stmts.len > 1) { - data.value.stmt = class_stmts[0]; - stmts.append(stmt.*) catch {}; - stmts.appendSlice(class_stmts[1..]) catch {}; - } else { - data.value.stmt = class_stmts[0]; - stmts.append(stmt.*) catch {}; - } - - if (p.options.features.server_components.wrapsExports()) { - data.value = .{ .expr = p.wrapValueForServerComponentReference(p.newExpr(class.class, stmt.loc), "default") }; - } - - return; }, else => {}, } - }, - } - }, - .s_export_equals => |data| { - // "module.exports = value" - stmts.append( - Stmt.assign( - p.@"module.exports"(stmt.loc), - p.visitExpr(data.value), - ), - ) catch unreachable; - p.recordUsage(p.module_ref); - return; - }, - .s_break => |data| { - if (data.label) |*label| { - const name = p.loadNameFromRef(label.ref orelse p.panicLoc("Expected label to have a ref", .{}, label.loc)); - const res = p.findLabelSymbol(label.loc, name); - if (res.found) { - label.ref = res.ref; - } else { - data.label = null; } - } else if (!p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) { - const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc); - p.log.addRangeError(p.source, r, "Cannot use \"break\" here") catch unreachable; - } - }, - .s_continue => |data| { - if (data.label) |*label| { - const name = p.loadNameFromRef(label.ref orelse p.panicLoc("Expected continue label to have a ref", .{}, label.loc)); - const res = p.findLabelSymbol(label.loc, name); - label.ref = res.ref; - if (res.found and !res.is_loop) { - const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc); - p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot \"continue\" to label {s}", .{name}) catch unreachable; + + if (data.default_name.ref.?.isSourceContentsSlice()) { + data.default_name = createDefaultName(p, data.value.expr.loc) catch unreachable; } - } else if (!p.fn_or_arrow_data_visit.is_inside_loop) { - const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc); - p.log.addRangeError(p.source, r, "Cannot use \"continue\" here") catch unreachable; - } - }, - .s_label => |data| { - p.pushScopeForVisitPass(.label, stmt.loc) catch unreachable; - const name = p.loadNameFromRef(data.name.ref.?); - const ref = p.newSymbol(.label, name) catch unreachable; - data.name.ref = ref; - p.current_scope.label_ref = ref; - switch (data.stmt.data) { - .s_for, .s_for_in, .s_for_of, .s_while, .s_do_while => { - p.current_scope.label_stmt_is_loop = true; - }, - else => {}, - } - data.stmt = p.visitSingleStmt(data.stmt, StmtsKind.none); - p.popScope(); - }, - .s_local => |data| { - // TODO: Silently remove unsupported top-level "await" in dead code branches - // (this was from 'await using' syntax) + if (p.options.features.server_components.wrapsExports()) { + data.value.expr = p.wrapValueForServerComponentReference(data.value.expr, "default"); + } - // Local statements do not end the const local prefix - p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; + // If there are lowered "using" declarations, change this into a "var" + if (p.current_scope.parent == null and p.will_wrap_module_in_try_catch_for_using) { + try stmts.ensureUnusedCapacity(2); - const decls_len = if (!(data.is_export and p.options.features.replace_exports.entries.len > 0)) - p.visitDecls(data.decls.slice(), data.kind == .k_const, false) - else - p.visitDecls(data.decls.slice(), data.kind == .k_const, true); + const decls = p.allocator.alloc(G.Decl, 1) catch bun.outOfMemory(); + decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = data.default_name.ref.? }, data.default_name.loc), + .value = data.value.expr, + }; + stmts.appendAssumeCapacity(p.s(S.Local{ + .decls = G.Decl.List.init(decls), + }, stmt.loc)); + const items = p.allocator.alloc(js_ast.ClauseItem, 1) catch bun.outOfMemory(); + items[0] = js_ast.ClauseItem{ + .alias = "default", + .alias_loc = data.default_name.loc, + .name = data.default_name, + }; + stmts.appendAssumeCapacity(p.s(S.ExportClause{ + .items = items, + }, stmt.loc)); + } - const is_now_dead = data.decls.len > 0 and decls_len == 0; - if (is_now_dead) { - return; - } - - data.decls.len = @as(u32, @truncate(decls_len)); - - // Handle being exported inside a namespace - if (data.is_export and p.enclosing_namespace_arg_ref != null) { - for (data.decls.slice()) |*d| { - if (d.value) |val| { - p.recordUsage((p.enclosing_namespace_arg_ref orelse unreachable)); - // TODO: is it necessary to lowerAssign? why does esbuild do it _most_ of the time? - stmts.append(p.s(S.SExpr{ - .value = Expr.assign(Binding.toExpr(&d.binding, p.to_expr_wrapper_namespace), val), - }, stmt.loc)) catch unreachable; + if (mark_for_replace) { + const entry = p.options.features.replace_exports.getPtr("default").?; + if (entry.* == .replace) { + data.value.expr = entry.replace; + } else { + _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); + return; } } + }, - return; - } - - // Optimization: Avoid unnecessary "using" machinery by changing ones - // initialized to "null" or "undefined" into a normal variable. Note that - // "await using" still needs the "await", so we can't do it for those. - if (p.options.features.minify_syntax and data.kind == .k_using) { - data.kind = .k_let; - for (data.decls.slice()) |*d| { - if (d.value) |val| { - if (val.data != .e_null and val.data != .e_undefined) { - data.kind = .k_using; - break; + .stmt => |s2| { + switch (s2.data) { + .s_function => |func| { + var name: string = ""; + if (func.func.name) |func_loc| { + name = p.loadNameFromRef(func_loc.ref.?); + } else { + func.func.name = data.default_name; + name = js_ast.ClauseItem.default_alias; } - } - } - } - // We must relocate vars in order to safely handle removing if/else depending on NODE_ENV. - // Edgecase: - // `export var` is skipped because it's unnecessary. That *should* be a noop, but it loses the `is_export` flag if we're in HMR. - const kind = p.selectLocalKind(data.kind); - if (kind == .k_var and !data.is_export) { - const relocated = p.maybeRelocateVarsToTopLevel(data.decls.slice(), .normal); - if (relocated.ok) { - if (relocated.stmt) |new_stmt| { - stmts.append(new_stmt) catch unreachable; - } + var react_hook_data: ?ReactRefresh.HookContext = null; + const prev = p.react_refresh.hook_ctx_storage; + defer p.react_refresh.hook_ctx_storage = prev; + p.react_refresh.hook_ctx_storage = &react_hook_data; - return; - } - } + func.func = p.visitFunc(func.func, func.func.open_parens_loc); - data.kind = kind; - try stmts.append(stmt.*); + if (react_hook_data) |*hook| { + stmts.append(p.getReactRefreshHookSignalDecl(hook.signature_cb)) catch bun.outOfMemory(); - if (data.is_export and p.options.features.server_components.wrapsExports()) { - for (data.decls.slice()) |*decl| try_annotate: { - const val = decl.value orelse break :try_annotate; - switch (val.data) { - .e_arrow, .e_function => {}, - else => break :try_annotate, - } - const id = switch (decl.binding.data) { - .b_identifier => |id| id.ref, - else => break :try_annotate, - }; - const original_name = p.symbols.items[id.innerIndex()].original_name; - decl.value = p.wrapValueForServerComponentReference(val, original_name); - } - } - - if (p.options.features.react_fast_refresh and p.current_scope == p.module_scope) { - for (data.decls.slice()) |decl| try_register: { - const val = decl.value orelse break :try_register; - switch (val.data) { - .e_arrow, .e_function => {}, - else => break :try_register, - } - const id = switch (decl.binding.data) { - .b_identifier => |id| id.ref, - else => break :try_register, - }; - const original_name = p.symbols.items[id.innerIndex()].original_name; - try p.handleReactRefreshRegister(stmts, original_name, id); - } - } - - return; - }, - .s_expr => |data| { - const should_trim_primitive = p.options.features.dead_code_elimination and - (p.options.features.minify_syntax and data.value.isPrimitiveLiteral()); - p.stmt_expr_value = data.value.data; - defer p.stmt_expr_value = .{ .e_missing = .{} }; - - const is_top_level = p.current_scope == p.module_scope; - if (p.shouldUnwrapCommonJSToESM()) { - p.commonjs_named_exports_needs_conversion = if (is_top_level) - std.math.maxInt(u32) - else - p.commonjs_named_exports_needs_conversion; - } - - data.value = p.visitExpr(data.value); - - if (should_trim_primitive and data.value.isPrimitiveLiteral()) { - return; - } - - // simplify unused - data.value = SideEffects.simplifyUnusedExpr(p, data.value) orelse return; - - if (p.shouldUnwrapCommonJSToESM()) { - if (is_top_level) { - if (data.value.data == .e_binary) { - const to_convert = p.commonjs_named_exports_needs_conversion; - if (to_convert != std.math.maxInt(u32)) { - p.commonjs_named_exports_needs_conversion = std.math.maxInt(u32); - convert: { - const bin: *E.Binary = data.value.data.e_binary; - if (bin.op == .bin_assign and bin.left.data == .e_commonjs_export_identifier) { - var last = &p.commonjs_named_exports.values()[to_convert]; - if (!last.needs_decl) break :convert; - last.needs_decl = false; - - var decls = p.allocator.alloc(Decl, 1) catch unreachable; - const ref = bin.left.data.e_commonjs_export_identifier.ref; - decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = ref }, bin.left.loc), - .value = bin.right, - }; - // we have to ensure these are known to be top-level - p.declared_symbols.append(p.allocator, .{ - .ref = ref, - .is_top_level = true, - }) catch unreachable; - p.esm_export_keyword.loc = stmt.loc; - p.esm_export_keyword.len = 5; - p.had_commonjs_named_exports_this_visit = true; - var clause_items = p.allocator.alloc(js_ast.ClauseItem, 1) catch unreachable; - clause_items[0] = js_ast.ClauseItem{ - // We want the generated name to not conflict - .alias = p.commonjs_named_exports.keys()[to_convert], - .alias_loc = bin.left.loc, - .name = .{ - .ref = ref, - .loc = last.loc_ref.loc, - }, - }; - stmts.appendSlice( - &[_]Stmt{ - p.s( - S.Local{ - .kind = .k_var, - .is_export = false, - .was_commonjs_export = true, - .decls = G.Decl.List.init(decls), - }, - stmt.loc, - ), - p.s( - S.ExportClause{ - .items = clause_items, - .is_single_line = true, - }, - stmt.loc, - ), - }, - ) catch unreachable; - - return; - } - } - } else if (p.commonjs_replacement_stmts.len > 0) { - if (stmts.items.len == 0) { - stmts.items = p.commonjs_replacement_stmts; - stmts.capacity = p.commonjs_replacement_stmts.len; - p.commonjs_replacement_stmts.len = 0; - } else { - stmts.appendSlice(p.commonjs_replacement_stmts) catch unreachable; - p.commonjs_replacement_stmts.len = 0; - } + data.value = .{ + .expr = p.getReactRefreshHookSignalInit(hook, p.newExpr( + E.Function{ .func = func.func }, + stmt.loc, + )), + }; + } + if (p.is_control_flow_dead) { return; } + + if (mark_for_replace) { + const entry = p.options.features.replace_exports.getPtr("default").?; + if (entry.* == .replace) { + data.value = .{ .expr = entry.replace }; + } else { + _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); + return; + } + } + + if (data.default_name.ref.?.isSourceContentsSlice()) { + data.default_name = createDefaultName(p, stmt.loc) catch unreachable; + } + + if (p.options.features.server_components.wrapsExports()) { + data.value = .{ .expr = p.wrapValueForServerComponentReference(p.newExpr(E.Function{ .func = func.func }, stmt.loc), "default") }; + } + + stmts.append(stmt.*) catch unreachable; + + // if (func.func.name != null and func.func.name.?.ref != null) { + // stmts.append(p.keepStmtSymbolName(func.func.name.?.loc, func.func.name.?.ref.?, name)) catch unreachable; + // } + // prevent doubling export default function name + return; + }, + .s_class => |class| { + _ = p.visitClass(s2.loc, &class.class, data.default_name.ref.?); + + if (p.is_control_flow_dead) + return; + + if (mark_for_replace) { + const entry = p.options.features.replace_exports.getPtr("default").?; + if (entry.* == .replace) { + data.value = .{ .expr = entry.replace }; + } else { + _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); + return; + } + } + + if (data.default_name.ref.?.isSourceContentsSlice()) { + data.default_name = createDefaultName(p, stmt.loc) catch unreachable; + } + + // We only inject a name into classes when there is a decorator + if (class.class.has_decorators) { + if (class.class.class_name == null or + class.class.class_name.?.ref == null) + { + class.class.class_name = data.default_name; + } + } + + // This is to handle TS decorators, mostly. + var class_stmts = p.lowerClass(.{ .stmt = s2 }); + bun.assert(class_stmts[0].data == .s_class); + + if (class_stmts.len > 1) { + data.value.stmt = class_stmts[0]; + stmts.append(stmt.*) catch {}; + stmts.appendSlice(class_stmts[1..]) catch {}; + } else { + data.value.stmt = class_stmts[0]; + stmts.append(stmt.*) catch {}; + } + + if (p.options.features.server_components.wrapsExports()) { + data.value = .{ .expr = p.wrapValueForServerComponentReference(p.newExpr(class.class, stmt.loc), "default") }; + } + + return; + }, + else => {}, + } + }, + } + + try stmts.append(stmt.*); + } + pub fn s_function(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Function) !void { + // We mark it as dead, but the value may not actually be dead + // We just want to be sure to not increment the usage counts for anything in the function + const mark_as_dead = p.options.features.dead_code_elimination and data.func.flags.contains(.is_export) and + p.options.features.replace_exports.count() > 0 and p.isExportToEliminate(data.func.name.?.ref.?); + const original_is_dead = p.is_control_flow_dead; + + if (mark_as_dead) { + p.is_control_flow_dead = true; + } + defer { + if (mark_as_dead) { + p.is_control_flow_dead = original_is_dead; + } + } + + var react_hook_data: ?ReactRefresh.HookContext = null; + const prev = p.react_refresh.hook_ctx_storage; + defer p.react_refresh.hook_ctx_storage = prev; + p.react_refresh.hook_ctx_storage = &react_hook_data; + + data.func = p.visitFunc(data.func, data.func.open_parens_loc); + + const name_ref = data.func.name.?.ref.?; + bun.assert(name_ref.tag == .symbol); + const name_symbol = &p.symbols.items[name_ref.innerIndex()]; + const original_name = name_symbol.original_name; + + // Handle exporting this function from a namespace + if (data.func.flags.contains(.is_export) and p.enclosing_namespace_arg_ref != null) { + data.func.flags.remove(.is_export); + + const enclosing_namespace_arg_ref = p.enclosing_namespace_arg_ref orelse bun.outOfMemory(); + stmts.ensureUnusedCapacity(3) catch bun.outOfMemory(); + stmts.appendAssumeCapacity(stmt.*); + stmts.appendAssumeCapacity(Stmt.assign( + p.newExpr(E.Dot{ + .target = p.newExpr(E.Identifier{ .ref = enclosing_namespace_arg_ref }, stmt.loc), + .name = original_name, + .name_loc = data.func.name.?.loc, + }, stmt.loc), + p.newExpr(E.Identifier{ .ref = data.func.name.?.ref.? }, data.func.name.?.loc), + )); + } else if (!mark_as_dead) { + if (name_symbol.remove_overwritten_function_declaration) { + return; + } + + if (p.options.features.server_components.wrapsExports() and data.func.flags.contains(.is_export)) { + // Convert this into `export var = registerClientReference(, ...);` + const name = data.func.name.?; + // From the inner scope, have code reference the wrapped function. + data.func.name = null; + try stmts.append(p.s(S.Local{ + .kind = .k_var, + .is_export = true, + .decls = try G.Decl.List.fromSlice(p.allocator, &.{.{ + .binding = p.b(B.Identifier{ .ref = name_ref }, name.loc), + .value = p.wrapValueForServerComponentReference( + p.newExpr(E.Function{ .func = data.func }, stmt.loc), + original_name, + ), + }}), + }, stmt.loc)); + } else { + stmts.append(stmt.*) catch bun.outOfMemory(); + } + } else if (mark_as_dead) { + if (p.options.features.replace_exports.getPtr(original_name)) |replacement| { + _ = p.injectReplacementExport(stmts, name_ref, data.func.name.?.loc, replacement); + } + } + + if (p.options.features.react_fast_refresh) { + if (react_hook_data) |*hook| { + try stmts.append(p.getReactRefreshHookSignalDecl(hook.signature_cb)); + try stmts.append(p.s(S.SExpr{ + .value = p.getReactRefreshHookSignalInit(hook, Expr.initIdentifier(name_ref, logger.Loc.Empty)), + }, logger.Loc.Empty)); + } + + if (p.current_scope == p.module_scope) { + try p.handleReactRefreshRegister(stmts, original_name, name_ref); + } + } + + return; + } + + pub fn s_class(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Class) !void { + const mark_as_dead = p.options.features.dead_code_elimination and data.is_export and + p.options.features.replace_exports.count() > 0 and p.isExportToEliminate(data.class.class_name.?.ref.?); + const original_is_dead = p.is_control_flow_dead; + + if (mark_as_dead) { + p.is_control_flow_dead = true; + } + defer { + if (mark_as_dead) { + p.is_control_flow_dead = original_is_dead; + } + } + + _ = p.visitClass(stmt.loc, &data.class, Ref.None); + + // Remove the export flag inside a namespace + const was_export_inside_namespace = data.is_export and p.enclosing_namespace_arg_ref != null; + if (was_export_inside_namespace) { + data.is_export = false; + } + + const lowered = p.lowerClass(js_ast.StmtOrExpr{ .stmt = stmt.* }); + + if (!mark_as_dead or was_export_inside_namespace) + // Lower class field syntax for browsers that don't support it + stmts.appendSlice(lowered) catch unreachable + else { + const ref = data.class.class_name.?.ref.?; + if (p.options.features.replace_exports.getPtr(p.loadNameFromRef(ref))) |replacement| { + if (p.injectReplacementExport(stmts, ref, data.class.class_name.?.loc, replacement)) { + p.is_control_flow_dead = original_is_dead; + } + } + } + + // Handle exporting this class from a namespace + if (was_export_inside_namespace) { + stmts.append( + Stmt.assign( + p.newExpr( + E.Dot{ + .target = p.newExpr( + E.Identifier{ .ref = p.enclosing_namespace_arg_ref.? }, + stmt.loc, + ), + .name = p.symbols.items[data.class.class_name.?.ref.?.innerIndex()].original_name, + .name_loc = data.class.class_name.?.loc, + }, + stmt.loc, + ), + p.newExpr( + E.Identifier{ .ref = data.class.class_name.?.ref.? }, + data.class.class_name.?.loc, + ), + ), + ) catch unreachable; + } + + return; + } + pub fn s_export_equals(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.ExportEquals) !void { + // "module.exports = value" + stmts.append( + Stmt.assign( + p.@"module.exports"(stmt.loc), + p.visitExpr(data.value), + ), + ) catch unreachable; + p.recordUsage(p.module_ref); + return; + } + pub fn s_break(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Break) !void { + if (data.label) |*label| { + const name = p.loadNameFromRef(label.ref orelse p.panicLoc("Expected label to have a ref", .{}, label.loc)); + const res = p.findLabelSymbol(label.loc, name); + if (res.found) { + label.ref = res.ref; + } else { + data.label = null; + } + } else if (!p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) { + const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc); + p.log.addRangeError(p.source, r, "Cannot use \"break\" here") catch unreachable; + } + + try stmts.append(stmt.*); + } + pub fn s_continue(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Continue) !void { + if (data.label) |*label| { + const name = p.loadNameFromRef(label.ref orelse p.panicLoc("Expected continue label to have a ref", .{}, label.loc)); + const res = p.findLabelSymbol(label.loc, name); + label.ref = res.ref; + if (res.found and !res.is_loop) { + const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot \"continue\" to label {s}", .{name}) catch unreachable; + } + } else if (!p.fn_or_arrow_data_visit.is_inside_loop) { + const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc); + p.log.addRangeError(p.source, r, "Cannot use \"continue\" here") catch unreachable; + } + + try stmts.append(stmt.*); + } + pub fn s_label(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Label) !void { + p.pushScopeForVisitPass(.label, stmt.loc) catch unreachable; + const name = p.loadNameFromRef(data.name.ref.?); + const ref = p.newSymbol(.label, name) catch unreachable; + data.name.ref = ref; + p.current_scope.label_ref = ref; + switch (data.stmt.data) { + .s_for, .s_for_in, .s_for_of, .s_while, .s_do_while => { + p.current_scope.label_stmt_is_loop = true; + }, + else => {}, + } + + data.stmt = p.visitSingleStmt(data.stmt, StmtsKind.none); + p.popScope(); + + try stmts.append(stmt.*); + } + pub fn s_local(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Local, was_after_after_const_local_prefix: bool) !void { + // TODO: Silently remove unsupported top-level "await" in dead code branches + // (this was from 'await using' syntax) + + // Local statements do not end the const local prefix + p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; + + const decls_len = if (!(data.is_export and p.options.features.replace_exports.entries.len > 0)) + p.visitDecls(data.decls.slice(), data.kind == .k_const, false) + else + p.visitDecls(data.decls.slice(), data.kind == .k_const, true); + + const is_now_dead = data.decls.len > 0 and decls_len == 0; + if (is_now_dead) { + return; + } + + data.decls.len = @as(u32, @truncate(decls_len)); + + // Handle being exported inside a namespace + if (data.is_export and p.enclosing_namespace_arg_ref != null) { + for (data.decls.slice()) |*d| { + if (d.value) |val| { + p.recordUsage((p.enclosing_namespace_arg_ref orelse unreachable)); + // TODO: is it necessary to lowerAssign? why does esbuild do it _most_ of the time? + stmts.append(p.s(S.SExpr{ + .value = Expr.assign(Binding.toExpr(&d.binding, p.to_expr_wrapper_namespace), val), + }, stmt.loc)) catch unreachable; + } + } + + return; + } + + // Optimization: Avoid unnecessary "using" machinery by changing ones + // initialized to "null" or "undefined" into a normal variable. Note that + // "await using" still needs the "await", so we can't do it for those. + if (p.options.features.minify_syntax and data.kind == .k_using) { + data.kind = .k_let; + for (data.decls.slice()) |*d| { + if (d.value) |val| { + if (val.data != .e_null and val.data != .e_undefined) { + data.kind = .k_using; + break; } } } - }, - .s_throw => |data| { - data.value = p.visitExpr(data.value); - }, - .s_return => |data| { - // Forbid top-level return inside modules with ECMAScript-style exports - if (p.fn_or_arrow_data_visit.is_outside_fn_or_arrow) { - const where = where: { - if (p.esm_export_keyword.len > 0) { - break :where p.esm_export_keyword; - } else if (p.top_level_await_keyword.len > 0) { - break :where p.top_level_await_keyword; - } else { - break :where logger.Range.None; - } + } + + // We must relocate vars in order to safely handle removing if/else depending on NODE_ENV. + // Edgecase: + // `export var` is skipped because it's unnecessary. That *should* be a noop, but it loses the `is_export` flag if we're in HMR. + const kind = p.selectLocalKind(data.kind); + if (kind == .k_var and !data.is_export) { + const relocated = p.maybeRelocateVarsToTopLevel(data.decls.slice(), .normal); + if (relocated.ok) { + if (relocated.stmt) |new_stmt| { + stmts.append(new_stmt) catch unreachable; + } + + return; + } + } + + data.kind = kind; + try stmts.append(stmt.*); + + if (data.is_export and p.options.features.server_components.wrapsExports()) { + for (data.decls.slice()) |*decl| try_annotate: { + const val = decl.value orelse break :try_annotate; + switch (val.data) { + .e_arrow, .e_function => {}, + else => break :try_annotate, + } + const id = switch (decl.binding.data) { + .b_identifier => |id| id.ref, + else => break :try_annotate, }; + const original_name = p.symbols.items[id.innerIndex()].original_name; + decl.value = p.wrapValueForServerComponentReference(val, original_name); + } + } - if (where.len > 0) { - p.log.addRangeError(p.source, where, "Top-level return cannot be used inside an ECMAScript module") catch unreachable; + if (p.options.features.react_fast_refresh and p.current_scope == p.module_scope) { + for (data.decls.slice()) |decl| try_register: { + const val = decl.value orelse break :try_register; + switch (val.data) { + .e_arrow, .e_function => {}, + else => break :try_register, + } + const id = switch (decl.binding.data) { + .b_identifier => |id| id.ref, + else => break :try_register, + }; + const original_name = p.symbols.items[id.innerIndex()].original_name; + try p.handleReactRefreshRegister(stmts, original_name, id); + } + } + + return; + } + pub fn s_expr(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.SExpr) !void { + const should_trim_primitive = p.options.features.dead_code_elimination and + (p.options.features.minify_syntax and data.value.isPrimitiveLiteral()); + p.stmt_expr_value = data.value.data; + defer p.stmt_expr_value = .{ .e_missing = .{} }; + + const is_top_level = p.current_scope == p.module_scope; + if (p.shouldUnwrapCommonJSToESM()) { + p.commonjs_named_exports_needs_conversion = if (is_top_level) + std.math.maxInt(u32) + else + p.commonjs_named_exports_needs_conversion; + } + + data.value = p.visitExpr(data.value); + + if (should_trim_primitive and data.value.isPrimitiveLiteral()) { + return; + } + + // simplify unused + data.value = SideEffects.simplifyUnusedExpr(p, data.value) orelse return; + + if (p.shouldUnwrapCommonJSToESM()) { + if (is_top_level) { + if (data.value.data == .e_binary) { + const to_convert = p.commonjs_named_exports_needs_conversion; + if (to_convert != std.math.maxInt(u32)) { + p.commonjs_named_exports_needs_conversion = std.math.maxInt(u32); + convert: { + const bin: *E.Binary = data.value.data.e_binary; + if (bin.op == .bin_assign and bin.left.data == .e_commonjs_export_identifier) { + var last = &p.commonjs_named_exports.values()[to_convert]; + if (!last.needs_decl) break :convert; + last.needs_decl = false; + + var decls = p.allocator.alloc(Decl, 1) catch unreachable; + const ref = bin.left.data.e_commonjs_export_identifier.ref; + decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = ref }, bin.left.loc), + .value = bin.right, + }; + // we have to ensure these are known to be top-level + p.declared_symbols.append(p.allocator, .{ + .ref = ref, + .is_top_level = true, + }) catch unreachable; + p.esm_export_keyword.loc = stmt.loc; + p.esm_export_keyword.len = 5; + p.had_commonjs_named_exports_this_visit = true; + var clause_items = p.allocator.alloc(js_ast.ClauseItem, 1) catch unreachable; + clause_items[0] = js_ast.ClauseItem{ + // We want the generated name to not conflict + .alias = p.commonjs_named_exports.keys()[to_convert], + .alias_loc = bin.left.loc, + .name = .{ + .ref = ref, + .loc = last.loc_ref.loc, + }, + }; + stmts.appendSlice( + &[_]Stmt{ + p.s( + S.Local{ + .kind = .k_var, + .is_export = false, + .was_commonjs_export = true, + .decls = G.Decl.List.init(decls), + }, + stmt.loc, + ), + p.s( + S.ExportClause{ + .items = clause_items, + .is_single_line = true, + }, + stmt.loc, + ), + }, + ) catch unreachable; + + return; + } + } + } else if (p.commonjs_replacement_stmts.len > 0) { + if (stmts.items.len == 0) { + stmts.items = p.commonjs_replacement_stmts; + stmts.capacity = p.commonjs_replacement_stmts.len; + p.commonjs_replacement_stmts.len = 0; + } else { + stmts.appendSlice(p.commonjs_replacement_stmts) catch unreachable; + p.commonjs_replacement_stmts.len = 0; + } + + return; + } } } + } - if (data.value) |val| { - data.value = p.visitExpr(val); - - // "return undefined;" can safely just always be "return;" - if (data.value != null and @as(Expr.Tag, data.value.?.data) == .e_undefined) { - // Returning undefined is implicit - data.value = null; + try stmts.append(stmt.*); + } + pub fn s_throw(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Throw) !void { + data.value = p.visitExpr(data.value); + try stmts.append(stmt.*); + } + pub fn s_return(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Return) !void { + // Forbid top-level return inside modules with ECMAScript-style exports + if (p.fn_or_arrow_data_visit.is_outside_fn_or_arrow) { + const where = where: { + if (p.esm_export_keyword.len > 0) { + break :where p.esm_export_keyword; + } else if (p.top_level_await_keyword.len > 0) { + break :where p.top_level_await_keyword; + } else { + break :where logger.Range.None; } + }; + + if (where.len > 0) { + p.log.addRangeError(p.source, where, "Top-level return cannot be used inside an ECMAScript module") catch unreachable; } - }, - .s_block => |data| { - { - p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + } - // Pass the "is loop body" status on to the direct children of a block used - // as a loop body. This is used to enable optimizations specific to the - // topmost scope in a loop body block. - const kind = if (std.meta.eql(p.loop_body, stmt.data)) StmtsKind.loop_body else StmtsKind.none; - var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, data.stmts); - p.visitStmts(&_stmts, kind) catch unreachable; - data.stmts = _stmts.items; - p.popScope(); + if (data.value) |val| { + data.value = p.visitExpr(val); + + // "return undefined;" can safely just always be "return;" + if (data.value != null and @as(Expr.Tag, data.value.?.data) == .e_undefined) { + // Returning undefined is implicit + data.value = null; } + } - if (p.options.features.minify_syntax) { - // // trim empty statements - if (data.stmts.len == 0) { - stmts.append(Stmt{ .data = Prefill.Data.SEmpty, .loc = stmt.loc }) catch unreachable; - return; - } else if (data.stmts.len == 1 and !statementCaresAboutScope(data.stmts[0])) { - // Unwrap blocks containing a single statement - stmts.append(data.stmts[0]) catch unreachable; - return; - } - } - }, - .s_with => |data| { - data.value = p.visitExpr(data.value); - - p.pushScopeForVisitPass(.with, data.body_loc) catch unreachable; - - // This can be many different kinds of statements. - // example code: - // - // with(this.document.defaultView || Object.create(null)) - // with(this.document) - // with(this.form) - // with(this.element) - // - data.body = p.visitSingleStmt(data.body, StmtsKind.none); + try stmts.append(stmt.*); + } + pub fn s_block(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Block) !void { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + // Pass the "is loop body" status on to the direct children of a block used + // as a loop body. This is used to enable optimizations specific to the + // topmost scope in a loop body block. + const kind = if (std.meta.eql(p.loop_body, stmt.data)) StmtsKind.loop_body else StmtsKind.none; + var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, data.stmts); + p.visitStmts(&_stmts, kind) catch unreachable; + data.stmts = _stmts.items; p.popScope(); - }, - .s_while => |data| { - data.test_ = p.visitExpr(data.test_); - data.body = p.visitLoopBody(data.body); + } - data.test_ = SideEffects.simplifyBoolean(p, data.test_); - const result = SideEffects.toBoolean(p, data.test_.data); - if (result.ok and result.side_effects == .no_side_effects) { - data.test_ = p.newExpr(E.Boolean{ .value = result.value }, data.test_.loc); + if (p.options.features.minify_syntax) { + // // trim empty statements + if (data.stmts.len == 0) { + stmts.append(Stmt{ .data = Prefill.Data.SEmpty, .loc = stmt.loc }) catch unreachable; + return; + } else if (data.stmts.len == 1 and !statementCaresAboutScope(data.stmts[0])) { + // Unwrap blocks containing a single statement + stmts.append(data.stmts[0]) catch unreachable; + return; } - }, - .s_do_while => |data| { - data.body = p.visitLoopBody(data.body); - data.test_ = p.visitExpr(data.test_); + } + try stmts.append(stmt.*); + } + pub fn s_with(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.With) !void { + data.value = p.visitExpr(data.value); + + p.pushScopeForVisitPass(.with, data.body_loc) catch unreachable; + + // This can be many different kinds of statements. + // example code: + // + // with(this.document.defaultView || Object.create(null)) + // with(this.document) + // with(this.form) + // with(this.element) + // + data.body = p.visitSingleStmt(data.body, StmtsKind.none); + + p.popScope(); + try stmts.append(stmt.*); + } + pub fn s_while(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.While) !void { + data.test_ = p.visitExpr(data.test_); + data.body = p.visitLoopBody(data.body); + + data.test_ = SideEffects.simplifyBoolean(p, data.test_); + const result = SideEffects.toBoolean(p, data.test_.data); + if (result.ok and result.side_effects == .no_side_effects) { + data.test_ = p.newExpr(E.Boolean{ .value = result.value }, data.test_.loc); + } + + try stmts.append(stmt.*); + } + pub fn s_do_while(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.DoWhile) !void { + data.body = p.visitLoopBody(data.body); + data.test_ = p.visitExpr(data.test_); + + data.test_ = SideEffects.simplifyBoolean(p, data.test_); + try stmts.append(stmt.*); + } + pub fn s_if(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.If) !void { + data.test_ = p.visitExpr(data.test_); + + if (p.options.features.minify_syntax) { data.test_ = SideEffects.simplifyBoolean(p, data.test_); - }, - .s_if => |data| { - data.test_ = p.visitExpr(data.test_); + } - if (p.options.features.minify_syntax) { - data.test_ = SideEffects.simplifyBoolean(p, data.test_); - } + const effects = SideEffects.toBoolean(p, data.test_.data); + if (effects.ok and !effects.value) { + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + data.yes = p.visitSingleStmt(data.yes, StmtsKind.none); + p.is_control_flow_dead = old; + } else { + data.yes = p.visitSingleStmt(data.yes, StmtsKind.none); + } - const effects = SideEffects.toBoolean(p, data.test_.data); - if (effects.ok and !effects.value) { + // The "else" clause is optional + if (data.no) |no| { + if (effects.ok and effects.value) { const old = p.is_control_flow_dead; p.is_control_flow_dead = true; - data.yes = p.visitSingleStmt(data.yes, StmtsKind.none); - p.is_control_flow_dead = old; + defer p.is_control_flow_dead = old; + data.no = p.visitSingleStmt(no, .none); } else { - data.yes = p.visitSingleStmt(data.yes, StmtsKind.none); - } - - // The "else" clause is optional - if (data.no) |no| { - if (effects.ok and effects.value) { - const old = p.is_control_flow_dead; - p.is_control_flow_dead = true; - defer p.is_control_flow_dead = old; - data.no = p.visitSingleStmt(no, .none); - } else { - data.no = p.visitSingleStmt(no, .none); - } - - // Trim unnecessary "else" clauses - if (p.options.features.minify_syntax) { - if (data.no != null and @as(Stmt.Tag, data.no.?.data) == .s_empty) { - data.no = null; - } - } + data.no = p.visitSingleStmt(no, .none); } + // Trim unnecessary "else" clauses if (p.options.features.minify_syntax) { - if (effects.ok) { - if (effects.value) { - if (data.no == null or !SideEffects.shouldKeepStmtInDeadControlFlow(p, data.no.?, p.allocator)) { - if (effects.side_effects == .could_have_side_effects) { - // Keep the condition if it could have side effects (but is still known to be truthy) - if (SideEffects.simplifyUnusedExpr(p, data.test_)) |test_| { - stmts.append(p.s(S.SExpr{ .value = test_ }, test_.loc)) catch unreachable; - } - } + if (data.no != null and @as(Stmt.Tag, data.no.?.data) == .s_empty) { + data.no = null; + } + } + } - return try p.appendIfBodyPreservingScope(stmts, data.yes); - } else { - // We have to keep the "no" branch + if (p.options.features.minify_syntax) { + if (effects.ok) { + if (effects.value) { + if (data.no == null or !SideEffects.shouldKeepStmtInDeadControlFlow(p, data.no.?, p.allocator)) { + if (effects.side_effects == .could_have_side_effects) { + // Keep the condition if it could have side effects (but is still known to be truthy) + if (SideEffects.simplifyUnusedExpr(p, data.test_)) |test_| { + stmts.append(p.s(S.SExpr{ .value = test_ }, test_.loc)) catch unreachable; + } } + + return try p.appendIfBodyPreservingScope(stmts, data.yes); } else { - // The test is falsy - if (!SideEffects.shouldKeepStmtInDeadControlFlow(p, data.yes, p.allocator)) { - if (effects.side_effects == .could_have_side_effects) { - // Keep the condition if it could have side effects (but is still known to be truthy) - if (SideEffects.simplifyUnusedExpr(p, data.test_)) |test_| { - stmts.append(p.s(S.SExpr{ .value = test_ }, test_.loc)) catch unreachable; - } + // We have to keep the "no" branch + } + } else { + // The test is falsy + if (!SideEffects.shouldKeepStmtInDeadControlFlow(p, data.yes, p.allocator)) { + if (effects.side_effects == .could_have_side_effects) { + // Keep the condition if it could have side effects (but is still known to be truthy) + if (SideEffects.simplifyUnusedExpr(p, data.test_)) |test_| { + stmts.append(p.s(S.SExpr{ .value = test_ }, test_.loc)) catch unreachable; } - - if (data.no == null) { - return; - } - - return try p.appendIfBodyPreservingScope(stmts, data.no.?); } + + if (data.no == null) { + return; + } + + return try p.appendIfBodyPreservingScope(stmts, data.no.?); } } + } - // TODO: more if statement syntax minification - const can_remove_test = p.exprCanBeRemovedIfUnused(&data.test_); - switch (data.yes.data) { - .s_expr => |yes_expr| { - if (yes_expr.value.isMissing()) { - if (data.no == null) { - if (can_remove_test) { - return; - } - } else if (data.no.?.isMissingExpr() and can_remove_test) { - return; - } - } - }, - .s_empty => { + // TODO: more if statement syntax minification + const can_remove_test = p.exprCanBeRemovedIfUnused(&data.test_); + switch (data.yes.data) { + .s_expr => |yes_expr| { + if (yes_expr.value.isMissing()) { if (data.no == null) { if (can_remove_test) { return; @@ -19761,590 +20052,457 @@ fn NewParser_( } else if (data.no.?.isMissingExpr() and can_remove_test) { return; } - }, - else => {}, - } - } - }, - .s_for => |data| { - p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; - - if (data.init) |initst| { - data.init = p.visitForLoopInit(initst, false); - } - - if (data.test_) |test_| { - data.test_ = SideEffects.simplifyBoolean(p, p.visitExpr(test_)); - - const result = SideEffects.toBoolean(p, data.test_.?.data); - if (result.ok and result.value and result.side_effects == .no_side_effects) { - data.test_ = null; - } - } - - if (data.update) |update| { - data.update = p.visitExpr(update); - } - - data.body = p.visitLoopBody(data.body); - - if (data.init) |for_init| { - if (for_init.data == .s_local) { - // Potentially relocate "var" declarations to the top level. Note that this - // must be done inside the scope of the for loop or they won't be relocated. - if (for_init.data.s_local.kind == .k_var) { - const relocate = p.maybeRelocateVarsToTopLevel(for_init.data.s_local.decls.slice(), .normal); - if (relocate.stmt) |relocated| { - data.init = relocated; + } + }, + .s_empty => { + if (data.no == null) { + if (can_remove_test) { + return; } + } else if (data.no.?.isMissingExpr() and can_remove_test) { + return; + } + }, + else => {}, + } + } + + try stmts.append(stmt.*); + } + pub fn s_for(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.For) !void { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + + if (data.init) |initst| { + data.init = p.visitForLoopInit(initst, false); + } + + if (data.test_) |test_| { + data.test_ = SideEffects.simplifyBoolean(p, p.visitExpr(test_)); + + const result = SideEffects.toBoolean(p, data.test_.?.data); + if (result.ok and result.value and result.side_effects == .no_side_effects) { + data.test_ = null; + } + } + + if (data.update) |update| { + data.update = p.visitExpr(update); + } + + data.body = p.visitLoopBody(data.body); + + if (data.init) |for_init| { + if (for_init.data == .s_local) { + // Potentially relocate "var" declarations to the top level. Note that this + // must be done inside the scope of the for loop or they won't be relocated. + if (for_init.data.s_local.kind == .k_var) { + const relocate = p.maybeRelocateVarsToTopLevel(for_init.data.s_local.decls.slice(), .normal); + if (relocate.stmt) |relocated| { + data.init = relocated; } } } + } - p.popScope(); - }, - .s_for_in => |data| { - { - p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; - defer p.popScope(); - _ = p.visitForLoopInit(data.init, true); - data.value = p.visitExpr(data.value); - data.body = p.visitLoopBody(data.body); + p.popScope(); - // Check for a variable initializer - if (data.init.data == .s_local and data.init.data.s_local.kind == .k_var) { - // Lower for-in variable initializers in case the output is used in strict mode - var local = data.init.data.s_local; - if (local.decls.len == 1) { - var decl: *G.Decl = &local.decls.ptr[0]; - if (decl.binding.data == .b_identifier) { - if (decl.value) |val| { - stmts.append( - Stmt.assign( - Expr.initIdentifier(decl.binding.data.b_identifier.ref, decl.binding.loc), - val, - ), - ) catch unreachable; - decl.value = null; - } - } - } - - const relocate = p.maybeRelocateVarsToTopLevel(data.init.data.s_local.decls.slice(), RelocateVars.Mode.for_in_or_for_of); - if (relocate.stmt) |relocated_stmt| { - data.init = relocated_stmt; - } - } - } - }, - .s_for_of => |data| { + try stmts.append(stmt.*); + } + pub fn s_for_in(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.ForIn) !void { + { p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; defer p.popScope(); _ = p.visitForLoopInit(data.init, true); data.value = p.visitExpr(data.value); data.body = p.visitLoopBody(data.body); - if (data.init.data == .s_local) { - if (data.init.data.s_local.kind == .k_var) { - const relocate = p.maybeRelocateVarsToTopLevel(data.init.data.s_local.decls.slice(), RelocateVars.Mode.for_in_or_for_of); - if (relocate.stmt) |relocated_stmt| { - data.init = relocated_stmt; + // Check for a variable initializer + if (data.init.data == .s_local and data.init.data.s_local.kind == .k_var) { + // Lower for-in variable initializers in case the output is used in strict mode + var local = data.init.data.s_local; + if (local.decls.len == 1) { + var decl: *G.Decl = &local.decls.ptr[0]; + if (decl.binding.data == .b_identifier) { + if (decl.value) |val| { + stmts.append( + Stmt.assign( + Expr.initIdentifier(decl.binding.data.b_identifier.ref, decl.binding.loc), + val, + ), + ) catch unreachable; + decl.value = null; + } } } - // Handle "for (using x of y)" and "for (await using x of y)" - if (data.init.data == .s_local and data.init.data.s_local.kind.isUsing() and p.options.features.lower_using) { - // fn lowerUsingDeclarationInForOf() - const loc = data.init.loc; - const init2 = data.init.data.s_local; - const binding = init2.decls.at(0).binding; - var id = binding.data.b_identifier; - const temp_ref = p.generateTempRef(p.symbols.items[id.ref.inner_index].original_name); - - const first = p.s(S.Local{ - .kind = init2.kind, - .decls = bindings: { - const decls = p.allocator.alloc(G.Decl, 1) catch bun.outOfMemory(); - decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = id.ref }, loc), - .value = p.newExpr(E.Identifier{ .ref = temp_ref }, loc), - }; - break :bindings G.Decl.List.init(decls); - }, - }, loc); - - const length = if (data.body.data == .s_block) data.body.data.s_block.stmts.len else 1; - const statements = p.allocator.alloc(Stmt, 1 + length) catch bun.outOfMemory(); - statements[0] = first; - if (data.body.data == .s_block) { - @memcpy(statements[1..], data.body.data.s_block.stmts); - } else { - statements[1] = data.body; - } - - var ctx = try P.LowerUsingDeclarationsContext.init(p); - ctx.scanStmts(p, statements); - const visited_stmts = ctx.finalize(p, statements, p.will_wrap_module_in_try_catch_for_using and p.current_scope.parent == null); - if (data.body.data == .s_block) { - data.body.data.s_block.stmts = visited_stmts.items; - } else { - data.body = p.s(S.Block{ - .stmts = visited_stmts.items, - }, loc); - } - id.ref = temp_ref; - init2.kind = .k_const; + const relocate = p.maybeRelocateVarsToTopLevel(data.init.data.s_local.decls.slice(), RelocateVars.Mode.for_in_or_for_of); + if (relocate.stmt) |relocated_stmt| { + data.init = relocated_stmt; } } - }, - .s_try => |data| { - p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + } + + try stmts.append(stmt.*); + } + pub fn s_for_of(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.ForOf) !void { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + _ = p.visitForLoopInit(data.init, true); + data.value = p.visitExpr(data.value); + data.body = p.visitLoopBody(data.body); + + if (data.init.data == .s_local) { + if (data.init.data.s_local.kind == .k_var) { + const relocate = p.maybeRelocateVarsToTopLevel(data.init.data.s_local.decls.slice(), RelocateVars.Mode.for_in_or_for_of); + if (relocate.stmt) |relocated_stmt| { + data.init = relocated_stmt; + } + } + + // Handle "for (using x of y)" and "for (await using x of y)" + if (data.init.data == .s_local and data.init.data.s_local.kind.isUsing() and p.options.features.lower_using) { + // fn lowerUsingDeclarationInForOf() + const loc = data.init.loc; + const init2 = data.init.data.s_local; + const binding = init2.decls.at(0).binding; + var id = binding.data.b_identifier; + const temp_ref = p.generateTempRef(p.symbols.items[id.ref.inner_index].original_name); + + const first = p.s(S.Local{ + .kind = init2.kind, + .decls = bindings: { + const decls = p.allocator.alloc(G.Decl, 1) catch bun.outOfMemory(); + decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = id.ref }, loc), + .value = p.newExpr(E.Identifier{ .ref = temp_ref }, loc), + }; + break :bindings G.Decl.List.init(decls); + }, + }, loc); + + const length = if (data.body.data == .s_block) data.body.data.s_block.stmts.len else 1; + const statements = p.allocator.alloc(Stmt, 1 + length) catch bun.outOfMemory(); + statements[0] = first; + if (data.body.data == .s_block) { + @memcpy(statements[1..], data.body.data.s_block.stmts); + } else { + statements[1] = data.body; + } + + var ctx = try P.LowerUsingDeclarationsContext.init(p); + ctx.scanStmts(p, statements); + const visited_stmts = ctx.finalize(p, statements, p.will_wrap_module_in_try_catch_for_using and p.current_scope.parent == null); + if (data.body.data == .s_block) { + data.body.data.s_block.stmts = visited_stmts.items; + } else { + data.body = p.s(S.Block{ + .stmts = visited_stmts.items, + }, loc); + } + id.ref = temp_ref; + init2.kind = .k_const; + } + } + + try stmts.append(stmt.*); + } + pub fn s_try(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Try) !void { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + { + var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, data.body); + p.fn_or_arrow_data_visit.try_body_count += 1; + p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; + p.fn_or_arrow_data_visit.try_body_count -= 1; + data.body = _stmts.items; + } + p.popScope(); + + if (data.catch_) |*catch_| { + p.pushScopeForVisitPass(.catch_binding, catch_.loc) catch unreachable; { - var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, data.body); - p.fn_or_arrow_data_visit.try_body_count += 1; + if (catch_.binding) |catch_binding| { + p.visitBinding(catch_binding, null); + } + var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, catch_.body); + p.pushScopeForVisitPass(.block, catch_.body_loc) catch unreachable; p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; - p.fn_or_arrow_data_visit.try_body_count -= 1; - data.body = _stmts.items; + p.popScope(); + catch_.body = _stmts.items; } p.popScope(); + } - if (data.catch_) |*catch_| { - p.pushScopeForVisitPass(.catch_binding, catch_.loc) catch unreachable; - { - if (catch_.binding) |catch_binding| { - p.visitBinding(catch_binding, null); - } - var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, catch_.body); - p.pushScopeForVisitPass(.block, catch_.body_loc) catch unreachable; - p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; - p.popScope(); - catch_.body = _stmts.items; - } - p.popScope(); - } - - if (data.finally) |*finally| { - p.pushScopeForVisitPass(.block, finally.loc) catch unreachable; - { - var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, finally.stmts); - p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; - finally.stmts = _stmts.items; - } - p.popScope(); - } - }, - .s_switch => |data| { - data.test_ = p.visitExpr(data.test_); + if (data.finally) |*finally| { + p.pushScopeForVisitPass(.block, finally.loc) catch unreachable; { - p.pushScopeForVisitPass(.block, data.body_loc) catch unreachable; - defer p.popScope(); - const old_is_inside_Swsitch = p.fn_or_arrow_data_visit.is_inside_switch; - p.fn_or_arrow_data_visit.is_inside_switch = true; - defer p.fn_or_arrow_data_visit.is_inside_switch = old_is_inside_Swsitch; - for (data.cases, 0..) |case, i| { - if (case.value) |val| { - data.cases[i].value = p.visitExpr(val); - // TODO: error messages - // Check("case", *c.Value, c.Value.Loc) - // p.warnAboutTypeofAndString(s.Test, *c.Value) - } - var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, case.body); - p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; - data.cases[i].body = _stmts.items; - } + var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, finally.stmts); + p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; + finally.stmts = _stmts.items; } - // TODO: duplicate case checker + p.popScope(); + } - }, - .s_function => |data| { - // We mark it as dead, but the value may not actually be dead - // We just want to be sure to not increment the usage counts for anything in the function - const mark_as_dead = p.options.features.dead_code_elimination and data.func.flags.contains(.is_export) and - p.options.features.replace_exports.count() > 0 and p.isExportToEliminate(data.func.name.?.ref.?); - const original_is_dead = p.is_control_flow_dead; - - if (mark_as_dead) { - p.is_control_flow_dead = true; - } - defer { - if (mark_as_dead) { - p.is_control_flow_dead = original_is_dead; - } - } - - var react_hook_data: ?ReactRefresh.HookContext = null; - const prev = p.react_refresh.hook_ctx_storage; - defer p.react_refresh.hook_ctx_storage = prev; - p.react_refresh.hook_ctx_storage = &react_hook_data; - - data.func = p.visitFunc(data.func, data.func.open_parens_loc); - - const name_ref = data.func.name.?.ref.?; - bun.assert(name_ref.tag == .symbol); - const name_symbol = &p.symbols.items[name_ref.innerIndex()]; - const original_name = name_symbol.original_name; - - // Handle exporting this function from a namespace - if (data.func.flags.contains(.is_export) and p.enclosing_namespace_arg_ref != null) { - data.func.flags.remove(.is_export); - - const enclosing_namespace_arg_ref = p.enclosing_namespace_arg_ref orelse bun.outOfMemory(); - stmts.ensureUnusedCapacity(3) catch bun.outOfMemory(); - stmts.appendAssumeCapacity(stmt.*); - stmts.appendAssumeCapacity(Stmt.assign( - p.newExpr(E.Dot{ - .target = p.newExpr(E.Identifier{ .ref = enclosing_namespace_arg_ref }, stmt.loc), - .name = original_name, - .name_loc = data.func.name.?.loc, - }, stmt.loc), - p.newExpr(E.Identifier{ .ref = data.func.name.?.ref.? }, data.func.name.?.loc), - )); - } else if (!mark_as_dead) { - if (name_symbol.remove_overwritten_function_declaration) { - return; - } - - if (p.options.features.server_components.wrapsExports() and data.func.flags.contains(.is_export)) { - // Convert this into `export var = registerClientReference(, ...);` - const name = data.func.name.?; - // From the inner scope, have code reference the wrapped function. - data.func.name = null; - try stmts.append(p.s(S.Local{ - .kind = .k_var, - .is_export = true, - .decls = try G.Decl.List.fromSlice(p.allocator, &.{.{ - .binding = p.b(B.Identifier{ .ref = name_ref }, name.loc), - .value = p.wrapValueForServerComponentReference( - p.newExpr(E.Function{ .func = data.func }, stmt.loc), - original_name, - ), - }}), - }, stmt.loc)); - } else { - stmts.append(stmt.*) catch bun.outOfMemory(); - } - } else if (mark_as_dead) { - if (p.options.features.replace_exports.getPtr(original_name)) |replacement| { - _ = p.injectReplacementExport(stmts, name_ref, data.func.name.?.loc, replacement); - } - } - - if (p.options.features.react_fast_refresh) { - if (react_hook_data) |*hook| { - try stmts.append(p.getReactRefreshHookSignalDecl(hook.signature_cb)); - try stmts.append(p.s(S.SExpr{ - .value = p.getReactRefreshHookSignalInit(hook, Expr.initIdentifier(name_ref, logger.Loc.Empty)), - }, logger.Loc.Empty)); - } - - if (p.current_scope == p.module_scope) { - try p.handleReactRefreshRegister(stmts, original_name, name_ref); - } - } - - return; - }, - .s_class => |data| { - const mark_as_dead = p.options.features.dead_code_elimination and data.is_export and - p.options.features.replace_exports.count() > 0 and p.isExportToEliminate(data.class.class_name.?.ref.?); - const original_is_dead = p.is_control_flow_dead; - - if (mark_as_dead) { - p.is_control_flow_dead = true; - } - defer { - if (mark_as_dead) { - p.is_control_flow_dead = original_is_dead; - } - } - - _ = p.visitClass(stmt.loc, &data.class, Ref.None); - - // Remove the export flag inside a namespace - const was_export_inside_namespace = data.is_export and p.enclosing_namespace_arg_ref != null; - if (was_export_inside_namespace) { - data.is_export = false; - } - - const lowered = p.lowerClass(js_ast.StmtOrExpr{ .stmt = stmt.* }); - - if (!mark_as_dead or was_export_inside_namespace) - // Lower class field syntax for browsers that don't support it - stmts.appendSlice(lowered) catch unreachable - else { - const ref = data.class.class_name.?.ref.?; - if (p.options.features.replace_exports.getPtr(p.loadNameFromRef(ref))) |replacement| { - if (p.injectReplacementExport(stmts, ref, data.class.class_name.?.loc, replacement)) { - p.is_control_flow_dead = original_is_dead; - } - } - } - - // Handle exporting this class from a namespace - if (was_export_inside_namespace) { - stmts.append( - Stmt.assign( - p.newExpr( - E.Dot{ - .target = p.newExpr( - E.Identifier{ .ref = p.enclosing_namespace_arg_ref.? }, - stmt.loc, - ), - .name = p.symbols.items[data.class.class_name.?.ref.?.innerIndex()].original_name, - .name_loc = data.class.class_name.?.loc, - }, - stmt.loc, - ), - p.newExpr( - E.Identifier{ .ref = data.class.class_name.?.ref.? }, - data.class.class_name.?.loc, - ), - ), - ) catch unreachable; - } - - return; - }, - .s_enum => |data| { - // Do not end the const local prefix after TypeScript enums. We process - // them first within their scope so that they are inlined into all code in - // that scope. We don't want that to cause the const local prefix to end. - p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; - - // Track cross-module enum constants during bundling. This - // part of the code is different from esbuilt in that we are - // only storing a list of enum indexes. At the time of - // referencing, `esbuild` builds a separate hash map of hash - // maps. We are avoiding that to reduce memory usage, since - // enum inlining already uses alot of hash maps. - if (p.current_scope == p.module_scope and p.options.bundle) { - try p.top_level_enums.append(p.allocator, data.name.ref.?); - } - - p.recordDeclaredSymbol(data.name.ref.?) catch bun.outOfMemory(); - p.pushScopeForVisitPass(.entry, stmt.loc) catch bun.outOfMemory(); + try stmts.append(stmt.*); + } + pub fn s_switch(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Switch) !void { + data.test_ = p.visitExpr(data.test_); + { + p.pushScopeForVisitPass(.block, data.body_loc) catch unreachable; defer p.popScope(); - p.recordDeclaredSymbol(data.arg) catch bun.outOfMemory(); - - const allocator = p.allocator; - // Scan ahead for any variables inside this namespace. This must be done - // ahead of time before visiting any statements inside the namespace - // because we may end up visiting the uses before the declarations. - // We need to convert the uses into property accesses on the namespace. - for (data.values) |value| { - if (value.ref.isValid()) { - p.is_exported_inside_namespace.put(allocator, value.ref, data.arg) catch bun.outOfMemory(); + const old_is_inside_Swsitch = p.fn_or_arrow_data_visit.is_inside_switch; + p.fn_or_arrow_data_visit.is_inside_switch = true; + defer p.fn_or_arrow_data_visit.is_inside_switch = old_is_inside_Swsitch; + for (data.cases, 0..) |case, i| { + if (case.value) |val| { + data.cases[i].value = p.visitExpr(val); + // TODO: error messages + // Check("case", *c.Value, c.Value.Loc) + // p.warnAboutTypeofAndString(s.Test, *c.Value) } + var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, case.body); + p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; + data.cases[i].body = _stmts.items; + } + } + // TODO: duplicate case checker + + try stmts.append(stmt.*); + } + + pub fn s_enum(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Enum, was_after_after_const_local_prefix: bool) !void { + + // Do not end the const local prefix after TypeScript enums. We process + // them first within their scope so that they are inlined into all code in + // that scope. We don't want that to cause the const local prefix to end. + p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; + + // Track cross-module enum constants during bundling. This + // part of the code is different from esbuilt in that we are + // only storing a list of enum indexes. At the time of + // referencing, `esbuild` builds a separate hash map of hash + // maps. We are avoiding that to reduce memory usage, since + // enum inlining already uses alot of hash maps. + if (p.current_scope == p.module_scope and p.options.bundle) { + try p.top_level_enums.append(p.allocator, data.name.ref.?); + } + + p.recordDeclaredSymbol(data.name.ref.?) catch bun.outOfMemory(); + p.pushScopeForVisitPass(.entry, stmt.loc) catch bun.outOfMemory(); + defer p.popScope(); + p.recordDeclaredSymbol(data.arg) catch bun.outOfMemory(); + + const allocator = p.allocator; + // Scan ahead for any variables inside this namespace. This must be done + // ahead of time before visiting any statements inside the namespace + // because we may end up visiting the uses before the declarations. + // We need to convert the uses into property accesses on the namespace. + for (data.values) |value| { + if (value.ref.isValid()) { + p.is_exported_inside_namespace.put(allocator, value.ref, data.arg) catch bun.outOfMemory(); + } + } + + // Values without initializers are initialized to one more than the + // previous value if the previous value is numeric. Otherwise values + // without initializers are initialized to undefined. + var next_numeric_value: ?f64 = 0.0; + + var value_exprs = ListManaged(Expr).initCapacity(allocator, data.values.len) catch bun.outOfMemory(); + + var all_values_are_pure = true; + + const exported_members = p.current_scope.ts_namespace.?.exported_members; + + // We normally don't fold numeric constants because they might increase code + // size, but it's important to fold numeric constants inside enums since + // that's what the TypeScript compiler does. + const old_should_fold_typescript_constant_expressions = p.should_fold_typescript_constant_expressions; + p.should_fold_typescript_constant_expressions = true; + + // Create an assignment for each enum value + for (data.values) |*value| { + const name = value.name; + + var has_string_value = false; + if (value.value) |enum_value| { + next_numeric_value = null; + + const visited = p.visitExpr(enum_value); + + // "See through" any wrapped comments + const underlying_value = if (visited.data == .e_inlined_enum) + visited.data.e_inlined_enum.value + else + visited; + value.value = underlying_value; + + switch (underlying_value.data) { + .e_number => |num| { + exported_members.getPtr(name).?.data = .{ .enum_number = num.value }; + + p.ref_to_ts_namespace_member.put( + p.allocator, + value.ref, + .{ .enum_number = num.value }, + ) catch bun.outOfMemory(); + + next_numeric_value = num.value + 1.0; + }, + .e_string => |str| { + has_string_value = true; + + exported_members.getPtr(name).?.data = .{ .enum_string = str }; + + p.ref_to_ts_namespace_member.put( + p.allocator, + value.ref, + .{ .enum_string = str }, + ) catch bun.outOfMemory(); + }, + else => { + if (visited.knownPrimitive() == .string) { + has_string_value = true; + } + + if (!p.exprCanBeRemovedIfUnused(&visited)) { + all_values_are_pure = false; + } + }, + } + } else if (next_numeric_value) |num| { + value.value = p.newExpr(E.Number{ .value = num }, value.loc); + + next_numeric_value = num + 1; + + exported_members.getPtr(name).?.data = .{ .enum_number = num }; + + p.ref_to_ts_namespace_member.put( + p.allocator, + value.ref, + .{ .enum_number = num }, + ) catch bun.outOfMemory(); + } else { + value.value = p.newExpr(E.Undefined{}, value.loc); } - // Values without initializers are initialized to one more than the - // previous value if the previous value is numeric. Otherwise values - // without initializers are initialized to undefined. - var next_numeric_value: ?f64 = 0.0; + const is_assign_target = p.options.features.minify_syntax and bun.js_lexer.isIdentifier(value.name); - var value_exprs = ListManaged(Expr).initCapacity(allocator, data.values.len) catch bun.outOfMemory(); + const name_as_e_string = if (!is_assign_target or !has_string_value) + p.newExpr(value.nameAsEString(allocator), value.loc) + else + null; - var all_values_are_pure = true; + const assign_target = if (is_assign_target) + // "Enum.Name = value" + Expr.assign( + p.newExpr(E.Dot{ + .target = p.newExpr( + E.Identifier{ .ref = data.arg }, + value.loc, + ), + .name = value.name, + .name_loc = value.loc, + }, value.loc), + value.value.?, + ) + else + // "Enum['Name'] = value" + Expr.assign( + p.newExpr(E.Index{ + .target = p.newExpr( + E.Identifier{ .ref = data.arg }, + value.loc, + ), + .index = name_as_e_string.?, + }, value.loc), + value.value.?, + ); - const exported_members = p.current_scope.ts_namespace.?.exported_members; + p.recordUsage(data.arg); - // We normally don't fold numeric constants because they might increase code - // size, but it's important to fold numeric constants inside enums since - // that's what the TypeScript compiler does. - const old_should_fold_typescript_constant_expressions = p.should_fold_typescript_constant_expressions; - p.should_fold_typescript_constant_expressions = true; - - // Create an assignment for each enum value - for (data.values) |*value| { - const name = value.name; - - var has_string_value = false; - if (value.value) |enum_value| { - next_numeric_value = null; - - const visited = p.visitExpr(enum_value); - - // "See through" any wrapped comments - const underlying_value = if (visited.data == .e_inlined_enum) - visited.data.e_inlined_enum.value - else - visited; - value.value = underlying_value; - - switch (underlying_value.data) { - .e_number => |num| { - exported_members.getPtr(name).?.data = .{ .enum_number = num.value }; - - p.ref_to_ts_namespace_member.put( - p.allocator, - value.ref, - .{ .enum_number = num.value }, - ) catch bun.outOfMemory(); - - next_numeric_value = num.value + 1.0; - }, - .e_string => |str| { - has_string_value = true; - - exported_members.getPtr(name).?.data = .{ .enum_string = str }; - - p.ref_to_ts_namespace_member.put( - p.allocator, - value.ref, - .{ .enum_string = str }, - ) catch bun.outOfMemory(); - }, - else => { - if (visited.knownPrimitive() == .string) { - has_string_value = true; - } - - if (!p.exprCanBeRemovedIfUnused(&visited)) { - all_values_are_pure = false; - } - }, - } - } else if (next_numeric_value) |num| { - value.value = p.newExpr(E.Number{ .value = num }, value.loc); - - next_numeric_value = num + 1; - - exported_members.getPtr(name).?.data = .{ .enum_number = num }; - - p.ref_to_ts_namespace_member.put( - p.allocator, - value.ref, - .{ .enum_number = num }, - ) catch bun.outOfMemory(); - } else { - value.value = p.newExpr(E.Undefined{}, value.loc); - } - - const is_assign_target = p.options.features.minify_syntax and bun.js_lexer.isIdentifier(value.name); - - const name_as_e_string = if (!is_assign_target or !has_string_value) - p.newExpr(value.nameAsEString(allocator), value.loc) - else - null; - - const assign_target = if (is_assign_target) - // "Enum.Name = value" - Expr.assign( - p.newExpr(E.Dot{ - .target = p.newExpr( - E.Identifier{ .ref = data.arg }, - value.loc, - ), - .name = value.name, - .name_loc = value.loc, - }, value.loc), - value.value.?, - ) - else - // "Enum['Name'] = value" + // String-valued enums do not form a two-way map + if (has_string_value) { + value_exprs.append(assign_target) catch bun.outOfMemory(); + } else { + // "Enum[assignTarget] = 'Name'" + value_exprs.append( Expr.assign( p.newExpr(E.Index{ .target = p.newExpr( E.Identifier{ .ref = data.arg }, value.loc, ), - .index = name_as_e_string.?, + .index = assign_target, }, value.loc), - value.value.?, - ); - + name_as_e_string.?, + ), + ) catch bun.outOfMemory(); p.recordUsage(data.arg); - - // String-valued enums do not form a two-way map - if (has_string_value) { - value_exprs.append(assign_target) catch bun.outOfMemory(); - } else { - // "Enum[assignTarget] = 'Name'" - value_exprs.append( - Expr.assign( - p.newExpr(E.Index{ - .target = p.newExpr( - E.Identifier{ .ref = data.arg }, - value.loc, - ), - .index = assign_target, - }, value.loc), - name_as_e_string.?, - ), - ) catch bun.outOfMemory(); - p.recordUsage(data.arg); - } } + } - p.should_fold_typescript_constant_expressions = old_should_fold_typescript_constant_expressions; + p.should_fold_typescript_constant_expressions = old_should_fold_typescript_constant_expressions; - var value_stmts = ListManaged(Stmt).initCapacity(allocator, value_exprs.items.len) catch unreachable; - // Generate statements from expressions - for (value_exprs.items) |expr| { - value_stmts.appendAssumeCapacity(p.s(S.SExpr{ .value = expr }, expr.loc)); - } - value_exprs.deinit(); - try p.generateClosureForTypeScriptNamespaceOrEnum( - stmts, - stmt.loc, - data.is_export, - data.name.loc, - data.name.ref.?, - data.arg, - value_stmts.items, - all_values_are_pure, - ); - return; - }, - .s_namespace => |data| { - p.recordDeclaredSymbol(data.name.ref.?) catch unreachable; - - // Scan ahead for any variables inside this namespace. This must be done - // ahead of time before visiting any statements inside the namespace - // because we may end up visiting the uses before the declarations. - // We need to convert the uses into property accesses on the namespace. - for (data.stmts) |child_stmt| { - switch (child_stmt.data) { - .s_local => |local| { - if (local.is_export) { - p.markExportedDeclsInsideNamespace(data.arg, local.decls.slice()); - } - }, - else => {}, - } - } - - var prepend_temp_refs = PrependTempRefsOpts{ .kind = StmtsKind.fn_body }; - var prepend_list = ListManaged(Stmt).fromOwnedSlice(p.allocator, data.stmts); - - const old_enclosing_namespace_arg_ref = p.enclosing_namespace_arg_ref; - p.enclosing_namespace_arg_ref = data.arg; - p.pushScopeForVisitPass(.entry, stmt.loc) catch unreachable; - p.recordDeclaredSymbol(data.arg) catch unreachable; - try p.visitStmtsAndPrependTempRefs(&prepend_list, &prepend_temp_refs); - p.popScope(); - p.enclosing_namespace_arg_ref = old_enclosing_namespace_arg_ref; - - try p.generateClosureForTypeScriptNamespaceOrEnum( - stmts, - stmt.loc, - data.is_export, - data.name.loc, - data.name.ref.?, - data.arg, - prepend_list.items, - false, - ); - return; - }, - else => { - notimpl(); - }, + var value_stmts = ListManaged(Stmt).initCapacity(allocator, value_exprs.items.len) catch unreachable; + // Generate statements from expressions + for (value_exprs.items) |expr| { + value_stmts.appendAssumeCapacity(p.s(S.SExpr{ .value = expr }, expr.loc)); + } + value_exprs.deinit(); + try p.generateClosureForTypeScriptNamespaceOrEnum( + stmts, + stmt.loc, + data.is_export, + data.name.loc, + data.name.ref.?, + data.arg, + value_stmts.items, + all_values_are_pure, + ); + return; } + pub fn s_namespace(p: *P, stmts: *ListManaged(Stmt), stmt: *Stmt, data: *S.Namespace) !void { + p.recordDeclaredSymbol(data.name.ref.?) catch unreachable; - // if we get this far, it stays - try stmts.append(stmt.*); - } + // Scan ahead for any variables inside this namespace. This must be done + // ahead of time before visiting any statements inside the namespace + // because we may end up visiting the uses before the declarations. + // We need to convert the uses into property accesses on the namespace. + for (data.stmts) |child_stmt| { + switch (child_stmt.data) { + .s_local => |local| { + if (local.is_export) { + p.markExportedDeclsInsideNamespace(data.arg, local.decls.slice()); + } + }, + else => {}, + } + } + + var prepend_temp_refs = PrependTempRefsOpts{ .kind = StmtsKind.fn_body }; + var prepend_list = ListManaged(Stmt).fromOwnedSlice(p.allocator, data.stmts); + + const old_enclosing_namespace_arg_ref = p.enclosing_namespace_arg_ref; + p.enclosing_namespace_arg_ref = data.arg; + p.pushScopeForVisitPass(.entry, stmt.loc) catch unreachable; + p.recordDeclaredSymbol(data.arg) catch unreachable; + try p.visitStmtsAndPrependTempRefs(&prepend_list, &prepend_temp_refs); + p.popScope(); + p.enclosing_namespace_arg_ref = old_enclosing_namespace_arg_ref; + + try p.generateClosureForTypeScriptNamespaceOrEnum( + stmts, + stmt.loc, + data.is_export, + data.name.loc, + data.name.ref.?, + data.arg, + prepend_list.items, + false, + ); + return; + } + }; fn isExportToEliminate(p: *P, ref: Ref) bool { const symbol_name = p.loadNameFromRef(ref); @@ -21562,20 +21720,24 @@ fn NewParser_( return res; } + fn visitSingleStmtBlock(p: *P, stmt: Stmt, kind: StmtsKind) Stmt { + var new_stmt = stmt; + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + var stmts = ListManaged(Stmt).initCapacity(p.allocator, stmt.data.s_block.stmts.len) catch unreachable; + stmts.appendSlice(stmt.data.s_block.stmts) catch unreachable; + p.visitStmts(&stmts, kind) catch unreachable; + p.popScope(); + new_stmt.data.s_block.stmts = stmts.items; + if (p.options.features.minify_syntax) { + new_stmt = p.stmtsToSingleStmt(stmt.loc, stmts.items); + } + + return new_stmt; + } + fn visitSingleStmt(p: *P, stmt: Stmt, kind: StmtsKind) Stmt { if (stmt.data == .s_block) { - var new_stmt = stmt; - p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; - var stmts = ListManaged(Stmt).initCapacity(p.allocator, stmt.data.s_block.stmts.len) catch unreachable; - stmts.appendSlice(stmt.data.s_block.stmts) catch unreachable; - p.visitStmts(&stmts, kind) catch unreachable; - p.popScope(); - new_stmt.data.s_block.stmts = stmts.items; - if (p.options.features.minify_syntax) { - new_stmt = p.stmtsToSingleStmt(stmt.loc, stmts.items); - } - - return new_stmt; + return p.visitSingleStmtBlock(stmt, kind); } const has_if_scope = switch (stmt.data) { @@ -21940,7 +22102,7 @@ fn NewParser_( @compileError("only_scan_imports_and_do_not_visit must not run this."); } - const initial_scope = if (comptime Environment.allow_assert) p.current_scope else {}; + const initial_scope = if (comptime Environment.allow_assert) p.current_scope; { // Save the current control-flow liveness. This represents if we are @@ -23283,13 +23445,12 @@ fn NewParser_( pub fn toAST( p: *P, - input_parts: []js_ast.Part, + parts: *ListManaged(js_ast.Part), exports_kind: js_ast.ExportsKind, wrap_mode: WrapMode, hashbang: []const u8, ) !js_ast.Ast { const allocator = p.allocator; - var parts = input_parts; // if (p.options.tree_shaking and p.options.features.trim_unused_imports) { // p.treeShake(&parts, false); @@ -23307,20 +23468,20 @@ fn NewParser_( bun.assert(!p.options.tree_shaking); bun.assert(p.options.features.hot_module_reloading); - var hmr_transform_ctx = ConvertESMExportsForHmr{ .last_part = &parts[parts.len - 1] }; + var hmr_transform_ctx = ConvertESMExportsForHmr{ .last_part = &parts.items[parts.items.len - 1] }; try hmr_transform_ctx.stmts.ensureTotalCapacity(p.allocator, prealloc_count: { // get a estimate on how many statements there are going to be var count: usize = 0; - for (parts) |part| count += part.stmts.len; + for (parts.items) |part| count += part.stmts.len; break :prealloc_count count + 2; }); - for (parts) |part| { + for (parts.items) |part| { // Bake does not care about 'import =', as it handles it on it's own _ = try ImportScanner.scan(P, p, part.stmts, wrap_mode != .none, true, &hmr_transform_ctx); } - parts = try hmr_transform_ctx.finalize(p, parts); + try hmr_transform_ctx.finalize(p, parts.items); } else { // Handle import paths after the whole file has been visited because we need // symbol usage counts to be able to remove unused type-only imports in @@ -23332,7 +23493,7 @@ fn NewParser_( const begin = parts_end; // Potentially remove some statements, then filter out parts to remove any // with no statements - for (parts[begin..]) |part_| { + for (parts.items[begin..]) |part_| { var part = part_; p.import_records_for_current_part.clearRetainingCapacity(); p.declared_symbols.clearRetainingCapacity(); @@ -23363,7 +23524,7 @@ fn NewParser_( part.import_record_indices.append(p.allocator, p.import_records_for_current_part.items) catch unreachable; } - parts[parts_end] = part; + parts.items[parts_end] = part; parts_end += 1; } } @@ -23376,11 +23537,11 @@ fn NewParser_( } // leave the first part in there for namespace export when bundling - parts = parts[0..parts_end]; + parts.items.len = parts_end; // Do a second pass for exported items now that imported items are filled out. // This isn't done for HMR because it already deletes all `.s_export_clause`s - for (parts) |part| { + for (parts.items) |part| { for (part.stmts) |stmt| { switch (stmt.data) { .s_export_clause => |clause| { @@ -23418,14 +23579,14 @@ fn NewParser_( } var total_stmts_count: usize = 0; - for (parts) |part| { + for (parts.items) |part| { total_stmts_count += part.stmts.len; } const preserve_strict_mode = p.module_scope.strict_mode == .explicit_strict_mode and - !(parts.len > 0 and - parts[0].stmts.len > 0 and - parts[0].stmts[0].data == .s_directive); + !(parts.items.len > 0 and + parts.items[0].stmts.len > 0 and + parts.items[0].stmts[0].data == .s_directive); total_stmts_count += @as(usize, @intCast(@intFromBool(preserve_strict_mode))); @@ -23442,7 +23603,7 @@ fn NewParser_( remaining_stmts = remaining_stmts[1..]; } - for (parts) |part| { + for (parts.items) |part| { for (part.stmts, remaining_stmts[0..part.stmts.len]) |src, *dest| { dest.* = src; } @@ -23464,14 +23625,16 @@ fn NewParser_( ); var top_level_stmts = p.allocator.alloc(Stmt, 1) catch bun.outOfMemory(); - parts[0].stmts = top_level_stmts; top_level_stmts[0] = p.s( S.SExpr{ .value = wrapper, }, logger.Loc.Empty, ); - parts.len = 1; + + try parts.ensureUnusedCapacity(1); + parts.items.len = 1; + parts.items[0].stmts = top_level_stmts; } var top_level_symbols_to_parts = js_ast.Ast.TopLevelSymbolToParts{}; @@ -23504,7 +23667,7 @@ fn NewParser_( }; // Each part tracks the other parts it depends on within this file - for (parts, 0..) |*part, part_index| { + for (parts.items, 0..) |*part, part_index| { const decls = &part.declared_symbols; const ctx = Ctx{ .allocator = p.allocator, @@ -23530,7 +23693,7 @@ fn NewParser_( } const wrapper_ref: Ref = brk: { - if (p.options.bundle and p.needsWrapperRef(parts)) { + if (p.options.bundle and p.needsWrapperRef(parts.items)) { break :brk p.newSymbol( .other, std.fmt.allocPrint( @@ -23544,8 +23707,7 @@ fn NewParser_( break :brk Ref.None; }; - var parts_list = bun.BabyList(js_ast.Part).init(parts); - parts_list.cap = @intCast(input_parts.len); + const parts_list = bun.BabyList(js_ast.Part).fromList(parts); return .{ .runtime_imports = p.runtime_imports, @@ -23584,8 +23746,11 @@ fn NewParser_( .force_cjs_to_esm = p.unwrap_all_requires or exports_kind == .esm_with_dynamic_fallback_from_cjs, .uses_module_ref = p.symbols.items[p.module_ref.inner_index].use_count_estimate > 0, .uses_exports_ref = p.symbols.items[p.exports_ref.inner_index].use_count_estimate > 0, - .uses_require_ref = p.runtime_imports.__require != null and - p.symbols.items[p.runtime_imports.__require.?.inner_index].use_count_estimate > 0, + .uses_require_ref = if (p.options.bundle) + p.runtime_imports.__require != null and + p.symbols.items[p.runtime_imports.__require.?.inner_index].use_count_estimate > 0 + else + p.symbols.items[p.require_ref.inner_index].use_count_estimate > 0, .commonjs_module_exports_assigned_deoptimized = p.commonjs_module_exports_assigned_deoptimized, .top_level_await_keyword = p.top_level_await_keyword, .commonjs_named_exports = p.commonjs_named_exports, @@ -23684,6 +23849,7 @@ fn NewParser_( .named_imports = undefined, .named_exports = .{}, .log = log, + .stack_check = bun.StackCheck.init(), .allocator = allocator, .options = opts, .then_catch_chain = ThenCatchChain{ .next_target = nullExprData }, @@ -23977,7 +24143,7 @@ pub const ConvertESMExportsForHmr = struct { return; // do not emit a statement here }, .s_export_from => |st| stmt: { - for (st.items) |item| { + for (st.items) |*item| { const ref = item.name.ref.?; const symbol = &p.symbols.items[ref.innerIndex()]; if (symbol.namespace_alias == null) { @@ -23988,6 +24154,15 @@ pub const ConvertESMExportsForHmr = struct { }; } try ctx.visitRefToExport(p, ref, item.alias, item.name.loc, true); + + // imports and export statements have their alias + + // original_name swapped. this is likely a design bug in + // the parser but since everything uses these + // assumptions, this hack is simpler than making it + // proper + const alias = item.alias; + item.alias = item.original_name; + item.original_name = alias; } const gop = try ctx.imports_seen.getOrPut(p.allocator, st.import_record_index); @@ -24097,7 +24272,7 @@ pub const ConvertESMExportsForHmr = struct { } } - pub fn finalize(ctx: *ConvertESMExportsForHmr, p: anytype, all_parts: []js_ast.Part) ![]js_ast.Part { + pub fn finalize(ctx: *ConvertESMExportsForHmr, p: anytype, all_parts: []js_ast.Part) !void { if (ctx.export_props.items.len > 0) { try ctx.stmts.append(p.allocator, Stmt.alloc(S.SExpr, .{ .value = Expr.assign( @@ -24144,8 +24319,6 @@ pub const ConvertESMExportsForHmr = struct { ctx.last_part.stmts = ctx.stmts.items; ctx.last_part.tag = .none; - - return all_parts; } }; diff --git a/src/js_printer.zig b/src/js_printer.zig index 2963b96e65..afd7fc1810 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -6,7 +6,7 @@ const js_ast = bun.JSAst; const options = @import("options.zig"); const rename = @import("renamer.zig"); const runtime = @import("runtime.zig"); -const Lock = @import("./lock.zig").Lock; +const Lock = bun.Mutex; const Api = @import("./api/schema.zig").Api; const fs = @import("fs.zig"); const bun = @import("root").bun; @@ -207,6 +207,7 @@ pub fn quoteForJSON(text: []const u8, output_: MutableString, comptime ascii_onl pub fn writePreQuotedString(text_in: []const u8, comptime Writer: type, writer: Writer, comptime quote_char: u8, comptime ascii_only: bool, comptime json: bool, comptime encoding: strings.Encoding) !void { const text = if (comptime encoding == .utf16) @as([]const u16, @alignCast(std.mem.bytesAsSlice(u16, text_in))) else text_in; + if (comptime json and quote_char != '"') @compileError("for json, quote_char must be '\"'"); var i: usize = 0; const n: usize = text.len; while (i < n) { @@ -434,6 +435,7 @@ pub const SourceMapHandler = struct { }; pub const Options = struct { + bundling: bool = false, transform_imports: bool = true, to_commonjs_ref: Ref = Ref.None, to_esm_ref: Ref = Ref.None, @@ -475,22 +477,6 @@ pub const Options = struct { // const_values: Ast.ConstValuesMap = .{}, ts_enums: Ast.TsEnumsMap = .{}, - // TODO: remove this - // The reason for this is: - // 1. You're bundling a React component - // 2. jsx auto imports are prepended to the list of parts - // 3. The AST modification for bundling only applies to the final part - // 4. This means that it will try to add a toplevel part which is not wrapped in the arrow function, which is an error - // TypeError: $30851277 is not a function. (In '$30851277()', '$30851277' is undefined) - // at (anonymous) (0/node_modules.server.e1b5ffcd183e9551.jsb:1463:21) - // at #init_react/jsx-dev-runtime.js (0/node_modules.server.e1b5ffcd183e9551.jsb:1309:8) - // at (esm) (0/node_modules.server.e1b5ffcd183e9551.jsb:1480:30) - - // The temporary fix here is to tag a stmts ptr as the one we want to prepend to - // Then, when we're JUST about to print it, we print the body of prepend_part_value first - prepend_part_key: ?*anyopaque = null, - prepend_part_value: ?*js_ast.Part = null, - // If we're writing out a source map, this table of line start indices lets // us do binary search on to figure out what line a given AST node came from line_offset_tables: ?SourceMap.LineOffsetTable.List = null, @@ -1836,9 +1822,7 @@ fn NewPrinter( p.print("("); } - if (module_type == .esm and is_bun_platform) { - p.print("import.meta.require"); - } else if (p.options.require_ref) |ref| { + if (p.options.require_ref) |ref| { p.printSymbol(ref); } else { p.print("require"); @@ -2072,7 +2056,9 @@ fn NewPrinter( // // This is currently only used in Bun's runtime for CommonJS modules // referencing import.meta - if (comptime Environment.allow_assert) + // + // TODO: This assertion trips when using `import.meta` with `--format=cjs` + if (comptime Environment.isDebug) bun.assert(p.options.module_type == .cjs); p.printSymbol(p.options.import_meta_ref); @@ -2277,9 +2263,7 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); - if (p.options.module_type == .esm and is_bun_platform) { - p.print("import.meta.require.main"); - } else if (p.options.require_ref) |require_ref| { + if (p.options.require_ref) |require_ref| { p.printSymbol(require_ref); p.print(".main"); } else { @@ -2290,9 +2274,7 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); - if (p.options.module_type == .esm and is_bun_platform) { - p.print("import.meta.require"); - } else if (p.options.require_ref) |require_ref| { + if (p.options.require_ref) |require_ref| { p.printSymbol(require_ref); } else { p.print("require"); @@ -2302,9 +2284,7 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); - if (p.options.module_type == .esm and is_bun_platform) { - p.print("import.meta.require.resolve"); - } else if (p.options.require_ref) |require_ref| { + if (p.options.require_ref) |require_ref| { p.printSymbol(require_ref); p.print(".resolve"); } else { @@ -2331,9 +2311,7 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); - if (p.options.module_type == .esm and is_bun_platform) { - p.print("import.meta.require.resolve"); - } else if (p.options.require_ref) |require_ref| { + if (p.options.require_ref) |require_ref| { p.printSymbol(require_ref); p.print(".resolve"); } else { @@ -2550,15 +2528,6 @@ fn NewPrinter( p.printWhitespacer(ws(" => ")); var wasPrinted = false; - - // This is more efficient than creating a new Part just for the JSX auto imports when bundling - if (comptime rewrite_esm_to_cjs) { - if (@intFromPtr(p.options.prepend_part_key) > 0 and @intFromPtr(e.body.stmts.ptr) == @intFromPtr(p.options.prepend_part_key)) { - p.printTwoBlocksInOne(e.body.loc, e.body.stmts, p.options.prepend_part_value.?.stmts); - wasPrinted = true; - } - } - if (e.body.stmts.len == 1 and e.prefer_expr) { switch (e.body.stmts[0].data) { .s_return => { @@ -5803,14 +5772,24 @@ pub fn printAst( defer { imported_module_ids_list = printer.imported_module_ids; } - if (tree.prepend_part) |part| { - for (part.stmts) |stmt| { - try printer.printStmt(stmt); - if (printer.writer.getError()) {} else |err| { - return err; - } - printer.printSemicolonIfNeeded(); - } + + if (!opts.bundling and + tree.uses_require_ref and + tree.exports_kind == .esm and + opts.target == .bun) + { + // Hoist the `var {require}=import.meta;` declaration. Previously, + // `import.meta.require` was inlined into transpiled files, which + // meant calling `func.toString()` on a function with `require` + // would observe `import.meta.require` inside of the source code. + // Normally, Bun doesn't guarantee `Function.prototype.toString` + // will match the untranspiled source code, but in this case the new + // code is not valid outside of an ES module (eg, in `new Function`) + // https://github.com/oven-sh/bun/issues/15738#issuecomment-2574283514 + // + // This is never a symbol collision because `uses_require_ref` means + // `require` must be an unbound variable. + printer.print("var {require}=import.meta;"); } for (tree.parts.slice()) |part| { @@ -6003,13 +5982,13 @@ pub fn printWithWriterAndPlatform( printer.printFnArgs(func.open_parens_loc, func.args, func.flags.contains(.has_rest_arg), false); printer.printSpace(); printer.print("{\n"); - if (func.body.stmts[0].data.s_lazy_export != .e_undefined) { + if (func.body.stmts[0].data.s_lazy_export.* != .e_undefined) { printer.indent(); printer.printIndent(); printer.printSymbol(printer.options.commonjs_module_ref); printer.print(".exports = "); printer.printExpr(.{ - .data = func.body.stmts[0].data.s_lazy_export, + .data = func.body.stmts[0].data.s_lazy_export.*, .loc = func.body.stmts[0].loc, }, .comma, .{}); printer.print("; // bun .s_lazy_export\n"); @@ -6089,15 +6068,6 @@ pub fn printCommonJS( imported_module_ids_list = printer.imported_module_ids; } - if (tree.prepend_part) |part| { - for (part.stmts) |stmt| { - try printer.printStmt(stmt); - if (printer.writer.getError()) {} else |err| { - return err; - } - printer.printSemicolonIfNeeded(); - } - } for (tree.parts.slice()) |part| { for (part.stmts) |stmt| { try printer.printStmt(stmt); diff --git a/src/jsc.zig b/src/jsc.zig index 7fa5521390..ebb70db42f 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -53,6 +53,7 @@ pub const API = struct { pub const H2FrameParser = @import("./bun.js/api/bun/h2_frame_parser.zig").H2FrameParser; pub const NativeZlib = @import("./bun.js/node/node_zlib_binding.zig").SNativeZlib; pub const NativeBrotli = @import("./bun.js/node/node_zlib_binding.zig").SNativeBrotli; + pub const HTMLBundle = @import("./bun.js/api/server/HTMLBundle.zig"); }; pub const Postgres = @import("./sql/postgres.zig"); pub const DNS = @import("./bun.js/api/bun/dns_resolver.zig"); diff --git a/src/jsc_stub.zig b/src/jsc_stub.zig index 3679179f3c..34069b04a2 100644 --- a/src/jsc_stub.zig +++ b/src/jsc_stub.zig @@ -1,5 +1,4 @@ // For WASM builds -pub const is_bindgen = true; pub const C = struct {}; pub const WebCore = struct {}; pub const Jest = struct {}; diff --git a/src/json_parser.zig b/src/json_parser.zig index 4d30b2d090..c7a95ead6e 100644 --- a/src/json_parser.zig +++ b/src/json_parser.zig @@ -34,7 +34,7 @@ const G = js_ast.G; const T = js_lexer.T; const E = js_ast.E; const Stmt = js_ast.Stmt; -const Expr = js_ast.Expr; +pub const Expr = js_ast.Expr; const Binding = js_ast.Binding; const Symbol = js_ast.Symbol; const Level = js_ast.Op.Level; @@ -250,13 +250,9 @@ fn JSONLikeParser_( const DuplicateNodeType = comptime if (opts.json_warn_duplicate_keys) *HashMapPool.LinkedList.Node else void; const HashMapType = comptime if (opts.json_warn_duplicate_keys) HashMapPool.HashMap else void; - var duplicates_node: DuplicateNodeType = if (comptime opts.json_warn_duplicate_keys) - HashMapPool.get(p.allocator) - else {}; + var duplicates_node: DuplicateNodeType = if (comptime opts.json_warn_duplicate_keys) HashMapPool.get(p.allocator); - var duplicates: HashMapType = if (comptime opts.json_warn_duplicate_keys) - duplicates_node.data - else {}; + var duplicates: HashMapType = if (comptime opts.json_warn_duplicate_keys) duplicates_node.data; defer { if (comptime opts.json_warn_duplicate_keys) { @@ -500,6 +496,7 @@ pub const PackageJSONVersionChecker = struct { } if (p.has_found_name and p.has_found_version) return newExpr(E.Missing{}, loc); + has_properties = true; } @@ -775,41 +772,6 @@ pub fn parsePackageJSONUTF8( return try parser.parseExpr(false, true); } -pub fn parsePackageJSONUTF8AlwaysDecode( - source: *const logger.Source, - log: *logger.Log, - allocator: std.mem.Allocator, -) !Expr { - const len = source.contents.len; - - switch (len) { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_object_data }; - }, - // This is a fast pass I guess - 2 => { - if (strings.eqlComptime(source.contents[0..1], "\"\"") or strings.eqlComptime(source.contents[0..1], "''")) { - return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_string_data }; - } else if (strings.eqlComptime(source.contents[0..1], "{}")) { - return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_object_data }; - } else if (strings.eqlComptime(source.contents[0..1], "[]")) { - return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_array_data }; - } - }, - else => {}, - } - - var parser = try JSONLikeParser(.{ - .is_json = true, - .allow_comments = true, - .allow_trailing_commas = true, - }).init(allocator, source.*, log); - bun.assert(parser.source().contents.len > 0); - - return try parser.parseExpr(false, true); -} - const JsonResult = struct { root: Expr, indentation: Indentation = .{}, @@ -1049,8 +1011,8 @@ const js_printer = bun.js_printer; const renamer = @import("renamer.zig"); const SymbolList = [][]Symbol; -const Bundler = bun.Bundler; -const ParseResult = bun.bundler.ParseResult; +const Transpiler = bun.Transpiler; +const ParseResult = bun.transpiler.ParseResult; fn expectPrintedJSON(_contents: string, expected: string) !void { Expr.Data.Store.create(default_allocator); Stmt.Data.Store.create(default_allocator); diff --git a/src/linker.zig b/src/linker.zig index f0136706d5..dcaecf25e8 100644 --- a/src/linker.zig +++ b/src/linker.zig @@ -32,15 +32,15 @@ const ImportKind = _import_record.ImportKind; const allocators = @import("./allocators.zig"); const MimeType = @import("./http/mime_type.zig"); const resolve_path = @import("./resolver/resolve_path.zig"); -const _bundler = bun.bundler; -const Bundler = _bundler.Bundler; -const ResolveQueue = _bundler.ResolveQueue; +const _transpiler = bun.transpiler; +const Transpiler = _transpiler.Transpiler; +const ResolveQueue = _transpiler.ResolveQueue; const ResolverType = Resolver.Resolver; const ESModule = @import("./resolver/package_json.zig").ESModule; const Runtime = @import("./runtime.zig").Runtime; const URL = @import("url.zig").URL; const JSC = bun.JSC; -const PluginRunner = bun.bundler.PluginRunner; +const PluginRunner = bun.transpiler.PluginRunner; pub const CSSResolveError = error{ResolveMessage}; pub const OnImportCallback = *const fn (resolve_result: *const Resolver.Result, import_record: *ImportRecord, origin: URL) void; @@ -54,7 +54,7 @@ pub const Linker = struct { log: *logger.Log, resolve_queue: *ResolveQueue, resolver: *ResolverType, - resolve_results: *_bundler.ResolveResults, + resolve_results: *_transpiler.ResolveResults, any_needs_runtime: bool = false, runtime_import_record: ?ImportRecord = null, hashed_filenames: HashedFileNameMap, @@ -80,7 +80,7 @@ pub const Linker = struct { resolve_queue: *ResolveQueue, options: *Options.BundleOptions, resolver: *ResolverType, - resolve_results: *_bundler.ResolveResults, + resolve_results: *_transpiler.ResolveResults, fs: *Fs.FileSystem, ) ThisLinker { relative_paths_list = ImportPathsList.init(allocator); @@ -116,7 +116,7 @@ pub const Linker = struct { file_path: Fs.Path, fd: ?FileDescriptorType, ) !string { - if (Bundler.isCacheEnabled) { + if (Transpiler.isCacheEnabled) { const hashed = bun.hash(file_path.text); const hashed_result = try this.hashed_filenames.getOrPut(hashed); if (hashed_result.found_existing) { @@ -127,7 +127,7 @@ pub const Linker = struct { const modkey = try this.getModKey(file_path, fd); const hash_name = modkey.hashName(file_path.text); - if (Bundler.isCacheEnabled) { + if (Transpiler.isCacheEnabled) { const hashed = bun.hash(file_path.text); try this.hashed_filenames.put(hashed, try this.allocator.dupe(u8, hash_name)); } @@ -183,7 +183,7 @@ pub const Linker = struct { pub fn link( linker: *ThisLinker, file_path: Fs.Path, - result: *_bundler.ParseResult, + result: *_transpiler.ParseResult, origin: URL, comptime import_path_format: Options.BundleOptions.ImportPathFormat, comptime ignore_runtime: bool, @@ -603,7 +603,7 @@ pub const Linker = struct { fn whenModuleNotFound( linker: *ThisLinker, import_record: *ImportRecord, - result: *_bundler.ParseResult, + result: *_transpiler.ParseResult, comptime is_bun: bool, ) !bool { if (import_record.handles_import_errors) { diff --git a/src/linux_c.zig b/src/linux_c.zig index 32292c6426..0709ca04f4 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -1,5 +1,6 @@ const std = @import("std"); const bun = @import("root").bun; +pub extern "C" fn memmem(haystack: [*]const u8, haystacklen: usize, needle: [*]const u8, needlelen: usize) ?[*]const u8; pub const SystemErrno = enum(u8) { SUCCESS = 0, EPERM = 1, @@ -151,146 +152,6 @@ pub const SystemErrno = enum(u8) { if (code >= max) return null; return @enumFromInt(code); } - - pub fn label(this: SystemErrno) ?[:0]const u8 { - return labels.get(this) orelse null; - } - - const LabelMap = std.EnumMap(SystemErrno, [:0]const u8); - pub const labels: LabelMap = brk: { - var map: LabelMap = LabelMap.initFull(""); - - map.put(.EPERM, "Operation not permitted"); - map.put(.ENOENT, "No such file or directory"); - map.put(.ESRCH, "No such process"); - map.put(.EINTR, "Interrupted system call"); - map.put(.EIO, "I/O error"); - map.put(.ENXIO, "No such device or address"); - map.put(.E2BIG, "Argument list too long"); - map.put(.ENOEXEC, "Exec format error"); - map.put(.EBADF, "Bad file descriptor"); - map.put(.ECHILD, "No child processes"); - map.put(.EAGAIN, "Try again"); - map.put(.ENOMEM, "Out of memory"); - map.put(.EACCES, "Permission denied"); - map.put(.EFAULT, "Bad address"); - map.put(.ENOTBLK, "Block device required"); - map.put(.EBUSY, "Device or resource busy"); - map.put(.EEXIST, "File or folder exists"); - map.put(.EXDEV, "Cross-device link"); - map.put(.ENODEV, "No such device"); - map.put(.ENOTDIR, "Not a directory"); - map.put(.EISDIR, "Is a directory"); - map.put(.EINVAL, "Invalid argument"); - map.put(.ENFILE, "File table overflow"); - map.put(.EMFILE, "Too many open files"); - map.put(.ENOTTY, "Not a typewriter"); - map.put(.ETXTBSY, "Text file busy"); - map.put(.EFBIG, "File too large"); - map.put(.ENOSPC, "No space left on device"); - map.put(.ESPIPE, "Illegal seek"); - map.put(.EROFS, "Read-only file system"); - map.put(.EMLINK, "Too many links"); - map.put(.EPIPE, "Broken pipe"); - map.put(.EDOM, "Math argument out of domain of func"); - map.put(.ERANGE, "Math result not representable"); - map.put(.EDEADLK, "Resource deadlock would occur"); - map.put(.ENAMETOOLONG, "File name too long"); - map.put(.ENOLCK, "No record locks available"); - map.put(.ENOSYS, "Function not implemented"); - map.put(.ENOTEMPTY, "Directory not empty"); - map.put(.ELOOP, "Too many symbolic links encountered"); - map.put(.ENOMSG, "No message of desired type"); - map.put(.EIDRM, "Identifier removed"); - map.put(.ECHRNG, "Channel number out of range"); - map.put(.EL2NSYNC, "Level 2 not synchronized"); - map.put(.EL3HLT, "Level 3 halted"); - map.put(.EL3RST, "Level 3 reset"); - map.put(.ELNRNG, "Link number out of range"); - map.put(.EUNATCH, "Protocol driver not attached"); - map.put(.ENOCSI, "No CSI structure available"); - map.put(.EL2HLT, "Level 2 halted"); - map.put(.EBADE, "Invalid exchange"); - map.put(.EBADR, "Invalid request descriptor"); - map.put(.EXFULL, "Exchange full"); - map.put(.ENOANO, "No anode"); - map.put(.EBADRQC, "Invalid request code"); - map.put(.EBADSLT, "Invalid slot"); - map.put(.EBFONT, "Bad font file format"); - map.put(.ENOSTR, "Device not a stream"); - map.put(.ENODATA, "No data available"); - map.put(.ETIME, "Timer expired"); - map.put(.ENOSR, "Out of streams resources"); - map.put(.ENONET, "Machine is not on the network"); - map.put(.ENOPKG, "Package not installed"); - map.put(.EREMOTE, "Object is remote"); - map.put(.ENOLINK, "Link has been severed"); - map.put(.EADV, "Advertise error"); - map.put(.ESRMNT, "Srmount error"); - map.put(.ECOMM, "Communication error on send"); - map.put(.EPROTO, "Protocol error"); - map.put(.EMULTIHOP, "Multihop attempted"); - map.put(.EDOTDOT, "RFS specific error"); - map.put(.EBADMSG, "Not a data message"); - map.put(.EOVERFLOW, "Value too large for defined data type"); - map.put(.ENOTUNIQ, "Name not unique on network"); - map.put(.EBADFD, "File descriptor in bad state"); - map.put(.EREMCHG, "Remote address changed"); - map.put(.ELIBACC, "Can not access a needed shared library"); - map.put(.ELIBBAD, "Accessing a corrupted shared library"); - map.put(.ELIBSCN, "lib section in a.out corrupted"); - map.put(.ELIBMAX, "Attempting to link in too many shared libraries"); - map.put(.ELIBEXEC, "Cannot exec a shared library directly"); - map.put(.EILSEQ, "Illegal byte sequence"); - map.put(.ERESTART, "Interrupted system call should be restarted"); - map.put(.ESTRPIPE, "Streams pipe error"); - map.put(.EUSERS, "Too many users"); - map.put(.ENOTSOCK, "Socket operation on non-socket"); - map.put(.EDESTADDRREQ, "Destination address required"); - map.put(.EMSGSIZE, "Message too long"); - map.put(.EPROTOTYPE, "Protocol wrong type for socket"); - map.put(.ENOPROTOOPT, "Protocol not available"); - map.put(.EPROTONOSUPPORT, "Protocol not supported"); - map.put(.ESOCKTNOSUPPORT, "Socket type not supported"); - map.put(.ENOTSUP, "Operation not supported on transport endpoint"); - map.put(.EPFNOSUPPORT, "Protocol family not supported"); - map.put(.EAFNOSUPPORT, "Address family not supported by protocol"); - map.put(.EADDRINUSE, "Address already in use"); - map.put(.EADDRNOTAVAIL, "Cannot assign requested address"); - map.put(.ENETDOWN, "Network is down"); - map.put(.ENETUNREACH, "Network is unreachable"); - map.put(.ENETRESET, "Network dropped connection because of reset"); - map.put(.ECONNABORTED, "Software caused connection abort"); - map.put(.ECONNRESET, "Connection reset by peer"); - map.put(.ENOBUFS, "No buffer space available"); - map.put(.EISCONN, "Transport endpoint is already connected"); - map.put(.ENOTCONN, "Transport endpoint is not connected"); - map.put(.ESHUTDOWN, "Cannot send after transport endpoint shutdown"); - map.put(.ETOOMANYREFS, "Too many references: cannot splice"); - map.put(.ETIMEDOUT, "Connection timed out"); - map.put(.ECONNREFUSED, "Connection refused"); - map.put(.EHOSTDOWN, "Host is down"); - map.put(.EHOSTUNREACH, "No route to host"); - map.put(.EALREADY, "Operation already in progress"); - map.put(.EINPROGRESS, "Operation now in progress"); - map.put(.ESTALE, "Stale NFS file handle"); - map.put(.EUCLEAN, "Structure needs cleaning"); - map.put(.ENOTNAM, "Not a XENIX named type file"); - map.put(.ENAVAIL, "No XENIX semaphores available"); - map.put(.EISNAM, "Is a named type file"); - map.put(.EREMOTEIO, "Remote I/O error"); - map.put(.EDQUOT, "Quota exceeded"); - map.put(.ENOMEDIUM, "No medium found"); - map.put(.EMEDIUMTYPE, "Wrong medium type"); - map.put(.ECANCELED, "Operation Canceled"); - map.put(.ENOKEY, "Required key not available"); - map.put(.EKEYEXPIRED, "Key has expired"); - map.put(.EKEYREVOKED, "Key has been revoked"); - map.put(.EKEYREJECTED, "Key was rejected by service"); - map.put(.EOWNERDEAD, "Owner died"); - map.put(.ENOTRECOVERABLE, "State not recoverable"); - break :brk map; - }; }; pub const UV_E2BIG: i32 = @intFromEnum(SystemErrno.E2BIG); @@ -785,3 +646,63 @@ export fn sys_epoll_pwait2(epfd: i32, events: ?[*]std.os.linux.epoll_event, maxe ), ); } + +// ********************************************************************************* +// libc overrides +// ********************************************************************************* + +fn simulateLibcErrno(rc: usize) c_int { + const signed: isize = @bitCast(rc); + const int: c_int = @intCast(if (signed > -4096 and signed < 0) -signed else 0); + std.c._errno().* = int; + return if (signed > -4096 and signed < 0) -1 else int; +} + +pub export fn stat(path: [*:0]const u8, buf: *std.os.linux.Stat) c_int { + // https://git.musl-libc.org/cgit/musl/tree/src/stat/stat.c + const rc = std.os.linux.fstatat(std.os.linux.AT.FDCWD, path, buf, 0); + return simulateLibcErrno(rc); +} + +pub const stat64 = stat; +pub const lstat64 = lstat; +pub const fstat64 = fstat; +pub const fstatat64 = fstatat; + +pub export fn lstat(path: [*:0]const u8, buf: *std.os.linux.Stat) c_int { + // https://git.musl-libc.org/cgit/musl/tree/src/stat/lstat.c + const rc = std.os.linux.fstatat(std.os.linux.AT.FDCWD, path, buf, std.os.linux.AT.SYMLINK_NOFOLLOW); + return simulateLibcErrno(rc); +} + +pub export fn fstat(fd: c_int, buf: *std.os.linux.Stat) c_int { + const rc = std.os.linux.fstat(fd, buf); + return simulateLibcErrno(rc); +} + +pub export fn fstatat(dirfd: i32, path: [*:0]const u8, buf: *std.os.linux.Stat, flags: u32) c_int { + const rc = std.os.linux.fstatat(dirfd, path, buf, flags); + return simulateLibcErrno(rc); +} + +pub export fn statx(dirfd: i32, path: [*:0]const u8, flags: u32, mask: u32, buf: *std.os.linux.Statx) c_int { + const rc = std.os.linux.statx(dirfd, path, flags, mask, buf); + return simulateLibcErrno(rc); +} + +comptime { + _ = stat; + _ = stat64; + _ = lstat; + _ = lstat64; + _ = fstat; + _ = fstat64; + _ = fstatat; + _ = statx; + @export(stat, .{ .name = "stat64" }); + @export(lstat, .{ .name = "lstat64" }); + @export(fstat, .{ .name = "fstat64" }); + @export(fstatat, .{ .name = "fstatat64" }); +} + +// ********************************************************************************* diff --git a/src/lock.zig b/src/lock.zig deleted file mode 100644 index 4334986ade..0000000000 --- a/src/lock.zig +++ /dev/null @@ -1,150 +0,0 @@ -// Note(2024-10-01): there is little reason to use this over std.Thread.Mutex, -// as we have dropped old macOS versions it didnt support. Additionally, the -// Zig Standard Library has deadlock protections in debug builds. -const std = @import("std"); -const Atomic = std.atomic.Value; -const Futex = @import("./futex.zig"); - -// Credit: this is copypasta from @kprotty. Thank you @kprotty! -pub const Mutex = struct { - state: Atomic(u32) = Atomic(u32).init(UNLOCKED), // if changed update loop.c in usockets - - const UNLOCKED = 0; // if changed update loop.c in usockets - const LOCKED = 0b01; - const CONTENDED = 0b11; - const is_x86 = @import("builtin").target.cpu.arch.isX86(); - - pub fn tryAcquire(self: *Mutex) bool { - return self.acquireFast(true); - } - - pub fn acquire(self: *Mutex) void { - if (!self.acquireFast(false)) { - self.acquireSlow(); - } - } - - inline fn acquireFast(self: *Mutex, comptime strong: bool) bool { - // On x86, "lock bts" uses less i-cache & can be faster than "lock cmpxchg" below. - if (comptime is_x86) { - return self.state.bitSet(@ctz(@as(u32, LOCKED)), .acquire) == UNLOCKED; - } - - const cas_fn = comptime switch (strong) { - true => Atomic(u32).cmpxchgStrong, - else => Atomic(u32).cmpxchgWeak, - }; - - return cas_fn( - &self.state, - UNLOCKED, - LOCKED, - .acquire, - .monotonic, - ) == null; - } - - noinline fn acquireSlow(self: *Mutex) void { - // Spin a little bit on the Mutex state in the hopes that - // we can acquire it without having to call Futex.wait(). - // Give up spinning if the Mutex is contended. - // This helps acquire() latency under micro-contention. - // - var spin: u8 = 100; - while (spin > 0) : (spin -= 1) { - std.atomic.spinLoopHint(); - - switch (self.state.load(.monotonic)) { - UNLOCKED => _ = self.state.cmpxchgWeak( - UNLOCKED, - LOCKED, - .acquire, - .monotonic, - ) orelse return, - LOCKED => continue, - CONTENDED => break, - else => unreachable, // invalid Mutex state - } - } - - // Make sure the state is CONTENDED before sleeping with Futex so release() can wake us up. - // Transitioning to CONTENDED may also acquire the mutex in the process. - // - // If we sleep, we must acquire the Mutex with CONTENDED to ensure that other threads - // sleeping on the Futex having seen CONTENDED before are eventually woken up by release(). - // This unfortunately ends up in an extra Futex.wake() for the last thread but that's ok. - while (true) : (Futex.wait(&self.state, CONTENDED, null) catch unreachable) { - // On x86, "xchg" can be faster than "lock cmpxchg" below. - if (comptime is_x86) { - switch (self.state.swap(CONTENDED, .acquire)) { - UNLOCKED => return, - LOCKED, CONTENDED => continue, - else => unreachable, // invalid Mutex state - } - } - - var state = self.state.load(.monotonic); - while (state != CONTENDED) { - state = switch (state) { - UNLOCKED => self.state.cmpxchgWeak(state, CONTENDED, .acquire, .monotonic) orelse return, - LOCKED => self.state.cmpxchgWeak(state, CONTENDED, .monotonic, .monotonic) orelse break, - CONTENDED => unreachable, // checked above - else => unreachable, // invalid Mutex state - }; - } - } - } - - pub fn release(self: *Mutex) void { - switch (self.state.swap(UNLOCKED, .release)) { - UNLOCKED => unreachable, // released without being acquired - LOCKED => {}, - CONTENDED => Futex.wake(&self.state, 1), - else => unreachable, // invalid Mutex state - } - } -}; - -pub const Lock = struct { - mutex: Mutex = .{}, - - pub inline fn lock(this: *Lock) void { - this.mutex.acquire(); - } - - pub inline fn unlock(this: *Lock) void { - this.mutex.release(); - } - - pub inline fn releaseAssertUnlocked(this: *Lock, comptime message: []const u8) void { - if (this.mutex.state.load(.monotonic) != 0) { - @panic(message); - } - } - - pub inline fn assertUnlocked(this: *Lock) void { - if (std.debug.runtime_safety) { - if (this.mutex.state.load(.monotonic) != 0) { - @panic("Mutex is expected to be unlocked"); - } - } - } - - pub inline fn assertLocked(this: *Lock) void { - if (std.debug.runtime_safety) { - if (this.mutex.state.load(.monotonic) == 0) { - @panic("Mutex is expected to be locked"); - } - } - } -}; - -pub fn spinCycle() void {} - -export fn Bun__lock(lock: *Lock) void { - lock.lock(); -} - -export fn Bun__unlock(lock: *Lock) void { - lock.unlock(); -} diff --git a/src/logger.zig b/src/logger.zig index 77234eda66..cddd6d75e9 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -116,6 +116,15 @@ pub const Location = struct { // TODO: document or remove offset: usize = 0, + pub fn memoryCost(this: *const Location) usize { + var cost: usize = 0; + cost += this.file.len; + cost += this.namespace.len; + if (this.line_text) |text| cost += text.len; + if (this.suggestion) |text| cost += text.len; + return cost; + } + pub fn count(this: Location, builder: *StringBuilder) void { builder.count(this.file); builder.count(this.namespace); @@ -214,6 +223,15 @@ pub const Data = struct { text: string, location: ?Location = null, + pub fn memoryCost(this: *const Data) usize { + var cost: usize = 0; + cost += this.text.len; + if (this.location) |*loc| { + cost += loc.memoryCost(); + } + return cost; + } + pub fn deinit(d: *Data, allocator: std.mem.Allocator) void { if (d.location) |*loc| { loc.deinit(allocator); @@ -399,6 +417,15 @@ pub const Msg = struct { notes: []Data = &.{}, redact_sensitive_information: bool = false, + pub fn memoryCost(this: *const Msg) usize { + var cost: usize = 0; + cost += this.data.memoryCost(); + for (this.notes) |*note| { + cost += note.memoryCost(); + } + return cost; + } + pub fn fromJS(allocator: std.mem.Allocator, globalObject: *bun.JSC.JSGlobalObject, file: string, err: bun.JSC.JSValue) OOM!Msg { var zig_exception_holder: bun.JSC.ZigException.Holder = bun.JSC.ZigException.Holder.init(); if (err.toError()) |value| { @@ -608,6 +635,14 @@ pub const Log = struct { clone_line_text: bool = false, + pub fn memoryCost(this: *const Log) usize { + var cost: usize = 0; + for (this.msgs.items) |msg| { + cost += msg.memoryCost(); + } + return cost; + } + pub inline fn hasErrors(this: *const Log) bool { return this.errors > 0; } @@ -742,35 +777,15 @@ pub const Log = struct { } /// unlike toJS, this always produces an AggregateError object - pub fn toJSAggregateError(this: Log, global: *JSC.JSGlobalObject, message: []const u8) JSC.JSValue { - const msgs: []const Msg = this.msgs.items; - - // TODO: remove arbitrary upper limit. cannot use the heap because - // values could be GC'd. to do this correctly, expose a binding that - // allows creating an AggregateError using an array - var errors_stack: [256]JSC.JSValue = undefined; - - const count = @min(msgs.len, errors_stack.len); - - for (msgs[0..count], 0..) |msg, i| { - errors_stack[i] = switch (msg.metadata) { - .build => JSC.BuildMessage.create(global, bun.default_allocator, msg), - .resolve => JSC.ResolveMessage.create(global, bun.default_allocator, msg, ""), - }; - } - - const out = JSC.ZigString.init(message); - return global.createAggregateError(errors_stack[0..count], &out); + pub fn toJSAggregateError(this: Log, global: *JSC.JSGlobalObject, message: bun.String) JSC.JSValue { + return global.createAggregateErrorWithArray(message, this.toJSArray(global, bun.default_allocator)); } pub fn toJSArray(this: Log, global: *JSC.JSGlobalObject, allocator: std.mem.Allocator) JSC.JSValue { const msgs: []const Msg = this.msgs.items; - const errors_stack: [256]*anyopaque = undefined; - const count = @as(u16, @intCast(@min(msgs.len, errors_stack.len))); - var arr = JSC.JSValue.createEmptyArray(global, count); - - for (msgs[0..count], 0..) |msg, i| { + const arr = JSC.JSValue.createEmptyArray(global, msgs.len); + for (msgs, 0..) |msg, i| { arr.putIndex(global, @as(u32, @intCast(i)), msg.toJS(global, allocator)); } @@ -1004,6 +1019,21 @@ pub const Log = struct { }); } + // Use a bun.sys.Error's message in addition to some extra context. + pub fn addSysError(log: *Log, alloc: std.mem.Allocator, e: bun.sys.Error, comptime fmt: string, args: anytype) OOM!void { + const tag_name, const sys_errno = e.getErrorCodeTagName() orelse { + try log.addErrorFmt(null, Loc.Empty, alloc, fmt, args); + return; + }; + try log.addErrorFmt( + null, + Loc.Empty, + alloc, + "{s}: " ++ fmt, + .{bun.sys.coreutils_error_map.get(sys_errno) orelse tag_name} ++ args, + ); + } + pub fn addZigErrorWithNote(log: *Log, allocator: std.mem.Allocator, err: anyerror, comptime noteFmt: string, args: anytype) OOM!void { @setCold(true); log.errors += 1; @@ -1505,6 +1535,48 @@ pub const Source = struct { .column_count = column_number, }; } + pub fn lineColToByteOffset(source_contents: []const u8, start_line: usize, start_col: usize, line: usize, col: usize) ?usize { + var iter_ = strings.CodepointIterator{ + .bytes = source_contents, + .i = 0, + }; + var iter = strings.CodepointIterator.Cursor{}; + + var line_count: usize = start_line; + var column_number: usize = start_col; + + _ = iter_.next(&iter); + while (true) { + const c = iter.c; + if (!iter_.next(&iter)) break; + switch (c) { + '\n' => { + column_number = 1; + line_count += 1; + }, + + '\r' => { + column_number = 1; + line_count += 1; + if (iter.c == '\n') { + _ = iter_.next(&iter); + } + }, + + 0x2028, 0x2029 => { + line_count += 1; + column_number = 1; + }, + else => { + column_number += 1; + }, + } + + if (line_count == line and column_number == col) return iter.i; + if (line_count > line) return null; + } + return null; + } }; pub fn rangeData(source: ?*const Source, r: Range, text: string) Data { diff --git a/src/main.zig b/src/main.zig index 9c4df6072f..d3b73b7ee5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -44,7 +44,7 @@ pub fn main() void { if (Environment.isX64 and Environment.enableSIMD and Environment.isPosix) { bun_warn_avx_missing(@import("./cli/upgrade_command.zig").Version.Bun__githubBaselineURL.ptr); } - + bun.StackCheck.configureThread(); bun.CLI.Cli.start(bun.default_allocator); bun.Global.exit(0); } diff --git a/src/main_api.zig b/src/main_api.zig deleted file mode 100644 index b3aead9658..0000000000 --- a/src/main_api.zig +++ /dev/null @@ -1,11 +0,0 @@ -const Api = @import("./api/schema.zig").Api; -const Options = @import("./options.zig"); -var options: Options.BundleOptions = undefined; - -export fn init() void { - if (!alloc.needs_setup) { - return; - } -} - -export fn setOptions(options_ptr: [*c]u8, options_len: c_int) void {} diff --git a/src/main_wasm.zig b/src/main_wasm.zig index 3a1cfc5c83..f94e2bc914 100644 --- a/src/main_wasm.zig +++ b/src/main_wasm.zig @@ -205,7 +205,7 @@ export fn init(heapsize: u32) void { buffer_writer = writer.ctx; } } -const Arena = @import("./mimalloc_arena.zig").Arena; +const Arena = @import("./allocators/mimalloc_arena.zig").Arena; var log: Logger.Log = undefined; diff --git a/src/multi_array_list.zig b/src/multi_array_list.zig index 8a85525034..46fbe6a936 100644 --- a/src/multi_array_list.zig +++ b/src/multi_array_list.zig @@ -562,6 +562,10 @@ pub fn MultiArrayList(comptime T: type) type { return self.bytes[0..capacityInBytes(self.capacity)]; } + pub fn memoryCost(self: Self) usize { + return capacityInBytes(self.capacity); + } + pub fn zero(self: Self) void { @memset(self.allocatedBytes(), 0); } diff --git a/src/napi/js_native_api_types.h b/src/napi/js_native_api_types.h index 2cfe45c574..9cad56ee2a 100644 --- a/src/napi/js_native_api_types.h +++ b/src/napi/js_native_api_types.h @@ -93,12 +93,10 @@ typedef enum { napi_would_deadlock // unused } napi_status; // Note: when adding a new enum value to `napi_status`, please also update -// * `const int last_status` in the definition of `napi_get_last_error_info()' -// in file js_native_api_v8.cc. -// * `const char* error_messages[]` in file js_native_api_v8.cc with a brief +// * `constexpr int last_status` in the definition of `napi_get_last_error_info()' +// in file napi.cpp. +// * `const char* error_messages[]` in file napi.cpp with a brief // message explaining the error. -// * the definition of `napi_status` in doc/api/n-api.md to reflect the newly -// added value(s). typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info); typedef void (*napi_finalize)(napi_env env, void *finalize_data, diff --git a/src/napi/napi.zig b/src/napi/napi.zig index d329b68c89..ff738aa8a0 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -2,7 +2,7 @@ const std = @import("std"); const JSC = bun.JSC; const strings = bun.strings; const bun = @import("root").bun; -const Lock = @import("../lock.zig").Lock; +const Lock = bun.Mutex; const JSValue = JSC.JSValue; const ZigString = JSC.ZigString; const TODO_EXCEPTION: JSC.C.ExceptionRef = null; @@ -11,59 +11,54 @@ const Channel = @import("../sync.zig").Channel; const log = bun.Output.scoped(.napi, false); -// These wrappers exist so we can set a breakpoint in lldb -fn invalidArg() napi_status { - if (comptime bun.Environment.allow_assert) { - log("invalid arg", .{}); - } - return .invalid_arg; -} - -fn genericFailure() napi_status { - if (comptime bun.Environment.allow_assert) { - log("generic failure", .{}); - } - return .generic_failure; -} const Async = bun.Async; -pub const napi_env = *JSC.JSGlobalObject; -pub const Ref = opaque { - pub fn create(globalThis: *JSC.JSGlobalObject, value: JSValue) *Ref { - JSC.markBinding(@src()); - var ref: *Ref = undefined; - bun.assert( - napi_create_reference( - globalThis, - value, - 1, - &ref, - ) == .ok, - ); - if (comptime bun.Environment.isDebug) { - bun.assert(ref.get() == value); +/// Actually a JSGlobalObject +pub const NapiEnv = opaque { + pub fn fromJS(global: *JSC.JSGlobalObject) *NapiEnv { + return @ptrCast(global); + } + + pub fn toJS(self: *NapiEnv) *JSC.JSGlobalObject { + return @ptrCast(self); + } + + extern fn napi_set_last_error(env: napi_env, status: NapiStatus) napi_status; + + /// Convert err to an extern napi_status, and store the error code in env so that it can be + /// accessed by napi_get_last_error_info + pub fn setLastError(self: *NapiEnv, err: NapiStatus) napi_status { + return napi_set_last_error(self, err); + } + + /// Convenience wrapper for setLastError(.ok) + pub fn ok(self: *NapiEnv) napi_status { + return self.setLastError(.ok); + } + + /// These wrappers exist for convenience and so we can set a breakpoint in lldb + pub fn invalidArg(self: *NapiEnv) napi_status { + if (comptime bun.Environment.allow_assert) { + log("invalid arg", .{}); } - return ref; + return self.setLastError(.invalid_arg); } - pub fn get(ref: *Ref) JSValue { - JSC.markBinding(@src()); - return napi_get_reference_value_internal(ref); + pub fn genericFailure(self: *NapiEnv) napi_status { + if (comptime bun.Environment.allow_assert) { + log("generic failure", .{}); + } + return self.setLastError(.generic_failure); } - - pub fn destroy(ref: *Ref) void { - JSC.markBinding(@src()); - napi_delete_reference_internal(ref); - } - - pub fn set(this: *Ref, value: JSC.JSValue) void { - JSC.markBinding(@src()); - napi_set_ref(this, value); - } - - extern fn napi_delete_reference_internal(ref: *Ref) void; - extern fn napi_set_ref(ref: *Ref, value: JSC.JSValue) void; }; + +pub const napi_env = *NapiEnv; + +/// Contents are not used by any Zig code +pub const Ref = opaque {}; + +pub const napi_ref = *Ref; + pub const NapiHandleScope = opaque { pub extern fn NapiHandleScope__open(globalObject: *JSC.JSGlobalObject, escapable: bool) ?*NapiHandleScope; pub extern fn NapiHandleScope__close(globalObject: *JSC.JSGlobalObject, current: ?*NapiHandleScope) void; @@ -73,20 +68,20 @@ pub const NapiHandleScope = opaque { /// Create a new handle scope in the given environment, or return null if creating one now is /// unsafe (i.e. inside a finalizer) pub fn open(env: napi_env, escapable: bool) ?*NapiHandleScope { - return NapiHandleScope__open(env, escapable); + return NapiHandleScope__open(env.toJS(), escapable); } /// Closes the given handle scope, releasing all values inside it, if it is safe to do so. /// Asserts that self is the current handle scope in env. pub fn close(self: ?*NapiHandleScope, env: napi_env) void { - NapiHandleScope__close(env, self); + NapiHandleScope__close(env.toJS(), self); } /// Place a value in the handle scope. Must be done while returning any JS value into NAPI /// callbacks, as the value must remain alive as long as the handle scope is active, even if the /// native module doesn't keep it visible on the stack. pub fn append(env: napi_env, value: JSC.JSValue) void { - NapiHandleScope__append(env, value); + NapiHandleScope__append(env.toJS(), value); } /// Move a value from the current handle scope (which must be escapable) to the reserved escape @@ -192,7 +187,8 @@ pub const napi_typedarray_type = enum(c_uint) { return this.toJSType().toC(); } }; -pub const napi_status = enum(c_uint) { + +pub const NapiStatus = enum(c_uint) { ok = 0, invalid_arg = 1, object_expected = 2, @@ -216,6 +212,12 @@ pub const napi_status = enum(c_uint) { detachable_arraybuffer_expected = 20, would_deadlock = 21, }; + +/// This is not an `enum` so that the enum values cannot be trivially returned from NAPI functions, +/// as that would skip storing the last error code. You should wrap return values in a call to +/// napi_env.setLastError. +pub const napi_status = c_uint; + pub const napi_callback = ?*const fn (napi_env, napi_callback_info) callconv(.C) napi_value; /// expects `napi_env`, `callback_data`, `context` @@ -248,99 +250,99 @@ pub extern fn napi_get_last_error_info(env: napi_env, result: [*c][*c]const napi pub export fn napi_get_undefined(env: napi_env, result_: ?*napi_value) napi_status { log("napi_get_undefined", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.set(env, JSValue.jsUndefined()); - return .ok; + return env.ok(); } pub export fn napi_get_null(env: napi_env, result_: ?*napi_value) napi_status { log("napi_get_null", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.set(env, JSValue.jsNull()); - return .ok; + return env.ok(); } pub extern fn napi_get_global(env: napi_env, result: *napi_value) napi_status; pub export fn napi_get_boolean(env: napi_env, value: bool, result_: ?*napi_value) napi_status { log("napi_get_boolean", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.set(env, JSValue.jsBoolean(value)); - return .ok; + return env.ok(); } pub export fn napi_create_array(env: napi_env, result_: ?*napi_value) napi_status { log("napi_create_array", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; - result.set(env, JSValue.createEmptyArray(env, 0)); - return .ok; + result.set(env, JSValue.createEmptyArray(env.toJS(), 0)); + return env.ok(); } pub export fn napi_create_array_with_length(env: napi_env, length: usize, result_: ?*napi_value) napi_status { log("napi_create_array_with_length", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; // JSC createEmptyArray takes u32 // Node and V8 convert out-of-bounds array sizes to 0 const len = std.math.cast(u32, length) orelse 0; - const array = JSC.JSValue.createEmptyArray(env, len); + const array = JSC.JSValue.createEmptyArray(env.toJS(), len); array.ensureStillAlive(); result.set(env, array); - return .ok; + return env.ok(); } pub extern fn napi_create_double(_: napi_env, value: f64, result: *napi_value) napi_status; pub export fn napi_create_int32(env: napi_env, value: i32, result_: ?*napi_value) napi_status { log("napi_create_int32", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.set(env, JSValue.jsNumber(value)); - return .ok; + return env.ok(); } pub export fn napi_create_uint32(env: napi_env, value: u32, result_: ?*napi_value) napi_status { log("napi_create_uint32", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.set(env, JSValue.jsNumber(value)); - return .ok; + return env.ok(); } pub export fn napi_create_int64(env: napi_env, value: i64, result_: ?*napi_value) napi_status { log("napi_create_int64", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.set(env, JSValue.jsNumber(value)); - return .ok; + return env.ok(); } pub export fn napi_create_string_latin1(env: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { const result: *napi_value = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const slice: []const u8 = brk: { if (NAPI_AUTO_LENGTH == length) { break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(str)), 0); } else if (length > std.math.maxInt(u32)) { - return invalidArg(); + return env.invalidArg(); } if (str) |ptr| break :brk ptr[0..length]; - return invalidArg(); + return env.invalidArg(); }; log("napi_create_string_latin1: {s}", .{slice}); if (slice.len == 0) { - result.set(env, bun.String.empty.toJS(env)); - return .ok; + result.set(env, bun.String.empty.toJS(env.toJS())); + return env.ok(); } var string, const bytes = bun.String.createUninitialized(.latin1, slice.len); @@ -348,69 +350,66 @@ pub export fn napi_create_string_latin1(env: napi_env, str: ?[*]const u8, length @memcpy(bytes, slice); - result.set(env, string.toJS(env)); - return .ok; + result.set(env, string.toJS(env.toJS())); + return env.ok(); } pub export fn napi_create_string_utf8(env: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { const result: *napi_value = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const slice: []const u8 = brk: { if (NAPI_AUTO_LENGTH == length) { break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(str)), 0); } else if (length > std.math.maxInt(u32)) { - return invalidArg(); + return env.invalidArg(); } if (str) |ptr| break :brk ptr[0..length]; - return invalidArg(); + return env.invalidArg(); }; log("napi_create_string_utf8: {s}", .{slice}); - var string = bun.String.createUTF8(slice); - if (string.tag == .Dead) { - return .generic_failure; + const globalObject = env.toJS(); + const string = bun.String.createUTF8ForJS(globalObject, slice); + if (globalObject.hasException()) { + return env.setLastError(.pending_exception); } - - defer string.deref(); - result.set(env, string.toJS(env)); - return .ok; + result.set(env, string); + return env.ok(); } pub export fn napi_create_string_utf16(env: napi_env, str: ?[*]const char16_t, length: usize, result_: ?*napi_value) napi_status { const result: *napi_value = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const slice: []const u16 = brk: { if (NAPI_AUTO_LENGTH == length) { break :brk bun.sliceTo(@as([*:0]const u16, @ptrCast(str)), 0); } else if (length > std.math.maxInt(u32)) { - return invalidArg(); + return env.invalidArg(); } if (str) |ptr| break :brk ptr[0..length]; - return invalidArg(); + return env.invalidArg(); }; if (comptime bun.Environment.allow_assert) log("napi_create_string_utf16: {d} {any}", .{ slice.len, bun.fmt.FormatUTF16{ .buf = slice[0..@min(slice.len, 512)] } }); if (slice.len == 0) { - result.set(env, bun.String.empty.toJS(env)); + result.set(env, bun.String.empty.toJS(env.toJS())); } var string, const chars = bun.String.createUninitialized(.utf16, slice.len); - defer string.deref(); - @memcpy(chars, slice); - result.set(env, string.toJS(env)); - return .ok; + result.set(env, string.transferToJS(env.toJS())); + return env.ok(); } pub extern fn napi_create_symbol(env: napi_env, description: napi_value, result: *napi_value) napi_status; pub extern fn napi_create_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; @@ -421,15 +420,15 @@ pub extern fn napi_get_value_double(env: napi_env, value: napi_value, result: *f pub extern fn napi_get_value_int32(_: napi_env, value_: napi_value, result: ?*i32) napi_status; pub extern fn napi_get_value_uint32(_: napi_env, value_: napi_value, result_: ?*u32) napi_status; pub extern fn napi_get_value_int64(_: napi_env, value_: napi_value, result_: ?*i64) napi_status; -pub export fn napi_get_value_bool(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_get_value_bool(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_get_value_bool", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); result.* = value.to(bool); - return .ok; + return env.ok(); } inline fn maybeAppendNull(ptr: anytype, doit: bool) void { if (doit) { @@ -442,7 +441,7 @@ pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, bu defer value.ensureStillAlive(); const buf_ptr = @as(?[*:0]u8, @ptrCast(buf_ptr_)); - const str = value.toBunString(env); + const str = value.toBunString(env.toJS()); defer str.deref(); var buf = buf_ptr orelse { @@ -450,7 +449,7 @@ pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, bu result.* = str.latin1ByteLength(); } - return .ok; + return env.ok(); }; if (str.isEmpty()) { @@ -459,7 +458,7 @@ pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, bu } buf[0] = 0; - return .ok; + return env.ok(); } var buf_ = buf[0..bufsize]; @@ -470,7 +469,7 @@ pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, bu if (result_ptr) |result| { result.* = 0; } - return .ok; + return env.ok(); } } const written = str.encodeInto(buf_, .latin1) catch unreachable; @@ -482,7 +481,7 @@ pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, bu buf[written] = 0; } - return .ok; + return env.ok(); } /// Copies a JavaScript string into a UTF-8 string buffer. The result is the @@ -498,7 +497,7 @@ pub export fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf log("napi_get_value_string_utf16", .{}); const value = value_.get(); defer value.ensureStillAlive(); - const str = value.toBunString(env); + const str = value.toBunString(env.toJS()); defer str.deref(); var buf = buf_ptr orelse { @@ -506,7 +505,7 @@ pub export fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf result.* = str.utf16ByteLength(); } - return .ok; + return env.ok(); }; if (str.isEmpty()) { @@ -515,7 +514,7 @@ pub export fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf } buf[0] = 0; - return .ok; + return env.ok(); } var buf_ = buf[0..bufsize]; @@ -526,7 +525,7 @@ pub export fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf if (result_ptr) |result| { result.* = 0; } - return .ok; + return env.ok(); } } @@ -541,47 +540,47 @@ pub export fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf buf[written] = 0; } - return .ok; + return env.ok(); } pub export fn napi_coerce_to_bool(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { log("napi_coerce_to_bool", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); - result.set(env, JSValue.jsBoolean(value.coerce(bool, env))); - return .ok; + result.set(env, JSValue.jsBoolean(value.coerce(bool, env.toJS()))); + return env.ok(); } pub export fn napi_coerce_to_number(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { log("napi_coerce_to_number", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); - result.set(env, JSC.JSValue.jsNumber(JSC.C.JSValueToNumber(env.ref(), value.asObjectRef(), TODO_EXCEPTION))); - return .ok; + result.set(env, JSC.JSValue.jsNumber(JSC.C.JSValueToNumber(env.toJS().ref(), value.asObjectRef(), TODO_EXCEPTION))); + return env.ok(); } pub export fn napi_coerce_to_object(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { log("napi_coerce_to_object", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); - result.set(env, JSValue.c(JSC.C.JSValueToObject(env.ref(), value.asObjectRef(), TODO_EXCEPTION))); - return .ok; + result.set(env, JSValue.c(JSC.C.JSValueToObject(env.toJS().ref(), value.asObjectRef(), TODO_EXCEPTION))); + return env.ok(); } pub export fn napi_get_prototype(env: napi_env, object_: napi_value, result_: ?*napi_value) napi_status { log("napi_get_prototype", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const object = object_.get(); if (!object.isObject()) { - return .object_expected; + return env.setLastError(.object_expected); } - result.set(env, JSValue.c(JSC.C.JSObjectGetPrototype(env.ref(), object.asObjectRef()))); - return .ok; + result.set(env, JSValue.c(JSC.C.JSObjectGetPrototype(env.toJS().ref(), object.asObjectRef()))); + return env.ok(); } // TODO: bind JSC::ownKeys // pub export fn napi_get_property_names(env: napi_env, object: napi_value, result: *napi_value) napi_status { @@ -597,74 +596,74 @@ pub export fn napi_set_element(env: napi_env, object_: napi_value, index: c_uint const object = object_.get(); const value = value_.get(); if (!object.jsType().isIndexable()) { - return .array_expected; + return env.setLastError(.array_expected); } if (value == .zero) - return invalidArg(); - JSC.C.JSObjectSetPropertyAtIndex(env.ref(), object.asObjectRef(), index, value.asObjectRef(), TODO_EXCEPTION); - return .ok; + return env.invalidArg(); + JSC.C.JSObjectSetPropertyAtIndex(env.toJS().ref(), object.asObjectRef(), index, value.asObjectRef(), TODO_EXCEPTION); + return env.ok(); } pub export fn napi_has_element(env: napi_env, object_: napi_value, index: c_uint, result_: ?*bool) napi_status { log("napi_has_element", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const object = object_.get(); if (!object.jsType().isIndexable()) { - return .array_expected; + return env.setLastError(.array_expected); } - result.* = object.getLength(env) > index; - return .ok; + result.* = object.getLength(env.toJS()) > index; + return env.ok(); } pub extern fn napi_get_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_delete_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_define_properties(env: napi_env, object: napi_value, property_count: usize, properties: [*c]const napi_property_descriptor) napi_status; -pub export fn napi_is_array(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_array(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_array", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); result.* = value.jsType().isArray(); - return .ok; + return env.ok(); } pub export fn napi_get_array_length(env: napi_env, value_: napi_value, result_: [*c]u32) napi_status { log("napi_get_array_length", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); if (!value.jsType().isArray()) { - return .array_expected; + return env.setLastError(.array_expected); } - result.* = @as(u32, @truncate(value.getLength(env))); - return .ok; + result.* = @as(u32, @truncate(value.getLength(env.toJS()))); + return env.ok(); } pub export fn napi_strict_equals(env: napi_env, lhs_: napi_value, rhs_: napi_value, result_: ?*bool) napi_status { log("napi_strict_equals", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const lhs, const rhs = .{ lhs_.get(), rhs_.get() }; // there is some nuance with NaN here i'm not sure about - result.* = lhs.isSameValue(rhs, env); - return .ok; + result.* = lhs.isSameValue(rhs, env.toJS()); + return env.ok(); } pub extern fn napi_call_function(env: napi_env, recv: napi_value, func: napi_value, argc: usize, argv: [*c]const napi_value, result: *napi_value) napi_status; pub extern fn napi_new_instance(env: napi_env, constructor: napi_value, argc: usize, argv: [*c]const napi_value, result_: ?*napi_value) napi_status; pub export fn napi_instanceof(env: napi_env, object_: napi_value, constructor_: napi_value, result_: ?*bool) napi_status { log("napi_instanceof", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const object, const constructor = .{ object_.get(), constructor_.get() }; // TODO: does this throw object_expected in node? - result.* = object.isObject() and object.isInstanceOf(env, constructor); - return .ok; + result.* = object.isObject() and object.isInstanceOf(env.toJS(), constructor); + return env.ok(); } pub extern fn napi_get_cb_info(env: napi_env, cbinfo: napi_callback_info, argc: [*c]usize, argv: *napi_value, this_arg: *napi_value, data: [*]*anyopaque) napi_status; pub extern fn napi_get_new_target(env: napi_env, cbinfo: napi_callback_info, result: *napi_value) napi_status; @@ -678,26 +677,26 @@ pub extern fn napi_define_class( properties: [*c]const napi_property_descriptor, result: *napi_value, ) napi_status; -pub extern fn napi_wrap(env: napi_env, js_object: napi_value, native_object: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: [*]*Ref) napi_status; +pub extern fn napi_wrap(env: napi_env, js_object: napi_value, native_object: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *napi_ref) napi_status; pub extern fn napi_unwrap(env: napi_env, js_object: napi_value, result: [*]*anyopaque) napi_status; pub extern fn napi_remove_wrap(env: napi_env, js_object: napi_value, result: [*]*anyopaque) napi_status; pub extern fn napi_create_object(env: napi_env, result: *napi_value) napi_status; pub extern fn napi_create_external(env: napi_env, data: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *napi_value) napi_status; pub extern fn napi_get_value_external(env: napi_env, value: napi_value, result: [*]*anyopaque) napi_status; -pub extern fn napi_create_reference(env: napi_env, value: napi_value, initial_refcount: u32, result: **Ref) napi_status; -pub extern fn napi_delete_reference(env: napi_env, ref: *Ref) napi_status; -pub extern fn napi_reference_ref(env: napi_env, ref: *Ref, result: [*c]u32) napi_status; -pub extern fn napi_reference_unref(env: napi_env, ref: *Ref, result: [*c]u32) napi_status; -pub extern fn napi_get_reference_value(env: napi_env, ref: *Ref, result: *napi_value) napi_status; -pub extern fn napi_get_reference_value_internal(ref: *Ref) JSC.JSValue; +pub extern fn napi_create_reference(env: napi_env, value: napi_value, initial_refcount: u32, result: *napi_ref) napi_status; +pub extern fn napi_delete_reference(env: napi_env, ref: napi_ref) napi_status; +pub extern fn napi_reference_ref(env: napi_env, ref: napi_ref, result: [*c]u32) napi_status; +pub extern fn napi_reference_unref(env: napi_env, ref: napi_ref, result: [*c]u32) napi_status; +pub extern fn napi_get_reference_value(env: napi_env, ref: napi_ref, result: *napi_value) napi_status; +pub extern fn napi_get_reference_value_internal(ref: napi_ref) JSC.JSValue; pub export fn napi_open_handle_scope(env: napi_env, result_: ?*napi_handle_scope) napi_status { log("napi_open_handle_scope", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.* = NapiHandleScope.open(env, false); - return .ok; + return env.ok(); } pub export fn napi_close_handle_scope(env: napi_env, handle_scope: napi_handle_scope) napi_status { @@ -706,32 +705,32 @@ pub export fn napi_close_handle_scope(env: napi_env, handle_scope: napi_handle_s scope.close(env); } - return .ok; + return env.ok(); } // we don't support async contexts pub export fn napi_async_init(env: napi_env, _: napi_value, _: napi_value, async_ctx: **anyopaque) napi_status { log("napi_async_init", .{}); async_ctx.* = env; - return .ok; + return env.ok(); } // we don't support async contexts -pub export fn napi_async_destroy(_: napi_env, _: *anyopaque) napi_status { +pub export fn napi_async_destroy(env: napi_env, _: *anyopaque) napi_status { log("napi_async_destroy", .{}); - return .ok; + return env.ok(); } // this is just a regular function call pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv_: napi_value, func_: napi_value, arg_count: usize, args: ?[*]const napi_value, maybe_result: ?*napi_value) napi_status { log("napi_make_callback", .{}); const recv, const func = .{ recv_.get(), func_.get() }; - if (func.isEmptyOrUndefinedOrNull() or !func.isCallable(env.vm())) { - return .function_expected; + if (func.isEmptyOrUndefinedOrNull() or !func.isCallable(env.toJS().vm())) { + return env.setLastError(.function_expected); } const res = func.call( - env, + env.toJS(), if (recv != .zero) recv else @@ -741,7 +740,7 @@ pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv_: napi_value else &.{}, ) catch |err| // TODO: handle errors correctly - env.takeException(err); + env.toJS().takeException(err); if (maybe_result) |result| { result.set(env, res); @@ -749,10 +748,10 @@ pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv_: napi_value // TODO: this is likely incorrect if (res.isAnyError()) { - return .pending_exception; + return env.setLastError(.pending_exception); } - return .ok; + return env.ok(); } // Sometimes shared libraries reference symbols which are not used @@ -775,62 +774,62 @@ fn notImplementedYet(comptime name: []const u8) void { pub export fn napi_open_escapable_handle_scope(env: napi_env, result_: ?*napi_escapable_handle_scope) napi_status { log("napi_open_escapable_handle_scope", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.* = NapiHandleScope.open(env, true); - return .ok; + return env.ok(); } pub export fn napi_close_escapable_handle_scope(env: napi_env, scope: napi_escapable_handle_scope) napi_status { log("napi_close_escapable_handle_scope", .{}); if (scope) |s| { s.close(env); } - return .ok; + return env.ok(); } -pub export fn napi_escape_handle(_: napi_env, scope_: napi_escapable_handle_scope, escapee: napi_value, result_: ?*napi_value) napi_status { +pub export fn napi_escape_handle(env: napi_env, scope_: napi_escapable_handle_scope, escapee: napi_value, result_: ?*napi_value) napi_status { log("napi_escape_handle", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const scope = scope_ orelse { - return invalidArg(); + return env.invalidArg(); }; - scope.escape(escapee.get()) catch return .escape_called_twice; + scope.escape(escapee.get()) catch return env.setLastError(.escape_called_twice); result.* = escapee; - return .ok; + return env.ok(); } pub extern fn napi_type_tag_object(_: napi_env, _: napi_value, _: [*c]const napi_type_tag) napi_status; pub extern fn napi_check_object_type_tag(_: napi_env, _: napi_value, _: [*c]const napi_type_tag, _: *bool) napi_status; // do nothing for both of these -pub export fn napi_open_callback_scope(_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status { +pub export fn napi_open_callback_scope(env: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status { log("napi_open_callback_scope", .{}); - return .ok; + return env.ok(); } -pub export fn napi_close_callback_scope(_: napi_env, _: *anyopaque) napi_status { +pub export fn napi_close_callback_scope(env: napi_env, _: *anyopaque) napi_status { log("napi_close_callback_scope", .{}); - return .ok; + return env.ok(); } pub extern fn napi_throw(env: napi_env, @"error": napi_value) napi_status; pub extern fn napi_throw_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; pub extern fn napi_throw_type_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; pub extern fn napi_throw_range_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; -pub export fn napi_is_error(_: napi_env, value_: napi_value, result: *bool) napi_status { +pub export fn napi_is_error(env: napi_env, value_: napi_value, result: *bool) napi_status { log("napi_is_error", .{}); const value = value_.get(); result.* = value.isAnyError(); - return .ok; + return env.ok(); } pub extern fn napi_is_exception_pending(env: napi_env, result: *bool) napi_status; pub extern fn napi_get_and_clear_last_exception(env: napi_env, result: *napi_value) napi_status; -pub export fn napi_is_arraybuffer(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_arraybuffer(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_arraybuffer", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); result.* = !value.isNumber() and value.jsTypeLoose() == .ArrayBuffer; - return .ok; + return env.ok(); } pub extern fn napi_create_arraybuffer(env: napi_env, byte_length: usize, data: [*]const u8, result: *napi_value) napi_status; @@ -839,30 +838,30 @@ pub extern fn napi_create_external_arraybuffer(env: napi_env, external_data: ?*a pub export fn napi_get_arraybuffer_info(env: napi_env, arraybuffer_: napi_value, data: ?*[*]u8, byte_length: ?*usize) napi_status { log("napi_get_arraybuffer_info", .{}); const arraybuffer = arraybuffer_.get(); - const array_buffer = arraybuffer.asArrayBuffer(env) orelse return .arraybuffer_expected; + const array_buffer = arraybuffer.asArrayBuffer(env.toJS()) orelse return env.setLastError(.arraybuffer_expected); const slice = array_buffer.slice(); if (data) |dat| dat.* = slice.ptr; if (byte_length) |len| len.* = slice.len; - return .ok; + return env.ok(); } -pub export fn napi_is_typedarray(_: napi_env, value_: napi_value, result: ?*bool) napi_status { +pub export fn napi_is_typedarray(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_typedarray", .{}); const value = value_.get(); - if (result != null) - result.?.* = value.jsTypeLoose().isTypedArray(); - return if (result != null) .ok else invalidArg(); + const result = result_ orelse return env.invalidArg(); + result.* = value.jsTypeLoose().isTypedArray(); + return env.ok(); } pub export fn napi_create_typedarray(env: napi_env, @"type": napi_typedarray_type, length: usize, arraybuffer_: napi_value, byte_offset: usize, result_: ?*napi_value) napi_status { log("napi_create_typedarray", .{}); const arraybuffer = arraybuffer_.get(); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.set(env, JSValue.c( JSC.C.JSObjectMakeTypedArrayWithArrayBufferAndOffset( - env.ref(), + env.toJS().ref(), @"type".toC(), arraybuffer.asObjectRef(), byte_offset, @@ -870,7 +869,7 @@ pub export fn napi_create_typedarray(env: napi_env, @"type": napi_typedarray_typ TODO_EXCEPTION, ), )); - return .ok; + return env.ok(); } pub export fn napi_get_typedarray_info( env: napi_env, @@ -884,12 +883,12 @@ pub export fn napi_get_typedarray_info( log("napi_get_typedarray_info", .{}); const typedarray = typedarray_.get(); if (typedarray.isEmptyOrUndefinedOrNull()) - return invalidArg(); + return env.invalidArg(); defer typedarray.ensureStillAlive(); - const array_buffer = typedarray.asArrayBuffer(env) orelse return invalidArg(); + const array_buffer = typedarray.asArrayBuffer(env.toJS()) orelse return env.invalidArg(); if (maybe_type) |@"type"| - @"type".* = napi_typedarray_type.fromJSType(array_buffer.typed_array_type) orelse return invalidArg(); + @"type".* = napi_typedarray_type.fromJSType(array_buffer.typed_array_type) orelse return env.invalidArg(); // TODO: handle detached if (maybe_data) |data| @@ -899,21 +898,21 @@ pub export fn napi_get_typedarray_info( length.* = array_buffer.len; if (maybe_arraybuffer) |arraybuffer| - arraybuffer.set(env, JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.ref(), typedarray.asObjectRef(), null))); + arraybuffer.set(env, JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), typedarray.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| byte_offset.* = array_buffer.offset; - return .ok; + return env.ok(); } pub extern fn napi_create_dataview(env: napi_env, length: usize, arraybuffer: napi_value, byte_offset: usize, result: *napi_value) napi_status; -pub export fn napi_is_dataview(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_dataview(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_dataview", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); result.* = !value.isEmptyOrUndefinedOrNull() and value.jsTypeLoose() == .DataView; - return .ok; + return env.ok(); } pub export fn napi_get_dataview_info( env: napi_env, @@ -925,7 +924,7 @@ pub export fn napi_get_dataview_info( ) napi_status { log("napi_get_dataview_info", .{}); const dataview = dataview_.get(); - const array_buffer = dataview.asArrayBuffer(env) orelse return .object_expected; + const array_buffer = dataview.asArrayBuffer(env.toJS()) orelse return env.setLastError(.object_expected); if (maybe_bytelength) |bytelength| bytelength.* = array_buffer.byte_len; @@ -933,125 +932,107 @@ pub export fn napi_get_dataview_info( data.* = array_buffer.ptr; if (maybe_arraybuffer) |arraybuffer| - arraybuffer.set(env, JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.ref(), dataview.asObjectRef(), null))); + arraybuffer.set(env, JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), dataview.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| byte_offset.* = array_buffer.offset; - return .ok; + return env.ok(); } -pub export fn napi_get_version(_: napi_env, result_: ?*u32) napi_status { +pub export fn napi_get_version(env: napi_env, result_: ?*u32) napi_status { log("napi_get_version", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; result.* = NAPI_VERSION; - return .ok; + return env.ok(); } pub export fn napi_create_promise(env: napi_env, deferred_: ?*napi_deferred, promise_: ?*napi_value) napi_status { log("napi_create_promise", .{}); const deferred = deferred_ orelse { - return invalidArg(); + return env.invalidArg(); }; const promise = promise_ orelse { - return invalidArg(); + return env.invalidArg(); }; deferred.* = bun.default_allocator.create(JSC.JSPromise.Strong) catch @panic("failed to allocate napi_deferred"); - deferred.*.* = JSC.JSPromise.Strong.init(env); - promise.set(env, deferred.*.get().asValue(env)); - return .ok; + deferred.*.* = JSC.JSPromise.Strong.init(env.toJS()); + promise.set(env, deferred.*.get().asValue(env.toJS())); + return env.ok(); } pub export fn napi_resolve_deferred(env: napi_env, deferred: napi_deferred, resolution_: napi_value) napi_status { log("napi_resolve_deferred", .{}); const resolution = resolution_.get(); var prom = deferred.get(); - prom.resolve(env, resolution); + prom.resolve(env.toJS(), resolution); deferred.deinit(); bun.default_allocator.destroy(deferred); - return .ok; + return env.ok(); } pub export fn napi_reject_deferred(env: napi_env, deferred: napi_deferred, rejection_: napi_value) napi_status { log("napi_reject_deferred", .{}); const rejection = rejection_.get(); var prom = deferred.get(); - prom.reject(env, rejection); + prom.reject(env.toJS(), rejection); deferred.deinit(); bun.default_allocator.destroy(deferred); - return .ok; + return env.ok(); } -pub export fn napi_is_promise(_: napi_env, value_: napi_value, is_promise_: ?*bool) napi_status { +pub export fn napi_is_promise(env: napi_env, value_: napi_value, is_promise_: ?*bool) napi_status { log("napi_is_promise", .{}); const value = value_.get(); const is_promise = is_promise_ orelse { - return invalidArg(); + return env.invalidArg(); }; if (value == .zero) { - return invalidArg(); + return env.invalidArg(); } is_promise.* = value.asAnyPromise() != null; - return .ok; + return env.ok(); } pub extern fn napi_run_script(env: napi_env, script: napi_value, result: *napi_value) napi_status; pub extern fn napi_adjust_external_memory(env: napi_env, change_in_bytes: i64, adjusted_value: [*c]i64) napi_status; pub export fn napi_create_date(env: napi_env, time: f64, result_: ?*napi_value) napi_status { log("napi_create_date", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; var args = [_]JSC.C.JSValueRef{JSC.JSValue.jsNumber(time).asObjectRef()}; - result.set(env, JSValue.c(JSC.C.JSObjectMakeDate(env.ref(), 1, &args, TODO_EXCEPTION))); - return .ok; + result.set(env, JSValue.c(JSC.C.JSObjectMakeDate(env.toJS().ref(), 1, &args, TODO_EXCEPTION))); + return env.ok(); } -pub export fn napi_is_date(_: napi_env, value_: napi_value, is_date_: ?*bool) napi_status { +pub export fn napi_is_date(env: napi_env, value_: napi_value, is_date_: ?*bool) napi_status { log("napi_is_date", .{}); const is_date = is_date_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); is_date.* = value.jsTypeLoose() == .JSDate; - return .ok; + return env.ok(); } pub extern fn napi_get_date_value(env: napi_env, value: napi_value, result: *f64) napi_status; -pub extern fn napi_add_finalizer(env: napi_env, js_object: napi_value, native_object: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *Ref) napi_status; +pub extern fn napi_add_finalizer(env: napi_env, js_object: napi_value, native_object: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: napi_ref) napi_status; pub export fn napi_create_bigint_int64(env: napi_env, value: i64, result_: ?*napi_value) napi_status { log("napi_create_bigint_int64", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; - result.set(env, JSC.JSValue.fromInt64NoTruncate(env, value)); - return .ok; + result.set(env, JSC.JSValue.fromInt64NoTruncate(env.toJS(), value)); + return env.ok(); } pub export fn napi_create_bigint_uint64(env: napi_env, value: u64, result_: ?*napi_value) napi_status { log("napi_create_bigint_uint64", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; - result.set(env, JSC.JSValue.fromUInt64NoTruncate(env, value)); - return .ok; + result.set(env, JSC.JSValue.fromUInt64NoTruncate(env.toJS(), value)); + return env.ok(); } pub extern fn napi_create_bigint_words(env: napi_env, sign_bit: c_int, word_count: usize, words: [*c]const u64, result: *napi_value) napi_status; -// TODO: lossless -pub export fn napi_get_value_bigint_int64(_: napi_env, value_: napi_value, result_: ?*i64, _: *bool) napi_status { - log("napi_get_value_bigint_int64", .{}); - const result = result_ orelse { - return invalidArg(); - }; - const value = value_.get(); - result.* = value.toInt64(); - return .ok; -} -// TODO: lossless -pub export fn napi_get_value_bigint_uint64(_: napi_env, value_: napi_value, result_: ?*u64, _: *bool) napi_status { - log("napi_get_value_bigint_uint64", .{}); - const result = result_ orelse { - return invalidArg(); - }; - const value = value_.get(); - result.* = value.toUInt64NoTruncate(); - return .ok; -} +pub extern fn napi_get_value_bigint_int64(env: napi_env, value: napi_value, result: ?*i64, lossless: ?*bool) napi_status; +pub extern fn napi_get_value_bigint_uint64(env: napi_env, value: napi_value, result: ?*u64, lossless: ?*bool) napi_status; pub extern fn napi_get_value_bigint_words(env: napi_env, value: napi_value, sign_bit: [*c]c_int, word_count: [*c]usize, words: [*c]u64) napi_status; pub extern fn napi_get_all_property_names(env: napi_env, object: napi_value, key_mode: napi_key_collection_mode, key_filter: napi_key_filter, key_conversion: napi_key_conversion, result: *napi_value) napi_status; @@ -1070,7 +1051,7 @@ pub const napi_async_work = struct { concurrent_task: JSC.ConcurrentTask = .{}, completion_task: ?*anyopaque = null, event_loop: *JSC.EventLoop, - global: napi_env, + global: *JSC.JSGlobalObject, execute: napi_async_execute_callback = null, complete: napi_async_complete_callback = null, ctx: ?*anyopaque = null, @@ -1086,7 +1067,7 @@ pub const napi_async_work = struct { cancelled = 3, }; - pub fn create(global: napi_env, execute: napi_async_execute_callback, complete: napi_async_complete_callback, ctx: ?*anyopaque) !*napi_async_work { + pub fn create(global: *JSC.JSGlobalObject, execute: napi_async_execute_callback, complete: napi_async_complete_callback, ctx: ?*anyopaque) !*napi_async_work { const work = try bun.default_allocator.create(napi_async_work); work.* = .{ .global = global, @@ -1113,7 +1094,7 @@ pub const napi_async_work = struct { } return; } - this.execute.?(this.global, this.ctx); + this.execute.?(NapiEnv.fromJS(this.global), this.ctx); this.status.store(@intFromEnum(Status.completed), .seq_cst); this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); @@ -1142,14 +1123,14 @@ pub const napi_async_work = struct { } fn runFromJSWithError(this: *napi_async_work) bun.JSError!void { - const handle_scope = NapiHandleScope.open(this.global, false); - defer if (handle_scope) |scope| scope.close(this.global); + const handle_scope = NapiHandleScope.open(NapiEnv.fromJS(this.global), false); + defer if (handle_scope) |scope| scope.close(NapiEnv.fromJS(this.global)); this.complete.?( - this.global, - if (this.status.load(.seq_cst) == @intFromEnum(Status.cancelled)) - napi_status.cancelled + NapiEnv.fromJS(this.global), + @intFromEnum(if (this.status.load(.seq_cst) == @intFromEnum(Status.cancelled)) + NapiStatus.cancelled else - napi_status.ok, + NapiStatus.ok), this.ctx.?, ); if (this.global.hasException()) { @@ -1230,23 +1211,23 @@ pub export fn napi_fatal_error(location_ptr: ?[*:0]const u8, location_len: usize } pub export fn napi_create_buffer(env: napi_env, length: usize, data: ?**anyopaque, result: *napi_value) napi_status { log("napi_create_buffer: {d}", .{length}); - var buffer = JSC.JSValue.createBufferFromLength(env, length); + var buffer = JSC.JSValue.createBufferFromLength(env.toJS(), length); if (length > 0) { if (data) |ptr| { - ptr.* = buffer.asArrayBuffer(env).?.ptr; + ptr.* = buffer.asArrayBuffer(env.toJS()).?.ptr; } } result.set(env, buffer); - return .ok; + return env.ok(); } pub extern fn napi_create_external_buffer(env: napi_env, length: usize, data: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *napi_value) napi_status; pub export fn napi_create_buffer_copy(env: napi_env, length: usize, data: [*]u8, result_data: ?*?*anyopaque, result_: ?*napi_value) napi_status { log("napi_create_buffer_copy: {d}", .{length}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; - var buffer = JSC.JSValue.createBufferFromLength(env, length); - if (buffer.asArrayBuffer(env)) |array_buf| { + var buffer = JSC.JSValue.createBufferFromLength(env.toJS(), length); + if (buffer.asArrayBuffer(env.toJS())) |array_buf| { if (length > 0) { @memcpy(array_buf.slice()[0..length], data[0..length]); } @@ -1257,23 +1238,23 @@ pub export fn napi_create_buffer_copy(env: napi_env, length: usize, data: [*]u8, result.set(env, buffer); - return .ok; + return env.ok(); } pub export fn napi_is_buffer(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_buffer", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const value = value_.get(); - result.* = value.isBuffer(env); - return .ok; + result.* = value.isBuffer(env.toJS()); + return env.ok(); } pub export fn napi_get_buffer_info(env: napi_env, value_: napi_value, data: ?*[*]u8, length: ?*usize) napi_status { log("napi_get_buffer_info", .{}); const value = value_.get(); - const array_buf = value.asArrayBuffer(env) orelse { + const array_buf = value.asArrayBuffer(env.toJS()) orelse { // TODO: is invalid_arg what to return here? - return .arraybuffer_expected; + return env.setLastError(.arraybuffer_expected); }; if (data) |dat| @@ -1282,7 +1263,7 @@ pub export fn napi_get_buffer_info(env: napi_env, value_: napi_value, data: ?*[* if (length) |len| len.* = array_buf.byte_len; - return .ok; + return env.ok(); } extern fn node_api_create_syntax_error(napi_env, napi_value, napi_value, *napi_value) napi_status; @@ -1302,56 +1283,56 @@ pub export fn napi_create_async_work( ) napi_status { log("napi_create_async_work", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; - result.* = napi_async_work.create(env, execute, complete, data) catch { - return genericFailure(); + result.* = napi_async_work.create(env.toJS(), execute, complete, data) catch { + return env.genericFailure(); }; - return .ok; + return env.ok(); } pub export fn napi_delete_async_work(env: napi_env, work_: ?*napi_async_work) napi_status { log("napi_delete_async_work", .{}); const work = work_ orelse { - return invalidArg(); + return env.invalidArg(); }; - bun.assert(env == work.global); + bun.assert(env.toJS() == work.global); work.deinit(); - return .ok; + return env.ok(); } pub export fn napi_queue_async_work(env: napi_env, work_: ?*napi_async_work) napi_status { log("napi_queue_async_work", .{}); const work = work_ orelse { - return invalidArg(); + return env.invalidArg(); }; - bun.assert(env == work.global); + bun.assert(env.toJS() == work.global); work.schedule(); - return .ok; + return env.ok(); } pub export fn napi_cancel_async_work(env: napi_env, work_: ?*napi_async_work) napi_status { log("napi_cancel_async_work", .{}); const work = work_ orelse { - return invalidArg(); + return env.invalidArg(); }; - bun.assert(env == work.global); + bun.assert(env.toJS() == work.global); if (work.cancel()) { - return .ok; + return env.ok(); } - return napi_status.generic_failure; + return env.genericFailure(); } -pub export fn napi_get_node_version(_: napi_env, version_: ?**const napi_node_version) napi_status { +pub export fn napi_get_node_version(env: napi_env, version_: ?**const napi_node_version) napi_status { log("napi_get_node_version", .{}); const version = version_ orelse { - return invalidArg(); + return env.invalidArg(); }; version.* = &napi_node_version.global; - return .ok; + return env.ok(); } const napi_event_loop = if (bun.Environment.isWindows) *bun.windows.libuv.Loop else *JSC.EventLoop; pub export fn napi_get_uv_event_loop(env: napi_env, loop_: ?*napi_event_loop) napi_status { log("napi_get_uv_event_loop", .{}); const loop = loop_ orelse { - return invalidArg(); + return env.invalidArg(); }; if (bun.Environment.isWindows) { // alignment error is incorrect. @@ -1359,9 +1340,9 @@ pub export fn napi_get_uv_event_loop(env: napi_env, loop_: ?*napi_event_loop) na loop.* = JSC.VirtualMachine.get().uvLoop(); } else { // there is no uv event loop on posix, we use our event loop handle. - loop.* = env.bunVM().eventLoop(); + loop.* = env.toJS().bunVM().eventLoop(); } - return .ok; + return env.ok(); } pub extern fn napi_fatal_exception(env: napi_env, err: napi_value) napi_status; @@ -1370,26 +1351,26 @@ pub extern fn napi_fatal_exception(env: napi_env, err: napi_value) napi_status; pub export fn napi_add_env_cleanup_hook(env: napi_env, fun: ?*const fn (?*anyopaque) callconv(.C) void, arg: ?*anyopaque) napi_status { log("napi_add_env_cleanup_hook", .{}); if (fun == null) - return .ok; + return env.ok(); - env.bunVM().rareData().pushCleanupHook(env, arg, fun.?); - return .ok; + env.toJS().bunVM().rareData().pushCleanupHook(env.toJS(), arg, fun.?); + return env.ok(); } pub export fn napi_remove_env_cleanup_hook(env: napi_env, fun: ?*const fn (?*anyopaque) callconv(.C) void, arg: ?*anyopaque) napi_status { log("napi_remove_env_cleanup_hook", .{}); // Avoid looking up env.bunVM(). if (bun.Global.isExiting()) { - return .ok; + return env.ok(); } const vm = JSC.VirtualMachine.get(); if (vm.rare_data == null or fun == null or vm.isShuttingDown()) - return .ok; + return env.ok(); var rare_data = vm.rare_data.?; - const cmp = JSC.RareData.CleanupHook.init(env, arg, fun.?); + const cmp = JSC.RareData.CleanupHook.init(env.toJS(), arg, fun.?); for (rare_data.cleanup_hooks.items, 0..) |*hook, i| { if (hook.eql(cmp)) { _ = rare_data.cleanup_hooks.orderedRemove(i); @@ -1397,7 +1378,7 @@ pub export fn napi_remove_env_cleanup_hook(env: napi_env, fun: ?*const fn (?*any } } - return .ok; + return env.ok(); } pub const Finalizer = struct { @@ -1436,7 +1417,9 @@ pub const ThreadSafeFunction = struct { // User implementation error can cause this number to go negative. thread_count: std.atomic.Value(i64) = std.atomic.Value(i64).init(0), + // for std.condvar lock: std.Thread.Mutex = .{}, + event_loop: *JSC.EventLoop, tracker: JSC.AsyncTaskTracker, @@ -1591,7 +1574,7 @@ pub const ThreadSafeFunction = struct { /// See: https://github.com/nodejs/node/pull/38506 /// In that case, we need to drain microtasks. fn call(this: *ThreadSafeFunction, task: ?*anyopaque, is_first: bool) void { - const globalObject = this.env; + const globalObject = this.env.toJS(); if (!is_first) { this.event_loop.drainMicrotasks(); } @@ -1612,9 +1595,9 @@ pub const ThreadSafeFunction = struct { .c => |cb| { const js = cb.js.get() orelse .undefined; - const handle_scope = NapiHandleScope.open(globalObject, false); - defer if (handle_scope) |scope| scope.close(globalObject); - cb.napi_threadsafe_function_call_js(globalObject, napi_value.create(globalObject, js), this.ctx, task); + const handle_scope = NapiHandleScope.open(this.env, false); + defer if (handle_scope) |scope| scope.close(this.env); + cb.napi_threadsafe_function_call_js(this.env, napi_value.create(this.env, js), this.ctx, task); }, } } @@ -1628,22 +1611,23 @@ pub const ThreadSafeFunction = struct { } } else { if (this.queue.isBlocked()) { - return .queue_full; + // don't set the error on the env as this is run from another thread + return @intFromEnum(NapiStatus.queue_full); } } if (this.isClosing()) { if (this.thread_count.load(.seq_cst) <= 0) { - return .invalid_arg; + return @intFromEnum(NapiStatus.invalid_arg); } _ = this.release(.release, true); - return .closing; + return @intFromEnum(NapiStatus.closing); } _ = this.queue.count.fetchAdd(1, .seq_cst); this.queue.data.writeItem(ctx) catch bun.outOfMemory(); this.scheduleDispatch(); - return .ok; + return @intFromEnum(NapiStatus.ok); } fn scheduleDispatch(this: *ThreadSafeFunction) void { @@ -1666,7 +1650,7 @@ pub const ThreadSafeFunction = struct { if (this.finalizer.fun) |fun| { const handle_scope = NapiHandleScope.open(this.env, false); defer if (handle_scope) |scope| scope.close(this.env); - fun(this.event_loop.global, this.finalizer.data, this.ctx); + fun(this.env, this.finalizer.data, this.ctx); } this.callback.deinit(); @@ -1686,10 +1670,10 @@ pub const ThreadSafeFunction = struct { this.lock.lock(); defer this.lock.unlock(); if (this.isClosing()) { - return .closing; + return @intFromEnum(NapiStatus.closing); } _ = this.thread_count.fetchAdd(1, .seq_cst); - return .ok; + return @intFromEnum(NapiStatus.ok); } pub fn release(this: *ThreadSafeFunction, mode: napi_threadsafe_function_release_mode, already_locked: bool) napi_status { @@ -1697,7 +1681,7 @@ pub const ThreadSafeFunction = struct { defer if (!already_locked) this.lock.unlock(); if (this.thread_count.load(.seq_cst) < 0) { - return .invalid_arg; + return @intFromEnum(NapiStatus.invalid_arg); } const prev_remaining = this.thread_count.fetchSub(1, .seq_cst); @@ -1715,7 +1699,7 @@ pub const ThreadSafeFunction = struct { } } - return .ok; + return @intFromEnum(NapiStatus.ok); } }; @@ -1734,25 +1718,26 @@ pub export fn napi_create_threadsafe_function( ) napi_status { log("napi_create_threadsafe_function", .{}); const result = result_ orelse { - return invalidArg(); + return env.invalidArg(); }; const func = func_.get(); + const global = env.toJS(); - if (call_js_cb == null and (func.isEmptyOrUndefinedOrNull() or !func.isCallable(env.vm()))) { - return napi_status.function_expected; + if (call_js_cb == null and (func.isEmptyOrUndefinedOrNull() or !func.isCallable(global.vm()))) { + return env.setLastError(.function_expected); } - const vm = env.bunVM(); + const vm = global.bunVM(); var function = ThreadSafeFunction.new(.{ .event_loop = vm.eventLoop(), .env = env, .callback = if (call_js_cb) |c| .{ .c = .{ .napi_threadsafe_function_call_js = c, - .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(env), vm.global), + .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(global), vm.global), }, } else .{ - .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(env), vm.global), + .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(global), vm.global), }, .ctx = context, .queue = ThreadSafeFunction.Queue.init(max_queue_size, bun.default_allocator), @@ -1767,12 +1752,12 @@ pub export fn napi_create_threadsafe_function( function.tracker.didSchedule(vm.global); result.* = function; - return .ok; + return env.ok(); } pub export fn napi_get_threadsafe_function_context(func: napi_threadsafe_function, result: *?*anyopaque) napi_status { log("napi_get_threadsafe_function_context", .{}); result.* = func.ctx; - return .ok; + return @intFromEnum(NapiStatus.ok); } pub export fn napi_call_threadsafe_function(func: napi_threadsafe_function, data: ?*anyopaque, is_blocking: napi_threadsafe_function_call_mode) napi_status { log("napi_call_threadsafe_function", .{}); @@ -1788,26 +1773,26 @@ pub export fn napi_release_threadsafe_function(func: napi_threadsafe_function, m } pub export fn napi_unref_threadsafe_function(env: napi_env, func: napi_threadsafe_function) napi_status { log("napi_unref_threadsafe_function", .{}); - bun.assert(func.event_loop.global == env); + bun.assert(func.event_loop.global == env.toJS()); func.unref(); - return .ok; + return env.ok(); } pub export fn napi_ref_threadsafe_function(env: napi_env, func: napi_threadsafe_function) napi_status { log("napi_ref_threadsafe_function", .{}); - bun.assert(func.event_loop.global == env); + bun.assert(func.event_loop.global == env.toJS()); func.ref(); - return .ok; + return env.ok(); } -pub export fn napi_add_async_cleanup_hook(_: napi_env, _: napi_async_cleanup_hook, _: ?*anyopaque, _: [*c]napi_async_cleanup_hook_handle) napi_status { +pub export fn napi_add_async_cleanup_hook(env: napi_env, _: napi_async_cleanup_hook, _: ?*anyopaque, _: [*c]napi_async_cleanup_hook_handle) napi_status { log("napi_add_async_cleanup_hook", .{}); // TODO: - return .ok; + return env.ok(); } pub export fn napi_remove_async_cleanup_hook(_: napi_async_cleanup_hook_handle) napi_status { log("napi_remove_async_cleanup_hook", .{}); // TODO: - return .ok; + return @intFromEnum(NapiStatus.ok); } const NAPI_VERSION = @as(c_int, 8); @@ -1885,6 +1870,7 @@ const V8API = if (!bun.Environment.isWindows) struct { pub extern fn _ZN2v812api_internal13DisposeGlobalEPm() *anyopaque; pub extern fn _ZNK2v88Function7GetNameEv() *anyopaque; pub extern fn _ZNK2v85Value10IsFunctionEv() *anyopaque; + pub extern fn _ZN2v812api_internal17FromJustIsNothingEv() *anyopaque; pub extern fn uv_os_getpid() *anyopaque; pub extern fn uv_os_getppid() *anyopaque; } else struct { @@ -1893,7 +1879,7 @@ const V8API = if (!bun.Environment.isWindows) struct { // // dumpbin .\build\CMakeFiles\bun-debug.dir\src\bun.js\bindings\v8\*.cpp.obj /symbols | where-object { $_.Contains(' node::') -or $_.Contains(' v8::') } | foreach-object { (($_ -split "\|")[1] -split " ")[1] } | ForEach-Object { "extern fn @`"${_}`"() *anyopaque;" } // - // Bug @paperdave if you get stuck here + // Bug @paperclover if you get stuck here pub extern fn @"?TryGetCurrent@Isolate@v8@@SAPEAV12@XZ"() *anyopaque; pub extern fn @"?GetCurrent@Isolate@v8@@SAPEAV12@XZ"() *anyopaque; pub extern fn @"?GetCurrentContext@Isolate@v8@@QEAA?AV?$Local@VContext@v8@@@2@XZ"() *anyopaque; @@ -1955,6 +1941,7 @@ const V8API = if (!bun.Environment.isWindows) struct { pub extern fn @"?DisposeGlobal@api_internal@v8@@YAXPEA_K@Z"() *anyopaque; pub extern fn @"?GetName@Function@v8@@QEBA?AV?$Local@VValue@v8@@@2@XZ"() *anyopaque; pub extern fn @"?IsFunction@Value@v8@@QEBA_NXZ"() *anyopaque; + pub extern fn @"?FromJustIsNothing@api_internal@v8@@YAXXZ"() *anyopaque; }; // To update this list, use find + multi-cursor in your editor. diff --git a/src/node_fallbacks.zig b/src/node_fallbacks.zig index 89998d3a13..01370964f5 100644 --- a/src/node_fallbacks.zig +++ b/src/node_fallbacks.zig @@ -17,7 +17,24 @@ comptime { pub const FallbackModule = struct { path: Fs.Path, package_json: *const PackageJSON, - code: string, + code: *const fn () string, + + // This workaround exists to allow bun.runtimeEmbedFile to work. + // Using `@embedFile` forces you to wait for the Zig build to finish in + // debug builds, even when you only changed JS builtins. + fn createSourceCodeGetter(comptime code_path: string) *const fn () string { + const Getter = struct { + fn get() string { + if (bun.Environment.codegen_embed) { + return @embedFile(code_path); + } + + return bun.runtimeEmbedFile(.codegen, code_path); + } + }; + + return Getter.get; + } pub fn init(comptime name: string) FallbackModule { @setEvalBranchQuota(99999); @@ -35,7 +52,7 @@ pub const FallbackModule = struct { .source = logger.Source.initPathString(import_path ++ name ++ "/package.json", ""), .side_effects = .false, }, - .code = @embedFile(code_path), + .code = createSourceCodeGetter(code_path), }; } }; @@ -74,7 +91,7 @@ pub fn contentsFromPath(path: string) ?string { module_name = module_name[0 .. std.mem.indexOfScalar(u8, module_name, '/') orelse module_name.len]; if (Map.get(module_name)) |mod| { - return mod.code; + return mod.code(); } return null; diff --git a/src/open.zig b/src/open.zig index c14c2895c9..e954892080 100644 --- a/src/open.zig +++ b/src/open.zig @@ -39,7 +39,7 @@ pub fn openURL(url: stringZ) void { .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), - } else {}, + }, }) catch break :maybe_fallback) { // don't fallback: .result => |*result| if (result.isOK()) return, diff --git a/src/options.zig b/src/options.zig index d1f669b821..9512e4bcc9 100644 --- a/src/options.zig +++ b/src/options.zig @@ -644,6 +644,14 @@ pub const Loader = enum(u8) { bunsh, sqlite, sqlite_embedded, + html, + + pub fn disableHTML(this: Loader) Loader { + return switch (this) { + .html => .file, + else => this, + }; + } pub inline fn isSQLite(this: Loader) bool { return switch (this) { @@ -652,28 +660,22 @@ pub const Loader = enum(u8) { }; } - pub fn shouldCopyForBundling(this: Loader, experimental_css: bool) bool { - if (experimental_css) { - return switch (this) { - .file, - .napi, - .sqlite, - .sqlite_embedded, - // TODO: loader for reading bytes and creating module or instance - .wasm, - => true, - else => false, - }; - } + pub const Experimental = struct { + css: bool = bun.FeatureFlags.breaking_changes_1_2, + html: bool = bun.FeatureFlags.breaking_changes_1_2, + }; + + pub fn shouldCopyForBundling(this: Loader, experimental: Experimental) bool { return switch (this) { .file, - .css, .napi, .sqlite, .sqlite_embedded, // TODO: loader for reading bytes and creating module or instance .wasm, => true, + .css => !experimental.css, + .html => !experimental.html, else => false, }; } @@ -684,6 +686,7 @@ pub const Loader = enum(u8) { .css => bun.http.MimeType.css, .toml, .json => bun.http.MimeType.json, .wasm => bun.http.MimeType.wasm, + .html => bun.http.MimeType.html, else => bun.http.MimeType.other, }; } @@ -719,6 +722,7 @@ pub const Loader = enum(u8) { map.set(.napi, "input.node"); map.set(.text, "input.txt"); map.set(.bunsh, "input.sh"); + map.set(.html, "input.html"); break :brk map; }; @@ -754,6 +758,7 @@ pub const Loader = enum(u8) { .{ "css", .css }, .{ "file", .file }, .{ "json", .json }, + .{ "jsonc", .json }, .{ "toml", .toml }, .{ "wasm", .wasm }, .{ "node", .napi }, @@ -764,6 +769,7 @@ pub const Loader = enum(u8) { .{ "sh", .bunsh }, .{ "sqlite", .sqlite }, .{ "sqlite_embedded", .sqlite_embedded }, + .{ "html", .html }, }); pub const api_names = bun.ComptimeStringMap(Api.Loader, .{ @@ -778,6 +784,7 @@ pub const Loader = enum(u8) { .{ "css", .css }, .{ "file", .file }, .{ "json", .json }, + .{ "jsonc", .json }, .{ "toml", .toml }, .{ "wasm", .wasm }, .{ "node", .napi }, @@ -787,6 +794,7 @@ pub const Loader = enum(u8) { .{ "text", .text }, .{ "sh", .file }, .{ "sqlite", .sqlite }, + .{ "html", .html }, }); pub fn fromString(slice_: string) ?Loader { @@ -812,6 +820,7 @@ pub const Loader = enum(u8) { .ts => .ts, .tsx => .tsx, .css => .css, + .html => .html, .file, .bunsh => .file, .json => .json, .toml => .toml, @@ -840,6 +849,7 @@ pub const Loader = enum(u8) { .base64 => .base64, .dataurl => .dataurl, .text => .text, + .html => .html, .sqlite => .sqlite, _ => .file, }; @@ -899,6 +909,8 @@ const default_loaders_posix = .{ .{ ".node", .napi }, .{ ".txt", .text }, .{ ".text", .text }, + .{ ".html", .html }, + .{ ".jsonc", .json }, }; const default_loaders_win32 = default_loaders_posix ++ .{ .{ ".sh", .bunsh }, @@ -909,9 +921,9 @@ pub const defaultLoaders = bun.ComptimeStringMap(Loader, default_loaders); // https://webpack.js.org/guides/package-exports/#reference-syntax pub const ESMConditions = struct { - default: ConditionsMap = undefined, - import: ConditionsMap = undefined, - require: ConditionsMap = undefined, + default: ConditionsMap, + import: ConditionsMap, + require: ConditionsMap, pub fn init(allocator: std.mem.Allocator, defaults: []const string) !ESMConditions { var default_condition_amp = ConditionsMap.init(allocator); @@ -936,22 +948,37 @@ pub const ESMConditions = struct { import_condition_map.putAssumeCapacity("default", {}); require_condition_map.putAssumeCapacity("default", {}); - return ESMConditions{ + return .{ .default = default_condition_amp, .import = import_condition_map, .require = require_condition_map, }; } + pub fn clone(self: *const ESMConditions) !ESMConditions { + var default = try self.default.clone(); + errdefer default.deinit(); + var import = try self.import.clone(); + errdefer import.deinit(); + var require = try self.require.clone(); + errdefer require.deinit(); + + return .{ + .default = default, + .import = import, + .require = require, + }; + } + pub fn appendSlice(self: *ESMConditions, conditions: []const string) !void { try self.default.ensureUnusedCapacity(conditions.len); try self.import.ensureUnusedCapacity(conditions.len); try self.require.ensureUnusedCapacity(conditions.len); for (conditions) |condition| { - self.default.putAssumeCapacityNoClobber(condition, {}); - self.import.putAssumeCapacityNoClobber(condition, {}); - self.require.putAssumeCapacityNoClobber(condition, {}); + self.default.putAssumeCapacity(condition, {}); + self.import.putAssumeCapacity(condition, {}); + self.require.putAssumeCapacity(condition, {}); } } }; @@ -1262,16 +1289,23 @@ pub fn definesFromTransformOptions( const default_loader_ext_bun = [_]string{".node"}; const default_loader_ext = [_]string{ - ".jsx", ".json", - ".js", ".mjs", - ".cjs", ".css", + ".jsx", ".json", + ".js", ".mjs", + ".cjs", ".css", // https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#new-file-extensions - ".ts", ".tsx", - ".mts", ".cts", + ".ts", ".tsx", + ".mts", ".cts", - ".toml", ".wasm", - ".txt", ".text", + ".toml", ".wasm", + ".txt", ".text", + + ".jsonc", +}; + +// Only set it for browsers by default. +const default_loader_ext_browser = [_]string{ + ".html", }; const node_modules_default_loader_ext_bun = [_]string{".node"}; @@ -1285,11 +1319,13 @@ const node_modules_default_loader_ext = [_]string{ ".toml", ".txt", ".json", + ".jsonc", ".css", ".tsx", ".cts", ".wasm", ".text", + ".html", }; pub const ResolveFileExtensions = struct { @@ -1308,7 +1344,7 @@ pub const ResolveFileExtensions = struct { pub fn kind(this: *const ResolveFileExtensions, kind_: bun.ImportKind, is_node_modules: bool) []const string { return switch (kind_) { - .stmt, .entry_point, .dynamic => this.group(is_node_modules).esm, + .stmt, .entry_point_build, .entry_point_run, .dynamic => this.group(is_node_modules).esm, else => this.group(is_node_modules).default, }; } @@ -1342,6 +1378,16 @@ pub fn loadersFromTransformOptions(allocator: std.mem.Allocator, _loaders: ?Api. inline for (default_loader_ext_bun) |ext| { _ = try loaders.getOrPutValue(ext, defaultLoaders.get(ext).?); } + + if (bun.CLI.Command.get().bundler_options.experimental.html) { + _ = try loaders.getOrPutValue(".html", .html); + } + } + + if (target == .browser) { + inline for (default_loader_ext_browser) |ext| { + _ = try loaders.getOrPutValue(ext, defaultLoaders.get(ext).?); + } } return loaders; @@ -1498,7 +1544,7 @@ pub const BundleOptions = struct { minify_identifiers: bool = false, dead_code_elimination: bool = true, - experimental_css: bool, + experimental: Loader.Experimental = .{}, css_chunking: bool, ignore_dce_annotations: bool = false, @@ -1683,7 +1729,7 @@ pub const BundleOptions = struct { .out_extensions = undefined, .env = Env.init(allocator), .transform_options = transform, - .experimental_css = false, + .experimental = .{}, .css_chunking = false, .drop = transform.drop, }; @@ -1714,11 +1760,6 @@ pub const BundleOptions = struct { opts.conditions = try ESMConditions.init(allocator, opts.target.defaultConditions()); - if (bun.FeatureFlags.breaking_changes_1_2) { - // This is currently done in DevServer by default, but not in Bun.build - @compileError("if (!production) { add \"development\" condition }"); - } - if (transform.conditions.len > 0) { opts.conditions.appendSlice(transform.conditions) catch bun.outOfMemory(); } @@ -1842,419 +1883,7 @@ pub const TransformOptions = struct { } }; -// Instead of keeping files in-memory, we: -// 1. Write directly to disk -// 2. (Optional) move the file to the destination -// This saves us from allocating a buffer -pub const OutputFile = struct { - loader: Loader, - input_loader: Loader = .js, - src_path: Fs.Path, - value: Value, - size: usize = 0, - size_without_sourcemap: usize = 0, - hash: u64 = 0, - is_executable: bool = false, - source_map_index: u32 = std.math.maxInt(u32), - bytecode_index: u32 = std.math.maxInt(u32), - output_kind: JSC.API.BuildArtifact.OutputKind, - /// Relative - dest_path: []const u8 = "", - side: ?bun.bake.Side, - /// This is only set for the JS bundle, and not files associated with an - /// entrypoint like sourcemaps and bytecode - entry_point_index: ?u32, - referenced_css_files: []const Index = &.{}, - - pub const Index = bun.GenericIndex(u32, OutputFile); - - // Depending on: - // - The target - // - The number of open file handles - // - Whether or not a file of the same name exists - // We may use a different system call - pub const FileOperation = struct { - pathname: string, - fd: FileDescriptorType = bun.invalid_fd, - dir: FileDescriptorType = bun.invalid_fd, - is_tmpdir: bool = false, - is_outdir: bool = false, - close_handle_on_complete: bool = false, - autowatch: bool = true, - - pub fn fromFile(fd: anytype, pathname: string) FileOperation { - return .{ - .pathname = pathname, - .fd = bun.toFD(fd), - }; - } - - pub fn getPathname(file: *const FileOperation) string { - if (file.is_tmpdir) { - return resolve_path.joinAbs(@TypeOf(Fs.FileSystem.instance.fs).tmpdir_path, .auto, file.pathname); - } else { - return file.pathname; - } - } - }; - - pub const Value = union(Kind) { - move: FileOperation, - copy: FileOperation, - noop: u0, - buffer: struct { - allocator: std.mem.Allocator, - bytes: []const u8, - }, - pending: resolver.Result, - saved: SavedFile, - - pub fn toBunString(v: Value) bun.String { - return switch (v) { - .noop => bun.String.empty, - .buffer => |buf| { - // Use ExternalStringImpl to avoid cloning the string, at - // the cost of allocating space to remember the allocator. - const FreeContext = struct { - allocator: std.mem.Allocator, - - fn onFree(uncast_ctx: *anyopaque, buffer: *anyopaque, len: u32) callconv(.C) void { - const ctx: *@This() = @alignCast(@ptrCast(uncast_ctx)); - ctx.allocator.free(@as([*]u8, @ptrCast(buffer))[0..len]); - bun.destroy(ctx); - } - }; - return bun.String.createExternal( - buf.bytes, - true, - bun.new(FreeContext, .{ .allocator = buf.allocator }), - FreeContext.onFree, - ); - }, - .pending => unreachable, - else => |tag| bun.todoPanic(@src(), "handle .{s}", .{@tagName(tag)}), - }; - } - }; - - pub const SavedFile = struct { - pub fn toJS( - globalThis: *JSC.JSGlobalObject, - path: []const u8, - byte_size: usize, - ) JSC.JSValue { - const mime_type = globalThis.bunVM().mimeType(path); - const store = JSC.WebCore.Blob.Store.initFile( - JSC.Node.PathOrFileDescriptor{ - .path = JSC.Node.PathLike{ - .string = JSC.PathString.init(path), - }, - }, - mime_type, - bun.default_allocator, - ) catch unreachable; - - var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; - blob.* = JSC.WebCore.Blob.initWithStore(store, globalThis); - if (mime_type) |mime| { - blob.content_type = mime.value; - } - blob.size = @as(JSC.WebCore.Blob.SizeType, @truncate(byte_size)); - blob.allocator = bun.default_allocator; - return blob.toJS(globalThis); - } - }; - - pub const Kind = enum { move, copy, noop, buffer, pending, saved }; - - pub fn initPending(loader: Loader, pending: resolver.Result) OutputFile { - return .{ - .loader = loader, - .src_path = pending.pathConst().?.*, - .size = 0, - .value = .{ .pending = pending }, - }; - } - - pub fn initFile(file: std.fs.File, pathname: string, size: usize) OutputFile { - return .{ - .loader = .file, - .src_path = Fs.Path.init(pathname), - .size = size, - .value = .{ .copy = FileOperation.fromFile(file.handle, pathname) }, - }; - } - - pub fn initFileWithDir(file: std.fs.File, pathname: string, size: usize, dir: std.fs.Dir) OutputFile { - var res = initFile(file, pathname, size); - res.value.copy.dir_handle = bun.toFD(dir.fd); - return res; - } - - pub const Options = struct { - loader: Loader, - input_loader: Loader, - hash: ?u64 = null, - source_map_index: ?u32 = null, - bytecode_index: ?u32 = null, - output_path: string, - size: ?usize = null, - input_path: []const u8 = "", - display_size: u32 = 0, - output_kind: JSC.API.BuildArtifact.OutputKind, - is_executable: bool, - data: union(enum) { - buffer: struct { - allocator: std.mem.Allocator, - data: []const u8, - }, - file: struct { - file: std.fs.File, - size: usize, - dir: std.fs.Dir, - }, - saved: usize, - }, - side: ?bun.bake.Side, - entry_point_index: ?u32, - referenced_css_files: []const Index = &.{}, - }; - - pub fn init(options: Options) OutputFile { - return .{ - .loader = options.loader, - .input_loader = options.input_loader, - .src_path = Fs.Path.init(options.input_path), - .dest_path = options.output_path, - .size = options.size orelse switch (options.data) { - .buffer => |buf| buf.data.len, - .file => |file| file.size, - .saved => 0, - }, - .size_without_sourcemap = options.display_size, - .hash = options.hash orelse 0, - .output_kind = options.output_kind, - .bytecode_index = options.bytecode_index orelse std.math.maxInt(u32), - .source_map_index = options.source_map_index orelse std.math.maxInt(u32), - .is_executable = options.is_executable, - .value = switch (options.data) { - .buffer => |buffer| Value{ .buffer = .{ .allocator = buffer.allocator, .bytes = buffer.data } }, - .file => |file| Value{ - .copy = brk: { - var op = FileOperation.fromFile(file.file.handle, options.output_path); - op.dir = bun.toFD(file.dir.fd); - break :brk op; - }, - }, - .saved => Value{ .saved = .{} }, - }, - .side = options.side, - .entry_point_index = options.entry_point_index, - .referenced_css_files = options.referenced_css_files, - }; - } - - pub fn initBuf(buf: []const u8, allocator: std.mem.Allocator, pathname: string, loader: Loader, hash: ?u64, source_map_index: ?u32) OutputFile { - return .{ - .loader = loader, - .src_path = Fs.Path.init(pathname), - .size = buf.len, - .hash = hash orelse 0, - .source_map_index = source_map_index orelse std.math.maxInt(u32), - .value = .{ - .buffer = .{ - .bytes = buf, - .allocator = allocator, - }, - }, - }; - } - - /// Given the `--outdir` as root_dir, this will return the relative path to display in terminal - pub fn writeToDisk(f: OutputFile, root_dir: std.fs.Dir, root_dir_path: []const u8) ![]const u8 { - switch (f.value) { - .saved => { - var rel_path = f.dest_path; - if (f.dest_path.len > root_dir_path.len) { - rel_path = resolve_path.relative(root_dir_path, f.dest_path); - } - return rel_path; - }, - .buffer => |value| { - var rel_path = f.dest_path; - if (f.dest_path.len > root_dir_path.len) { - rel_path = resolve_path.relative(root_dir_path, f.dest_path); - if (std.fs.path.dirname(rel_path)) |parent| { - if (parent.len > root_dir_path.len) { - try root_dir.makePath(parent); - } - } - } - - var path_buf: bun.PathBuffer = undefined; - _ = try JSC.Node.NodeFS.writeFileWithPathBuffer(&path_buf, .{ - .data = .{ .buffer = .{ - .buffer = .{ - .ptr = @constCast(value.bytes.ptr), - .len = value.bytes.len, - .byte_len = value.bytes.len, - }, - } }, - .encoding = .buffer, - .mode = if (f.is_executable) 0o755 else 0o644, - .dirfd = bun.toFD(root_dir.fd), - .file = .{ .path = .{ - .string = JSC.PathString.init(rel_path), - } }, - }).unwrap(); - - return rel_path; - }, - .move => |value| { - _ = value; - // var filepath_buf: bun.PathBuffer = undefined; - // filepath_buf[0] = '.'; - // filepath_buf[1] = '/'; - // const primary = f.dest_path[root_dir_path.len..]; - // bun.copy(u8, filepath_buf[2..], primary); - // var rel_path: []const u8 = filepath_buf[0 .. primary.len + 2]; - // rel_path = value.pathname; - - // try f.moveTo(root_path, @constCast(rel_path), bun.toFD(root_dir.fd)); - { - @panic("TODO: Regressed behavior"); - } - - // return primary; - }, - .copy => |value| { - _ = value; - // rel_path = value.pathname; - - // try f.copyTo(root_path, @constCast(rel_path), bun.toFD(root_dir.fd)); - { - @panic("TODO: Regressed behavior"); - } - }, - .noop => { - return f.dest_path; - }, - .pending => unreachable, - } - } - - pub fn moveTo(file: *const OutputFile, _: string, rel_path: []u8, dir: FileDescriptorType) !void { - try bun.C.moveFileZ(file.value.move.dir, bun.sliceTo(&(try std.posix.toPosixPath(file.value.move.getPathname())), 0), dir, bun.sliceTo(&(try std.posix.toPosixPath(rel_path)), 0)); - } - - pub fn copyTo(file: *const OutputFile, _: string, rel_path: []u8, dir: FileDescriptorType) !void { - const file_out = (try dir.asDir().createFile(rel_path, .{})); - - const fd_out = file_out.handle; - var do_close = false; - const fd_in = (try std.fs.openFileAbsolute(file.src_path.text, .{ .mode = .read_only })).handle; - - if (Environment.isWindows) { - Fs.FileSystem.setMaxFd(fd_out); - Fs.FileSystem.setMaxFd(fd_in); - do_close = Fs.FileSystem.instance.fs.needToCloseFiles(); - - // use paths instead of bun.getFdPathW() - @panic("TODO windows"); - } - - defer { - if (do_close) { - _ = bun.sys.close(bun.toFD(fd_out)); - _ = bun.sys.close(bun.toFD(fd_in)); - } - } - - try bun.copyFile(fd_in, fd_out).unwrap(); - } - - pub fn toJS( - this: *OutputFile, - owned_pathname: ?[]const u8, - globalObject: *JSC.JSGlobalObject, - ) bun.JSC.JSValue { - return switch (this.value) { - .move, .pending => @panic("Unexpected pending output file"), - .noop => JSC.JSValue.undefined, - .copy => |copy| brk: { - const file_blob = JSC.WebCore.Blob.Store.initFile( - if (copy.fd != .zero) - JSC.Node.PathOrFileDescriptor{ - .fd = copy.fd, - } - else - JSC.Node.PathOrFileDescriptor{ - .path = JSC.Node.PathLike{ .string = bun.PathString.init(globalObject.allocator().dupe(u8, copy.pathname) catch unreachable) }, - }, - this.loader.toMimeType(), - globalObject.allocator(), - ) catch |err| { - Output.panic("error: Unable to create file blob: \"{s}\"", .{@errorName(err)}); - }; - - var build_output = bun.new(JSC.API.BuildArtifact, .{ - .blob = JSC.WebCore.Blob.initWithStore(file_blob, globalObject), - .hash = this.hash, - .loader = this.input_loader, - .output_kind = this.output_kind, - .path = bun.default_allocator.dupe(u8, copy.pathname) catch @panic("Failed to allocate path"), - }); - - break :brk build_output.toJS(globalObject); - }, - .saved => brk: { - var build_output = bun.default_allocator.create(JSC.API.BuildArtifact) catch @panic("Unable to allocate Artifact"); - const path_to_use = owned_pathname orelse this.src_path.text; - - const file_blob = JSC.WebCore.Blob.Store.initFile( - JSC.Node.PathOrFileDescriptor{ - .path = JSC.Node.PathLike{ .string = bun.PathString.init(owned_pathname orelse (bun.default_allocator.dupe(u8, this.src_path.text) catch unreachable)) }, - }, - this.loader.toMimeType(), - globalObject.allocator(), - ) catch |err| { - Output.panic("error: Unable to create file blob: \"{s}\"", .{@errorName(err)}); - }; - - build_output.* = JSC.API.BuildArtifact{ - .blob = JSC.WebCore.Blob.initWithStore(file_blob, globalObject), - .hash = this.hash, - .loader = this.input_loader, - .output_kind = this.output_kind, - .path = bun.default_allocator.dupe(u8, path_to_use) catch @panic("Failed to allocate path"), - }; - - break :brk build_output.toJS(globalObject); - }, - .buffer => |buffer| brk: { - var blob = JSC.WebCore.Blob.init(@constCast(buffer.bytes), buffer.allocator, globalObject); - if (blob.store) |store| { - store.mime_type = this.loader.toMimeType(); - blob.content_type = store.mime_type.value; - } else { - blob.content_type = this.loader.toMimeType().value; - } - - blob.size = @as(JSC.WebCore.Blob.SizeType, @truncate(buffer.bytes.len)); - - var build_output = bun.default_allocator.create(JSC.API.BuildArtifact) catch @panic("Unable to allocate Artifact"); - build_output.* = JSC.API.BuildArtifact{ - .blob = blob, - .hash = this.hash, - .loader = this.input_loader, - .output_kind = this.output_kind, - .path = owned_pathname orelse bun.default_allocator.dupe(u8, this.src_path.text) catch unreachable, - }; - break :brk build_output.toJS(globalObject); - }, - }; - } -}; +pub const OutputFile = @import("./OutputFile.zig"); pub const TransformResult = struct { errors: []logger.Msg = &([_]logger.Msg{}), diff --git a/src/output.zig b/src/output.zig index aa25b45301..53c09fd79f 100644 --- a/src/output.zig +++ b/src/output.zig @@ -88,6 +88,7 @@ pub const Source = struct { if (source_set) return; bun.debugAssert(stdout_stream_set); source = Source.init(stdout_stream, stderr_stream); + bun.StackCheck.configureThread(); } pub fn configureNamedThread(name: StringTypes.stringZ) void { @@ -309,19 +310,18 @@ pub const Source = struct { } if (bun.getenvZ("TERM_PROGRAM")) |term_program| { - if (strings.eqlComptime(term_program, "iTerm.app")) { - lazy_color_depth = .@"16m"; - return; - } - - if (strings.eqlComptime(term_program, "WezTerm")) { - lazy_color_depth = .@"16m"; - return; - } - - if (strings.eqlComptime(term_program, "ghostty")) { - lazy_color_depth = .@"16m"; - return; + const use_16m = .{ + "ghostty", + "MacTerm", + "WezTerm", + "HyperTerm", + "iTerm.app", + }; + inline for (use_16m) |program| { + if (strings.eqlComptime(term_program, program)) { + lazy_color_depth = .@"16m"; + return; + } } } @@ -468,16 +468,16 @@ pub fn isVerbose() bool { return false; } -var _source_for_test: if (Environment.isTest) Source else void = undefined; -var _source_for_test_set = false; -pub fn initTest() void { - if (_source_for_test_set) return; - _source_for_test_set = true; - const in = std.io.getStdErr(); - const out = std.io.getStdOut(); - _source_for_test = Source.init(File.from(out), File.from(in)); - Source.set(&_source_for_test); -} +// var _source_for_test: if (Environment.isTest) Source else void = undefined; +// var _source_for_test_set = false; +// pub fn initTest() void { +// if (_source_for_test_set) return; +// _source_for_test_set = true; +// const in = std.io.getStdErr(); +// const out = std.io.getStdOut(); +// _source_for_test = Source.init(File.from(out), File.from(in)); +// Source.set(&_source_for_test); +// } pub fn enableBuffering() void { if (comptime Environment.isNative) enable_buffering = true; } @@ -674,7 +674,7 @@ pub noinline fn println(comptime fmt: string, args: anytype) void { /// Print to stdout, but only in debug builds. /// Text automatically buffers pub fn debug(comptime fmt: string, args: anytype) void { - if (comptime Environment.isRelease) return; + if (!Environment.isDebug) return; prettyErrorln("DEBUG: " ++ fmt, args); flush(); } @@ -745,7 +745,7 @@ fn ScopedLogger(comptime tagname: []const u8, comptime disabled: bool) type { var out_set = false; var really_disable = disabled; var evaluated_disable = false; - var lock = std.Thread.Mutex{}; + var lock = bun.Mutex{}; pub fn isVisible() bool { if (!evaluated_disable) { @@ -842,27 +842,31 @@ pub fn scoped(comptime tag: anytype, comptime disabled: bool) LogFunction { // // // +// // // +// // // // - bold // - dim // - reset // - reset -const ED = "\x1b["; +const CSI = "\x1b["; pub const color_map = ComptimeStringMap(string, .{ - &.{ "black", ED ++ "30m" }, - &.{ "blue", ED ++ "34m" }, - &.{ "b", ED ++ "1m" }, - &.{ "d", ED ++ "2m" }, - &.{ "i", ED ++ "3m" }, - &.{ "cyan", ED ++ "36m" }, - &.{ "green", ED ++ "32m" }, - &.{ "magenta", ED ++ "35m" }, - &.{ "red", ED ++ "31m" }, - &.{ "white", ED ++ "37m" }, - &.{ "yellow", ED ++ "33m" }, + &.{ "b", CSI ++ "1m" }, + &.{ "d", CSI ++ "2m" }, + &.{ "i", CSI ++ "3m" }, + &.{ "black", CSI ++ "30m" }, + &.{ "red", CSI ++ "31m" }, + &.{ "green", CSI ++ "32m" }, + &.{ "yellow", CSI ++ "33m" }, + &.{ "blue", CSI ++ "34m" }, + &.{ "magenta", CSI ++ "35m" }, + &.{ "cyan", CSI ++ "36m" }, + &.{ "white", CSI ++ "37m" }, + &.{ "bgred", CSI ++ "41m" }, + &.{ "bggreen", CSI ++ "42m" }, }); const RESET: string = "\x1b[0m"; pub fn prettyFmt(comptime fmt: string, comptime is_enabled: bool) [:0]const u8 { @@ -1063,12 +1067,20 @@ pub inline fn err(error_name: anytype, comptime fmt: []const u8, args: anytype) const info = @typeInfo(T); if (comptime T == bun.sys.Error or info == .Pointer and info.Pointer.child == bun.sys.Error) { - prettyErrorln("error:: " ++ fmt, args ++ .{error_name}); + const e: bun.sys.Error = error_name; + const tag_name, const sys_errno = e.getErrorCodeTagName() orelse { + err("unknown error", fmt, args); + return; + }; + if (bun.sys.coreutils_error_map.get(sys_errno)) |label| { + prettyErrorln("{s}: {s}: " ++ fmt ++ " ({s})", .{ tag_name, label } ++ args ++ .{@tagName(e.syscall)}); + } else { + prettyErrorln("{s}: " ++ fmt ++ " ({s})", .{tag_name} ++ args ++ .{@tagName(e.syscall)}); + } return; } const display_name, const is_comptime_name = display_name: { - // Zig string literals are of type *const [n:0]u8 // we assume that no one will pass this type from not using a string literal. if (info == .Pointer and info.Pointer.size == .One and info.Pointer.is_const) { diff --git a/src/patch.zig b/src/patch.zig index 55b5780a3b..771643d8ce 100644 --- a/src/patch.zig +++ b/src/patch.zig @@ -75,7 +75,7 @@ pub const PatchFile = struct { } }; - pub fn apply(this: *const PatchFile, allocator: Allocator, patch_dir: bun.FileDescriptor) ?JSC.SystemError { + pub fn apply(this: *const PatchFile, allocator: Allocator, patch_dir: bun.FileDescriptor) ?bun.sys.Error { var state: ApplyState = .{}; var sfb = std.heap.stackFallback(1024, allocator); var arena = bun.ArenaAllocator.init(sfb.get()); @@ -87,7 +87,7 @@ pub const PatchFile = struct { const pathz = arena.allocator().dupeZ(u8, part.file_deletion.path) catch bun.outOfMemory(); if (bun.sys.unlinkat(patch_dir, pathz).asErr()) |e| { - return e.withPath(pathz).toSystemError(); + return e.withPath(pathz); } }, .file_rename => { @@ -97,7 +97,7 @@ pub const PatchFile = struct { if (std.fs.path.dirname(to_path)) |todir| { const abs_patch_dir = switch (state.patchDirAbsPath(patch_dir)) { .result => |p| p, - .err => |e| return e.toSystemError(), + .err => |e| return e, }; const path_to_make = bun.path.joinZ(&[_][]const u8{ abs_patch_dir, @@ -108,11 +108,11 @@ pub const PatchFile = struct { .path = .{ .string = bun.PathString.init(path_to_make) }, .recursive = true, .mode = 0o755, - }, .sync).asErr()) |e| return e.toSystemError(); + }).asErr()) |e| return e; } if (bun.sys.renameat(patch_dir, from_path, patch_dir, to_path).asErr()) |e| { - return e.toSystemError(); + return e; } }, .file_creation => { @@ -126,7 +126,7 @@ pub const PatchFile = struct { .path = .{ .string = bun.PathString.init(filedir) }, .recursive = true, .mode = @intCast(@intFromEnum(mode)), - }, .sync).asErr()) |e| return e.toSystemError(); + }).asErr()) |e| return e; } const newfile_fd = switch (bun.sys.openat( @@ -136,7 +136,7 @@ pub const PatchFile = struct { mode.toBunMode(), )) { .result => |fd| fd, - .err => |e| return e.withPath(filepath.slice()).toSystemError(), + .err => |e| return e.withPath(filepath.slice()), }; defer _ = bun.sys.close(newfile_fd); @@ -180,14 +180,14 @@ pub const PatchFile = struct { while (written < file_contents.len) { switch (bun.sys.write(newfile_fd, file_contents[written..])) { .result => |bytes| written += bytes, - .err => |e| return e.withPath(filepath.slice()).toSystemError(), + .err => |e| return e.withPath(filepath.slice()), } } }, .file_patch => { // TODO: should we compute the hash of the original file and check it against the on in the patch? if (applyPatch(part.file_patch, &arena, patch_dir, &state).asErr()) |e| { - return e.toSystemError(); + return e; } }, .file_mode_change => { @@ -195,22 +195,22 @@ pub const PatchFile = struct { const filepath = arena.allocator().dupeZ(u8, part.file_mode_change.path) catch bun.outOfMemory(); if (comptime bun.Environment.isPosix) { if (bun.sys.fchmodat(patch_dir, filepath, newmode.toBunMode(), 0).asErr()) |e| { - return e.toSystemError(); + return e; } } if (comptime bun.Environment.isWindows) { const absfilepath = switch (state.patchDirAbsPath(patch_dir)) { .result => |p| p, - .err => |e| return e.toSystemError(), + .err => |e| return e, }; const fd = switch (bun.sys.open(bun.path.joinZ(&[_][]const u8{ absfilepath, filepath }, .auto), bun.O.RDWR, 0)) { - .err => |e| return e.toSystemError(), + .err => |e| return e, .result => |f| f, }; defer _ = bun.sys.close(fd); if (bun.sys.fchmod(fd, newmode.toBunMode()).asErr()) |e| { - return e.toSystemError(); + return e; } } }, @@ -915,7 +915,7 @@ const PatchLinesParser = struct { fn parseHunkHeaderLineImpl(text_: []const u8) ParseErr!struct { line_nr: u32, line_count: u32, rest: []const u8 } { var text = text_; const DIGITS = brk: { - var set = std.bit_set.IntegerBitSet(256).initEmpty(); + var set = bun.bit_set.IntegerBitSet(256).initEmpty(); for ('0'..'9' + 1) |c| set.set(c); break :brk set; }; @@ -1026,8 +1026,8 @@ const PatchLinesParser = struct { const delimiter_start = std.mem.indexOf(u8, line, "..") orelse return null; - const VALID_CHARS: std.bit_set.IntegerBitSet(256) = comptime brk: { - var bitset = std.bit_set.IntegerBitSet(256).initEmpty(); + const VALID_CHARS: bun.bit_set.IntegerBitSet(256) = comptime brk: { + var bitset = bun.bit_set.IntegerBitSet(256).initEmpty(); // TODO: the regex uses \w which is [a-zA-Z0-9_] for ('0'..'9' + 1) |c| bitset.set(c); for ('a'..'z' + 1) |c| bitset.set(c); @@ -1150,7 +1150,7 @@ pub const TestingAPIs = struct { defer args.deinit(); if (args.patchfile.apply(bun.default_allocator, args.dirfd)) |err| { - return globalThis.throwValue(err.toErrorInstance(globalThis)); + return globalThis.throwValue(err.toJSC(globalThis)); } return .true; @@ -1285,7 +1285,7 @@ pub fn spawnOpts( .windows = if (bun.Environment.isWindows) .{ .loop = switch (loop.*) { .js => |x| .{ .js = x }, .mini => |*x| .{ .mini = x }, - } } else {}, + } }, }; } diff --git a/src/pool.zig b/src/pool.zig index 7d994c752f..30e5d3e31f 100644 --- a/src/pool.zig +++ b/src/pool.zig @@ -233,5 +233,21 @@ pub fn ObjectPool( data().list = LinkedList{ .first = node }; data().loaded = true; } + + pub fn deleteAll() void { + var dat = data(); + if (!dat.loaded) { + return; + } + dat.loaded = false; + dat.count = 0; + var next = dat.list.first; + dat.list.first = null; + while (next) |node| { + next = node.next; + if (std.meta.hasFn(Type, "deinit")) node.data.deinit(); + node.allocator.destroy(node); + } + } }; } diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index b801b6c11c..21860d42a3 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -601,9 +601,9 @@ pub const PackageJSON = struct { input_path: string, dirname_fd: StoredFileDescriptorType, package_id: ?Install.PackageID, - comptime include_scripts_: @Type(.EnumLiteral), - comptime include_dependencies: @Type(.EnumLiteral), - comptime generate_hash_: @Type(.EnumLiteral), + comptime include_scripts_: enum { ignore_scripts, include_scripts }, + comptime include_dependencies: enum { main, local, none }, + comptime generate_hash_: enum { generate_hash, no_hash }, ) ?PackageJSON { const generate_hash = generate_hash_ == .generate_hash; const include_scripts = include_scripts_ == .include_scripts; @@ -687,155 +687,151 @@ pub const PackageJSON = struct { } } - // If we're coming from `bun run` - // We do not need to parse all this stuff. - if (comptime !include_scripts) { - if (json.asProperty("type")) |type_json| { - if (type_json.expr.asString(allocator)) |type_str| { - switch (options.ModuleType.List.get(type_str) orelse options.ModuleType.unknown) { - .cjs => { - package_json.module_type = .cjs; - }, - .esm => { - package_json.module_type = .esm; - }, - .unknown => { - r.log.addRangeWarningFmt( + if (json.asProperty("type")) |type_json| { + if (type_json.expr.asString(allocator)) |type_str| { + switch (options.ModuleType.List.get(type_str) orelse options.ModuleType.unknown) { + .cjs => { + package_json.module_type = .cjs; + }, + .esm => { + package_json.module_type = .esm; + }, + .unknown => { + r.log.addRangeWarningFmt( + &json_source, + json_source.rangeOfString(type_json.loc), + allocator, + "\"{s}\" is not a valid value for \"type\" field (must be either \"commonjs\" or \"module\")", + .{type_str}, + ) catch unreachable; + }, + } + } else { + r.log.addWarning(&json_source, type_json.loc, "The value for \"type\" must be a string") catch unreachable; + } + } + + // Read the "main" fields + for (r.opts.main_fields) |main| { + if (json.asProperty(main)) |main_json| { + const expr: js_ast.Expr = main_json.expr; + + if ((expr.asString(allocator))) |str| { + if (str.len > 0) { + package_json.main_fields.put(main, str) catch unreachable; + } + } + } + } + + // Read the "browser" property, but only when targeting the browser + if (r.opts.target == .browser) { + // We both want the ability to have the option of CJS vs. ESM and the + // option of having node vs. browser. The way to do this is to use the + // object literal form of the "browser" field like this: + // + // "main": "dist/index.node.cjs.js", + // "module": "dist/index.node.esm.js", + // "browser": { + // "./dist/index.node.cjs.js": "./dist/index.browser.cjs.js", + // "./dist/index.node.esm.js": "./dist/index.browser.esm.js" + // }, + // + if (json.asProperty("browser")) |browser_prop| { + switch (browser_prop.expr.data) { + .e_object => |obj| { + // The value is an object + + // Remap all files in the browser field + for (obj.properties.slice()) |*prop| { + const _key_str = (prop.key orelse continue).asString(allocator) orelse continue; + const value: js_ast.Expr = prop.value orelse continue; + + // Normalize the path so we can compare against it without getting + // confused by "./". There is no distinction between package paths and + // relative paths for these values because some tools (i.e. Browserify) + // don't make such a distinction. + // + // This leads to weird things like a mapping for "./foo" matching an + // import of "foo", but that's actually not a bug. Or arguably it's a + // bug in Browserify but we have to replicate this bug because packages + // do this in the wild. + const key = allocator.dupe(u8, r.fs.normalize(_key_str)) catch unreachable; + + switch (value.data) { + .e_string => |str| { + // If this is a string, it's a replacement package + package_json.browser_map.put(key, str.string(allocator) catch unreachable) catch unreachable; + }, + .e_boolean => |boolean| { + if (!boolean.value) { + package_json.browser_map.put(key, "") catch unreachable; + } + }, + else => { + r.log.addWarning(&json_source, value.loc, "Each \"browser\" mapping must be a string or boolean") catch unreachable; + }, + } + } + }, + else => {}, + } + } + } + + if (json.asProperty("exports")) |exports_prop| { + if (ExportsMap.parse(bun.default_allocator, &json_source, r.log, exports_prop.expr, exports_prop.loc)) |exports_map| { + package_json.exports = exports_map; + } + } + + if (json.asProperty("imports")) |imports_prop| { + if (ExportsMap.parse(bun.default_allocator, &json_source, r.log, imports_prop.expr, imports_prop.loc)) |imports_map| { + package_json.imports = imports_map; + } + } + + if (json.get("sideEffects")) |side_effects_field| outer: { + if (side_effects_field.asBool()) |boolean| { + if (!boolean) + package_json.side_effects = .{ .false = {} }; + } else if (side_effects_field.asArray()) |array_| { + var array = array_; + // TODO: switch to only storing hashes + var map = SideEffects.Map{}; + map.ensureTotalCapacity(allocator, array.array.items.len) catch unreachable; + while (array.next()) |item| { + if (item.asString(allocator)) |name| { + // TODO: support RegExp using JavaScriptCore <> C++ bindings + if (strings.containsChar(name, '*')) { + // https://sourcegraph.com/search?q=context:global+file:package.json+sideEffects%22:+%5B&patternType=standard&sm=1&groupBy=repo + // a lot of these seem to be css files which we don't care about for now anyway + // so we can just skip them in here + if (strings.eqlComptime(std.fs.path.extension(name), ".css")) + continue; + + r.log.addWarning( &json_source, - json_source.rangeOfString(type_json.loc), - allocator, - "\"{s}\" is not a valid value for \"type\" field (must be either \"commonjs\" or \"module\")", - .{type_str}, + item.loc, + "wildcard sideEffects are not supported yet, which means this package will be deoptimized", ) catch unreachable; - }, - } - } else { - r.log.addWarning(&json_source, type_json.loc, "The value for \"type\" must be a string") catch unreachable; - } - } + map.deinit(allocator); - // Read the "main" fields - for (r.opts.main_fields) |main| { - if (json.asProperty(main)) |main_json| { - const expr: js_ast.Expr = main_json.expr; - - if ((expr.asString(allocator))) |str| { - if (str.len > 0) { - package_json.main_fields.put(main, str) catch unreachable; + package_json.side_effects = .{ .unspecified = {} }; + break :outer; } + + var joined = [_]string{ + json_source.path.name.dirWithTrailingSlash(), + name, + }; + + _ = map.getOrPutAssumeCapacity( + bun.StringHashMapUnowned.Key.init(r.fs.join(&joined)), + ); } } - } - - // Read the "browser" property, but only when targeting the browser - if (r.opts.target == .browser) { - // We both want the ability to have the option of CJS vs. ESM and the - // option of having node vs. browser. The way to do this is to use the - // object literal form of the "browser" field like this: - // - // "main": "dist/index.node.cjs.js", - // "module": "dist/index.node.esm.js", - // "browser": { - // "./dist/index.node.cjs.js": "./dist/index.browser.cjs.js", - // "./dist/index.node.esm.js": "./dist/index.browser.esm.js" - // }, - // - if (json.asProperty("browser")) |browser_prop| { - switch (browser_prop.expr.data) { - .e_object => |obj| { - // The value is an object - - // Remap all files in the browser field - for (obj.properties.slice()) |*prop| { - const _key_str = (prop.key orelse continue).asString(allocator) orelse continue; - const value: js_ast.Expr = prop.value orelse continue; - - // Normalize the path so we can compare against it without getting - // confused by "./". There is no distinction between package paths and - // relative paths for these values because some tools (i.e. Browserify) - // don't make such a distinction. - // - // This leads to weird things like a mapping for "./foo" matching an - // import of "foo", but that's actually not a bug. Or arguably it's a - // bug in Browserify but we have to replicate this bug because packages - // do this in the wild. - const key = allocator.dupe(u8, r.fs.normalize(_key_str)) catch unreachable; - - switch (value.data) { - .e_string => |str| { - // If this is a string, it's a replacement package - package_json.browser_map.put(key, str.string(allocator) catch unreachable) catch unreachable; - }, - .e_boolean => |boolean| { - if (!boolean.value) { - package_json.browser_map.put(key, "") catch unreachable; - } - }, - else => { - r.log.addWarning(&json_source, value.loc, "Each \"browser\" mapping must be a string or boolean") catch unreachable; - }, - } - } - }, - else => {}, - } - } - } - - if (json.asProperty("exports")) |exports_prop| { - if (ExportsMap.parse(bun.default_allocator, &json_source, r.log, exports_prop.expr, exports_prop.loc)) |exports_map| { - package_json.exports = exports_map; - } - } - - if (json.asProperty("imports")) |imports_prop| { - if (ExportsMap.parse(bun.default_allocator, &json_source, r.log, imports_prop.expr, imports_prop.loc)) |imports_map| { - package_json.imports = imports_map; - } - } - - if (json.get("sideEffects")) |side_effects_field| outer: { - if (side_effects_field.asBool()) |boolean| { - if (!boolean) - package_json.side_effects = .{ .false = {} }; - } else if (side_effects_field.asArray()) |array_| { - var array = array_; - // TODO: switch to only storing hashes - var map = SideEffects.Map{}; - map.ensureTotalCapacity(allocator, array.array.items.len) catch unreachable; - while (array.next()) |item| { - if (item.asString(allocator)) |name| { - // TODO: support RegExp using JavaScriptCore <> C++ bindings - if (strings.containsChar(name, '*')) { - // https://sourcegraph.com/search?q=context:global+file:package.json+sideEffects%22:+%5B&patternType=standard&sm=1&groupBy=repo - // a lot of these seem to be css files which we don't care about for now anyway - // so we can just skip them in here - if (strings.eqlComptime(std.fs.path.extension(name), ".css")) - continue; - - r.log.addWarning( - &json_source, - item.loc, - "wildcard sideEffects are not supported yet, which means this package will be deoptimized", - ) catch unreachable; - map.deinit(allocator); - - package_json.side_effects = .{ .unspecified = {} }; - break :outer; - } - - var joined = [_]string{ - json_source.path.name.dirWithTrailingSlash(), - name, - }; - - _ = map.getOrPutAssumeCapacity( - bun.StringHashMapUnowned.Key.init(r.fs.join(&joined)), - ); - } - } - package_json.side_effects = .{ .map = map }; - } + package_json.side_effects = .{ .map = map }; } } @@ -864,7 +860,7 @@ pub const PackageJSON = struct { pm, )) |dependency_version| { if (dependency_version.value.npm.version.isExact()) { - if (pm.lockfile.resolve(package_json.name, dependency_version)) |resolved| { + if (pm.lockfile.resolvePackageFromNameAndVersion(package_json.name, dependency_version)) |resolved| { package_json.package_manager_package_id = resolved; if (resolved > 0) { break :update_dependencies; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index e493bcd998..dec3d1bc49 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -2,7 +2,7 @@ const tester = @import("../test/tester.zig"); const std = @import("std"); const strings = @import("../string_immutable.zig"); const FeatureFlags = @import("../feature_flags.zig"); -const default_allocator = @import("../memory_allocator.zig").c_allocator; +const default_allocator = @import("../allocators/memory_allocator.zig").c_allocator; const bun = @import("root").bun; const Fs = @import("../fs.zig"); @@ -583,10 +583,10 @@ pub fn relativeAlloc(allocator: std.mem.Allocator, from: []const u8, to: []const // This function is based on Go's volumeNameLen function // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path_windows.go;l=57 // volumeNameLen returns length of the leading volume name on Windows. -fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { +pub fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { return windowsVolumeNameLenT(u8, path); } -fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { +pub fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { if (path.len < 2) return .{ 0, 0 }; // with drive letter const c = path[0]; @@ -682,13 +682,16 @@ pub fn windowsFilesystemRootT(comptime T: type, path: []const T) []const T { } } - // UNC + // UNC and device paths if (path.len >= 5 and Platform.windows.isSeparatorT(T, path[0]) and Platform.windows.isSeparatorT(T, path[1]) and - !Platform.windows.isSeparatorT(T, path[2]) and - path[2] != '.') + !Platform.windows.isSeparatorT(T, path[2])) { + // device path + if (path[2] == '.' and Platform.windows.isSeparatorT(T, path[3])) return path[0..4]; + + // UNC if (bun.strings.indexOfAnyT(T, path[3..], "/\\")) |idx| { if (bun.strings.indexOfAnyT(T, path[4 + idx ..], "/\\")) |idx_second| { return path[0 .. idx + idx_second + 4 + 1]; // +1 to skip second separator @@ -1252,9 +1255,11 @@ pub fn joinAbs(cwd: []const u8, comptime _platform: Platform, part: []const u8) return joinAbsString(cwd, &.{part}, _platform); } -// Convert parts of potentially invalid file paths into a single valid filpeath -// without querying the filesystem -// This is the equivalent of path.resolve +/// Convert parts of potentially invalid file paths into a single valid filpeath +/// without querying the filesystem +/// This is the equivalent of path.resolve +/// +/// Returned path is stored in a temporary buffer. It must be copied if it needs to be stored. pub fn joinAbsString(_cwd: []const u8, parts: anytype, comptime _platform: Platform) []const u8 { return joinAbsStringBuf( _cwd, @@ -1264,6 +1269,11 @@ pub fn joinAbsString(_cwd: []const u8, parts: anytype, comptime _platform: Platf ); } +/// Convert parts of potentially invalid file paths into a single valid filpeath +/// without querying the filesystem +/// This is the equivalent of path.resolve +/// +/// Returned path is stored in a temporary buffer. It must be copied if it needs to be stored. pub fn joinAbsStringZ(_cwd: []const u8, parts: anytype, comptime _platform: Platform) [:0]const u8 { return joinAbsStringBufZ( _cwd, @@ -2048,7 +2058,7 @@ export fn ResolvePath__joinAbsStringBufCurrentPlatformBunString( defer str.deinit(); const out_slice = joinAbsStringBuf( - globalObject.bunVM().bundler.fs.top_level_dir, + globalObject.bunVM().transpiler.fs.top_level_dir, &join_buf, &.{str.slice()}, comptime Platform.auto.resolve(), @@ -2067,11 +2077,29 @@ pub fn platformToPosixInPlace(comptime T: type, path_buffer: []T) void { pub fn dangerouslyConvertPathToPosixInPlace(comptime T: type, path: []T) void { var idx: usize = 0; + if (comptime bun.Environment.isWindows) { + if (path.len > "C:".len and isDriveLetter(path[0]) and path[1] == ':' and isSepAny(path[2])) { + // Uppercase drive letter + switch (path[0]) { + 'a'...'z' => path[0] = 'A' + (path[0] - 'a'), + 'A'...'Z' => {}, + else => unreachable, + } + } + } + while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_windows)) |index| : (idx = index + 1) { path[index] = '/'; } } +pub fn dangerouslyConvertPathToWindowsInPlace(comptime T: type, path: []T) void { + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_posix)) |index| : (idx = index + 1) { + path[index] = '\\'; + } +} + pub fn pathToPosixBuf(comptime T: type, path: []const T, buf: []T) []T { var idx: usize = 0; while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_windows)) |index| : (idx = index + 1) { diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index c765c9f79b..0e4250b271 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -28,7 +28,7 @@ const DataURL = @import("./data_url.zig").DataURL; pub const DirInfo = @import("./dir_info.zig"); const ResolvePath = @import("./resolve_path.zig"); const NodeFallbackModules = @import("../node_fallbacks.zig"); -const Mutex = @import("../lock.zig").Lock; +const Mutex = bun.Mutex; const StringBoolMap = bun.StringHashMap(bool); const FileDescriptorType = bun.FileDescriptor; const JSC = bun.JSC; @@ -687,7 +687,7 @@ pub const Resolver = struct { defer r.extension_order = original_order; r.extension_order = switch (kind) { .url, .at_conditional, .at => options.BundleOptions.Defaults.CSSExtensionOrder[0..], - .entry_point, .stmt, .dynamic => r.opts.extension_order.default.esm, + .entry_point_build, .entry_point_run, .stmt, .dynamic => r.opts.extension_order.default.esm, else => r.opts.extension_order.default.default, }; @@ -731,7 +731,7 @@ pub const Resolver = struct { // Certain types of URLs default to being external for convenience, // while these rules should not be applied to the entrypoint as it is never external (#12734) - if (kind != .entry_point and + if (kind != .entry_point_build and kind != .entry_point_run and (r.isExternalPattern(import_path) or // "fill: url(#filter);" (kind.isFromCSS() and strings.startsWith(import_path, "#")) or @@ -926,7 +926,7 @@ pub const Resolver = struct { .primary_side_effects_data = .no_side_effects__pure_data, }; }, - .import => |path| return r.resolve(r.fs.top_level_dir, path, .entry_point), + .import => |path| return r.resolve(r.fs.top_level_dir, path, .entry_point_build), } return .{}; } @@ -1155,7 +1155,7 @@ pub const Resolver = struct { // Check both relative and package paths for CSS URL tokens, with relative // paths taking precedence over package paths to match Webpack behavior. - const is_package_path = isPackagePathNotAbsolute(import_path); + const is_package_path = kind != .entry_point_run and isPackagePathNotAbsolute(import_path); var check_relative = !is_package_path or kind == .url; var check_package = is_package_path; @@ -1797,7 +1797,7 @@ pub const Resolver = struct { // check the global cache directory for a package.json file. const manager = r.getPackageManager(); var dependency_version = Dependency.Version{}; - var dependency_behavior = Dependency.Behavior.normal; + var dependency_behavior = Dependency.Behavior.prod; var string_buf = esm.version; // const initial_pending_tasks = manager.pending_tasks; @@ -1877,7 +1877,7 @@ pub const Resolver = struct { ) orelse break :load_module_from_cache; } - if (manager.lockfile.resolve(esm.name, dependency_version)) |id| { + if (manager.lockfile.resolvePackageFromNameAndVersion(esm.name, dependency_version)) |id| { resolved_package_id = id; } } @@ -1939,7 +1939,7 @@ pub const Resolver = struct { .root_request_id = 0, }, null, - ); + ) catch |enqueue_download_err| return .{ .failure = enqueue_download_err }; return .{ .pending = .{ @@ -2186,7 +2186,7 @@ pub const Resolver = struct { var pm = r.getPackageManager(); if (comptime Environment.allow_assert) { // we should never be trying to resolve a dependency that is already resolved - assert(pm.lockfile.resolve(esm.name, version) == null); + assert(pm.lockfile.resolvePackageFromNameAndVersion(esm.name, version) == null); } // Add the containing package to the lockfile @@ -3307,7 +3307,7 @@ pub const Resolver = struct { const in_str = argument.toBunString(globalThis); defer in_str.deref(); - const r = &globalThis.bunVM().bundler.resolver; + const r = &globalThis.bunVM().transpiler.resolver; return nodeModulePathsJSValue(r, in_str, globalThis); } @@ -3315,7 +3315,7 @@ pub const Resolver = struct { bun.JSC.markBinding(@src()); const in_str = bun.String.createUTF8("."); - const r = &globalThis.bunVM().bundler.resolver; + const r = &globalThis.bunVM().transpiler.resolver; return nodeModulePathsJSValue(r, in_str, globalThis); } diff --git a/src/resolver/tsconfig_json.zig b/src/resolver/tsconfig_json.zig index e203a2f5b4..c4e40a7056 100644 --- a/src/resolver/tsconfig_json.zig +++ b/src/resolver/tsconfig_json.zig @@ -264,8 +264,18 @@ pub const TSConfigJSON = struct { } if (compiler_opts.expr.asProperty("moduleSuffixes")) |prefixes| { - if (!source.path.isNodeModule()) { - log.addWarning(&source, prefixes.expr.loc, "moduleSuffixes is not supported yet") catch {}; + if (!source.path.isNodeModule()) handle_module_prefixes: { + var array = prefixes.expr.asArray() orelse break :handle_module_prefixes; + while (array.next()) |*element| { + if (element.asString(allocator)) |str| { + if (str.len > 0) { + // Only warn when there is actually content + // Sometimes, people do "moduleSuffixes": [""] + log.addWarning(&source, prefixes.loc, "moduleSuffixes is not supported yet") catch {}; + break :handle_module_prefixes; + } + } + } } } diff --git a/src/runtime.zig b/src/runtime.zig index c7309a280e..7fa0c80049 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -149,12 +149,15 @@ pub const Fallback = struct { }; pub const Runtime = struct { - pub const source_code = @embedFile("runtime.out.js"); - pub const hash = brk: { - @setEvalBranchQuota(source_code.len * 50); - break :brk bun.Wyhash11.hash(0, source_code); - }; + pub fn sourceCode() string { + return if (Environment.codegen_embed) + @embedFile("runtime.out.js") + else + bun.runtimeEmbedFile(.codegen, "runtime.out.js"); + } + pub fn versionHash() u32 { + const hash = bun.Wyhash11.hash(0, sourceCode()); return @truncate(hash); } @@ -191,10 +194,6 @@ pub const Runtime = struct { trim_unused_imports: bool = false, - /// Use `import.meta.require()` instead of require()? - /// This is only supported with --target=bun - use_import_meta_require: bool = false, - /// Allow runtime usage of require(), converting `require` into `__require` auto_polyfill_require: bool = false, @@ -240,7 +239,6 @@ pub const Runtime = struct { .dead_code_elimination, .set_breakpoint_on_first_line, .trim_unused_imports, - .use_import_meta_require, .dont_bundle_twice, .commonjs_at_runtime, .emit_decorator_metadata, diff --git a/src/s3/acl.zig b/src/s3/acl.zig new file mode 100644 index 0000000000..2d69bed30d --- /dev/null +++ b/src/s3/acl.zig @@ -0,0 +1,43 @@ +const bun = @import("root").bun; + +pub const ACL = enum { + /// Owner gets FULL_CONTROL. No one else has access rights (default). + private, + /// Owner gets FULL_CONTROL. The AllUsers group (see Who is a grantee?) gets READ access. + public_read, + /// Owner gets FULL_CONTROL. The AllUsers group gets READ and WRITE access. Granting this on a bucket is generally not recommended. + public_read_write, + /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an Amazon Machine Image (AMI) bundle from Amazon S3. + aws_exec_read, + /// Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access. + authenticated_read, + /// Object owner gets FULL_CONTROL. Bucket owner gets READ access. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + bucket_owner_read, + /// Both the object owner and the bucket owner get FULL_CONTROL over the object. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + bucket_owner_full_control, + log_delivery_write, + + pub fn toString(this: @This()) []const u8 { + return switch (this) { + .private => "private", + .public_read => "public-read", + .public_read_write => "public-read-write", + .aws_exec_read => "aws-exec-read", + .authenticated_read => "authenticated-read", + .bucket_owner_read => "bucket-owner-read", + .bucket_owner_full_control => "bucket-owner-full-control", + .log_delivery_write => "log-delivery-write", + }; + } + + pub const Map = bun.ComptimeStringMap(ACL, .{ + .{ "private", .private }, + .{ "public-read", .public_read }, + .{ "public-read-write", .public_read_write }, + .{ "aws-exec-read", .aws_exec_read }, + .{ "authenticated-read", .authenticated_read }, + .{ "bucket-owner-read", .bucket_owner_read }, + .{ "bucket-owner-full-control", .bucket_owner_full_control }, + .{ "log-delivery-write", .log_delivery_write }, + }); +}; diff --git a/src/s3/client.zig b/src/s3/client.zig new file mode 100644 index 0000000000..80231c3d87 --- /dev/null +++ b/src/s3/client.zig @@ -0,0 +1,627 @@ +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const picohttp = bun.picohttp; + +pub const ACL = @import("./acl.zig").ACL; +pub const S3HttpDownloadStreamingTask = @import("./download_stream.zig").S3HttpDownloadStreamingTask; +pub const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions; +pub const MultiPartUpload = @import("./multipart.zig").MultiPartUpload; + +pub const Error = @import("./error.zig"); +pub const throwSignError = Error.throwSignError; +pub const getJSSignError = Error.getJSSignError; + +const Credentials = @import("./credentials.zig"); +pub const S3Credentials = Credentials.S3Credentials; +pub const S3CredentialsWithOptions = Credentials.S3CredentialsWithOptions; + +const S3SimpleRequest = @import("./simple_request.zig"); +pub const S3HttpSimpleTask = S3SimpleRequest.S3HttpSimpleTask; +pub const S3UploadResult = S3SimpleRequest.S3UploadResult; +pub const S3StatResult = S3SimpleRequest.S3StatResult; +pub const S3DownloadResult = S3SimpleRequest.S3DownloadResult; +pub const S3DeleteResult = S3SimpleRequest.S3DeleteResult; + +pub fn stat( + this: *S3Credentials, + path: []const u8, + callback: *const fn (S3StatResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .HEAD, + .proxy_url = proxy_url, + .body = "", + }, .{ .stat = callback }, callback_context); +} + +pub fn download( + this: *S3Credentials, + path: []const u8, + callback: *const fn (S3DownloadResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .GET, + .proxy_url = proxy_url, + .body = "", + }, .{ .download = callback }, callback_context); +} + +pub fn downloadSlice( + this: *S3Credentials, + path: []const u8, + offset: usize, + size: ?usize, + callback: *const fn (S3DownloadResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + const range = brk: { + if (size) |size_| { + var end = (offset + size_); + if (size_ > 0) { + end -= 1; + } + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-{}", .{ offset, end }) catch bun.outOfMemory(); + } + if (offset == 0) break :brk null; + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-", .{offset}) catch bun.outOfMemory(); + }; + + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .GET, + .proxy_url = proxy_url, + .body = "", + .range = range, + }, .{ .download = callback }, callback_context); +} + +pub fn delete( + this: *S3Credentials, + path: []const u8, + callback: *const fn (S3DeleteResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .DELETE, + .proxy_url = proxy_url, + .body = "", + }, .{ .delete = callback }, callback_context); +} + +pub fn upload( + this: *S3Credentials, + path: []const u8, + content: []const u8, + content_type: ?[]const u8, + acl: ?ACL, + proxy_url: ?[]const u8, + callback: *const fn (S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .PUT, + .proxy_url = proxy_url, + .body = content, + .content_type = content_type, + .acl = acl, + }, .{ .upload = callback }, callback_context); +} +/// returns a writable stream that writes to the s3 path +pub fn writableStream( + this: *S3Credentials, + path: []const u8, + globalThis: *JSC.JSGlobalObject, + options: MultiPartUploadOptions, + content_type: ?[]const u8, + proxy: ?[]const u8, +) bun.JSError!JSC.JSValue { + const Wrapper = struct { + pub fn callback(result: S3UploadResult, sink: *JSC.WebCore.NetworkSink) void { + if (sink.endPromise.hasValue()) { + if (sink.endPromise.globalObject()) |globalObject| { + const event_loop = globalObject.bunVM().eventLoop(); + event_loop.enter(); + defer event_loop.exit(); + switch (result) { + .success => { + sink.endPromise.resolve(globalObject, JSC.jsNumber(0)); + }, + .failure => |err| { + if (!sink.done) { + sink.abort(); + return; + } + + sink.endPromise.reject(globalObject, err.toJS(globalObject, sink.path())); + }, + } + } + } + sink.finalize(); + } + }; + const proxy_url = (proxy orelse ""); + this.ref(); // ref the credentials + const task = MultiPartUpload.new(.{ + .credentials = this, + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + .proxy = if (proxy_url.len > 0) bun.default_allocator.dupe(u8, proxy_url) catch bun.outOfMemory() else "", + .content_type = if (content_type) |ct| bun.default_allocator.dupe(u8, ct) catch bun.outOfMemory() else null, + + .callback = @ptrCast(&Wrapper.callback), + .callback_context = undefined, + .globalThis = globalThis, + .options = options, + .vm = JSC.VirtualMachine.get(), + }); + + task.poll_ref.ref(task.vm); + + task.ref(); // + 1 for the stream + var response_stream = JSC.WebCore.NetworkSink.new(.{ + .task = .{ .s3_upload = task }, + .buffer = .{}, + .globalThis = globalThis, + .encoded = false, + .endPromise = JSC.JSPromise.Strong.init(globalThis), + }).toSink(); + + task.callback_context = @ptrCast(response_stream); + var signal = &response_stream.sink.signal; + + signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + bun.assert(signal.isDead()); + return response_stream.sink.toJS(globalThis); +} + +const S3UploadStreamWrapper = struct { + readable_stream_ref: JSC.WebCore.ReadableStream.Strong, + sink: *JSC.WebCore.NetworkSink, + task: *MultiPartUpload, + callback: ?*const fn (S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, + ref_count: u32 = 1, + path: []const u8, // this is owned by the task not by the wrapper + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + pub fn resolve(result: S3UploadResult, self: *@This()) void { + const sink = self.sink; + defer self.deref(); + if (sink.endPromise.hasValue()) { + if (sink.endPromise.globalObject()) |globalObject| { + switch (result) { + .success => sink.endPromise.resolve(globalObject, JSC.jsNumber(0)), + .failure => |err| { + if (!sink.done) { + sink.abort(); + return; + } + sink.endPromise.reject(globalObject, err.toJS(globalObject, self.path)); + }, + } + } + } + if (self.callback) |callback| { + callback(result, self.callback_context); + } + } + + pub fn deinit(self: *@This()) void { + self.readable_stream_ref.deinit(); + self.sink.finalize(); + self.sink.destroy(); + self.task.deref(); + self.destroy(); + } +}; + +pub fn onUploadStreamResolveRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); + defer this.deref(); + + if (this.readable_stream_ref.get()) |stream| { + stream.done(globalThis); + } + this.readable_stream_ref.deinit(); + this.task.continueStream(); + + return .undefined; +} + +pub fn onUploadStreamRejectRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); + defer this.deref(); + + const err = args.ptr[0]; + if (this.sink.endPromise.hasValue()) { + this.sink.endPromise.reject(globalThis, err); + } + + if (this.readable_stream_ref.get()) |stream| { + stream.cancel(globalThis); + this.readable_stream_ref.deinit(); + } + if (this.sink.task) |task| { + if (task == .s3_upload) { + task.s3_upload.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + } + } + this.task.continueStream(); + + return .undefined; +} +pub const shim = JSC.Shimmer("Bun", "S3UploadStream", @This()); + +pub const Export = shim.exportFunctions(.{ + .onResolveRequestStream = onUploadStreamResolveRequestStream, + .onRejectRequestStream = onUploadStreamRejectRequestStream, +}); +comptime { + const jsonResolveRequestStream = JSC.toJSHostFunction(onUploadStreamResolveRequestStream); + @export(jsonResolveRequestStream, .{ .name = Export[0].symbol_name }); + const jsonRejectRequestStream = JSC.toJSHostFunction(onUploadStreamRejectRequestStream); + @export(jsonRejectRequestStream, .{ .name = Export[1].symbol_name }); +} + +/// consumes the readable stream and upload to s3 +pub fn uploadStream( + this: *S3Credentials, + path: []const u8, + readable_stream: JSC.WebCore.ReadableStream, + globalThis: *JSC.JSGlobalObject, + options: MultiPartUploadOptions, + acl: ?ACL, + content_type: ?[]const u8, + proxy: ?[]const u8, + callback: ?*const fn (S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, +) JSC.JSValue { + this.ref(); // ref the credentials + const proxy_url = (proxy orelse ""); + + if (readable_stream.isDisturbed(globalThis)) { + return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is already disturbed").toErrorInstance(globalThis)); + } + + switch (readable_stream.ptr) { + .Invalid => { + return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is invalid").toErrorInstance(globalThis)); + }, + inline .File, .Bytes => |stream| { + if (stream.pending.result == .err) { + // we got an error, fail early + const err = stream.pending.result.err; + stream.pending = .{ .result = .{ .done = {} } }; + const js_err, const was_strong = err.toJSWeak(globalThis); + if (was_strong == .Strong) { + js_err.unprotect(); + } + js_err.ensureStillAlive(); + return JSC.JSPromise.rejectedPromise(globalThis, js_err).asValue(globalThis); + } + }, + else => {}, + } + + const task = MultiPartUpload.new(.{ + .credentials = this, + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + .proxy = if (proxy_url.len > 0) bun.default_allocator.dupe(u8, proxy_url) catch bun.outOfMemory() else "", + .content_type = if (content_type) |ct| bun.default_allocator.dupe(u8, ct) catch bun.outOfMemory() else null, + .callback = @ptrCast(&S3UploadStreamWrapper.resolve), + .callback_context = undefined, + .globalThis = globalThis, + .state = .wait_stream_check, + .options = options, + .acl = acl, + .vm = JSC.VirtualMachine.get(), + }); + + task.poll_ref.ref(task.vm); + + task.ref(); // + 1 for the stream sink + + var response_stream = JSC.WebCore.NetworkSink.new(.{ + .task = .{ .s3_upload = task }, + .buffer = .{}, + .globalThis = globalThis, + .encoded = false, + .endPromise = JSC.JSPromise.Strong.init(globalThis), + }).toSink(); + task.ref(); // + 1 for the stream wrapper + + const endPromise = response_stream.sink.endPromise.value(); + const ctx = S3UploadStreamWrapper.new(.{ + .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable_stream, globalThis), + .sink = &response_stream.sink, + .callback = callback, + .callback_context = callback_context, + .path = task.path, + .task = task, + }); + task.callback_context = @ptrCast(ctx); + // keep the task alive until we are done configuring the signal + task.ref(); + defer task.deref(); + + var signal = &response_stream.sink.signal; + + signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + bun.assert(signal.isDead()); + + // We are already corked! + const assignment_result: JSC.JSValue = JSC.WebCore.NetworkSink.JSSink.assignToStream( + globalThis, + readable_stream.value, + response_stream, + @as(**anyopaque, @ptrCast(&signal.ptr)), + ); + + assignment_result.ensureStillAlive(); + + // assert that it was updated + bun.assert(!signal.isDead()); + + if (assignment_result.toError()) |err| { + if (response_stream.sink.endPromise.hasValue()) { + response_stream.sink.endPromise.reject(globalThis, err); + } + + task.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + readable_stream.cancel(globalThis); + return endPromise; + } + + if (!assignment_result.isEmptyOrUndefinedOrNull()) { + assignment_result.ensureStillAlive(); + // it returns a Promise when it goes through ReadableStreamDefaultReader + if (assignment_result.asAnyPromise()) |promise| { + switch (promise.status(globalThis.vm())) { + .pending => { + // if we eended and its not canceled the promise is the endPromise + // because assignToStream can return the sink.end() promise + // we set the endPromise in the NetworkSink so we need to resolve it + if (response_stream.sink.ended and !response_stream.sink.cancel) { + task.continueStream(); + + readable_stream.done(globalThis); + return endPromise; + } + ctx.ref(); + + assignment_result.then( + globalThis, + task.callback_context, + onUploadStreamResolveRequestStream, + onUploadStreamRejectRequestStream, + ); + // we need to wait the promise to resolve because can be an error/cancel here + if (!task.ended) + task.continueStream(); + }, + .fulfilled => { + task.continueStream(); + + readable_stream.done(globalThis); + }, + .rejected => { + if (response_stream.sink.endPromise.hasValue()) { + response_stream.sink.endPromise.reject(globalThis, promise.result(globalThis.vm())); + } + + task.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + readable_stream.cancel(globalThis); + }, + } + } else { + if (response_stream.sink.endPromise.hasValue()) { + response_stream.sink.endPromise.reject(globalThis, assignment_result); + } + + task.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + readable_stream.cancel(globalThis); + } + } + return endPromise; +} + +/// download a file from s3 chunk by chunk aka streaming (used on readableStream) +pub fn downloadStream( + this: *S3Credentials, + path: []const u8, + offset: usize, + size: ?usize, + proxy_url: ?[]const u8, + callback: *const fn (chunk: bun.MutableString, has_more: bool, err: ?Error.S3Error, *anyopaque) void, + callback_context: *anyopaque, +) void { + const range = brk: { + if (size) |size_| { + if (offset == 0) break :brk null; + + var end = (offset + size_); + if (size_ > 0) { + end -= 1; + } + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-{}", .{ offset, end }) catch bun.outOfMemory(); + } + if (offset == 0) break :brk null; + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-", .{offset}) catch bun.outOfMemory(); + }; + + var result = this.signRequest(.{ + .path = path, + .method = .GET, + }, null) catch |sign_err| { + if (range) |range_| bun.default_allocator.free(range_); + const error_code_and_message = Error.getSignErrorCodeAndMessage(sign_err); + callback(.{ .allocator = bun.default_allocator, .list = .{} }, false, .{ + .code = error_code_and_message.code, + .message = error_code_and_message.message, + }, callback_context); + return; + }; + + var header_buffer: [10]picohttp.Header = undefined; + const headers = brk: { + if (range) |range_| { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); + } else { + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(result.headers(), bun.default_allocator) catch bun.outOfMemory(); + } + }; + const proxy = proxy_url orelse ""; + const owned_proxy = if (proxy.len > 0) bun.default_allocator.dupe(u8, proxy) catch bun.outOfMemory() else ""; + const task = S3HttpDownloadStreamingTask.new(.{ + .http = undefined, + .sign_result = result, + .proxy_url = owned_proxy, + .callback_context = callback_context, + .callback = callback, + .range = range, + .headers = headers, + .vm = JSC.VirtualMachine.get(), + }); + task.poll_ref.ref(task.vm); + + const url = bun.URL.parse(result.url); + + task.signals = task.signal_store.to(); + + task.http = bun.http.AsyncHTTP.init( + bun.default_allocator, + .GET, + url, + task.headers.entries, + task.headers.buf.items, + &task.response_buffer, + "", + bun.http.HTTPClientResult.Callback.New( + *S3HttpDownloadStreamingTask, + S3HttpDownloadStreamingTask.httpCallback, + ).init(task), + .follow, + .{ + .http_proxy = if (owned_proxy.len > 0) bun.URL.parse(owned_proxy) else null, + .verbose = task.vm.getVerboseFetch(), + .signals = task.signals, + .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), + }, + ); + // enable streaming + task.http.enableBodyStreaming(); + // queue http request + bun.http.HTTPThread.init(&.{}); + var batch = bun.ThreadPool.Batch{}; + task.http.schedule(bun.default_allocator, &batch); + bun.http.http_thread.schedule(batch); +} + +/// returns a readable stream that reads from the s3 path +pub fn readableStream( + this: *S3Credentials, + path: []const u8, + offset: usize, + size: ?usize, + proxy_url: ?[]const u8, + globalThis: *JSC.JSGlobalObject, +) JSC.JSValue { + var reader = JSC.WebCore.ByteStream.Source.new(.{ + .context = undefined, + .globalThis = globalThis, + }); + + reader.context.setup(); + const readable_value = reader.toReadableStream(globalThis); + + const S3DownloadStreamWrapper = struct { + readable_stream_ref: JSC.WebCore.ReadableStream.Strong, + path: []const u8, + pub usingnamespace bun.New(@This()); + + pub fn callback(chunk: bun.MutableString, has_more: bool, request_err: ?Error.S3Error, self: *@This()) void { + defer if (!has_more) self.deinit(); + + if (self.readable_stream_ref.get()) |readable| { + if (readable.ptr == .Bytes) { + if (request_err) |err| { + readable.ptr.Bytes.onData( + .{ + .err = .{ + .JSValue = err.toJS(self.readable_stream_ref.globalThis().?, self.path), + }, + }, + bun.default_allocator, + ); + return; + } + if (has_more) { + readable.ptr.Bytes.onData( + .{ + .temporary = bun.ByteList.initConst(chunk.list.items), + }, + bun.default_allocator, + ); + return; + } + + readable.ptr.Bytes.onData( + .{ + .temporary_and_done = bun.ByteList.initConst(chunk.list.items), + }, + bun.default_allocator, + ); + return; + } + } + } + + pub fn deinit(self: *@This()) void { + self.readable_stream_ref.deinit(); + bun.default_allocator.free(self.path); + self.destroy(); + } + }; + + downloadStream(this, path, offset, size, proxy_url, @ptrCast(&S3DownloadStreamWrapper.callback), S3DownloadStreamWrapper.new(.{ + .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(.{ + .ptr = .{ .Bytes = &reader.context }, + .value = readable_value, + }, globalThis), + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + })); + return readable_value; +} diff --git a/src/s3/credentials.zig b/src/s3/credentials.zig new file mode 100644 index 0000000000..b72c0b8c88 --- /dev/null +++ b/src/s3/credentials.zig @@ -0,0 +1,774 @@ +const bun = @import("root").bun; +const picohttp = bun.picohttp; +const std = @import("std"); + +const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions; +const ACL = @import("./acl.zig").ACL; +const JSC = bun.JSC; +const RareData = JSC.RareData; +const strings = bun.strings; +const DotEnv = bun.DotEnv; + +pub const S3Credentials = struct { + accessKeyId: []const u8, + secretAccessKey: []const u8, + region: []const u8, + endpoint: []const u8, + bucket: []const u8, + sessionToken: []const u8, + + /// Important for MinIO support. + insecure_http: bool = false, + + ref_count: u32 = 1, + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + + pub fn estimatedSize(this: *const @This()) usize { + return @sizeOf(S3Credentials) + this.accessKeyId.len + this.region.len + this.secretAccessKey.len + this.endpoint.len + this.bucket.len; + } + + fn hashConst(acl: []const u8) u64 { + var hasher = std.hash.Wyhash.init(0); + var remain = acl; + + var buf: [@sizeOf(@TypeOf(hasher.buf))]u8 = undefined; + + while (remain.len > 0) { + const end = @min(hasher.buf.len, remain.len); + + hasher.update(strings.copyLowercaseIfNeeded(remain[0..end], &buf)); + remain = remain[end..]; + } + + return hasher.final(); + } + pub fn getCredentialsWithOptions(this: S3Credentials, default_options: MultiPartUploadOptions, options: ?JSC.JSValue, default_acl: ?ACL, globalObject: *JSC.JSGlobalObject) bun.JSError!S3CredentialsWithOptions { + // get ENV config + var new_credentials = S3CredentialsWithOptions{ + .credentials = this, + .options = default_options, + .acl = default_acl, + }; + errdefer { + new_credentials.deinit(); + } + + if (options) |opts| { + if (opts.isObject()) { + if (try opts.getTruthyComptime(globalObject, "accessKeyId")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._accessKeyIdSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.accessKeyId = new_credentials._accessKeyIdSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("accessKeyId", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "secretAccessKey")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._secretAccessKeySlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.secretAccessKey = new_credentials._secretAccessKeySlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("secretAccessKey", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "region")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._regionSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.region = new_credentials._regionSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("region", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "endpoint")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._endpointSlice = str.toUTF8(bun.default_allocator); + const endpoint = new_credentials._endpointSlice.?.slice(); + const url = bun.URL.parse(endpoint); + const normalized_endpoint = url.host; + if (normalized_endpoint.len > 0) { + new_credentials.credentials.endpoint = normalized_endpoint; + + // Default to https:// + // Only use http:// if the endpoint specifically starts with 'http://' + new_credentials.credentials.insecure_http = url.isHTTP(); + + new_credentials.changed_credentials = true; + } else if (endpoint.len > 0) { + // endpoint is not a valid URL + return globalObject.throwInvalidArgumentTypeValue("endpoint", "string", js_value); + } + } + } else { + return globalObject.throwInvalidArgumentTypeValue("endpoint", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "bucket")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._bucketSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.bucket = new_credentials._bucketSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); + } + } + } + + if (try opts.getTruthyComptime(globalObject, "sessionToken")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._sessionTokenSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.sessionToken = new_credentials._sessionTokenSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); + } + } + } + + if (try opts.getOptional(globalObject, "pageSize", i64)) |pageSize| { + if (pageSize < MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE and pageSize > MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE) { + return globalObject.throwRangeError(pageSize, .{ + .min = @intCast(MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE), + .max = @intCast(MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE), + .field_name = "pageSize", + }); + } else { + new_credentials.options.partSize = @intCast(pageSize); + } + } + + if (try opts.getOptional(globalObject, "queueSize", i32)) |queueSize| { + if (queueSize < 1) { + return globalObject.throwRangeError(queueSize, .{ + .min = 1, + .field_name = "queueSize", + }); + } else { + new_credentials.options.queueSize = @intCast(@max(queueSize, std.math.maxInt(u8))); + } + } + + if (try opts.getOptional(globalObject, "retry", i32)) |retry| { + if (retry < 0 and retry > 255) { + return globalObject.throwRangeError(retry, .{ + .min = 0, + .max = 255, + .field_name = "retry", + }); + } else { + new_credentials.options.retry = @intCast(retry); + } + } + if (try opts.getOptionalEnum(globalObject, "acl", ACL)) |acl| { + new_credentials.acl = acl; + } + } + } + return new_credentials; + } + pub fn dupe(this: *const @This()) *S3Credentials { + return S3Credentials.new(.{ + .accessKeyId = if (this.accessKeyId.len > 0) + bun.default_allocator.dupe(u8, this.accessKeyId) catch bun.outOfMemory() + else + "", + + .secretAccessKey = if (this.secretAccessKey.len > 0) + bun.default_allocator.dupe(u8, this.secretAccessKey) catch bun.outOfMemory() + else + "", + + .region = if (this.region.len > 0) + bun.default_allocator.dupe(u8, this.region) catch bun.outOfMemory() + else + "", + + .endpoint = if (this.endpoint.len > 0) + bun.default_allocator.dupe(u8, this.endpoint) catch bun.outOfMemory() + else + "", + + .bucket = if (this.bucket.len > 0) + bun.default_allocator.dupe(u8, this.bucket) catch bun.outOfMemory() + else + "", + + .sessionToken = if (this.sessionToken.len > 0) + bun.default_allocator.dupe(u8, this.sessionToken) catch bun.outOfMemory() + else + "", + + .insecure_http = this.insecure_http, + }); + } + pub fn deinit(this: *@This()) void { + if (this.accessKeyId.len > 0) { + bun.default_allocator.free(this.accessKeyId); + } + if (this.secretAccessKey.len > 0) { + bun.default_allocator.free(this.secretAccessKey); + } + if (this.region.len > 0) { + bun.default_allocator.free(this.region); + } + if (this.endpoint.len > 0) { + bun.default_allocator.free(this.endpoint); + } + if (this.bucket.len > 0) { + bun.default_allocator.free(this.bucket); + } + if (this.sessionToken.len > 0) { + bun.default_allocator.free(this.sessionToken); + } + this.destroy(); + } + + const log = bun.Output.scoped(.AWS, false); + + const DateResult = struct { + // numeric representation of year, month and day (excluding time components) + numeric_day: u64, + date: []const u8, + }; + + fn getAMZDate(allocator: std.mem.Allocator) DateResult { + // We can also use Date.now() but would be slower and would add JSC dependency + // var buffer: [28]u8 = undefined; + // the code bellow is the same as new Date(Date.now()).toISOString() + // JSC.JSValue.getDateNowISOString(globalObject, &buffer); + + // Create UTC timestamp + const secs: u64 = @intCast(@divFloor(std.time.milliTimestamp(), 1000)); + const utc_seconds = std.time.epoch.EpochSeconds{ .secs = secs }; + const utc_day = utc_seconds.getEpochDay(); + const year_and_day = utc_day.calculateYearDay(); + const month_and_day = year_and_day.calculateMonthDay(); + // Get UTC date components + const year = year_and_day.year; + const day = @as(u32, month_and_day.day_index) + 1; // this starts in 0 + const month = month_and_day.month.numeric(); // starts in 1 + + // Get UTC time components + const time = utc_seconds.getDaySeconds(); + const hours = time.getHoursIntoDay(); + const minutes = time.getMinutesIntoHour(); + const seconds = time.getSecondsIntoMinute(); + + // Format the date + return .{ + .numeric_day = secs - time.secs, + .date = std.fmt.allocPrint(allocator, "{d:0>4}{d:0>2}{d:0>2}T{d:0>2}{d:0>2}{d:0>2}Z", .{ + year, + month, + day, + hours, + minutes, + seconds, + }) catch bun.outOfMemory(), + }; + } + + const DIGESTED_HMAC_256_LEN = 32; + pub const SignResult = struct { + amz_date: []const u8, + host: []const u8, + authorization: []const u8, + url: []const u8, + + content_disposition: []const u8 = "", + session_token: []const u8 = "", + acl: ?ACL = null, + _headers: [7]picohttp.Header = .{ + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + }, + _headers_len: u8 = 0, + + pub fn headers(this: *const @This()) []const picohttp.Header { + return this._headers[0..this._headers_len]; + } + + pub fn mixWithHeader(this: *const @This(), headers_buffer: []picohttp.Header, header: picohttp.Header) []const picohttp.Header { + // copy the headers to buffer + const len = this._headers_len; + for (this._headers[0..len], 0..len) |existing_header, i| { + headers_buffer[i] = existing_header; + } + headers_buffer[len] = header; + return headers_buffer[0 .. len + 1]; + } + + pub fn deinit(this: *const @This()) void { + if (this.amz_date.len > 0) { + bun.default_allocator.free(this.amz_date); + } + + if (this.session_token.len > 0) { + bun.default_allocator.free(this.session_token); + } + + if (this.content_disposition.len > 0) { + bun.default_allocator.free(this.content_disposition); + } + + if (this.host.len > 0) { + bun.default_allocator.free(this.host); + } + + if (this.authorization.len > 0) { + bun.default_allocator.free(this.authorization); + } + + if (this.url.len > 0) { + bun.default_allocator.free(this.url); + } + } + }; + + pub const SignQueryOptions = struct { + expires: usize = 86400, + }; + pub const SignOptions = struct { + path: []const u8, + method: bun.http.Method, + content_hash: ?[]const u8 = null, + search_params: ?[]const u8 = null, + content_disposition: ?[]const u8 = null, + acl: ?ACL = null, + }; + + pub fn guessRegion(endpoint: []const u8) []const u8 { + if (endpoint.len > 0) { + if (strings.endsWith(endpoint, ".r2.cloudflarestorage.com")) return "auto"; + if (strings.indexOf(endpoint, ".amazonaws.com")) |end| { + if (strings.indexOf(endpoint, "s3.")) |start| { + return endpoint[start + 3 .. end]; + } + } + // endpoint is informed but is not s3 so auto detect + return "auto"; + } + + // no endpoint so we default to us-east-1 because s3.us-east-1.amazonaws.com is the default endpoint + return "us-east-1"; + } + fn toHexChar(value: u8) !u8 { + return switch (value) { + 0...9 => value + '0', + 10...15 => (value - 10) + 'A', + else => error.InvalidHexChar, + }; + } + fn encodeURIComponent(input: []const u8, buffer: []u8, comptime encode_slash: bool) ![]const u8 { + var written: usize = 0; + + for (input) |c| { + switch (c) { + // RFC 3986 Unreserved Characters (do not encode) + 'A'...'Z', 'a'...'z', '0'...'9', '-', '_', '.', '~' => { + if (written >= buffer.len) return error.BufferTooSmall; + buffer[written] = c; + written += 1; + }, + // All other characters need to be percent-encoded + else => { + if (!encode_slash and (c == '/' or c == '\\')) { + if (written >= buffer.len) return error.BufferTooSmall; + buffer[written] = if (c == '\\') '/' else c; + written += 1; + continue; + } + if (written + 3 > buffer.len) return error.BufferTooSmall; + buffer[written] = '%'; + // Convert byte to hex + const high_nibble: u8 = (c >> 4) & 0xF; + const low_nibble: u8 = c & 0xF; + buffer[written + 1] = try toHexChar(high_nibble); + buffer[written + 2] = try toHexChar(low_nibble); + written += 3; + }, + } + } + + return buffer[0..written]; + } + + pub fn signRequest(this: *const @This(), signOptions: SignOptions, signQueryOption: ?SignQueryOptions) !SignResult { + const method = signOptions.method; + const request_path = signOptions.path; + const content_hash = signOptions.content_hash; + + const search_params = signOptions.search_params; + + var content_disposition = signOptions.content_disposition; + if (content_disposition != null and content_disposition.?.len == 0) { + content_disposition = null; + } + const session_token: ?[]const u8 = if (this.sessionToken.len == 0) null else this.sessionToken; + + const acl: ?[]const u8 = if (signOptions.acl) |acl_value| acl_value.toString() else null; + + if (this.accessKeyId.len == 0 or this.secretAccessKey.len == 0) return error.MissingCredentials; + const signQuery = signQueryOption != null; + const expires = if (signQueryOption) |options| options.expires else 0; + const method_name = switch (method) { + .GET => "GET", + .POST => "POST", + .PUT => "PUT", + .DELETE => "DELETE", + .HEAD => "HEAD", + else => return error.InvalidMethod, + }; + + const region = if (this.region.len > 0) this.region else guessRegion(this.endpoint); + var full_path = request_path; + // handle \\ on bucket name + if (strings.startsWith(full_path, "/")) { + full_path = full_path[1..]; + } else if (strings.startsWith(full_path, "\\")) { + full_path = full_path[1..]; + } + + var path: []const u8 = full_path; + var bucket: []const u8 = this.bucket; + + if (bucket.len == 0) { + //TODO: r2 supports bucket in the endpoint + + // guess bucket using path + if (strings.indexOf(full_path, "/")) |end| { + if (strings.indexOf(full_path, "\\")) |backslash_index| { + if (backslash_index < end) { + bucket = full_path[0..backslash_index]; + path = full_path[backslash_index + 1 ..]; + } + } + bucket = full_path[0..end]; + path = full_path[end + 1 ..]; + } else if (strings.indexOf(full_path, "\\")) |backslash_index| { + bucket = full_path[0..backslash_index]; + path = full_path[backslash_index + 1 ..]; + } else { + return error.InvalidPath; + } + } + if (strings.endsWith(path, "/")) { + path = path[0..path.len]; + } else if (strings.endsWith(path, "\\")) { + path = path[0 .. path.len - 1]; + } + if (strings.startsWith(path, "/")) { + path = path[1..]; + } else if (strings.startsWith(path, "\\")) { + path = path[1..]; + } + + // if we allow path.len == 0 it will list the bucket for now we disallow + if (path.len == 0) return error.InvalidPath; + + var normalized_path_buffer: [1024 + 63 + 2]u8 = undefined; // 1024 max key size and 63 max bucket name + var path_buffer: [1024]u8 = undefined; + var bucket_buffer: [63]u8 = undefined; + bucket = encodeURIComponent(bucket, &bucket_buffer, false) catch return error.InvalidPath; + path = encodeURIComponent(path, &path_buffer, false) catch return error.InvalidPath; + const normalizedPath = std.fmt.bufPrint(&normalized_path_buffer, "/{s}/{s}", .{ bucket, path }) catch return error.InvalidPath; + + const date_result = getAMZDate(bun.default_allocator); + const amz_date = date_result.date; + errdefer bun.default_allocator.free(amz_date); + + const amz_day = amz_date[0..8]; + const signed_headers = if (signQuery) "host" else brk: { + if (acl != null) { + if (content_disposition != null) { + if (session_token != null) { + break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date"; + } + } else { + if (session_token != null) { + break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "host;x-amz-acl;x-amz-content-sha256;x-amz-date"; + } + } + } else { + if (content_disposition != null) { + if (session_token != null) { + break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date"; + } + } else { + if (session_token != null) { + break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "host;x-amz-content-sha256;x-amz-date"; + } + } + } + }; + + // Default to https. Only use http if they explicit pass "http://" as the endpoint. + const protocol = if (this.insecure_http) "http" else "https"; + + // detect service name and host from region or endpoint + const host = brk_host: { + if (this.endpoint.len > 0) { + if (this.endpoint.len >= 512) return error.InvalidEndpoint; + break :brk_host try bun.default_allocator.dupe(u8, this.endpoint); + } else { + break :brk_host try std.fmt.allocPrint(bun.default_allocator, "s3.{s}.amazonaws.com", .{region}); + } + }; + const service_name = "s3"; + + errdefer bun.default_allocator.free(host); + + const aws_content_hash = if (content_hash) |hash| hash else ("UNSIGNED-PAYLOAD"); + var tmp_buffer: [4096]u8 = undefined; + + const authorization = brk: { + // we hash the hash so we need 2 buffers + var hmac_sig_service: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; + var hmac_sig_service2: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; + + const sigDateRegionServiceReq = brk_sign: { + const key = try std.fmt.bufPrint(&tmp_buffer, "{s}{s}{s}", .{ region, service_name, this.secretAccessKey }); + var cache = (JSC.VirtualMachine.getMainThreadVM() orelse JSC.VirtualMachine.get()).rareData().awsCache(); + if (cache.get(date_result.numeric_day, key)) |cached| { + break :brk_sign cached; + } + // not cached yet lets generate a new one + const sigDate = bun.hmac.generate(try std.fmt.bufPrint(&tmp_buffer, "AWS4{s}", .{this.secretAccessKey}), amz_day, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + const sigDateRegion = bun.hmac.generate(sigDate, region, .sha256, &hmac_sig_service2) orelse return error.FailedToGenerateSignature; + const sigDateRegionService = bun.hmac.generate(sigDateRegion, service_name, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + const result = bun.hmac.generate(sigDateRegionService, "aws4_request", .sha256, &hmac_sig_service2) orelse return error.FailedToGenerateSignature; + + cache.set(date_result.numeric_day, key, hmac_sig_service2[0..DIGESTED_HMAC_256_LEN].*); + break :brk_sign result; + }; + if (signQuery) { + var token_encoded_buffer: [2048]u8 = undefined; // token is normaly like 600-700 but can be up to 2k + var encoded_session_token: ?[]const u8 = null; + if (session_token) |token| { + encoded_session_token = encodeURIComponent(token, &token_encoded_buffer, true) catch return error.InvalidSessionToken; + } + const canonical = brk_canonical: { + if (acl) |acl_value| { + if (encoded_session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash }); + } + } else { + if (encoded_session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash }); + } + } + }; + var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); + bun.sha.SHA256.hash(canonical, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); + + const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) }); + + const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + if (acl) |acl_value| { + if (encoded_session_token) |token| { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } else { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + } else { + if (encoded_session_token) |token| { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } else { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + } + } else { + var encoded_content_disposition_buffer: [255]u8 = undefined; + const encoded_content_disposition: []const u8 = if (content_disposition) |cd| encodeURIComponent(cd, &encoded_content_disposition_buffer, true) catch return error.ContentTypeIsTooLong else ""; + const canonical = brk_canonical: { + if (acl) |acl_value| { + if (content_disposition != null) { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } else { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } + } else { + if (content_disposition != null) { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } else { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } + } + }; + var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); + bun.sha.SHA256.hash(canonical, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); + + const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) }); + + const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "AWS4-HMAC-SHA256 Credential={s}/{s}/{s}/{s}/aws4_request, SignedHeaders={s}, Signature={s}", + .{ this.accessKeyId, amz_day, region, service_name, signed_headers, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + }; + errdefer bun.default_allocator.free(authorization); + + if (signQuery) { + defer bun.default_allocator.free(host); + defer bun.default_allocator.free(amz_date); + + return SignResult{ + .amz_date = "", + .host = "", + .authorization = "", + .acl = signOptions.acl, + .url = authorization, + }; + } + + var result = SignResult{ + .amz_date = amz_date, + .host = host, + .authorization = authorization, + .acl = signOptions.acl, + .url = try std.fmt.allocPrint(bun.default_allocator, "{s}://{s}{s}{s}", .{ protocol, host, normalizedPath, if (search_params) |s| s else "" }), + ._headers = [_]picohttp.Header{ + .{ .name = "x-amz-content-sha256", .value = aws_content_hash }, + .{ .name = "x-amz-date", .value = amz_date }, + .{ .name = "Host", .value = host }, + .{ .name = "Authorization", .value = authorization[0..] }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + }, + ._headers_len = 4, + }; + + if (acl) |acl_value| { + result._headers[result._headers_len] = .{ .name = "x-amz-acl", .value = acl_value }; + result._headers_len += 1; + } + + if (session_token) |token| { + const session_token_value = bun.default_allocator.dupe(u8, token) catch bun.outOfMemory(); + result.session_token = session_token_value; + result._headers[result._headers_len] = .{ .name = "x-amz-security-token", .value = session_token_value }; + result._headers_len += 1; + } + + if (content_disposition) |cd| { + const content_disposition_value = bun.default_allocator.dupe(u8, cd) catch bun.outOfMemory(); + result.content_disposition = content_disposition_value; + result._headers[result._headers_len] = .{ .name = "Content-Disposition", .value = content_disposition_value }; + result._headers_len += 1; + } + + return result; + } +}; + +pub const S3CredentialsWithOptions = struct { + credentials: S3Credentials, + options: MultiPartUploadOptions = .{}, + acl: ?ACL = null, + /// indicates if the credentials have changed + changed_credentials: bool = false, + + _accessKeyIdSlice: ?JSC.ZigString.Slice = null, + _secretAccessKeySlice: ?JSC.ZigString.Slice = null, + _regionSlice: ?JSC.ZigString.Slice = null, + _endpointSlice: ?JSC.ZigString.Slice = null, + _bucketSlice: ?JSC.ZigString.Slice = null, + _sessionTokenSlice: ?JSC.ZigString.Slice = null, + + pub fn deinit(this: *@This()) void { + if (this._accessKeyIdSlice) |slice| slice.deinit(); + if (this._secretAccessKeySlice) |slice| slice.deinit(); + if (this._regionSlice) |slice| slice.deinit(); + if (this._endpointSlice) |slice| slice.deinit(); + if (this._bucketSlice) |slice| slice.deinit(); + if (this._sessionTokenSlice) |slice| slice.deinit(); + } +}; diff --git a/src/s3/download_stream.zig b/src/s3/download_stream.zig new file mode 100644 index 0000000000..fd2470b19b --- /dev/null +++ b/src/s3/download_stream.zig @@ -0,0 +1,242 @@ +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const picohttp = JSC.WebCore.picohttp; +const S3Error = @import("./error.zig").S3Error; +const S3Credentials = @import("./credentials.zig").S3Credentials; +const SignResult = S3Credentials.SignResult; +const strings = bun.strings; +const log = bun.Output.scoped(.S3, true); +pub const S3HttpDownloadStreamingTask = struct { + http: bun.http.AsyncHTTP, + vm: *JSC.VirtualMachine, + sign_result: SignResult, + headers: JSC.WebCore.Headers, + callback_context: *anyopaque, + // this transfers ownership from the chunk + callback: *const fn (chunk: bun.MutableString, has_more: bool, err: ?S3Error, *anyopaque) void, + has_schedule_callback: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + signal_store: bun.http.Signals.Store = .{}, + signals: bun.http.Signals = .{}, + poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), + + response_buffer: bun.MutableString = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }, + mutex: bun.Mutex = .{}, + reported_response_buffer: bun.MutableString = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }, + state: State.AtomicType = State.AtomicType.init(@bitCast(State{})), + + concurrent_task: JSC.ConcurrentTask = .{}, + range: ?[]const u8, + proxy_url: []const u8, + + pub usingnamespace bun.New(@This()); + pub const State = packed struct(u64) { + pub const AtomicType = std.atomic.Value(u64); + status_code: u32 = 0, + request_error: u16 = 0, + has_more: bool = true, + _reserved: u15 = 0, + }; + + pub fn getState(this: @This()) State { + const state: State = @bitCast(this.state.load(.acquire)); + return state; + } + + pub fn setState(this: *@This(), state: State) void { + this.state.store(@bitCast(state), .monotonic); + } + + pub fn deinit(this: *@This()) void { + this.poll_ref.unref(this.vm); + this.response_buffer.deinit(); + this.reported_response_buffer.deinit(); + this.headers.deinit(); + this.sign_result.deinit(); + this.http.clearData(); + if (this.range) |range| { + bun.default_allocator.free(range); + } + if (this.proxy_url.len > 0) { + bun.default_allocator.free(this.proxy_url); + } + + this.destroy(); + } + + fn reportProgress(this: *@This(), state: State) void { + const has_more = state.has_more; + var err: ?S3Error = null; + var failed = false; + + const chunk = brk: { + switch (state.status_code) { + 200, 204, 206 => { + failed = state.request_error != 0; + }, + else => { + failed = true; + }, + } + if (failed) { + if (!has_more) { + var has_body_code = false; + var has_body_message = false; + + var code: []const u8 = "UnknownError"; + var message: []const u8 = "an unexpected error has occurred"; + if (state.request_error != 0) { + const req_err = @errorFromInt(state.request_error); + code = @errorName(req_err); + has_body_code = true; + } else { + const bytes = this.reported_response_buffer.list.items; + if (bytes.len > 0) { + message = bytes[0..]; + + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + code = bytes[start + "".len .. end]; + has_body_code = true; + } + } + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + message = bytes[start + "".len .. end]; + has_body_message = true; + } + } + } + } + + err = .{ + .code = code, + .message = message, + }; + } + break :brk bun.MutableString{ .allocator = bun.default_allocator, .list = .{} }; + } else { + const buffer = this.reported_response_buffer; + break :brk buffer; + } + }; + log("reportProgres failed: {} has_more: {} len: {d}", .{ failed, has_more, chunk.list.items.len }); + if (failed) { + if (!has_more) { + this.callback(chunk, false, err, this.callback_context); + } + } else { + // dont report empty chunks if we have more data to read + if (!has_more or chunk.list.items.len > 0) { + this.callback(chunk, has_more, null, this.callback_context); + this.reported_response_buffer.reset(); + } + } + } + /// this is the task callback from the last task result and is always in the main thread + pub fn onResponse(this: *@This()) void { + // lets lock and unlock the reported response buffer + this.mutex.lock(); + // the state is atomic let's load it once + const state = this.getState(); + const has_more = state.has_more; + defer { + // always unlock when done + this.mutex.unlock(); + // if we dont have more we should deinit at the end of the function + if (!has_more) this.deinit(); + } + + // there is no reason to set has_schedule_callback to true if we dont have more data to read + if (has_more) this.has_schedule_callback.store(false, .monotonic); + this.reportProgress(state); + } + + /// this function is only called from the http callback in the HTTPThread and returns true if we should wait until we are done buffering the response body to report + /// should only be called when already locked + fn updateState(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult, state: *State) bool { + const is_done = !result.has_more; + // if we got a error or fail wait until we are done buffering the response body to report + var wait_until_done = false; + { + state.has_more = !is_done; + + state.request_error = if (result.fail) |err| @intFromError(err) else 0; + if (state.status_code == 0) { + if (result.certificate_info) |*certificate| { + certificate.deinit(bun.default_allocator); + } + if (result.metadata) |m| { + var metadata = m; + state.status_code = metadata.response.status_code; + metadata.deinit(bun.default_allocator); + } + } + switch (state.status_code) { + 200, 204, 206 => wait_until_done = state.request_error != 0, + else => wait_until_done = true, + } + // store the new state + this.setState(state.*); + this.http = async_http.*; + } + return wait_until_done; + } + + /// this functions is only called from the http callback in the HTTPThread and returns true if we should enqueue another task + fn processHttpCallback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) bool { + // lets lock and unlock to be safe we know the state is not in the middle of a callback when locked + this.mutex.lock(); + defer this.mutex.unlock(); + + // remember the state is atomic load it once, and store it again + var state = this.getState(); + // old state should have more otherwise its a http.zig bug + bun.assert(state.has_more); + const is_done = !result.has_more; + const wait_until_done = updateState(this, async_http, result, &state); + const should_enqueue = !wait_until_done or is_done; + log("state err: {} status_code: {} has_more: {} should_enqueue: {}", .{ state.request_error, state.status_code, state.has_more, should_enqueue }); + + if (should_enqueue) { + if (result.body) |body| { + this.response_buffer = body.*; + if (body.list.items.len > 0) { + _ = this.reported_response_buffer.write(body.list.items) catch bun.outOfMemory(); + } + this.response_buffer.reset(); + if (this.reported_response_buffer.list.items.len == 0 and !is_done) { + return false; + } + } else if (!is_done) { + return false; + } + if (this.has_schedule_callback.cmpxchgStrong(false, true, .acquire, .monotonic)) |has_schedule_callback| { + if (has_schedule_callback) { + return false; + } + } + return true; + } + return false; + } + /// this is the callback from the http.zig AsyncHTTP is always called from the HTTPThread + pub fn httpCallback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) void { + if (processHttpCallback(this, async_http, result)) { + // we are always unlocked here and its safe to enqueue + this.vm.eventLoop().enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); + } + } +}; diff --git a/src/s3/error.zig b/src/s3/error.zig new file mode 100644 index 0000000000..fd8f732e84 --- /dev/null +++ b/src/s3/error.zig @@ -0,0 +1,86 @@ +const bun = @import("root").bun; +const JSC = bun.JSC; +pub const ErrorCodeAndMessage = struct { + code: []const u8, + message: []const u8, +}; +pub fn getSignErrorMessage(comptime err: anyerror) [:0]const u8 { + return switch (err) { + error.MissingCredentials => return "Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required", + error.InvalidMethod => return "Method must be GET, PUT, DELETE or HEAD when using s3:// protocol", + error.InvalidPath => return "Invalid S3 bucket, key combination", + error.InvalidEndpoint => return "Invalid S3 endpoint", + error.InvalidSessionToken => return "Invalid session token", + else => return "Failed to retrieve S3 content. Are the credentials correct?", + }; +} +pub fn getJSSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + return switch (err) { + error.MissingCredentials => return globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).toJS(), + error.InvalidMethod => return globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).toJS(), + error.InvalidPath => return globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).toJS(), + error.InvalidEndpoint => return globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).toJS(), + error.InvalidSessionToken => return globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).toJS(), + else => return globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).toJS(), + }; +} +pub fn throwSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) bun.JSError { + return switch (err) { + error.MissingCredentials => globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).throw(), + error.InvalidMethod => globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).throw(), + error.InvalidPath => globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).throw(), + error.InvalidEndpoint => globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).throw(), + error.InvalidSessionToken => globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).throw(), + else => globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).throw(), + }; +} +pub fn getSignErrorCodeAndMessage(err: anyerror) ErrorCodeAndMessage { + // keep error codes consistent for internal errors + return switch (err) { + error.MissingCredentials => .{ .code = "ERR_S3_MISSING_CREDENTIALS", .message = getSignErrorMessage(error.MissingCredentials) }, + error.InvalidMethod => .{ .code = "ERR_S3_INVALID_METHOD", .message = getSignErrorMessage(error.InvalidMethod) }, + error.InvalidPath => .{ .code = "ERR_S3_INVALID_PATH", .message = getSignErrorMessage(error.InvalidPath) }, + error.InvalidEndpoint => .{ .code = "ERR_S3_INVALID_ENDPOINT", .message = getSignErrorMessage(error.InvalidEndpoint) }, + error.InvalidSessionToken => .{ .code = "ERR_S3_INVALID_SESSION_TOKEN", .message = getSignErrorMessage(error.InvalidSessionToken) }, + else => .{ .code = "ERR_S3_INVALID_SIGNATURE", .message = getSignErrorMessage(error.SignError) }, + }; +} + +const JSS3Error = extern struct { + code: bun.String = bun.String.empty, + message: bun.String = bun.String.empty, + path: bun.String = bun.String.empty, + + pub fn init(code: []const u8, message: []const u8, path: ?[]const u8) @This() { + return .{ + // lets make sure we can reuse code and message and keep it service independent + .code = bun.String.createAtomIfPossible(code), + .message = bun.String.createAtomIfPossible(message), + .path = if (path) |p| bun.String.init(p) else bun.String.empty, + }; + } + + pub fn deinit(this: *const @This()) void { + this.path.deref(); + this.code.deref(); + this.message.deref(); + } + + pub fn toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) JSC.JSValue { + defer this.deinit(); + + return S3Error__toErrorInstance(this, global); + } + extern fn S3Error__toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue; +}; + +pub const S3Error = struct { + code: []const u8, + message: []const u8, + + pub fn toJS(err: *const @This(), globalObject: *JSC.JSGlobalObject, path: ?[]const u8) JSC.JSValue { + const value = JSS3Error.init(err.code, err.message, path).toErrorInstance(globalObject); + bun.assert(!globalObject.hasException()); + return value; + } +}; diff --git a/src/s3/multipart.zig b/src/s3/multipart.zig new file mode 100644 index 0000000000..af5a37ebcb --- /dev/null +++ b/src/s3/multipart.zig @@ -0,0 +1,537 @@ +const std = @import("std"); +const bun = @import("root").bun; +const strings = bun.strings; +const S3Credentials = @import("./credentials.zig").S3Credentials; +const ACL = @import("./acl.zig").ACL; +const JSC = bun.JSC; +const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions; +const S3SimpleRequest = @import("./simple_request.zig"); +const executeSimpleS3Request = S3SimpleRequest.executeSimpleS3Request; +const S3Error = @import("./error.zig").S3Error; + +pub const MultiPartUpload = struct { + const OneMiB: usize = MultiPartUploadOptions.OneMiB; + const MAX_SINGLE_UPLOAD_SIZE: usize = MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE; // we limit to 5 GiB + const MIN_SINGLE_UPLOAD_SIZE: usize = MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE; + const DefaultPartSize = MultiPartUploadOptions.DefaultPartSize; + const MAX_QUEUE_SIZE = MultiPartUploadOptions.MAX_QUEUE_SIZE; + const AWS = S3Credentials; + queue: std.ArrayListUnmanaged(UploadPart) = .{}, + available: bun.bit_set.IntegerBitSet(MAX_QUEUE_SIZE) = bun.bit_set.IntegerBitSet(MAX_QUEUE_SIZE).initFull(), + + currentPartNumber: u16 = 1, + ref_count: u16 = 1, + ended: bool = false, + + options: MultiPartUploadOptions = .{}, + acl: ?ACL = null, + credentials: *S3Credentials, + poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), + vm: *JSC.VirtualMachine, + globalThis: *JSC.JSGlobalObject, + + buffered: std.ArrayListUnmanaged(u8) = .{}, + offset: usize = 0, + + path: []const u8, + proxy: []const u8, + content_type: ?[]const u8 = null, + upload_id: []const u8 = "", + uploadid_buffer: bun.MutableString = .{ .allocator = bun.default_allocator, .list = .{} }, + + multipart_etags: std.ArrayListUnmanaged(UploadPart.UploadPartResult) = .{}, + multipart_upload_list: bun.ByteList = .{}, + + state: enum { + wait_stream_check, + not_started, + multipart_started, + multipart_completed, + singlefile_started, + finished, + } = .not_started, + + callback: *const fn (S3SimpleRequest.S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, + + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + + const log = bun.Output.scoped(.S3MultiPartUpload, true); + + pub const UploadPart = struct { + data: []const u8, + state: enum { + pending, + started, + completed, + canceled, + }, + owns_data: bool, + partNumber: u16, // max is 10,000 + retry: u8, // auto retry, decrement until 0 and fail after this + index: u8, + ctx: *MultiPartUpload, + + pub const UploadPartResult = struct { + number: u16, + etag: []const u8, + }; + fn sortEtags(_: *MultiPartUpload, a: UploadPart.UploadPartResult, b: UploadPart.UploadPartResult) bool { + return a.number < b.number; + } + + pub fn onPartResponse(result: S3SimpleRequest.S3PartResult, this: *@This()) void { + if (this.state == .canceled or this.ctx.state == .finished) { + log("onPartResponse {} canceled", .{this.partNumber}); + if (this.owns_data) bun.default_allocator.free(this.data); + this.ctx.deref(); + return; + } + + this.state = .completed; + + switch (result) { + .failure => |err| { + if (this.retry > 0) { + log("onPartResponse {} retry", .{this.partNumber}); + this.retry -= 1; + // retry failed + this.perform(); + return; + } else { + log("onPartResponse {} failed", .{this.partNumber}); + if (this.owns_data) bun.default_allocator.free(this.data); + defer this.ctx.deref(); + return this.ctx.fail(err); + } + }, + .etag => |etag| { + log("onPartResponse {} success", .{this.partNumber}); + + if (this.owns_data) bun.default_allocator.free(this.data); + // we will need to order this + this.ctx.multipart_etags.append(bun.default_allocator, .{ + .number = this.partNumber, + .etag = bun.default_allocator.dupe(u8, etag) catch bun.outOfMemory(), + }) catch bun.outOfMemory(); + + defer this.ctx.deref(); + // mark as available + this.ctx.available.set(this.index); + // drain more + this.ctx.drainEnqueuedParts(); + }, + } + } + + fn perform(this: *@This()) void { + var params_buffer: [2048]u8 = undefined; + const search_params = std.fmt.bufPrint(¶ms_buffer, "?partNumber={}&uploadId={s}&x-id=UploadPart", .{ + this.partNumber, + this.ctx.upload_id, + }) catch unreachable; + executeSimpleS3Request(this.ctx.credentials, .{ + .path = this.ctx.path, + .method = .PUT, + .proxy_url = this.ctx.proxyUrl(), + .body = this.data, + .search_params = search_params, + }, .{ .part = @ptrCast(&onPartResponse) }, this); + } + pub fn start(this: *@This()) void { + if (this.state != .pending or this.ctx.state != .multipart_completed or this.ctx.state == .finished) return; + this.ctx.ref(); + this.state = .started; + this.perform(); + } + pub fn cancel(this: *@This()) void { + const state = this.state; + this.state = .canceled; + + switch (state) { + .pending => { + if (this.owns_data) bun.default_allocator.free(this.data); + }, + // if is not pending we will free later or is already freed + else => {}, + } + } + }; + + fn deinit(this: *@This()) void { + log("deinit", .{}); + if (this.queue.capacity > 0) + this.queue.deinit(bun.default_allocator); + this.poll_ref.unref(this.vm); + bun.default_allocator.free(this.path); + if (this.proxy.len > 0) { + bun.default_allocator.free(this.proxy); + } + if (this.content_type) |ct| { + if (ct.len > 0) { + bun.default_allocator.free(ct); + } + } + this.credentials.deref(); + this.uploadid_buffer.deinit(); + for (this.multipart_etags.items) |tag| { + bun.default_allocator.free(tag.etag); + } + if (this.multipart_etags.capacity > 0) + this.multipart_etags.deinit(bun.default_allocator); + if (this.multipart_upload_list.cap > 0) + this.multipart_upload_list.deinitWithAllocator(bun.default_allocator); + this.destroy(); + } + + pub fn singleSendUploadResponse(result: S3SimpleRequest.S3UploadResult, this: *@This()) void { + defer this.deref(); + if (this.state == .finished) return; + switch (result) { + .failure => |err| { + if (this.options.retry > 0) { + log("singleSendUploadResponse {} retry", .{this.options.retry}); + this.options.retry -= 1; + this.ref(); + // retry failed + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .PUT, + .proxy_url = this.proxyUrl(), + .body = this.buffered.items, + .content_type = this.content_type, + .acl = this.acl, + }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); + + return; + } else { + log("singleSendUploadResponse failed", .{}); + return this.fail(err); + } + }, + .success => { + log("singleSendUploadResponse success", .{}); + this.done(); + }, + } + } + + fn getCreatePart(this: *@This(), chunk: []const u8, owns_data: bool) ?*UploadPart { + const index = this.available.findFirstSet() orelse { + // this means that the queue is full and we cannot flush it + return null; + }; + + if (index >= this.options.queueSize) { + // ops too much concurrency wait more + return null; + } + this.available.unset(index); + defer this.currentPartNumber += 1; + + if (this.queue.items.len <= index) { + this.queue.append(bun.default_allocator, .{ + .data = chunk, + .partNumber = this.currentPartNumber, + .owns_data = owns_data, + .ctx = this, + .index = @truncate(index), + .retry = this.options.retry, + .state = .pending, + }) catch bun.outOfMemory(); + return &this.queue.items[index]; + } + this.queue.items[index] = .{ + .data = chunk, + .partNumber = this.currentPartNumber, + .owns_data = owns_data, + .ctx = this, + .index = @truncate(index), + .retry = this.options.retry, + .state = .pending, + }; + return &this.queue.items[index]; + } + + fn drainEnqueuedParts(this: *@This()) void { + if (this.state == .finished) { + return; + } + // check pending to start or transformed buffered ones into tasks + if (this.state == .multipart_completed) { + for (this.queue.items) |*part| { + if (part.state == .pending) { + // lets start the part request + part.start(); + } + } + } + const partSize = this.partSizeInBytes(); + if (this.ended or this.buffered.items.len >= partSize) { + this.processMultiPart(partSize); + } + + if (this.ended and this.available.mask == std.bit_set.IntegerBitSet(MAX_QUEUE_SIZE).initFull().mask) { + // we are done + this.done(); + } + } + pub fn fail(this: *@This(), _err: S3Error) void { + log("fail {s}:{s}", .{ _err.code, _err.message }); + this.ended = true; + for (this.queue.items) |*task| { + task.cancel(); + } + if (this.state != .finished) { + const old_state = this.state; + this.state = .finished; + this.callback(.{ .failure = _err }, this.callback_context); + + if (old_state == .multipart_completed) { + // will deref after rollback + this.rollbackMultiPartRequest(); + } else { + this.deref(); + } + } + } + + fn done(this: *@This()) void { + if (this.state == .multipart_completed) { + this.state = .finished; + + std.sort.block(UploadPart.UploadPartResult, this.multipart_etags.items, this, UploadPart.sortEtags); + this.multipart_upload_list.append(bun.default_allocator, "") catch bun.outOfMemory(); + for (this.multipart_etags.items) |tag| { + this.multipart_upload_list.appendFmt(bun.default_allocator, "{}{s}", .{ tag.number, tag.etag }) catch bun.outOfMemory(); + + bun.default_allocator.free(tag.etag); + } + this.multipart_etags.deinit(bun.default_allocator); + this.multipart_etags = .{}; + this.multipart_upload_list.append(bun.default_allocator, "") catch bun.outOfMemory(); + // will deref and ends after commit + this.commitMultiPartRequest(); + } else { + this.callback(.{ .success = {} }, this.callback_context); + this.state = .finished; + this.deref(); + } + } + pub fn startMultiPartRequestResult(result: S3SimpleRequest.S3DownloadResult, this: *@This()) void { + defer this.deref(); + if (this.state == .finished) return; + switch (result) { + .failure => |err| { + log("startMultiPartRequestResult {s} failed {s}: {s}", .{ this.path, err.message, err.message }); + this.fail(err); + }, + .success => |response| { + const slice = response.body.list.items; + this.uploadid_buffer = result.success.body; + + if (strings.indexOf(slice, "")) |start| { + if (strings.indexOf(slice, "")) |end| { + this.upload_id = slice[start + 10 .. end]; + } + } + if (this.upload_id.len == 0) { + // Unknown type of response error from AWS + log("startMultiPartRequestResult {s} failed invalid id", .{this.path}); + this.fail(.{ + .code = "UnknownError", + .message = "Failed to initiate multipart upload", + }); + return; + } + log("startMultiPartRequestResult {s} success id: {s}", .{ this.path, this.upload_id }); + this.state = .multipart_completed; + this.drainEnqueuedParts(); + }, + // this is "unreachable" but we cover in case AWS returns 404 + .not_found => this.fail(.{ + .code = "UnknownError", + .message = "Failed to initiate multipart upload", + }), + } + } + + pub fn onCommitMultiPartRequest(result: S3SimpleRequest.S3CommitResult, this: *@This()) void { + log("onCommitMultiPartRequest {s}", .{this.upload_id}); + + switch (result) { + .failure => |err| { + if (this.options.retry > 0) { + this.options.retry -= 1; + // retry commit + this.commitMultiPartRequest(); + return; + } + this.callback(.{ .failure = err }, this.callback_context); + this.deref(); + }, + .success => { + this.callback(.{ .success = {} }, this.callback_context); + this.state = .finished; + this.deref(); + }, + } + } + + pub fn onRollbackMultiPartRequest(result: S3SimpleRequest.S3UploadResult, this: *@This()) void { + log("onRollbackMultiPartRequest {s}", .{this.upload_id}); + switch (result) { + .failure => { + if (this.options.retry > 0) { + this.options.retry -= 1; + // retry rollback + this.rollbackMultiPartRequest(); + return; + } + this.deref(); + }, + .success => { + this.deref(); + }, + } + } + + fn commitMultiPartRequest(this: *@This()) void { + log("commitMultiPartRequest {s}", .{this.upload_id}); + var params_buffer: [2048]u8 = undefined; + const searchParams = std.fmt.bufPrint(¶ms_buffer, "?uploadId={s}", .{ + this.upload_id, + }) catch unreachable; + + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .POST, + .proxy_url = this.proxyUrl(), + .body = this.multipart_upload_list.slice(), + .search_params = searchParams, + }, .{ .commit = @ptrCast(&onCommitMultiPartRequest) }, this); + } + fn rollbackMultiPartRequest(this: *@This()) void { + log("rollbackMultiPartRequest {s}", .{this.upload_id}); + var params_buffer: [2048]u8 = undefined; + const search_params = std.fmt.bufPrint(¶ms_buffer, "?uploadId={s}", .{ + this.upload_id, + }) catch unreachable; + + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .DELETE, + .proxy_url = this.proxyUrl(), + .body = "", + .search_params = search_params, + }, .{ .upload = @ptrCast(&onRollbackMultiPartRequest) }, this); + } + fn enqueuePart(this: *@This(), chunk: []const u8, owns_data: bool) bool { + const part = this.getCreatePart(chunk, owns_data) orelse return false; + + if (this.state == .not_started) { + // will auto start later + this.state = .multipart_started; + this.ref(); + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .POST, + .proxy_url = this.proxyUrl(), + .body = "", + .search_params = "?uploads=", + .content_type = this.content_type, + .acl = this.acl, + }, .{ .download = @ptrCast(&startMultiPartRequestResult) }, this); + } else if (this.state == .multipart_completed) { + part.start(); + } + return true; + } + + fn processMultiPart(this: *@This(), part_size: usize) void { + // need to split in multiple parts because of the size + var buffer = this.buffered.items[this.offset..]; + var queue_full = false; + defer if (!this.ended and queue_full == false) { + this.buffered = .{}; + this.offset = 0; + }; + + while (buffer.len > 0) { + const len = @min(part_size, buffer.len); + const slice = buffer[0..len]; + buffer = buffer[len..]; + // its one big buffer lets free after we are done with everything, part dont own the data + if (this.enqueuePart(slice, this.ended)) { + this.offset += len; + } else { + queue_full = true; + break; + } + } + } + + pub fn proxyUrl(this: *@This()) ?[]const u8 { + return this.proxy; + } + fn processBuffered(this: *@This(), part_size: usize) void { + if (this.ended and this.buffered.items.len < this.partSizeInBytes() and this.state == .not_started) { + log("processBuffered {s} singlefile_started", .{this.path}); + this.state = .singlefile_started; + this.ref(); + // we can do only 1 request + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .PUT, + .proxy_url = this.proxyUrl(), + .body = this.buffered.items, + .content_type = this.content_type, + .acl = this.acl, + }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); + } else { + // we need to split + this.processMultiPart(part_size); + } + } + + pub fn partSizeInBytes(this: *@This()) usize { + return this.options.partSize; + } + + pub fn continueStream(this: *@This()) void { + if (this.state == .wait_stream_check) { + this.state = .not_started; + if (this.ended) { + this.processBuffered(this.partSizeInBytes()); + } + } + } + + pub fn sendRequestData(this: *@This(), chunk: []const u8, is_last: bool) void { + if (this.ended) return; + if (this.state == .wait_stream_check and chunk.len == 0 and is_last) { + // we do this because stream will close if the file dont exists and we dont wanna to send an empty part in this case + this.ended = true; + return; + } + if (is_last) { + this.ended = true; + if (chunk.len > 0) { + this.buffered.appendSlice(bun.default_allocator, chunk) catch bun.outOfMemory(); + } + this.processBuffered(this.partSizeInBytes()); + } else { + // still have more data and receive empty, nothing todo here + if (chunk.len == 0) return; + this.buffered.appendSlice(bun.default_allocator, chunk) catch bun.outOfMemory(); + const partSize = this.partSizeInBytes(); + if (this.buffered.items.len >= partSize) { + // send the part we have enough data + this.processBuffered(partSize); + return; + } + + // wait for more + } + } +}; diff --git a/src/s3/multipart_options.zig b/src/s3/multipart_options.zig new file mode 100644 index 0000000000..e84aa59b6b --- /dev/null +++ b/src/s3/multipart_options.zig @@ -0,0 +1,22 @@ +pub const MultiPartUploadOptions = struct { + pub const OneMiB: usize = 1048576; + pub const MAX_SINGLE_UPLOAD_SIZE: usize = 5120 * OneMiB; // we limit to 5 GiB + pub const MIN_SINGLE_UPLOAD_SIZE: usize = 5 * OneMiB; + + pub const DefaultPartSize = MIN_SINGLE_UPLOAD_SIZE; + pub const MAX_QUEUE_SIZE = 64; // dont make sense more than this because we use fetch anything greater will be 64 + + /// more than 255 dont make sense http thread cannot handle more than that + queueSize: u8 = 5, + /// in s3 client sdk they set it in bytes but the min is still 5 MiB + /// var params = {Bucket: 'bucket', Key: 'key', Body: stream}; + /// var options = {partSize: 10 * 1024 * 1024, queueSize: 1}; + /// s3.upload(params, options, function(err, data) { + /// console.log(err, data); + /// }); + /// See. https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property + /// The value is in MiB min is 5 and max 5120 (but we limit to 4 GiB aka 4096) + partSize: u64 = DefaultPartSize, + /// default is 3 max 255 + retry: u8 = 3, +}; diff --git a/src/s3/simple_request.zig b/src/s3/simple_request.zig new file mode 100644 index 0000000000..d9a6891b7b --- /dev/null +++ b/src/s3/simple_request.zig @@ -0,0 +1,410 @@ +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const strings = bun.strings; +const SignResult = @import("./credentials.zig").S3Credentials.SignResult; +const S3Error = @import("./error.zig").S3Error; +const getSignErrorCodeAndMessage = @import("./error.zig").getSignErrorCodeAndMessage; +const S3Credentials = @import("./credentials.zig").S3Credentials; +const picohttp = bun.picohttp; +const ACL = @import("./acl.zig").ACL; +pub const S3StatResult = union(enum) { + success: struct { + size: usize = 0, + /// etag is not owned and need to be copied if used after this callback + etag: []const u8 = "", + /// format: Mon, 06 Jan 2025 22:40:57 GMT, lastModified is not owned and need to be copied if used after this callback + lastModified: []const u8 = "", + /// format: text/plain, contentType is not owned and need to be copied if used after this callback + contentType: []const u8 = "", + }, + not_found: S3Error, + + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +pub const S3DownloadResult = union(enum) { + success: struct { + /// etag is not owned and need to be copied if used after this callback + etag: []const u8 = "", + /// body is owned and dont need to be copied, but dont forget to free it + body: bun.MutableString, + }, + not_found: S3Error, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +pub const S3UploadResult = union(enum) { + success: void, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +pub const S3DeleteResult = union(enum) { + success: void, + not_found: S3Error, + + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +// commit result also fails if status 200 but with body containing an Error +pub const S3CommitResult = union(enum) { + success: void, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +// commit result also fails if status 200 but with body containing an Error +pub const S3PartResult = union(enum) { + etag: []const u8, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; + +pub const S3HttpSimpleTask = struct { + http: bun.http.AsyncHTTP, + vm: *JSC.VirtualMachine, + sign_result: SignResult, + headers: JSC.WebCore.Headers, + callback_context: *anyopaque, + callback: Callback, + response_buffer: bun.MutableString = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }, + result: bun.http.HTTPClientResult = .{}, + concurrent_task: JSC.ConcurrentTask = .{}, + range: ?[]const u8, + poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), + + usingnamespace bun.New(@This()); + pub const Callback = union(enum) { + stat: *const fn (S3StatResult, *anyopaque) void, + download: *const fn (S3DownloadResult, *anyopaque) void, + upload: *const fn (S3UploadResult, *anyopaque) void, + delete: *const fn (S3DeleteResult, *anyopaque) void, + commit: *const fn (S3CommitResult, *anyopaque) void, + part: *const fn (S3PartResult, *anyopaque) void, + + pub fn fail(this: @This(), code: []const u8, message: []const u8, context: *anyopaque) void { + switch (this) { + inline .upload, + .download, + .stat, + .delete, + .commit, + .part, + => |callback| callback(.{ + .failure = .{ + .code = code, + .message = message, + }, + }, context), + } + } + pub fn notFound(this: @This(), code: []const u8, message: []const u8, context: *anyopaque) void { + switch (this) { + inline .download, + .stat, + .delete, + => |callback| callback(.{ + .not_found = .{ + .code = code, + .message = message, + }, + }, context), + else => this.fail(code, message, context), + } + } + }; + pub fn deinit(this: *@This()) void { + if (this.result.certificate_info) |*certificate| { + certificate.deinit(bun.default_allocator); + } + this.poll_ref.unref(this.vm); + this.response_buffer.deinit(); + this.headers.deinit(); + this.sign_result.deinit(); + this.http.clearData(); + if (this.range) |range| { + bun.default_allocator.free(range); + } + if (this.result.metadata) |*metadata| { + metadata.deinit(bun.default_allocator); + } + this.destroy(); + } + + const ErrorType = enum { + not_found, + failure, + }; + fn errorWithBody(this: @This(), comptime error_type: ErrorType) void { + var code: []const u8 = "UnknownError"; + var message: []const u8 = "an unexpected error has occurred"; + var has_error_code = false; + if (this.result.fail) |err| { + code = @errorName(err); + has_error_code = true; + } else if (this.result.body) |body| { + const bytes = body.list.items; + if (bytes.len > 0) { + message = bytes[0..]; + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + code = bytes[start + "".len .. end]; + has_error_code = true; + } + } + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + message = bytes[start + "".len .. end]; + } + } + } + } + + if (error_type == .not_found) { + if (!has_error_code) { + code = "NoSuchKey"; + message = "The specified key does not exist."; + } + this.callback.notFound(code, message, this.callback_context); + } else { + this.callback.fail(code, message, this.callback_context); + } + } + + fn failIfContainsError(this: *@This(), status: u32) bool { + var code: []const u8 = "UnknownError"; + var message: []const u8 = "an unexpected error has occurred"; + + if (this.result.fail) |err| { + code = @errorName(err); + } else if (this.result.body) |body| { + const bytes = body.list.items; + var has_error = false; + if (bytes.len > 0) { + message = bytes[0..]; + if (strings.indexOf(bytes, "") != null) { + has_error = true; + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + code = bytes[start + "".len .. end]; + } + } + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + message = bytes[start + "".len .. end]; + } + } + } + } + if (!has_error and status == 200 or status == 206) { + return false; + } + } else if (status == 200 or status == 206) { + return false; + } + this.callback.fail(code, message, this.callback_context); + return true; + } + /// this is the task callback from the last task result and is always in the main thread + pub fn onResponse(this: *@This()) void { + defer this.deinit(); + if (!this.result.isSuccess()) { + this.errorWithBody(.failure); + return; + } + bun.assert(this.result.metadata != null); + const response = this.result.metadata.?.response; + switch (this.callback) { + .stat => |callback| { + switch (response.status_code) { + 200 => { + callback(.{ + .success = .{ + .etag = response.headers.get("etag") orelse "", + .lastModified = response.headers.get("last-modified") orelse "", + .contentType = response.headers.get("content-type") orelse "", + .size = if (response.headers.get("content-length")) |content_len| (std.fmt.parseInt(usize, content_len, 10) catch 0) else 0, + }, + }, this.callback_context); + }, + 404 => { + this.errorWithBody(.not_found); + }, + else => { + this.errorWithBody(.failure); + }, + } + }, + .delete => |callback| { + switch (response.status_code) { + 200, 204 => { + callback(.{ .success = {} }, this.callback_context); + }, + 404 => { + this.errorWithBody(.not_found); + }, + else => { + this.errorWithBody(.failure); + }, + } + }, + .upload => |callback| { + switch (response.status_code) { + 200 => { + callback(.{ .success = {} }, this.callback_context); + }, + else => { + this.errorWithBody(.failure); + }, + } + }, + .download => |callback| { + switch (response.status_code) { + 200, 204, 206 => { + const body = this.response_buffer; + this.response_buffer = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }; + callback(.{ + .success = .{ + .etag = response.headers.get("etag") orelse "", + .body = body, + }, + }, this.callback_context); + }, + 404 => { + this.errorWithBody(.not_found); + }, + else => { + //error + this.errorWithBody(.failure); + }, + } + }, + .commit => |callback| { + // commit multipart upload can fail with status 200 + if (!this.failIfContainsError(response.status_code)) { + callback(.{ .success = {} }, this.callback_context); + } + }, + .part => |callback| { + if (!this.failIfContainsError(response.status_code)) { + if (response.headers.get("etag")) |etag| { + callback(.{ .etag = etag }, this.callback_context); + } else { + this.errorWithBody(.failure); + } + } + }, + } + } + + /// this is the callback from the http.zig AsyncHTTP is always called from the HTTPThread + pub fn httpCallback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) void { + const is_done = !result.has_more; + this.result = result; + this.http = async_http.*; + this.response_buffer = async_http.response_buffer.*; + if (is_done) { + this.vm.eventLoop().enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); + } + } +}; + +pub const S3SimpleRequestOptions = struct { + // signing options + path: []const u8, + method: bun.http.Method, + search_params: ?[]const u8 = null, + content_type: ?[]const u8 = null, + content_disposition: ?[]const u8 = null, + + // http request options + body: []const u8, + proxy_url: ?[]const u8 = null, + range: ?[]const u8 = null, + acl: ?ACL = null, +}; + +pub fn executeSimpleS3Request( + this: *const S3Credentials, + options: S3SimpleRequestOptions, + callback: S3HttpSimpleTask.Callback, + callback_context: *anyopaque, +) void { + var result = this.signRequest(.{ + .path = options.path, + .method = options.method, + .search_params = options.search_params, + .content_disposition = options.content_disposition, + .acl = options.acl, + }, null) catch |sign_err| { + if (options.range) |range_| bun.default_allocator.free(range_); + const error_code_and_message = getSignErrorCodeAndMessage(sign_err); + callback.fail(error_code_and_message.code, error_code_and_message.message, callback_context); + return; + }; + + const headers = brk: { + var header_buffer: [10]picohttp.Header = undefined; + if (options.range) |range_| { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); + } else { + if (options.content_type) |content_type| { + if (content_type.len > 0) { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = content_type }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); + } + } + + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(result.headers(), bun.default_allocator) catch bun.outOfMemory(); + } + }; + const task = S3HttpSimpleTask.new(.{ + .http = undefined, + .sign_result = result, + .callback_context = callback_context, + .callback = callback, + .range = options.range, + .headers = headers, + .vm = JSC.VirtualMachine.get(), + }); + task.poll_ref.ref(task.vm); + + const url = bun.URL.parse(result.url); + const proxy = options.proxy_url orelse ""; + task.http = bun.http.AsyncHTTP.init( + bun.default_allocator, + options.method, + url, + task.headers.entries, + task.headers.buf.items, + &task.response_buffer, + options.body, + bun.http.HTTPClientResult.Callback.New( + *S3HttpSimpleTask, + S3HttpSimpleTask.httpCallback, + ).init(task), + .follow, + .{ + .http_proxy = if (proxy.len > 0) bun.URL.parse(proxy) else null, + .verbose = task.vm.getVerboseFetch(), + .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), + }, + ); + // queue http request + bun.http.HTTPThread.init(&.{}); + var batch = bun.ThreadPool.Batch{}; + task.http.schedule(bun.default_allocator, &batch); + bun.http.http_thread.schedule(batch); +} diff --git a/src/shell/braces.zig b/src/shell/braces.zig index 26ccec354f..3532be44f8 100644 --- a/src/shell/braces.zig +++ b/src/shell/braces.zig @@ -66,7 +66,6 @@ pub fn StackStack(comptime T: type, comptime SizeType: type, comptime N: SizeTyp len: SizeType = 0, pub const Error = error{ - StackEmpty, StackFull, }; @@ -159,7 +158,7 @@ pub fn expand( tokens: []Token, out: []std.ArrayList(u8), contains_nested: bool, -) (error{ StackFull, StackEmpty } || ParserError)!void { +) (error{StackFull} || ParserError)!void { var out_key_counter: u16 = 1; if (!contains_nested) { var expansions_table = try buildExpansionTableAlloc(allocator, tokens); diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index ec729b3da6..8ed314fe94 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -50,7 +50,7 @@ const ShellError = shell.ShellError; const ast = shell.AST; const SmolList = shell.SmolList; -const GlobWalker = Glob.GlobWalker_(null, Glob.SyscallAccessor, true); +const GlobWalker = Glob.BunGlobWalkerZ; const stdin_no = 0; const stdout_no = 1; @@ -721,31 +721,24 @@ pub const ParsedShellScript = struct { } pub fn setEnv(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var env = - if (this.export_env) |*env| - brk: { - env.clearRetainingCapacity(); - break :brk env.*; - } else EnvMap.init(bun.default_allocator); - defer this.export_env = env; - const value1 = callframe.argument(0); if (!value1.isObject()) { return globalThis.throwInvalidArguments("env must be an object", .{}); } - var object_iter = JSC.JSPropertyIterator(.{ + var object_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, value1); defer object_iter.deinit(); + var env: EnvMap = EnvMap.init(bun.default_allocator); env.ensureTotalCapacity(object_iter.len); // If the env object does not include a $PATH, it must disable path lookup for argv[0] // PATH = ""; - while (object_iter.next()) |key| { + while (try object_iter.next()) |key| { const keyslice = key.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); var value = object_iter.value; if (value == .undefined) continue; @@ -759,7 +752,10 @@ pub const ParsedShellScript = struct { env.insert(keyref, valueref); } - + if (this.export_env) |*previous| { + previous.deinit(); + } + this.export_env = env; return .undefined; } @@ -1303,7 +1299,7 @@ pub const Interpreter = struct { var env_loader: *bun.DotEnv.Loader = env_loader: { if (event_loop == .js) { - break :env_loader event_loop.js.virtual_machine.bundler.env; + break :env_loader event_loop.js.virtual_machine.transpiler.env; } break :env_loader event_loop.env(); @@ -1323,18 +1319,20 @@ pub const Interpreter = struct { break :brk export_env; }; - var pathbuf: bun.PathBuffer = undefined; - const cwd: [:0]const u8 = switch (Syscall.getcwdZ(&pathbuf)) { + // Avoid the large stack allocation on Windows. + const pathbuf = bun.default_allocator.create(bun.PathBuffer) catch bun.outOfMemory(); + defer bun.default_allocator.destroy(pathbuf); + const cwd: [:0]const u8 = switch (Syscall.getcwdZ(pathbuf)) { .result => |cwd| cwd, .err => |err| { - return .{ .err = .{ .sys = err.toSystemError() } }; + return .{ .err = .{ .sys = err.toShellSystemError() } }; }, }; const cwd_fd = switch (Syscall.open(cwd, bun.O.DIRECTORY | bun.O.RDONLY, 0)) { .result => |fd| fd, .err => |err| { - return .{ .err = .{ .sys = err.toSystemError() } }; + return .{ .err = .{ .sys = err.toShellSystemError() } }; }, }; @@ -1348,7 +1346,7 @@ pub const Interpreter = struct { log("Duping stdin", .{}); const stdin_fd = switch (if (bun.Output.Source.Stdio.isStdinNull()) bun.sys.openNullDevice() else ShellSyscall.dup(shell.STDIN_FD)) { .result => |fd| fd, - .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + .err => |err| return .{ .err = .{ .sys = err.toShellSystemError() } }, }; const stdin_reader = IOReader.init(stdin_fd, event_loop); @@ -1389,7 +1387,7 @@ pub const Interpreter = struct { }; if (cwd_) |c| { - if (interpreter.root_shell.changeCwdImpl(interpreter, c, true).asErr()) |e| return .{ .err = .{ .sys = e.toSystemError() } }; + if (interpreter.root_shell.changeCwdImpl(interpreter, c, true).asErr()) |e| return .{ .err = .{ .sys = e.toShellSystemError() } }; } return .{ .result = interpreter }; @@ -1463,7 +1461,7 @@ pub const Interpreter = struct { switch (try interp.run()) { .err => |e| { interp.deinitEverything(); - bun.Output.prettyErrorln("error: Failed to run script {s} due to error {}", .{ std.fs.path.basename(path), e.toSystemError() }); + bun.Output.err(e, "Failed to run script {s}", .{std.fs.path.basename(path)}); bun.Global.exit(1); return 1; }, @@ -1475,7 +1473,7 @@ pub const Interpreter = struct { return code; } - pub fn initAndRunFromSource(ctx: bun.CLI.Command.Context, mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8) !ExitCode { + pub fn initAndRunFromSource(ctx: bun.CLI.Command.Context, mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8, cwd: ?[]const u8) !ExitCode { bun.Analytics.Features.standalone_shell += 1; var shargs = ShellArgs.init(); defer shargs.deinit(); @@ -1507,7 +1505,7 @@ pub const Interpreter = struct { shargs, jsobjs, null, - null, + cwd, )) { .err => |*e| { e.throwMini(); @@ -1530,7 +1528,7 @@ pub const Interpreter = struct { switch (try interp.run()) { .err => |e| { interp.deinitEverything(); - bun.Output.prettyErrorln("error: Failed to run script {s} due to error {}", .{ path_for_errors, e.toSystemError() }); + bun.Output.err(e, "Failed to run script {s}", .{path_for_errors}); bun.Global.exit(1); return 1; }, @@ -3405,7 +3403,7 @@ pub const Interpreter = struct { closefd(pipe[0]); closefd(pipe[1]); } - const system_err = err.toSystemError(); + const system_err = err.toShellSystemError(); this.writeFailingError("bun: {s}\n", .{system_err.message}); return .yield; } @@ -3425,7 +3423,7 @@ pub const Interpreter = struct { const subshell_state = switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, cmd_io, .pipeline)) { .result => |s| s, .err => |err| { - const system_err = err.toSystemError(); + const system_err = err.toShellSystemError(); this.writeFailingError("bun: {s}\n", .{system_err.message}); return .yield; }, @@ -3574,31 +3572,16 @@ pub const Interpreter = struct { pipe[0] = bun.FDImpl.fromUV(fds[0]).encode(); pipe[1] = bun.FDImpl.fromUV(fds[1]).encode(); } else { - const fds: [2]bun.FileDescriptor = brk: { - var fds_: [2]std.c.fd_t = undefined; - const rc = std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &fds_); - if (rc != 0) { - return bun.sys.Maybe(void).errno(bun.sys.getErrno(rc), .socketpair); - } - - var before = std.c.fcntl(fds_[0], std.posix.F.GETFL); - - const result = std.c.fcntl(fds_[0], std.posix.F.SETFL, before | bun.O.CLOEXEC); - if (result == -1) { - _ = bun.sys.close(bun.toFD(fds_[0])); - _ = bun.sys.close(bun.toFD(fds_[1])); - return Maybe(void).errno(bun.sys.getErrno(result), .fcntl); - } - - if (comptime bun.Environment.isMac) { - // SO_NOSIGPIPE - before = 1; - _ = std.c.setsockopt(fds_[0], std.posix.SOL.SOCKET, std.posix.SO.NOSIGPIPE, &before, @sizeOf(c_int)); - } - - break :brk .{ bun.toFD(fds_[0]), bun.toFD(fds_[1]) }; + const fds: [2]bun.FileDescriptor = switch (bun.sys.socketpair( + std.posix.AF.UNIX, + std.posix.SOCK.STREAM, + 0, + .blocking, + )) { + .result => |fds| .{ bun.toFD(fds[0]), bun.toFD(fds[1]) }, + .err => |err| return .{ .err = err }, }; - log("socketpair() = {{{}, {}}}", .{ fds[0], fds[1] }); + pipe.* = fds; } set_count.* += 1; @@ -4891,15 +4874,16 @@ pub const Interpreter = struct { switch (this.exec.bltn.start()) { .result => {}, .err => |e| { - this.writeFailingError("bun: {s}: {s}", .{ @tagName(this.exec.bltn.kind), e.toSystemError().message }); + this.writeFailingError("bun: {s}: {s}", .{ @tagName(this.exec.bltn.kind), e.toShellSystemError().message }); return; }, } return; } - var path_buf: bun.PathBuffer = undefined; - const resolved = which(&path_buf, spawn_args.PATH, spawn_args.cwd, first_arg_real) orelse blk: { + const path_buf = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(path_buf); + const resolved = which(path_buf, spawn_args.PATH, spawn_args.cwd, first_arg_real) orelse blk: { if (bun.strings.eqlComptime(first_arg_real, "bun") or bun.strings.eqlComptime(first_arg_real, "bun-debug")) blk2: { break :blk bun.selfExePath() catch break :blk2; } @@ -4984,7 +4968,7 @@ pub const Interpreter = struct { const flags = this.node.redirect.toFlags(); const redirfd = switch (ShellSyscall.openat(this.base.shell.cwd_fd, path, flags, perm)) { .err => |e| { - return this.writeFailingError("bun: {s}: {s}", .{ e.toSystemError().message, path }); + return this.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); }, .result => |f| f, }; @@ -5575,7 +5559,7 @@ pub const Interpreter = struct { const flags = node.redirect.toFlags(); const redirfd = switch (ShellSyscall.openat(cmd.base.shell.cwd_fd, path, flags, perm)) { .err => |e| { - cmd.writeFailingError("bun: {s}: {s}", .{ e.toSystemError().message, path }); + cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); return .yield; }, .result => |f| f, @@ -5816,12 +5800,17 @@ pub const Interpreter = struct { /// Error messages formatted to match bash fn taskErrorToString(this: *Builtin, comptime kind: Kind, err: anytype) []const u8 { switch (@TypeOf(err)) { - Syscall.Error => return switch (err.getErrno()) { - bun.C.E.NOENT => this.fmtErrorArena(kind, "{s}: No such file or directory\n", .{err.path}), - bun.C.E.NAMETOOLONG => this.fmtErrorArena(kind, "{s}: File name too long\n", .{err.path}), - bun.C.E.ISDIR => this.fmtErrorArena(kind, "{s}: is a directory\n", .{err.path}), - bun.C.E.NOTEMPTY => this.fmtErrorArena(kind, "{s}: Directory not empty\n", .{err.path}), - else => this.fmtErrorArena(kind, "{s}\n", .{err.toSystemError().message.byteSlice()}), + Syscall.Error => { + if (err.getErrorCodeTagName()) |entry| { + _, const sys_errno = entry; + if (bun.sys.coreutils_error_map.get(sys_errno)) |message| { + if (err.path.len > 0) { + return this.fmtErrorArena(kind, "{s}: {s}\n", .{ err.path, message }); + } + return this.fmtErrorArena(kind, "{s}\n", .{message}); + } + } + return this.fmtErrorArena(kind, "unknown error {d}\n", .{err.errno}); }, JSC.SystemError => { if (err.path.length() == 0) return this.fmtErrorArena(kind, "{s}\n", .{err.message.byteSlice()}); @@ -6443,7 +6432,7 @@ pub const Interpreter = struct { .mtime = mtime, .path = .{ .string = bun.PathString.init(filepath) }, }; - if (node_fs.utimes(args, .callback).asErr()) |err| out: { + if (node_fs.utimes(args, .sync).asErr()) |err| out: { if (err.getErrno() == bun.C.E.NOENT) { const perm = 0o664; switch (Syscall.open(filepath, bun.O.CREAT | bun.O.WRONLY, perm)) { @@ -6452,12 +6441,12 @@ pub const Interpreter = struct { break :out; }, .err => |e| { - this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); break :out; }, } } - this.err = err.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = err.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); } if (this.event_loop == .js) { @@ -6847,10 +6836,10 @@ pub const Interpreter = struct { var vtable = MkdirVerboseVTable{ .inner = this, .active = this.opts.verbose }; - switch (node_fs.mkdirRecursiveImpl(args, .callback, *MkdirVerboseVTable, &vtable)) { + switch (node_fs.mkdirRecursiveImpl(args, *MkdirVerboseVTable, &vtable)) { .result => {}, .err => |e| { - this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); std.mem.doNotOptimizeAway(&node_fs); }, } @@ -6860,7 +6849,7 @@ pub const Interpreter = struct { .recursive = false, .always_return_none = true, }; - switch (node_fs.mkdirNonRecursive(args, .callback)) { + switch (node_fs.mkdirNonRecursive(args)) { .result => { if (this.opts.verbose) { this.created_directories.appendSlice(filepath[0..filepath.len]) catch bun.outOfMemory(); @@ -6868,7 +6857,7 @@ pub const Interpreter = struct { } }, .err => |e| { - this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); std.mem.doNotOptimizeAway(&node_fs); }, } @@ -7173,12 +7162,13 @@ pub const Interpreter = struct { } if (this.bltn.stdout.needsIO() == null) { - var path_buf: bun.PathBuffer = undefined; + const path_buf = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(path_buf); const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); var had_not_found = false; for (args) |arg_raw| { const arg = arg_raw[0..std.mem.len(arg_raw)]; - const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { + const resolved = which(path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { had_not_found = true; const buf = this.bltn.fmtErrorArena(.which, "{s} not found\n", .{arg}); _ = this.bltn.writeNoIO(.stdout, buf); @@ -7213,10 +7203,11 @@ pub const Interpreter = struct { const arg_raw = multiargs.args_slice[multiargs.arg_idx]; const arg = arg_raw[0..std.mem.len(arg_raw)]; - var path_buf: bun.PathBuffer = undefined; + const path_buf = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(path_buf); const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); - const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { + const resolved = which(path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { multiargs.had_not_found = true; if (this.bltn.stdout.needsIO()) |safeguard| { multiargs.state = .waiting_write; @@ -8503,7 +8494,7 @@ pub const Interpreter = struct { return this.writeFailingError(buf, 1); }, else => { - const sys_err = e.toSystemError(); + const sys_err = e.toShellSystemError(); const buf = this.bltn.fmtErrorArena(.mv, "{s}: {s}\n", .{ sys_err.path.byteSlice(), sys_err.message.byteSlice() }); return this.writeFailingError(buf, 1); }, @@ -8637,7 +8628,7 @@ pub const Interpreter = struct { exec.tasks_done += 1; if (exec.tasks_done >= exec.task_count) { if (exec.err) |err| { - const e = err.toSystemError(); + const e = err.toShellSystemError(); const buf = this.bltn.fmtErrorArena(.mv, "{}: {}\n", .{ e.path, e.message }); _ = this.writeFailingError(buf, err.errno); return; @@ -8779,7 +8770,7 @@ pub const Interpreter = struct { filepath_args: []const [*:0]const u8, total_tasks: usize, err: ?Syscall.Error = null, - lock: std.Thread.Mutex = std.Thread.Mutex{}, + lock: bun.Mutex = bun.Mutex{}, error_signal: std.atomic.Value(bool) = .{ .raw = false }, output_done: std.atomic.Value(usize) = .{ .raw = 0 }, output_count: std.atomic.Value(usize) = .{ .raw = 0 }, @@ -9257,7 +9248,7 @@ pub const Interpreter = struct { root_is_absolute: bool, error_signal: *std.atomic.Value(bool), - err_mutex: bun.Lock = .{}, + err_mutex: bun.Mutex = .{}, err: ?Syscall.Error = null, event_loop: JSC.EventLoopHandle, @@ -10760,7 +10751,7 @@ pub const Interpreter = struct { src_absolute: ?[:0]const u8 = null, tgt_absolute: ?[:0]const u8 = null, cwd_path: [:0]const u8, - verbose_output_lock: std.Thread.Mutex = .{}, + verbose_output_lock: bun.Mutex = .{}, verbose_output: ArrayList(u8) = ArrayList(u8).init(bun.default_allocator), task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool }, @@ -11338,7 +11329,7 @@ pub const Interpreter = struct { pub fn onReaderError(this: *IOReader, err: bun.sys.Error) void { this.setReading(false); - this.err = err.toSystemError(); + this.err = err.toShellSystemError(); for (this.readers.slice()) |r| { r.onReaderDone(if (this.err) |*e| brk: { e.ref(); @@ -11730,7 +11721,7 @@ pub const Interpreter = struct { pub fn onError(this: *This, err__: bun.sys.Error) void { this.setWriting(false); - const ee = err__.toSystemError(); + const ee = err__.toShellSystemError(); this.err = ee; log("IOWriter(0x{x}, fd={}) onError errno={s} errmsg={} errsyscall={}", .{ @intFromPtr(this), this.fd, @tagName(ee.getErrno()), ee.message, ee.syscall }); var seen_alloc = std.heap.stackFallback(@sizeOf(usize) * 64, bun.default_allocator); diff --git a/src/shell/shell.zig b/src/shell/shell.zig index a475cdd763..6133dc62c1 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -55,7 +55,7 @@ pub const ShellErr = union(enum) { pub fn newSys(e: anytype) @This() { return .{ .sys = switch (@TypeOf(e)) { - Syscall.Error => e.toSystemError(), + Syscall.Error => e.toShellSystemError(), JSC.SystemError => e, else => @compileError("Invalid `e`: " ++ @typeName(e)), }, @@ -227,7 +227,7 @@ pub const GlobalJS = struct { } pub inline fn createNullDelimitedEnvMap(this: @This(), alloc: Allocator) ![:null]?[*:0]u8 { - return this.globalThis.bunVM().bundler.env.map.createNullDelimitedEnvMap(alloc); + return this.globalThis.bunVM().transpiler.env.map.createNullDelimitedEnvMap(alloc); } pub inline fn getAllocator(this: @This()) Allocator { @@ -239,11 +239,11 @@ pub const GlobalJS = struct { } pub inline fn topLevelDir(this: @This()) []const u8 { - return this.globalThis.bunVM().bundler.fs.top_level_dir; + return this.globalThis.bunVM().transpiler.fs.top_level_dir; } pub inline fn env(this: @This()) *bun.DotEnv.Loader { - return this.globalThis.bunVM().bundler.env; + return this.globalThis.bunVM().transpiler.env; } pub inline fn platformEventLoop(this: @This()) *JSC.PlatformEventLoop { @@ -3566,7 +3566,7 @@ fn isValidVarNameAscii(var_name: []const u8) bool { return true; } -var stderr_mutex = std.Thread.Mutex{}; +var stderr_mutex = bun.Mutex{}; pub fn hasEqSign(str: []const u8) ?u32 { if (isAllAscii(str)) { @@ -3987,8 +3987,8 @@ pub const ShellSrcBuilder = struct { /// Characters that need to escaped const SPECIAL_CHARS = [_]u8{ '~', '[', ']', '#', ';', '\n', '*', '{', ',', '}', '`', '$', '=', '(', ')', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '|', '>', '<', '&', '\'', '"', ' ', '\\' }; -const SPECIAL_CHARS_TABLE: std.bit_set.IntegerBitSet(256) = brk: { - var table = std.bit_set.IntegerBitSet(256).initEmpty(); +const SPECIAL_CHARS_TABLE: bun.bit_set.IntegerBitSet(256) = brk: { + var table = bun.bit_set.IntegerBitSet(256).initEmpty(); for (SPECIAL_CHARS) |c| { table.set(c); } diff --git a/src/shell/subproc.zig b/src/shell/subproc.zig index 1eed3bdf53..940e117562 100644 --- a/src/shell/subproc.zig +++ b/src/shell/subproc.zig @@ -704,7 +704,7 @@ pub const ShellSubprocess = struct { } pub fn fillEnvFromProcess(this: *SpawnArgs, globalThis: *JSGlobalObject) void { - var env_iter = EnvMapIter.init(globalThis.bunVM().bundler.env.map, this.arena.allocator()); + var env_iter = EnvMapIter.init(globalThis.bunVM().transpiler.env.map, this.arena.allocator()); return this.fillEnv(globalThis, &env_iter, false); } @@ -782,7 +782,7 @@ pub const ShellSubprocess = struct { const is_sync = config.is_sync; if (!spawn_args.override_env and spawn_args.env_array.items.len == 0) { - // spawn_args.env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory(); + // spawn_args.env_array.items = jsc_vm.transpiler.env.map.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory(); spawn_args.env_array.items = event_loop.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory(); spawn_args.env_array.capacity = spawn_args.env_array.items.len; } @@ -830,7 +830,7 @@ pub const ShellSubprocess = struct { .windows = if (Environment.isWindows) bun.spawn.WindowsSpawnOptions.WindowsOptions{ .hide_window = true, .loop = event_loop, - } else {}, + }, }; spawn_args.argv.append(allocator, null) catch { @@ -848,7 +848,7 @@ pub const ShellSubprocess = struct { ) catch |err| { return .{ .err = .{ .custom = std.fmt.allocPrint(bun.default_allocator, "Failed to spawn process: {s}", .{@errorName(err)}) catch bun.outOfMemory() } }; }) { - .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + .err => |err| return .{ .err = .{ .sys = err.toShellSystemError() } }, .result => |result| result, }; diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 026706100e..c0f2bbef84 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -13,6 +13,37 @@ pub const PostgresShort = u16; const Crypto = JSC.API.Bun.Crypto; const JSValue = JSC.JSValue; const BoringSSL = @import("../boringssl.zig"); +pub const AnyPostgresError = error{ + ConnectionClosed, + ExpectedRequest, + ExpectedStatement, + InvalidBackendKeyData, + InvalidBinaryData, + InvalidByteSequence, + InvalidByteSequenceForEncoding, + InvalidCharacter, + InvalidMessage, + InvalidMessageLength, + InvalidQueryBinding, + InvalidServerKey, + InvalidServerSignature, + JSError, + MultidimensionalArrayNotSupportedYet, + NullsInArrayNotSupportedYet, + OutOfMemory, + Overflow, + PBKDFD2, + SASL_SIGNATURE_MISMATCH, + SASL_SIGNATURE_INVALID_BASE64, + ShortRead, + TLSNotAvailable, + TLSUpgradeFailed, + UnexpectedMessage, + UNKNOWN_AUTHENTICATION_METHOD, + UNSUPPORTED_AUTHENTICATION_METHOD, + UnsupportedByteaFormat, + UnsupportedIntegerSize, +}; pub const SSLMode = enum(u8) { disable = 0, @@ -176,16 +207,23 @@ pub const PostgresSQLQuery = struct { statement: ?*PostgresSQLStatement = null, query: bun.String = bun.String.empty, cursor_name: bun.String = bun.String.empty, + + // Kept alive by being in the "queries" array from JS. thisValue: JSValue = .undefined, - target: JSC.Strong = JSC.Strong.init(), + status: Status = Status.pending, is_done: bool = false, ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), binary: bool = false, - pending_value: JSC.Strong = .{}, pub usingnamespace JSC.Codegen.JSPostgresSQLQuery; + pub fn getTarget(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + const target = PostgresSQLQuery.targetGetCached(this.thisValue) orelse return .zero; + PostgresSQLQuery.targetSetCached(this.thisValue, globalObject, .zero); + return target; + } + pub const Status = enum(u8) { pending, written, @@ -209,9 +247,6 @@ pub const PostgresSQLQuery = struct { } this.query.deref(); this.cursor_name.deref(); - this.target.deinit(); - this.pending_value.deinit(); - bun.default_allocator.destroy(this); } @@ -233,12 +268,12 @@ pub const PostgresSQLQuery = struct { bun.assert(this.ref_count.fetchAdd(1, .monotonic) > 0); } - pub fn onNoData(this: *@This(), globalObject: *JSC.JSGlobalObject) void { + pub fn onNoData(this: *@This(), globalObject: *JSC.JSGlobalObject, queries_array: JSValue) void { this.status = .success; defer this.deref(); const thisValue = this.thisValue; - const targetValue = this.target.trySwap() orelse JSValue.zero; + const targetValue = this.getTarget(globalObject); if (thisValue == .zero or targetValue == .zero) { return; } @@ -251,13 +286,18 @@ pub const PostgresSQLQuery = struct { this.pending_value.trySwap() orelse .undefined, JSValue.jsNumber(0), JSValue.jsNumber(0), + queries_array, }); } - pub fn onWriteFail(this: *@This(), err: anyerror, globalObject: *JSC.JSGlobalObject) void { + pub fn onWriteFail( + this: *@This(), + err: AnyPostgresError, + globalObject: *JSC.JSGlobalObject, + queries_array: JSValue, + ) void { this.status = .fail; - this.pending_value.deinit(); const thisValue = this.thisValue; - const targetValue = this.target.trySwap() orelse JSValue.zero; + const targetValue = this.getTarget(globalObject); if (thisValue == .zero or targetValue == .zero) { return; } @@ -271,6 +311,7 @@ pub const PostgresSQLQuery = struct { event_loop.runCallback(function, globalObject, thisValue, &.{ targetValue, instance, + queries_array, }); } @@ -279,7 +320,7 @@ pub const PostgresSQLQuery = struct { defer this.deref(); const thisValue = this.thisValue; - const targetValue = this.target.trySwap() orelse JSValue.zero; + const targetValue = this.getTarget(globalObject); if (thisValue == .zero or targetValue == .zero) { return; } @@ -388,14 +429,31 @@ pub const PostgresSQLQuery = struct { } }; - pub fn onSuccess(this: *@This(), command_tag_str: []const u8, globalObject: *JSC.JSGlobalObject) void { + pub fn allowGC(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) void { + if (thisValue == .zero) { + return; + } + + defer thisValue.ensureStillAlive(); + PostgresSQLQuery.bindingSetCached(thisValue, globalObject, .zero); + PostgresSQLQuery.pendingValueSetCached(thisValue, globalObject, .zero); + PostgresSQLQuery.targetSetCached(thisValue, globalObject, .zero); + } + + fn consumePendingValue(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) ?JSValue { + const pending_value = PostgresSQLQuery.pendingValueGetCached(thisValue) orelse return null; + PostgresSQLQuery.pendingValueSetCached(thisValue, globalObject, .zero); + return pending_value; + } + + pub fn onSuccess(this: *@This(), command_tag_str: []const u8, globalObject: *JSC.JSGlobalObject, connection: JSC.JSValue) void { this.status = .success; defer this.deref(); const thisValue = this.thisValue; - const targetValue = this.target.trySwap() orelse JSValue.zero; + const targetValue = this.getTarget(globalObject); + defer allowGC(thisValue, globalObject); if (thisValue == .zero or targetValue == .zero) { - this.pending_value.deinit(); return; } @@ -407,9 +465,10 @@ pub const PostgresSQLQuery = struct { event_loop.runCallback(function, globalObject, thisValue, &.{ targetValue, - this.pending_value.trySwap() orelse .undefined, + consumePendingValue(thisValue, globalObject) orelse .undefined, tag.toJSTag(globalObject), tag.toJSNumber(), + PostgresSQLConnection.queriesGetCached(connection) orelse .undefined, }); } @@ -458,7 +517,6 @@ pub const PostgresSQLQuery = struct { if (columns != .undefined) { PostgresSQLQuery.columnsSetCached(this_value, globalThis, columns); } - ptr.pending_value.set(globalThis, pending_value); return this_value; } @@ -477,7 +535,7 @@ pub const PostgresSQLQuery = struct { pub fn doRun(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { var arguments_ = callframe.arguments_old(2); const arguments = arguments_.slice(); - var connection = arguments[0].as(PostgresSQLConnection) orelse { + const connection: *PostgresSQLConnection = arguments[0].as(PostgresSQLConnection) orelse { return globalObject.throw("connection must be a PostgresSQLConnection", .{}); }; var query = arguments[1]; @@ -486,11 +544,11 @@ pub const PostgresSQLQuery = struct { return globalObject.throwInvalidArgumentType("run", "query", "Query"); } - this.target.set(globalObject, query); - const binding_value = PostgresSQLQuery.bindingGetCached(callframe.this()) orelse .zero; + const this_value = callframe.this(); + const binding_value = PostgresSQLQuery.bindingGetCached(this_value) orelse .zero; var query_str = this.query.toUTF8(bun.default_allocator); defer query_str.deinit(); - const columns_value = PostgresSQLQuery.columnsGetCached(callframe.this()) orelse .undefined; + const columns_value = PostgresSQLQuery.columnsGetCached(this_value) orelse .undefined; var signature = Signature.generate(globalObject, query_str.slice(), binding_value, columns_value) catch |err| { if (!globalObject.hasException()) @@ -568,9 +626,12 @@ pub const PostgresSQLQuery = struct { connection.requests.writeItem(this) catch {}; this.ref(); this.status = if (did_write) .binding else .pending; + PostgresSQLQuery.targetSetCached(this_value, globalObject, query); if (connection.is_ready_for_query) - connection.flushData(); + connection.flushDataAndResetTimeout() + else if (did_write) + connection.resetConnectionTimeout(); return .undefined; } @@ -665,8 +726,10 @@ pub const PostgresRequest = struct { try writer.int4(@bitCast(@as(i32, -1))); continue; } + if (comptime bun.Environment.enable_logs) { + debug(" -> {s}", .{tag.name() orelse "(unknown)"}); + } - debug(" -> {s}", .{@tagName(tag)}); switch ( // If they pass a value as a string, let's avoid attempting to // convert it to the binary representation. This minimizes the room @@ -674,7 +737,7 @@ pub const PostgresRequest = struct { // differently than what Postgres does when given a timestamp with // timezone. if (tag.isBinaryFormatSupported() and value.isString()) .text else tag) { - .json => { + .jsonb, .json => { var str = bun.String.empty; defer str.deref(); value.jsonStringify(globalObject, 0, &str); @@ -761,7 +824,7 @@ pub const PostgresRequest = struct { params: []const int4, comptime Context: type, writer: protocol.NewWriter(Context), - ) !void { + ) AnyPostgresError!void { { var q = protocol.Parse{ .name = name, @@ -790,7 +853,7 @@ pub const PostgresRequest = struct { comptime Context: type, writer: protocol.NewWriter(Context), signature: *Signature, - ) !void { + ) AnyPostgresError!void { try writeQuery(query, signature.name, signature.fields, Context, writer); try writeBind(signature.name, bun.String.empty, globalObject, array_value, .zero, &.{}, &.{}, Context, writer); var exec = protocol.Execute{ @@ -863,7 +926,7 @@ pub const PostgresRequest = struct { connection.tls_status = .ssl_not_available; debug("Server does not support SSL", .{}); if (connection.ssl_mode == .require) { - connection.fail("Server does not support SSL", error.SSLNotAvailable); + connection.fail("Server does not support SSL", error.TLSNotAvailable); return; } continue; @@ -912,9 +975,6 @@ pub const PostgresSQLConnection = struct { pending_disconnect: bool = false, - on_connect: JSC.Strong = .{}, - on_close: JSC.Strong = .{}, - database: []const u8 = "", user: []const u8 = "", password: []const u8 = "", @@ -928,6 +988,31 @@ pub const PostgresSQLConnection = struct { tls_status: TLSStatus = .none, ssl_mode: SSLMode = .disable, + idle_timeout_interval_ms: u32 = 0, + connection_timeout_ms: u32 = 0, + + /// Before being connected, this is a connection timeout timer. + /// After being connected, this is an idle timeout timer. + timer: JSC.BunTimer.EventLoopTimer = .{ + .tag = .PostgresSQLConnectionTimeout, + .next = .{ + .sec = 0, + .nsec = 0, + }, + }, + + /// This timer controls the maximum lifetime of a connection. + /// It starts when the connection successfully starts (i.e. after handshake is complete). + /// It stops when the connection is closed. + max_lifetime_interval_ms: u32 = 0, + max_lifetime_timer: JSC.BunTimer.EventLoopTimer = .{ + .tag = .PostgresSQLConnectionMaxLifetime, + .next = .{ + .sec = 0, + .nsec = 0, + }, + }, + pub const TLSStatus = union(enum) { none, pending, @@ -942,100 +1027,107 @@ pub const PostgresSQLConnection = struct { pub const AuthenticationState = union(enum) { pending: void, - SASL: SASL, + none: void, ok: void, + SASL: SASL, + md5: void, pub fn zero(this: *AuthenticationState) void { - const bytes = std.mem.asBytes(this); - @memset(bytes, 0); + switch (this.*) { + .SASL => |*sasl| { + sasl.deinit(); + }, + else => {}, + } + this.* = .{ .none = {} }; + } + }; + + pub const SASL = struct { + const nonce_byte_len = 18; + const nonce_base64_len = bun.base64.encodeLenFromSize(nonce_byte_len); + + const server_signature_byte_len = 32; + const server_signature_base64_len = bun.base64.encodeLenFromSize(server_signature_byte_len); + + const salted_password_byte_len = 32; + + nonce_base64_bytes: [nonce_base64_len]u8 = .{0} ** nonce_base64_len, + nonce_len: u8 = 0, + + server_signature_base64_bytes: [server_signature_base64_len]u8 = .{0} ** server_signature_base64_len, + server_signature_len: u8 = 0, + + salted_password_bytes: [salted_password_byte_len]u8 = .{0} ** salted_password_byte_len, + salted_password_created: bool = false, + + status: SASLStatus = .init, + + pub const SASLStatus = enum { + init, + @"continue", + }; + + fn hmac(password: []const u8, data: []const u8) ?[32]u8 { + var buf = std.mem.zeroes([bun.BoringSSL.EVP_MAX_MD_SIZE]u8); + + // TODO: I don't think this is failable. + const result = bun.hmac.generate(password, data, .sha256, &buf) orelse return null; + + assert(result.len == 32); + return buf[0..32].*; } - pub const SASL = struct { - const nonce_byte_len = 18; - const nonce_base64_len = bun.base64.encodeLenFromSize(nonce_byte_len); - - const server_signature_byte_len = 32; - const server_signature_base64_len = bun.base64.encodeLenFromSize(server_signature_byte_len); - - const salted_password_byte_len = 32; - - nonce_base64_bytes: [nonce_base64_len]u8 = .{0} ** nonce_base64_len, - nonce_len: u8 = 0, - - server_signature_base64_bytes: [server_signature_base64_len]u8 = .{0} ** server_signature_base64_len, - server_signature_len: u8 = 0, - - salted_password_bytes: [salted_password_byte_len]u8 = .{0} ** salted_password_byte_len, - salted_password_created: bool = false, - - status: SASLStatus = .init, - - pub const SASLStatus = enum { - init, - @"continue", - }; - - fn hmac(password: []const u8, data: []const u8) ?[32]u8 { - var buf = std.mem.zeroes([bun.BoringSSL.EVP_MAX_MD_SIZE]u8); - - // TODO: I don't think this is failable. - const result = bun.hmac.generate(password, data, .sha256, &buf) orelse return null; - - assert(result.len == 32); - return buf[0..32].*; + pub fn computeSaltedPassword(this: *SASL, salt_bytes: []const u8, iteration_count: u32, connection: *PostgresSQLConnection) !void { + this.salted_password_created = true; + if (Crypto.EVP.pbkdf2(&this.salted_password_bytes, connection.password, salt_bytes, iteration_count, .sha256) == null) { + return error.PBKDFD2; } + } - pub fn computeSaltedPassword(this: *SASL, salt_bytes: []const u8, iteration_count: u32, connection: *PostgresSQLConnection) !void { - this.salted_password_created = true; - if (Crypto.EVP.pbkdf2(&this.salted_password_bytes, connection.password, salt_bytes, iteration_count, .sha256) == null) { - return error.PBKDF2Failed; - } + pub fn saltedPassword(this: *const SASL) []const u8 { + assert(this.salted_password_created); + return this.salted_password_bytes[0..salted_password_byte_len]; + } + + pub fn serverSignature(this: *const SASL) []const u8 { + assert(this.server_signature_len > 0); + return this.server_signature_base64_bytes[0..this.server_signature_len]; + } + + pub fn computeServerSignature(this: *SASL, auth_string: []const u8) !void { + assert(this.server_signature_len == 0); + + const server_key = hmac(this.saltedPassword(), "Server Key") orelse return error.InvalidServerKey; + const server_signature_bytes = hmac(&server_key, auth_string) orelse return error.InvalidServerSignature; + this.server_signature_len = @intCast(bun.base64.encode(&this.server_signature_base64_bytes, &server_signature_bytes)); + } + + pub fn clientKey(this: *const SASL) [32]u8 { + return hmac(this.saltedPassword(), "Client Key").?; + } + + pub fn clientKeySignature(_: *const SASL, client_key: []const u8, auth_string: []const u8) [32]u8 { + var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); + bun.sha.SHA256.hash(client_key, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); + return hmac(&sha_digest, auth_string).?; + } + + pub fn nonce(this: *SASL) []const u8 { + if (this.nonce_len == 0) { + var bytes: [nonce_byte_len]u8 = .{0} ** nonce_byte_len; + bun.rand(&bytes); + this.nonce_len = @intCast(bun.base64.encode(&this.nonce_base64_bytes, &bytes)); } + return this.nonce_base64_bytes[0..this.nonce_len]; + } - pub fn saltedPassword(this: *const SASL) []const u8 { - assert(this.salted_password_created); - return this.salted_password_bytes[0..salted_password_byte_len]; - } - - pub fn serverSignature(this: *const SASL) []const u8 { - assert(this.server_signature_len > 0); - return this.server_signature_base64_bytes[0..this.server_signature_len]; - } - - pub fn computeServerSignature(this: *SASL, auth_string: []const u8) !void { - assert(this.server_signature_len == 0); - - const server_key = hmac(this.saltedPassword(), "Server Key") orelse return error.InvalidServerKey; - const server_signature_bytes = hmac(&server_key, auth_string) orelse return error.InvalidServerSignature; - this.server_signature_len = @intCast(bun.base64.encode(&this.server_signature_base64_bytes, &server_signature_bytes)); - } - - pub fn clientKey(this: *const SASL) [32]u8 { - return hmac(this.saltedPassword(), "Client Key").?; - } - - pub fn clientKeySignature(_: *const SASL, client_key: []const u8, auth_string: []const u8) [32]u8 { - var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); - bun.sha.SHA256.hash(client_key, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); - return hmac(&sha_digest, auth_string).?; - } - - pub fn nonce(this: *SASL) []const u8 { - if (this.nonce_len == 0) { - var bytes: [nonce_byte_len]u8 = .{0} ** nonce_byte_len; - bun.rand(&bytes); - this.nonce_len = @intCast(bun.base64.encode(&this.nonce_base64_bytes, &bytes)); - } - return this.nonce_base64_bytes[0..this.nonce_len]; - } - - pub fn deinit(this: *SASL) void { - this.nonce_len = 0; - this.salted_password_created = false; - this.server_signature_len = 0; - this.status = .init; - } - }; + pub fn deinit(this: *SASL) void { + this.nonce_len = 0; + this.salted_password_created = false; + this.server_signature_len = 0; + this.status = .init; + } }; pub const Status = enum { @@ -1050,6 +1142,64 @@ pub const PostgresSQLConnection = struct { pub usingnamespace JSC.Codegen.JSPostgresSQLConnection; + fn getTimeoutInterval(this: *const PostgresSQLConnection) u32 { + return switch (this.status) { + .connected => this.idle_timeout_interval_ms, + .failed => 0, + else => this.connection_timeout_ms, + }; + } + + pub fn resetConnectionTimeout(this: *PostgresSQLConnection) void { + const interval = this.getTimeoutInterval(); + if (this.timer.state == .ACTIVE) { + this.globalObject.bunVM().timer.remove(&this.timer); + } + if (interval == 0) { + return; + } + + this.timer.next = bun.timespec.msFromNow(@intCast(interval)); + this.globalObject.bunVM().timer.insert(&this.timer); + } + + pub fn getQueries(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + if (PostgresSQLConnection.queriesGetCached(thisValue)) |value| { + return value; + } + + const array = JSC.JSValue.createEmptyArray(globalObject, 0); + PostgresSQLConnection.queriesSetCached(thisValue, globalObject, array); + + return array; + } + + pub fn getOnConnect(_: *PostgresSQLConnection, thisValue: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { + if (PostgresSQLConnection.onconnectGetCached(thisValue)) |value| { + return value; + } + + return .undefined; + } + + pub fn setOnConnect(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) bool { + PostgresSQLConnection.onconnectSetCached(thisValue, globalObject, value); + return true; + } + + pub fn getOnClose(_: *PostgresSQLConnection, thisValue: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { + if (PostgresSQLConnection.oncloseGetCached(thisValue)) |value| { + return value; + } + + return .undefined; + } + + pub fn setOnClose(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) bool { + PostgresSQLConnection.oncloseSetCached(thisValue, globalObject, value); + return true; + } + pub fn setupTLS(this: *PostgresSQLConnection) void { debug("setupTLS", .{}); const new_socket = uws.us_socket_upgrade_to_tls(this.socket.SocketTCP.socket.connected, this.tls_ctx.?, this.tls_config.server_name) orelse { @@ -1066,8 +1216,47 @@ pub const PostgresSQLConnection = struct { this.start(); } + fn setupMaxLifetimeTimerIfNecessary(this: *PostgresSQLConnection) void { + if (this.max_lifetime_interval_ms == 0) return; + if (this.max_lifetime_timer.state == .ACTIVE) return; + + this.max_lifetime_timer.next = bun.timespec.msFromNow(@intCast(this.max_lifetime_interval_ms)); + this.globalObject.bunVM().timer.insert(&this.max_lifetime_timer); + } + + pub fn onConnectionTimeout(this: *PostgresSQLConnection) JSC.BunTimer.EventLoopTimer.Arm { + debug("onConnectionTimeout", .{}); + this.timer.state = .FIRED; + if (this.getTimeoutInterval() == 0) { + this.resetConnectionTimeout(); + return .disarm; + } + + switch (this.status) { + .connected => { + this.failFmt(.ERR_POSTGRES_IDLE_TIMEOUT, "Idle timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.idle_timeout_interval_ms) *| std.time.ns_per_ms)}); + }, + else => { + this.failFmt(.ERR_POSTGRES_CONNECTION_TIMEOUT, "Connection timeout after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)}); + }, + .sent_startup_message => { + this.failFmt(.ERR_POSTGRES_CONNECTION_TIMEOUT, "Connection timed out after {} (sent startup message, but never received response)", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)}); + }, + } + return .disarm; + } + + pub fn onMaxLifetimeTimeout(this: *PostgresSQLConnection) JSC.BunTimer.EventLoopTimer.Arm { + debug("onMaxLifetimeTimeout", .{}); + this.max_lifetime_timer.state = .FIRED; + if (this.status == .failed) return .disarm; + this.failFmt(.ERR_POSTGRES_LIFETIME_TIMEOUT, "Max lifetime timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.max_lifetime_interval_ms) *| std.time.ns_per_ms)}); + return .disarm; + } fn start(this: *PostgresSQLConnection) void { + this.setupMaxLifetimeTimerIfNecessary(); + this.resetConnectionTimeout(); this.sendStartupMessage(); const event_loop = this.globalObject.bunVM().eventLoop(); @@ -1094,10 +1283,11 @@ pub const PostgresSQLConnection = struct { if (this.status == status) return; this.status = status; + this.resetConnectionTimeout(); + switch (status) { .connected => { - const on_connect = this.on_connect.swap(); - if (on_connect == .zero) return; + const on_connect = this.consumeOnConnectCallback(this.globalObject) orelse return; const js_value = this.js_value; js_value.ensureStillAlive(); this.globalObject.queueMicrotask(on_connect, &[_]JSValue{ JSValue.jsNull(), js_value }); @@ -1110,10 +1300,16 @@ pub const PostgresSQLConnection = struct { pub fn finalize(this: *PostgresSQLConnection) void { debug("PostgresSQLConnection finalize", .{}); + this.stopTimers(); this.js_value = .zero; this.deref(); } + pub fn flushDataAndResetTimeout(this: *PostgresSQLConnection) void { + this.resetConnectionTimeout(); + this.flushData(); + } + pub fn flushData(this: *PostgresSQLConnection) void { const chunk = this.write_buffer.remaining(); if (chunk.len == 0) return; @@ -1126,32 +1322,85 @@ pub const PostgresSQLConnection = struct { pub fn failWithJSValue(this: *PostgresSQLConnection, value: JSValue) void { defer this.updateHasPendingActivity(); + this.stopTimers(); if (this.status == .failed) return; this.status = .failed; - if (!this.socket.isClosed()) this.socket.close(); - const on_close = this.on_close.swap(); - if (on_close == .zero) return; + this.ref(); + defer this.deref(); + if (!this.socket.isClosed()) this.socket.close(); + const on_close = this.consumeOnCloseCallback(this.globalObject) orelse return; + + const loop = this.globalObject.bunVM().eventLoop(); + loop.enter(); + defer loop.exit(); _ = on_close.call( this.globalObject, this.js_value, &[_]JSValue{ value, + this.getQueriesArray(), }, ) catch |e| this.globalObject.reportActiveExceptionAsUnhandled(e); } - pub fn fail(this: *PostgresSQLConnection, message: []const u8, err: anyerror) void { + pub fn failFmt(this: *PostgresSQLConnection, comptime error_code: JSC.Error, comptime fmt: [:0]const u8, args: anytype) void { + this.failWithJSValue(error_code.fmt(this.globalObject, fmt, args)); + } + + pub fn fail(this: *PostgresSQLConnection, message: []const u8, err: AnyPostgresError) void { debug("failed: {s}: {s}", .{ message, @errorName(err) }); - const instance = this.globalObject.createErrorInstance("{s}", .{message}); - instance.put(this.globalObject, JSC.ZigString.static("code"), String.init(@errorName(err)).toJS(this.globalObject)); - this.failWithJSValue(instance); + + const globalObject = this.globalObject; + const error_code: JSC.Error = switch (err) { + error.ConnectionClosed => JSC.Error.ERR_POSTGRES_CONNECTION_CLOSED, + error.ExpectedRequest => JSC.Error.ERR_POSTGRES_EXPECTED_REQUEST, + error.ExpectedStatement => JSC.Error.ERR_POSTGRES_EXPECTED_STATEMENT, + error.InvalidBackendKeyData => JSC.Error.ERR_POSTGRES_INVALID_BACKEND_KEY_DATA, + error.InvalidBinaryData => JSC.Error.ERR_POSTGRES_INVALID_BINARY_DATA, + error.InvalidByteSequence => JSC.Error.ERR_POSTGRES_INVALID_BYTE_SEQUENCE, + error.InvalidByteSequenceForEncoding => JSC.Error.ERR_POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING, + error.InvalidCharacter => JSC.Error.ERR_POSTGRES_INVALID_CHARACTER, + error.InvalidMessage => JSC.Error.ERR_POSTGRES_INVALID_MESSAGE, + error.InvalidMessageLength => JSC.Error.ERR_POSTGRES_INVALID_MESSAGE_LENGTH, + error.InvalidQueryBinding => JSC.Error.ERR_POSTGRES_INVALID_QUERY_BINDING, + error.InvalidServerKey => JSC.Error.ERR_POSTGRES_INVALID_SERVER_KEY, + error.InvalidServerSignature => JSC.Error.ERR_POSTGRES_INVALID_SERVER_SIGNATURE, + error.MultidimensionalArrayNotSupportedYet => JSC.Error.ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET, + error.NullsInArrayNotSupportedYet => JSC.Error.ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET, + error.Overflow => JSC.Error.ERR_POSTGRES_OVERFLOW, + error.PBKDFD2 => JSC.Error.ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2, + error.SASL_SIGNATURE_MISMATCH => JSC.Error.ERR_POSTGRES_SASL_SIGNATURE_MISMATCH, + error.SASL_SIGNATURE_INVALID_BASE64 => JSC.Error.ERR_POSTGRES_SASL_SIGNATURE_INVALID_BASE64, + error.TLSNotAvailable => JSC.Error.ERR_POSTGRES_TLS_NOT_AVAILABLE, + error.TLSUpgradeFailed => JSC.Error.ERR_POSTGRES_TLS_UPGRADE_FAILED, + error.UnexpectedMessage => JSC.Error.ERR_POSTGRES_UNEXPECTED_MESSAGE, + error.UNKNOWN_AUTHENTICATION_METHOD => JSC.Error.ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD, + error.UNSUPPORTED_AUTHENTICATION_METHOD => JSC.Error.ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD, + error.UnsupportedByteaFormat => JSC.Error.ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT, + error.UnsupportedIntegerSize => JSC.Error.ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE, + error.JSError => { + this.failWithJSValue(globalObject.takeException(error.JSError)); + return; + }, + error.OutOfMemory => { + // TODO: add binding for creating an out of memory error? + this.failWithJSValue(globalObject.takeException(globalObject.throwOutOfMemory())); + return; + }, + error.ShortRead => { + bun.unreachablePanic("Assertion failed: ShortRead should be handled by the caller in postgres", .{}); + }, + }; + this.failWithJSValue(error_code.fmt(globalObject, "{s}", .{message})); } pub fn onClose(this: *PostgresSQLConnection) void { var vm = this.globalObject.bunVM(); - defer vm.drainMicrotasks(); + const loop = vm.eventLoop(); + loop.enter(); + defer loop.exit(); this.fail("Connection closed", error.ConnectionClosed); } @@ -1210,22 +1459,35 @@ pub const PostgresSQLConnection = struct { pub fn onHandshake(this: *PostgresSQLConnection, success: i32, ssl_error: uws.us_bun_verify_error_t) void { debug("onHandshake: {d} {d}", .{ success, ssl_error.error_no }); + if (this.tls_config.reject_unauthorized == 0) { + return; + } + + const do_tls_verification = switch (this.ssl_mode) { + // https://github.com/porsager/postgres/blob/6ec85a432b17661ccacbdf7f765c651e88969d36/src/connection.js#L272-L279 + .verify_ca, .verify_full => true, + else => false, + }; + + if (!do_tls_verification) { + return; + } + if (success != 1) { this.failWithJSValue(ssl_error.toJS(this.globalObject)); return; } - if (this.tls_config.reject_unauthorized == 1) { - if (ssl_error.error_no != 0) { + if (ssl_error.error_no != 0) { + this.failWithJSValue(ssl_error.toJS(this.globalObject)); + return; + } + + const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(this.socket.getNativeHandle())); + if (BoringSSL.SSL_get_servername(ssl_ptr, 0)) |servername| { + const hostname = servername[0..bun.len(servername)]; + if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) { this.failWithJSValue(ssl_error.toJS(this.globalObject)); - return; - } - const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(this.socket.getNativeHandle())); - if (BoringSSL.SSL_get_servername(ssl_ptr, 0)) |servername| { - const hostname = servername[0..bun.len(servername)]; - if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) { - this.failWithJSValue(ssl_error.toJS(this.globalObject)); - } } } } @@ -1264,6 +1526,7 @@ pub const PostgresSQLConnection = struct { this.poll_ref.ref(vm); } + this.resetConnectionTimeout(); this.deref(); } @@ -1299,11 +1562,8 @@ pub const PostgresSQLConnection = struct { this.read_buffer.byte_list.len = 0; this.read_buffer.write(bun.default_allocator, data[offset..]) catch @panic("failed to write to read buffer"); } else { - if (comptime bun.Environment.allow_assert) { - if (@errorReturnTrace()) |trace| { - debug("Error: {s}\n{}", .{ @errorName(err), trace }); - } - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + this.fail("Failed to read data", err); } }; @@ -1315,11 +1575,7 @@ pub const PostgresSQLConnection = struct { this.read_buffer.write(bun.default_allocator, data) catch @panic("failed to write to read buffer"); PostgresRequest.onData(this, Reader, this.bufferedReader()) catch |err| { if (err != error.ShortRead) { - if (comptime bun.Environment.allow_assert) { - if (@errorReturnTrace()) |trace| { - debug("Error: {s}\n{}", .{ @errorName(err), trace }); - } - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); this.fail("Failed to read data", err); return; } @@ -1363,7 +1619,7 @@ pub const PostgresSQLConnection = struct { pub fn call(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { var vm = globalObject.bunVM(); - const arguments = callframe.arguments_old(10).slice(); + const arguments = callframe.arguments_old(13).slice(); const hostname_str = arguments[0].toBunString(globalObject); defer hostname_str.deref(); const port = arguments[1].coerce(i32, globalObject); @@ -1460,13 +1716,15 @@ pub const PostgresSQLConnection = struct { const on_connect = arguments[8]; const on_close = arguments[9]; + const idle_timeout = arguments[10].toInt32(); + const connection_timeout = arguments[11].toInt32(); + const max_lifetime = arguments[12].toInt32(); - var ptr = try bun.default_allocator.create(PostgresSQLConnection); + const ptr: *PostgresSQLConnection = try bun.default_allocator.create(PostgresSQLConnection); ptr.* = PostgresSQLConnection{ .globalObject = globalObject, - .on_connect = JSC.Strong.create(on_connect, globalObject), - .on_close = JSC.Strong.create(on_close, globalObject), + .database = database, .user = username, .password = password, @@ -1479,6 +1737,9 @@ pub const PostgresSQLConnection = struct { .tls_ctx = tls_ctx, .ssl_mode = ssl_mode, .tls_status = if (ssl_mode != .disable) .pending else .none, + .idle_timeout_interval_ms = @intCast(idle_timeout), + .connection_timeout_ms = @intCast(connection_timeout), + .max_lifetime_interval_ms = @intCast(max_lifetime), }; ptr.updateHasPendingActivity(); @@ -1487,6 +1748,9 @@ pub const PostgresSQLConnection = struct { js_value.ensureStillAlive(); ptr.js_value = js_value; + PostgresSQLConnection.onconnectSetCached(js_value, globalObject, on_connect); + PostgresSQLConnection.oncloseSetCached(js_value, globalObject, on_close); + { const hostname = hostname_str.toUTF8(bun.default_allocator); defer hostname.deinit(); @@ -1498,6 +1762,7 @@ pub const PostgresSQLConnection = struct { vm.rareData().postgresql_context.tcp = ctx_; break :brk ctx_; }; + ptr.socket = .{ .SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false) catch |err| { tls_config.deinit(); @@ -1508,6 +1773,8 @@ pub const PostgresSQLConnection = struct { return globalObject.throwError(err, "failed to connect to postgresql"); }, }; + + ptr.resetConnectionTimeout(); } return js_value; @@ -1600,7 +1867,17 @@ pub const PostgresSQLConnection = struct { return .undefined; } + pub fn stopTimers(this: *PostgresSQLConnection) void { + if (this.timer.state == .ACTIVE) { + this.globalObject.bunVM().timer.remove(&this.timer); + } + if (this.max_lifetime_timer.state == .ACTIVE) { + this.globalObject.bunVM().timer.remove(&this.max_lifetime_timer); + } + } + pub fn deinit(this: *@This()) void { + this.stopTimers(); var iter = this.statements.valueIterator(); while (iter.next()) |stmt_ptr| { var stmt = stmt_ptr.*; @@ -1609,8 +1886,6 @@ pub const PostgresSQLConnection = struct { this.statements.deinit(bun.default_allocator); this.write_buffer.deinit(bun.default_allocator); this.read_buffer.deinit(bun.default_allocator); - this.on_close.deinit(); - this.on_connect.deinit(); this.backend_parameters.deinit(); bun.default_allocator.free(this.options_buf); this.tls_config.deinit(); @@ -1618,6 +1893,8 @@ pub const PostgresSQLConnection = struct { } pub fn disconnect(this: *@This()) void { + this.stopTimers(); + if (this.status == .connected) { this.status = .disconnected; this.poll_ref.disable(); @@ -1636,12 +1913,12 @@ pub const PostgresSQLConnection = struct { pub const Writer = struct { connection: *PostgresSQLConnection, - pub fn write(this: Writer, data: []const u8) anyerror!void { + pub fn write(this: Writer, data: []const u8) AnyPostgresError!void { var buffer = &this.connection.write_buffer; try buffer.write(bun.default_allocator, data); } - pub fn pwrite(this: Writer, data: []const u8, index: usize) anyerror!void { + pub fn pwrite(this: Writer, data: []const u8, index: usize) AnyPostgresError!void { @memcpy(this.connection.write_buffer.byte_list.slice()[index..][0..data.len], data); } @@ -1676,7 +1953,7 @@ pub const PostgresSQLConnection = struct { pub fn ensureCapacity(this: Reader, count: usize) bool { return @as(usize, this.connection.read_buffer.head) + count <= @as(usize, this.connection.read_buffer.byte_list.len); } - pub fn read(this: Reader, count: usize) anyerror!Data { + pub fn read(this: Reader, count: usize) AnyPostgresError!Data { var remaining = this.connection.read_buffer.remaining(); if (@as(usize, remaining.len) < count) { return error.ShortRead; @@ -1687,7 +1964,7 @@ pub const PostgresSQLConnection = struct { .temporary = remaining[0..count], }; } - pub fn readZ(this: Reader) anyerror!Data { + pub fn readZ(this: Reader) AnyPostgresError!Data { const remain = this.connection.read_buffer.remaining(); if (bun.strings.indexOfChar(remain, 0)) |zero| { @@ -1712,6 +1989,8 @@ pub const PostgresSQLConnection = struct { value: Value, free_value: u8 = 0, + isIndexedColumn: u8 = 0, + index: u32 = 0, pub const Tag = enum(u8) { null = 0, @@ -1799,7 +2078,7 @@ pub const PostgresSQLConnection = struct { } } - pub fn fromBytes(binary: bool, oid: int4, bytes: []const u8, globalObject: *JSC.JSGlobalObject) anyerror!DataCell { + pub fn fromBytes(binary: bool, oid: int4, bytes: []const u8, globalObject: *JSC.JSGlobalObject) !DataCell { switch (@as(types.Tag, @enumFromInt(@as(short, @intCast(oid))))) { // TODO: .int2_array, .float8_array inline .int4_array, .float4_array => |tag| { @@ -1876,7 +2155,7 @@ pub const PostgresSQLConnection = struct { return DataCell{ .tag = .float8, .value = .{ .float8 = float4 } }; } }, - .json => { + .jsonb, .json => { return DataCell{ .tag = .json, .value = .{ .json = String.createUTF8(bytes).value.WTFStringImpl }, .free_value = 1 }; }, .bool => { @@ -1956,7 +2235,7 @@ pub const PostgresSQLConnection = struct { return pg_ntoT(32, x); } - pub fn parseBinary(comptime tag: types.Tag, comptime ReturnType: type, bytes: []const u8) anyerror!ReturnType { + pub fn parseBinary(comptime tag: types.Tag, comptime ReturnType: type, bytes: []const u8) AnyPostgresError!ReturnType { switch (comptime tag) { .float8 => { return @as(f64, @bitCast(try parseBinary(.int8, i64, bytes))); @@ -2005,6 +2284,13 @@ pub const PostgresSQLConnection = struct { } } + pub const Flags = packed struct(u32) { + has_indexed_columns: bool = false, + has_named_columns: bool = false, + has_duplicate_columns: bool = false, + _: u29 = 0, + }; + pub const Putter = struct { list: []DataCell, fields: []const protocol.FieldDescription, @@ -2012,16 +2298,25 @@ pub const PostgresSQLConnection = struct { count: usize = 0, globalObject: *JSC.JSGlobalObject, - extern fn JSC__constructObjectFromDataCell(*JSC.JSGlobalObject, JSValue, JSValue, [*]DataCell, u32) JSValue; - pub fn toJS(this: *Putter, globalObject: *JSC.JSGlobalObject, array: JSValue, structure: JSValue) JSValue { - return JSC__constructObjectFromDataCell(globalObject, array, structure, this.list.ptr, @truncate(this.fields.len)); + extern fn JSC__constructObjectFromDataCell( + *JSC.JSGlobalObject, + JSValue, + JSValue, + [*]DataCell, + u32, + Flags, + ) JSValue; + + pub fn toJS(this: *Putter, globalObject: *JSC.JSGlobalObject, array: JSValue, structure: JSValue, flags: Flags) JSValue { + return JSC__constructObjectFromDataCell(globalObject, array, structure, this.list.ptr, @truncate(this.fields.len), flags); } - pub fn put(this: *Putter, index: u32, optional_bytes: ?*Data) anyerror!bool { - const oid = this.fields[index].type_oid; + pub fn put(this: *Putter, index: u32, optional_bytes: ?*Data) !bool { + const field = &this.fields[index]; + const oid = field.type_oid; debug("index: {d}, oid: {d}", .{ index, oid }); - - this.list[index] = if (optional_bytes) |data| + const cell: *DataCell = &this.list[index]; + cell.* = if (optional_bytes) |data| try DataCell.fromBytes(this.binary, oid, data.slice(), this.globalObject) else DataCell{ @@ -2031,6 +2326,21 @@ pub const PostgresSQLConnection = struct { }, }; this.count += 1; + cell.index = switch (field.name_or_index) { + // The indexed columns can be out of order. + .index => |i| i, + + else => @intCast(index), + }; + + // TODO: when duplicate and we know the result will be an object + // and not a .values() array, we can discard the data + // immediately. + cell.isIndexedColumn = switch (field.name_or_index) { + .duplicate => 2, + .index => 1, + .name => 0, + }; return true; } }; @@ -2072,7 +2382,7 @@ pub const PostgresSQLConnection = struct { const binding_value = PostgresSQLQuery.bindingGetCached(req.thisValue) orelse .zero; const columns_value = PostgresSQLQuery.columnsGetCached(req.thisValue) orelse .zero; PostgresRequest.bindAndExecute(this.globalObject, stmt, binding_value, columns_value, PostgresSQLConnection.Writer, this.writer()) catch |err| { - req.onWriteFail(err, this.globalObject); + req.onWriteFail(err, this.globalObject, this.getQueriesArray()); req.deref(); this.requests.discard(1); continue; @@ -2091,7 +2401,11 @@ pub const PostgresSQLConnection = struct { return any; } - pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.EnumLiteral), comptime Context: type, reader: protocol.NewReader(Context)) !void { + pub fn getQueriesArray(this: *const PostgresSQLConnection) JSValue { + return PostgresSQLConnection.queriesGetCached(this.js_value) orelse .zero; + } + + pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.EnumLiteral), comptime Context: type, reader: protocol.NewReader(Context)) AnyPostgresError!void { debug("on({s})", .{@tagName(MessageType)}); if (comptime MessageType != .ReadyForQuery) { this.is_ready_for_query = false; @@ -2101,6 +2415,7 @@ pub const PostgresSQLConnection = struct { .DataRow => { const request = this.current() orelse return error.ExpectedRequest; var statement = request.statement orelse return error.ExpectedStatement; + statement.checkForDuplicateFields(); var putter = DataCell.Putter{ .list = &.{}, @@ -2134,7 +2449,7 @@ pub const PostgresSQLConnection = struct { const pending_value = PostgresSQLQuery.pendingValueGetCached(request.thisValue) orelse .zero; pending_value.ensureStillAlive(); - const result = putter.toJS(this.globalObject, pending_value, statement.structure(this.js_value, this.globalObject)); + const result = putter.toJS(this.globalObject, pending_value, statement.structure(this.js_value, this.globalObject), statement.fields_flags); if (pending_value == .zero) { PostgresSQLQuery.pendingValueSetCached(request.thisValue, this.globalObject, result); @@ -2181,7 +2496,7 @@ pub const PostgresSQLConnection = struct { debug("-> {s}", .{cmd.command_tag.slice()}); _ = this.requests.discard(1); defer this.updateRef(); - request.onSuccess(cmd.command_tag.slice(), this.globalObject); + request.onSuccess(cmd.command_tag.slice(), this.globalObject, this.js_value); }, .BindComplete => { try reader.eatMessage(protocol.BindComplete); @@ -2255,7 +2570,12 @@ pub const PostgresSQLConnection = struct { const iteration_count = try cont.iterationCount(); - const server_salt_decoded_base64 = try bun.base64.decodeAlloc(bun.z_allocator, cont.s); + const server_salt_decoded_base64 = bun.base64.decodeAlloc(bun.z_allocator, cont.s) catch |err| { + return switch (err) { + error.DecodingFailed => error.SASL_SIGNATURE_INVALID_BASE64, + else => |e| e, + }; + }; defer bun.z_allocator.free(server_salt_decoded_base64); try sasl.computeSaltedPassword(server_salt_decoded_base64, iteration_count, this); @@ -2352,8 +2672,46 @@ pub const PostgresSQLConnection = struct { this.flushData(); }, + .MD5Password => |md5| { + debug("MD5Password", .{}); + // Format is: md5 + md5(md5(password + username) + salt) + var first_hash_buf: bun.sha.MD5.Digest = undefined; + var first_hash_str: [32]u8 = undefined; + var final_hash_buf: bun.sha.MD5.Digest = undefined; + var final_hash_str: [32]u8 = undefined; + var final_password_buf: [36]u8 = undefined; + + // First hash: md5(password + username) + var first_hasher = bun.sha.MD5.init(); + first_hasher.update(this.password); + first_hasher.update(this.user); + first_hasher.final(&first_hash_buf); + const first_hash_str_output = std.fmt.bufPrint(&first_hash_str, "{x}", .{std.fmt.fmtSliceHexLower(&first_hash_buf)}) catch unreachable; + + // Second hash: md5(first_hash + salt) + var final_hasher = bun.sha.MD5.init(); + final_hasher.update(first_hash_str_output); + final_hasher.update(&md5.salt); + final_hasher.final(&final_hash_buf); + const final_hash_str_output = std.fmt.bufPrint(&final_hash_str, "{x}", .{std.fmt.fmtSliceHexLower(&final_hash_buf)}) catch unreachable; + + // Format final password as "md5" + final_hash + const final_password = std.fmt.bufPrintZ(&final_password_buf, "md5{s}", .{final_hash_str_output}) catch unreachable; + + var response = protocol.PasswordMessage{ + .password = .{ + .temporary = final_password, + }, + }; + + this.authentication_state = .{ .md5 = {} }; + try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); + this.flushData(); + }, + else => { debug("TODO auth: {s}", .{@tagName(std.meta.activeTag(auth))}); + this.fail("TODO: support authentication method: {s}", error.UNSUPPORTED_AUTHENTICATION_METHOD); }, } }, @@ -2371,19 +2729,12 @@ pub const PostgresSQLConnection = struct { var err: protocol.ErrorResponse = undefined; try err.decodeInternal(Context, reader); - if (this.status == .connecting) { - this.status = .failed; + if (this.status == .connecting or this.status == .sent_startup_message) { defer { err.deinit(); - this.poll_ref.unref(this.globalObject.bunVM()); - this.updateHasPendingActivity(); } - const on_connect = this.on_connect.swap(); - if (on_connect == .zero) return; - const js_value = this.js_value; - js_value.ensureStillAlive(); - this.globalObject.queueMicrotask(on_connect, &[_]JSValue{ err.toJS(this.globalObject), js_value }); + this.failWithJSValue(err.toJS(this.globalObject)); // it shouldn't enqueue any requests while connecting bun.assert(this.requests.count == 0); @@ -2426,7 +2777,7 @@ pub const PostgresSQLConnection = struct { try reader.eatMessage(protocol.CloseComplete); var request = this.current() orelse return error.ExpectedRequest; _ = this.requests.discard(1); - request.onSuccess("CLOSECOMPLETE", this.globalObject); + request.onSuccess("CLOSECOMPLETE", this.globalObject, this.getQueriesArray()); }, .CopyInResponse => { debug("TODO CopyInResponse", .{}); @@ -2443,7 +2794,7 @@ pub const PostgresSQLConnection = struct { var request = this.current() orelse return error.ExpectedRequest; _ = this.requests.discard(1); this.updateRef(); - request.onSuccess("", this.globalObject); + request.onSuccess("", this.globalObject, this.getQueriesArray()); }, .CopyOutResponse => { debug("TODO CopyOutResponse", .{}); @@ -2467,35 +2818,33 @@ pub const PostgresSQLConnection = struct { } } - pub fn doFlush(this: *PostgresSQLConnection, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - _ = callframe; - _ = globalObject; - _ = this; - - return .undefined; - } - - pub fn createQuery(this: *PostgresSQLConnection, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - _ = callframe; - _ = globalObject; - _ = this; - - return .undefined; - } - pub fn getConnected(this: *PostgresSQLConnection, _: *JSC.JSGlobalObject) JSValue { return JSValue.jsBoolean(this.status == Status.connected); } + + pub fn consumeOnConnectCallback(this: *const PostgresSQLConnection, globalObject: *JSC.JSGlobalObject) ?JSC.JSValue { + const on_connect = PostgresSQLConnection.onconnectGetCached(this.js_value) orelse return null; + PostgresSQLConnection.onconnectSetCached(this.js_value, globalObject, .zero); + return on_connect; + } + + pub fn consumeOnCloseCallback(this: *const PostgresSQLConnection, globalObject: *JSC.JSGlobalObject) ?JSC.JSValue { + const on_close = PostgresSQLConnection.oncloseGetCached(this.js_value) orelse return null; + PostgresSQLConnection.oncloseSetCached(this.js_value, globalObject, .zero); + return on_close; + } }; pub const PostgresSQLStatement = struct { cached_structure: JSC.Strong = .{}, ref_count: u32 = 1, - fields: []const protocol.FieldDescription = &[_]protocol.FieldDescription{}, + fields: []protocol.FieldDescription = &[_]protocol.FieldDescription{}, parameters: []const int4 = &[_]int4{}, signature: Signature, status: Status = Status.parsing, error_response: protocol.ErrorResponse = .{}, + needs_duplicate_check: bool = true, + fields_flags: PostgresSQLConnection.DataCell.Flags = .{}, pub const Status = enum { parsing, @@ -2516,13 +2865,58 @@ pub const PostgresSQLStatement = struct { } } + pub fn checkForDuplicateFields(this: *PostgresSQLStatement) void { + if (!this.needs_duplicate_check) return; + this.needs_duplicate_check = false; + + var seen_numbers = std.ArrayList(u32).init(bun.default_allocator); + defer seen_numbers.deinit(); + var seen_fields = bun.StringHashMap(void).init(bun.default_allocator); + seen_fields.ensureUnusedCapacity(@intCast(this.fields.len)) catch bun.outOfMemory(); + defer seen_fields.deinit(); + + // iterate backwards + var remaining = this.fields.len; + var flags: PostgresSQLConnection.DataCell.Flags = .{}; + while (remaining > 0) { + remaining -= 1; + const field: *protocol.FieldDescription = &this.fields[remaining]; + switch (field.name_or_index) { + .name => |*name| { + const seen = seen_fields.getOrPut(name.slice()) catch unreachable; + if (seen.found_existing) { + field.name_or_index = .duplicate; + flags.has_duplicate_columns = true; + } + + flags.has_named_columns = true; + }, + .index => |index| { + if (std.mem.indexOfScalar(u32, seen_numbers.items, index) != null) { + field.name_or_index = .duplicate; + flags.has_duplicate_columns = true; + } else { + seen_numbers.append(index) catch bun.outOfMemory(); + } + + flags.has_indexed_columns = true; + }, + .duplicate => { + flags.has_duplicate_columns = true; + }, + } + } + + this.fields_flags = flags; + } + pub fn deinit(this: *PostgresSQLStatement) void { debug("PostgresSQLStatement deinit", .{}); bun.assert(this.ref_count == 0); for (this.fields) |*field| { - @constCast(field).deinit(); + field.deinit(); } bun.default_allocator.free(this.fields); bun.default_allocator.free(this.parameters); @@ -2534,21 +2928,37 @@ pub const PostgresSQLStatement = struct { pub fn structure(this: *PostgresSQLStatement, owner: JSValue, globalObject: *JSC.JSGlobalObject) JSValue { return this.cached_structure.get() orelse { - const names = bun.default_allocator.alloc(bun.String, this.fields.len) catch return .undefined; + const ids = bun.default_allocator.alloc(JSC.JSObject.ExternColumnIdentifier, this.fields.len) catch return .undefined; + this.checkForDuplicateFields(); defer { - for (names) |*name| { - name.deref(); + for (ids) |*name| { + name.deinit(); } - bun.default_allocator.free(names); + bun.default_allocator.free(ids); } - for (this.fields, names) |*field, *name| { - name.* = String.fromUTF8(field.name.slice()); + + for (this.fields, ids) |*field, *id| { + id.tag = switch (field.name_or_index) { + .name => 2, + .index => 1, + .duplicate => 0, + }; + switch (field.name_or_index) { + .name => |name| { + id.value.name = String.createUTF8(name.slice()); + }, + .index => |index| { + id.value.index = index; + }, + .duplicate => {}, + } } const structure_ = JSC.JSObject.createStructure( globalObject, owner, - @truncate(this.fields.len), - names.ptr, + @truncate(ids.len), + ids.ptr, + @bitCast(this.fields_flags), ); this.cached_structure.set(globalObject, structure_); return structure_; @@ -2723,7 +3133,7 @@ const Signature = struct { .float8 => try name.appendSlice(".float8"), .float4 => try name.appendSlice(".float4"), .numeric => try name.appendSlice(".numeric"), - .json => try name.appendSlice(".json"), + .json, .jsonb => try name.appendSlice(".json"), .bool => try name.appendSlice(".bool"), .timestamp => try name.appendSlice(".timestamp"), .timestamptz => try name.appendSlice(".timestamptz"), diff --git a/src/sql/postgres/postgres_protocol.zig b/src/sql/postgres/postgres_protocol.zig index 4aee1791f9..2abeff0787 100644 --- a/src/sql/postgres/postgres_protocol.zig +++ b/src/sql/postgres/postgres_protocol.zig @@ -15,7 +15,7 @@ const int4 = postgres.int4; const int8 = postgres.int8; const PostgresInt64 = postgres.PostgresInt64; const types = postgres.types; - +const AnyPostgresError = postgres.AnyPostgresError; pub const ArrayList = struct { array: *std.ArrayList(u8), @@ -23,11 +23,11 @@ pub const ArrayList = struct { return this.array.items.len; } - pub fn write(this: @This(), bytes: []const u8) anyerror!void { + pub fn write(this: @This(), bytes: []const u8) AnyPostgresError!void { try this.array.appendSlice(bytes); } - pub fn pwrite(this: @This(), bytes: []const u8, i: usize) anyerror!void { + pub fn pwrite(this: @This(), bytes: []const u8, i: usize) AnyPostgresError!void { @memcpy(this.array.items[i..][0..bytes.len], bytes); } @@ -71,7 +71,7 @@ pub const StackReader = struct { pub fn ensureCapacity(this: StackReader, count: usize) bool { return this.buffer.len >= (this.offset.* + count); } - pub fn read(this: StackReader, count: usize) anyerror!Data { + pub fn read(this: StackReader, count: usize) AnyPostgresError!Data { const offset = this.offset.*; if (!this.ensureCapacity(count)) { return error.ShortRead; @@ -82,7 +82,7 @@ pub const StackReader = struct { .temporary = this.buffer[offset..this.offset.*], }; } - pub fn readZ(this: StackReader) anyerror!Data { + pub fn readZ(this: StackReader) AnyPostgresError!Data { const remaining = this.peek(); if (bun.strings.indexOfChar(remaining, 0)) |zero| { this.skip(zero + 1); @@ -98,8 +98,8 @@ pub const StackReader = struct { pub fn NewWriterWrap( comptime Context: type, comptime offsetFn_: (fn (ctx: Context) usize), - comptime writeFunction_: (fn (ctx: Context, bytes: []const u8) anyerror!void), - comptime pwriteFunction_: (fn (ctx: Context, bytes: []const u8, offset: usize) anyerror!void), + comptime writeFunction_: (fn (ctx: Context, bytes: []const u8) AnyPostgresError!void), + comptime pwriteFunction_: (fn (ctx: Context, bytes: []const u8, offset: usize) AnyPostgresError!void), ) type { return struct { wrapped: Context, @@ -111,7 +111,7 @@ pub fn NewWriterWrap( pub const WrappedWriter = @This(); - pub inline fn write(this: @This(), data: []const u8) anyerror!void { + pub inline fn write(this: @This(), data: []const u8) AnyPostgresError!void { try writeFn(this.wrapped, data); } @@ -119,16 +119,16 @@ pub fn NewWriterWrap( index: usize, context: WrappedWriter, - pub fn write(this: LengthWriter) anyerror!void { + pub fn write(this: LengthWriter) AnyPostgresError!void { try this.context.pwrite(&Int32(this.context.offset() - this.index), this.index); } - pub fn writeExcludingSelf(this: LengthWriter) anyerror!void { + pub fn writeExcludingSelf(this: LengthWriter) AnyPostgresError!void { try this.context.pwrite(&Int32(this.context.offset() -| (this.index + 4)), this.index); } }; - pub inline fn length(this: @This()) anyerror!LengthWriter { + pub inline fn length(this: @This()) AnyPostgresError!LengthWriter { const i = this.offset(); try this.int4(0); return LengthWriter{ @@ -141,7 +141,7 @@ pub fn NewWriterWrap( return offsetFn(this.wrapped); } - pub inline fn pwrite(this: @This(), data: []const u8, i: usize) anyerror!void { + pub inline fn pwrite(this: @This(), data: []const u8, i: usize) AnyPostgresError!void { try pwriteFn(this.wrapped, data, i); } @@ -208,81 +208,81 @@ pub fn NewWriterWrap( pub const FieldType = enum(u8) { /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message), or a localized translation of one of these. Always present. - S = 'S', + severity = 'S', /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message). This is identical to the S field except that the contents are never localized. This is present only in messages generated by PostgreSQL versions 9.6 and later. - V = 'V', + localized_severity = 'V', /// Code: the SQLSTATE code for the error (see Appendix A). Not localizable. Always present. - C = 'C', + code = 'C', /// Message: the primary human-readable error message. This should be accurate but terse (typically one line). Always present. - M = 'M', + message = 'M', /// Detail: an optional secondary error message carrying more detail about the problem. Might run to multiple lines. - D = 'D', + detail = 'D', /// Hint: an optional suggestion what to do about the problem. This is intended to differ from Detail in that it offers advice (potentially inappropriate) rather than hard facts. Might run to multiple lines. - H = 'H', + hint = 'H', /// Position: the field value is a decimal ASCII integer, indicating an error cursor position as an index into the original query string. The first character has index 1, and positions are measured in characters not bytes. - P = 'P', + position = 'P', /// Internal position: this is defined the same as the P field, but it is used when the cursor position refers to an internally generated command rather than the one submitted by the client. The q field will always appear when this field appears. - p = 'p', + internal_position = 'p', /// Internal query: the text of a failed internally-generated command. This could be, for example, an SQL query issued by a PL/pgSQL function. - q = 'q', + internal = 'q', /// Where: an indication of the context in which the error occurred. Presently this includes a call stack traceback of active procedural language functions and internally-generated queries. The trace is one entry per line, most recent first. - W = 'W', + where = 'W', /// Schema name: if the error was associated with a specific database object, the name of the schema containing that object, if any. - s = 's', + schema = 's', /// Table name: if the error was associated with a specific table, the name of the table. (Refer to the schema name field for the name of the table's schema.) - t = 't', + table = 't', /// Column name: if the error was associated with a specific table column, the name of the column. (Refer to the schema and table name fields to identify the table.) - c = 'c', + column = 'c', /// Data type name: if the error was associated with a specific data type, the name of the data type. (Refer to the schema name field for the name of the data type's schema.) - d = 'd', + datatype = 'd', /// Constraint name: if the error was associated with a specific constraint, the name of the constraint. Refer to fields listed above for the associated table or domain. (For this purpose, indexes are treated as constraints, even if they weren't created with constraint syntax.) - n = 'n', + constraint = 'n', /// File: the file name of the source-code location where the error was reported. - F = 'F', + file = 'F', /// Line: the line number of the source-code location where the error was reported. - L = 'L', + line = 'L', /// Routine: the name of the source-code routine reporting the error. - R = 'R', + routine = 'R', _, }; pub const FieldMessage = union(FieldType) { - S: String, - V: String, - C: String, - M: String, - D: String, - H: String, - P: String, - p: String, - q: String, - W: String, - s: String, - t: String, - c: String, - d: String, - n: String, - F: String, - L: String, - R: String, + severity: String, + localized_severity: String, + code: String, + message: String, + detail: String, + hint: String, + position: String, + internal_position: String, + internal: String, + where: String, + schema: String, + table: String, + column: String, + datatype: String, + constraint: String, + file: String, + line: String, + routine: String, pub fn format(this: FieldMessage, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { switch (this) { @@ -319,24 +319,25 @@ pub const FieldMessage = union(FieldType) { pub fn init(tag: FieldType, message: []const u8) !FieldMessage { return switch (tag) { - .S => FieldMessage{ .S = String.createUTF8(message) }, - .V => FieldMessage{ .V = String.createUTF8(message) }, - .C => FieldMessage{ .C = String.createUTF8(message) }, - .M => FieldMessage{ .M = String.createUTF8(message) }, - .D => FieldMessage{ .D = String.createUTF8(message) }, - .H => FieldMessage{ .H = String.createUTF8(message) }, - .P => FieldMessage{ .P = String.createUTF8(message) }, - .p => FieldMessage{ .p = String.createUTF8(message) }, - .q => FieldMessage{ .q = String.createUTF8(message) }, - .W => FieldMessage{ .W = String.createUTF8(message) }, - .s => FieldMessage{ .s = String.createUTF8(message) }, - .t => FieldMessage{ .t = String.createUTF8(message) }, - .c => FieldMessage{ .c = String.createUTF8(message) }, - .d => FieldMessage{ .d = String.createUTF8(message) }, - .n => FieldMessage{ .n = String.createUTF8(message) }, - .F => FieldMessage{ .F = String.createUTF8(message) }, - .L => FieldMessage{ .L = String.createUTF8(message) }, - .R => FieldMessage{ .R = String.createUTF8(message) }, + .severity => FieldMessage{ .severity = String.createUTF8(message) }, + // Ignore this one for now. + // .localized_severity => FieldMessage{ .localized_severity = String.createUTF8(message) }, + .code => FieldMessage{ .code = String.createUTF8(message) }, + .message => FieldMessage{ .message = String.createUTF8(message) }, + .detail => FieldMessage{ .detail = String.createUTF8(message) }, + .hint => FieldMessage{ .hint = String.createUTF8(message) }, + .position => FieldMessage{ .position = String.createUTF8(message) }, + .internal_position => FieldMessage{ .internal_position = String.createUTF8(message) }, + .internal => FieldMessage{ .internal = String.createUTF8(message) }, + .where => FieldMessage{ .where = String.createUTF8(message) }, + .schema => FieldMessage{ .schema = String.createUTF8(message) }, + .table => FieldMessage{ .table = String.createUTF8(message) }, + .column => FieldMessage{ .column = String.createUTF8(message) }, + .datatype => FieldMessage{ .datatype = String.createUTF8(message) }, + .constraint => FieldMessage{ .constraint = String.createUTF8(message) }, + .file => FieldMessage{ .file = String.createUTF8(message) }, + .line => FieldMessage{ .line = String.createUTF8(message) }, + .routine => FieldMessage{ .routine = String.createUTF8(message) }, else => error.UnknownFieldType, }; } @@ -348,8 +349,8 @@ pub fn NewReaderWrap( comptime peekFn_: (fn (ctx: Context) []const u8), comptime skipFn_: (fn (ctx: Context, count: usize) void), comptime ensureCapacityFn_: (fn (ctx: Context, count: usize) bool), - comptime readFunction_: (fn (ctx: Context, count: usize) anyerror!Data), - comptime readZ_: (fn (ctx: Context) anyerror!Data), + comptime readFunction_: (fn (ctx: Context, count: usize) AnyPostgresError!Data), + comptime readZ_: (fn (ctx: Context) AnyPostgresError!Data), ) type { return struct { wrapped: Context, @@ -366,11 +367,11 @@ pub fn NewReaderWrap( markMessageStartFn(this.wrapped); } - pub inline fn read(this: @This(), count: usize) anyerror!Data { + pub inline fn read(this: @This(), count: usize) AnyPostgresError!Data { return try readFn(this.wrapped, count); } - pub inline fn eatMessage(this: @This(), comptime msg_: anytype) anyerror!void { + pub inline fn eatMessage(this: @This(), comptime msg_: anytype) AnyPostgresError!void { const msg = msg_[1..]; try this.ensureCapacity(msg.len); @@ -380,7 +381,7 @@ pub fn NewReaderWrap( return error.InvalidMessage; } - pub fn skip(this: @This(), count: usize) anyerror!void { + pub fn skip(this: @This(), count: usize) AnyPostgresError!void { skipFn(this.wrapped, count); } @@ -388,11 +389,11 @@ pub fn NewReaderWrap( return peekFn(this.wrapped); } - pub inline fn readZ(this: @This()) anyerror!Data { + pub inline fn readZ(this: @This()) AnyPostgresError!Data { return try readZFn(this.wrapped); } - pub inline fn ensureCapacity(this: @This(), count: usize) anyerror!void { + pub inline fn ensureCapacity(this: @This(), count: usize) AnyPostgresError!void { if (!ensureCapacityFn(this.wrapped, count)) { return error.ShortRead; } @@ -457,7 +458,7 @@ pub fn NewWriter(comptime Context: type) type { fn decoderWrap(comptime Container: type, comptime decodeFn: anytype) type { return struct { - pub fn decode(this: *Container, context: anytype) anyerror!void { + pub fn decode(this: *Container, context: anytype) AnyPostgresError!void { const Context = @TypeOf(context); try decodeFn(this, Context, NewReader(Context){ .wrapped = context }); } @@ -466,7 +467,7 @@ fn decoderWrap(comptime Container: type, comptime decodeFn: anytype) type { fn writeWrap(comptime Container: type, comptime writeFn: anytype) type { return struct { - pub fn write(this: *Container, context: anytype) anyerror!void { + pub fn write(this: *Container, context: anytype) AnyPostgresError!void { const Context = @TypeOf(context); try writeFn(this, Context, NewWriter(Context){ .wrapped = context }); } @@ -538,9 +539,6 @@ pub const Authentication = union(enum) { }, 5 => { if (message_length != 12) return error.InvalidMessageLength; - if (!try reader.expectInt(u32, 5)) { - return error.InvalidMessage; - } var salt_data = try reader.bytes(4); defer salt_data.deinit(); this.* = .{ @@ -722,23 +720,117 @@ pub const ErrorResponse = struct { var b = bun.StringBuilder{}; defer b.deinit(bun.default_allocator); - for (this.messages.items) |msg| { - b.cap += switch (msg) { + // Pre-calculate capacity to avoid reallocations + for (this.messages.items) |*msg| { + b.cap += switch (msg.*) { inline else => |m| m.utf8ByteLength(), } + 1; } b.allocate(bun.default_allocator) catch {}; - for (this.messages.items) |msg| { - var str = switch (msg) { - inline else => |m| m.toUTF8(bun.default_allocator), - }; - defer str.deinit(); - _ = b.append(str.slice()); - _ = b.append("\n"); + // Build a more structured error message + var severity: String = String.dead; + var code: String = String.dead; + var message: String = String.dead; + var detail: String = String.dead; + var hint: String = String.dead; + var position: String = String.dead; + var where: String = String.dead; + var schema: String = String.dead; + var table: String = String.dead; + var column: String = String.dead; + var datatype: String = String.dead; + var constraint: String = String.dead; + var file: String = String.dead; + var line: String = String.dead; + var routine: String = String.dead; + + for (this.messages.items) |*msg| { + switch (msg.*) { + .severity => |str| severity = str, + .code => |str| code = str, + .message => |str| message = str, + .detail => |str| detail = str, + .hint => |str| hint = str, + .position => |str| position = str, + .where => |str| where = str, + .schema => |str| schema = str, + .table => |str| table = str, + .column => |str| column = str, + .datatype => |str| datatype = str, + .constraint => |str| constraint = str, + .file => |str| file = str, + .line => |str| line = str, + .routine => |str| routine = str, + else => {}, + } } - return globalObject.createSyntaxErrorInstance("Postgres error occurred\n{s}", .{b.allocatedSlice()[0..b.len]}); + var needs_newline = false; + construct_message: { + if (!message.isEmpty()) { + _ = b.appendStr(message); + needs_newline = true; + break :construct_message; + } + if (!detail.isEmpty()) { + if (needs_newline) { + _ = b.append("\n"); + } else { + _ = b.append(" "); + } + needs_newline = true; + _ = b.appendStr(detail); + } + if (!hint.isEmpty()) { + if (needs_newline) { + _ = b.append("\n"); + } else { + _ = b.append(" "); + } + needs_newline = true; + _ = b.appendStr(hint); + } + } + + const possible_fields = .{ + .{ "detail", detail, void }, + .{ "hint", hint, void }, + .{ "column", column, void }, + .{ "constraint", constraint, void }, + .{ "datatype", datatype, void }, + .{ "errno", code, i32 }, + .{ "position", position, i32 }, + .{ "schema", schema, void }, + .{ "table", table, void }, + .{ "where", where, void }, + }; + + const error_code: JSC.Error = + // https://www.postgresql.org/docs/8.1/errcodes-appendix.html + if (code.toInt32() orelse 0 == 42601) + JSC.Error.ERR_POSTGRES_SYNTAX_ERROR + else + JSC.Error.ERR_POSTGRES_SERVER_ERROR; + const err = error_code.fmt(globalObject, "{s}", .{b.allocatedSlice()[0..b.len]}); + + inline for (possible_fields) |field| { + if (!field.@"1".isEmpty()) { + const value = brk: { + if (field.@"2" == i32) { + if (field.@"1".toInt32()) |val| { + break :brk JSC.JSValue.jsNumberFromInt32(val); + } + } + + break :brk field.@"1".toJS(globalObject); + }; + + err.put(globalObject, JSC.ZigString.static(field.@"0"), value); + } + } + + return err; } }; @@ -847,7 +939,7 @@ pub const FormatCode = enum { pub const null_int4 = 4294967295; pub const DataRow = struct { - pub fn decode(context: anytype, comptime ContextType: type, reader: NewReader(ContextType), comptime forEach: fn (@TypeOf(context), index: u32, bytes: ?*Data) anyerror!bool) anyerror!void { + pub fn decode(context: anytype, comptime ContextType: type, reader: NewReader(ContextType), comptime forEach: fn (@TypeOf(context), index: u32, bytes: ?*Data) AnyPostgresError!bool) AnyPostgresError!void { var remaining_bytes = try reader.length(); remaining_bytes -|= 4; @@ -871,8 +963,47 @@ pub const DataRow = struct { pub const BindComplete = [_]u8{'2'} ++ toBytes(Int32(4)); +pub const ColumnIdentifier = union(enum) { + name: Data, + index: u32, + duplicate: void, + + pub fn init(name: Data) !@This() { + if (switch (name.slice().len) { + 1..."4294967295".len => true, + 0 => return .{ .name = .{ .empty = {} } }, + else => false, + }) might_be_int: { + // use a u64 to avoid overflow + var int: u64 = 0; + for (name.slice()) |byte| { + int = int * 10 + switch (byte) { + '0'...'9' => @as(u64, byte - '0'), + else => break :might_be_int, + }; + } + + // JSC only supports indexed property names up to 2^32 + if (int < std.math.maxInt(u32)) + return .{ .index = @intCast(int) }; + } + + return .{ .name = .{ .owned = try name.toOwned() } }; + } + + pub fn deinit(this: *@This()) void { + switch (this.*) { + .name => |*name| name.deinit(), + else => {}, + } + } +}; pub const FieldDescription = struct { - name: Data = .{ .empty = {} }, + /// JavaScriptCore treats numeric property names differently than string property names. + /// so we do the work to figure out if the property name is a number ahead of time. + name_or_index: ColumnIdentifier = .{ + .name = .{ .empty = {} }, + }, table_oid: int4 = 0, column_index: short = 0, type_oid: int4 = 0, @@ -882,10 +1013,10 @@ pub const FieldDescription = struct { } pub fn deinit(this: *@This()) void { - this.name.deinit(); + this.name_or_index.deinit(); } - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) AnyPostgresError!void { var name = try reader.readZ(); errdefer { name.deinit(); @@ -905,7 +1036,7 @@ pub const FieldDescription = struct { .table_oid = try reader.int4(), .column_index = try reader.short(), .type_oid = try reader.int4(), - .name = .{ .owned = try name.toOwned() }, + .name_or_index = try ColumnIdentifier.init(name), }; try reader.skip(2 + 4 + 2); @@ -915,10 +1046,10 @@ pub const FieldDescription = struct { }; pub const RowDescription = struct { - fields: []const FieldDescription = &[_]FieldDescription{}, + fields: []FieldDescription = &[_]FieldDescription{}, pub fn deinit(this: *@This()) void { for (this.fields) |*field| { - @constCast(field).deinit(); + field.deinit(); } bun.default_allocator.free(this.fields); @@ -1355,6 +1486,29 @@ pub const NoticeResponse = struct { } } pub const decode = decoderWrap(NoticeResponse, decodeInternal).decode; + + pub fn toJS(this: NoticeResponse, globalObject: *JSC.JSGlobalObject) JSValue { + var b = bun.StringBuilder{}; + defer b.deinit(bun.default_allocator); + + for (this.messages.items) |msg| { + b.cap += switch (msg) { + inline else => |m| m.utf8ByteLength(), + } + 1; + } + b.allocate(bun.default_allocator) catch {}; + + for (this.messages.items) |msg| { + var str = switch (msg) { + inline else => |m| m.toUTF8(bun.default_allocator), + }; + defer str.deinit(); + _ = b.append(str.slice()); + _ = b.append("\n"); + } + + return JSC.ZigString.init(b.allocatedSlice()[0..b.len]).toJS(globalObject); + } }; pub const CopyFail = struct { diff --git a/src/sql/postgres/postgres_types.zig b/src/sql/postgres/postgres_types.zig index 498b7f1d0a..74e8ff104b 100644 --- a/src/sql/postgres/postgres_types.zig +++ b/src/sql/postgres/postgres_types.zig @@ -12,6 +12,7 @@ const JSValue = JSC.JSValue; const JSC = bun.JSC; const short = postgres.short; const int4 = postgres.int4; +const AnyPostgresError = postgres.AnyPostgresError; // select b.typname, b.oid, b.typarray // from pg_catalog.pg_type a @@ -169,8 +170,17 @@ pub const Tag = enum(short) { bit_array = 1561, varbit_array = 1563, numeric_array = 1231, + jsonb = 3802, + jsonb_array = 3807, + // Not really sure what this is. + jsonpath = 4072, + jsonpath_array = 4073, _, + pub fn name(this: Tag) ?[]const u8 { + return std.enums.tagName(Tag, this); + } + pub fn isBinaryFormatSupported(this: Tag) bool { return switch (this) { // TODO: .int2_array, .float8_array, @@ -282,7 +292,7 @@ pub const Tag = enum(short) { globalObject: *JSC.JSGlobalObject, comptime Type: type, value: Type, - ) anyerror!JSValue { + ) AnyPostgresError!JSValue { switch (tag) { .numeric => { return numeric.toJS(globalObject, value); @@ -292,7 +302,7 @@ pub const Tag = enum(short) { return numeric.toJS(globalObject, value); }, - .json => { + .json, .jsonb => { return json.toJS(globalObject, value); }, @@ -326,7 +336,7 @@ pub const Tag = enum(short) { tag: Tag, globalObject: *JSC.JSGlobalObject, value: anytype, - ) anyerror!JSValue { + ) AnyPostgresError!JSValue { return toJSWithType(tag, globalObject, @TypeOf(value), value); } @@ -363,16 +373,16 @@ pub const Tag = enum(short) { // Ban these types: if (tag == .NumberObject) { - return error.JSError; + return globalObject.ERR_INVALID_ARG_TYPE("Number object is ambiguous and cannot be used as a PostgreSQL type", .{}).throw(); } if (tag == .BooleanObject) { - return error.JSError; + return globalObject.ERR_INVALID_ARG_TYPE("Boolean object is ambiguous and cannot be used as a PostgreSQL type", .{}).throw(); } // It's something internal if (!tag.isIndexable()) { - return error.JSError; + return globalObject.ERR_INVALID_ARG_TYPE("Unknown object is not a valid PostgreSQL type", .{}).throw(); } // We will JSON.stringify anything else. @@ -414,7 +424,7 @@ pub const string = struct { globalThis: *JSC.JSGlobalObject, comptime Type: type, value: Type, - ) anyerror!JSValue { + ) AnyPostgresError!JSValue { switch (comptime Type) { [:0]u8, []u8, []const u8, [:0]const u8 => { var str = String.fromUTF8(value); @@ -456,7 +466,7 @@ pub const numeric = struct { pub fn toJS( _: *JSC.JSGlobalObject, value: anytype, - ) anyerror!JSValue { + ) AnyPostgresError!JSValue { return JSValue.jsNumber(value); } }; @@ -468,12 +478,12 @@ pub const json = struct { pub fn toJS( globalObject: *JSC.JSGlobalObject, value: *Data, - ) anyerror!JSValue { + ) AnyPostgresError!JSValue { defer value.deinit(); var str = bun.String.fromUTF8(value.slice()); defer str.deref(); const parse_result = JSValue.parse(str.toJS(globalObject), globalObject); - if (parse_result.isAnyError()) { + if (parse_result.AnyPostgresError()) { return globalObject.throwValue(parse_result); } @@ -488,7 +498,7 @@ pub const @"bool" = struct { pub fn toJS( _: *JSC.JSGlobalObject, value: bool, - ) anyerror!JSValue { + ) AnyPostgresError!JSValue { return JSValue.jsBoolean(value); } }; @@ -548,7 +558,7 @@ pub const bytea = struct { pub fn toJS( globalObject: *JSC.JSGlobalObject, value: *Data, - ) anyerror!JSValue { + ) AnyPostgresError!JSValue { defer value.deinit(); // var slice = value.slice()[@min(1, value.len)..]; diff --git a/src/string.zig b/src/string.zig index 3b2dab9fcb..791ea81add 100644 --- a/src/string.zig +++ b/src/string.zig @@ -47,6 +47,10 @@ pub const WTFStringImplStruct = extern struct { return this.m_refCount / s_refCountIncrement; } + pub fn memoryCost(this: WTFStringImpl) usize { + return this.byteLength(); + } + pub fn isStatic(this: WTFStringImpl) bool { return this.m_refCount & s_refCountIncrement != 0; } @@ -193,6 +197,14 @@ pub const WTFStringImplStruct = extern struct { return this.is8Bit() and bun.strings.isAllASCII(this.latin1Slice()); } + pub fn utf16ByteLength(this: WTFStringImpl) usize { + if (this.is8Bit()) { + return this.length() * 2; + } else { + return this.length(); + } + } + pub fn utf8ByteLength(this: WTFStringImpl) usize { if (this.is8Bit()) { const input = this.latin1Slice(); @@ -203,11 +215,6 @@ pub const WTFStringImplStruct = extern struct { } } - pub fn utf16ByteLength(this: WTFStringImpl) usize { - // All latin1 characters fit in a single UTF-16 code unit. - return this.length() * 2; - } - pub fn latin1ByteLength(this: WTFStringImpl) usize { // Not all UTF-16 characters fit are representable in latin1. // Those get truncated? @@ -316,6 +323,12 @@ pub const String = extern struct { extern fn BunString__fromUTF16ToLatin1(bytes: [*]const u16, len: usize) String; extern fn BunString__fromLatin1Unitialized(len: usize) String; extern fn BunString__fromUTF16Unitialized(len: usize) String; + extern fn BunString__toInt32(this: String) i64; + pub fn toInt32(this: String) ?i32 { + const val = BunString__toInt32(this); + if (val > std.math.maxInt(i32)) return null; + return @intCast(val); + } pub fn ascii(bytes: []const u8) String { return String{ .tag = .ZigString, .value = .{ .ZigString = ZigString.init(bytes) } }; @@ -329,11 +342,10 @@ pub const String = extern struct { if (this.tag == .WTFStringImpl) this.value.WTFStringImpl.ensureHash(); } + extern fn BunString__transferToJS(this: *String, globalThis: *JSC.JSGlobalObject) JSC.JSValue; pub fn transferToJS(this: *String, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const js_value = this.toJS(globalThis); - this.deref(); - this.* = dead; - return js_value; + JSC.markBinding(@src()); + return BunString__transferToJS(this, globalThis); } pub fn toOwnedSlice(this: String, allocator: std.mem.Allocator) ![]u8 { @@ -981,6 +993,7 @@ pub const String = extern struct { return ZigString.Slice.empty; } + /// use `byteSlice` to get a `[]const u8`. pub fn toSlice(this: String, allocator: std.mem.Allocator) SliceWithUnderlyingString { return SliceWithUnderlyingString{ .utf8 = this.toUTF8(allocator), @@ -988,7 +1001,7 @@ pub const String = extern struct { }; } - pub fn toThreadSafeSlice(this: *String, allocator: std.mem.Allocator) SliceWithUnderlyingString { + pub fn toThreadSafeSlice(this: *const String, allocator: std.mem.Allocator) SliceWithUnderlyingString { if (this.tag == .WTFStringImpl) { if (!this.value.WTFStringImpl.isThreadSafe()) { const slice = this.value.WTFStringImpl.toUTF8WithoutRef(allocator); @@ -1055,6 +1068,12 @@ pub const String = extern struct { extern fn Bun__parseDate(*JSC.JSGlobalObject, *String) f64; extern fn BunString__fromJSRef(globalObject: *JSC.JSGlobalObject, value: bun.JSC.JSValue, out: *String) bool; extern fn BunString__toWTFString(this: *String) void; + extern fn BunString__createUTF8ForJS(globalObject: *JSC.JSGlobalObject, ptr: [*]const u8, len: usize) JSC.JSValue; + + pub fn createUTF8ForJS(globalObject: *JSC.JSGlobalObject, utf8_slice: []const u8) JSC.JSValue { + JSC.markBinding(@src()); + return BunString__createUTF8ForJS(globalObject, utf8_slice.ptr, utf8_slice.len); + } pub fn parseDate(this: *String, globalObject: *JSC.JSGlobalObject) f64 { JSC.markBinding(@src()); @@ -1361,7 +1380,7 @@ pub const SliceWithUnderlyingString = struct { utf8: ZigString.Slice = ZigString.Slice.empty, underlying: String = String.dead, - did_report_extra_memory_debug: bun.DebugOnly(bool) = if (bun.Environment.allow_assert) false else {}, + did_report_extra_memory_debug: bun.DebugOnly(bool) = if (bun.Environment.allow_assert) false, pub inline fn reportExtraMemory(this: *SliceWithUnderlyingString, vm: *JSC.VM) void { if (comptime bun.Environment.allow_assert) { @@ -1452,6 +1471,14 @@ pub const SliceWithUnderlyingString = struct { } pub fn toJS(this: *SliceWithUnderlyingString, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return this.toJSWithOptions(globalObject, false); + } + + pub fn transferToJS(this: *SliceWithUnderlyingString, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return this.toJSWithOptions(globalObject, true); + } + + fn toJSWithOptions(this: *SliceWithUnderlyingString, globalObject: *JSC.JSGlobalObject, transfer: bool) JSC.JSValue { if ((this.underlying.tag == .Dead or this.underlying.tag == .Empty) and this.utf8.length() > 0) { if (comptime bun.Environment.allow_assert) { if (this.utf8.allocator.get()) |allocator| { @@ -1473,11 +1500,27 @@ pub const SliceWithUnderlyingString = struct { } } - const out = bun.String.createUTF8(this.utf8.slice()); - defer out.deref(); - return out.toJS(globalObject); + defer { + if (transfer) { + this.utf8.deinit(); + this.utf8 = .{}; + } + } + + return String.createUTF8ForJS(globalObject, this.utf8.slice()); } - return this.underlying.toJS(globalObject); + if (transfer) { + this.utf8.deinit(); + this.utf8 = .{}; + return this.underlying.transferToJS(globalObject); + } else { + return this.underlying.toJS(globalObject); + } } }; + +comptime { + bun.assert_eql(@sizeOf(bun.String), 24); + bun.assert_eql(@alignOf(bun.String), 8); +} diff --git a/src/string_builder.zig b/src/string_builder.zig index e5e3d0bd47..0178663a4f 100644 --- a/src/string_builder.zig +++ b/src/string_builder.zig @@ -89,6 +89,12 @@ pub fn appendZ(this: *StringBuilder, slice: string) [:0]const u8 { return result; } +pub fn appendStr(this: *StringBuilder, str: bun.String) string { + const slice = str.toUTF8(bun.default_allocator); + defer slice.deinit(); + return this.append(slice.slice()); +} + pub fn append(this: *StringBuilder, slice: string) string { if (comptime Environment.allow_assert) { assert(this.len <= this.cap); // didn't count everything diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 2703a74e24..4fed847464 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -9,6 +9,7 @@ const log = bun.Output.scoped(.STR, true); const js_lexer = @import("./js_lexer.zig"); const grapheme = @import("./grapheme.zig"); const JSC = bun.JSC; +const OOM = bun.OOM; pub const Encoding = enum { ascii, @@ -640,7 +641,7 @@ pub const StringOrTinyString = struct { // allocator.free(slice_); } - pub fn initAppendIfNeeded(stringy: string, comptime Appender: type, appendy: Appender) !StringOrTinyString { + pub fn initAppendIfNeeded(stringy: string, comptime Appender: type, appendy: Appender) OOM!StringOrTinyString { if (stringy.len <= StringOrTinyString.Max) { return StringOrTinyString.init(stringy); } @@ -648,7 +649,7 @@ pub const StringOrTinyString = struct { return StringOrTinyString.init(try appendy.append(string, stringy)); } - pub fn initLowerCaseAppendIfNeeded(stringy: string, comptime Appender: type, appendy: Appender) !StringOrTinyString { + pub fn initLowerCaseAppendIfNeeded(stringy: string, comptime Appender: type, appendy: Appender) OOM!StringOrTinyString { if (stringy.len <= StringOrTinyString.Max) { return StringOrTinyString.initLowerCase(stringy); } @@ -831,7 +832,7 @@ pub fn startsWithGeneric(comptime T: type, self: []const T, str: []const T) bool return false; } - return eqlLong(bun.reinterpretSlice(u8, self[0..str.len]), str, false); + return eqlLong(bun.reinterpretSlice(u8, self[0..str.len]), bun.reinterpretSlice(u8, str[0..str.len]), false); } pub inline fn endsWith(self: string, str: string) bool { @@ -1136,6 +1137,19 @@ pub fn hasPrefixCaseInsensitive(str: []const u8, prefix: []const u8) bool { return hasPrefixCaseInsensitiveT(u8, str, prefix); } +pub fn eqlLongT(comptime T: type, a_str: []const T, b_str: []const T, comptime check_len: bool) bool { + if (comptime check_len) { + const len = b_str.len; + if (len == 0) { + return a_str.len == 0; + } + if (a_str.len != len) { + return false; + } + } + return eqlLong(bun.reinterpretSlice(u8, a_str), bun.reinterpretSlice(u8, b_str), false); +} + pub fn eqlLong(a_str: string, b_str: string, comptime check_len: bool) bool { const len = b_str.len; @@ -1559,20 +1573,14 @@ pub fn nonASCIISequenceLength(first_byte: u8) u3 { pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) ?[:0]u16 else ?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { - const trimmed = bun.simdutf.trim.utf8(bytes); - - if (trimmed.len == 0) - break :simd null; - - const out_length = bun.simdutf.length.utf16.from.utf8(trimmed); - + const out_length = bun.simdutf.length.utf16.from.utf8(bytes); if (out_length == 0) break :simd null; var out = try allocator.alloc(u16, out_length + if (sentinel) 1 else 0); log("toUTF16 {d} UTF8 -> {d} UTF16", .{ bytes.len, out_length }); - const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(trimmed, out); + const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(bytes, out); if (res.status == .success) { if (comptime sentinel) { out[out_length] = 0; @@ -1778,104 +1786,6 @@ pub fn toUTF16AllocMaybeBuffered( return .{ output.items, .{0} ** 3, 0 }; } -pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime _: bool) !?[]u16 { - if (strings.firstNonASCII(bytes)) |i| { - const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { - const out_length = bun.simdutf.length.utf16.from.utf8(bytes); - - if (out_length == 0) - break :simd null; - - var out = try allocator.alloc(u16, out_length); - log("toUTF16 {d} UTF8 -> {d} UTF16", .{ bytes.len, out_length }); - - const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(bytes, out); - if (res.status == .success) { - return out; - } - - if (comptime fail_if_invalid) { - allocator.free(out); - return error.InvalidByteSequence; - } - - break :simd .{ - .items = out[0..i], - .capacity = out.len, - .allocator = allocator, - }; - } else null; - var output = output_ orelse fallback: { - var list = try std.ArrayList(u16).initCapacity(allocator, i + 2); - list.items.len = i; - strings.copyU8IntoU16(list.items, bytes[0..i]); - break :fallback list; - }; - errdefer output.deinit(); - - var remaining = bytes[i..]; - - { - const replacement = strings.convertUTF8BytesIntoUTF16(remaining); - if (comptime fail_if_invalid) { - if (replacement.fail) { - if (comptime Environment.allow_assert) assert(replacement.code_point == unicode_replacement); - return error.InvalidByteSequence; - } - } - remaining = remaining[@max(replacement.len, 1)..]; - - //#define U16_LENGTH(c) ((uint32_t)(c)<=0xffff ? 1 : 2) - switch (replacement.code_point) { - 0...0xffff => |c| { - try output.append(@as(u16, @intCast(c))); - }, - else => |c| { - try output.appendSlice(&[_]u16{ strings.u16Lead(c), strings.u16Trail(c) }); - }, - } - } - - while (strings.firstNonASCII(remaining)) |j| { - const end = output.items.len; - try output.ensureUnusedCapacity(j); - output.items.len += j; - strings.copyU8IntoU16(output.items[end..][0..j], remaining[0..j]); - remaining = remaining[j..]; - - const replacement = strings.convertUTF8BytesIntoUTF16(remaining); - if (comptime fail_if_invalid) { - if (replacement.fail) { - if (comptime Environment.allow_assert) assert(replacement.code_point == unicode_replacement); - return error.InvalidByteSequence; - } - } - remaining = remaining[@max(replacement.len, 1)..]; - - //#define U16_LENGTH(c) ((uint32_t)(c)<=0xffff ? 1 : 2) - switch (replacement.code_point) { - 0...0xffff => |c| { - try output.append(@as(u16, @intCast(c))); - }, - else => |c| { - try output.appendSlice(&[_]u16{ strings.u16Lead(c), strings.u16Trail(c) }); - }, - } - } - - if (remaining.len > 0) { - try output.ensureTotalCapacityPrecise(output.items.len + remaining.len); - - output.items.len += remaining.len; - strings.copyU8IntoU16(output.items[output.items.len - remaining.len ..], remaining); - } - - return output.items; - } - - return null; -} - pub fn utf16CodepointWithFFFD(comptime Type: type, input: Type) UTF16Replacement { return utf16CodepointWithFFFDAndFirstInputChar(Type, input[0], input); } @@ -1989,7 +1899,7 @@ pub fn fromWPath(buf: []u8, utf16: []const u16) [:0]const u8 { return buf[0..encode_into_result.written :0]; } -pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { +pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]u16 { if (!std.fs.path.isAbsoluteWindows(utf8)) { return toWPathNormalized(wbuf, utf8); } @@ -2006,19 +1916,35 @@ pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { return wbuf[0 .. toWPathNormalized(wbuf[prefix.len..], utf8).len + prefix.len :0]; } -pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { +pub fn toNTMaxPath(buf: []u8, utf8: []const u8) [:0]const u8 { + if (!std.fs.path.isAbsoluteWindows(utf8) or utf8.len <= 260) { + @memcpy(buf[0..utf8.len], utf8); + buf[utf8.len] = 0; + return buf[0..utf8.len :0]; + } + + const prefix = bun.windows.nt_maxpath_prefix_u8; + buf[0..prefix.len].* = prefix; + return buf[0 .. toPathNormalized(buf[prefix.len..], utf8).len + prefix.len :0]; +} + +pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]u16 { wbuf[0..bun.windows.nt_object_prefix.len].* = bun.windows.nt_object_prefix; @memcpy(wbuf[bun.windows.nt_object_prefix.len..][0..utf16.len], utf16); wbuf[utf16.len + bun.windows.nt_object_prefix.len] = 0; return wbuf[0 .. utf16.len + bun.windows.nt_object_prefix.len :0]; } -pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]const u16 { +pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]u16 { if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_object_prefix)) { @memcpy(wbuf[0..utf16.len], utf16); wbuf[utf16.len] = 0; return wbuf[0..utf16.len :0]; } + if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_maxpath_prefix)) { + // Replace prefix + return addNTPathPrefix(wbuf, utf16[bun.windows.nt_maxpath_prefix.len..]); + } return addNTPathPrefix(wbuf, utf16); } @@ -2039,10 +1965,11 @@ pub fn toWPathNormalizeAutoExtend(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathNormalized(wbuf, utf8); } -pub fn toWPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { - var renormalized: bun.PathBuffer = undefined; +pub fn toWPathNormalized(wbuf: []u16, utf8: []const u8) [:0]u16 { + const renormalized = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(renormalized); - var path_to_use = normalizeSlashesOnly(&renormalized, utf8, '\\'); + var path_to_use = normalizeSlashesOnly(renormalized, utf8, '\\'); // is there a trailing slash? Let's remove it before converting to UTF-16 if (path_to_use.len > 3 and bun.path.isSepAny(path_to_use[path_to_use.len - 1])) { @@ -2051,6 +1978,19 @@ pub fn toWPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPath(wbuf, path_to_use); } +pub fn toPathNormalized(buf: []u8, utf8: []const u8) [:0]const u8 { + const renormalized = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(renormalized); + + var path_to_use = normalizeSlashesOnly(renormalized, utf8, '\\'); + + // is there a trailing slash? Let's remove it before converting to UTF-16 + if (path_to_use.len > 3 and bun.path.isSepAny(path_to_use[path_to_use.len - 1])) { + path_to_use = path_to_use[0 .. path_to_use.len - 1]; + } + + return toPath(buf, path_to_use); +} pub fn normalizeSlashesOnly(buf: []u8, utf8: []const u8, comptime desired_slash: u8) []const u8 { comptime bun.unsafeAssert(desired_slash == '/' or desired_slash == '\\'); @@ -2070,25 +2010,31 @@ pub fn normalizeSlashesOnly(buf: []u8, utf8: []const u8, comptime desired_slash: } pub fn toWDirNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { - var renormalized: bun.PathBuffer = undefined; + var renormalized: ?*bun.PathBuffer = null; + defer if (renormalized) |r| bun.PathBufferPool.put(r); + var path_to_use = utf8; if (bun.strings.containsChar(utf8, '/')) { - @memcpy(renormalized[0..utf8.len], utf8); - for (renormalized[0..utf8.len]) |*c| { + renormalized = bun.PathBufferPool.get(); + @memcpy(renormalized.?[0..utf8.len], utf8); + for (renormalized.?[0..utf8.len]) |*c| { if (c.* == '/') { c.* = '\\'; } } - path_to_use = renormalized[0..utf8.len]; + path_to_use = renormalized.?[0..utf8.len]; } return toWDirPath(wbuf, path_to_use); } -pub fn toWPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { +pub fn toWPath(wbuf: []u16, utf8: []const u8) [:0]u16 { return toWPathMaybeDir(wbuf, utf8, false); } +pub fn toPath(buf: []u8, utf8: []const u8) [:0]u8 { + return toPathMaybeDir(buf, utf8, false); +} pub fn toWDirPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathMaybeDir(wbuf, utf8, true); @@ -2119,7 +2065,7 @@ pub fn assertIsValidWindowsPath(comptime T: type, path: []const T) void { } } -pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash: bool) [:0]const u16 { +pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash: bool) [:0]u16 { bun.unsafeAssert(wbuf.len > 0); var result = bun.simdutf.convert.utf8.to.utf16.with_errors.le( @@ -2136,6 +2082,19 @@ pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash return wbuf[0..result.count :0]; } +pub fn toPathMaybeDir(buf: []u8, utf8: []const u8, comptime add_trailing_lash: bool) [:0]u8 { + bun.unsafeAssert(buf.len > 0); + + var len = utf8.len; + @memcpy(buf[0..len], utf8[0..len]); + + if (add_trailing_lash and len > 0 and buf[len - 1] != '\\') { + buf[len] = '\\'; + len += 1; + } + buf[len] = 0; + return buf[0..len :0]; +} pub fn convertUTF16ToUTF8(list_: std.ArrayList(u8), comptime Type: type, utf16: Type) !std.ArrayList(u8) { var list = list_; @@ -4589,6 +4548,14 @@ pub fn trimLeadingPattern2(slice_: []const u8, comptime byte1: u8, comptime byte return slice; } +/// prefix is of type []const u8 or []const u16 +pub fn trimPrefixComptime(comptime T: type, buffer: []const T, comptime prefix: anytype) []const T { + return if (hasPrefixComptimeType(T, buffer, prefix)) + buffer[prefix.len..] + else + buffer; +} + /// Get the line number and the byte offsets of `line_range_count` above the desired line number /// The final element is the end index of the desired line const LineRange = struct { diff --git a/src/string_mutable.zig b/src/string_mutable.zig index 042184d501..6b3b08aa4c 100644 --- a/src/string_mutable.zig +++ b/src/string_mutable.zig @@ -19,6 +19,10 @@ pub const MutableString = struct { return MutableString.init(allocator, 2048); } + pub fn clone(self: *MutableString) !MutableString { + return MutableString.initCopy(self.allocator, self.list.items); + } + pub const Writer = std.io.Writer(*@This(), OOM, MutableString.writeAll); pub fn writer(self: *MutableString) Writer { return Writer{ @@ -306,18 +310,18 @@ pub const MutableString = struct { const max = 2048; - pub const Writer = std.io.Writer(*BufferedWriter, anyerror, BufferedWriter.writeAll); + pub const Writer = std.io.Writer(*BufferedWriter, OOM, BufferedWriter.writeAll); inline fn remain(this: *BufferedWriter) []u8 { return this.buffer[this.pos..]; } - pub fn flush(this: *BufferedWriter) !void { + pub fn flush(this: *BufferedWriter) OOM!void { _ = try this.context.writeAll(this.buffer[0..this.pos]); this.pos = 0; } - pub fn writeAll(this: *BufferedWriter, bytes: []const u8) anyerror!usize { + pub fn writeAll(this: *BufferedWriter, bytes: []const u8) OOM!usize { const pending = bytes; if (pending.len >= max) { @@ -342,7 +346,7 @@ pub const MutableString = struct { /// Write a E.String to the buffer. /// This automatically encodes UTF-16 into UTF-8 using /// the same code path as TextEncoder - pub fn writeString(this: *BufferedWriter, bytes: *E.String) anyerror!usize { + pub fn writeString(this: *BufferedWriter, bytes: *E.String) OOM!usize { if (bytes.isUTF8()) { return try this.writeAll(bytes.slice(this.context.allocator)); } @@ -353,7 +357,7 @@ pub const MutableString = struct { /// Write a UTF-16 string to the (UTF-8) buffer /// This automatically encodes UTF-16 into UTF-8 using /// the same code path as TextEncoder - pub fn writeAll16(this: *BufferedWriter, bytes: []const u16) anyerror!usize { + pub fn writeAll16(this: *BufferedWriter, bytes: []const u16) OOM!usize { const pending = bytes; if (pending.len >= max) { @@ -385,7 +389,7 @@ pub const MutableString = struct { return pending.len; } - pub fn writeHTMLAttributeValueString(this: *BufferedWriter, str: *E.String) anyerror!void { + pub fn writeHTMLAttributeValueString(this: *BufferedWriter, str: *E.String) OOM!void { if (str.isUTF8()) { try this.writeHTMLAttributeValue(str.slice(this.context.allocator)); return; @@ -394,7 +398,7 @@ pub const MutableString = struct { try this.writeHTMLAttributeValue16(str.slice16()); } - pub fn writeHTMLAttributeValue(this: *BufferedWriter, bytes: []const u8) anyerror!void { + pub fn writeHTMLAttributeValue(this: *BufferedWriter, bytes: []const u8) OOM!void { var items = bytes; while (items.len > 0) { // TODO: SIMD @@ -416,7 +420,7 @@ pub const MutableString = struct { } } - pub fn writeHTMLAttributeValue16(this: *BufferedWriter, bytes: []const u16) anyerror!void { + pub fn writeHTMLAttributeValue16(this: *BufferedWriter, bytes: []const u16) OOM!void { var items = bytes; while (items.len > 0) { if (strings.indexOfAny16(items, "\"<>")) |j| { diff --git a/src/symbols.def b/src/symbols.def index 4c45040d11..38a1355b7d 100644 --- a/src/symbols.def +++ b/src/symbols.def @@ -628,3 +628,4 @@ EXPORTS ?DisposeGlobal@api_internal@v8@@YAXPEA_K@Z ?GetName@Function@v8@@QEBA?AV?$Local@VValue@v8@@@2@XZ ?IsFunction@Value@v8@@QEBA_NXZ + ?FromJustIsNothing@api_internal@v8@@YAXXZ diff --git a/src/symbols.dyn b/src/symbols.dyn index 585ae65d46..6fc7fd75dd 100644 --- a/src/symbols.dyn +++ b/src/symbols.dyn @@ -215,6 +215,7 @@ __ZN2v812api_internal13DisposeGlobalEPm; __ZNK2v88Function7GetNameEv; __ZNK2v85Value10IsFunctionEv; + __ZN2v812api_internal17FromJustIsNothingEv; _uv_os_getpid; _uv_os_getppid; }; diff --git a/src/symbols.txt b/src/symbols.txt index 60166f4ddc..cba22cc90e 100644 --- a/src/symbols.txt +++ b/src/symbols.txt @@ -214,5 +214,6 @@ __ZN2v812api_internal18GlobalizeReferenceEPNS_8internal7IsolateEm __ZN2v812api_internal13DisposeGlobalEPm __ZNK2v88Function7GetNameEv __ZNK2v85Value10IsFunctionEv +__ZN2v812api_internal17FromJustIsNothingEv _uv_os_getpid _uv_os_getppid diff --git a/src/sys.zig b/src/sys.zig index de27a70a0a..e4ad293710 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -130,6 +130,8 @@ pub const O = switch (Environment.os) { pub const TMPFILE = 0o20040000; pub const NDELAY = NONBLOCK; + pub const SYMLINK = bun.C.translated.O_SYMLINK; + pub const toPacked = toPackedO; }, }, @@ -194,7 +196,7 @@ pub const Tag = enum(u8) { link, lseek, lstat, - lutimes, + lutime, mkdir, mkdtemp, fnctl, @@ -211,7 +213,7 @@ pub const Tag = enum(u8) { symlink, symlinkat, unlink, - utimes, + utime, write, getcwd, getenv, @@ -229,6 +231,7 @@ pub const Tag = enum(u8) { pidfd_open, poll, watch, + scandir, kevent, kqueue, @@ -249,11 +252,14 @@ pub const Tag = enum(u8) { pipe, try_write, socketpair, + setsockopt, + statx, uv_spawn, uv_pipe, uv_tty_set_mode, uv_open_osfhandle, + uv_os_homedir, // Below this line are Windows API calls only. @@ -294,10 +300,12 @@ pub const Error = struct { from_libuv: if (Environment.isWindows) bool else void = if (Environment.isWindows) false else undefined, path: []const u8 = "", syscall: Syscall.Tag = Syscall.Tag.TODO, + dest: []const u8 = "", pub fn clone(this: *const Error, allocator: std.mem.Allocator) !Error { var copy = this.*; copy.path = try allocator.dupe(u8, copy.path); + copy.dest = try allocator.dupe(u8, copy.dest); return copy; } @@ -316,7 +324,7 @@ pub const Error = struct { } pub fn format(self: Error, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { - try self.toSystemError().format(fmt, opts, writer); + try self.toShellSystemError().format(fmt, opts, writer); } pub inline fn getErrno(this: Error) E { @@ -352,6 +360,21 @@ pub const Error = struct { }; } + pub inline fn withPathDest(this: Error, path: anytype, dest: anytype) Error { + if (std.meta.Child(@TypeOf(path)) == u16) { + @compileError("Do not pass WString path to withPathDest, it needs the path encoded as utf8 (path)"); + } + if (std.meta.Child(@TypeOf(dest)) == u16) { + @compileError("Do not pass WString path to withPathDest, it needs the path encoded as utf8 (dest)"); + } + return Error{ + .errno = this.errno, + .syscall = this.syscall, + .path = bun.span(path), + .dest = bun.span(dest), + }; + } + pub inline fn withPathLike(this: Error, pathlike: anytype) Error { return switch (pathlike) { .fd => |fd| this.withFd(fd), @@ -387,36 +410,44 @@ pub const Error = struct { return bun.errnoToZigErr(this.errno); } - pub fn toSystemError(this: Error) SystemError { + /// 1. Convert libuv errno values into libc ones. + /// 2. Get the tag name as a string for printing. + pub fn getErrorCodeTagName(err: *const Error) ?struct { [:0]const u8, C.SystemErrno } { + if (!Environment.isWindows) { + if (err.errno > 0 and err.errno < C.SystemErrno.max) { + const system_errno = @as(C.SystemErrno, @enumFromInt(err.errno)); + return .{ @tagName(system_errno), system_errno }; + } + } else { + const system_errno: C.SystemErrno = brk: { + // setRuntimeSafety(false) because we use tagName function, which will be null on invalid enum value. + @setRuntimeSafety(false); + if (err.from_libuv) { + break :brk @enumFromInt(@intFromEnum(bun.windows.libuv.translateUVErrorToE(@as(c_int, err.errno) * -1))); + } + + break :brk @enumFromInt(err.errno); + }; + if (bun.tagName(bun.C.SystemErrno, system_errno)) |errname| { + return .{ errname, system_errno }; + } + } + return null; + } + + /// Simpler formatting which does not allocate a message + pub fn toShellSystemError(this: Error) SystemError { var err = SystemError{ .errno = @as(c_int, this.errno) * -1, .syscall = bun.String.static(@tagName(this.syscall)), }; // errno label - if (!Environment.isWindows) { - if (this.errno > 0 and this.errno < C.SystemErrno.max) { - const system_errno = @as(C.SystemErrno, @enumFromInt(this.errno)); - err.code = bun.String.static(@tagName(system_errno)); - if (C.SystemErrno.labels.get(system_errno)) |label| { - err.message = bun.String.static(label); - } - } - } else { - const system_errno = brk: { - // setRuntimeSafety(false) because we use tagName function, which will be null on invalid enum value. - @setRuntimeSafety(false); - if (this.from_libuv) { - break :brk @as(C.SystemErrno, @enumFromInt(@intFromEnum(bun.windows.libuv.translateUVErrorToE(err.errno)))); - } - - break :brk @as(C.SystemErrno, @enumFromInt(this.errno)); - }; - if (bun.tagName(bun.C.SystemErrno, system_errno)) |errname| { - err.code = bun.String.static(errname); - if (C.SystemErrno.labels.get(system_errno)) |label| { - err.message = bun.String.static(label); - } + if (this.getErrorCodeTagName()) |resolved_errno| { + const code, const system_errno = resolved_errno; + err.code = bun.String.static(code); + if (coreutils_error_map.get(system_errno)) |label| { + err.message = bun.String.static(label); } } @@ -424,6 +455,72 @@ pub const Error = struct { err.path = bun.String.createUTF8(this.path); } + if (this.dest.len > 0) { + err.dest = bun.String.createUTF8(this.dest); + } + + if (this.fd != bun.invalid_fd) { + err.fd = this.fd; + } + + return err; + } + + /// More complex formatting to precisely match the printing that Node.js emits. + /// Use this whenever the error will be sent to JavaScript instead of the shell variant above. + pub fn toSystemError(this: Error) SystemError { + var err = SystemError{ + .errno = -%@as(c_int, this.errno), + .syscall = bun.String.static(@tagName(this.syscall)), + }; + + // errno label + var code: ?[:0]const u8 = null; + var label: ?[]const u8 = null; + if (this.getErrorCodeTagName()) |resolved_errno| { + code, const system_errno = resolved_errno; + err.code = bun.String.static(code.?); + label = libuv_error_map.get(system_errno); + } + + // format taken from Node.js 'exceptions.cc' + // search keyword: `Local UVException(Isolate* isolate,` + var message_buf: [4096]u8 = undefined; + const message = message: { + var stream = std.io.fixedBufferStream(&message_buf); + const writer = stream.writer(); + brk: { + if (code) |c| { + writer.writeAll(c) catch break :brk; + writer.writeAll(": ") catch break :brk; + } + writer.writeAll(label orelse "Unknown Error") catch break :brk; + writer.writeAll(", ") catch break :brk; + writer.writeAll(@tagName(this.syscall)) catch break :brk; + if (this.path.len > 0) { + writer.writeAll(" '") catch break :brk; + writer.writeAll(this.path) catch break :brk; + writer.writeAll("'") catch break :brk; + + if (this.dest.len > 0) { + writer.writeAll(" -> '") catch break :brk; + writer.writeAll(this.dest) catch break :brk; + writer.writeAll("'") catch break :brk; + } + } + } + break :message stream.getWritten(); + }; + err.message = bun.String.createUTF8(message); + + if (this.path.len > 0) { + err.path = bun.String.createUTF8(this.path); + } + + if (this.dest.len > 0) { + err.dest = bun.String.createUTF8(this.dest); + } + if (this.fd != bun.invalid_fd) { err.fd = this.fd; } @@ -464,9 +561,10 @@ pub fn getcwdZ(buf: *bun.PathBuffer) Maybe([:0]const u8) { buf[0] = 0; if (comptime Environment.isWindows) { - var wbuf: bun.WPathBuffer = undefined; - const len: windows.DWORD = kernel32.GetCurrentDirectoryW(wbuf.len, &wbuf); - if (Result.errnoSys(len, .getcwd)) |err| return err; + var wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); + const len: windows.DWORD = kernel32.GetCurrentDirectoryW(wbuf.len, wbuf); + if (Result.errnoSysP(len, .getcwd, buf)) |err| return err; return Result{ .result = bun.strings.fromWPath(buf, wbuf[0..len]) }; } @@ -474,7 +572,7 @@ pub fn getcwdZ(buf: *bun.PathBuffer) Maybe([:0]const u8) { return if (rc != null) Result{ .result = rc.?[0..std.mem.len(rc.?) :0] } else - Result.errnoSys(@as(c_int, 0), .getcwd).?; + Result.errnoSysP(@as(c_int, 0), .getcwd, buf).?; } pub fn fchmod(fd: bun.FileDescriptor, mode: bun.Mode) Maybe(void) { @@ -482,14 +580,14 @@ pub fn fchmod(fd: bun.FileDescriptor, mode: bun.Mode) Maybe(void) { return sys_uv.fchmod(fd, mode); } - return Maybe(void).errnoSys(C.fchmod(fd.cast(), mode), .fchmod) orelse + return Maybe(void).errnoSysFd(C.fchmod(fd.cast(), mode), .fchmod, fd) orelse Maybe(void).success; } pub fn fchmodat(fd: bun.FileDescriptor, path: [:0]const u8, mode: bun.Mode, flags: i32) Maybe(void) { if (comptime Environment.isWindows) @compileError("Use fchmod instead"); - return Maybe(void).errnoSys(C.fchmodat(fd.cast(), path.ptr, mode, flags), .fchmodat) orelse + return Maybe(void).errnoSysFd(C.fchmodat(fd.cast(), path.ptr, mode, flags), .fchmodat, fd) orelse Maybe(void).success; } @@ -502,21 +600,21 @@ pub fn chmod(path: [:0]const u8, mode: bun.Mode) Maybe(void) { Maybe(void).success; } -pub fn chdirOSPath(destination: bun.OSPathSliceZ) Maybe(void) { - assertIsValidWindowsPath(bun.OSPathChar, destination); - +pub fn chdirOSPath(path: bun.stringZ, destination: if (Environment.isPosix) bun.stringZ else bun.string) Maybe(void) { if (comptime Environment.isPosix) { const rc = syscall.chdir(destination); - return Maybe(void).errnoSys(rc, .chdir) orelse Maybe(void).success; + return Maybe(void).errnoSysPD(rc, .chdir, path, destination) orelse Maybe(void).success; } if (comptime Environment.isWindows) { - if (kernel32.SetCurrentDirectory(destination) == windows.FALSE) { - log("SetCurrentDirectory({}) = {d}", .{ bun.fmt.utf16(destination), kernel32.GetLastError() }); - return Maybe(void).errnoSys(0, .chdir) orelse Maybe(void).success; + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); + if (kernel32.SetCurrentDirectory(bun.strings.toWDirPath(wbuf, destination)) == windows.FALSE) { + log("SetCurrentDirectory({s}) = {d}", .{ destination, kernel32.GetLastError() }); + return Maybe(void).errnoSysPD(0, .chdir, path, destination) orelse Maybe(void).success; } - log("SetCurrentDirectory({}) = {d}", .{ bun.fmt.utf16(destination), 0 }); + log("SetCurrentDirectory({s}) = {d}", .{ destination, 0 }); return Maybe(void).success; } @@ -524,12 +622,16 @@ pub fn chdirOSPath(destination: bun.OSPathSliceZ) Maybe(void) { @compileError("Not implemented yet"); } -pub fn chdir(destination: anytype) Maybe(void) { +pub fn chdir(path: anytype, destination: anytype) Maybe(void) { const Type = @TypeOf(destination); if (comptime Environment.isPosix) { if (comptime Type == []u8 or Type == []const u8) { return chdirOSPath( + &(std.posix.toPosixPath(path) catch return .{ .err = .{ + .errno = @intFromEnum(bun.C.SystemErrno.EINVAL), + .syscall = .chdir, + } }), &(std.posix.toPosixPath(destination) catch return .{ .err = .{ .errno = @intFromEnum(bun.C.SystemErrno.EINVAL), .syscall = .chdir, @@ -537,24 +639,23 @@ pub fn chdir(destination: anytype) Maybe(void) { ); } - return chdirOSPath(destination); + return chdirOSPath(path, destination); } if (comptime Environment.isWindows) { if (comptime Type == *[*:0]u16) { if (kernel32.SetCurrentDirectory(destination) != 0) { - return Maybe(void).errnoSys(0, .chdir) orelse Maybe(void).success; + return Maybe(void).errnoSysPD(0, .chdir, path, destination) orelse Maybe(void).success; } return Maybe(void).success; } if (comptime Type == bun.OSPathSliceZ or Type == [:0]u16) { - return chdirOSPath(@as(bun.OSPathSliceZ, destination)); + return chdirOSPath(path, @as(bun.OSPathSliceZ, destination)); } - var wbuf: bun.WPathBuffer = undefined; - return chdirOSPath(bun.strings.toWDirPath(&wbuf, destination)); + return chdirOSPath(path, destination); } return Maybe(void).todo(); @@ -588,7 +689,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { if (comptime Environment.allow_assert) log("stat({s}) = {d}", .{ bun.asByteSlice(path), rc }); - if (Maybe(bun.Stat).errnoSys(rc, .stat)) |err| return err; + if (Maybe(bun.Stat).errnoSysP(rc, .stat, path)) |err| return err; return Maybe(bun.Stat){ .result = stat_ }; } } @@ -597,9 +698,9 @@ pub fn lstat(path: [:0]const u8) Maybe(bun.Stat) { if (Environment.isWindows) { return sys_uv.lstat(path); } else { - var stat_ = mem.zeroes(bun.Stat); - if (Maybe(bun.Stat).errnoSys(C.lstat(path, &stat_), .lstat)) |err| return err; - return Maybe(bun.Stat){ .result = stat_ }; + var stat_buf = mem.zeroes(bun.Stat); + if (Maybe(bun.Stat).errnoSysP(C.lstat(path, &stat_buf), .lstat, path)) |err| return err; + return Maybe(bun.Stat){ .result = stat_buf }; } } @@ -619,13 +720,14 @@ pub fn fstat(fd: bun.FileDescriptor) Maybe(bun.Stat) { if (comptime Environment.allow_assert) log("fstat({}) = {d}", .{ fd, rc }); - if (Maybe(bun.Stat).errnoSys(rc, .fstat)) |err| return err; + if (Maybe(bun.Stat).errnoSysFd(rc, .fstat, fd)) |err| return err; return Maybe(bun.Stat){ .result = stat_ }; } pub fn mkdiratA(dir_fd: bun.FileDescriptor, file_path: []const u8) Maybe(void) { - var buf: bun.WPathBuffer = undefined; - return mkdiratW(dir_fd, bun.strings.toWPathNormalized(&buf, file_path)); + const buf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(buf); + return mkdiratW(dir_fd, bun.strings.toWPathNormalized(buf, file_path)); } pub fn mkdiratZ(dir_fd: bun.FileDescriptor, file_path: [*:0]const u8, mode: mode_t) Maybe(void) { @@ -654,7 +756,6 @@ pub fn mkdiratW(dir_fd: bun.FileDescriptor, file_path: []const u16, _: i32) Mayb if (dir_to_make == .err) { return .{ .err = dir_to_make.err }; } - _ = close(dir_to_make.result); return .{ .result = {} }; } @@ -671,7 +772,7 @@ pub fn fstatat(fd: bun.FileDescriptor, path: [:0]const u8) Maybe(bun.Stat) { }; } var stat_ = mem.zeroes(bun.Stat); - if (Maybe(bun.Stat).errnoSys(syscall.fstatat(fd.int(), path, &stat_, 0), .fstatat)) |err| { + if (Maybe(bun.Stat).errnoSysFP(syscall.fstatat(fd.int(), path, &stat_, 0), .fstatat, fd, path)) |err| { log("fstatat({}, {s}) = {s}", .{ fd, path, @tagName(err.getErrno()) }); return err; } @@ -686,9 +787,10 @@ pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { .linux => Maybe(void).errnoSysP(syscall.mkdir(file_path, flags), .mkdir, file_path) orelse Maybe(void).success, .windows => { - var wbuf: bun.WPathBuffer = undefined; + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); return Maybe(void).errnoSysP( - kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null), + kernel32.CreateDirectoryW(bun.strings.toWPath(wbuf, file_path).ptr, null), .mkdir, file_path, ) orelse Maybe(void).success; @@ -718,8 +820,9 @@ pub fn mkdirA(file_path: []const u8, flags: bun.Mode) Maybe(void) { } if (comptime Environment.isWindows) { - var wbuf: bun.WPathBuffer = undefined; - const wpath = bun.strings.toWPath(&wbuf, file_path); + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); + const wpath = bun.strings.toWPath(wbuf, file_path); assertIsValidWindowsPath(u16, wpath); return Maybe(void).errnoSysP( kernel32.CreateDirectoryW(wpath.ptr, null), @@ -751,9 +854,16 @@ pub fn mkdirOSPath(file_path: bun.OSPathSliceZ, flags: bun.Mode) Maybe(void) { const fnctl_int = if (Environment.isLinux) usize else c_int; pub fn fcntl(fd: bun.FileDescriptor, cmd: i32, arg: fnctl_int) Maybe(fnctl_int) { - const result = fcntl_symbol(fd.cast(), cmd, arg); - if (Maybe(fnctl_int).errnoSys(result, .fcntl)) |err| return err; - return .{ .result = @intCast(result) }; + while (true) { + const result = fcntl_symbol(fd.cast(), cmd, arg); + if (Maybe(fnctl_int).errnoSysFd(result, .fcntl, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + return .{ .result = @intCast(result) }; + } + + unreachable; } pub fn getErrno(rc: anytype) bun.C.E { @@ -783,19 +893,29 @@ pub fn normalizePathWindows( if (comptime T != u8 and T != u16) { @compileError("normalizePathWindows only supports u8 and u16 character types"); } - var wbuf: if (T == u16) void else bun.WPathBuffer = undefined; - var path = if (T == u16) path_ else bun.strings.convertUTF8toUTF16InBuffer(&wbuf, path_); + const wbuf = if (T != u16) bun.WPathBufferPool.get(); + defer if (T != u16) bun.WPathBufferPool.put(wbuf); + var path = if (T == u16) path_ else bun.strings.convertUTF8toUTF16InBuffer(wbuf, path_); if (std.fs.path.isAbsoluteWindowsWTF16(path)) { - // handle the special "nul" device - // we technically should handle the other DOS devices too. - if (path_.len >= "\\nul".len and - (bun.strings.eqlComptimeT(T, path_[path_.len - "\\nul".len ..], "\\nul") or - bun.strings.eqlComptimeT(T, path_[path_.len - "\\NUL".len ..], "\\NUL"))) - { - @memcpy(buf[0..bun.strings.w("\\??\\NUL").len], bun.strings.w("\\??\\NUL")); - buf[bun.strings.w("\\??\\NUL").len] = 0; - return .{ .result = buf[0..bun.strings.w("\\??\\NUL").len :0] }; + if (path_.len >= 4) { + if ((bun.strings.eqlComptimeT(T, path_[path_.len - "\\nul".len ..], "\\nul") or + bun.strings.eqlComptimeT(T, path_[path_.len - "\\NUL".len ..], "\\NUL"))) + { + @memcpy(buf[0..bun.strings.w("\\??\\NUL").len], bun.strings.w("\\??\\NUL")); + buf[bun.strings.w("\\??\\NUL").len] = 0; + return .{ .result = buf[0..bun.strings.w("\\??\\NUL").len :0] }; + } + if ((path[1] == '/' or path[1] == '\\') and + (path[2] == '.' or path[2] == '?') and + (path[3] == '/' or path[3] == '\\')) + { + buf[0..4].* = .{ '\\', '\\', path[2], '\\' }; + const rest = path[4..]; + @memcpy(buf[4..][0..rest.len], rest); + buf[path.len] = 0; + return .{ .result = buf[0..path.len :0] }; + } } const norm = bun.path.normalizeStringGenericTZ(u16, path, buf, .{ .add_nt_prefix = true, .zero_terminate = true }); @@ -836,7 +956,8 @@ pub fn normalizePathWindows( path = path[2..]; } - var buf1: bun.WPathBuffer = undefined; + const buf1 = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(buf1); @memcpy(buf1[0..base_path.len], base_path); buf1[base_path.len] = '\\'; @memcpy(buf1[base_path.len + 1 .. base_path.len + 1 + path.len], path); @@ -848,7 +969,7 @@ pub fn normalizePathWindows( fn openDirAtWindowsNtPath( dirFd: bun.FileDescriptor, - path: []const u16, + path: [:0]const u16, options: WindowsOpenDirOptions, ) Maybe(bun.FileDescriptor) { const iterable = options.iterable; @@ -862,6 +983,17 @@ fn openDirAtWindowsNtPath( const rename_flag: u32 = if (can_rename_or_delete) w.DELETE else 0; const read_only_flag: u32 = if (read_only) 0 else w.FILE_ADD_FILE | w.FILE_ADD_SUBDIRECTORY; const flags: u32 = iterable_flag | base_flags | rename_flag | read_only_flag; + const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; + + // NtCreateFile seems to not function on device paths. + // Since it is absolute, it can just use CreateFileW + if (bun.strings.hasPrefixComptimeUTF16(path, "\\\\.\\")) + return openWindowsDevicePath( + path, + flags, + if (options.create) w.FILE_OPEN_IF else w.FILE_OPEN, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + ); const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ @@ -882,7 +1014,6 @@ fn openDirAtWindowsNtPath( .SecurityDescriptor = null, .SecurityQualityOfService = null, }; - const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; var fd: w.HANDLE = w.INVALID_HANDLE_VALUE; var io: w.IO_STATUS_BLOCK = undefined; @@ -941,6 +1072,33 @@ fn openDirAtWindowsNtPath( } } +fn openWindowsDevicePath( + path: [:0]const u16, + dwDesiredAccess: u32, + dwCreationDisposition: u32, + dwFlagsAndAttributes: u32, +) Maybe(bun.FileDescriptor) { + const rc = std.os.windows.kernel32.CreateFileW( + path, + dwDesiredAccess, + FILE_SHARE, + null, + dwCreationDisposition, + dwFlagsAndAttributes, + null, + ); + if (rc == w.INVALID_HANDLE_VALUE) { + return .{ .err = .{ + .errno = if (windows.Win32Error.get().toSystemErrno()) |e| + @intFromEnum(e) + else + @intFromEnum(bun.C.E.UNKNOWN), + .syscall = .open, + } }; + } + return .{ .result = bun.toFD(rc) }; +} + pub const WindowsOpenDirOptions = packed struct { iterable: bool = false, no_follow: bool = false, @@ -955,9 +1113,10 @@ fn openDirAtWindowsT( path: []const T, options: WindowsOpenDirOptions, ) Maybe(bun.FileDescriptor) { - var wbuf: bun.WPathBuffer = undefined; + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); - const norm = switch (normalizePathWindows(T, dirFd, path, &wbuf)) { + const norm = switch (normalizePathWindows(T, dirFd, path, wbuf)) { .err => |err| return .{ .err = err }, .result => |norm| norm, }; @@ -1148,9 +1307,10 @@ pub fn openFileAtWindowsT( disposition: w.ULONG, options: w.ULONG, ) Maybe(bun.FileDescriptor) { - var wbuf: bun.WPathBuffer = undefined; + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); - const norm = switch (normalizePathWindows(T, dirFd, path, &wbuf)) { + const norm = switch (normalizePathWindows(T, dirFd, path, wbuf)) { .err => |err| return .{ .err = err }, .result => |norm| norm, }; @@ -1262,7 +1422,7 @@ pub fn openatOSPath(dirfd: bun.FileDescriptor, file_path: bun.OSPathSliceZ, flag if (comptime Environment.allow_assert) log("openat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(file_path, 0), rc }); - return Maybe(bun.FileDescriptor).errnoSys(rc, .open) orelse .{ .result = bun.toFD(rc) }; + return Maybe(bun.FileDescriptor).errnoSysFP(rc, .open, dirfd, file_path) orelse .{ .result = bun.toFD(rc) }; } else if (comptime Environment.isWindows) { return openatWindowsT(bun.OSPathChar, dirfd, file_path, flags); } @@ -1604,7 +1764,7 @@ pub fn pread(fd: bun.FileDescriptor, buf: []u8, offset: i64) Maybe(usize) { const ioffset = @as(i64, @bitCast(offset)); // the OS treats this as unsigned while (true) { const rc = pread_sym(fd.cast(), buf.ptr, adjusted_len, ioffset); - if (Maybe(usize).errnoSys(rc, .pread)) |err| { + if (Maybe(usize).errnoSysFd(rc, .pread, fd)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1716,7 +1876,7 @@ pub fn recv(fd: bun.FileDescriptor, buf: []u8, flag: u32) Maybe(usize) { if (comptime Environment.isMac) { const rc = syscall.@"recvfrom$NOCANCEL"(fd.cast(), buf.ptr, adjusted_len, flag, null, null); - if (Maybe(usize).errnoSys(rc, .recv)) |err| { + if (Maybe(usize).errnoSysFd(rc, .recv, fd)) |err| { log("recv({}, {d}) = {s} {}", .{ fd, adjusted_len, err.err.name(), debug_timer }); return err; } @@ -1747,7 +1907,7 @@ pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { if (comptime Environment.isMac) { const rc = syscall.@"sendto$NOCANCEL"(fd.cast(), buf.ptr, buf.len, flag, null, 0); - if (Maybe(usize).errnoSys(rc, .send)) |err| { + if (Maybe(usize).errnoSysFd(rc, .send, fd)) |err| { syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); return err; } @@ -1759,7 +1919,7 @@ pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { while (true) { const rc = linux.sendto(fd.cast(), buf.ptr, buf.len, flag, null, 0); - if (Maybe(usize).errnoSys(rc, .send)) |err| { + if (Maybe(usize).errnoSysFd(rc, .send, fd)) |err| { if (err.getErrno() == .INTR) continue; syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); return err; @@ -1774,7 +1934,7 @@ pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { pub fn lseek(fd: bun.FileDescriptor, offset: i64, whence: usize) Maybe(usize) { while (true) { const rc = syscall.lseek(fd.cast(), offset, whence); - if (Maybe(usize).errnoSys(rc, .lseek)) |err| { + if (Maybe(usize).errnoSysFd(rc, .lseek, fd)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1791,7 +1951,7 @@ pub fn readlink(in: [:0]const u8, buf: []u8) Maybe([:0]u8) { while (true) { const rc = syscall.readlink(in, buf.ptr, buf.len); - if (Maybe([:0]u8).errnoSys(rc, .readlink)) |err| { + if (Maybe([:0]u8).errnoSysP(rc, .readlink, in)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1804,7 +1964,7 @@ pub fn readlinkat(fd: bun.FileDescriptor, in: [:0]const u8, buf: []u8) Maybe([:0 while (true) { const rc = syscall.readlinkat(fd.cast(), in, buf.ptr, buf.len); - if (Maybe([:0]u8).errnoSys(rc, .readlink)) |err| { + if (Maybe([:0]u8).errnoSysFP(rc, .readlink, fd, in)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1816,14 +1976,14 @@ pub fn readlinkat(fd: bun.FileDescriptor, in: [:0]const u8, buf: []u8) Maybe([:0 pub fn ftruncate(fd: bun.FileDescriptor, size: isize) Maybe(void) { if (comptime Environment.isWindows) { if (kernel32.SetFileValidData(fd.cast(), size) == 0) { - return Maybe(void).errnoSys(0, .ftruncate) orelse Maybe(void).success; + return Maybe(void).errnoSysFd(0, .ftruncate, fd) orelse Maybe(void).success; } return Maybe(void).success; } return while (true) { - if (Maybe(void).errnoSys(syscall.ftruncate(fd.cast(), size), .ftruncate)) |err| { + if (Maybe(void).errnoSysFd(syscall.ftruncate(fd.cast(), size), .ftruncate, fd)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1965,14 +2125,18 @@ pub fn renameat2(from_dir: bun.FileDescriptor, from: [:0]const u8, to_dir: bun.F pub fn renameat(from_dir: bun.FileDescriptor, from: [:0]const u8, to_dir: bun.FileDescriptor, to: [:0]const u8) Maybe(void) { if (Environment.isWindows) { - var w_buf_from: bun.WPathBuffer = undefined; - var w_buf_to: bun.WPathBuffer = undefined; + const w_buf_from = bun.WPathBufferPool.get(); + const w_buf_to = bun.WPathBufferPool.get(); + defer { + bun.WPathBufferPool.put(w_buf_from); + bun.WPathBufferPool.put(w_buf_to); + } const rc = bun.C.renameAtW( from_dir, - bun.strings.toNTPath(&w_buf_from, from), + bun.strings.toNTPath(w_buf_from, from), to_dir, - bun.strings.toNTPath(&w_buf_to, to), + bun.strings.toNTPath(w_buf_to, to), true, ); @@ -1993,7 +2157,7 @@ pub fn renameat(from_dir: bun.FileDescriptor, from: [:0]const u8, to_dir: bun.Fi pub fn chown(path: [:0]const u8, uid: posix.uid_t, gid: posix.gid_t) Maybe(void) { while (true) { - if (Maybe(void).errnoSys(C.chown(path, uid, gid), .chown)) |err| { + if (Maybe(void).errnoSysP(C.chown(path, uid, gid), .chown, path)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -2044,10 +2208,14 @@ pub fn symlinkOrJunction(dest: [:0]const u8, target: [:0]const u8) Maybe(void) { if (comptime !Environment.isWindows) @compileError("symlinkOrJunction is windows only"); if (!WindowsSymlinkOptions.has_failed_to_create_symlink) { - var sym16: bun.WPathBuffer = undefined; - var target16: bun.WPathBuffer = undefined; - const sym_path = bun.strings.toWPathNormalizeAutoExtend(&sym16, dest); - const target_path = bun.strings.toWPathNormalizeAutoExtend(&target16, target); + const sym16 = bun.WPathBufferPool.get(); + const target16 = bun.WPathBufferPool.get(); + defer { + bun.WPathBufferPool.put(sym16); + bun.WPathBufferPool.put(target16); + } + const sym_path = bun.strings.toWPathNormalizeAutoExtend(sym16, dest); + const target_path = bun.strings.toWPathNormalizeAutoExtend(target16, target); switch (symlinkW(sym_path, target_path, .{ .directory = true })) { .result => { return Maybe(void).success; @@ -2154,12 +2322,13 @@ pub fn unlinkW(from: [:0]const u16) Maybe(void) { pub fn unlink(from: [:0]const u8) Maybe(void) { if (comptime Environment.isWindows) { - var w_buf: bun.WPathBuffer = undefined; - return unlinkW(bun.strings.toNTPath(&w_buf, from)); + const w_buf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(w_buf); + return unlinkW(bun.strings.toNTPath(w_buf, from)); } while (true) { - if (Maybe(void).errnoSys(syscall.unlink(from), .unlink)) |err| { + if (Maybe(void).errnoSysP(syscall.unlink(from), .unlink, from)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -2176,8 +2345,9 @@ pub fn rmdirat(dirfd: bun.FileDescriptor, to: anytype) Maybe(void) { pub fn unlinkatWithFlags(dirfd: bun.FileDescriptor, to: anytype, flags: c_uint) Maybe(void) { if (Environment.isWindows) { if (comptime std.meta.Elem(@TypeOf(to)) == u8) { - var w_buf: bun.WPathBuffer = undefined; - return unlinkatWithFlags(dirfd, bun.strings.toNTPath(&w_buf, bun.span(to)), flags); + const w_buf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(w_buf); + return unlinkatWithFlags(dirfd, bun.strings.toNTPath(w_buf, bun.span(to)), flags); } return bun.windows.DeleteFileBun(to, .{ @@ -2187,7 +2357,7 @@ pub fn unlinkatWithFlags(dirfd: bun.FileDescriptor, to: anytype, flags: c_uint) } while (true) { - if (Maybe(void).errnoSys(syscall.unlinkat(dirfd.cast(), to, flags), .unlink)) |err| { + if (Maybe(void).errnoSysFP(syscall.unlinkat(dirfd.cast(), to, flags), .unlink, dirfd, to)) |err| { if (err.getErrno() == .INTR) continue; if (comptime Environment.allow_assert) log("unlinkat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(to, 0), @intFromEnum(err.getErrno()) }); @@ -2205,7 +2375,7 @@ pub fn unlinkat(dirfd: bun.FileDescriptor, to: anytype) Maybe(void) { return unlinkatWithFlags(dirfd, to, 0); } while (true) { - if (Maybe(void).errnoSys(syscall.unlinkat(dirfd.cast(), to, 0), .unlink)) |err| { + if (Maybe(void).errnoSysFP(syscall.unlinkat(dirfd.cast(), to, 0), .unlink, dirfd, to)) |err| { if (err.getErrno() == .INTR) continue; if (comptime Environment.allow_assert) log("unlinkat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(to, 0), @intFromEnum(err.getErrno()) }); @@ -2308,6 +2478,132 @@ pub fn mmapFile(path: [:0]const u8, flags: std.c.MAP, wanted_size: ?usize, offse return .{ .result = map }; } +pub fn setCloseOnExec(fd: bun.FileDescriptor) Maybe(void) { + switch (fcntl(fd, std.posix.F.GETFD, 0)) { + .result => |fl| { + switch (fcntl(fd, std.posix.F.SETFD, fl | std.posix.FD_CLOEXEC)) { + .result => {}, + .err => |err| return .{ .err = err }, + } + }, + .err => |err| return .{ .err = err }, + } + + return .{ .result = {} }; +} + +pub fn setsockopt(fd: bun.FileDescriptor, level: c_int, optname: u32, value: i32) Maybe(i32) { + while (true) { + const rc = syscall.setsockopt(fd.cast(), level, optname, &value, @sizeOf(i32)); + if (Maybe(i32).errnoSysFd(rc, .setsockopt, fd)) |err| { + if (err.getErrno() == .INTR) continue; + log("setsockopt() = {d} {s}", .{ err.err.errno, err.err.name() }); + return err; + } + log("setsockopt({d}, {d}, {d}) = {d}", .{ fd.cast(), level, optname, rc }); + return .{ .result = @intCast(rc) }; + } + + unreachable; +} + +pub fn setNoSigpipe(fd: bun.FileDescriptor) Maybe(void) { + if (comptime Environment.isMac) { + return switch (setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.NOSIGPIPE, 1)) { + .result => .{ .result = {} }, + .err => |err| .{ .err = err }, + }; + } + + return .{ .result = {} }; +} + +const socketpair_t = if (Environment.isLinux) i32 else c_uint; + +/// libc socketpair() except it defaults to: +/// - SOCK_CLOEXEC on Linux +/// - SO_NOSIGPIPE on macOS +/// +/// On POSIX it otherwise makes it do O_CLOEXEC. +pub fn socketpair(domain: socketpair_t, socktype: socketpair_t, protocol: socketpair_t, nonblocking_status: enum { blocking, nonblocking }) Maybe([2]bun.FileDescriptor) { + if (comptime !Environment.isPosix) @compileError("linux only!"); + + var fds_i: [2]syscall.fd_t = .{ 0, 0 }; + + if (comptime Environment.isLinux) { + while (true) { + const nonblock_flag: i32 = if (nonblocking_status == .nonblocking) linux.SOCK.NONBLOCK else 0; + const rc = std.os.linux.socketpair(domain, socktype | linux.SOCK.CLOEXEC | nonblock_flag, protocol, &fds_i); + if (Maybe([2]bun.FileDescriptor).errnoSys(rc, .socketpair)) |err| { + if (err.getErrno() == .INTR) continue; + + log("socketpair() = {d} {s}", .{ err.err.errno, err.err.name() }); + return err; + } + + break; + } + } else { + while (true) { + const err = libc.socketpair(domain, socktype, protocol, &fds_i); + + if (Maybe([2]bun.FileDescriptor).errnoSys(err, .socketpair)) |err2| { + if (err2.getErrno() == .INTR) continue; + log("socketpair() = {d} {s}", .{ err2.err.errno, err2.err.name() }); + return err2; + } + + break; + } + + const err: ?Syscall.Error = err: { + + // Set O_CLOEXEC first. + inline for (0..2) |i| { + switch (setCloseOnExec(bun.toFD(fds_i[i]))) { + .err => |err| break :err err, + .result => {}, + } + } + + if (comptime Environment.isMac) { + inline for (0..2) |i| { + switch (setNoSigpipe(bun.toFD(fds_i[i]))) { + .err => |err| break :err err, + else => {}, + } + } + } + + if (nonblocking_status == .nonblocking) { + inline for (0..2) |i| { + switch (setNonblocking(bun.toFD(fds_i[i]))) { + .err => |err| break :err err, + .result => {}, + } + } + } + + break :err null; + }; + + // On any error after socketpair(), we need to close it. + if (err) |errr| { + inline for (0..2) |i| { + _ = close(bun.toFD(fds_i[i])); + } + + log("socketpair() = {d} {s}", .{ errr.errno, errr.name() }); + + return .{ .err = errr }; + } + } + + log("socketpair() = [{d} {d}]", .{ fds_i[0], fds_i[1] }); + + return Maybe([2]bun.FileDescriptor){ .result = .{ bun.toFD(fds_i[0]), bun.toFD(fds_i[1]) } }; +} + pub fn munmap(memory: []align(mem.page_size) const u8) Maybe(void) { if (Maybe(void).errnoSys(syscall.munmap(memory.ptr, memory.len), .munmap)) |err| { return err; @@ -2345,12 +2641,12 @@ pub fn setPipeCapacityOnLinux(fd: bun.FileDescriptor, capacity: usize) Maybe(usi // We don't use glibc here // It didn't work. Always returned 0. const pipe_len = std.os.linux.fcntl(fd.cast(), F_GETPIPE_SZ, 0); - if (Maybe(usize).errnoSys(pipe_len, .fcntl)) |err| return err; + if (Maybe(usize).errnoSysFd(pipe_len, .fcntl, fd)) |err| return err; if (pipe_len == 0) return Maybe(usize){ .result = 0 }; if (pipe_len >= capacity) return Maybe(usize){ .result = pipe_len }; const new_pipe_len = std.os.linux.fcntl(fd.cast(), F_SETPIPE_SZ, capacity); - if (Maybe(usize).errnoSys(new_pipe_len, .fcntl)) |err| return err; + if (Maybe(usize).errnoSysFd(new_pipe_len, .fcntl, fd)) |err| return err; return Maybe(usize){ .result = new_pipe_len }; } @@ -2464,8 +2760,9 @@ pub fn getFileAttributes(path: anytype) ?WindowsFileAttributes { const attributes: WindowsFileAttributes = @bitCast(dword); return attributes; } else { - var wbuf: bun.WPathBuffer = undefined; - const path_to_use = bun.strings.toWPath(&wbuf, path); + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); + const path_to_use = bun.strings.toWPath(wbuf, path); return getFileAttributes(path_to_use); } } @@ -2540,11 +2837,17 @@ pub fn faccessat(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { return JSC.Maybe(bool){ .result = false }; } -pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { - const dir_fd = bun.toFD(dir_); +pub fn directoryExistsAt(dir: anytype, subpath: anytype) JSC.Maybe(bool) { + const dir_fd = bun.toFD(dir); if (comptime Environment.isWindows) { - var wbuf: bun.WPathBuffer = undefined; - const path = bun.strings.toNTPath(&wbuf, subpath); + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); + const path = if (std.meta.Child(@TypeOf(subpath)) == u16) + bun.strings.addNTPathPrefixIfNeeded(wbuf, subpath) + else + bun.strings.toNTPath(wbuf, subpath); + bun.path.dangerouslyConvertPathToWindowsInPlace(u16, path); + const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ .Length = path_len_bytes, @@ -2566,8 +2869,11 @@ pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { }; var basic_info: w.FILE_BASIC_INFORMATION = undefined; const rc = kernel32.NtQueryAttributesFile(&attr, &basic_info); - if (JSC.Maybe(bool).errnoSysP(rc, .access, subpath)) |err| { - syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), err }); + if (rc == .OBJECT_NAME_INVALID) { + bun.Output.warn("internal error: invalid object name: {}", .{bun.fmt.fmtOSPath(path, .{})}); + } + if (JSC.Maybe(bool).errnoSys(rc, .access)) |err| { + syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {} {d}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), err, rc }); return err; } @@ -2576,12 +2882,40 @@ pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_READONLY == 0; syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {d}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), @intFromBool(is_dir) }); - return .{ - .result = is_dir, - }; + return .{ .result = is_dir }; } - return faccessat(dir_fd, subpath); + // TODO: use statx to query less information. this path is currently broken + // const have_statx = Environment.isLinux; + // if (have_statx) brk: { + // var statx: std.os.linux.Statx = undefined; + // if (Maybe(bool).errnoSys(bun.C.linux.statx( + // dir_fd.cast(), + // subpath, + // // Don't follow symlinks, don't automount, minimize permissions needed + // std.os.linux.AT.SYMLINK_NOFOLLOW | std.os.linux.AT.NO_AUTOMOUNT, + // // We only need the file type to check if it's a directory + // std.os.linux.STATX_TYPE, + // &statx, + // ), .statx)) |err| { + // switch (err.err.getErrno()) { + // .OPNOTSUPP, .NOSYS => break :brk, // Linux < 4.11 + // // truly doesn't exist. + // .NOENT => return .{ .result = false }, + // else => return err, + // } + // return err; + // } + // return .{ .result = S.ISDIR(statx.mode) }; + // } + + return switch (fstatat(dir_fd, subpath)) { + .err => |err| switch (err.getErrno()) { + .NOENT => .{ .result = false }, + else => .{ .err = err }, + }, + .result => |result| .{ .result = S.ISDIR(result.mode) }, + }; } pub fn setNonblocking(fd: bun.FileDescriptor) Maybe(void) { @@ -2610,8 +2944,9 @@ pub fn existsAt(fd: bun.FileDescriptor, subpath: [:0]const u8) bool { } if (comptime Environment.isWindows) { - var wbuf: bun.WPathBuffer = undefined; - const path = bun.strings.toNTPath(&wbuf, subpath); + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); + const path = bun.strings.toNTPath(wbuf, subpath); const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ .Length = path_len_bytes, @@ -2737,7 +3072,7 @@ pub fn setFileOffset(fd: bun.FileDescriptor, offset: usize) Maybe(void) { windows.FILE_BEGIN, ); if (rc == windows.FALSE) { - return Maybe(void).errnoSys(0, .lseek) orelse Maybe(void).success; + return Maybe(void).errnoSysFd(0, .lseek, fd) orelse Maybe(void).success; } return Maybe(void).success; } @@ -2748,7 +3083,7 @@ pub fn setFileOffsetToEndWindows(fd: bun.FileDescriptor) Maybe(usize) { var new_ptr: std.os.windows.LARGE_INTEGER = undefined; const rc = kernel32.SetFilePointerEx(fd.cast(), 0, &new_ptr, windows.FILE_END); if (rc == windows.FALSE) { - return Maybe(usize).errnoSys(0, .lseek) orelse Maybe(usize){ .result = 0 }; + return Maybe(usize).errnoSysFd(0, .lseek, fd) orelse Maybe(usize){ .result = 0 }; } return Maybe(usize){ .result = @intCast(new_ptr) }; } @@ -2767,10 +3102,7 @@ pub fn pipe() Maybe([2]bun.FileDescriptor) { var fds: [2]i32 = undefined; const rc = syscall.pipe(&fds); - if (Maybe([2]bun.FileDescriptor).errnoSys( - rc, - .pipe, - )) |err| { + if (Maybe([2]bun.FileDescriptor).errnoSys(rc, .pipe)) |err| { return err; } log("pipe() = [{d}, {d}]", .{ fds[0], fds[1] }); @@ -3277,15 +3609,19 @@ pub const File = struct { return .{ .result = buf[0..read_amount] }; } - pub fn readToEndWithArrayList(this: File, list: *std.ArrayList(u8)) Maybe(usize) { - const size = switch (this.getEndPos()) { - .err => |err| { - return .{ .err = err }; - }, - .result => |s| s, - }; - - list.ensureTotalCapacityPrecise(size + 16) catch bun.outOfMemory(); + pub fn readToEndWithArrayList(this: File, list: *std.ArrayList(u8), probably_small: bool) Maybe(usize) { + if (probably_small) { + list.ensureUnusedCapacity(64) catch bun.outOfMemory(); + } else { + list.ensureTotalCapacityPrecise( + switch (this.getEndPos()) { + .err => |err| { + return .{ .err = err }; + }, + .result => |s| s, + } + 16, + ) catch bun.outOfMemory(); + } var total: i64 = 0; while (true) { @@ -3313,9 +3649,22 @@ pub const File = struct { return .{ .result = @intCast(total) }; } + + /// Use this function on potentially large files. + /// Calls fstat() on the file to get the size of the file and avoids reallocations + extra read() calls. pub fn readToEnd(this: File, allocator: std.mem.Allocator) ReadToEndResult { var list = std.ArrayList(u8).init(allocator); - return switch (readToEndWithArrayList(this, &list)) { + return switch (readToEndWithArrayList(this, &list, false)) { + .err => |err| .{ .err = err, .bytes = list }, + .result => .{ .err = null, .bytes = list }, + }; + } + + /// Use this function on small files <= 1024 bytes. + /// This will skip the fstat() call, preallocating 64 bytes instead of the file's size. + pub fn readToEndSmall(this: File, allocator: std.mem.Allocator) ReadToEndResult { + var list = std.ArrayList(u8).init(allocator); + return switch (readToEndWithArrayList(this, &list, true)) { .err => |err| .{ .err = err, .bytes = list }, .result => .{ .err = null, .bytes = list }, }; @@ -3436,3 +3785,372 @@ pub inline fn toLibUVOwnedFD( pub const Dir = @import("./dir.zig"); const FILE_SHARE = w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE; + +/// This map is derived off of uv.h's definitions, and is what Node.js uses in printing errors. +pub const libuv_error_map = brk: { + const entries: []const struct { [:0]const u8, [:0]const u8 } = &.{ + .{ "E2BIG", "argument list too long" }, + .{ "EACCES", "permission denied" }, + .{ "EADDRINUSE", "address already in use" }, + .{ "EADDRNOTAVAIL", "address not available" }, + .{ "EAFNOSUPPORT", "address family not supported" }, + .{ "EAGAIN", "resource temporarily unavailable" }, + .{ "EAI_ADDRFAMILY", "address family not supported" }, + .{ "EAI_AGAIN", "temporary failure" }, + .{ "EAI_BADFLAGS", "bad ai_flags value" }, + .{ "EAI_BADHINTS", "invalid value for hints" }, + .{ "EAI_CANCELED", "request canceled" }, + .{ "EAI_FAIL", "permanent failure" }, + .{ "EAI_FAMILY", "ai_family not supported" }, + .{ "EAI_MEMORY", "out of memory" }, + .{ "EAI_NODATA", "no address" }, + .{ "EAI_NONAME", "unknown node or service" }, + .{ "EAI_OVERFLOW", "argument buffer overflow" }, + .{ "EAI_PROTOCOL", "resolved protocol is unknown" }, + .{ "EAI_SERVICE", "service not available for socket type" }, + .{ "EAI_SOCKTYPE", "socket type not supported" }, + .{ "EALREADY", "connection already in progress" }, + .{ "EBADF", "bad file descriptor" }, + .{ "EBUSY", "resource busy or locked" }, + .{ "ECANCELED", "operation canceled" }, + .{ "ECHARSET", "invalid Unicode character" }, + .{ "ECONNABORTED", "software caused connection abort" }, + .{ "ECONNREFUSED", "connection refused" }, + .{ "ECONNRESET", "connection reset by peer" }, + .{ "EDESTADDRREQ", "destination address required" }, + .{ "EEXIST", "file already exists" }, + .{ "EFAULT", "bad address in system call argument" }, + .{ "EFBIG", "file too large" }, + .{ "EHOSTUNREACH", "host is unreachable" }, + .{ "EINTR", "interrupted system call" }, + .{ "EINVAL", "invalid argument" }, + .{ "EIO", "i/o error" }, + .{ "EISCONN", "socket is already connected" }, + .{ "EISDIR", "illegal operation on a directory" }, + .{ "ELOOP", "too many symbolic links encountered" }, + .{ "EMFILE", "too many open files" }, + .{ "EMSGSIZE", "message too long" }, + .{ "ENAMETOOLONG", "name too long" }, + .{ "ENETDOWN", "network is down" }, + .{ "ENETUNREACH", "network is unreachable" }, + .{ "ENFILE", "file table overflow" }, + .{ "ENOBUFS", "no buffer space available" }, + .{ "ENODEV", "no such device" }, + .{ "ENOENT", "no such file or directory" }, + .{ "ENOMEM", "not enough memory" }, + .{ "ENONET", "machine is not on the network" }, + .{ "ENOPROTOOPT", "protocol not available" }, + .{ "ENOSPC", "no space left on device" }, + .{ "ENOSYS", "function not implemented" }, + .{ "ENOTCONN", "socket is not connected" }, + .{ "ENOTDIR", "not a directory" }, + .{ "ENOTEMPTY", "directory not empty" }, + .{ "ENOTSOCK", "socket operation on non-socket" }, + .{ "ENOTSUP", "operation not supported on socket" }, + .{ "EOVERFLOW", "value too large for defined data type" }, + .{ "EPERM", "operation not permitted" }, + .{ "EPIPE", "broken pipe" }, + .{ "EPROTO", "protocol error" }, + .{ "EPROTONOSUPPORT", "protocol not supported" }, + .{ "EPROTOTYPE", "protocol wrong type for socket" }, + .{ "ERANGE", "result too large" }, + .{ "EROFS", "read-only file system" }, + .{ "ESHUTDOWN", "cannot send after transport endpoint shutdown" }, + .{ "ESPIPE", "invalid seek" }, + .{ "ESRCH", "no such process" }, + .{ "ETIMEDOUT", "connection timed out" }, + .{ "ETXTBSY", "text file is busy" }, + .{ "EXDEV", "cross-device link not permitted" }, + .{ "UNKNOWN", "unknown error" }, + .{ "EOF", "end of file" }, + .{ "ENXIO", "no such device or address" }, + .{ "EMLINK", "too many links" }, + .{ "EHOSTDOWN", "host is down" }, + .{ "EREMOTEIO", "remote I/O error" }, + .{ "ENOTTY", "inappropriate ioctl for device" }, + .{ "EFTYPE", "inappropriate file type or format" }, + .{ "EILSEQ", "illegal byte sequence" }, + .{ "ESOCKTNOSUPPORT", "socket type not supported" }, + .{ "ENODATA", "no data available" }, + .{ "EUNATCH", "protocol driver not attached" }, + }; + const SystemErrno = bun.C.SystemErrno; + var map = std.EnumMap(SystemErrno, [:0]const u8).initFull("unknown error"); + for (entries) |entry| { + const key, const text = entry; + if (@hasField(SystemErrno, key)) { + map.put(@field(SystemErrno, key), text); + } + } + + // sanity check + bun.assert(std.mem.eql(u8, map.get(SystemErrno.ENOENT).?, "no such file or directory")); + + break :brk map; +}; + +/// This map is derived off of what coreutils uses in printing errors. This is +/// equivalent to `strerror`, but as strings with constant lifetime. +pub const coreutils_error_map = brk: { + // macOS and Linux have slightly different error messages. + const entries: []const struct { [:0]const u8, [:0]const u8 } = switch (Environment.os) { + // Since windows is just an emulation of linux, it will derive the linux error messages. + .linux, .windows, .wasm => &.{ + .{ "EPERM", "Operation not permitted" }, + .{ "ENOENT", "No such file or directory" }, + .{ "ESRCH", "No such process" }, + .{ "EINTR", "Interrupted system call" }, + .{ "EIO", "Input/output error" }, + .{ "ENXIO", "No such device or address" }, + .{ "E2BIG", "Argument list too long" }, + .{ "ENOEXEC", "Exec format error" }, + .{ "EBADF", "Bad file descriptor" }, + .{ "ECHILD", "No child processes" }, + .{ "EAGAIN", "Resource temporarily unavailable" }, + .{ "ENOMEM", "Cannot allocate memory" }, + .{ "EACCES", "Permission denied" }, + .{ "EFAULT", "Bad address" }, + .{ "ENOTBLK", "Block device required" }, + .{ "EBUSY", "Device or resource busy" }, + .{ "EEXIST", "File exists" }, + .{ "EXDEV", "Invalid cross-device link" }, + .{ "ENODEV", "No such device" }, + .{ "ENOTDIR", "Not a directory" }, + .{ "EISDIR", "Is a directory" }, + .{ "EINVAL", "Invalid argument" }, + .{ "ENFILE", "Too many open files in system" }, + .{ "EMFILE", "Too many open files" }, + .{ "ENOTTY", "Inappropriate ioctl for device" }, + .{ "ETXTBSY", "Text file busy" }, + .{ "EFBIG", "File too large" }, + .{ "ENOSPC", "No space left on device" }, + .{ "ESPIPE", "Illegal seek" }, + .{ "EROFS", "Read-only file system" }, + .{ "EMLINK", "Too many links" }, + .{ "EPIPE", "Broken pipe" }, + .{ "EDOM", "Numerical argument out of domain" }, + .{ "ERANGE", "Numerical result out of range" }, + .{ "EDEADLK", "Resource deadlock avoided" }, + .{ "ENAMETOOLONG", "File name too long" }, + .{ "ENOLCK", "No locks available" }, + .{ "ENOSYS", "Function not implemented" }, + .{ "ENOTEMPTY", "Directory not empty" }, + .{ "ELOOP", "Too many levels of symbolic links" }, + .{ "ENOMSG", "No message of desired type" }, + .{ "EIDRM", "Identifier removed" }, + .{ "ECHRNG", "Channel number out of range" }, + .{ "EL2NSYNC", "Level 2 not synchronized" }, + .{ "EL3HLT", "Level 3 halted" }, + .{ "EL3RST", "Level 3 reset" }, + .{ "ELNRNG", "Link number out of range" }, + .{ "EUNATCH", "Protocol driver not attached" }, + .{ "ENOCSI", "No CSI structure available" }, + .{ "EL2HLT", "Level 2 halted" }, + .{ "EBADE", "Invalid exchange" }, + .{ "EBADR", "Invalid request descriptor" }, + .{ "EXFULL", "Exchange full" }, + .{ "ENOANO", "No anode" }, + .{ "EBADRQC", "Invalid request code" }, + .{ "EBADSLT", "Invalid slot" }, + .{ "EBFONT", "Bad font file format" }, + .{ "ENOSTR", "Device not a stream" }, + .{ "ENODATA", "No data available" }, + .{ "ETIME", "Timer expired" }, + .{ "ENOSR", "Out of streams resources" }, + .{ "ENONET", "Machine is not on the network" }, + .{ "ENOPKG", "Package not installed" }, + .{ "EREMOTE", "Object is remote" }, + .{ "ENOLINK", "Link has been severed" }, + .{ "EADV", "Advertise error" }, + .{ "ESRMNT", "Srmount error" }, + .{ "ECOMM", "Communication error on send" }, + .{ "EPROTO", "Protocol error" }, + .{ "EMULTIHOP", "Multihop attempted" }, + .{ "EDOTDOT", "RFS specific error" }, + .{ "EBADMSG", "Bad message" }, + .{ "EOVERFLOW", "Value too large for defined data type" }, + .{ "ENOTUNIQ", "Name not unique on network" }, + .{ "EBADFD", "File descriptor in bad state" }, + .{ "EREMCHG", "Remote address changed" }, + .{ "ELIBACC", "Can not access a needed shared library" }, + .{ "ELIBBAD", "Accessing a corrupted shared library" }, + .{ "ELIBSCN", ".lib section in a.out corrupted" }, + .{ "ELIBMAX", "Attempting to link in too many shared libraries" }, + .{ "ELIBEXEC", "Cannot exec a shared library directly" }, + .{ "EILSEQ", "Invalid or incomplete multibyte or wide character" }, + .{ "ERESTART", "Interrupted system call should be restarted" }, + .{ "ESTRPIPE", "Streams pipe error" }, + .{ "EUSERS", "Too many users" }, + .{ "ENOTSOCK", "Socket operation on non-socket" }, + .{ "EDESTADDRREQ", "Destination address required" }, + .{ "EMSGSIZE", "Message too long" }, + .{ "EPROTOTYPE", "Protocol wrong type for socket" }, + .{ "ENOPROTOOPT", "Protocol not available" }, + .{ "EPROTONOSUPPORT", "Protocol not supported" }, + .{ "ESOCKTNOSUPPORT", "Socket type not supported" }, + .{ "EOPNOTSUPP", "Operation not supported" }, + .{ "EPFNOSUPPORT", "Protocol family not supported" }, + .{ "EAFNOSUPPORT", "Address family not supported by protocol" }, + .{ "EADDRINUSE", "Address already in use" }, + .{ "EADDRNOTAVAIL", "Cannot assign requested address" }, + .{ "ENETDOWN", "Network is down" }, + .{ "ENETUNREACH", "Network is unreachable" }, + .{ "ENETRESET", "Network dropped connection on reset" }, + .{ "ECONNABORTED", "Software caused connection abort" }, + .{ "ECONNRESET", "Connection reset by peer" }, + .{ "ENOBUFS", "No buffer space available" }, + .{ "EISCONN", "Transport endpoint is already connected" }, + .{ "ENOTCONN", "Transport endpoint is not connected" }, + .{ "ESHUTDOWN", "Cannot send after transport endpoint shutdown" }, + .{ "ETOOMANYREFS", "Too many references: cannot splice" }, + .{ "ETIMEDOUT", "Connection timed out" }, + .{ "ECONNREFUSED", "Connection refused" }, + .{ "EHOSTDOWN", "Host is down" }, + .{ "EHOSTUNREACH", "No route to host" }, + .{ "EALREADY", "Operation already in progress" }, + .{ "EINPROGRESS", "Operation now in progress" }, + .{ "ESTALE", "Stale file handle" }, + .{ "EUCLEAN", "Structure needs cleaning" }, + .{ "ENOTNAM", "Not a XENIX named type file" }, + .{ "ENAVAIL", "No XENIX semaphores available" }, + .{ "EISNAM", "Is a named type file" }, + .{ "EREMOTEIO", "Remote I/O error" }, + .{ "EDQUOT", "Disk quota exceeded" }, + .{ "ENOMEDIUM", "No medium found" }, + .{ "EMEDIUMTYPE", "Wrong medium type" }, + .{ "ECANCELED", "Operation canceled" }, + .{ "ENOKEY", "Required key not available" }, + .{ "EKEYEXPIRED", "Key has expired" }, + .{ "EKEYREVOKED", "Key has been revoked" }, + .{ "EKEYREJECTED", "Key was rejected by service" }, + .{ "EOWNERDEAD", "Owner died" }, + .{ "ENOTRECOVERABLE", "State not recoverable" }, + .{ "ERFKILL", "Operation not possible due to RF-kill" }, + .{ "EHWPOISON", "Memory page has hardware error" }, + }, + // Mac has slightly different messages. To keep it consistent with bash/coreutils, + // it will use those altered messages. + .mac => &.{ + .{ "E2BIG", "Argument list too long" }, + .{ "EACCES", "Permission denied" }, + .{ "EADDRINUSE", "Address already in use" }, + .{ "EADDRNOTAVAIL", "Can't assign requested address" }, + .{ "EAFNOSUPPORT", "Address family not supported by protocol family" }, + .{ "EAGAIN", "non-blocking and interrupt i/o. Resource temporarily unavailable" }, + .{ "EALREADY", "Operation already in progress" }, + .{ "EAUTH", "Authentication error" }, + .{ "EBADARCH", "Bad CPU type in executable" }, + .{ "EBADEXEC", "Program loading errors. Bad executable" }, + .{ "EBADF", "Bad file descriptor" }, + .{ "EBADMACHO", "Malformed Macho file" }, + .{ "EBADMSG", "Bad message" }, + .{ "EBADRPC", "RPC struct is bad" }, + .{ "EBUSY", "Device / Resource busy" }, + .{ "ECANCELED", "Operation canceled" }, + .{ "ECHILD", "No child processes" }, + .{ "ECONNABORTED", "Software caused connection abort" }, + .{ "ECONNREFUSED", "Connection refused" }, + .{ "ECONNRESET", "Connection reset by peer" }, + .{ "EDEADLK", "Resource deadlock avoided" }, + .{ "EDESTADDRREQ", "Destination address required" }, + .{ "EDEVERR", "Device error, for example paper out" }, + .{ "EDOM", "math software. Numerical argument out of domain" }, + .{ "EDQUOT", "Disc quota exceeded" }, + .{ "EEXIST", "File or folder exists" }, + .{ "EFAULT", "Bad address" }, + .{ "EFBIG", "File too large" }, + .{ "EFTYPE", "Inappropriate file type or format" }, + .{ "EHOSTDOWN", "Host is down" }, + .{ "EHOSTUNREACH", "No route to host" }, + .{ "EIDRM", "Identifier removed" }, + .{ "EILSEQ", "Illegal byte sequence" }, + .{ "EINPROGRESS", "Operation now in progress" }, + .{ "EINTR", "Interrupted system call" }, + .{ "EINVAL", "Invalid argument" }, + .{ "EIO", "Input/output error" }, + .{ "EISCONN", "Socket is already connected" }, + .{ "EISDIR", "Is a directory" }, + .{ "ELOOP", "Too many levels of symbolic links" }, + .{ "EMFILE", "Too many open files" }, + .{ "EMLINK", "Too many links" }, + .{ "EMSGSIZE", "Message too long" }, + .{ "EMULTIHOP", "Reserved" }, + .{ "ENAMETOOLONG", "File name too long" }, + .{ "ENEEDAUTH", "Need authenticator" }, + .{ "ENETDOWN", "ipc/network software - operational errors Network is down" }, + .{ "ENETRESET", "Network dropped connection on reset" }, + .{ "ENETUNREACH", "Network is unreachable" }, + .{ "ENFILE", "Too many open files in system" }, + .{ "ENOATTR", "Attribute not found" }, + .{ "ENOBUFS", "No buffer space available" }, + .{ "ENODATA", "No message available on STREAM" }, + .{ "ENODEV", "Operation not supported by device" }, + .{ "ENOENT", "No such file or directory" }, + .{ "ENOEXEC", "Exec format error" }, + .{ "ENOLCK", "No locks available" }, + .{ "ENOLINK", "Reserved" }, + .{ "ENOMEM", "Out of memory" }, + .{ "ENOMSG", "No message of desired type" }, + .{ "ENOPOLICY", "No such policy registered" }, + .{ "ENOPROTOOPT", "Protocol not available" }, + .{ "ENOSPC", "No space left on device" }, + .{ "ENOSR", "No STREAM resources" }, + .{ "ENOSTR", "Not a STREAM" }, + .{ "ENOSYS", "Function not implemented" }, + .{ "ENOTBLK", "Block device required" }, + .{ "ENOTCONN", "Socket is not connected" }, + .{ "ENOTDIR", "Not a directory" }, + .{ "ENOTEMPTY", "Directory not empty" }, + .{ "ENOTRECOVERABLE", "State not recoverable" }, + .{ "ENOTSOCK", "ipc/network software - argument errors. Socket operation on non-socket" }, + .{ "ENOTSUP", "Operation not supported" }, + .{ "ENOTTY", "Inappropriate ioctl for device" }, + .{ "ENXIO", "Device not configured" }, + .{ "EOVERFLOW", "Value too large to be stored in data type" }, + .{ "EOWNERDEAD", "Previous owner died" }, + .{ "EPERM", "Operation not permitted" }, + .{ "EPFNOSUPPORT", "Protocol family not supported" }, + .{ "EPIPE", "Broken pipe" }, + .{ "EPROCLIM", "quotas & mush. Too many processes" }, + .{ "EPROCUNAVAIL", "Bad procedure for program" }, + .{ "EPROGMISMATCH", "Program version wrong" }, + .{ "EPROGUNAVAIL", "RPC prog. not avail" }, + .{ "EPROTO", "Protocol error" }, + .{ "EPROTONOSUPPORT", "Protocol not supported" }, + .{ "EPROTOTYPE", "Protocol wrong type for socket" }, + .{ "EPWROFF", "Intelligent device errors. Device power is off" }, + .{ "EQFULL", "Interface output queue is full" }, + .{ "ERANGE", "Result too large" }, + .{ "EREMOTE", "Too many levels of remote in path" }, + .{ "EROFS", "Read-only file system" }, + .{ "ERPCMISMATCH", "RPC version wrong" }, + .{ "ESHLIBVERS", "Shared library version mismatch" }, + .{ "ESHUTDOWN", "Can't send after socket shutdown" }, + .{ "ESOCKTNOSUPPORT", "Socket type not supported" }, + .{ "ESPIPE", "Illegal seek" }, + .{ "ESRCH", "No such process" }, + .{ "ESTALE", "Network File System. Stale NFS file handle" }, + .{ "ETIME", "STREAM ioctl timeout" }, + .{ "ETIMEDOUT", "Operation timed out" }, + .{ "ETOOMANYREFS", "Too many references: can't splice" }, + .{ "ETXTBSY", "Text file busy" }, + .{ "EUSERS", "Too many users" }, + .{ "EWOULDBLOCK", "Operation would block" }, + .{ "EXDEV", "Cross-device link" }, + }, + }; + + const SystemErrno = bun.C.SystemErrno; + var map = std.EnumMap(SystemErrno, [:0]const u8).initFull("unknown error"); + for (entries) |entry| { + const key, const text = entry; + if (@hasField(SystemErrno, key)) { + map.put(@field(SystemErrno, key), text); + } + } + + // sanity check + bun.assert(std.mem.eql(u8, map.get(SystemErrno.ENOENT).?, "No such file or directory")); + + break :brk map; +}; diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 68f1c7f20e..27b87a19aa 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -54,7 +54,7 @@ pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, _perm: bun.Mode) Maybe(b const rc = uv.uv_fs_open(uv.Loop.get(), &req, file_path.ptr, flags, perm, null); log("uv open({s}, {d}, {d}) = {d}", .{ file_path, flags, perm, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .open } } + .{ .err = .{ .errno = errno, .syscall = .open, .path = file_path } } else .{ .result = bun.toFD(@as(i32, @intCast(req.result.int()))) }; } @@ -67,7 +67,7 @@ pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { log("uv mkdir({s}, {d}) = {d}", .{ file_path, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .mkdir } } + .{ .err = .{ .errno = errno, .syscall = .mkdir, .path = file_path } } else .{ .result = {} }; } @@ -81,7 +81,7 @@ pub fn chmod(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { log("uv chmod({s}, {d}) = {d}", .{ file_path, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .chmod } } + .{ .err = .{ .errno = errno, .syscall = .chmod, .path = file_path } } else .{ .result = {} }; } @@ -94,7 +94,7 @@ pub fn fchmod(fd: FileDescriptor, flags: bun.Mode) Maybe(void) { log("uv fchmod({}, {d}) = {d}", .{ uv_fd, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .fchmod } } + .{ .err = .{ .errno = errno, .syscall = .fchmod, .fd = fd } } else .{ .result = {} }; } @@ -107,7 +107,7 @@ pub fn chown(file_path: [:0]const u8, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe( log("uv chown({s}, {d}, {d}) = {d}", .{ file_path, uid, gid, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .chown } } + .{ .err = .{ .errno = errno, .syscall = .chown, .path = file_path } } else .{ .result = {} }; } @@ -121,7 +121,7 @@ pub fn fchown(fd: FileDescriptor, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe(void log("uv chown({}, {d}, {d}) = {d}", .{ uv_fd, uid, gid, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .fchown } } + .{ .err = .{ .errno = errno, .syscall = .fchown, .fd = fd } } else .{ .result = {} }; } @@ -134,7 +134,7 @@ pub fn access(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { log("uv access({s}, {d}) = {d}", .{ file_path, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .access } } + .{ .err = .{ .errno = errno, .syscall = .access, .path = file_path } } else .{ .result = {} }; } @@ -147,7 +147,7 @@ pub fn rmdir(file_path: [:0]const u8) Maybe(void) { log("uv rmdir({s}) = {d}", .{ file_path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .rmdir } } + .{ .err = .{ .errno = errno, .syscall = .rmdir, .path = file_path } } else .{ .result = {} }; } @@ -160,7 +160,7 @@ pub fn unlink(file_path: [:0]const u8) Maybe(void) { log("uv unlink({s}) = {d}", .{ file_path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .unlink } } + .{ .err = .{ .errno = errno, .syscall = .unlink, .path = file_path } } else .{ .result = {} }; } @@ -174,14 +174,14 @@ pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe([:0]u8) { if (rc.errno()) |errno| { log("uv readlink({s}) = {d}, [err]", .{ file_path, rc.int() }); - return .{ .err = .{ .errno = errno, .syscall = .readlink } }; + return .{ .err = .{ .errno = errno, .syscall = .readlink, .path = file_path } }; } else { // Seems like `rc` does not contain the size? bun.assert(rc.int() == 0); const slice = bun.span(req.ptrAs([*:0]u8)); if (slice.len > buf.len) { log("uv readlink({s}) = {d}, {s} TRUNCATED", .{ file_path, rc.int(), slice }); - return .{ .err = .{ .errno = @intFromEnum(E.NOMEM), .syscall = .readlink } }; + return .{ .err = .{ .errno = @intFromEnum(E.NOMEM), .syscall = .readlink, .path = file_path } }; } log("uv readlink({s}) = {d}, {s}", .{ file_path, rc.int(), slice }); @memcpy(buf[0..slice.len], slice); @@ -199,6 +199,7 @@ pub fn rename(from: [:0]const u8, to: [:0]const u8) Maybe(void) { log("uv rename({s}, {s}) = {d}", .{ from, to, rc.int() }); return if (rc.errno()) |errno| + // which one goes in the .path field? .{ .err = .{ .errno = errno, .syscall = .rename } } else .{ .result = {} }; @@ -213,7 +214,7 @@ pub fn link(from: [:0]const u8, to: [:0]const u8) Maybe(void) { log("uv link({s}, {s}) = {d}", .{ from, to, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .link } } + .{ .err = .{ .errno = errno, .syscall = .link, .path = from, .dest = to } } else .{ .result = {} }; } @@ -227,6 +228,7 @@ pub fn symlinkUV(from: [:0]const u8, to: [:0]const u8, flags: c_int) Maybe(void) log("uv symlink({s}, {s}) = {d}", .{ from, to, rc.int() }); return if (rc.errno()) |errno| + // which one goes in the .path field? .{ .err = .{ .errno = errno, .syscall = .symlink } } else .{ .result = {} }; @@ -292,7 +294,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { log("uv stat({s}) = {d}", .{ path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .stat } } + .{ .err = .{ .errno = errno, .syscall = .stat, .path = path } } else .{ .result = req.statbuf }; } @@ -305,7 +307,7 @@ pub fn lstat(path: [:0]const u8) Maybe(bun.Stat) { log("uv lstat({s}) = {d}", .{ path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .lstat } } + .{ .err = .{ .errno = errno, .syscall = .lstat, .path = path } } else .{ .result = req.statbuf }; } diff --git a/src/tagged_pointer.zig b/src/tagged_pointer.zig index ef9122582f..f81aa656b3 100644 --- a/src/tagged_pointer.zig +++ b/src/tagged_pointer.zig @@ -153,6 +153,14 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { return this.repr.get(Type); } + pub inline fn setUintptr(this: *This, value: AddressableSize) void { + this.repr._ptr = value; + } + + pub inline fn asUintptr(this: This) AddressableSize { + return this.repr._ptr; + } + pub inline fn is(this: This, comptime Type: type) bool { comptime assert_type(Type); return this.repr.data == comptime @intFromEnum(@field(Tag, typeBaseName(@typeName(Type)))); diff --git a/src/thread_pool.zig b/src/thread_pool.zig index e7e8b8d107..4186d489c2 100644 --- a/src/thread_pool.zig +++ b/src/thread_pool.zig @@ -132,7 +132,7 @@ pub const Batch = struct { }; pub const WaitGroup = struct { - mutex: std.Thread.Mutex = .{}, + mutex: bun.Mutex = .{}, counter: u32 = 0, event: std.Thread.ResetEvent = .{}, @@ -806,57 +806,6 @@ const Event = struct { } } - /// Wait for and consume a notification - /// or wait for the event to be shutdown entirely - noinline fn waitFor(self: *Event, timeout: usize) void { - var acquire_with: u32 = EMPTY; - var state = self.state.load(.monotonic); - - while (true) { - // If we're shutdown then exit early. - // Acquire barrier to ensure operations before the shutdown() are seen after the wait(). - // Shutdown is rare so it's better to have an Acquire barrier here instead of on CAS failure + load which are common. - if (state == SHUTDOWN) { - @fence(.acquire); - return; - } - - // Consume a notification when it pops up. - // Acquire barrier to ensure operations before the notify() appear after the wait(). - if (state == NOTIFIED) { - state = self.state.cmpxchgWeak( - state, - acquire_with, - .acquire, - .monotonic, - ) orelse return; - continue; - } - - // There is no notification to consume, we should wait on the event by ensuring its WAITING. - if (state != WAITING) blk: { - state = self.state.cmpxchgWeak( - state, - WAITING, - .monotonic, - .monotonic, - ) orelse break :blk; - continue; - } - - // Wait on the event until a notify() or shutdown(). - // If we wake up to a notification, we must acquire it with WAITING instead of EMPTY - // since there may be other threads sleeping on the Futex who haven't been woken up yet. - // - // Acquiring to WAITING will make the next notify() or shutdown() wake a sleeping futex thread - // who will either exit on SHUTDOWN or acquire with WAITING again, ensuring all threads are awoken. - // This unfortunately results in the last notify() or shutdown() doing an extra futex wake but that's fine. - Futex.wait(&self.state, WAITING, timeout) catch {}; - state = self.state.load(.monotonic); - acquire_with = WAITING; - } - } - /// Post a notification to the event if it doesn't have one already /// then wake up a waiting thread if there is one as well. fn notify(self: *Event) void { diff --git a/src/toml/toml_parser.zig b/src/toml/toml_parser.zig index b569be7495..0d07d49714 100644 --- a/src/toml/toml_parser.zig +++ b/src/toml/toml_parser.zig @@ -251,6 +251,7 @@ pub const TOML = struct { pub fn parseAssignment(p: *TOML, obj: *E.Object, allocator: std.mem.Allocator) anyerror!void { p.lexer.allow_double_bracket = false; const rope = try p.parseKey(allocator); + const rope_end = p.lexer.start; const is_array = p.lexer.token == .t_empty_array; if (is_array) { @@ -262,7 +263,11 @@ pub const TOML = struct { obj.setRope(rope, p.allocator, try p.parseValue()) catch |err| { switch (err) { error.Clobber => { - try p.lexer.addDefaultError("Cannot redefine key"); + const loc = rope.head.loc; + assert(loc.start > 0); + const start: u32 = @intCast(loc.start); + const key_name = std.mem.trimRight(u8, p.source().contents[start..rope_end], &std.ascii.whitespace); + p.lexer.addError(start, "Cannot redefine key '{s}'", .{key_name}); return error.SyntaxError; }, else => return err, diff --git a/src/transpiler.zig b/src/transpiler.zig new file mode 100644 index 0000000000..6be3b668fe --- /dev/null +++ b/src/transpiler.zig @@ -0,0 +1,1964 @@ +const bun = @import("root").bun; +const string = bun.string; +const Output = bun.Output; +const Global = bun.Global; +const Environment = bun.Environment; +const strings = bun.strings; +const MutableString = bun.MutableString; +const stringZ = bun.stringZ; +const default_allocator = bun.default_allocator; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const FeatureFlags = bun.FeatureFlags; +const C = bun.C; +const std = @import("std"); +const lex = bun.js_lexer; +const logger = bun.logger; +pub const options = @import("options.zig"); +const js_parser = bun.js_parser; +const JSON = bun.JSON; +const js_printer = bun.js_printer; +const js_ast = bun.JSAst; +const linker = @import("linker.zig"); +const Ref = @import("ast/base.zig").Ref; +const Define = @import("defines.zig").Define; +const DebugOptions = @import("./cli.zig").Command.DebugOptions; +const ThreadPoolLib = @import("./thread_pool.zig"); + +const Fs = @import("fs.zig"); +const schema = @import("api/schema.zig"); +const Api = schema.Api; +const _resolver = @import("./resolver/resolver.zig"); +const sync = @import("sync.zig"); +const ImportRecord = @import("./import_record.zig").ImportRecord; +const allocators = @import("./allocators.zig"); +const MimeType = @import("./http/mime_type.zig"); +const resolve_path = @import("./resolver/resolve_path.zig"); +const runtime = @import("./runtime.zig"); +const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; +const MacroRemap = @import("./resolver/package_json.zig").MacroMap; +const DebugLogs = _resolver.DebugLogs; +const Router = @import("./router.zig"); +const isPackagePath = _resolver.isPackagePath; +const Css = @import("css_scanner.zig"); +const DotEnv = @import("./env_loader.zig"); +const Lock = bun.Mutex; +const NodeFallbackModules = @import("./node_fallbacks.zig"); +const CacheEntry = @import("./cache.zig").FsCacheEntry; +const Analytics = @import("./analytics/analytics_thread.zig"); +const URL = @import("./url.zig").URL; +const Linker = linker.Linker; +const Resolver = _resolver.Resolver; +const TOML = @import("./toml/toml_parser.zig").TOML; +const JSC = bun.JSC; +const PackageManager = @import("./install/install.zig").PackageManager; +const DataURL = @import("./resolver/data_url.zig").DataURL; + +pub fn MacroJSValueType_() type { + if (comptime JSC.is_bindgen) { + return struct { + pub const zero = @This(){}; + }; + } + return JSC.JSValue; +} +pub const MacroJSValueType = MacroJSValueType_(); +const default_macro_js_value = if (JSC.is_bindgen) MacroJSValueType{} else JSC.JSValue.zero; + +const EntryPoints = @import("./bundler/entry_points.zig"); +const SystemTimer = @import("./system_timer.zig").Timer; +pub usingnamespace EntryPoints; +pub const ParseResult = struct { + source: logger.Source, + loader: options.Loader, + ast: js_ast.Ast, + already_bundled: AlreadyBundled = .none, + input_fd: ?StoredFileDescriptorType = null, + empty: bool = false, + pending_imports: _resolver.PendingResolution.List = .{}, + + runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache = null, + + pub const AlreadyBundled = union(enum) { + none: void, + source_code: void, + source_code_cjs: void, + bytecode: []u8, + bytecode_cjs: []u8, + + pub fn bytecodeSlice(this: AlreadyBundled) []u8 { + return switch (this) { + inline .bytecode, .bytecode_cjs => |slice| slice, + else => &.{}, + }; + } + + pub fn isBytecode(this: AlreadyBundled) bool { + return this == .bytecode or this == .bytecode_cjs; + } + + pub fn isCommonJS(this: AlreadyBundled) bool { + return this == .source_code_cjs or this == .bytecode_cjs; + } + }; + + pub fn isPendingImport(this: *const ParseResult, id: u32) bool { + const import_record_ids = this.pending_imports.items(.import_record_id); + + return std.mem.indexOfScalar(u32, import_record_ids, id) != null; + } + + /// **DO NOT CALL THIS UNDER NORMAL CIRCUMSTANCES** + /// Normally, we allocate each AST in an arena and free all at once + /// So this function only should be used when we globally allocate an AST + pub fn deinit(this: *ParseResult) void { + _resolver.PendingResolution.deinitListItems(this.pending_imports, bun.default_allocator); + this.pending_imports.deinit(bun.default_allocator); + this.ast.deinit(); + bun.default_allocator.free(@constCast(this.source.contents)); + } +}; + +const cache_files = false; + +pub const PluginRunner = struct { + global_object: *JSC.JSGlobalObject, + allocator: std.mem.Allocator, + + pub fn extractNamespace(specifier: string) string { + const colon = strings.indexOfChar(specifier, ':') orelse return ""; + if (Environment.isWindows and + colon == 1 and + specifier.len > 3 and + bun.path.isSepAny(specifier[2]) and + ((specifier[0] > 'a' and specifier[0] < 'z') or (specifier[0] > 'A' and specifier[0] < 'Z'))) + return ""; + return specifier[0..colon]; + } + + pub fn couldBePlugin(specifier: string) bool { + if (strings.lastIndexOfChar(specifier, '.')) |last_dor| { + const ext = specifier[last_dor + 1 ..]; + // '.' followed by either a letter or a non-ascii character + // maybe there are non-ascii file extensions? + // we mostly want to cheaply rule out "../" and ".." and "./" + if (ext.len > 0 and ((ext[0] >= 'a' and ext[0] <= 'z') or (ext[0] >= 'A' and ext[0] <= 'Z') or ext[0] > 127)) + return true; + } + return (!std.fs.path.isAbsolute(specifier) and strings.containsChar(specifier, ':')); + } + + pub fn onResolve( + this: *PluginRunner, + specifier: []const u8, + importer: []const u8, + log: *logger.Log, + loc: logger.Loc, + target: JSC.JSGlobalObject.BunPluginTarget, + ) bun.JSError!?Fs.Path { + var global = this.global_object; + const namespace_slice = extractNamespace(specifier); + const namespace = if (namespace_slice.len > 0 and !strings.eqlComptime(namespace_slice, "file")) + bun.String.init(namespace_slice) + else + bun.String.empty; + const on_resolve_plugin = global.runOnResolvePlugins( + namespace, + bun.String.init(specifier).substring(if (namespace.length() > 0) namespace.length() + 1 else 0), + bun.String.init(importer), + target, + ) orelse return null; + const path_value = try on_resolve_plugin.get(global, "path") orelse return null; + if (path_value.isEmptyOrUndefinedOrNull()) return null; + if (!path_value.isString()) { + log.addError(null, loc, "Expected \"path\" to be a string") catch unreachable; + return null; + } + + const file_path = path_value.toBunString(global); + defer file_path.deref(); + + if (file_path.length() == 0) { + log.addError( + null, + loc, + "Expected \"path\" to be a non-empty string in onResolve plugin", + ) catch unreachable; + return null; + } else if + // TODO: validate this better + (file_path.eqlComptime(".") or + file_path.eqlComptime("..") or + file_path.eqlComptime("...") or + file_path.eqlComptime(" ")) + { + log.addError( + null, + loc, + "Invalid file path from onResolve plugin", + ) catch unreachable; + return null; + } + var static_namespace = true; + const user_namespace: bun.String = brk: { + if (try on_resolve_plugin.get(global, "namespace")) |namespace_value| { + if (!namespace_value.isString()) { + log.addError(null, loc, "Expected \"namespace\" to be a string") catch unreachable; + return null; + } + + const namespace_str = namespace_value.toBunString(global); + if (namespace_str.length() == 0) { + namespace_str.deref(); + break :brk bun.String.init("file"); + } + + if (namespace_str.eqlComptime("file")) { + namespace_str.deref(); + break :brk bun.String.init("file"); + } + + if (namespace_str.eqlComptime("bun")) { + namespace_str.deref(); + break :brk bun.String.init("bun"); + } + + if (namespace_str.eqlComptime("node")) { + namespace_str.deref(); + break :brk bun.String.init("node"); + } + + static_namespace = false; + + break :brk namespace_str; + } + + break :brk bun.String.init("file"); + }; + defer user_namespace.deref(); + + if (static_namespace) { + return Fs.Path.initWithNamespace( + std.fmt.allocPrint(this.allocator, "{any}", .{file_path}) catch unreachable, + user_namespace.byteSlice(), + ); + } else { + return Fs.Path.initWithNamespace( + std.fmt.allocPrint(this.allocator, "{any}", .{file_path}) catch unreachable, + std.fmt.allocPrint(this.allocator, "{any}", .{user_namespace}) catch unreachable, + ); + } + } + + pub fn onResolveJSC(this: *const PluginRunner, namespace: bun.String, specifier: bun.String, importer: bun.String, target: JSC.JSGlobalObject.BunPluginTarget) bun.JSError!?JSC.ErrorableString { + var global = this.global_object; + const on_resolve_plugin = global.runOnResolvePlugins( + if (namespace.length() > 0 and !namespace.eqlComptime("file")) + namespace + else + bun.String.static(""), + specifier, + importer, + target, + ) orelse return null; + if (!on_resolve_plugin.isObject()) return null; + const path_value = try on_resolve_plugin.get(global, "path") orelse return null; + if (path_value.isEmptyOrUndefinedOrNull()) return null; + if (!path_value.isString()) { + return JSC.ErrorableString.err( + error.JSErrorObject, + bun.String.static("Expected \"path\" to be a string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + ); + } + + const file_path = path_value.toBunString(global); + + if (file_path.length() == 0) { + return JSC.ErrorableString.err( + error.JSErrorObject, + bun.String.static("Expected \"path\" to be a non-empty string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + ); + } else if + // TODO: validate this better + (file_path.eqlComptime(".") or + file_path.eqlComptime("..") or + file_path.eqlComptime("...") or + file_path.eqlComptime(" ")) + { + return JSC.ErrorableString.err( + error.JSErrorObject, + bun.String.static("\"path\" is invalid in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + ); + } + var static_namespace = true; + const user_namespace: bun.String = brk: { + if (try on_resolve_plugin.get(global, "namespace")) |namespace_value| { + if (!namespace_value.isString()) { + return JSC.ErrorableString.err( + error.JSErrorObject, + bun.String.static("Expected \"namespace\" to be a string").toErrorInstance(this.global_object).asVoid(), + ); + } + + const namespace_str = namespace_value.toBunString(global); + if (namespace_str.length() == 0) { + break :brk bun.String.static("file"); + } + + if (namespace_str.eqlComptime("file")) { + defer namespace_str.deref(); + break :brk bun.String.static("file"); + } + + if (namespace_str.eqlComptime("bun")) { + defer namespace_str.deref(); + break :brk bun.String.static("bun"); + } + + if (namespace_str.eqlComptime("node")) { + defer namespace_str.deref(); + break :brk bun.String.static("node"); + } + + static_namespace = false; + + break :brk namespace_str; + } + + break :brk bun.String.static("file"); + }; + defer user_namespace.deref(); + + // Our super slow way of cloning the string into memory owned by JSC + const combined_string = std.fmt.allocPrint( + this.allocator, + "{any}:{any}", + .{ user_namespace, file_path }, + ) catch unreachable; + var out_ = bun.String.init(combined_string); + const out = out_.toJS(this.global_object).toBunString(this.global_object); + this.allocator.free(combined_string); + return JSC.ErrorableString.ok(out); + } +}; + +/// This structure was the JavaScript transpiler before bundle_v2 was written. It now +/// acts mostly as a configuration object, but it also contains stateful logic around +/// logging errors (.log) and module resolution (.resolve_queue) +/// +/// This object is not exclusive to bundle_v2/Bun.build, one of these is stored +/// on every VM so that the options can be used for transpilation. +pub const Transpiler = struct { + options: options.BundleOptions, + log: *logger.Log, + allocator: std.mem.Allocator, + result: options.TransformResult, + resolver: Resolver, + fs: *Fs.FileSystem, + output_files: std.ArrayList(options.OutputFile), + resolve_results: *ResolveResults, + resolve_queue: ResolveQueue, + elapsed: u64 = 0, + needs_runtime: bool = false, + router: ?Router = null, + source_map: options.SourceMapOption = .none, + + linker: Linker, + timer: SystemTimer, + env: *DotEnv.Loader, + + macro_context: ?js_ast.Macro.MacroContext = null, + + pub const isCacheEnabled = cache_files; + + pub fn clone(this: *Transpiler, allocator: std.mem.Allocator, to: *Transpiler) !void { + to.* = this.*; + to.setAllocator(allocator); + to.log = try allocator.create(logger.Log); + to.log.* = logger.Log.init(allocator); + to.setLog(to.log); + to.macro_context = null; + to.linker.resolver = &to.resolver; + } + + pub inline fn getPackageManager(this: *Transpiler) *PackageManager { + return this.resolver.getPackageManager(); + } + + pub fn setLog(this: *Transpiler, log: *logger.Log) void { + this.log = log; + this.linker.log = log; + this.resolver.log = log; + } + + pub fn setAllocator(this: *Transpiler, allocator: std.mem.Allocator) void { + this.allocator = allocator; + this.linker.allocator = allocator; + this.resolver.allocator = allocator; + } + + fn _resolveEntryPoint(transpiler: *Transpiler, entry_point: string) !_resolver.Result { + return transpiler.resolver.resolveWithFramework(transpiler.fs.top_level_dir, entry_point, .entry_point_build) catch |err| { + // Relative entry points that were not resolved to a node_modules package are + // interpreted as relative to the current working directory. + if (!std.fs.path.isAbsolute(entry_point) and + !(strings.hasPrefix(entry_point, "./") or strings.hasPrefix(entry_point, ".\\"))) + { + brk: { + return transpiler.resolver.resolve( + transpiler.fs.top_level_dir, + try strings.append(transpiler.allocator, "./", entry_point), + .entry_point_build, + ) catch { + // return the original error + break :brk; + }; + } + } + return err; + }; + } + + pub fn resolveEntryPoint(transpiler: *Transpiler, entry_point: string) !_resolver.Result { + return _resolveEntryPoint(transpiler, entry_point) catch |err| { + var cache_bust_buf: bun.PathBuffer = 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| { + // Normalized with trailing slash + break :name bun.strings.normalizeSlashesOnly(&cache_bust_buf, dir, std.fs.path.sep); + } + } + + var parts = [_]string{ + entry_point, + bun.pathLiteral(".."), + }; + + break :name bun.path.joinAbsStringBufZ( + transpiler.fs.top_level_dir, + &cache_bust_buf, + &parts, + .auto, + ); + }; + + // Only re-query if we previously had something cached. + if (transpiler.resolver.bustDirCache(bun.strings.withoutTrailingSlashWindowsPath(buster_name))) { + if (_resolveEntryPoint(transpiler, entry_point)) |result| + return result + else |_| { + // ignore this error, we will print the original error + } + } + + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.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, + opts: Api.TransformOptions, + env_loader_: ?*DotEnv.Loader, + ) !Transpiler { + js_ast.Expr.Data.Store.create(); + js_ast.Stmt.Data.Store.create(); + + const fs = try Fs.FileSystem.init(opts.absolute_working_dir); + const bundle_options = try options.BundleOptions.fromApi( + allocator, + fs, + log, + opts, + ); + + var env_loader: *DotEnv.Loader = env_loader_ orelse DotEnv.instance orelse brk: { + const map = try allocator.create(DotEnv.Map); + map.* = DotEnv.Map.init(allocator); + + const loader = try allocator.create(DotEnv.Loader); + loader.* = DotEnv.Loader.init(map, allocator); + break :brk loader; + }; + + if (DotEnv.instance == null) { + DotEnv.instance = env_loader; + } + + // hide elapsed time when loglevel is warn or error + env_loader.quiet = !log.level.atLeast(.info); + + // var pool = try allocator.create(ThreadPool); + // try pool.init(ThreadPool.InitConfig{ + // .allocator = allocator, + // }); + const resolve_results = try allocator.create(ResolveResults); + resolve_results.* = ResolveResults.init(allocator); + return Transpiler{ + .options = bundle_options, + .fs = fs, + .allocator = allocator, + .timer = SystemTimer.start() catch @panic("Timer fail"), + .resolver = Resolver.init1(allocator, log, fs, bundle_options), + .log = log, + // .thread_pool = pool, + .linker = undefined, + .result = options.TransformResult{ .outbase = bundle_options.output_dir }, + .resolve_results = resolve_results, + .resolve_queue = ResolveQueue.init(allocator), + .output_files = std.ArrayList(options.OutputFile).init(allocator), + .env = env_loader, + }; + } + + pub fn configureLinkerWithAutoJSX(transpiler: *Transpiler, auto_jsx: bool) void { + transpiler.linker = Linker.init( + transpiler.allocator, + transpiler.log, + &transpiler.resolve_queue, + &transpiler.options, + &transpiler.resolver, + transpiler.resolve_results, + transpiler.fs, + ); + + if (auto_jsx) { + // Most of the time, this will already be cached + if (transpiler.resolver.readDirInfo(transpiler.fs.top_level_dir) catch null) |root_dir| { + if (root_dir.tsconfig_json) |tsconfig| { + // If we don't explicitly pass JSX, try to get it from the root tsconfig + if (transpiler.options.transform_options.jsx == null) { + transpiler.options.jsx = tsconfig.jsx; + } + transpiler.options.emit_decorator_metadata = tsconfig.emit_decorator_metadata; + } + } + } + } + + pub fn configureLinker(transpiler: *Transpiler) void { + transpiler.configureLinkerWithAutoJSX(true); + } + + pub fn runEnvLoader(this: *Transpiler, skip_default_env: bool) !void { + switch (this.options.env.behavior) { + .prefix, .load_all, .load_all_without_inlining => { + // Step 1. Load the project root. + const dir_info = this.resolver.readDirInfo(this.fs.top_level_dir) catch return orelse return; + + if (dir_info.tsconfig_json) |tsconfig| { + this.options.jsx = tsconfig.mergeJSX(this.options.jsx); + } + + const dir = dir_info.getEntries(this.resolver.generation) orelse return; + + // Process always has highest priority. + const was_production = this.options.production; + this.env.loadProcess(); + const has_production_env = this.env.isProduction(); + if (!was_production and has_production_env) { + this.options.setProduction(true); + } + + if (this.options.isTest() or this.env.isTest()) { + try this.env.load(dir, this.options.env.files, .@"test", skip_default_env); + } else if (this.options.production) { + try this.env.load(dir, this.options.env.files, .production, skip_default_env); + } else { + try this.env.load(dir, this.options.env.files, .development, skip_default_env); + } + }, + .disable => { + this.env.loadProcess(); + if (this.env.isProduction()) { + this.options.setProduction(true); + } + }, + else => {}, + } + + if (strings.eqlComptime(this.env.get("BUN_DISABLE_TRANSPILER") orelse "0", "1")) { + this.options.disable_transpilation = true; + } + } + + // This must be run after a framework is configured, if a framework is enabled + pub fn configureDefines(this: *Transpiler) !void { + if (this.options.defines_loaded) { + return; + } + + if (this.options.target == .bun_macro) { + this.options.env.behavior = .prefix; + this.options.env.prefix = "BUN_"; + } + + try this.runEnvLoader(false); + + this.options.jsx.setProduction(this.env.isProduction()); + + js_ast.Expr.Data.Store.create(); + js_ast.Stmt.Data.Store.create(); + + defer js_ast.Expr.Data.Store.reset(); + defer js_ast.Stmt.Data.Store.reset(); + + try this.options.loadDefines(this.allocator, this.env, &this.options.env); + + if (this.options.define.dots.get("NODE_ENV")) |NODE_ENV| { + if (NODE_ENV.len > 0 and NODE_ENV[0].data.value == .e_string and NODE_ENV[0].data.value.e_string.eqlComptime("production")) { + this.options.production = true; + } + } + } + + pub fn resetStore(_: *const Transpiler) void { + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); + } + + pub noinline fn dumpEnvironmentVariables(transpiler: *const Transpiler) void { + @setCold(true); + const opts = std.json.StringifyOptions{ + .whitespace = .indent_2, + }; + Output.flush(); + std.json.stringify(transpiler.env.map.*, opts, Output.writer()) catch unreachable; + Output.flush(); + } + + pub const BuildResolveResultPair = struct { + written: usize, + input_fd: ?StoredFileDescriptorType, + empty: bool = false, + }; + + pub fn buildWithResolveResult( + transpiler: *Transpiler, + resolve_result: _resolver.Result, + allocator: std.mem.Allocator, + loader: options.Loader, + comptime Writer: type, + writer: Writer, + comptime import_path_format: options.BundleOptions.ImportPathFormat, + file_descriptor: ?StoredFileDescriptorType, + filepath_hash: u32, + comptime WatcherType: type, + watcher: *WatcherType, + client_entry_point: ?*EntryPoints.ClientEntryPoint, + origin: URL, + comptime is_source_map: bool, + source_map_handler: ?js_printer.SourceMapHandler, + ) !BuildResolveResultPair { + if (resolve_result.is_external) { + return BuildResolveResultPair{ + .written = 0, + .input_fd = null, + }; + } + + errdefer transpiler.resetStore(); + + var file_path = (resolve_result.pathConst() orelse { + return BuildResolveResultPair{ + .written = 0, + .input_fd = null, + }; + }).*; + + if (strings.indexOf(file_path.text, transpiler.fs.top_level_dir)) |i| { + file_path.pretty = file_path.text[i + transpiler.fs.top_level_dir.len ..]; + } else if (!file_path.is_symlink) { + file_path.pretty = allocator.dupe(u8, transpiler.fs.relativeTo(file_path.text)) catch unreachable; + } + + const old_bundler_allocator = transpiler.allocator; + transpiler.allocator = allocator; + defer transpiler.allocator = old_bundler_allocator; + const old_linker_allocator = transpiler.linker.allocator; + defer transpiler.linker.allocator = old_linker_allocator; + transpiler.linker.allocator = allocator; + + switch (loader) { + .css => { + const CSSBundlerHMR = Css.NewBundler( + Writer, + @TypeOf(&transpiler.linker), + @TypeOf(&transpiler.resolver.caches.fs), + WatcherType, + @TypeOf(transpiler.fs), + true, + import_path_format, + ); + + const CSSBundler = Css.NewBundler( + Writer, + @TypeOf(&transpiler.linker), + @TypeOf(&transpiler.resolver.caches.fs), + WatcherType, + @TypeOf(transpiler.fs), + false, + import_path_format, + ); + + const written = brk: { + if (transpiler.options.hot_module_reloading) { + break :brk (try CSSBundlerHMR.bundle( + file_path.text, + transpiler.fs, + writer, + watcher, + &transpiler.resolver.caches.fs, + filepath_hash, + file_descriptor, + allocator, + transpiler.log, + &transpiler.linker, + origin, + )).written; + } else { + break :brk (try CSSBundler.bundle( + file_path.text, + transpiler.fs, + writer, + watcher, + &transpiler.resolver.caches.fs, + filepath_hash, + file_descriptor, + allocator, + transpiler.log, + &transpiler.linker, + origin, + )).written; + } + }; + + return BuildResolveResultPair{ + .written = written, + .input_fd = file_descriptor, + }; + }, + else => { + var result = transpiler.parse( + ParseOptions{ + .allocator = allocator, + .path = file_path, + .loader = loader, + .dirname_fd = resolve_result.dirname_fd, + .file_descriptor = file_descriptor, + .file_hash = filepath_hash, + .macro_remappings = transpiler.options.macro_remap, + .emit_decorator_metadata = resolve_result.emit_decorator_metadata, + .jsx = resolve_result.jsx, + }, + client_entry_point, + ) orelse { + transpiler.resetStore(); + return BuildResolveResultPair{ + .written = 0, + .input_fd = null, + }; + }; + + if (result.empty) { + return BuildResolveResultPair{ .written = 0, .input_fd = result.input_fd, .empty = true }; + } + + if (transpiler.options.target.isBun()) { + if (!transpiler.options.transform_only) { + try transpiler.linker.link(file_path, &result, origin, import_path_format, false, true); + } + + return BuildResolveResultPair{ + .written = switch (result.ast.exports_kind) { + .esm => try transpiler.printWithSourceMapMaybe( + result.ast, + &result.source, + Writer, + writer, + .esm_ascii, + is_source_map, + source_map_handler, + null, + ), + .cjs => try transpiler.printWithSourceMapMaybe( + result.ast, + &result.source, + Writer, + writer, + .cjs, + is_source_map, + source_map_handler, + null, + ), + else => unreachable, + }, + .input_fd = result.input_fd, + }; + } + + if (!transpiler.options.transform_only) { + try transpiler.linker.link(file_path, &result, origin, import_path_format, false, false); + } + + return BuildResolveResultPair{ + .written = switch (result.ast.exports_kind) { + .none, .esm => try transpiler.printWithSourceMapMaybe( + result.ast, + &result.source, + Writer, + writer, + .esm, + is_source_map, + source_map_handler, + null, + ), + .cjs => try transpiler.printWithSourceMapMaybe( + result.ast, + &result.source, + Writer, + writer, + .cjs, + is_source_map, + source_map_handler, + null, + ), + else => unreachable, + }, + .input_fd = result.input_fd, + }; + }, + } + } + + pub fn buildWithResolveResultEager( + transpiler: *Transpiler, + resolve_result: _resolver.Result, + comptime import_path_format: options.BundleOptions.ImportPathFormat, + comptime Outstream: type, + outstream: Outstream, + client_entry_point_: ?*EntryPoints.ClientEntryPoint, + ) !?options.OutputFile { + if (resolve_result.is_external) { + return null; + } + + var file_path = (resolve_result.pathConst() orelse return null).*; + + // Step 1. Parse & scan + const loader = transpiler.options.loader(file_path.name.ext); + + if (client_entry_point_) |client_entry_point| { + file_path = client_entry_point.source.path; + } + + file_path.pretty = Linker.relative_paths_list.append(string, transpiler.fs.relativeTo(file_path.text)) catch unreachable; + + var output_file = options.OutputFile{ + .src_path = file_path, + .loader = loader, + .value = undefined, + .side = null, + .entry_point_index = null, + .output_kind = .chunk, + }; + + switch (loader) { + .jsx, .tsx, .js, .ts, .json, .toml, .text => { + var result = transpiler.parse( + ParseOptions{ + .allocator = transpiler.allocator, + .path = file_path, + .loader = loader, + .dirname_fd = resolve_result.dirname_fd, + .file_descriptor = null, + .file_hash = null, + .macro_remappings = transpiler.options.macro_remap, + .jsx = resolve_result.jsx, + .emit_decorator_metadata = resolve_result.emit_decorator_metadata, + }, + client_entry_point_, + ) orelse { + return null; + }; + if (!transpiler.options.transform_only) { + if (!transpiler.options.target.isBun()) + try transpiler.linker.link( + file_path, + &result, + transpiler.options.origin, + import_path_format, + false, + false, + ) + else + try transpiler.linker.link( + file_path, + &result, + transpiler.options.origin, + import_path_format, + false, + true, + ); + } + + const buffer_writer = try js_printer.BufferWriter.init(transpiler.allocator); + var writer = js_printer.BufferPrinter.init(buffer_writer); + + output_file.size = switch (transpiler.options.target) { + .browser, .node => try transpiler.print( + result, + *js_printer.BufferPrinter, + &writer, + .esm, + ), + .bun, .bun_macro, .bake_server_components_ssr => try transpiler.print( + result, + *js_printer.BufferPrinter, + &writer, + .esm_ascii, + ), + }; + output_file.value = .{ + .buffer = .{ + .allocator = transpiler.allocator, + .bytes = writer.ctx.written, + }, + }; + }, + .dataurl, .base64 => { + Output.panic("TODO: dataurl, base64", .{}); // TODO + }, + .css => { + if (transpiler.options.experimental.css) { + const alloc = transpiler.allocator; + + const entry = transpiler.resolver.caches.fs.readFileWithAllocator( + transpiler.allocator, + transpiler.fs, + file_path.text, + resolve_result.dirname_fd, + false, + null, + ) catch |err| { + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{s} reading \"{s}\"", .{ @errorName(err), file_path.pretty }) catch {}; + return null; + }; + var sheet = switch (bun.css.StyleSheet(bun.css.DefaultAtRule).parse(alloc, entry.contents, bun.css.ParserOptions.default(alloc, transpiler.log), null)) { + .result => |v| v, + .err => |e| { + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{} parsing", .{e}) catch unreachable; + return null; + }, + }; + if (sheet.minify(alloc, bun.css.MinifyOptions.default()).asErr()) |e| { + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{} while minifying", .{e.kind}) catch bun.outOfMemory(); + return null; + } + const result = sheet.toCss(alloc, bun.css.PrinterOptions{ + .targets = bun.css.Targets.forBundlerTarget(transpiler.options.target), + .minify = transpiler.options.minify_whitespace, + }, null) catch |e| { + bun.handleErrorReturnTrace(e, @errorReturnTrace()); + return null; + }; + output_file.value = .{ .buffer = .{ .allocator = alloc, .bytes = result.code } }; + } else { + var file: bun.sys.File = undefined; + + if (Outstream == std.fs.Dir) { + const output_dir = outstream; + + if (std.fs.path.dirname(file_path.pretty)) |dirname| { + try output_dir.makePath(dirname); + } + file = bun.sys.File.from(try output_dir.createFile(file_path.pretty, .{})); + } else { + file = bun.sys.File.from(outstream); + } + + const CSSBuildContext = struct { + origin: URL, + }; + const build_ctx = CSSBuildContext{ .origin = transpiler.options.origin }; + + const BufferedWriter = std.io.CountingWriter(std.io.BufferedWriter(8192, bun.sys.File.Writer)); + const CSSWriter = Css.NewWriter( + BufferedWriter.Writer, + @TypeOf(&transpiler.linker), + import_path_format, + CSSBuildContext, + ); + var buffered_writer = BufferedWriter{ + .child_stream = .{ .unbuffered_writer = file.writer() }, + .bytes_written = 0, + }; + const entry = transpiler.resolver.caches.fs.readFile( + transpiler.fs, + file_path.text, + resolve_result.dirname_fd, + !cache_files, + null, + ) catch return null; + + const _file = Fs.PathContentsPair{ .path = file_path, .contents = entry.contents }; + var source = try logger.Source.initFile(_file, transpiler.allocator); + source.contents_is_recycled = !cache_files; + + var css_writer = CSSWriter.init( + &source, + buffered_writer.writer(), + &transpiler.linker, + transpiler.log, + ); + + css_writer.buildCtx = build_ctx; + + try css_writer.run(transpiler.log, transpiler.allocator); + try css_writer.ctx.context.child_stream.flush(); + output_file.size = css_writer.ctx.context.bytes_written; + var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty); + + file_op.fd = bun.toFD(file.handle); + + file_op.is_tmpdir = false; + + if (Outstream == std.fs.Dir) { + file_op.dir = bun.toFD(outstream.fd); + + if (transpiler.fs.fs.needToCloseFiles()) { + file.close(); + file_op.fd = .zero; + } + } + + output_file.value = .{ .move = file_op }; + } + }, + + .html, .bunsh, .sqlite_embedded, .sqlite, .wasm, .file, .napi => { + const hashed_name = try transpiler.linker.getHashedFilename(file_path, null); + var pathname = try transpiler.allocator.alloc(u8, hashed_name.len + file_path.name.ext.len); + bun.copy(u8, pathname, hashed_name); + bun.copy(u8, pathname[hashed_name.len..], file_path.name.ext); + const dir = if (transpiler.options.output_dir_handle) |output_handle| bun.toFD(output_handle.fd) else .zero; + + output_file.value = .{ + .copy = options.OutputFile.FileOperation{ + .pathname = pathname, + .dir = dir, + .is_outdir = true, + }, + }; + }, + } + + return output_file; + } + + pub fn printWithSourceMapMaybe( + transpiler: *Transpiler, + ast: js_ast.Ast, + source: *const logger.Source, + comptime Writer: type, + writer: Writer, + comptime format: js_printer.Format, + comptime enable_source_map: bool, + source_map_context: ?js_printer.SourceMapHandler, + runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache, + ) !usize { + const tracer = bun.tracy.traceNamed(@src(), if (enable_source_map) "JSPrinter.printWithSourceMap" else "JSPrinter.print"); + defer tracer.end(); + + const symbols = js_ast.Symbol.NestedList.init(&[_]js_ast.Symbol.List{ast.symbols}); + + return switch (format) { + .cjs => try js_printer.printCommonJS( + Writer, + writer, + ast, + js_ast.Symbol.Map.initList(symbols), + source, + false, + .{ + .bundling = false, + .runtime_imports = ast.runtime_imports, + .require_ref = ast.require_ref, + .css_import_behavior = transpiler.options.cssImportBehavior(), + .source_map_handler = source_map_context, + .minify_whitespace = transpiler.options.minify_whitespace, + .minify_syntax = transpiler.options.minify_syntax, + .minify_identifiers = transpiler.options.minify_identifiers, + .transform_only = transpiler.options.transform_only, + .runtime_transpiler_cache = runtime_transpiler_cache, + .print_dce_annotations = transpiler.options.emit_dce_annotations, + }, + enable_source_map, + ), + + .esm => try js_printer.printAst( + Writer, + writer, + ast, + js_ast.Symbol.Map.initList(symbols), + source, + false, + .{ + .bundling = false, + .runtime_imports = ast.runtime_imports, + .require_ref = ast.require_ref, + .source_map_handler = source_map_context, + .css_import_behavior = transpiler.options.cssImportBehavior(), + .minify_whitespace = transpiler.options.minify_whitespace, + .minify_syntax = transpiler.options.minify_syntax, + .minify_identifiers = transpiler.options.minify_identifiers, + .transform_only = transpiler.options.transform_only, + .import_meta_ref = ast.import_meta_ref, + .runtime_transpiler_cache = runtime_transpiler_cache, + .print_dce_annotations = transpiler.options.emit_dce_annotations, + }, + enable_source_map, + ), + .esm_ascii => switch (transpiler.options.target.isBun()) { + inline else => |is_bun| try js_printer.printAst( + Writer, + writer, + ast, + js_ast.Symbol.Map.initList(symbols), + source, + is_bun, + .{ + .bundling = false, + .runtime_imports = ast.runtime_imports, + .require_ref = ast.require_ref, + .css_import_behavior = transpiler.options.cssImportBehavior(), + .source_map_handler = source_map_context, + .minify_whitespace = transpiler.options.minify_whitespace, + .minify_syntax = transpiler.options.minify_syntax, + .minify_identifiers = transpiler.options.minify_identifiers, + .transform_only = transpiler.options.transform_only, + .module_type = if (is_bun and transpiler.options.transform_only) + // this is for when using `bun build --no-bundle` + // it should copy what was passed for the cli + transpiler.options.output_format + else if (ast.exports_kind == .cjs) + .cjs + else + .esm, + .inline_require_and_import_errors = false, + .import_meta_ref = ast.import_meta_ref, + .runtime_transpiler_cache = runtime_transpiler_cache, + .target = transpiler.options.target, + .print_dce_annotations = transpiler.options.emit_dce_annotations, + }, + enable_source_map, + ), + }, + else => unreachable, + }; + } + + pub fn print( + transpiler: *Transpiler, + result: ParseResult, + comptime Writer: type, + writer: Writer, + comptime format: js_printer.Format, + ) !usize { + return transpiler.printWithSourceMapMaybe( + result.ast, + &result.source, + Writer, + writer, + format, + false, + null, + null, + ); + } + + pub fn printWithSourceMap( + transpiler: *Transpiler, + result: ParseResult, + comptime Writer: type, + writer: Writer, + comptime format: js_printer.Format, + handler: js_printer.SourceMapHandler, + ) !usize { + if (bun.getRuntimeFeatureFlag("BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS")) { + return transpiler.printWithSourceMapMaybe( + result.ast, + &result.source, + Writer, + writer, + format, + false, + handler, + result.runtime_transpiler_cache, + ); + } + return transpiler.printWithSourceMapMaybe( + result.ast, + &result.source, + Writer, + writer, + format, + true, + handler, + result.runtime_transpiler_cache, + ); + } + + pub const ParseOptions = struct { + allocator: std.mem.Allocator, + dirname_fd: StoredFileDescriptorType, + file_descriptor: ?StoredFileDescriptorType = null, + file_hash: ?u32 = null, + + /// On exception, we might still want to watch the file. + file_fd_ptr: ?*StoredFileDescriptorType = null, + + path: Fs.Path, + loader: options.Loader, + jsx: options.JSX.Pragma, + macro_remappings: MacroRemap, + macro_js_ctx: MacroJSValueType = default_macro_js_value, + virtual_source: ?*const logger.Source = null, + replace_exports: runtime.Runtime.Features.ReplaceableExport.Map = .{}, + inject_jest_globals: bool = false, + set_breakpoint_on_first_line: bool = false, + emit_decorator_metadata: bool = false, + remove_cjs_module_wrapper: bool = false, + + dont_bundle_twice: bool = false, + allow_commonjs: bool = false, + + runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache = null, + + keep_json_and_toml_as_one_statement: bool = false, + allow_bytecode_cache: bool = false, + }; + + pub fn parse( + transpiler: *Transpiler, + this_parse: ParseOptions, + client_entry_point_: anytype, + ) ?ParseResult { + return parseMaybeReturnFileOnly(transpiler, this_parse, client_entry_point_, false); + } + + pub fn parseMaybeReturnFileOnly( + transpiler: *Transpiler, + this_parse: ParseOptions, + client_entry_point_: anytype, + comptime return_file_only: bool, + ) ?ParseResult { + return parseMaybeReturnFileOnlyAllowSharedBuffer( + transpiler, + this_parse, + client_entry_point_, + return_file_only, + false, + ); + } + + pub fn parseMaybeReturnFileOnlyAllowSharedBuffer( + transpiler: *Transpiler, + this_parse: ParseOptions, + client_entry_point_: anytype, + comptime return_file_only: bool, + comptime use_shared_buffer: bool, + ) ?ParseResult { + var allocator = this_parse.allocator; + const dirname_fd = this_parse.dirname_fd; + const file_descriptor = this_parse.file_descriptor; + const file_hash = this_parse.file_hash; + const path = this_parse.path; + const loader = this_parse.loader; + + var input_fd: ?StoredFileDescriptorType = null; + + const source: logger.Source = brk: { + if (this_parse.virtual_source) |virtual_source| { + break :brk virtual_source.*; + } + + if (client_entry_point_) |client_entry_point| { + if (@hasField(std.meta.Child(@TypeOf(client_entry_point)), "source")) { + break :brk client_entry_point.source; + } + } + + if (strings.eqlComptime(path.namespace, "node")) { + if (NodeFallbackModules.contentsFromPath(path.text)) |code| { + break :brk logger.Source.initPathString(path.text, code); + } + + break :brk logger.Source.initPathString(path.text, ""); + } + + if (strings.startsWith(path.text, "data:")) { + const data_url = DataURL.parseWithoutCheck(path.text) catch |err| { + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{s} parsing data url \"{s}\"", .{ @errorName(err), path.text }) catch {}; + return null; + }; + const body = data_url.decodeData(this_parse.allocator) catch |err| { + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{s} decoding data \"{s}\"", .{ @errorName(err), path.text }) catch {}; + return null; + }; + break :brk logger.Source.initPathString(path.text, body); + } + + const entry = transpiler.resolver.caches.fs.readFileWithAllocator( + if (use_shared_buffer) bun.fs_allocator else this_parse.allocator, + transpiler.fs, + path.text, + dirname_fd, + use_shared_buffer, + file_descriptor, + ) catch |err| { + transpiler.log.addErrorFmt(null, logger.Loc.Empty, transpiler.allocator, "{s} reading \"{s}\"", .{ @errorName(err), path.text }) catch {}; + return null; + }; + input_fd = entry.fd; + if (this_parse.file_fd_ptr) |file_fd_ptr| { + file_fd_ptr.* = entry.fd; + } + break :brk logger.Source.initRecycledFile(.{ .path = path, .contents = entry.contents }, transpiler.allocator) catch return null; + }; + + if (comptime return_file_only) { + return ParseResult{ .source = source, .input_fd = input_fd, .loader = loader, .empty = true, .ast = js_ast.Ast.empty }; + } + + if (loader != .wasm and source.contents.len == 0 and source.contents.len < 33 and std.mem.trim(u8, source.contents, "\n\r ").len == 0) { + return ParseResult{ .source = source, .input_fd = input_fd, .loader = loader, .empty = true, .ast = js_ast.Ast.empty }; + } + + switch (loader) { + .js, + .jsx, + .ts, + .tsx, + => { + // wasm magic number + if (source.isWebAssembly()) { + return ParseResult{ + .source = source, + .input_fd = input_fd, + .loader = .wasm, + .empty = true, + .ast = js_ast.Ast.empty, + }; + } + + const target = transpiler.options.target; + + var jsx = this_parse.jsx; + jsx.parse = loader.isJSX(); + + var opts = js_parser.Parser.Options.init(jsx, loader); + + opts.features.emit_decorator_metadata = this_parse.emit_decorator_metadata; + opts.features.allow_runtime = transpiler.options.allow_runtime; + opts.features.set_breakpoint_on_first_line = this_parse.set_breakpoint_on_first_line; + opts.features.trim_unused_imports = transpiler.options.trim_unused_imports orelse loader.isTypeScript(); + opts.features.no_macros = transpiler.options.no_macros; + opts.features.runtime_transpiler_cache = this_parse.runtime_transpiler_cache; + opts.transform_only = transpiler.options.transform_only; + + opts.ignore_dce_annotations = transpiler.options.ignore_dce_annotations; + + // @bun annotation + opts.features.dont_bundle_twice = this_parse.dont_bundle_twice; + + opts.features.commonjs_at_runtime = this_parse.allow_commonjs; + + opts.tree_shaking = transpiler.options.tree_shaking; + opts.features.inlining = transpiler.options.inlining; + + opts.filepath_hash_for_hmr = file_hash orelse 0; + opts.features.auto_import_jsx = transpiler.options.auto_import_jsx; + opts.warn_about_unbundled_modules = !target.isBun(); + + opts.features.inject_jest_globals = this_parse.inject_jest_globals; + opts.features.minify_syntax = transpiler.options.minify_syntax; + opts.features.minify_identifiers = transpiler.options.minify_identifiers; + opts.features.dead_code_elimination = transpiler.options.dead_code_elimination; + opts.features.remove_cjs_module_wrapper = this_parse.remove_cjs_module_wrapper; + + if (transpiler.macro_context == null) { + transpiler.macro_context = js_ast.Macro.MacroContext.init(transpiler); + } + + // we'll just always enable top-level await + // this is incorrect for Node.js files which are CommonJS modules + opts.features.top_level_await = true; + + opts.macro_context = &transpiler.macro_context.?; + if (comptime !JSC.is_bindgen) { + if (target != .bun_macro) { + opts.macro_context.javascript_object = this_parse.macro_js_ctx; + } + } + + opts.features.is_macro_runtime = target == .bun_macro; + opts.features.replace_exports = this_parse.replace_exports; + + return switch ((transpiler.resolver.caches.js.parse( + allocator, + opts, + transpiler.options.define, + transpiler.log, + &source, + ) catch null) orelse return null) { + .ast => |value| ParseResult{ + .ast = value, + .source = source, + .loader = loader, + .input_fd = input_fd, + .runtime_transpiler_cache = this_parse.runtime_transpiler_cache, + }, + .cached => ParseResult{ + .ast = undefined, + .runtime_transpiler_cache = this_parse.runtime_transpiler_cache, + .source = source, + .loader = loader, + .input_fd = input_fd, + }, + .already_bundled => |already_bundled| ParseResult{ + .ast = undefined, + .already_bundled = switch (already_bundled) { + .bun => .source_code, + .bun_cjs => .source_code_cjs, + .bytecode_cjs, .bytecode => brk: { + const default_value: ParseResult.AlreadyBundled = if (already_bundled == .bytecode_cjs) .source_code_cjs else .source_code; + if (this_parse.virtual_source == null and this_parse.allow_bytecode_cache) { + var path_buf2: bun.PathBuffer = undefined; + @memcpy(path_buf2[0..path.text.len], path.text); + path_buf2[path.text.len..][0..bun.bytecode_extension.len].* = bun.bytecode_extension.*; + const bytecode = bun.sys.File.toSourceAt(dirname_fd, path_buf2[0 .. path.text.len + bun.bytecode_extension.len], bun.default_allocator).asValue() orelse break :brk default_value; + if (bytecode.contents.len == 0) { + break :brk default_value; + } + break :brk if (already_bundled == .bytecode_cjs) .{ .bytecode_cjs = @constCast(bytecode.contents) } else .{ .bytecode = @constCast(bytecode.contents) }; + } + break :brk default_value; + }, + }, + .source = source, + .loader = loader, + .input_fd = input_fd, + }, + }; + }, + // TODO: use lazy export AST + inline .toml, .json => |kind| { + var expr = if (kind == .json) + // We allow importing tsconfig.*.json or jsconfig.*.json with comments + // These files implicitly become JSONC files, which aligns with the behavior of text editors. + if (source.path.isJSONCFile()) + JSON.parseTSConfig(&source, transpiler.log, allocator, false) catch return null + else + JSON.parse(&source, transpiler.log, allocator, false) catch return null + else if (kind == .toml) + TOML.parse(&source, transpiler.log, allocator, false) catch return null + else + @compileError("unreachable"); + + var symbols: []js_ast.Symbol = &.{}; + + const parts = brk: { + if (this_parse.keep_json_and_toml_as_one_statement) { + var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable; + stmts[0] = js_ast.Stmt.allocate(allocator, js_ast.S.SExpr, js_ast.S.SExpr{ .value = expr }, logger.Loc{ .start = 0 }); + var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable; + parts_[0] = js_ast.Part{ .stmts = stmts }; + break :brk parts_; + } + + if (expr.data == .e_object) { + const properties: []js_ast.G.Property = expr.data.e_object.properties.slice(); + if (properties.len > 0) { + var stmts = allocator.alloc(js_ast.Stmt, 3) catch return null; + var decls = allocator.alloc(js_ast.G.Decl, properties.len) catch return null; + symbols = allocator.alloc(js_ast.Symbol, properties.len) catch return null; + var export_clauses = allocator.alloc(js_ast.ClauseItem, properties.len) catch return null; + var duplicate_key_checker = bun.StringHashMap(u32).init(allocator); + defer duplicate_key_checker.deinit(); + var count: usize = 0; + for (properties, decls, symbols, 0..) |*prop, *decl, *symbol, i| { + const name = prop.key.?.data.e_string.slice(allocator); + // Do not make named exports for "default" exports + if (strings.eqlComptime(name, "default")) + continue; + + const visited = duplicate_key_checker.getOrPut(name) catch continue; + if (visited.found_existing) { + decls[visited.value_ptr.*].value = prop.value.?; + continue; + } + visited.value_ptr.* = @truncate(i); + + symbol.* = js_ast.Symbol{ + .original_name = MutableString.ensureValidIdentifier(name, allocator) catch return null, + }; + + const ref = Ref.init(@truncate(i), 0, false); + decl.* = js_ast.G.Decl{ + .binding = js_ast.Binding.alloc(allocator, js_ast.B.Identifier{ + .ref = ref, + }, prop.key.?.loc), + .value = prop.value.?, + }; + export_clauses[i] = js_ast.ClauseItem{ + .name = .{ + .ref = ref, + .loc = prop.key.?.loc, + }, + .alias = name, + .alias_loc = prop.key.?.loc, + }; + prop.value = js_ast.Expr.initIdentifier(ref, prop.value.?.loc); + count += 1; + } + + stmts[0] = js_ast.Stmt.alloc( + js_ast.S.Local, + js_ast.S.Local{ + .decls = js_ast.G.Decl.List.init(decls[0..count]), + .kind = .k_var, + }, + logger.Loc{ + .start = 0, + }, + ); + stmts[1] = js_ast.Stmt.alloc( + js_ast.S.ExportClause, + js_ast.S.ExportClause{ + .items = export_clauses[0..count], + }, + logger.Loc{ + .start = 0, + }, + ); + stmts[2] = js_ast.Stmt.alloc( + js_ast.S.ExportDefault, + js_ast.S.ExportDefault{ + .value = js_ast.StmtOrExpr{ .expr = expr }, + .default_name = js_ast.LocRef{ + .loc = logger.Loc{}, + .ref = Ref.None, + }, + }, + logger.Loc{ + .start = 0, + }, + ); + + var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable; + parts_[0] = js_ast.Part{ .stmts = stmts }; + break :brk parts_; + } + } + + { + var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable; + stmts[0] = js_ast.Stmt.alloc(js_ast.S.ExportDefault, js_ast.S.ExportDefault{ + .value = js_ast.StmtOrExpr{ .expr = expr }, + .default_name = js_ast.LocRef{ + .loc = logger.Loc{}, + .ref = Ref.None, + }, + }, logger.Loc{ .start = 0 }); + + var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable; + parts_[0] = js_ast.Part{ .stmts = stmts }; + break :brk parts_; + } + }; + var ast = js_ast.Ast.fromParts(parts); + ast.symbols = js_ast.Symbol.List.init(symbols); + + return ParseResult{ + .ast = ast, + .source = source, + .loader = loader, + .input_fd = input_fd, + }; + }, + // TODO: use lazy export AST + .text => { + const expr = js_ast.Expr.init(js_ast.E.String, js_ast.E.String{ + .data = source.contents, + }, logger.Loc.Empty); + const stmt = js_ast.Stmt.alloc(js_ast.S.ExportDefault, js_ast.S.ExportDefault{ + .value = js_ast.StmtOrExpr{ .expr = expr }, + .default_name = js_ast.LocRef{ + .loc = logger.Loc{}, + .ref = Ref.None, + }, + }, logger.Loc{ .start = 0 }); + var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable; + stmts[0] = stmt; + var parts = allocator.alloc(js_ast.Part, 1) catch unreachable; + parts[0] = js_ast.Part{ .stmts = stmts }; + + return ParseResult{ + .ast = js_ast.Ast.initTest(parts), + .source = source, + .loader = loader, + .input_fd = input_fd, + }; + }, + .wasm => { + if (transpiler.options.target.isBun()) { + if (!source.isWebAssembly()) { + transpiler.log.addErrorFmt( + null, + logger.Loc.Empty, + transpiler.allocator, + "Invalid wasm file \"{s}\" (missing magic header)", + .{path.text}, + ) catch {}; + return null; + } + + return ParseResult{ + .ast = js_ast.Ast.empty, + .source = source, + .loader = loader, + .input_fd = input_fd, + }; + } + }, + .css => {}, + else => Output.panic("Unsupported loader {s} for path: {s}", .{ @tagName(loader), source.path.text }), + } + + return null; + } + + // This is public so it can be used by the HTTP handler when matching against public dir. + pub threadlocal var tmp_buildfile_buf: bun.PathBuffer = undefined; + threadlocal var tmp_buildfile_buf2: bun.PathBuffer = undefined; + threadlocal var tmp_buildfile_buf3: bun.PathBuffer = undefined; + + pub fn buildFile( + transpiler: *Transpiler, + log: *logger.Log, + path_to_use_: string, + comptime client_entry_point_enabled: bool, + ) !ServeResult { + const old_log = transpiler.log; + + transpiler.setLog(log); + defer transpiler.setLog(old_log); + + var path_to_use = path_to_use_; + + defer { + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); + } + + // All non-absolute paths are ./paths + if (path_to_use[0] != '/' and path_to_use[0] != '.') { + tmp_buildfile_buf3[0..2].* = "./".*; + @memcpy(tmp_buildfile_buf3[2..][0..path_to_use.len], path_to_use); + path_to_use = tmp_buildfile_buf3[0 .. 2 + path_to_use.len]; + } + + const resolved = if (comptime !client_entry_point_enabled) (try transpiler.resolver.resolve(transpiler.fs.top_level_dir, path_to_use, .stmt)) else brk: { + const absolute_pathname = Fs.PathName.init(path_to_use); + + const loader_for_ext = transpiler.options.loader(absolute_pathname.ext); + + // The expected pathname looks like: + // /pages/index.entry.tsx + // /pages/index.entry.js + // /pages/index.entry.ts + // /pages/index.entry.jsx + if (loader_for_ext.supportsClientEntryPoint()) { + const absolute_pathname_pathname = Fs.PathName.init(absolute_pathname.base); + + if (strings.eqlComptime(absolute_pathname_pathname.ext, ".entry")) { + const trail_dir = absolute_pathname.dirWithTrailingSlash(); + var len = trail_dir.len; + + bun.copy(u8, &tmp_buildfile_buf2, trail_dir); + bun.copy(u8, tmp_buildfile_buf2[len..], absolute_pathname_pathname.base); + len += absolute_pathname_pathname.base.len; + bun.copy(u8, tmp_buildfile_buf2[len..], absolute_pathname.ext); + len += absolute_pathname.ext.len; + + if (comptime Environment.allow_assert) bun.assert(len > 0); + + const decoded_entry_point_path = tmp_buildfile_buf2[0..len]; + break :brk try transpiler.resolver.resolve(transpiler.fs.top_level_dir, decoded_entry_point_path, .entry_point); + } + } + + break :brk (try transpiler.resolver.resolve(transpiler.fs.top_level_dir, path_to_use, .stmt)); + }; + + const path = (resolved.pathConst() orelse return error.ModuleNotFound); + + const loader = transpiler.options.loader(path.name.ext); + const mime_type_ext = transpiler.options.out_extensions.get(path.name.ext) orelse path.name.ext; + + switch (loader) { + .js, .jsx, .ts, .tsx, .css => { + return ServeResult{ + .file = options.OutputFile.initPending(loader, resolved), + .mime_type = MimeType.byLoader( + loader, + mime_type_ext[1..], + ), + }; + }, + .toml, .json => { + return ServeResult{ + .file = options.OutputFile.initPending(loader, resolved), + .mime_type = MimeType.transpiled_json, + }; + }, + else => { + const abs_path = path.text; + const file = try std.fs.openFileAbsolute(abs_path, .{ .mode = .read_only }); + const size = try file.getEndPos(); + return ServeResult{ + .file = options.OutputFile.initFile(file, abs_path, size), + .mime_type = MimeType.byLoader( + loader, + mime_type_ext[1..], + ), + }; + }, + } + } + + pub fn normalizeEntryPointPath(transpiler: *Transpiler, _entry: string) string { + var paths = [_]string{_entry}; + var entry = transpiler.fs.abs(&paths); + + std.fs.accessAbsolute(entry, .{}) catch + return _entry; + + entry = transpiler.fs.relativeTo(entry); + + if (!strings.startsWith(entry, "./")) { + // Entry point paths without a leading "./" are interpreted as package + // paths. This happens because they go through general path resolution + // like all other import paths so that plugins can run on them. Requiring + // a leading "./" for a relative path simplifies writing plugins because + // entry points aren't a special case. + // + // However, requiring a leading "./" also breaks backward compatibility + // and makes working with the CLI more difficult. So attempt to insert + // "./" automatically when needed. We don't want to unconditionally insert + // a leading "./" because the path may not be a file system path. For + // example, it may be a URL. So only insert a leading "./" when the path + // is an exact match for an existing file. + var __entry = transpiler.allocator.alloc(u8, "./".len + entry.len) catch unreachable; + __entry[0] = '.'; + __entry[1] = '/'; + bun.copy(u8, __entry[2..__entry.len], entry); + entry = __entry; + } + + return entry; + } + + fn enqueueEntryPoints(transpiler: *Transpiler, entry_points: []_resolver.Result, comptime normalize_entry_point: bool) usize { + var entry_point_i: usize = 0; + + for (transpiler.options.entry_points) |_entry| { + const entry: string = if (comptime normalize_entry_point) transpiler.normalizeEntryPointPath(_entry) else _entry; + + defer { + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); + } + + const result = transpiler.resolver.resolve(transpiler.fs.top_level_dir, entry, .entry_point_build) catch |err| { + Output.prettyError("Error resolving \"{s}\": {s}\n", .{ entry, @errorName(err) }); + continue; + }; + + if (result.pathConst() == null) { + Output.prettyError("\"{s}\" is disabled due to \"browser\" field in package.json.\n", .{ + entry, + }); + continue; + } + + if (transpiler.linker.enqueueResolveResult(&result) catch unreachable) { + entry_points[entry_point_i] = result; + entry_point_i += 1; + } + } + + return entry_point_i; + } + + pub fn transform( + transpiler: *Transpiler, + allocator: std.mem.Allocator, + log: *logger.Log, + opts: Api.TransformOptions, + ) !options.TransformResult { + _ = opts; + var entry_points = try allocator.alloc(_resolver.Result, transpiler.options.entry_points.len); + entry_points = entry_points[0..transpiler.enqueueEntryPoints(entry_points, true)]; + + if (log.level.atLeast(.debug)) { + transpiler.resolver.debug_logs = try DebugLogs.init(allocator); + } + transpiler.options.transform_only = true; + const did_start = false; + + if (transpiler.options.output_dir_handle == null) { + const outstream = bun.sys.File.from(std.io.getStdOut()); + + if (!did_start) { + try switch (transpiler.options.import_path_format) { + .relative => transpiler.processResolveQueue(.relative, false, @TypeOf(outstream), outstream), + .absolute_url => transpiler.processResolveQueue(.absolute_url, false, @TypeOf(outstream), outstream), + .absolute_path => transpiler.processResolveQueue(.absolute_path, false, @TypeOf(outstream), outstream), + .package_path => transpiler.processResolveQueue(.package_path, false, @TypeOf(outstream), outstream), + }; + } + } else { + const output_dir = transpiler.options.output_dir_handle orelse { + Output.printError("Invalid or missing output directory.", .{}); + Global.crash(); + }; + + if (!did_start) { + try switch (transpiler.options.import_path_format) { + .relative => transpiler.processResolveQueue(.relative, false, std.fs.Dir, output_dir), + .absolute_url => transpiler.processResolveQueue(.absolute_url, false, std.fs.Dir, output_dir), + .absolute_path => transpiler.processResolveQueue(.absolute_path, false, std.fs.Dir, output_dir), + .package_path => transpiler.processResolveQueue(.package_path, false, std.fs.Dir, output_dir), + }; + } + } + + // if (log.level == .verbose) { + // for (log.msgs.items) |msg| { + // try msg.writeFormat(std.io.getStdOut().writer()); + // } + // } + + if (transpiler.linker.any_needs_runtime) { + // try transpiler.output_files.append( + // options.OutputFile.initBuf( + // runtime.Runtime.source_code, + // bun.default_allocator, + // Linker.runtime_source_path, + // .js, + // null, + // null, + // ), + // ); + } + + if (FeatureFlags.tracing and transpiler.options.log.level.atLeast(.info)) { + Output.prettyErrorln( + "\n---Tracing---\nResolve time: {d}\nParsing time: {d}\n---Tracing--\n\n", + .{ + transpiler.resolver.elapsed, + transpiler.elapsed, + }, + ); + } + + var final_result = try options.TransformResult.init(try allocator.dupe(u8, transpiler.result.outbase), try transpiler.output_files.toOwnedSlice(), log, allocator); + final_result.root_dir = transpiler.options.output_dir_handle; + return final_result; + } + + // pub fn processResolveQueueWithThreadPool(transpiler) + + pub fn processResolveQueue( + transpiler: *Transpiler, + comptime import_path_format: options.BundleOptions.ImportPathFormat, + comptime wrap_entry_point: bool, + comptime Outstream: type, + outstream: Outstream, + ) !void { + // var count: u8 = 0; + while (transpiler.resolve_queue.readItem()) |item| { + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); + + // defer count += 1; + + if (comptime wrap_entry_point) { + const path = item.pathConst() orelse unreachable; + const loader = transpiler.options.loader(path.name.ext); + + if (item.import_kind == .entry_point and loader.supportsClientEntryPoint()) { + var client_entry_point = try transpiler.allocator.create(EntryPoints.ClientEntryPoint); + client_entry_point.* = EntryPoints.ClientEntryPoint{}; + try client_entry_point.generate(Transpiler, transpiler, path.name, transpiler.options.framework.?.client.path); + + const entry_point_output_file = transpiler.buildWithResolveResultEager( + item, + import_path_format, + Outstream, + outstream, + client_entry_point, + ) catch continue orelse continue; + transpiler.output_files.append(entry_point_output_file) catch unreachable; + + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); + + // At this point, the entry point will be de-duped. + // So we just immediately build it. + var item_not_entrypointed = item; + item_not_entrypointed.import_kind = .stmt; + const original_output_file = transpiler.buildWithResolveResultEager( + item_not_entrypointed, + import_path_format, + Outstream, + outstream, + null, + ) catch continue orelse continue; + transpiler.output_files.append(original_output_file) catch unreachable; + + continue; + } + } + + const output_file = transpiler.buildWithResolveResultEager( + item, + import_path_format, + Outstream, + outstream, + null, + ) catch continue orelse continue; + transpiler.output_files.append(output_file) catch unreachable; + + // if (count >= 3) return try transpiler.processResolveQueueWithThreadPool(import_path_format, wrap_entry_point, Outstream, outstream); + } + } +}; + +pub const ServeResult = struct { + file: options.OutputFile, + mime_type: MimeType, +}; +pub const ResolveResults = std.AutoHashMap( + u64, + void, +); +pub const ResolveQueue = std.fifo.LinearFifo( + _resolver.Result, + std.fifo.LinearFifoBufferType.Dynamic, +); diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000000..927851b4f9 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + // Path remapping + "baseUrl": ".", + "paths": { + "bindgen": ["./codegen/bindgen-lib.ts"], + } + }, + "include": ["**/*.ts", "**/*.tsx"], + // separate projects have extra settings that only apply in those scopes + "exclude": ["js", "bake"] +} diff --git a/src/url.zig b/src/url.zig index 3e7260aa8a..f96325ade9 100644 --- a/src/url.zig +++ b/src/url.zig @@ -93,6 +93,10 @@ pub const URL = struct { return strings.eqlComptime(this.protocol, "https"); } + pub inline fn isS3(this: *const URL) bool { + return strings.eqlComptime(this.protocol, "s3"); + } + pub inline fn isHTTP(this: *const URL) bool { return strings.eqlComptime(this.protocol, "http"); } @@ -105,6 +109,12 @@ pub const URL = struct { return "localhost"; } + pub fn s3Path(this: *const URL) string { + // we need to remove protocol if exists and ignore searchParams, should be host + pathname + const href = if (this.protocol.len > 0 and this.href.len > this.protocol.len + 2) this.href[this.protocol.len + 2 ..] else this.href; + return href[0 .. href.len - (this.search.len + this.hash.len)]; + } + pub fn displayHost(this: *const URL) bun.fmt.HostFormatter { return bun.fmt.HostFormatter{ .host = if (this.host.len > 0) this.host else this.displayHostname(), @@ -488,7 +498,7 @@ pub const QueryStringMap = struct { pub const Iterator = struct { // Assume no query string param map will exceed 2048 keys // Browsers typically limit URL lengths to around 64k - const VisitedMap = std.bit_set.ArrayBitSet(usize, 2048); + const VisitedMap = bun.bit_set.ArrayBitSet(usize, 2048); i: usize = 0, map: *const QueryStringMap, diff --git a/src/watcher.zig b/src/watcher.zig index 1a412eb7ef..1cd75586b5 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -9,7 +9,7 @@ const stringZ = bun.stringZ; const FeatureFlags = bun.FeatureFlags; const options = @import("./options.zig"); -const Mutex = @import("./lock.zig").Lock; +const Mutex = bun.Mutex; const Futex = @import("./futex.zig"); pub const WatchItemIndex = u16; const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; @@ -117,9 +117,7 @@ const INotify = struct { bun.assert(this.loaded_inotify); restart: while (true) { - Futex.wait(&this.watch_count, 0, null) catch |err| switch (err) { - error.TimedOut => unreachable, // timeout is infinite - }; + Futex.waitForever(&this.watch_count, 0); const rc = std.posix.system.read( this.inotify_fd, @@ -647,9 +645,6 @@ pub const NewWatcher = if (true) this.close_descriptors = close_descriptors; this.running = false; } else { - // if the mutex is locked, then that's now a UAF. - this.mutex.releaseAssertUnlocked("Watcher mutex is locked when it should not be."); - if (close_descriptors and this.running) { const fds = this.watchlist.items(.fd); for (fds) |fd| { diff --git a/src/which.zig b/src/which.zig index 093b141c4f..0470af9d61 100644 --- a/src/which.zig +++ b/src/which.zig @@ -20,8 +20,9 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []con bun.Output.scoped(.which, true)("path={s} cwd={s} bin={s}", .{ path, cwd, bin }); if (bun.Environment.os == .windows) { - var convert_buf: bun.WPathBuffer = undefined; - const result = whichWin(&convert_buf, path, cwd, bin) orelse return null; + const convert_buf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(convert_buf); + const result = whichWin(convert_buf, path, cwd, bin) orelse return null; const result_converted = bun.strings.convertUTF16toUTF8InBuffer(buf, result) catch unreachable; buf[result_converted.len] = 0; bun.assert(result_converted.ptr == buf.ptr); @@ -132,13 +133,14 @@ fn searchBinInPath(buf: *bun.WPathBuffer, path_buf: *bun.PathBuffer, path: []con /// It is similar to Get-Command in powershell. pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: []const u8) ?[:0]const u16 { if (bin.len == 0) return null; - var path_buf: bun.PathBuffer = undefined; + const path_buf = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(path_buf); const check_windows_extensions = !endsWithExtension(bin); // handle absolute paths if (std.fs.path.isAbsolute(bin)) { - const normalized_bin = PosixToWinNormalizer.resolveCWDWithExternalBuf(&path_buf, bin) catch return null; + const normalized_bin = PosixToWinNormalizer.resolveCWDWithExternalBuf(path_buf, bin) catch return null; const bin_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, normalized_bin); buf[bin_utf16.len] = 0; return searchBin(buf, bin_utf16.len, check_windows_extensions); @@ -148,7 +150,7 @@ pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: [ if (bun.strings.containsChar(bin, '/') or bun.strings.containsChar(bin, '\\')) { if (searchBinInPath( buf, - &path_buf, + path_buf, cwd, bun.strings.withoutPrefixComptime(bin, "./"), check_windows_extensions, @@ -163,7 +165,7 @@ pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: [ // iterate over system path delimiter var path_iter = std.mem.tokenizeScalar(u8, path, ';'); while (path_iter.next()) |segment_part| { - if (searchBinInPath(buf, &path_buf, segment_part, bin, check_windows_extensions)) |bin_path| { + if (searchBinInPath(buf, path_buf, segment_part, bin, check_windows_extensions)) |bin_path| { return bin_path; } } diff --git a/src/windows-app-info.rc b/src/windows-app-info.rc index 2822798ed0..361145deb4 100644 --- a/src/windows-app-info.rc +++ b/src/windows-app-info.rc @@ -1,6 +1,6 @@ #include "windows.h" -IDI_MYICON ICON "@BUN_ICO_PATH@" +IDI_MYICON ICON "bun.ico" VS_VERSION_INFO VERSIONINFO FILEVERSION @Bun_VERSION_MAJOR@,@Bun_VERSION_MINOR@,@Bun_VERSION_PATCH@,0 diff --git a/src/windows.zig b/src/windows.zig index e0e31a1fc9..3bc67c7d4b 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -72,6 +72,10 @@ pub const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; pub const nt_unc_object_prefix = [8]u16{ '\\', '?', '?', '\\', 'U', 'N', 'C', '\\' }; pub const nt_maxpath_prefix = [4]u16{ '\\', '\\', '?', '\\' }; +pub const nt_object_prefix_u8 = [4]u8{ '\\', '?', '?', '\\' }; +pub const nt_unc_object_prefix_u8 = [8]u8{ '\\', '?', '?', '\\', 'U', 'N', 'C', '\\' }; +pub const nt_maxpath_prefix_u8 = [4]u8{ '\\', '\\', '?', '\\' }; + const std = @import("std"); const Environment = bun.Environment; @@ -3064,7 +3068,7 @@ pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { } else .BUSY, .OBJECT_NAME_INVALID => if (comptime Environment.isDebug) brk: { bun.Output.debugWarn("Received OBJECT_NAME_INVALID, indicates a file path conversion issue.", .{}); - std.debug.dumpCurrentStackTrace(null); + bun.crash_handler.dumpCurrentStackTrace(null); break :brk .INVAL; } else .INVAL, @@ -3623,3 +3627,33 @@ pub const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000; pub const JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x400; pub const JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x800; pub const JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000; + +const pe_header_offset_location = 0x3C; +const subsystem_offset = 0x5C; + +pub const Subsystem = enum(u16) { + windows_gui = 2, +}; + +pub fn editWin32BinarySubsystem(fd: bun.sys.File, subsystem: Subsystem) !void { + comptime bun.assert(bun.Environment.isWindows); + if (bun.windows.SetFilePointerEx(fd.handle.cast(), pe_header_offset_location, null, std.os.windows.FILE_BEGIN) == 0) + return error.Win32Error; + const offset = try fd.reader().readInt(u32, .little); + if (bun.windows.SetFilePointerEx(fd.handle.cast(), offset + subsystem_offset, null, std.os.windows.FILE_BEGIN) == 0) + return error.Win32Error; + try fd.writer().writeInt(u16, @intFromEnum(subsystem), .little); +} + +pub const rescle = struct { + extern fn rescle__setIcon([*:0]const u16, [*:0]const u16) c_int; + + pub fn setIcon(exe_path: [*:0]const u16, icon: [*:0]const u16) !void { + comptime bun.assert(bun.Environment.isWindows); + const status = rescle__setIcon(exe_path, icon); + return switch (status) { + 0 => {}, + else => error.IconEditError, + }; + } +}; diff --git a/src/windows_c.zig b/src/windows_c.zig index 7c0c5d0d9e..956fc5d81c 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -8,6 +8,42 @@ const Stat = std.fs.File.Stat; const Kind = std.fs.File.Kind; const StatError = std.fs.File.StatError; +// Windows doesn't have memmem, so we need to implement it +pub export fn memmem(haystack: ?[*]const u8, haystacklen: usize, needle: ?[*]const u8, needlelen: usize) ?[*]const u8 { + // Handle null pointers + if (haystack == null or needle == null) return null; + + // Handle empty needle case + if (needlelen == 0) return haystack; + + // Handle case where needle is longer than haystack + if (needlelen > haystacklen) return null; + + const hay = haystack.?[0..haystacklen]; + const nee = needle.?[0..needlelen]; + + const i = std.mem.indexOf(u8, hay, nee) orelse return null; + return hay.ptr + i; +} + +comptime { + @export(memmem, .{ .name = "zig_memmem" }); +} + +pub const lstat = blk: { + const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + break :blk @extern(T, .{ .name = "lstat64" }); +}; + +pub const fstat = blk: { + const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + break :blk @extern(T, .{ .name = "fstat64" }); +}; +pub const stat = blk: { + const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + break :blk @extern(T, .{ .name = "stat64" }); +}; + pub fn getTotalMemory() usize { return uv.uv_get_total_memory(); } @@ -814,149 +850,6 @@ pub const SystemErrno = enum(u16) { if (code >= max) return null; return @as(SystemErrno, @enumFromInt(code)); } - - pub fn label(this: SystemErrno) ?[:0]const u8 { - return labels.get(this) orelse null; - } - - const LabelMap = std.enums.EnumMap(SystemErrno, [:0]const u8); - pub const labels: LabelMap = brk: { - var map: LabelMap = LabelMap.initFull(""); - - map.put(.EPERM, "Operation not permitted"); - map.put(.ENOENT, "No such file or directory"); - map.put(.ESRCH, "No such process"); - map.put(.EINTR, "Interrupted system call"); - map.put(.EIO, "I/O error"); - map.put(.ENXIO, "No such device or address"); - map.put(.E2BIG, "Argument list too long"); - map.put(.ENOEXEC, "Exec format error"); - map.put(.EBADF, "Bad file descriptor"); - map.put(.ECHILD, "No child processes"); - map.put(.EAGAIN, "Try again"); - map.put(.EOF, "End of file"); - map.put(.ENOMEM, "Out of memory"); - map.put(.EACCES, "Permission denied"); - map.put(.EFAULT, "Bad address"); - map.put(.ENOTBLK, "Block device required"); - map.put(.EBUSY, "Device or resource busy"); - map.put(.EEXIST, "File or folder exists"); - map.put(.EXDEV, "Cross-device link"); - map.put(.ENODEV, "No such device"); - map.put(.ENOTDIR, "Not a directory"); - map.put(.EISDIR, "Is a directory"); - map.put(.EINVAL, "Invalid argument"); - map.put(.ENFILE, "File table overflow"); - map.put(.EMFILE, "Too many open files"); - map.put(.ECHARSET, "Invalid or incomplete multibyte or wide character"); - map.put(.ENOTTY, "Not a typewriter"); - map.put(.ETXTBSY, "Text file busy"); - map.put(.EFBIG, "File too large"); - map.put(.ENOSPC, "No space left on device"); - map.put(.ESPIPE, "Illegal seek"); - map.put(.EROFS, "Read-only file system"); - map.put(.EMLINK, "Too many links"); - map.put(.EPIPE, "Broken pipe"); - map.put(.EDOM, "Math argument out of domain of func"); - map.put(.ERANGE, "Math result not representable"); - map.put(.EDEADLK, "Resource deadlock would occur"); - map.put(.ENAMETOOLONG, "File name too long"); - map.put(.ENOLCK, "No record locks available"); - map.put(.EUNKNOWN, "An unknown error occurred"); - map.put(.ENOSYS, "Function not implemented"); - map.put(.ENOTEMPTY, "Directory not empty"); - map.put(.ELOOP, "Too many symbolic links encountered"); - map.put(.ENOMSG, "No message of desired type"); - map.put(.EIDRM, "Identifier removed"); - map.put(.ECHRNG, "Channel number out of range"); - map.put(.EL2NSYNC, "Level 2 not synchronized"); - map.put(.EL3HLT, "Level 3 halted"); - map.put(.EL3RST, "Level 3 reset"); - map.put(.ELNRNG, "Link number out of range"); - map.put(.EUNATCH, "Protocol driver not attached"); - map.put(.ENOCSI, "No CSI structure available"); - map.put(.EL2HLT, "Level 2 halted"); - map.put(.EBADE, "Invalid exchange"); - map.put(.EBADR, "Invalid request descriptor"); - map.put(.EXFULL, "Exchange full"); - map.put(.ENOANO, "No anode"); - map.put(.EBADRQC, "Invalid request code"); - map.put(.EBADSLT, "Invalid slot"); - map.put(.EBFONT, "Bad font file format"); - map.put(.ENOSTR, "Device not a stream"); - map.put(.ENODATA, "No data available"); - map.put(.ETIME, "Timer expired"); - map.put(.ENOSR, "Out of streams resources"); - map.put(.ENONET, "Machine is not on the network"); - map.put(.ENOPKG, "Package not installed"); - map.put(.EREMOTE, "Object is remote"); - map.put(.ENOLINK, "Link has been severed"); - map.put(.EADV, "Advertise error"); - map.put(.ESRMNT, "Srmount error"); - map.put(.ECOMM, "Communication error on send"); - map.put(.EPROTO, "Protocol error"); - map.put(.EMULTIHOP, "Multihop attempted"); - map.put(.EDOTDOT, "RFS specific error"); - map.put(.EBADMSG, "Not a data message"); - map.put(.EOVERFLOW, "Value too large for defined data type"); - map.put(.ENOTUNIQ, "Name not unique on network"); - map.put(.EBADFD, "File descriptor in bad state"); - map.put(.EREMCHG, "Remote address changed"); - map.put(.ELIBACC, "Can not access a needed shared library"); - map.put(.ELIBBAD, "Accessing a corrupted shared library"); - map.put(.ELIBSCN, "lib section in a.out corrupted"); - map.put(.ELIBMAX, "Attempting to link in too many shared libraries"); - map.put(.ELIBEXEC, "Cannot exec a shared library directly"); - map.put(.EILSEQ, "Illegal byte sequence"); - map.put(.ERESTART, "Interrupted system call should be restarted"); - map.put(.ESTRPIPE, "Streams pipe error"); - map.put(.EUSERS, "Too many users"); - map.put(.ENOTSOCK, "Socket operation on non-socket"); - map.put(.EDESTADDRREQ, "Destination address required"); - map.put(.EMSGSIZE, "Message too long"); - map.put(.EPROTOTYPE, "Protocol wrong type for socket"); - map.put(.ENOPROTOOPT, "Protocol not available"); - map.put(.EPROTONOSUPPORT, "Protocol not supported"); - map.put(.ESOCKTNOSUPPORT, "Socket type not supported"); - map.put(.ENOTSUP, "Operation not supported on transport endpoint"); - map.put(.EPFNOSUPPORT, "Protocol family not supported"); - map.put(.EAFNOSUPPORT, "Address family not supported by protocol"); - map.put(.EADDRINUSE, "Address already in use"); - map.put(.EADDRNOTAVAIL, "Cannot assign requested address"); - map.put(.ENETDOWN, "Network is down"); - map.put(.ENETUNREACH, "Network is unreachable"); - map.put(.ENETRESET, "Network dropped connection because of reset"); - map.put(.ECONNABORTED, "Software caused connection abort"); - map.put(.ECONNRESET, "Connection reset by peer"); - map.put(.ENOBUFS, "No buffer space available"); - map.put(.EISCONN, "Transport endpoint is already connected"); - map.put(.ENOTCONN, "Transport endpoint is not connected"); - map.put(.ESHUTDOWN, "Cannot send after transport endpoint shutdown"); - map.put(.ETOOMANYREFS, "Too many references: cannot splice"); - map.put(.ETIMEDOUT, "Connection timed out"); - map.put(.ECONNREFUSED, "Connection refused"); - map.put(.EHOSTDOWN, "Host is down"); - map.put(.EHOSTUNREACH, "No route to host"); - map.put(.EALREADY, "Operation already in progress"); - map.put(.EINPROGRESS, "Operation now in progress"); - map.put(.ESTALE, "Stale NFS file handle"); - map.put(.EUCLEAN, "Structure needs cleaning"); - map.put(.ENOTNAM, "Not a XENIX named type file"); - map.put(.ENAVAIL, "No XENIX semaphores available"); - map.put(.EISNAM, "Is a named type file"); - map.put(.EREMOTEIO, "Remote I/O error"); - map.put(.EDQUOT, "Quota exceeded"); - map.put(.ENOMEDIUM, "No medium found"); - map.put(.EMEDIUMTYPE, "Wrong medium type"); - map.put(.ECANCELED, "Operation Canceled"); - map.put(.ENOKEY, "Required key not available"); - map.put(.EKEYEXPIRED, "Key has expired"); - map.put(.EKEYREVOKED, "Key has been revoked"); - map.put(.EKEYREJECTED, "Key was rejected by service"); - map.put(.EOWNERDEAD, "Owner died"); - map.put(.ENOTRECOVERABLE, "State not recoverable"); - break :brk map; - }; }; pub const UV_E2BIG = -uv.UV_E2BIG; diff --git a/test/bake/dev-server-harness.ts b/test/bake/dev-server-harness.ts index 947b37d132..8714d208dc 100644 --- a/test/bake/dev-server-harness.ts +++ b/test/bake/dev-server-harness.ts @@ -8,7 +8,7 @@ import { test } from "bun:test"; import { EventEmitter } from "node:events"; // @ts-ignore import { dedent } from "../bundler/expectBundled.ts"; -import { bunEnv, isWindows, mergeWindowEnvs } from "harness"; +import { bunEnv, isCI, isWindows, mergeWindowEnvs } from "harness"; import { expect } from "bun:test"; /** For testing bundler related bugs in the DevServer */ @@ -27,21 +27,31 @@ export const minimalFramework: Bake.Framework = { }, }; -export interface DevServerTest { - /** - * Framework to use. Consider `minimalFramework` if possible. - * Provide this object or `files['bun.app.ts']` for a dynamic one. - */ - framework?: Bake.Framework | "react"; - /** - * Source code for a TSX file that `export default`s an array of BunPlugin, - * combined with the `framework` option. - */ - pluginFile?: string; - /** Starting files */ - files: FileObject; +export type DevServerTest = ( + | { + /** Starting files */ + files: FileObject; + /** + * Framework to use. Consider `minimalFramework` if possible. + * Provide this object or `files['bun.app.ts']` for a dynamic one. + */ + framework?: Bake.Framework | "react"; + /** + * Source code for a TSX file that `export default`s an array of BunPlugin, + * combined with the `framework` option. + */ + pluginFile?: string; + } + | { + /** + * Copy all files from test/bake/fixtures/ + * This directory must contain `bun.app.ts` to allow hacking on fixtures manually via `bun run .` + */ + fixture: string; + } +) & { test: (dev: Dev) => Promise; -} +}; type FileObject = Record; @@ -67,9 +77,9 @@ export class Dev { } fetch(url: string, init?: RequestInit) { - return new DevFetchPromise((resolve, reject) => - fetch(new URL(url, this.baseUrl).toString(), init).then(resolve, reject), - this + return new DevFetchPromise( + (resolve, reject) => fetch(new URL(url, this.baseUrl).toString(), init).then(resolve, reject), + this, ); } @@ -113,7 +123,10 @@ export class Dev { await Promise.race([ // On failure, give a little time in case a partial write caused a // bundling error, and a success came in. - err.then(() => Bun.sleep(500), () => {}), + err.then( + () => Bun.sleep(500), + () => {}, + ), success, ]); } @@ -131,7 +144,10 @@ export interface Step { class DevFetchPromise extends Promise { dev: Dev; - constructor(executor: (resolve: (value: Response | PromiseLike) => void, reject: (reason?: any) => void) => void, dev: Dev) { + constructor( + executor: (resolve: (value: Response | PromiseLike) => void, reject: (reason?: any) => void) => void, + dev: Dev, + ) { super(executor); this.dev = dev; } @@ -177,18 +193,52 @@ class DevFetchPromise extends Promise { function snapshotCallerLocation(): string { const stack = new Error().stack!; - const lines = stack.split("\n"); + const lines = stack.replaceAll("\r\n", "\n").split("\n"); let i = 1; for (; i < lines.length; i++) { - if (!lines[i].includes(import.meta.filename)) { - return lines[i]; + const line = lines[i].replaceAll("\\", "/"); + if (line.includes(import.meta.path.replaceAll("\\", "/"))) { + return line; } } throw new Error("Couldn't find caller location in stack trace"); } - function stackTraceFileName(line: string): string { - return / \(((?:[A-Za-z]:)?.*?)[:)]/.exec(line)![1].replaceAll("\\", "/"); + let result = line.trim(); + + // Remove leading "at " and any parentheses + if (result.startsWith("at ")) { + result = result.slice(3).trim(); + } + + // Handle case with angle brackets like "" + const angleStart = result.indexOf("<"); + const angleEnd = result.indexOf(">"); + if (angleStart >= 0 && angleEnd > angleStart) { + result = result.slice(angleEnd + 1).trim(); + } + + // Remove parentheses and everything after colon + const openParen = result.indexOf("("); + if (openParen >= 0) { + result = result.slice(openParen + 1).trim(); + } + + // Handle drive letters (e.g. C:) and line numbers + let colon = result.indexOf(":"); + + // Check for drive letter (e.g. C:) by looking for single letter before colon + if (colon > 0 && /[a-zA-Z]/.test(result[colon - 1])) { + // On Windows, skip past drive letter colon to find line number colon + colon = result.indexOf(":", colon + 1); + } + + if (colon >= 0) { + result = result.slice(0, colon); + } + + result = result.trim(); + return result.replaceAll("\\", "/"); } async function withAnnotatedStack(stackLine: string, cb: () => Promise): Promise { @@ -319,39 +369,53 @@ export function devTest(description: string, options: T const basename = path.basename(caller, ".test" + path.extname(caller)); const count = (counts[basename] = (counts[basename] ?? 0) + 1); - // TODO: Tests are too flaky on Windows. Cannot reproduce locally. - if (isWindows) { + // TODO: Tests are flaky on all platforms. Disable + if (isCI) { jest.test.todo(`DevServer > ${basename}.${count}: ${description}`); return options; } jest.test(`DevServer > ${basename}.${count}: ${description}`, async () => { const root = path.join(tempDir, basename + count); - writeAll(root, options.files); - if (options.files["bun.app.ts"] == undefined) { - if (!options.framework) { - throw new Error("Must specify a options.framework or provide a bun.app.ts file"); + if ("files" in options) { + writeAll(root, options.files); + if (options.files["bun.app.ts"] == undefined) { + if (!options.framework) { + throw new Error("Must specify a options.framework or provide a bun.app.ts file"); + } + if (options.pluginFile) { + fs.writeFileSync(path.join(root, "pluginFile.ts"), dedent(options.pluginFile)); + } + fs.writeFileSync( + path.join(root, "bun.app.ts"), + dedent` + ${options.pluginFile ? `import plugins from './pluginFile.ts';` : "let plugins = undefined;"} + export default { + app: { + framework: ${JSON.stringify(options.framework)}, + plugins, + }, + }; + `, + ); + } else { + if (options.pluginFile) { + throw new Error("Cannot provide both bun.app.ts and pluginFile"); + } } - if (options.pluginFile) { - fs.writeFileSync(path.join(root, "pluginFile.ts"), dedent(options.pluginFile)); - } - fs.writeFileSync( - path.join(root, "bun.app.ts"), - dedent` - ${options.pluginFile ? - `import plugins from './pluginFile.ts';` : "let plugins = undefined;" - } - export default { - app: { - framework: ${JSON.stringify(options.framework)}, - plugins, - }, - }; - `, - ); } else { - if (options.pluginFile) { - throw new Error("Cannot provide both bun.app.ts and pluginFile"); + if (!options.fixture) { + throw new Error("Must provide either `fixture` or `files`"); + } + const fixture = path.join(devTestRoot, "../fixtures", options.fixture); + fs.cpSync(fixture, root, { recursive: true }); + + if (!fs.existsSync(path.join(root, "bun.app.ts"))) { + throw new Error(`Fixture ${fixture} must contain a bun.app.ts file.`); + } + if (!fs.existsSync(path.join(root, "node_modules"))) { + // link the node_modules directory from test/node_modules to the temp directory + fs.symlinkSync(path.join(devTestRoot, "../../node_modules"), path.join(root, "node_modules"), "junction"); } } fs.writeFileSync( @@ -359,8 +423,8 @@ export function devTest(description: string, options: T dedent` import appConfig from "./bun.app.ts"; export default { + ...appConfig, port: 0, - ...appConfig }; `, ); @@ -373,6 +437,7 @@ export function devTest(description: string, options: T { FORCE_COLOR: "1", BUN_DEV_SERVER_TEST_RUNNER: "1", + BUN_DUMP_STATE_ON_CRASH: "1", }, ]), stdio: ["pipe", "pipe", "pipe"], diff --git a/test/bake/dev/ecosystem.test.ts b/test/bake/dev/ecosystem.test.ts index 068795a875..bb0e58d89e 100644 --- a/test/bake/dev/ecosystem.test.ts +++ b/test/bake/dev/ecosystem.test.ts @@ -2,10 +2,22 @@ // should be preferred to write specific tests for the bugs that these libraries // discovered, but it easy and still a reasonable idea to just test the library // entirely. +import { expect } from "bun:test"; import { devTest } from "../dev-server-harness"; -// TODO: svelte server component example project // Bugs discovered thanks to Svelte: -// - Valid circular import use. -// - Re-export `.e_import_identifier`, including live bindings. -// TODO: - something related to the wrong push function being called \ No newline at end of file +// - Circular import situations +// - export { live_binding } +// - export { x as y } +devTest('svelte component islands example', { + fixture: 'svelte-component-islands', + async test(dev) { + const html = await dev.fetch('/').text() + if (html.includes('Bun__renderFallbackError')) throw new Error('failed'); + expect(html).toContain('self.$islands={\"pages/_Counter.svelte\":[[0,\"default\",{initial:5}]]}'); + expect(html).toContain(`

This is my svelte server component (non-interactive)

Bun v${Bun.version}

`); + expect(html).toContain(`>This is a client component (interactive island)

`); + // TODO: puppeteer test for client-side interactivity, hmr. + // care must be taken to implement this in a way that is not flaky. + }, +}); diff --git a/test/bake/dev/esm.test.ts b/test/bake/dev/esm.test.ts index 1864c5e387..b08fcf1418 100644 --- a/test/bake/dev/esm.test.ts +++ b/test/bake/dev/esm.test.ts @@ -135,4 +135,73 @@ devTest("export { x as y }", { }); await dev.fetch("/").expect("Value: 3"); } -}); \ No newline at end of file +}); +devTest("import { x as y }", { + framework: minimalFramework, + files: { + "module.ts": ` + export const x = 1; + `, + "routes/index.ts": ` + import { x as y } from '../module'; + export default function(req, meta) { + return new Response('Value: ' + y); + } + `, + }, + async test(dev) { + await dev.fetch("/").expect("Value: 1"); + await dev.patch("module.ts", { + find: "1", + replace: "2", + }); + await dev.fetch("/").expect("Value: 2"); + } +}); +devTest("import { default as y }", { + framework: minimalFramework, + files: { + "module.ts": ` + export default 1; + `, + "routes/index.ts": ` + import { default as y } from '../module'; + export default function(req, meta) { + return new Response('Value: ' + y); + } + `, + }, + async test(dev) { + await dev.fetch("/").expect("Value: 1"); + await dev.patch("module.ts", { + find: "1", + replace: "2", + }); + await dev.fetch("/").expect("Value: 2"); + } +}); +devTest("export { default as y }", { + framework: minimalFramework, + files: { + "module.ts": ` + export default 1; + `, + "middle.ts": ` + export { default as y } from './module'; + `, + "routes/index.ts": ` + import { y } from '../middle'; + export default function(req, meta) { + return new Response('Value: ' + y); + } + `, + }, + async test(dev) { + await dev.fetch("/").expect("Value: 1"); + await dev.patch("module.ts", { + find: "1", + replace: "2", + }); + await dev.fetch("/").expect("Value: 2"); + } +}); diff --git a/test/bake/fixtures/svelte-component-islands/bun.app.ts b/test/bake/fixtures/svelte-component-islands/bun.app.ts new file mode 100644 index 0000000000..9813768a22 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/bun.app.ts @@ -0,0 +1,8 @@ +import svelte from "./framework"; + +export default { + port: 3000, + app: { + framework: svelte(), + }, +}; diff --git a/test/bake/fixtures/svelte-component-islands/framework/client.ts b/test/bake/fixtures/svelte-component-islands/framework/client.ts new file mode 100644 index 0000000000..aca37275e0 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/framework/client.ts @@ -0,0 +1,14 @@ +import type { IslandMap } from "./server"; +import { hydrate } from 'svelte'; + +declare var $islands: IslandMap; +Object.entries($islands).forEach(async([moduleId, islands]) => { + const mod = await import(moduleId); + for(const [islandId, exportId, props] of islands) { + const elem = document.getElementById(`I:${islandId}`)!; + hydrate(mod[exportId], { + target: elem, + props, + }); + } +}); diff --git a/test/bake/fixtures/svelte-component-islands/framework/index.ts b/test/bake/fixtures/svelte-component-islands/framework/index.ts new file mode 100644 index 0000000000..7cecf75358 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/framework/index.ts @@ -0,0 +1,68 @@ +import type { Bake } from "bun"; +import * as path from "node:path"; +import * as fs from "node:fs/promises"; +import * as svelte from "svelte/compiler"; + +export default function (): Bake.Framework { + return { + serverComponents: { + separateSSRGraph: false, + serverRuntimeImportSource: "./framework/server.ts", + }, + fileSystemRouterTypes: [ + { + root: "pages", + serverEntryPoint: "./framework/server.ts", + clientEntryPoint: "./framework/client.ts", + style: "nextjs-pages", // later, this will be fully programmable + extensions: [".svelte"], + }, + ], + plugins: [ + { + // This is missing a lot of code that a plugin like `esbuild-svelte` + // handles, but this is only an examplea of how such a plugin could + // have server-components at a minimal level. + name: "svelte-server-components", + setup(b) { + const cssMap = new Map(); + b.onLoad({ filter: /.svelte$/ }, async (args) => { + const contents = await fs.readFile(args.path, "utf-8"); + const result = svelte.compile(contents, { + filename: args.path, + css: "external", + cssOutputFilename: path.basename(args.path, ".svelte") + ".css", + hmr: true, + dev: true, + generate: args.side, + }); + // If CSS is specified, add a CSS import + let jsCode = result.js.code; + if (result.css) { + cssMap.set(args.path, result.css.code); + jsCode = `import ${JSON.stringify("svelte-css:" + args.path)};` + jsCode; + } + // Extract a "use client" directive from the file. + const header = contents.match(/^\s*\s*("[^"\n]*"|'[^'\n]*')/)?.[1]; + if (header) { + jsCode = header + ';' + jsCode; + } + return { + contents: jsCode, + loader: "js", + watchFiles: [args.path], + }; + }); + + // Resolve CSS files + b.onResolve({ filter: /^svelte-css:/ }, async (args) => { + return { path: args.path.replace(/^svelte-css:/, ""), namespace: "svelte-css" }; + }); + b.onLoad({ filter: /./, namespace: "svelte-css" }, async (args) => { + return { contents: cssMap.get(args.path) ?? "", loader: "css" }; + }); + }, + }, + ], + }; +} diff --git a/test/bake/fixtures/svelte-component-islands/framework/server.ts b/test/bake/fixtures/svelte-component-islands/framework/server.ts new file mode 100644 index 0000000000..29253245f8 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/framework/server.ts @@ -0,0 +1,71 @@ +/// +import type { Bake } from "bun"; +import * as svelte from "svelte/server"; +import { uneval } from "devalue"; + +export function render(req: Request, meta: Bake.RouteMetadata) { + isInsideIsland = false; + islands = {}; + const { body, head } = svelte.render(meta.pageModule.default, { + props: { + params: meta.params, + }, + }); + + // Add stylesheets and preloaded modules to the head + const extraHead = meta.styles.map((style) => ``).join("") + + meta.modulepreload.map((style) => ``).join(""); + // Script tags + const scripts = nextIslandId > 0 + ? `` + + meta.modules.map((module) => ``).join("") + : ""; // If no islands, no JavaScript + + return new Response( + "" + head + extraHead + "" + + body + "" + scripts + "", + { headers: { "content-type": "text/html" } }, + ); +} + +// To allow static site generation, frameworks can specify a prerender function +export function prerender(meta: Bake.RouteMetadata) { + return { + files: { + '/index.html': render(null!, meta), + }, + }; +} + +let isInsideIsland = false; +let nextIslandId = 0; +let islands: IslandMap; +export type IslandMap = Record; +export type Island = [islandId: number, exportId: string, props: any]; + +/** + * @param component The original export value, as is. + * @param clientModuleId A string that the browser will pass to `import()`. + * @param clientExportId The export ID from the imported module. + * @returns A wrapped value for the export. + */ +export function registerClientReference( + component: Function, + clientModuleId: string, + clientExportId: string, +) { + return function Island(...args: any[]) { + if (isInsideIsland) { + return component(...args); + } + isInsideIsland = true; + const [payload, props] = args; + const islandId = nextIslandId++; + payload.out += ``; + const file = (islands[clientModuleId] ??= []); + file.push([islandId, clientExportId, props]); + component(...args); + payload.out += ``; + isInsideIsland = false; + }; +} diff --git a/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte b/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte new file mode 100644 index 0000000000..7c4a618edb --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte @@ -0,0 +1,24 @@ + + +
+

This is a client component (interactive island)

+ +
+ diff --git a/test/bake/fixtures/svelte-component-islands/pages/index.svelte b/test/bake/fixtures/svelte-component-islands/pages/index.svelte new file mode 100644 index 0000000000..f5b8c0728c --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/pages/index.svelte @@ -0,0 +1,18 @@ + +
+

hello

+

This is my svelte server component (non-interactive)

+

Bun v{Bun.version}

+ +
+ \ No newline at end of file diff --git a/test/bun.lockb b/test/bun.lockb index 6a1061e911..2d9775c983 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index df1624622e..195f8fbe1d 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; import { bunEnv, bunExe, tempDirWithFiles } from "harness"; import path, { join } from "path"; +import assert from "assert"; describe("Bun.build", () => { test("experimentalCss = true works", async () => { @@ -175,6 +176,26 @@ describe("Bun.build", () => { Bun.gc(true); }); + test("`throw: true` works", async () => { + Bun.gc(true); + try { + await Bun.build({ + entrypoints: [join(import.meta.dir, "does-not-exist.ts")], + throw: true, + }); + expect.unreachable(); + } catch (e) { + assert(e instanceof AggregateError); + expect(e.errors).toHaveLength(1); + expect(e.errors[0]).toBeInstanceOf(BuildMessage); + expect(e.errors[0].message).toMatch(/ModuleNotFound/); + expect(e.errors[0].name).toBe("BuildMessage"); + expect(e.errors[0].position).toEqual(null); + expect(e.errors[0].level).toEqual("error"); + Bun.gc(true); + } + }); + test("returns output files", async () => { Bun.gc(true); const build = await Bun.build({ @@ -546,4 +567,78 @@ describe("Bun.build", () => { expect(await bundle.outputs[0].text()).toBe("var o=/*@__PURE__*/console.log(1);export{o as OUT};\n"); }); + + test("you can write onLoad and onResolve plugins using the 'html' loader, and it includes script and link tags as bundled entrypoints", async () => { + const fixture = tempDirWithFiles("build-html-plugins", { + "index.html": ` + + + + + + + + `, + "style.css": ".foo { color: red; }", + + // Check we actually do bundle the script + "script.js": "console.log(1 + 2)", + }); + + let onLoadCalled = false; + let onResolveCalled = false; + + const build = await Bun.build({ + entrypoints: [join(fixture, "index.html")], + html: true, + experimentalCss: true, + minify: { + syntax: true, + }, + plugins: [ + { + name: "test-plugin", + setup(build) { + build.onLoad({ filter: /\.html$/ }, async args => { + onLoadCalled = true; + const contents = await Bun.file(args.path).text(); + return { + contents: contents.replace("", ""), + loader: "html", + }; + }); + + build.onResolve({ filter: /\.(js|css)$/ }, args => { + onResolveCalled = true; + return { + path: join(fixture, args.path), + namespace: "file", + }; + }); + }, + }, + ], + }); + + expect(build.success).toBe(true); + expect(onLoadCalled).toBe(true); + expect(onResolveCalled).toBe(true); + + // Should have 3 outputs - HTML, JS and CSS + expect(build.outputs).toHaveLength(3); + + // Verify we have one of each type + const types = build.outputs.map(o => o.type); + expect(types).toContain("text/html;charset=utf-8"); + expect(types).toContain("text/javascript;charset=utf-8"); + expect(types).toContain("text/css;charset=utf-8"); + + // Verify the JS output contains the __dirname + const js = build.outputs.find(o => o.type === "text/javascript;charset=utf-8"); + expect(await js?.text()).toContain("console.log(3)"); + + // Verify our plugin modified the HTML + const html = build.outputs.find(o => o.type === "text/html;charset=utf-8"); + expect(await html?.text()).toContain(""); + }); }); diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 43b29c517e..9638fe5254 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -102,4 +102,51 @@ error: Hello World`, }, run: { stdout: "" }, }); + if (Bun.version.startsWith("1.2")) { + throw new Error("TODO: enable these tests please"); + for (const backend of ["api", "cli"] as const) { + itBundled("bun/ExportsConditionsDevelopment" + backend.toUpperCase(), { + files: { + "src/entry.js": `import 'pkg1'`, + "node_modules/pkg1/package.json": /* json */ ` + { + "exports": { + "development": "./custom1.js", + "default": "./default.js" + } + } + `, + "node_modules/pkg1/custom1.js": `console.log('SUCCESS')`, + "node_modules/pkg1/default.js": `console.log('FAIL')`, + }, + backend, + outfile: "out.js", + define: { "process.env.NODE_ENV": '"development"' }, + run: { + stdout: "SUCCESS", + }, + }); + itBundled("bun/ExportsConditionsDevelopmentInProduction" + backend.toUpperCase(), { + files: { + "src/entry.js": `import 'pkg1'`, + "node_modules/pkg1/package.json": /* json */ ` + { + "exports": { + "development": "./custom1.js", + "default": "./default.js" + } + } + `, + "node_modules/pkg1/custom1.js": `console.log('FAIL')`, + "node_modules/pkg1/default.js": `console.log('SUCCESS')`, + }, + backend, + outfile: "/Users/user/project/out.js", + define: { "process.env.NODE_ENV": '"production"' }, + run: { + stdout: "SUCCESS", + }, + }); + } + } }); diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index d4c3610527..3949d409ea 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -73,7 +73,7 @@ describe.todoIf(isFlaky && isWindows)("bundler", () => { import {rmSync} from 'fs'; // Verify we're not just importing from the filesystem rmSync("./worker.ts", {force: true}); - + console.log("Hello, world!"); new Worker("./worker"); `, diff --git a/test/bundler/bundler_defer.test.ts b/test/bundler/bundler_defer.test.ts index c3becd5ac4..7f9bca5f13 100644 --- a/test/bundler/bundler_defer.test.ts +++ b/test/bundler/bundler_defer.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; -import { itBundled } from './expectBundled'; -import { bunExe, bunEnv, tempDirWithFiles } from 'harness'; -import * as path from 'node:path'; +import { itBundled } from "./expectBundled"; +import { bunExe, bunEnv, tempDirWithFiles } from "harness"; +import * as path from "node:path"; describe("defer", () => { { @@ -179,10 +179,12 @@ describe("defer", () => { }, }, ], + throw: true, }); console.log(result); - } catch (err) { + } catch (err: any) { expect(err).toBeDefined(); + expect(err.message).toBe("WOOPS"); return; } throw new Error("DIDNT GET ERROR!"); @@ -213,15 +215,15 @@ describe("defer", () => { console.log("Foo", foo, lmao); `, - "/lmao.ts": ` + "/lmao.ts": ` import { foo } from "./foo.ts"; export const lmao = "lolss"; console.log(foo); `, - "/foo.ts": ` + "/foo.ts": ` export const foo = 'lkdfjlsdf'; console.log('hi')`, - "/a.css": ` + "/a.css": ` h1 { color: blue; } diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 9d46ebf0b3..a3bf53f778 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -186,18 +186,7 @@ describe("bundler", () => { NODE_ENV: "development", }, }); - itBundled("edgecase/ProcessEnvArbitrary", { - files: { - "/entry.js": /* js */ ` - capture(process.env.ARBITRARY); - `, - }, - target: "browser", - capture: ["process.env.ARBITRARY"], - env: { - ARBITRARY: "secret environment stuff!", - }, - }); + itBundled("edgecase/StarExternal", { files: { "/entry.js": /* js */ ` @@ -1347,7 +1336,7 @@ describe("bundler", () => { target: "bun", run: true, todo: isBroken && isWindows, - debugTimeoutScale: 5, + timeoutScale: 5, }); itBundled("edgecase/PackageExternalDoNotBundleNodeModules", { files: { @@ -2275,3 +2264,21 @@ describe("bundler", () => { }, }); }); + +for (const backend of ["api", "cli"] as const) { + describe(`bundler_edgecase/${backend}`, () => { + itBundled("edgecase/ProcessEnvArbitrary", { + files: { + "/entry.js": /* js */ ` + capture(process.env.ARBITRARY); + `, + }, + target: "browser", + backend, + capture: ["process.env.ARBITRARY"], + env: { + ARBITRARY: "secret environment stuff!", + }, + }); + }); +} diff --git a/test/bundler/bundler_env.test.ts b/test/bundler/bundler_env.test.ts new file mode 100644 index 0000000000..eec75c70ed --- /dev/null +++ b/test/bundler/bundler_env.test.ts @@ -0,0 +1,120 @@ +import { describe } from "bun:test"; +import { itBundled } from "./expectBundled"; + +for (let backend of ["api", "cli"] as const) { + describe(`bundler/${backend}`, () => { + // TODO: make this work as expected with process.env isntead of relying on the initial env vars. + if (backend === "cli") + itBundled("env/inline", { + env: { + FOO: "bar", + BAZ: "123", + }, + backend: backend, + dotenv: "inline", + files: { + "/a.js": ` + console.log(process.env.FOO); + console.log(process.env.BAZ); + `, + }, + run: { + env: { + FOO: "barz", + BAZ: "123z", + }, + stdout: "bar\n123\n", + }, + }); + + itBundled("env/inline system", { + env: { + PATH: process.env.PATH, + }, + backend: backend, + dotenv: "inline", + files: { + "/a.js": ` + console.log(process.env.PATH); + `, + }, + run: { + env: { + PATH: "/bar", + }, + stdout: process.env.PATH + "\n", + }, + }); + + // Test disable mode - no env vars are inlined + itBundled("env/disable", { + env: { + FOO: "bar", + BAZ: "123", + }, + backend: backend, + dotenv: "disable", + files: { + "/a.js": ` + console.log(process.env.FOO); + console.log(process.env.BAZ); + `, + }, + run: { + stdout: "undefined\nundefined\n", + }, + }); + + // TODO: make this work as expected with process.env isntead of relying on the initial env vars. + // Test pattern matching - only vars with prefix are inlined + if (backend === "cli") + itBundled("env/pattern-matching", { + env: { + PUBLIC_FOO: "public_value", + PUBLIC_BAR: "another_public", + PRIVATE_SECRET: "secret_value", + }, + dotenv: "PUBLIC_*", + backend: backend, + files: { + "/a.js": ` + console.log(process.env.PUBLIC_FOO); + console.log(process.env.PUBLIC_BAR); + console.log(process.env.PRIVATE_SECRET); + `, + }, + run: { + env: { + PUBLIC_FOO: "BAD_FOO", + PUBLIC_BAR: "BAD_BAR", + }, + stdout: "public_value\nanother_public\nundefined\n", + }, + }); + + if (backend === "cli") + // Test nested environment variable references + itBundled("nested-refs", { + env: { + BASE_URL: "https://api.example.com", + SHOULD_PRINT_BASE_URL: "process.env.BASE_URL", + SHOULD_PRINT_$BASE_URL: "$BASE_URL", + }, + dotenv: "inline", + backend: backend, + files: { + "/a.js": ` + // Test nested references + console.log(process.env.SHOULD_PRINT_BASE_URL); + console.log(process.env.SHOULD_PRINT_$BASE_URL); + `, + }, + run: { + env: { + "BASE_URL": "https://api.example.com", + }, + stdout: "process.env.BASE_URL\n$BASE_URL", + }, + }); + }); +} diff --git a/test/bundler/bundler_html.test.ts b/test/bundler/bundler_html.test.ts new file mode 100644 index 0000000000..1a2d57334e --- /dev/null +++ b/test/bundler/bundler_html.test.ts @@ -0,0 +1,914 @@ +import { describe, expect } from "bun:test"; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + // Basic test for bundling HTML with JS and CSS + itBundled("html/basic", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

Hello World

+ +`, + "/styles.css": "body { background-color: red; }", + "/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + + onAfterBundle(api) { + // Check that output HTML references hashed filenames + api.expectFile("out/index.html").not.toContain("styles.css"); + api.expectFile("out/index.html").not.toContain("script.js"); + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + }, + }); + + // Test relative paths without "./" in script src + itBundled("html/implicit-relative-paths", { + outdir: "out/", + files: { + "/src/index.html": ` + + + + + + + +

Hello World

+ +`, + "/src/styles.css": "body { background-color: red; }", + "/src/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + root: "/src", + entryPoints: ["/src/index.html"], + + onAfterBundle(api) { + // Check that output HTML references hashed filenames + api.expectFile("out/index.html").not.toContain("styles.css"); + api.expectFile("out/index.html").not.toContain("script.js"); + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + }, + }); + + // Test multiple script and style bundling + itBundled("html/multiple-assets", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + + + +

Multiple Assets

+ +`, + "/style1.css": "body { color: blue; }", + "/style2.css": "h1 { color: red; }", + "/script1.js": "console.log('First script')", + "/script2.js": "console.log('Second script')", + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Should combine CSS files into one + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + api.expectFile("out/index.html").not.toMatch(/href=".*style1\.css"/); + api.expectFile("out/index.html").not.toMatch(/href=".*style2\.css"/); + + // Should combine JS files into one + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + api.expectFile("out/index.html").not.toMatch(/src=".*script1\.js"/); + api.expectFile("out/index.html").not.toMatch(/src=".*script2\.js"/); + }, + }); + + // Test image hashing + itBundled("html/image-hashing", { + outdir: "out/", + files: { + "/index.html": ` + + + + Local image + External image + +`, + "/image.jpg": "fake image content", + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Local image should be hashed + api.expectFile("out/index.html").not.toContain("./image.jpg"); + api.expectFile("out/index.html").toMatch(/src=".*-[a-zA-Z0-9]+\.jpg"/); + + // External image URL should remain unchanged + api.expectFile("out/index.html").toContain("https://example.com/image.jpg"); + }, + }); + + // Test external assets preservation + itBundled("html/external-assets", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

External Assets

+ +`, + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + onAfterBundle(api) { + // External URLs should remain unchanged + api.expectFile("out/index.html").toContain("https://cdn.example.com/style.css"); + api.expectFile("out/index.html").toContain("https://cdn.example.com/script.js"); + }, + }); + + // Test mixed local and external assets + itBundled("html/mixed-assets", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + + + +

Mixed Assets

+ + + +`, + "/local.css": "body { margin: 0; }", + "/local.js": "console.log('Local script')", + "/local.jpg": "fake image content", + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Local assets should be hashed + api.expectFile("out/index.html").not.toContain("local.css"); + api.expectFile("out/index.html").not.toContain("local.js"); + api.expectFile("out/index.html").not.toContain("local.jpg"); + + // External assets should remain unchanged + api.expectFile("out/index.html").toContain("https://cdn.example.com/style.css"); + api.expectFile("out/index.html").toContain("https://cdn.example.com/script.js"); + api.expectFile("out/index.html").toContain("https://cdn.example.com/image.jpg"); + }, + }); + + // Test JS imports + itBundled("html/js-imports", { + outdir: "out/", + files: { + "/in/index.html": ` + + + + + + +

JS Imports

+ +`, + "/in/main.js": ` +import { greeting } from './utils/strings.js'; +import { formatDate } from './utils/date.js'; +console.log(greeting('World')); +console.log(formatDate(new Date()));`, + "/in/utils/strings.js": ` +export const greeting = (name) => \`Hello, \${name}!\`;`, + "/in/utils/date.js": ` +import { padZero } from './numbers.js'; +export const formatDate = (date) => \`\${date.getFullYear()}-\${padZero(date.getMonth() + 1)}-\${padZero(date.getDate())}\`;`, + "/in/utils/numbers.js": ` +export const padZero = (num) => String(num).padStart(2, '0');`, + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/in/index.html"], + onAfterBundle(api) { + // All JS should be bundled into one file + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + api.expectFile("out/index.html").not.toContain("main.js"); + + const htmlContent = api.readFile("out/index.html"); + // Check that the bundle contains all the imported code + const jsMatch = htmlContent.match(/src="(.*\.js)"/); + const jsBundle = api.readFile("out/" + jsMatch![1]); + expect(jsBundle).toContain("Hello"); + expect(jsBundle).toContain("padZero"); + expect(jsBundle).toContain("formatDate"); + }, + }); + + // Test CSS imports + itBundled("html/css-imports", { + outdir: "out/", + files: { + "/in/index.html": ` + + + + + + +

CSS Imports

+ +`, + "/in/styles/main.css": ` +@import './variables.css'; +@import './typography.css'; +body { + background-color: var(--background-color); +}`, + "/in/styles/variables.css": ` +:root { + --background-color: #f0f0f0; + --text-color: #333; + --heading-color: #000; +}`, + "/in/styles/typography.css": ` +@import './fonts.css'; +h1 { + color: var(--heading-color); + font-family: var(--heading-font); +}`, + "/in/styles/fonts.css": ` +:root { + --heading-font: 'Arial', sans-serif; + --body-font: 'Helvetica', sans-serif; +}`, + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/in/index.html"], + onAfterBundle(api) { + // All CSS should be bundled into one file + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + api.expectFile("out/index.html").not.toContain("main.css"); + + // Check that the bundle contains all the imported CSS + const htmlContent = api.readFile("out/index.html"); + const cssMatch = htmlContent.match(/href="(.*?\.css)"/); + if (!cssMatch) throw new Error("Could not find CSS file reference in HTML"); + const cssBundle = api.readFile("out/" + cssMatch[1]); + expect(cssBundle).toContain("--background-color"); + expect(cssBundle).toContain("--heading-font"); + expect(cssBundle).toContain("font-family"); + }, + }); + + // Test multiple HTML entry points + itBundled("html/multiple-entries", { + outdir: "out/", + files: { + "/in/pages/index.html": ` + + + + + + + +

Home Page

+ About + +`, + "/in/pages/about.html": ` + + + + + + + +

About Page

+ Home + +`, + "/in/styles/home.css": ` +@import './common.css'; +.home { color: blue; }`, + "/in/styles/about.css": ` +@import './common.css'; +.about { color: green; }`, + "/in/styles/common.css": ` +body { margin: 0; padding: 20px; }`, + "/in/scripts/home.js": ` +import { initNav } from './common.js'; +console.log('Home page'); +initNav();`, + "/in/scripts/about.js": ` +import { initNav } from './common.js'; +console.log('About page'); +initNav();`, + "/in/scripts/common.js": ` +export const initNav = () => console.log('Navigation initialized');`, + }, + entryPoints: ["/in/pages/index.html", "/in/pages/about.html"], + experimentalHtml: true, + experimentalCss: true, + onAfterBundle(api) { + // Check index.html + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + api.expectFile("out/index.html").not.toContain("home.css"); + api.expectFile("out/index.html").not.toContain("home.js"); + + // Check about.html + api.expectFile("out/about.html").toMatch(/href=".*\.css"/); + api.expectFile("out/about.html").toMatch(/src=".*\.js"/); + api.expectFile("out/about.html").not.toContain("about.css"); + api.expectFile("out/about.html").not.toContain("about.js"); + + // Verify we don't update the filenames for these + const indexHtml = api.readFile("out/index.html"); + const aboutHtml = api.readFile("out/about.html"); + expect(indexHtml).toContain('href="./about.html"'); + expect(aboutHtml).toContain('href="index.html"'); + + // Check that each page has its own bundle + const indexHtmlContent = api.readFile("out/index.html"); + const aboutHtmlContent = api.readFile("out/about.html"); + + const indexJsMatch = indexHtmlContent.match(/src="(.*\.js)"/); + const aboutJsMatch = aboutHtmlContent.match(/src="(.*\.js)"/); + + const indexJs = api.readFile("out/" + indexJsMatch![1]); + const aboutJs = api.readFile("out/" + aboutJsMatch![1]); + + expect(indexJs).toContain("Home page"); + expect(aboutJs).toContain("About page"); + expect(indexJs).toContain("Navigation initialized"); + expect(aboutJs).toContain("Navigation initialized"); + + // Check that each page has its own CSS bundle + const indexCssMatch = indexHtmlContent.match(/href="(.*\.css)"/); + const aboutCssMatch = aboutHtmlContent.match(/href="(.*\.css)"/); + + const indexCss = api.readFile("out/" + indexCssMatch![1]); + const aboutCss = api.readFile("out/" + aboutCssMatch![1]); + + expect(indexCss).toContain(".home"); + expect(aboutCss).toContain(".about"); + expect(indexCss).toContain("margin: 0"); + expect(aboutCss).toContain("margin: 0"); + }, + }); + + // Test multiple HTML entries with shared chunks + itBundled("html/shared-chunks", { + outdir: "out/", + // Makes this test easier to write + minifyWhitespace: true, + + files: { + "/in/pages/page1.html": ` + + + + + + + +

Page 1

+ +`, + "/in/pages/page2.html": ` + + + + + + + +

Page 2

+ +`, + "/in/styles/page1.css": ` +@import './shared.css'; +.page1 { font-size: 20px; }`, + "/in/styles/page2.css": ` +@import './shared.css'; +.page2 { font-size: 18px; }`, + "/in/styles/shared.css": ` +@import './reset.css'; +.shared { color: blue; }`, + "/in/styles/reset.css": ` +* { box-sizing: border-box; }`, + "/in/scripts/page1.js": ` +import { sharedUtil } from './shared.js'; +import { largeModule } from './large-module.js'; +console.log('Page 1'); +sharedUtil();`, + "/in/scripts/page2.js": ` +import { sharedUtil } from './shared.js'; +import { largeModule } from './large-module.js'; +console.log('Page 2'); +sharedUtil();`, + "/in/scripts/shared.js": ` +export const sharedUtil = () => console.log('Shared utility');`, + "/in/scripts/large-module.js": ` +export const largeModule = { + // Simulate a large shared module + bigData: new Array(1000).fill('data'), + methods: { /* ... */ } +};`, + }, + entryPoints: ["/in/pages/page1.html", "/in/pages/page2.html"], + experimentalHtml: true, + experimentalCss: true, + splitting: true, + onAfterBundle(api) { + // Check both pages + for (const page of ["page1", "page2"]) { + api.expectFile(`out/${page}.html`).toMatch(/href=".*\.css"/); + api.expectFile(`out/${page}.html`).toMatch(/src=".*\.js"/); + api.expectFile(`out/${page}.html`).not.toContain(`${page}.css`); + api.expectFile(`out/${page}.html`).not.toContain(`${page}.js`); + } + + // Verify that shared code exists in both bundles + const page1Html = api.readFile("out/page1.html"); + const page2Html = api.readFile("out/page2.html"); + + const page1JsPath = page1Html.match(/src="(.*\.js)"/)?.[1]; + const page2JsPath = page2Html.match(/src="(.*\.js)"/)?.[1]; + + expect(page1JsPath).toBeDefined(); + expect(page2JsPath).toBeDefined(); + + const page1Js = api.readFile("out/" + page1JsPath!); + const page2Js = api.readFile("out/" + page2JsPath!); + + // Check we imported the shared module + expect(page2Js).toContain("import{sharedUtil}"); + expect(page1Js).toContain("import{sharedUtil}"); + + // Check CSS bundles + const page1CssPath = page1Html.match(/href="(.*\.css)"/)?.[1]; + const page2CssPath = page2Html.match(/href="(.*\.css)"/)?.[1]; + + expect(page1CssPath).toBeDefined(); + expect(page2CssPath).toBeDefined(); + + const page1Css = api.readFile("out/" + page1CssPath!); + const page2Css = api.readFile("out/" + page2CssPath!); + expect(page1Css).toContain("box-sizing:border-box"); + expect(page2Css).toContain("box-sizing:border-box"); + expect(page1Css).toContain(".shared"); + expect(page2Css).toContain(".shared"); + }, + }); + + // Test JS importing HTML + itBundled("html/js-importing-html", { + outdir: "out/", + files: { + "/in/entry.js": ` +import htmlContent from './template.html'; +console.log('Loaded HTML:', htmlContent);`, + + "/in/template.html": ` + + + + HTML Template + + +

HTML Template

+ +`, + }, + experimentalHtml: true, + + // This becomes: + // + // - out/entry.js + // - out/template-hash.html + // + // Like a regular asset. + entryPoints: ["/in/entry.js"], + onAfterBundle(api) { + const entryBundle = api.readFile("out/entry.js"); + // Check taht we dind't bundle the HTML file + expect(entryBundle).toMatch(/\.\/template-.*\.html/); + }, + }); + + itBundled("html/js-importing-html-and-entry-point-side-effect-import", { + outdir: "out/", + target: "browser", + files: { + "/in/2nd.js": ` +console.log('2nd');`, + "/in/entry.js": ` +import './template.html'; +console.log('Loaded HTML!');`, + + "/in/template.html": ` + + + + HTML Template + + +

HTML Template

+ + + +`, + }, + experimentalHtml: true, + // This becomes: + // - ./template.html + // - ./template-*.js + // - ./entry.js + entryPointsRaw: ["in/template.html", "in/entry.js"], + onAfterBundle(api) { + const templateBundle = api.readFile("out/template.html"); + expect(templateBundle).toContain("HTML Template"); + + // Get the entry.js file from looking at + + +`, + }, + experimentalHtml: true, + entryPointsRaw: ["in/template.html", "in/entry.js"], + bundleErrors: { + "/in/entry.js": ['No matching export in "in/template.html" for import "default"'], + }, + onAfterBundle(api) { + const templateBundle = api.readFile("out/template.html"); + expect(templateBundle).toContain("HTML Template"); + + // Get the entry.js file from looking at + + +`, + }, + experimentalHtml: false, + entryPointsRaw: ["in/template.html", "in/entry.js"], + onAfterBundle(api) { + const entryBundle = api.readFile("out/entry.js"); + + // Verify we DID bundle the HTML file + expect(entryBundle).toMatch(/\.\/template-.*\.html/); + const filename = entryBundle.match(/\.\/(template-.*\.html)/)?.[1]; + expect(filename).toBeDefined(); + const templateBundle = api.readFile("out/" + filename!); + expect(templateBundle).toContain("HTML Template"); + }, + }); + + // Test circular dependencies between JS and HTML + itBundled("html/circular-js-html", { + outdir: "out/", + files: { + "/in/main.js": ` +import page from './page.html'; +console.log('Main JS loaded page:', page);`, + + "/in/page.html": ` + + + + + + +
Circular Import Test
+ +`, + }, + experimentalHtml: true, + entryPoints: ["/in/main.js"], + onAfterBundle(api) { + const bundle = api.readFile("out/main.js"); + + // Check that it is a hashed file + expect(bundle).toMatch(/\.\/page-.*\.html/); + }, + }); + + // Test HTML with only CSS (no JavaScript) + itBundled("html/css-only", { + outdir: "out/", + files: { + "/in/page.html": ` + + + + + + + +
+

Styled Page

+

This page only has CSS styling.

+
+ +`, + "/in/styles-imported.css": ` +* { + box-sizing: border-box; +} +`, + "/in/styles.css": ` +@import "./styles-imported.css"; +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} +.title { + color: navy; +}`, + "/in/theme.css": ` +@import "./styles-imported.css"; +.content { + line-height: 1.6; + color: #333; +} +body { + background-color: #f5f5f5; +}`, + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/in/page.html"], + onAfterBundle(api) { + const htmlBundle = api.readFile("out/page.html"); + + // Check that CSS is properly referenced and hashed + expect(htmlBundle).toMatch(/href=".*\.css"/); + expect(htmlBundle).not.toContain("styles.css"); + expect(htmlBundle).not.toContain("theme.css"); + + // Get the CSS bundle path + const cssPath = htmlBundle.match(/href="(.*\.css)"/)?.[1]; + expect(cssPath).toBeDefined(); + + // Check the CSS bundle contents + const cssBundle = api.readFile("out/" + cssPath!); + expect(cssBundle).toContain(".container"); + expect(cssBundle).toContain(".title"); + expect(cssBundle).toContain(".content"); + expect(cssBundle).toContain("background-color"); + expect(cssBundle).toContain("box-sizing: border-box"); + }, + }); + + // Test absolute paths in HTML + itBundled("html/absolute-paths", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

Absolute Paths

+ + +`, + "/styles/main.css": "body { margin: 0; }", + "/scripts/app.js": "console.log('App loaded')", + "/images/logo.png": "fake image content", + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check that absolute paths are handled correctly + const htmlBundle = api.readFile("out/index.html"); + + // CSS should be bundled and hashed + api.expectFile("out/index.html").not.toContain("/styles/main.css"); + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + + // JS should be bundled and hashed + api.expectFile("out/index.html").not.toContain("/scripts/app.js"); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + + // Image should be hashed + api.expectFile("out/index.html").not.toContain("/images/logo.png"); + api.expectFile("out/index.html").toMatch(/src=".*\.png"/); + + // Get the bundled files and verify their contents + const cssMatch = htmlBundle.match(/href="(.*\.css)"/); + const jsMatch = htmlBundle.match(/src="(.*\.js)"/); + const imgMatch = htmlBundle.match(/src="(.*\.png)"/); + + expect(cssMatch).not.toBeNull(); + expect(jsMatch).not.toBeNull(); + expect(imgMatch).not.toBeNull(); + + const cssBundle = api.readFile("out/" + cssMatch![1]); + const jsBundle = api.readFile("out/" + jsMatch![1]); + + expect(cssBundle).toContain("margin: 0"); + expect(jsBundle).toContain("App loaded"); + }, + }); + + // Test that sourcemap comments are not included in HTML and CSS files + itBundled("html/no-sourcemap-comments", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

No Sourcemap Comments

+ +`, + "/styles.css": ` +body { + background-color: red; +} +/* This is a comment */`, + "/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + sourceMap: "linked", + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check HTML file doesn't contain sourcemap comments + const htmlContent = api.readFile("out/index.html"); + api.expectFile("out/index.html").not.toContain("sourceMappingURL"); + api.expectFile("out/index.html").not.toContain("debugId"); + + // Get the CSS filename from the HTML + const cssMatch = htmlContent.match(/href="(.*\.css)"/); + expect(cssMatch).not.toBeNull(); + const cssFile = cssMatch![1]; + + // Check CSS file doesn't contain sourcemap comments + api.expectFile("out/" + cssFile).not.toContain("sourceMappingURL"); + api.expectFile("out/" + cssFile).not.toContain("debugId"); + + // Get the JS filename from the HTML + const jsMatch = htmlContent.match(/src="(.*\.js)"/); + expect(jsMatch).not.toBeNull(); + const jsFile = jsMatch![1]; + + // JS file SHOULD contain sourcemap comment since it's supported + api.expectFile("out/" + jsFile).toContain("sourceMappingURL"); + }, + }); + + // Also test with inline sourcemaps + itBundled("html/no-sourcemap-comments-inline", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

No Sourcemap Comments

+ +`, + "/styles.css": ` +body { + background-color: red; +} +/* This is a comment */`, + "/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + sourceMap: "inline", + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check HTML file doesn't contain sourcemap comments + const htmlContent = api.readFile("out/index.html"); + api.expectFile("out/index.html").not.toContain("sourceMappingURL"); + api.expectFile("out/index.html").not.toContain("debugId"); + + // Get the CSS filename from the HTML + const cssMatch = htmlContent.match(/href="(.*\.css)"/); + expect(cssMatch).not.toBeNull(); + const cssFile = cssMatch![1]; + + // Check CSS file doesn't contain sourcemap comments + api.expectFile("out/" + cssFile).not.toContain("sourceMappingURL"); + api.expectFile("out/" + cssFile).not.toContain("debugId"); + + // Get the JS filename from the HTML + const jsMatch = htmlContent.match(/src="(.*\.js)"/); + expect(jsMatch).not.toBeNull(); + const jsFile = jsMatch![1]; + + // JS file SHOULD contain sourcemap comment since it's supported + api.expectFile("out/" + jsFile).toContain("sourceMappingURL"); + }, + }); +}); diff --git a/test/bundler/bundler_plugin.test.ts b/test/bundler/bundler_plugin.test.ts index bd264f17dc..56939258cd 100644 --- a/test/bundler/bundler_plugin.test.ts +++ b/test/bundler/bundler_plugin.test.ts @@ -75,7 +75,6 @@ describe("bundler", () => { // Load Plugin Errors itBundled("plugin/LoadThrow", { - todo: true, files: loadFixture, plugins(builder) { builder.onLoad({ filter: /\.magic$/ }, args => { @@ -99,7 +98,6 @@ describe("bundler", () => { }, }); itBundled("plugin/LoadThrowAsync", { - todo: true, files: loadFixture, plugins(builder) { builder.onLoad({ filter: /\.magic$/ }, async args => { @@ -123,7 +121,6 @@ describe("bundler", () => { }, }); itBundled("plugin/ResolveAndLoadDefaultExport", { - todo: true, files: { "index.ts": /* ts */ ` import foo from "./foo.magic"; @@ -153,7 +150,6 @@ describe("bundler", () => { // Load Plugin Errors itBundled("plugin/ResolveThrow", { - todo: true, files: resolveFixture, plugins(builder) { builder.onResolve({ filter: /\.magic$/ }, args => { @@ -177,7 +173,6 @@ describe("bundler", () => { }, }); itBundled("plugin/ResolveThrowAsync", { - todo: true, files: resolveFixture, plugins(builder) { builder.onResolve({ filter: /\.magic$/ }, async args => { @@ -206,7 +201,6 @@ describe("bundler", () => { let onResolveCount = 0; return { - todo: true, files: { "index.ts": /* ts */ ` import * as foo from "magic:some_string"; @@ -248,7 +242,6 @@ describe("bundler", () => { let onResolveCountBad = 0; return { - todo: true, files: { "index.ts": /* ts */ ` import * as foo from "magic:some_string"; @@ -409,7 +402,6 @@ describe("bundler", () => { }); itBundled("plugin/ResolveOverrideFile", ({ root }) => { return { - todo: true, files: { "index.ts": /* ts */ ` import * as foo from "./foo.ts"; @@ -473,7 +465,6 @@ describe("bundler", () => { let onResolveCount = 0; let importers: string[] = []; return { - todo: true, files: { "index.ts": /* ts */ ` import * as foo from "./one.ts"; @@ -689,7 +680,7 @@ describe("bundler", () => { expect(resolveCount).toBe(5050); expect(loadCount).toBe(101); }, - debugTimeoutScale: 3, + timeoutScale: 3, }; }); // itBundled("plugin/ManyPlugins", ({ root }) => { @@ -832,4 +823,3 @@ describe("bundler", () => { }; }); }); - diff --git a/test/bundler/esbuild/css.test.ts b/test/bundler/esbuild/css.test.ts index 2666a81c42..db04947753 100644 --- a/test/bundler/esbuild/css.test.ts +++ b/test/bundler/esbuild/css.test.ts @@ -41,6 +41,7 @@ body { itBundled("css/CSSNesting", { experimentalCss: true, + target: "bun", files: { "/entry.css": /* css */ ` body { diff --git a/test/bundler/esbuild/default.test.ts b/test/bundler/esbuild/default.test.ts index 39847ec22a..785a5eca75 100644 --- a/test/bundler/esbuild/default.test.ts +++ b/test/bundler/esbuild/default.test.ts @@ -1,4 +1,5 @@ import assert from "assert"; +import path from "path"; import { describe, expect } from "bun:test"; import { osSlashes } from "harness"; import { dedent, ESBUILD_PATH, itBundled } from "../expectBundled"; @@ -2797,20 +2798,25 @@ describe("bundler", () => { }, bundling: false, }); - itBundled("default/ImportMetaCommonJS", { + itBundled("default/ImportMetaCommonJS", ({ root }) => ({ + // Currently Bun emits `import.meta` instead of correctly + // polyfilling its properties. + todo: true, files: { "/entry.js": ` - import fs from "fs"; - import { fileURLToPath } from "url"; - console.log(fs.existsSync(fileURLToPath(import.meta.url)), fs.existsSync(import.meta.path)); + import fs from "fs"; + import { fileURLToPath } from "url"; + console.log(fileURLToPath(import.meta.url) === ${JSON.stringify(path.join(root, "out.cjs"))}); `, }, + outfile: "out.cjs", format: "cjs", target: "node", run: { + runtime: "node", stdout: "true true", }, - }); + })); itBundled("default/ImportMetaES6", { files: { "/entry.js": `console.log(import.meta.url, import.meta.path)`, diff --git a/test/bundler/esbuild/hello.ts b/test/bundler/esbuild/hello.ts new file mode 100644 index 0000000000..483b7660f6 --- /dev/null +++ b/test/bundler/esbuild/hello.ts @@ -0,0 +1,3 @@ +import fs from "fs"; +import { fileURLToPath } from "url"; +console.log(fs.existsSync(fileURLToPath(import.meta.url)), fs.existsSync(import.meta.path)); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 240b7205e6..f229a60367 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -6,7 +6,7 @@ import { callerSourceOrigin } from "bun:jsc"; import type { Matchers } from "bun:test"; import * as esbuild from "esbuild"; import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, isDebug } from "harness"; +import { bunEnv, bunExe, isCI, isDebug } from "harness"; import { tmpdir } from "os"; import path from "path"; import { SourceMapConsumer } from "source-map"; @@ -194,6 +194,7 @@ export interface BundlerTestInput { minifyIdentifiers?: boolean; minifySyntax?: boolean; experimentalCss?: boolean; + experimentalHtml?: boolean; targetFromAPI?: "TargetWasConfigured"; minifyWhitespace?: boolean; splitting?: boolean; @@ -210,6 +211,7 @@ export interface BundlerTestInput { // pass subprocess.env env?: Record; nodePaths?: string[]; + dotenv?: "inline" | "disable" | string; // assertion options @@ -277,8 +279,6 @@ export interface BundlerTestInput { /** Multiplier for test timeout */ timeoutScale?: number; - /** Multiplier for test timeout when using bun-debug. Debug builds already have a higher timeout. */ - debugTimeoutScale?: number; /* determines whether or not anything should be passed to outfile, outdir, etc. */ generateOutput?: boolean; @@ -400,8 +400,10 @@ function expectBundled( dryRun = false, ignoreFilter = false, ): Promise | BundlerTestRef { - if (!new Error().stack!.includes('test/bundler/')) { - throw new Error(`All bundler tests must be placed in ./test/bundler/ so that regressions can be quickly detected locally via the 'bun test bundler' command`); + if (!new Error().stack!.includes("test/bundler/")) { + throw new Error( + `All bundler tests must be placed in ./test/bundler/ so that regressions can be quickly detected locally via the 'bun test bundler' command`, + ); } var { expect, it, test } = testForFile(currentFile ?? callerSourceOrigin()); @@ -447,8 +449,10 @@ function expectBundled( minifySyntax, minifyWhitespace, experimentalCss, + experimentalHtml, onAfterBundle, outdir, + dotenv, outfile, outputPaths, plugins, @@ -549,6 +553,9 @@ function expectBundled( if (ESBUILD && skipOnEsbuild) { return testRef(id, opts); } + if (ESBUILD && dotenv) { + throw new Error("dotenv not implemented in esbuild"); + } if (dryRun) { return testRef(id, opts); } @@ -658,7 +665,7 @@ function expectBundled( } } - // Run bun build cli. In the future we can move to using `Bun.Bundler` + // Run bun build cli. In the future we can move to using `Bun.Transpiler.` let warningReference: Record = {}; const expectedErrors = bundleErrors ? Object.entries(bundleErrors).flatMap(([file, v]) => v.map(error => ({ file, error }))) @@ -690,11 +697,13 @@ function expectBundled( minifyWhitespace && `--minify-whitespace`, drop?.length && drop.map(x => ["--drop=" + x]), experimentalCss && "--experimental-css", + experimentalHtml && "--experimental-html", globalName && `--global-name=${globalName}`, jsx.runtime && ["--jsx-runtime", jsx.runtime], jsx.factory && ["--jsx-factory", jsx.factory], jsx.fragment && ["--jsx-fragment", jsx.fragment], jsx.importSource && ["--jsx-import-source", jsx.importSource], + dotenv && ["--env", dotenv], // metafile && `--manifest=${metafile}`, sourceMap && `--sourcemap=${sourceMap}`, entryNaming && entryNaming !== "[dir]/[name].[ext]" && [`--entry-naming`, entryNaming], @@ -1027,9 +1036,15 @@ function expectBundled( emitDCEAnnotations, ignoreDCEAnnotations, experimentalCss, + html: experimentalHtml ? true : undefined, drop, + define: define ?? {}, } as BuildConfig; + if (dotenv) { + buildConfig.env = dotenv as any; + } + if (conditions?.length) { buildConfig.conditions = conditions; } @@ -1040,12 +1055,9 @@ function expectBundled( const debugFile = `import path from 'path'; import assert from 'assert'; const {plugins} = (${x})({ root: ${JSON.stringify(root)} }); -const options = ${JSON.stringify({ ...buildConfig, plugins: undefined }, null, 2)}; +const options = ${JSON.stringify({ ...buildConfig, throw: true, plugins: undefined }, null, 2)}; options.plugins = typeof plugins === "function" ? [{ name: "plugin", setup: plugins }] : plugins; const build = await Bun.build(options); -if (build.logs) { - throw build.logs; -} for (const [key, blob] of build.outputs) { await Bun.write(path.join(options.outdir, blob.path), blob.result); } @@ -1546,7 +1558,7 @@ for (const [key, blob] of build.outputs) { if (run.errorLineMatch) { const stackTraceLine = stack.pop()!; - const match = /at (.*):(\d+):(\d+)$/.exec(stackTraceLine); + const match = /at (?:<[^>]+> \()?([^)]+):(\d+):(\d+)\)?$/.exec(stackTraceLine); if (match) { const line = readFileSync(match[1], "utf-8").split("\n")[+match[2] - 1]; if (!run.errorLineMatch.test(line)) { @@ -1577,6 +1589,11 @@ for (const [key, blob] of build.outputs) { // no idea why this logs. ¯\_(ツ)_/¯ result = result.replace(/\[Event_?Loop\] enqueueTaskConcurrent\(RuntimeTranspilerStore\)\n/gi, ""); + // when the inspector runs (can be due to VSCode extension), there is + // a bug that in debug modes the console logs extra stuff + if (name === "stderr" && process.env.BUN_INSPECT_CONNECT_TO) { + result = result.replace(/(?:^|\n)\/[^\n]*: CONSOLE LOG[^\n]*(\n|$)/g, "$1").trim(); + } if (typeof expected === "string") { expected = dedent(expected).trim(); @@ -1651,8 +1668,7 @@ export function itBundled( id, () => expectBundled(id, opts as any), // sourcemap code is slow - (opts.snapshotSourceMap ? (isDebug ? Infinity : 30_000) : isDebug ? 15_000 : 5_000) * - ((isDebug ? opts.debugTimeoutScale : opts.timeoutScale) ?? 1), + isCI ? undefined : isDebug ? Infinity : (opts.snapshotSourceMap ? 30_000 : 5_000) * (opts.timeoutScale ?? 1), ); } return ref; @@ -1664,8 +1680,7 @@ itBundled.only = (id: string, opts: BundlerTestInput) => { id, () => expectBundled(id, opts as any), // sourcemap code is slow - (opts.snapshotSourceMap ? (isDebug ? Infinity : 30_000) : isDebug ? 15_000 : 5_000) * - ((isDebug ? opts.debugTimeoutScale : opts.timeoutScale) ?? 1), + isCI ? undefined : isDebug ? Infinity : (opts.snapshotSourceMap ? 30_000 : 5_000) * (opts.timeoutScale ?? 1), ); }; diff --git a/test/bundler/native-plugin.test.ts b/test/bundler/native-plugin.test.ts new file mode 100644 index 0000000000..9348a80995 --- /dev/null +++ b/test/bundler/native-plugin.test.ts @@ -0,0 +1,688 @@ +import { BunFile, Loader, plugin } from "bun"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import path, { dirname, join, resolve } from "path"; +import source from "./native_plugin.cc" with { type: "file" }; +import notAPlugin from "./not_native_plugin.cc" with { type: "file" }; +import bundlerPluginHeader from "../../packages/bun-native-bundler-plugin-api/bundler_plugin.h" with { type: "file" }; +import { bunEnv, bunExe, makeTree, tempDirWithFiles } from "harness"; +import { itBundled } from "bundler/expectBundled"; +import os from "os"; +import fs from "fs"; + +describe("native-plugins", async () => { + const cwd = process.cwd(); + let tempdir: string = ""; + let outdir: string = ""; + + beforeAll(async () => { + const files = { + "bun-native-bundler-plugin-api/bundler_plugin.h": await Bun.file(bundlerPluginHeader).text(), + "plugin.cc": await Bun.file(source).text(), + "not_a_plugin.cc": await Bun.file(notAPlugin).text(), + "package.json": JSON.stringify({ + "name": "fake-plugin", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + "scripts": { + "build:napi": "node-gyp configure && node-gyp build", + }, + "dependencies": { + "node-gyp": "10.2.0", + }, + }), + + "index.ts": /* ts */ `import values from "./stuff.ts"; +import json from "./lmao.json"; +const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] +const many_bar = ["bar","bar","bar","bar","bar","bar","bar"] +const many_baz = ["baz","baz","baz","baz","baz","baz","baz"] +console.log(JSON.stringify(json)); +values;`, + "stuff.ts": `export default { foo: "bar", baz: "baz" }`, + "lmao.json": ``, + "binding.gyp": /* gyp */ `{ + "targets": [ + { + "target_name": "xXx123_foo_counter_321xXx", + "sources": [ "plugin.cc" ], + "include_dirs": [ "." ] + }, + { + "target_name": "not_a_plugin", + "sources": [ "not_a_plugin.cc" ], + "include_dirs": [ "." ] + } + ] + }`, + }; + + tempdir = tempDirWithFiles("native-plugins", files); + + await makeTree(tempdir, files); + outdir = path.join(tempdir, "dist"); + + console.log("tempdir", tempdir); + + process.chdir(tempdir); + + await Bun.$`${bunExe()} i && ${bunExe()} build:napi`.env(bunEnv).cwd(tempdir); + }); + + beforeEach(() => { + const tempdir2 = tempDirWithFiles("native-plugins", {}); + process.chdir(tempdir2); + }); + + afterEach(async () => { + await Bun.$`rm -rf ${outdir}`; + process.chdir(cwd); + }); + + it("works in a basic case", async () => { + await Bun.$`${bunExe()} i && ${bunExe()} build:napi`.env(bunEnv).cwd(tempdir); + + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + + const result = await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "plugin_impl", external }); + + build.onLoad({ filter: /lmao\.json/ }, async ({ defer }) => { + await defer(); + const count = napiModule.getFooCount(external); + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + }); + + if (!result.success) console.log(result); + expect(result.success).toBeTrue(); + const output = await Bun.$`${bunExe()} run dist/index.js`.cwd(tempdir).json(); + expect(output).toStrictEqual({ fooCount: 9 }); + + const compilationCtxFreedCount = await napiModule.getCompilationCtxFreedCount(external); + expect(compilationCtxFreedCount).toBe(2); + }); + + it("doesn't explode when there are a lot of concurrent files", async () => { + // Generate 100 json files + const files: [filepath: string, var_name: string][] = await Promise.all( + Array.from({ length: 100 }, async (_, i) => { + await Bun.write(path.join(tempdir, "json_files", `lmao${i}.json`), `{}`); + return [`import json${i} from "./json_files/lmao${i}.json"`, `json${i}`]; + }), + ); + + // Append the imports to index.ts + const prelude = /* ts */ `import values from "./stuff.ts" + const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + `; + await Bun.$`echo ${prelude} > index.ts`; + await Bun.$`echo ${files.map(([fp]) => fp).join("\n")} >> index.ts`; + await Bun.$`echo ${files.map(([, varname]) => `console.log(JSON.stringify(${varname}))`).join("\n")} >> index.ts`; + + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + + const result = await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "plugin_impl", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + const count = napiModule.getFooCount(external); + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + }); + + if (!result.success) console.log(result); + console.log(result); + expect(result.success).toBeTrue(); + const output = await Bun.$`${bunExe()} run dist/index.js`.cwd(tempdir).text(); + const outputJsons = output + .trim() + .split("\n") + .map(s => JSON.parse(s)); + for (const json of outputJsons) { + expect(json).toStrictEqual({ fooCount: 9 }); + } + + const compilationCtxFreedCount = await napiModule.getCompilationCtxFreedCount(external); + expect(compilationCtxFreedCount).toBe(2); + }); + + // We clone the RegExp object in the C++ code so this test ensures that there + // is no funny business regarding the filter regular expression and multiple + // threads + it("doesn't explode when there are a lot of concurrent files AND the filter regex is used on the JS thread", async () => { + const filter = /\.ts/; + // Generate 100 json files + const files: [filepath: string, var_name: string][] = await Promise.all( + Array.from({ length: 100 }, async (_, i) => { + await Bun.write(path.join(tempdir, "json_files", `lmao${i}.json`), `{}`); + return [`import json${i} from "./json_files/lmao${i}.json"`, `json${i}`]; + }), + ); + + // Append the imports to index.ts + const prelude = /* ts */ `import values from "./stuff.ts" +const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + `; + await Bun.$`echo ${prelude} > index.ts`; + await Bun.$`echo ${files.map(([fp]) => fp).join("\n")} >> index.ts`; + await Bun.$`echo ${files.map(([, varname]) => `console.log(JSON.stringify(${varname}))`).join("\n")} >> index.ts`; + await Bun.$`echo '(() => values)();' >> index.ts`; + + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + + const resultPromise = Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + const count = napiModule.getFooCount(external); + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + }); + + // Now saturate this thread with uses of the filter regex to test that nothing bad happens + // when the JS thread and the bundler thread use regexes concurrently + let dummy = 0; + for (let i = 0; i < 10000; i++) { + // Match the filter regex on some dummy string + dummy += filter.test("foo") ? 1 : 0; + } + + const result = await resultPromise; + + if (!result.success) console.log(result); + expect(result.success).toBeTrue(); + const output = await Bun.$`${bunExe()} run dist/index.js`.cwd(tempdir).text(); + const outputJsons = output + .trim() + .split("\n") + .map(s => JSON.parse(s)); + for (const json of outputJsons) { + expect(json).toStrictEqual({ fooCount: 9 }); + } + + const compilationCtxFreedCount = await napiModule.getCompilationCtxFreedCount(external); + expect(compilationCtxFreedCount).toBe(2); + }); + + it("doesn't explode when passing invalid external", async () => { + const filter = /\.ts/; + // Generate 100 json files + const files: [filepath: string, var_name: string][] = await Promise.all( + Array.from({ length: 100 }, async (_, i) => { + await Bun.write(path.join(tempdir, "json_files", `lmao${i}.json`), `{}`); + return [`import json${i} from "./json_files/lmao${i}.json"`, `json${i}`]; + }), + ); + + // Append the imports to index.ts + const prelude = /* ts */ `import values from "./stuff.ts" +const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + `; + await Bun.$`echo ${prelude} > index.ts`; + await Bun.$`echo ${files.map(([fp]) => fp).join("\n")} >> index.ts`; + await Bun.$`echo ${files.map(([, varname]) => `console.log(JSON.stringify(${varname}))`).join("\n")} >> index.ts`; + + const result = await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = undefined; + + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + let count = 0; + try { + count = napiModule.getFooCount(external); + } catch (e) {} + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + throw: true, + }); + + const output = await Bun.$`${bunExe()} run dist/index.js`.cwd(tempdir).text(); + const outputJsons = output + .trim() + .split("\n") + .map(s => JSON.parse(s)); + for (const json of outputJsons) { + expect(json).toStrictEqual({ fooCount: 0 }); + } + }); + + it("works when logging an error", async () => { + const filter = /\.ts/; + + const prelude = /* ts */ `import values from "./stuff.ts" + const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + `; + await Bun.$`echo ${prelude} > index.ts`; + + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + + const resultPromise = Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + napiModule.setThrowsErrors(external, true); + + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + let count = 0; + try { + count = napiModule.getFooCount(external); + } catch (e) {} + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + }); + + const result = await resultPromise; + + if (result.success) console.log(result); + expect(result.success).toBeFalse(); + const log = result.logs[0]; + expect(log.message).toContain("Throwing an error"); + expect(log.level).toBe("error"); + + const compilationCtxFreedCount = await napiModule.getCompilationCtxFreedCount(external); + expect(compilationCtxFreedCount).toBe(0); + }); + + it("works with versioning", async () => { + const filter = /\.ts/; + + const prelude = /* ts */ `import values from "./stuff.ts" + const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + `; + await Bun.$`echo ${prelude} > index.ts`; + + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + + const resultPromise = Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + build.onBeforeParse({ filter }, { napiModule, symbol: "incompatible_version_plugin_impl", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + let count = 0; + try { + count = napiModule.getFooCount(external); + } catch (e) {} + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + }); + + const result = await resultPromise; + + if (result.success) console.log(result); + expect(result.success).toBeFalse(); + const log = result.logs[0]; + expect(log.message).toContain("This plugin is built for a newer version of Bun than the one currently running."); + expect(log.level).toBe("error"); + + const compilationCtxFreedCount = await napiModule.getCompilationCtxFreedCount(external); + expect(compilationCtxFreedCount).toBe(0); + }); + + // don't know how to reliably test this on windows + it.skipIf(process.platform === "win32")("prints name when plugin crashes", async () => { + const prelude = /* ts */ `import values from "./stuff.ts" + const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + `; + await Bun.$`echo ${prelude} > index.ts`; + + const build_code = /* ts */ ` + import * as path from "path"; + const tempdir = process.env.BUN_TEST_TEMP_DIR; + const filter = /\.ts/; + const resultPromise = await Bun.build({ + outdir: "dist", + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + napiModule.setWillCrash(external, true); + + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + let count = 0; + try { + count = napiModule.getFooCount(external); + } catch (e) {} + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + }); + console.log(resultPromise); + `; + + await Bun.$`echo ${build_code} > build.ts`; + const { stdout, stderr } = await Bun.$`BUN_TEST_TEMP_DIR=${tempdir} ${bunExe()} run build.ts`.throws(false); + const errorString = stderr.toString(); + expect(errorString).toContain('\x1b[31m\x1b[2m"native_plugin_test"\x1b[0m'); + }); + + it("detects when plugin sets function pointer but does not user context pointer", async () => { + const filter = /\.ts/; + + const prelude = /* ts */ `import values from "./stuff.ts" + const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + `; + await Bun.$`echo ${prelude} > index.ts`; + + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + + const resultPromise = Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl_bad_free_function_pointer", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + let count = 0; + try { + count = napiModule.getFooCount(external); + } catch (e) {} + return { + contents: JSON.stringify({ fooCount: count }), + loader: "json", + }; + }); + }, + }, + ], + }); + + const result = await resultPromise; + + if (result.success) console.log(result); + expect(result.success).toBeFalse(); + const log = result.logs[0]; + expect(log.message).toContain( + "Native plugin set the `free_plugin_source_code_context` field without setting the `plugin_source_code_context` field.", + ); + expect(log.level).toBe("error"); + + const compilationCtxFreedCount = await napiModule.getCompilationCtxFreedCount(external); + expect(compilationCtxFreedCount).toBe(0); + }); + + it("should fail gracefully when passing something that is NOT a bunler plugin", async () => { + const not_plugins = [require(path.join(tempdir, "build/Release/not_a_plugin.node")), 420, "hi", {}]; + + for (const napiModule of not_plugins) { + try { + await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "not_a_plugin", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "plugin_impl" }); + }, + }, + ], + }); + expect.unreachable(); + } catch (e) { + expect(e.toString()).toContain( + "onBeforeParse `napiModule` must be a Napi module which exports the `BUN_PLUGIN_NAME` symbol.", + ); + } + } + }); + + it("should fail gracefully when can't find the symbol", async () => { + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + + try { + await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "not_a_plugin", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "OOGA_BOOGA_420" }); + }, + }, + ], + }); + expect.unreachable(); + } catch (e) { + expect(e.toString()).toContain( + 'TypeError [ERR_INVALID_ARG_TYPE]: Could not find the symbol "OOGA_BOOGA_420" in the given napi module.', + ); + } + }); + + it("should use result of the first plugin that runs and doesn't execute the others", async () => { + const filter = /\.ts/; + + const prelude = /* ts */ `import values from "./stuff.ts" +import json from "./lmao.json"; + const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] + const many_bar = ["bar","bar","bar","bar","bar","bar","bar"] + const many_baz = ["baz","baz","baz","baz","baz","baz","baz"] +console.log(JSON.stringify(json)) + `; + await Bun.$`echo ${prelude} > index.ts`; + + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + const external = napiModule.createExternal(); + + const resultPromise = Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "xXx123_foo_counter_321xXx", + setup(build) { + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl", external }); + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl_bar", external }); + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl_baz", external }); + + build.onLoad({ filter: /\.json/ }, async ({ defer, path }) => { + await defer(); + let fooCount = 0; + let barCount = 0; + let bazCount = 0; + try { + fooCount = napiModule.getFooCount(external); + barCount = napiModule.getBarCount(external); + bazCount = napiModule.getBazCount(external); + } catch (e) {} + return { + contents: JSON.stringify({ fooCount, barCount, bazCount }), + loader: "json", + }; + }); + }, + }, + ], + }); + + const result = await resultPromise; + + if (result.success) console.log(result); + expect(result.success).toBeTrue(); + + const output = await Bun.$`${bunExe()} run dist/index.js`.cwd(tempdir).json(); + + expect(output).toStrictEqual({ fooCount: 9, barCount: 0, bazCount: 0 }); + + const compilationCtxFreedCount = await napiModule.getCompilationCtxFreedCount(external); + expect(compilationCtxFreedCount).toBe(2); + }); + + type AdditionalFile = { + name: string; + contents: BunFile | string; + loader: Loader; + }; + const additional_files: AdditionalFile[] = [ + { + name: "bun.png", + contents: await Bun.file(path.join(import.meta.dir, "../integration/sharp/bun.png")), + loader: "file", + }, + { + name: "index.js", + contents: /* ts */ `console.log('HELLO FRIENDS')`, + loader: "js", + }, + { + name: "index.ts", + contents: /* ts */ `console.log('HELLO FRIENDS')`, + loader: "ts", + }, + { + name: "lmao.jsx", + contents: /* ts */ `console.log('HELLO FRIENDS')`, + loader: "jsx", + }, + { + name: "lmao.tsx", + contents: /* ts */ `console.log('HELLO FRIENDS')`, + loader: "tsx", + }, + { + name: "lmao.toml", + contents: /* toml */ `foo = "bar"`, + loader: "toml", + }, + { + name: "lmao.text", + contents: "HELLO FRIENDS", + loader: "text", + }, + ]; + + for (const { name, contents, loader } of additional_files) { + it(`works with ${loader} loader`, async () => { + await Bun.$`echo ${contents} > ${name}`; + const source = /* ts */ `import foo from "./${name}"; + console.log(foo);`; + await Bun.$`echo ${source} > index.ts`; + + const result = await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "test", + setup(build) { + const ext = name.split(".").pop()!; + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + + // Construct regexp to match the file extension + const filter = new RegExp(`\\.${ext}$`); + build.onBeforeParse({ filter }, { napiModule, symbol: "plugin_impl" }); + }, + }, + ], + throw: true, + }); + + expect(result.success).toBeTrue(); + }); + } +}); diff --git a/test/bundler/native_plugin.cc b/test/bundler/native_plugin.cc new file mode 100644 index 0000000000..51b13fd07d --- /dev/null +++ b/test/bundler/native_plugin.cc @@ -0,0 +1,651 @@ +/* + Dummy plugin which counts the occurences of the word "foo" in the source code, + replacing it with "boo". + + It stores the number of occurences in the External struct. +*/ +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define BUN_PLUGIN_EXPORT __declspec(dllexport) +#else +#define BUN_PLUGIN_EXPORT +#include +#include +#endif + +extern "C" BUN_PLUGIN_EXPORT const char *BUN_PLUGIN_NAME = "native_plugin_test"; + +struct External { + std::atomic foo_count; + std::atomic bar_count; + std::atomic baz_count; + + // For testing logging error logic + std::atomic throws_an_error; + // For testing crash reporting + std::atomic simulate_crash; + + std::atomic compilation_ctx_freed_count; +}; + +struct CompilationCtx { + const char *source_ptr; + size_t source_len; + std::atomic *free_counter; +}; + +CompilationCtx *compilation_ctx_new(const char *source_ptr, size_t source_len, + std::atomic *free_counter) { + CompilationCtx *ctx = new CompilationCtx; + ctx->source_ptr = source_ptr; + ctx->source_len = source_len; + ctx->free_counter = free_counter; + return ctx; +} + +void compilation_ctx_free(CompilationCtx *ctx) { + printf("Freed compilation ctx!\n"); + if (ctx->free_counter != nullptr) { + ctx->free_counter->fetch_add(1); + } + free((void *)ctx->source_ptr); + delete ctx; +} + +void log_error(const OnBeforeParseArguments *args, + const OnBeforeParseResult *result, BunLogLevel level, + const char *message, size_t message_len) { + BunLogOptions options; + options.message_ptr = (uint8_t *)message; + options.message_len = message_len; + options.path_ptr = args->path_ptr; + options.path_len = args->path_len; + options.source_line_text_ptr = nullptr; + options.source_line_text_len = 0; + options.level = (int8_t)level; + options.line = 0; + options.lineEnd = 0; + options.column = 0; + options.columnEnd = 0; + (result->log)(args, &options); +} + +extern "C" BUN_PLUGIN_EXPORT void +plugin_impl_with_needle(const OnBeforeParseArguments *args, + OnBeforeParseResult *result, const char *needle) { + // if (args->__struct_size < sizeof(OnBeforeParseArguments)) { + // log_error(args, result, BUN_LOG_LEVEL_ERROR, "Invalid + // OnBeforeParseArguments struct size", sizeof("Invalid + // OnBeforeParseArguments struct size") - 1); return; + // } + + if (args->external) { + External *external = (External *)args->external; + if (external->throws_an_error.load()) { + log_error(args, result, BUN_LOG_LEVEL_ERROR, "Throwing an error", + sizeof("Throwing an error") - 1); + return; + } else if (external->simulate_crash.load()) { +#ifndef _WIN32 + raise(SIGSEGV); +#endif + } + } + + int fetch_result = result->fetchSourceCode(args, result); + if (fetch_result != 0) { + printf("FUCK\n"); + exit(1); + } + + size_t needle_len = strlen(needle); + + int needle_count = 0; + + const char *end = (const char *)result->source_ptr + result->source_len; + + char *cursor = (char *)strstr((const char *)result->source_ptr, needle); + while (cursor != nullptr) { + needle_count++; + cursor += needle_len; + if (cursor + needle_len < end) { + cursor = (char *)strstr((const char *)cursor, needle); + } else + break; + } + + if (needle_count > 0) { + char *new_source = (char *)malloc(result->source_len); + if (new_source == nullptr) { + printf("FUCK\n"); + exit(1); + } + memcpy(new_source, result->source_ptr, result->source_len); + cursor = strstr(new_source, needle); + while (cursor != nullptr) { + cursor[0] = 'q'; + cursor += 3; + if (cursor + 3 < end) { + cursor = (char *)strstr((const char *)cursor, needle); + } else + break; + } + std::atomic *free_counter = nullptr; + if (args->external) { + External *external = (External *)args->external; + std::atomic *needle_atomic_value = nullptr; + if (strcmp(needle, "foo") == 0) { + needle_atomic_value = &external->foo_count; + } else if (strcmp(needle, "bar") == 0) { + needle_atomic_value = &external->bar_count; + } else if (strcmp(needle, "baz") == 0) { + needle_atomic_value = &external->baz_count; + } + printf("FUCK: %d %s\n", needle_count, needle); + needle_atomic_value->fetch_add(needle_count); + free_counter = &external->compilation_ctx_freed_count; + } + result->source_ptr = (uint8_t *)new_source; + result->source_len = result->source_len; + result->plugin_source_code_context = + compilation_ctx_new(new_source, result->source_len, free_counter); + result->free_plugin_source_code_context = + (void (*)(void *))compilation_ctx_free; + } else { + result->source_ptr = nullptr; + result->source_len = 0; + result->loader = 0; + } +} + +extern "C" BUN_PLUGIN_EXPORT void +plugin_impl(const OnBeforeParseArguments *args, OnBeforeParseResult *result) { + plugin_impl_with_needle(args, result, "foo"); +} + +extern "C" BUN_PLUGIN_EXPORT void +plugin_impl_bar(const OnBeforeParseArguments *args, + OnBeforeParseResult *result) { + plugin_impl_with_needle(args, result, "bar"); +} + +extern "C" BUN_PLUGIN_EXPORT void +plugin_impl_baz(const OnBeforeParseArguments *args, + OnBeforeParseResult *result) { + plugin_impl_with_needle(args, result, "baz"); +} + +extern "C" void finalizer(napi_env env, void *data, void *hint) { + External *external = (External *)data; + if (external != nullptr) { + delete external; + } +} + +napi_value create_external(napi_env env, napi_callback_info info) { + napi_status status; + + // Allocate the External struct + External *external = new External(); + if (external == nullptr) { + napi_throw_error(env, nullptr, "Failed to allocate memory"); + return nullptr; + } + + external->foo_count = 0; + external->compilation_ctx_freed_count = 0; + + // Create the external wrapper + napi_value result; + status = napi_create_external(env, external, finalizer, nullptr, &result); + if (status != napi_ok) { + delete external; + napi_throw_error(env, nullptr, "Failed to create external"); + return nullptr; + } + + return result; +} + +napi_value set_will_crash(napi_env env, napi_callback_info info) { + napi_status status; + External *external; + + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to parse arguments"); + return nullptr; + } + + if (argc < 1) { + napi_throw_error(env, nullptr, "Wrong number of arguments"); + return nullptr; + } + + status = napi_get_value_external(env, args[0], (void **)&external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get external"); + return nullptr; + } + + bool throws; + status = napi_get_value_bool(env, args[0], &throws); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get boolean value"); + return nullptr; + } + + external->simulate_crash.store(throws); + + return nullptr; +} + +napi_value set_throws_errors(napi_env env, napi_callback_info info) { + napi_status status; + External *external; + + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to parse arguments"); + return nullptr; + } + + if (argc < 1) { + napi_throw_error(env, nullptr, "Wrong number of arguments"); + return nullptr; + } + + status = napi_get_value_external(env, args[0], (void **)&external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get external"); + return nullptr; + } + + bool throws; + status = napi_get_value_bool(env, args[0], &throws); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get boolean value"); + return nullptr; + } + + external->throws_an_error.store(throws); + + return nullptr; +} + +napi_value get_compilation_ctx_freed_count(napi_env env, + napi_callback_info info) { + napi_status status; + External *external; + + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to parse arguments"); + return nullptr; + } + + if (argc < 1) { + napi_throw_error(env, nullptr, "Wrong number of arguments"); + return nullptr; + } + + status = napi_get_value_external(env, args[0], (void **)&external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get external"); + return nullptr; + } + + napi_value result; + status = napi_create_int32(env, external->compilation_ctx_freed_count.load(), + &result); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create array"); + return nullptr; + } + + return result; +} + +napi_value get_foo_count(napi_env env, napi_callback_info info) { + napi_status status; + External *external; + + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to parse arguments"); + return nullptr; + } + + if (argc < 1) { + napi_throw_error(env, nullptr, "Wrong number of arguments"); + return nullptr; + } + + status = napi_get_value_external(env, args[0], (void **)&external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get external"); + return nullptr; + } + + size_t foo_count = external->foo_count.load(); + if (foo_count > INT32_MAX) { + napi_throw_error(env, nullptr, + "Too many foos! This probably means undefined memory or " + "heap corruption."); + return nullptr; + } + + napi_value result; + status = napi_create_int32(env, foo_count, &result); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create array"); + return nullptr; + } + + return result; +} + +napi_value get_bar_count(napi_env env, napi_callback_info info) { + napi_status status; + External *external; + + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to parse arguments"); + return nullptr; + } + + if (argc < 1) { + napi_throw_error(env, nullptr, "Wrong number of arguments"); + return nullptr; + } + + status = napi_get_value_external(env, args[0], (void **)&external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get external"); + return nullptr; + } + + size_t bar_count = external->bar_count.load(); + if (bar_count > INT32_MAX) { + napi_throw_error(env, nullptr, + "Too many bars! This probably means undefined memory or " + "heap corruption."); + return nullptr; + } + + napi_value result; + status = napi_create_int32(env, bar_count, &result); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create array"); + return nullptr; + } + + return result; +} + +napi_value get_baz_count(napi_env env, napi_callback_info info) { + napi_status status; + External *external; + + size_t argc = 1; + napi_value args[1]; + status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to parse arguments"); + return nullptr; + } + + if (argc < 1) { + napi_throw_error(env, nullptr, "Wrong number of arguments"); + return nullptr; + } + + status = napi_get_value_external(env, args[0], (void **)&external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to get external"); + return nullptr; + } + + size_t baz_count = external->baz_count.load(); + if (baz_count > INT32_MAX) { + napi_throw_error(env, nullptr, + "Too many bazs! This probably means undefined memory or " + "heap corruption."); + return nullptr; + } + + napi_value result; + status = napi_create_int32(env, baz_count, &result); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create array"); + return nullptr; + } + + return result; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_status status; + napi_value fn_get_foo_count; + napi_value fn_get_bar_count; + napi_value fn_get_baz_count; + + napi_value fn_get_compilation_ctx_freed_count; + napi_value fn_create_external; + napi_value fn_set_throws_errors; + napi_value fn_set_will_crash; + + // Register get_foo_count function + status = napi_create_function(env, nullptr, 0, get_foo_count, nullptr, + &fn_get_foo_count); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create get_names function"); + return nullptr; + } + status = + napi_set_named_property(env, exports, "getFooCount", fn_get_foo_count); + if (status != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to add get_names function to exports"); + return nullptr; + } + + // Register get_bar_count function + status = napi_create_function(env, nullptr, 0, get_bar_count, nullptr, + &fn_get_bar_count); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create get_names function"); + return nullptr; + } + status = + napi_set_named_property(env, exports, "getBarCount", fn_get_bar_count); + if (status != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to add get_names function to exports"); + return nullptr; + } + + // Register get_baz_count function + status = napi_create_function(env, nullptr, 0, get_baz_count, nullptr, + &fn_get_baz_count); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create get_names function"); + return nullptr; + } + status = + napi_set_named_property(env, exports, "getBazCount", fn_get_baz_count); + if (status != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to add get_names function to exports"); + return nullptr; + } + + // Register get_compilation_ctx_freed_count function + status = + napi_create_function(env, nullptr, 0, get_compilation_ctx_freed_count, + nullptr, &fn_get_compilation_ctx_freed_count); + if (status != napi_ok) { + napi_throw_error( + env, nullptr, + "Failed to create get_compilation_ctx_freed_count function"); + return nullptr; + } + status = napi_set_named_property(env, exports, "getCompilationCtxFreedCount", + fn_get_compilation_ctx_freed_count); + if (status != napi_ok) { + napi_throw_error( + env, nullptr, + "Failed to add get_compilation_ctx_freed_count function to exports"); + return nullptr; + } + + // Register set_throws_errors function + status = napi_create_function(env, nullptr, 0, set_throws_errors, nullptr, + &fn_set_throws_errors); + if (status != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to create set_throws_errors function"); + return nullptr; + } + status = napi_set_named_property(env, exports, "setThrowsErrors", + fn_set_throws_errors); + if (status != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to add set_throws_errors function to exports"); + return nullptr; + } + + // Register set_will_crash function + status = napi_create_function(env, nullptr, 0, set_will_crash, nullptr, + &fn_set_will_crash); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create set_will_crash function"); + return nullptr; + } + status = + napi_set_named_property(env, exports, "setWillCrash", fn_set_will_crash); + if (status != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to add set_will_crash function to exports"); + return nullptr; + } + + // Register create_external function + status = napi_create_function(env, nullptr, 0, create_external, nullptr, + &fn_create_external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create create_external function"); + return nullptr; + } + status = napi_set_named_property(env, exports, "createExternal", + fn_create_external); + if (status != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to add create_external function to exports"); + return nullptr; + } + + return exports; +} + +struct NewOnBeforeParseArguments { + size_t __struct_size; + void *bun; + const uint8_t *path_ptr; + size_t path_len; + const uint8_t *namespace_ptr; + size_t namespace_len; + uint8_t default_loader; + void *external; + size_t new_field_one; + size_t new_field_two; + size_t new_field_three; +}; + +struct NewOnBeforeParseResult { + size_t __struct_size; + uint8_t *source_ptr; + size_t source_len; + uint8_t loader; + int (*fetchSourceCode)(const NewOnBeforeParseArguments *args, + struct NewOnBeforeParseResult *result); + void *plugin_source_code_context; + void (*free_plugin_source_code_context)(void *ctx); + void (*log)(const NewOnBeforeParseArguments *args, BunLogOptions *options); + size_t new_field_one; + size_t new_field_two; + size_t new_field_three; +}; + +void new_log_error(const NewOnBeforeParseArguments *args, + const NewOnBeforeParseResult *result, BunLogLevel level, + const char *message, size_t message_len) { + BunLogOptions options; + options.message_ptr = (uint8_t *)message; + options.message_len = message_len; + options.path_ptr = args->path_ptr; + options.path_len = args->path_len; + options.source_line_text_ptr = nullptr; + options.source_line_text_len = 0; + options.level = (int8_t)level; + options.line = 0; + options.lineEnd = 0; + options.column = 0; + options.columnEnd = 0; + (result->log)(args, &options); +} + +extern "C" BUN_PLUGIN_EXPORT void +incompatible_version_plugin_impl(const NewOnBeforeParseArguments *args, + NewOnBeforeParseResult *result) { + if (args->__struct_size < sizeof(NewOnBeforeParseArguments)) { + const char *msg = "This plugin is built for a newer version of Bun than " + "the one currently running."; + new_log_error(args, result, BUN_LOG_LEVEL_ERROR, msg, strlen(msg)); + return; + } + + if (result->__struct_size < sizeof(NewOnBeforeParseResult)) { + const char *msg = "This plugin is built for a newer version of Bun than " + "the one currently running."; + new_log_error(args, result, BUN_LOG_LEVEL_ERROR, msg, strlen(msg)); + return; + } +} + +struct RandomUserContext { + const char *foo; + size_t bar; +}; + +extern "C" BUN_PLUGIN_EXPORT void random_user_context_free(void *ptr) { + free(ptr); +} + +extern "C" BUN_PLUGIN_EXPORT void +plugin_impl_bad_free_function_pointer(const OnBeforeParseArguments *args, + OnBeforeParseResult *result) { + + // Intentionally not setting the context here: + // result->plugin_source_code_context = ctx; + result->free_plugin_source_code_context = random_user_context_free; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/bundler/not_native_plugin.cc b/test/bundler/not_native_plugin.cc new file mode 100644 index 0000000000..1de24320d9 --- /dev/null +++ b/test/bundler/not_native_plugin.cc @@ -0,0 +1,27 @@ +/* + */ +#include +#include +#include +#include + +#ifdef _WIN32 +#define BUN_PLUGIN_EXPORT __declspec(dllexport) +#else +#define BUN_PLUGIN_EXPORT +#endif + +napi_value HelloWorld(napi_env env, napi_callback_info info) { + napi_value result; + napi_create_string_utf8(env, "hello world", NAPI_AUTO_LENGTH, &result); + return result; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_value fn; + napi_create_function(env, nullptr, 0, HelloWorld, nullptr, &fn); + napi_set_named_property(env, exports, "helloWorld", fn); + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/bundler/transpiler/bun-pragma.test.ts b/test/bundler/transpiler/bun-pragma.test.ts new file mode 100644 index 0000000000..71d5e87f32 --- /dev/null +++ b/test/bundler/transpiler/bun-pragma.test.ts @@ -0,0 +1,45 @@ +import path from "path"; +import { promises as fs } from "fs"; +import { bunExe, bunEnv } from "harness"; + +const fixturePath = (...segs: string[]): string => path.join(import.meta.dirname, "fixtures", "bun-pragma", ...segs); + +const OK = 0; +const ERR = 1; + +const runFixture = async (path: string): Promise => { + const child = Bun.spawn({ + cmd: [bunExe(), "run", path], + env: bunEnv, + stdio: ["ignore", "ignore", "ignore"], + }); + await child.exited; + expect(child.exitCode).not.toBeNull(); + return child.exitCode!; +}; + +describe("@bun pragma", () => { + describe("valid files", async () => { + const passPath = fixturePath("pass"); + const passFiles: string[] = await fs.readdir(passPath, { encoding: "utf-8" }); + expect(passFiles).not.toHaveLength(0); + + it.each(passFiles)("bun run %s", async file => { + const fullpath = path.join(passPath, file); + const exitCode = await runFixture(fullpath); + expect(exitCode).toBe(OK); + }); + }); + + describe("invalid files", async () => { + const failPath = fixturePath("fail"); + const failFiles: string[] = await fs.readdir(failPath, { encoding: "utf-8" }); + expect(failFiles).not.toHaveLength(0); + + it.each(failFiles)("bun run %s", async file => { + const fullpath = path.join(failPath, file); + const exitCode = await runFixture(fullpath); + expect(exitCode).toBe(ERR); + }); + }); +}); diff --git a/test/bundler/transpiler/fixtures/bun-pragma/fail/bun-pragma-before-hashbang.ts b/test/bundler/transpiler/fixtures/bun-pragma/fail/bun-pragma-before-hashbang.ts new file mode 100644 index 0000000000..f2ed8f50d4 --- /dev/null +++ b/test/bundler/transpiler/fixtures/bun-pragma/fail/bun-pragma-before-hashbang.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env bun +// @bun +export const foo: number = 123; + +// Not valid syntax. Bun pragma must be at the top of the file. diff --git a/test/bundler/transpiler/fixtures/bun-pragma/fail/ts-with-pragma.ts b/test/bundler/transpiler/fixtures/bun-pragma/fail/ts-with-pragma.ts new file mode 100644 index 0000000000..27e7c411d2 --- /dev/null +++ b/test/bundler/transpiler/fixtures/bun-pragma/fail/ts-with-pragma.ts @@ -0,0 +1,2 @@ +// @bun +export const foo: number = 123; diff --git a/test/bundler/transpiler/fixtures/bun-pragma/pass/bun-in-url.ts b/test/bundler/transpiler/fixtures/bun-pragma/pass/bun-in-url.ts new file mode 100644 index 0000000000..64af8d8e3b --- /dev/null +++ b/test/bundler/transpiler/fixtures/bun-pragma/pass/bun-in-url.ts @@ -0,0 +1,5 @@ +// https://bun.sh/docs/api/http#bun-serve +const a: string = "hello"; +console.log(a); + +// '#bun' spotted in first comment but it's not a valid bun pragma diff --git a/test/bundler/transpiler/fixtures/bun-pragma/pass/not-a-pragma.ts b/test/bundler/transpiler/fixtures/bun-pragma/pass/not-a-pragma.ts new file mode 100644 index 0000000000..47e995f78d --- /dev/null +++ b/test/bundler/transpiler/fixtures/bun-pragma/pass/not-a-pragma.ts @@ -0,0 +1,2 @@ +// @not-bun @bytecode +export const foo: number = 123; diff --git a/test/bundler/transpiler/fixtures/bun-pragma/pass/pragma-only-no-newline.ts b/test/bundler/transpiler/fixtures/bun-pragma/pass/pragma-only-no-newline.ts new file mode 100644 index 0000000000..aaa250af21 --- /dev/null +++ b/test/bundler/transpiler/fixtures/bun-pragma/pass/pragma-only-no-newline.ts @@ -0,0 +1 @@ +// @bun diff --git a/test/bundler/transpiler/fixtures/bun-pragma/pass/pragma-only-with-newline.ts b/test/bundler/transpiler/fixtures/bun-pragma/pass/pragma-only-with-newline.ts new file mode 100644 index 0000000000..440005bdd3 --- /dev/null +++ b/test/bundler/transpiler/fixtures/bun-pragma/pass/pragma-only-with-newline.ts @@ -0,0 +1,2 @@ +// @bun + diff --git a/test/bundler/transpiler/fixtures/bun-pragma/pass/valid-pragma-without-bun-prefix.ts b/test/bundler/transpiler/fixtures/bun-pragma/pass/valid-pragma-without-bun-prefix.ts new file mode 100644 index 0000000000..23d4025f62 --- /dev/null +++ b/test/bundler/transpiler/fixtures/bun-pragma/pass/valid-pragma-without-bun-prefix.ts @@ -0,0 +1,5 @@ +// @bytecode +export const foo: number = 122; + +// explanation: @bytecode is a valid pragma checked by the parser, but only if +// @bun is found before it. diff --git a/test/bundler/transpiler/fixtures/lots-of-for-loop.js b/test/bundler/transpiler/fixtures/lots-of-for-loop.js new file mode 100644 index 0000000000..096ff28911 --- /dev/null +++ b/test/bundler/transpiler/fixtures/lots-of-for-loop.js @@ -0,0 +1,713 @@ +let counter = 0; +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) + +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) + +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) + +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) + +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) + +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) + +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) + +for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) for (let i = 0; i < 1; i++) counter++; +console.log(counter); \ No newline at end of file diff --git a/test/bundler/transpiler/function-tostring-require.test.ts b/test/bundler/transpiler/function-tostring-require.test.ts new file mode 100644 index 0000000000..2ea355f1a3 --- /dev/null +++ b/test/bundler/transpiler/function-tostring-require.test.ts @@ -0,0 +1,13 @@ +import { test, expect } from "bun:test"; + +test("toString doesnt observe import.meta.require", () => { + function hello() { + return typeof require("fs") === "string" ? "from eval" : "main function"; + } + const newFunctionBody = `return ${hello.toString()}`; + const loadFakeModule = new Function("require", newFunctionBody)(id => `fake require ${id}`); + expect(hello()).toBe("main function"); + expect(loadFakeModule()).toBe("from eval"); +}); + +export {}; diff --git a/test/bundler/transpiler/macro-test.test.ts b/test/bundler/transpiler/macro-test.test.ts index 5c95553400..be7aed0ac5 100644 --- a/test/bundler/transpiler/macro-test.test.ts +++ b/test/bundler/transpiler/macro-test.test.ts @@ -1,6 +1,13 @@ import { escapeHTML } from "bun" assert { type: "macro" }; import { expect, test } from "bun:test"; import { addStrings, addStringsUTF16, escape, identity } from "./macro.ts" assert { type: "macro" }; +import defaultMacro, { + default as defaultMacroAlias, + identity as identity1, + identity as identity2, +} from "./macro.ts" assert { type: "macro" }; + +import * as macros from "./macro.ts" assert { type: "macro" }; test("bun builtins can be used in macros", async () => { expect(escapeHTML("abc!")).toBe("abc!"); @@ -89,6 +96,24 @@ test("utf16 string", () => { expect(identity("😊 Smiling Face with Smiling Eyes Emoji")).toBe("😊 Smiling Face with Smiling Eyes Emoji"); }); +test("import aliases", () => { + expect(identity1({ a: 1 })).toEqual({ a: 1 }); + expect(identity1([1, 2, 3])).toEqual([1, 2, 3]); + expect(identity2({ a: 1 })).toEqual({ a: 1 }); + expect(identity2([1, 2, 3])).toEqual([1, 2, 3]); +}); + +test("default import", () => { + expect(defaultMacro()).toBe("defaultdefaultdefault"); + expect(defaultMacroAlias()).toBe("defaultdefaultdefault"); +}); + +test("namespace import", () => { + expect(macros.identity({ a: 1 })).toEqual({ a: 1 }); + expect(macros.identity([1, 2, 3])).toEqual([1, 2, 3]); + expect(macros.escape()).toBe("\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C"); +}); + // test("template string ascii", () => { // expect(identity(`A${""}`)).toBe("A"); // }); diff --git a/test/bundler/transpiler/macro.ts b/test/bundler/transpiler/macro.ts index fc747333cb..430fab84ee 100644 --- a/test/bundler/transpiler/macro.ts +++ b/test/bundler/transpiler/macro.ts @@ -13,3 +13,7 @@ export function addStrings(arg: string) { export function addStringsUTF16(arg: string) { return arg + "\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C" + "😊"; } + +export default function() { + return "defaultdefaultdefault"; +} diff --git a/test/bundler/transpiler/runtime-transpiler.test.ts b/test/bundler/transpiler/runtime-transpiler.test.ts index 7534e42042..19a234f556 100644 --- a/test/bundler/transpiler/runtime-transpiler.test.ts +++ b/test/bundler/transpiler/runtime-transpiler.test.ts @@ -1,6 +1,15 @@ import { beforeEach, describe, expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; +test("use strict causes CommonJS", () => { + const { stdout, exitCode } = Bun.spawnSync({ + cmd: [bunExe(), require.resolve("./use-strict-fixture.js")], + env: bunEnv, + }); + expect(stdout.toString()).toBe("function\n"); + expect(exitCode).toBe(0); +}); + test("non-ascii regexp literals", () => { var str = "🔴11 54 / 10,000"; expect(str.replace(/[🔵🔴,]+/g, "")).toBe("11 54 / 10000"); diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 8757b968ae..0a49f8723f 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -315,6 +315,13 @@ describe("Bun.Transpiler", () => { exp("f<{}>()\nfunction f() {}", "let f = function() {\n};\nf()"); }); + it("malformed enums", () => { + const err = ts.expectParseError; + + err("enum Foo { [2]: 'hi' }", 'Expected identifier but found "["'); + err("enum [] { a }", 'Expected identifier but found "["'); + }); + // TODO: fix all the cases that report generic "Parse error" it("types", () => { const exp = ts.expectPrinted_; @@ -3441,3 +3448,19 @@ it("does not crash with 9 comments and typescript type skipping", () => { expect(stdout.toString()).toContain("success!"); expect(exitCode).toBe(0); }); + +it("runtime transpiler stack overflows", async () => { + expect(async () => await import("./fixtures/lots-of-for-loop.js")).toThrow(`Maximum call stack size exceeded`); +}); + +it("Bun.Transpiler.transformSync stack overflows", async () => { + const code = await Bun.file(join(import.meta.dir, "fixtures", "lots-of-for-loop.js")).text(); + const transpiler = new Bun.Transpiler(); + expect(() => transpiler.transformSync(code)).toThrow(`Maximum call stack size exceeded`); +}); + +it("Bun.Transpiler.transform stack overflows", async () => { + const code = await Bun.file(join(import.meta.dir, "fixtures", "lots-of-for-loop.js")).text(); + const transpiler = new Bun.Transpiler(); + expect(async () => await transpiler.transform(code)).toThrow(`Maximum call stack size exceeded`); +}); diff --git a/test/bundler/transpiler/use-strict-fixture.js b/test/bundler/transpiler/use-strict-fixture.js new file mode 100644 index 0000000000..49ee828032 --- /dev/null +++ b/test/bundler/transpiler/use-strict-fixture.js @@ -0,0 +1,5 @@ +"use strict"; + +// Test that 'use strict' makes it CommonJS when we otherwise don't know which one to pick. +// Without that, this direct eval becomes indirect, throwing a ReferenceError. +console.log(eval("typeof module.require")); diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 9b53bb733c..8453a87dde 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun"; +import { spawn, stderr } from "bun"; import { beforeEach, expect, it } from "bun:test"; import { copyFileSync, cpSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs"; import { bunEnv, bunExe, isDebug, tmpdirSync, waitForFileToExist } from "harness"; @@ -450,7 +450,7 @@ ${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`, let it = str.split("\n"); let line; while ((line = it.shift())) { - if (!line.includes("error")) continue; + if (!line.includes("error:")) continue; str = ""; if (reloadCounter === 50) { @@ -530,7 +530,7 @@ ${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`, let it = str.split("\n"); let line; while ((line = it.shift())) { - if (!line.includes("error")) continue; + if (!line.includes("error:")) continue; str = ""; if (reloadCounter === 50) { diff --git a/test/cli/inspect/inspect.test.ts b/test/cli/inspect/inspect.test.ts index ce16df2320..5709101ebc 100644 --- a/test/cli/inspect/inspect.test.ts +++ b/test/cli/inspect/inspect.test.ts @@ -1,8 +1,9 @@ import { Subprocess, spawn } from "bun"; -import { afterEach, expect, test, describe } from "bun:test"; -import { bunEnv, bunExe, isPosix, randomPort, tempDirWithFiles } from "harness"; +import { afterEach, expect, test, describe, beforeAll, afterAll } from "bun:test"; +import { bunEnv, bunExe, isPosix, randomPort, tempDirWithFiles, tmpdirSync } from "harness"; import { WebSocket } from "ws"; import { join } from "node:path"; +import fs from "fs"; let inspectee: Subprocess; import { SocketFramer } from "./socket-framer"; import { JUnitReporter, InspectorSession, connect } from "./junit-reporter"; @@ -10,6 +11,12 @@ import stripAnsi from "strip-ansi"; const anyPort = expect.stringMatching(/^\d+$/); const anyPathname = expect.stringMatching(/^\/[a-z0-9]+$/); +/** + * Get a function that creates a random `.sock` file in the specified temporary directory. + */ +const randomSocketPathFn = (tempdir: string) => (): string => + join(tempdir, Math.random().toString(36).substring(2, 15) + ".sock"); + describe("websocket", () => { const tests = [ { @@ -294,6 +301,20 @@ describe("websocket", () => { }); describe("unix domain socket without websocket", () => { + let tempdir: string; + let randomSocketPath: () => string; + + beforeAll(() => { + // Create .tmp in root repo directory to avoid long paths on Windows + tempdir = ".tmp"; + fs.mkdirSync(tempdir, { recursive: true }); + randomSocketPath = randomSocketPathFn(tempdir); + }); + + afterAll(() => { + fs.rmdirSync(tempdir, { recursive: true }); + }); + if (isPosix) { async function runTest(path: string, args: string[], env = bunEnv) { let { promise, resolve, reject } = Promise.withResolvers(); @@ -337,23 +358,23 @@ describe("unix domain socket without websocket", () => { } test("bun --inspect=unix://", async () => { - const path = Math.random().toString(36).substring(2, 15) + ".sock"; + const path = randomSocketPath(); const url = new URL(`unix://${path}`); await runTest(path, ["--inspect=" + url.href]); }); test("bun --inspect=unix:", async () => { - const path = Math.random().toString(36).substring(2, 15) + ".sock"; + const path = randomSocketPath(); await runTest(path, ["--inspect=unix:" + path]); }); test("BUN_INSPECT=' unix://' bun --inspect", async () => { - const path = Math.random().toString(36).substring(2, 15) + ".sock"; + const path = randomSocketPath(); await runTest(path, [], { ...bunEnv, BUN_INSPECT: "unix://" + path }); }); test("BUN_INSPECT='unix:' bun --inspect", async () => { - const path = Math.random().toString(36).substring(2, 15) + ".sock"; + const path = randomSocketPath(); await runTest(path, [], { ...bunEnv, BUN_INSPECT: "unix:" + path }); }); } @@ -362,7 +383,6 @@ describe("unix domain socket without websocket", () => { /// TODO: this test is flaky because the inspect may not send all messages before the process exit /// we need to implement a way/option so we wait every message from the inspector before exiting test.todo("junit reporter", async () => { - const path = Math.random().toString(36).substring(2, 15) + ".sock"; let reporter: JUnitReporter; let session: InspectorSession; @@ -386,6 +406,7 @@ test.todo("junit reporter", async () => { }); `, }); + const path = randomSocketPathFn(tempdir)(); let { resolve, reject, promise } = Promise.withResolvers(); const [socket, subprocess] = await Promise.all([ connect(`unix://${path}`, resolve), diff --git a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap new file mode 100644 index 0000000000..c7af63e377 --- /dev/null +++ b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap @@ -0,0 +1,347 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`duplicate dependency in optionalDependencies maintains sort order 1`] = ` +"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 +# bun ./bun.lockb --hash: A1A17280329F8383-20d6659d9c0623de-94508CB3B7915517-4b22a59b37f2f4f6 + + +"@types/is-number@>=1.0.0": + version "2.0.0" + resolved "http://localhost:4873/@types/is-number/-/is-number-2.0.0.tgz" + integrity sha512-GEeIxCB+NpM1NrDBqmkYPeU8bI//i+xPzdOY4E1YHet51IcFmz4js6k57m69fLl/cbn7sOR7wj9RNNw53X8AiA== + +a-dep@1.0.1: + version "1.0.1" + resolved "http://localhost:4873/a-dep/-/a-dep-1.0.1.tgz" + integrity sha512-6nmTaPgO2U/uOODqOhbjbnaB4xHuZ+UB7AjKUA3g2dT4WRWeNxgp0dC8Db4swXSnO5/uLLUdFmUJKINNBO/3wg== + +duplicate-optional@1.0.1: + version "1.0.1" + resolved "http://localhost:4873/duplicate-optional/-/duplicate-optional-1.0.1.tgz" + integrity sha512-tL28+yJiTPehPLq7QnOu9jbRBpRgfBkyTVveaJojKcyae2khKuLaPRvsiX5gXv+iNGpYiYcNnV1eDBLXS+L85A== + dependencies: + a-dep "1.0.1" + what-bin "1.0.0" + optionalDependencies: + no-deps "1.0.1" + +no-deps@1.0.1, no-deps@^1.0.0: + version "1.0.1" + resolved "http://localhost:4873/no-deps/-/no-deps-1.0.1.tgz" + integrity sha512-3X6cn4+UJdXJuLPu11v8i/fGLe2PdI6v1yKTELam04lY5esCAFdG/qQts6N6rLrL6g1YRq+MKBAwxbmUQk355A== + +two-range-deps@1.0.0: + version "1.0.0" + resolved "http://localhost:4873/two-range-deps/-/two-range-deps-1.0.0.tgz" + integrity sha512-N+6kPy/GxuMncNz/EKuIrwdoYbh1qmvHDnw1UbM3sQE184kBn+6qAQgtf1wgT9dJnt6X+tWcTzSmfDvtJikVBA== + dependencies: + no-deps "^1.0.0" + "@types/is-number" ">=1.0.0" + +what-bin@1.0.0: + version "1.0.0" + resolved "http://localhost:4873/what-bin/-/what-bin-1.0.0.tgz" + integrity sha512-sa99On1k5aDqCvpni/TQ6rLzYprUWBlb8fNwWOzbjDlM24fRr7FKDOuaBO/Y9WEIcZuzoPkCW5EkBCpflj8REQ== +" +`; + +exports[`outdated normal dep, smaller than column title 1`] = ` +"┌──────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├──────────┼─────────┼────────┼────────┤ +│ no-deps │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└──────────┴─────────┴────────┴────────┘ +" +`; + +exports[`outdated normal dep, larger than column title 1`] = ` +"┌───────────────┬────────────────┬────────────────┬────────────────┐ +│ Package │ Current │ Update │ Latest │ +├───────────────┼────────────────┼────────────────┼────────────────┤ +│ prereleases-1 │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ +└───────────────┴────────────────┴────────────────┴────────────────┘ +" +`; + +exports[`outdated dev dep, smaller than column title 1`] = ` +"┌───────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├───────────────┼─────────┼────────┼────────┤ +│ no-deps (dev) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└───────────────┴─────────┴────────┴────────┘ +" +`; + +exports[`outdated dev dep, larger than column title 1`] = ` +"┌─────────────────────┬────────────────┬────────────────┬────────────────┐ +│ Package │ Current │ Update │ Latest │ +├─────────────────────┼────────────────┼────────────────┼────────────────┤ +│ prereleases-1 (dev) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ +└─────────────────────┴────────────────┴────────────────┴────────────────┘ +" +`; + +exports[`outdated peer dep, smaller than column title 1`] = ` +"┌────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├────────────────┼─────────┼────────┼────────┤ +│ no-deps (peer) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└────────────────┴─────────┴────────┴────────┘ +" +`; + +exports[`outdated peer dep, larger than column title 1`] = ` +"┌──────────────────────┬────────────────┬────────────────┬────────────────┐ +│ Package │ Current │ Update │ Latest │ +├──────────────────────┼────────────────┼────────────────┼────────────────┤ +│ prereleases-1 (peer) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ +└──────────────────────┴────────────────┴────────────────┴────────────────┘ +" +`; + +exports[`outdated optional dep, smaller than column title 1`] = ` +"┌────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├────────────────────┼─────────┼────────┼────────┤ +│ no-deps (optional) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└────────────────────┴─────────┴────────┴────────┘ +" +`; + +exports[`outdated optional dep, larger than column title 1`] = ` +"┌──────────────────────────┬────────────────┬────────────────┬────────────────┐ +│ Package │ Current │ Update │ Latest │ +├──────────────────────────┼────────────────┼────────────────┼────────────────┤ +│ prereleases-1 (optional) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ +└──────────────────────────┴────────────────┴────────────────┴────────────────┘ +" +`; + +exports[`outdated NO_COLOR works 1`] = ` +"|--------------------------------------| +| Package | Current | Update | Latest | +|----------|---------|--------|--------| +| a-dep | 1.0.1 | 1.0.1 | 1.0.10 | +|--------------------------------------| +" +`; + +exports[`auto-install symlinks (and junctions) are created correctly in the install cache 1`] = ` +"{ + name: "is-number", + version: "2.0.0", +} +" +`; + +exports[`text lockfile workspace sorting 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "foo", + "dependencies": { + "no-deps": "1.0.0", + }, + }, + "packages/b": { + "name": "b", + "dependencies": { + "no-deps": "1.0.0", + }, + }, + "packages/c": { + "name": "c", + "dependencies": { + "no-deps": "1.0.0", + }, + }, + }, + "packages": { + "b": ["b@workspace:packages/b", { "dependencies": { "no-deps": "1.0.0" } }], + + "c": ["c@workspace:packages/c", { "dependencies": { "no-deps": "1.0.0" } }], + + "no-deps": ["no-deps@1.0.0", "http://localhost:1234/no-deps/-/no-deps-1.0.0.tgz", {}, "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw=="], + } +} +" +`; + +exports[`text lockfile workspace sorting 2`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "foo", + "dependencies": { + "no-deps": "1.0.0", + }, + }, + "packages/a": { + "name": "a", + "dependencies": { + "no-deps": "1.0.0", + }, + }, + "packages/b": { + "name": "b", + "dependencies": { + "no-deps": "1.0.0", + }, + }, + "packages/c": { + "name": "c", + "dependencies": { + "no-deps": "1.0.0", + }, + }, + }, + "packages": { + "a": ["a@workspace:packages/a", { "dependencies": { "no-deps": "1.0.0" } }], + + "b": ["b@workspace:packages/b", { "dependencies": { "no-deps": "1.0.0" } }], + + "c": ["c@workspace:packages/c", { "dependencies": { "no-deps": "1.0.0" } }], + + "no-deps": ["no-deps@1.0.0", "http://localhost:1234/no-deps/-/no-deps-1.0.0.tgz", {}, "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw=="], + } +} +" +`; + +exports[`text lockfile --frozen-lockfile 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "foo", + "dependencies": { + "a-dep": "^1.0.2", + "no-deps": "^1.0.0", + }, + }, + "packages/pkg1": { + "name": "package1", + "dependencies": { + "peer-deps-too": "1.0.0", + }, + }, + }, + "packages": { + "a-dep": ["a-dep@1.0.10", "http://localhost:1234/a-dep/-/a-dep-1.0.10.tgz", {}, "sha512-NeQ6Ql9jRW8V+VOiVb+PSQAYOvVoSimW+tXaR0CoJk4kM9RIk/XlAUGCsNtn5XqjlDO4hcH8NcyaL507InevEg=="], + + "no-deps": ["no-deps@1.1.0", "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz", {}, "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w=="], + + "package1": ["package1@workspace:packages/pkg1", { "dependencies": { "peer-deps-too": "1.0.0" } }], + + "peer-deps-too": ["peer-deps-too@1.0.0", "http://localhost:1234/peer-deps-too/-/peer-deps-too-1.0.0.tgz", { "peerDependencies": { "no-deps": "*" } }, "sha512-sBx0TKrsB8FkRN2lzkDjMuctPGEKn1TmNUBv3dJOtnZM8nd255o5ZAPRpAI2XFLHZAavBlK/e73cZNwnUxlRog=="], + } +} +" +`; + +exports[`binaries each type of binary serializes correctly to text lockfile 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "foo", + "dependencies": { + "dir-bin": "./dir-bin", + "file-bin": "./file-bin", + "map-bin": "./map-bin", + "named-file-bin": "./named-file-bin", + }, + }, + }, + "packages": { + "dir-bin": ["dir-bin@file:dir-bin", { "binDir": "./bins" }], + + "file-bin": ["file-bin@file:file-bin", { "bin": "./file-bin.js" }], + + "map-bin": ["map-bin@file:map-bin", { "bin": { "map-bin-1": "./map-bin-1.js", "map-bin-2": "./map-bin-2.js" } }], + + "named-file-bin": ["named-file-bin@file:named-file-bin", { "bin": { "named-file-bin": "./named-file-bin.js" } }], + } +} +" +`; + +exports[`binaries root resolution bins 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "fooooo", + "dependencies": { + "fooooo": ".", + "no-deps": "1.0.0", + }, + }, + }, + "packages": { + "fooooo": ["fooooo@root:", { "bin": "fooooo.js" }], + + "no-deps": ["no-deps@1.0.0", "http://localhost:1234/no-deps/-/no-deps-1.0.0.tgz", {}, "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw=="], + } +} +" +`; + +exports[`hoisting text lockfile is hoisted 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "foo", + "dependencies": { + "hoist-lockfile-1": "1.0.0", + "hoist-lockfile-2": "1.0.0", + "hoist-lockfile-3": "1.0.0", + }, + }, + }, + "packages": { + "hoist-lockfile-1": ["hoist-lockfile-1@1.0.0", "http://localhost:1234/hoist-lockfile-1/-/hoist-lockfile-1-1.0.0.tgz", { "dependencies": { "hoist-lockfile-shared": "*" } }, "sha512-E2nwR7egMFDoYjeRno7CAa59kiwkLGfhTFy2Q335JWp2r2bDkwoAt1LdChd5PdGYkbo7SfViHkW44ga+WXA+eA=="], + + "hoist-lockfile-2": ["hoist-lockfile-2@1.0.0", "http://localhost:1234/hoist-lockfile-2/-/hoist-lockfile-2-1.0.0.tgz", { "dependencies": { "hoist-lockfile-shared": "^1.0.1" } }, "sha512-7iNRBJF/U078n9oZW7aDvVLkA7+076a2ONEFvITpjKdhT07KWaBei0SzHkFYW4f3foGZPNlHsv0aAgk949TPJg=="], + + "hoist-lockfile-3": ["hoist-lockfile-3@1.0.0", "http://localhost:1234/hoist-lockfile-3/-/hoist-lockfile-3-1.0.0.tgz", { "dependencies": { "hoist-lockfile-shared": ">=1.0.1" } }, "sha512-iGz7jH7jxz/zq4OZM8hhT7kUX2Ye1m+45SoyMVcWTM7ZB+cY306Ff1mQePKTjkn84/pJMITMdRgDv/qF8PuQUw=="], + + "hoist-lockfile-shared": ["hoist-lockfile-shared@2.0.2", "http://localhost:1234/hoist-lockfile-shared/-/hoist-lockfile-shared-2.0.2.tgz", {}, "sha512-xPWoyP8lv+/JrbClRzhJx1eUsHqDflSTmWOxx82xvMIEs6mbiIuvIp3/L+Ojc6mqex6y426h7L5j0hjLZE3V9w=="], + + "hoist-lockfile-2/hoist-lockfile-shared": ["hoist-lockfile-shared@1.0.2", "http://localhost:1234/hoist-lockfile-shared/-/hoist-lockfile-shared-1.0.2.tgz", {}, "sha512-p7IQ/BbkTRLG/GUx6j2cDQ+vTUc/v9OW9Ss9igh/GFysbr0Qjriz/DiETnISkxYaTFitqOkUSOUkEKyeLNJsfQ=="], + } +} +" +`; + +exports[`it should ignore peerDependencies within workspaces 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "foo", + "peerDependencies": { + "no-deps": ">=1.0.0", + }, + }, + "packages/baz": { + "name": "Baz", + "peerDependencies": { + "a-dep": ">=1.0.1", + }, + }, + }, + "packages": { + "Baz": ["Baz@workspace:packages/baz", { "peerDependencies": { "a-dep": ">=1.0.1" } }], + + "a-dep": ["a-dep@1.0.10", "http://localhost:1234/a-dep/-/a-dep-1.0.10.tgz", {}, "sha512-NeQ6Ql9jRW8V+VOiVb+PSQAYOvVoSimW+tXaR0CoJk4kM9RIk/XlAUGCsNtn5XqjlDO4hcH8NcyaL507InevEg=="], + + "no-deps": ["no-deps@2.0.0", "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz", {}, "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g=="], + } +} +" +`; diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap index bd375763ac..593bd7b0dd 100644 --- a/test/cli/install/__snapshots__/bun-install.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -54,3 +54,24 @@ note: Package name is also declared here at [dir]/bar/package.json:1:9 " `; + +exports[`should handle modified git resolutions in bun.lock 1`] = `"{"lockfileVersion":0,"workspaces":{"":{"dependencies":{"jquery":"3.7.1"}}},"packages":{"jquery":["jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea",{},"3a1288830817d13da39e9231302261896f8721ea"]}}"`; + +exports[`should read install.saveTextLockfile from bunfig.toml 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "foo", + }, + "packages/pkg1": { + "name": "pkg-one", + "version": "1.0.0", + }, + }, + "packages": { + "pkg-one": ["pkg-one@workspace:packages/pkg1", {}], + } +} +" +`; diff --git a/test/cli/install/__snapshots__/bun-lock.test.ts.snap b/test/cli/install/__snapshots__/bun-lock.test.ts.snap new file mode 100644 index 0000000000..4f15c6c30c --- /dev/null +++ b/test/cli/install/__snapshots__/bun-lock.test.ts.snap @@ -0,0 +1,45 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`should escape names 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "quote-in-dependency-name", + }, + "packages/\\"": { + "name": "\\"", + }, + "packages/pkg1": { + "name": "pkg1", + "dependencies": { + "\\"": "*", + }, + }, + }, + "packages": { + "\\"": ["\\"@workspace:packages/\\"", {}], + + "pkg1": ["pkg1@workspace:packages/pkg1", { "dependencies": { "\\"": "*" } }], + } +} +" +`; + +exports[`should write plaintext lockfiles 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "test-package", + "dependencies": { + "dummy-package": "file:./bar-0.0.2.tgz", + }, + }, + }, + "packages": { + "dummy-package": ["bar@./bar-0.0.2.tgz", {}], + } +} +" +`; diff --git a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap index 2fca1723d8..b419206fba 100644 --- a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap @@ -1,2177 +1,2177 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP exports[`dependency on workspace without version in package.json: version: * 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *.*.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "*.*.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*.*.*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: =* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "=*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "=*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: kjwoehcojrgjoj 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "kjwoehcojrgjoj", - "name": "lodash", - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "kjwoehcojrgjoj", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *.1.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "*.1.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*.1.*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *-pre 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "*-pre", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*-pre", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "1", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "<2.0.0 >=1.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "56c714bc8ac0cdbf731de74d216134f3ce156ab45adda065fa84e4b2ce349f4b", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-F7AB8u+6d00CCgnbjWzq9fFLpzOMCgq6mPjOW4+8+dYbrnc0obRrC+IHctzfZ1KKTQxX0xo/punrlpOWcf4gpw==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", - "tag": "npm", - "value": "1.3.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1", + "npm": { + "name": "no-deps", + "version": "<2.0.0 >=1.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "1.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "<2.0.0 >=1.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "56c714bc8ac0cdbf731de74d216134f3ce156ab45adda065fa84e4b2ce349f4b", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-F7AB8u+6d00CCgnbjWzq9fFLpzOMCgq6mPjOW4+8+dYbrnc0obRrC+IHctzfZ1KKTQxX0xo/punrlpOWcf4gpw==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", - "tag": "npm", - "value": "1.3.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1.*", + "npm": { + "name": "no-deps", + "version": "<2.0.0 >=1.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1.1.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "1.1.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "<1.2.0 >=1.1.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "56ec928a6d5f1d18236abc348bc711d6cfd08ca0a068bfc9fda24e7b22bed046", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-SFeNKyKPh4kvYv0yd95fwLKw4JXM45PJLsPRdA8v7/q0lBzFeK6XS8xJTl6mlhb8PbAzioMkHli1W/1g0y4XQQ==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.1.1.tgz", - "tag": "npm", - "value": "1.1.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1.1.*", + "npm": { + "name": "no-deps", + "version": "<1.2.0 >=1.1.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; -exports[`dependency on workspace without version in package.json: version: 1.1.1 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "1.1.1", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "==1.1.1", - }, - "package_id": 3, - }, - ], +exports[`dependency on workspace without version in package.json: version: 1.1.0 1`] = ` +"{ "format": "v2", - "meta_hash": "56ec928a6d5f1d18236abc348bc711d6cfd08ca0a068bfc9fda24e7b22bed046", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-SFeNKyKPh4kvYv0yd95fwLKw4JXM45PJLsPRdA8v7/q0lBzFeK6XS8xJTl6mlhb8PbAzioMkHli1W/1g0y4XQQ==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.1.1.tgz", - "tag": "npm", - "value": "1.1.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1.1.0", + "npm": { + "name": "no-deps", + "version": "==1.1.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *-pre+build 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "*-pre+build", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*-pre+build", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *+build 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "id": 2, - "literal": "*+build", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*+build", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: latest 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "latest", - "name": "lodash", - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "latest", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "", - "name": "lodash", - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "normal": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "latest", - "name": "lodash", - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "latest", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { "11592711315645265694": "1.0.0", - "15298228331728003776": "4.17.21", - }, -} + "5128161233225832376": "4.17.21" + } +}" `; diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index f670bc6827..d2deadd510 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { access, appendFile, copyFile, mkdir, readlink, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins, readdirSorted } from "harness"; import { join, relative, resolve } from "path"; import { check_npm_auth_type, @@ -11,7 +11,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, @@ -404,6 +403,165 @@ it("should add exact version with --exact", async () => { ); await access(join(package_dir, "bun.lockb")); }); +it("should add to devDependencies with --dev", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--dev", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + devDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); +it("should add to optionalDependencies with --optional", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--optional", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + optionalDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); +it("should add to peerDependencies with --peer", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--peer", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + peerDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); it("should add exact version with install.exact", async () => { const urls: string[] = []; @@ -1224,7 +1382,6 @@ it("should install version tagged with `latest` by default", async () => { }); const err2 = await new Response(stderr2).text(); expect(err2).not.toContain("error:"); - expect(err2).toContain("Saved lockfile"); const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), @@ -1234,8 +1391,8 @@ it("should install version tagged with `latest` by default", async () => { "1 package installed", ]); expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(4); + expect(urls.sort()).toEqual([`${root_url}/baz-0.0.3.tgz`]); + expect(requested).toBe(3); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "baz"]); expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ name: "baz", @@ -1607,8 +1764,7 @@ it("should add dependency without duplication", async () => { "installed bar@0.0.2", ]); expect(await exited2).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(2); + expect(requested).toBe(3); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts new file mode 100644 index 0000000000..44bcf1bb5c --- /dev/null +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -0,0 +1,2910 @@ +import { + VerdaccioRegistry, + isLinux, + bunEnv as env, + bunExe, + assertManifestsPopulated, + readdirSorted, + isWindows, + stderrForInstall, + runBunInstall, +} from "harness"; +import { beforeAll, afterAll, beforeEach, test, expect, describe, setDefaultTimeout } from "bun:test"; +import { writeFile, exists, rm, mkdir } from "fs/promises"; +import { join, sep } from "path"; +import { spawn, file, write } from "bun"; + +var verdaccio = new VerdaccioRegistry(); +var packageDir: string; +var packageJson: string; + +beforeAll(async () => { + setDefaultTimeout(1000 * 60 * 5); + await verdaccio.start(); +}); + +afterAll(() => { + verdaccio.stop(); +}); + +beforeEach(async () => { + ({ packageDir, packageJson } = await verdaccio.createTestDir()); + env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); + env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); +}); + +// waiter thread is only a thing on Linux. +for (const forceWaiterThread of isLinux ? [false, true] : [false]) { + describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => { + test("root package with all lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + const writeScript = async (name: string) => { + const contents = ` + import { writeFileSync, existsSync, rmSync } from "fs"; + import { join } from "path"; + + const file = join(import.meta.dir, "${name}.txt"); + + if (existsSync(file)) { + rmSync(file); + writeFileSync(file, "${name} exists!"); + } else { + writeFileSync(file, "${name}!"); + } + `; + await writeFile(join(packageDir, `${name}.js`), contents); + }; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} preinstall.js`, + install: `${bunExe()} install.js`, + postinstall: `${bunExe()} postinstall.js`, + preprepare: `${bunExe()} preprepare.js`, + prepare: `${bunExe()} prepare.js`, + postprepare: `${bunExe()} postprepare.js`, + }, + }), + ); + + await writeScript("preinstall"); + await writeScript("install"); + await writeScript("postinstall"); + await writeScript("preprepare"); + await writeScript("prepare"); + await writeScript("postprepare"); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); + + // add a dependency with all lifecycle scripts + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} preinstall.js`, + install: `${bunExe()} install.js`, + postinstall: `${bunExe()} postinstall.js`, + preprepare: `${bunExe()} preprepare.js`, + prepare: `${bunExe()} prepare.js`, + postprepare: `${bunExe()} postprepare.js`, + }, + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: ["all-lifecycle-scripts"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare exists!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare exists!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare exists!"); + + const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); + + expect(await exists(join(depDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(depDir, "install.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + + await rm(join(packageDir, "preinstall.txt")); + await rm(join(packageDir, "install.txt")); + await rm(join(packageDir, "postinstall.txt")); + await rm(join(packageDir, "preprepare.txt")); + await rm(join(packageDir, "prepare.txt")); + await rm(join(packageDir, "postprepare.txt")); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + // all at once + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + ]); + + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + }); + + test("workspace lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await writeFile( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await writeFile( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + version: "1.0.0", + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg1", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "postprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg2", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg2", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse(); + }); + + test("dependency lifecycle scripts run before root lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]'; + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin-slow": "1.0.0", + }, + trustedDependencies: ["uses-what-bin-slow"], + scripts: { + install: script, + postinstall: script, + preinstall: script, + prepare: script, + postprepare: script, + preprepare: script, + }, + }), + ); + + // uses-what-bin-slow will wait one second then write a file to disk. The root package should wait for + // for this to happen before running its lifecycle scripts. + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("install a dependency with lifecycle scripts, then add to trusted dependencies and install again", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: [], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + "", + "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); + expect(await exists(join(depDir, "preinstall.txt"))).toBeFalse(); + expect(await exists(join(depDir, "install.txt"))).toBeFalse(); + expect(await exists(join(depDir, "postinstall.txt"))).toBeFalse(); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + + // add to trusted dependencies + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: ["all-lifecycle-scripts"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("Checked 1 install across 2 packages (no changes)"), + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + }); + + test("adding a package without scripts to trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + }, + trustedDependencies: ["what-bin"], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"]; + // prettier-ignore + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { "what-bin": "1.0.0" }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + // add it to trusted dependencies + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + }, + trustedDependencies: ["what-bin"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + }); + + test("lifecycle scripts run if node_modules is deleted", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-postinstall"], + }), + ); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-postinstall@1.0.0", + "", + // @ts-ignore + "1 package installed", + ]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { force: true, recursive: true }); + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-postinstall@1.0.0", + "", + "1 package installed", + ]); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("INIT_CWD is set to the correct directory", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "bun install.js", + }, + dependencies: { + "lifecycle-init-cwd": "1.0.0", + "another-init-cwd": "npm:lifecycle-init-cwd@1.0.0", + }, + trustedDependencies: ["lifecycle-init-cwd", "another-init-cwd"], + }), + ); + + await writeFile( + join(packageDir, "install.js"), + ` + const fs = require("fs"); + const path = require("path"); + + fs.writeFileSync( + path.join(__dirname, "test.txt"), + process.env.INIT_CWD || "does not exist" + ); + `, + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ another-init-cwd@1.0.0", + "+ lifecycle-init-cwd@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir); + expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir); + expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir); + }); + + test("failing lifecycle script should print output", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-failing-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-failing-postinstall"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("hello"); + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const out = await new Response(stdout).text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + }); + + test("failing root lifecycle script should print output correctly", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "fooooooooo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} -e "throw new Error('Oops!')"`, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await Bun.readableStreamToText(stdout)).toEqual(expect.stringContaining("bun install v1.")); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("error: Oops!"); + expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1'); + }); + + test("exit 0 in lifecycle scripts works", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + postinstall: "exit 0", + prepare: "exit 0", + postprepare: "exit 0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("--ignore-scripts should skip lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "lifecycle-failing-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-failing-postinstall"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ignore-scripts"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("hello"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-failing-postinstall@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("it should add `node-gyp rebuild` as the `install` script when `install` and `postinstall` don't exist and `binding.gyp` exists in the root of the package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "binding-gyp-scripts": "1.5.0", + }, + trustedDependencies: ["binding-gyp-scripts"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ binding-gyp-scripts@1.5.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules/binding-gyp-scripts/build.node"))).toBeTrue(); + }); + + test("automatic node-gyp scripts should not run for untrusted dependencies, and should run after adding to `trustedDependencies`", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const packageJSON: any = { + name: "foo", + version: "1.0.0", + dependencies: { + "binding-gyp-scripts": "1.5.0", + }, + }; + await writeFile(packageJson, JSON.stringify(packageJSON)); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ binding-gyp-scripts@1.5.0", + "", + "2 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeFalse(); + + packageJSON.trustedDependencies = ["binding-gyp-scripts"]; + await writeFile(packageJson, JSON.stringify(packageJSON)); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue(); + }); + + test("automatic node-gyp scripts work in package root", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + }), + ); + + await writeFile(join(packageDir, "binding.gyp"), ""); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + + await rm(join(packageDir, "build.node")); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + }); + + test("auto node-gyp scripts work when scripts exists other than `install` and `preinstall`", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + scripts: { + postinstall: "exit 0", + prepare: "exit 0", + postprepare: "exit 0", + }, + }), + ); + + await writeFile(join(packageDir, "binding.gyp"), ""); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + }); + + for (const script of ["install", "preinstall"]) { + test(`does not add auto node-gyp script when ${script} script exists`, async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const packageJSON: any = { + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + scripts: { + [script]: "exit 0", + }, + }; + await writeFile(packageJson, JSON.stringify(packageJSON)); + await writeFile(join(packageDir, "binding.gyp"), ""); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeFalse(); + }); + } + + test("git dependencies also run `preprepare`, `prepare`, and `postprepare` scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-install-test@github:dylan-conway/lifecycle-install-test#3ba6af5", + "", + "1 package installed", + "", + "Blocked 6 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", + }, + trustedDependencies: ["lifecycle-install-test"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue(); + }); + + test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin-slow": "1.0.0", + }, + trustedDependencies: ["uses-what-bin-slow"], + scripts: { + install: '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]', + }, + }), + ); + + // Package `uses-what-bin-slow` has an install script that will sleep for 1 second + // before writing `what-bin.txt` to disk. The root package has an install script that + // checks if this file exists. If the root package install script does not wait for + // the other to finish, it will fail. + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uses-what-bin-slow@1.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + async function createPackagesWithScripts( + packagesCount: number, + scripts: Record, + ): Promise { + const dependencies: Record = {}; + const dependenciesList: string[] = []; + + for (let i = 0; i < packagesCount; i++) { + const packageName: string = "stress-test-package-" + i; + const packageVersion = "1.0." + i; + + dependencies[packageName] = "file:./" + packageName; + dependenciesList[i] = packageName; + + const packagePath = join(packageDir, packageName); + await mkdir(packagePath); + await writeFile( + join(packagePath, "package.json"), + JSON.stringify({ + name: packageName, + version: packageVersion, + scripts, + }), + ); + } + + await writeFile( + packageJson, + JSON.stringify({ + name: "stress-test", + version: "1.0.0", + dependencies, + trustedDependencies: dependenciesList, + }), + ); + + return dependenciesList; + } + + test("reach max concurrent scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const scripts = { + "preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`, + }; + + const dependenciesList = await createPackagesWithScripts(4, scripts); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--concurrent-scripts=2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("Blocked"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + ...dependenciesList.map(dep => `+ ${dep}@${dep}`), + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("stress test", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const dependenciesList = await createPackagesWithScripts(500, { + "postinstall": `${bunExe()} --version`, + }); + + // the script is quick, default number for max concurrent scripts + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("Blocked"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + ...dependenciesList.map(dep => `+ ${dep}@${dep}`).sort((a, b) => a.localeCompare(b)), + "", + "500 packages installed", + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("it should install and use correct binary version", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + // this should install `what-bin` in two places: + // + // - node_modules/.bin/what-bin@1.5.0 + // - node_modules/uses-what-bin/node_modules/.bin/what-bin@1.0.0 + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin": "1.0.0", + "what-bin": "1.5.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "+ what-bin@1.5.0", + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( + "what-bin@1.5.0", + ); + expect( + await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), + ).toContain("what-bin@1.0.0"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin": "1.5.0", + "what-bin": "1.0.0", + }, + scripts: { + install: "what-bin", + }, + trustedDependencies: ["uses-what-bin"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( + "what-bin@1.0.0", + ); + expect( + await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), + ).toContain("what-bin@1.5.0"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + out = await new Response(stdout).text(); + err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("node-gyp should always be available for lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "node-gyp --version", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + + // if node-gyp isn't available, it would return a non-zero exit code + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + // if this test fails, `electron` might be removed from the default list + test("default trusted dependencies should work", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + ]); + expect(out).not.toContain("Blocked"); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("default trusted dependencies should not be used of trustedDependencies is populated", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + // fake electron package because it's in the default trustedDependencies list + "electron": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + // electron lifecycle scripts should run, uses-what-bin scripts should not run + var err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + "electron": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }), + ); + + // now uses-what-bin scripts should run and electron scripts should not run. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + }); + + test("does not run any scripts if trustedDependencies is an empty list", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + "electron": "1.0.0", + }, + trustedDependencies: [], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 2 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + }); + + test("will run default trustedDependencies after install that didn't include them", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + electron: "1.0.0", + }, + trustedDependencies: ["blah"], + }), + ); + + // first install does not run electron scripts + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + electron: "1.0.0", + }, + }), + ); + + // The electron scripts should run now because it's in default trusted dependencies. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + }); + + describe("--trust", async () => { + test("unhoisted untrusted scripts, none at root node_modules", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // prevents real `uses-what-bin` from hoisting to root + "uses-what-bin": "npm:a-dep@1.0.3", + }, + workspaces: ["pkg1"], + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ), + ]); + + await runBunInstall(testEnv, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const results = await Promise.all([ + exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin")), + exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), + ]); + + expect(results).toEqual([true, false]); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "--all"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + expect( + await exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), + ).toBeTrue(); + }); + const trustTests = [ + { + label: "only name", + packageJson: { + name: "foo", + }, + }, + { + label: "empty dependencies", + packageJson: { + name: "foo", + dependencies: {}, + }, + }, + { + label: "populated dependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }, + }, + + { + label: "empty trustedDependencies", + packageJson: { + name: "foo", + trustedDependencies: [], + }, + }, + + { + label: "populated dependencies, empty trustedDependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: [], + }, + }, + + { + label: "populated dependencies and trustedDependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }, + }, + + { + label: "empty dependencies and trustedDependencies", + packageJson: { + name: "foo", + dependencies: {}, + trustedDependencies: [], + }, + }, + ]; + for (const { label, packageJson } of trustTests) { + test(label, async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile(join(packageDir, "package.json"), JSON.stringify(packageJson)); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "uses-what-bin@1.0.0"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed uses-what-bin@1.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(join(packageDir, "package.json")).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // another install should not error with json SyntaxError + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + }); + } + describe("packages without lifecycle scripts", async () => { + test("initial install", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "no-deps@1.0.0"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + const out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + }); + }); + test("already installed", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^2.0.0", + }, + }); + + // oops, I wanted to run the lifecycle scripts for no-deps, I'll install + // again with --trust. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + // oh, I didn't realize no-deps doesn't have + // any lifecycle scripts. It shouldn't automatically add to + // trustedDependencies. + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^2.0.0", + }, + }); + }); + }); + }); + + describe("updating trustedDependencies", async () => { + test("existing trustedDependencies, unchanged trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["uses-what-bin"], + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // no changes, lockfile shouldn't be saved + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("existing trustedDependencies, removing trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["uses-what-bin"], + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + // this script should not run because uses-what-bin is no longer in trustedDependencies + await rm(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"), { force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + }); + + test("non-existent trustedDependencies, then adding it", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "electron": "1.0.0", + }, + }); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["electron"], + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + await rm(join(packageDir, "node_modules", "electron", "preinstall.txt"), { force: true }); + + // lockfile should save evenn though there are no changes to trustedDependencies due to + // the default list + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + }); + }); + + test("node -p should work in postinstall scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + postinstall: `node -p "require('fs').writeFileSync('postinstall.txt', 'postinstall')"`, + }, + }), + ); + + const originalPath = env.PATH; + env.PATH = ""; + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + env.PATH = originalPath; + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + }); + + test("ensureTempNodeGypScript works", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: "node-gyp --version", + }, + }), + ); + + const originalPath = env.PATH; + env.PATH = ""; + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env, + }); + + env.PATH = originalPath; + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("bun pm trust and untrusted on missing package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "uses-what-bin": "1.5.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + "", + "2 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + // remove uses-what-bin from node_modules, bun pm trust and untrusted should handle missing package + await rm(join(packageDir, "node_modules", "uses-what-bin"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "untrusted"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("bun pm untrusted"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("Found 0 untrusted dependencies with scripts"); + expect(await exited).toBe(0); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + expect(await exited).toBe(1); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("bun pm trust"); + expect(err).toContain("0 scripts ran"); + expect(err).toContain("uses-what-bin"); + }); + + describe("add trusted, delete, then add again", async () => { + // when we change bun install to delete dependencies from node_modules + // for both cases, we need to update this test + for (const withRm of [true, false]) { + test(withRm ? "withRm" : "withoutRm", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 script ran across 1 package"); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // now remove and install again + if (withRm) { + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "rm", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 package removed"); + expect(out).toContain("uses-what-bin"); + expect(await exited).toBe(0); + } + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + let expected = withRm + ? ["", "Checked 1 install across 2 packages (no changes)"] + : ["", expect.stringContaining("1 package removed")]; + expected = [expect.stringContaining("bun install v1."), ...expected]; + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual(expected); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin"))).toBe(!withRm); + + // add again, bun pm untrusted should report it as untrusted + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expected = withRm + ? [ + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "1 package installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ] + : ["", expect.stringContaining("Checked 3 installs across 4 packages (no changes)"), ""]; + expected = [expect.stringContaining("bun install v1."), ...expected]; + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual(expected); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "untrusted"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep)); + expect(await exited).toBe(0); + }); + } + }); + + describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => { + test("install", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} -e 'Bun.sleepSync(1000)'`, + }, + }), + ); + + const proc = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + stdin: "ignore", + env: testEnv, + }); + + expect(await proc.exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000); + }); + + // https://github.com/oven-sh/bun/issues/11252 + test.todoIf(isWindows)("bun pm trust", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const dep = isWindows ? "uses-what-bin-slow-window" : "uses-what-bin-slow"; + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + [dep]: "1.0.0", + }, + }), + ); + + var { exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env: testEnv, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeFalse(); + + const proc = spawn({ + cmd: [bunExe(), "pm", "trust", "--all"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env: testEnv, + }); + + expect(await proc.exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeTrue(); + + expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000 * (isWindows ? 5 : 1)); + }); + }); + }); + + describe("stdout/stderr is inherited from root scripts during install", async () => { + test("without packages", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const exe = bunExe().replace(/\\/g, "\\\\"); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + scripts: { + "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err.split(/\r?\n/)).toEqual([ + "No packages! Deleted empty lockfile", + "", + `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "preinstall stderr 🍦", + `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + "", + ]); + const out = await Bun.readableStreamToText(stdout); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "install stdout 🚀", + "prepare stdout done ✅", + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("with a package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const exe = bunExe().replace(/\\/g, "\\\\"); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + scripts: { + "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + }, + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err.split(/\r?\n/)).toEqual([ + "Resolving dependencies", + expect.stringContaining("Resolved, downloaded and extracted "), + "Saved lockfile", + "", + `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "preinstall stderr 🍦", + `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + "", + ]); + const out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "install stdout 🚀", + "prepare stdout done ✅", + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + }); +} diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts new file mode 100644 index 0000000000..8a97ed54f6 --- /dev/null +++ b/test/cli/install/bun-install-registry.test.ts @@ -0,0 +1,9759 @@ +import { file, spawn, write } from "bun"; +import { install_test_helpers } from "bun:internal-for-testing"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, setDefaultTimeout, test } from "bun:test"; +import { copyFileSync, mkdirSync } from "fs"; +import { cp, exists, mkdir, readlink, rm, writeFile } from "fs/promises"; +import { + assertManifestsPopulated, + bunExe, + bunEnv as env, + isWindows, + mergeWindowEnvs, + runBunInstall, + runBunUpdate, + pack, + tempDirWithFiles, + tmpdirSync, + toBeValidBin, + toHaveBins, + toMatchNodeModulesAt, + writeShebangScript, + stderrForInstall, + tls, + isFlaky, + isMacOS, + readdirSorted, + VerdaccioRegistry, +} from "harness"; +import { join, resolve } from "path"; +const { parseLockfile } = install_test_helpers; +const { iniInternals } = require("bun:internal-for-testing"); +const { loadNpmrc } = iniInternals; + +expect.extend({ + toBeValidBin, + toHaveBins, + toMatchNodeModulesAt, +}); + +var verdaccio: VerdaccioRegistry; +var port: number; +var packageDir: string; +/** packageJson = join(packageDir, "package.json"); */ +var packageJson: string; + +let users: Record = {}; + +beforeAll(async () => { + setDefaultTimeout(1000 * 60 * 5); + verdaccio = new VerdaccioRegistry(); + port = verdaccio.port; + await verdaccio.start(); +}); + +afterAll(async () => { + await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); + verdaccio.stop(); +}); + +beforeEach(async () => { + ({ packageDir, packageJson } = await verdaccio.createTestDir()); + await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); + await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); + users = {}; + env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); + env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); +}); + +function registryUrl() { + return verdaccio.registryUrl(); +} + +/** + * Returns auth token + */ +async function generateRegistryUser(username: string, password: string): Promise { + if (users[username]) { + throw new Error("that user already exists"); + } else users[username] = password; + + const url = `http://localhost:${port}/-/user/org.couchdb.user:${username}`; + const user = { + name: username, + password: password, + email: `${username}@example.com`, + }; + + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(user), + }); + + if (response.ok) { + const data = await response.json(); + return data.token; + } else { + throw new Error("Failed to create user:", response.statusText); + } +} + +describe("npmrc", async () => { + const isBase64Encoded = (opt: string) => opt === "_auth" || opt === "_password"; + + it("works with empty file", async () => { + console.log("package dir", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ``; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: {}, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); + }); + + it("sets default registry", async () => { + console.log("package dir", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` +registry = http://localhost:${port}/ +`; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); + }); + + it("sets scoped registry", async () => { + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` + @types:registry=http://localhost:${port}/ + `; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "@types/no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); + }); + + it("works with home config", async () => { + console.log("package dir", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const homeDir = `${packageDir}/home_dir`; + await Bun.$`mkdir -p ${homeDir}`; + console.log("home dir", homeDir); + + const ini = /* ini */ ` + registry=http://localhost:${port}/ + `; + + await Bun.$`echo ${ini} > ${homeDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install` + .env({ + ...process.env, + XDG_CONFIG_HOME: `${homeDir}`, + }) + .cwd(packageDir) + .throws(true); + }); + + it("works with two configs", async () => { + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + console.log("package dir", packageDir); + const packageIni = /* ini */ ` + @types:registry=http://localhost:${port}/ + `; + await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; + + const homeDir = `${packageDir}/home_dir`; + await Bun.$`mkdir -p ${homeDir}`; + console.log("home dir", homeDir); + const homeIni = /* ini */ ` + registry = http://localhost:${port}/ + `; + await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; + + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "@types/no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install` + .env({ + ...process.env, + XDG_CONFIG_HOME: `${homeDir}`, + }) + .cwd(packageDir) + .throws(true); + }); + + it("package config overrides home config", async () => { + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + console.log("package dir", packageDir); + const packageIni = /* ini */ ` + @types:registry=http://localhost:${port}/ + `; + await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; + + const homeDir = `${packageDir}/home_dir`; + await Bun.$`mkdir -p ${homeDir}`; + console.log("home dir", homeDir); + const homeIni = /* ini */ "@types:registry=https://registry.npmjs.org/"; + await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; + + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "@types/no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install` + .env({ + ...process.env, + XDG_CONFIG_HOME: `${homeDir}`, + }) + .cwd(packageDir) + .throws(true); + }); + + it("default registry from env variable", async () => { + const ini = /* ini */ ` +registry=\${LOL} + `; + + const result = loadNpmrc(ini, { LOL: `http://localhost:${port}/` }); + + expect(result.default_registry_url).toBe(`http://localhost:${port}/`); + }); + + it("default registry from env variable 2", async () => { + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` +registry=http://localhost:\${PORT}/ + `; + + const result = loadNpmrc(ini, { ...env, PORT: port }); + + expect(result.default_registry_url).toEqual(`http://localhost:${port}/`); + }); + + async function makeTest( + options: [option: string, value: string][], + check: (result: { + default_registry_url: string; + default_registry_token: string; + default_registry_username: string; + default_registry_password: string; + }) => void, + ) { + const optionName = await Promise.all(options.map(async ([name, val]) => `${name} = ${val}`)); + test(optionName.join(" "), async () => { + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const iniInner = await Promise.all( + options.map(async ([option, value]) => { + let finalValue = value; + finalValue = isBase64Encoded(option) ? Buffer.from(finalValue).toString("base64") : finalValue; + return `//registry.npmjs.org/:${option}=${finalValue}`; + }), + ); + + const ini = /* ini */ ` +${iniInner.join("\n")} +`; + + await Bun.$`echo ${JSON.stringify({ + name: "hello", + main: "index.js", + version: "1.0.0", + dependencies: { + "is-even": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + + const result = loadNpmrc(ini); + + check(result); + }); + } + + await makeTest([["_authToken", "skibidi"]], result => { + expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); + expect(result.default_registry_token).toEqual("skibidi"); + }); + + await makeTest( + [ + ["username", "zorp"], + ["_password", "skibidi"], + ], + result => { + expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); + expect(result.default_registry_username).toEqual("zorp"); + expect(result.default_registry_password).toEqual("skibidi"); + }, + ); + + it("authentication works", async () => { + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` +registry = http://localhost:${port}/ +@needs-auth:registry=http://localhost:${port}/ +//localhost:${port}/:_authToken=${await generateRegistryUser("bilbo_swaggins", "verysecure")} +`; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "hi", + main: "index.js", + version: "1.0.0", + dependencies: { + "no-deps": "1.0.0", + "@needs-auth/test-pkg": "1.0.0", + }, + "publishConfig": { + "registry": `http://localhost:${port}`, + }, + })} > package.json`.cwd(packageDir); + + await Bun.$`${bunExe()} install`.env(env).cwd(packageDir).throws(true); + }); + + type EnvMap = + | Omit< + { + [key: string]: string; + }, + "dotEnv" + > + | { dotEnv?: Record }; + + function registryConfigOptionTest( + name: string, + _opts: Record | (() => Promise>), + _env?: EnvMap | (() => Promise), + check?: (stdout: string, stderr: string) => void, + ) { + it(`sets scoped registry option: ${name}`, async () => { + console.log("PACKAGE DIR", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const { dotEnv, ...restOfEnv } = _env + ? typeof _env === "function" + ? await _env() + : _env + : { dotEnv: undefined }; + const opts = _opts ? (typeof _opts === "function" ? await _opts() : _opts) : {}; + const dotEnvInner = dotEnv + ? Object.entries(dotEnv) + .map(([k, v]) => `${k}=${k.includes("SECRET_") ? Buffer.from(v).toString("base64") : v}`) + .join("\n") + : ""; + + const ini = ` +registry = http://localhost:${port}/ +${Object.keys(opts) + .map( + k => + `//localhost:${port}/:${k}=${isBase64Encoded(k) && !opts[k].includes("${") ? Buffer.from(opts[k]).toString("base64") : opts[k]}`, + ) + .join("\n")} +`; + + if (dotEnvInner.length > 0) await Bun.$`echo ${dotEnvInner} > ${packageDir}/.env`; + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "hi", + main: "index.js", + version: "1.0.0", + dependencies: { + "@needs-auth/test-pkg": "1.0.0", + }, + "publishConfig": { + "registry": `http://localhost:${port}`, + }, + })} > package.json`.cwd(packageDir); + + const { stdout, stderr } = await Bun.$`${bunExe()} install` + .env({ ...env, ...restOfEnv }) + .cwd(packageDir) + .throws(check === undefined); + + if (check) check(stdout.toString(), stderr.toString()); + }); + } + + registryConfigOptionTest("_authToken", async () => ({ + "_authToken": await generateRegistryUser("bilbo_baggins", "verysecure"), + })); + registryConfigOptionTest( + "_authToken with env variable value", + async () => ({ _authToken: "${SUPER_SECRET_TOKEN}" }), + async () => ({ SUPER_SECRET_TOKEN: await generateRegistryUser("bilbo_baggins420", "verysecure") }), + ); + registryConfigOptionTest("username and password", async () => { + await generateRegistryUser("gandalf429", "verysecure"); + return { username: "gandalf429", _password: "verysecure" }; + }); + registryConfigOptionTest( + "username and password with env variable password", + async () => { + await generateRegistryUser("gandalf422", "verysecure"); + return { username: "gandalf422", _password: "${SUPER_SECRET_PASSWORD}" }; + }, + { + SUPER_SECRET_PASSWORD: Buffer.from("verysecure").toString("base64"), + }, + ); + registryConfigOptionTest( + "username and password with .env variable password", + async () => { + await generateRegistryUser("gandalf421", "verysecure"); + return { username: "gandalf421", _password: "${SUPER_SECRET_PASSWORD}" }; + }, + { + dotEnv: { SUPER_SECRET_PASSWORD: "verysecure" }, + }, + ); + + registryConfigOptionTest("_auth", async () => { + await generateRegistryUser("linus", "verysecure"); + const _auth = "linus:verysecure"; + return { _auth }; + }); + + registryConfigOptionTest( + "_auth from .env variable", + async () => { + await generateRegistryUser("zack", "verysecure"); + return { _auth: "${SECRET_AUTH}" }; + }, + { + dotEnv: { SECRET_AUTH: "zack:verysecure" }, + }, + ); + + registryConfigOptionTest( + "_auth from .env variable with no value", + async () => { + await generateRegistryUser("zack420", "verysecure"); + return { _auth: "${SECRET_AUTH}" }; + }, + { + dotEnv: { SECRET_AUTH: "" }, + }, + (stdout: string, stderr: string) => { + expect(stderr).toContain("received an empty string"); + }, + ); +}); + +describe("auto-install", () => { + test("symlinks (and junctions) are created correctly in the install cache", async () => { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "--print", "require('is-number')"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: { + ...env, + BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache"), + }, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).toMatchSnapshot(); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + + expect(resolve(await readlink(join(packageDir, ".bun-cache", "is-number", "2.0.0@@localhost@@@1")))).toBe( + join(packageDir, ".bun-cache", "is-number@2.0.0@@localhost@@@1"), + ); + }); +}); + +describe("certificate authority", () => { + const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { + return async function (req: Request) { + if (req.url.includes("no-deps")) { + return new Response(Bun.file(join(import.meta.dir, "registry", "packages", "no-deps", "no-deps-1.0.0.tgz"))); + } + return new Response("OK", { status: 200 }); + }; + }; + test("valid --cafile", async () => { + using server = Bun.serve({ + port: 0, + fetch: mockRegistryFetch(), + ...tls, + }); + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.1.1", + dependencies: { + "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "https://localhost:${server.port}/"`, + ), + write(join(packageDir, "cafile"), tls.cert), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", "cafile"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).toContain("+ no-deps@"); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("ConnectionClosed"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(await exited).toBe(0); + }); + test("valid --ca", async () => { + using server = Bun.serve({ + port: 0, + fetch: mockRegistryFetch(), + ...tls, + }); + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.1.1", + dependencies: { + "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "https://localhost:${server.port}/"`, + ), + ]); + + // first without ca, should fail + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + let out = await Bun.readableStreamToText(stdout); + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(await exited).toBe(1); + + // now with a valid ca + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ca", tls.cert], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("+ no-deps@"); + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); + test(`non-existent --cafile`, async () => { + await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", "does-not-exist"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); + expect(await exited).toBe(1); + }); + + test("non-existent --cafile (absolute path)", async () => { + await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", "/does/not/exist"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: could not find CA file: '/does/not/exist'`); + expect(await exited).toBe(1); + }); + + test("cafile from bunfig does not exist", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "http://localhost:${port}/" + cafile = "does-not-exist"`, + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); + expect(await exited).toBe(1); + }); + test("invalid cafile", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ), + write( + join(packageDir, "invalid-cafile"), + `-----BEGIN CERTIFICATE----- +jlwkjekfjwlejlgldjfljlkwjef +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +ljelkjwelkgjw;lekj;lkejflkj +-----END CERTIFICATE-----`, + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", join(packageDir, "invalid-cafile")], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: invalid CA file: '${join(packageDir, "invalid-cafile")}'`); + expect(await exited).toBe(1); + }); + test("invalid --ca", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ca", "not-valid"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("HTTPThread: the CA is invalid"); + expect(await exited).toBe(1); + }); +}); + +export async function publish( + env: any, + cwd: string, + ...args: string[] +): Promise<{ out: string; err: string; exitCode: number }> { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "publish", ...args], + cwd, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + const exitCode = await exited; + return { out, err, exitCode }; +} + +async function authBunfig(user: string) { + const authToken = await generateRegistryUser(user, user); + return ` + [install] + cache = false + registry = { url = "http://localhost:${port}/", token = "${authToken}" } + `; +} + +describe("whoami", async () => { + test("can get username", async () => { + const bunfig = await authBunfig("whoami"); + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "whoami-pkg", + version: "1.1.1", + }), + ), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "whoami"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).toBe("whoami\n"); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); + test("username from .npmrc", async () => { + // It should report the username from npmrc, even without an account + const bunfig = ` + [install] + cache = false + registry = "http://localhost:${port}/"`; + const npmrc = ` + //localhost:${port}/:username=whoami-npmrc + //localhost:${port}/:_password=123456 + `; + await Promise.all([ + write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), + write(join(packageDir, "bunfig.toml"), bunfig), + write(join(packageDir, ".npmrc"), npmrc), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "whoami"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).toBe("whoami-npmrc\n"); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); + test("only .npmrc", async () => { + const token = await generateRegistryUser("whoami-npmrc", "whoami-npmrc"); + const npmrc = ` + //localhost:${port}/:_authToken=${token} + registry=http://localhost:${port}/`; + await Promise.all([ + write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), + write(join(packageDir, ".npmrc"), npmrc), + ]); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "whoami"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).toBe("whoami-npmrc\n"); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); + test("two .npmrc", async () => { + const token = await generateRegistryUser("whoami-two-npmrc", "whoami-two-npmrc"); + const packageNpmrc = `registry=http://localhost:${port}/`; + const homeNpmrc = `//localhost:${port}/:_authToken=${token}`; + const homeDir = `${packageDir}/home_dir`; + await Bun.$`mkdir -p ${homeDir}`; + await Promise.all([ + write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), + write(join(packageDir, ".npmrc"), packageNpmrc), + write(join(homeDir, ".npmrc"), homeNpmrc), + ]); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "whoami"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: { + ...env, + XDG_CONFIG_HOME: `${homeDir}`, + }, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).toBe("whoami-two-npmrc\n"); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); + test("not logged in", async () => { + await write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "whoami"], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).toBeEmpty(); + const err = await Bun.readableStreamToText(stderr); + expect(err).toBe("error: missing authentication (run `bunx npm login`)\n"); + expect(await exited).toBe(1); + }); + test("invalid token", async () => { + // create the user and provide an invalid token + const token = await generateRegistryUser("invalid-token", "invalid-token"); + const bunfig = ` + [install] + cache = false + registry = { url = "http://localhost:${port}/", token = "1234567" }`; + await Promise.all([ + write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "whoami"], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).toBeEmpty(); + const err = await Bun.readableStreamToText(stderr); + expect(err).toBe(`error: failed to authenticate with registry 'http://localhost:${port}/'\n`); + expect(await exited).toBe(1); + }); +}); + +describe("publish", async () => { + describe("otp", async () => { + const mockRegistryFetch = function (opts: { + token: string; + setAuthHeader?: boolean; + otpFail?: boolean; + npmNotice?: boolean; + xLocalCache?: boolean; + expectedCI?: string; + }) { + return async function (req: Request) { + const { token, setAuthHeader = true, otpFail = false, npmNotice = false, xLocalCache = false } = opts; + if (req.url.includes("otp-pkg")) { + if (opts.expectedCI) { + expect(req.headers.get("user-agent")).toContain("ci/" + opts.expectedCI); + } + if (req.headers.get("npm-otp") === token) { + if (otpFail) { + return new Response( + JSON.stringify({ + error: "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.", + }), + { status: 401 }, + ); + } else { + return new Response("OK", { status: 200 }); + } + } else { + const headers = new Headers(); + if (setAuthHeader) headers.set("www-authenticate", "OTP"); + + // `bun publish` won't request a url from a message in the npm-notice header, but we + // can test that it's displayed + if (npmNotice) headers.set("npm-notice", `visit http://localhost:${this.port}/auth to login`); + + // npm-notice will be ignored + if (xLocalCache) headers.set("x-local-cache", "true"); + + return new Response( + JSON.stringify({ + // this isn't accurate, but we just want to check that finding this string works + mock: setAuthHeader ? "" : "one-time password", + + authUrl: `http://localhost:${this.port}/auth`, + doneUrl: `http://localhost:${this.port}/done`, + }), + { + status: 401, + headers, + }, + ); + } + } else if (req.url.endsWith("auth")) { + expect.unreachable("url given to user, bun publish should not request"); + } else if (req.url.endsWith("done")) { + // send a fake response saying the user has authenticated successfully with the auth url + return new Response(JSON.stringify({ token: token }), { status: 200 }); + } + + expect.unreachable("unexpected url"); + }; + }; + + for (const setAuthHeader of [true, false]) { + test("mock web login" + (setAuthHeader ? "" : " (without auth header)"), async () => { + const token = await generateRegistryUser("otp" + (setAuthHeader ? "" : "noheader"), "otp"); + + using mockRegistry = Bun.serve({ + port: 0, + fetch: mockRegistryFetch({ token }), + }); + + const bunfig = ` + [install] + cache = false + registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; + await Promise.all([ + rm(join(verdaccio.packagesPath, "otp-pkg-1"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "otp-pkg-1", + version: "2.2.2", + dependencies: { + "otp-pkg-1": "2.2.2", + }, + }), + ), + ]); + + const { out, err, exitCode } = await publish(env, packageDir); + expect(exitCode).toBe(0); + }); + } + + test("otp failure", async () => { + const token = await generateRegistryUser("otp-fail", "otp"); + using mockRegistry = Bun.serve({ + port: 0, + fetch: mockRegistryFetch({ token, otpFail: true }), + }); + + const bunfig = ` + [install] + cache = false + registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; + + await Promise.all([ + rm(join(verdaccio.packagesPath, "otp-pkg-2"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "otp-pkg-2", + version: "1.1.1", + dependencies: { + "otp-pkg-2": "1.1.1", + }, + }), + ), + ]); + + const { out, err, exitCode } = await publish(env, packageDir); + expect(exitCode).toBe(1); + expect(err).toContain(" - Received invalid OTP"); + }); + + for (const shouldIgnoreNotice of [false, true]) { + test(`npm-notice with login url${shouldIgnoreNotice ? " (ignored)" : ""}`, async () => { + // Situation: user has 2FA enabled account with faceid sign-in. + // They run `bun publish` with --auth-type=legacy, prompting them + // to enter their OTP. Because they have faceid sign-in, they don't + // have a code to enter, so npm sends a message in the npm-notice + // header with a url for logging in. + const token = await generateRegistryUser(`otp-notice${shouldIgnoreNotice ? "-ignore" : ""}`, "otp"); + using mockRegistry = Bun.serve({ + port: 0, + fetch: mockRegistryFetch({ token, npmNotice: true, xLocalCache: shouldIgnoreNotice }), + }); + + const bunfig = ` + [install] + cache = false + registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; + + await Promise.all([ + rm(join(verdaccio.packagesPath, "otp-pkg-3"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "otp-pkg-3", + version: "3.3.3", + dependencies: { + "otp-pkg-3": "3.3.3", + }, + }), + ), + ]); + + const { out, err, exitCode } = await publish(env, packageDir); + expect(exitCode).toBe(0); + if (shouldIgnoreNotice) { + expect(err).not.toContain(`note: visit http://localhost:${mockRegistry.port}/auth to login`); + } else { + expect(err).toContain(`note: visit http://localhost:${mockRegistry.port}/auth to login`); + } + }); + } + + const fakeCIEnvs = [ + { ci: "expo-application-services", envs: { EAS_BUILD: "hi" } }, + { ci: "codemagic", envs: { CM_BUILD_ID: "hi" } }, + { ci: "vercel", envs: { "NOW_BUILDER": "hi" } }, + ]; + for (const envInfo of fakeCIEnvs) { + test(`CI user agent name: ${envInfo.ci}`, async () => { + const token = await generateRegistryUser(`otp-${envInfo.ci}`, "otp"); + using mockRegistry = Bun.serve({ + port: 0, + fetch: mockRegistryFetch({ token, expectedCI: envInfo.ci }), + }); + + const bunfig = ` + [install] + cache = false + registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; + + await Promise.all([ + rm(join(verdaccio.packagesPath, "otp-pkg-4"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "otp-pkg-4", + version: "4.4.4", + dependencies: { + "otp-pkg-4": "4.4.4", + }, + }), + ), + ]); + + const { out, err, exitCode } = await publish( + { ...env, ...envInfo.envs, ...{ BUILDKITE: undefined, GITHUB_ACTIONS: undefined } }, + packageDir, + ); + expect(exitCode).toBe(0); + }); + } + }); + + test("can publish a package then install it", async () => { + const bunfig = await authBunfig("basic"); + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-1"), { recursive: true, force: true }), + write( + packageJson, + JSON.stringify({ + name: "publish-pkg-1", + version: "1.1.1", + dependencies: { + "publish-pkg-1": "1.1.1", + }, + }), + ), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + + const { out, err, exitCode } = await publish(env, packageDir); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + expect(await exists(join(packageDir, "node_modules", "publish-pkg-1", "package.json"))).toBeTrue(); + }); + test("can publish from a tarball", async () => { + const bunfig = await authBunfig("tarball"); + const json = { + name: "publish-pkg-2", + version: "2.2.2", + dependencies: { + "publish-pkg-2": "2.2.2", + }, + }; + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), + write(packageJson, JSON.stringify(json)), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + + await pack(packageDir, env); + + let { out, err, exitCode } = await publish(env, packageDir, "./publish-pkg-2-2.2.2.tgz"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + expect(await exists(join(packageDir, "node_modules", "publish-pkg-2", "package.json"))).toBeTrue(); + + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), + rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }), + rm(join(packageDir, "node_modules"), { recursive: true, force: true }), + ]); + + // now with an absoute path + ({ out, err, exitCode } = await publish(env, packageDir, join(packageDir, "publish-pkg-2-2.2.2.tgz"))); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + expect(await file(join(packageDir, "node_modules", "publish-pkg-2", "package.json")).json()).toEqual(json); + }); + + for (const info of [ + { user: "bin1", bin: "bin1.js" }, + { user: "bin2", bin: { bin1: "bin1.js", bin2: "bin2.js" } }, + { user: "bin3", directories: { bin: "bins" } }, + ]) { + test(`can publish and install binaries with ${JSON.stringify(info)}`, async () => { + const publishDir = tmpdirSync(); + const bunfig = await authBunfig("binaries-" + info.user); + console.log({ packageDir, publishDir }); + + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-bins"), { recursive: true, force: true }), + write( + join(publishDir, "package.json"), + JSON.stringify({ + name: "publish-pkg-bins", + version: "1.1.1", + ...info, + }), + ), + write(join(publishDir, "bunfig.toml"), bunfig), + write(join(publishDir, "bin1.js"), `#!/usr/bin/env bun\nconsole.log("bin1!")`), + write(join(publishDir, "bin2.js"), `#!/usr/bin/env bun\nconsole.log("bin2!")`), + write(join(publishDir, "bins", "bin3.js"), `#!/usr/bin/env bun\nconsole.log("bin3!")`), + write(join(publishDir, "bins", "moredir", "bin4.js"), `#!/usr/bin/env bun\nconsole.log("bin4!")`), + + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "publish-pkg-bins": "1.1.1", + }, + }), + ), + ]); + + const { out, err, exitCode } = await publish(env, publishDir); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(out).toContain("+ publish-pkg-bins@1.1.1"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + + const results = await Promise.all([ + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin1.bunx" : "bin1")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin2.bunx" : "bin2")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin3.js.bunx" : "bin3.js")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin4.js.bunx" : "bin4.js")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "moredir" : "moredir/bin4.js")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "publish-pkg-bins.bunx" : "publish-pkg-bins")), + ]); + + switch (info.user) { + case "bin1": { + expect(results).toEqual([false, false, false, false, false, true]); + break; + } + case "bin2": { + expect(results).toEqual([true, true, false, false, false, false]); + break; + } + case "bin3": { + expect(results).toEqual([false, false, true, true, !isWindows, false]); + break; + } + } + }); + } + + test("dependencies are installed", async () => { + const publishDir = tmpdirSync(); + const bunfig = await authBunfig("manydeps"); + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-deps"), { recursive: true, force: true }), + write( + join(publishDir, "package.json"), + JSON.stringify( + { + name: "publish-pkg-deps", + version: "1.1.1", + dependencies: { + "no-deps": "1.0.0", + }, + peerDependencies: { + "a-dep": "1.0.1", + }, + optionalDependencies: { + "basic-1": "1.0.0", + }, + }, + null, + 2, + ), + ), + write(join(publishDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "publish-pkg-deps": "1.1.1", + }, + }), + ), + ]); + + let { out, err, exitCode } = await publish(env, publishDir); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(out).toContain("+ publish-pkg-deps@1.1.1"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + + const results = await Promise.all([ + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "a-dep", "package.json")), + exists(join(packageDir, "node_modules", "basic-1", "package.json")), + ]); + + expect(results).toEqual([true, true, true]); + }); + + test("can publish workspace package", async () => { + const bunfig = await authBunfig("workspace"); + const pkgJson = { + name: "publish-pkg-3", + version: "3.3.3", + dependencies: { + "publish-pkg-3": "3.3.3", + }, + }; + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-3"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + }), + ), + write(join(packageDir, "packages", "publish-pkg-3", "package.json"), JSON.stringify(pkgJson)), + ]); + + await publish(env, join(packageDir, "packages", "publish-pkg-3")); + + await write(packageJson, JSON.stringify({ name: "root", "dependencies": { "publish-pkg-3": "3.3.3" } })); + + await runBunInstall(env, packageDir); + + expect(await file(join(packageDir, "node_modules", "publish-pkg-3", "package.json")).json()).toEqual(pkgJson); + }); + + describe("--dry-run", async () => { + test("does not publish", async () => { + const bunfig = await authBunfig("dryrun"); + await Promise.all([ + rm(join(verdaccio.packagesPath, "dry-run-1"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "dry-run-1", + version: "1.1.1", + dependencies: { + "dry-run-1": "1.1.1", + }, + }), + ), + ]); + + const { out, err, exitCode } = await publish(env, packageDir, "--dry-run"); + expect(exitCode).toBe(0); + + expect(await exists(join(verdaccio.packagesPath, "dry-run-1"))).toBeFalse(); + }); + test("does not publish from tarball path", async () => { + const bunfig = await authBunfig("dryruntarball"); + await Promise.all([ + rm(join(verdaccio.packagesPath, "dry-run-2"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "dry-run-2", + version: "2.2.2", + dependencies: { + "dry-run-2": "2.2.2", + }, + }), + ), + ]); + + await pack(packageDir, env); + + const { out, err, exitCode } = await publish(env, packageDir, "./dry-run-2-2.2.2.tgz", "--dry-run"); + expect(exitCode).toBe(0); + + expect(await exists(join(verdaccio.packagesPath, "dry-run-2"))).toBeFalse(); + }); + }); + + describe("lifecycle scripts", async () => { + const script = `const fs = require("fs"); + fs.writeFileSync(process.argv[2] + ".txt", \` +prepublishOnly: \${fs.existsSync("prepublishOnly.txt")} +publish: \${fs.existsSync("publish.txt")} +postpublish: \${fs.existsSync("postpublish.txt")} +prepack: \${fs.existsSync("prepack.txt")} +prepare: \${fs.existsSync("prepare.txt")} +postpack: \${fs.existsSync("postpack.txt")}\`)`; + const json = { + name: "publish-pkg-4", + version: "4.4.4", + scripts: { + // should happen in this order + "prepublishOnly": `${bunExe()} script.js prepublishOnly`, + "prepack": `${bunExe()} script.js prepack`, + "prepare": `${bunExe()} script.js prepare`, + "postpack": `${bunExe()} script.js postpack`, + "publish": `${bunExe()} script.js publish`, + "postpublish": `${bunExe()} script.js postpublish`, + }, + dependencies: { + "publish-pkg-4": "4.4.4", + }, + }; + + for (const arg of ["", "--dry-run"]) { + test(`should run in order${arg ? " (--dry-run)" : ""}`, async () => { + const bunfig = await authBunfig("lifecycle" + (arg ? "dry" : "")); + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-4"), { recursive: true, force: true }), + write(packageJson, JSON.stringify(json)), + write(join(packageDir, "script.js"), script), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + + const { out, err, exitCode } = await publish(env, packageDir, arg); + expect(exitCode).toBe(0); + + const results = await Promise.all([ + file(join(packageDir, "prepublishOnly.txt")).text(), + file(join(packageDir, "prepack.txt")).text(), + file(join(packageDir, "prepare.txt")).text(), + file(join(packageDir, "postpack.txt")).text(), + file(join(packageDir, "publish.txt")).text(), + file(join(packageDir, "postpublish.txt")).text(), + ]); + + expect(results).toEqual([ + "\nprepublishOnly: false\npublish: false\npostpublish: false\nprepack: false\nprepare: false\npostpack: false", + "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: false\nprepare: false\npostpack: false", + "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: false\npostpack: false", + "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: true\npostpack: false", + "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: true\npostpack: true", + "\nprepublishOnly: true\npublish: true\npostpublish: false\nprepack: true\nprepare: true\npostpack: true", + ]); + }); + } + + test("--ignore-scripts", async () => { + const bunfig = await authBunfig("ignorescripts"); + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-5"), { recursive: true, force: true }), + write(packageJson, JSON.stringify(json)), + write(join(packageDir, "script.js"), script), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + + const { out, err, exitCode } = await publish(env, packageDir, "--ignore-scripts"); + expect(exitCode).toBe(0); + + const results = await Promise.all([ + exists(join(packageDir, "prepublishOnly.txt")), + exists(join(packageDir, "prepack.txt")), + exists(join(packageDir, "prepare.txt")), + exists(join(packageDir, "postpack.txt")), + exists(join(packageDir, "publish.txt")), + exists(join(packageDir, "postpublish.txt")), + ]); + + expect(results).toEqual([false, false, false, false, false, false]); + }); + }); + + test("attempting to publish a private package should fail", async () => { + const bunfig = await authBunfig("privatepackage"); + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-6"), { recursive: true, force: true }), + write( + packageJson, + JSON.stringify({ + name: "publish-pkg-6", + version: "6.6.6", + private: true, + dependencies: { + "publish-pkg-6": "6.6.6", + }, + }), + ), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + + // should fail + let { out, err, exitCode } = await publish(env, packageDir); + expect(exitCode).toBe(1); + expect(err).toContain("error: attempted to publish a private package"); + expect(await exists(join(verdaccio.packagesPath, "publish-pkg-6-6.6.6.tgz"))).toBeFalse(); + + // try tarball + await pack(packageDir, env); + ({ out, err, exitCode } = await publish(env, packageDir, "./publish-pkg-6-6.6.6.tgz")); + expect(exitCode).toBe(1); + expect(err).toContain("error: attempted to publish a private package"); + expect(await exists(join(packageDir, "publish-pkg-6-6.6.6.tgz"))).toBeTrue(); + }); + + describe("access", async () => { + test("--access", async () => { + const bunfig = await authBunfig("accessflag"); + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-7"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write( + packageJson, + JSON.stringify({ + name: "publish-pkg-7", + version: "7.7.7", + }), + ), + ]); + + // should fail + let { out, err, exitCode } = await publish(env, packageDir, "--access", "restricted"); + expect(exitCode).toBe(1); + expect(err).toContain("error: unable to restrict access to unscoped package"); + + ({ out, err, exitCode } = await publish(env, packageDir, "--access", "public")); + expect(exitCode).toBe(0); + + expect(await exists(join(verdaccio.packagesPath, "publish-pkg-7"))).toBeTrue(); + }); + + for (const access of ["restricted", "public"]) { + test(`access ${access}`, async () => { + const bunfig = await authBunfig("access" + access); + + const pkgJson = { + name: "@secret/publish-pkg-8", + version: "8.8.8", + dependencies: { + "@secret/publish-pkg-8": "8.8.8", + }, + publishConfig: { + access, + }, + }; + + await Promise.all([ + rm(join(verdaccio.packagesPath, "@secret", "publish-pkg-8"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write(packageJson, JSON.stringify(pkgJson)), + ]); + + let { out, err, exitCode } = await publish(env, packageDir); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + + expect(await file(join(packageDir, "node_modules", "@secret", "publish-pkg-8", "package.json")).json()).toEqual( + pkgJson, + ); + }); + } + }); + + describe("tag", async () => { + test("can publish with a tag", async () => { + const bunfig = await authBunfig("simpletag"); + const pkgJson = { + name: "publish-pkg-9", + version: "9.9.9", + dependencies: { + "publish-pkg-9": "simpletag", + }, + }; + await Promise.all([ + rm(join(verdaccio.packagesPath, "publish-pkg-9"), { recursive: true, force: true }), + write(join(packageDir, "bunfig.toml"), bunfig), + write(packageJson, JSON.stringify(pkgJson)), + ]); + + let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + expect(await file(join(packageDir, "node_modules", "publish-pkg-9", "package.json")).json()).toEqual(pkgJson); + }); + }); +}); + +describe("package.json indentation", async () => { + test("works for root and workspace packages", async () => { + await Promise.all([ + // 5 space indentation + write(packageJson, `\n{\n\n "name": "foo",\n"workspaces": ["packages/*"]\n}`), + // 1 tab indentation + write(join(packageDir, "packages", "bar", "package.json"), `\n{\n\n\t"name": "bar",\n}`), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const rootPackageJson = await file(packageJson).text(); + + expect(rootPackageJson).toBe( + `{\n "name": "foo",\n "workspaces": ["packages/*"],\n "dependencies": {\n "no-deps": "^2.0.0"\n }\n}`, + ); + + // now add to workspace. it should keep tab indentation + ({ exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: join(packageDir, "packages", "bar"), + stdout: "inherit", + stderr: "inherit", + env, + })); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).text()).toBe(rootPackageJson); + const workspacePackageJson = await file(join(packageDir, "packages", "bar", "package.json")).text(); + expect(workspacePackageJson).toBe(`{\n\t"name": "bar",\n\t"dependencies": {\n\t\t"no-deps": "^2.0.0"\n\t}\n}`); + }); + + test("install maintains indentation", async () => { + await write(packageJson, `{\n "dependencies": {}\n }\n`); + let { exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + expect(await exited).toBe(0); + expect(await file(packageJson).text()).toBe(`{\n "dependencies": {\n "no-deps": "^2.0.0"\n }\n}\n`); + }); +}); + +describe("text lockfile", () => { + test("workspace sorting", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "b", "package.json"), + JSON.stringify({ + name: "b", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "c", "package.json"), + JSON.stringify({ + name: "c", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect( + (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), + ).toMatchSnapshot(); + + // now add workspace 'a' + await write( + join(packageDir, "packages", "a", "package.json"), + JSON.stringify({ + name: "a", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + const lockfile = await Bun.file(join(packageDir, "bun.lock")).text(); + expect(lockfile.replaceAll(/localhost:\d+/g, "localhost:1234")).toMatchSnapshot(); + }); + + test("--frozen-lockfile", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "^1.0.0", + "a-dep": "^1.0.2", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "package1", + dependencies: { + "peer-deps-too": "1.0.0", + }, + }), + ), + ]); + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + let err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + const firstLockfile = await Bun.file(join(packageDir, "bun.lock")).text(); + + expect(firstLockfile.replace(/localhost:\d+/g, "localhost:1234")).toMatchSnapshot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--frozen-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "a-dep", + "no-deps", + "package1", + "peer-deps-too", + ]); + + expect(await Bun.file(join(packageDir, "bun.lock")).text()).toBe(firstLockfile); + }); + + for (const omit of ["dev", "peer", "optional"]) { + test(`resolvable lockfile with ${omit} dependencies disabled`, async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + peerDependencies: { "no-deps": "1.0.0" }, + devDependencies: { "a-dep": "1.0.1" }, + optionalDependencies: { "basic-1": "1.0.0" }, + }), + ), + ]); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile", `--omit=${omit}`], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + let err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + const depName = omit === "dev" ? "a-dep" : omit === "peer" ? "no-deps" : "basic-1"; + + expect(await exists(join(packageDir, "node_modules", depName))).toBeFalse(); + + const lockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", depName))).toBeTrue(); + + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + lockfile, + ); + }); + } + + test("optionalPeers", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + peerDependencies: { + "no-deps": "1.0.0", + }, + peerDependenciesMeta: { + "no-deps": { + optional: true, + }, + }, + }), + ), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // another install should recognize the peer dependency as `"optional": true` + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + firstLockfile, + ); + }); +}); + +test("--lockfile-only", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "^1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "package1", + dependencies: { + "two-range-deps": "1.0.0", + }, + }), + ), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile", "--lockfile-only"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + // nothing changes with another --lockfile-only + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--lockfile-only"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + firstLockfile, + ); + + // --silent works + const { + stdout, + stderr, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install", "--lockfile-only", "--silent"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + expect(await exited2).toBe(0); + const out = await Bun.readableStreamToText(stdout); + const err = await Bun.readableStreamToText(stderr); + expect(out).toBe(""); + expect(err).toBe(""); +}); + +describe("bundledDependencies", () => { + for (const textLockfile of [true, false]) { + test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) basic`, async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "bundled-basic", + version: "1.1.1", + dependencies: { + "bundled-1": "1.0.0", + }, + }), + ), + ]); + + const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; + let { exited } = spawn({ + cmd, + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "bundled-1", "node_modules", "no-deps", "package.json")), + ]), + ).toEqual([false, true]); + + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "bundled-1", "node_modules", "no-deps", "package.json")), + ]), + ).toEqual([false, true]); + }); + + test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) bundledDependencies === true`, async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "bundled-true", + version: "1.1.1", + dependencies: { + "bundled-true": "1.0.0", + }, + }), + ), + ]); + + const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; + let { exited } = spawn({ + cmd, + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + async function check() { + return Promise.all([ + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "one-dep", "package.json")), + exists(join(packageDir, "node_modules", "bundled-true", "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "bundled-true", "node_modules", "one-dep", "package.json")), + exists( + join( + packageDir, + "node_modules", + "bundled-true", + "node_modules", + "one-dep", + "node_modules", + "no-deps", + "package.json", + ), + ), + ]); + } + + expect(await check()).toEqual([false, false, true, true, true]); + + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + expect(await check()).toEqual([false, false, true, true, true]); + }); + + test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) transitive bundled dependency collision`, async () => { + // Install a package with one bundled dependency and one regular dependency. + // The bundled dependency has a transitive dependency of the same regular dependency, + // but at a different version. Test that the regular dependency does not replace the + // other version (both should exist). + + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "bundled-collision", + dependencies: { + "bundled-transitive": "1.0.0", + // prevent both transitive dependencies from hoisting + "no-deps": "npm:a-dep@1.0.2", + "one-dep": "npm:a-dep@1.0.3", + }, + }), + ), + ]); + + const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; + let { exited } = spawn({ + cmd, + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + async function check() { + expect( + await Promise.all([ + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + file( + join(packageDir, "node_modules", "bundled-transitive", "node_modules", "no-deps", "package.json"), + ).json(), + file( + join( + packageDir, + "node_modules", + "bundled-transitive", + "node_modules", + "one-dep", + "node_modules", + "no-deps", + "package.json", + ), + ).json(), + exists(join(packageDir, "node_modules", "bundled-transitive", "node_modules", "one-dep", "package.json")), + ]), + ).toEqual([ + { name: "a-dep", version: "1.0.2" }, + { name: "no-deps", version: "1.0.0" }, + { name: "no-deps", version: "1.0.1" }, + true, + ]); + } + + await check(); + + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + await check(); + }); + + test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) git dependencies`, async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "bundled-git", + dependencies: { + // bundledDependencies: ["zod"], + "install-test1": "dylan-conway/bundled-install-test#7824752", + // bundledDependencies: true, + "install-test2": "git+ssh://git@github.com/dylan-conway/bundled-install-test#1301309", + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` +[install] +cache = "${join(packageDir, ".bun-cache")}" + `, + ), + ]); + + const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; + let { exited } = spawn({ + cmd, + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + async function check() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "zod", "package.json")), + exists(join(packageDir, "node_modules", "install-test1", "node_modules", "zod", "package.json")), + exists(join(packageDir, "node_modules", "install-test2", "node_modules", "zod", "package.json")), + ]), + ).toEqual([false, true, true]); + } + + await check(); + + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + await check(); + }); + + test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) workspace dependencies bundle correctly`, async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "bundled-workspace", + workspaces: ["packages/*"], + }), + ), + write( + join(packageDir, "packages", "pkg-one-one-one", "package.json"), + JSON.stringify({ + name: "pkg-one-one-one", + dependencies: { + "no-deps": "1.0.0", + "bundled-1": "1.0.0", + }, + bundledDependencies: ["no-deps"], + }), + ), + ]); + + const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; + let { exited } = spawn({ + cmd, + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + async function check() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "packages", "pkg-one-one-one", "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "bundled-1", "node_modules", "no-deps", "package.json")), + ]), + ).toEqual([true, false, true]); + } + + await check(); + + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + await check(); + }); + } +}); + +describe("optionalDependencies", () => { + for (const optional of [true, false]) { + test(`exit code is ${optional ? 0 : 1} when ${optional ? "optional" : ""} dependency tarball is missing`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + [optional ? "optionalDependencies" : "dependencies"]: { + "missing-tarball": "1.0.0", + "uses-what-bin": "1.0.0", + }, + "trustedDependencies": ["uses-what-bin"], + }), + ); + + const { exited, err } = await runBunInstall(env, packageDir, { + [optional ? "allowWarnings" : "allowErrors"]: true, + expectedExitCode: optional ? 0 : 1, + savesLockfile: false, + }); + expect(err).toContain( + `${optional ? "warn" : "error"}: GET http://localhost:${port}/missing-tarball/-/missing-tarball-1.0.0.tgz - `, + ); + expect(await exited).toBe(optional ? 0 : 1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "uses-what-bin", "what-bin"]); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + }); + } + + for (const rootOptional of [true, false]) { + test(`exit code is 0 when ${rootOptional ? "root" : ""} optional dependency does not exist in registry`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + [rootOptional ? "optionalDependencies" : "dependencies"]: { + [rootOptional ? "this-package-does-not-exist-in-the-registry" : "has-missing-optional-dep"]: "||", + }, + }), + ); + + const { err } = await runBunInstall(env, packageDir, { + allowWarnings: true, + savesLockfile: !rootOptional, + }); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(err).toMatch(`warn: GET http://localhost:${port}/this-package-does-not-exist-in-the-registry - 404`); + }); + } + test("should not install optional deps if false in bunfig", async () => { + await writeFile( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = "${join(packageDir, ".bun-cache")}" + optional = false + registry = "http://localhost:${port}/" + `, + ); + await writeFile( + packageJson, + JSON.stringify( + { + name: "publish-pkg-deps", + version: "1.1.1", + dependencies: { + "no-deps": "1.0.0", + }, + optionalDependencies: { + "basic-1": "1.0.0", + }, + }, + null, + 2, + ), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "1 package installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps"]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("lifecycle scripts failures from transitive dependencies are ignored", async () => { + // Dependency with a transitive optional dependency that fails during its preinstall script. + await write( + packageJson, + JSON.stringify({ + name: "foo", + version: "2.2.2", + dependencies: { + "optional-lifecycle-fail": "1.1.1", + }, + trustedDependencies: ["lifecycle-fail"], + }), + ); + + const { err, exited } = await runBunInstall(env, packageDir); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "optional-lifecycle-fail", "package.json")), + exists(join(packageDir, "node_modules", "lifecycle-fail", "package.json")), + ]), + ).toEqual([true, false]); + }); +}); + +test("it should ignore peerDependencies within workspaces", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/baz"], + peerDependencies: { + "no-deps": ">=1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "baz", "package.json"), + JSON.stringify({ + name: "Baz", + peerDependencies: { + "a-dep": ">=1.0.1", + }, + }), + ), + write(join(packageDir, ".npmrc"), `omit=peer`), + ]); + + const { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + env, + }); + + expect(await exited).toBe(0); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["Baz"]); + expect( + (await file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), + ).toMatchSnapshot(); + + // installing with them enabled works + await rm(join(packageDir, ".npmrc")); + await runBunInstall(env, packageDir, { savesLockfile: false }); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["Baz", "a-dep", "no-deps"]); +}); + +test("disabled dev/peer/optional dependencies are still included in the lockfile", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + devDependencies: { + "no-deps": "1.0.0", + }, + peerDependencies: { + "a-dep": "1.0.1", + }, + optionalDependencies: { + "basic-1": "1.0.0", + }, + }), + ), + ]); + + await runBunInstall; +}); + +test("tarball override does not crash", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "two-range-deps": "||", + }, + overrides: { + "no-deps": `http://localhost:${port}/no-deps/-/no-deps-2.0.0.tgz`, + }, + }), + ); + + await runBunInstall(env, packageDir); + + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "2.0.0", + }); +}); + +describe.each(["--production", "without --production"])("%s", flag => { + const prod = flag === "--production"; + const order = ["devDependencies", "dependencies"]; + // const stdio = process.versions.bun.includes("debug") ? "inherit" : "ignore"; + const stdio = "ignore"; + + if (prod) { + test("modifying package.json with --production should not save lockfile", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "bin-change-dir": "1.0.0", + }, + devDependencies: { + "bin-change-dir": "1.0.1", + "basic-1": "1.0.0", + }, + }), + ); + + var { exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: stdio, + stdin: stdio, + stderr: stdio, + env, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const initialHash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer()); + + expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ + name: "bin-change-dir", + version: "1.0.1", + }); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--production"], + cwd: packageDir, + stdout: stdio, + stdin: stdio, + stderr: stdio, + env, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ + name: "bin-change-dir", + version: "1.0.0", + }); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--production", "bin-change-dir@1.0.1"], + cwd: packageDir, + stdout: stdio, + stdin: stdio, + stderr: stdio, + env, + }); + + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + // We should not have saved bun.lockb + expect(Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())).toBe(initialHash); + + // We should not have installed bin-change-dir@1.0.1 + expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ + name: "bin-change-dir", + version: "1.0.0", + }); + + // This is a no-op. It should work. + var { exited } = spawn({ + cmd: [bunExe(), "install", "--production", "bin-change-dir@1.0.0"], + cwd: packageDir, + stdout: stdio, + stdin: stdio, + stderr: stdio, + env, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + // We should not have saved bun.lockb + expect(Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())).toBe(initialHash); + + // We should have installed bin-change-dir@1.0.0 + expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ + name: "bin-change-dir", + version: "1.0.0", + }); + }); + } + + test(`should prefer ${order[+prod % 2]} over ${order[1 - (+prod % 2)]}`, async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "bin-change-dir": "1.0.0", + }, + devDependencies: { + "bin-change-dir": "1.0.1", + "basic-1": "1.0.0", + }, + }), + ); + + let initialLockfileHash; + async function saveWithoutProd() { + var hash; + // First install without --production + // so that the lockfile is up to date + var { exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: stdio, + stdin: stdio, + stderr: stdio, + env, + }); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await Promise.all([ + (async () => + expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ + name: "bin-change-dir", + version: "1.0.1", + }))(), + (async () => + expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toMatchObject({ + name: "basic-1", + version: "1.0.0", + }))().then( + async () => await rm(join(packageDir, "node_modules", "basic-1"), { recursive: true, force: true }), + ), + + (async () => (hash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())))(), + ]); + + return hash; + } + if (prod) { + initialLockfileHash = await saveWithoutProd(); + } + + var { exited } = spawn({ + cmd: [bunExe(), "install", prod ? "--production" : ""].filter(Boolean), + cwd: packageDir, + stdout: stdio, + stdin: stdio, + stderr: stdio, + env, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ + name: "bin-change-dir", + version: prod ? "1.0.0" : "1.0.1", + }); + + if (!prod) { + expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toMatchObject({ + name: "basic-1", + version: "1.0.0", + }); + } else { + // it should not install devDependencies + expect(await exists(join(packageDir, "node_modules", "basic-1"))).toBeFalse(); + + // it should not mutate the lockfile when there were no changes to begin with. + const newHash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer()); + + expect(newHash).toBe(initialLockfileHash!); + } + + if (prod) { + // lets now try to install again without --production + const newHash = await saveWithoutProd(); + expect(newHash).toBe(initialLockfileHash); + } + }); +}); + +test("hardlinks on windows dont fail with long paths", async () => { + await mkdir(join(packageDir, "a-package")); + await writeFile( + join(packageDir, "a-package", "package.json"), + JSON.stringify({ + name: "a-package", + version: "1.0.0", + }), + ); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + // 255 characters + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": + "file:./a-package", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@a-package", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); +}); + +test("basic 1", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "basic-1": "1.0.0", + }, + }), + ); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ basic-1@1.0.0", + "", + "1 package installed", + ]); + expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toEqual({ + name: "basic-1", + version: "1.0.0", + } as any); + expect(await exited).toBe(0); + + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ basic-1@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); +}); + +test("manifest cache will invalidate when registry changes", async () => { + const cacheDir = join(packageDir, ".bun-cache"); + await Promise.all([ + write( + join(packageDir, "bunfig.toml"), + ` +[install] +cache = "${cacheDir}" +registry = "http://localhost:${port}" + `, + ), + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // is-number exists in our custom registry and in npm. Switching the registry should invalidate + // the manifest cache, the package could be a completely different package. + "is-number": "2.0.0", + }, + }), + ), + ]); + + // first install this package from verdaccio + await runBunInstall(env, packageDir); + const lockfile = await parseLockfile(packageDir); + for (const pkg of Object.values(lockfile.packages) as any) { + if (pkg.tag === "npm") { + expect(pkg.resolution.resolved).toContain(`http://localhost:${port}`); + } + } + + // now use default registry + await Promise.all([ + rm(join(packageDir, "node_modules"), { force: true, recursive: true }), + rm(join(packageDir, "bun.lockb"), { force: true }), + write( + join(packageDir, "bunfig.toml"), + ` +[install] +cache = "${cacheDir}" +`, + ), + ]); + + await runBunInstall(env, packageDir); + const npmLockfile = await parseLockfile(packageDir); + for (const pkg of Object.values(npmLockfile.packages) as any) { + if (pkg.tag === "npm") { + expect(pkg.resolution.resolved).not.toContain(`http://localhost:${port}`); + } + } +}); + +test("dependency from root satisfies range from dependency", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "one-range-dep": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ one-range-dep@1.0.0", + "", + "2 packages installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ one-range-dep@1.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); +}); + +test("duplicate names and versions in a manifest do not install incorrect packages", async () => { + /** + * `duplicate-name-and-version` has two versions: + * 1.0.1: + * dependencies: { + * "no-deps": "a-dep" + * } + * 1.0.2: + * dependencies: { + * "a-dep": "1.0.1" + * } + * Note: version for `no-deps` is the same as second dependency name. + * + * When this manifest is parsed, the strings for dependency names and versions are stored + * with different lists offset length pairs, but we were deduping with the same map. Since + * the version of the first dependency is the same as the name as the second, it would try to + * dedupe them, and doing so would give the wrong name for the deduped dependency. + * (`a-dep@1.0.1` would become `no-deps@1.0.1`) + */ + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "duplicate-name-and-version": "1.0.2", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const lockfile = parseLockfile(packageDir); + expect(lockfile).toMatchNodeModulesAt(packageDir); + const results = await Promise.all([ + file(join(packageDir, "node_modules", "duplicate-name-and-version", "package.json")).json(), + file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), + exists(join(packageDir, "node_modules", "no-deps")), + ]); + + expect(results).toMatchObject([ + { name: "duplicate-name-and-version", version: "1.0.2" }, + { name: "a-dep", version: "1.0.1" }, + false, + ]); +}); + +describe("peerDependency index out of bounds", async () => { + // Test for "index of out bounds" errors with peer dependencies when adding/removing a package + // + // Repro: + // - Install `1-peer-dep-a`. It depends on peer dep `no-deps@1.0.0`. + // - Replace `1-peer-dep-a` with `1-peer-dep-b` (identical other than name), delete manifest cache and + // node_modules, then reinstall. + // - `no-deps` will enqueue a dependency id that goes out of bounds + + const dependencies = ["1-peer-dep-a", "1-peer-dep-b", "2-peer-deps-c"]; + + for (const firstDep of dependencies) { + for (const secondDep of dependencies) { + if (firstDep === secondDep) continue; + test(`replacing ${firstDep} with ${secondDep}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + [firstDep]: "1.0.0", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const lockfile = parseLockfile(packageDir); + expect(lockfile).toMatchNodeModulesAt(packageDir); + const results = await Promise.all([ + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + file(join(packageDir, "node_modules", firstDep, "package.json")).json(), + exists(join(packageDir, "node_modules", firstDep, "node_modules", "no-deps")), + ]); + + expect(results).toMatchObject([ + { name: "no-deps", version: "1.0.0" }, + { name: firstDep, version: "1.0.0" }, + false, + ]); + + await Promise.all([ + rm(join(packageDir, "node_modules"), { recursive: true, force: true }), + rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }), + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + [secondDep]: "1.0.0", + }, + }), + ), + ]); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const newLockfile = parseLockfile(packageDir); + expect(newLockfile).toMatchNodeModulesAt(packageDir); + const newResults = await Promise.all([ + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + file(join(packageDir, "node_modules", secondDep, "package.json")).json(), + exists(join(packageDir, "node_modules", secondDep, "node_modules", "no-deps")), + ]); + + expect(newResults).toMatchObject([ + { name: "no-deps", version: "1.0.0" }, + { name: secondDep, version: "1.0.0" }, + false, + ]); + }); + } + } + + // Install 2 dependencies, one is a normal dependency, the other is a dependency with a optional + // peer dependency on the first dependency. Delete node_modules and cache, then update the dependency + // with the optional peer to a new version. Doing this will cause the peer dependency to get enqueued + // internally, testing for index out of bounds. It's also important cache is deleted to ensure a tarball + // task is created for it. + test("optional", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "optional-peer-deps": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + // update version and delete node_modules and cache + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "optional-peer-deps": "1.0.1", + "no-deps": "1.0.0", + }, + }), + ), + rm(join(packageDir, "node_modules"), { recursive: true, force: true }), + rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }), + ]); + + // this install would trigger the index out of bounds error + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const lockfile = parseLockfile(packageDir); + expect(lockfile).toMatchNodeModulesAt(packageDir); + }); +}); + +test("peerDependency in child npm dependency should not maintain old version when package is upgraded", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ peer-deps-fixed@1.0.0", + "", + "2 packages installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.1", // upgrade the package + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.1", + } as any); + expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.1"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); +}); + +test("package added after install", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "one-range-dep": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ one-range-dep@1.0.0", + "", + "2 packages installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.1.0", + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + // add `no-deps` to root package.json with a smaller but still compatible + // version for `one-range-dep`. + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "one-range-dep": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "2 packages installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + } as any); + expect( + await file(join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps", "package.json")).json(), + ).toEqual({ + name: "no-deps", + version: "1.1.0", + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ one-range-dep@1.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); +}); + +test("--production excludes devDependencies in workspaces", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "1.0.0", + }, + devDependencies: { + "a1": "npm:no-deps@1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "a-dep": "1.0.2", + }, + devDependencies: { + "a2": "npm:a-dep@1.0.2", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + devDependencies: { + "a3": "npm:a-dep@1.0.3", + "a4": "npm:a-dep@1.0.4", + "a5": "npm:a-dep@1.0.5", + }, + }), + ), + ]); + + // without lockfile + const expectedResults = [ + ["a-dep", "no-deps", "pkg1", "pkg2"], + { name: "no-deps", version: "1.0.0" }, + { name: "a-dep", version: "1.0.2" }, + ]; + let { out } = await runBunInstall(env, packageDir, { production: true }); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "4 packages installed", + ]); + let results = await Promise.all([ + readdirSorted(join(packageDir, "node_modules")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), + ]); + + expect(results).toMatchObject(expectedResults); + + // create non-production lockfile, then install with --production + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + ({ out } = await runBunInstall(env, packageDir)); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ a1@1.0.0", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "7 packages installed", + ]); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + ({ out } = await runBunInstall(env, packageDir, { production: true })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "4 packages installed", + ]); + results = await Promise.all([ + readdirSorted(join(packageDir, "node_modules")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), + ]); + expect(results).toMatchObject(expectedResults); +}); + +test("--production without a lockfile will install and not save lockfile", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--production"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "no-deps", "index.js"))).toBeTrue(); +}); + +describe("binaries", () => { + for (const global of [false, true]) { + describe(`existing destinations${global ? " (global)" : ""}`, () => { + test("existing non-symlink", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "what-bin": "1.0.0", + }, + }), + ), + write(join(packageDir, "node_modules", ".bin", "what-bin"), "hi"), + ]); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin( + join("..", "what-bin", "what-bin.js"), + ); + }); + }); + } + test("it should correctly link binaries after deleting node_modules", async () => { + const json: any = { + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + "uses-what-bin": "1.5.0", + }, + }; + await writeFile(packageJson, JSON.stringify(json)); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("will link binaries for packages installed multiple times", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin": "1.5.0", + }, + workspaces: ["packages/*"], + trustedDependencies: ["uses-what-bin"], + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ), + ]); + + // Root dependends on `uses-what-bin@1.5.0` and both packages depend on `uses-what-bin@1.0.0`. + // This test makes sure the binaries used by `pkg1` and `pkg2` are the correct version (`1.0.0`) + // instead of using the root version (`1.5.0`). + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const results = await Promise.all([ + file(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt")).text(), + file(join(packageDir, "packages", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")).text(), + file(join(packageDir, "packages", "pkg2", "node_modules", "uses-what-bin", "what-bin.txt")).text(), + ]); + + expect(results).toEqual(["what-bin@1.5.0", "what-bin@1.0.0", "what-bin@1.0.0"]); + }); + + test("it should re-symlink binaries that become invalid when updating package versions", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "bin-change-dir": "1.0.0", + }, + scripts: { + postinstall: "bin-change-dir", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ bin-change-dir@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "bin-1.0.0.txt")).text()).toEqual("success!"); + expect(await exists(join(packageDir, "bin-1.0.1.txt"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "bin-change-dir": "1.0.1", + }, + scripts: { + postinstall: "bin-change-dir", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ bin-change-dir@1.0.1"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "bin-1.0.0.txt")).text()).toEqual("success!"); + expect(await file(join(packageDir, "bin-1.0.1.txt")).text()).toEqual("success!"); + }); + + test("will only link global binaries for requested packages", async () => { + await Promise.all([ + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "http://localhost:${port}/" + globalBinDir = "${join(packageDir, "global-bin-dir").replace(/\\/g, "\\\\")}" + `, + ), + , + ]); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "-g", `--config=${join(packageDir, "bunfig.toml")}`, "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") }, + }); + + let err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + let out = await Bun.readableStreamToText(stdout); + expect(out).toContain("uses-what-bin@1.5.0"); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "global-bin-dir", "what-bin"))).toBeFalse(); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "-g", `--config=${join(packageDir, "bunfig.toml")}`, "what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") }, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + + expect(out).toContain("what-bin@1.5.0"); + expect(await exited).toBe(0); + + // now `what-bin` should be installed in the global bin directory + if (isWindows) { + expect( + await Promise.all([ + exists(join(packageDir, "global-bin-dir", "what-bin.exe")), + exists(join(packageDir, "global-bin-dir", "what-bin.bunx")), + ]), + ).toEqual([true, true]); + } else { + expect(await exists(join(packageDir, "global-bin-dir", "what-bin"))).toBeTrue(); + } + }); + + for (const global of [false, true]) { + test(`bin types${global ? " (global)" : ""}`, async () => { + if (global) { + await write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "http://localhost:${port}/" + globalBinDir = "${join(packageDir, "global-bin-dir").replace(/\\/g, "\\\\")}" + `, + ); + } else { + await write( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + } + + const args = [ + bunExe(), + "install", + ...(global ? ["-g"] : []), + ...(global ? [`--config=${join(packageDir, "bunfig.toml")}`] : []), + "dep-with-file-bin", + "dep-with-single-entry-map-bin", + "dep-with-directory-bins", + "dep-with-map-bins", + ]; + const { stdout, stderr, exited } = spawn({ + cmd: args, + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: global ? { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") } : env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + + const out = await Bun.readableStreamToText(stdout); + expect(await exited).toBe(0); + + await runBin("dep-with-file-bin", "file-bin\n", global); + await runBin("single-entry-map-bin", "single-entry-map-bin\n", global); + await runBin("directory-bin-1", "directory-bin-1\n", global); + await runBin("directory-bin-2", "directory-bin-2\n", global); + await runBin("map-bin-1", "map-bin-1\n", global); + await runBin("map-bin-2", "map-bin-2\n", global); + }); + } + + test("each type of binary serializes correctly to text lockfile", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.1.1", + dependencies: { + "file-bin": "./file-bin", + "named-file-bin": "./named-file-bin", + "dir-bin": "./dir-bin", + "map-bin": "./map-bin", + }, + }), + ), + write( + join(packageDir, "file-bin", "package.json"), + JSON.stringify({ + name: "file-bin", + version: "1.1.1", + bin: "./file-bin.js", + }), + ), + write(join(packageDir, "file-bin", "file-bin.js"), `#!/usr/bin/env node\nconsole.log("file-bin")`), + write( + join(packageDir, "named-file-bin", "package.json"), + JSON.stringify({ + name: "named-file-bin", + version: "1.1.1", + bin: { "named-file-bin": "./named-file-bin.js" }, + }), + ), + write( + join(packageDir, "named-file-bin", "named-file-bin.js"), + `#!/usr/bin/env node\nconsole.log("named-file-bin")`, + ), + write( + join(packageDir, "dir-bin", "package.json"), + JSON.stringify({ + name: "dir-bin", + version: "1.1.1", + directories: { + bin: "./bins", + }, + }), + ), + write(join(packageDir, "dir-bin", "bins", "dir-bin-1.js"), `#!/usr/bin/env node\nconsole.log("dir-bin-1")`), + write( + join(packageDir, "map-bin", "package.json"), + JSON.stringify({ + name: "map-bin", + version: "1.1.1", + bin: { + "map-bin-1": "./map-bin-1.js", + "map-bin-2": "./map-bin-2.js", + }, + }), + ), + write(join(packageDir, "map-bin", "map-bin-1.js"), `#!/usr/bin/env node\nconsole.log("map-bin-1")`), + write(join(packageDir, "map-bin", "map-bin-2.js"), `#!/usr/bin/env node\nconsole.log("map-bin-2")`), + ]); + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + let err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + expect(join(packageDir, "node_modules", ".bin", "file-bin")).toBeValidBin(join("..", "file-bin", "file-bin.js")); + expect(join(packageDir, "node_modules", ".bin", "named-file-bin")).toBeValidBin( + join("..", "named-file-bin", "named-file-bin.js"), + ); + expect(join(packageDir, "node_modules", ".bin", "dir-bin-1.js")).toBeValidBin( + join("..", "dir-bin", "bins", "dir-bin-1.js"), + ); + expect(join(packageDir, "node_modules", ".bin", "map-bin-1")).toBeValidBin(join("..", "map-bin", "map-bin-1.js")); + expect(join(packageDir, "node_modules", ".bin", "map-bin-2")).toBeValidBin(join("..", "map-bin", "map-bin-2.js")); + + await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); + + // now install from the lockfile, only linking bins + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).not.toContain("Saved lockfile"); + + expect(await exited).toBe(0); + + expect(firstLockfile).toBe( + (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), + ); + expect(firstLockfile).toMatchSnapshot(); + + expect(join(packageDir, "node_modules", ".bin", "file-bin")).toBeValidBin(join("..", "file-bin", "file-bin.js")); + expect(join(packageDir, "node_modules", ".bin", "named-file-bin")).toBeValidBin( + join("..", "named-file-bin", "named-file-bin.js"), + ); + expect(join(packageDir, "node_modules", ".bin", "dir-bin-1.js")).toBeValidBin( + join("..", "dir-bin", "bins", "dir-bin-1.js"), + ); + expect(join(packageDir, "node_modules", ".bin", "map-bin-1")).toBeValidBin(join("..", "map-bin", "map-bin-1.js")); + expect(join(packageDir, "node_modules", ".bin", "map-bin-2")).toBeValidBin(join("..", "map-bin", "map-bin-2.js")); + }); + + test.todo("text lockfile updates with new bin entry for folder dependencies", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "change-bin": "./change-bin", + }, + }), + ), + write( + join(packageDir, "change-bin", "package.json"), + JSON.stringify({ + name: "change-bin", + version: "1.0.0", + bin: { + "change-bin-1": "./change-bin-1.js", + }, + }), + ), + write(join(packageDir, "change-bin", "change-bin-1.js"), `#!/usr/bin/env node\nconsole.log("change-bin-1")`), + ]); + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + let err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + + expect(await exited).toBe(0); + + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + expect(join(packageDir, "node_modules", ".bin", "change-bin-1")).toBeValidBin( + join("..", "change-bin", "change-bin-1.js"), + ); + + await Promise.all([ + write( + join(packageDir, "change-bin", "package.json"), + JSON.stringify({ + name: "change-bin", + version: "1.0.0", + bin: { + "change-bin-1": "./change-bin-1.js", + "change-bin-2": "./change-bin-2.js", + }, + }), + ), + write(join(packageDir, "change-bin", "change-bin-2.js"), `#!/usr/bin/env node\nconsole.log("change-bin-2")`), + ]); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + + // it should save + expect(err).toContain("Saved lockfile"); + + expect(await exited).toBe(0); + + const secondLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + expect(firstLockfile).not.toBe(secondLockfile); + + expect(secondLockfile).toMatchSnapshot(); + + expect(join(packageDir, "node_modules", ".bin", "change-bin-1")).toBeValidBin( + join("..", "change-bin", "change-bin-1.js"), + ); + + expect(join(packageDir, "node_modules", ".bin", "change-bin-2")).toBeValidBin( + join("..", "change-bin", "change-bin-2.js"), + ); + }); + + test("root resolution bins", async () => { + // As of writing this test, the only way to get a root resolution + // is to migrate a package-lock.json with a root resolution. For now, + // we'll just mock the bun.lock. + + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "fooooo", + version: "2.2.2", + dependencies: { + "fooooo": ".", + "no-deps": "1.0.0", + }, + bin: "fooooo.js", + }), + ), + write(join(packageDir, "fooooo.js"), `#!/usr/bin/env node\nconsole.log("fooooo")`), + write( + join(packageDir, "bun.lock"), + JSON.stringify({ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "fooooo", + "dependencies": { + "fooooo": ".", + // out of date, no no-deps + }, + }, + }, + "packages": { + "fooooo": ["fooooo@root:", { bin: "fooooo.js" }], + }, + }), + ), + ]); + + let { stderr, stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + let err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + + let out = await Bun.readableStreamToText(stdout); + expect(out).toContain("no-deps@1.0.0"); + + expect(await exited).toBe(0); + + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + expect(join(packageDir, "node_modules", ".bin", "fooooo")).toBeValidBin(join("..", "fooooo", "fooooo.js")); + + await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); + + ({ stderr, stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).not.toContain("Saved lockfile"); + + out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps@1.0.0"); + + expect(await exited).toBe(0); + + expect(firstLockfile).toBe( + (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), + ); + expect(firstLockfile).toMatchSnapshot(); + + expect(join(packageDir, "node_modules", ".bin", "fooooo")).toBeValidBin(join("..", "fooooo", "fooooo.js")); + }); + + async function runBin(binName: string, expected: string, global: boolean) { + const args = global ? [`./global-bin-dir/${binName}`] : [bunExe(), binName]; + const result = Bun.spawn({ + cmd: args, + stdout: "pipe", + stderr: "pipe", + cwd: packageDir, + env, + }); + + const out = await Bun.readableStreamToText(result.stdout); + expect(out).toEqual(expected); + const err = await Bun.readableStreamToText(result.stderr); + expect(err).toBeEmpty(); + expect(await result.exited).toBe(0); + } + + test("it will skip (without errors) if a folder from `directories.bin` does not exist", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "missing-directory-bin": "file:missing-directory-bin-1.1.1.tgz", + }, + }), + ), + cp(join(import.meta.dir, "missing-directory-bin-1.1.1.tgz"), join(packageDir, "missing-directory-bin-1.1.1.tgz")), + ]); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); +}); + +test("--config cli flag works", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + devDependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "bunfig2.toml"), + ` +[install] +cache = "${join(packageDir, ".bun-cache")}" +registry = "http://localhost:${port}/" +dev = false +`, + ), + ]); + + // should install dev dependencies + let { exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({ + name: "a-dep", + version: "1.0.1", + }); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // should not install dev dependencies + ({ exited } = spawn({ + cmd: [bunExe(), "i", "--config=bunfig2.toml"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "a-dep"))).toBeFalse(); +}); + +test("it should invalid cached package if package.json is missing", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + ]); + + let { out } = await runBunInstall(env, packageDir); + expect(out).toContain("+ no-deps@2.0.0"); + + // node_modules and cache should be populated + expect( + await Promise.all([ + readdirSorted(join(packageDir, "node_modules", "no-deps")), + readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), + ]), + ).toEqual([ + ["index.js", "package.json"], + ["index.js", "package.json"], + ]); + + // with node_modules package.json deleted, the package should be reinstalled + await rm(join(packageDir, "node_modules", "no-deps", "package.json")); + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + expect(out).toContain("+ no-deps@2.0.0"); + + // another install is a no-op + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + expect(out).not.toContain("+ no-deps@2.0.0"); + + // with cache package.json deleted, install is a no-op and cache is untouched + await rm(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1", "package.json")); + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + expect(out).not.toContain("+ no-deps@2.0.0"); + expect( + await Promise.all([ + readdirSorted(join(packageDir, "node_modules", "no-deps")), + readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), + ]), + ).toEqual([["index.js", "package.json"], ["index.js"]]); + + // now with node_modules package.json deleted, the package AND the cache should + // be repopulated + await rm(join(packageDir, "node_modules", "no-deps", "package.json")); + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + expect(out).toContain("+ no-deps@2.0.0"); + expect( + await Promise.all([ + readdirSorted(join(packageDir, "node_modules", "no-deps")), + readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), + ]), + ).toEqual([ + ["index.js", "package.json"], + ["index.js", "package.json"], + ]); +}); + +test("it should install with missing bun.lockb, node_modules, and/or cache", async () => { + // first clean install + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + "uses-what-bin": "1.5.0", + "optional-native": "1.0.0", + "peer-deps-too": "1.0.0", + "two-range-deps": "1.0.0", + "one-fixed-dep": "2.0.0", + "no-deps-bins": "2.0.0", + "left-pad": "1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "3.0.0", + "dev-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dep-loop-entry@1.0.0", + expect.stringContaining("+ dep-with-tags@3.0.0"), + "+ dev-deps@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ no-deps-bins@2.0.0", + expect.stringContaining("+ one-fixed-dep@2.0.0"), + "+ optional-native@1.0.0", + "+ peer-deps-too@1.0.0", + "+ two-range-deps@1.0.0", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "19 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + let lockfile = parseLockfile(packageDir); + expect(lockfile).toMatchNodeModulesAt(packageDir); + + // delete node_modules + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dep-loop-entry@1.0.0", + expect.stringContaining("+ dep-with-tags@3.0.0"), + "+ dev-deps@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ no-deps-bins@2.0.0", + "+ one-fixed-dep@2.0.0", + "+ optional-native@1.0.0", + "+ peer-deps-too@1.0.0", + "+ two-range-deps@1.0.0", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "19 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + lockfile = parseLockfile(packageDir); + expect(lockfile).toMatchNodeModulesAt(packageDir); + + for (var i = 0; i < 100; i++) { + // Situation: + // + // Root package has a dependency on one-fixed-dep, peer-deps-too and two-range-deps. + // Each of these dependencies depends on no-deps. + // + // - one-fixed-dep: no-deps@2.0.0 + // - two-range-deps: no-deps@^1.0.0 (will choose 1.1.0) + // - peer-deps-too: peer no-deps@* + // + // We want peer-deps-too to choose the version of no-deps from one-fixed-dep because + // it's the highest version. It should hoist to the root. + + // delete bun.lockb + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + } + + // delete cache + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + // delete bun.lockb and cache + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), + ]); +}); + +describe("hoisting", async () => { + var tests: any = [ + { + situation: "1.0.0 - 1.0.10 is in order", + dependencies: { + "uses-a-dep-1": "1.0.0", + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + }, + expected: "1.0.1", + }, + { + situation: "1.0.1 in the middle", + dependencies: { + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-1": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + }, + expected: "1.0.1", + }, + { + situation: "1.0.1 is missing", + dependencies: { + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + }, + expected: "1.0.10", + }, + { + situation: "1.0.10 and 1.0.1 are missing", + dependencies: { + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + }, + expected: "1.0.2", + }, + { + situation: "1.0.10 is missing and 1.0.1 is last", + dependencies: { + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-1": "1.0.0", + }, + expected: "1.0.1", + }, + ]; + + for (const { dependencies, expected, situation } of tests) { + test(`it should hoist ${expected} when ${situation}`, async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + for (const dep of Object.keys(dependencies)) { + expect(out).toContain(`+ ${dep}@${dependencies[dep]}`); + } + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); + + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out).not.toContain("package installed"); + expect(out).toContain(`Checked ${Object.keys(dependencies).length * 2} installs across`); + expect(await exited).toBe(0); + }); + } + + describe("peers", async () => { + var peerTests: any = [ + { + situation: "peer 1.0.2", + dependencies: { + "uses-a-dep-1": "1.0.0", + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + "peer-a-dep-1-0-2": "1.0.0", + }, + expected: "1.0.2", + }, + { + situation: "peer >= 1.0.2", + dependencies: { + "uses-a-dep-1": "1.0.0", + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + "peer-a-dep-gte-1-0-2": "1.0.0", + }, + expected: "1.0.10", + }, + { + situation: "peer ^1.0.2", + dependencies: { + "uses-a-dep-1": "1.0.0", + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + "peer-a-dep-caret-1-0-2": "1.0.0", + }, + expected: "1.0.10", + }, + { + situation: "peer ~1.0.2", + dependencies: { + "uses-a-dep-1": "1.0.0", + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + "peer-a-dep-tilde-1-0-2": "1.0.0", + }, + expected: "1.0.10", + }, + { + situation: "peer *", + dependencies: { + "uses-a-dep-1": "1.0.0", + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + "peer-a-dep-star": "1.0.0", + }, + expected: "1.0.1", + }, + { + situation: "peer * and peer 1.0.2", + dependencies: { + "uses-a-dep-1": "1.0.0", + "uses-a-dep-2": "1.0.0", + "uses-a-dep-3": "1.0.0", + "uses-a-dep-4": "1.0.0", + "uses-a-dep-5": "1.0.0", + "uses-a-dep-6": "1.0.0", + "uses-a-dep-7": "1.0.0", + "uses-a-dep-8": "1.0.0", + "uses-a-dep-9": "1.0.0", + "uses-a-dep-10": "1.0.0", + "peer-a-dep-1-0-2": "1.0.0", + "peer-a-dep-star": "1.0.0", + }, + expected: "1.0.2", + }, + ]; + for (const { dependencies, expected, situation } of peerTests) { + test.todoIf(isFlaky && isMacOS && situation === "peer ^1.0.2")( + `it should hoist ${expected} when ${situation}`, + async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + for (const dep of Object.keys(dependencies)) { + expect(out).toContain(`+ ${dep}@${dependencies[dep]}`); + } + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); + + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + if (out.includes("installed")) { + console.log("stdout:", out); + } + expect(out).not.toContain("package installed"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out).not.toContain("package installed"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); + }, + ); + } + }); + + test("hoisting/using incorrect peer dep after install", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("incorrect peer dependency"); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ peer-deps-fixed@1.0.0", + "", + "2 packages installed", + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + } as any); + expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ + name: "peer-deps-fixed", + version: "1.0.0", + peerDependencies: { + "no-deps": "^1.0.0", + }, + } as any); + expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "2.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@2.0.0", + "", + "1 package installed", + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "2.0.0", + } as any); + expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ + name: "peer-deps-fixed", + version: "1.0.0", + peerDependencies: { + "no-deps": "^1.0.0", + }, + } as any); + expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); + }); + + test("root workspace (other than root) dependency will not hoist incorrect peer", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["bar"], + }), + ), + write( + join(packageDir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ), + ]); + + let { exited, stdout } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "ignore", + stdout: "pipe", + env, + }); + + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + + // now run the install again but from the workspace and with `no-deps@2.0.0` + await write( + join(packageDir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "2.0.0", + }, + }), + ); + + ({ exited, stdout } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "bar"), + stderr: "ignore", + stdout: "pipe", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@2.0.0", + "", + "2 packages installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "2.0.0", + }); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("hoisting/using incorrect peer dep on initial install", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "2.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).toContain("incorrect peer dependency"); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@2.0.0", + "+ peer-deps-fixed@1.0.0", + "", + "2 packages installed", + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "2.0.0", + } as any); + expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ + name: "peer-deps-fixed", + version: "1.0.0", + peerDependencies: { + "no-deps": "^1.0.0", + }, + } as any); + expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "1 package installed", + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + } as any); + expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ + name: "peer-deps-fixed", + version: "1.0.0", + peerDependencies: { + "no-deps": "^1.0.0", + }, + } as any); + expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); + }); + + describe("devDependencies", () => { + test("from normal dependency", async () => { + // Root package should choose no-deps@1.0.1. + // + // `normal-dep-and-dev-dep` should install `no-deps@1.0.0` and `normal-dep@1.0.1`. + // It should not hoist (skip) `no-deps` for `normal-dep-and-dev-dep`. + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.0.0", + "normal-dep-and-dev-dep": "1.0.2", + }, + devDependencies: { + "no-deps": "1.0.1", + }, + }), + ); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + stdin: "ignore", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.1", + }); + + expect( + await file( + join(packageDir, "node_modules", "normal-dep-and-dev-dep", "node_modules", "no-deps", "package.json"), + ).json(), + ).toEqual({ + name: "no-deps", + version: "1.0.0", + }); + }); + + test("from workspace", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "1.0.0", + }, + devDependencies: { + "no-deps": "1.0.1", + }, + }), + ); + + await mkdir(join(packageDir, "packages", "moo"), { recursive: true }); + await writeFile( + join(packageDir, "packages", "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "1.2.3", + dependencies: { + "no-deps": "2.0.0", + "normal-dep-and-dev-dep": "1.0.0", + }, + devDependencies: { + "no-deps": "1.1.0", + }, + }), + ); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "ignore", + stdin: "ignore", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.1", + }); + + expect( + await file(join(packageDir, "node_modules", "moo", "node_modules", "no-deps", "package.json")).json(), + ).toEqual({ + name: "no-deps", + version: "1.1.0", + }); + }); + + test("from linked package", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.0", + "folder-dep": "file:./folder-dep", + }, + devDependencies: { + "no-deps": "2.0.0", + }, + }), + ); + + await mkdir(join(packageDir, "folder-dep")); + await writeFile( + join(packageDir, "folder-dep", "package.json"), + JSON.stringify({ + name: "folder-dep", + version: "1.2.3", + dependencies: { + "no-deps": "1.0.0", + "normal-dep-and-dev-dep": "1.0.1", + }, + devDependencies: { + "no-deps": "1.0.1", + }, + }), + ); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "ignore", + stdin: "ignore", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "2.0.0", + }); + expect( + await file( + join(packageDir, "node_modules", "normal-dep-and-dev-dep", "node_modules", "no-deps", "package.json"), + ).json(), + ).toEqual({ + "name": "no-deps", + "version": "1.1.0", + }); + expect( + await file(join(packageDir, "node_modules", "folder-dep", "node_modules", "no-deps", "package.json")).json(), + ).toEqual({ + name: "no-deps", + version: "1.0.1", + }); + }); + + test("dependency with normal dependency same as root", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.0.0", + "one-dep": "1.0.0", + }, + devDependencies: { + "no-deps": "2.0.0", + }, + }), + ); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "ignore", + stdin: "ignore", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "2.0.0", + }); + expect( + await file(join(packageDir, "node_modules", "one-dep", "node_modules", "no-deps", "package.json")).json(), + ).toEqual({ + name: "no-deps", + version: "1.0.1", + }); + }); + }); + + test("text lockfile is hoisted", async () => { + // Each dependency depends on 'hoist-lockfile-shared'. + // 1 - "*" + // 2 - "^1.0.1" + // 3 - ">=1.0.1" + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "hoist-lockfile-1": "1.0.0", + "hoist-lockfile-2": "1.0.0", + "hoist-lockfile-3": "1.0.0", + }, + }), + ); + + let { exited, stderr } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stderr: "pipe", + stdout: "ignore", + env, + }); + + let err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + const lockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + expect(lockfile).toMatchSnapshot(); + + // second install should not save the lockfile + // with a different set of resolutions + ({ exited, stderr } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "ignore", + env, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + lockfile, + ); + }); +}); + +describe("transitive file dependencies", () => { + async function checkHoistedFiles() { + const aliasedFileDepFilesPackageJson = join( + packageDir, + "node_modules", + "aliased-file-dep", + "node_modules", + "files", + "the-files", + "package.json", + ); + const results = await Promise.all([ + exists(join(packageDir, "node_modules", "file-dep", "node_modules", "files", "package.json")), + readdirSorted(join(packageDir, "node_modules", "missing-file-dep", "node_modules")), + exists(join(packageDir, "node_modules", "aliased-file-dep", "package.json")), + isWindows + ? file(await readlink(aliasedFileDepFilesPackageJson)).json() + : file(aliasedFileDepFilesPackageJson).json(), + exists( + join(packageDir, "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json"), + ), + exists( + join( + packageDir, + "node_modules", + "@another-scope", + "file-dep", + "node_modules", + "@scoped", + "files", + "package.json", + ), + ), + exists(join(packageDir, "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json")), + ]); + + expect(results).toEqual([ + true, + [], + true, + { + "name": "files", + "version": "1.1.1", + "dependencies": { + "no-deps": "2.0.0", + }, + }, + true, + true, + true, + ]); + } + + async function checkUnhoistedFiles() { + const results = await Promise.all([ + file(join(packageDir, "node_modules", "dep-file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "missing-file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "aliased-file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "@scoped", "file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "@another-scope", "file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "self-file-dep", "package.json")).json(), + + exists(join(packageDir, "pkg1", "node_modules", "file-dep", "node_modules", "files", "package.json")), // true + readdirSorted(join(packageDir, "pkg1", "node_modules", "missing-file-dep", "node_modules")), // [] + exists(join(packageDir, "pkg1", "node_modules", "aliased-file-dep")), // false + exists( + join( + packageDir, + "pkg1", + "node_modules", + "@scoped", + "file-dep", + "node_modules", + "@scoped", + "files", + "package.json", + ), + ), + exists( + join( + packageDir, + "pkg1", + "node_modules", + "@another-scope", + "file-dep", + "node_modules", + "@scoped", + "files", + "package.json", + ), + ), + exists( + join(packageDir, "pkg1", "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"), + ), + readdirSorted(join(packageDir, "pkg1", "node_modules")), + ]); + + const expected = [ + ...(Array(7).fill({ name: "a-dep", version: "1.0.1" }) as any), + true, + [] as string[], + false, + true, + true, + true, + ["@another-scope", "@scoped", "dep-file-dep", "file-dep", "missing-file-dep", "self-file-dep"], + ]; + + // @ts-ignore + expect(results).toEqual(expected); + } + + test("from hoisted workspace dependencies", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["pkg1"], + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + // hoisted + "dep-file-dep": "1.0.0", + // root + "file-dep": "1.0.0", + // dangling symlink + "missing-file-dep": "1.0.0", + // aliased. has `"file-dep": "file:."` + "aliased-file-dep": "npm:file-dep@1.0.1", + // scoped + "@scoped/file-dep": "1.0.0", + // scoped with different names + "@another-scope/file-dep": "1.0.0", + // file dependency on itself + "self-file-dep": "1.0.0", + }, + }), + ), + ]); + + var { out } = await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "14 packages installed", + ]); + + await checkHoistedFiles(); + expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // reinstall + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "14 packages installed", + ]); + + await checkHoistedFiles(); + + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + + await checkHoistedFiles(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + + // install from workspace + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "14 packages installed", + ]); + + await checkHoistedFiles(); + expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); + + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "14 packages installed", + ]); + }); + + test("from non-hoisted workspace dependencies", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["pkg1"], + // these dependencies exist to make the workspace + // dependencies non-hoisted + dependencies: { + "dep-file-dep": "npm:a-dep@1.0.1", + "file-dep": "npm:a-dep@1.0.1", + "missing-file-dep": "npm:a-dep@1.0.1", + "aliased-file-dep": "npm:a-dep@1.0.1", + "@scoped/file-dep": "npm:a-dep@1.0.1", + "@another-scope/file-dep": "npm:a-dep@1.0.1", + "self-file-dep": "npm:a-dep@1.0.1", + }, + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + // hoisted + "dep-file-dep": "1.0.0", + // root + "file-dep": "1.0.0", + // dangling symlink + "missing-file-dep": "1.0.0", + // aliased. has `"file-dep": "file:."` + "aliased-file-dep": "npm:file-dep@1.0.1", + // scoped + "@scoped/file-dep": "1.0.0", + // scoped with different names + "@another-scope/file-dep": "1.0.0", + // file dependency on itself + "self-file-dep": "1.0.0", + }, + }), + ), + ]); + + var { out } = await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.1", + "+ @scoped/file-dep@1.0.1", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.1", + expect.stringContaining("+ file-dep@1.0.1"), + "+ missing-file-dep@1.0.1", + "+ self-file-dep@1.0.1", + "", + "13 packages installed", + ]); + + await checkUnhoistedFiles(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + + // reinstall + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.1", + "+ @scoped/file-dep@1.0.1", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.1", + expect.stringContaining("+ file-dep@1.0.1"), + "+ missing-file-dep@1.0.1", + "+ self-file-dep@1.0.1", + "", + "13 packages installed", + ]); + + await checkUnhoistedFiles(); + + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + + await checkUnhoistedFiles(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + + // install from workspace + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "13 packages installed", + ]); + + await checkUnhoistedFiles(); + + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "13 packages installed", + ]); + }); + + test("from root dependencies", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + // hoisted + "dep-file-dep": "1.0.0", + // root + "file-dep": "1.0.0", + // dangling symlink + "missing-file-dep": "1.0.0", + // aliased. has `"file-dep": "file:."` + "aliased-file-dep": "npm:file-dep@1.0.1", + // scoped + "@scoped/file-dep": "1.0.0", + // scoped with different names + "@another-scope/file-dep": "1.0.0", + // file dependency on itself + "self-file-dep": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await Bun.readableStreamToText(stderr); + var out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "13 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@another-scope", + "@scoped", + "aliased-file-dep", + "dep-file-dep", + "file-dep", + "missing-file-dep", + "self-file-dep", + ]); + + await checkHoistedFiles(); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + out = await Bun.readableStreamToText(stdout); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await checkHoistedFiles(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + out = await Bun.readableStreamToText(stdout); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@another-scope", + "@scoped", + "aliased-file-dep", + "dep-file-dep", + "file-dep", + "missing-file-dep", + "self-file-dep", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await checkHoistedFiles(); + }); + test("it should install folder dependencies with absolute paths", async () => { + async function writePackages(num: number) { + await rm(join(packageDir, `pkg0`), { recursive: true, force: true }); + for (let i = 0; i < num; i++) { + await mkdir(join(packageDir, `pkg${i}`)); + await writeFile( + join(packageDir, `pkg${i}`, "package.json"), + JSON.stringify({ + name: `pkg${i}`, + version: "1.1.1", + }), + ); + } + } + + await writePackages(2); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + // without and without file protocol + "pkg0": `file:${resolve(packageDir, "pkg0").replace(/\\/g, "\\\\")}`, + "pkg1": `${resolve(packageDir, "pkg1").replace(/\\/g, "\\\\")}`, + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env, + }); + + var err = await Bun.readableStreamToText(stderr); + var out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ pkg0@pkg0", + "+ pkg1@pkg1", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["pkg0", "pkg1"]); + expect(await file(join(packageDir, "node_modules", "pkg0", "package.json")).json()).toEqual({ + name: "pkg0", + version: "1.1.1", + }); + expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toEqual({ + name: "pkg1", + version: "1.1.1", + }); + }); +}); + +test("name from manifest is scoped and url encoded", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // `name` in the manifest for these packages is manually changed + // to use `%40` and `%2f` + "@url/encoding.2": "1.0.1", + "@url/encoding.3": "1.0.1", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const files = await Promise.all([ + file(join(packageDir, "node_modules", "@url", "encoding.2", "package.json")).json(), + file(join(packageDir, "node_modules", "@url", "encoding.3", "package.json")).json(), + ]); + + expect(files).toEqual([ + { name: "@url/encoding.2", version: "1.0.1" }, + { name: "@url/encoding.3", version: "1.0.1" }, + ]); +}); + +describe("update", () => { + test("duplicate peer dependency (one package is invalid_package_id)", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "^1.0.0", + }, + peerDependencies: { + "no-deps": "^1.0.0", + }, + }), + ); + + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^1.1.0", + }, + peerDependencies: { + "no-deps": "^1.0.0", + }, + }); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.1.0", + }); + }); + test("dist-tags", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "latest", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + name: "a-dep", + version: "1.0.10", + }); + + // Update without args, `latest` should stay + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "latest", + }, + }); + + // Update with `a-dep` and `--latest`, `latest` should be replaced with the installed version + await runBunUpdate(env, packageDir, ["a-dep"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.10", + }, + }); + await runBunUpdate(env, packageDir, ["--latest"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.10", + }, + }); + }); + test("exact versions stay exact", async () => { + const runs = [ + { version: "1.0.1", dependency: "a-dep" }, + { version: "npm:a-dep@1.0.1", dependency: "aliased" }, + ]; + for (const { version, dependency } of runs) { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + [dependency]: version, + }, + }), + ); + async function check(version: string) { + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", dependency, "package.json")).json()).toMatchObject({ + name: "a-dep", + version: version.replace(/.*@/, ""), + }); + + expect(await file(packageJson).json()).toMatchObject({ + dependencies: { + [dependency]: version, + }, + }); + } + await runBunInstall(env, packageDir); + await check(version); + + await runBunUpdate(env, packageDir); + await check(version); + + await runBunUpdate(env, packageDir, [dependency]); + await check(version); + + // this will actually update the package, but the version should remain exact + await runBunUpdate(env, packageDir, ["--latest"]); + await check(dependency === "aliased" ? "npm:a-dep@1.0.10" : "1.0.10"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + } + }); + describe("tilde", () => { + test("without args", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "~1.0.0", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.0.1", + }); + + let { out } = await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "~1.0.1", + }, + }); + + // another update does not change anything (previously the version would update because it was changed to `^1.0.1`) + ({ out } = await runBunUpdate(env, packageDir)); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "~1.0.1", + }, + }); + }); + + for (const latest of [true, false]) { + test(`update no args${latest ? " --latest" : ""}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a1": "npm:no-deps@1", + "a10": "npm:no-deps@~1.0", + "a11": "npm:no-deps@^1.0", + "a12": "npm:no-deps@~1.0.1", + "a13": "npm:no-deps@^1.0.1", + "a14": "npm:no-deps@~1.1.0", + "a15": "npm:no-deps@^1.1.0", + "a2": "npm:no-deps@1.0", + "a3": "npm:no-deps@1.1", + "a4": "npm:no-deps@1.0.1", + "a5": "npm:no-deps@1.1.0", + "a6": "npm:no-deps@~1", + "a7": "npm:no-deps@^1", + "a8": "npm:no-deps@~1.1", + "a9": "npm:no-deps@^1.1", + }, + }), + ); + + if (latest) { + await runBunUpdate(env, packageDir, ["--latest"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a1": "npm:no-deps@^2.0.0", + "a10": "npm:no-deps@~2.0.0", + "a11": "npm:no-deps@^2.0.0", + "a12": "npm:no-deps@~2.0.0", + "a13": "npm:no-deps@^2.0.0", + "a14": "npm:no-deps@~2.0.0", + "a15": "npm:no-deps@^2.0.0", + "a2": "npm:no-deps@~2.0.0", + "a3": "npm:no-deps@~2.0.0", + "a4": "npm:no-deps@2.0.0", + "a5": "npm:no-deps@2.0.0", + "a6": "npm:no-deps@~2.0.0", + "a7": "npm:no-deps@^2.0.0", + "a8": "npm:no-deps@~2.0.0", + "a9": "npm:no-deps@^2.0.0", + }, + }); + } else { + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a1": "npm:no-deps@^1.1.0", + "a10": "npm:no-deps@~1.0.1", + "a11": "npm:no-deps@^1.1.0", + "a12": "npm:no-deps@~1.0.1", + "a13": "npm:no-deps@^1.1.0", + "a14": "npm:no-deps@~1.1.0", + "a15": "npm:no-deps@^1.1.0", + "a2": "npm:no-deps@~1.0.1", + "a3": "npm:no-deps@~1.1.0", + "a4": "npm:no-deps@1.0.1", + "a5": "npm:no-deps@1.1.0", + "a6": "npm:no-deps@~1.1.0", + "a7": "npm:no-deps@^1.1.0", + "a8": "npm:no-deps@~1.1.0", + "a9": "npm:no-deps@^1.1.0", + }, + }); + } + const files = await Promise.all( + ["a1", "a10", "a11", "a12", "a13", "a14", "a15", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9"].map(alias => + file(join(packageDir, "node_modules", alias, "package.json")).json(), + ), + ); + + if (latest) { + // each version should be "2.0.0" + expect(files).toMatchObject(Array(15).fill({ version: "2.0.0" })); + } else { + expect(files).toMatchObject([ + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + ]); + } + }); + } + + test("with package name in args", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "1.0.3", + "no-deps": "~1.0.0", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.0.1", + }); + + let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@1.0.1", + "", + expect.stringContaining("done"), + "", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "1.0.3", + "no-deps": "~1.0.1", + }, + }); + + // update with --latest should only change the update request and keep `~` + ({ out } = await runBunUpdate(env, packageDir, ["no-deps", "--latest"])); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "1.0.3", + "no-deps": "~2.0.0", + }, + }); + }); + }); + describe("alises", () => { + test("update all", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "aliased-dep": "npm:no-deps@^1.0.0", + }, + }), + ); + + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "aliased-dep": "npm:no-deps@^1.1.0", + }, + }); + expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.1.0", + }); + }); + test("update specific aliased package", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "aliased-dep": "npm:no-deps@^1.0.0", + }, + }), + ); + + await runBunUpdate(env, packageDir, ["aliased-dep"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "aliased-dep": "npm:no-deps@^1.1.0", + }, + }); + expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.1.0", + }); + }); + test("with pre and build tags", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", + }, + }), + ); + + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toMatchObject({ + name: "foo", + dependencies: { + "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", + }, + }); + + expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ + name: "prereleases-3", + version: "5.0.0-alpha.150", + }); + + const { out } = await runBunUpdate(env, packageDir, ["--latest"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "^ aliased-dep 5.0.0-alpha.150 -> 5.0.0-alpha.153", + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toMatchObject({ + name: "foo", + dependencies: { + "aliased-dep": "npm:prereleases-3@5.0.0-alpha.153", + }, + }); + }); + }); + test("--no-save will update packages in node_modules and not save to package.json", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "1.0.1", + }, + }), + ); + + let { out } = await runBunUpdate(env, packageDir, ["--no-save"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + expect.stringContaining("+ a-dep@1.0.1"), + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "1.0.1", + }, + }); + + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "^1.0.1", + }, + }), + ); + + ({ out } = await runBunUpdate(env, packageDir, ["--no-save"])); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + expect.stringContaining("+ a-dep@1.0.10"), + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.1", + }, + }); + + // now save + ({ out } = await runBunUpdate(env, packageDir)); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.10", + }, + }); + }); + test("update won't update beyond version range unless the specified version allows it", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "dep-with-tags": "^1.0.0", + }, + }), + ); + + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "dep-with-tags": "^1.0.1", + }, + }); + expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ + version: "1.0.1", + }); + // update with package name does not update beyond version range + await runBunUpdate(env, packageDir, ["dep-with-tags"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "dep-with-tags": "^1.0.1", + }, + }); + expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ + version: "1.0.1", + }); + + // now update with a higher version range + await runBunUpdate(env, packageDir, ["dep-with-tags@^2.0.0"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "dep-with-tags": "^2.0.1", + }, + }); + expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ + version: "2.0.1", + }); + }); + test("update should update all packages in the current workspace", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "what-bin": "^1.0.0", + "uses-what-bin": "^1.0.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", + "a-dep": "^1.0.0", + }, + }), + ); + + const originalWorkspaceJSON = { + name: "pkg1", + version: "1.0.0", + dependencies: { + "what-bin": "^1.0.0", + "uses-what-bin": "^1.0.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", + "a-dep": "^1.0.0", + }, + }; + + await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify(originalWorkspaceJSON)); + + // initial install, update root + let { out } = await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "+ a-dep@1.0.10", + "+ dep-loop-entry@1.0.0", + expect.stringContaining("+ dep-with-tags@2.0.1"), + "+ dev-deps@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ no-deps-bins@2.0.0", + expect.stringContaining("+ one-fixed-dep@1.0.0"), + "+ optional-native@1.0.0", + "+ peer-deps-too@1.0.0", + "+ two-range-deps@1.0.0", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.5.0"), + "", + // Due to optional-native dependency, this can be either 20 or 19 packages + expect.stringMatching(/(?:20|19) packages installed/), + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + + let lockfile = parseLockfile(packageDir); + // make sure this is valid + expect(lockfile).toMatchNodeModulesAt(packageDir); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "what-bin": "^1.5.0", + "uses-what-bin": "^1.5.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.1", + "dev-deps": "1.0.0", + "a-dep": "^1.0.10", + }, + }); + // workspace hasn't changed + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toEqual(originalWorkspaceJSON); + + // now update the workspace, first a couple packages, then all + ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), [ + "what-bin", + "uses-what-bin", + "a-dep@1.0.5", + ])); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed what-bin@1.5.0 with binaries:", + " - what-bin", + "installed uses-what-bin@1.5.0", + "installed a-dep@1.0.5", + "", + "3 packages installed", + ]); + // lockfile = parseLockfile(packageDir); + // expect(lockfile).toMatchNodeModulesAt(packageDir); + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ + dependencies: { + "what-bin": "^1.5.0", + "uses-what-bin": "^1.5.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", + + // a-dep should keep caret + "a-dep": "^1.0.5", + }, + }); + + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + name: "a-dep", + version: "1.0.10", + }); + + expect( + await file(join(packageDir, "packages", "pkg1", "node_modules", "a-dep", "package.json")).json(), + ).toMatchObject({ + name: "a-dep", + version: "1.0.5", + }); + + ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["a-dep@^1.0.5"])); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed a-dep@1.0.10", + "", + expect.stringMatching(/(\[\d+\.\d+m?s\])/), + "", + ]); + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + name: "a-dep", + version: "1.0.10", + }); + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ + dependencies: { + "what-bin": "^1.5.0", + "uses-what-bin": "^1.5.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", + "a-dep": "^1.0.10", + }, + }); + }); + test("update different dependency groups", async () => { + for (const args of [true, false]) { + for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { + await write( + packageJson, + JSON.stringify({ + name: "foo", + [group]: { + "a-dep": "^1.0.0", + }, + }), + ); + + const { out } = args ? await runBunUpdate(env, packageDir, ["a-dep"]) : await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + args ? "installed a-dep@1.0.10" : expect.stringContaining("+ a-dep@1.0.10"), + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + [group]: { + "a-dep": "^1.0.10", + }, + }); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + } + } + }); + test("it should update packages from update requests", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + workspaces: ["packages/*"], + }), + ); + + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "a-dep": "^1.0.0", + }, + }), + ); + + await write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "pkg1": "*", + "is-number": "*", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + version: "1.0.10", + }); + expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); + + // update no-deps, no range, no change + let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@1.0.0", + "", + expect.stringMatching(/(\[\d+\.\d+m?s\])/), + "", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); + + // update package that doesn't exist to workspace, should add to package.json + ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["no-deps"])); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "a-dep": "^1.0.0", + "no-deps": "^2.0.0", + }, + }); + + // update root package.json no-deps to ^1.0.0 and update it + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "^1.0.0", + }, + workspaces: ["packages/*"], + }), + ); + + ({ out } = await runBunUpdate(env, packageDir, ["no-deps"])); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@1.1.0", + "", + "1 package installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.1.0", + }); + }); + + test("--latest works with packages from arguments", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + await runBunUpdate(env, packageDir, ["no-deps", "--latest"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const files = await Promise.all([ + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + file(packageJson).json(), + ]); + + expect(files).toMatchObject([{ version: "2.0.0" }, { dependencies: { "no-deps": "2.0.0" } }]); + }); +}); + +test("packages dependening on each other with aliases does not infinitely loop", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "alias-loop-1": "1.0.0", + "alias-loop-2": "1.0.0", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const files = await Promise.all([ + file(join(packageDir, "node_modules", "alias-loop-1", "package.json")).json(), + file(join(packageDir, "node_modules", "alias-loop-2", "package.json")).json(), + file(join(packageDir, "node_modules", "alias1", "package.json")).json(), + file(join(packageDir, "node_modules", "alias2", "package.json")).json(), + ]); + expect(files).toMatchObject([ + { name: "alias-loop-1", version: "1.0.0" }, + { name: "alias-loop-2", version: "1.0.0" }, + { name: "alias-loop-2", version: "1.0.0" }, + { name: "alias-loop-1", version: "1.0.0" }, + ]); +}); + +test("it should re-populate .bin folder if package is reinstalled", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "what-bin": "1.5.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ what-bin@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; + expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( + join(packageDir, "node_modules", ".bin", bin), + ); + if (process.platform === "win32") { + expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); + } else { + expect(await file(join(packageDir, "node_modules", ".bin", bin)).text()).toContain("what-bin@1.5.0"); + } + + await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "what-bin", "package.json"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ what-bin@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( + join(packageDir, "node_modules", ".bin", bin), + ); + if (process.platform === "win32") { + expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); + } else { + expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); + } +}); + +test("one version with binary map", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "map-bin": "1.0.2", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ map-bin@1.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); + expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); + expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); +}); + +test("multiple versions with binary map", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "map-bin-multiple": "1.0.2", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ map-bin-multiple@1.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); + expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin( + join("..", "map-bin-multiple", "bin", "map-bin"), + ); + expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin( + join("..", "map-bin-multiple", "bin", "map-bin"), + ); +}); + +test("duplicate dependency in optionalDependencies maintains sort order", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // `duplicate-optional` has `no-deps` as a normal dependency (1.0.0) and as an + // optional dependency (1.0.1). The optional dependency version should be installed and + // the sort order should remain the same (tested by `bun-debug bun.lockb`). + "duplicate-optional": "1.0.1", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const lockfile = parseLockfile(packageDir); + expect(lockfile).toMatchNodeModulesAt(packageDir); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.1", + }); + + const { stdout, exited } = spawn({ + cmd: [bunExe(), "bun.lockb"], + cwd: packageDir, + stderr: "inherit", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out.replaceAll(`${port}`, "4873")).toMatchSnapshot(); + expect(await exited).toBe(0); +}); + +test("missing package on reinstall, some with binaries", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "fooooo", + dependencies: { + "what-bin": "1.0.0", + "uses-what-bin": "1.5.0", + "optional-native": "1.0.0", + "peer-deps-too": "1.0.0", + "two-range-deps": "1.0.0", + "one-fixed-dep": "2.0.0", + "no-deps-bins": "2.0.0", + "left-pad": "1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "3.0.0", + "dev-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dep-loop-entry@1.0.0", + expect.stringContaining("+ dep-with-tags@3.0.0"), + "+ dev-deps@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ no-deps-bins@2.0.0", + "+ one-fixed-dep@2.0.0", + "+ optional-native@1.0.0", + "+ peer-deps-too@1.0.0", + "+ two-range-deps@1.0.0", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "19 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await rm(join(packageDir, "node_modules", "native"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "left-pad"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "dep-loop-entry"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "peer-deps-too"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"), { + recursive: true, + force: true, + }); + await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"), { + recursive: true, + force: true, + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dep-loop-entry@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ one-fixed-dep@2.0.0", + "+ peer-deps-too@1.0.0", + "", + "7 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "native", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "left-pad", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "dep-loop-entry", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "peer-deps-too", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"))).toBe(true); + const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; + expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( + join(packageDir, "node_modules", ".bin", bin), + ); + expect( + Bun.which("what-bin", { PATH: join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin") }), + ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); +}); + +describe("pm trust", async () => { + test("--default", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "default-trusted"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out).toContain("Default trusted dependencies"); + expect(await exited).toBe(0); + }); + + describe("--all", async () => { + test("no dependencies", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "--all"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("error: Lockfile not found"); + let out = await Bun.readableStreamToText(stdout); + expect(out).toBeEmpty(); + expect(await exited).toBe(1); + }); + + test("some dependencies, non with scripts", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "2 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 script ran across 1 package"); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + }); + }); +}); + +test("it should be able to find binary in node_modules/.bin from parent directory of root package", async () => { + await mkdir(join(packageDir, "node_modules", ".bin"), { recursive: true }); + await mkdir(join(packageDir, "morePackageDir")); + await writeFile( + join(packageDir, "morePackageDir", "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "missing-bin", + }, + dependencies: { + "what-bin": "1.0.0", + }, + }), + ); + + await cp(join(packageDir, "bunfig.toml"), join(packageDir, "morePackageDir", "bunfig.toml")); + + await writeShebangScript( + join(packageDir, "node_modules", ".bin", "missing-bin"), + "node", + `require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");`, + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "morePackageDir"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "morePackageDir", "missing-bin.txt")).text()).toBe("missing-bin@WHAT"); +}); + +describe("semver", () => { + const taggedVersionTests = [ + { + title: "tagged version last in range", + depVersion: "1 || 2 || pre-3", + expected: "2.0.1", + }, + { + title: "tagged version in middle of range", + depVersion: "1 || pre-3 || 2", + expected: "2.0.1", + }, + { + title: "tagged version first in range", + depVersion: "pre-3 || 2 || 1", + expected: "2.0.1", + }, + { + title: "multiple tagged versions in range", + depVersion: "pre-3 || 2 || pre-1 || 1 || 3 || pre-3", + expected: "3.0.0", + }, + { + title: "start with ||", + depVersion: "|| 1", + expected: "1.0.1", + }, + { + title: "start with || no space", + depVersion: "||2", + expected: "2.0.1", + }, + { + title: "|| with no space on both sides", + depVersion: "1||2", + expected: "2.0.1", + }, + { + title: "no version is latest", + depVersion: "", + expected: "3.0.0", + }, + { + title: "tagged version works", + depVersion: "pre-2", + expected: "2.0.1", + }, + { + title: "tagged above latest", + depVersion: "pre-3", + expected: "3.0.1", + }, + { + title: "'||'", + depVersion: "||", + expected: "3.0.0", + }, + { + title: "'|'", + depVersion: "|", + expected: "3.0.0", + }, + { + title: "'|||'", + depVersion: "|||", + expected: "3.0.0", + }, + { + title: "'|| ||'", + depVersion: "|| ||", + expected: "3.0.0", + }, + { + title: "'|| 1 ||'", + depVersion: "|| 1 ||", + expected: "1.0.1", + }, + { + title: "'| | |'", + depVersion: "| | |", + expected: "3.0.0", + }, + { + title: "'|||||||||||||||||||||||||'", + depVersion: "|||||||||||||||||||||||||", + expected: "3.0.0", + }, + { + title: "'2 ||| 1'", + depVersion: "2 ||| 1", + expected: "2.0.1", + }, + { + title: "'2 |||| 1'", + depVersion: "2 |||| 1", + expected: "2.0.1", + }, + ]; + + for (const { title, depVersion, expected } of taggedVersionTests) { + test(title, async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "dep-with-tags": depVersion, + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining(`+ dep-with-tags@${expected}`), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + } + + test.todo("only tagged versions in range errors", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "dep-with-tags": "pre-1 || pre-2", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain('InvalidDependencyVersion parsing version "pre-1 || pre-2"'); + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual(expect.stringContaining("bun install v1.")); + }); +}); + +test("doesn't error when the migration is out of sync", async () => { + const cwd = tempDirWithFiles("out-of-sync-1", { + "package.json": JSON.stringify({ + "devDependencies": { + "no-deps": "1.0.0", + }, + }), + "package-lock.json": JSON.stringify({ + "name": "reproo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reproo", + "dependencies": { + "no-deps": "2.0.0", + }, + "devDependencies": { + "no-deps": "1.0.0", + }, + }, + "node_modules/no-deps": { + "version": "1.0.0", + "resolved": `http://localhost:${port}/no-deps/-/no-deps-1.0.0.tgz`, + "integrity": + "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw==", + "dev": true, + }, + }, + }), + }); + + const subprocess = Bun.spawn([bunExe(), "install"], { + env, + cwd, + stdio: ["ignore", "ignore", "inherit"], + }); + + await subprocess.exited; + + expect(subprocess.exitCode).toBe(0); + + let { stdout, exitCode } = Bun.spawnSync({ + cmd: [bunExe(), "pm", "ls"], + env, + cwd, + stdio: ["ignore", "pipe", "inherit"], + }); + let out = stdout.toString().trim(); + expect(out).toContain("no-deps@1.0.0"); + // only one no-deps is installed + expect(out.lastIndexOf("no-deps")).toEqual(out.indexOf("no-deps")); + expect(exitCode).toBe(0); + + expect(await file(join(cwd, "node_modules/no-deps/package.json")).json()).toMatchObject({ + version: "1.0.0", + name: "no-deps", + }); +}); + +const prereleaseTests = [ + [ + { title: "specific", depVersion: "1.0.0-future.1", expected: "1.0.0-future.1" }, + { title: "latest", depVersion: "latest", expected: "1.0.0-future.4" }, + { title: "range starting with latest", depVersion: "^1.0.0-future.4", expected: "1.0.0-future.4" }, + { title: "range above latest", depVersion: "^1.0.0-future.5", expected: "1.0.0-future.7" }, + ], + [ + { title: "#6683", depVersion: "^1.0.0-next.23", expected: "1.0.0-next.23" }, + { + title: "greater than or equal to", + depVersion: ">=1.0.0-next.23", + expected: "1.0.0-next.23", + }, + { title: "latest", depVersion: "latest", expected: "0.5.0" }, + { title: "greater than or equal to latest", depVersion: ">=0.5.0", expected: "0.5.0" }, + ], + + // package "prereleases-3" has four versions, all with prerelease tags: + // - 5.0.0-alpha.150 + // - 5.0.0-alpha.151 + // - 5.0.0-alpha.152 + // - 5.0.0-alpha.153 + [ + { title: "#6956", depVersion: "^5.0.0-alpha.153", expected: "5.0.0-alpha.153" }, + { title: "range matches highest possible", depVersion: "^5.0.0-alpha.152", expected: "5.0.0-alpha.153" }, + { title: "exact", depVersion: "5.0.0-alpha.152", expected: "5.0.0-alpha.152" }, + { title: "exact latest", depVersion: "5.0.0-alpha.153", expected: "5.0.0-alpha.153" }, + { title: "latest", depVersion: "latest", expected: "5.0.0-alpha.153" }, + { title: "~ lower than latest", depVersion: "~5.0.0-alpha.151", expected: "5.0.0-alpha.153" }, + { + title: "~ equal semver and lower non-existant prerelease", + depVersion: "~5.0.0-alpha.100", + expected: "5.0.0-alpha.153", + }, + { + title: "^ equal semver and lower non-existant prerelease", + depVersion: "^5.0.0-alpha.100", + expected: "5.0.0-alpha.153", + }, + { + title: "~ and ^ latest prerelease", + depVersion: "~5.0.0-alpha.153 || ^5.0.0-alpha.153", + expected: "5.0.0-alpha.153", + }, + { + title: "< latest prerelease", + depVersion: "<5.0.0-alpha.153", + expected: "5.0.0-alpha.152", + }, + { + title: "< lower than latest prerelease", + depVersion: "<5.0.0-alpha.152", + expected: "5.0.0-alpha.151", + }, + { + title: "< higher than latest prerelease", + depVersion: "<5.0.0-alpha.22343423", + expected: "5.0.0-alpha.153", + }, + { + title: "< at lowest possible version", + depVersion: "<5.0.0-alpha.151", + expected: "5.0.0-alpha.150", + }, + { + title: "<= latest prerelease", + depVersion: "<=5.0.0-alpha.153", + expected: "5.0.0-alpha.153", + }, + { + title: "<= lower than latest prerelease", + depVersion: "<=5.0.0-alpha.152", + expected: "5.0.0-alpha.152", + }, + { + title: "<= lowest possible version", + depVersion: "<=5.0.0-alpha.150", + expected: "5.0.0-alpha.150", + }, + { + title: "<= higher than latest prerelease", + depVersion: "<=5.0.0-alpha.153261345", + expected: "5.0.0-alpha.153", + }, + { + title: "> latest prerelease", + depVersion: ">=5.0.0-alpha.153", + expected: "5.0.0-alpha.153", + }, + ], +]; +for (let i = 0; i < prereleaseTests.length; i++) { + const tests = prereleaseTests[i]; + const depName = `prereleases-${i + 1}`; + describe(`${depName} should pass`, () => { + for (const { title, depVersion, expected } of tests) { + test(title, async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + [`${depName}`]: depVersion, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ ${depName}@${expected}`, + "", + "1 package installed", + ]); + expect(await file(join(packageDir, "node_modules", depName, "package.json")).json()).toEqual({ + name: depName, + version: expected, + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + } + }); +} +const prereleaseFailTests = [ + [ + // { title: "specific", depVersion: "1.0.0-future.1", expected: "1.0.0-future.1" }, + // { title: "latest", depVersion: "latest", expected: "1.0.0-future.4" }, + // { title: "range starting with latest", depVersion: "^1.0.0-future.4", expected: "1.0.0-future.4" }, + // { title: "range above latest", depVersion: "^1.0.0-future.5", expected: "1.0.0-future.7" }, + ], + [ + // { title: "#6683", depVersion: "^1.0.0-next.23", expected: "1.0.0-next.23" }, + // { + // title: "greater than or equal to", + // depVersion: ">=1.0.0-next.23", + // expected: "1.0.0-next.23", + // }, + // { title: "latest", depVersion: "latest", expected: "0.5.0" }, + // { title: "greater than or equal to latest", depVersion: ">=0.5.0", expected: "0.5.0" }, + ], + + // package "prereleases-3" has four versions, all with prerelease tags: + // - 5.0.0-alpha.150 + // - 5.0.0-alpha.151 + // - 5.0.0-alpha.152 + // - 5.0.0-alpha.153 + [ + { + title: "^ with higher non-existant prerelease", + depVersion: "^5.0.0-alpha.1000", + }, + { + title: "~ with higher non-existant prerelease", + depVersion: "~5.0.0-alpha.1000", + }, + { + title: "> with higher non-existant prerelease", + depVersion: ">5.0.0-alpha.1000", + }, + { + title: ">= with higher non-existant prerelease", + depVersion: ">=5.0.0-alpha.1000", + }, + { + title: "^4.3.0", + depVersion: "^4.3.0", + }, + { + title: "~4.3.0", + depVersion: "~4.3.0", + }, + { + title: ">4.3.0", + depVersion: ">4.3.0", + }, + { + title: ">=4.3.0", + depVersion: ">=4.3.0", + }, + { + title: "<5.0.0-alpha.150", + depVersion: "<5.0.0-alpha.150", + }, + { + title: "<=5.0.0-alpha.149", + depVersion: "<=5.0.0-alpha.149", + }, + { + title: "greater than highest prerelease", + depVersion: ">5.0.0-alpha.153", + }, + { + title: "greater than or equal to highest prerelease + 1", + depVersion: ">=5.0.0-alpha.154", + }, + { + title: "`.` instead of `-` should fail", + depVersion: "5.0.0.alpha.150", + }, + ], + // prereleases-4 has one version + // - 2.0.0-pre.0 + [ + { + title: "wildcard should not match prerelease", + depVersion: "x", + }, + { + title: "major wildcard should not match prerelease", + depVersion: "x.0.0", + }, + { + title: "minor wildcard should not match prerelease", + depVersion: "2.x", + }, + { + title: "patch wildcard should not match prerelease", + depVersion: "2.0.x", + }, + ], +]; +for (let i = 0; i < prereleaseFailTests.length; i++) { + const tests = prereleaseFailTests[i]; + const depName = `prereleases-${i + 1}`; + describe(`${depName} should fail`, () => { + for (const { title, depVersion } of tests) { + test(title, async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + [`${depName}`]: depVersion, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(err).toContain(`No version matching "${depVersion}" found for specifier "${depName}"`); + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + } + }); +} + +describe("yarn tests", () => { + test("dragon test 1", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "dragon-test-1", + version: "1.0.0", + dependencies: { + "dragon-test-1-d": "1.0.0", + "dragon-test-1-e": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dragon-test-1-d@1.0.0", + "+ dragon-test-1-e@1.0.0", + "", + "6 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "dragon-test-1-a", + "dragon-test-1-b", + "dragon-test-1-c", + "dragon-test-1-d", + "dragon-test-1-e", + ]); + expect(await file(join(packageDir, "node_modules", "dragon-test-1-b", "package.json")).json()).toEqual({ + name: "dragon-test-1-b", + version: "2.0.0", + } as any); + expect(await readdirSorted(join(packageDir, "node_modules", "dragon-test-1-c", "node_modules"))).toEqual([ + "dragon-test-1-b", + ]); + expect( + await file( + join(packageDir, "node_modules", "dragon-test-1-c", "node_modules", "dragon-test-1-b", "package.json"), + ).json(), + ).toEqual({ + name: "dragon-test-1-b", + version: "1.0.0", + dependencies: { + "dragon-test-1-a": "1.0.0", + }, + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("dragon test 2", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "dragon-test-2", + version: "1.0.0", + workspaces: ["dragon-test-2-a", "dragon-test-2-b"], + dependencies: { + "dragon-test-2-a": "1.0.0", + }, + }), + ); + + await mkdir(join(packageDir, "dragon-test-2-a")); + await mkdir(join(packageDir, "dragon-test-2-b")); + + await writeFile( + join(packageDir, "dragon-test-2-a", "package.json"), + JSON.stringify({ + name: "dragon-test-2-a", + version: "1.0.0", + dependencies: { + "dragon-test-2-b": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + await writeFile( + join(packageDir, "dragon-test-2-b", "package.json"), + JSON.stringify({ + name: "dragon-test-2-b", + version: "1.0.0", + dependencies: { + "no-deps": "*", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dragon-test-2-a@workspace:dragon-test-2-a", + "", + "3 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "dragon-test-2-a", + "dragon-test-2-b", + "no-deps", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + }); + expect(await exists(join(packageDir, "dragon-test-2-a", "node_modules"))).toBeFalse(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("dragon test 3", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "dragon-test-3", + version: "1.0.0", + dependencies: { + "dragon-test-3-a": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dragon-test-3-a@1.0.0", + "", + "3 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "dragon-test-3-a", + "dragon-test-3-b", + "no-deps", + ]); + expect(await file(join(packageDir, "node_modules", "dragon-test-3-a", "package.json")).json()).toEqual({ + name: "dragon-test-3-a", + version: "1.0.0", + dependencies: { + "dragon-test-3-b": "1.0.0", + }, + peerDependencies: { + "no-deps": "*", + }, + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("dragon test 4", async () => { + await writeFile( + packageJson, + JSON.stringify({ + "name": "dragon-test-4", + "version": "1.0.0", + "workspaces": ["my-workspace"], + }), + ); + + await mkdir(join(packageDir, "my-workspace")); + await writeFile( + join(packageDir, "my-workspace", "package.json"), + JSON.stringify({ + "name": "my-workspace", + "version": "1.0.0", + "peerDependencies": { + "no-deps": "*", + "peer-deps": "*", + }, + "devDependencies": { + "no-deps": "1.0.0", + "peer-deps": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "3 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["my-workspace", "no-deps", "peer-deps"]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + } as any); + expect(await file(join(packageDir, "node_modules", "peer-deps", "package.json")).json()).toEqual({ + name: "peer-deps", + version: "1.0.0", + peerDependencies: { + "no-deps": "*", + }, + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("dragon test 5", async () => { + await writeFile( + packageJson, + JSON.stringify({ + "name": "dragon-test-5", + "version": "1.0.0", + "workspaces": ["packages/*"], + }), + ); + + await mkdir(join(packageDir, "packages", "a"), { recursive: true }); + await mkdir(join(packageDir, "packages", "b"), { recursive: true }); + + await writeFile( + join(packageDir, "packages", "a", "package.json"), + JSON.stringify({ + "name": "a", + "peerDependencies": { + "various-requires": "*", + }, + "devDependencies": { + "no-deps": "1.0.0", + "peer-deps": "1.0.0", + }, + }), + ); + + await writeFile( + join(packageDir, "packages", "b", "package.json"), + JSON.stringify({ + "name": "b", + "devDependencies": { + "a": "workspace:*", + "various-requires": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "5 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "a", + "b", + "no-deps", + "peer-deps", + "various-requires", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + } as any); + expect(await file(join(packageDir, "node_modules", "peer-deps", "package.json")).json()).toEqual({ + name: "peer-deps", + version: "1.0.0", + peerDependencies: { + "no-deps": "*", + }, + } as any); + expect(await file(join(packageDir, "node_modules", "various-requires", "package.json")).json()).toEqual({ + name: "various-requires", + version: "1.0.0", + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test.todo("dragon test 6", async () => { + await writeFile( + packageJson, + JSON.stringify({ + "name": "dragon-test-6", + "version": "1.0.0", + "workspaces": ["packages/*"], + }), + ); + + await mkdir(join(packageDir, "packages", "a"), { recursive: true }); + await mkdir(join(packageDir, "packages", "b"), { recursive: true }); + await mkdir(join(packageDir, "packages", "c"), { recursive: true }); + await mkdir(join(packageDir, "packages", "u"), { recursive: true }); + await mkdir(join(packageDir, "packages", "v"), { recursive: true }); + await mkdir(join(packageDir, "packages", "y"), { recursive: true }); + await mkdir(join(packageDir, "packages", "z"), { recursive: true }); + + await writeFile( + join(packageDir, "packages", "a", "package.json"), + JSON.stringify({ + name: `a`, + dependencies: { + [`z`]: `workspace:*`, + }, + }), + ); + await writeFile( + join(packageDir, "packages", "b", "package.json"), + JSON.stringify({ + name: `b`, + dependencies: { + [`u`]: `workspace:*`, + [`v`]: `workspace:*`, + }, + }), + ); + await writeFile( + join(packageDir, "packages", "c", "package.json"), + JSON.stringify({ + name: `c`, + dependencies: { + [`u`]: `workspace:*`, + [`v`]: `workspace:*`, + [`y`]: `workspace:*`, + [`z`]: `workspace:*`, + }, + }), + ); + await writeFile( + join(packageDir, "packages", "u", "package.json"), + JSON.stringify({ + name: `u`, + }), + ); + await writeFile( + join(packageDir, "packages", "v", "package.json"), + JSON.stringify({ + name: `v`, + peerDependencies: { + [`u`]: `*`, + }, + }), + ); + await writeFile( + join(packageDir, "packages", "y", "package.json"), + JSON.stringify({ + name: `y`, + peerDependencies: { + [`v`]: `*`, + }, + }), + ); + await writeFile( + join(packageDir, "packages", "z", "package.json"), + JSON.stringify({ + name: `z`, + dependencies: { + [`y`]: `workspace:*`, + }, + peerDependencies: { + [`v`]: `*`, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ a@workspace:packages/a", + "+ b@workspace:packages/b", + "+ c@workspace:packages/c", + "+ u@workspace:packages/u", + "+ v@workspace:packages/v", + "+ y@workspace:packages/y", + "+ z@workspace:packages/z", + "", + "7 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test.todo("dragon test 7", async () => { + await writeFile( + packageJson, + JSON.stringify({ + "name": "dragon-test-7", + "version": "1.0.0", + "dependencies": { + "dragon-test-7-a": "1.0.0", + "dragon-test-7-d": "1.0.0", + "dragon-test-7-b": "2.0.0", + "dragon-test-7-c": "3.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dragon-test-7-a@1.0.0", + "+ dragon-test-7-b@2.0.0", + "+ dragon-test-7-c@3.0.0", + "+ dragon-test-7-d@1.0.0", + "", + "7 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await writeFile( + join(packageDir, "test.js"), + `console.log(require("dragon-test-7-a"), require("dragon-test-7-d"));`, + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "test.js"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toBeEmpty(); + expect(out).toBe("1.0.0 1.0.0\n"); + + expect( + await exists( + join( + packageDir, + "node_modules", + "dragon-test-7-a", + "node_modules", + "dragon-test-7-b", + "node_modules", + "dragon-test-7-c", + ), + ), + ).toBeTrue(); + expect( + await exists( + join(packageDir, "node_modules", "dragon-test-7-d", "node_modules", "dragon-test-7-b", "node_modules"), + ), + ).toBeFalse(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("dragon test 8", async () => { + await writeFile( + packageJson, + JSON.stringify({ + "name": "dragon-test-8", + version: "1.0.0", + dependencies: { + "dragon-test-8-a": "1.0.0", + "dragon-test-8-b": "1.0.0", + "dragon-test-8-c": "1.0.0", + "dragon-test-8-d": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dragon-test-8-a@1.0.0", + "+ dragon-test-8-b@1.0.0", + "+ dragon-test-8-c@1.0.0", + "+ dragon-test-8-d@1.0.0", + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("dragon test 9", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "dragon-test-9", + version: "1.0.0", + dependencies: { + [`first`]: `npm:peer-deps@1.0.0`, + [`second`]: `npm:peer-deps@1.0.0`, + [`no-deps`]: `1.0.0`, + }, + }), + ); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ first@1.0.0", + expect.stringContaining("+ no-deps@1.0.0"), + "+ second@1.0.0", + "", + "2 packages installed", + ]); + expect(await file(join(packageDir, "node_modules", "first", "package.json")).json()).toEqual( + await file(join(packageDir, "node_modules", "second", "package.json")).json(), + ); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test.todo("dragon test 10", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "dragon-test-10", + version: "1.0.0", + workspaces: ["packages/*"], + }), + ); + + await mkdir(join(packageDir, "packages", "a"), { recursive: true }); + await mkdir(join(packageDir, "packages", "b"), { recursive: true }); + await mkdir(join(packageDir, "packages", "c"), { recursive: true }); + + await writeFile( + join(packageDir, "packages", "a", "package.json"), + JSON.stringify({ + name: "a", + devDependencies: { + b: "workspace:*", + }, + }), + ); + await writeFile( + join(packageDir, "packages", "b", "package.json"), + JSON.stringify({ + name: "b", + peerDependencies: { + c: "*", + }, + devDependencies: { + c: "workspace:*", + }, + }), + ); + await writeFile( + join(packageDir, "packages", "c", "package.json"), + JSON.stringify({ + name: "c", + peerDependencies: { + "no-deps": "*", + }, + depedencies: { + b: "workspace:*", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--dev"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const out = await new Response(stdout).text(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ a@workspace:packages/a", + "+ b@workspace:packages/b", + "+ c@workspace:packages/c", + "", + " packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("dragon test 12", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "dragon-test-12", + version: "1.0.0", + workspaces: ["pkg-a", "pkg-b"], + }), + ); + + await mkdir(join(packageDir, "pkg-a"), { recursive: true }); + await mkdir(join(packageDir, "pkg-b"), { recursive: true }); + + await writeFile( + join(packageDir, "pkg-a", "package.json"), + JSON.stringify({ + name: "pkg-a", + dependencies: { + "pkg-b": "workspace:*", + }, + }), + ); + await writeFile( + join(packageDir, "pkg-b", "package.json"), + JSON.stringify({ + name: "pkg-b", + dependencies: { + "peer-deps": "1.0.0", + "fake-peer-deps": "npm:peer-deps@1.0.0", + }, + peerDependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const out = await new Response(stdout).text(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "4 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "fake-peer-deps", + "no-deps", + "peer-deps", + "pkg-a", + "pkg-b", + ]); + expect(await file(join(packageDir, "node_modules", "fake-peer-deps", "package.json")).json()).toEqual({ + name: "peer-deps", + version: "1.0.0", + peerDependencies: { + "no-deps": "*", + }, + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("it should not warn when the peer dependency resolution is compatible", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "compatible-peer-deps", + version: "1.0.0", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--dev"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const out = await new Response(stdout).text(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("incorrect peer dependency"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ peer-deps-fixed@1.0.0", + "", + "2 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps", "peer-deps-fixed"]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("it should warn when the peer dependency resolution is incompatible", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "incompatible-peer-deps", + version: "1.0.0", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "2.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--dev"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const out = await new Response(stdout).text(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).toContain("incorrect peer dependency"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@2.0.0", + "+ peer-deps-fixed@1.0.0", + "", + "2 packages installed", + ]); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps", "peer-deps-fixed"]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("it should install in such a way that two identical packages with different peer dependencies are different instances", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "provides-peer-deps-1-0-0": "1.0.0", + "provides-peer-deps-2-0-0": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("incorrect peer dependency"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ provides-peer-deps-1-0-0@1.0.0", + "+ provides-peer-deps-2-0-0@1.0.0", + "", + "5 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await writeFile( + join(packageDir, "test.js"), + `console.log( + require("provides-peer-deps-1-0-0").dependencies["peer-deps"] === + require("provides-peer-deps-2-0-0").dependencies["peer-deps"] + ); + console.log( + Bun.deepEquals(require("provides-peer-deps-1-0-0"), { + name: "provides-peer-deps-1-0-0", + version: "1.0.0", + dependencies: { + "peer-deps": { + name: "peer-deps", + version: "1.0.0", + peerDependencies: { + "no-deps": { + name: "no-deps", + version: "1.0.0", + }, + }, + }, + "no-deps": { + name: "no-deps", + version: "1.0.0", + }, + }, + }) + ); + console.log( + Bun.deepEquals(require("provides-peer-deps-2-0-0"), { + name: "provides-peer-deps-2-0-0", + version: "1.0.0", + dependencies: { + "peer-deps": { + name: "peer-deps", + version: "1.0.0", + peerDependencies: { + "no-deps": { + name: "no-deps", + version: "2.0.0", + }, + }, + }, + "no-deps": { + name: "no-deps", + version: "2.0.0", + }, + }, + }) + );`, + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "test.js"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(out).toBe("true\ntrue\nfalse\n"); + expect(err).toBeEmpty(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (simple)", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "provides-peer-deps-1-0-0": "1.0.0", + "provides-peer-deps-1-0-0-too": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("incorrect peer dependency"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ provides-peer-deps-1-0-0@1.0.0", + "+ provides-peer-deps-1-0-0-too@1.0.0", + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await writeFile( + join(packageDir, "test.js"), + `console.log( + require("provides-peer-deps-1-0-0").dependencies["peer-deps"] === + require("provides-peer-deps-1-0-0-too").dependencies["peer-deps"] + );`, + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "test.js"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(out).toBe("true\n"); + expect(err).toBeEmpty(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (complex)", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "forward-peer-deps": "1.0.0", + "forward-peer-deps-too": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("incorrect peer dependency"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ forward-peer-deps@1.0.0", + "+ forward-peer-deps-too@1.0.0", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await writeFile( + join(packageDir, "test.js"), + `console.log( + require("forward-peer-deps").dependencies["peer-deps"] === + require("forward-peer-deps-too").dependencies["peer-deps"] + );`, + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "test.js"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(out).toBe("true\n"); + expect(err).toBeEmpty(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("it shouldn't deduplicate two packages with similar peer dependencies but different names", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "peer-deps": "1.0.0", + "peer-deps-too": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("incorrect peer dependency"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ peer-deps@1.0.0", + "+ peer-deps-too@1.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await writeFile(join(packageDir, "test.js"), `console.log(require('peer-deps') === require('peer-deps-too'));`); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "test.js"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(out).toBe("false\n"); + expect(err).toBeEmpty(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); + + test("it should reinstall and rebuild dependencies deleted by the user on the next install", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps-scripted": "1.0.0", + "one-dep-scripted": "1.5.0", + }, + trustedDependencies: ["no-deps-scripted", "one-dep-scripted"], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--dev"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps-scripted@1.0.0", + "+ one-dep-scripted@1.5.0", + "", + "4 packages installed", + ]); + expect(await exists(join(packageDir, "node_modules/one-dep-scripted/success.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + await rm(join(packageDir, "node_modules/one-dep-scripted"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--dev"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(await exists(join(packageDir, "node_modules/one-dep-scripted/success.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); +}); + +test("tarball `./` prefix, duplicate directory with file, and empty directory", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "tarball-without-package-prefix": "1.0.0", + }, + }), + ); + + // Entries in this tarball: + // + // ./ + // ./package1000.js + // ./package2/ + // ./package3/ + // ./package4/ + // ./package.json + // ./package/ + // ./package1000/ + // ./package/index.js + // ./package4/package5/ + // ./package4/package.json + // ./package3/package6/ + // ./package3/package6/index.js + // ./package2/index.js + // package3/ + // package3/package6/ + // package3/package6/index.js + // + // The directory `package3` is added twice, but because one doesn't start + // with `./`, it is stripped from the path and a copy of `package6` is placed + // at the root of the output directory. Also `package1000` is not included in + // the output because it is an empty directory. + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const prefix = join(packageDir, "node_modules", "tarball-without-package-prefix"); + const results = await Promise.all([ + file(join(prefix, "package.json")).json(), + file(join(prefix, "package1000.js")).text(), + file(join(prefix, "package", "index.js")).text(), + file(join(prefix, "package2", "index.js")).text(), + file(join(prefix, "package3", "package6", "index.js")).text(), + file(join(prefix, "package4", "package.json")).json(), + exists(join(prefix, "package4", "package5")), + exists(join(prefix, "package1000")), + file(join(prefix, "package6", "index.js")).text(), + ]); + expect(results).toEqual([ + { + name: "tarball-without-package-prefix", + version: "1.0.0", + }, + "hi", + "ooops", + "ooooops", + "oooooops", + { + "name": "tarball-without-package-prefix", + "version": "2.0.0", + }, + false, + false, + "oooooops", + ]); + expect(await file(join(packageDir, "node_modules", "tarball-without-package-prefix", "package.json")).json()).toEqual( + { + name: "tarball-without-package-prefix", + version: "1.0.0", + }, + ); +}); + +describe("outdated", () => { + const edgeCaseTests = [ + { + description: "normal dep, smaller than column title", + packageJson: { + dependencies: { + "no-deps": "1.0.0", + }, + }, + }, + { + description: "normal dep, larger than column title", + packageJson: { + dependencies: { + "prereleases-1": "1.0.0-future.1", + }, + }, + }, + { + description: "dev dep, smaller than column title", + packageJson: { + devDependencies: { + "no-deps": "1.0.0", + }, + }, + }, + { + description: "dev dep, larger than column title", + packageJson: { + devDependencies: { + "prereleases-1": "1.0.0-future.1", + }, + }, + }, + { + description: "peer dep, smaller than column title", + packageJson: { + peerDependencies: { + "no-deps": "1.0.0", + }, + }, + }, + { + description: "peer dep, larger than column title", + packageJson: { + peerDependencies: { + "prereleases-1": "1.0.0-future.1", + }, + }, + }, + { + description: "optional dep, smaller than column title", + packageJson: { + optionalDependencies: { + "no-deps": "1.0.0", + }, + }, + }, + { + description: "optional dep, larger than column title", + packageJson: { + optionalDependencies: { + "prereleases-1": "1.0.0-future.1", + }, + }, + }, + ]; + + for (const { description, packageJson } of edgeCaseTests) { + test(description, async () => { + await write(join(packageDir, "package.json"), JSON.stringify(packageJson)); + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + const testEnv = { ...env, FORCE_COLOR: "1" }; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + expect(await exited).toBe(0); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + const out = await Bun.readableStreamToText(stdout); + const first = out.slice(0, out.indexOf("\n")); + expect(first).toEqual(expect.stringContaining("bun outdated ")); + expect(first).toEqual(expect.stringContaining("v1.")); + const rest = out.slice(out.indexOf("\n") + 1); + expect(rest).toMatchSnapshot(); + }); + } + test("in workspace", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["pkg1"], + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + ]); + + await runBunInstall(env, packageDir); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated"], + cwd: join(packageDir, "pkg1"), + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + let out = await Bun.readableStreamToText(stdout); + expect(out).toContain("a-dep"); + expect(out).not.toContain("no-deps"); + expect(await exited).toBe(0); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + })); + + const err2 = await Bun.readableStreamToText(stderr); + expect(err2).not.toContain("error:"); + expect(err2).not.toContain("panic:"); + let out2 = await Bun.readableStreamToText(stdout); + expect(out2).toContain("no-deps"); + expect(out2).not.toContain("a-dep"); + expect(await exited).toBe(0); + }); + + test("NO_COLOR works", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "1.0.1", + }, + }), + ); + + await runBunInstall(env, packageDir); + + const testEnv = { ...env, NO_COLOR: "1" }; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + + const out = await Bun.readableStreamToText(stdout); + expect(out).toContain("a-dep"); + const first = out.slice(0, out.indexOf("\n")); + expect(first).toEqual(expect.stringContaining("bun outdated ")); + expect(first).toEqual(expect.stringContaining("v1.")); + const rest = out.slice(out.indexOf("\n") + 1); + expect(rest).toMatchSnapshot(); + + expect(await exited).toBe(0); + }); + + async function setupWorkspace() { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2222222222222", + dependencies: { + "prereleases-1": "1.0.0-future.1", + }, + }), + ), + ]); + } + + async function runBunOutdated(env: any, cwd: string, ...args: string[]): Promise { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated", ...args], + cwd, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + const out = await Bun.readableStreamToText(stdout); + const exitCode = await exited; + expect(exitCode).toBe(0); + return out; + } + + test("--filter with workspace names and paths", async () => { + await setupWorkspace(); + await runBunInstall(env, packageDir); + + let out = await runBunOutdated(env, packageDir, "--filter", "*"); + expect(out).toContain("foo"); + expect(out).toContain("pkg1"); + expect(out).toContain("pkg2222222222222"); + + out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "./"); + expect(out).toContain("pkg1"); + expect(out).not.toContain("foo"); + expect(out).not.toContain("pkg2222222222222"); + + // in directory that isn't a workspace + out = await runBunOutdated(env, join(packageDir, "packages"), "--filter", "./*", "--filter", "!pkg1"); + expect(out).toContain("pkg2222222222222"); + expect(out).not.toContain("pkg1"); + expect(out).not.toContain("foo"); + + out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "../*"); + expect(out).not.toContain("foo"); + expect(out).toContain("pkg2222222222222"); + expect(out).toContain("pkg1"); + }); + + test("dependency pattern args", async () => { + await setupWorkspace(); + await runBunInstall(env, packageDir); + + let out = await runBunOutdated(env, packageDir, "no-deps", "--filter", "*"); + expect(out).toContain("no-deps"); + expect(out).not.toContain("a-dep"); + expect(out).not.toContain("prerelease-1"); + + out = await runBunOutdated(env, packageDir, "a-dep"); + expect(out).not.toContain("a-dep"); + expect(out).not.toContain("no-deps"); + expect(out).not.toContain("prerelease-1"); + + out = await runBunOutdated(env, packageDir, "*", "--filter", "*"); + expect(out).toContain("no-deps"); + expect(out).toContain("a-dep"); + expect(out).toContain("prereleases-1"); + }); + + test("scoped workspace names", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "@foo/bar", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "@scope/pkg1", + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + ]); + + await runBunInstall(env, packageDir); + + let out = await runBunOutdated(env, packageDir, "--filter", "*"); + expect(out).toContain("@foo/bar"); + expect(out).toContain("@scope/pkg1"); + + out = await runBunOutdated(env, packageDir, "--filter", "*", "--filter", "!@foo/*"); + expect(out).not.toContain("@foo/bar"); + expect(out).toContain("@scope/pkg1"); + }); +}); + +// TODO: setup verdaccio to run across multiple test files, then move this and a few other describe +// scopes (update, hoisting, ...) to other files +// +// test/cli/install/registry/bun-install-windowsshim.test.ts: +// +// This test is to verify that BinLinkingShim.zig creates correct shim files as +// well as bun_shim_impl.exe works in various edge cases. There are many fast +// paths for many many cases. +describe("windows bin linking shim should work", async () => { + if (!isWindows) return; + + const packageDir = tmpdirSync(); + + await writeFile( + join(packageDir, "bunfig.toml"), + ` +[install] +cache = false +registry = "http://localhost:${port}/" +`, + ); + + await writeFile( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "bunx-bins": "*", + }, + }), + ); + console.log(packageDir); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--dev"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + console.log(err); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(err).not.toContain("not found"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bunx-bins@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + + const temp_bin_dir = join(packageDir, "temp"); + mkdirSync(temp_bin_dir); + + for (let i = 1; i <= 7; i++) { + const target = join(temp_bin_dir, "a".repeat(i) + ".exe"); + copyFileSync(bunExe(), target); + } + + copyFileSync(join(packageDir, "node_modules\\bunx-bins\\native.exe"), join(temp_bin_dir, "native.exe")); + + const PATH = process.env.PATH + ";" + temp_bin_dir; + + const bins = [ + { bin: "bin1", name: "bin1" }, + { bin: "bin2", name: "bin2" }, + { bin: "bin3", name: "bin3" }, + { bin: "bin4", name: "bin4" }, + { bin: "bin5", name: "bin5" }, + { bin: "bin6", name: "bin6" }, + { bin: "bin7", name: "bin7" }, + { bin: "bin-node", name: "bin-node" }, + { bin: "bin-bun", name: "bin-bun" }, + { bin: "native", name: "exe" }, + { bin: "uses-native", name: `exe ${packageDir}\\node_modules\\bunx-bins\\uses-native.ts` }, + ]; + + for (const { bin, name } of bins) { + test(`bun run ${bin} arg1 arg2`, async () => { + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "run", bin, "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: mergeWindowEnvs([env, { PATH: PATH }]), + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + }); + } + + for (const { bin, name } of bins) { + test(`bun --bun run ${bin} arg1 arg2`, async () => { + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "--bun", "run", bin, "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: mergeWindowEnvs([env, { PATH: PATH }]), + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + }); + } + + for (const { bin, name } of bins) { + test(`bun --bun x ${bin} arg1 arg2`, async () => { + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "--bun", "x", bin, "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: mergeWindowEnvs([env, { PATH: PATH }]), + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + }); + } + + for (const { bin, name } of bins) { + test(`${bin} arg1 arg2`, async () => { + var { stdout, stderr, exited } = spawn({ + cmd: [join(packageDir, "node_modules", ".bin", bin + ".exe"), "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: mergeWindowEnvs([env, { PATH: PATH }]), + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + }); + } +}); + +it("$npm_command is accurate during publish", async () => { + await write( + packageJson, + JSON.stringify({ + name: "publish-pkg-10", + version: "1.0.0", + scripts: { + publish: "echo $npm_command", + }, + }), + ); + await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_command")); + await rm(join(verdaccio.packagesPath, "publish-pkg-10"), { recursive: true, force: true }); + let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); + expect(err).toBe(`$ echo $npm_command\n`); + expect(out.split("\n")).toEqual([ + `bun publish ${Bun.version_with_sha}`, + ``, + `packed 95B package.json`, + ``, + `Total files: 1`, + expect.stringContaining(`Shasum: `), + expect.stringContaining(`Integrity: sha512-`), + `Unpacked size: 95B`, + expect.stringContaining(`Packed size: `), + `Tag: simpletag`, + `Access: default`, + `Registry: http://localhost:${port}/`, + ``, + ` + publish-pkg-10@1.0.0`, + `publish`, + ``, + ]); + expect(exitCode).toBe(0); +}); + +it("$npm_lifecycle_event is accurate during publish", async () => { + await write( + packageJson, + `{ + "name": "publish-pkg-11", + "version": "1.0.0", + "scripts": { + "prepublish": "echo 1 $npm_lifecycle_event", + "publish": "echo 2 $npm_lifecycle_event", + "postpublish": "echo 3 $npm_lifecycle_event", + }, + } + `, + ); + await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_lifecycle_event")); + await rm(join(verdaccio.packagesPath, "publish-pkg-11"), { recursive: true, force: true }); + let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); + expect(err).toBe(`$ echo 2 $npm_lifecycle_event\n$ echo 3 $npm_lifecycle_event\n`); + expect(out.split("\n")).toEqual([ + `bun publish ${Bun.version_with_sha}`, + ``, + `packed 256B package.json`, + ``, + `Total files: 1`, + expect.stringContaining(`Shasum: `), + expect.stringContaining(`Integrity: sha512-`), + `Unpacked size: 256B`, + expect.stringContaining(`Packed size: `), + `Tag: simpletag`, + `Access: default`, + `Registry: http://localhost:${port}/`, + ``, + ` + publish-pkg-11@1.0.0`, + `2 publish`, + `3 postpublish`, + ``, + ]); + expect(exitCode).toBe(0); +}); diff --git a/test/cli/install/bun-install-retry.test.ts b/test/cli/install/bun-install-retry.test.ts index 842691bb0b..cbba8e2b37 100644 --- a/test/cli/install/bun-install-retry.test.ts +++ b/test/cli/install/bun-install-retry.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { access, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index b6470eba7f..fb4a1d7c40 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -1,4 +1,4 @@ -import { file, listen, Socket, spawn } from "bun"; +import { file, listen, Socket, spawn, write } from "bun"; import { afterAll, afterEach, @@ -11,7 +11,7 @@ import { setDefaultTimeout, test, } from "bun:test"; -import { access, mkdir, readlink, rm, writeFile, cp, stat } from "fs/promises"; +import { access, mkdir, readlink, rm, writeFile, cp, stat, exists } from "fs/promises"; import { bunEnv, bunExe, @@ -22,6 +22,8 @@ import { toHaveBins, runBunInstall, isWindows, + textLockfile, + readdirSorted, } from "harness"; import { join, sep, resolve } from "path"; import { @@ -31,10 +33,10 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, + getPort, } from "./dummy.registry.js"; expect.extend({ @@ -1121,59 +1123,6 @@ it("should handle inter-dependency between workspaces (optionalDependencies)", a await access(join(package_dir, "bun.lockb")); }); -it("should ignore peerDependencies within workspaces", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - workspaces: ["packages/baz"], - peerDependencies: { - Bar: ">=0.0.2", - }, - }), - ); - await mkdir(join(package_dir, "packages", "baz"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "baz", "package.json"), - JSON.stringify({ - name: "Baz", - version: "0.0.3", - peerDependencies: { - Moo: ">=0.0.4", - }, - }), - ); - await writeFile( - join(package_dir, "bunfig.toml"), - ` - [install] - peer = false - `, - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual(["Baz"]); - expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); - await access(join(package_dir, "bun.lockb")); -}); - it("should handle installing the same peerDependency with different versions", async () => { const urls: string[] = []; setHandler(dummyRegistry(urls)); @@ -5708,14 +5657,8 @@ it("should handle tarball URL with existing lockfile", async () => { "3 packages installed", ]); expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/bar`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz`, - `${root_url}/baz-0.0.3.tgz`, - `${root_url}/moo-0.1.0.tgz`, - ]); - expect(requested).toBe(10); + expect(urls.sort()).toEqual([`${root_url}/bar-0.0.2.tgz`, `${root_url}/baz-0.0.3.tgz`, `${root_url}/moo-0.1.0.tgz`]); + expect(requested).toBe(8); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "bar", "baz"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); @@ -5850,13 +5793,8 @@ it("should handle tarball path with existing lockfile", async () => { "3 packages installed", ]); expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/bar`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz`, - `${root_url}/baz-0.0.3.tgz`, - ]); - expect(requested).toBe(8); + expect(urls.sort()).toEqual([`${root_url}/bar-0.0.2.tgz`, `${root_url}/baz-0.0.3.tgz`]); + expect(requested).toBe(6); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "bar", "baz"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); @@ -8361,3 +8299,240 @@ cache = false expect(await new Response(stdout).text()).toContain("installed @~39/empty@1.0.0"); expect(await exited).toBe(0); }); + +it("should handle modified git resolutions in bun.lock", async () => { + // install-test-8 has a dependency but because it's not in the lockfile + // it won't be included in the install. + await Promise.all([ + write( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "jquery": "3.7.1", + }, + }), + ), + write( + join(package_dir, "bun.lock"), + JSON.stringify({ + "lockfileVersion": 0, + "workspaces": { + "": { + "dependencies": { + "jquery": "3.7.1", + }, + }, + }, + "packages": { + "jquery": [ + "jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea", + {}, + "3a1288830817d13da39e9231302261896f8721ea", + ], + }, + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + + expect(out).toContain("1 package installed"); + expect(await exited).toBe(0); + + expect( + (await file(join(package_dir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), + ).toMatchSnapshot(); +}); + +it("should read install.saveTextLockfile from bunfig.toml", async () => { + await Promise.all([ + write( + join(package_dir, "bunfig.toml"), + ` +[install] +cache = false +registry = "http://localhost:${getPort()}/" +saveTextLockfile = true +`, + ), + write( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ), + write( + join(package_dir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg-one", + version: "1.0.0", + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 package installed"); + + expect(await exited).toBe(0); + expect(await Bun.file(join(package_dir, "node_modules", "pkg-one", "package.json")).json()).toEqual({ + name: "pkg-one", + version: "1.0.0", + }); + expect(await exists(join(package_dir, "bun.lockb"))).toBeFalse(); + expect(await file(join(package_dir, "bun.lock")).text()).toMatchSnapshot(); +}); + +test("providing invalid url in lockfile does not crash", async () => { + await Promise.all([ + write( + join(package_dir, "package.json"), + JSON.stringify({ + dependencies: { + "jquery": "3.7.1", + }, + }), + ), + write( + join(package_dir, "bun.lock"), + textLockfile(0, { + "workspaces": { + "": { + "dependencies": { + "jquery": "3.7.1", + }, + }, + }, + "packages": { + "jquery": [ + "jquery@3.7.1", + "invalid-url", + {}, + "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + ], + }, + }), + ), + ]); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain( + 'error: Expected tarball URL to start with https:// or http://, got "invalid-url" while fetching package "jquery"', + ); + expect(await exited).toBe(1); +}); + +test("optional dependencies do not need to be resolvable in text lockfile", async () => { + await Promise.all([ + write( + join(package_dir, "package.json"), + JSON.stringify({ + optionalDependencies: { + jquery: "3.7.1", + }, + }), + ), + write( + join(package_dir, "bun.lock"), + textLockfile(0, { + "workspaces": { + "": { + "optionalDependencies": { + "jquery": "3.7.1", + }, + }, + }, + "packages": {}, + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("Saved lockfile"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("1 package installed"); + + expect(await exited).toBe(0); +}); + +test("non-optional dependencies need to be resolvable in text lockfile", async () => { + await Promise.all([ + write( + join(package_dir, "package.json"), + JSON.stringify({ + dependencies: { + jquery: "3.7.1", + }, + }), + ), + write( + join(package_dir, "bun.lock"), + textLockfile(0, { + workspaces: { + "": { + dependencies: { + "jquery": "3.7.1", + }, + }, + }, + packages: {}, + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + // --production to fail early + cmd: [bunExe(), "install", "--production"], + cwd: package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("Saved lockfile"); + expect(err).toContain("error: Failed to resolve root prod dependency 'jquery'"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("1 package installed"); + + expect(await exited).toBe(1); +}); diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 8cdeaa4413..68f5160faa 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -1,16 +1,18 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, mkdir, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, runBunInstall, tmpdirSync, toBeValidBin, toHaveBins } from "harness"; -import { basename, join } from "path"; import { - dummyAfterAll, - dummyAfterEach, - dummyBeforeAll, - dummyBeforeEach, - package_dir, + bunExe, + bunEnv as env, + runBunInstall, + tmpdirSync, + toBeValidBin, + toHaveBins, + stderrForInstall, readdirSorted, -} from "./dummy.registry"; +} from "harness"; +import { basename, join } from "path"; +import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry"; beforeAll(dummyBeforeAll); afterAll(dummyAfterAll); @@ -56,7 +58,7 @@ it("should link and unlink workspace package", async () => { }), ); let { out, err } = await runBunInstall(env, link_dir); - expect(err.split(/\r?\n/)).toEqual(["Saved lockfile", ""]); + expect(err.split(/\r?\n/).slice(-2)).toEqual(["Saved lockfile", ""]); expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", @@ -72,7 +74,7 @@ it("should link and unlink workspace package", async () => { env, }); - err = await new Response(stderr).text(); + err = stderrForInstall(await new Response(stderr).text()); expect(err.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout).text()).toContain(`Success! Registered "moo"`); expect(await exited).toBe(0); @@ -86,7 +88,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = await new Response(stderr).text(); + err = stderrForInstall(await new Response(stderr).text()); expect(err.split(/\r?\n/)).toEqual([""]); expect((await new Response(stdout).text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun link v1."), @@ -110,7 +112,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = await new Response(stderr).text(); + err = stderrForInstall(await new Response(stderr).text()); expect(err.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout).text()).toContain(`success: unlinked package "moo"`); expect(await exited).toBe(0); @@ -125,7 +127,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = await new Response(stderr).text(); + err = stderrForInstall(await new Response(stderr).text()); expect(err.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout).text()).toContain(`Success! Registered "foo"`); expect(await exited).toBe(0); @@ -139,7 +141,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = await new Response(stderr).text(); + err = stderrForInstall(await new Response(stderr).text()); expect(err.split(/\r?\n/)).toEqual([""]); expect((await new Response(stdout).text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun link v1."), @@ -164,7 +166,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = await new Response(stderr).text(); + err = stderrForInstall(await new Response(stderr).text()); expect(err.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout).text()).toContain(`success: unlinked package "foo"`); expect(await exited).toBe(0); @@ -199,7 +201,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err1 = await new Response(stderr1).text(); + const err1 = stderrForInstall(await new Response(stderr1).text()); expect(err1.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`); expect(await exited1).toBe(0); @@ -216,7 +218,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err2 = await new Response(stderr2).text(); + const err2 = stderrForInstall(await new Response(stderr2).text()); expect(err2.split(/\r?\n/)).toEqual([""]); const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -240,7 +242,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err3 = await new Response(stderr3).text(); + const err3 = stderrForInstall(await new Response(stderr3).text()); expect(err3.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`); expect(await exited3).toBe(0); @@ -257,7 +259,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err4 = await new Response(stderr4).text(); + const err4 = stderrForInstall(await new Response(stderr4).text()); expect(err4).toContain(`error: Package "${link_name}" is not linked`); expect(await new Response(stdout4).text()).toEqual(expect.stringContaining("bun link v1.")); expect(await exited4).toBe(1); @@ -292,7 +294,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err1 = await new Response(stderr1).text(); + const err1 = stderrForInstall(await new Response(stderr1).text()); expect(err1.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`); expect(await exited1).toBe(0); @@ -309,7 +311,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err2 = await new Response(stderr2).text(); + const err2 = stderrForInstall(await new Response(stderr2).text()); expect(err2.split(/\r?\n/)).toEqual([""]); const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -333,7 +335,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err3 = await new Response(stderr3).text(); + const err3 = stderrForInstall(await new Response(stderr3).text()); expect(err3.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`); expect(await exited3).toBe(0); @@ -350,7 +352,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err4 = await new Response(stderr4).text(); + const err4 = stderrForInstall(await new Response(stderr4).text()); expect(err4).toContain(`error: Package "${link_name}" is not linked`); expect((await new Response(stdout4).text()).split(/\r?\n/)).toEqual([expect.stringContaining("bun link v1."), ""]); expect(await exited4).toBe(1); @@ -392,14 +394,14 @@ it("should link dependency without crashing", async () => { stderr: "pipe", env, }); - const err1 = await new Response(stderr1).text(); + const err1 = stderrForInstall(await new Response(stderr1).text()); expect(err1.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`); expect(await exited1).toBe(0); const { out: stdout2, err: stderr2, exited: exited2 } = await runBunInstall(env, package_dir); - const err2 = await new Response(stderr2).text(); - expect(err2.split(/\r?\n/)).toEqual(["Saved lockfile", ""]); + const err2 = stderrForInstall(await new Response(stderr2).text()); + expect(err2.split(/\r?\n/).slice(-2)).toEqual(["Saved lockfile", ""]); const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), @@ -429,7 +431,7 @@ it("should link dependency without crashing", async () => { stderr: "pipe", env, }); - const err3 = await new Response(stderr3).text(); + const err3 = stderrForInstall(await new Response(stderr3).text()); expect(err3.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`); expect(await exited3).toBe(0); @@ -446,7 +448,7 @@ it("should link dependency without crashing", async () => { stderr: "pipe", env, }); - const err4 = await new Response(stderr4).text(); + const err4 = stderrForInstall(await new Response(stderr4).text()); expect(err4).toContain(`error: FileNotFound installing ${link_name}`); const out4 = await new Response(stdout4).text(); expect(out4.replace(/\[[0-9\.]+m?s\]/, "[]").split(/\r?\n/)).toEqual([ diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts new file mode 100644 index 0000000000..f60725be5f --- /dev/null +++ b/test/cli/install/bun-lock.test.ts @@ -0,0 +1,85 @@ +import { spawn, write, file } from "bun"; +import { expect, it } from "bun:test"; +import { access, copyFile, open, writeFile } from "fs/promises"; +import { bunExe, bunEnv as env, isWindows, tmpdirSync } from "harness"; +import { join } from "path"; + +it("should write plaintext lockfiles", async () => { + const package_dir = tmpdirSync(); + + // copy bar-0.0.2.tgz to package_dir + await copyFile(join(__dirname, "bar-0.0.2.tgz"), join(package_dir, "bar-0.0.2.tgz")); + + // Create a simple package.json + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "test-package", + version: "1.0.0", + dependencies: { + "dummy-package": "file:./bar-0.0.2.tgz", + }, + }), + ); + + // Run 'bun install' to generate the lockfile + const installResult = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: package_dir, + env, + }); + await installResult.exited; + + // Ensure the lockfile was created + await access(join(package_dir, "bun.lock")); + + // Assert that the lockfile has the correct permissions + const file = await open(join(package_dir, "bun.lock"), "r"); + const stat = await file.stat(); + + // in unix, 0o644 == 33188 + let mode = 33188; + // ..but windows is different + if (isWindows) { + mode = 33206; + } + expect(stat.mode).toBe(mode); + + expect(await file.readFile({ encoding: "utf8" })).toMatchSnapshot(); +}); + +// won't work on windows, " is not a valid character in a filename +it.skipIf(isWindows)("should escape names", async () => { + const packageDir = tmpdirSync(); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "quote-in-dependency-name", + workspaces: ["packages/*"], + }), + ), + write(join(packageDir, "packages", '"', "package.json"), JSON.stringify({ name: '"' })), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + '"': "*", + }, + }), + ), + ]); + + const { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "bun.lock")).text()).toMatchSnapshot(); +}); diff --git a/test/cli/install/bun-lockb.test.ts b/test/cli/install/bun-lockb.test.ts index b97bc1759a..cf0ab704ef 100644 --- a/test/cli/install/bun-lockb.test.ts +++ b/test/cli/install/bun-lockb.test.ts @@ -1,7 +1,7 @@ import { spawn } from "bun"; import { expect, it } from "bun:test"; -import { access, copyFile, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync } from "harness"; +import { access, copyFile, open, writeFile } from "fs/promises"; +import { bunExe, bunEnv as env, isWindows, tmpdirSync } from "harness"; import { join } from "path"; it("should not print anything to stderr when running bun.lockb", async () => { @@ -33,6 +33,18 @@ it("should not print anything to stderr when running bun.lockb", async () => { // Ensure the lockfile was created await access(join(package_dir, "bun.lockb")); + // Assert that the lockfile has the correct permissions + const file = await open(join(package_dir, "bun.lockb"), "r"); + const stat = await file.stat(); + + // in unix, 0o755 == 33261 + let mode = 33261; + // ..but windows is different + if(isWindows) { + mode = 33206; + } + expect(stat.mode).toBe(mode); + // create a .env await writeFile(join(package_dir, ".env"), "FOO=bar"); diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 37b3d09563..ac74efa8e7 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -602,6 +602,110 @@ describe("bundledDependnecies", () => { }); } + test(`basic (bundledDependencies: true)`, async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "pack-bundled", + version: "4.4.4", + dependencies: { + "dep1": "1.1.1", + }, + devDependencies: { + "dep2": "1.1.1", + }, + bundledDependencies: true, + }), + ), + write( + join(packageDir, "node_modules", "dep1", "package.json"), + JSON.stringify({ + name: "dep1", + version: "1.1.1", + }), + ), + write( + join(packageDir, "node_modules", "dep2", "package.json"), + JSON.stringify({ + name: "dep2", + version: "1.1.1", + }), + ), + ]); + + await pack(packageDir, bunEnv); + + const tarball = readTarball(join(packageDir, "pack-bundled-4.4.4.tgz")); + expect(tarball.entries).toMatchObject([ + { "pathname": "package/package.json" }, + { "pathname": "package/node_modules/dep1/package.json" }, + ]); + }); + + test(`scoped bundledDependencies`, async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "pack-bundled", + version: "4.4.4", + dependencies: { + "@oven/bun": "1.1.1", + }, + bundledDependencies: ["@oven/bun"], + }), + ), + write( + join(packageDir, "node_modules", "@oven", "bun", "package.json"), + JSON.stringify({ + name: "@oven/bun", + version: "1.1.1", + }), + ), + ]); + + await pack(packageDir, bunEnv); + + const tarball = readTarball(join(packageDir, "pack-bundled-4.4.4.tgz")); + expect(tarball.entries).toMatchObject([ + { "pathname": "package/package.json" }, + { "pathname": "package/node_modules/@oven/bun/package.json" }, + ]); + }); + + test(`invalid bundledDependencies value should throw`, async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "pack-bundled", + version: "4.4.4", + bundledDependencies: "a", + }), + ), + ]); + + const { stdout, stderr, exited } = Bun.spawn({ + cmd: [bunExe(), "pm", "pack"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env: bunEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("error:"); + expect(err).toContain("to be a boolean or an array of strings"); + expect(err).not.toContain("warning:"); + expect(err).not.toContain("failed"); + expect(err).not.toContain("panic:"); + + const exitCode = await exited; + expect(exitCode).toBe(1); + }); + test("resolve dep of bundled dep", async () => { // Test that a bundled dep can have it's dependencies resolved without // needing to add them to `bundledDependencies`. Also test that only @@ -660,7 +764,7 @@ describe("bundledDependnecies", () => { ]); }); - test.todo("scoped names", async () => { + test("scoped names", async () => { await Promise.all([ write( join(packageDir, "package.json"), diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index d1a6042f96..8a49dcaf16 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -1,7 +1,7 @@ import { spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { exists, mkdir, writeFile } from "fs/promises"; -import { bunEnv, bunExe, bunEnv as env, tmpdirSync } from "harness"; +import { bunEnv, bunExe, bunEnv as env, tmpdirSync, readdirSorted } from "harness"; import { cpSync } from "node:fs"; import { join } from "path"; import { @@ -11,7 +11,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index dc1866ec4b..4697357e22 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -1,9 +1,18 @@ -import { file, spawn, spawnSync } from "bun"; +import { $, file, spawn, spawnSync } from "bun"; import { beforeEach, describe, expect, it } from "bun:test"; import { exists, mkdir, rm, writeFile } from "fs/promises"; -import { bunEnv, bunExe, bunEnv as env, isWindows, tempDirWithFiles, tmpdirSync, stderrForInstall } from "harness"; +import { + bunEnv, + bunExe, + bunEnv as env, + isWindows, + tempDirWithFiles, + tmpdirSync, + stderrForInstall, + readdirSorted, +} from "harness"; import { join } from "path"; -import { readdirSorted } from "./dummy.registry"; +import { chmodSync } from "fs"; let run_dir: string; @@ -275,7 +284,7 @@ console.log(minify("print(6 * 7)").code); BUN_INSTALL_CACHE_DIR: join(run_dir, ".cache"), }, }); - const err1 = await new Response(stderr1).text(); + const err1 = stderrForInstall(await new Response(stderr1).text()); expect(err1).toBe(""); expect(await readdirSorted(run_dir)).toEqual([".cache", "test.js"]); expect(await readdirSorted(join(run_dir, ".cache"))).toContain("uglify-js"); @@ -339,7 +348,7 @@ for (const entry of await decompress(Buffer.from(buffer))) { BUN_INSTALL_CACHE_DIR: join(run_dir, ".cache"), }, }); - const err1 = await new Response(stderr1).text(); + const err1 = stderrForInstall(await new Response(stderr1).text()); expect(err1).toBe(""); expect(await readdirSorted(run_dir)).toEqual([".cache", "test.js"]); expect(await readdirSorted(join(run_dir, ".cache"))).toContain("decompress"); @@ -591,22 +600,388 @@ it("should pass arguments correctly in scripts", async () => { } }); -it("should run with bun instead of npm even with leading spaces", async () => { +const cases = [ + ["yarn run", "run"], + ["yarn add", "passthrough"], + ["yarn audit", "passthrough"], + ["yarn -abcd run", "passthrough"], + ["yarn info", "passthrough"], + ["yarn generate-lock-entry", "passthrough"], + ["yarn", "run"], + ["npm run", "run"], + ["npx", "x"], + ["pnpm run", "run"], + ["pnpm dlx", "x"], + ["pnpx", "x"], +]; +describe("should handle run case", () => { + for (const ccase of cases) { + it(ccase[0], async () => { + const dir = tempDirWithFiles("test", { + "package.json": JSON.stringify({ + scripts: { + "root_script": ` ${ccase[0]} target_script% `, + "target_script%": " echo target_script ", + }, + }), + }); + { + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), "root_script"], + cwd: dir, + env: bunEnv, + }); + + if (ccase[1] === "run") { + expect(stderr.toString()).toMatch( + /^\$ bun(-debug)? run target_script% \n\$ echo target_script \n/, + ); + expect(stdout.toString()).toEndWith("target_script\n"); + expect(exitCode).toBe(0); + } else if (ccase[1] === "x") { + expect(stderr.toString()).toMatch( + /^\$ bun(-debug)? x target_script% \nerror: unrecognised dependency format: target_script%/, + ); + expect(exitCode).toBe(1); + } else { + expect(stderr.toString()).toStartWith(`$ ${ccase[0]} target_script% \n`); + } + } + }); + } +}); + +describe("'bun run' priority", async () => { + // priority: + // - 1: run script with matching name + // - 2: load module and run that module + // - 3: execute a node_modules/.bin/ command + // - 4: ('run' only): execute a system command, like 'ls' + const dir = tempDirWithFiles("test", { + "test": { "index.js": "console.log('test/index.js');" }, + "build": { "script.js": "console.log('build/script.js');" }, + "consume": { "index.js": "console.log('consume/index.js');" }, + "index.js": "console.log('index.js')", + "main.js": "console.log('main.js')", + "typescript.ts": "console.log('typescript.ts')", + "sample.js": "console.log('sample.js')", + "noext": "console.log('noext')", + "folderandfile": { "index.js": "console.log('folderandfile/index.js')" }, + "folderandfile.js": "console.log('folderandfile.js')", + "shellscript.sh": "echo shellscript.sh", + ".secretscript.js": "console.log('.secretscript.js')", + "package.json": JSON.stringify({ + scripts: { + "build": "echo scripts/build", + "test": "echo scripts/test", + "sample.js": "echo scripts/sample.js", + "§'.js": 'echo "scripts/§\'.js"', + "test.todo": "echo scripts/test.todo", + "/absolute": "echo DO_NOT_RUN", + "./relative": "echo DO_NOT_RUN", + }, + main: "main.js", + }), + "§'.js": 'console.log("§\'.js")', + "node_modules": { ".bin": { "confabulate": `#!${bunExe()}\nconsole.log("node_modules/.bin/confabulate")` } }, + }); + chmodSync(dir + "/node_modules/.bin/confabulate", 0o755); + + const commands: { + command: string[]; + req_run?: boolean; + stdout: string; + stderr?: string | RegExp; + exitCode?: number; + }[] = [ + { command: ["test"], stdout: "scripts/test", stderr: "$ echo scripts/test", req_run: true }, + { command: ["build"], stdout: "scripts/build", stderr: "$ echo scripts/build", req_run: true }, + { command: ["consume"], stdout: "consume/index.js", stderr: "" }, + + { command: ["test/index"], stdout: "test/index.js", stderr: "" }, + { command: ["test/index.js"], stdout: "test/index.js", stderr: "" }, + { command: ["build/script"], stdout: "build/script.js", stderr: "" }, + { command: ["build/script.js"], stdout: "build/script.js", stderr: "" }, + { command: ["consume/index"], stdout: "consume/index.js", stderr: "" }, + { command: ["consume/index.js"], stdout: "consume/index.js", stderr: "" }, + + { command: ["./test"], stdout: "test/index.js", stderr: "" }, + { command: ["./build"], stdout: "", stderr: /error: Module not found "\.(\/|\\|\\\\)build"|EACCES/, exitCode: 1 }, + { command: ["./consume"], stdout: "consume/index.js", stderr: "" }, + + { command: ["index.js"], stdout: "index.js", stderr: "" }, + { command: ["./index.js"], stdout: "index.js", stderr: "" }, + { command: ["index"], stdout: "index.js", stderr: "" }, + { command: ["./index"], stdout: "index.js", stderr: "" }, + + { command: ["."], stdout: "main.js", stderr: "" }, + { command: ["./"], stdout: "main.js", stderr: "" }, + + { command: ["typescript.ts"], stdout: "typescript.ts", stderr: "" }, + { command: ["./typescript.ts"], stdout: "typescript.ts", stderr: "" }, + { command: ["typescript.js"], stdout: "typescript.ts", stderr: "" }, + { command: ["./typescript.js"], stdout: "typescript.ts", stderr: "" }, + { command: ["typescript"], stdout: "typescript.ts", stderr: "" }, + { command: ["./typescript"], stdout: "typescript.ts", stderr: "" }, + + { command: ["sample.js"], stdout: "scripts/sample.js", stderr: "$ echo scripts/sample.js", req_run: true }, + { command: ["sample.js"], stdout: "sample.js", stderr: "", req_run: false }, + { command: ["./sample.js"], stdout: "sample.js", stderr: "" }, + { command: ["sample"], stdout: "sample.js", stderr: "" }, + { command: ["./sample"], stdout: "sample.js", stderr: "" }, + + { command: ["test.todo"], stdout: "scripts/test.todo", stderr: "$ echo scripts/test.todo" }, + + { command: ["§'.js"], stdout: "scripts/§'.js", stderr: '$ echo "scripts/§\'.js"', req_run: true }, + { command: ["§'.js"], stdout: "§'.js", stderr: "", req_run: false }, + { command: ["./§'.js"], stdout: "§'.js", stderr: "" }, + { command: ["§'"], stdout: "§'.js", stderr: "" }, + { command: ["./§'"], stdout: "§'.js", stderr: "" }, + + { command: ["noext"], stdout: "noext", stderr: "" }, + { command: ["./noext"], stdout: "noext", stderr: "" }, + + { command: ["folderandfile"], stdout: "folderandfile.js", stderr: "" }, + { command: ["./folderandfile"], stdout: "folderandfile.js", stderr: "" }, + { command: ["folderandfile.js"], stdout: "folderandfile.js", stderr: "" }, + { command: ["./folderandfile.js"], stdout: "folderandfile.js", stderr: "" }, + ...(isWindows + ? [] // on windows these ones run "folderandfile.js" but the absolute path ones run "folderandfile/index.js" + : [ + { command: ["folderandfile/"], stdout: "folderandfile/index.js", stderr: "" }, + { command: ["./folderandfile/"], stdout: "folderandfile/index.js", stderr: "" }, + ]), + { command: ["folderandfile/index"], stdout: "folderandfile/index.js", stderr: "" }, + { command: ["./folderandfile/index"], stdout: "folderandfile/index.js", stderr: "" }, + { command: ["folderandfile/index.js"], stdout: "folderandfile/index.js", stderr: "" }, + { command: ["./folderandfile/index.js"], stdout: "folderandfile/index.js", stderr: "" }, + { command: [dir + "/folderandfile"], stdout: "folderandfile.js", stderr: "" }, + { command: [dir + "/folderandfile/"], stdout: "folderandfile/index.js", stderr: "" }, + + { command: ["shellscript.sh"], stdout: "shellscript.sh", stderr: "" }, + { command: ["./shellscript.sh"], stdout: "shellscript.sh", stderr: "" }, + + { command: [".secretscript.js"], stdout: ".secretscript.js", stderr: "" }, + { command: ["./.secretscript"], stdout: ".secretscript.js", stderr: "" }, + { command: [dir + "/.secretscript"], stdout: ".secretscript.js", stderr: "" }, + + { + command: ["/absolute"], + stdout: "", + stderr: /error: Module not found "(\/|\\|\\\\)absolute"|EACCES/, + exitCode: 1, + }, + { + command: ["./relative"], + stdout: "", + stderr: /error: Module not found ".(\/|\\|\\\\)relative"|EACCES/, + exitCode: 1, + }, + + ...(isWindows + ? [ + // TODO: node_modules command + // TODO: system command + ] + : [ + // node_modules command + { command: ["confabulate"], stdout: "node_modules/.bin/confabulate", stderr: "" }, + + // system command + { command: ["echo", "abc"], stdout: "abc", stderr: "", req_run: true }, + { command: ["echo", "abc"], stdout: "", exitCode: 1, req_run: false }, + ]), + + // TODO: test preloads (https://bun.sh/docs/runtime/bunfig#preload), test $npm_lifecycle_event + // TODO: test with path overrides in tsconfig.json + ]; + if (isWindows) { + for (const cmd of [...commands]) { + if (cmd.command[0].includes("/")) { + commands.push({ + ...cmd, + command: [cmd.command[0].replaceAll("/", "\\"), ...cmd.command.slice(1)], + }); + } + } + } + + for (const cmd of commands) { + for (const flag of [[], ["--bun"]]) { + for (const postflag of cmd.req_run === true ? [["run"]] : cmd.req_run === false ? [[]] : [[], ["run"]]) { + const full_command = [...flag, ...postflag, ...cmd.command]; + it("bun " + full_command.join(" "), () => { + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), ...full_command], + cwd: dir, + env: { ...bunEnv, BUN_DEBUG_QUIET_LOGS: "1" }, + }); + + if (cmd.stderr != null && typeof cmd.stderr !== "string") expect(stderr.toString()).toMatch(cmd.stderr); + expect({ + ...(cmd.stderr != null && typeof cmd.stderr === "string" ? { stderr: stderr.toString().trim() } : {}), + stdout: stdout.toString().trim(), + exitCode, + }).toStrictEqual({ + ...(cmd.stderr != null && typeof cmd.stderr === "string" ? { stderr: cmd.stderr } : {}), + stdout: cmd.stdout, + exitCode: cmd.exitCode ?? 0, + }); + }); + } + } + } +}); + +it("should run from stdin", async () => { + const res = await $`echo "console.log('hello')" | bun run -`.text(); + expect(res).toBe(`hello\n`); +}); + +describe.todo("run from stdin", async () => { + // TODO: write this test + // note limit of around 1gb when running from stdin + // - which says 'catch return false' +}); + +describe("should run scripts from the project root (#16169)", async () => { + const dir = tempDirWithFiles("test", { + "run_here": { + "myscript.ts": "console.log('successful run')", + "package.json": JSON.stringify({ + scripts: { "sample": "pwd", "runscript": "bun myscript.ts" }, + }), + "dont_run_in_here": { + "runme.ts": "console.log('do run this script')", + }, + }, + }); + + it("outside", () => { + const run_outside = spawnSync({ + cmd: [bunExe(), "run", "sample"], + cwd: dir + "/run_here", + env: bunEnv, + }); + expect(run_outside.stdout.toString()).toContain("run_here"); + expect(run_outside.stdout.toString()).not.toContain("dont_run_in_here"); + expect(run_outside.exitCode).toBe(0); + }); + + it("inside", () => { + const run_inside = spawnSync({ + cmd: [bunExe(), "run", "sample"], + cwd: dir + "/run_here/dont_run_in_here", + env: bunEnv, + }); + expect(run_inside.stdout.toString()).toContain("run_here"); + expect(run_inside.stdout.toString()).not.toContain("dont_run_in_here"); + expect(run_inside.exitCode).toBe(0); + }); + + it("inside --shell=bun", () => { + const run_inside = spawnSync({ + cmd: [bunExe(), "--shell=bun", "run", "sample"], + cwd: dir + "/run_here/dont_run_in_here", + env: bunEnv, + }); + expect(run_inside.stdout.toString()).toContain("run_here"); + expect(run_inside.stdout.toString()).not.toContain("dont_run_in_here"); + expect(run_inside.exitCode).toBe(0); + }); + + it("inside script", () => { + const run_inside = spawnSync({ + cmd: [bunExe(), "run", "runme.ts"], + cwd: dir + "/run_here/dont_run_in_here", + env: bunEnv, + }); + expect(run_inside.stdout.toString()).toContain("do run this script"); + expect(run_inside.exitCode).toBe(0); + }); + + it("inside wrong script", () => { + const run_inside = spawnSync({ + cmd: [bunExe(), "run", "myscript.ts"], + cwd: dir + "/run_here/dont_run_in_here", + env: bunEnv, + }); + const stderr = run_inside.stderr.toString(); + if (stderr.includes("myscript.ts") && stderr.includes("EACCES")) { + // for some reason on musl, the run_here folder is in $PATH + // 'error: Failed to run "myscript.ts" due to:\nEACCES: run_here/myscript.ts: Permission denied (posix_spawn())' + } else { + expect(stderr).toBe('error: Module not found "myscript.ts"\n'); + } + expect(run_inside.exitCode).toBe(1); + }); + + it("outside 2", () => { + const run_outside_script = spawnSync({ + cmd: [bunExe(), "runscript"], + cwd: dir + "/run_here", + env: bunEnv, + }); + expect(run_outside_script.stdout.toString()).toBe("successful run\n"); + expect(run_outside_script.exitCode).toBe(0); + }); + + it("inside 2", () => { + const run_inside_script = spawnSync({ + cmd: [bunExe(), "runscript"], + cwd: dir + "/run_here/dont_run_in_here", + env: bunEnv, + }); + expect(run_inside_script.stdout.toString()).toBe("successful run\n"); + expect(run_inside_script.exitCode).toBe(0); + }); +}); + +describe("run main within monorepo", async () => { const dir = tempDirWithFiles("test", { "package.json": JSON.stringify({ - workspaces: ["a", "b"], - scripts: { "root_script": " npm run other_script ", "other_script": " echo hi " }, + name: "monorepo_root", + main: "monorepo_root.ts", + workspaces: ["packages/*"], }), + "monorepo_root.ts": "console.log('monorepo_root')", + "packages": { + "package_a": { + "package.json": JSON.stringify({ name: "package_a", main: "package_a.ts" }), + "package_a.ts": "console.log('package_a')", + }, + "package_b": { + "package.json": JSON.stringify({ name: "package_b" }), + }, + }, }); - { + + it("should run main from monorepo root", () => { const { stdout, stderr, exitCode } = spawnSync({ - cmd: [bunExe(), "run", "root_script"], + cmd: [bunExe(), "."], cwd: dir, env: bunEnv, }); - - expect(stderr.toString()).toBe("$ bun run other_script \n$ echo hi \n"); - expect(stdout.toString()).toEndWith("hi\n"); + expect(stdout.toString()).toBe("monorepo_root\n"); expect(exitCode).toBe(0); - } + }); + + it("should run package_a from package_a", () => { + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), "."], + cwd: dir + "/packages/package_a", + env: bunEnv, + }); + expect(stdout.toString()).toBe("package_a\n"); + expect(exitCode).toBe(0); + }); + + it("should fail from package_b", () => { + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), "."], + cwd: dir + "/packages/package_b", + env: bunEnv, + }); + expect(exitCode).toBe(1); + }); }); diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts index 2ecbbb9daa..e81f65184a 100644 --- a/test/cli/install/bun-update.test.ts +++ b/test/cli/install/bun-update.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, readFile, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, toBeValidBin, toHaveBins, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 7cf4d49d37..a72ece2280 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -1,31 +1,41 @@ -import { file, write } from "bun"; +import { file, write, spawn } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; -import { beforeEach, describe, expect, test } from "bun:test"; +import { beforeEach, describe, expect, test, beforeAll, afterAll } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "fs"; -import { cp } from "fs/promises"; -import { bunExe, bunEnv as env, runBunInstall, tmpdirSync, toMatchNodeModulesAt } from "harness"; +import { cp, mkdir, rm, exists } from "fs/promises"; +import { + bunExe, + bunEnv as env, + runBunInstall, + toMatchNodeModulesAt, + assertManifestsPopulated, + VerdaccioRegistry, + readdirSorted, +} from "harness"; import { join } from "path"; const { parseLockfile } = install_test_helpers; expect.extend({ toMatchNodeModulesAt }); -var testCounter: number = 0; - // not necessary, but verdaccio will be added to this file in the near future -var port: number = 4873; -var packageDir: string; -beforeEach(() => { - packageDir = tmpdirSync(); +var verdaccio: VerdaccioRegistry; +var packageDir: string; +var packageJson: string; + +beforeAll(async () => { + verdaccio = new VerdaccioRegistry(); + await verdaccio.start(); +}); + +afterAll(() => { + verdaccio.stop(); +}); + +beforeEach(async () => { + ({ packageDir, packageJson } = await verdaccio.createTestDir()); env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); - writeFileSync( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = false -`, - ); }); test("dependency on workspace without version in package.json", async () => { @@ -41,7 +51,7 @@ test("dependency on workspace without version in package.json", async () => { write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ - name: "lodash", + name: "no-deps", }), ), ]); @@ -60,7 +70,7 @@ test("dependency on workspace without version in package.json", async () => { "1", "1.*", "1.1.*", - "1.1.1", + "1.1.0", "*-pre+build", "*+build", "latest", // dist-tag exists, should choose package from npm @@ -74,7 +84,7 @@ test("dependency on workspace without version in package.json", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: version, + "no-deps": version, }, }), ); @@ -82,7 +92,9 @@ test("dependency on workspace without version in package.json", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(lockfile).toMatchSnapshot(`version: ${version}`); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", @@ -101,7 +113,7 @@ test("dependency on workspace without version in package.json", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: version, + "no-deps": version, }, }), ); @@ -109,7 +121,9 @@ test("dependency on workspace without version in package.json", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(lockfile).toMatchSnapshot(`version: ${version}`); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", @@ -134,7 +148,7 @@ test("dependency on same name as workspace and dist-tag", async () => { write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ - name: "lodash", + name: "no-deps", version: "4.17.21", }), ), @@ -145,7 +159,7 @@ test("dependency on same name as workspace and dist-tag", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: "latest", + "no-deps": "latest", }, }), ), @@ -153,7 +167,9 @@ test("dependency on same name as workspace and dist-tag", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchSnapshot("with version"); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot("with version"); expect(lockfile).toMatchNodeModulesAt(packageDir); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), @@ -365,8 +381,6 @@ describe("workspace aliases", async () => { ), ]); - console.log({ packageDir }); - await runBunInstall(env, packageDir); const files = await Promise.all( ["a0", "a1", "a2", "a3", "a4", "a5"].map(name => @@ -658,3 +672,957 @@ test("$npm_package_config_ works in root in subpackage", async () => { expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_package_config_foo $npm_package_config_qux\n`); expect(await new Response(p.stdout).text()).toBe(`tab\n`); }); + +test("adding packages in a subdirectory of a workspace", async () => { + await write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["foo"], + }), + ); + + await mkdir(join(packageDir, "folder1")); + await mkdir(join(packageDir, "foo", "folder2"), { recursive: true }); + await write( + join(packageDir, "foo", "package.json"), + JSON.stringify({ + name: "foo", + }), + ); + + // add package to root workspace from `folder1` + let { stdout, exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: join(packageDir, "folder1"), + stdout: "pipe", + stderr: "inherit", + env, + }); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "root", + workspaces: ["foo"], + dependencies: { + "no-deps": "^2.0.0", + }, + }); + + // add package to foo from `folder2` + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "what-bin"], + cwd: join(packageDir, "foo", "folder2"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed what-bin@1.5.0 with binaries:", + " - what-bin", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "foo", "package.json")).json()).toEqual({ + name: "foo", + dependencies: { + "what-bin": "^1.5.0", + }, + }); + + // now delete node_modules and bun.lockb and install + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "folder1"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@2.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "foo", "folder2"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ what-bin@1.5.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); +}); +test("adding packages in workspaces", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "bar": "workspace:*", + }, + }), + ); + + await mkdir(join(packageDir, "packages", "bar"), { recursive: true }); + await mkdir(join(packageDir, "packages", "boba")); + await mkdir(join(packageDir, "packages", "pkg5")); + + await write(join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar" })); + await write( + join(packageDir, "packages", "boba", "package.json"), + JSON.stringify({ name: "boba", version: "1.0.0", dependencies: { "pkg5": "*" } }), + ); + await write( + join(packageDir, "packages", "pkg5", "package.json"), + JSON.stringify({ + name: "pkg5", + version: "1.2.3", + dependencies: { + "bar": "workspace:*", + }, + }), + ); + + let { stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "inherit", + env, + }); + + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:packages/bar", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "bar"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "boba"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "pkg5"))).toBeTrue(); + + // add a package to the root workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + bar: "workspace:*", + "no-deps": "^2.0.0", + }, + }); + + // add a package in a workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "two-range-deps"], + cwd: join(packageDir, "packages", "boba"), + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed two-range-deps@1.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ + name: "boba", + version: "1.0.0", + dependencies: { + "pkg5": "*", + "two-range-deps": "^1.0.0", + }, + }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@types", + "bar", + "boba", + "no-deps", + "pkg5", + "two-range-deps", + ]); + + // add a dependency to a workspace with the same name as another workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "bar@0.0.7"], + cwd: join(packageDir, "packages", "boba"), + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed bar@0.0.7", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ + name: "boba", + version: "1.0.0", + dependencies: { + "pkg5": "*", + "two-range-deps": "^1.0.0", + "bar": "0.0.7", + }, + }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@types", + "bar", + "boba", + "no-deps", + "pkg5", + "two-range-deps", + ]); + expect(await file(join(packageDir, "node_modules", "boba", "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.7", + description: "not a workspace", + }); +}); +test("it should detect duplicate workspace dependencies", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1" })); + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await write(join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg1" })); + + var { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + expect(err).toContain('Workspace name "pkg1" already exists'); + expect(await exited).toBe(1); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + expect(err).toContain('Workspace name "pkg1" already exists'); + expect(await exited).toBe(1); +}); + +const versions = ["workspace:1.0.0", "workspace:*", "workspace:^1.0.0", "1.0.0", "*"]; + +for (const rootVersion of versions) { + for (const packageVersion of versions) { + test(`it should allow duplicates, root@${rootVersion}, package@${packageVersion}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + dependencies: { + pkg2: rootVersion, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + pkg2: packageVersion, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ name: "pkg2", version: "1.0.0" }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ pkg2@workspace:packages/pkg2`, + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ pkg2@workspace:packages/pkg2`, + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + } +} + +for (const version of versions) { + test(`it should allow listing workspace as dependency of the root package version ${version}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "workspace-1": version, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "workspace-1"), { recursive: true }); + await write( + join(packageDir, "packages", "workspace-1", "package.json"), + JSON.stringify({ + name: "workspace-1", + version: "1.0.0", + }), + ); + // install first from the root, the workspace package + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ workspace-1@workspace:packages/workspace-1`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "workspace-1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + + // install from workspace package then from root + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "workspace-1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + }); +} + +describe("install --filter", () => { + test("does not run root scripts if root is filtered out", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + scripts: { + postinstall: `${bunExe()} root.js`, + }, + }), + ), + write(join(packageDir, "root.js"), `require("fs").writeFileSync("root.txt", "")`), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + scripts: { + postinstall: `${bunExe()} pkg1.js`, + }, + }), + ), + write(join(packageDir, "packages", "pkg1", "pkg1.js"), `require("fs").writeFileSync("pkg1.txt", "")`), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "root.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg1", "pkg1.txt"))).toBeTrue(); + + await rm(join(packageDir, "packages", "pkg1", "pkg1.txt")); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "root"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "root.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1.txt"))).toBeFalse(); + }); + + test("basic", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, false]); + + // add workspace + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, true]); + }); + + test("all but one or two", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg2", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, false]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // exclude the root by name + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!root"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([false, true, true, true]); + }); + + test("matched workspace depends on filtered workspace", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "pkg1": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, true, true]); + }); + + test("filter with a path", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "path-pattern", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + ]); + + async function checkRoot() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([true, false, false]); + } + + async function checkWorkspace() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([false, { name: "no-deps", version: "2.0.0" }, true]); + } + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + }); +}); diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index 87a26b0c7b..a43da3c5eb 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -1,11 +1,10 @@ import { spawn } from "bun"; -import { beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; +import { describe, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { rm, writeFile } from "fs/promises"; -import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isWindows, tmpdirSync, readdirSorted } from "harness"; import { readdirSync } from "node:fs"; import { tmpdir } from "os"; import { join, resolve } from "path"; -import { readdirSorted } from "./dummy.registry"; let x_dir: string; let current_tmpdir: string; @@ -84,7 +83,7 @@ it("should choose the tagged versions instead of the PATH versions when a tag is cwd: x_dir, stdout: "pipe", stdin: "ignore", - stderr: "inherit", + stderr: "ignore", env: { ...env, // BUN_DEBUG_QUIET_LOGS: undefined, @@ -281,7 +280,7 @@ it("should work for github repository with committish", async () => { // cached const cached = spawn({ - cmd: [bunExe(), "x", "github:piuccio/cowsay#HEAD", "hello bun!"], + cmd: [bunExe(), "x", "--no-install", "github:piuccio/cowsay#HEAD", "hello bun!"], cwd: x_dir, stdout: "pipe", stdin: "inherit", @@ -397,3 +396,77 @@ it('should set "npm_config_user_agent" to bun', async () => { expect(out.trim()).toContain(`bun/${Bun.version}`); expect(exited).toBe(0); }); + +/** + * IMPORTANT + * Please only use packages with small unpacked sizes for tests. It helps keep them fast. + */ +describe("bunx --no-install", () => { + const run = (...args: string[]): Promise<[stderr: string, stdout: string, exitCode: number]> => { + const subprocess = spawn({ + cmd: [bunExe(), "x", ...args], + cwd: x_dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + return Promise.all([ + new Response(subprocess.stderr).text(), + new Response(subprocess.stdout).text(), + subprocess.exited, + ] as const); + }; + + it("if the package is not installed, it should fail and print an error message", async () => { + const [err, out, exited] = await run("--no-install", "http-server", "--version"); + + expect(err.trim()).toContain( + "Could not find an existing 'http-server' binary to run.", + ); + expect(out).toHaveLength(0); + expect(exited).toBe(1); + }); + + /* + yes, multiple package tests are neccessary. + 1. there's specialized logic for `bunx tsc` and `bunx typescript` + 2. http-server checks for non-alphanumeric edge cases. Plus it's small + 3. eslint is alphanumeric and extremely common + */ + it.each(["typescript", "http-server", "eslint"])("`bunx --no-install %s` should find cached packages", async pkg => { + // not cached + { + const [err, out, code] = await run(pkg, "--version"); + expect(err).not.toContain("error:"); + expect(out).not.toBeEmpty(); + expect(code).toBe(0); + } + + // cached + { + const [err, out, code] = await run("--no-install", pkg, "--version"); + expect(err).not.toContain("error:"); + expect(out).not.toBeEmpty(); + expect(code).toBe(0); + } + }); + + it("when an exact version match is found, should find cached packages", async () => { + // not cached + { + const [err, out, code] = await run("http-server@14.0.0", "--version"); + expect(err).not.toContain("error:"); + expect(out).not.toBeEmpty(); + expect(code).toBe(0); + } + + // cached + { + const [err, out, code] = await run("--no-install", "http-server@14.0.0", "--version"); + expect(err).not.toContain("error:"); + expect(out).not.toBeEmpty(); + expect(code).toBe(0); + } + }); +}); diff --git a/test/cli/install/dummy.registry.ts b/test/cli/install/dummy.registry.ts index 474511d489..f83f719542 100644 --- a/test/cli/install/dummy.registry.ts +++ b/test/cli/install/dummy.registry.ts @@ -87,12 +87,6 @@ export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numbe return _handler; } -export async function readdirSorted(path: PathLike): Promise { - const results = await readdir(path); - results.sort(); - return results; -} - export function setHandler(newHandler: Handler) { handler = newHandler; } @@ -116,6 +110,10 @@ export function dummyAfterAll() { server.stop(); } +export function getPort() { + return server.port; +} + let packageDirGetter: () => string = () => { return tmpdirSync(); }; diff --git a/test/cli/install/migration/migrate.test.ts b/test/cli/install/migration/migrate.test.ts index ccd379af61..b1527621f4 100644 --- a/test/cli/install/migration/migrate.test.ts +++ b/test/cli/install/migration/migrate.test.ts @@ -78,6 +78,24 @@ test("migrate package with dependency on root package", async () => { expect(fs.existsSync(join(testDir, "node_modules", "test-pkg", "package.json"))).toBeTrue(); }); +test("migrate package with npm dependency that resolves to a git package", async () => { + const testDir = tmpdirSync(); + + fs.cpSync(join(import.meta.dir, "npm-version-to-git-resolution"), testDir, { recursive: true }); + + const { exitCode } = Bun.spawnSync([bunExe(), "install"], { + env: bunEnv, + cwd: testDir, + stdout: "pipe", + }); + + expect(exitCode).toBe(0); + expect(await Bun.file(join(testDir, "node_modules", "jquery", "package.json")).json()).toHaveProperty( + "name", + "install-test", + ); +}); + test("migrate from npm lockfile that is missing `resolved` properties", async () => { const testDir = tmpdirSync(); @@ -95,6 +113,7 @@ test("migrate from npm lockfile that is missing `resolved` properties", async () test("npm lockfile with relative workspaces", async () => { const testDir = tmpdirSync(); + console.log(join(import.meta.dir, "lockfile-with-workspaces"), testDir, { recursive: true }); fs.cpSync(join(import.meta.dir, "lockfile-with-workspaces"), testDir, { recursive: true }); const { exitCode, stderr } = Bun.spawnSync([bunExe(), "install"], { env: bunEnv, diff --git a/test/cli/install/migration/npm-version-to-git-resolution/.gitignore b/test/cli/install/migration/npm-version-to-git-resolution/.gitignore new file mode 100644 index 0000000000..2fe28d55d5 --- /dev/null +++ b/test/cli/install/migration/npm-version-to-git-resolution/.gitignore @@ -0,0 +1 @@ +!package-lock.json \ No newline at end of file diff --git a/test/cli/install/migration/npm-version-to-git-resolution/package-lock.json b/test/cli/install/migration/npm-version-to-git-resolution/package-lock.json new file mode 100644 index 0000000000..dc1ba33747 --- /dev/null +++ b/test/cli/install/migration/npm-version-to-git-resolution/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "lock", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lock", + "version": "0.0.1", + "dependencies": { + "jquery": "3.7.1" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "git+ssh://git@github.com/dylan-conway/install-test.git#596234dab30564f37adae1e5c4d7123bcffce537", + "license": "MIT" + } + } +} diff --git a/test/cli/install/migration/npm-version-to-git-resolution/package.json b/test/cli/install/migration/npm-version-to-git-resolution/package.json new file mode 100644 index 0000000000..fdcf09b244 --- /dev/null +++ b/test/cli/install/migration/npm-version-to-git-resolution/package.json @@ -0,0 +1,7 @@ +{ + "name": "npm-version-to-git-resolution", + "version": "1.1.1", + "dependencies": { + "jquery": "3.7.1" + } +} diff --git a/test/cli/install/registry/missing-directory-bin-1.1.1.tgz b/test/cli/install/missing-directory-bin-1.1.1.tgz similarity index 100% rename from test/cli/install/registry/missing-directory-bin-1.1.1.tgz rename to test/cli/install/missing-directory-bin-1.1.1.tgz diff --git a/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap deleted file mode 100644 index f21ad002a6..0000000000 --- a/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap +++ /dev/null @@ -1,136 +0,0 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP - -exports[`duplicate dependency in optionalDependencies maintains sort order 1`] = ` -"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 -# bun ./bun.lockb --hash: A1A17280329F8383-20d6659d9c0623de-94508CB3B7915517-4b22a59b37f2f4f6 - - -"@types/is-number@>=1.0.0": - version "2.0.0" - resolved "http://localhost:4873/@types/is-number/-/is-number-2.0.0.tgz" - integrity sha512-GEeIxCB+NpM1NrDBqmkYPeU8bI//i+xPzdOY4E1YHet51IcFmz4js6k57m69fLl/cbn7sOR7wj9RNNw53X8AiA== - -a-dep@1.0.1: - version "1.0.1" - resolved "http://localhost:4873/a-dep/-/a-dep-1.0.1.tgz" - integrity sha512-6nmTaPgO2U/uOODqOhbjbnaB4xHuZ+UB7AjKUA3g2dT4WRWeNxgp0dC8Db4swXSnO5/uLLUdFmUJKINNBO/3wg== - -duplicate-optional@1.0.1: - version "1.0.1" - resolved "http://localhost:4873/duplicate-optional/-/duplicate-optional-1.0.1.tgz" - integrity sha512-tL28+yJiTPehPLq7QnOu9jbRBpRgfBkyTVveaJojKcyae2khKuLaPRvsiX5gXv+iNGpYiYcNnV1eDBLXS+L85A== - dependencies: - a-dep "1.0.1" - what-bin "1.0.0" - optionalDependencies: - no-deps "1.0.1" - -no-deps@1.0.1, no-deps@^1.0.0: - version "1.0.1" - resolved "http://localhost:4873/no-deps/-/no-deps-1.0.1.tgz" - integrity sha512-3X6cn4+UJdXJuLPu11v8i/fGLe2PdI6v1yKTELam04lY5esCAFdG/qQts6N6rLrL6g1YRq+MKBAwxbmUQk355A== - -two-range-deps@1.0.0: - version "1.0.0" - resolved "http://localhost:4873/two-range-deps/-/two-range-deps-1.0.0.tgz" - integrity sha512-N+6kPy/GxuMncNz/EKuIrwdoYbh1qmvHDnw1UbM3sQE184kBn+6qAQgtf1wgT9dJnt6X+tWcTzSmfDvtJikVBA== - dependencies: - no-deps "^1.0.0" - "@types/is-number" ">=1.0.0" - -what-bin@1.0.0: - version "1.0.0" - resolved "http://localhost:4873/what-bin/-/what-bin-1.0.0.tgz" - integrity sha512-sa99On1k5aDqCvpni/TQ6rLzYprUWBlb8fNwWOzbjDlM24fRr7FKDOuaBO/Y9WEIcZuzoPkCW5EkBCpflj8REQ== -" -`; - -exports[`outdated normal dep, smaller than column title 1`] = ` -"┌──────────┬─────────┬────────┬────────┐ -│ Package │ Current │ Update │ Latest │ -├──────────┼─────────┼────────┼────────┤ -│ no-deps │ 1.0.0 │ 1.0.0 │ 2.0.0 │ -└──────────┴─────────┴────────┴────────┘ -" -`; - -exports[`outdated normal dep, larger than column title 1`] = ` -"┌───────────────┬────────────────┬────────────────┬────────────────┐ -│ Package │ Current │ Update │ Latest │ -├───────────────┼────────────────┼────────────────┼────────────────┤ -│ prereleases-1 │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ -└───────────────┴────────────────┴────────────────┴────────────────┘ -" -`; - -exports[`outdated dev dep, smaller than column title 1`] = ` -"┌───────────────┬─────────┬────────┬────────┐ -│ Package │ Current │ Update │ Latest │ -├───────────────┼─────────┼────────┼────────┤ -│ no-deps (dev) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ -└───────────────┴─────────┴────────┴────────┘ -" -`; - -exports[`outdated dev dep, larger than column title 1`] = ` -"┌─────────────────────┬────────────────┬────────────────┬────────────────┐ -│ Package │ Current │ Update │ Latest │ -├─────────────────────┼────────────────┼────────────────┼────────────────┤ -│ prereleases-1 (dev) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ -└─────────────────────┴────────────────┴────────────────┴────────────────┘ -" -`; - -exports[`outdated peer dep, smaller than column title 1`] = ` -"┌────────────────┬─────────┬────────┬────────┐ -│ Package │ Current │ Update │ Latest │ -├────────────────┼─────────┼────────┼────────┤ -│ no-deps (peer) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ -└────────────────┴─────────┴────────┴────────┘ -" -`; - -exports[`outdated peer dep, larger than column title 1`] = ` -"┌──────────────────────┬────────────────┬────────────────┬────────────────┐ -│ Package │ Current │ Update │ Latest │ -├──────────────────────┼────────────────┼────────────────┼────────────────┤ -│ prereleases-1 (peer) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ -└──────────────────────┴────────────────┴────────────────┴────────────────┘ -" -`; - -exports[`outdated optional dep, smaller than column title 1`] = ` -"┌────────────────────┬─────────┬────────┬────────┐ -│ Package │ Current │ Update │ Latest │ -├────────────────────┼─────────┼────────┼────────┤ -│ no-deps (optional) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ -└────────────────────┴─────────┴────────┴────────┘ -" -`; - -exports[`outdated optional dep, larger than column title 1`] = ` -"┌──────────────────────────┬────────────────┬────────────────┬────────────────┐ -│ Package │ Current │ Update │ Latest │ -├──────────────────────────┼────────────────┼────────────────┼────────────────┤ -│ prereleases-1 (optional) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ -└──────────────────────────┴────────────────┴────────────────┴────────────────┘ -" -`; - -exports[`outdated NO_COLOR works 1`] = ` -"|--------------------------------------| -| Package | Current | Update | Latest | -|----------|---------|--------|--------| -| a-dep | 1.0.1 | 1.0.1 | 1.0.10 | -|--------------------------------------| -" -`; - -exports[`auto-install symlinks (and junctions) are created correctly in the install cache 1`] = ` -"{ - name: "is-number", - version: "2.0.0", -} -" -`; diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts deleted file mode 100644 index 6475dbfe64..0000000000 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ /dev/null @@ -1,12133 +0,0 @@ -import { file, spawn, write } from "bun"; -import { install_test_helpers } from "bun:internal-for-testing"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, setDefaultTimeout, test } from "bun:test"; -import { ChildProcess, fork } from "child_process"; -import { copyFileSync, mkdirSync } from "fs"; -import { cp, exists, mkdir, readlink, rm, writeFile } from "fs/promises"; -import { - assertManifestsPopulated, - bunExe, - bunEnv as env, - isLinux, - isWindows, - mergeWindowEnvs, - randomPort, - runBunInstall, - runBunUpdate, - pack, - tempDirWithFiles, - tmpdirSync, - toBeValidBin, - toHaveBins, - toMatchNodeModulesAt, - writeShebangScript, - stderrForInstall, - tls, - isFlaky, - isMacOS, -} from "harness"; -import { join, resolve, sep } from "path"; -import { readdirSorted } from "../dummy.registry"; -const { parseLockfile } = install_test_helpers; -const { iniInternals } = require("bun:internal-for-testing"); -const { loadNpmrc } = iniInternals; - -expect.extend({ - toBeValidBin, - toHaveBins, - toMatchNodeModulesAt, -}); - -var verdaccioServer: ChildProcess; -var port: number = randomPort(); -var packageDir: string; -/** packageJson = join(packageDir, "package.json"); */ -var packageJson: string; - -let users: Record = {}; - -beforeAll(async () => { - console.log("STARTING VERDACCIO"); - setDefaultTimeout(1000 * 60 * 5); - verdaccioServer = fork( - require.resolve("verdaccio/bin/verdaccio"), - ["-c", join(import.meta.dir, "verdaccio.yaml"), "-l", `${port}`], - { - silent: true, - // Prefer using a release build of Bun since it's faster - execPath: Bun.which("bun") || bunExe(), - }, - ); - - verdaccioServer.stderr?.on("data", data => { - console.error(`Error: ${data}`); - }); - - verdaccioServer.on("error", error => { - console.error(`Failed to start child process: ${error}`); - }); - - verdaccioServer.on("exit", (code, signal) => { - if (code !== 0) { - console.error(`Child process exited with code ${code} and signal ${signal}`); - } else { - console.log("Child process exited successfully"); - } - }); - - await new Promise(done => { - verdaccioServer.on("message", (msg: { verdaccio_started: boolean }) => { - if (msg.verdaccio_started) { - console.log("Verdaccio started"); - done(); - } - }); - }); -}); - -afterAll(async () => { - await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); - if (verdaccioServer) verdaccioServer.kill(); -}); - -beforeEach(async () => { - packageDir = tmpdirSync(); - packageJson = join(packageDir, "package.json"); - await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); - await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); - users = {}; - env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); - env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); - await writeFile( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = "${join(packageDir, ".bun-cache")}" -registry = "http://localhost:${port}/" -`, - ); -}); - -function registryUrl() { - return `http://localhost:${port}/`; -} - -/** - * Returns auth token - */ -async function generateRegistryUser(username: string, password: string): Promise { - if (users[username]) { - throw new Error("that user already exists"); - } else users[username] = password; - - const url = `http://localhost:${port}/-/user/org.couchdb.user:${username}`; - const user = { - name: username, - password: password, - email: `${username}@example.com`, - }; - - const response = await fetch(url, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(user), - }); - - if (response.ok) { - const data = await response.json(); - return data.token; - } else { - throw new Error("Failed to create user:", response.statusText); - } -} - -describe("npmrc", async () => { - const isBase64Encoded = (opt: string) => opt === "_auth" || opt === "_password"; - - it("works with empty file", async () => { - console.log("package dir", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ``; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: {}, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); - }); - - it("sets default registry", async () => { - console.log("package dir", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` -registry = http://localhost:${port}/ -`; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); - }); - - it("sets scoped registry", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` - @types:registry=http://localhost:${port}/ - `; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "@types/no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); - }); - - it("works with home config", async () => { - console.log("package dir", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const homeDir = `${packageDir}/home_dir`; - await Bun.$`mkdir -p ${homeDir}`; - console.log("home dir", homeDir); - - const ini = /* ini */ ` - registry=http://localhost:${port}/ - `; - - await Bun.$`echo ${ini} > ${homeDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install` - .env({ - ...process.env, - XDG_CONFIG_HOME: `${homeDir}`, - }) - .cwd(packageDir) - .throws(true); - }); - - it("works with two configs", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - console.log("package dir", packageDir); - const packageIni = /* ini */ ` - @types:registry=http://localhost:${port}/ - `; - await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; - - const homeDir = `${packageDir}/home_dir`; - await Bun.$`mkdir -p ${homeDir}`; - console.log("home dir", homeDir); - const homeIni = /* ini */ ` - registry = http://localhost:${port}/ - `; - await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; - - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "@types/no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install` - .env({ - ...process.env, - XDG_CONFIG_HOME: `${homeDir}`, - }) - .cwd(packageDir) - .throws(true); - }); - - it("package config overrides home config", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - console.log("package dir", packageDir); - const packageIni = /* ini */ ` - @types:registry=http://localhost:${port}/ - `; - await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; - - const homeDir = `${packageDir}/home_dir`; - await Bun.$`mkdir -p ${homeDir}`; - console.log("home dir", homeDir); - const homeIni = /* ini */ "@types:registry=https://registry.npmjs.org/"; - await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; - - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "@types/no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install` - .env({ - ...process.env, - XDG_CONFIG_HOME: `${homeDir}`, - }) - .cwd(packageDir) - .throws(true); - }); - - it("default registry from env variable", async () => { - const ini = /* ini */ ` -registry=\${LOL} - `; - - const result = loadNpmrc(ini, { LOL: `http://localhost:${port}/` }); - - expect(result.default_registry_url).toBe(`http://localhost:${port}/`); - }); - - it("default registry from env variable 2", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` -registry=http://localhost:\${PORT}/ - `; - - const result = loadNpmrc(ini, { ...env, PORT: port }); - - expect(result.default_registry_url).toEqual(`http://localhost:${port}/`); - }); - - async function makeTest( - options: [option: string, value: string][], - check: (result: { - default_registry_url: string; - default_registry_token: string; - default_registry_username: string; - default_registry_password: string; - }) => void, - ) { - const optionName = await Promise.all(options.map(async ([name, val]) => `${name} = ${val}`)); - test(optionName.join(" "), async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const iniInner = await Promise.all( - options.map(async ([option, value]) => { - let finalValue = value; - finalValue = isBase64Encoded(option) ? Buffer.from(finalValue).toString("base64") : finalValue; - return `//registry.npmjs.org/:${option}=${finalValue}`; - }), - ); - - const ini = /* ini */ ` -${iniInner.join("\n")} -`; - - await Bun.$`echo ${JSON.stringify({ - name: "hello", - main: "index.js", - version: "1.0.0", - dependencies: { - "is-even": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - - const result = loadNpmrc(ini); - - check(result); - }); - } - - await makeTest([["_authToken", "skibidi"]], result => { - expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); - expect(result.default_registry_token).toEqual("skibidi"); - }); - - await makeTest( - [ - ["username", "zorp"], - ["_password", "skibidi"], - ], - result => { - expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); - expect(result.default_registry_username).toEqual("zorp"); - expect(result.default_registry_password).toEqual("skibidi"); - }, - ); - - it("authentication works", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` -registry = http://localhost:${port}/ -//localhost:${port}/:_authToken=${await generateRegistryUser("bilbo_swaggins", "verysecure")} -`; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "hi", - main: "index.js", - version: "1.0.0", - dependencies: { - "@needs-auth/test-pkg": "1.0.0", - }, - "publishConfig": { - "registry": `http://localhost:${port}`, - }, - })} > package.json`.cwd(packageDir); - - await Bun.$`${bunExe()} install`.env(env).cwd(packageDir).throws(true); - }); - - type EnvMap = - | Omit< - { - [key: string]: string; - }, - "dotEnv" - > - | { dotEnv?: Record }; - - function registryConfigOptionTest( - name: string, - _opts: Record | (() => Promise>), - _env?: EnvMap | (() => Promise), - check?: (stdout: string, stderr: string) => void, - ) { - it(`sets scoped registry option: ${name}`, async () => { - console.log("PACKAGE DIR", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const { dotEnv, ...restOfEnv } = _env - ? typeof _env === "function" - ? await _env() - : _env - : { dotEnv: undefined }; - const opts = _opts ? (typeof _opts === "function" ? await _opts() : _opts) : {}; - const dotEnvInner = dotEnv - ? Object.entries(dotEnv) - .map(([k, v]) => `${k}=${k.includes("SECRET_") ? Buffer.from(v).toString("base64") : v}`) - .join("\n") - : ""; - - const ini = ` -registry = http://localhost:${port}/ -${Object.keys(opts) - .map( - k => - `//localhost:${port}/:${k}=${isBase64Encoded(k) && !opts[k].includes("${") ? Buffer.from(opts[k]).toString("base64") : opts[k]}`, - ) - .join("\n")} -`; - - if (dotEnvInner.length > 0) await Bun.$`echo ${dotEnvInner} > ${packageDir}/.env`; - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "hi", - main: "index.js", - version: "1.0.0", - dependencies: { - "@needs-auth/test-pkg": "1.0.0", - }, - "publishConfig": { - "registry": `http://localhost:${port}`, - }, - })} > package.json`.cwd(packageDir); - - const { stdout, stderr } = await Bun.$`${bunExe()} install` - .env({ ...env, ...restOfEnv }) - .cwd(packageDir) - .throws(check === undefined); - - if (check) check(stdout.toString(), stderr.toString()); - }); - } - - registryConfigOptionTest("_authToken", async () => ({ - "_authToken": await generateRegistryUser("bilbo_baggins", "verysecure"), - })); - registryConfigOptionTest( - "_authToken with env variable value", - async () => ({ _authToken: "${SUPER_SECRET_TOKEN}" }), - async () => ({ SUPER_SECRET_TOKEN: await generateRegistryUser("bilbo_baggins420", "verysecure") }), - ); - registryConfigOptionTest("username and password", async () => { - await generateRegistryUser("gandalf429", "verysecure"); - return { username: "gandalf429", _password: "verysecure" }; - }); - registryConfigOptionTest( - "username and password with env variable password", - async () => { - await generateRegistryUser("gandalf422", "verysecure"); - return { username: "gandalf422", _password: "${SUPER_SECRET_PASSWORD}" }; - }, - { - SUPER_SECRET_PASSWORD: Buffer.from("verysecure").toString("base64"), - }, - ); - registryConfigOptionTest( - "username and password with .env variable password", - async () => { - await generateRegistryUser("gandalf421", "verysecure"); - return { username: "gandalf421", _password: "${SUPER_SECRET_PASSWORD}" }; - }, - { - dotEnv: { SUPER_SECRET_PASSWORD: "verysecure" }, - }, - ); - - registryConfigOptionTest("_auth", async () => { - await generateRegistryUser("linus", "verysecure"); - const _auth = "linus:verysecure"; - return { _auth }; - }); - - registryConfigOptionTest( - "_auth from .env variable", - async () => { - await generateRegistryUser("zack", "verysecure"); - return { _auth: "${SECRET_AUTH}" }; - }, - { - dotEnv: { SECRET_AUTH: "zack:verysecure" }, - }, - ); - - registryConfigOptionTest( - "_auth from .env variable with no value", - async () => { - await generateRegistryUser("zack420", "verysecure"); - return { _auth: "${SECRET_AUTH}" }; - }, - { - dotEnv: { SECRET_AUTH: "" }, - }, - (stdout: string, stderr: string) => { - expect(stderr).toContain("received an empty string"); - }, - ); -}); - -describe("auto-install", () => { - test("symlinks (and junctions) are created correctly in the install cache", async () => { - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "--print", "require('is-number')"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: { - ...env, - BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache"), - }, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out).toMatchSnapshot(); - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - - expect(resolve(await readlink(join(packageDir, ".bun-cache", "is-number", "2.0.0@@localhost@@@1")))).toBe( - join(packageDir, ".bun-cache", "is-number@2.0.0@@localhost@@@1"), - ); - }); -}); - -describe("certificate authority", () => { - const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { - return async function (req: Request) { - if (req.url.includes("no-deps")) { - return new Response(Bun.file(join(import.meta.dir, "packages", "no-deps", "no-deps-1.0.0.tgz"))); - } - return new Response("OK", { status: 200 }); - }; - }; - test("valid --cafile", async () => { - using server = Bun.serve({ - port: 0, - fetch: mockRegistryFetch(), - ...tls, - }); - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.1.1", - dependencies: { - "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, - }, - }), - ), - write( - join(packageDir, "bunfig.toml"), - ` - [install] - cache = false - registry = "https://localhost:${server.port}/"`, - ), - write(join(packageDir, "cafile"), tls.cert), - ]); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--cafile", "cafile"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - const out = await Bun.readableStreamToText(stdout); - expect(out).toContain("+ no-deps@"); - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("ConnectionClosed"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); - expect(await exited).toBe(0); - }); - test("valid --ca", async () => { - using server = Bun.serve({ - port: 0, - fetch: mockRegistryFetch(), - ...tls, - }); - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.1.1", - dependencies: { - "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, - }, - }), - ), - write( - join(packageDir, "bunfig.toml"), - ` - [install] - cache = false - registry = "https://localhost:${server.port}/"`, - ), - ]); - - // first without ca, should fail - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - let out = await Bun.readableStreamToText(stdout); - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); - expect(await exited).toBe(1); - - // now with a valid ca - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--ca", tls.cert], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("+ no-deps@"); - err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - }); - test(`non-existent --cafile`, async () => { - await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--cafile", "does-not-exist"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("no-deps"); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); - expect(await exited).toBe(1); - }); - - test("non-existent --cafile (absolute path)", async () => { - await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--cafile", "/does/not/exist"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("no-deps"); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain(`HTTPThread: could not find CA file: '/does/not/exist'`); - expect(await exited).toBe(1); - }); - - test("cafile from bunfig does not exist", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps": "1.1.1", - }, - }), - ), - write( - join(packageDir, "bunfig.toml"), - ` - [install] - cache = false - registry = "http://localhost:${port}/" - cafile = "does-not-exist"`, - ), - ]); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("no-deps"); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); - expect(await exited).toBe(1); - }); - test("invalid cafile", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps": "1.1.1", - }, - }), - ), - write( - join(packageDir, "invalid-cafile"), - `-----BEGIN CERTIFICATE----- -jlwkjekfjwlejlgldjfljlkwjef ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -ljelkjwelkgjw;lekj;lkejflkj ------END CERTIFICATE-----`, - ), - ]); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--cafile", join(packageDir, "invalid-cafile")], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("no-deps"); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain(`HTTPThread: invalid CA file: '${join(packageDir, "invalid-cafile")}'`); - expect(await exited).toBe(1); - }); - test("invalid --ca", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps": "1.1.1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--ca", "not-valid"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("no-deps"); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("HTTPThread: the CA is invalid"); - expect(await exited).toBe(1); - }); -}); - -export async function publish( - env: any, - cwd: string, - ...args: string[] -): Promise<{ out: string; err: string; exitCode: number }> { - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "publish", ...args], - cwd, - stdout: "pipe", - stderr: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - const exitCode = await exited; - return { out, err, exitCode }; -} - -async function authBunfig(user: string) { - const authToken = await generateRegistryUser(user, user); - return ` - [install] - cache = false - registry = { url = "http://localhost:${port}/", token = "${authToken}" } - `; -} - -describe("whoami", async () => { - test("can get username", async () => { - const bunfig = await authBunfig("whoami"); - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "whoami-pkg", - version: "1.1.1", - }), - ), - write(join(packageDir, "bunfig.toml"), bunfig), - ]); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "whoami"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out).toBe("whoami\n"); - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - }); - test("username from .npmrc", async () => { - // It should report the username from npmrc, even without an account - const bunfig = ` - [install] - cache = false - registry = "http://localhost:${port}/"`; - const npmrc = ` - //localhost:${port}/:username=whoami-npmrc - //localhost:${port}/:_password=123456 - `; - await Promise.all([ - write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), - write(join(packageDir, "bunfig.toml"), bunfig), - write(join(packageDir, ".npmrc"), npmrc), - ]); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "whoami"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out).toBe("whoami-npmrc\n"); - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - }); - test("only .npmrc", async () => { - const token = await generateRegistryUser("whoami-npmrc", "whoami-npmrc"); - const npmrc = ` - //localhost:${port}/:_authToken=${token} - registry=http://localhost:${port}/`; - await Promise.all([ - write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), - write(join(packageDir, ".npmrc"), npmrc), - ]); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "whoami"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const out = await Bun.readableStreamToText(stdout); - expect(out).toBe("whoami-npmrc\n"); - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - }); - test("not logged in", async () => { - await write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "whoami"], - cwd: packageDir, - env, - stdout: "pipe", - stderr: "pipe", - }); - const out = await Bun.readableStreamToText(stdout); - expect(out).toBeEmpty(); - const err = await Bun.readableStreamToText(stderr); - expect(err).toBe("error: missing authentication (run `bunx npm login`)\n"); - expect(await exited).toBe(1); - }); - test("invalid token", async () => { - // create the user and provide an invalid token - const token = await generateRegistryUser("invalid-token", "invalid-token"); - const bunfig = ` - [install] - cache = false - registry = { url = "http://localhost:${port}/", token = "1234567" }`; - await Promise.all([ - write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), - write(join(packageDir, "bunfig.toml"), bunfig), - ]); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "whoami"], - cwd: packageDir, - env, - stdout: "pipe", - stderr: "pipe", - }); - const out = await Bun.readableStreamToText(stdout); - expect(out).toBeEmpty(); - const err = await Bun.readableStreamToText(stderr); - expect(err).toBe(`error: failed to authenticate with registry 'http://localhost:${port}/'\n`); - expect(await exited).toBe(1); - }); -}); - -describe("publish", async () => { - describe("otp", async () => { - const mockRegistryFetch = function (opts: { - token: string; - setAuthHeader?: boolean; - otpFail?: boolean; - npmNotice?: boolean; - xLocalCache?: boolean; - expectedCI?: string; - }) { - return async function (req: Request) { - const { token, setAuthHeader = true, otpFail = false, npmNotice = false, xLocalCache = false } = opts; - if (req.url.includes("otp-pkg")) { - if (opts.expectedCI) { - expect(req.headers.get("user-agent")).toContain("ci/" + opts.expectedCI); - } - if (req.headers.get("npm-otp") === token) { - if (otpFail) { - return new Response( - JSON.stringify({ - error: "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.", - }), - { status: 401 }, - ); - } else { - return new Response("OK", { status: 200 }); - } - } else { - const headers = new Headers(); - if (setAuthHeader) headers.set("www-authenticate", "OTP"); - - // `bun publish` won't request a url from a message in the npm-notice header, but we - // can test that it's displayed - if (npmNotice) headers.set("npm-notice", `visit http://localhost:${this.port}/auth to login`); - - // npm-notice will be ignored - if (xLocalCache) headers.set("x-local-cache", "true"); - - return new Response( - JSON.stringify({ - // this isn't accurate, but we just want to check that finding this string works - mock: setAuthHeader ? "" : "one-time password", - - authUrl: `http://localhost:${this.port}/auth`, - doneUrl: `http://localhost:${this.port}/done`, - }), - { - status: 401, - headers, - }, - ); - } - } else if (req.url.endsWith("auth")) { - expect.unreachable("url given to user, bun publish should not request"); - } else if (req.url.endsWith("done")) { - // send a fake response saying the user has authenticated successfully with the auth url - return new Response(JSON.stringify({ token: token }), { status: 200 }); - } - - expect.unreachable("unexpected url"); - }; - }; - - for (const setAuthHeader of [true, false]) { - test("mock web login" + (setAuthHeader ? "" : " (without auth header)"), async () => { - const token = await generateRegistryUser("otp" + (setAuthHeader ? "" : "noheader"), "otp"); - - using mockRegistry = Bun.serve({ - port: 0, - fetch: mockRegistryFetch({ token }), - }); - - const bunfig = ` - [install] - cache = false - registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; - await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-1"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "otp-pkg-1", - version: "2.2.2", - dependencies: { - "otp-pkg-1": "2.2.2", - }, - }), - ), - ]); - - const { out, err, exitCode } = await publish(env, packageDir); - expect(exitCode).toBe(0); - }); - } - - test("otp failure", async () => { - const token = await generateRegistryUser("otp-fail", "otp"); - using mockRegistry = Bun.serve({ - port: 0, - fetch: mockRegistryFetch({ token, otpFail: true }), - }); - - const bunfig = ` - [install] - cache = false - registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; - - await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-2"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "otp-pkg-2", - version: "1.1.1", - dependencies: { - "otp-pkg-2": "1.1.1", - }, - }), - ), - ]); - - const { out, err, exitCode } = await publish(env, packageDir); - expect(exitCode).toBe(1); - expect(err).toContain(" - Received invalid OTP"); - }); - - for (const shouldIgnoreNotice of [false, true]) { - test(`npm-notice with login url${shouldIgnoreNotice ? " (ignored)" : ""}`, async () => { - // Situation: user has 2FA enabled account with faceid sign-in. - // They run `bun publish` with --auth-type=legacy, prompting them - // to enter their OTP. Because they have faceid sign-in, they don't - // have a code to enter, so npm sends a message in the npm-notice - // header with a url for logging in. - const token = await generateRegistryUser(`otp-notice${shouldIgnoreNotice ? "-ignore" : ""}`, "otp"); - using mockRegistry = Bun.serve({ - port: 0, - fetch: mockRegistryFetch({ token, npmNotice: true, xLocalCache: shouldIgnoreNotice }), - }); - - const bunfig = ` - [install] - cache = false - registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; - - await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-3"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "otp-pkg-3", - version: "3.3.3", - dependencies: { - "otp-pkg-3": "3.3.3", - }, - }), - ), - ]); - - const { out, err, exitCode } = await publish(env, packageDir); - expect(exitCode).toBe(0); - if (shouldIgnoreNotice) { - expect(err).not.toContain(`note: visit http://localhost:${mockRegistry.port}/auth to login`); - } else { - expect(err).toContain(`note: visit http://localhost:${mockRegistry.port}/auth to login`); - } - }); - } - - const fakeCIEnvs = [ - { ci: "expo-application-services", envs: { EAS_BUILD: "hi" } }, - { ci: "codemagic", envs: { CM_BUILD_ID: "hi" } }, - { ci: "vercel", envs: { "NOW_BUILDER": "hi" } }, - ]; - for (const envInfo of fakeCIEnvs) { - test(`CI user agent name: ${envInfo.ci}`, async () => { - const token = await generateRegistryUser(`otp-${envInfo.ci}`, "otp"); - using mockRegistry = Bun.serve({ - port: 0, - fetch: mockRegistryFetch({ token, expectedCI: envInfo.ci }), - }); - - const bunfig = ` - [install] - cache = false - registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; - - await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-4"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "otp-pkg-4", - version: "4.4.4", - dependencies: { - "otp-pkg-4": "4.4.4", - }, - }), - ), - ]); - - const { out, err, exitCode } = await publish( - { ...env, ...envInfo.envs, ...{ BUILDKITE: undefined, GITHUB_ACTIONS: undefined } }, - packageDir, - ); - expect(exitCode).toBe(0); - }); - } - }); - - test("can publish a package then install it", async () => { - const bunfig = await authBunfig("basic"); - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-1"), { recursive: true, force: true }), - write( - packageJson, - JSON.stringify({ - name: "publish-pkg-1", - version: "1.1.1", - dependencies: { - "publish-pkg-1": "1.1.1", - }, - }), - ), - write(join(packageDir, "bunfig.toml"), bunfig), - ]); - - const { out, err, exitCode } = await publish(env, packageDir); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(exitCode).toBe(0); - - await runBunInstall(env, packageDir); - expect(await exists(join(packageDir, "node_modules", "publish-pkg-1", "package.json"))).toBeTrue(); - }); - test("can publish from a tarball", async () => { - const bunfig = await authBunfig("tarball"); - const json = { - name: "publish-pkg-2", - version: "2.2.2", - dependencies: { - "publish-pkg-2": "2.2.2", - }, - }; - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-2"), { recursive: true, force: true }), - write(packageJson, JSON.stringify(json)), - write(join(packageDir, "bunfig.toml"), bunfig), - ]); - - await pack(packageDir, env); - - let { out, err, exitCode } = await publish(env, packageDir, "./publish-pkg-2-2.2.2.tgz"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(exitCode).toBe(0); - - await runBunInstall(env, packageDir); - expect(await exists(join(packageDir, "node_modules", "publish-pkg-2", "package.json"))).toBeTrue(); - - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-2"), { recursive: true, force: true }), - rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }), - rm(join(packageDir, "node_modules"), { recursive: true, force: true }), - ]); - - // now with an absoute path - ({ out, err, exitCode } = await publish(env, packageDir, join(packageDir, "publish-pkg-2-2.2.2.tgz"))); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(exitCode).toBe(0); - - await runBunInstall(env, packageDir); - expect(await file(join(packageDir, "node_modules", "publish-pkg-2", "package.json")).json()).toEqual(json); - }); - - for (const info of [ - { user: "bin1", bin: "bin1.js" }, - { user: "bin2", bin: { bin1: "bin1.js", bin2: "bin2.js" } }, - { user: "bin3", directories: { bin: "bins" } }, - ]) { - test(`can publish and install binaries with ${JSON.stringify(info)}`, async () => { - const publishDir = tmpdirSync(); - const bunfig = await authBunfig("binaries-" + info.user); - console.log({ packageDir, publishDir }); - - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-bins"), { recursive: true, force: true }), - write( - join(publishDir, "package.json"), - JSON.stringify({ - name: "publish-pkg-bins", - version: "1.1.1", - ...info, - }), - ), - write(join(publishDir, "bunfig.toml"), bunfig), - write(join(publishDir, "bin1.js"), `#!/usr/bin/env bun\nconsole.log("bin1!")`), - write(join(publishDir, "bin2.js"), `#!/usr/bin/env bun\nconsole.log("bin2!")`), - write(join(publishDir, "bins", "bin3.js"), `#!/usr/bin/env bun\nconsole.log("bin3!")`), - write(join(publishDir, "bins", "moredir", "bin4.js"), `#!/usr/bin/env bun\nconsole.log("bin4!")`), - - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "publish-pkg-bins": "1.1.1", - }, - }), - ), - ]); - - const { out, err, exitCode } = await publish(env, publishDir); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(out).toContain("+ publish-pkg-bins@1.1.1"); - expect(exitCode).toBe(0); - - await runBunInstall(env, packageDir); - - const results = await Promise.all([ - exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin1.bunx" : "bin1")), - exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin2.bunx" : "bin2")), - exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin3.js.bunx" : "bin3.js")), - exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin4.js.bunx" : "bin4.js")), - exists(join(packageDir, "node_modules", ".bin", isWindows ? "moredir" : "moredir/bin4.js")), - exists(join(packageDir, "node_modules", ".bin", isWindows ? "publish-pkg-bins.bunx" : "publish-pkg-bins")), - ]); - - switch (info.user) { - case "bin1": { - expect(results).toEqual([false, false, false, false, false, true]); - break; - } - case "bin2": { - expect(results).toEqual([true, true, false, false, false, false]); - break; - } - case "bin3": { - expect(results).toEqual([false, false, true, true, !isWindows, false]); - break; - } - } - }); - } - - test("dependencies are installed", async () => { - const publishDir = tmpdirSync(); - const bunfig = await authBunfig("manydeps"); - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-deps"), { recursive: true, force: true }), - write( - join(publishDir, "package.json"), - JSON.stringify( - { - name: "publish-pkg-deps", - version: "1.1.1", - dependencies: { - "no-deps": "1.0.0", - }, - peerDependencies: { - "a-dep": "1.0.1", - }, - optionalDependencies: { - "basic-1": "1.0.0", - }, - }, - null, - 2, - ), - ), - write(join(publishDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "publish-pkg-deps": "1.1.1", - }, - }), - ), - ]); - - let { out, err, exitCode } = await publish(env, publishDir); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(out).toContain("+ publish-pkg-deps@1.1.1"); - expect(exitCode).toBe(0); - - await runBunInstall(env, packageDir); - - const results = await Promise.all([ - exists(join(packageDir, "node_modules", "no-deps", "package.json")), - exists(join(packageDir, "node_modules", "a-dep", "package.json")), - exists(join(packageDir, "node_modules", "basic-1", "package.json")), - ]); - - expect(results).toEqual([true, true, true]); - }); - - test("can publish workspace package", async () => { - const bunfig = await authBunfig("workspace"); - const pkgJson = { - name: "publish-pkg-3", - version: "3.3.3", - dependencies: { - "publish-pkg-3": "3.3.3", - }, - }; - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-3"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "root", - workspaces: ["packages/*"], - }), - ), - write(join(packageDir, "packages", "publish-pkg-3", "package.json"), JSON.stringify(pkgJson)), - ]); - - await publish(env, join(packageDir, "packages", "publish-pkg-3")); - - await write(packageJson, JSON.stringify({ name: "root", "dependencies": { "publish-pkg-3": "3.3.3" } })); - - await runBunInstall(env, packageDir); - - expect(await file(join(packageDir, "node_modules", "publish-pkg-3", "package.json")).json()).toEqual(pkgJson); - }); - - describe("--dry-run", async () => { - test("does not publish", async () => { - const bunfig = await authBunfig("dryrun"); - await Promise.all([ - rm(join(import.meta.dir, "packages", "dry-run-1"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "dry-run-1", - version: "1.1.1", - dependencies: { - "dry-run-1": "1.1.1", - }, - }), - ), - ]); - - const { out, err, exitCode } = await publish(env, packageDir, "--dry-run"); - expect(exitCode).toBe(0); - - expect(await exists(join(import.meta.dir, "packages", "dry-run-1"))).toBeFalse(); - }); - test("does not publish from tarball path", async () => { - const bunfig = await authBunfig("dryruntarball"); - await Promise.all([ - rm(join(import.meta.dir, "packages", "dry-run-2"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "dry-run-2", - version: "2.2.2", - dependencies: { - "dry-run-2": "2.2.2", - }, - }), - ), - ]); - - await pack(packageDir, env); - - const { out, err, exitCode } = await publish(env, packageDir, "./dry-run-2-2.2.2.tgz", "--dry-run"); - expect(exitCode).toBe(0); - - expect(await exists(join(import.meta.dir, "packages", "dry-run-2"))).toBeFalse(); - }); - }); - - describe("lifecycle scripts", async () => { - const script = `const fs = require("fs"); - fs.writeFileSync(process.argv[2] + ".txt", \` -prepublishOnly: \${fs.existsSync("prepublishOnly.txt")} -publish: \${fs.existsSync("publish.txt")} -postpublish: \${fs.existsSync("postpublish.txt")} -prepack: \${fs.existsSync("prepack.txt")} -prepare: \${fs.existsSync("prepare.txt")} -postpack: \${fs.existsSync("postpack.txt")}\`)`; - const json = { - name: "publish-pkg-4", - version: "4.4.4", - scripts: { - // should happen in this order - "prepublishOnly": `${bunExe()} script.js prepublishOnly`, - "prepack": `${bunExe()} script.js prepack`, - "prepare": `${bunExe()} script.js prepare`, - "postpack": `${bunExe()} script.js postpack`, - "publish": `${bunExe()} script.js publish`, - "postpublish": `${bunExe()} script.js postpublish`, - }, - dependencies: { - "publish-pkg-4": "4.4.4", - }, - }; - - for (const arg of ["", "--dry-run"]) { - test(`should run in order${arg ? " (--dry-run)" : ""}`, async () => { - const bunfig = await authBunfig("lifecycle" + (arg ? "dry" : "")); - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-4"), { recursive: true, force: true }), - write(packageJson, JSON.stringify(json)), - write(join(packageDir, "script.js"), script), - write(join(packageDir, "bunfig.toml"), bunfig), - ]); - - const { out, err, exitCode } = await publish(env, packageDir, arg); - expect(exitCode).toBe(0); - - const results = await Promise.all([ - file(join(packageDir, "prepublishOnly.txt")).text(), - file(join(packageDir, "prepack.txt")).text(), - file(join(packageDir, "prepare.txt")).text(), - file(join(packageDir, "postpack.txt")).text(), - file(join(packageDir, "publish.txt")).text(), - file(join(packageDir, "postpublish.txt")).text(), - ]); - - expect(results).toEqual([ - "\nprepublishOnly: false\npublish: false\npostpublish: false\nprepack: false\nprepare: false\npostpack: false", - "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: false\nprepare: false\npostpack: false", - "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: false\npostpack: false", - "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: true\npostpack: false", - "\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: true\npostpack: true", - "\nprepublishOnly: true\npublish: true\npostpublish: false\nprepack: true\nprepare: true\npostpack: true", - ]); - }); - } - - test("--ignore-scripts", async () => { - const bunfig = await authBunfig("ignorescripts"); - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-5"), { recursive: true, force: true }), - write(packageJson, JSON.stringify(json)), - write(join(packageDir, "script.js"), script), - write(join(packageDir, "bunfig.toml"), bunfig), - ]); - - const { out, err, exitCode } = await publish(env, packageDir, "--ignore-scripts"); - expect(exitCode).toBe(0); - - const results = await Promise.all([ - exists(join(packageDir, "prepublishOnly.txt")), - exists(join(packageDir, "prepack.txt")), - exists(join(packageDir, "prepare.txt")), - exists(join(packageDir, "postpack.txt")), - exists(join(packageDir, "publish.txt")), - exists(join(packageDir, "postpublish.txt")), - ]); - - expect(results).toEqual([false, false, false, false, false, false]); - }); - }); - - test("attempting to publish a private package should fail", async () => { - const bunfig = await authBunfig("privatepackage"); - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-6"), { recursive: true, force: true }), - write( - packageJson, - JSON.stringify({ - name: "publish-pkg-6", - version: "6.6.6", - private: true, - dependencies: { - "publish-pkg-6": "6.6.6", - }, - }), - ), - write(join(packageDir, "bunfig.toml"), bunfig), - ]); - - // should fail - let { out, err, exitCode } = await publish(env, packageDir); - expect(exitCode).toBe(1); - expect(err).toContain("error: attempted to publish a private package"); - expect(await exists(join(import.meta.dir, "packages", "publish-pkg-6-6.6.6.tgz"))).toBeFalse(); - - // try tarball - await pack(packageDir, env); - ({ out, err, exitCode } = await publish(env, packageDir, "./publish-pkg-6-6.6.6.tgz")); - expect(exitCode).toBe(1); - expect(err).toContain("error: attempted to publish a private package"); - expect(await exists(join(packageDir, "publish-pkg-6-6.6.6.tgz"))).toBeTrue(); - }); - - describe("access", async () => { - test("--access", async () => { - const bunfig = await authBunfig("accessflag"); - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-7"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write( - packageJson, - JSON.stringify({ - name: "publish-pkg-7", - version: "7.7.7", - }), - ), - ]); - - // should fail - let { out, err, exitCode } = await publish(env, packageDir, "--access", "restricted"); - expect(exitCode).toBe(1); - expect(err).toContain("error: unable to restrict access to unscoped package"); - - ({ out, err, exitCode } = await publish(env, packageDir, "--access", "public")); - expect(exitCode).toBe(0); - - expect(await exists(join(import.meta.dir, "packages", "publish-pkg-7"))).toBeTrue(); - }); - - for (const access of ["restricted", "public"]) { - test(`access ${access}`, async () => { - const bunfig = await authBunfig("access" + access); - - const pkgJson = { - name: "@secret/publish-pkg-8", - version: "8.8.8", - dependencies: { - "@secret/publish-pkg-8": "8.8.8", - }, - publishConfig: { - access, - }, - }; - - await Promise.all([ - rm(join(import.meta.dir, "packages", "@secret", "publish-pkg-8"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write(packageJson, JSON.stringify(pkgJson)), - ]); - - let { out, err, exitCode } = await publish(env, packageDir); - expect(exitCode).toBe(0); - - await runBunInstall(env, packageDir); - - expect(await file(join(packageDir, "node_modules", "@secret", "publish-pkg-8", "package.json")).json()).toEqual( - pkgJson, - ); - }); - } - }); - - describe("tag", async () => { - test("can publish with a tag", async () => { - const bunfig = await authBunfig("simpletag"); - const pkgJson = { - name: "publish-pkg-9", - version: "9.9.9", - dependencies: { - "publish-pkg-9": "simpletag", - }, - }; - await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-9"), { recursive: true, force: true }), - write(join(packageDir, "bunfig.toml"), bunfig), - write(packageJson, JSON.stringify(pkgJson)), - ]); - - let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); - expect(exitCode).toBe(0); - - await runBunInstall(env, packageDir); - expect(await file(join(packageDir, "node_modules", "publish-pkg-9", "package.json")).json()).toEqual(pkgJson); - }); - }); -}); - -describe("package.json indentation", async () => { - test("works for root and workspace packages", async () => { - await Promise.all([ - // 5 space indentation - write(packageJson, `\n{\n\n "name": "foo",\n"workspaces": ["packages/*"]\n}`), - // 1 tab indentation - write(join(packageDir, "packages", "bar", "package.json"), `\n{\n\n\t"name": "bar",\n}`), - ]); - - let { exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const rootPackageJson = await file(packageJson).text(); - - expect(rootPackageJson).toBe( - `{\n "name": "foo",\n "workspaces": ["packages/*"],\n "dependencies": {\n "no-deps": "^2.0.0"\n }\n}`, - ); - - // now add to workspace. it should keep tab indentation - ({ exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: join(packageDir, "packages", "bar"), - stdout: "inherit", - stderr: "inherit", - env, - })); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).text()).toBe(rootPackageJson); - const workspacePackageJson = await file(join(packageDir, "packages", "bar", "package.json")).text(); - expect(workspacePackageJson).toBe(`{\n\t"name": "bar",\n\t"dependencies": {\n\t\t"no-deps": "^2.0.0"\n\t}\n}`); - }); - - test("install maintains indentation", async () => { - await write(packageJson, `{\n "dependencies": {}\n }\n`); - let { exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - expect(await exited).toBe(0); - expect(await file(packageJson).text()).toBe(`{\n "dependencies": {\n "no-deps": "^2.0.0"\n }\n}\n`); - }); -}); - -describe("optionalDependencies", () => { - for (const optional of [true, false]) { - test(`exit code is ${optional ? 0 : 1} when ${optional ? "optional" : ""} dependency tarball is missing`, async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - [optional ? "optionalDependencies" : "dependencies"]: { - "missing-tarball": "1.0.0", - "uses-what-bin": "1.0.0", - }, - "trustedDependencies": ["uses-what-bin"], - }), - ); - - const { exited, err } = await runBunInstall(env, packageDir, { - [optional ? "allowWarnings" : "allowErrors"]: true, - expectedExitCode: optional ? 0 : 1, - savesLockfile: false, - }); - expect(err).toContain( - `${optional ? "warn" : "error"}: GET http://localhost:${port}/missing-tarball/-/missing-tarball-1.0.0.tgz - `, - ); - expect(await exited).toBe(optional ? 0 : 1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "uses-what-bin", "what-bin"]); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - }); - } - - for (const rootOptional of [true, false]) { - test(`exit code is 0 when ${rootOptional ? "root" : ""} optional dependency does not exist in registry`, async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - [rootOptional ? "optionalDependencies" : "dependencies"]: { - [rootOptional ? "this-package-does-not-exist-in-the-registry" : "has-missing-optional-dep"]: "||", - }, - }), - ); - - const { err } = await runBunInstall(env, packageDir, { - allowWarnings: true, - savesLockfile: !rootOptional, - }); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(err).toMatch(`warn: GET http://localhost:${port}/this-package-does-not-exist-in-the-registry - 404`); - }); - } - test("should not install optional deps if false in bunfig", async () => { - await writeFile( - join(packageDir, "bunfig.toml"), - ` - [install] - cache = "${join(packageDir, ".bun-cache")}" - optional = false - registry = "http://localhost:${port}/" - `, - ); - await writeFile( - packageJson, - JSON.stringify( - { - name: "publish-pkg-deps", - version: "1.1.1", - dependencies: { - "no-deps": "1.0.0", - }, - optionalDependencies: { - "basic-1": "1.0.0", - }, - }, - null, - 2, - ), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "1 package installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps"]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("lifecycle scripts failures from transitive dependencies are ignored", async () => { - // Dependency with a transitive optional dependency that fails during its preinstall script. - await write( - packageJson, - JSON.stringify({ - name: "foo", - version: "2.2.2", - dependencies: { - "optional-lifecycle-fail": "1.1.1", - }, - trustedDependencies: ["lifecycle-fail"], - }), - ); - - const { err, exited } = await runBunInstall(env, packageDir); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect( - await Promise.all([ - exists(join(packageDir, "node_modules", "optional-lifecycle-fail", "package.json")), - exists(join(packageDir, "node_modules", "lifecycle-fail", "package.json")), - ]), - ).toEqual([true, false]); - }); -}); - -test("tarball override does not crash", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "two-range-deps": "||", - }, - overrides: { - "no-deps": `http://localhost:${port}/no-deps/-/no-deps-2.0.0.tgz`, - }, - }), - ); - - await runBunInstall(env, packageDir); - - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "2.0.0", - }); -}); - -describe.each(["--production", "without --production"])("%s", flag => { - const prod = flag === "--production"; - const order = ["devDependencies", "dependencies"]; - // const stdio = process.versions.bun.includes("debug") ? "inherit" : "ignore"; - const stdio = "ignore"; - - if (prod) { - test("modifying package.json with --production should not save lockfile", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "bin-change-dir": "1.0.0", - }, - devDependencies: { - "bin-change-dir": "1.0.1", - "basic-1": "1.0.0", - }, - }), - ); - - var { exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: stdio, - stdin: stdio, - stderr: stdio, - env, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const initialHash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer()); - - expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ - name: "bin-change-dir", - version: "1.0.1", - }); - - var { exited } = spawn({ - cmd: [bunExe(), "install", "--production"], - cwd: packageDir, - stdout: stdio, - stdin: stdio, - stderr: stdio, - env, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ - name: "bin-change-dir", - version: "1.0.0", - }); - - var { exited } = spawn({ - cmd: [bunExe(), "install", "--production", "bin-change-dir@1.0.1"], - cwd: packageDir, - stdout: stdio, - stdin: stdio, - stderr: stdio, - env, - }); - - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // We should not have saved bun.lockb - expect(Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())).toBe(initialHash); - - // We should not have installed bin-change-dir@1.0.1 - expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ - name: "bin-change-dir", - version: "1.0.0", - }); - - // This is a no-op. It should work. - var { exited } = spawn({ - cmd: [bunExe(), "install", "--production", "bin-change-dir@1.0.0"], - cwd: packageDir, - stdout: stdio, - stdin: stdio, - stderr: stdio, - env, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // We should not have saved bun.lockb - expect(Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())).toBe(initialHash); - - // We should have installed bin-change-dir@1.0.0 - expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ - name: "bin-change-dir", - version: "1.0.0", - }); - }); - } - - test(`should prefer ${order[+prod % 2]} over ${order[1 - (+prod % 2)]}`, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "bin-change-dir": "1.0.0", - }, - devDependencies: { - "bin-change-dir": "1.0.1", - "basic-1": "1.0.0", - }, - }), - ); - - let initialLockfileHash; - async function saveWithoutProd() { - var hash; - // First install without --production - // so that the lockfile is up to date - var { exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: stdio, - stdin: stdio, - stderr: stdio, - env, - }); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await Promise.all([ - (async () => - expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ - name: "bin-change-dir", - version: "1.0.1", - }))(), - (async () => - expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toMatchObject({ - name: "basic-1", - version: "1.0.0", - }))().then( - async () => await rm(join(packageDir, "node_modules", "basic-1"), { recursive: true, force: true }), - ), - - (async () => (hash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())))(), - ]); - - return hash; - } - if (prod) { - initialLockfileHash = await saveWithoutProd(); - } - - var { exited } = spawn({ - cmd: [bunExe(), "install", prod ? "--production" : ""].filter(Boolean), - cwd: packageDir, - stdout: stdio, - stdin: stdio, - stderr: stdio, - env, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ - name: "bin-change-dir", - version: prod ? "1.0.0" : "1.0.1", - }); - - if (!prod) { - expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toMatchObject({ - name: "basic-1", - version: "1.0.0", - }); - } else { - // it should not install devDependencies - expect(await exists(join(packageDir, "node_modules", "basic-1"))).toBeFalse(); - - // it should not mutate the lockfile when there were no changes to begin with. - const newHash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer()); - - expect(newHash).toBe(initialLockfileHash!); - } - - if (prod) { - // lets now try to install again without --production - const newHash = await saveWithoutProd(); - expect(newHash).toBe(initialLockfileHash); - } - }); -}); - -test("hardlinks on windows dont fail with long paths", async () => { - await mkdir(join(packageDir, "a-package")); - await writeFile( - join(packageDir, "a-package", "package.json"), - JSON.stringify({ - name: "a-package", - version: "1.0.0", - }), - ); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - // 255 characters - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": - "file:./a-package", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@a-package", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); -}); - -test("basic 1", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "basic-1": "1.0.0", - }, - }), - ); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ basic-1@1.0.0", - "", - "1 package installed", - ]); - expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toEqual({ - name: "basic-1", - version: "1.0.0", - } as any); - expect(await exited).toBe(0); - - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ basic-1@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); -}); - -test("manifest cache will invalidate when registry changes", async () => { - const cacheDir = join(packageDir, ".bun-cache"); - await Promise.all([ - write( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = "${cacheDir}" -registry = "http://localhost:${port}" - `, - ), - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - // is-number exists in our custom registry and in npm. Switching the registry should invalidate - // the manifest cache, the package could be a completely different package. - "is-number": "2.0.0", - }, - }), - ), - ]); - - // first install this package from verdaccio - await runBunInstall(env, packageDir); - const lockfile = await parseLockfile(packageDir); - for (const pkg of Object.values(lockfile.packages) as any) { - if (pkg.tag === "npm") { - expect(pkg.resolution.resolved).toContain(`http://localhost:${port}`); - } - } - - // now use default registry - await Promise.all([ - rm(join(packageDir, "node_modules"), { force: true, recursive: true }), - rm(join(packageDir, "bun.lockb"), { force: true }), - write( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = "${cacheDir}" -`, - ), - ]); - - await runBunInstall(env, packageDir); - const npmLockfile = await parseLockfile(packageDir); - for (const pkg of Object.values(npmLockfile.packages) as any) { - if (pkg.tag === "npm") { - expect(pkg.resolution.resolved).not.toContain(`http://localhost:${port}`); - } - } -}); - -test("dependency from root satisfies range from dependency", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "one-range-dep": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ one-range-dep@1.0.0", - "", - "2 packages installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ one-range-dep@1.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); -}); - -test("duplicate names and versions in a manifest do not install incorrect packages", async () => { - /** - * `duplicate-name-and-version` has two versions: - * 1.0.1: - * dependencies: { - * "no-deps": "a-dep" - * } - * 1.0.2: - * dependencies: { - * "a-dep": "1.0.1" - * } - * Note: version for `no-deps` is the same as second dependency name. - * - * When this manifest is parsed, the strings for dependency names and versions are stored - * with different lists offset length pairs, but we were deduping with the same map. Since - * the version of the first dependency is the same as the name as the second, it would try to - * dedupe them, and doing so would give the wrong name for the deduped dependency. - * (`a-dep@1.0.1` would become `no-deps@1.0.1`) - */ - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "duplicate-name-and-version": "1.0.2", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchNodeModulesAt(packageDir); - const results = await Promise.all([ - file(join(packageDir, "node_modules", "duplicate-name-and-version", "package.json")).json(), - file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), - exists(join(packageDir, "node_modules", "no-deps")), - ]); - - expect(results).toMatchObject([ - { name: "duplicate-name-and-version", version: "1.0.2" }, - { name: "a-dep", version: "1.0.1" }, - false, - ]); -}); - -describe("peerDependency index out of bounds", async () => { - // Test for "index of out bounds" errors with peer dependencies when adding/removing a package - // - // Repro: - // - Install `1-peer-dep-a`. It depends on peer dep `no-deps@1.0.0`. - // - Replace `1-peer-dep-a` with `1-peer-dep-b` (identical other than name), delete manifest cache and - // node_modules, then reinstall. - // - `no-deps` will enqueue a dependency id that goes out of bounds - - const dependencies = ["1-peer-dep-a", "1-peer-dep-b", "2-peer-deps-c"]; - - for (const firstDep of dependencies) { - for (const secondDep of dependencies) { - if (firstDep === secondDep) continue; - test(`replacing ${firstDep} with ${secondDep}`, async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - [firstDep]: "1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchNodeModulesAt(packageDir); - const results = await Promise.all([ - file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), - file(join(packageDir, "node_modules", firstDep, "package.json")).json(), - exists(join(packageDir, "node_modules", firstDep, "node_modules", "no-deps")), - ]); - - expect(results).toMatchObject([ - { name: "no-deps", version: "1.0.0" }, - { name: firstDep, version: "1.0.0" }, - false, - ]); - - await Promise.all([ - rm(join(packageDir, "node_modules"), { recursive: true, force: true }), - rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }), - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - [secondDep]: "1.0.0", - }, - }), - ), - ]); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const newLockfile = parseLockfile(packageDir); - expect(newLockfile).toMatchNodeModulesAt(packageDir); - const newResults = await Promise.all([ - file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), - file(join(packageDir, "node_modules", secondDep, "package.json")).json(), - exists(join(packageDir, "node_modules", secondDep, "node_modules", "no-deps")), - ]); - - expect(newResults).toMatchObject([ - { name: "no-deps", version: "1.0.0" }, - { name: secondDep, version: "1.0.0" }, - false, - ]); - }); - } - } - - // Install 2 dependencies, one is a normal dependency, the other is a dependency with a optional - // peer dependency on the first dependency. Delete node_modules and cache, then update the dependency - // with the optional peer to a new version. Doing this will cause the peer dependency to get enqueued - // internally, testing for index out of bounds. It's also important cache is deleted to ensure a tarball - // task is created for it. - test("optional", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "optional-peer-deps": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // update version and delete node_modules and cache - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "optional-peer-deps": "1.0.1", - "no-deps": "1.0.0", - }, - }), - ), - rm(join(packageDir, "node_modules"), { recursive: true, force: true }), - rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }), - ]); - - // this install would trigger the index out of bounds error - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchNodeModulesAt(packageDir); - }); -}); - -test("peerDependency in child npm dependency should not maintain old version when package is upgraded", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ peer-deps-fixed@1.0.0", - "", - "2 packages installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.1", // upgrade the package - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.1", - } as any); - expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.1"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); -}); - -test("package added after install", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "one-range-dep": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ one-range-dep@1.0.0", - "", - "2 packages installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.1.0", - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // add `no-deps` to root package.json with a smaller but still compatible - // version for `one-range-dep`. - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "one-range-dep": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "2 packages installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect( - await file(join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps", "package.json")).json(), - ).toEqual({ - name: "no-deps", - version: "1.1.0", - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ one-range-dep@1.0.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); -}); - -test("--production excludes devDependencies in workspaces", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "no-deps": "1.0.0", - }, - devDependencies: { - "a1": "npm:no-deps@1.0.0", - }, - }), - ), - write( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - "a-dep": "1.0.2", - }, - devDependencies: { - "a2": "npm:a-dep@1.0.2", - }, - }), - ), - write( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - devDependencies: { - "a3": "npm:a-dep@1.0.3", - "a4": "npm:a-dep@1.0.4", - "a5": "npm:a-dep@1.0.5", - }, - }), - ), - ]); - - // without lockfile - const expectedResults = [ - ["a-dep", "no-deps", "pkg1", "pkg2"], - { name: "no-deps", version: "1.0.0" }, - { name: "a-dep", version: "1.0.2" }, - ]; - let { out } = await runBunInstall(env, packageDir, { production: true }); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "4 packages installed", - ]); - let results = await Promise.all([ - readdirSorted(join(packageDir, "node_modules")), - file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), - file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), - ]); - - expect(results).toMatchObject(expectedResults); - - // create non-production lockfile, then install with --production - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - ({ out } = await runBunInstall(env, packageDir)); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ a1@1.0.0", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "7 packages installed", - ]); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - ({ out } = await runBunInstall(env, packageDir, { production: true })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "4 packages installed", - ]); - results = await Promise.all([ - readdirSorted(join(packageDir, "node_modules")), - file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), - file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), - ]); - expect(results).toMatchObject(expectedResults); -}); - -test("--production without a lockfile will install and not save lockfile", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--production"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "no-deps", "index.js"))).toBeTrue(); -}); - -describe("binaries", () => { - for (const global of [false, true]) { - describe(`existing destinations${global ? " (global)" : ""}`, () => { - test("existing non-symlink", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "what-bin": "1.0.0", - }, - }), - ), - write(join(packageDir, "node_modules", ".bin", "what-bin"), "hi"), - ]); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin( - join("..", "what-bin", "what-bin.js"), - ); - }); - }); - } - test("it should correctly link binaries after deleting node_modules", async () => { - const json: any = { - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - "uses-what-bin": "1.5.0", - }, - }; - await writeFile(packageJson, JSON.stringify(json)); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("will link binaries for packages installed multiple times", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin": "1.5.0", - }, - workspaces: ["packages/*"], - trustedDependencies: ["uses-what-bin"], - }), - ), - write( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ), - write( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ), - ]); - - // Root dependends on `uses-what-bin@1.5.0` and both packages depend on `uses-what-bin@1.0.0`. - // This test makes sure the binaries used by `pkg1` and `pkg2` are the correct version (`1.0.0`) - // instead of using the root version (`1.5.0`). - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const results = await Promise.all([ - file(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt")).text(), - file(join(packageDir, "packages", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")).text(), - file(join(packageDir, "packages", "pkg2", "node_modules", "uses-what-bin", "what-bin.txt")).text(), - ]); - - expect(results).toEqual(["what-bin@1.5.0", "what-bin@1.0.0", "what-bin@1.0.0"]); - }); - - test("it should re-symlink binaries that become invalid when updating package versions", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "bin-change-dir": "1.0.0", - }, - scripts: { - postinstall: "bin-change-dir", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ bin-change-dir@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "bin-1.0.0.txt")).text()).toEqual("success!"); - expect(await exists(join(packageDir, "bin-1.0.1.txt"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "bin-change-dir": "1.0.1", - }, - scripts: { - postinstall: "bin-change-dir", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ bin-change-dir@1.0.1"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "bin-1.0.0.txt")).text()).toEqual("success!"); - expect(await file(join(packageDir, "bin-1.0.1.txt")).text()).toEqual("success!"); - }); - - test("will only link global binaries for requested packages", async () => { - await Promise.all([ - write( - join(packageDir, "bunfig.toml"), - ` - [install] - cache = false - registry = "http://localhost:${port}/" - globalBinDir = "${join(packageDir, "global-bin-dir").replace(/\\/g, "\\\\")}" - `, - ), - , - ]); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "-g", `--config=${join(packageDir, "bunfig.toml")}`, "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") }, - }); - - let err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - let out = await Bun.readableStreamToText(stdout); - expect(out).toContain("uses-what-bin@1.5.0"); - expect(await exited).toBe(0); - - expect(await exists(join(packageDir, "global-bin-dir", "what-bin"))).toBeFalse(); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "-g", `--config=${join(packageDir, "bunfig.toml")}`, "what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") }, - })); - - err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - - expect(out).toContain("what-bin@1.5.0"); - expect(await exited).toBe(0); - - // now `what-bin` should be installed in the global bin directory - if (isWindows) { - expect( - await Promise.all([ - exists(join(packageDir, "global-bin-dir", "what-bin.exe")), - exists(join(packageDir, "global-bin-dir", "what-bin.bunx")), - ]), - ).toEqual([true, true]); - } else { - expect(await exists(join(packageDir, "global-bin-dir", "what-bin"))).toBeTrue(); - } - }); - - for (const global of [false, true]) { - test(`bin types${global ? " (global)" : ""}`, async () => { - if (global) { - await write( - join(packageDir, "bunfig.toml"), - ` - [install] - cache = false - registry = "http://localhost:${port}/" - globalBinDir = "${join(packageDir, "global-bin-dir").replace(/\\/g, "\\\\")}" - `, - ); - } else { - await write( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - } - - const args = [ - bunExe(), - "install", - ...(global ? ["-g"] : []), - ...(global ? [`--config=${join(packageDir, "bunfig.toml")}`] : []), - "dep-with-file-bin", - "dep-with-single-entry-map-bin", - "dep-with-directory-bins", - "dep-with-map-bins", - ]; - const { stdout, stderr, exited } = spawn({ - cmd: args, - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: global ? { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") } : env, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - - const out = await Bun.readableStreamToText(stdout); - expect(await exited).toBe(0); - - await runBin("dep-with-file-bin", "file-bin\n", global); - await runBin("single-entry-map-bin", "single-entry-map-bin\n", global); - await runBin("directory-bin-1", "directory-bin-1\n", global); - await runBin("directory-bin-2", "directory-bin-2\n", global); - await runBin("map-bin-1", "map-bin-1\n", global); - await runBin("map-bin-2", "map-bin-2\n", global); - }); - } - - async function runBin(binName: string, expected: string, global: boolean) { - const args = global ? [`./global-bin-dir/${binName}`] : [bunExe(), binName]; - const result = Bun.spawn({ - cmd: args, - stdout: "pipe", - stderr: "pipe", - cwd: packageDir, - env, - }); - - const out = await Bun.readableStreamToText(result.stdout); - expect(out).toEqual(expected); - const err = await Bun.readableStreamToText(result.stderr); - expect(err).toBeEmpty(); - expect(await result.exited).toBe(0); - } - - test("it will skip (without errors) if a folder from `directories.bin` does not exist", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "missing-directory-bin": "file:missing-directory-bin-1.1.1.tgz", - }, - }), - ), - cp(join(import.meta.dir, "missing-directory-bin-1.1.1.tgz"), join(packageDir, "missing-directory-bin-1.1.1.tgz")), - ]); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - }); -}); - -test("it should invalid cached package if package.json is missing", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "2.0.0", - }, - }), - ), - ]); - - let { out } = await runBunInstall(env, packageDir); - expect(out).toContain("+ no-deps@2.0.0"); - - // node_modules and cache should be populated - expect( - await Promise.all([ - readdirSorted(join(packageDir, "node_modules", "no-deps")), - readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), - ]), - ).toEqual([ - ["index.js", "package.json"], - ["index.js", "package.json"], - ]); - - // with node_modules package.json deleted, the package should be reinstalled - await rm(join(packageDir, "node_modules", "no-deps", "package.json")); - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - expect(out).toContain("+ no-deps@2.0.0"); - - // another install is a no-op - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - expect(out).not.toContain("+ no-deps@2.0.0"); - - // with cache package.json deleted, install is a no-op and cache is untouched - await rm(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1", "package.json")); - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - expect(out).not.toContain("+ no-deps@2.0.0"); - expect( - await Promise.all([ - readdirSorted(join(packageDir, "node_modules", "no-deps")), - readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), - ]), - ).toEqual([["index.js", "package.json"], ["index.js"]]); - - // now with node_modules package.json deleted, the package AND the cache should - // be repopulated - await rm(join(packageDir, "node_modules", "no-deps", "package.json")); - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - expect(out).toContain("+ no-deps@2.0.0"); - expect( - await Promise.all([ - readdirSorted(join(packageDir, "node_modules", "no-deps")), - readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), - ]), - ).toEqual([ - ["index.js", "package.json"], - ["index.js", "package.json"], - ]); -}); - -test("it should install with missing bun.lockb, node_modules, and/or cache", async () => { - // first clean install - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - "uses-what-bin": "1.5.0", - "optional-native": "1.0.0", - "peer-deps-too": "1.0.0", - "two-range-deps": "1.0.0", - "one-fixed-dep": "2.0.0", - "no-deps-bins": "2.0.0", - "left-pad": "1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "3.0.0", - "dev-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dep-loop-entry@1.0.0", - expect.stringContaining("+ dep-with-tags@3.0.0"), - "+ dev-deps@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ no-deps-bins@2.0.0", - expect.stringContaining("+ one-fixed-dep@2.0.0"), - "+ optional-native@1.0.0", - "+ peer-deps-too@1.0.0", - "+ two-range-deps@1.0.0", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "19 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - let lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchNodeModulesAt(packageDir); - - // delete node_modules - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); - - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dep-loop-entry@1.0.0", - expect.stringContaining("+ dep-with-tags@3.0.0"), - "+ dev-deps@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ no-deps-bins@2.0.0", - "+ one-fixed-dep@2.0.0", - "+ optional-native@1.0.0", - "+ peer-deps-too@1.0.0", - "+ two-range-deps@1.0.0", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "19 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchNodeModulesAt(packageDir); - - for (var i = 0; i < 100; i++) { - // Situation: - // - // Root package has a dependency on one-fixed-dep, peer-deps-too and two-range-deps. - // Each of these dependencies depends on no-deps. - // - // - one-fixed-dep: no-deps@2.0.0 - // - two-range-deps: no-deps@^1.0.0 (will choose 1.1.0) - // - peer-deps-too: peer no-deps@* - // - // We want peer-deps-too to choose the version of no-deps from one-fixed-dep because - // it's the highest version. It should hoist to the root. - - // delete bun.lockb - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); - - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - } - - // delete cache - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); - - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // delete bun.lockb and cache - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); - - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), - ]); -}); - -describe("hoisting", async () => { - var tests: any = [ - { - situation: "1.0.0 - 1.0.10 is in order", - dependencies: { - "uses-a-dep-1": "1.0.0", - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - }, - expected: "1.0.1", - }, - { - situation: "1.0.1 in the middle", - dependencies: { - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-1": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - }, - expected: "1.0.1", - }, - { - situation: "1.0.1 is missing", - dependencies: { - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - }, - expected: "1.0.10", - }, - { - situation: "1.0.10 and 1.0.1 are missing", - dependencies: { - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - }, - expected: "1.0.2", - }, - { - situation: "1.0.10 is missing and 1.0.1 is last", - dependencies: { - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-1": "1.0.0", - }, - expected: "1.0.1", - }, - ]; - - for (const { dependencies, expected, situation } of tests) { - test(`it should hoist ${expected} when ${situation}`, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - for (const dep of Object.keys(dependencies)) { - expect(out).toContain(`+ ${dep}@${dependencies[dep]}`); - } - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); - - await rm(join(packageDir, "bun.lockb")); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out).not.toContain("package installed"); - expect(out).toContain(`Checked ${Object.keys(dependencies).length * 2} installs across`); - expect(await exited).toBe(0); - }); - } - - describe("peers", async () => { - var peerTests: any = [ - { - situation: "peer 1.0.2", - dependencies: { - "uses-a-dep-1": "1.0.0", - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - "peer-a-dep-1-0-2": "1.0.0", - }, - expected: "1.0.2", - }, - { - situation: "peer >= 1.0.2", - dependencies: { - "uses-a-dep-1": "1.0.0", - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - "peer-a-dep-gte-1-0-2": "1.0.0", - }, - expected: "1.0.10", - }, - { - situation: "peer ^1.0.2", - dependencies: { - "uses-a-dep-1": "1.0.0", - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - "peer-a-dep-caret-1-0-2": "1.0.0", - }, - expected: "1.0.10", - }, - { - situation: "peer ~1.0.2", - dependencies: { - "uses-a-dep-1": "1.0.0", - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - "peer-a-dep-tilde-1-0-2": "1.0.0", - }, - expected: "1.0.10", - }, - { - situation: "peer *", - dependencies: { - "uses-a-dep-1": "1.0.0", - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - "peer-a-dep-star": "1.0.0", - }, - expected: "1.0.1", - }, - { - situation: "peer * and peer 1.0.2", - dependencies: { - "uses-a-dep-1": "1.0.0", - "uses-a-dep-2": "1.0.0", - "uses-a-dep-3": "1.0.0", - "uses-a-dep-4": "1.0.0", - "uses-a-dep-5": "1.0.0", - "uses-a-dep-6": "1.0.0", - "uses-a-dep-7": "1.0.0", - "uses-a-dep-8": "1.0.0", - "uses-a-dep-9": "1.0.0", - "uses-a-dep-10": "1.0.0", - "peer-a-dep-1-0-2": "1.0.0", - "peer-a-dep-star": "1.0.0", - }, - expected: "1.0.2", - }, - ]; - for (const { dependencies, expected, situation } of peerTests) { - test.todoIf(isFlaky && isMacOS && situation === "peer ^1.0.2")( - `it should hoist ${expected} when ${situation}`, - async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - for (const dep of Object.keys(dependencies)) { - expect(out).toContain(`+ ${dep}@${dependencies[dep]}`); - } - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); - - await rm(join(packageDir, "bun.lockb")); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - if (out.includes("installed")) { - console.log("stdout:", out); - } - expect(out).not.toContain("package installed"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out).not.toContain("package installed"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); - }, - ); - } - }); - - test("hoisting/using incorrect peer dep after install", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("incorrect peer dependency"); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ peer-deps-fixed@1.0.0", - "", - "2 packages installed", - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ - name: "peer-deps-fixed", - version: "1.0.0", - peerDependencies: { - "no-deps": "^1.0.0", - }, - } as any); - expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "2.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ no-deps@2.0.0", - "", - "1 package installed", - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "2.0.0", - } as any); - expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ - name: "peer-deps-fixed", - version: "1.0.0", - peerDependencies: { - "no-deps": "^1.0.0", - }, - } as any); - expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); - }); - - test("root workspace (other than root) dependency will not hoist incorrect peer", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["bar"], - }), - ), - write( - join(packageDir, "bar", "package.json"), - JSON.stringify({ - name: "bar", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ), - ]); - - let { exited, stdout } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "ignore", - stdout: "pipe", - env, - }); - - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - - // now run the install again but from the workspace and with `no-deps@2.0.0` - await write( - join(packageDir, "bar", "package.json"), - JSON.stringify({ - name: "bar", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "2.0.0", - }, - }), - ); - - ({ exited, stdout } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "bar"), - stderr: "ignore", - stdout: "pipe", - env, - })); - - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ no-deps@2.0.0", - "", - "2 packages installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "2.0.0", - }); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("hoisting/using incorrect peer dep on initial install", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "2.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).toContain("incorrect peer dependency"); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ no-deps@2.0.0", - "+ peer-deps-fixed@1.0.0", - "", - "2 packages installed", - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "2.0.0", - } as any); - expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ - name: "peer-deps-fixed", - version: "1.0.0", - peerDependencies: { - "no-deps": "^1.0.0", - }, - } as any); - expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "1 package installed", - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ - name: "peer-deps-fixed", - version: "1.0.0", - peerDependencies: { - "no-deps": "^1.0.0", - }, - } as any); - expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); - }); - - describe("devDependencies", () => { - test("from normal dependency", async () => { - // Root package should choose no-deps@1.0.1. - // - // `normal-dep-and-dev-dep` should install `no-deps@1.0.0` and `normal-dep@1.0.1`. - // It should not hoist (skip) `no-deps` for `normal-dep-and-dev-dep`. - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps": "1.0.0", - "normal-dep-and-dev-dep": "1.0.2", - }, - devDependencies: { - "no-deps": "1.0.1", - }, - }), - ); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "pipe", - stdin: "ignore", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.1", - }); - - expect( - await file( - join(packageDir, "node_modules", "normal-dep-and-dev-dep", "node_modules", "no-deps", "package.json"), - ).json(), - ).toEqual({ - name: "no-deps", - version: "1.0.0", - }); - }); - - test("from workspace", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - workspaces: ["packages/*"], - dependencies: { - "no-deps": "1.0.0", - }, - devDependencies: { - "no-deps": "1.0.1", - }, - }), - ); - - await mkdir(join(packageDir, "packages", "moo"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "moo", "package.json"), - JSON.stringify({ - name: "moo", - version: "1.2.3", - dependencies: { - "no-deps": "2.0.0", - "normal-dep-and-dev-dep": "1.0.0", - }, - devDependencies: { - "no-deps": "1.1.0", - }, - }), - ); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "ignore", - stdin: "ignore", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.1", - }); - - expect( - await file(join(packageDir, "node_modules", "moo", "node_modules", "no-deps", "package.json")).json(), - ).toEqual({ - name: "no-deps", - version: "1.1.0", - }); - }); - - test("from linked package", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps": "1.1.0", - "folder-dep": "file:./folder-dep", - }, - devDependencies: { - "no-deps": "2.0.0", - }, - }), - ); - - await mkdir(join(packageDir, "folder-dep")); - await writeFile( - join(packageDir, "folder-dep", "package.json"), - JSON.stringify({ - name: "folder-dep", - version: "1.2.3", - dependencies: { - "no-deps": "1.0.0", - "normal-dep-and-dev-dep": "1.0.1", - }, - devDependencies: { - "no-deps": "1.0.1", - }, - }), - ); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "ignore", - stdin: "ignore", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "2.0.0", - }); - expect( - await file( - join(packageDir, "node_modules", "normal-dep-and-dev-dep", "node_modules", "no-deps", "package.json"), - ).json(), - ).toEqual({ - "name": "no-deps", - "version": "1.1.0", - }); - expect( - await file(join(packageDir, "node_modules", "folder-dep", "node_modules", "no-deps", "package.json")).json(), - ).toEqual({ - name: "no-deps", - version: "1.0.1", - }); - }); - - test("dependency with normal dependency same as root", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps": "1.0.0", - "one-dep": "1.0.0", - }, - devDependencies: { - "no-deps": "2.0.0", - }, - }), - ); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "ignore", - stdin: "ignore", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "2.0.0", - }); - expect( - await file(join(packageDir, "node_modules", "one-dep", "node_modules", "no-deps", "package.json")).json(), - ).toEqual({ - name: "no-deps", - version: "1.0.1", - }); - }); - }); -}); - -describe("workspaces", async () => { - test("adding packages in a subdirectory of a workspace", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "root", - workspaces: ["foo"], - }), - ); - - await mkdir(join(packageDir, "folder1")); - await mkdir(join(packageDir, "foo", "folder2"), { recursive: true }); - await writeFile( - join(packageDir, "foo", "package.json"), - JSON.stringify({ - name: "foo", - }), - ); - - // add package to root workspace from `folder1` - let { stdout, exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: join(packageDir, "folder1"), - stdout: "pipe", - stderr: "inherit", - env, - }); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "root", - workspaces: ["foo"], - dependencies: { - "no-deps": "^2.0.0", - }, - }); - - // add package to foo from `folder2` - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "what-bin"], - cwd: join(packageDir, "foo", "folder2"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed what-bin@1.5.0 with binaries:", - " - what-bin", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "foo", "package.json")).json()).toEqual({ - name: "foo", - dependencies: { - "what-bin": "^1.5.0", - }, - }); - - // now delete node_modules and bun.lockb and install - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "folder1"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ no-deps@2.0.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "foo", "folder2"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ what-bin@1.5.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); - }); - test("adding packages in workspaces", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "bar": "workspace:*", - }, - }), - ); - - await mkdir(join(packageDir, "packages", "bar"), { recursive: true }); - await mkdir(join(packageDir, "packages", "boba")); - await mkdir(join(packageDir, "packages", "pkg5")); - - await writeFile(join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar" })); - await writeFile( - join(packageDir, "packages", "boba", "package.json"), - JSON.stringify({ name: "boba", version: "1.0.0", dependencies: { "pkg5": "*" } }), - ); - await writeFile( - join(packageDir, "packages", "pkg5", "package.json"), - JSON.stringify({ - name: "pkg5", - version: "1.2.3", - dependencies: { - "bar": "workspace:*", - }, - }), - ); - - let { stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "inherit", - env, - }); - - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:packages/bar", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "bar"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "boba"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "pkg5"))).toBeTrue(); - - // add a package to the root workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "inherit", - env, - })); - - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - bar: "workspace:*", - "no-deps": "^2.0.0", - }, - }); - - // add a package in a workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "two-range-deps"], - cwd: join(packageDir, "packages", "boba"), - stdout: "pipe", - stderr: "inherit", - env, - })); - - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed two-range-deps@1.0.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ - name: "boba", - version: "1.0.0", - dependencies: { - "pkg5": "*", - "two-range-deps": "^1.0.0", - }, - }); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@types", - "bar", - "boba", - "no-deps", - "pkg5", - "two-range-deps", - ]); - - // add a dependency to a workspace with the same name as another workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "bar@0.0.7"], - cwd: join(packageDir, "packages", "boba"), - stdout: "pipe", - stderr: "inherit", - env, - })); - - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed bar@0.0.7", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ - name: "boba", - version: "1.0.0", - dependencies: { - "pkg5": "*", - "two-range-deps": "^1.0.0", - "bar": "0.0.7", - }, - }); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@types", - "bar", - "boba", - "no-deps", - "pkg5", - "two-range-deps", - ]); - expect(await file(join(packageDir, "node_modules", "boba", "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.7", - description: "not a workspace", - }); - }); - test("it should detect duplicate workspace dependencies", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - }), - ); - - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1" })); - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile(join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg1" })); - - var { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - expect(err).toContain('Workspace name "pkg1" already exists'); - expect(await exited).toBe(1); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); - - ({ stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - expect(err).toContain('Workspace name "pkg1" already exists'); - expect(await exited).toBe(1); - }); - - const versions = ["workspace:1.0.0", "workspace:*", "workspace:^1.0.0", "1.0.0", "*"]; - - for (const rootVersion of versions) { - for (const packageVersion of versions) { - test(`it should allow duplicates, root@${rootVersion}, package@${packageVersion}`, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - workspaces: ["packages/*"], - dependencies: { - pkg2: rootVersion, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "1.0.0", - dependencies: { - pkg2: packageVersion, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ name: "pkg2", version: "1.0.0" }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ pkg2@workspace:packages/pkg2`, - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ pkg2@workspace:packages/pkg2`, - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - } - } - - for (const version of versions) { - test(`it should allow listing workspace as dependency of the root package version ${version}`, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "workspace-1": version, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "workspace-1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "workspace-1", "package.json"), - JSON.stringify({ - name: "workspace-1", - version: "1.0.0", - }), - ); - // install first from the root, the workspace package - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ workspace-1@workspace:packages/workspace-1`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "workspace-1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); - - // install from workspace package then from root - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "workspace-1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - }); - } -}); - -describe("transitive file dependencies", () => { - async function checkHoistedFiles() { - const aliasedFileDepFilesPackageJson = join( - packageDir, - "node_modules", - "aliased-file-dep", - "node_modules", - "files", - "the-files", - "package.json", - ); - const results = await Promise.all([ - exists(join(packageDir, "node_modules", "file-dep", "node_modules", "files", "package.json")), - readdirSorted(join(packageDir, "node_modules", "missing-file-dep", "node_modules")), - exists(join(packageDir, "node_modules", "aliased-file-dep", "package.json")), - isWindows - ? file(await readlink(aliasedFileDepFilesPackageJson)).json() - : file(aliasedFileDepFilesPackageJson).json(), - exists( - join(packageDir, "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json"), - ), - exists( - join( - packageDir, - "node_modules", - "@another-scope", - "file-dep", - "node_modules", - "@scoped", - "files", - "package.json", - ), - ), - exists(join(packageDir, "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json")), - ]); - - expect(results).toEqual([ - true, - [], - true, - { - "name": "files", - "version": "1.1.1", - "dependencies": { - "no-deps": "2.0.0", - }, - }, - true, - true, - true, - ]); - } - - async function checkUnhoistedFiles() { - const results = await Promise.all([ - file(join(packageDir, "node_modules", "dep-file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "missing-file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "aliased-file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "@scoped", "file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "@another-scope", "file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "self-file-dep", "package.json")).json(), - - exists(join(packageDir, "pkg1", "node_modules", "file-dep", "node_modules", "files", "package.json")), // true - readdirSorted(join(packageDir, "pkg1", "node_modules", "missing-file-dep", "node_modules")), // [] - exists(join(packageDir, "pkg1", "node_modules", "aliased-file-dep")), // false - exists( - join( - packageDir, - "pkg1", - "node_modules", - "@scoped", - "file-dep", - "node_modules", - "@scoped", - "files", - "package.json", - ), - ), - exists( - join( - packageDir, - "pkg1", - "node_modules", - "@another-scope", - "file-dep", - "node_modules", - "@scoped", - "files", - "package.json", - ), - ), - exists( - join(packageDir, "pkg1", "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"), - ), - readdirSorted(join(packageDir, "pkg1", "node_modules")), - ]); - - const expected = [ - ...(Array(7).fill({ name: "a-dep", version: "1.0.1" }) as any), - true, - [] as string[], - false, - true, - true, - true, - ["@another-scope", "@scoped", "dep-file-dep", "file-dep", "missing-file-dep", "self-file-dep"], - ]; - - // @ts-ignore - expect(results).toEqual(expected); - } - - test("from hoisted workspace dependencies", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["pkg1"], - }), - ), - write( - join(packageDir, "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - // hoisted - "dep-file-dep": "1.0.0", - // root - "file-dep": "1.0.0", - // dangling symlink - "missing-file-dep": "1.0.0", - // aliased. has `"file-dep": "file:."` - "aliased-file-dep": "npm:file-dep@1.0.1", - // scoped - "@scoped/file-dep": "1.0.0", - // scoped with different names - "@another-scope/file-dep": "1.0.0", - // file dependency on itself - "self-file-dep": "1.0.0", - }, - }), - ), - ]); - - var { out } = await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "14 packages installed", - ]); - - await checkHoistedFiles(); - expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - // reinstall - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "14 packages installed", - ]); - - await checkHoistedFiles(); - - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - - await checkHoistedFiles(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); - - // install from workspace - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", - "", - "14 packages installed", - ]); - - await checkHoistedFiles(); - expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); - - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", - "", - "14 packages installed", - ]); - }); - - test("from non-hoisted workspace dependencies", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["pkg1"], - // these dependencies exist to make the workspace - // dependencies non-hoisted - dependencies: { - "dep-file-dep": "npm:a-dep@1.0.1", - "file-dep": "npm:a-dep@1.0.1", - "missing-file-dep": "npm:a-dep@1.0.1", - "aliased-file-dep": "npm:a-dep@1.0.1", - "@scoped/file-dep": "npm:a-dep@1.0.1", - "@another-scope/file-dep": "npm:a-dep@1.0.1", - "self-file-dep": "npm:a-dep@1.0.1", - }, - }), - ), - write( - join(packageDir, "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - // hoisted - "dep-file-dep": "1.0.0", - // root - "file-dep": "1.0.0", - // dangling symlink - "missing-file-dep": "1.0.0", - // aliased. has `"file-dep": "file:."` - "aliased-file-dep": "npm:file-dep@1.0.1", - // scoped - "@scoped/file-dep": "1.0.0", - // scoped with different names - "@another-scope/file-dep": "1.0.0", - // file dependency on itself - "self-file-dep": "1.0.0", - }, - }), - ), - ]); - - var { out } = await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.1", - "+ @scoped/file-dep@1.0.1", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.1", - expect.stringContaining("+ file-dep@1.0.1"), - "+ missing-file-dep@1.0.1", - "+ self-file-dep@1.0.1", - "", - "13 packages installed", - ]); - - await checkUnhoistedFiles(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); - - // reinstall - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.1", - "+ @scoped/file-dep@1.0.1", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.1", - expect.stringContaining("+ file-dep@1.0.1"), - "+ missing-file-dep@1.0.1", - "+ self-file-dep@1.0.1", - "", - "13 packages installed", - ]); - - await checkUnhoistedFiles(); - - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - - await checkUnhoistedFiles(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); - - // install from workspace - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", - "", - "13 packages installed", - ]); - - await checkUnhoistedFiles(); - - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); - - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", - "", - "13 packages installed", - ]); - }); - - test("from root dependencies", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - // hoisted - "dep-file-dep": "1.0.0", - // root - "file-dep": "1.0.0", - // dangling symlink - "missing-file-dep": "1.0.0", - // aliased. has `"file-dep": "file:."` - "aliased-file-dep": "npm:file-dep@1.0.1", - // scoped - "@scoped/file-dep": "1.0.0", - // scoped with different names - "@another-scope/file-dep": "1.0.0", - // file dependency on itself - "self-file-dep": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await Bun.readableStreamToText(stderr); - var out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", - "", - "13 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@another-scope", - "@scoped", - "aliased-file-dep", - "dep-file-dep", - "file-dep", - "missing-file-dep", - "self-file-dep", - ]); - - await checkHoistedFiles(); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await Bun.readableStreamToText(stderr); - out = await Bun.readableStreamToText(stdout); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await checkHoistedFiles(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await Bun.readableStreamToText(stderr); - out = await Bun.readableStreamToText(stdout); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@another-scope", - "@scoped", - "aliased-file-dep", - "dep-file-dep", - "file-dep", - "missing-file-dep", - "self-file-dep", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await checkHoistedFiles(); - }); - test("it should install folder dependencies with absolute paths", async () => { - async function writePackages(num: number) { - await rm(join(packageDir, `pkg0`), { recursive: true, force: true }); - for (let i = 0; i < num; i++) { - await mkdir(join(packageDir, `pkg${i}`)); - await writeFile( - join(packageDir, `pkg${i}`, "package.json"), - JSON.stringify({ - name: `pkg${i}`, - version: "1.1.1", - }), - ); - } - } - - await writePackages(2); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - // without and without file protocol - "pkg0": `file:${resolve(packageDir, "pkg0").replace(/\\/g, "\\\\")}`, - "pkg1": `${resolve(packageDir, "pkg1").replace(/\\/g, "\\\\")}`, - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env, - }); - - var err = await Bun.readableStreamToText(stderr); - var out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ pkg0@pkg0", - "+ pkg1@pkg1", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["pkg0", "pkg1"]); - expect(await file(join(packageDir, "node_modules", "pkg0", "package.json")).json()).toEqual({ - name: "pkg0", - version: "1.1.1", - }); - expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toEqual({ - name: "pkg1", - version: "1.1.1", - }); - }); -}); - -test("name from manifest is scoped and url encoded", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - // `name` in the manifest for these packages is manually changed - // to use `%40` and `%2f` - "@url/encoding.2": "1.0.1", - "@url/encoding.3": "1.0.1", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const files = await Promise.all([ - file(join(packageDir, "node_modules", "@url", "encoding.2", "package.json")).json(), - file(join(packageDir, "node_modules", "@url", "encoding.3", "package.json")).json(), - ]); - - expect(files).toEqual([ - { name: "@url/encoding.2", version: "1.0.1" }, - { name: "@url/encoding.3", version: "1.0.1" }, - ]); -}); - -describe("update", () => { - test("duplicate peer dependency (one package is invalid_package_id)", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "^1.0.0", - }, - peerDependencies: { - "no-deps": "^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "^1.1.0", - }, - peerDependencies: { - "no-deps": "^1.0.0", - }, - }); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.1.0", - }); - }); - test("dist-tags", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "latest", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - name: "a-dep", - version: "1.0.10", - }); - - // Update without args, `latest` should stay - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "latest", - }, - }); - - // Update with `a-dep` and `--latest`, `latest` should be replaced with the installed version - await runBunUpdate(env, packageDir, ["a-dep"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "^1.0.10", - }, - }); - await runBunUpdate(env, packageDir, ["--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "^1.0.10", - }, - }); - }); - test("exact versions stay exact", async () => { - const runs = [ - { version: "1.0.1", dependency: "a-dep" }, - { version: "npm:a-dep@1.0.1", dependency: "aliased" }, - ]; - for (const { version, dependency } of runs) { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - [dependency]: version, - }, - }), - ); - async function check(version: string) { - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", dependency, "package.json")).json()).toMatchObject({ - name: "a-dep", - version: version.replace(/.*@/, ""), - }); - - expect(await file(packageJson).json()).toMatchObject({ - dependencies: { - [dependency]: version, - }, - }); - } - await runBunInstall(env, packageDir); - await check(version); - - await runBunUpdate(env, packageDir); - await check(version); - - await runBunUpdate(env, packageDir, [dependency]); - await check(version); - - // this will actually update the package, but the version should remain exact - await runBunUpdate(env, packageDir, ["--latest"]); - await check(dependency === "aliased" ? "npm:a-dep@1.0.10" : "1.0.10"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - } - }); - describe("tilde", () => { - test("without args", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "~1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.0.1", - }); - - let { out } = await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "~1.0.1", - }, - }); - - // another update does not change anything (previously the version would update because it was changed to `^1.0.1`) - ({ out } = await runBunUpdate(env, packageDir)); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "~1.0.1", - }, - }); - }); - - for (const latest of [true, false]) { - test(`update no args${latest ? " --latest" : ""}`, async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a1": "npm:no-deps@1", - "a10": "npm:no-deps@~1.0", - "a11": "npm:no-deps@^1.0", - "a12": "npm:no-deps@~1.0.1", - "a13": "npm:no-deps@^1.0.1", - "a14": "npm:no-deps@~1.1.0", - "a15": "npm:no-deps@^1.1.0", - "a2": "npm:no-deps@1.0", - "a3": "npm:no-deps@1.1", - "a4": "npm:no-deps@1.0.1", - "a5": "npm:no-deps@1.1.0", - "a6": "npm:no-deps@~1", - "a7": "npm:no-deps@^1", - "a8": "npm:no-deps@~1.1", - "a9": "npm:no-deps@^1.1", - }, - }), - ); - - if (latest) { - await runBunUpdate(env, packageDir, ["--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a1": "npm:no-deps@^2.0.0", - "a10": "npm:no-deps@~2.0.0", - "a11": "npm:no-deps@^2.0.0", - "a12": "npm:no-deps@~2.0.0", - "a13": "npm:no-deps@^2.0.0", - "a14": "npm:no-deps@~2.0.0", - "a15": "npm:no-deps@^2.0.0", - "a2": "npm:no-deps@~2.0.0", - "a3": "npm:no-deps@~2.0.0", - "a4": "npm:no-deps@2.0.0", - "a5": "npm:no-deps@2.0.0", - "a6": "npm:no-deps@~2.0.0", - "a7": "npm:no-deps@^2.0.0", - "a8": "npm:no-deps@~2.0.0", - "a9": "npm:no-deps@^2.0.0", - }, - }); - } else { - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a1": "npm:no-deps@^1.1.0", - "a10": "npm:no-deps@~1.0.1", - "a11": "npm:no-deps@^1.1.0", - "a12": "npm:no-deps@~1.0.1", - "a13": "npm:no-deps@^1.1.0", - "a14": "npm:no-deps@~1.1.0", - "a15": "npm:no-deps@^1.1.0", - "a2": "npm:no-deps@~1.0.1", - "a3": "npm:no-deps@~1.1.0", - "a4": "npm:no-deps@1.0.1", - "a5": "npm:no-deps@1.1.0", - "a6": "npm:no-deps@~1.1.0", - "a7": "npm:no-deps@^1.1.0", - "a8": "npm:no-deps@~1.1.0", - "a9": "npm:no-deps@^1.1.0", - }, - }); - } - const files = await Promise.all( - ["a1", "a10", "a11", "a12", "a13", "a14", "a15", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9"].map(alias => - file(join(packageDir, "node_modules", alias, "package.json")).json(), - ), - ); - - if (latest) { - // each version should be "2.0.0" - expect(files).toMatchObject(Array(15).fill({ version: "2.0.0" })); - } else { - expect(files).toMatchObject([ - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - ]); - } - }); - } - - test("with package name in args", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "1.0.3", - "no-deps": "~1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.0.1", - }); - - let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@1.0.1", - "", - expect.stringContaining("done"), - "", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "1.0.3", - "no-deps": "~1.0.1", - }, - }); - - // update with --latest should only change the update request and keep `~` - ({ out } = await runBunUpdate(env, packageDir, ["no-deps", "--latest"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "1.0.3", - "no-deps": "~2.0.0", - }, - }); - }); - }); - describe("alises", () => { - test("update all", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.1.0", - }, - }); - expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.1.0", - }); - }); - test("update specific aliased package", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir, ["aliased-dep"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.1.0", - }, - }); - expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.1.0", - }); - }); - test("with pre and build tags", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toMatchObject({ - name: "foo", - dependencies: { - "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", - }, - }); - - expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ - name: "prereleases-3", - version: "5.0.0-alpha.150", - }); - - const { out } = await runBunUpdate(env, packageDir, ["--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "^ aliased-dep 5.0.0-alpha.150 -> 5.0.0-alpha.153", - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toMatchObject({ - name: "foo", - dependencies: { - "aliased-dep": "npm:prereleases-3@5.0.0-alpha.153", - }, - }); - }); - }); - test("--no-save will update packages in node_modules and not save to package.json", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "1.0.1", - }, - }), - ); - - let { out } = await runBunUpdate(env, packageDir, ["--no-save"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - expect.stringContaining("+ a-dep@1.0.1"), - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "1.0.1", - }, - }); - - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "^1.0.1", - }, - }), - ); - - ({ out } = await runBunUpdate(env, packageDir, ["--no-save"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - expect.stringContaining("+ a-dep@1.0.10"), - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "^1.0.1", - }, - }); - - // now save - ({ out } = await runBunUpdate(env, packageDir)); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "^1.0.10", - }, - }); - }); - test("update won't update beyond version range unless the specified version allows it", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "dep-with-tags": "^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "dep-with-tags": "^1.0.1", - }, - }); - expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ - version: "1.0.1", - }); - // update with package name does not update beyond version range - await runBunUpdate(env, packageDir, ["dep-with-tags"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "dep-with-tags": "^1.0.1", - }, - }); - expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ - version: "1.0.1", - }); - - // now update with a higher version range - await runBunUpdate(env, packageDir, ["dep-with-tags@^2.0.0"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "dep-with-tags": "^2.0.1", - }, - }); - expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ - version: "2.0.1", - }); - }); - test("update should update all packages in the current workspace", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "what-bin": "^1.0.0", - "uses-what-bin": "^1.0.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - "a-dep": "^1.0.0", - }, - }), - ); - - const originalWorkspaceJSON = { - name: "pkg1", - version: "1.0.0", - dependencies: { - "what-bin": "^1.0.0", - "uses-what-bin": "^1.0.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - "a-dep": "^1.0.0", - }, - }; - - await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify(originalWorkspaceJSON)); - - // initial install, update root - let { out } = await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "+ a-dep@1.0.10", - "+ dep-loop-entry@1.0.0", - expect.stringContaining("+ dep-with-tags@2.0.1"), - "+ dev-deps@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ no-deps-bins@2.0.0", - expect.stringContaining("+ one-fixed-dep@1.0.0"), - "+ optional-native@1.0.0", - "+ peer-deps-too@1.0.0", - "+ two-range-deps@1.0.0", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.5.0"), - "", - // Due to optional-native dependency, this can be either 20 or 19 packages - expect.stringMatching(/(?:20|19) packages installed/), - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - - let lockfile = parseLockfile(packageDir); - // make sure this is valid - expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "what-bin": "^1.5.0", - "uses-what-bin": "^1.5.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.1", - "dev-deps": "1.0.0", - "a-dep": "^1.0.10", - }, - }); - // workspace hasn't changed - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toEqual(originalWorkspaceJSON); - - // now update the workspace, first a couple packages, then all - ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), [ - "what-bin", - "uses-what-bin", - "a-dep@1.0.5", - ])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed what-bin@1.5.0 with binaries:", - " - what-bin", - "installed uses-what-bin@1.5.0", - "installed a-dep@1.0.5", - "", - "3 packages installed", - ]); - // lockfile = parseLockfile(packageDir); - // expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ - dependencies: { - "what-bin": "^1.5.0", - "uses-what-bin": "^1.5.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - - // a-dep should keep caret - "a-dep": "^1.0.5", - }, - }); - - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - name: "a-dep", - version: "1.0.10", - }); - - expect( - await file(join(packageDir, "packages", "pkg1", "node_modules", "a-dep", "package.json")).json(), - ).toMatchObject({ - name: "a-dep", - version: "1.0.5", - }); - - ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["a-dep@^1.0.5"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed a-dep@1.0.10", - "", - expect.stringMatching(/(\[\d+\.\d+m?s\])/), - "", - ]); - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - name: "a-dep", - version: "1.0.10", - }); - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ - dependencies: { - "what-bin": "^1.5.0", - "uses-what-bin": "^1.5.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - "a-dep": "^1.0.10", - }, - }); - }); - test("update different dependency groups", async () => { - for (const args of [true, false]) { - for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { - await write( - packageJson, - JSON.stringify({ - name: "foo", - [group]: { - "a-dep": "^1.0.0", - }, - }), - ); - - const { out } = args ? await runBunUpdate(env, packageDir, ["a-dep"]) : await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - args ? "installed a-dep@1.0.10" : expect.stringContaining("+ a-dep@1.0.10"), - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - [group]: { - "a-dep": "^1.0.10", - }, - }); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - } - } - }); - test("it should update packages from update requests", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - workspaces: ["packages/*"], - }), - ); - - await write( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "1.0.0", - dependencies: { - "a-dep": "^1.0.0", - }, - }), - ); - - await write( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - dependencies: { - "pkg1": "*", - "is-number": "*", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - version: "1.0.10", - }); - expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - - // update no-deps, no range, no change - let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@1.0.0", - "", - expect.stringMatching(/(\[\d+\.\d+m?s\])/), - "", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - - // update package that doesn't exist to workspace, should add to package.json - ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["no-deps"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ - name: "pkg1", - version: "1.0.0", - dependencies: { - "a-dep": "^1.0.0", - "no-deps": "^2.0.0", - }, - }); - - // update root package.json no-deps to ^1.0.0 and update it - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "^1.0.0", - }, - workspaces: ["packages/*"], - }), - ); - - ({ out } = await runBunUpdate(env, packageDir, ["no-deps"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@1.1.0", - "", - "1 package installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.1.0", - }); - }); - - test("--latest works with packages from arguments", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir, ["no-deps", "--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const files = await Promise.all([ - file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), - file(packageJson).json(), - ]); - - expect(files).toMatchObject([{ version: "2.0.0" }, { dependencies: { "no-deps": "2.0.0" } }]); - }); -}); - -test("packages dependening on each other with aliases does not infinitely loop", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "alias-loop-1": "1.0.0", - "alias-loop-2": "1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const files = await Promise.all([ - file(join(packageDir, "node_modules", "alias-loop-1", "package.json")).json(), - file(join(packageDir, "node_modules", "alias-loop-2", "package.json")).json(), - file(join(packageDir, "node_modules", "alias1", "package.json")).json(), - file(join(packageDir, "node_modules", "alias2", "package.json")).json(), - ]); - expect(files).toMatchObject([ - { name: "alias-loop-1", version: "1.0.0" }, - { name: "alias-loop-2", version: "1.0.0" }, - { name: "alias-loop-2", version: "1.0.0" }, - { name: "alias-loop-1", version: "1.0.0" }, - ]); -}); - -test("it should re-populate .bin folder if package is reinstalled", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "what-bin": "1.5.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ what-bin@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; - expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", bin), - ); - if (process.platform === "win32") { - expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); - } else { - expect(await file(join(packageDir, "node_modules", ".bin", bin)).text()).toContain("what-bin@1.5.0"); - } - - await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "what-bin", "package.json"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ what-bin@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", bin), - ); - if (process.platform === "win32") { - expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); - } else { - expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); - } -}); - -test("one version with binary map", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "map-bin": "1.0.2", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ map-bin@1.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); - expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); - expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); -}); - -test("multiple versions with binary map", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "map-bin-multiple": "1.0.2", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ map-bin-multiple@1.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); - expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin( - join("..", "map-bin-multiple", "bin", "map-bin"), - ); - expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin( - join("..", "map-bin-multiple", "bin", "map-bin"), - ); -}); - -test("duplicate dependency in optionalDependencies maintains sort order", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - // `duplicate-optional` has `no-deps` as a normal dependency (1.0.0) and as an - // optional dependency (1.0.1). The optional dependency version should be installed and - // the sort order should remain the same (tested by `bun-debug bun.lockb`). - "duplicate-optional": "1.0.1", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchNodeModulesAt(packageDir); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.1", - }); - - const { stdout, exited } = spawn({ - cmd: [bunExe(), "bun.lockb"], - cwd: packageDir, - stderr: "inherit", - stdout: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out.replaceAll(`${port}`, "4873")).toMatchSnapshot(); - expect(await exited).toBe(0); -}); - -test("missing package on reinstall, some with binaries", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "fooooo", - dependencies: { - "what-bin": "1.0.0", - "uses-what-bin": "1.5.0", - "optional-native": "1.0.0", - "peer-deps-too": "1.0.0", - "two-range-deps": "1.0.0", - "one-fixed-dep": "2.0.0", - "no-deps-bins": "2.0.0", - "left-pad": "1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "3.0.0", - "dev-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dep-loop-entry@1.0.0", - expect.stringContaining("+ dep-with-tags@3.0.0"), - "+ dev-deps@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ no-deps-bins@2.0.0", - "+ one-fixed-dep@2.0.0", - "+ optional-native@1.0.0", - "+ peer-deps-too@1.0.0", - "+ two-range-deps@1.0.0", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "19 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules", "native"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "left-pad"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "dep-loop-entry"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "peer-deps-too"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"), { - recursive: true, - force: true, - }); - await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"), { - recursive: true, - force: true, - }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dep-loop-entry@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ one-fixed-dep@2.0.0", - "+ peer-deps-too@1.0.0", - "", - "7 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "native", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "left-pad", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "dep-loop-entry", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "peer-deps-too", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"))).toBe(true); - const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; - expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", bin), - ); - expect( - Bun.which("what-bin", { PATH: join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin") }), - ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); -}); - -// waiter thread is only a thing on Linux. -for (const forceWaiterThread of isLinux ? [false, true] : [false]) { - describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => { - test("root package with all lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - const writeScript = async (name: string) => { - const contents = ` - import { writeFileSync, existsSync, rmSync } from "fs"; - import { join } from "path"; - - const file = join(import.meta.dir, "${name}.txt"); - - if (existsSync(file)) { - rmSync(file); - writeFileSync(file, "${name} exists!"); - } else { - writeFileSync(file, "${name}!"); - } - `; - await writeFile(join(packageDir, `${name}.js`), contents); - }; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} preinstall.js`, - install: `${bunExe()} install.js`, - postinstall: `${bunExe()} postinstall.js`, - preprepare: `${bunExe()} preprepare.js`, - prepare: `${bunExe()} prepare.js`, - postprepare: `${bunExe()} postprepare.js`, - }, - }), - ); - - await writeScript("preinstall"); - await writeScript("install"); - await writeScript("postinstall"); - await writeScript("preprepare"); - await writeScript("prepare"); - await writeScript("postprepare"); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); - - // add a dependency with all lifecycle scripts - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} preinstall.js`, - install: `${bunExe()} install.js`, - postinstall: `${bunExe()} postinstall.js`, - preprepare: `${bunExe()} preprepare.js`, - prepare: `${bunExe()} prepare.js`, - postprepare: `${bunExe()} postprepare.js`, - }, - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: ["all-lifecycle-scripts"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare exists!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare exists!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare exists!"); - - const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); - - expect(await exists(join(depDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(depDir, "install.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - - await rm(join(packageDir, "preinstall.txt")); - await rm(join(packageDir, "install.txt")); - await rm(join(packageDir, "postinstall.txt")); - await rm(join(packageDir, "preprepare.txt")); - await rm(join(packageDir, "prepare.txt")); - await rm(join(packageDir, "postprepare.txt")); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - // all at once - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - ]); - - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - }); - - test("workspace lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - workspaces: ["packages/*"], - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "1.0.0", - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - version: "1.0.0", - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).toContain("Saved lockfile"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg1", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "postprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg2", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg2", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse(); - }); - - test("dependency lifecycle scripts run before root lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]'; - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin-slow": "1.0.0", - }, - trustedDependencies: ["uses-what-bin-slow"], - scripts: { - install: script, - postinstall: script, - preinstall: script, - prepare: script, - postprepare: script, - preprepare: script, - }, - }), - ); - - // uses-what-bin-slow will wait one second then write a file to disk. The root package should wait for - // for this to happen before running its lifecycle scripts. - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("install a dependency with lifecycle scripts, then add to trusted dependencies and install again", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: [], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - "", - "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); - expect(await exists(join(depDir, "preinstall.txt"))).toBeFalse(); - expect(await exists(join(depDir, "install.txt"))).toBeFalse(); - expect(await exists(join(depDir, "postinstall.txt"))).toBeFalse(); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - - // add to trusted dependencies - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: ["all-lifecycle-scripts"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("Checked 1 install across 2 packages (no changes)"), - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - }); - - test("adding a package without scripts to trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - }, - trustedDependencies: ["what-bin"], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"]; - // prettier-ignore - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { "what-bin": "1.0.0" }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - // add it to trusted dependencies - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - }, - trustedDependencies: ["what-bin"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - }); - - test("lifecycle scripts run if node_modules is deleted", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-postinstall"], - }), - ); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-postinstall@1.0.0", - "", - // @ts-ignore - "1 package installed", - ]); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { force: true, recursive: true }); - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-postinstall@1.0.0", - "", - "1 package installed", - ]); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("INIT_CWD is set to the correct directory", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "bun install.js", - }, - dependencies: { - "lifecycle-init-cwd": "1.0.0", - "another-init-cwd": "npm:lifecycle-init-cwd@1.0.0", - }, - trustedDependencies: ["lifecycle-init-cwd", "another-init-cwd"], - }), - ); - - await writeFile( - join(packageDir, "install.js"), - ` - const fs = require("fs"); - const path = require("path"); - - fs.writeFileSync( - path.join(__dirname, "test.txt"), - process.env.INIT_CWD || "does not exist" - ); - `, - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ another-init-cwd@1.0.0", - "+ lifecycle-init-cwd@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir); - expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir); - expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir); - }); - - test("failing lifecycle script should print output", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-failing-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-failing-postinstall"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("hello"); - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const out = await new Response(stdout).text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - }); - - test("failing root lifecycle script should print output correctly", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "fooooooooo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} -e "throw new Error('Oops!')"`, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await Bun.readableStreamToText(stdout)).toEqual(expect.stringContaining("bun install v1.")); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("error: Oops!"); - expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1'); - }); - - test("exit 0 in lifecycle scripts works", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - postinstall: "exit 0", - prepare: "exit 0", - postprepare: "exit 0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("--ignore-scripts should skip lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "lifecycle-failing-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-failing-postinstall"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--ignore-scripts"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("hello"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-failing-postinstall@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should add `node-gyp rebuild` as the `install` script when `install` and `postinstall` don't exist and `binding.gyp` exists in the root of the package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "binding-gyp-scripts": "1.5.0", - }, - trustedDependencies: ["binding-gyp-scripts"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ binding-gyp-scripts@1.5.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules/binding-gyp-scripts/build.node"))).toBeTrue(); - }); - - test("automatic node-gyp scripts should not run for untrusted dependencies, and should run after adding to `trustedDependencies`", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const packageJSON: any = { - name: "foo", - version: "1.0.0", - dependencies: { - "binding-gyp-scripts": "1.5.0", - }, - }; - await writeFile(packageJson, JSON.stringify(packageJSON)); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ binding-gyp-scripts@1.5.0", - "", - "2 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeFalse(); - - packageJSON.trustedDependencies = ["binding-gyp-scripts"]; - await writeFile(packageJson, JSON.stringify(packageJSON)); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue(); - }); - - test("automatic node-gyp scripts work in package root", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - }), - ); - - await writeFile(join(packageDir, "binding.gyp"), ""); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - - await rm(join(packageDir, "build.node")); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - }); - - test("auto node-gyp scripts work when scripts exists other than `install` and `preinstall`", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - scripts: { - postinstall: "exit 0", - prepare: "exit 0", - postprepare: "exit 0", - }, - }), - ); - - await writeFile(join(packageDir, "binding.gyp"), ""); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - }); - - for (const script of ["install", "preinstall"]) { - test(`does not add auto node-gyp script when ${script} script exists`, async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const packageJSON: any = { - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - scripts: { - [script]: "exit 0", - }, - }; - await writeFile(packageJson, JSON.stringify(packageJSON)); - await writeFile(join(packageDir, "binding.gyp"), ""); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeFalse(); - }); - } - - test("git dependencies also run `preprepare`, `prepare`, and `postprepare` scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-install-test@github:dylan-conway/lifecycle-install-test#3ba6af5", - "", - "1 package installed", - "", - "Blocked 6 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", - }, - trustedDependencies: ["lifecycle-install-test"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue(); - }); - - test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin-slow": "1.0.0", - }, - trustedDependencies: ["uses-what-bin-slow"], - scripts: { - install: '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]', - }, - }), - ); - - // Package `uses-what-bin-slow` has an install script that will sleep for 1 second - // before writing `what-bin.txt` to disk. The root package has an install script that - // checks if this file exists. If the root package install script does not wait for - // the other to finish, it will fail. - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uses-what-bin-slow@1.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - async function createPackagesWithScripts( - packagesCount: number, - scripts: Record, - ): Promise { - const dependencies: Record = {}; - const dependenciesList = []; - - for (let i = 0; i < packagesCount; i++) { - const packageName: string = "stress-test-package-" + i; - const packageVersion = "1.0." + i; - - dependencies[packageName] = "file:./" + packageName; - dependenciesList[i] = packageName; - - const packagePath = join(packageDir, packageName); - await mkdir(packagePath); - await writeFile( - join(packagePath, "package.json"), - JSON.stringify({ - name: packageName, - version: packageVersion, - scripts, - }), - ); - } - - await writeFile( - packageJson, - JSON.stringify({ - name: "stress-test", - version: "1.0.0", - dependencies, - trustedDependencies: dependenciesList, - }), - ); - - return dependenciesList; - } - - test("reach max concurrent scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const scripts = { - "preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`, - }; - - const dependenciesList = await createPackagesWithScripts(4, scripts); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--concurrent-scripts=2"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("Blocked"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - ...dependenciesList.map(dep => `+ ${dep}@${dep}`), - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("stress test", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const dependenciesList = await createPackagesWithScripts(500, { - "postinstall": `${bunExe()} --version`, - }); - - // the script is quick, default number for max concurrent scripts - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("Blocked"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - ...dependenciesList.map(dep => `+ ${dep}@${dep}`).sort((a, b) => a.localeCompare(b)), - "", - "500 packages installed", - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should install and use correct binary version", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - // this should install `what-bin` in two places: - // - // - node_modules/.bin/what-bin@1.5.0 - // - node_modules/uses-what-bin/node_modules/.bin/what-bin@1.0.0 - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin": "1.0.0", - "what-bin": "1.5.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "+ what-bin@1.5.0", - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( - "what-bin@1.5.0", - ); - expect( - await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), - ).toContain("what-bin@1.0.0"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin": "1.5.0", - "what-bin": "1.0.0", - }, - scripts: { - install: "what-bin", - }, - trustedDependencies: ["uses-what-bin"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( - "what-bin@1.0.0", - ); - expect( - await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), - ).toContain("what-bin@1.5.0"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - out = await new Response(stdout).text(); - err = await new Response(stderr).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("node-gyp should always be available for lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "node-gyp --version", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - - // if node-gyp isn't available, it would return a non-zero exit code - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - // if this test fails, `electron` might be removed from the default list - test("default trusted dependencies should work", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - ]); - expect(out).not.toContain("Blocked"); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("default trusted dependencies should not be used of trustedDependencies is populated", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - // fake electron package because it's in the default trustedDependencies list - "electron": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - // electron lifecycle scripts should run, uses-what-bin scripts should not run - var err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - "electron": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }), - ); - - // now uses-what-bin scripts should run and electron scripts should not run. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - }); - - test("does not run any scripts if trustedDependencies is an empty list", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - "electron": "1.0.0", - }, - trustedDependencies: [], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 2 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - }); - - test("will run default trustedDependencies after install that didn't include them", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - electron: "1.0.0", - }, - trustedDependencies: ["blah"], - }), - ); - - // first install does not run electron scripts - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - electron: "1.0.0", - }, - }), - ); - - // The electron scripts should run now because it's in default trusted dependencies. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - }); - - describe("--trust", async () => { - test("unhoisted untrusted scripts, none at root node_modules", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - // prevents real `uses-what-bin` from hoisting to root - "uses-what-bin": "npm:a-dep@1.0.3", - }, - workspaces: ["pkg1"], - }), - ), - write( - join(packageDir, "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ), - ]); - - await runBunInstall(testEnv, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const results = await Promise.all([ - exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin")), - exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), - ]); - - expect(results).toEqual([true, false]); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "--all"], - cwd: packageDir, - stdout: "ignore", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - - expect(await exited).toBe(0); - - expect( - await exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), - ).toBeTrue(); - }); - const trustTests = [ - { - label: "only name", - packageJson: { - name: "foo", - }, - }, - { - label: "empty dependencies", - packageJson: { - name: "foo", - dependencies: {}, - }, - }, - { - label: "populated dependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }, - }, - - { - label: "empty trustedDependencies", - packageJson: { - name: "foo", - trustedDependencies: [], - }, - }, - - { - label: "populated dependencies, empty trustedDependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: [], - }, - }, - - { - label: "populated dependencies and trustedDependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }, - }, - - { - label: "empty dependencies and trustedDependencies", - packageJson: { - name: "foo", - dependencies: {}, - trustedDependencies: [], - }, - }, - ]; - for (const { label, packageJson } of trustTests) { - test(label, async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile(join(packageDir, "package.json"), JSON.stringify(packageJson)); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "uses-what-bin@1.0.0"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed uses-what-bin@1.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(join(packageDir, "package.json")).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // another install should not error with json SyntaxError - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - }); - } - describe("packages without lifecycle scripts", async () => { - test("initial install", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "no-deps@1.0.0"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - const out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }); - }); - test("already installed", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "^2.0.0", - }, - }); - - // oops, I wanted to run the lifecycle scripts for no-deps, I'll install - // again with --trust. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - // oh, I didn't realize no-deps doesn't have - // any lifecycle scripts. It shouldn't automatically add to - // trustedDependencies. - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "^2.0.0", - }, - }); - }); - }); - }); - - describe("updating trustedDependencies", async () => { - test("existing trustedDependencies, unchanged trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["uses-what-bin"], - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // no changes, lockfile shouldn't be saved - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("existing trustedDependencies, removing trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["uses-what-bin"], - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - // this script should not run because uses-what-bin is no longer in trustedDependencies - await rm(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"), { force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - }); - - test("non-existent trustedDependencies, then adding it", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "electron": "1.0.0", - }, - }); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["electron"], - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - await rm(join(packageDir, "node_modules", "electron", "preinstall.txt"), { force: true }); - - // lockfile should save evenn though there are no changes to trustedDependencies due to - // the default list - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - }); - }); - - test("node -p should work in postinstall scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - postinstall: `node -p "require('fs').writeFileSync('postinstall.txt', 'postinstall')"`, - }, - }), - ); - - const originalPath = env.PATH; - env.PATH = ""; - - let { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - env.PATH = originalPath; - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - }); - - test("ensureTempNodeGypScript works", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: "node-gyp --version", - }, - }), - ); - - const originalPath = env.PATH; - env.PATH = ""; - - let { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "ignore", - env, - }); - - env.PATH = originalPath; - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("bun pm trust and untrusted on missing package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "uses-what-bin": "1.5.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - "", - "2 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // remove uses-what-bin from node_modules, bun pm trust and untrusted should handle missing package - await rm(join(packageDir, "node_modules", "uses-what-bin"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "untrusted"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("bun pm untrusted"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("Found 0 untrusted dependencies with scripts"); - expect(await exited).toBe(0); - - ({ stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - expect(await exited).toBe(1); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("bun pm trust"); - expect(err).toContain("0 scripts ran"); - expect(err).toContain("uses-what-bin"); - }); - - describe("add trusted, delete, then add again", async () => { - // when we change bun install to delete dependencies from node_modules - // for both cases, we need to update this test - for (const withRm of [true, false]) { - test(withRm ? "withRm" : "withoutRm", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("1 script ran across 1 package"); - expect(await exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // now remove and install again - if (withRm) { - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "rm", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("1 package removed"); - expect(out).toContain("uses-what-bin"); - expect(await exited).toBe(0); - } - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - let expected = withRm - ? ["", "Checked 1 install across 2 packages (no changes)"] - : ["", expect.stringContaining("1 package removed")]; - expected = [expect.stringContaining("bun install v1."), ...expected]; - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual(expected); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin"))).toBe(!withRm); - - // add again, bun pm untrusted should report it as untrusted - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expected = withRm - ? [ - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "1 package installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ] - : ["", expect.stringContaining("Checked 3 installs across 4 packages (no changes)"), ""]; - expected = [expect.stringContaining("bun install v1."), ...expected]; - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual(expected); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "untrusted"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep)); - expect(await exited).toBe(0); - }); - } - }); - - describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => { - test("install", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} -e 'Bun.sleepSync(1000)'`, - }, - }), - ); - - const proc = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - stdin: "ignore", - env: testEnv, - }); - - expect(await proc.exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000); - }); - - // https://github.com/oven-sh/bun/issues/11252 - test.todoIf(isWindows)("bun pm trust", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const dep = isWindows ? "uses-what-bin-slow-window" : "uses-what-bin-slow"; - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - [dep]: "1.0.0", - }, - }), - ); - - var { exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env: testEnv, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeFalse(); - - const proc = spawn({ - cmd: [bunExe(), "pm", "trust", "--all"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env: testEnv, - }); - - expect(await proc.exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeTrue(); - - expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000 * (isWindows ? 5 : 1)); - }); - }); - }); - - describe("stdout/stderr is inherited from root scripts during install", async () => { - test("without packages", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const exe = bunExe().replace(/\\/g, "\\\\"); - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - scripts: { - "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(err.split(/\r?\n/)).toEqual([ - "No packages! Deleted empty lockfile", - "", - `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "preinstall stderr 🍦", - `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - "", - ]); - const out = await Bun.readableStreamToText(stdout); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "install stdout 🚀", - "prepare stdout done ✅", - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("with a package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const exe = bunExe().replace(/\\/g, "\\\\"); - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - scripts: { - "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - }, - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(err.split(/\r?\n/)).toEqual([ - "Resolving dependencies", - expect.stringContaining("Resolved, downloaded and extracted "), - "Saved lockfile", - "", - `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "preinstall stderr 🍦", - `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - "", - ]); - const out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "install stdout 🚀", - "prepare stdout done ✅", - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - }); -} - -describe("pm trust", async () => { - test("--default", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "default-trusted"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out).toContain("Default trusted dependencies"); - expect(await exited).toBe(0); - }); - - describe("--all", async () => { - test("no dependencies", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "--all"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("error: Lockfile not found"); - let out = await Bun.readableStreamToText(stdout); - expect(out).toBeEmpty(); - expect(await exited).toBe(1); - }); - - test("some dependencies, non with scripts", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "2 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("1 script ran across 1 package"); - expect(await exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - }); - }); -}); - -test("it should be able to find binary in node_modules/.bin from parent directory of root package", async () => { - await mkdir(join(packageDir, "node_modules", ".bin"), { recursive: true }); - await mkdir(join(packageDir, "morePackageDir")); - await writeFile( - join(packageDir, "morePackageDir", "package.json"), - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "missing-bin", - }, - dependencies: { - "what-bin": "1.0.0", - }, - }), - ); - - await cp(join(packageDir, "bunfig.toml"), join(packageDir, "morePackageDir", "bunfig.toml")); - - await writeShebangScript( - join(packageDir, "node_modules", ".bin", "missing-bin"), - "node", - `require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");`, - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "morePackageDir"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "morePackageDir", "missing-bin.txt")).text()).toBe("missing-bin@WHAT"); -}); - -describe("semver", () => { - const taggedVersionTests = [ - { - title: "tagged version last in range", - depVersion: "1 || 2 || pre-3", - expected: "2.0.1", - }, - { - title: "tagged version in middle of range", - depVersion: "1 || pre-3 || 2", - expected: "2.0.1", - }, - { - title: "tagged version first in range", - depVersion: "pre-3 || 2 || 1", - expected: "2.0.1", - }, - { - title: "multiple tagged versions in range", - depVersion: "pre-3 || 2 || pre-1 || 1 || 3 || pre-3", - expected: "3.0.0", - }, - { - title: "start with ||", - depVersion: "|| 1", - expected: "1.0.1", - }, - { - title: "start with || no space", - depVersion: "||2", - expected: "2.0.1", - }, - { - title: "|| with no space on both sides", - depVersion: "1||2", - expected: "2.0.1", - }, - { - title: "no version is latest", - depVersion: "", - expected: "3.0.0", - }, - { - title: "tagged version works", - depVersion: "pre-2", - expected: "2.0.1", - }, - { - title: "tagged above latest", - depVersion: "pre-3", - expected: "3.0.1", - }, - { - title: "'||'", - depVersion: "||", - expected: "3.0.0", - }, - { - title: "'|'", - depVersion: "|", - expected: "3.0.0", - }, - { - title: "'|||'", - depVersion: "|||", - expected: "3.0.0", - }, - { - title: "'|| ||'", - depVersion: "|| ||", - expected: "3.0.0", - }, - { - title: "'|| 1 ||'", - depVersion: "|| 1 ||", - expected: "1.0.1", - }, - { - title: "'| | |'", - depVersion: "| | |", - expected: "3.0.0", - }, - { - title: "'|||||||||||||||||||||||||'", - depVersion: "|||||||||||||||||||||||||", - expected: "3.0.0", - }, - { - title: "'2 ||| 1'", - depVersion: "2 ||| 1", - expected: "2.0.1", - }, - { - title: "'2 |||| 1'", - depVersion: "2 |||| 1", - expected: "2.0.1", - }, - ]; - - for (const { title, depVersion, expected } of taggedVersionTests) { - test(title, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "dep-with-tags": depVersion, - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining(`+ dep-with-tags@${expected}`), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - } - - test.todo("only tagged versions in range errors", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "dep-with-tags": "pre-1 || pre-2", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain('InvalidDependencyVersion parsing version "pre-1 || pre-2"'); - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual(expect.stringContaining("bun install v1.")); - }); -}); - -test("doesn't error when the migration is out of sync", async () => { - const cwd = tempDirWithFiles("out-of-sync-1", { - "package.json": JSON.stringify({ - "devDependencies": { - "no-deps": "1.0.0", - }, - }), - "package-lock.json": JSON.stringify({ - "name": "reproo", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "reproo", - "dependencies": { - "no-deps": "2.0.0", - }, - "devDependencies": { - "no-deps": "1.0.0", - }, - }, - "node_modules/no-deps": { - "version": "1.0.0", - "resolved": `http://localhost:${port}/no-deps/-/no-deps-1.0.0.tgz`, - "integrity": - "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw==", - "dev": true, - }, - }, - }), - }); - - const subprocess = Bun.spawn([bunExe(), "install"], { - env, - cwd, - stdio: ["ignore", "ignore", "inherit"], - }); - - await subprocess.exited; - - expect(subprocess.exitCode).toBe(0); - - let { stdout, exitCode } = Bun.spawnSync({ - cmd: [bunExe(), "pm", "ls"], - env, - cwd, - stdio: ["ignore", "pipe", "inherit"], - }); - let out = stdout.toString().trim(); - expect(out).toContain("no-deps@1.0.0"); - // only one no-deps is installed - expect(out.lastIndexOf("no-deps")).toEqual(out.indexOf("no-deps")); - expect(exitCode).toBe(0); - - expect(await file(join(cwd, "node_modules/no-deps/package.json")).json()).toMatchObject({ - version: "1.0.0", - name: "no-deps", - }); -}); - -const prereleaseTests = [ - [ - { title: "specific", depVersion: "1.0.0-future.1", expected: "1.0.0-future.1" }, - { title: "latest", depVersion: "latest", expected: "1.0.0-future.4" }, - { title: "range starting with latest", depVersion: "^1.0.0-future.4", expected: "1.0.0-future.4" }, - { title: "range above latest", depVersion: "^1.0.0-future.5", expected: "1.0.0-future.7" }, - ], - [ - { title: "#6683", depVersion: "^1.0.0-next.23", expected: "1.0.0-next.23" }, - { - title: "greater than or equal to", - depVersion: ">=1.0.0-next.23", - expected: "1.0.0-next.23", - }, - { title: "latest", depVersion: "latest", expected: "0.5.0" }, - { title: "greater than or equal to latest", depVersion: ">=0.5.0", expected: "0.5.0" }, - ], - - // package "prereleases-3" has four versions, all with prerelease tags: - // - 5.0.0-alpha.150 - // - 5.0.0-alpha.151 - // - 5.0.0-alpha.152 - // - 5.0.0-alpha.153 - [ - { title: "#6956", depVersion: "^5.0.0-alpha.153", expected: "5.0.0-alpha.153" }, - { title: "range matches highest possible", depVersion: "^5.0.0-alpha.152", expected: "5.0.0-alpha.153" }, - { title: "exact", depVersion: "5.0.0-alpha.152", expected: "5.0.0-alpha.152" }, - { title: "exact latest", depVersion: "5.0.0-alpha.153", expected: "5.0.0-alpha.153" }, - { title: "latest", depVersion: "latest", expected: "5.0.0-alpha.153" }, - { title: "~ lower than latest", depVersion: "~5.0.0-alpha.151", expected: "5.0.0-alpha.153" }, - { - title: "~ equal semver and lower non-existant prerelease", - depVersion: "~5.0.0-alpha.100", - expected: "5.0.0-alpha.153", - }, - { - title: "^ equal semver and lower non-existant prerelease", - depVersion: "^5.0.0-alpha.100", - expected: "5.0.0-alpha.153", - }, - { - title: "~ and ^ latest prerelease", - depVersion: "~5.0.0-alpha.153 || ^5.0.0-alpha.153", - expected: "5.0.0-alpha.153", - }, - { - title: "< latest prerelease", - depVersion: "<5.0.0-alpha.153", - expected: "5.0.0-alpha.152", - }, - { - title: "< lower than latest prerelease", - depVersion: "<5.0.0-alpha.152", - expected: "5.0.0-alpha.151", - }, - { - title: "< higher than latest prerelease", - depVersion: "<5.0.0-alpha.22343423", - expected: "5.0.0-alpha.153", - }, - { - title: "< at lowest possible version", - depVersion: "<5.0.0-alpha.151", - expected: "5.0.0-alpha.150", - }, - { - title: "<= latest prerelease", - depVersion: "<=5.0.0-alpha.153", - expected: "5.0.0-alpha.153", - }, - { - title: "<= lower than latest prerelease", - depVersion: "<=5.0.0-alpha.152", - expected: "5.0.0-alpha.152", - }, - { - title: "<= lowest possible version", - depVersion: "<=5.0.0-alpha.150", - expected: "5.0.0-alpha.150", - }, - { - title: "<= higher than latest prerelease", - depVersion: "<=5.0.0-alpha.153261345", - expected: "5.0.0-alpha.153", - }, - { - title: "> latest prerelease", - depVersion: ">=5.0.0-alpha.153", - expected: "5.0.0-alpha.153", - }, - ], -]; -for (let i = 0; i < prereleaseTests.length; i++) { - const tests = prereleaseTests[i]; - const depName = `prereleases-${i + 1}`; - describe(`${depName} should pass`, () => { - for (const { title, depVersion, expected } of tests) { - test(title, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - [`${depName}`]: depVersion, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ ${depName}@${expected}`, - "", - "1 package installed", - ]); - expect(await file(join(packageDir, "node_modules", depName, "package.json")).json()).toEqual({ - name: depName, - version: expected, - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - } - }); -} -const prereleaseFailTests = [ - [ - // { title: "specific", depVersion: "1.0.0-future.1", expected: "1.0.0-future.1" }, - // { title: "latest", depVersion: "latest", expected: "1.0.0-future.4" }, - // { title: "range starting with latest", depVersion: "^1.0.0-future.4", expected: "1.0.0-future.4" }, - // { title: "range above latest", depVersion: "^1.0.0-future.5", expected: "1.0.0-future.7" }, - ], - [ - // { title: "#6683", depVersion: "^1.0.0-next.23", expected: "1.0.0-next.23" }, - // { - // title: "greater than or equal to", - // depVersion: ">=1.0.0-next.23", - // expected: "1.0.0-next.23", - // }, - // { title: "latest", depVersion: "latest", expected: "0.5.0" }, - // { title: "greater than or equal to latest", depVersion: ">=0.5.0", expected: "0.5.0" }, - ], - - // package "prereleases-3" has four versions, all with prerelease tags: - // - 5.0.0-alpha.150 - // - 5.0.0-alpha.151 - // - 5.0.0-alpha.152 - // - 5.0.0-alpha.153 - [ - { - title: "^ with higher non-existant prerelease", - depVersion: "^5.0.0-alpha.1000", - }, - { - title: "~ with higher non-existant prerelease", - depVersion: "~5.0.0-alpha.1000", - }, - { - title: "> with higher non-existant prerelease", - depVersion: ">5.0.0-alpha.1000", - }, - { - title: ">= with higher non-existant prerelease", - depVersion: ">=5.0.0-alpha.1000", - }, - { - title: "^4.3.0", - depVersion: "^4.3.0", - }, - { - title: "~4.3.0", - depVersion: "~4.3.0", - }, - { - title: ">4.3.0", - depVersion: ">4.3.0", - }, - { - title: ">=4.3.0", - depVersion: ">=4.3.0", - }, - { - title: "<5.0.0-alpha.150", - depVersion: "<5.0.0-alpha.150", - }, - { - title: "<=5.0.0-alpha.149", - depVersion: "<=5.0.0-alpha.149", - }, - { - title: "greater than highest prerelease", - depVersion: ">5.0.0-alpha.153", - }, - { - title: "greater than or equal to highest prerelease + 1", - depVersion: ">=5.0.0-alpha.154", - }, - { - title: "`.` instead of `-` should fail", - depVersion: "5.0.0.alpha.150", - }, - ], - // prereleases-4 has one version - // - 2.0.0-pre.0 - [ - { - title: "wildcard should not match prerelease", - depVersion: "x", - }, - { - title: "major wildcard should not match prerelease", - depVersion: "x.0.0", - }, - { - title: "minor wildcard should not match prerelease", - depVersion: "2.x", - }, - { - title: "patch wildcard should not match prerelease", - depVersion: "2.0.x", - }, - ], -]; -for (let i = 0; i < prereleaseFailTests.length; i++) { - const tests = prereleaseFailTests[i]; - const depName = `prereleases-${i + 1}`; - describe(`${depName} should fail`, () => { - for (const { title, depVersion } of tests) { - test(title, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - [`${depName}`]: depVersion, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(err).toContain(`No version matching "${depVersion}" found for specifier "${depName}"`); - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - } - }); -} - -describe("yarn tests", () => { - test("dragon test 1", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "dragon-test-1", - version: "1.0.0", - dependencies: { - "dragon-test-1-d": "1.0.0", - "dragon-test-1-e": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dragon-test-1-d@1.0.0", - "+ dragon-test-1-e@1.0.0", - "", - "6 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "dragon-test-1-a", - "dragon-test-1-b", - "dragon-test-1-c", - "dragon-test-1-d", - "dragon-test-1-e", - ]); - expect(await file(join(packageDir, "node_modules", "dragon-test-1-b", "package.json")).json()).toEqual({ - name: "dragon-test-1-b", - version: "2.0.0", - } as any); - expect(await readdirSorted(join(packageDir, "node_modules", "dragon-test-1-c", "node_modules"))).toEqual([ - "dragon-test-1-b", - ]); - expect( - await file( - join(packageDir, "node_modules", "dragon-test-1-c", "node_modules", "dragon-test-1-b", "package.json"), - ).json(), - ).toEqual({ - name: "dragon-test-1-b", - version: "1.0.0", - dependencies: { - "dragon-test-1-a": "1.0.0", - }, - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("dragon test 2", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "dragon-test-2", - version: "1.0.0", - workspaces: ["dragon-test-2-a", "dragon-test-2-b"], - dependencies: { - "dragon-test-2-a": "1.0.0", - }, - }), - ); - - await mkdir(join(packageDir, "dragon-test-2-a")); - await mkdir(join(packageDir, "dragon-test-2-b")); - - await writeFile( - join(packageDir, "dragon-test-2-a", "package.json"), - JSON.stringify({ - name: "dragon-test-2-a", - version: "1.0.0", - dependencies: { - "dragon-test-2-b": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - await writeFile( - join(packageDir, "dragon-test-2-b", "package.json"), - JSON.stringify({ - name: "dragon-test-2-b", - version: "1.0.0", - dependencies: { - "no-deps": "*", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dragon-test-2-a@workspace:dragon-test-2-a", - "", - "3 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "dragon-test-2-a", - "dragon-test-2-b", - "no-deps", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - }); - expect(await exists(join(packageDir, "dragon-test-2-a", "node_modules"))).toBeFalse(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("dragon test 3", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "dragon-test-3", - version: "1.0.0", - dependencies: { - "dragon-test-3-a": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dragon-test-3-a@1.0.0", - "", - "3 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "dragon-test-3-a", - "dragon-test-3-b", - "no-deps", - ]); - expect(await file(join(packageDir, "node_modules", "dragon-test-3-a", "package.json")).json()).toEqual({ - name: "dragon-test-3-a", - version: "1.0.0", - dependencies: { - "dragon-test-3-b": "1.0.0", - }, - peerDependencies: { - "no-deps": "*", - }, - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("dragon test 4", async () => { - await writeFile( - packageJson, - JSON.stringify({ - "name": "dragon-test-4", - "version": "1.0.0", - "workspaces": ["my-workspace"], - }), - ); - - await mkdir(join(packageDir, "my-workspace")); - await writeFile( - join(packageDir, "my-workspace", "package.json"), - JSON.stringify({ - "name": "my-workspace", - "version": "1.0.0", - "peerDependencies": { - "no-deps": "*", - "peer-deps": "*", - }, - "devDependencies": { - "no-deps": "1.0.0", - "peer-deps": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "3 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["my-workspace", "no-deps", "peer-deps"]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect(await file(join(packageDir, "node_modules", "peer-deps", "package.json")).json()).toEqual({ - name: "peer-deps", - version: "1.0.0", - peerDependencies: { - "no-deps": "*", - }, - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("dragon test 5", async () => { - await writeFile( - packageJson, - JSON.stringify({ - "name": "dragon-test-5", - "version": "1.0.0", - "workspaces": ["packages/*"], - }), - ); - - await mkdir(join(packageDir, "packages", "a"), { recursive: true }); - await mkdir(join(packageDir, "packages", "b"), { recursive: true }); - - await writeFile( - join(packageDir, "packages", "a", "package.json"), - JSON.stringify({ - "name": "a", - "peerDependencies": { - "various-requires": "*", - }, - "devDependencies": { - "no-deps": "1.0.0", - "peer-deps": "1.0.0", - }, - }), - ); - - await writeFile( - join(packageDir, "packages", "b", "package.json"), - JSON.stringify({ - "name": "b", - "devDependencies": { - "a": "workspace:*", - "various-requires": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "5 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "a", - "b", - "no-deps", - "peer-deps", - "various-requires", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect(await file(join(packageDir, "node_modules", "peer-deps", "package.json")).json()).toEqual({ - name: "peer-deps", - version: "1.0.0", - peerDependencies: { - "no-deps": "*", - }, - } as any); - expect(await file(join(packageDir, "node_modules", "various-requires", "package.json")).json()).toEqual({ - name: "various-requires", - version: "1.0.0", - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test.todo("dragon test 6", async () => { - await writeFile( - packageJson, - JSON.stringify({ - "name": "dragon-test-6", - "version": "1.0.0", - "workspaces": ["packages/*"], - }), - ); - - await mkdir(join(packageDir, "packages", "a"), { recursive: true }); - await mkdir(join(packageDir, "packages", "b"), { recursive: true }); - await mkdir(join(packageDir, "packages", "c"), { recursive: true }); - await mkdir(join(packageDir, "packages", "u"), { recursive: true }); - await mkdir(join(packageDir, "packages", "v"), { recursive: true }); - await mkdir(join(packageDir, "packages", "y"), { recursive: true }); - await mkdir(join(packageDir, "packages", "z"), { recursive: true }); - - await writeFile( - join(packageDir, "packages", "a", "package.json"), - JSON.stringify({ - name: `a`, - dependencies: { - [`z`]: `workspace:*`, - }, - }), - ); - await writeFile( - join(packageDir, "packages", "b", "package.json"), - JSON.stringify({ - name: `b`, - dependencies: { - [`u`]: `workspace:*`, - [`v`]: `workspace:*`, - }, - }), - ); - await writeFile( - join(packageDir, "packages", "c", "package.json"), - JSON.stringify({ - name: `c`, - dependencies: { - [`u`]: `workspace:*`, - [`v`]: `workspace:*`, - [`y`]: `workspace:*`, - [`z`]: `workspace:*`, - }, - }), - ); - await writeFile( - join(packageDir, "packages", "u", "package.json"), - JSON.stringify({ - name: `u`, - }), - ); - await writeFile( - join(packageDir, "packages", "v", "package.json"), - JSON.stringify({ - name: `v`, - peerDependencies: { - [`u`]: `*`, - }, - }), - ); - await writeFile( - join(packageDir, "packages", "y", "package.json"), - JSON.stringify({ - name: `y`, - peerDependencies: { - [`v`]: `*`, - }, - }), - ); - await writeFile( - join(packageDir, "packages", "z", "package.json"), - JSON.stringify({ - name: `z`, - dependencies: { - [`y`]: `workspace:*`, - }, - peerDependencies: { - [`v`]: `*`, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ a@workspace:packages/a", - "+ b@workspace:packages/b", - "+ c@workspace:packages/c", - "+ u@workspace:packages/u", - "+ v@workspace:packages/v", - "+ y@workspace:packages/y", - "+ z@workspace:packages/z", - "", - "7 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test.todo("dragon test 7", async () => { - await writeFile( - packageJson, - JSON.stringify({ - "name": "dragon-test-7", - "version": "1.0.0", - "dependencies": { - "dragon-test-7-a": "1.0.0", - "dragon-test-7-d": "1.0.0", - "dragon-test-7-b": "2.0.0", - "dragon-test-7-c": "3.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dragon-test-7-a@1.0.0", - "+ dragon-test-7-b@2.0.0", - "+ dragon-test-7-c@3.0.0", - "+ dragon-test-7-d@1.0.0", - "", - "7 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await writeFile( - join(packageDir, "test.js"), - `console.log(require("dragon-test-7-a"), require("dragon-test-7-d"));`, - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "test.js"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toBeEmpty(); - expect(out).toBe("1.0.0 1.0.0\n"); - - expect( - await exists( - join( - packageDir, - "node_modules", - "dragon-test-7-a", - "node_modules", - "dragon-test-7-b", - "node_modules", - "dragon-test-7-c", - ), - ), - ).toBeTrue(); - expect( - await exists( - join(packageDir, "node_modules", "dragon-test-7-d", "node_modules", "dragon-test-7-b", "node_modules"), - ), - ).toBeFalse(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("dragon test 8", async () => { - await writeFile( - packageJson, - JSON.stringify({ - "name": "dragon-test-8", - version: "1.0.0", - dependencies: { - "dragon-test-8-a": "1.0.0", - "dragon-test-8-b": "1.0.0", - "dragon-test-8-c": "1.0.0", - "dragon-test-8-d": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dragon-test-8-a@1.0.0", - "+ dragon-test-8-b@1.0.0", - "+ dragon-test-8-c@1.0.0", - "+ dragon-test-8-d@1.0.0", - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("dragon test 9", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "dragon-test-9", - version: "1.0.0", - dependencies: { - [`first`]: `npm:peer-deps@1.0.0`, - [`second`]: `npm:peer-deps@1.0.0`, - [`no-deps`]: `1.0.0`, - }, - }), - ); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ first@1.0.0", - expect.stringContaining("+ no-deps@1.0.0"), - "+ second@1.0.0", - "", - "2 packages installed", - ]); - expect(await file(join(packageDir, "node_modules", "first", "package.json")).json()).toEqual( - await file(join(packageDir, "node_modules", "second", "package.json")).json(), - ); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test.todo("dragon test 10", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "dragon-test-10", - version: "1.0.0", - workspaces: ["packages/*"], - }), - ); - - await mkdir(join(packageDir, "packages", "a"), { recursive: true }); - await mkdir(join(packageDir, "packages", "b"), { recursive: true }); - await mkdir(join(packageDir, "packages", "c"), { recursive: true }); - - await writeFile( - join(packageDir, "packages", "a", "package.json"), - JSON.stringify({ - name: "a", - devDependencies: { - b: "workspace:*", - }, - }), - ); - await writeFile( - join(packageDir, "packages", "b", "package.json"), - JSON.stringify({ - name: "b", - peerDependencies: { - c: "*", - }, - devDependencies: { - c: "workspace:*", - }, - }), - ); - await writeFile( - join(packageDir, "packages", "c", "package.json"), - JSON.stringify({ - name: "c", - peerDependencies: { - "no-deps": "*", - }, - depedencies: { - b: "workspace:*", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--dev"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const out = await new Response(stdout).text(); - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ a@workspace:packages/a", - "+ b@workspace:packages/b", - "+ c@workspace:packages/c", - "", - " packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("dragon test 12", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "dragon-test-12", - version: "1.0.0", - workspaces: ["pkg-a", "pkg-b"], - }), - ); - - await mkdir(join(packageDir, "pkg-a"), { recursive: true }); - await mkdir(join(packageDir, "pkg-b"), { recursive: true }); - - await writeFile( - join(packageDir, "pkg-a", "package.json"), - JSON.stringify({ - name: "pkg-a", - dependencies: { - "pkg-b": "workspace:*", - }, - }), - ); - await writeFile( - join(packageDir, "pkg-b", "package.json"), - JSON.stringify({ - name: "pkg-b", - dependencies: { - "peer-deps": "1.0.0", - "fake-peer-deps": "npm:peer-deps@1.0.0", - }, - peerDependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const out = await new Response(stdout).text(); - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "4 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "fake-peer-deps", - "no-deps", - "peer-deps", - "pkg-a", - "pkg-b", - ]); - expect(await file(join(packageDir, "node_modules", "fake-peer-deps", "package.json")).json()).toEqual({ - name: "peer-deps", - version: "1.0.0", - peerDependencies: { - "no-deps": "*", - }, - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should not warn when the peer dependency resolution is compatible", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "compatible-peer-deps", - version: "1.0.0", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--dev"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const out = await new Response(stdout).text(); - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("incorrect peer dependency"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ peer-deps-fixed@1.0.0", - "", - "2 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps", "peer-deps-fixed"]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should warn when the peer dependency resolution is incompatible", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "incompatible-peer-deps", - version: "1.0.0", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "2.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--dev"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const out = await new Response(stdout).text(); - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).toContain("incorrect peer dependency"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ no-deps@2.0.0", - "+ peer-deps-fixed@1.0.0", - "", - "2 packages installed", - ]); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps", "peer-deps-fixed"]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should install in such a way that two identical packages with different peer dependencies are different instances", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "provides-peer-deps-1-0-0": "1.0.0", - "provides-peer-deps-2-0-0": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("incorrect peer dependency"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ provides-peer-deps-1-0-0@1.0.0", - "+ provides-peer-deps-2-0-0@1.0.0", - "", - "5 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await writeFile( - join(packageDir, "test.js"), - `console.log( - require("provides-peer-deps-1-0-0").dependencies["peer-deps"] === - require("provides-peer-deps-2-0-0").dependencies["peer-deps"] - ); - console.log( - Bun.deepEquals(require("provides-peer-deps-1-0-0"), { - name: "provides-peer-deps-1-0-0", - version: "1.0.0", - dependencies: { - "peer-deps": { - name: "peer-deps", - version: "1.0.0", - peerDependencies: { - "no-deps": { - name: "no-deps", - version: "1.0.0", - }, - }, - }, - "no-deps": { - name: "no-deps", - version: "1.0.0", - }, - }, - }) - ); - console.log( - Bun.deepEquals(require("provides-peer-deps-2-0-0"), { - name: "provides-peer-deps-2-0-0", - version: "1.0.0", - dependencies: { - "peer-deps": { - name: "peer-deps", - version: "1.0.0", - peerDependencies: { - "no-deps": { - name: "no-deps", - version: "2.0.0", - }, - }, - }, - "no-deps": { - name: "no-deps", - version: "2.0.0", - }, - }, - }) - );`, - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "test.js"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(out).toBe("true\ntrue\nfalse\n"); - expect(err).toBeEmpty(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (simple)", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "provides-peer-deps-1-0-0": "1.0.0", - "provides-peer-deps-1-0-0-too": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("incorrect peer dependency"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ provides-peer-deps-1-0-0@1.0.0", - "+ provides-peer-deps-1-0-0-too@1.0.0", - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await writeFile( - join(packageDir, "test.js"), - `console.log( - require("provides-peer-deps-1-0-0").dependencies["peer-deps"] === - require("provides-peer-deps-1-0-0-too").dependencies["peer-deps"] - );`, - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "test.js"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(out).toBe("true\n"); - expect(err).toBeEmpty(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (complex)", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "forward-peer-deps": "1.0.0", - "forward-peer-deps-too": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("incorrect peer dependency"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ forward-peer-deps@1.0.0", - "+ forward-peer-deps-too@1.0.0", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await writeFile( - join(packageDir, "test.js"), - `console.log( - require("forward-peer-deps").dependencies["peer-deps"] === - require("forward-peer-deps-too").dependencies["peer-deps"] - );`, - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "test.js"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(out).toBe("true\n"); - expect(err).toBeEmpty(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it shouldn't deduplicate two packages with similar peer dependencies but different names", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "peer-deps": "1.0.0", - "peer-deps-too": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("incorrect peer dependency"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ peer-deps@1.0.0", - "+ peer-deps-too@1.0.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await writeFile(join(packageDir, "test.js"), `console.log(require('peer-deps') === require('peer-deps-too'));`); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "test.js"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(out).toBe("false\n"); - expect(err).toBeEmpty(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should reinstall and rebuild dependencies deleted by the user on the next install", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps-scripted": "1.0.0", - "one-dep-scripted": "1.5.0", - }, - trustedDependencies: ["no-deps-scripted", "one-dep-scripted"], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--dev"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ no-deps-scripted@1.0.0", - "+ one-dep-scripted@1.5.0", - "", - "4 packages installed", - ]); - expect(await exists(join(packageDir, "node_modules/one-dep-scripted/success.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules/one-dep-scripted"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--dev"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(await exists(join(packageDir, "node_modules/one-dep-scripted/success.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); -}); - -test("tarball `./` prefix, duplicate directory with file, and empty directory", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "tarball-without-package-prefix": "1.0.0", - }, - }), - ); - - // Entries in this tarball: - // - // ./ - // ./package1000.js - // ./package2/ - // ./package3/ - // ./package4/ - // ./package.json - // ./package/ - // ./package1000/ - // ./package/index.js - // ./package4/package5/ - // ./package4/package.json - // ./package3/package6/ - // ./package3/package6/index.js - // ./package2/index.js - // package3/ - // package3/package6/ - // package3/package6/index.js - // - // The directory `package3` is added twice, but because one doesn't start - // with `./`, it is stripped from the path and a copy of `package6` is placed - // at the root of the output directory. Also `package1000` is not included in - // the output because it is an empty directory. - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const prefix = join(packageDir, "node_modules", "tarball-without-package-prefix"); - const results = await Promise.all([ - file(join(prefix, "package.json")).json(), - file(join(prefix, "package1000.js")).text(), - file(join(prefix, "package", "index.js")).text(), - file(join(prefix, "package2", "index.js")).text(), - file(join(prefix, "package3", "package6", "index.js")).text(), - file(join(prefix, "package4", "package.json")).json(), - exists(join(prefix, "package4", "package5")), - exists(join(prefix, "package1000")), - file(join(prefix, "package6", "index.js")).text(), - ]); - expect(results).toEqual([ - { - name: "tarball-without-package-prefix", - version: "1.0.0", - }, - "hi", - "ooops", - "ooooops", - "oooooops", - { - "name": "tarball-without-package-prefix", - "version": "2.0.0", - }, - false, - false, - "oooooops", - ]); - expect(await file(join(packageDir, "node_modules", "tarball-without-package-prefix", "package.json")).json()).toEqual( - { - name: "tarball-without-package-prefix", - version: "1.0.0", - }, - ); -}); - -describe("outdated", () => { - const edgeCaseTests = [ - { - description: "normal dep, smaller than column title", - packageJson: { - dependencies: { - "no-deps": "1.0.0", - }, - }, - }, - { - description: "normal dep, larger than column title", - packageJson: { - dependencies: { - "prereleases-1": "1.0.0-future.1", - }, - }, - }, - { - description: "dev dep, smaller than column title", - packageJson: { - devDependencies: { - "no-deps": "1.0.0", - }, - }, - }, - { - description: "dev dep, larger than column title", - packageJson: { - devDependencies: { - "prereleases-1": "1.0.0-future.1", - }, - }, - }, - { - description: "peer dep, smaller than column title", - packageJson: { - peerDependencies: { - "no-deps": "1.0.0", - }, - }, - }, - { - description: "peer dep, larger than column title", - packageJson: { - peerDependencies: { - "prereleases-1": "1.0.0-future.1", - }, - }, - }, - { - description: "optional dep, smaller than column title", - packageJson: { - optionalDependencies: { - "no-deps": "1.0.0", - }, - }, - }, - { - description: "optional dep, larger than column title", - packageJson: { - optionalDependencies: { - "prereleases-1": "1.0.0-future.1", - }, - }, - }, - ]; - - for (const { description, packageJson } of edgeCaseTests) { - test(description, async () => { - await write(join(packageDir, "package.json"), JSON.stringify(packageJson)); - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const testEnv = { ...env, FORCE_COLOR: "1" }; - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "outdated"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - expect(await exited).toBe(0); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - const out = await Bun.readableStreamToText(stdout); - const first = out.slice(0, out.indexOf("\n")); - expect(first).toEqual(expect.stringContaining("bun outdated ")); - expect(first).toEqual(expect.stringContaining("v1.")); - const rest = out.slice(out.indexOf("\n") + 1); - expect(rest).toMatchSnapshot(); - }); - } - test("in workspace", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["pkg1"], - dependencies: { - "no-deps": "1.0.0", - }, - }), - ), - write( - join(packageDir, "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - "a-dep": "1.0.1", - }, - }), - ), - ]); - - await runBunInstall(env, packageDir); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "outdated"], - cwd: join(packageDir, "pkg1"), - stdout: "pipe", - stderr: "pipe", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - let out = await Bun.readableStreamToText(stdout); - expect(out).toContain("a-dep"); - expect(out).not.toContain("no-deps"); - expect(await exited).toBe(0); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "outdated"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env, - })); - - const err2 = await Bun.readableStreamToText(stderr); - expect(err2).not.toContain("error:"); - expect(err2).not.toContain("panic:"); - let out2 = await Bun.readableStreamToText(stdout); - expect(out2).toContain("no-deps"); - expect(out2).not.toContain("a-dep"); - expect(await exited).toBe(0); - }); - - test("NO_COLOR works", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "1.0.1", - }, - }), - ); - - await runBunInstall(env, packageDir); - - const testEnv = { ...env, NO_COLOR: "1" }; - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "outdated"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - - const out = await Bun.readableStreamToText(stdout); - expect(out).toContain("a-dep"); - const first = out.slice(0, out.indexOf("\n")); - expect(first).toEqual(expect.stringContaining("bun outdated ")); - expect(first).toEqual(expect.stringContaining("v1.")); - const rest = out.slice(out.indexOf("\n") + 1); - expect(rest).toMatchSnapshot(); - - expect(await exited).toBe(0); - }); - - async function setupWorkspace() { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "no-deps": "1.0.0", - }, - }), - ), - write( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - "a-dep": "1.0.1", - }, - }), - ), - write( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2222222222222", - dependencies: { - "prereleases-1": "1.0.0-future.1", - }, - }), - ), - ]); - } - - async function runBunOutdated(env: any, cwd: string, ...args: string[]): Promise { - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "outdated", ...args], - cwd, - stdout: "pipe", - stderr: "pipe", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - const out = await Bun.readableStreamToText(stdout); - const exitCode = await exited; - expect(exitCode).toBe(0); - return out; - } - - test("--filter with workspace names and paths", async () => { - await setupWorkspace(); - await runBunInstall(env, packageDir); - - let out = await runBunOutdated(env, packageDir, "--filter", "*"); - expect(out).toContain("foo"); - expect(out).toContain("pkg1"); - expect(out).toContain("pkg2222222222222"); - - out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "./"); - expect(out).toContain("pkg1"); - expect(out).not.toContain("foo"); - expect(out).not.toContain("pkg2222222222222"); - - // in directory that isn't a workspace - out = await runBunOutdated(env, join(packageDir, "packages"), "--filter", "./*", "--filter", "!pkg1"); - expect(out).toContain("pkg2222222222222"); - expect(out).not.toContain("pkg1"); - expect(out).not.toContain("foo"); - - out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "../*"); - expect(out).not.toContain("foo"); - expect(out).toContain("pkg2222222222222"); - expect(out).toContain("pkg1"); - }); - - test("dependency pattern args", async () => { - await setupWorkspace(); - await runBunInstall(env, packageDir); - - let out = await runBunOutdated(env, packageDir, "no-deps", "--filter", "*"); - expect(out).toContain("no-deps"); - expect(out).not.toContain("a-dep"); - expect(out).not.toContain("prerelease-1"); - - out = await runBunOutdated(env, packageDir, "a-dep"); - expect(out).not.toContain("a-dep"); - expect(out).not.toContain("no-deps"); - expect(out).not.toContain("prerelease-1"); - - out = await runBunOutdated(env, packageDir, "*", "--filter", "*"); - expect(out).toContain("no-deps"); - expect(out).toContain("a-dep"); - expect(out).toContain("prereleases-1"); - }); - - test("scoped workspace names", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "@foo/bar", - workspaces: ["packages/*"], - dependencies: { - "no-deps": "1.0.0", - }, - }), - ), - write( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "@scope/pkg1", - dependencies: { - "a-dep": "1.0.1", - }, - }), - ), - ]); - - await runBunInstall(env, packageDir); - - let out = await runBunOutdated(env, packageDir, "--filter", "*"); - expect(out).toContain("@foo/bar"); - expect(out).toContain("@scope/pkg1"); - - out = await runBunOutdated(env, packageDir, "--filter", "*", "--filter", "!@foo/*"); - expect(out).not.toContain("@foo/bar"); - expect(out).toContain("@scope/pkg1"); - }); -}); - -// TODO: setup verdaccio to run across multiple test files, then move this and a few other describe -// scopes (update, hoisting, ...) to other files -// -// test/cli/install/registry/bun-install-windowsshim.test.ts: -// -// This test is to verify that BinLinkingShim.zig creates correct shim files as -// well as bun_shim_impl.exe works in various edge cases. There are many fast -// paths for many many cases. -describe("windows bin linking shim should work", async () => { - if (!isWindows) return; - - const packageDir = tmpdirSync(); - - await writeFile( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = false -registry = "http://localhost:${port}/" -`, - ); - - await writeFile( - join(packageDir, "package.json"), - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "bunx-bins": "*", - }, - }), - ); - console.log(packageDir); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--dev"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - console.log(err); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(err).not.toContain("not found"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bunx-bins@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - - const temp_bin_dir = join(packageDir, "temp"); - mkdirSync(temp_bin_dir); - - for (let i = 1; i <= 7; i++) { - const target = join(temp_bin_dir, "a".repeat(i) + ".exe"); - copyFileSync(bunExe(), target); - } - - copyFileSync(join(packageDir, "node_modules\\bunx-bins\\native.exe"), join(temp_bin_dir, "native.exe")); - - const PATH = process.env.PATH + ";" + temp_bin_dir; - - const bins = [ - { bin: "bin1", name: "bin1" }, - { bin: "bin2", name: "bin2" }, - { bin: "bin3", name: "bin3" }, - { bin: "bin4", name: "bin4" }, - { bin: "bin5", name: "bin5" }, - { bin: "bin6", name: "bin6" }, - { bin: "bin7", name: "bin7" }, - { bin: "bin-node", name: "bin-node" }, - { bin: "bin-bun", name: "bin-bun" }, - { bin: "native", name: "exe" }, - { bin: "uses-native", name: `exe ${packageDir}\\node_modules\\bunx-bins\\uses-native.ts` }, - ]; - - for (const { bin, name } of bins) { - test(`bun run ${bin} arg1 arg2`, async () => { - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "run", bin, "arg1", "arg2"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), - }); - expect(stderr).toBeDefined(); - const err = await new Response(stderr).text(); - expect(err.trim()).toBe(""); - const out = await new Response(stdout).text(); - expect(out.trim()).toBe(`i am ${name} arg1 arg2`); - expect(await exited).toBe(0); - }); - } - - for (const { bin, name } of bins) { - test(`bun --bun run ${bin} arg1 arg2`, async () => { - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "--bun", "run", bin, "arg1", "arg2"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), - }); - expect(stderr).toBeDefined(); - const err = await new Response(stderr).text(); - expect(err.trim()).toBe(""); - const out = await new Response(stdout).text(); - expect(out.trim()).toBe(`i am ${name} arg1 arg2`); - expect(await exited).toBe(0); - }); - } - - for (const { bin, name } of bins) { - test(`bun --bun x ${bin} arg1 arg2`, async () => { - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "--bun", "x", bin, "arg1", "arg2"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), - }); - expect(stderr).toBeDefined(); - const err = await new Response(stderr).text(); - expect(err.trim()).toBe(""); - const out = await new Response(stdout).text(); - expect(out.trim()).toBe(`i am ${name} arg1 arg2`); - expect(await exited).toBe(0); - }); - } - - for (const { bin, name } of bins) { - test(`${bin} arg1 arg2`, async () => { - var { stdout, stderr, exited } = spawn({ - cmd: [join(packageDir, "node_modules", ".bin", bin + ".exe"), "arg1", "arg2"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), - }); - expect(stderr).toBeDefined(); - const err = await new Response(stderr).text(); - expect(err.trim()).toBe(""); - const out = await new Response(stdout).text(); - expect(out.trim()).toBe(`i am ${name} arg1 arg2`); - expect(await exited).toBe(0); - }); - } -}); - -it("$npm_command is accurate during publish", async () => { - await write( - packageJson, - JSON.stringify({ - name: "publish-pkg-10", - version: "1.0.0", - scripts: { - publish: "echo $npm_command", - }, - }), - ); - await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_command")); - await rm(join(import.meta.dir, "packages", "publish-pkg-10"), { recursive: true, force: true }); - let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); - expect(err).toBe(`$ echo $npm_command\n`); - expect(out.split("\n")).toEqual([ - `bun publish ${Bun.version_with_sha}`, - ``, - `packed 95B package.json`, - ``, - `Total files: 1`, - expect.stringContaining(`Shasum: `), - expect.stringContaining(`Integrity: sha512-`), - `Unpacked size: 95B`, - expect.stringContaining(`Packed size: `), - `Tag: simpletag`, - `Access: default`, - `Registry: http://localhost:${port}/`, - ``, - ` + publish-pkg-10@1.0.0`, - `publish`, - ``, - ]); - expect(exitCode).toBe(0); -}); - -it("$npm_lifecycle_event is accurate during publish", async () => { - await write( - packageJson, - `{ - "name": "publish-pkg-11", - "version": "1.0.0", - "scripts": { - "prepublish": "echo 1 $npm_lifecycle_event", - "publish": "echo 2 $npm_lifecycle_event", - "postpublish": "echo 3 $npm_lifecycle_event", - }, - } - `, - ); - await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_lifecycle_event")); - await rm(join(import.meta.dir, "packages", "publish-pkg-11"), { recursive: true, force: true }); - let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); - expect(err).toBe(`$ echo 2 $npm_lifecycle_event\n$ echo 3 $npm_lifecycle_event\n`); - expect(out.split("\n")).toEqual([ - `bun publish ${Bun.version_with_sha}`, - ``, - `packed 256B package.json`, - ``, - `Total files: 1`, - expect.stringContaining(`Shasum: `), - expect.stringContaining(`Integrity: sha512-`), - `Unpacked size: 256B`, - expect.stringContaining(`Packed size: `), - `Tag: simpletag`, - `Access: default`, - `Registry: http://localhost:${port}/`, - ``, - ` + publish-pkg-11@1.0.0`, - `2 publish`, - `3 postpublish`, - ``, - ]); - expect(exitCode).toBe(0); -}); diff --git a/test/cli/install/registry/packages/bundled-1/bundled-1-1.0.0.tgz b/test/cli/install/registry/packages/bundled-1/bundled-1-1.0.0.tgz new file mode 100644 index 0000000000..a9f21c1b24 Binary files /dev/null and b/test/cli/install/registry/packages/bundled-1/bundled-1-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/bundled-1/package.json b/test/cli/install/registry/packages/bundled-1/package.json new file mode 100644 index 0000000000..f1c614636e --- /dev/null +++ b/test/cli/install/registry/packages/bundled-1/package.json @@ -0,0 +1,47 @@ +{ + "name": "bundled-1", + "versions": { + "1.0.0": { + "name": "bundled-1", + "version": "1.0.0", + "dependencies": { + "no-deps": "1.0.0" + }, + "bundleDependencies": [ + "no-deps" + ], + "_id": "bundled-1@1.0.0", + "_integrity": "sha512-YQ/maWZliKQyp1VIdYnPBH6qBHLCQ8Iy6G5vRZFXUHVXufiXT5aTjPVnLQ7xpVAgURFrzd/Fu1113ROLlaJBkQ==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-YQ/maWZliKQyp1VIdYnPBH6qBHLCQ8Iy6G5vRZFXUHVXufiXT5aTjPVnLQ7xpVAgURFrzd/Fu1113ROLlaJBkQ==", + "shasum": "b9052ebbae98de99bf628f65d9150bdcad3dba9d", + "dist": { + "integrity": "sha512-YQ/maWZliKQyp1VIdYnPBH6qBHLCQ8Iy6G5vRZFXUHVXufiXT5aTjPVnLQ7xpVAgURFrzd/Fu1113ROLlaJBkQ==", + "shasum": "b9052ebbae98de99bf628f65d9150bdcad3dba9d", + "tarball": "http://http://localhost:4873/bundled-1/-/bundled-1-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-12-30T18:14:22.143Z", + "created": "2024-12-30T18:14:22.143Z", + "1.0.0": "2024-12-30T18:14:22.143Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "bundled-1-1.0.0.tgz": { + "shasum": "b9052ebbae98de99bf628f65d9150bdcad3dba9d", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "bundled-1", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/bundled-transitive/bundled-transitive-1.0.0.tgz b/test/cli/install/registry/packages/bundled-transitive/bundled-transitive-1.0.0.tgz new file mode 100644 index 0000000000..9f11f84695 Binary files /dev/null and b/test/cli/install/registry/packages/bundled-transitive/bundled-transitive-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/bundled-transitive/package.json b/test/cli/install/registry/packages/bundled-transitive/package.json new file mode 100644 index 0000000000..45bb091b16 --- /dev/null +++ b/test/cli/install/registry/packages/bundled-transitive/package.json @@ -0,0 +1,48 @@ +{ + "name": "bundled-transitive", + "versions": { + "1.0.0": { + "name": "bundled-transitive", + "version": "1.0.0", + "dependencies": { + "no-deps": "1.0.0", + "one-dep": "1.0.0" + }, + "bundleDependencies": [ + "no-deps" + ], + "_id": "bundled-transitive@1.0.0", + "_integrity": "sha512-1CC+XKeBwsdseQEm4yOpfBzom2zEyrXQcgb6N4t83pL2DalEunEFO80yUpkzylsQYIH1uxGLI8G+4jhwvyH1bQ==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-1CC+XKeBwsdseQEm4yOpfBzom2zEyrXQcgb6N4t83pL2DalEunEFO80yUpkzylsQYIH1uxGLI8G+4jhwvyH1bQ==", + "shasum": "8e0417711660590c7317add4a4a28388d4628cb1", + "dist": { + "integrity": "sha512-1CC+XKeBwsdseQEm4yOpfBzom2zEyrXQcgb6N4t83pL2DalEunEFO80yUpkzylsQYIH1uxGLI8G+4jhwvyH1bQ==", + "shasum": "8e0417711660590c7317add4a4a28388d4628cb1", + "tarball": "http://http://localhost:4873/bundled-transitive/-/bundled-transitive-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-12-30T22:05:28.027Z", + "created": "2024-12-30T22:05:28.027Z", + "1.0.0": "2024-12-30T22:05:28.027Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "bundled-transitive-1.0.0.tgz": { + "shasum": "8e0417711660590c7317add4a4a28388d4628cb1", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "bundled-transitive", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/bundled-true/bundled-true-1.0.0.tgz b/test/cli/install/registry/packages/bundled-true/bundled-true-1.0.0.tgz new file mode 100644 index 0000000000..aad2709ff7 Binary files /dev/null and b/test/cli/install/registry/packages/bundled-true/bundled-true-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/bundled-true/package.json b/test/cli/install/registry/packages/bundled-true/package.json new file mode 100644 index 0000000000..1eb11cb00b --- /dev/null +++ b/test/cli/install/registry/packages/bundled-true/package.json @@ -0,0 +1,43 @@ +{ + "name": "bundled-true", + "versions": { + "1.0.0": { + "name": "bundled-true", + "version": "1.0.0", + "dependencies": { + "no-deps": "1.0.0", + "one-dep": "1.0.0" + }, + "bundleDependencies": true, + "_id": "bundled-true@1.0.0", + "_nodeVersion": "23.5.0", + "_npmVersion": "10.9.2", + "dist": { + "integrity": "sha512-VRh+fxqRwtIsAn8P8EH8sETvUD8Yh5zMwujrjISki+37DHQr7jfj8tZW0LaE5rGZW49BeT83zqddRoI237zA/Q==", + "shasum": "e45979379fbf62c4f92e9d7297374c37cb191537", + "tarball": "http://localhost:4873/bundled-true/-/bundled-true-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-12-30T21:52:11.900Z", + "created": "2024-12-30T21:52:11.900Z", + "1.0.0": "2024-12-30T21:52:11.900Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "bundled-true-1.0.0.tgz": { + "shasum": "e45979379fbf62c4f92e9d7297374c37cb191537", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "bundled-true", + "readme": "ERROR: No README data found!" +} diff --git a/test/cli/install/registry/packages/hoist-lockfile-1/hoist-lockfile-1-1.0.0.tgz b/test/cli/install/registry/packages/hoist-lockfile-1/hoist-lockfile-1-1.0.0.tgz new file mode 100644 index 0000000000..d44c7526c8 Binary files /dev/null and b/test/cli/install/registry/packages/hoist-lockfile-1/hoist-lockfile-1-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/hoist-lockfile-1/package.json b/test/cli/install/registry/packages/hoist-lockfile-1/package.json new file mode 100644 index 0000000000..54b1a2b812 --- /dev/null +++ b/test/cli/install/registry/packages/hoist-lockfile-1/package.json @@ -0,0 +1,44 @@ +{ + "name": "hoist-lockfile-1", + "versions": { + "1.0.0": { + "name": "hoist-lockfile-1", + "version": "1.0.0", + "dependencies": { + "hoist-lockfile-shared": "*" + }, + "_id": "hoist-lockfile-1@1.0.0", + "_integrity": "sha512-E2nwR7egMFDoYjeRno7CAa59kiwkLGfhTFy2Q335JWp2r2bDkwoAt1LdChd5PdGYkbo7SfViHkW44ga+WXA+eA==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-E2nwR7egMFDoYjeRno7CAa59kiwkLGfhTFy2Q335JWp2r2bDkwoAt1LdChd5PdGYkbo7SfViHkW44ga+WXA+eA==", + "shasum": "5765633927473de7697d72d0c0d6bfe04b6a7c71", + "dist": { + "integrity": "sha512-E2nwR7egMFDoYjeRno7CAa59kiwkLGfhTFy2Q335JWp2r2bDkwoAt1LdChd5PdGYkbo7SfViHkW44ga+WXA+eA==", + "shasum": "5765633927473de7697d72d0c0d6bfe04b6a7c71", + "tarball": "http://http://localhost:4873/hoist-lockfile-1/-/hoist-lockfile-1-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-12-15T10:01:42.154Z", + "created": "2024-12-15T10:01:42.154Z", + "1.0.0": "2024-12-15T10:01:42.154Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "hoist-lockfile-1-1.0.0.tgz": { + "shasum": "5765633927473de7697d72d0c0d6bfe04b6a7c71", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "hoist-lockfile-1", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/hoist-lockfile-2/hoist-lockfile-2-1.0.0.tgz b/test/cli/install/registry/packages/hoist-lockfile-2/hoist-lockfile-2-1.0.0.tgz new file mode 100644 index 0000000000..e2fe749dec Binary files /dev/null and b/test/cli/install/registry/packages/hoist-lockfile-2/hoist-lockfile-2-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/hoist-lockfile-2/package.json b/test/cli/install/registry/packages/hoist-lockfile-2/package.json new file mode 100644 index 0000000000..ae6a8286eb --- /dev/null +++ b/test/cli/install/registry/packages/hoist-lockfile-2/package.json @@ -0,0 +1,44 @@ +{ + "name": "hoist-lockfile-2", + "versions": { + "1.0.0": { + "name": "hoist-lockfile-2", + "version": "1.0.0", + "dependencies": { + "hoist-lockfile-shared": "^1.0.1" + }, + "_id": "hoist-lockfile-2@1.0.0", + "_integrity": "sha512-7iNRBJF/U078n9oZW7aDvVLkA7+076a2ONEFvITpjKdhT07KWaBei0SzHkFYW4f3foGZPNlHsv0aAgk949TPJg==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-7iNRBJF/U078n9oZW7aDvVLkA7+076a2ONEFvITpjKdhT07KWaBei0SzHkFYW4f3foGZPNlHsv0aAgk949TPJg==", + "shasum": "9d4e79abefd802c410cb2710daeccfda9ecf8995", + "dist": { + "integrity": "sha512-7iNRBJF/U078n9oZW7aDvVLkA7+076a2ONEFvITpjKdhT07KWaBei0SzHkFYW4f3foGZPNlHsv0aAgk949TPJg==", + "shasum": "9d4e79abefd802c410cb2710daeccfda9ecf8995", + "tarball": "http://http://localhost:4873/hoist-lockfile-2/-/hoist-lockfile-2-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-12-15T10:02:22.248Z", + "created": "2024-12-15T10:02:22.248Z", + "1.0.0": "2024-12-15T10:02:22.248Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "hoist-lockfile-2-1.0.0.tgz": { + "shasum": "9d4e79abefd802c410cb2710daeccfda9ecf8995", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "hoist-lockfile-2", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/hoist-lockfile-3/hoist-lockfile-3-1.0.0.tgz b/test/cli/install/registry/packages/hoist-lockfile-3/hoist-lockfile-3-1.0.0.tgz new file mode 100644 index 0000000000..3bf1f448c9 Binary files /dev/null and b/test/cli/install/registry/packages/hoist-lockfile-3/hoist-lockfile-3-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/hoist-lockfile-3/package.json b/test/cli/install/registry/packages/hoist-lockfile-3/package.json new file mode 100644 index 0000000000..a8edbef37c --- /dev/null +++ b/test/cli/install/registry/packages/hoist-lockfile-3/package.json @@ -0,0 +1,44 @@ +{ + "name": "hoist-lockfile-3", + "versions": { + "1.0.0": { + "name": "hoist-lockfile-3", + "version": "1.0.0", + "dependencies": { + "hoist-lockfile-shared": ">=1.0.1" + }, + "_id": "hoist-lockfile-3@1.0.0", + "_integrity": "sha512-iGz7jH7jxz/zq4OZM8hhT7kUX2Ye1m+45SoyMVcWTM7ZB+cY306Ff1mQePKTjkn84/pJMITMdRgDv/qF8PuQUw==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-iGz7jH7jxz/zq4OZM8hhT7kUX2Ye1m+45SoyMVcWTM7ZB+cY306Ff1mQePKTjkn84/pJMITMdRgDv/qF8PuQUw==", + "shasum": "04cab7133b1c33a55aa0b8158d0f615ebdc4b99c", + "dist": { + "integrity": "sha512-iGz7jH7jxz/zq4OZM8hhT7kUX2Ye1m+45SoyMVcWTM7ZB+cY306Ff1mQePKTjkn84/pJMITMdRgDv/qF8PuQUw==", + "shasum": "04cab7133b1c33a55aa0b8158d0f615ebdc4b99c", + "tarball": "http://http://localhost:4873/hoist-lockfile-3/-/hoist-lockfile-3-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-12-15T10:02:33.352Z", + "created": "2024-12-15T10:02:33.352Z", + "1.0.0": "2024-12-15T10:02:33.352Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "hoist-lockfile-3-1.0.0.tgz": { + "shasum": "04cab7133b1c33a55aa0b8158d0f615ebdc4b99c", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "hoist-lockfile-3", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-1.0.1.tgz b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-1.0.1.tgz new file mode 100644 index 0000000000..cf3aca12ca Binary files /dev/null and b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-1.0.1.tgz differ diff --git a/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-1.0.2.tgz b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-1.0.2.tgz new file mode 100644 index 0000000000..d5d8f9652d Binary files /dev/null and b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-1.0.2.tgz differ diff --git a/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-2.0.1.tgz b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-2.0.1.tgz new file mode 100644 index 0000000000..4b60dcfb4f Binary files /dev/null and b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-2.0.1.tgz differ diff --git a/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-2.0.2.tgz b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-2.0.2.tgz new file mode 100644 index 0000000000..3a0b3053da Binary files /dev/null and b/test/cli/install/registry/packages/hoist-lockfile-shared/hoist-lockfile-shared-2.0.2.tgz differ diff --git a/test/cli/install/registry/packages/hoist-lockfile-shared/package.json b/test/cli/install/registry/packages/hoist-lockfile-shared/package.json new file mode 100644 index 0000000000..969fa9dd4e --- /dev/null +++ b/test/cli/install/registry/packages/hoist-lockfile-shared/package.json @@ -0,0 +1,104 @@ +{ + "name": "hoist-lockfile-shared", + "versions": { + "1.0.1": { + "name": "hoist-lockfile-shared", + "version": "1.0.1", + "_id": "hoist-lockfile-shared@1.0.1", + "_integrity": "sha512-wPw8pTRj2OeZ/n7NeixjaSeI7FoM9DbMHWzdLv1kuBesSXJn+17UA0N7LV7t9dREnIMLw7ycRomhDL+56NRBmQ==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-wPw8pTRj2OeZ/n7NeixjaSeI7FoM9DbMHWzdLv1kuBesSXJn+17UA0N7LV7t9dREnIMLw7ycRomhDL+56NRBmQ==", + "shasum": "fde345ca6900e410c285e0468c8baf1e97400899", + "dist": { + "integrity": "sha512-wPw8pTRj2OeZ/n7NeixjaSeI7FoM9DbMHWzdLv1kuBesSXJn+17UA0N7LV7t9dREnIMLw7ycRomhDL+56NRBmQ==", + "shasum": "fde345ca6900e410c285e0468c8baf1e97400899", + "tarball": "http://http://localhost:4873/hoist-lockfile-shared/-/hoist-lockfile-shared-1.0.1.tgz" + }, + "contributors": [] + }, + "1.0.2": { + "name": "hoist-lockfile-shared", + "version": "1.0.2", + "_id": "hoist-lockfile-shared@1.0.2", + "_integrity": "sha512-p7IQ/BbkTRLG/GUx6j2cDQ+vTUc/v9OW9Ss9igh/GFysbr0Qjriz/DiETnISkxYaTFitqOkUSOUkEKyeLNJsfQ==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-p7IQ/BbkTRLG/GUx6j2cDQ+vTUc/v9OW9Ss9igh/GFysbr0Qjriz/DiETnISkxYaTFitqOkUSOUkEKyeLNJsfQ==", + "shasum": "027dac365f2e611c171bbca4f3cad0d0dd17d3c1", + "dist": { + "integrity": "sha512-p7IQ/BbkTRLG/GUx6j2cDQ+vTUc/v9OW9Ss9igh/GFysbr0Qjriz/DiETnISkxYaTFitqOkUSOUkEKyeLNJsfQ==", + "shasum": "027dac365f2e611c171bbca4f3cad0d0dd17d3c1", + "tarball": "http://http://localhost:4873/hoist-lockfile-shared/-/hoist-lockfile-shared-1.0.2.tgz" + }, + "contributors": [] + }, + "2.0.1": { + "name": "hoist-lockfile-shared", + "version": "2.0.1", + "_id": "hoist-lockfile-shared@2.0.1", + "_integrity": "sha512-r5/0DwsWyuolaBZtsYFTP0jBhwvDGNIekb/KrgBlPSROcYEgggIFBAuMPe2uU5CrsLkSH+MTc++K/reLLXgHNQ==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-r5/0DwsWyuolaBZtsYFTP0jBhwvDGNIekb/KrgBlPSROcYEgggIFBAuMPe2uU5CrsLkSH+MTc++K/reLLXgHNQ==", + "shasum": "e125f0cb27dd6e6fd8871060dbf6fba36ad1f917", + "dist": { + "integrity": "sha512-r5/0DwsWyuolaBZtsYFTP0jBhwvDGNIekb/KrgBlPSROcYEgggIFBAuMPe2uU5CrsLkSH+MTc++K/reLLXgHNQ==", + "shasum": "e125f0cb27dd6e6fd8871060dbf6fba36ad1f917", + "tarball": "http://http://localhost:4873/hoist-lockfile-shared/-/hoist-lockfile-shared-2.0.1.tgz" + }, + "contributors": [] + }, + "2.0.2": { + "name": "hoist-lockfile-shared", + "version": "2.0.2", + "_id": "hoist-lockfile-shared@2.0.2", + "_integrity": "sha512-xPWoyP8lv+/JrbClRzhJx1eUsHqDflSTmWOxx82xvMIEs6mbiIuvIp3/L+Ojc6mqex6y426h7L5j0hjLZE3V9w==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-xPWoyP8lv+/JrbClRzhJx1eUsHqDflSTmWOxx82xvMIEs6mbiIuvIp3/L+Ojc6mqex6y426h7L5j0hjLZE3V9w==", + "shasum": "1ca0ded36e6aa42702055f61e9d65083831ae849", + "dist": { + "integrity": "sha512-xPWoyP8lv+/JrbClRzhJx1eUsHqDflSTmWOxx82xvMIEs6mbiIuvIp3/L+Ojc6mqex6y426h7L5j0hjLZE3V9w==", + "shasum": "1ca0ded36e6aa42702055f61e9d65083831ae849", + "tarball": "http://http://localhost:4873/hoist-lockfile-shared/-/hoist-lockfile-shared-2.0.2.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-12-15T09:57:52.172Z", + "created": "2024-12-15T09:57:36.725Z", + "1.0.1": "2024-12-15T09:57:36.725Z", + "1.0.2": "2024-12-15T09:57:44.209Z", + "2.0.1": "2024-12-15T09:57:49.265Z", + "2.0.2": "2024-12-15T09:57:52.172Z" + }, + "users": {}, + "dist-tags": { + "latest": "2.0.2" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "hoist-lockfile-shared-1.0.1.tgz": { + "shasum": "fde345ca6900e410c285e0468c8baf1e97400899", + "version": "1.0.1" + }, + "hoist-lockfile-shared-1.0.2.tgz": { + "shasum": "027dac365f2e611c171bbca4f3cad0d0dd17d3c1", + "version": "1.0.2" + }, + "hoist-lockfile-shared-2.0.1.tgz": { + "shasum": "e125f0cb27dd6e6fd8871060dbf6fba36ad1f917", + "version": "2.0.1" + }, + "hoist-lockfile-shared-2.0.2.tgz": { + "shasum": "1ca0ded36e6aa42702055f61e9d65083831ae849", + "version": "2.0.2" + } + }, + "_rev": "", + "_id": "hoist-lockfile-shared", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/run/cjs-fixture-bad.cjs b/test/cli/run/cjs-fixture-bad.cjs new file mode 100644 index 0000000000..a502af2f4b --- /dev/null +++ b/test/cli/run/cjs-fixture-bad.cjs @@ -0,0 +1,7 @@ +// @bun @bun-cjs + +function f() { + console.log("This module has been evaluated!"); +} + +f(); diff --git a/test/cli/run/commonjs-invalid.test.ts b/test/cli/run/commonjs-invalid.test.ts new file mode 100644 index 0000000000..b0461dcd62 --- /dev/null +++ b/test/cli/run/commonjs-invalid.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; +import { join } from "path"; + +test("Loading an invalid commonjs module", () => { + const { stderr, exitCode } = Bun.spawnSync({ + cmd: [bunExe(), "run", join(import.meta.dir, "cjs-fixture-bad.cjs")], + env: bunEnv, + stdout: "inherit", + stderr: "pipe", + stdin: "inherit", + }); + + expect(stderr.toString().trim()).toContain("Expected CommonJS module to have a function wrapper"); + expect(exitCode).toBe(1); +}); diff --git a/test/cli/run/esm-fixture-leak-small.mjs b/test/cli/run/esm-fixture-leak-small.mjs index a045d59c5d..38abc2121a 100644 --- a/test/cli/run/esm-fixture-leak-small.mjs +++ b/test/cli/run/esm-fixture-leak-small.mjs @@ -20,12 +20,22 @@ setTimeout(() => { let diff = process.memoryUsage.rss() - baseline; diff = (diff / 1024 / 1024) | 0; console.log({ leaked: diff + " MB" }); + // This test seems to be more flaky on slow filesystems. // This used to be 40 MB, but the original version of Bun which this triggered on would reach 120 MB - // so we can increase it to 60 and still catch the leak. - if (diff > 60) { + // so we can increase it to 100 and still catch the leak. + // + // ❯ bunx bun@1.0.0 --smol test/cli/run/esm-fixture-leak-small.mjs + // { + // leaked: "100 MB" + // } + // ❯ bunx bun@1.1.0 --smol test/cli/run/esm-fixture-leak-small.mjs + // { + // leaked: "38 MB", + // } + if (diff >= 100) { console.log("\n--fail--\n"); process.exit(1); } else { console.log("\n--pass--\n"); } -}, 16); +}, 24); diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index a6c402c8a6..8a6b064a77 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -86,6 +86,8 @@ function runInCwdSuccess({ antipattern, command = ["present"], auto = false, + env = {}, + elideCount, }: { cwd: string; pattern: string | string[]; @@ -93,22 +95,32 @@ function runInCwdSuccess({ antipattern?: RegExp | RegExp[]; command?: string[]; auto?: boolean; + env?: Record; + elideCount?: number; }) { const cmd = auto ? [bunExe()] : [bunExe(), "run"]; + + // Add elide-lines first if specified + if (elideCount !== undefined) { + cmd.push("--elide-lines", elideCount.toString()); + } + if (Array.isArray(pattern)) { for (const p of pattern) { cmd.push("--filter", p); } } else { - cmd.push("--filter", pattern); + cmd.push("-F", pattern); } + for (const c of command) { cmd.push(c); } + const { exitCode, stdout, stderr } = spawnSync({ - cwd: cwd, - cmd: cmd, - env: bunEnv, + cwd, + cmd, + env: { ...bunEnv, ...env }, stdout: "pipe", stderr: "pipe", }); @@ -416,4 +428,82 @@ describe("bun", () => { expect(stdoutval).toMatch(/code 23/); expect(exitCode).toBe(23); }); + + function runElideLinesTest({ + elideLines, + target_pattern, + antipattern, + win32ExpectedError, + }: { + elideLines: number; + target_pattern: RegExp[]; + antipattern?: RegExp[]; + win32ExpectedError: RegExp; + }) { + const dir = tempDirWithFiles("testworkspace", { + packages: { + dep0: { + "index.js": Array(20).fill("console.log('log_line');").join("\n"), + "package.json": JSON.stringify({ + name: "dep0", + scripts: { + script: `${bunExe()} run index.js`, + }, + }), + }, + }, + "package.json": JSON.stringify({ + name: "ws", + workspaces: ["packages/*"], + }), + }); + + if (process.platform === "win32") { + const { exitCode, stderr } = spawnSync({ + cwd: dir, + cmd: [bunExe(), "run", "--filter", "./packages/dep0", "--elide-lines", String(elideLines), "script"], + env: { ...bunEnv, FORCE_COLOR: "1", NO_COLOR: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + expect(stderr.toString()).toMatch(win32ExpectedError); + expect(exitCode).not.toBe(0); + return; + } + + runInCwdSuccess({ + cwd: dir, + pattern: "./packages/dep0", + env: { FORCE_COLOR: "1", NO_COLOR: "0" }, + target_pattern, + antipattern, + command: ["script"], + elideCount: elideLines, + }); + } + + test("elides output by default when using --filter", () => { + runElideLinesTest({ + elideLines: 10, + target_pattern: [/\[10 lines elided\]/, /(?:log_line[\s\S]*?){20}/], + win32ExpectedError: /--elide-lines is only supported in terminal environments/, + }); + }); + + test("respects --elide-lines argument", () => { + runElideLinesTest({ + elideLines: 15, + target_pattern: [/\[5 lines elided\]/, /(?:log_line[\s\S]*?){20}/], + win32ExpectedError: /--elide-lines is only supported in terminal environments/, + }); + }); + + test("--elide-lines=0 shows all output", () => { + runElideLinesTest({ + elideLines: 0, + target_pattern: [/(?:log_line[\s\S]*?){20}/], + antipattern: [/lines elided/], + win32ExpectedError: /--elide-lines is only supported in terminal environments/, + }); + }); }); diff --git a/test/cli/run/if-present.test.ts b/test/cli/run/if-present.test.ts index 824ee4a239..96e712a466 100644 --- a/test/cli/run/if-present.test.ts +++ b/test/cli/run/if-present.test.ts @@ -50,7 +50,7 @@ describe("bun", () => { stderr: "pipe", }); expect(stdout.toString()).toBeEmpty(); - expect(stderr.toString()).toMatch(/File not found/); + expect(stderr.toString()).toMatch(/Module not found/); expect(exitCode).toBe(1); }); }); diff --git a/test/cli/run/require-cache.test.ts b/test/cli/run/require-cache.test.ts index dc45bd828a..a950c27a5a 100644 --- a/test/cli/run/require-cache.test.ts +++ b/test/cli/run/require-cache.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, isBroken, isIntelMacOS, isWindows, tempDirWithFiles } from "harness"; import { join } from "path"; test("require.cache is not an empty object literal when inspected", () => { @@ -32,7 +32,7 @@ test("require.cache does not include unevaluated modules", () => { expect(exitCode).toBe(0); }); -describe("files transpiled and loaded don't leak the output source code", () => { +describe.skipIf(isBroken && isIntelMacOS)("files transpiled and loaded don't leak the output source code", () => { test("via require() with a lot of long export names", () => { let text = ""; for (let i = 0; i < 10000; i++) { diff --git a/test/cli/run/syntax.test.ts b/test/cli/run/syntax.test.ts new file mode 100644 index 0000000000..8da864184f --- /dev/null +++ b/test/cli/run/syntax.test.ts @@ -0,0 +1,340 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { join } from "path"; + +const exitCode0 = [ + " ", + "\n", + "\t", + "\r\n", + " ", + "\n\n\n", + "export default '\\u{000C}'", + "export default '\\u{200B}'", + "export default '\\u{200C}'", + "export default '\\u{200D}'", + "export default '\\u{FEFF}'", + '"use strict";', + '"use strict";\n', + "'use strict';", + '"use strict";\n\n', + '"use asm";', + '"use asm";\n', + "'use strict'; 'use asm';", + '"use strict"; "use strict";', + "// empty comment", + "/* empty block comment */", + "/** JSDoc comment */", + "//\n//\n//", + "/* \n Multi\n line\n comment\n */", + "-->", + "// TODO: ", + "/** @type {number} */", + "const a = 123;", + "let a;", + "var a = undefined;", + "const a = null;", + "const [a] = [];", + "const {a} = {};", + "const [...a] = [];", + "const {...a} = {};", + "let [a = 1] = [];", + "let {a: b = 1} = {};", + "``", + "`template`", + "`template${123}`", + "`${null}`", + "`${'`'}`", + "`\n`", + "`\n`", + "({});", + "({ a: 1 });", + "({ ['computed']: 1 });", + "({ get x() { return 1; } });", + "({ __proto__: null });", + "({ get [1+2](){}, set [1+2](x){} });", + "({ a: 1, ...{b: 2} });", + "[];", + "[,];", + "[,,];", + "[1,];", + "[,...[1]];", + "[,,...[1]];", + "[[[[[]]]]]", + "[...[...[...[]]]]", + "[1,,2,,3];", + "Array(1_000_000).fill(1);", + "()=>{};", + "async()=>{};", + "(function(){}).bind(null);", + "()=>()=>()=>1;", + "function f(a=1,{b}={},...c){};", + "async function* f(){await 1; yield 2;};", + "(async()=>await 1)();", + "class A{}", + "class A extends null{}", + "class A{#private=1}", + "class A{static{}}", + "class A{get #a(){} set #a(x){}}", + "class A{*#gen(){yield this.#p}#p=1}", + "class A extends class B{}{};", + "export {};", + "export default 1;", + "export default function(){};", + "import.meta.url;", + "const pi = Math.PI;", + "const hello = 'world';", + "const star = '⭐';", + "'\\u0000\\uFFFF';", + "'\\x00\\xFF';", + "const \\u{61} = 1;", + "'\\0';", + "'\\v\\f\\b';", + "'\\\r\n';", + "/./;", + "/[]/;", + "/[^]/;", + "/\\u{0}/u;", + "/[\\u0000-\\u{10FFFF}]/u;", + "/./gimsuyd;", + "/\\p{Script=Latin}/u;", + "/(?<=a)b/;", + "/(?/;", + "1n;", + "0n;", + "9007199254740991n;", + "0b1n;", + "0o7n;", + "0xFn;", + "1_2_3n;", + "BigInt(1);", + "-0n;", + "~0n;", + "0b1_0_1;", + "0o7_7_7;", + "0xF_F_F;", + "1_2_3_4;", + "try{}catch{}", + "try{}catch(e){}", + "try{}catch{}finally{}", + "try{}finally{}", + "try{throw 1}catch(e){}", + "try{throw new Error}catch{}", + "try{throw{}}catch({message}){}", + "try{throw null}catch(e){}", + "try{throw undefined}catch(e){}", + "try{throw function(){}}catch(e){}", + "function*g(){yield;}", + "function*g(){yield*g();}", + "async function*g(){yield;}", + "async function*g(){await 1;}", + "(async function*(){for await(x of y){}});", + "function*g(){try{yield}finally{}}", + "[...new Set];", + "for(x of[]){}", + "for await(x of[]){}", + "async function f(){for await(x of y){}}", + "void 0;", + "new (class{})();", + "(class extends null{});", + "(class{[Symbol.hasInstance](){}});", + "function*g(){yield yield yield;}", + "1..toString();", + "`${`${`${1}`}`}`", + "String.raw`\n`", + "String.raw`\\n`", + "`${`${function*(){yield 1}}`}`", + "`${class{}}${()=>{}}${/./}`", + "`${`${async()=>await 1}`}`", + "`${`${class{static{``}}}`}`", + "await import('bun');", + "await import('bun:ffi');", + "await import(import.meta.path);", + "/(?:)/", + "/\\b\\B\\d\\D\\w\\W\\s\\S/", + "/\\cA\\cZ\\ca\\cz/", + "/\\p{General_Category=Letter}/u", + "/\\p{Script_Extensions=Latin}/u", + "/(?<=(?=a)b)c/", + "/(?\\k/", + "/\\u{10FFFF}\\u{0000}/u", + "/[\\p{ASCII}--\\p{Decimal_Number}]/v", + "const [{a: [b = 1] = []} = [{a: [2]}]] = [];", + "let {a: {b: {c = 1} = {}} = {}} = {};", + "const [a, , ...{0: b, length}] = [];", + "const {[class{}.prop]: x} = {};", + "const {[()=>{}]: x} = {};", + "let {[/./]: x} = {};", + "const {[class extends null{}]: x} = {};", + "const {[function*(){}]: x} = {};", + "function f([a] = [1], {b} = {b: 2}, ...c){}", + "(function({[key]: value} = {}){})", + "(({[this?.prop]: x} = {})=>{})", + "function f(a = () => b, b = 1){}", + "(a = class{}, b = new a)=>{}", + "function f(a = new.target){}", + "for await(const x of async function*(){yield 1}()){}", + "for(const x of function*(){yield 1}()){}", + "for(const {[key]: x} of []){}", + "for(let [{a = 1} = {}] of []){}", + "do{continue}while(false);", + "label:do{break label}while(false);", + "outer:for(;;)inner:break outer;", + "block:{break block;}", + "while(false)with(null);", + "switch(0){case 1:default:case 2:}", + "try{throw async()=>{}}catch(e){await e}", + "try{throw class{}}catch(e){new e}", + "try{throw{[Symbol.iterator]:()=>{}}}catch(e){}", + "try{throw Object.create(null)}catch(e){}", + "try{throw new Proxy({},{})}catch(e){}", + "try{throw new WeakMap}catch(e){}", + "try{throw new Int32Array}catch(e){}", + "try{throw new SharedArrayBuffer(0)}catch(e){}", + "try{throw new WebAssembly.Module(new Uint8Array)}catch(e){}", + "void void void 0;", + "typeof typeof typeof 0;", + "delete (delete (delete 0));", + "await (await (await 1));", + "--{a:1}.a;", + "(class{}).prototype.constructor;", + "(()=>{}).prototype?.call;", + "(async()=>{}).prototype?.call;", + "(function*(){}).prototype.next;", + "(async function*(){}).prototype.next;", + "this?.prop?.[key]?.();", + "0b0_1_0_1;", + "0B0_1_0_1;", + "0o0_1_2_3;", + "0O0_1_2_3;", + "0x0_1_2_3;", + "0X0_1_2_3;", + "1_2_3.4_5_6;", + "1_2_3e1_2_3;", + "1_2_3E1_2_3;", + ".0_1_2_3;", + "0b1_0_1n;", + "0B1_0_1n;", + "0o1_2_3n;", + "0O1_2_3n;", + "0x1_2_3n;", + "0X1_2_3n;", + "1_2_3_4_5n;", + "BigInt(0b101);", + "BigInt(0o777);", + "BigInt(0xff);", + "for(const [,,...a] of [[]]){}", + "for(let {a: [b]} of []){}", + "for await(const [a = await 1] of []){}", + "for(const {[await 1]: x} of []){}", + "for(var {[class{}]: x} of []){}", + "for(let {[/./]: x} of []){}", + "for(const {[`${a}`]: x} of []){}", + "for await(const x of async function*(){yield*[1,2,3]}()){}", + "new class extends (await Promise.resolve(class{})){}", + "function*g(){yield yield*function*(){yield yield yield;}();}", + "async function*f(){yield await Promise.all([1,2,3])}", + "async function*f(){yield*[await 1,await 2]}", + "(async function(){await (async()=>1)()})();", + "async function*f(){for await(const x of async function*(){yield 1}())yield x}", + "async function f(){return await new Promise(r=>r(1))}", + "async function*f(){try{yield await 1}finally{await 2}}", + "(async()=>{for await(const x of[])for await(const y of[]){}})();", + "async function*f(){yield await(yield await(yield await 1))}", + "async function*f(){yield*async function*(){yield await 1}()}", + "export default await(async()=>1)();", + "function*g(){yield*{[Symbol.iterator]:function*(){yield 1}}}", + "function*g(){yield*{async*[Symbol.asyncIterator](){yield 1}}}", + "function*g(){yield*function*(){yield function*(){yield 1}}()}", + "function*g(){yield*{get[Symbol.iterator](){return function*(){yield 1}}}}", + "function*g(){yield*(class{static*[Symbol.iterator](){yield 1}});}", + "function*g(){yield*class{static*[Symbol.iterator](){yield 1}}}", + "function*g(){yield*{*[Symbol.iterator](){yield class{}}}}", + "function*g(){yield*{*[Symbol.iterator](){yield function(){}}}}", + "function*g(){yield*{*[Symbol.iterator](){yield async()=>{}}}}", + "function*g(){yield*{*[Symbol.iterator](){yield function*(){}}}}", + "({[function*(){yield 1}]:1});", + "({[async function*(){yield 1}]:1});", + "({[(()=>class{})()]:1});", + "({[new class{valueOf(){return 1}}]:2});", + "({[new class{[Symbol.toPrimitive](){return 1}}]:2});", + "({[new class{toString(){return '1'}}]:2});", + "({[new class{get [Symbol.toPrimitive](){return()=>1}}]:2});", + "({[new class{static{this.prototype.valueOf=()=>1}}]:2});", + "label:while(1)try{continue label}finally{break label}", + "new Proxy(class{},{construct(){return{}}});", + "new Proxy(function(){},{apply(){return{}}});", + "new Proxy({},{get(){return new Proxy({},{})}})", + "new Proxy([],{get(){return new Proxy({},{})}});", + "new Proxy(function(){},{construct(){return new Proxy({},{})}})", + "new Proxy(class{},{construct(){return new Proxy({},{})}})", + "new Proxy({},{get(){return function(){return new Proxy({},{})}}})", + "new Proxy(function(){},{apply(){return new Proxy(class{},{})}})", + "new Proxy(class{},{construct(){return new Proxy(function(){},{})}})", + "Reflect.get(new Proxy({},{}),'')", + "Reflect.set(new Proxy({},{}),'')", + "Reflect.has(new Proxy({},{}),'')", + "Reflect.deleteProperty(new Proxy({},{}),'')", + "Reflect.getOwnPropertyDescriptor(new Proxy({},{}),'')", + "Reflect.getPrototypeOf(new Proxy({},{}))", + "Reflect.setPrototypeOf(new Proxy({},{}),{})", + "Reflect.isExtensible(new Proxy({},{}))", + "Reflect.preventExtensions(new Proxy({},{}))", + "Promise.all([Promise.race([]),Promise.allSettled([])])", + "Promise.race([Promise.all([]),Promise.any([])])", + "Promise.allSettled([Promise.race([]),Promise.all([])])", + "Promise.any([Promise.allSettled([]),Promise.race([])])", + "Promise.resolve(Promise.reject().catch(()=>Promise.all([])))", + "new Function(`return function*(){${`yield${`yield${`yield`}`}`}}`)();", + "new Function(`return async()=>${`await${`await${`1`}`}`}`)();", + "new Function(`return class{${`*[Symbol.iterator](){${`yield 1`}}`}}`)();", + "new Function(`return class{${`async*[Symbol.asyncIterator](){${`yield 1`}}`}}`)();", + "new Function(`return class{${`get #a(){${`return 1`}}`}}`)();", + "new Function(`return class{${`set #a(v){${`this.#b=v`}}#b`}}`)();", + "new Function(`return class{${`#a;${`#b;${`#c`}`}`}}`)();", + "for await(let {...x} of async function*(){yield*[]}()){}", + "for await(const x of (async function*(){yield await 1})()){}", + "function f(){let a=b,b=1}", + "(function(){const[a=b,b=1]=[]})", + "(()=>{let{a=b,b=1}={}})", +]; + +describe("exit code 0", () => { + const fixturePath = tempDirWithFiles("fixture", { + [`package.json`]: `{ + "name": "test", + + }`, + "fixture.js": "export default 1", + }); + + for (let i = 0; i < exitCode0.length; i++) { + const source = exitCode0[i]; + + test(`file #${i}: ${JSON.stringify(source)}`, async () => { + await Bun.write(join(fixturePath, "fixture.js"), source); + await using proc = Bun.spawn([bunExe(), "./fixture.js"], { + stdout: "inherit", + env: bunEnv, + cwd: fixturePath, + stderr: "inherit", + stdin: "inherit", + }); + const exitCode = await proc.exited; + expect(exitCode).toBe(0); + }); + + test(`eval #${i}: ${JSON.stringify(source)}`, async () => { + await using proc = Bun.spawn([bunExe(), "--eval", source], { + stdout: "inherit", + env: bunEnv, + stderr: "inherit", + stdin: "inherit", + }); + const exitCode = await proc.exited; + expect(exitCode).toBe(0); + }); + } +}); diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index d4d2e83a19..cc9f72a03c 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -577,7 +577,7 @@ describe("bun test", () => { GITHUB_ACTIONS: "true", }, }); - expect(stderr).toMatch(/::error title=error: Oops!::/); + expect(stderr).toMatch(/::error file=.*,line=\d+,col=\d+,title=error: Oops!::/m); }); test("should annotate a test timeout", () => { const stderr = runTest({ diff --git a/test/cli/test/expectations.test.ts b/test/cli/test/expectations.test.ts new file mode 100644 index 0000000000..20698874da --- /dev/null +++ b/test/cli/test/expectations.test.ts @@ -0,0 +1,11 @@ +describe(".toThrow()", () => { + it(".toThrow() behaves the same as .toThrow('')", () => { + expect(() => { + throw new Error("test"); + }).toThrow(); + + expect(() => { + throw new Error("test"); + }).toThrow(""); + }); +}); diff --git a/test/harness.ts b/test/harness.ts index 8754c256ea..95a84519d3 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,11 +1,12 @@ -import { gc as bunGC, sleepSync, spawnSync, unsafe, which } from "bun"; +import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun"; import { heapStats } from "bun:jsc"; +import { fork, ChildProcess } from "child_process"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { readFile, readlink, writeFile } from "fs/promises"; -import fs, { closeSync, openSync } from "node:fs"; +import { readFile, readlink, writeFile, readdir, rm } from "fs/promises"; +import fs, { closeSync, openSync, rmSync } from "node:fs"; import os from "node:os"; import { dirname, isAbsolute, join } from "path"; -import detect_libc from "detect-libc"; +import detectLibc from "detect-libc"; type Awaitable = T | Promise; @@ -18,8 +19,11 @@ export const isWindows = process.platform === "win32"; export const isIntelMacOS = isMacOS && process.arch === "x64"; export const isDebug = Bun.version.includes("debug"); export const isCI = process.env.CI !== undefined; +export const libcFamily = detectLibc.familySync() as "glibc" | "musl"; +export const isMusl = isLinux && libcFamily === "musl"; +export const isGlibc = isLinux && libcFamily === "glibc"; export const isBuildKite = process.env.BUILDKITE === "true"; -export const libc_family = detect_libc.familySync(); +export const isVerbose = process.env.DEBUG === "1"; // Use these to mark a test as flaky or broken. // This will help us keep track of these tests. @@ -42,18 +46,27 @@ export const bunEnv: NodeJS.ProcessEnv = { BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE: "1", }; +const ciEnv = { ...bunEnv }; + if (isWindows) { bunEnv.SHELLOPTS = "igncr"; // Ignore carriage return } for (let key in bunEnv) { if (bunEnv[key] === undefined) { + delete ciEnv[key]; delete bunEnv[key]; } if (key.startsWith("BUN_DEBUG_") && key !== "BUN_DEBUG_QUIET_LOGS") { + delete ciEnv[key]; delete bunEnv[key]; } + + if (key.startsWith("BUILDKITE")) { + delete bunEnv[key]; + delete process.env[key]; + } } delete bunEnv.NODE_ENV; @@ -143,7 +156,7 @@ export function hideFromStackTrace(block: CallableFunction) { }); } -type DirectoryTree = { +export type DirectoryTree = { [name: string]: | string | Buffer @@ -151,25 +164,29 @@ type DirectoryTree = { | ((opts: { root: string }) => Awaitable); }; -export function tempDirWithFiles(basename: string, files: DirectoryTree): string { - async function makeTree(base: string, tree: DirectoryTree) { - for (const [name, raw_contents] of Object.entries(tree)) { - const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents; - const joined = join(base, name); - if (name.includes("/")) { - const dir = dirname(name); - if (dir !== name && dir !== ".") { - fs.mkdirSync(join(base, dir), { recursive: true }); - } +export async function makeTree(base: string, tree: DirectoryTree) { + const isDirectoryTree = (value: string | DirectoryTree | Buffer): value is DirectoryTree => + typeof value === "object" && value && typeof value?.byteLength === "undefined"; + + for (const [name, raw_contents] of Object.entries(tree)) { + const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents; + const joined = join(base, name); + if (name.includes("/")) { + const dir = dirname(name); + if (dir !== name && dir !== ".") { + fs.mkdirSync(join(base, dir), { recursive: true }); } - if (typeof contents === "object" && contents && typeof contents?.byteLength === "undefined") { - fs.mkdirSync(joined); - makeTree(joined, contents); - continue; - } - fs.writeFileSync(joined, contents); } + if (isDirectoryTree(contents)) { + fs.mkdirSync(joined); + makeTree(joined, contents); + continue; + } + fs.writeFileSync(joined, contents); } +} + +export function tempDirWithFiles(basename: string, files: DirectoryTree): string { const base = fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()), basename + "_")); makeTree(base, files); return base; @@ -477,6 +494,32 @@ if (expect.extend) }; } }, + toBeLatin1String(actual: unknown) { + if ((actual as string).isLatin1()) { + return { + pass: true, + message: () => `Expected ${actual} to be a Latin1 string`, + }; + } + + return { + pass: false, + message: () => `Expected ${actual} to be a Latin1 string`, + }; + }, + toBeUTF16String(actual: unknown) { + if ((actual as string).isUTF16()) { + return { + pass: true, + message: () => `Expected ${actual} to be a UTF16 string`, + }; + } + + return { + pass: false, + message: () => `Expected ${actual} to be a UTF16 string`, + }; + }, }); export function ospath(path: string) { @@ -534,6 +577,10 @@ Received ${JSON.stringify({ name: onDisk.name, version: onDisk.version })}`, case "npm": const name = dep.is_alias ? dep.npm.name : dep.name; if (!Bun.deepMatch({ name, version: pkg.resolution.value }, resolved)) { + if (dep.literal === "*") { + // allow any version, just needs to be resolvable + continue; + } if (dep.behavior.peer && dep.npm) { // allow peer dependencies to not match exactly, but still satisfy if (Bun.semver.satisfies(pkg.resolution.value, dep.npm.version)) continue; @@ -573,6 +620,10 @@ Received ${JSON.stringify({ name: onDisk.name, version: onDisk.version })}`, case "npm": const name = dep.is_alias ? dep.npm.name : dep.name; if (!Bun.deepMatch({ name, version: pkg.resolution.value }, resolved)) { + if (dep.literal === "*") { + // allow any version, just needs to be resolvable + continue; + } // workspaces don't need a version if (treePkg.resolution.tag === "workspace" && !resolved.version) continue; if (dep.behavior.peer && dep.npm) { @@ -1118,36 +1169,6 @@ String.prototype.isUTF16 = function () { return require("bun:internal-for-testing").jscInternals.isUTF16String(this); }; -if (expect.extend) - expect.extend({ - toBeLatin1String(actual: unknown) { - if ((actual as string).isLatin1()) { - return { - pass: true, - message: () => `Expected ${actual} to be a Latin1 string`, - }; - } - - return { - pass: false, - message: () => `Expected ${actual} to be a Latin1 string`, - }; - }, - toBeUTF16String(actual: unknown) { - if ((actual as string).isUTF16()) { - return { - pass: true, - message: () => `Expected ${actual} to be a UTF16 string`, - }; - } - - return { - pass: false, - message: () => `Expected ${actual} to be a UTF16 string`, - }; - }, - }); - interface BunHarnessTestMatchers { toBeLatin1String(): void; toBeUTF16String(): void; @@ -1317,6 +1338,7 @@ export function getSecret(name: string): string | undefined { const { exitCode, stdout } = spawnSync({ cmd: ["buildkite-agent", "secret", "get", name], stdout: "pipe", + env: ciEnv, stderr: "inherit", }); if (exitCode === 0) { @@ -1363,16 +1385,16 @@ Object.defineProperty(globalThis, "gc", { configurable: true, }); -export function waitForFileToExist(path: string, interval: number) { +export function waitForFileToExist(path: string, interval_ms: number) { while (!fs.existsSync(path)) { - sleepSync(interval); + sleepSync(interval_ms); } } export function libcPathForDlopen() { switch (process.platform) { case "linux": - switch (libc_family) { + switch (libcFamily) { case "glibc": return "libc.so.6"; case "musl": @@ -1402,3 +1424,89 @@ export function rmScope(path: string) { }, }; } + +export function textLockfile(version: number, pkgs: any): string { + return JSON.stringify({ + lockfileVersion: version, + ...pkgs, + }); +} + +export class VerdaccioRegistry { + port: number; + process: ChildProcess | undefined; + configPath: string; + packagesPath: string; + + constructor(opts?: { configPath?: string; packagesPath?: string; verbose?: boolean }) { + this.port = randomPort(); + this.configPath = opts?.configPath ?? join(import.meta.dir, "cli", "install", "registry", "verdaccio.yaml"); + this.packagesPath = opts?.packagesPath ?? join(import.meta.dir, "cli", "install", "registry", "packages"); + } + + async start(silent: boolean = true) { + await rm(join(dirname(this.configPath), "htpasswd"), { force: true }); + this.process = fork(require.resolve("verdaccio/bin/verdaccio"), ["-c", this.configPath, "-l", `${this.port}`], { + silent, + // Prefer using a release build of Bun since it's faster + execPath: Bun.which("bun") || bunExe(), + }); + + this.process.stderr?.on("data", data => { + console.error(`[verdaccio] stderr: ${data}`); + }); + + const started = Promise.withResolvers(); + + this.process.on("error", error => { + console.error(`Failed to start verdaccio: ${error}`); + started.reject(error); + }); + + this.process.on("exit", (code, signal) => { + if (code !== 0) { + console.error(`Verdaccio exited with code ${code} and signal ${signal}`); + } else { + console.log("Verdaccio exited successfully"); + } + }); + + this.process.on("message", (message: { verdaccio_started: boolean }) => { + if (message.verdaccio_started) { + started.resolve(); + } + }); + + await started.promise; + } + + registryUrl() { + return `http://localhost:${this.port}/`; + } + + stop() { + rmSync(join(dirname(this.configPath), "htpasswd"), { force: true }); + this.process?.kill(); + } + + async createTestDir() { + const packageDir = tmpdirSync(); + const packageJson = join(packageDir, "package.json"); + await write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = "${join(packageDir, ".bun-cache")}" + registry = "${this.registryUrl()}" + `, + ); + + return { packageDir, packageJson }; + } +} + +export async function readdirSorted(path: string): Promise { + const results = await readdir(path); + results.sort(); + return results; +} diff --git a/test/http-test-server.ts b/test/http-test-server.ts index 4b94371f8f..17413ba000 100644 --- a/test/http-test-server.ts +++ b/test/http-test-server.ts @@ -67,6 +67,7 @@ function makeTestJsonResponse( } // Check to set headers headers.set("Content-Type", "text/plain"); + break; default: } diff --git a/test/integration/jsdom/jsdom.test.ts b/test/integration/jsdom/jsdom.test.ts new file mode 100644 index 0000000000..b816e33d73 --- /dev/null +++ b/test/integration/jsdom/jsdom.test.ts @@ -0,0 +1,14 @@ +import { test, expect, describe } from "bun:test"; +import { JSDOM } from "jsdom"; + +describe("jsdom", () => { + for (const runScripts of ["dangerously", "outside-only", undefined]) { + test(`runScripts: ${runScripts}`, () => { + const dom = new JSDOM(`

Hello World!

`, { + url: "https://example.com", + runScripts, + }); + expect(dom.window.document.querySelector("h1").textContent).toBe("Hello World!"); + }); + } +}); diff --git a/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap b/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap index da10c802c8..b1bb52a16f 100644 --- a/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap @@ -5,7 +5,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "dependencies": [ { "behavior": { - "normal": true, + "prod": true, }, "id": 0, "literal": "20.7.0", @@ -18,7 +18,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 1, "literal": "18.2.22", @@ -31,7 +31,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 2, "literal": "18.2.7", @@ -44,7 +44,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 3, "literal": "10.4.16", @@ -57,7 +57,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 4, "literal": "^1.0.3", @@ -70,7 +70,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 5, "literal": "8.50.0", @@ -83,7 +83,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 6, "literal": "14.1.3", @@ -96,7 +96,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 7, "literal": "14.1.3", @@ -109,7 +109,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 8, "literal": "8.4.30", @@ -122,7 +122,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 9, "literal": "22.12.0", @@ -135,7 +135,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 10, "literal": "18.2.0", @@ -148,7 +148,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 11, "literal": "18.2.0", @@ -161,7 +161,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 12, "literal": "3.3.3", @@ -174,7 +174,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 13, "literal": "5.2.2", @@ -187,7 +187,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 14, "literal": "^5.0.2", @@ -200,7 +200,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 15, "literal": "^1.1.3", @@ -213,7 +213,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 16, "literal": "^1.18.2", @@ -226,7 +226,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 17, "literal": "^4.0.3", @@ -239,7 +239,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 18, "literal": "^8.4.23", @@ -252,7 +252,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 19, "literal": "^1.22.2", @@ -265,7 +265,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 20, "literal": "^3.32.0", @@ -278,7 +278,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 21, "literal": "^3.5.3", @@ -291,7 +291,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 22, "literal": "^3.2.12", @@ -304,7 +304,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 23, "literal": "^2.1.0", @@ -317,7 +317,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 24, "literal": "^1.2.2", @@ -330,7 +330,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 25, "literal": "^4.0.5", @@ -343,7 +343,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 26, "literal": "^1.0.0", @@ -356,7 +356,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 27, "literal": "^4.0.1", @@ -369,7 +369,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 28, "literal": "^6.0.2", @@ -382,7 +382,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 29, "literal": "^3.0.0", @@ -395,7 +395,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 30, "literal": "^3.0.0", @@ -408,7 +408,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 31, "literal": "^15.1.0", @@ -421,7 +421,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 32, "literal": "^6.0.1", @@ -434,7 +434,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 33, "literal": "^5.2.0", @@ -447,7 +447,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 34, "literal": "^4.0.1", @@ -460,7 +460,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 35, "literal": "^6.0.11", @@ -473,7 +473,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 36, "literal": "^3.0.0", @@ -486,7 +486,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 37, "literal": "^1.0.2", @@ -499,7 +499,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 38, "literal": "^2.3.4", @@ -512,7 +512,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 39, "literal": "^3.0.0", @@ -553,7 +553,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 42, "literal": "^3.3.6", @@ -566,7 +566,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 43, "literal": "^1.0.0", @@ -579,7 +579,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 44, "literal": "^1.0.2", @@ -592,7 +592,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 45, "literal": "^6.0.11", @@ -618,7 +618,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 47, "literal": "^4.0.0", @@ -631,7 +631,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 48, "literal": "^1.0.0", @@ -644,7 +644,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 49, "literal": "^1.1.7", @@ -670,7 +670,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 51, "literal": "^2.13.0", @@ -683,7 +683,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 52, "literal": "^1.0.7", @@ -696,7 +696,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 53, "literal": "^1.0.0", @@ -709,7 +709,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 54, "literal": "^2.0.0", @@ -722,7 +722,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 55, "literal": "^1.1.2", @@ -735,7 +735,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 56, "literal": "^2.3.0", @@ -748,7 +748,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 57, "literal": "^4.0.3", @@ -761,7 +761,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 58, "literal": "^2.1.1", @@ -774,7 +774,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 59, "literal": "^2.0.1", @@ -800,7 +800,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 61, "literal": "^3.0.3", @@ -813,7 +813,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 62, "literal": "^2.3.1", @@ -826,7 +826,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 63, "literal": "^7.1.1", @@ -839,7 +839,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 64, "literal": "^5.0.1", @@ -852,7 +852,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 65, "literal": "^7.0.0", @@ -865,7 +865,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 66, "literal": "^2.0.2", @@ -878,7 +878,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 67, "literal": "^1.2.3", @@ -891,7 +891,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 68, "literal": "^5.1.2", @@ -904,7 +904,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 69, "literal": "^1.3.0", @@ -917,7 +917,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 70, "literal": "^4.0.4", @@ -930,7 +930,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 71, "literal": "^4.0.1", @@ -943,7 +943,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 72, "literal": "2.1.5", @@ -956,7 +956,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 73, "literal": "^1.6.0", @@ -969,7 +969,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 74, "literal": "^1.0.4", @@ -982,7 +982,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 75, "literal": "2.0.5", @@ -995,7 +995,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 76, "literal": "^1.1.9", @@ -1008,7 +1008,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 77, "literal": "^1.2.2", @@ -1021,7 +1021,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 78, "literal": "~3.1.2", @@ -1034,7 +1034,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 79, "literal": "~3.0.2", @@ -1047,7 +1047,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 80, "literal": "~5.1.2", @@ -1060,7 +1060,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 81, "literal": "~2.1.0", @@ -1073,7 +1073,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 82, "literal": "~4.0.1", @@ -1086,7 +1086,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 83, "literal": "~3.0.0", @@ -1099,7 +1099,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 84, "literal": "~3.6.0", @@ -1125,7 +1125,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 86, "literal": "^2.2.1", @@ -1138,7 +1138,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 87, "literal": "^2.0.0", @@ -1151,7 +1151,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 88, "literal": "^3.0.0", @@ -1164,7 +1164,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 89, "literal": "^2.0.4", @@ -1177,7 +1177,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 90, "literal": "^0.3.2", @@ -1190,7 +1190,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 91, "literal": "^4.0.0", @@ -1203,7 +1203,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 92, "literal": "^10.3.10", @@ -1216,7 +1216,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 93, "literal": "^1.1.6", @@ -1229,7 +1229,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 94, "literal": "^2.7.0", @@ -1242,7 +1242,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 95, "literal": "^4.0.1", @@ -1255,7 +1255,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 96, "literal": "^0.1.9", @@ -1268,7 +1268,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 97, "literal": "^1.0.0", @@ -1281,7 +1281,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 98, "literal": "^4.0.1", @@ -1294,7 +1294,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 99, "literal": "^1.0.0", @@ -1307,7 +1307,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 100, "literal": ">= 3.1.0 < 4", @@ -1320,7 +1320,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 101, "literal": "^1.0.0", @@ -1333,7 +1333,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 102, "literal": "^3.1.0", @@ -1346,7 +1346,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 103, "literal": "^2.3.5", @@ -1359,7 +1359,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 104, "literal": "^9.0.1", @@ -1372,7 +1372,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 105, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -1385,7 +1385,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 106, "literal": "^1.10.1", @@ -1398,7 +1398,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 107, "literal": "^10.2.0", @@ -1411,7 +1411,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 108, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -1424,7 +1424,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 109, "literal": "^2.0.1", @@ -1437,7 +1437,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 110, "literal": "^1.0.0", @@ -1450,7 +1450,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 111, "literal": "^8.0.2", @@ -1476,7 +1476,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 113, "literal": "^5.1.2", @@ -1489,7 +1489,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 114, "is_alias": true, @@ -1503,7 +1503,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 115, "literal": "^7.0.1", @@ -1516,7 +1516,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 116, "is_alias": true, @@ -1530,7 +1530,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 117, "literal": "^8.1.0", @@ -1543,7 +1543,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 118, "is_alias": true, @@ -1557,7 +1557,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 119, "literal": "^4.0.0", @@ -1570,7 +1570,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 120, "literal": "^4.1.0", @@ -1583,7 +1583,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 121, "literal": "^6.0.0", @@ -1596,7 +1596,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 122, "literal": "^5.0.1", @@ -1609,7 +1609,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 123, "literal": "^8.0.0", @@ -1622,7 +1622,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 124, "literal": "^3.0.0", @@ -1635,7 +1635,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 125, "literal": "^6.0.1", @@ -1648,7 +1648,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 126, "literal": "^2.0.1", @@ -1661,7 +1661,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 127, "literal": "~1.1.4", @@ -1674,7 +1674,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 128, "literal": "^6.1.0", @@ -1687,7 +1687,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 129, "literal": "^5.0.1", @@ -1700,7 +1700,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 130, "literal": "^7.0.1", @@ -1713,7 +1713,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 131, "literal": "^6.0.1", @@ -1726,7 +1726,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 132, "literal": "^0.2.0", @@ -1739,7 +1739,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 133, "literal": "^9.2.2", @@ -1752,7 +1752,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 134, "literal": "^7.0.1", @@ -1765,7 +1765,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 135, "literal": "^7.0.0", @@ -1778,7 +1778,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 136, "literal": "^4.0.1", @@ -1791,7 +1791,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 137, "literal": "^3.1.0", @@ -1804,7 +1804,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 138, "literal": "^2.0.0", @@ -1817,7 +1817,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 139, "literal": "^2.0.1", @@ -1830,7 +1830,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 140, "literal": "^2.0.0", @@ -1843,7 +1843,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 141, "literal": "^3.0.0", @@ -1856,7 +1856,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 142, "literal": "^1.2.1", @@ -1869,7 +1869,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 143, "literal": "^1.4.10", @@ -1882,7 +1882,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 144, "literal": "^0.3.24", @@ -1895,7 +1895,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 145, "literal": "^3.1.0", @@ -1908,7 +1908,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 146, "literal": "^1.4.14", @@ -1921,7 +1921,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 147, "literal": "^0.23.0", @@ -1934,7 +1934,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 148, "literal": "^1.1.0", @@ -1960,7 +1960,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 150, "literal": "^1.1.0", @@ -1973,7 +1973,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 151, "literal": "^3.0.0 || ^4.0.0", @@ -1986,7 +1986,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 152, "literal": "^1.1.0", @@ -1999,7 +1999,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 153, "literal": "9.0.0", @@ -2012,7 +2012,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 154, "literal": "22.12.0", @@ -2025,7 +2025,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 155, "literal": "2.2.3", @@ -2038,7 +2038,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 156, "literal": "0.0.1299070", @@ -2051,7 +2051,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 157, "literal": "4.3.4", @@ -2064,7 +2064,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 158, "literal": "2.0.1", @@ -2077,7 +2077,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 159, "literal": "2.0.3", @@ -2090,7 +2090,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 160, "literal": "6.4.0", @@ -2103,7 +2103,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 161, "literal": "3.0.5", @@ -2116,7 +2116,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 162, "literal": "1.4.3", @@ -2129,7 +2129,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 163, "literal": "17.7.2", @@ -2142,7 +2142,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 164, "literal": "7.6.0", @@ -2155,7 +2155,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 165, "literal": "^6.0.0", @@ -2168,7 +2168,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 166, "literal": "^4.0.0", @@ -2181,7 +2181,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 167, "literal": "^8.0.1", @@ -2194,7 +2194,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 168, "literal": "^3.1.1", @@ -2207,7 +2207,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 169, "literal": "^2.0.5", @@ -2220,7 +2220,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 170, "literal": "^2.1.1", @@ -2233,7 +2233,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 171, "literal": "^4.2.3", @@ -2246,7 +2246,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 172, "literal": "^5.0.5", @@ -2259,7 +2259,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 173, "literal": "^21.1.1", @@ -2272,7 +2272,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 174, "literal": "^4.2.0", @@ -2285,7 +2285,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 175, "literal": "^6.0.1", @@ -2298,7 +2298,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 176, "literal": "^7.0.0", @@ -2311,7 +2311,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 177, "literal": "^5.2.1", @@ -2324,7 +2324,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 178, "literal": "^2.3.8", @@ -2337,7 +2337,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 179, "literal": "^1.3.1", @@ -2350,7 +2350,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 180, "literal": "^1.1.13", @@ -2363,7 +2363,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 181, "literal": "^3.0.0", @@ -2376,7 +2376,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 182, "literal": "^3.1.5", @@ -2415,7 +2415,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 185, "literal": "^2.1.0", @@ -2428,7 +2428,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 186, "literal": "^2.0.0", @@ -2441,7 +2441,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 187, "literal": "^2.0.0", @@ -2454,7 +2454,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 188, "literal": "^2.0.0", @@ -2467,7 +2467,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 189, "literal": "^2.18.0", @@ -2480,7 +2480,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 190, "literal": "^1.3.2", @@ -2493,7 +2493,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 191, "literal": "^1.0.1", @@ -2506,7 +2506,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 192, "literal": "^1.1.0", @@ -2532,7 +2532,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 194, "literal": "^1.6.4", @@ -2545,7 +2545,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 195, "literal": "^1.6.4", @@ -2558,7 +2558,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 196, "literal": "^1.2.0", @@ -2571,7 +2571,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 197, "literal": "^2.15.0", @@ -2584,7 +2584,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 198, "literal": "^1.1.0", @@ -2597,7 +2597,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 199, "literal": "^1.3.1", @@ -2610,7 +2610,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 200, "literal": "1", @@ -2623,7 +2623,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 201, "literal": "^1.4.0", @@ -2636,7 +2636,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 202, "literal": "^7.0.2", @@ -2649,7 +2649,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 203, "literal": "^4.3.4", @@ -2662,7 +2662,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 204, "literal": "^7.0.1", @@ -2675,7 +2675,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 205, "literal": "^7.0.3", @@ -2688,7 +2688,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 206, "literal": "^7.14.1", @@ -2701,7 +2701,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 207, "literal": "^7.0.1", @@ -2714,7 +2714,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 208, "literal": "^1.1.0", @@ -2727,7 +2727,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 209, "literal": "^8.0.2", @@ -2740,7 +2740,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 210, "literal": "^7.1.1", @@ -2753,7 +2753,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 211, "literal": "^4.3.4", @@ -2766,7 +2766,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 212, "literal": "^2.7.1", @@ -2779,7 +2779,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 213, "literal": "^9.0.5", @@ -2792,7 +2792,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 214, "literal": "^4.2.0", @@ -2805,7 +2805,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 215, "literal": "1.1.0", @@ -2818,7 +2818,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 216, "literal": "^1.1.3", @@ -2831,7 +2831,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 217, "literal": "2.1.2", @@ -2844,7 +2844,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 218, "literal": "^4.3.4", @@ -2857,7 +2857,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 219, "literal": "^0.23.0", @@ -2870,7 +2870,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 220, "literal": "^7.0.2", @@ -2883,7 +2883,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 221, "literal": "^4.3.4", @@ -2896,7 +2896,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 222, "literal": "^6.0.1", @@ -2909,7 +2909,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 223, "literal": "^7.0.0", @@ -2922,7 +2922,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 224, "literal": "^7.0.2", @@ -2935,7 +2935,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 225, "literal": "^7.0.0", @@ -2948,7 +2948,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 226, "literal": "^8.0.2", @@ -2961,7 +2961,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 227, "literal": "^5.0.0", @@ -2974,7 +2974,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 228, "literal": "^2.0.2", @@ -2987,7 +2987,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 229, "literal": "^0.13.4", @@ -3000,7 +3000,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 230, "literal": "^2.1.0", @@ -3013,7 +3013,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 231, "literal": "^4.0.1", @@ -3026,7 +3026,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 232, "literal": "^5.2.0", @@ -3039,7 +3039,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 233, "literal": "^2.0.2", @@ -3052,7 +3052,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 234, "literal": "^4.0.1", @@ -3078,7 +3078,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 236, "literal": "^2.0.1", @@ -3091,7 +3091,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 237, "literal": "^7.0.2", @@ -3104,7 +3104,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 238, "literal": "4", @@ -3117,7 +3117,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 239, "literal": "^7.1.0", @@ -3130,7 +3130,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 240, "literal": "^4.3.4", @@ -3143,7 +3143,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 241, "literal": "^5.0.2", @@ -3156,7 +3156,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 242, "literal": "^6.0.2", @@ -3169,7 +3169,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 243, "literal": "^4.3.4", @@ -3182,7 +3182,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 244, "literal": "^11.2.0", @@ -3195,7 +3195,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 245, "literal": "^4.2.0", @@ -3208,7 +3208,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 246, "literal": "^6.0.1", @@ -3221,7 +3221,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 247, "literal": "^2.0.0", @@ -3234,7 +3234,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 248, "literal": "^2.0.0", @@ -3260,7 +3260,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 250, "literal": "^4.1.1", @@ -3273,7 +3273,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 251, "literal": "^5.1.0", @@ -3286,7 +3286,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 252, "literal": "^2.10.0", @@ -3312,7 +3312,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 254, "literal": "*", @@ -3325,7 +3325,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 255, "literal": "~5.26.4", @@ -3338,7 +3338,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 256, "literal": "~1.1.0", @@ -3351,7 +3351,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 257, "literal": "~0.2.3", @@ -3364,7 +3364,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 258, "literal": "~1.2.0", @@ -3377,7 +3377,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 259, "literal": "^3.0.0", @@ -3390,7 +3390,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 260, "literal": "2.1.2", @@ -3403,7 +3403,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 261, "literal": "2.2.3", @@ -3416,7 +3416,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 262, "literal": "0.5.24", @@ -3429,7 +3429,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 263, "literal": "4.3.5", @@ -3442,7 +3442,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 264, "literal": "0.0.1299070", @@ -3455,7 +3455,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 265, "literal": "8.17.1", @@ -3496,7 +3496,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 268, "literal": "3.0.1", @@ -3509,7 +3509,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 269, "literal": "10.0.0", @@ -3522,7 +3522,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 270, "literal": "3.23.8", @@ -3548,7 +3548,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 272, "literal": "^2.2.1", @@ -3561,7 +3561,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 273, "literal": "^3.3.0", @@ -3574,7 +3574,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 274, "literal": "^4.1.0", @@ -3587,7 +3587,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 275, "literal": "^5.2.0", @@ -3614,7 +3614,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 277, "literal": "^7.0.0", @@ -3627,7 +3627,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 278, "literal": "^1.3.1", @@ -3640,7 +3640,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 279, "literal": "^2.3.0", @@ -3653,7 +3653,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 280, "literal": "^1.1.6", @@ -3666,7 +3666,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 281, "literal": "^0.2.1", @@ -3679,7 +3679,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 282, "literal": "^7.24.7", @@ -3692,7 +3692,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 283, "literal": "^1.0.0", @@ -3705,7 +3705,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 284, "literal": "^7.24.7", @@ -3718,7 +3718,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 285, "literal": "^2.4.2", @@ -3731,7 +3731,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 286, "literal": "^4.0.0", @@ -3744,7 +3744,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 287, "literal": "^1.0.0", @@ -3757,7 +3757,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 288, "literal": "^3.2.1", @@ -3770,7 +3770,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 289, "literal": "^1.0.5", @@ -3783,7 +3783,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 290, "literal": "^5.3.0", @@ -3796,7 +3796,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 291, "literal": "^3.0.0", @@ -3809,7 +3809,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 292, "literal": "^1.9.0", @@ -3822,7 +3822,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 293, "literal": "1.1.3", @@ -3835,7 +3835,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 294, "literal": "^2.0.1", @@ -3848,7 +3848,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 295, "literal": "^1.0.0", @@ -3861,7 +3861,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 296, "literal": "^4.0.0", @@ -3874,7 +3874,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 297, "literal": "^3.0.0", @@ -3887,7 +3887,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 298, "literal": "1.6.0", @@ -3900,7 +3900,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 299, "literal": "8.4.31", @@ -3913,7 +3913,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 300, "literal": "14.1.3", @@ -3926,7 +3926,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 301, "literal": "5.1.1", @@ -3939,7 +3939,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 302, "literal": "^4.2.11", @@ -3952,7 +3952,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 303, "literal": "0.5.2", @@ -3965,7 +3965,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 304, "literal": "^1.0.30001579", @@ -4149,7 +4149,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 318, "literal": "^2.4.0", @@ -4162,7 +4162,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 319, "literal": "0.0.1", @@ -4188,7 +4188,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 321, "literal": "^3.3.6", @@ -4201,7 +4201,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 322, "literal": "^1.0.0", @@ -4214,7 +4214,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 323, "literal": "^1.0.2", @@ -4227,7 +4227,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 324, "literal": "^1.1.0", @@ -4240,7 +4240,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 325, "literal": "^7.33.2", @@ -4253,7 +4253,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 326, "literal": "^2.28.1", @@ -4266,7 +4266,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 327, "literal": "^6.7.1", @@ -4279,7 +4279,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 328, "literal": "^1.3.3", @@ -4292,7 +4292,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 329, "literal": "14.1.3", @@ -4305,7 +4305,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 330, "literal": "^5.4.2 || ^6.0.0", @@ -4318,7 +4318,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 331, "literal": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705", @@ -4331,7 +4331,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 332, "literal": "^0.3.6", @@ -4344,7 +4344,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 333, "literal": "^3.5.2", @@ -4384,7 +4384,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 336, "literal": "^6.12.4", @@ -4397,7 +4397,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 337, "literal": "^0.4.1", @@ -4410,7 +4410,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 338, "literal": "^4.0.0", @@ -4423,7 +4423,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 339, "literal": "^4.3.2", @@ -4436,7 +4436,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 340, "literal": "^9.6.1", @@ -4449,7 +4449,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 341, "literal": "^5.2.0", @@ -4462,7 +4462,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 342, "literal": "^1.4.2", @@ -4475,7 +4475,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 343, "literal": "^2.0.2", @@ -4488,7 +4488,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 344, "literal": "^5.0.0", @@ -4501,7 +4501,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 345, "literal": "^13.19.0", @@ -4514,7 +4514,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 346, "literal": "^4.0.0", @@ -4527,7 +4527,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 347, "literal": "^4.1.0", @@ -4540,7 +4540,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 348, "literal": "^3.0.0", @@ -4553,7 +4553,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 349, "literal": "^1.4.0", @@ -4566,7 +4566,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 350, "literal": "^3.1.2", @@ -4579,7 +4579,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 351, "literal": "8.50.0", @@ -4592,7 +4592,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 352, "literal": "^0.9.3", @@ -4605,7 +4605,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 353, "literal": "^6.0.1", @@ -4618,7 +4618,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 354, "literal": "^0.2.0", @@ -4631,7 +4631,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 355, "literal": "^7.0.2", @@ -4644,7 +4644,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 356, "literal": "^6.0.2", @@ -4657,7 +4657,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 357, "literal": "^0.1.4", @@ -4670,7 +4670,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 358, "literal": "^7.2.2", @@ -4683,7 +4683,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 359, "literal": "^4.6.2", @@ -4696,7 +4696,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 360, "literal": "^3.0.3", @@ -4709,7 +4709,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 361, "literal": "^3.1.3", @@ -4722,7 +4722,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 362, "literal": "^1.4.0", @@ -4735,7 +4735,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 363, "literal": "^2.1.2", @@ -4748,7 +4748,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 364, "literal": "^1.2.8", @@ -4761,7 +4761,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 365, "literal": "^6.0.1", @@ -4774,7 +4774,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 366, "literal": "^3.4.3", @@ -4787,7 +4787,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 367, "literal": "^4.0.0", @@ -4800,7 +4800,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 368, "literal": "^4.6.1", @@ -4813,7 +4813,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 369, "literal": "^0.11.11", @@ -4826,7 +4826,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 370, "literal": "^4.2.0", @@ -4839,7 +4839,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 371, "literal": "^1.0.1", @@ -4852,7 +4852,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 372, "literal": "^1.0.1", @@ -4865,7 +4865,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 373, "literal": "^3.3.0", @@ -4891,7 +4891,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 375, "literal": "^2.0.2", @@ -4904,7 +4904,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 376, "literal": "^4.3.1", @@ -4917,7 +4917,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 377, "literal": "^3.0.5", @@ -4930,7 +4930,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 378, "literal": "^1.1.7", @@ -4943,7 +4943,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 379, "literal": "^1.0.0", @@ -4956,7 +4956,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 380, "literal": "0.0.1", @@ -4969,7 +4969,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 381, "literal": "^3.0.4", @@ -4982,7 +4982,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 382, "literal": "^3.2.9", @@ -4995,7 +4995,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 383, "literal": "^4.5.3", @@ -5008,7 +5008,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 384, "literal": "^3.0.2", @@ -5021,7 +5021,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 385, "literal": "^7.1.3", @@ -5034,7 +5034,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 386, "literal": "^1.0.0", @@ -5047,7 +5047,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 387, "literal": "^1.0.4", @@ -5060,7 +5060,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 388, "literal": "2", @@ -5073,7 +5073,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 389, "literal": "^3.1.1", @@ -5086,7 +5086,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 390, "literal": "^1.3.0", @@ -5099,7 +5099,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 391, "literal": "^1.0.0", @@ -5112,7 +5112,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 392, "literal": "^1.3.0", @@ -5125,7 +5125,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 393, "literal": "1", @@ -5138,7 +5138,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 394, "literal": "3.0.1", @@ -5151,7 +5151,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 395, "literal": "^6.12.4", @@ -5164,7 +5164,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 396, "literal": "^4.3.2", @@ -5177,7 +5177,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 397, "literal": "^9.6.0", @@ -5190,7 +5190,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 398, "literal": "^13.19.0", @@ -5203,7 +5203,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 399, "literal": "^5.2.0", @@ -5216,7 +5216,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 400, "literal": "^3.2.1", @@ -5229,7 +5229,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 401, "literal": "^4.1.0", @@ -5242,7 +5242,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 402, "literal": "^3.1.2", @@ -5255,7 +5255,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 403, "literal": "^3.1.1", @@ -5268,7 +5268,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 404, "literal": "^0.20.2", @@ -5281,7 +5281,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 405, "literal": "^8.9.0", @@ -5294,7 +5294,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 406, "literal": "^5.3.2", @@ -5307,7 +5307,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 407, "literal": "^3.4.1", @@ -5333,7 +5333,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 409, "literal": "^3.1.1", @@ -5346,7 +5346,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 410, "literal": "^2.0.0", @@ -5359,7 +5359,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 411, "literal": "^0.4.1", @@ -5372,7 +5372,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 412, "literal": "^4.2.2", @@ -5385,7 +5385,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 413, "literal": "^2.1.0", @@ -5398,7 +5398,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 414, "literal": "^4.3.0", @@ -5411,7 +5411,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 415, "literal": "^5.2.0", @@ -5424,7 +5424,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 416, "literal": "^5.2.0", @@ -5437,7 +5437,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 417, "literal": "^1.2.1", @@ -5450,7 +5450,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 418, "literal": "^0.1.3", @@ -5463,7 +5463,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 419, "literal": "^1.2.5", @@ -5476,7 +5476,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 420, "literal": "^0.4.0", @@ -5489,7 +5489,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 421, "literal": "^0.4.1", @@ -5502,7 +5502,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 422, "literal": "^2.0.6", @@ -5515,7 +5515,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 423, "literal": "^1.2.1", @@ -5528,7 +5528,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 424, "literal": "~0.4.0", @@ -5541,7 +5541,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 425, "literal": "^1.2.1", @@ -5554,7 +5554,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 426, "literal": "^2.0.2", @@ -5567,7 +5567,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 427, "literal": "^6.0.0", @@ -5580,7 +5580,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 428, "literal": "^4.0.0", @@ -5593,7 +5593,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 429, "literal": "^5.0.0", @@ -5606,7 +5606,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 430, "literal": "^3.0.2", @@ -5619,7 +5619,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 431, "literal": "^0.1.0", @@ -5632,7 +5632,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 432, "literal": "^5.1.0", @@ -5645,7 +5645,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 433, "literal": "^4.1.0", @@ -5658,7 +5658,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 434, "literal": "^7.1.0", @@ -5671,7 +5671,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 435, "literal": "^4.0.0", @@ -5684,7 +5684,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 436, "literal": "^4.3.4", @@ -5697,7 +5697,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 437, "literal": "^5.12.0", @@ -5710,7 +5710,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 438, "literal": "^2.7.4", @@ -5723,7 +5723,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 439, "literal": "^3.3.1", @@ -5736,7 +5736,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 440, "literal": "^4.5.0", @@ -5749,7 +5749,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 441, "literal": "^2.11.0", @@ -5762,7 +5762,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 442, "literal": "^4.0.3", @@ -5801,7 +5801,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 445, "literal": "^3.1.7", @@ -5814,7 +5814,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 446, "literal": "^1.2.3", @@ -5827,7 +5827,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 447, "literal": "^1.3.2", @@ -5840,7 +5840,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 448, "literal": "^1.3.2", @@ -5853,7 +5853,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 449, "literal": "^3.2.7", @@ -5866,7 +5866,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 450, "literal": "^2.1.0", @@ -5879,7 +5879,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 451, "literal": "^0.3.9", @@ -5892,7 +5892,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 452, "literal": "^2.8.0", @@ -5905,7 +5905,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 453, "literal": "^2.0.0", @@ -5918,7 +5918,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 454, "literal": "^2.13.1", @@ -5931,7 +5931,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 455, "literal": "^4.0.3", @@ -5944,7 +5944,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 456, "literal": "^3.1.2", @@ -5957,7 +5957,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 457, "literal": "^2.0.7", @@ -5970,7 +5970,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 458, "literal": "^1.0.1", @@ -5983,7 +5983,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 459, "literal": "^1.1.7", @@ -5996,7 +5996,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 460, "literal": "^6.3.1", @@ -6009,7 +6009,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 461, "literal": "^3.15.0", @@ -6035,7 +6035,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 463, "literal": "^0.0.29", @@ -6048,7 +6048,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 464, "literal": "^1.0.2", @@ -6061,7 +6061,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 465, "literal": "^1.2.6", @@ -6074,7 +6074,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 466, "literal": "^3.0.0", @@ -6087,7 +6087,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 467, "literal": "^1.2.0", @@ -6100,7 +6100,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 468, "literal": "^1.0.7", @@ -6113,7 +6113,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 469, "literal": "^1.2.1", @@ -6126,7 +6126,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 470, "literal": "^1.0.0", @@ -6139,7 +6139,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 471, "literal": "^1.3.0", @@ -6152,7 +6152,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 472, "literal": "^1.0.1", @@ -6165,7 +6165,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 473, "literal": "^1.0.0", @@ -6178,7 +6178,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 474, "literal": "^1.1.1", @@ -6191,7 +6191,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 475, "literal": "^1.0.0", @@ -6204,7 +6204,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 476, "literal": "^1.2.4", @@ -6217,7 +6217,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 477, "literal": "^1.3.0", @@ -6230,7 +6230,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 478, "literal": "^1.1.2", @@ -6243,7 +6243,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 479, "literal": "^1.0.1", @@ -6256,7 +6256,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 480, "literal": "^1.0.3", @@ -6269,7 +6269,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 481, "literal": "^2.0.0", @@ -6282,7 +6282,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 482, "literal": "^1.0.0", @@ -6295,7 +6295,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 483, "literal": "^1.3.0", @@ -6308,7 +6308,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 484, "literal": "^1.0.1", @@ -6321,7 +6321,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 485, "literal": "^1.1.3", @@ -6334,7 +6334,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 486, "literal": "^1.0.0", @@ -6347,7 +6347,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 487, "literal": "^1.3.0", @@ -6360,7 +6360,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 488, "literal": "^1.1.2", @@ -6373,7 +6373,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 489, "literal": "^1.2.4", @@ -6386,7 +6386,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 490, "literal": "^1.2.1", @@ -6399,7 +6399,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 491, "literal": "^1.1.4", @@ -6412,7 +6412,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 492, "literal": "^1.3.0", @@ -6425,7 +6425,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 493, "literal": "^1.1.2", @@ -6438,7 +6438,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 494, "literal": "^1.2.4", @@ -6451,7 +6451,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 495, "literal": "^1.0.1", @@ -6464,7 +6464,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 496, "literal": "^1.0.2", @@ -6477,7 +6477,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 497, "literal": "^1.0.7", @@ -6490,7 +6490,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 498, "literal": "^1.2.1", @@ -6503,7 +6503,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 499, "literal": "^1.23.2", @@ -6516,7 +6516,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 500, "literal": "^1.0.1", @@ -6529,7 +6529,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 501, "literal": "^1.0.3", @@ -6542,7 +6542,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 502, "literal": "^1.0.7", @@ -6555,7 +6555,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 503, "literal": "^1.0.7", @@ -6568,7 +6568,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 504, "literal": "^1.0.1", @@ -6581,7 +6581,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 505, "literal": "^1.0.1", @@ -6594,7 +6594,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 506, "literal": "^1.0.0", @@ -6607,7 +6607,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 507, "literal": "^1.0.0", @@ -6620,7 +6620,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 508, "literal": "^1.3.0", @@ -6633,7 +6633,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 509, "literal": "^1.0.0", @@ -6646,7 +6646,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 510, "literal": "^2.0.3", @@ -6659,7 +6659,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 511, "literal": "^1.2.1", @@ -6672,7 +6672,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 512, "literal": "^1.1.6", @@ -6685,7 +6685,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 513, "literal": "^1.2.4", @@ -6698,7 +6698,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 514, "literal": "^1.0.2", @@ -6711,7 +6711,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 515, "literal": "^1.0.3", @@ -6724,7 +6724,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 516, "literal": "^1.0.1", @@ -6737,7 +6737,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 517, "literal": "^1.0.2", @@ -6750,7 +6750,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 518, "literal": "^1.0.3", @@ -6763,7 +6763,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 519, "literal": "^1.0.3", @@ -6776,7 +6776,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 520, "literal": "^2.0.2", @@ -6789,7 +6789,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 521, "literal": "^1.0.7", @@ -6802,7 +6802,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 522, "literal": "^3.0.4", @@ -6815,7 +6815,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 523, "literal": "^1.2.7", @@ -6828,7 +6828,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 524, "literal": "^1.0.1", @@ -6841,7 +6841,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 525, "literal": "^2.0.3", @@ -6854,7 +6854,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 526, "literal": "^1.1.4", @@ -6867,7 +6867,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 527, "literal": "^1.0.3", @@ -6880,7 +6880,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 528, "literal": "^1.0.7", @@ -6893,7 +6893,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 529, "literal": "^1.1.13", @@ -6906,7 +6906,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 530, "literal": "^1.0.2", @@ -6919,7 +6919,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 531, "literal": "^1.13.1", @@ -6932,7 +6932,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 532, "literal": "^1.1.1", @@ -6945,7 +6945,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 533, "literal": "^4.1.5", @@ -6958,7 +6958,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 534, "literal": "^1.5.2", @@ -6971,7 +6971,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 535, "literal": "^1.1.2", @@ -6984,7 +6984,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 536, "literal": "^1.0.3", @@ -6997,7 +6997,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 537, "literal": "^1.2.9", @@ -7010,7 +7010,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 538, "literal": "^1.0.8", @@ -7023,7 +7023,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 539, "literal": "^1.0.8", @@ -7036,7 +7036,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 540, "literal": "^1.0.2", @@ -7049,7 +7049,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 541, "literal": "^1.0.1", @@ -7062,7 +7062,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 542, "literal": "^1.0.2", @@ -7075,7 +7075,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 543, "literal": "^1.0.6", @@ -7088,7 +7088,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 544, "literal": "^1.0.2", @@ -7101,7 +7101,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 545, "literal": "^1.1.15", @@ -7114,7 +7114,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 546, "literal": "^1.0.7", @@ -7127,7 +7127,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 547, "literal": "^1.0.7", @@ -7140,7 +7140,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 548, "literal": "^0.3.3", @@ -7153,7 +7153,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 549, "literal": "^1.0.1", @@ -7166,7 +7166,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 550, "literal": "^1.0.2", @@ -7179,7 +7179,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 551, "literal": "^1.0.3", @@ -7192,7 +7192,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 552, "literal": "^1.1.3", @@ -7205,7 +7205,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 553, "literal": "^1.0.0", @@ -7218,7 +7218,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 554, "literal": "^1.0.2", @@ -7231,7 +7231,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 555, "literal": "^1.0.2", @@ -7244,7 +7244,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 556, "literal": "^1.0.3", @@ -7257,7 +7257,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 557, "literal": "^1.0.2", @@ -7270,7 +7270,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 558, "literal": "^1.0.1", @@ -7283,7 +7283,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 559, "literal": "^1.1.0", @@ -7296,7 +7296,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 560, "literal": "^1.0.4", @@ -7309,7 +7309,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 561, "literal": "^1.0.5", @@ -7322,7 +7322,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 562, "literal": "^1.0.3", @@ -7335,7 +7335,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 563, "literal": "^1.0.2", @@ -7348,7 +7348,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 564, "literal": "^1.0.0", @@ -7361,7 +7361,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 565, "literal": "^1.0.0", @@ -7374,7 +7374,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 566, "literal": "^1.0.2", @@ -7387,7 +7387,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 567, "literal": "^1.0.0", @@ -7400,7 +7400,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 568, "literal": "^1.0.1", @@ -7413,7 +7413,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 569, "literal": "^1.0.7", @@ -7426,7 +7426,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 570, "literal": "^0.3.3", @@ -7439,7 +7439,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 571, "literal": "^1.0.1", @@ -7452,7 +7452,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 572, "literal": "^1.0.3", @@ -7465,7 +7465,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 573, "literal": "^1.1.13", @@ -7478,7 +7478,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 574, "literal": "^1.0.0", @@ -7491,7 +7491,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 575, "literal": "^1.1.14", @@ -7504,7 +7504,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 576, "literal": "^1.0.7", @@ -7517,7 +7517,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 577, "literal": "^1.0.7", @@ -7530,7 +7530,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 578, "literal": "^0.3.3", @@ -7543,7 +7543,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 579, "literal": "^1.0.1", @@ -7556,7 +7556,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 580, "literal": "^1.0.3", @@ -7569,7 +7569,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 581, "literal": "^1.1.13", @@ -7582,7 +7582,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 582, "literal": "^1.0.7", @@ -7595,7 +7595,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 583, "literal": "^0.3.3", @@ -7608,7 +7608,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 584, "literal": "^1.0.1", @@ -7621,7 +7621,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 585, "literal": "^1.0.3", @@ -7634,7 +7634,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 586, "literal": "^1.1.13", @@ -7647,7 +7647,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 587, "literal": "^1.0.7", @@ -7660,7 +7660,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 588, "literal": "^1.3.0", @@ -7673,7 +7673,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 589, "literal": "^1.1.13", @@ -7686,7 +7686,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 590, "literal": "^1.0.7", @@ -7699,7 +7699,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 591, "literal": "^1.2.1", @@ -7712,7 +7712,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 592, "literal": "^1.0.0", @@ -7725,7 +7725,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 593, "literal": "^1.0.7", @@ -7738,7 +7738,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 594, "literal": "^1.2.1", @@ -7751,7 +7751,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 595, "literal": "^1.0.0", @@ -7764,7 +7764,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 596, "literal": "^1.0.7", @@ -7777,7 +7777,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 597, "literal": "^1.2.1", @@ -7790,7 +7790,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 598, "literal": "^1.23.0", @@ -7803,7 +7803,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 599, "literal": "^1.0.0", @@ -7816,7 +7816,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 600, "literal": "^1.0.6", @@ -7829,7 +7829,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 601, "literal": "^1.3.0", @@ -7842,7 +7842,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 602, "literal": "^1.1.4", @@ -7855,7 +7855,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 603, "literal": "^1.0.2", @@ -7868,7 +7868,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 604, "literal": "^1.0.0", @@ -7881,7 +7881,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 605, "literal": "^1.0.7", @@ -7894,7 +7894,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 606, "literal": "^1.2.4", @@ -7907,7 +7907,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 607, "literal": "^1.0.3", @@ -7920,7 +7920,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 608, "literal": "^2.0.5", @@ -7933,7 +7933,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 609, "literal": "^1.0.6", @@ -7946,7 +7946,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 610, "literal": "^1.2.1", @@ -7959,7 +7959,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 611, "literal": "^1.3.0", @@ -7972,7 +7972,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 612, "literal": "^2.0.1", @@ -7985,7 +7985,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 613, "literal": "^1.1.4", @@ -7998,7 +7998,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 614, "literal": "^1.3.0", @@ -8011,7 +8011,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 615, "literal": "^1.2.3", @@ -8024,7 +8024,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 616, "literal": "^1.0.2", @@ -8037,7 +8037,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 617, "literal": "^1.0.5", @@ -8050,7 +8050,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 618, "literal": "^1.2.1", @@ -8063,7 +8063,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 619, "literal": "^1.0.3", @@ -8076,7 +8076,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 620, "literal": "^1.1.1", @@ -8089,7 +8089,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 621, "literal": "^1.0.2", @@ -8102,7 +8102,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 622, "literal": "^1.0.7", @@ -8115,7 +8115,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 623, "literal": "^1.1.13", @@ -8128,7 +8128,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 624, "literal": "^1.0.2", @@ -8141,7 +8141,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 625, "literal": "^1.2.1", @@ -8154,7 +8154,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 626, "literal": "^1.3.0", @@ -8167,7 +8167,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 627, "literal": "^2.0.0", @@ -8180,7 +8180,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 628, "literal": "^1.0.4", @@ -8193,7 +8193,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 629, "literal": "^1.0.7", @@ -8206,7 +8206,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 630, "literal": "^1.3.0", @@ -8219,7 +8219,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 631, "literal": "^1.2.4", @@ -8232,7 +8232,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 632, "literal": "^1.13.1", @@ -8245,7 +8245,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 633, "literal": "^1.2.1", @@ -8258,7 +8258,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 634, "literal": "^1.0.1", @@ -8271,7 +8271,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 635, "literal": "^1.0.5", @@ -8284,7 +8284,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 636, "literal": "^1.3.0", @@ -8297,7 +8297,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 637, "literal": "^1.2.4", @@ -8310,7 +8310,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 638, "literal": "^1.0.2", @@ -8323,7 +8323,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 639, "literal": "^1.2.0", @@ -8336,7 +8336,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 640, "literal": "^1.22.1", @@ -8349,7 +8349,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 641, "literal": "^1.2.3", @@ -8362,7 +8362,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 642, "literal": "^1.1.4", @@ -8375,7 +8375,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 643, "literal": "^1.0.1", @@ -8388,7 +8388,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 644, "literal": "^1.0.2", @@ -8401,7 +8401,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 645, "literal": "^1.0.0", @@ -8414,7 +8414,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 646, "literal": "^1.2.4", @@ -8427,7 +8427,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 647, "literal": "^1.0.2", @@ -8440,7 +8440,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 648, "literal": "^2.0.1", @@ -8453,7 +8453,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 649, "literal": "^1.0.6", @@ -8466,7 +8466,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 650, "literal": "^1.3.0", @@ -8479,7 +8479,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 651, "literal": "^1.0.1", @@ -8492,7 +8492,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 652, "literal": "^1.0.7", @@ -8505,7 +8505,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 653, "literal": "^1.3.0", @@ -8518,7 +8518,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 654, "literal": "^1.0.1", @@ -8531,7 +8531,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 655, "literal": "^1.0.6", @@ -8544,7 +8544,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 656, "literal": "^1.3.0", @@ -8557,7 +8557,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 657, "literal": "^1.0.1", @@ -8570,7 +8570,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 658, "literal": "^1.0.1", @@ -8583,7 +8583,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 659, "literal": "^1.0.5", @@ -8596,7 +8596,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 660, "literal": "^1.2.1", @@ -8609,7 +8609,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 661, "literal": "^1.22.3", @@ -8622,7 +8622,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 662, "literal": "^1.2.1", @@ -8635,7 +8635,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 663, "literal": "^1.2.3", @@ -8648,7 +8648,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 664, "literal": "^3.0.4", @@ -8661,7 +8661,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 665, "literal": "^1.0.2", @@ -8674,7 +8674,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 666, "literal": "^1.0.5", @@ -8687,7 +8687,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 667, "literal": "^3.0.4", @@ -8700,7 +8700,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 668, "literal": "^1.0.7", @@ -8713,7 +8713,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 669, "literal": "^1.2.1", @@ -8726,7 +8726,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 670, "literal": "^1.23.2", @@ -8739,7 +8739,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 671, "literal": "^1.0.0", @@ -8752,7 +8752,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 672, "literal": "^3.2.7", @@ -8765,7 +8765,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 673, "literal": "^2.1.1", @@ -8778,7 +8778,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 674, "literal": "^3.2.7", @@ -8791,7 +8791,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 675, "literal": "^2.13.0", @@ -8804,7 +8804,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 676, "literal": "^1.22.4", @@ -8817,7 +8817,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 677, "literal": "^2.0.2", @@ -8830,7 +8830,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 678, "literal": "^1.0.2", @@ -8843,7 +8843,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 679, "literal": "^1.2.0", @@ -8856,7 +8856,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 680, "literal": "^1.22.1", @@ -8869,7 +8869,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 681, "literal": "^1.0.0", @@ -8882,7 +8882,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 682, "literal": "^2.0.0", @@ -8895,7 +8895,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 683, "literal": "^1.0.2", @@ -8908,7 +8908,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 684, "literal": "^1.2.0", @@ -8921,7 +8921,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 685, "literal": "^1.22.1", @@ -8934,7 +8934,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 686, "literal": "^1.0.0", @@ -8947,7 +8947,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 687, "literal": "^1.0.7", @@ -8960,7 +8960,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 688, "literal": "^1.2.1", @@ -8973,7 +8973,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 689, "literal": "^1.23.2", @@ -8986,7 +8986,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 690, "literal": "^1.3.0", @@ -8999,7 +8999,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 691, "literal": "^1.0.0", @@ -9012,7 +9012,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 692, "literal": "^1.0.2", @@ -9025,7 +9025,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 693, "literal": "^1.0.7", @@ -9038,7 +9038,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 694, "literal": "^1.2.1", @@ -9051,7 +9051,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 695, "literal": "^1.23.2", @@ -9064,7 +9064,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 696, "literal": "^1.0.0", @@ -9077,7 +9077,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 697, "literal": "^1.2.4", @@ -9090,7 +9090,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 698, "literal": "^1.0.7", @@ -9103,7 +9103,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 699, "literal": "^1.0.0", @@ -9116,7 +9116,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 700, "literal": "^4.2.4", @@ -9129,7 +9129,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 701, "literal": "^2.2.0", @@ -9155,7 +9155,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 703, "literal": "^4.3.4", @@ -9168,7 +9168,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 704, "literal": "6.21.0", @@ -9181,7 +9181,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 705, "literal": "6.21.0", @@ -9194,7 +9194,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 706, "literal": "6.21.0", @@ -9207,7 +9207,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 707, "literal": "6.21.0", @@ -9233,7 +9233,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 709, "literal": "^4.3.4", @@ -9246,7 +9246,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 710, "literal": "^11.1.0", @@ -9259,7 +9259,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 711, "literal": "^7.5.4", @@ -9272,7 +9272,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 712, "literal": "^4.0.3", @@ -9285,7 +9285,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 713, "literal": "9.0.3", @@ -9298,7 +9298,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 714, "literal": "^1.0.1", @@ -9311,7 +9311,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 715, "literal": "6.21.0", @@ -9324,7 +9324,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 716, "literal": "6.21.0", @@ -9337,7 +9337,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 717, "literal": "^3.4.1", @@ -9350,7 +9350,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 718, "literal": "6.21.0", @@ -9376,7 +9376,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 720, "literal": "^2.0.1", @@ -9389,7 +9389,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 721, "literal": "^2.1.0", @@ -9402,7 +9402,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 722, "literal": "^3.0.1", @@ -9415,7 +9415,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 723, "literal": "^3.2.9", @@ -9428,7 +9428,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 724, "literal": "^5.2.0", @@ -9441,7 +9441,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 725, "literal": "^1.4.1", @@ -9454,7 +9454,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 726, "literal": "^3.0.0", @@ -9467,7 +9467,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 727, "literal": "^4.0.0", @@ -9480,7 +9480,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 728, "literal": "6.21.0", @@ -9493,7 +9493,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 729, "literal": "6.21.0", @@ -9506,7 +9506,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 730, "literal": "10.3.10", @@ -9519,7 +9519,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 731, "literal": "^7.23.2", @@ -9532,7 +9532,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 732, "literal": "^5.3.0", @@ -9545,7 +9545,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 733, "literal": "^3.1.7", @@ -9558,7 +9558,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 734, "literal": "^1.3.2", @@ -9571,7 +9571,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 735, "literal": "^0.0.8", @@ -9584,7 +9584,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 736, "literal": "=4.7.0", @@ -9597,7 +9597,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 737, "literal": "^3.2.1", @@ -9610,7 +9610,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 738, "literal": "^1.0.8", @@ -9623,7 +9623,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 739, "literal": "^9.2.2", @@ -9636,7 +9636,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 740, "literal": "^1.0.15", @@ -9649,7 +9649,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 741, "literal": "^2.0.0", @@ -9662,7 +9662,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 742, "literal": "^3.3.5", @@ -9675,7 +9675,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 743, "literal": "^1.0.9", @@ -9688,7 +9688,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 744, "literal": "^3.1.2", @@ -9701,7 +9701,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 745, "literal": "^1.1.7", @@ -9714,7 +9714,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 746, "literal": "^2.0.7", @@ -9740,7 +9740,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 748, "literal": "^1.0.7", @@ -9753,7 +9753,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 749, "literal": "^1.2.1", @@ -9766,7 +9766,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 750, "literal": "^1.0.0", @@ -9779,7 +9779,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 751, "literal": "^0.3.20", @@ -9792,7 +9792,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 752, "literal": "^3.1.6", @@ -9805,7 +9805,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 753, "literal": "^1.3.1", @@ -9818,7 +9818,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 754, "literal": "^4.1.4", @@ -9831,7 +9831,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 755, "literal": "^1.1.6", @@ -9844,7 +9844,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 756, "literal": "^1.0.7", @@ -9857,7 +9857,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 757, "literal": "^1.2.1", @@ -9870,7 +9870,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 758, "literal": "^1.23.3", @@ -9883,7 +9883,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 759, "literal": "^1.3.0", @@ -9896,7 +9896,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 760, "literal": "^2.0.3", @@ -9909,7 +9909,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 761, "literal": "^1.1.2", @@ -9922,7 +9922,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 762, "literal": "^1.2.4", @@ -9935,7 +9935,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 763, "literal": "^1.0.3", @@ -9948,7 +9948,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 764, "literal": "^1.0.2", @@ -9961,7 +9961,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 765, "literal": "^1.0.3", @@ -9974,7 +9974,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 766, "literal": "^1.0.3", @@ -9987,7 +9987,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 767, "literal": "^1.0.7", @@ -10000,7 +10000,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 768, "literal": "^1.1.2", @@ -10013,7 +10013,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 769, "literal": "^1.1.2", @@ -10026,7 +10026,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 770, "literal": "^1.2.1", @@ -10039,7 +10039,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 771, "literal": "^1.2.1", @@ -10052,7 +10052,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 772, "literal": "^1.0.3", @@ -10065,7 +10065,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 773, "literal": "^1.0.4", @@ -10078,7 +10078,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 774, "literal": "^2.0.1", @@ -10091,7 +10091,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 775, "literal": "^1.0.7", @@ -10104,7 +10104,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 776, "literal": "^1.2.1", @@ -10117,7 +10117,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 777, "literal": "^1.23.1", @@ -10130,7 +10130,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 778, "literal": "^1.3.0", @@ -10143,7 +10143,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 779, "literal": "^1.2.4", @@ -10156,7 +10156,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 780, "literal": "^1.0.3", @@ -10169,7 +10169,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 781, "literal": "^1.1.3", @@ -10182,7 +10182,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 782, "literal": "^1.1.5", @@ -10195,7 +10195,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 783, "literal": "^1.0.0", @@ -10208,7 +10208,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 784, "literal": "^2.0.0", @@ -10221,7 +10221,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 785, "literal": "^1.0.5", @@ -10234,7 +10234,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 786, "literal": "^1.0.2", @@ -10247,7 +10247,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 787, "literal": "^1.0.10", @@ -10260,7 +10260,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 788, "literal": "^1.1.4", @@ -10273,7 +10273,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 789, "literal": "^1.0.2", @@ -10286,7 +10286,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 790, "literal": "^2.0.5", @@ -10299,7 +10299,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 791, "literal": "^1.0.2", @@ -10312,7 +10312,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 792, "literal": "^1.0.1", @@ -10325,7 +10325,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 793, "literal": "^1.1.9", @@ -10338,7 +10338,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 794, "literal": "^2.0.3", @@ -10351,7 +10351,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 795, "literal": "^2.0.3", @@ -10364,7 +10364,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 796, "literal": "^2.0.2", @@ -10377,7 +10377,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 797, "literal": "^2.0.3", @@ -10390,7 +10390,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 798, "literal": "^1.0.7", @@ -10403,7 +10403,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 799, "literal": "^1.2.4", @@ -10416,7 +10416,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 800, "literal": "^1.0.0", @@ -10429,7 +10429,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 801, "literal": "^1.0.2", @@ -10442,7 +10442,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 802, "literal": "^1.0.0", @@ -10455,7 +10455,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 803, "literal": "^2.0.3", @@ -10468,7 +10468,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 804, "literal": "^2.0.3", @@ -10481,7 +10481,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 805, "literal": "^0.14.0", @@ -10494,7 +10494,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 806, "literal": "^3.1.8", @@ -10507,7 +10507,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 807, "literal": "^1.2.5", @@ -10520,7 +10520,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 808, "literal": "^1.3.2", @@ -10533,7 +10533,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 809, "literal": "^1.1.2", @@ -10546,7 +10546,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 810, "literal": "^1.1.3", @@ -10559,7 +10559,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 811, "literal": "^2.1.0", @@ -10572,7 +10572,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 812, "literal": "^1.0.19", @@ -10585,7 +10585,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 813, "literal": "^5.3.0", @@ -10598,7 +10598,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 814, "literal": "^2.4.1 || ^3.0.0", @@ -10611,7 +10611,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 815, "literal": "^3.1.2", @@ -10624,7 +10624,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 816, "literal": "^1.1.8", @@ -10637,7 +10637,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 817, "literal": "^2.0.8", @@ -10650,7 +10650,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 818, "literal": "^1.1.4", @@ -10663,7 +10663,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 819, "literal": "^1.2.0", @@ -10676,7 +10676,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 820, "literal": "^15.8.1", @@ -10689,7 +10689,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 821, "literal": "^2.0.0-next.5", @@ -10702,7 +10702,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 822, "literal": "^6.3.1", @@ -10715,7 +10715,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 823, "literal": "^4.0.11", @@ -10741,7 +10741,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 825, "literal": "^1.0.7", @@ -10754,7 +10754,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 826, "literal": "^1.2.1", @@ -10767,7 +10767,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 827, "literal": "^1.23.2", @@ -10780,7 +10780,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 828, "literal": "^1.3.0", @@ -10793,7 +10793,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 829, "literal": "^1.0.0", @@ -10806,7 +10806,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 830, "literal": "^1.2.4", @@ -10819,7 +10819,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 831, "literal": "^1.0.1", @@ -10832,7 +10832,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 832, "literal": "^1.0.3", @@ -10845,7 +10845,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 833, "literal": "^1.0.7", @@ -10858,7 +10858,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 834, "literal": "^1.5.2", @@ -10871,7 +10871,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 835, "literal": "^2.0.2", @@ -10884,7 +10884,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 836, "literal": "^1.0.6", @@ -10897,7 +10897,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 837, "literal": "^2.13.0", @@ -10910,7 +10910,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 838, "literal": "^1.0.7", @@ -10923,7 +10923,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 839, "literal": "^1.0.0", @@ -10936,7 +10936,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 840, "literal": "^1.4.0", @@ -10949,7 +10949,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 841, "literal": "^4.1.1", @@ -10962,7 +10962,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 842, "literal": "^16.13.1", @@ -10975,7 +10975,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 843, "literal": "^1.2.1", @@ -10988,7 +10988,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 844, "literal": "^1.23.2", @@ -11001,7 +11001,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 845, "literal": "^1.0.0", @@ -11014,7 +11014,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 846, "literal": "^1.0.7", @@ -11027,7 +11027,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 847, "literal": "^1.2.1", @@ -11040,7 +11040,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 848, "literal": "^1.23.3", @@ -11053,7 +11053,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 849, "literal": "^1.3.0", @@ -11066,7 +11066,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 850, "literal": "^1.0.2", @@ -11079,7 +11079,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 851, "literal": "^1.0.2", @@ -11092,7 +11092,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 852, "literal": "^1.2.0", @@ -11105,7 +11105,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 853, "literal": "^1.22.1", @@ -11118,7 +11118,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 854, "literal": "^1.0.0", @@ -11131,7 +11131,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 855, "literal": "^1.0.7", @@ -11144,7 +11144,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 856, "literal": "^1.2.1", @@ -11157,7 +11157,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 857, "literal": "^1.23.2", @@ -11170,7 +11170,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 858, "literal": "^1.3.0", @@ -11183,7 +11183,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 859, "literal": "^1.0.0", @@ -11196,7 +11196,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 860, "literal": "^1.0.2", @@ -11209,7 +11209,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 861, "literal": "~8.5.10", @@ -11222,7 +11222,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 862, "literal": "~20.12.8", @@ -11235,7 +11235,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 863, "literal": "*", @@ -11248,7 +11248,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 864, "literal": "^4.21.10", @@ -11261,7 +11261,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 865, "literal": "^1.0.30001538", @@ -11274,7 +11274,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 866, "literal": "^4.3.6", @@ -11287,7 +11287,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 867, "literal": "^0.1.2", @@ -11300,7 +11300,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 868, "literal": "^1.0.0", @@ -11313,7 +11313,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 869, "literal": "^4.2.0", @@ -11339,7 +11339,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 871, "literal": "^1.0.30001587", @@ -11352,7 +11352,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 872, "literal": "^1.4.668", @@ -11365,7 +11365,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 873, "literal": "^2.0.14", @@ -11378,7 +11378,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 874, "literal": "^1.0.13", @@ -11391,7 +11391,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 875, "literal": "^3.1.2", @@ -11404,7 +11404,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 876, "literal": "^1.0.1", @@ -11430,7 +11430,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 878, "literal": "*", @@ -11443,7 +11443,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 879, "literal": "*", @@ -11456,7 +11456,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 880, "literal": "*", @@ -11469,7 +11469,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 881, "literal": "^3.0.2", @@ -20772,7 +20772,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 374, }, "array-includes": { - "id": 806, + "id": 445, "package_id": 384, }, "array-union": { @@ -20792,7 +20792,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 382, }, "array.prototype.flatmap": { - "id": 808, + "id": 448, "package_id": 380, }, "array.prototype.toreversed": { @@ -21068,11 +21068,11 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 316, }, "es-errors": { - "id": 858, + "id": 690, "package_id": 312, }, "es-iterator-helpers": { - "id": 812, + "id": 740, "package_id": 409, }, "es-object-atoms": { @@ -21084,7 +21084,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 369, }, "es-shim-unscopables": { - "id": 860, + "id": 692, "package_id": 381, }, "es-to-primitive": { @@ -21120,7 +21120,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 302, }, "eslint-module-utils": { - "id": 452, + "id": 438, "package_id": 376, }, "eslint-plugin-import": { @@ -21164,7 +21164,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 279, }, "estraverse": { - "id": 432, + "id": 415, "package_id": 166, }, "esutils": { @@ -21248,7 +21248,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 51, }, "function-bind": { - "id": 761, + "id": 55, "package_id": 21, }, "function.prototype.name": { @@ -21412,7 +21412,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 329, }, "is-core-module": { - "id": 454, + "id": 675, "package_id": 19, }, "is-data-view": { @@ -21556,7 +21556,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 174, }, "jsx-ast-utils": { - "id": 814, + "id": 742, "package_id": 408, }, "keyv": { @@ -21680,11 +21680,11 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 355, }, "object.entries": { - "id": 816, + "id": 745, "package_id": 405, }, "object.fromentries": { - "id": 817, + "id": 457, "package_id": 375, }, "object.groupby": { @@ -21696,7 +21696,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 434, }, "object.values": { - "id": 819, + "id": 459, "package_id": 310, }, "once": { @@ -21924,7 +21924,7 @@ exports[`ssr works for 100-ish requests 1`] = ` "package_id": 112, }, "semver": { - "id": 822, + "id": 460, "package_id": 309, }, "set-function-length": { @@ -22271,18 +22271,14 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "dependencies": { - "doctrine": { - "id": 811, - "package_id": 379, - }, - "resolve": { - "id": 821, - "package_id": 431, + "debug": { + "id": 674, + "package_id": 377, }, }, "depth": 1, "id": 4, - "path": "node_modules/eslint-plugin-react/node_modules", + "path": "node_modules/eslint-import-resolver-node/node_modules", }, { "dependencies": { @@ -22301,14 +22297,18 @@ exports[`ssr works for 100-ish requests 1`] = ` }, { "dependencies": { - "debug": { - "id": 674, - "package_id": 377, + "doctrine": { + "id": 811, + "package_id": 379, + }, + "resolve": { + "id": 821, + "package_id": 431, }, }, "depth": 1, "id": 6, - "path": "node_modules/eslint-import-resolver-node/node_modules", + "path": "node_modules/eslint-plugin-react/node_modules", }, { "dependencies": { @@ -22358,17 +22358,6 @@ exports[`ssr works for 100-ish requests 1`] = ` "id": 10, "path": "node_modules/postcss-load-config/node_modules", }, - { - "dependencies": { - "debug": { - "id": 672, - "package_id": 377, - }, - }, - "depth": 1, - "id": 11, - "path": "node_modules/eslint-module-utils/node_modules", - }, { "dependencies": { "minimatch": { @@ -22377,7 +22366,7 @@ exports[`ssr works for 100-ish requests 1`] = ` }, }, "depth": 1, - "id": 12, + "id": 11, "path": "node_modules/glob/node_modules", }, { @@ -22392,9 +22381,20 @@ exports[`ssr works for 100-ish requests 1`] = ` }, }, "depth": 1, - "id": 13, + "id": 12, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules", }, + { + "dependencies": { + "debug": { + "id": 672, + "package_id": 377, + }, + }, + "depth": 1, + "id": 13, + "path": "node_modules/eslint-module-utils/node_modules", + }, { "dependencies": { "lru-cache": { @@ -22439,17 +22439,6 @@ exports[`ssr works for 100-ish requests 1`] = ` "id": 17, "path": "node_modules/path-scurry/node_modules", }, - { - "dependencies": { - "lru-cache": { - "id": 165, - "package_id": 117, - }, - }, - "depth": 2, - "id": 18, - "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", - }, { "dependencies": { "brace-expansion": { @@ -22458,9 +22447,20 @@ exports[`ssr works for 100-ish requests 1`] = ` }, }, "depth": 2, - "id": 19, + "id": 18, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules", }, + { + "dependencies": { + "lru-cache": { + "id": 165, + "package_id": 117, + }, + }, + "depth": 2, + "id": 19, + "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", + }, { "dependencies": { "@types/node": { diff --git a/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap b/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap index 7b40a27d78..6bacb22ab5 100644 --- a/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap @@ -5,7 +5,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "dependencies": [ { "behavior": { - "normal": true, + "prod": true, }, "id": 0, "literal": "20.7.0", @@ -18,7 +18,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 1, "literal": "18.2.22", @@ -31,7 +31,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 2, "literal": "18.2.7", @@ -44,7 +44,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 3, "literal": "10.4.16", @@ -57,7 +57,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 4, "literal": "^1.0.3", @@ -70,7 +70,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 5, "literal": "8.50.0", @@ -83,7 +83,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 6, "literal": "14.1.3", @@ -96,7 +96,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 7, "literal": "14.1.3", @@ -109,7 +109,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 8, "literal": "8.4.30", @@ -122,7 +122,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 9, "literal": "22.12.0", @@ -135,7 +135,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 10, "literal": "18.2.0", @@ -148,7 +148,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 11, "literal": "18.2.0", @@ -161,7 +161,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 12, "literal": "3.3.3", @@ -174,7 +174,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 13, "literal": "5.2.2", @@ -187,7 +187,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 14, "literal": "^5.0.2", @@ -200,7 +200,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 15, "literal": "^1.1.3", @@ -213,7 +213,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 16, "literal": "^1.18.2", @@ -226,7 +226,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 17, "literal": "^4.0.3", @@ -239,7 +239,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 18, "literal": "^8.4.23", @@ -252,7 +252,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 19, "literal": "^1.22.2", @@ -265,7 +265,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 20, "literal": "^3.32.0", @@ -278,7 +278,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 21, "literal": "^3.5.3", @@ -291,7 +291,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 22, "literal": "^3.2.12", @@ -304,7 +304,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 23, "literal": "^2.1.0", @@ -317,7 +317,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 24, "literal": "^1.2.2", @@ -330,7 +330,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 25, "literal": "^4.0.5", @@ -343,7 +343,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 26, "literal": "^1.0.0", @@ -356,7 +356,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 27, "literal": "^4.0.1", @@ -369,7 +369,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 28, "literal": "^6.0.2", @@ -382,7 +382,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 29, "literal": "^3.0.0", @@ -395,7 +395,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 30, "literal": "^3.0.0", @@ -408,7 +408,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 31, "literal": "^15.1.0", @@ -421,7 +421,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 32, "literal": "^6.0.1", @@ -434,7 +434,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 33, "literal": "^5.2.0", @@ -447,7 +447,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 34, "literal": "^4.0.1", @@ -460,7 +460,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 35, "literal": "^6.0.11", @@ -473,7 +473,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 36, "literal": "^3.0.0", @@ -486,7 +486,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 37, "literal": "^1.0.2", @@ -499,7 +499,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 38, "literal": "^2.3.4", @@ -512,7 +512,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 39, "literal": "^3.0.0", @@ -553,7 +553,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 42, "literal": "^3.3.6", @@ -566,7 +566,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 43, "literal": "^1.0.0", @@ -579,7 +579,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 44, "literal": "^1.0.2", @@ -592,7 +592,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 45, "literal": "^6.0.11", @@ -618,7 +618,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 47, "literal": "^4.0.0", @@ -631,7 +631,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 48, "literal": "^1.0.0", @@ -644,7 +644,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 49, "literal": "^1.1.7", @@ -670,7 +670,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 51, "literal": "^2.13.0", @@ -683,7 +683,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 52, "literal": "^1.0.7", @@ -696,7 +696,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 53, "literal": "^1.0.0", @@ -709,7 +709,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 54, "literal": "^2.0.0", @@ -722,7 +722,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 55, "literal": "^1.1.2", @@ -735,7 +735,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 56, "literal": "^2.3.0", @@ -748,7 +748,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 57, "literal": "^4.0.3", @@ -761,7 +761,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 58, "literal": "^2.1.1", @@ -774,7 +774,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 59, "literal": "^2.0.1", @@ -800,7 +800,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 61, "literal": "^3.0.3", @@ -813,7 +813,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 62, "literal": "^2.3.1", @@ -826,7 +826,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 63, "literal": "^7.1.1", @@ -839,7 +839,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 64, "literal": "^5.0.1", @@ -852,7 +852,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 65, "literal": "^7.0.0", @@ -865,7 +865,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 66, "literal": "^2.0.2", @@ -878,7 +878,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 67, "literal": "^1.2.3", @@ -891,7 +891,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 68, "literal": "^5.1.2", @@ -904,7 +904,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 69, "literal": "^1.3.0", @@ -917,7 +917,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 70, "literal": "^4.0.4", @@ -930,7 +930,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 71, "literal": "^4.0.1", @@ -943,7 +943,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 72, "literal": "2.1.5", @@ -956,7 +956,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 73, "literal": "^1.6.0", @@ -969,7 +969,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 74, "literal": "^1.0.4", @@ -982,7 +982,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 75, "literal": "2.0.5", @@ -995,7 +995,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 76, "literal": "^1.1.9", @@ -1008,7 +1008,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 77, "literal": "^1.2.2", @@ -1021,7 +1021,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 78, "literal": "~3.1.2", @@ -1034,7 +1034,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 79, "literal": "~3.0.2", @@ -1047,7 +1047,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 80, "literal": "~5.1.2", @@ -1060,7 +1060,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 81, "literal": "~2.1.0", @@ -1073,7 +1073,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 82, "literal": "~4.0.1", @@ -1086,7 +1086,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 83, "literal": "~3.0.0", @@ -1099,7 +1099,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 84, "literal": "~3.6.0", @@ -1125,7 +1125,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 86, "literal": "^2.2.1", @@ -1138,7 +1138,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 87, "literal": "^2.0.0", @@ -1151,7 +1151,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 88, "literal": "^3.0.0", @@ -1164,7 +1164,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 89, "literal": "^2.0.4", @@ -1177,7 +1177,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 90, "literal": "^0.3.2", @@ -1190,7 +1190,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 91, "literal": "^4.0.0", @@ -1203,7 +1203,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 92, "literal": "^10.3.10", @@ -1216,7 +1216,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 93, "literal": "^1.1.6", @@ -1229,7 +1229,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 94, "literal": "^2.7.0", @@ -1242,7 +1242,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 95, "literal": "^4.0.1", @@ -1255,7 +1255,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 96, "literal": "^0.1.9", @@ -1268,7 +1268,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 97, "literal": "^1.0.0", @@ -1281,7 +1281,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 98, "literal": "^4.0.1", @@ -1294,7 +1294,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 99, "literal": "^1.0.0", @@ -1307,7 +1307,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 100, "literal": ">= 3.1.0 < 4", @@ -1320,7 +1320,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 101, "literal": "^1.0.0", @@ -1333,7 +1333,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 102, "literal": "^3.1.0", @@ -1346,7 +1346,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 103, "literal": "^2.3.5", @@ -1359,7 +1359,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 104, "literal": "^9.0.1", @@ -1372,7 +1372,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 105, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -1385,7 +1385,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 106, "literal": "^1.10.1", @@ -1398,7 +1398,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 107, "literal": "^10.2.0", @@ -1411,7 +1411,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 108, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -1424,7 +1424,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 109, "literal": "^2.0.1", @@ -1437,7 +1437,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 110, "literal": "^1.0.0", @@ -1450,7 +1450,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 111, "literal": "^8.0.2", @@ -1476,7 +1476,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 113, "literal": "^5.1.2", @@ -1489,7 +1489,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 114, "is_alias": true, @@ -1503,7 +1503,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 115, "literal": "^7.0.1", @@ -1516,7 +1516,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 116, "is_alias": true, @@ -1530,7 +1530,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 117, "literal": "^8.1.0", @@ -1543,7 +1543,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 118, "is_alias": true, @@ -1557,7 +1557,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 119, "literal": "^4.0.0", @@ -1570,7 +1570,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 120, "literal": "^4.1.0", @@ -1583,7 +1583,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 121, "literal": "^6.0.0", @@ -1596,7 +1596,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 122, "literal": "^5.0.1", @@ -1609,7 +1609,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 123, "literal": "^8.0.0", @@ -1622,7 +1622,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 124, "literal": "^3.0.0", @@ -1635,7 +1635,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 125, "literal": "^6.0.1", @@ -1648,7 +1648,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 126, "literal": "^2.0.1", @@ -1661,7 +1661,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 127, "literal": "~1.1.4", @@ -1674,7 +1674,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 128, "literal": "^6.1.0", @@ -1687,7 +1687,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 129, "literal": "^5.0.1", @@ -1700,7 +1700,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 130, "literal": "^7.0.1", @@ -1713,7 +1713,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 131, "literal": "^6.0.1", @@ -1726,7 +1726,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 132, "literal": "^0.2.0", @@ -1739,7 +1739,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 133, "literal": "^9.2.2", @@ -1752,7 +1752,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 134, "literal": "^7.0.1", @@ -1765,7 +1765,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 135, "literal": "^7.0.0", @@ -1778,7 +1778,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 136, "literal": "^4.0.1", @@ -1791,7 +1791,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 137, "literal": "^3.1.0", @@ -1804,7 +1804,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 138, "literal": "^2.0.0", @@ -1817,7 +1817,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 139, "literal": "^2.0.1", @@ -1830,7 +1830,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 140, "literal": "^2.0.0", @@ -1843,7 +1843,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 141, "literal": "^3.0.0", @@ -1856,7 +1856,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 142, "literal": "^1.2.1", @@ -1869,7 +1869,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 143, "literal": "^1.4.10", @@ -1882,7 +1882,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 144, "literal": "^0.3.24", @@ -1895,7 +1895,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 145, "literal": "^3.1.0", @@ -1908,7 +1908,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 146, "literal": "^1.4.14", @@ -1921,7 +1921,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 147, "literal": "^0.23.0", @@ -1934,7 +1934,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 148, "literal": "^1.1.0", @@ -1960,7 +1960,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 150, "literal": "^1.1.0", @@ -1973,7 +1973,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 151, "literal": "^3.0.0 || ^4.0.0", @@ -1986,7 +1986,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 152, "literal": "^1.1.0", @@ -1999,7 +1999,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 153, "literal": "9.0.0", @@ -2012,7 +2012,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 154, "literal": "22.12.0", @@ -2025,7 +2025,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 155, "literal": "2.2.3", @@ -2038,7 +2038,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 156, "literal": "0.0.1299070", @@ -2051,7 +2051,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 157, "literal": "4.3.4", @@ -2064,7 +2064,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 158, "literal": "2.0.1", @@ -2077,7 +2077,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 159, "literal": "2.0.3", @@ -2090,7 +2090,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 160, "literal": "6.4.0", @@ -2103,7 +2103,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 161, "literal": "3.0.5", @@ -2116,7 +2116,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 162, "literal": "1.4.3", @@ -2129,7 +2129,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 163, "literal": "17.7.2", @@ -2142,7 +2142,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 164, "literal": "7.6.0", @@ -2155,7 +2155,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 165, "literal": "^6.0.0", @@ -2168,7 +2168,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 166, "literal": "^4.0.0", @@ -2181,7 +2181,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 167, "literal": "^8.0.1", @@ -2194,7 +2194,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 168, "literal": "^3.1.1", @@ -2207,7 +2207,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 169, "literal": "^2.0.5", @@ -2220,7 +2220,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 170, "literal": "^2.1.1", @@ -2233,7 +2233,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 171, "literal": "^4.2.3", @@ -2246,7 +2246,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 172, "literal": "^5.0.5", @@ -2259,7 +2259,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 173, "literal": "^21.1.1", @@ -2272,7 +2272,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 174, "literal": "^4.2.0", @@ -2285,7 +2285,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 175, "literal": "^6.0.1", @@ -2298,7 +2298,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 176, "literal": "^7.0.0", @@ -2311,7 +2311,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 177, "literal": "^5.2.1", @@ -2324,7 +2324,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 178, "literal": "^2.3.8", @@ -2337,7 +2337,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 179, "literal": "^1.3.1", @@ -2350,7 +2350,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 180, "literal": "^1.1.13", @@ -2363,7 +2363,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 181, "literal": "^3.0.0", @@ -2376,7 +2376,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 182, "literal": "^3.1.5", @@ -2415,7 +2415,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 185, "literal": "^2.1.0", @@ -2428,7 +2428,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 186, "literal": "^2.0.0", @@ -2441,7 +2441,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 187, "literal": "^2.0.0", @@ -2454,7 +2454,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 188, "literal": "^2.0.0", @@ -2467,7 +2467,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 189, "literal": "^2.18.0", @@ -2480,7 +2480,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 190, "literal": "^1.3.2", @@ -2493,7 +2493,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 191, "literal": "^1.0.1", @@ -2506,7 +2506,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 192, "literal": "^1.1.0", @@ -2532,7 +2532,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 194, "literal": "^1.6.4", @@ -2545,7 +2545,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 195, "literal": "^1.6.4", @@ -2558,7 +2558,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 196, "literal": "^1.2.0", @@ -2571,7 +2571,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 197, "literal": "^2.15.0", @@ -2584,7 +2584,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 198, "literal": "^1.1.0", @@ -2597,7 +2597,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 199, "literal": "^1.3.1", @@ -2610,7 +2610,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 200, "literal": "1", @@ -2623,7 +2623,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 201, "literal": "^1.4.0", @@ -2636,7 +2636,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 202, "literal": "^7.0.2", @@ -2649,7 +2649,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 203, "literal": "^4.3.4", @@ -2662,7 +2662,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 204, "literal": "^7.0.1", @@ -2675,7 +2675,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 205, "literal": "^7.0.3", @@ -2688,7 +2688,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 206, "literal": "^7.14.1", @@ -2701,7 +2701,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 207, "literal": "^7.0.1", @@ -2714,7 +2714,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 208, "literal": "^1.1.0", @@ -2727,7 +2727,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 209, "literal": "^8.0.2", @@ -2740,7 +2740,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 210, "literal": "^7.1.1", @@ -2753,7 +2753,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 211, "literal": "^4.3.4", @@ -2766,7 +2766,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 212, "literal": "^2.7.1", @@ -2779,7 +2779,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 213, "literal": "^9.0.5", @@ -2792,7 +2792,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 214, "literal": "^4.2.0", @@ -2805,7 +2805,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 215, "literal": "1.1.0", @@ -2818,7 +2818,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 216, "literal": "^1.1.3", @@ -2831,7 +2831,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 217, "literal": "2.1.2", @@ -2844,7 +2844,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 218, "literal": "^4.3.4", @@ -2857,7 +2857,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 219, "literal": "^0.23.0", @@ -2870,7 +2870,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 220, "literal": "^7.0.2", @@ -2883,7 +2883,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 221, "literal": "^4.3.4", @@ -2896,7 +2896,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 222, "literal": "^6.0.1", @@ -2909,7 +2909,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 223, "literal": "^7.0.0", @@ -2922,7 +2922,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 224, "literal": "^7.0.2", @@ -2935,7 +2935,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 225, "literal": "^7.0.0", @@ -2948,7 +2948,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 226, "literal": "^8.0.2", @@ -2961,7 +2961,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 227, "literal": "^5.0.0", @@ -2974,7 +2974,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 228, "literal": "^2.0.2", @@ -2987,7 +2987,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 229, "literal": "^0.13.4", @@ -3000,7 +3000,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 230, "literal": "^2.1.0", @@ -3013,7 +3013,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 231, "literal": "^4.0.1", @@ -3026,7 +3026,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 232, "literal": "^5.2.0", @@ -3039,7 +3039,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 233, "literal": "^2.0.2", @@ -3052,7 +3052,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 234, "literal": "^4.0.1", @@ -3078,7 +3078,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 236, "literal": "^2.0.1", @@ -3091,7 +3091,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 237, "literal": "^7.0.2", @@ -3104,7 +3104,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 238, "literal": "4", @@ -3117,7 +3117,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 239, "literal": "^7.1.0", @@ -3130,7 +3130,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 240, "literal": "^4.3.4", @@ -3143,7 +3143,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 241, "literal": "^5.0.2", @@ -3156,7 +3156,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 242, "literal": "^6.0.2", @@ -3169,7 +3169,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 243, "literal": "^4.3.4", @@ -3182,7 +3182,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 244, "literal": "^11.2.0", @@ -3195,7 +3195,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 245, "literal": "^4.2.0", @@ -3208,7 +3208,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 246, "literal": "^6.0.1", @@ -3221,7 +3221,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 247, "literal": "^2.0.0", @@ -3234,7 +3234,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 248, "literal": "^2.0.0", @@ -3260,7 +3260,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 250, "literal": "^4.1.1", @@ -3273,7 +3273,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 251, "literal": "^5.1.0", @@ -3286,7 +3286,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 252, "literal": "^2.10.0", @@ -3312,7 +3312,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 254, "literal": "*", @@ -3325,7 +3325,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 255, "literal": "~5.26.4", @@ -3338,7 +3338,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 256, "literal": "~1.1.0", @@ -3351,7 +3351,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 257, "literal": "~0.2.3", @@ -3364,7 +3364,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 258, "literal": "~1.2.0", @@ -3377,7 +3377,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 259, "literal": "^3.0.0", @@ -3390,7 +3390,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 260, "literal": "2.1.2", @@ -3403,7 +3403,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 261, "literal": "2.2.3", @@ -3416,7 +3416,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 262, "literal": "0.5.24", @@ -3429,7 +3429,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 263, "literal": "4.3.5", @@ -3442,7 +3442,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 264, "literal": "0.0.1299070", @@ -3455,7 +3455,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 265, "literal": "8.17.1", @@ -3496,7 +3496,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 268, "literal": "3.0.1", @@ -3509,7 +3509,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 269, "literal": "10.0.0", @@ -3522,7 +3522,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 270, "literal": "3.23.8", @@ -3548,7 +3548,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 272, "literal": "^2.2.1", @@ -3561,7 +3561,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 273, "literal": "^3.3.0", @@ -3574,7 +3574,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 274, "literal": "^4.1.0", @@ -3587,7 +3587,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 275, "literal": "^5.2.0", @@ -3614,7 +3614,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 277, "literal": "^7.0.0", @@ -3627,7 +3627,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 278, "literal": "^1.3.1", @@ -3640,7 +3640,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 279, "literal": "^2.3.0", @@ -3653,7 +3653,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 280, "literal": "^1.1.6", @@ -3666,7 +3666,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 281, "literal": "^0.2.1", @@ -3679,7 +3679,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 282, "literal": "^7.24.7", @@ -3692,7 +3692,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 283, "literal": "^1.0.0", @@ -3705,7 +3705,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 284, "literal": "^7.24.7", @@ -3718,7 +3718,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 285, "literal": "^2.4.2", @@ -3731,7 +3731,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 286, "literal": "^4.0.0", @@ -3744,7 +3744,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 287, "literal": "^1.0.0", @@ -3757,7 +3757,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 288, "literal": "^3.2.1", @@ -3770,7 +3770,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 289, "literal": "^1.0.5", @@ -3783,7 +3783,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 290, "literal": "^5.3.0", @@ -3796,7 +3796,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 291, "literal": "^3.0.0", @@ -3809,7 +3809,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 292, "literal": "^1.9.0", @@ -3822,7 +3822,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 293, "literal": "1.1.3", @@ -3835,7 +3835,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 294, "literal": "^2.0.1", @@ -3848,7 +3848,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 295, "literal": "^1.0.0", @@ -3861,7 +3861,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 296, "literal": "^4.0.0", @@ -3874,7 +3874,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 297, "literal": "^3.0.0", @@ -3887,7 +3887,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 298, "literal": "1.6.0", @@ -3900,7 +3900,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 299, "literal": "8.4.31", @@ -3913,7 +3913,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 300, "literal": "14.1.3", @@ -3926,7 +3926,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 301, "literal": "5.1.1", @@ -3939,7 +3939,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 302, "literal": "^4.2.11", @@ -3952,7 +3952,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 303, "literal": "0.5.2", @@ -3965,7 +3965,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 304, "literal": "^1.0.30001579", @@ -4149,7 +4149,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 318, "literal": "^2.4.0", @@ -4162,7 +4162,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 319, "literal": "0.0.1", @@ -4188,7 +4188,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 321, "literal": "^3.3.6", @@ -4201,7 +4201,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 322, "literal": "^1.0.0", @@ -4214,7 +4214,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 323, "literal": "^1.0.2", @@ -4227,7 +4227,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 324, "literal": "^1.1.0", @@ -4240,7 +4240,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 325, "literal": "^7.33.2", @@ -4253,7 +4253,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 326, "literal": "^2.28.1", @@ -4266,7 +4266,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 327, "literal": "^6.7.1", @@ -4279,7 +4279,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 328, "literal": "^1.3.3", @@ -4292,7 +4292,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 329, "literal": "14.1.3", @@ -4305,7 +4305,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 330, "literal": "^5.4.2 || ^6.0.0", @@ -4318,7 +4318,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 331, "literal": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705", @@ -4331,7 +4331,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 332, "literal": "^0.3.6", @@ -4344,7 +4344,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 333, "literal": "^3.5.2", @@ -4384,7 +4384,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 336, "literal": "^6.12.4", @@ -4397,7 +4397,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 337, "literal": "^0.4.1", @@ -4410,7 +4410,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 338, "literal": "^4.0.0", @@ -4423,7 +4423,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 339, "literal": "^4.3.2", @@ -4436,7 +4436,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 340, "literal": "^9.6.1", @@ -4449,7 +4449,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 341, "literal": "^5.2.0", @@ -4462,7 +4462,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 342, "literal": "^1.4.2", @@ -4475,7 +4475,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 343, "literal": "^2.0.2", @@ -4488,7 +4488,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 344, "literal": "^5.0.0", @@ -4501,7 +4501,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 345, "literal": "^13.19.0", @@ -4514,7 +4514,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 346, "literal": "^4.0.0", @@ -4527,7 +4527,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 347, "literal": "^4.1.0", @@ -4540,7 +4540,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 348, "literal": "^3.0.0", @@ -4553,7 +4553,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 349, "literal": "^1.4.0", @@ -4566,7 +4566,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 350, "literal": "^3.1.2", @@ -4579,7 +4579,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 351, "literal": "8.50.0", @@ -4592,7 +4592,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 352, "literal": "^0.9.3", @@ -4605,7 +4605,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 353, "literal": "^6.0.1", @@ -4618,7 +4618,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 354, "literal": "^0.2.0", @@ -4631,7 +4631,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 355, "literal": "^7.0.2", @@ -4644,7 +4644,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 356, "literal": "^6.0.2", @@ -4657,7 +4657,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 357, "literal": "^0.1.4", @@ -4670,7 +4670,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 358, "literal": "^7.2.2", @@ -4683,7 +4683,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 359, "literal": "^4.6.2", @@ -4696,7 +4696,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 360, "literal": "^3.0.3", @@ -4709,7 +4709,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 361, "literal": "^3.1.3", @@ -4722,7 +4722,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 362, "literal": "^1.4.0", @@ -4735,7 +4735,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 363, "literal": "^2.1.2", @@ -4748,7 +4748,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 364, "literal": "^1.2.8", @@ -4761,7 +4761,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 365, "literal": "^6.0.1", @@ -4774,7 +4774,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 366, "literal": "^3.4.3", @@ -4787,7 +4787,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 367, "literal": "^4.0.0", @@ -4800,7 +4800,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 368, "literal": "^4.6.1", @@ -4813,7 +4813,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 369, "literal": "^0.11.11", @@ -4826,7 +4826,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 370, "literal": "^4.2.0", @@ -4839,7 +4839,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 371, "literal": "^1.0.1", @@ -4852,7 +4852,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 372, "literal": "^1.0.1", @@ -4865,7 +4865,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 373, "literal": "^3.3.0", @@ -4891,7 +4891,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 375, "literal": "^2.0.2", @@ -4904,7 +4904,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 376, "literal": "^4.3.1", @@ -4917,7 +4917,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 377, "literal": "^3.0.5", @@ -4930,7 +4930,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 378, "literal": "^1.1.7", @@ -4943,7 +4943,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 379, "literal": "^1.0.0", @@ -4956,7 +4956,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 380, "literal": "0.0.1", @@ -4969,7 +4969,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 381, "literal": "^3.0.4", @@ -4982,7 +4982,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 382, "literal": "^3.2.9", @@ -4995,7 +4995,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 383, "literal": "^4.5.3", @@ -5008,7 +5008,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 384, "literal": "^3.0.2", @@ -5021,7 +5021,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 385, "literal": "^7.1.3", @@ -5034,7 +5034,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 386, "literal": "^1.0.0", @@ -5047,7 +5047,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 387, "literal": "^1.0.4", @@ -5060,7 +5060,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 388, "literal": "2", @@ -5073,7 +5073,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 389, "literal": "^3.1.1", @@ -5086,7 +5086,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 390, "literal": "^1.3.0", @@ -5099,7 +5099,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 391, "literal": "^1.0.0", @@ -5112,7 +5112,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 392, "literal": "^1.3.0", @@ -5125,7 +5125,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 393, "literal": "1", @@ -5138,7 +5138,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 394, "literal": "3.0.1", @@ -5151,7 +5151,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 395, "literal": "^6.12.4", @@ -5164,7 +5164,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 396, "literal": "^4.3.2", @@ -5177,7 +5177,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 397, "literal": "^9.6.0", @@ -5190,7 +5190,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 398, "literal": "^13.19.0", @@ -5203,7 +5203,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 399, "literal": "^5.2.0", @@ -5216,7 +5216,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 400, "literal": "^3.2.1", @@ -5229,7 +5229,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 401, "literal": "^4.1.0", @@ -5242,7 +5242,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 402, "literal": "^3.1.2", @@ -5255,7 +5255,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 403, "literal": "^3.1.1", @@ -5268,7 +5268,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 404, "literal": "^0.20.2", @@ -5281,7 +5281,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 405, "literal": "^8.9.0", @@ -5294,7 +5294,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 406, "literal": "^5.3.2", @@ -5307,7 +5307,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 407, "literal": "^3.4.1", @@ -5333,7 +5333,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 409, "literal": "^3.1.1", @@ -5346,7 +5346,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 410, "literal": "^2.0.0", @@ -5359,7 +5359,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 411, "literal": "^0.4.1", @@ -5372,7 +5372,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 412, "literal": "^4.2.2", @@ -5385,7 +5385,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 413, "literal": "^2.1.0", @@ -5398,7 +5398,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 414, "literal": "^4.3.0", @@ -5411,7 +5411,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 415, "literal": "^5.2.0", @@ -5424,7 +5424,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 416, "literal": "^5.2.0", @@ -5437,7 +5437,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 417, "literal": "^1.2.1", @@ -5450,7 +5450,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 418, "literal": "^0.1.3", @@ -5463,7 +5463,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 419, "literal": "^1.2.5", @@ -5476,7 +5476,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 420, "literal": "^0.4.0", @@ -5489,7 +5489,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 421, "literal": "^0.4.1", @@ -5502,7 +5502,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 422, "literal": "^2.0.6", @@ -5515,7 +5515,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 423, "literal": "^1.2.1", @@ -5528,7 +5528,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 424, "literal": "~0.4.0", @@ -5541,7 +5541,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 425, "literal": "^1.2.1", @@ -5554,7 +5554,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 426, "literal": "^2.0.2", @@ -5567,7 +5567,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 427, "literal": "^6.0.0", @@ -5580,7 +5580,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 428, "literal": "^4.0.0", @@ -5593,7 +5593,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 429, "literal": "^5.0.0", @@ -5606,7 +5606,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 430, "literal": "^3.0.2", @@ -5619,7 +5619,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 431, "literal": "^0.1.0", @@ -5632,7 +5632,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 432, "literal": "^5.1.0", @@ -5645,7 +5645,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 433, "literal": "^4.1.0", @@ -5658,7 +5658,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 434, "literal": "^7.1.0", @@ -5671,7 +5671,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 435, "literal": "^4.0.0", @@ -5684,7 +5684,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 436, "literal": "^4.3.4", @@ -5697,7 +5697,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 437, "literal": "^5.12.0", @@ -5710,7 +5710,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 438, "literal": "^2.7.4", @@ -5723,7 +5723,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 439, "literal": "^3.3.1", @@ -5736,7 +5736,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 440, "literal": "^4.5.0", @@ -5749,7 +5749,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 441, "literal": "^2.11.0", @@ -5762,7 +5762,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 442, "literal": "^4.0.3", @@ -5801,7 +5801,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 445, "literal": "^3.1.7", @@ -5814,7 +5814,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 446, "literal": "^1.2.3", @@ -5827,7 +5827,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 447, "literal": "^1.3.2", @@ -5840,7 +5840,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 448, "literal": "^1.3.2", @@ -5853,7 +5853,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 449, "literal": "^3.2.7", @@ -5866,7 +5866,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 450, "literal": "^2.1.0", @@ -5879,7 +5879,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 451, "literal": "^0.3.9", @@ -5892,7 +5892,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 452, "literal": "^2.8.0", @@ -5905,7 +5905,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 453, "literal": "^2.0.0", @@ -5918,7 +5918,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 454, "literal": "^2.13.1", @@ -5931,7 +5931,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 455, "literal": "^4.0.3", @@ -5944,7 +5944,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 456, "literal": "^3.1.2", @@ -5957,7 +5957,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 457, "literal": "^2.0.7", @@ -5970,7 +5970,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 458, "literal": "^1.0.1", @@ -5983,7 +5983,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 459, "literal": "^1.1.7", @@ -5996,7 +5996,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 460, "literal": "^6.3.1", @@ -6009,7 +6009,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 461, "literal": "^3.15.0", @@ -6035,7 +6035,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 463, "literal": "^0.0.29", @@ -6048,7 +6048,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 464, "literal": "^1.0.2", @@ -6061,7 +6061,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 465, "literal": "^1.2.6", @@ -6074,7 +6074,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 466, "literal": "^3.0.0", @@ -6087,7 +6087,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 467, "literal": "^1.2.0", @@ -6100,7 +6100,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 468, "literal": "^1.0.7", @@ -6113,7 +6113,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 469, "literal": "^1.2.1", @@ -6126,7 +6126,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 470, "literal": "^1.0.0", @@ -6139,7 +6139,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 471, "literal": "^1.3.0", @@ -6152,7 +6152,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 472, "literal": "^1.0.1", @@ -6165,7 +6165,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 473, "literal": "^1.0.0", @@ -6178,7 +6178,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 474, "literal": "^1.1.1", @@ -6191,7 +6191,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 475, "literal": "^1.0.0", @@ -6204,7 +6204,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 476, "literal": "^1.2.4", @@ -6217,7 +6217,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 477, "literal": "^1.3.0", @@ -6230,7 +6230,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 478, "literal": "^1.1.2", @@ -6243,7 +6243,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 479, "literal": "^1.0.1", @@ -6256,7 +6256,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 480, "literal": "^1.0.3", @@ -6269,7 +6269,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 481, "literal": "^2.0.0", @@ -6282,7 +6282,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 482, "literal": "^1.0.0", @@ -6295,7 +6295,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 483, "literal": "^1.3.0", @@ -6308,7 +6308,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 484, "literal": "^1.0.1", @@ -6321,7 +6321,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 485, "literal": "^1.1.3", @@ -6334,7 +6334,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 486, "literal": "^1.0.0", @@ -6347,7 +6347,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 487, "literal": "^1.3.0", @@ -6360,7 +6360,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 488, "literal": "^1.1.2", @@ -6373,7 +6373,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 489, "literal": "^1.2.4", @@ -6386,7 +6386,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 490, "literal": "^1.2.1", @@ -6399,7 +6399,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 491, "literal": "^1.1.4", @@ -6412,7 +6412,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 492, "literal": "^1.3.0", @@ -6425,7 +6425,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 493, "literal": "^1.1.2", @@ -6438,7 +6438,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 494, "literal": "^1.2.4", @@ -6451,7 +6451,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 495, "literal": "^1.0.1", @@ -6464,7 +6464,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 496, "literal": "^1.0.2", @@ -6477,7 +6477,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 497, "literal": "^1.0.7", @@ -6490,7 +6490,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 498, "literal": "^1.2.1", @@ -6503,7 +6503,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 499, "literal": "^1.23.2", @@ -6516,7 +6516,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 500, "literal": "^1.0.1", @@ -6529,7 +6529,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 501, "literal": "^1.0.3", @@ -6542,7 +6542,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 502, "literal": "^1.0.7", @@ -6555,7 +6555,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 503, "literal": "^1.0.7", @@ -6568,7 +6568,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 504, "literal": "^1.0.1", @@ -6581,7 +6581,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 505, "literal": "^1.0.1", @@ -6594,7 +6594,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 506, "literal": "^1.0.0", @@ -6607,7 +6607,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 507, "literal": "^1.0.0", @@ -6620,7 +6620,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 508, "literal": "^1.3.0", @@ -6633,7 +6633,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 509, "literal": "^1.0.0", @@ -6646,7 +6646,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 510, "literal": "^2.0.3", @@ -6659,7 +6659,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 511, "literal": "^1.2.1", @@ -6672,7 +6672,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 512, "literal": "^1.1.6", @@ -6685,7 +6685,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 513, "literal": "^1.2.4", @@ -6698,7 +6698,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 514, "literal": "^1.0.2", @@ -6711,7 +6711,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 515, "literal": "^1.0.3", @@ -6724,7 +6724,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 516, "literal": "^1.0.1", @@ -6737,7 +6737,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 517, "literal": "^1.0.2", @@ -6750,7 +6750,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 518, "literal": "^1.0.3", @@ -6763,7 +6763,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 519, "literal": "^1.0.3", @@ -6776,7 +6776,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 520, "literal": "^2.0.2", @@ -6789,7 +6789,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 521, "literal": "^1.0.7", @@ -6802,7 +6802,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 522, "literal": "^3.0.4", @@ -6815,7 +6815,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 523, "literal": "^1.2.7", @@ -6828,7 +6828,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 524, "literal": "^1.0.1", @@ -6841,7 +6841,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 525, "literal": "^2.0.3", @@ -6854,7 +6854,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 526, "literal": "^1.1.4", @@ -6867,7 +6867,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 527, "literal": "^1.0.3", @@ -6880,7 +6880,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 528, "literal": "^1.0.7", @@ -6893,7 +6893,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 529, "literal": "^1.1.13", @@ -6906,7 +6906,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 530, "literal": "^1.0.2", @@ -6919,7 +6919,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 531, "literal": "^1.13.1", @@ -6932,7 +6932,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 532, "literal": "^1.1.1", @@ -6945,7 +6945,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 533, "literal": "^4.1.5", @@ -6958,7 +6958,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 534, "literal": "^1.5.2", @@ -6971,7 +6971,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 535, "literal": "^1.1.2", @@ -6984,7 +6984,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 536, "literal": "^1.0.3", @@ -6997,7 +6997,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 537, "literal": "^1.2.9", @@ -7010,7 +7010,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 538, "literal": "^1.0.8", @@ -7023,7 +7023,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 539, "literal": "^1.0.8", @@ -7036,7 +7036,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 540, "literal": "^1.0.2", @@ -7049,7 +7049,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 541, "literal": "^1.0.1", @@ -7062,7 +7062,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 542, "literal": "^1.0.2", @@ -7075,7 +7075,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 543, "literal": "^1.0.6", @@ -7088,7 +7088,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 544, "literal": "^1.0.2", @@ -7101,7 +7101,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 545, "literal": "^1.1.15", @@ -7114,7 +7114,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 546, "literal": "^1.0.7", @@ -7127,7 +7127,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 547, "literal": "^1.0.7", @@ -7140,7 +7140,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 548, "literal": "^0.3.3", @@ -7153,7 +7153,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 549, "literal": "^1.0.1", @@ -7166,7 +7166,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 550, "literal": "^1.0.2", @@ -7179,7 +7179,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 551, "literal": "^1.0.3", @@ -7192,7 +7192,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 552, "literal": "^1.1.3", @@ -7205,7 +7205,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 553, "literal": "^1.0.0", @@ -7218,7 +7218,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 554, "literal": "^1.0.2", @@ -7231,7 +7231,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 555, "literal": "^1.0.2", @@ -7244,7 +7244,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 556, "literal": "^1.0.3", @@ -7257,7 +7257,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 557, "literal": "^1.0.2", @@ -7270,7 +7270,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 558, "literal": "^1.0.1", @@ -7283,7 +7283,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 559, "literal": "^1.1.0", @@ -7296,7 +7296,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 560, "literal": "^1.0.4", @@ -7309,7 +7309,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 561, "literal": "^1.0.5", @@ -7322,7 +7322,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 562, "literal": "^1.0.3", @@ -7335,7 +7335,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 563, "literal": "^1.0.2", @@ -7348,7 +7348,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 564, "literal": "^1.0.0", @@ -7361,7 +7361,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 565, "literal": "^1.0.0", @@ -7374,7 +7374,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 566, "literal": "^1.0.2", @@ -7387,7 +7387,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 567, "literal": "^1.0.0", @@ -7400,7 +7400,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 568, "literal": "^1.0.1", @@ -7413,7 +7413,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 569, "literal": "^1.0.7", @@ -7426,7 +7426,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 570, "literal": "^0.3.3", @@ -7439,7 +7439,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 571, "literal": "^1.0.1", @@ -7452,7 +7452,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 572, "literal": "^1.0.3", @@ -7465,7 +7465,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 573, "literal": "^1.1.13", @@ -7478,7 +7478,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 574, "literal": "^1.0.0", @@ -7491,7 +7491,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 575, "literal": "^1.1.14", @@ -7504,7 +7504,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 576, "literal": "^1.0.7", @@ -7517,7 +7517,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 577, "literal": "^1.0.7", @@ -7530,7 +7530,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 578, "literal": "^0.3.3", @@ -7543,7 +7543,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 579, "literal": "^1.0.1", @@ -7556,7 +7556,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 580, "literal": "^1.0.3", @@ -7569,7 +7569,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 581, "literal": "^1.1.13", @@ -7582,7 +7582,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 582, "literal": "^1.0.7", @@ -7595,7 +7595,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 583, "literal": "^0.3.3", @@ -7608,7 +7608,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 584, "literal": "^1.0.1", @@ -7621,7 +7621,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 585, "literal": "^1.0.3", @@ -7634,7 +7634,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 586, "literal": "^1.1.13", @@ -7647,7 +7647,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 587, "literal": "^1.0.7", @@ -7660,7 +7660,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 588, "literal": "^1.3.0", @@ -7673,7 +7673,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 589, "literal": "^1.1.13", @@ -7686,7 +7686,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 590, "literal": "^1.0.7", @@ -7699,7 +7699,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 591, "literal": "^1.2.1", @@ -7712,7 +7712,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 592, "literal": "^1.0.0", @@ -7725,7 +7725,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 593, "literal": "^1.0.7", @@ -7738,7 +7738,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 594, "literal": "^1.2.1", @@ -7751,7 +7751,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 595, "literal": "^1.0.0", @@ -7764,7 +7764,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 596, "literal": "^1.0.7", @@ -7777,7 +7777,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 597, "literal": "^1.2.1", @@ -7790,7 +7790,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 598, "literal": "^1.23.0", @@ -7803,7 +7803,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 599, "literal": "^1.0.0", @@ -7816,7 +7816,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 600, "literal": "^1.0.6", @@ -7829,7 +7829,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 601, "literal": "^1.3.0", @@ -7842,7 +7842,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 602, "literal": "^1.1.4", @@ -7855,7 +7855,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 603, "literal": "^1.0.2", @@ -7868,7 +7868,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 604, "literal": "^1.0.0", @@ -7881,7 +7881,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 605, "literal": "^1.0.7", @@ -7894,7 +7894,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 606, "literal": "^1.2.4", @@ -7907,7 +7907,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 607, "literal": "^1.0.3", @@ -7920,7 +7920,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 608, "literal": "^2.0.5", @@ -7933,7 +7933,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 609, "literal": "^1.0.6", @@ -7946,7 +7946,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 610, "literal": "^1.2.1", @@ -7959,7 +7959,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 611, "literal": "^1.3.0", @@ -7972,7 +7972,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 612, "literal": "^2.0.1", @@ -7985,7 +7985,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 613, "literal": "^1.1.4", @@ -7998,7 +7998,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 614, "literal": "^1.3.0", @@ -8011,7 +8011,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 615, "literal": "^1.2.3", @@ -8024,7 +8024,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 616, "literal": "^1.0.2", @@ -8037,7 +8037,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 617, "literal": "^1.0.5", @@ -8050,7 +8050,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 618, "literal": "^1.2.1", @@ -8063,7 +8063,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 619, "literal": "^1.0.3", @@ -8076,7 +8076,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 620, "literal": "^1.1.1", @@ -8089,7 +8089,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 621, "literal": "^1.0.2", @@ -8102,7 +8102,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 622, "literal": "^1.0.7", @@ -8115,7 +8115,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 623, "literal": "^1.1.13", @@ -8128,7 +8128,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 624, "literal": "^1.0.2", @@ -8141,7 +8141,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 625, "literal": "^1.2.1", @@ -8154,7 +8154,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 626, "literal": "^1.3.0", @@ -8167,7 +8167,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 627, "literal": "^2.0.0", @@ -8180,7 +8180,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 628, "literal": "^1.0.4", @@ -8193,7 +8193,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 629, "literal": "^1.0.7", @@ -8206,7 +8206,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 630, "literal": "^1.3.0", @@ -8219,7 +8219,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 631, "literal": "^1.2.4", @@ -8232,7 +8232,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 632, "literal": "^1.13.1", @@ -8245,7 +8245,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 633, "literal": "^1.2.1", @@ -8258,7 +8258,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 634, "literal": "^1.0.1", @@ -8271,7 +8271,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 635, "literal": "^1.0.5", @@ -8284,7 +8284,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 636, "literal": "^1.3.0", @@ -8297,7 +8297,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 637, "literal": "^1.2.4", @@ -8310,7 +8310,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 638, "literal": "^1.0.2", @@ -8323,7 +8323,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 639, "literal": "^1.2.0", @@ -8336,7 +8336,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 640, "literal": "^1.22.1", @@ -8349,7 +8349,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 641, "literal": "^1.2.3", @@ -8362,7 +8362,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 642, "literal": "^1.1.4", @@ -8375,7 +8375,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 643, "literal": "^1.0.1", @@ -8388,7 +8388,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 644, "literal": "^1.0.2", @@ -8401,7 +8401,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 645, "literal": "^1.0.0", @@ -8414,7 +8414,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 646, "literal": "^1.2.4", @@ -8427,7 +8427,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 647, "literal": "^1.0.2", @@ -8440,7 +8440,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 648, "literal": "^2.0.1", @@ -8453,7 +8453,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 649, "literal": "^1.0.6", @@ -8466,7 +8466,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 650, "literal": "^1.3.0", @@ -8479,7 +8479,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 651, "literal": "^1.0.1", @@ -8492,7 +8492,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 652, "literal": "^1.0.7", @@ -8505,7 +8505,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 653, "literal": "^1.3.0", @@ -8518,7 +8518,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 654, "literal": "^1.0.1", @@ -8531,7 +8531,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 655, "literal": "^1.0.6", @@ -8544,7 +8544,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 656, "literal": "^1.3.0", @@ -8557,7 +8557,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 657, "literal": "^1.0.1", @@ -8570,7 +8570,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 658, "literal": "^1.0.1", @@ -8583,7 +8583,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 659, "literal": "^1.0.5", @@ -8596,7 +8596,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 660, "literal": "^1.2.1", @@ -8609,7 +8609,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 661, "literal": "^1.22.3", @@ -8622,7 +8622,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 662, "literal": "^1.2.1", @@ -8635,7 +8635,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 663, "literal": "^1.2.3", @@ -8648,7 +8648,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 664, "literal": "^3.0.4", @@ -8661,7 +8661,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 665, "literal": "^1.0.2", @@ -8674,7 +8674,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 666, "literal": "^1.0.5", @@ -8687,7 +8687,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 667, "literal": "^3.0.4", @@ -8700,7 +8700,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 668, "literal": "^1.0.7", @@ -8713,7 +8713,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 669, "literal": "^1.2.1", @@ -8726,7 +8726,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 670, "literal": "^1.23.2", @@ -8739,7 +8739,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 671, "literal": "^1.0.0", @@ -8752,7 +8752,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 672, "literal": "^3.2.7", @@ -8765,7 +8765,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 673, "literal": "^2.1.1", @@ -8778,7 +8778,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 674, "literal": "^3.2.7", @@ -8791,7 +8791,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 675, "literal": "^2.13.0", @@ -8804,7 +8804,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 676, "literal": "^1.22.4", @@ -8817,7 +8817,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 677, "literal": "^2.0.2", @@ -8830,7 +8830,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 678, "literal": "^1.0.2", @@ -8843,7 +8843,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 679, "literal": "^1.2.0", @@ -8856,7 +8856,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 680, "literal": "^1.22.1", @@ -8869,7 +8869,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 681, "literal": "^1.0.0", @@ -8882,7 +8882,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 682, "literal": "^2.0.0", @@ -8895,7 +8895,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 683, "literal": "^1.0.2", @@ -8908,7 +8908,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 684, "literal": "^1.2.0", @@ -8921,7 +8921,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 685, "literal": "^1.22.1", @@ -8934,7 +8934,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 686, "literal": "^1.0.0", @@ -8947,7 +8947,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 687, "literal": "^1.0.7", @@ -8960,7 +8960,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 688, "literal": "^1.2.1", @@ -8973,7 +8973,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 689, "literal": "^1.23.2", @@ -8986,7 +8986,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 690, "literal": "^1.3.0", @@ -8999,7 +8999,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 691, "literal": "^1.0.0", @@ -9012,7 +9012,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 692, "literal": "^1.0.2", @@ -9025,7 +9025,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 693, "literal": "^1.0.7", @@ -9038,7 +9038,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 694, "literal": "^1.2.1", @@ -9051,7 +9051,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 695, "literal": "^1.23.2", @@ -9064,7 +9064,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 696, "literal": "^1.0.0", @@ -9077,7 +9077,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 697, "literal": "^1.2.4", @@ -9090,7 +9090,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 698, "literal": "^1.0.7", @@ -9103,7 +9103,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 699, "literal": "^1.0.0", @@ -9116,7 +9116,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 700, "literal": "^4.2.4", @@ -9129,7 +9129,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 701, "literal": "^2.2.0", @@ -9155,7 +9155,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 703, "literal": "^4.3.4", @@ -9168,7 +9168,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 704, "literal": "6.21.0", @@ -9181,7 +9181,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 705, "literal": "6.21.0", @@ -9194,7 +9194,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 706, "literal": "6.21.0", @@ -9207,7 +9207,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 707, "literal": "6.21.0", @@ -9233,7 +9233,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 709, "literal": "^4.3.4", @@ -9246,7 +9246,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 710, "literal": "^11.1.0", @@ -9259,7 +9259,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 711, "literal": "^7.5.4", @@ -9272,7 +9272,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 712, "literal": "^4.0.3", @@ -9285,7 +9285,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 713, "literal": "9.0.3", @@ -9298,7 +9298,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 714, "literal": "^1.0.1", @@ -9311,7 +9311,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 715, "literal": "6.21.0", @@ -9324,7 +9324,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 716, "literal": "6.21.0", @@ -9337,7 +9337,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 717, "literal": "^3.4.1", @@ -9350,7 +9350,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 718, "literal": "6.21.0", @@ -9376,7 +9376,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 720, "literal": "^2.0.1", @@ -9389,7 +9389,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 721, "literal": "^2.1.0", @@ -9402,7 +9402,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 722, "literal": "^3.0.1", @@ -9415,7 +9415,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 723, "literal": "^3.2.9", @@ -9428,7 +9428,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 724, "literal": "^5.2.0", @@ -9441,7 +9441,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 725, "literal": "^1.4.1", @@ -9454,7 +9454,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 726, "literal": "^3.0.0", @@ -9467,7 +9467,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 727, "literal": "^4.0.0", @@ -9480,7 +9480,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 728, "literal": "6.21.0", @@ -9493,7 +9493,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 729, "literal": "6.21.0", @@ -9506,7 +9506,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 730, "literal": "10.3.10", @@ -9519,7 +9519,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 731, "literal": "^7.23.2", @@ -9532,7 +9532,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 732, "literal": "^5.3.0", @@ -9545,7 +9545,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 733, "literal": "^3.1.7", @@ -9558,7 +9558,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 734, "literal": "^1.3.2", @@ -9571,7 +9571,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 735, "literal": "^0.0.8", @@ -9584,7 +9584,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 736, "literal": "=4.7.0", @@ -9597,7 +9597,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 737, "literal": "^3.2.1", @@ -9610,7 +9610,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 738, "literal": "^1.0.8", @@ -9623,7 +9623,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 739, "literal": "^9.2.2", @@ -9636,7 +9636,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 740, "literal": "^1.0.15", @@ -9649,7 +9649,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 741, "literal": "^2.0.0", @@ -9662,7 +9662,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 742, "literal": "^3.3.5", @@ -9675,7 +9675,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 743, "literal": "^1.0.9", @@ -9688,7 +9688,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 744, "literal": "^3.1.2", @@ -9701,7 +9701,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 745, "literal": "^1.1.7", @@ -9714,7 +9714,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 746, "literal": "^2.0.7", @@ -9740,7 +9740,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 748, "literal": "^1.0.7", @@ -9753,7 +9753,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 749, "literal": "^1.2.1", @@ -9766,7 +9766,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 750, "literal": "^1.0.0", @@ -9779,7 +9779,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 751, "literal": "^0.3.20", @@ -9792,7 +9792,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 752, "literal": "^3.1.6", @@ -9805,7 +9805,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 753, "literal": "^1.3.1", @@ -9818,7 +9818,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 754, "literal": "^4.1.4", @@ -9831,7 +9831,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 755, "literal": "^1.1.6", @@ -9844,7 +9844,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 756, "literal": "^1.0.7", @@ -9857,7 +9857,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 757, "literal": "^1.2.1", @@ -9870,7 +9870,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 758, "literal": "^1.23.3", @@ -9883,7 +9883,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 759, "literal": "^1.3.0", @@ -9896,7 +9896,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 760, "literal": "^2.0.3", @@ -9909,7 +9909,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 761, "literal": "^1.1.2", @@ -9922,7 +9922,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 762, "literal": "^1.2.4", @@ -9935,7 +9935,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 763, "literal": "^1.0.3", @@ -9948,7 +9948,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 764, "literal": "^1.0.2", @@ -9961,7 +9961,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 765, "literal": "^1.0.3", @@ -9974,7 +9974,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 766, "literal": "^1.0.3", @@ -9987,7 +9987,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 767, "literal": "^1.0.7", @@ -10000,7 +10000,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 768, "literal": "^1.1.2", @@ -10013,7 +10013,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 769, "literal": "^1.1.2", @@ -10026,7 +10026,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 770, "literal": "^1.2.1", @@ -10039,7 +10039,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 771, "literal": "^1.2.1", @@ -10052,7 +10052,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 772, "literal": "^1.0.3", @@ -10065,7 +10065,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 773, "literal": "^1.0.4", @@ -10078,7 +10078,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 774, "literal": "^2.0.1", @@ -10091,7 +10091,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 775, "literal": "^1.0.7", @@ -10104,7 +10104,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 776, "literal": "^1.2.1", @@ -10117,7 +10117,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 777, "literal": "^1.23.1", @@ -10130,7 +10130,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 778, "literal": "^1.3.0", @@ -10143,7 +10143,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 779, "literal": "^1.2.4", @@ -10156,7 +10156,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 780, "literal": "^1.0.3", @@ -10169,7 +10169,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 781, "literal": "^1.1.3", @@ -10182,7 +10182,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 782, "literal": "^1.1.5", @@ -10195,7 +10195,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 783, "literal": "^1.0.0", @@ -10208,7 +10208,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 784, "literal": "^2.0.0", @@ -10221,7 +10221,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 785, "literal": "^1.0.5", @@ -10234,7 +10234,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 786, "literal": "^1.0.2", @@ -10247,7 +10247,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 787, "literal": "^1.0.10", @@ -10260,7 +10260,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 788, "literal": "^1.1.4", @@ -10273,7 +10273,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 789, "literal": "^1.0.2", @@ -10286,7 +10286,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 790, "literal": "^2.0.5", @@ -10299,7 +10299,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 791, "literal": "^1.0.2", @@ -10312,7 +10312,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 792, "literal": "^1.0.1", @@ -10325,7 +10325,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 793, "literal": "^1.1.9", @@ -10338,7 +10338,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 794, "literal": "^2.0.3", @@ -10351,7 +10351,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 795, "literal": "^2.0.3", @@ -10364,7 +10364,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 796, "literal": "^2.0.2", @@ -10377,7 +10377,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 797, "literal": "^2.0.3", @@ -10390,7 +10390,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 798, "literal": "^1.0.7", @@ -10403,7 +10403,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 799, "literal": "^1.2.4", @@ -10416,7 +10416,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 800, "literal": "^1.0.0", @@ -10429,7 +10429,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 801, "literal": "^1.0.2", @@ -10442,7 +10442,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 802, "literal": "^1.0.0", @@ -10455,7 +10455,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 803, "literal": "^2.0.3", @@ -10468,7 +10468,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 804, "literal": "^2.0.3", @@ -10481,7 +10481,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 805, "literal": "^0.14.0", @@ -10494,7 +10494,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 806, "literal": "^3.1.8", @@ -10507,7 +10507,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 807, "literal": "^1.2.5", @@ -10520,7 +10520,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 808, "literal": "^1.3.2", @@ -10533,7 +10533,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 809, "literal": "^1.1.2", @@ -10546,7 +10546,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 810, "literal": "^1.1.3", @@ -10559,7 +10559,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 811, "literal": "^2.1.0", @@ -10572,7 +10572,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 812, "literal": "^1.0.19", @@ -10585,7 +10585,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 813, "literal": "^5.3.0", @@ -10598,7 +10598,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 814, "literal": "^2.4.1 || ^3.0.0", @@ -10611,7 +10611,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 815, "literal": "^3.1.2", @@ -10624,7 +10624,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 816, "literal": "^1.1.8", @@ -10637,7 +10637,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 817, "literal": "^2.0.8", @@ -10650,7 +10650,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 818, "literal": "^1.1.4", @@ -10663,7 +10663,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 819, "literal": "^1.2.0", @@ -10676,7 +10676,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 820, "literal": "^15.8.1", @@ -10689,7 +10689,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 821, "literal": "^2.0.0-next.5", @@ -10702,7 +10702,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 822, "literal": "^6.3.1", @@ -10715,7 +10715,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 823, "literal": "^4.0.11", @@ -10741,7 +10741,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 825, "literal": "^1.0.7", @@ -10754,7 +10754,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 826, "literal": "^1.2.1", @@ -10767,7 +10767,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 827, "literal": "^1.23.2", @@ -10780,7 +10780,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 828, "literal": "^1.3.0", @@ -10793,7 +10793,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 829, "literal": "^1.0.0", @@ -10806,7 +10806,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 830, "literal": "^1.2.4", @@ -10819,7 +10819,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 831, "literal": "^1.0.1", @@ -10832,7 +10832,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 832, "literal": "^1.0.3", @@ -10845,7 +10845,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 833, "literal": "^1.0.7", @@ -10858,7 +10858,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 834, "literal": "^1.5.2", @@ -10871,7 +10871,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 835, "literal": "^2.0.2", @@ -10884,7 +10884,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 836, "literal": "^1.0.6", @@ -10897,7 +10897,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 837, "literal": "^2.13.0", @@ -10910,7 +10910,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 838, "literal": "^1.0.7", @@ -10923,7 +10923,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 839, "literal": "^1.0.0", @@ -10936,7 +10936,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 840, "literal": "^1.4.0", @@ -10949,7 +10949,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 841, "literal": "^4.1.1", @@ -10962,7 +10962,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 842, "literal": "^16.13.1", @@ -10975,7 +10975,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 843, "literal": "^1.2.1", @@ -10988,7 +10988,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 844, "literal": "^1.23.2", @@ -11001,7 +11001,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 845, "literal": "^1.0.0", @@ -11014,7 +11014,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 846, "literal": "^1.0.7", @@ -11027,7 +11027,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 847, "literal": "^1.2.1", @@ -11040,7 +11040,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 848, "literal": "^1.23.3", @@ -11053,7 +11053,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 849, "literal": "^1.3.0", @@ -11066,7 +11066,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 850, "literal": "^1.0.2", @@ -11079,7 +11079,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 851, "literal": "^1.0.2", @@ -11092,7 +11092,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 852, "literal": "^1.2.0", @@ -11105,7 +11105,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 853, "literal": "^1.22.1", @@ -11118,7 +11118,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 854, "literal": "^1.0.0", @@ -11131,7 +11131,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 855, "literal": "^1.0.7", @@ -11144,7 +11144,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 856, "literal": "^1.2.1", @@ -11157,7 +11157,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 857, "literal": "^1.23.2", @@ -11170,7 +11170,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 858, "literal": "^1.3.0", @@ -11183,7 +11183,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 859, "literal": "^1.0.0", @@ -11196,7 +11196,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 860, "literal": "^1.0.2", @@ -11209,7 +11209,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 861, "literal": "~8.5.10", @@ -11222,7 +11222,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 862, "literal": "~20.12.8", @@ -11235,7 +11235,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 863, "literal": "*", @@ -11248,7 +11248,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 864, "literal": "^4.21.10", @@ -11261,7 +11261,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 865, "literal": "^1.0.30001538", @@ -11274,7 +11274,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 866, "literal": "^4.3.6", @@ -11287,7 +11287,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 867, "literal": "^0.1.2", @@ -11300,7 +11300,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 868, "literal": "^1.0.0", @@ -11313,7 +11313,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 869, "literal": "^4.2.0", @@ -11339,7 +11339,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 871, "literal": "^1.0.30001587", @@ -11352,7 +11352,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 872, "literal": "^1.4.668", @@ -11365,7 +11365,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 873, "literal": "^2.0.14", @@ -11378,7 +11378,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 874, "literal": "^1.0.13", @@ -11391,7 +11391,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 875, "literal": "^3.1.2", @@ -11404,7 +11404,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 876, "literal": "^1.0.1", @@ -11430,7 +11430,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 878, "literal": "*", @@ -11443,7 +11443,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 879, "literal": "*", @@ -11456,7 +11456,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 880, "literal": "*", @@ -11469,7 +11469,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 881, "literal": "^3.0.2", @@ -20772,7 +20772,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 374, }, "array-includes": { - "id": 806, + "id": 445, "package_id": 384, }, "array-union": { @@ -20792,7 +20792,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 382, }, "array.prototype.flatmap": { - "id": 808, + "id": 448, "package_id": 380, }, "array.prototype.toreversed": { @@ -21068,11 +21068,11 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 316, }, "es-errors": { - "id": 858, + "id": 690, "package_id": 312, }, "es-iterator-helpers": { - "id": 812, + "id": 740, "package_id": 409, }, "es-object-atoms": { @@ -21084,7 +21084,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 369, }, "es-shim-unscopables": { - "id": 860, + "id": 692, "package_id": 381, }, "es-to-primitive": { @@ -21120,7 +21120,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 302, }, "eslint-module-utils": { - "id": 452, + "id": 438, "package_id": 376, }, "eslint-plugin-import": { @@ -21164,7 +21164,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 279, }, "estraverse": { - "id": 432, + "id": 415, "package_id": 166, }, "esutils": { @@ -21248,7 +21248,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 51, }, "function-bind": { - "id": 761, + "id": 55, "package_id": 21, }, "function.prototype.name": { @@ -21412,7 +21412,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 329, }, "is-core-module": { - "id": 454, + "id": 675, "package_id": 19, }, "is-data-view": { @@ -21556,7 +21556,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 174, }, "jsx-ast-utils": { - "id": 814, + "id": 742, "package_id": 408, }, "keyv": { @@ -21680,11 +21680,11 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 355, }, "object.entries": { - "id": 816, + "id": 745, "package_id": 405, }, "object.fromentries": { - "id": 817, + "id": 457, "package_id": 375, }, "object.groupby": { @@ -21696,7 +21696,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 434, }, "object.values": { - "id": 819, + "id": 459, "package_id": 310, }, "once": { @@ -21924,7 +21924,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "package_id": 112, }, "semver": { - "id": 822, + "id": 460, "package_id": 309, }, "set-function-length": { @@ -22271,18 +22271,14 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "dependencies": { - "doctrine": { - "id": 811, - "package_id": 379, - }, - "resolve": { - "id": 821, - "package_id": 431, + "debug": { + "id": 674, + "package_id": 377, }, }, "depth": 1, "id": 4, - "path": "node_modules/eslint-plugin-react/node_modules", + "path": "node_modules/eslint-import-resolver-node/node_modules", }, { "dependencies": { @@ -22301,14 +22297,18 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, { "dependencies": { - "debug": { - "id": 674, - "package_id": 377, + "doctrine": { + "id": 811, + "package_id": 379, + }, + "resolve": { + "id": 821, + "package_id": 431, }, }, "depth": 1, "id": 6, - "path": "node_modules/eslint-import-resolver-node/node_modules", + "path": "node_modules/eslint-plugin-react/node_modules", }, { "dependencies": { @@ -22358,17 +22358,6 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "id": 10, "path": "node_modules/postcss-load-config/node_modules", }, - { - "dependencies": { - "debug": { - "id": 672, - "package_id": 377, - }, - }, - "depth": 1, - "id": 11, - "path": "node_modules/eslint-module-utils/node_modules", - }, { "dependencies": { "minimatch": { @@ -22377,7 +22366,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, }, "depth": 1, - "id": 12, + "id": 11, "path": "node_modules/glob/node_modules", }, { @@ -22392,9 +22381,20 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, }, "depth": 1, - "id": 13, + "id": 12, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules", }, + { + "dependencies": { + "debug": { + "id": 672, + "package_id": 377, + }, + }, + "depth": 1, + "id": 13, + "path": "node_modules/eslint-module-utils/node_modules", + }, { "dependencies": { "lru-cache": { @@ -22439,17 +22439,6 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` "id": 17, "path": "node_modules/path-scurry/node_modules", }, - { - "dependencies": { - "lru-cache": { - "id": 165, - "package_id": 117, - }, - }, - "depth": 2, - "id": 18, - "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", - }, { "dependencies": { "brace-expansion": { @@ -22458,9 +22447,20 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` }, }, "depth": 2, - "id": 19, + "id": 18, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules", }, + { + "dependencies": { + "lru-cache": { + "id": 165, + "package_id": 117, + }, + }, + "depth": 2, + "id": 19, + "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", + }, { "dependencies": { "@types/node": { diff --git a/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap b/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap index c23871f444..fe8eb9c7d8 100644 --- a/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap @@ -5,7 +5,7 @@ exports[`next build works: bun 1`] = ` "dependencies": [ { "behavior": { - "normal": true, + "prod": true, }, "id": 0, "literal": "20.7.0", @@ -18,7 +18,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 1, "literal": "18.2.22", @@ -31,7 +31,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 2, "literal": "18.2.7", @@ -44,7 +44,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 3, "literal": "10.4.16", @@ -57,7 +57,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 4, "literal": "^1.0.3", @@ -70,7 +70,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 5, "literal": "8.50.0", @@ -83,7 +83,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 6, "literal": "14.1.3", @@ -96,7 +96,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 7, "literal": "14.1.3", @@ -109,7 +109,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 8, "literal": "8.4.30", @@ -122,7 +122,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 9, "literal": "22.12.0", @@ -135,7 +135,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 10, "literal": "18.2.0", @@ -148,7 +148,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 11, "literal": "18.2.0", @@ -161,7 +161,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 12, "literal": "3.3.3", @@ -174,7 +174,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 13, "literal": "5.2.2", @@ -187,7 +187,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 14, "literal": "^5.0.2", @@ -200,7 +200,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 15, "literal": "^1.1.3", @@ -213,7 +213,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 16, "literal": "^1.18.2", @@ -226,7 +226,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 17, "literal": "^4.0.3", @@ -239,7 +239,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 18, "literal": "^8.4.23", @@ -252,7 +252,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 19, "literal": "^1.22.2", @@ -265,7 +265,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 20, "literal": "^3.32.0", @@ -278,7 +278,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 21, "literal": "^3.5.3", @@ -291,7 +291,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 22, "literal": "^3.2.12", @@ -304,7 +304,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 23, "literal": "^2.1.0", @@ -317,7 +317,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 24, "literal": "^1.2.2", @@ -330,7 +330,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 25, "literal": "^4.0.5", @@ -343,7 +343,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 26, "literal": "^1.0.0", @@ -356,7 +356,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 27, "literal": "^4.0.1", @@ -369,7 +369,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 28, "literal": "^6.0.2", @@ -382,7 +382,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 29, "literal": "^3.0.0", @@ -395,7 +395,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 30, "literal": "^3.0.0", @@ -408,7 +408,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 31, "literal": "^15.1.0", @@ -421,7 +421,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 32, "literal": "^6.0.1", @@ -434,7 +434,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 33, "literal": "^5.2.0", @@ -447,7 +447,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 34, "literal": "^4.0.1", @@ -460,7 +460,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 35, "literal": "^6.0.11", @@ -473,7 +473,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 36, "literal": "^3.0.0", @@ -486,7 +486,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 37, "literal": "^1.0.2", @@ -499,7 +499,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 38, "literal": "^2.3.4", @@ -512,7 +512,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 39, "literal": "^3.0.0", @@ -553,7 +553,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 42, "literal": "^3.3.6", @@ -566,7 +566,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 43, "literal": "^1.0.0", @@ -579,7 +579,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 44, "literal": "^1.0.2", @@ -592,7 +592,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 45, "literal": "^6.0.11", @@ -618,7 +618,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 47, "literal": "^4.0.0", @@ -631,7 +631,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 48, "literal": "^1.0.0", @@ -644,7 +644,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 49, "literal": "^1.1.7", @@ -670,7 +670,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 51, "literal": "^2.13.0", @@ -683,7 +683,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 52, "literal": "^1.0.7", @@ -696,7 +696,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 53, "literal": "^1.0.0", @@ -709,7 +709,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 54, "literal": "^2.0.0", @@ -722,7 +722,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 55, "literal": "^1.1.2", @@ -735,7 +735,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 56, "literal": "^2.3.0", @@ -748,7 +748,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 57, "literal": "^4.0.3", @@ -761,7 +761,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 58, "literal": "^2.1.1", @@ -774,7 +774,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 59, "literal": "^2.0.1", @@ -800,7 +800,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 61, "literal": "^3.0.3", @@ -813,7 +813,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 62, "literal": "^2.3.1", @@ -826,7 +826,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 63, "literal": "^7.1.1", @@ -839,7 +839,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 64, "literal": "^5.0.1", @@ -852,7 +852,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 65, "literal": "^7.0.0", @@ -865,7 +865,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 66, "literal": "^2.0.2", @@ -878,7 +878,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 67, "literal": "^1.2.3", @@ -891,7 +891,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 68, "literal": "^5.1.2", @@ -904,7 +904,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 69, "literal": "^1.3.0", @@ -917,7 +917,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 70, "literal": "^4.0.4", @@ -930,7 +930,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 71, "literal": "^4.0.1", @@ -943,7 +943,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 72, "literal": "2.1.5", @@ -956,7 +956,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 73, "literal": "^1.6.0", @@ -969,7 +969,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 74, "literal": "^1.0.4", @@ -982,7 +982,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 75, "literal": "2.0.5", @@ -995,7 +995,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 76, "literal": "^1.1.9", @@ -1008,7 +1008,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 77, "literal": "^1.2.2", @@ -1021,7 +1021,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 78, "literal": "~3.1.2", @@ -1034,7 +1034,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 79, "literal": "~3.0.2", @@ -1047,7 +1047,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 80, "literal": "~5.1.2", @@ -1060,7 +1060,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 81, "literal": "~2.1.0", @@ -1073,7 +1073,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 82, "literal": "~4.0.1", @@ -1086,7 +1086,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 83, "literal": "~3.0.0", @@ -1099,7 +1099,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 84, "literal": "~3.6.0", @@ -1125,7 +1125,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 86, "literal": "^2.2.1", @@ -1138,7 +1138,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 87, "literal": "^2.0.0", @@ -1151,7 +1151,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 88, "literal": "^3.0.0", @@ -1164,7 +1164,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 89, "literal": "^2.0.4", @@ -1177,7 +1177,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 90, "literal": "^0.3.2", @@ -1190,7 +1190,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 91, "literal": "^4.0.0", @@ -1203,7 +1203,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 92, "literal": "^10.3.10", @@ -1216,7 +1216,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 93, "literal": "^1.1.6", @@ -1229,7 +1229,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 94, "literal": "^2.7.0", @@ -1242,7 +1242,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 95, "literal": "^4.0.1", @@ -1255,7 +1255,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 96, "literal": "^0.1.9", @@ -1268,7 +1268,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 97, "literal": "^1.0.0", @@ -1281,7 +1281,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 98, "literal": "^4.0.1", @@ -1294,7 +1294,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 99, "literal": "^1.0.0", @@ -1307,7 +1307,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 100, "literal": ">= 3.1.0 < 4", @@ -1320,7 +1320,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 101, "literal": "^1.0.0", @@ -1333,7 +1333,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 102, "literal": "^3.1.0", @@ -1346,7 +1346,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 103, "literal": "^2.3.5", @@ -1359,7 +1359,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 104, "literal": "^9.0.1", @@ -1372,7 +1372,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 105, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -1385,7 +1385,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 106, "literal": "^1.10.1", @@ -1398,7 +1398,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 107, "literal": "^10.2.0", @@ -1411,7 +1411,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 108, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -1424,7 +1424,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 109, "literal": "^2.0.1", @@ -1437,7 +1437,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 110, "literal": "^1.0.0", @@ -1450,7 +1450,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 111, "literal": "^8.0.2", @@ -1476,7 +1476,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 113, "literal": "^5.1.2", @@ -1489,7 +1489,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 114, "is_alias": true, @@ -1503,7 +1503,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 115, "literal": "^7.0.1", @@ -1516,7 +1516,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 116, "is_alias": true, @@ -1530,7 +1530,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 117, "literal": "^8.1.0", @@ -1543,7 +1543,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 118, "is_alias": true, @@ -1557,7 +1557,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 119, "literal": "^4.0.0", @@ -1570,7 +1570,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 120, "literal": "^4.1.0", @@ -1583,7 +1583,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 121, "literal": "^6.0.0", @@ -1596,7 +1596,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 122, "literal": "^5.0.1", @@ -1609,7 +1609,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 123, "literal": "^8.0.0", @@ -1622,7 +1622,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 124, "literal": "^3.0.0", @@ -1635,7 +1635,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 125, "literal": "^6.0.1", @@ -1648,7 +1648,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 126, "literal": "^2.0.1", @@ -1661,7 +1661,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 127, "literal": "~1.1.4", @@ -1674,7 +1674,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 128, "literal": "^6.1.0", @@ -1687,7 +1687,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 129, "literal": "^5.0.1", @@ -1700,7 +1700,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 130, "literal": "^7.0.1", @@ -1713,7 +1713,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 131, "literal": "^6.0.1", @@ -1726,7 +1726,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 132, "literal": "^0.2.0", @@ -1739,7 +1739,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 133, "literal": "^9.2.2", @@ -1752,7 +1752,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 134, "literal": "^7.0.1", @@ -1765,7 +1765,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 135, "literal": "^7.0.0", @@ -1778,7 +1778,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 136, "literal": "^4.0.1", @@ -1791,7 +1791,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 137, "literal": "^3.1.0", @@ -1804,7 +1804,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 138, "literal": "^2.0.0", @@ -1817,7 +1817,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 139, "literal": "^2.0.1", @@ -1830,7 +1830,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 140, "literal": "^2.0.0", @@ -1843,7 +1843,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 141, "literal": "^3.0.0", @@ -1856,7 +1856,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 142, "literal": "^1.2.1", @@ -1869,7 +1869,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 143, "literal": "^1.4.10", @@ -1882,7 +1882,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 144, "literal": "^0.3.24", @@ -1895,7 +1895,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 145, "literal": "^3.1.0", @@ -1908,7 +1908,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 146, "literal": "^1.4.14", @@ -1921,7 +1921,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 147, "literal": "^0.23.0", @@ -1934,7 +1934,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 148, "literal": "^1.1.0", @@ -1960,7 +1960,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 150, "literal": "^1.1.0", @@ -1973,7 +1973,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 151, "literal": "^3.0.0 || ^4.0.0", @@ -1986,7 +1986,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 152, "literal": "^1.1.0", @@ -1999,7 +1999,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 153, "literal": "9.0.0", @@ -2012,7 +2012,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 154, "literal": "22.12.0", @@ -2025,7 +2025,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 155, "literal": "2.2.3", @@ -2038,7 +2038,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 156, "literal": "0.0.1299070", @@ -2051,7 +2051,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 157, "literal": "4.3.4", @@ -2064,7 +2064,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 158, "literal": "2.0.1", @@ -2077,7 +2077,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 159, "literal": "2.0.3", @@ -2090,7 +2090,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 160, "literal": "6.4.0", @@ -2103,7 +2103,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 161, "literal": "3.0.5", @@ -2116,7 +2116,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 162, "literal": "1.4.3", @@ -2129,7 +2129,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 163, "literal": "17.7.2", @@ -2142,7 +2142,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 164, "literal": "7.6.0", @@ -2155,7 +2155,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 165, "literal": "^6.0.0", @@ -2168,7 +2168,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 166, "literal": "^4.0.0", @@ -2181,7 +2181,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 167, "literal": "^8.0.1", @@ -2194,7 +2194,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 168, "literal": "^3.1.1", @@ -2207,7 +2207,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 169, "literal": "^2.0.5", @@ -2220,7 +2220,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 170, "literal": "^2.1.1", @@ -2233,7 +2233,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 171, "literal": "^4.2.3", @@ -2246,7 +2246,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 172, "literal": "^5.0.5", @@ -2259,7 +2259,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 173, "literal": "^21.1.1", @@ -2272,7 +2272,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 174, "literal": "^4.2.0", @@ -2285,7 +2285,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 175, "literal": "^6.0.1", @@ -2298,7 +2298,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 176, "literal": "^7.0.0", @@ -2311,7 +2311,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 177, "literal": "^5.2.1", @@ -2324,7 +2324,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 178, "literal": "^2.3.8", @@ -2337,7 +2337,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 179, "literal": "^1.3.1", @@ -2350,7 +2350,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 180, "literal": "^1.1.13", @@ -2363,7 +2363,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 181, "literal": "^3.0.0", @@ -2376,7 +2376,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 182, "literal": "^3.1.5", @@ -2415,7 +2415,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 185, "literal": "^2.1.0", @@ -2428,7 +2428,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 186, "literal": "^2.0.0", @@ -2441,7 +2441,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 187, "literal": "^2.0.0", @@ -2454,7 +2454,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 188, "literal": "^2.0.0", @@ -2467,7 +2467,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 189, "literal": "^2.18.0", @@ -2480,7 +2480,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 190, "literal": "^1.3.2", @@ -2493,7 +2493,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 191, "literal": "^1.0.1", @@ -2506,7 +2506,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 192, "literal": "^1.1.0", @@ -2532,7 +2532,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 194, "literal": "^1.6.4", @@ -2545,7 +2545,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 195, "literal": "^1.6.4", @@ -2558,7 +2558,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 196, "literal": "^1.2.0", @@ -2571,7 +2571,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 197, "literal": "^2.15.0", @@ -2584,7 +2584,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 198, "literal": "^1.1.0", @@ -2597,7 +2597,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 199, "literal": "^1.3.1", @@ -2610,7 +2610,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 200, "literal": "1", @@ -2623,7 +2623,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 201, "literal": "^1.4.0", @@ -2636,7 +2636,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 202, "literal": "^7.0.2", @@ -2649,7 +2649,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 203, "literal": "^4.3.4", @@ -2662,7 +2662,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 204, "literal": "^7.0.1", @@ -2675,7 +2675,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 205, "literal": "^7.0.3", @@ -2688,7 +2688,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 206, "literal": "^7.14.1", @@ -2701,7 +2701,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 207, "literal": "^7.0.1", @@ -2714,7 +2714,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 208, "literal": "^1.1.0", @@ -2727,7 +2727,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 209, "literal": "^8.0.2", @@ -2740,7 +2740,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 210, "literal": "^7.1.1", @@ -2753,7 +2753,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 211, "literal": "^4.3.4", @@ -2766,7 +2766,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 212, "literal": "^2.7.1", @@ -2779,7 +2779,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 213, "literal": "^9.0.5", @@ -2792,7 +2792,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 214, "literal": "^4.2.0", @@ -2805,7 +2805,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 215, "literal": "1.1.0", @@ -2818,7 +2818,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 216, "literal": "^1.1.3", @@ -2831,7 +2831,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 217, "literal": "2.1.2", @@ -2844,7 +2844,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 218, "literal": "^4.3.4", @@ -2857,7 +2857,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 219, "literal": "^0.23.0", @@ -2870,7 +2870,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 220, "literal": "^7.0.2", @@ -2883,7 +2883,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 221, "literal": "^4.3.4", @@ -2896,7 +2896,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 222, "literal": "^6.0.1", @@ -2909,7 +2909,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 223, "literal": "^7.0.0", @@ -2922,7 +2922,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 224, "literal": "^7.0.2", @@ -2935,7 +2935,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 225, "literal": "^7.0.0", @@ -2948,7 +2948,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 226, "literal": "^8.0.2", @@ -2961,7 +2961,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 227, "literal": "^5.0.0", @@ -2974,7 +2974,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 228, "literal": "^2.0.2", @@ -2987,7 +2987,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 229, "literal": "^0.13.4", @@ -3000,7 +3000,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 230, "literal": "^2.1.0", @@ -3013,7 +3013,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 231, "literal": "^4.0.1", @@ -3026,7 +3026,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 232, "literal": "^5.2.0", @@ -3039,7 +3039,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 233, "literal": "^2.0.2", @@ -3052,7 +3052,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 234, "literal": "^4.0.1", @@ -3078,7 +3078,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 236, "literal": "^2.0.1", @@ -3091,7 +3091,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 237, "literal": "^7.0.2", @@ -3104,7 +3104,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 238, "literal": "4", @@ -3117,7 +3117,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 239, "literal": "^7.1.0", @@ -3130,7 +3130,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 240, "literal": "^4.3.4", @@ -3143,7 +3143,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 241, "literal": "^5.0.2", @@ -3156,7 +3156,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 242, "literal": "^6.0.2", @@ -3169,7 +3169,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 243, "literal": "^4.3.4", @@ -3182,7 +3182,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 244, "literal": "^11.2.0", @@ -3195,7 +3195,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 245, "literal": "^4.2.0", @@ -3208,7 +3208,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 246, "literal": "^6.0.1", @@ -3221,7 +3221,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 247, "literal": "^2.0.0", @@ -3234,7 +3234,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 248, "literal": "^2.0.0", @@ -3260,7 +3260,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 250, "literal": "^4.1.1", @@ -3273,7 +3273,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 251, "literal": "^5.1.0", @@ -3286,7 +3286,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 252, "literal": "^2.10.0", @@ -3312,7 +3312,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 254, "literal": "*", @@ -3325,7 +3325,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 255, "literal": "~5.26.4", @@ -3338,7 +3338,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 256, "literal": "~1.1.0", @@ -3351,7 +3351,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 257, "literal": "~0.2.3", @@ -3364,7 +3364,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 258, "literal": "~1.2.0", @@ -3377,7 +3377,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 259, "literal": "^3.0.0", @@ -3390,7 +3390,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 260, "literal": "2.1.2", @@ -3403,7 +3403,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 261, "literal": "2.2.3", @@ -3416,7 +3416,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 262, "literal": "0.5.24", @@ -3429,7 +3429,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 263, "literal": "4.3.5", @@ -3442,7 +3442,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 264, "literal": "0.0.1299070", @@ -3455,7 +3455,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 265, "literal": "8.17.1", @@ -3496,7 +3496,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 268, "literal": "3.0.1", @@ -3509,7 +3509,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 269, "literal": "10.0.0", @@ -3522,7 +3522,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 270, "literal": "3.23.8", @@ -3548,7 +3548,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 272, "literal": "^2.2.1", @@ -3561,7 +3561,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 273, "literal": "^3.3.0", @@ -3574,7 +3574,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 274, "literal": "^4.1.0", @@ -3587,7 +3587,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 275, "literal": "^5.2.0", @@ -3614,7 +3614,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 277, "literal": "^7.0.0", @@ -3627,7 +3627,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 278, "literal": "^1.3.1", @@ -3640,7 +3640,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 279, "literal": "^2.3.0", @@ -3653,7 +3653,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 280, "literal": "^1.1.6", @@ -3666,7 +3666,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 281, "literal": "^0.2.1", @@ -3679,7 +3679,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 282, "literal": "^7.24.7", @@ -3692,7 +3692,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 283, "literal": "^1.0.0", @@ -3705,7 +3705,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 284, "literal": "^7.24.7", @@ -3718,7 +3718,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 285, "literal": "^2.4.2", @@ -3731,7 +3731,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 286, "literal": "^4.0.0", @@ -3744,7 +3744,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 287, "literal": "^1.0.0", @@ -3757,7 +3757,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 288, "literal": "^3.2.1", @@ -3770,7 +3770,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 289, "literal": "^1.0.5", @@ -3783,7 +3783,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 290, "literal": "^5.3.0", @@ -3796,7 +3796,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 291, "literal": "^3.0.0", @@ -3809,7 +3809,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 292, "literal": "^1.9.0", @@ -3822,7 +3822,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 293, "literal": "1.1.3", @@ -3835,7 +3835,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 294, "literal": "^2.0.1", @@ -3848,7 +3848,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 295, "literal": "^1.0.0", @@ -3861,7 +3861,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 296, "literal": "^4.0.0", @@ -3874,7 +3874,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 297, "literal": "^3.0.0", @@ -3887,7 +3887,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 298, "literal": "1.6.0", @@ -3900,7 +3900,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 299, "literal": "8.4.31", @@ -3913,7 +3913,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 300, "literal": "14.1.3", @@ -3926,7 +3926,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 301, "literal": "5.1.1", @@ -3939,7 +3939,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 302, "literal": "^4.2.11", @@ -3952,7 +3952,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 303, "literal": "0.5.2", @@ -3965,7 +3965,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 304, "literal": "^1.0.30001579", @@ -4149,7 +4149,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 318, "literal": "^2.4.0", @@ -4162,7 +4162,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 319, "literal": "0.0.1", @@ -4188,7 +4188,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 321, "literal": "^3.3.6", @@ -4201,7 +4201,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 322, "literal": "^1.0.0", @@ -4214,7 +4214,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 323, "literal": "^1.0.2", @@ -4227,7 +4227,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 324, "literal": "^1.1.0", @@ -4240,7 +4240,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 325, "literal": "^7.33.2", @@ -4253,7 +4253,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 326, "literal": "^2.28.1", @@ -4266,7 +4266,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 327, "literal": "^6.7.1", @@ -4279,7 +4279,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 328, "literal": "^1.3.3", @@ -4292,7 +4292,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 329, "literal": "14.1.3", @@ -4305,7 +4305,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 330, "literal": "^5.4.2 || ^6.0.0", @@ -4318,7 +4318,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 331, "literal": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705", @@ -4331,7 +4331,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 332, "literal": "^0.3.6", @@ -4344,7 +4344,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 333, "literal": "^3.5.2", @@ -4384,7 +4384,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 336, "literal": "^6.12.4", @@ -4397,7 +4397,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 337, "literal": "^0.4.1", @@ -4410,7 +4410,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 338, "literal": "^4.0.0", @@ -4423,7 +4423,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 339, "literal": "^4.3.2", @@ -4436,7 +4436,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 340, "literal": "^9.6.1", @@ -4449,7 +4449,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 341, "literal": "^5.2.0", @@ -4462,7 +4462,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 342, "literal": "^1.4.2", @@ -4475,7 +4475,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 343, "literal": "^2.0.2", @@ -4488,7 +4488,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 344, "literal": "^5.0.0", @@ -4501,7 +4501,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 345, "literal": "^13.19.0", @@ -4514,7 +4514,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 346, "literal": "^4.0.0", @@ -4527,7 +4527,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 347, "literal": "^4.1.0", @@ -4540,7 +4540,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 348, "literal": "^3.0.0", @@ -4553,7 +4553,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 349, "literal": "^1.4.0", @@ -4566,7 +4566,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 350, "literal": "^3.1.2", @@ -4579,7 +4579,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 351, "literal": "8.50.0", @@ -4592,7 +4592,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 352, "literal": "^0.9.3", @@ -4605,7 +4605,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 353, "literal": "^6.0.1", @@ -4618,7 +4618,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 354, "literal": "^0.2.0", @@ -4631,7 +4631,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 355, "literal": "^7.0.2", @@ -4644,7 +4644,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 356, "literal": "^6.0.2", @@ -4657,7 +4657,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 357, "literal": "^0.1.4", @@ -4670,7 +4670,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 358, "literal": "^7.2.2", @@ -4683,7 +4683,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 359, "literal": "^4.6.2", @@ -4696,7 +4696,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 360, "literal": "^3.0.3", @@ -4709,7 +4709,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 361, "literal": "^3.1.3", @@ -4722,7 +4722,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 362, "literal": "^1.4.0", @@ -4735,7 +4735,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 363, "literal": "^2.1.2", @@ -4748,7 +4748,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 364, "literal": "^1.2.8", @@ -4761,7 +4761,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 365, "literal": "^6.0.1", @@ -4774,7 +4774,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 366, "literal": "^3.4.3", @@ -4787,7 +4787,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 367, "literal": "^4.0.0", @@ -4800,7 +4800,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 368, "literal": "^4.6.1", @@ -4813,7 +4813,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 369, "literal": "^0.11.11", @@ -4826,7 +4826,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 370, "literal": "^4.2.0", @@ -4839,7 +4839,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 371, "literal": "^1.0.1", @@ -4852,7 +4852,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 372, "literal": "^1.0.1", @@ -4865,7 +4865,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 373, "literal": "^3.3.0", @@ -4891,7 +4891,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 375, "literal": "^2.0.2", @@ -4904,7 +4904,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 376, "literal": "^4.3.1", @@ -4917,7 +4917,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 377, "literal": "^3.0.5", @@ -4930,7 +4930,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 378, "literal": "^1.1.7", @@ -4943,7 +4943,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 379, "literal": "^1.0.0", @@ -4956,7 +4956,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 380, "literal": "0.0.1", @@ -4969,7 +4969,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 381, "literal": "^3.0.4", @@ -4982,7 +4982,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 382, "literal": "^3.2.9", @@ -4995,7 +4995,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 383, "literal": "^4.5.3", @@ -5008,7 +5008,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 384, "literal": "^3.0.2", @@ -5021,7 +5021,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 385, "literal": "^7.1.3", @@ -5034,7 +5034,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 386, "literal": "^1.0.0", @@ -5047,7 +5047,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 387, "literal": "^1.0.4", @@ -5060,7 +5060,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 388, "literal": "2", @@ -5073,7 +5073,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 389, "literal": "^3.1.1", @@ -5086,7 +5086,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 390, "literal": "^1.3.0", @@ -5099,7 +5099,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 391, "literal": "^1.0.0", @@ -5112,7 +5112,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 392, "literal": "^1.3.0", @@ -5125,7 +5125,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 393, "literal": "1", @@ -5138,7 +5138,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 394, "literal": "3.0.1", @@ -5151,7 +5151,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 395, "literal": "^6.12.4", @@ -5164,7 +5164,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 396, "literal": "^4.3.2", @@ -5177,7 +5177,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 397, "literal": "^9.6.0", @@ -5190,7 +5190,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 398, "literal": "^13.19.0", @@ -5203,7 +5203,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 399, "literal": "^5.2.0", @@ -5216,7 +5216,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 400, "literal": "^3.2.1", @@ -5229,7 +5229,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 401, "literal": "^4.1.0", @@ -5242,7 +5242,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 402, "literal": "^3.1.2", @@ -5255,7 +5255,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 403, "literal": "^3.1.1", @@ -5268,7 +5268,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 404, "literal": "^0.20.2", @@ -5281,7 +5281,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 405, "literal": "^8.9.0", @@ -5294,7 +5294,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 406, "literal": "^5.3.2", @@ -5307,7 +5307,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 407, "literal": "^3.4.1", @@ -5333,7 +5333,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 409, "literal": "^3.1.1", @@ -5346,7 +5346,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 410, "literal": "^2.0.0", @@ -5359,7 +5359,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 411, "literal": "^0.4.1", @@ -5372,7 +5372,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 412, "literal": "^4.2.2", @@ -5385,7 +5385,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 413, "literal": "^2.1.0", @@ -5398,7 +5398,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 414, "literal": "^4.3.0", @@ -5411,7 +5411,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 415, "literal": "^5.2.0", @@ -5424,7 +5424,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 416, "literal": "^5.2.0", @@ -5437,7 +5437,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 417, "literal": "^1.2.1", @@ -5450,7 +5450,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 418, "literal": "^0.1.3", @@ -5463,7 +5463,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 419, "literal": "^1.2.5", @@ -5476,7 +5476,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 420, "literal": "^0.4.0", @@ -5489,7 +5489,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 421, "literal": "^0.4.1", @@ -5502,7 +5502,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 422, "literal": "^2.0.6", @@ -5515,7 +5515,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 423, "literal": "^1.2.1", @@ -5528,7 +5528,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 424, "literal": "~0.4.0", @@ -5541,7 +5541,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 425, "literal": "^1.2.1", @@ -5554,7 +5554,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 426, "literal": "^2.0.2", @@ -5567,7 +5567,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 427, "literal": "^6.0.0", @@ -5580,7 +5580,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 428, "literal": "^4.0.0", @@ -5593,7 +5593,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 429, "literal": "^5.0.0", @@ -5606,7 +5606,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 430, "literal": "^3.0.2", @@ -5619,7 +5619,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 431, "literal": "^0.1.0", @@ -5632,7 +5632,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 432, "literal": "^5.1.0", @@ -5645,7 +5645,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 433, "literal": "^4.1.0", @@ -5658,7 +5658,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 434, "literal": "^7.1.0", @@ -5671,7 +5671,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 435, "literal": "^4.0.0", @@ -5684,7 +5684,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 436, "literal": "^4.3.4", @@ -5697,7 +5697,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 437, "literal": "^5.12.0", @@ -5710,7 +5710,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 438, "literal": "^2.7.4", @@ -5723,7 +5723,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 439, "literal": "^3.3.1", @@ -5736,7 +5736,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 440, "literal": "^4.5.0", @@ -5749,7 +5749,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 441, "literal": "^2.11.0", @@ -5762,7 +5762,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 442, "literal": "^4.0.3", @@ -5801,7 +5801,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 445, "literal": "^3.1.7", @@ -5814,7 +5814,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 446, "literal": "^1.2.3", @@ -5827,7 +5827,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 447, "literal": "^1.3.2", @@ -5840,7 +5840,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 448, "literal": "^1.3.2", @@ -5853,7 +5853,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 449, "literal": "^3.2.7", @@ -5866,7 +5866,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 450, "literal": "^2.1.0", @@ -5879,7 +5879,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 451, "literal": "^0.3.9", @@ -5892,7 +5892,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 452, "literal": "^2.8.0", @@ -5905,7 +5905,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 453, "literal": "^2.0.0", @@ -5918,7 +5918,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 454, "literal": "^2.13.1", @@ -5931,7 +5931,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 455, "literal": "^4.0.3", @@ -5944,7 +5944,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 456, "literal": "^3.1.2", @@ -5957,7 +5957,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 457, "literal": "^2.0.7", @@ -5970,7 +5970,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 458, "literal": "^1.0.1", @@ -5983,7 +5983,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 459, "literal": "^1.1.7", @@ -5996,7 +5996,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 460, "literal": "^6.3.1", @@ -6009,7 +6009,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 461, "literal": "^3.15.0", @@ -6035,7 +6035,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 463, "literal": "^0.0.29", @@ -6048,7 +6048,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 464, "literal": "^1.0.2", @@ -6061,7 +6061,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 465, "literal": "^1.2.6", @@ -6074,7 +6074,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 466, "literal": "^3.0.0", @@ -6087,7 +6087,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 467, "literal": "^1.2.0", @@ -6100,7 +6100,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 468, "literal": "^1.0.7", @@ -6113,7 +6113,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 469, "literal": "^1.2.1", @@ -6126,7 +6126,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 470, "literal": "^1.0.0", @@ -6139,7 +6139,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 471, "literal": "^1.3.0", @@ -6152,7 +6152,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 472, "literal": "^1.0.1", @@ -6165,7 +6165,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 473, "literal": "^1.0.0", @@ -6178,7 +6178,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 474, "literal": "^1.1.1", @@ -6191,7 +6191,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 475, "literal": "^1.0.0", @@ -6204,7 +6204,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 476, "literal": "^1.2.4", @@ -6217,7 +6217,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 477, "literal": "^1.3.0", @@ -6230,7 +6230,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 478, "literal": "^1.1.2", @@ -6243,7 +6243,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 479, "literal": "^1.0.1", @@ -6256,7 +6256,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 480, "literal": "^1.0.3", @@ -6269,7 +6269,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 481, "literal": "^2.0.0", @@ -6282,7 +6282,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 482, "literal": "^1.0.0", @@ -6295,7 +6295,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 483, "literal": "^1.3.0", @@ -6308,7 +6308,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 484, "literal": "^1.0.1", @@ -6321,7 +6321,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 485, "literal": "^1.1.3", @@ -6334,7 +6334,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 486, "literal": "^1.0.0", @@ -6347,7 +6347,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 487, "literal": "^1.3.0", @@ -6360,7 +6360,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 488, "literal": "^1.1.2", @@ -6373,7 +6373,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 489, "literal": "^1.2.4", @@ -6386,7 +6386,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 490, "literal": "^1.2.1", @@ -6399,7 +6399,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 491, "literal": "^1.1.4", @@ -6412,7 +6412,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 492, "literal": "^1.3.0", @@ -6425,7 +6425,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 493, "literal": "^1.1.2", @@ -6438,7 +6438,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 494, "literal": "^1.2.4", @@ -6451,7 +6451,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 495, "literal": "^1.0.1", @@ -6464,7 +6464,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 496, "literal": "^1.0.2", @@ -6477,7 +6477,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 497, "literal": "^1.0.7", @@ -6490,7 +6490,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 498, "literal": "^1.2.1", @@ -6503,7 +6503,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 499, "literal": "^1.23.2", @@ -6516,7 +6516,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 500, "literal": "^1.0.1", @@ -6529,7 +6529,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 501, "literal": "^1.0.3", @@ -6542,7 +6542,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 502, "literal": "^1.0.7", @@ -6555,7 +6555,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 503, "literal": "^1.0.7", @@ -6568,7 +6568,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 504, "literal": "^1.0.1", @@ -6581,7 +6581,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 505, "literal": "^1.0.1", @@ -6594,7 +6594,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 506, "literal": "^1.0.0", @@ -6607,7 +6607,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 507, "literal": "^1.0.0", @@ -6620,7 +6620,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 508, "literal": "^1.3.0", @@ -6633,7 +6633,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 509, "literal": "^1.0.0", @@ -6646,7 +6646,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 510, "literal": "^2.0.3", @@ -6659,7 +6659,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 511, "literal": "^1.2.1", @@ -6672,7 +6672,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 512, "literal": "^1.1.6", @@ -6685,7 +6685,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 513, "literal": "^1.2.4", @@ -6698,7 +6698,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 514, "literal": "^1.0.2", @@ -6711,7 +6711,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 515, "literal": "^1.0.3", @@ -6724,7 +6724,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 516, "literal": "^1.0.1", @@ -6737,7 +6737,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 517, "literal": "^1.0.2", @@ -6750,7 +6750,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 518, "literal": "^1.0.3", @@ -6763,7 +6763,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 519, "literal": "^1.0.3", @@ -6776,7 +6776,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 520, "literal": "^2.0.2", @@ -6789,7 +6789,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 521, "literal": "^1.0.7", @@ -6802,7 +6802,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 522, "literal": "^3.0.4", @@ -6815,7 +6815,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 523, "literal": "^1.2.7", @@ -6828,7 +6828,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 524, "literal": "^1.0.1", @@ -6841,7 +6841,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 525, "literal": "^2.0.3", @@ -6854,7 +6854,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 526, "literal": "^1.1.4", @@ -6867,7 +6867,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 527, "literal": "^1.0.3", @@ -6880,7 +6880,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 528, "literal": "^1.0.7", @@ -6893,7 +6893,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 529, "literal": "^1.1.13", @@ -6906,7 +6906,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 530, "literal": "^1.0.2", @@ -6919,7 +6919,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 531, "literal": "^1.13.1", @@ -6932,7 +6932,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 532, "literal": "^1.1.1", @@ -6945,7 +6945,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 533, "literal": "^4.1.5", @@ -6958,7 +6958,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 534, "literal": "^1.5.2", @@ -6971,7 +6971,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 535, "literal": "^1.1.2", @@ -6984,7 +6984,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 536, "literal": "^1.0.3", @@ -6997,7 +6997,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 537, "literal": "^1.2.9", @@ -7010,7 +7010,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 538, "literal": "^1.0.8", @@ -7023,7 +7023,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 539, "literal": "^1.0.8", @@ -7036,7 +7036,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 540, "literal": "^1.0.2", @@ -7049,7 +7049,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 541, "literal": "^1.0.1", @@ -7062,7 +7062,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 542, "literal": "^1.0.2", @@ -7075,7 +7075,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 543, "literal": "^1.0.6", @@ -7088,7 +7088,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 544, "literal": "^1.0.2", @@ -7101,7 +7101,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 545, "literal": "^1.1.15", @@ -7114,7 +7114,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 546, "literal": "^1.0.7", @@ -7127,7 +7127,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 547, "literal": "^1.0.7", @@ -7140,7 +7140,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 548, "literal": "^0.3.3", @@ -7153,7 +7153,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 549, "literal": "^1.0.1", @@ -7166,7 +7166,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 550, "literal": "^1.0.2", @@ -7179,7 +7179,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 551, "literal": "^1.0.3", @@ -7192,7 +7192,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 552, "literal": "^1.1.3", @@ -7205,7 +7205,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 553, "literal": "^1.0.0", @@ -7218,7 +7218,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 554, "literal": "^1.0.2", @@ -7231,7 +7231,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 555, "literal": "^1.0.2", @@ -7244,7 +7244,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 556, "literal": "^1.0.3", @@ -7257,7 +7257,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 557, "literal": "^1.0.2", @@ -7270,7 +7270,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 558, "literal": "^1.0.1", @@ -7283,7 +7283,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 559, "literal": "^1.1.0", @@ -7296,7 +7296,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 560, "literal": "^1.0.4", @@ -7309,7 +7309,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 561, "literal": "^1.0.5", @@ -7322,7 +7322,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 562, "literal": "^1.0.3", @@ -7335,7 +7335,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 563, "literal": "^1.0.2", @@ -7348,7 +7348,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 564, "literal": "^1.0.0", @@ -7361,7 +7361,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 565, "literal": "^1.0.0", @@ -7374,7 +7374,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 566, "literal": "^1.0.2", @@ -7387,7 +7387,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 567, "literal": "^1.0.0", @@ -7400,7 +7400,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 568, "literal": "^1.0.1", @@ -7413,7 +7413,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 569, "literal": "^1.0.7", @@ -7426,7 +7426,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 570, "literal": "^0.3.3", @@ -7439,7 +7439,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 571, "literal": "^1.0.1", @@ -7452,7 +7452,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 572, "literal": "^1.0.3", @@ -7465,7 +7465,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 573, "literal": "^1.1.13", @@ -7478,7 +7478,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 574, "literal": "^1.0.0", @@ -7491,7 +7491,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 575, "literal": "^1.1.14", @@ -7504,7 +7504,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 576, "literal": "^1.0.7", @@ -7517,7 +7517,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 577, "literal": "^1.0.7", @@ -7530,7 +7530,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 578, "literal": "^0.3.3", @@ -7543,7 +7543,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 579, "literal": "^1.0.1", @@ -7556,7 +7556,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 580, "literal": "^1.0.3", @@ -7569,7 +7569,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 581, "literal": "^1.1.13", @@ -7582,7 +7582,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 582, "literal": "^1.0.7", @@ -7595,7 +7595,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 583, "literal": "^0.3.3", @@ -7608,7 +7608,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 584, "literal": "^1.0.1", @@ -7621,7 +7621,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 585, "literal": "^1.0.3", @@ -7634,7 +7634,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 586, "literal": "^1.1.13", @@ -7647,7 +7647,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 587, "literal": "^1.0.7", @@ -7660,7 +7660,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 588, "literal": "^1.3.0", @@ -7673,7 +7673,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 589, "literal": "^1.1.13", @@ -7686,7 +7686,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 590, "literal": "^1.0.7", @@ -7699,7 +7699,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 591, "literal": "^1.2.1", @@ -7712,7 +7712,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 592, "literal": "^1.0.0", @@ -7725,7 +7725,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 593, "literal": "^1.0.7", @@ -7738,7 +7738,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 594, "literal": "^1.2.1", @@ -7751,7 +7751,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 595, "literal": "^1.0.0", @@ -7764,7 +7764,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 596, "literal": "^1.0.7", @@ -7777,7 +7777,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 597, "literal": "^1.2.1", @@ -7790,7 +7790,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 598, "literal": "^1.23.0", @@ -7803,7 +7803,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 599, "literal": "^1.0.0", @@ -7816,7 +7816,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 600, "literal": "^1.0.6", @@ -7829,7 +7829,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 601, "literal": "^1.3.0", @@ -7842,7 +7842,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 602, "literal": "^1.1.4", @@ -7855,7 +7855,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 603, "literal": "^1.0.2", @@ -7868,7 +7868,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 604, "literal": "^1.0.0", @@ -7881,7 +7881,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 605, "literal": "^1.0.7", @@ -7894,7 +7894,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 606, "literal": "^1.2.4", @@ -7907,7 +7907,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 607, "literal": "^1.0.3", @@ -7920,7 +7920,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 608, "literal": "^2.0.5", @@ -7933,7 +7933,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 609, "literal": "^1.0.6", @@ -7946,7 +7946,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 610, "literal": "^1.2.1", @@ -7959,7 +7959,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 611, "literal": "^1.3.0", @@ -7972,7 +7972,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 612, "literal": "^2.0.1", @@ -7985,7 +7985,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 613, "literal": "^1.1.4", @@ -7998,7 +7998,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 614, "literal": "^1.3.0", @@ -8011,7 +8011,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 615, "literal": "^1.2.3", @@ -8024,7 +8024,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 616, "literal": "^1.0.2", @@ -8037,7 +8037,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 617, "literal": "^1.0.5", @@ -8050,7 +8050,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 618, "literal": "^1.2.1", @@ -8063,7 +8063,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 619, "literal": "^1.0.3", @@ -8076,7 +8076,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 620, "literal": "^1.1.1", @@ -8089,7 +8089,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 621, "literal": "^1.0.2", @@ -8102,7 +8102,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 622, "literal": "^1.0.7", @@ -8115,7 +8115,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 623, "literal": "^1.1.13", @@ -8128,7 +8128,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 624, "literal": "^1.0.2", @@ -8141,7 +8141,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 625, "literal": "^1.2.1", @@ -8154,7 +8154,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 626, "literal": "^1.3.0", @@ -8167,7 +8167,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 627, "literal": "^2.0.0", @@ -8180,7 +8180,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 628, "literal": "^1.0.4", @@ -8193,7 +8193,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 629, "literal": "^1.0.7", @@ -8206,7 +8206,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 630, "literal": "^1.3.0", @@ -8219,7 +8219,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 631, "literal": "^1.2.4", @@ -8232,7 +8232,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 632, "literal": "^1.13.1", @@ -8245,7 +8245,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 633, "literal": "^1.2.1", @@ -8258,7 +8258,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 634, "literal": "^1.0.1", @@ -8271,7 +8271,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 635, "literal": "^1.0.5", @@ -8284,7 +8284,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 636, "literal": "^1.3.0", @@ -8297,7 +8297,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 637, "literal": "^1.2.4", @@ -8310,7 +8310,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 638, "literal": "^1.0.2", @@ -8323,7 +8323,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 639, "literal": "^1.2.0", @@ -8336,7 +8336,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 640, "literal": "^1.22.1", @@ -8349,7 +8349,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 641, "literal": "^1.2.3", @@ -8362,7 +8362,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 642, "literal": "^1.1.4", @@ -8375,7 +8375,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 643, "literal": "^1.0.1", @@ -8388,7 +8388,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 644, "literal": "^1.0.2", @@ -8401,7 +8401,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 645, "literal": "^1.0.0", @@ -8414,7 +8414,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 646, "literal": "^1.2.4", @@ -8427,7 +8427,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 647, "literal": "^1.0.2", @@ -8440,7 +8440,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 648, "literal": "^2.0.1", @@ -8453,7 +8453,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 649, "literal": "^1.0.6", @@ -8466,7 +8466,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 650, "literal": "^1.3.0", @@ -8479,7 +8479,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 651, "literal": "^1.0.1", @@ -8492,7 +8492,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 652, "literal": "^1.0.7", @@ -8505,7 +8505,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 653, "literal": "^1.3.0", @@ -8518,7 +8518,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 654, "literal": "^1.0.1", @@ -8531,7 +8531,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 655, "literal": "^1.0.6", @@ -8544,7 +8544,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 656, "literal": "^1.3.0", @@ -8557,7 +8557,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 657, "literal": "^1.0.1", @@ -8570,7 +8570,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 658, "literal": "^1.0.1", @@ -8583,7 +8583,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 659, "literal": "^1.0.5", @@ -8596,7 +8596,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 660, "literal": "^1.2.1", @@ -8609,7 +8609,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 661, "literal": "^1.22.3", @@ -8622,7 +8622,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 662, "literal": "^1.2.1", @@ -8635,7 +8635,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 663, "literal": "^1.2.3", @@ -8648,7 +8648,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 664, "literal": "^3.0.4", @@ -8661,7 +8661,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 665, "literal": "^1.0.2", @@ -8674,7 +8674,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 666, "literal": "^1.0.5", @@ -8687,7 +8687,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 667, "literal": "^3.0.4", @@ -8700,7 +8700,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 668, "literal": "^1.0.7", @@ -8713,7 +8713,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 669, "literal": "^1.2.1", @@ -8726,7 +8726,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 670, "literal": "^1.23.2", @@ -8739,7 +8739,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 671, "literal": "^1.0.0", @@ -8752,7 +8752,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 672, "literal": "^3.2.7", @@ -8765,7 +8765,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 673, "literal": "^2.1.1", @@ -8778,7 +8778,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 674, "literal": "^3.2.7", @@ -8791,7 +8791,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 675, "literal": "^2.13.0", @@ -8804,7 +8804,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 676, "literal": "^1.22.4", @@ -8817,7 +8817,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 677, "literal": "^2.0.2", @@ -8830,7 +8830,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 678, "literal": "^1.0.2", @@ -8843,7 +8843,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 679, "literal": "^1.2.0", @@ -8856,7 +8856,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 680, "literal": "^1.22.1", @@ -8869,7 +8869,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 681, "literal": "^1.0.0", @@ -8882,7 +8882,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 682, "literal": "^2.0.0", @@ -8895,7 +8895,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 683, "literal": "^1.0.2", @@ -8908,7 +8908,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 684, "literal": "^1.2.0", @@ -8921,7 +8921,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 685, "literal": "^1.22.1", @@ -8934,7 +8934,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 686, "literal": "^1.0.0", @@ -8947,7 +8947,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 687, "literal": "^1.0.7", @@ -8960,7 +8960,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 688, "literal": "^1.2.1", @@ -8973,7 +8973,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 689, "literal": "^1.23.2", @@ -8986,7 +8986,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 690, "literal": "^1.3.0", @@ -8999,7 +8999,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 691, "literal": "^1.0.0", @@ -9012,7 +9012,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 692, "literal": "^1.0.2", @@ -9025,7 +9025,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 693, "literal": "^1.0.7", @@ -9038,7 +9038,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 694, "literal": "^1.2.1", @@ -9051,7 +9051,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 695, "literal": "^1.23.2", @@ -9064,7 +9064,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 696, "literal": "^1.0.0", @@ -9077,7 +9077,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 697, "literal": "^1.2.4", @@ -9090,7 +9090,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 698, "literal": "^1.0.7", @@ -9103,7 +9103,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 699, "literal": "^1.0.0", @@ -9116,7 +9116,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 700, "literal": "^4.2.4", @@ -9129,7 +9129,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 701, "literal": "^2.2.0", @@ -9155,7 +9155,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 703, "literal": "^4.3.4", @@ -9168,7 +9168,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 704, "literal": "6.21.0", @@ -9181,7 +9181,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 705, "literal": "6.21.0", @@ -9194,7 +9194,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 706, "literal": "6.21.0", @@ -9207,7 +9207,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 707, "literal": "6.21.0", @@ -9233,7 +9233,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 709, "literal": "^4.3.4", @@ -9246,7 +9246,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 710, "literal": "^11.1.0", @@ -9259,7 +9259,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 711, "literal": "^7.5.4", @@ -9272,7 +9272,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 712, "literal": "^4.0.3", @@ -9285,7 +9285,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 713, "literal": "9.0.3", @@ -9298,7 +9298,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 714, "literal": "^1.0.1", @@ -9311,7 +9311,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 715, "literal": "6.21.0", @@ -9324,7 +9324,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 716, "literal": "6.21.0", @@ -9337,7 +9337,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 717, "literal": "^3.4.1", @@ -9350,7 +9350,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 718, "literal": "6.21.0", @@ -9376,7 +9376,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 720, "literal": "^2.0.1", @@ -9389,7 +9389,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 721, "literal": "^2.1.0", @@ -9402,7 +9402,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 722, "literal": "^3.0.1", @@ -9415,7 +9415,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 723, "literal": "^3.2.9", @@ -9428,7 +9428,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 724, "literal": "^5.2.0", @@ -9441,7 +9441,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 725, "literal": "^1.4.1", @@ -9454,7 +9454,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 726, "literal": "^3.0.0", @@ -9467,7 +9467,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 727, "literal": "^4.0.0", @@ -9480,7 +9480,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 728, "literal": "6.21.0", @@ -9493,7 +9493,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 729, "literal": "6.21.0", @@ -9506,7 +9506,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 730, "literal": "10.3.10", @@ -9519,7 +9519,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 731, "literal": "^7.23.2", @@ -9532,7 +9532,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 732, "literal": "^5.3.0", @@ -9545,7 +9545,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 733, "literal": "^3.1.7", @@ -9558,7 +9558,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 734, "literal": "^1.3.2", @@ -9571,7 +9571,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 735, "literal": "^0.0.8", @@ -9584,7 +9584,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 736, "literal": "=4.7.0", @@ -9597,7 +9597,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 737, "literal": "^3.2.1", @@ -9610,7 +9610,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 738, "literal": "^1.0.8", @@ -9623,7 +9623,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 739, "literal": "^9.2.2", @@ -9636,7 +9636,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 740, "literal": "^1.0.15", @@ -9649,7 +9649,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 741, "literal": "^2.0.0", @@ -9662,7 +9662,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 742, "literal": "^3.3.5", @@ -9675,7 +9675,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 743, "literal": "^1.0.9", @@ -9688,7 +9688,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 744, "literal": "^3.1.2", @@ -9701,7 +9701,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 745, "literal": "^1.1.7", @@ -9714,7 +9714,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 746, "literal": "^2.0.7", @@ -9740,7 +9740,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 748, "literal": "^1.0.7", @@ -9753,7 +9753,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 749, "literal": "^1.2.1", @@ -9766,7 +9766,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 750, "literal": "^1.0.0", @@ -9779,7 +9779,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 751, "literal": "^0.3.20", @@ -9792,7 +9792,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 752, "literal": "^3.1.6", @@ -9805,7 +9805,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 753, "literal": "^1.3.1", @@ -9818,7 +9818,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 754, "literal": "^4.1.4", @@ -9831,7 +9831,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 755, "literal": "^1.1.6", @@ -9844,7 +9844,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 756, "literal": "^1.0.7", @@ -9857,7 +9857,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 757, "literal": "^1.2.1", @@ -9870,7 +9870,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 758, "literal": "^1.23.3", @@ -9883,7 +9883,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 759, "literal": "^1.3.0", @@ -9896,7 +9896,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 760, "literal": "^2.0.3", @@ -9909,7 +9909,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 761, "literal": "^1.1.2", @@ -9922,7 +9922,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 762, "literal": "^1.2.4", @@ -9935,7 +9935,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 763, "literal": "^1.0.3", @@ -9948,7 +9948,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 764, "literal": "^1.0.2", @@ -9961,7 +9961,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 765, "literal": "^1.0.3", @@ -9974,7 +9974,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 766, "literal": "^1.0.3", @@ -9987,7 +9987,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 767, "literal": "^1.0.7", @@ -10000,7 +10000,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 768, "literal": "^1.1.2", @@ -10013,7 +10013,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 769, "literal": "^1.1.2", @@ -10026,7 +10026,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 770, "literal": "^1.2.1", @@ -10039,7 +10039,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 771, "literal": "^1.2.1", @@ -10052,7 +10052,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 772, "literal": "^1.0.3", @@ -10065,7 +10065,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 773, "literal": "^1.0.4", @@ -10078,7 +10078,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 774, "literal": "^2.0.1", @@ -10091,7 +10091,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 775, "literal": "^1.0.7", @@ -10104,7 +10104,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 776, "literal": "^1.2.1", @@ -10117,7 +10117,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 777, "literal": "^1.23.1", @@ -10130,7 +10130,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 778, "literal": "^1.3.0", @@ -10143,7 +10143,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 779, "literal": "^1.2.4", @@ -10156,7 +10156,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 780, "literal": "^1.0.3", @@ -10169,7 +10169,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 781, "literal": "^1.1.3", @@ -10182,7 +10182,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 782, "literal": "^1.1.5", @@ -10195,7 +10195,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 783, "literal": "^1.0.0", @@ -10208,7 +10208,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 784, "literal": "^2.0.0", @@ -10221,7 +10221,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 785, "literal": "^1.0.5", @@ -10234,7 +10234,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 786, "literal": "^1.0.2", @@ -10247,7 +10247,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 787, "literal": "^1.0.10", @@ -10260,7 +10260,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 788, "literal": "^1.1.4", @@ -10273,7 +10273,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 789, "literal": "^1.0.2", @@ -10286,7 +10286,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 790, "literal": "^2.0.5", @@ -10299,7 +10299,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 791, "literal": "^1.0.2", @@ -10312,7 +10312,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 792, "literal": "^1.0.1", @@ -10325,7 +10325,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 793, "literal": "^1.1.9", @@ -10338,7 +10338,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 794, "literal": "^2.0.3", @@ -10351,7 +10351,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 795, "literal": "^2.0.3", @@ -10364,7 +10364,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 796, "literal": "^2.0.2", @@ -10377,7 +10377,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 797, "literal": "^2.0.3", @@ -10390,7 +10390,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 798, "literal": "^1.0.7", @@ -10403,7 +10403,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 799, "literal": "^1.2.4", @@ -10416,7 +10416,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 800, "literal": "^1.0.0", @@ -10429,7 +10429,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 801, "literal": "^1.0.2", @@ -10442,7 +10442,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 802, "literal": "^1.0.0", @@ -10455,7 +10455,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 803, "literal": "^2.0.3", @@ -10468,7 +10468,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 804, "literal": "^2.0.3", @@ -10481,7 +10481,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 805, "literal": "^0.14.0", @@ -10494,7 +10494,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 806, "literal": "^3.1.8", @@ -10507,7 +10507,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 807, "literal": "^1.2.5", @@ -10520,7 +10520,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 808, "literal": "^1.3.2", @@ -10533,7 +10533,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 809, "literal": "^1.1.2", @@ -10546,7 +10546,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 810, "literal": "^1.1.3", @@ -10559,7 +10559,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 811, "literal": "^2.1.0", @@ -10572,7 +10572,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 812, "literal": "^1.0.19", @@ -10585,7 +10585,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 813, "literal": "^5.3.0", @@ -10598,7 +10598,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 814, "literal": "^2.4.1 || ^3.0.0", @@ -10611,7 +10611,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 815, "literal": "^3.1.2", @@ -10624,7 +10624,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 816, "literal": "^1.1.8", @@ -10637,7 +10637,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 817, "literal": "^2.0.8", @@ -10650,7 +10650,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 818, "literal": "^1.1.4", @@ -10663,7 +10663,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 819, "literal": "^1.2.0", @@ -10676,7 +10676,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 820, "literal": "^15.8.1", @@ -10689,7 +10689,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 821, "literal": "^2.0.0-next.5", @@ -10702,7 +10702,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 822, "literal": "^6.3.1", @@ -10715,7 +10715,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 823, "literal": "^4.0.11", @@ -10741,7 +10741,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 825, "literal": "^1.0.7", @@ -10754,7 +10754,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 826, "literal": "^1.2.1", @@ -10767,7 +10767,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 827, "literal": "^1.23.2", @@ -10780,7 +10780,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 828, "literal": "^1.3.0", @@ -10793,7 +10793,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 829, "literal": "^1.0.0", @@ -10806,7 +10806,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 830, "literal": "^1.2.4", @@ -10819,7 +10819,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 831, "literal": "^1.0.1", @@ -10832,7 +10832,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 832, "literal": "^1.0.3", @@ -10845,7 +10845,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 833, "literal": "^1.0.7", @@ -10858,7 +10858,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 834, "literal": "^1.5.2", @@ -10871,7 +10871,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 835, "literal": "^2.0.2", @@ -10884,7 +10884,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 836, "literal": "^1.0.6", @@ -10897,7 +10897,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 837, "literal": "^2.13.0", @@ -10910,7 +10910,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 838, "literal": "^1.0.7", @@ -10923,7 +10923,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 839, "literal": "^1.0.0", @@ -10936,7 +10936,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 840, "literal": "^1.4.0", @@ -10949,7 +10949,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 841, "literal": "^4.1.1", @@ -10962,7 +10962,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 842, "literal": "^16.13.1", @@ -10975,7 +10975,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 843, "literal": "^1.2.1", @@ -10988,7 +10988,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 844, "literal": "^1.23.2", @@ -11001,7 +11001,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 845, "literal": "^1.0.0", @@ -11014,7 +11014,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 846, "literal": "^1.0.7", @@ -11027,7 +11027,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 847, "literal": "^1.2.1", @@ -11040,7 +11040,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 848, "literal": "^1.23.3", @@ -11053,7 +11053,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 849, "literal": "^1.3.0", @@ -11066,7 +11066,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 850, "literal": "^1.0.2", @@ -11079,7 +11079,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 851, "literal": "^1.0.2", @@ -11092,7 +11092,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 852, "literal": "^1.2.0", @@ -11105,7 +11105,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 853, "literal": "^1.22.1", @@ -11118,7 +11118,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 854, "literal": "^1.0.0", @@ -11131,7 +11131,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 855, "literal": "^1.0.7", @@ -11144,7 +11144,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 856, "literal": "^1.2.1", @@ -11157,7 +11157,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 857, "literal": "^1.23.2", @@ -11170,7 +11170,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 858, "literal": "^1.3.0", @@ -11183,7 +11183,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 859, "literal": "^1.0.0", @@ -11196,7 +11196,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 860, "literal": "^1.0.2", @@ -11209,7 +11209,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 861, "literal": "~8.5.10", @@ -11222,7 +11222,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 862, "literal": "~20.12.8", @@ -11235,7 +11235,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 863, "literal": "*", @@ -11248,7 +11248,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 864, "literal": "^4.21.10", @@ -11261,7 +11261,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 865, "literal": "^1.0.30001538", @@ -11274,7 +11274,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 866, "literal": "^4.3.6", @@ -11287,7 +11287,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 867, "literal": "^0.1.2", @@ -11300,7 +11300,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 868, "literal": "^1.0.0", @@ -11313,7 +11313,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 869, "literal": "^4.2.0", @@ -11339,7 +11339,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 871, "literal": "^1.0.30001587", @@ -11352,7 +11352,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 872, "literal": "^1.4.668", @@ -11365,7 +11365,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 873, "literal": "^2.0.14", @@ -11378,7 +11378,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 874, "literal": "^1.0.13", @@ -11391,7 +11391,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 875, "literal": "^3.1.2", @@ -11404,7 +11404,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 876, "literal": "^1.0.1", @@ -11430,7 +11430,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 878, "literal": "*", @@ -11443,7 +11443,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 879, "literal": "*", @@ -11456,7 +11456,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 880, "literal": "*", @@ -11469,7 +11469,7 @@ exports[`next build works: bun 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 881, "literal": "^3.0.2", @@ -20772,7 +20772,7 @@ exports[`next build works: bun 1`] = ` "package_id": 374, }, "array-includes": { - "id": 806, + "id": 445, "package_id": 384, }, "array-union": { @@ -20792,7 +20792,7 @@ exports[`next build works: bun 1`] = ` "package_id": 382, }, "array.prototype.flatmap": { - "id": 808, + "id": 448, "package_id": 380, }, "array.prototype.toreversed": { @@ -21068,11 +21068,11 @@ exports[`next build works: bun 1`] = ` "package_id": 316, }, "es-errors": { - "id": 858, + "id": 690, "package_id": 312, }, "es-iterator-helpers": { - "id": 812, + "id": 740, "package_id": 409, }, "es-object-atoms": { @@ -21084,7 +21084,7 @@ exports[`next build works: bun 1`] = ` "package_id": 369, }, "es-shim-unscopables": { - "id": 860, + "id": 692, "package_id": 381, }, "es-to-primitive": { @@ -21120,7 +21120,7 @@ exports[`next build works: bun 1`] = ` "package_id": 302, }, "eslint-module-utils": { - "id": 452, + "id": 438, "package_id": 376, }, "eslint-plugin-import": { @@ -21164,7 +21164,7 @@ exports[`next build works: bun 1`] = ` "package_id": 279, }, "estraverse": { - "id": 432, + "id": 415, "package_id": 166, }, "esutils": { @@ -21248,7 +21248,7 @@ exports[`next build works: bun 1`] = ` "package_id": 51, }, "function-bind": { - "id": 761, + "id": 55, "package_id": 21, }, "function.prototype.name": { @@ -21412,7 +21412,7 @@ exports[`next build works: bun 1`] = ` "package_id": 329, }, "is-core-module": { - "id": 454, + "id": 675, "package_id": 19, }, "is-data-view": { @@ -21556,7 +21556,7 @@ exports[`next build works: bun 1`] = ` "package_id": 174, }, "jsx-ast-utils": { - "id": 814, + "id": 742, "package_id": 408, }, "keyv": { @@ -21680,11 +21680,11 @@ exports[`next build works: bun 1`] = ` "package_id": 355, }, "object.entries": { - "id": 816, + "id": 745, "package_id": 405, }, "object.fromentries": { - "id": 817, + "id": 457, "package_id": 375, }, "object.groupby": { @@ -21696,7 +21696,7 @@ exports[`next build works: bun 1`] = ` "package_id": 434, }, "object.values": { - "id": 819, + "id": 459, "package_id": 310, }, "once": { @@ -21924,7 +21924,7 @@ exports[`next build works: bun 1`] = ` "package_id": 112, }, "semver": { - "id": 822, + "id": 460, "package_id": 309, }, "set-function-length": { @@ -22271,18 +22271,14 @@ exports[`next build works: bun 1`] = ` }, { "dependencies": { - "doctrine": { - "id": 811, - "package_id": 379, - }, - "resolve": { - "id": 821, - "package_id": 431, + "debug": { + "id": 674, + "package_id": 377, }, }, "depth": 1, "id": 4, - "path": "node_modules/eslint-plugin-react/node_modules", + "path": "node_modules/eslint-import-resolver-node/node_modules", }, { "dependencies": { @@ -22301,14 +22297,18 @@ exports[`next build works: bun 1`] = ` }, { "dependencies": { - "debug": { - "id": 674, - "package_id": 377, + "doctrine": { + "id": 811, + "package_id": 379, + }, + "resolve": { + "id": 821, + "package_id": 431, }, }, "depth": 1, "id": 6, - "path": "node_modules/eslint-import-resolver-node/node_modules", + "path": "node_modules/eslint-plugin-react/node_modules", }, { "dependencies": { @@ -22358,17 +22358,6 @@ exports[`next build works: bun 1`] = ` "id": 10, "path": "node_modules/postcss-load-config/node_modules", }, - { - "dependencies": { - "debug": { - "id": 672, - "package_id": 377, - }, - }, - "depth": 1, - "id": 11, - "path": "node_modules/eslint-module-utils/node_modules", - }, { "dependencies": { "minimatch": { @@ -22377,7 +22366,7 @@ exports[`next build works: bun 1`] = ` }, }, "depth": 1, - "id": 12, + "id": 11, "path": "node_modules/glob/node_modules", }, { @@ -22392,9 +22381,20 @@ exports[`next build works: bun 1`] = ` }, }, "depth": 1, - "id": 13, + "id": 12, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules", }, + { + "dependencies": { + "debug": { + "id": 672, + "package_id": 377, + }, + }, + "depth": 1, + "id": 13, + "path": "node_modules/eslint-module-utils/node_modules", + }, { "dependencies": { "lru-cache": { @@ -22439,17 +22439,6 @@ exports[`next build works: bun 1`] = ` "id": 17, "path": "node_modules/path-scurry/node_modules", }, - { - "dependencies": { - "lru-cache": { - "id": 165, - "package_id": 117, - }, - }, - "depth": 2, - "id": 18, - "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", - }, { "dependencies": { "brace-expansion": { @@ -22458,9 +22447,20 @@ exports[`next build works: bun 1`] = ` }, }, "depth": 2, - "id": 19, + "id": 18, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules", }, + { + "dependencies": { + "lru-cache": { + "id": 165, + "package_id": 117, + }, + }, + "depth": 2, + "id": 19, + "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", + }, { "dependencies": { "@types/node": { @@ -22609,7 +22609,7 @@ exports[`next build works: node 1`] = ` "dependencies": [ { "behavior": { - "normal": true, + "prod": true, }, "id": 0, "literal": "20.7.0", @@ -22622,7 +22622,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 1, "literal": "18.2.22", @@ -22635,7 +22635,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 2, "literal": "18.2.7", @@ -22648,7 +22648,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 3, "literal": "10.4.16", @@ -22661,7 +22661,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 4, "literal": "^1.0.3", @@ -22674,7 +22674,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 5, "literal": "8.50.0", @@ -22687,7 +22687,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 6, "literal": "14.1.3", @@ -22700,7 +22700,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 7, "literal": "14.1.3", @@ -22713,7 +22713,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 8, "literal": "8.4.30", @@ -22726,7 +22726,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 9, "literal": "22.12.0", @@ -22739,7 +22739,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 10, "literal": "18.2.0", @@ -22752,7 +22752,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 11, "literal": "18.2.0", @@ -22765,7 +22765,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 12, "literal": "3.3.3", @@ -22778,7 +22778,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 13, "literal": "5.2.2", @@ -22791,7 +22791,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 14, "literal": "^5.0.2", @@ -22804,7 +22804,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 15, "literal": "^1.1.3", @@ -22817,7 +22817,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 16, "literal": "^1.18.2", @@ -22830,7 +22830,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 17, "literal": "^4.0.3", @@ -22843,7 +22843,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 18, "literal": "^8.4.23", @@ -22856,7 +22856,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 19, "literal": "^1.22.2", @@ -22869,7 +22869,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 20, "literal": "^3.32.0", @@ -22882,7 +22882,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 21, "literal": "^3.5.3", @@ -22895,7 +22895,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 22, "literal": "^3.2.12", @@ -22908,7 +22908,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 23, "literal": "^2.1.0", @@ -22921,7 +22921,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 24, "literal": "^1.2.2", @@ -22934,7 +22934,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 25, "literal": "^4.0.5", @@ -22947,7 +22947,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 26, "literal": "^1.0.0", @@ -22960,7 +22960,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 27, "literal": "^4.0.1", @@ -22973,7 +22973,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 28, "literal": "^6.0.2", @@ -22986,7 +22986,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 29, "literal": "^3.0.0", @@ -22999,7 +22999,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 30, "literal": "^3.0.0", @@ -23012,7 +23012,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 31, "literal": "^15.1.0", @@ -23025,7 +23025,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 32, "literal": "^6.0.1", @@ -23038,7 +23038,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 33, "literal": "^5.2.0", @@ -23051,7 +23051,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 34, "literal": "^4.0.1", @@ -23064,7 +23064,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 35, "literal": "^6.0.11", @@ -23077,7 +23077,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 36, "literal": "^3.0.0", @@ -23090,7 +23090,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 37, "literal": "^1.0.2", @@ -23103,7 +23103,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 38, "literal": "^2.3.4", @@ -23116,7 +23116,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 39, "literal": "^3.0.0", @@ -23157,7 +23157,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 42, "literal": "^3.3.6", @@ -23170,7 +23170,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 43, "literal": "^1.0.0", @@ -23183,7 +23183,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 44, "literal": "^1.0.2", @@ -23196,7 +23196,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 45, "literal": "^6.0.11", @@ -23222,7 +23222,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 47, "literal": "^4.0.0", @@ -23235,7 +23235,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 48, "literal": "^1.0.0", @@ -23248,7 +23248,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 49, "literal": "^1.1.7", @@ -23274,7 +23274,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 51, "literal": "^2.13.0", @@ -23287,7 +23287,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 52, "literal": "^1.0.7", @@ -23300,7 +23300,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 53, "literal": "^1.0.0", @@ -23313,7 +23313,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 54, "literal": "^2.0.0", @@ -23326,7 +23326,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 55, "literal": "^1.1.2", @@ -23339,7 +23339,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 56, "literal": "^2.3.0", @@ -23352,7 +23352,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 57, "literal": "^4.0.3", @@ -23365,7 +23365,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 58, "literal": "^2.1.1", @@ -23378,7 +23378,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 59, "literal": "^2.0.1", @@ -23404,7 +23404,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 61, "literal": "^3.0.3", @@ -23417,7 +23417,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 62, "literal": "^2.3.1", @@ -23430,7 +23430,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 63, "literal": "^7.1.1", @@ -23443,7 +23443,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 64, "literal": "^5.0.1", @@ -23456,7 +23456,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 65, "literal": "^7.0.0", @@ -23469,7 +23469,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 66, "literal": "^2.0.2", @@ -23482,7 +23482,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 67, "literal": "^1.2.3", @@ -23495,7 +23495,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 68, "literal": "^5.1.2", @@ -23508,7 +23508,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 69, "literal": "^1.3.0", @@ -23521,7 +23521,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 70, "literal": "^4.0.4", @@ -23534,7 +23534,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 71, "literal": "^4.0.1", @@ -23547,7 +23547,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 72, "literal": "2.1.5", @@ -23560,7 +23560,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 73, "literal": "^1.6.0", @@ -23573,7 +23573,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 74, "literal": "^1.0.4", @@ -23586,7 +23586,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 75, "literal": "2.0.5", @@ -23599,7 +23599,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 76, "literal": "^1.1.9", @@ -23612,7 +23612,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 77, "literal": "^1.2.2", @@ -23625,7 +23625,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 78, "literal": "~3.1.2", @@ -23638,7 +23638,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 79, "literal": "~3.0.2", @@ -23651,7 +23651,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 80, "literal": "~5.1.2", @@ -23664,7 +23664,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 81, "literal": "~2.1.0", @@ -23677,7 +23677,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 82, "literal": "~4.0.1", @@ -23690,7 +23690,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 83, "literal": "~3.0.0", @@ -23703,7 +23703,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 84, "literal": "~3.6.0", @@ -23729,7 +23729,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 86, "literal": "^2.2.1", @@ -23742,7 +23742,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 87, "literal": "^2.0.0", @@ -23755,7 +23755,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 88, "literal": "^3.0.0", @@ -23768,7 +23768,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 89, "literal": "^2.0.4", @@ -23781,7 +23781,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 90, "literal": "^0.3.2", @@ -23794,7 +23794,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 91, "literal": "^4.0.0", @@ -23807,7 +23807,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 92, "literal": "^10.3.10", @@ -23820,7 +23820,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 93, "literal": "^1.1.6", @@ -23833,7 +23833,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 94, "literal": "^2.7.0", @@ -23846,7 +23846,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 95, "literal": "^4.0.1", @@ -23859,7 +23859,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 96, "literal": "^0.1.9", @@ -23872,7 +23872,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 97, "literal": "^1.0.0", @@ -23885,7 +23885,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 98, "literal": "^4.0.1", @@ -23898,7 +23898,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 99, "literal": "^1.0.0", @@ -23911,7 +23911,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 100, "literal": ">= 3.1.0 < 4", @@ -23924,7 +23924,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 101, "literal": "^1.0.0", @@ -23937,7 +23937,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 102, "literal": "^3.1.0", @@ -23950,7 +23950,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 103, "literal": "^2.3.5", @@ -23963,7 +23963,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 104, "literal": "^9.0.1", @@ -23976,7 +23976,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 105, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -23989,7 +23989,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 106, "literal": "^1.10.1", @@ -24002,7 +24002,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 107, "literal": "^10.2.0", @@ -24015,7 +24015,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 108, "literal": "^5.0.0 || ^6.0.2 || ^7.0.0", @@ -24028,7 +24028,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 109, "literal": "^2.0.1", @@ -24041,7 +24041,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 110, "literal": "^1.0.0", @@ -24054,7 +24054,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 111, "literal": "^8.0.2", @@ -24080,7 +24080,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 113, "literal": "^5.1.2", @@ -24093,7 +24093,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 114, "is_alias": true, @@ -24107,7 +24107,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 115, "literal": "^7.0.1", @@ -24120,7 +24120,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 116, "is_alias": true, @@ -24134,7 +24134,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 117, "literal": "^8.1.0", @@ -24147,7 +24147,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 118, "is_alias": true, @@ -24161,7 +24161,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 119, "literal": "^4.0.0", @@ -24174,7 +24174,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 120, "literal": "^4.1.0", @@ -24187,7 +24187,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 121, "literal": "^6.0.0", @@ -24200,7 +24200,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 122, "literal": "^5.0.1", @@ -24213,7 +24213,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 123, "literal": "^8.0.0", @@ -24226,7 +24226,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 124, "literal": "^3.0.0", @@ -24239,7 +24239,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 125, "literal": "^6.0.1", @@ -24252,7 +24252,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 126, "literal": "^2.0.1", @@ -24265,7 +24265,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 127, "literal": "~1.1.4", @@ -24278,7 +24278,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 128, "literal": "^6.1.0", @@ -24291,7 +24291,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 129, "literal": "^5.0.1", @@ -24304,7 +24304,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 130, "literal": "^7.0.1", @@ -24317,7 +24317,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 131, "literal": "^6.0.1", @@ -24330,7 +24330,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 132, "literal": "^0.2.0", @@ -24343,7 +24343,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 133, "literal": "^9.2.2", @@ -24356,7 +24356,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 134, "literal": "^7.0.1", @@ -24369,7 +24369,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 135, "literal": "^7.0.0", @@ -24382,7 +24382,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 136, "literal": "^4.0.1", @@ -24395,7 +24395,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 137, "literal": "^3.1.0", @@ -24408,7 +24408,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 138, "literal": "^2.0.0", @@ -24421,7 +24421,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 139, "literal": "^2.0.1", @@ -24434,7 +24434,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 140, "literal": "^2.0.0", @@ -24447,7 +24447,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 141, "literal": "^3.0.0", @@ -24460,7 +24460,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 142, "literal": "^1.2.1", @@ -24473,7 +24473,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 143, "literal": "^1.4.10", @@ -24486,7 +24486,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 144, "literal": "^0.3.24", @@ -24499,7 +24499,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 145, "literal": "^3.1.0", @@ -24512,7 +24512,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 146, "literal": "^1.4.14", @@ -24525,7 +24525,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 147, "literal": "^0.23.0", @@ -24538,7 +24538,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 148, "literal": "^1.1.0", @@ -24564,7 +24564,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 150, "literal": "^1.1.0", @@ -24577,7 +24577,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 151, "literal": "^3.0.0 || ^4.0.0", @@ -24590,7 +24590,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 152, "literal": "^1.1.0", @@ -24603,7 +24603,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 153, "literal": "9.0.0", @@ -24616,7 +24616,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 154, "literal": "22.12.0", @@ -24629,7 +24629,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 155, "literal": "2.2.3", @@ -24642,7 +24642,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 156, "literal": "0.0.1299070", @@ -24655,7 +24655,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 157, "literal": "4.3.4", @@ -24668,7 +24668,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 158, "literal": "2.0.1", @@ -24681,7 +24681,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 159, "literal": "2.0.3", @@ -24694,7 +24694,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 160, "literal": "6.4.0", @@ -24707,7 +24707,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 161, "literal": "3.0.5", @@ -24720,7 +24720,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 162, "literal": "1.4.3", @@ -24733,7 +24733,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 163, "literal": "17.7.2", @@ -24746,7 +24746,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 164, "literal": "7.6.0", @@ -24759,7 +24759,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 165, "literal": "^6.0.0", @@ -24772,7 +24772,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 166, "literal": "^4.0.0", @@ -24785,7 +24785,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 167, "literal": "^8.0.1", @@ -24798,7 +24798,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 168, "literal": "^3.1.1", @@ -24811,7 +24811,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 169, "literal": "^2.0.5", @@ -24824,7 +24824,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 170, "literal": "^2.1.1", @@ -24837,7 +24837,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 171, "literal": "^4.2.3", @@ -24850,7 +24850,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 172, "literal": "^5.0.5", @@ -24863,7 +24863,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 173, "literal": "^21.1.1", @@ -24876,7 +24876,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 174, "literal": "^4.2.0", @@ -24889,7 +24889,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 175, "literal": "^6.0.1", @@ -24902,7 +24902,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 176, "literal": "^7.0.0", @@ -24915,7 +24915,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 177, "literal": "^5.2.1", @@ -24928,7 +24928,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 178, "literal": "^2.3.8", @@ -24941,7 +24941,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 179, "literal": "^1.3.1", @@ -24954,7 +24954,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 180, "literal": "^1.1.13", @@ -24967,7 +24967,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 181, "literal": "^3.0.0", @@ -24980,7 +24980,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 182, "literal": "^3.1.5", @@ -25019,7 +25019,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 185, "literal": "^2.1.0", @@ -25032,7 +25032,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 186, "literal": "^2.0.0", @@ -25045,7 +25045,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 187, "literal": "^2.0.0", @@ -25058,7 +25058,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 188, "literal": "^2.0.0", @@ -25071,7 +25071,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 189, "literal": "^2.18.0", @@ -25084,7 +25084,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 190, "literal": "^1.3.2", @@ -25097,7 +25097,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 191, "literal": "^1.0.1", @@ -25110,7 +25110,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 192, "literal": "^1.1.0", @@ -25136,7 +25136,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 194, "literal": "^1.6.4", @@ -25149,7 +25149,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 195, "literal": "^1.6.4", @@ -25162,7 +25162,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 196, "literal": "^1.2.0", @@ -25175,7 +25175,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 197, "literal": "^2.15.0", @@ -25188,7 +25188,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 198, "literal": "^1.1.0", @@ -25201,7 +25201,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 199, "literal": "^1.3.1", @@ -25214,7 +25214,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 200, "literal": "1", @@ -25227,7 +25227,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 201, "literal": "^1.4.0", @@ -25240,7 +25240,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 202, "literal": "^7.0.2", @@ -25253,7 +25253,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 203, "literal": "^4.3.4", @@ -25266,7 +25266,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 204, "literal": "^7.0.1", @@ -25279,7 +25279,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 205, "literal": "^7.0.3", @@ -25292,7 +25292,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 206, "literal": "^7.14.1", @@ -25305,7 +25305,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 207, "literal": "^7.0.1", @@ -25318,7 +25318,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 208, "literal": "^1.1.0", @@ -25331,7 +25331,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 209, "literal": "^8.0.2", @@ -25344,7 +25344,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 210, "literal": "^7.1.1", @@ -25357,7 +25357,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 211, "literal": "^4.3.4", @@ -25370,7 +25370,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 212, "literal": "^2.7.1", @@ -25383,7 +25383,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 213, "literal": "^9.0.5", @@ -25396,7 +25396,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 214, "literal": "^4.2.0", @@ -25409,7 +25409,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 215, "literal": "1.1.0", @@ -25422,7 +25422,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 216, "literal": "^1.1.3", @@ -25435,7 +25435,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 217, "literal": "2.1.2", @@ -25448,7 +25448,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 218, "literal": "^4.3.4", @@ -25461,7 +25461,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 219, "literal": "^0.23.0", @@ -25474,7 +25474,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 220, "literal": "^7.0.2", @@ -25487,7 +25487,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 221, "literal": "^4.3.4", @@ -25500,7 +25500,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 222, "literal": "^6.0.1", @@ -25513,7 +25513,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 223, "literal": "^7.0.0", @@ -25526,7 +25526,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 224, "literal": "^7.0.2", @@ -25539,7 +25539,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 225, "literal": "^7.0.0", @@ -25552,7 +25552,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 226, "literal": "^8.0.2", @@ -25565,7 +25565,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 227, "literal": "^5.0.0", @@ -25578,7 +25578,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 228, "literal": "^2.0.2", @@ -25591,7 +25591,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 229, "literal": "^0.13.4", @@ -25604,7 +25604,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 230, "literal": "^2.1.0", @@ -25617,7 +25617,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 231, "literal": "^4.0.1", @@ -25630,7 +25630,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 232, "literal": "^5.2.0", @@ -25643,7 +25643,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 233, "literal": "^2.0.2", @@ -25656,7 +25656,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 234, "literal": "^4.0.1", @@ -25682,7 +25682,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 236, "literal": "^2.0.1", @@ -25695,7 +25695,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 237, "literal": "^7.0.2", @@ -25708,7 +25708,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 238, "literal": "4", @@ -25721,7 +25721,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 239, "literal": "^7.1.0", @@ -25734,7 +25734,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 240, "literal": "^4.3.4", @@ -25747,7 +25747,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 241, "literal": "^5.0.2", @@ -25760,7 +25760,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 242, "literal": "^6.0.2", @@ -25773,7 +25773,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 243, "literal": "^4.3.4", @@ -25786,7 +25786,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 244, "literal": "^11.2.0", @@ -25799,7 +25799,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 245, "literal": "^4.2.0", @@ -25812,7 +25812,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 246, "literal": "^6.0.1", @@ -25825,7 +25825,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 247, "literal": "^2.0.0", @@ -25838,7 +25838,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 248, "literal": "^2.0.0", @@ -25864,7 +25864,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 250, "literal": "^4.1.1", @@ -25877,7 +25877,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 251, "literal": "^5.1.0", @@ -25890,7 +25890,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 252, "literal": "^2.10.0", @@ -25916,7 +25916,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 254, "literal": "*", @@ -25929,7 +25929,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 255, "literal": "~5.26.4", @@ -25942,7 +25942,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 256, "literal": "~1.1.0", @@ -25955,7 +25955,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 257, "literal": "~0.2.3", @@ -25968,7 +25968,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 258, "literal": "~1.2.0", @@ -25981,7 +25981,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 259, "literal": "^3.0.0", @@ -25994,7 +25994,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 260, "literal": "2.1.2", @@ -26007,7 +26007,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 261, "literal": "2.2.3", @@ -26020,7 +26020,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 262, "literal": "0.5.24", @@ -26033,7 +26033,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 263, "literal": "4.3.5", @@ -26046,7 +26046,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 264, "literal": "0.0.1299070", @@ -26059,7 +26059,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 265, "literal": "8.17.1", @@ -26100,7 +26100,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 268, "literal": "3.0.1", @@ -26113,7 +26113,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 269, "literal": "10.0.0", @@ -26126,7 +26126,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 270, "literal": "3.23.8", @@ -26152,7 +26152,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 272, "literal": "^2.2.1", @@ -26165,7 +26165,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 273, "literal": "^3.3.0", @@ -26178,7 +26178,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 274, "literal": "^4.1.0", @@ -26191,7 +26191,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 275, "literal": "^5.2.0", @@ -26218,7 +26218,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 277, "literal": "^7.0.0", @@ -26231,7 +26231,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 278, "literal": "^1.3.1", @@ -26244,7 +26244,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 279, "literal": "^2.3.0", @@ -26257,7 +26257,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 280, "literal": "^1.1.6", @@ -26270,7 +26270,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 281, "literal": "^0.2.1", @@ -26283,7 +26283,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 282, "literal": "^7.24.7", @@ -26296,7 +26296,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 283, "literal": "^1.0.0", @@ -26309,7 +26309,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 284, "literal": "^7.24.7", @@ -26322,7 +26322,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 285, "literal": "^2.4.2", @@ -26335,7 +26335,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 286, "literal": "^4.0.0", @@ -26348,7 +26348,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 287, "literal": "^1.0.0", @@ -26361,7 +26361,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 288, "literal": "^3.2.1", @@ -26374,7 +26374,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 289, "literal": "^1.0.5", @@ -26387,7 +26387,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 290, "literal": "^5.3.0", @@ -26400,7 +26400,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 291, "literal": "^3.0.0", @@ -26413,7 +26413,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 292, "literal": "^1.9.0", @@ -26426,7 +26426,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 293, "literal": "1.1.3", @@ -26439,7 +26439,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 294, "literal": "^2.0.1", @@ -26452,7 +26452,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 295, "literal": "^1.0.0", @@ -26465,7 +26465,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 296, "literal": "^4.0.0", @@ -26478,7 +26478,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 297, "literal": "^3.0.0", @@ -26491,7 +26491,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 298, "literal": "1.6.0", @@ -26504,7 +26504,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 299, "literal": "8.4.31", @@ -26517,7 +26517,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 300, "literal": "14.1.3", @@ -26530,7 +26530,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 301, "literal": "5.1.1", @@ -26543,7 +26543,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 302, "literal": "^4.2.11", @@ -26556,7 +26556,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 303, "literal": "0.5.2", @@ -26569,7 +26569,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 304, "literal": "^1.0.30001579", @@ -26753,7 +26753,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 318, "literal": "^2.4.0", @@ -26766,7 +26766,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 319, "literal": "0.0.1", @@ -26792,7 +26792,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 321, "literal": "^3.3.6", @@ -26805,7 +26805,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 322, "literal": "^1.0.0", @@ -26818,7 +26818,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 323, "literal": "^1.0.2", @@ -26831,7 +26831,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 324, "literal": "^1.1.0", @@ -26844,7 +26844,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 325, "literal": "^7.33.2", @@ -26857,7 +26857,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 326, "literal": "^2.28.1", @@ -26870,7 +26870,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 327, "literal": "^6.7.1", @@ -26883,7 +26883,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 328, "literal": "^1.3.3", @@ -26896,7 +26896,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 329, "literal": "14.1.3", @@ -26909,7 +26909,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 330, "literal": "^5.4.2 || ^6.0.0", @@ -26922,7 +26922,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 331, "literal": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705", @@ -26935,7 +26935,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 332, "literal": "^0.3.6", @@ -26948,7 +26948,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 333, "literal": "^3.5.2", @@ -26988,7 +26988,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 336, "literal": "^6.12.4", @@ -27001,7 +27001,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 337, "literal": "^0.4.1", @@ -27014,7 +27014,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 338, "literal": "^4.0.0", @@ -27027,7 +27027,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 339, "literal": "^4.3.2", @@ -27040,7 +27040,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 340, "literal": "^9.6.1", @@ -27053,7 +27053,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 341, "literal": "^5.2.0", @@ -27066,7 +27066,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 342, "literal": "^1.4.2", @@ -27079,7 +27079,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 343, "literal": "^2.0.2", @@ -27092,7 +27092,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 344, "literal": "^5.0.0", @@ -27105,7 +27105,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 345, "literal": "^13.19.0", @@ -27118,7 +27118,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 346, "literal": "^4.0.0", @@ -27131,7 +27131,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 347, "literal": "^4.1.0", @@ -27144,7 +27144,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 348, "literal": "^3.0.0", @@ -27157,7 +27157,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 349, "literal": "^1.4.0", @@ -27170,7 +27170,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 350, "literal": "^3.1.2", @@ -27183,7 +27183,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 351, "literal": "8.50.0", @@ -27196,7 +27196,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 352, "literal": "^0.9.3", @@ -27209,7 +27209,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 353, "literal": "^6.0.1", @@ -27222,7 +27222,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 354, "literal": "^0.2.0", @@ -27235,7 +27235,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 355, "literal": "^7.0.2", @@ -27248,7 +27248,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 356, "literal": "^6.0.2", @@ -27261,7 +27261,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 357, "literal": "^0.1.4", @@ -27274,7 +27274,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 358, "literal": "^7.2.2", @@ -27287,7 +27287,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 359, "literal": "^4.6.2", @@ -27300,7 +27300,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 360, "literal": "^3.0.3", @@ -27313,7 +27313,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 361, "literal": "^3.1.3", @@ -27326,7 +27326,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 362, "literal": "^1.4.0", @@ -27339,7 +27339,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 363, "literal": "^2.1.2", @@ -27352,7 +27352,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 364, "literal": "^1.2.8", @@ -27365,7 +27365,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 365, "literal": "^6.0.1", @@ -27378,7 +27378,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 366, "literal": "^3.4.3", @@ -27391,7 +27391,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 367, "literal": "^4.0.0", @@ -27404,7 +27404,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 368, "literal": "^4.6.1", @@ -27417,7 +27417,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 369, "literal": "^0.11.11", @@ -27430,7 +27430,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 370, "literal": "^4.2.0", @@ -27443,7 +27443,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 371, "literal": "^1.0.1", @@ -27456,7 +27456,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 372, "literal": "^1.0.1", @@ -27469,7 +27469,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 373, "literal": "^3.3.0", @@ -27495,7 +27495,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 375, "literal": "^2.0.2", @@ -27508,7 +27508,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 376, "literal": "^4.3.1", @@ -27521,7 +27521,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 377, "literal": "^3.0.5", @@ -27534,7 +27534,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 378, "literal": "^1.1.7", @@ -27547,7 +27547,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 379, "literal": "^1.0.0", @@ -27560,7 +27560,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 380, "literal": "0.0.1", @@ -27573,7 +27573,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 381, "literal": "^3.0.4", @@ -27586,7 +27586,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 382, "literal": "^3.2.9", @@ -27599,7 +27599,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 383, "literal": "^4.5.3", @@ -27612,7 +27612,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 384, "literal": "^3.0.2", @@ -27625,7 +27625,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 385, "literal": "^7.1.3", @@ -27638,7 +27638,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 386, "literal": "^1.0.0", @@ -27651,7 +27651,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 387, "literal": "^1.0.4", @@ -27664,7 +27664,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 388, "literal": "2", @@ -27677,7 +27677,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 389, "literal": "^3.1.1", @@ -27690,7 +27690,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 390, "literal": "^1.3.0", @@ -27703,7 +27703,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 391, "literal": "^1.0.0", @@ -27716,7 +27716,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 392, "literal": "^1.3.0", @@ -27729,7 +27729,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 393, "literal": "1", @@ -27742,7 +27742,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 394, "literal": "3.0.1", @@ -27755,7 +27755,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 395, "literal": "^6.12.4", @@ -27768,7 +27768,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 396, "literal": "^4.3.2", @@ -27781,7 +27781,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 397, "literal": "^9.6.0", @@ -27794,7 +27794,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 398, "literal": "^13.19.0", @@ -27807,7 +27807,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 399, "literal": "^5.2.0", @@ -27820,7 +27820,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 400, "literal": "^3.2.1", @@ -27833,7 +27833,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 401, "literal": "^4.1.0", @@ -27846,7 +27846,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 402, "literal": "^3.1.2", @@ -27859,7 +27859,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 403, "literal": "^3.1.1", @@ -27872,7 +27872,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 404, "literal": "^0.20.2", @@ -27885,7 +27885,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 405, "literal": "^8.9.0", @@ -27898,7 +27898,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 406, "literal": "^5.3.2", @@ -27911,7 +27911,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 407, "literal": "^3.4.1", @@ -27937,7 +27937,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 409, "literal": "^3.1.1", @@ -27950,7 +27950,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 410, "literal": "^2.0.0", @@ -27963,7 +27963,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 411, "literal": "^0.4.1", @@ -27976,7 +27976,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 412, "literal": "^4.2.2", @@ -27989,7 +27989,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 413, "literal": "^2.1.0", @@ -28002,7 +28002,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 414, "literal": "^4.3.0", @@ -28015,7 +28015,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 415, "literal": "^5.2.0", @@ -28028,7 +28028,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 416, "literal": "^5.2.0", @@ -28041,7 +28041,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 417, "literal": "^1.2.1", @@ -28054,7 +28054,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 418, "literal": "^0.1.3", @@ -28067,7 +28067,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 419, "literal": "^1.2.5", @@ -28080,7 +28080,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 420, "literal": "^0.4.0", @@ -28093,7 +28093,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 421, "literal": "^0.4.1", @@ -28106,7 +28106,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 422, "literal": "^2.0.6", @@ -28119,7 +28119,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 423, "literal": "^1.2.1", @@ -28132,7 +28132,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 424, "literal": "~0.4.0", @@ -28145,7 +28145,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 425, "literal": "^1.2.1", @@ -28158,7 +28158,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 426, "literal": "^2.0.2", @@ -28171,7 +28171,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 427, "literal": "^6.0.0", @@ -28184,7 +28184,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 428, "literal": "^4.0.0", @@ -28197,7 +28197,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 429, "literal": "^5.0.0", @@ -28210,7 +28210,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 430, "literal": "^3.0.2", @@ -28223,7 +28223,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 431, "literal": "^0.1.0", @@ -28236,7 +28236,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 432, "literal": "^5.1.0", @@ -28249,7 +28249,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 433, "literal": "^4.1.0", @@ -28262,7 +28262,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 434, "literal": "^7.1.0", @@ -28275,7 +28275,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 435, "literal": "^4.0.0", @@ -28288,7 +28288,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 436, "literal": "^4.3.4", @@ -28301,7 +28301,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 437, "literal": "^5.12.0", @@ -28314,7 +28314,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 438, "literal": "^2.7.4", @@ -28327,7 +28327,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 439, "literal": "^3.3.1", @@ -28340,7 +28340,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 440, "literal": "^4.5.0", @@ -28353,7 +28353,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 441, "literal": "^2.11.0", @@ -28366,7 +28366,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 442, "literal": "^4.0.3", @@ -28405,7 +28405,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 445, "literal": "^3.1.7", @@ -28418,7 +28418,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 446, "literal": "^1.2.3", @@ -28431,7 +28431,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 447, "literal": "^1.3.2", @@ -28444,7 +28444,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 448, "literal": "^1.3.2", @@ -28457,7 +28457,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 449, "literal": "^3.2.7", @@ -28470,7 +28470,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 450, "literal": "^2.1.0", @@ -28483,7 +28483,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 451, "literal": "^0.3.9", @@ -28496,7 +28496,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 452, "literal": "^2.8.0", @@ -28509,7 +28509,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 453, "literal": "^2.0.0", @@ -28522,7 +28522,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 454, "literal": "^2.13.1", @@ -28535,7 +28535,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 455, "literal": "^4.0.3", @@ -28548,7 +28548,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 456, "literal": "^3.1.2", @@ -28561,7 +28561,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 457, "literal": "^2.0.7", @@ -28574,7 +28574,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 458, "literal": "^1.0.1", @@ -28587,7 +28587,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 459, "literal": "^1.1.7", @@ -28600,7 +28600,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 460, "literal": "^6.3.1", @@ -28613,7 +28613,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 461, "literal": "^3.15.0", @@ -28639,7 +28639,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 463, "literal": "^0.0.29", @@ -28652,7 +28652,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 464, "literal": "^1.0.2", @@ -28665,7 +28665,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 465, "literal": "^1.2.6", @@ -28678,7 +28678,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 466, "literal": "^3.0.0", @@ -28691,7 +28691,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 467, "literal": "^1.2.0", @@ -28704,7 +28704,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 468, "literal": "^1.0.7", @@ -28717,7 +28717,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 469, "literal": "^1.2.1", @@ -28730,7 +28730,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 470, "literal": "^1.0.0", @@ -28743,7 +28743,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 471, "literal": "^1.3.0", @@ -28756,7 +28756,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 472, "literal": "^1.0.1", @@ -28769,7 +28769,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 473, "literal": "^1.0.0", @@ -28782,7 +28782,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 474, "literal": "^1.1.1", @@ -28795,7 +28795,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 475, "literal": "^1.0.0", @@ -28808,7 +28808,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 476, "literal": "^1.2.4", @@ -28821,7 +28821,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 477, "literal": "^1.3.0", @@ -28834,7 +28834,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 478, "literal": "^1.1.2", @@ -28847,7 +28847,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 479, "literal": "^1.0.1", @@ -28860,7 +28860,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 480, "literal": "^1.0.3", @@ -28873,7 +28873,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 481, "literal": "^2.0.0", @@ -28886,7 +28886,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 482, "literal": "^1.0.0", @@ -28899,7 +28899,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 483, "literal": "^1.3.0", @@ -28912,7 +28912,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 484, "literal": "^1.0.1", @@ -28925,7 +28925,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 485, "literal": "^1.1.3", @@ -28938,7 +28938,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 486, "literal": "^1.0.0", @@ -28951,7 +28951,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 487, "literal": "^1.3.0", @@ -28964,7 +28964,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 488, "literal": "^1.1.2", @@ -28977,7 +28977,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 489, "literal": "^1.2.4", @@ -28990,7 +28990,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 490, "literal": "^1.2.1", @@ -29003,7 +29003,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 491, "literal": "^1.1.4", @@ -29016,7 +29016,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 492, "literal": "^1.3.0", @@ -29029,7 +29029,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 493, "literal": "^1.1.2", @@ -29042,7 +29042,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 494, "literal": "^1.2.4", @@ -29055,7 +29055,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 495, "literal": "^1.0.1", @@ -29068,7 +29068,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 496, "literal": "^1.0.2", @@ -29081,7 +29081,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 497, "literal": "^1.0.7", @@ -29094,7 +29094,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 498, "literal": "^1.2.1", @@ -29107,7 +29107,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 499, "literal": "^1.23.2", @@ -29120,7 +29120,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 500, "literal": "^1.0.1", @@ -29133,7 +29133,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 501, "literal": "^1.0.3", @@ -29146,7 +29146,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 502, "literal": "^1.0.7", @@ -29159,7 +29159,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 503, "literal": "^1.0.7", @@ -29172,7 +29172,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 504, "literal": "^1.0.1", @@ -29185,7 +29185,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 505, "literal": "^1.0.1", @@ -29198,7 +29198,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 506, "literal": "^1.0.0", @@ -29211,7 +29211,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 507, "literal": "^1.0.0", @@ -29224,7 +29224,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 508, "literal": "^1.3.0", @@ -29237,7 +29237,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 509, "literal": "^1.0.0", @@ -29250,7 +29250,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 510, "literal": "^2.0.3", @@ -29263,7 +29263,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 511, "literal": "^1.2.1", @@ -29276,7 +29276,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 512, "literal": "^1.1.6", @@ -29289,7 +29289,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 513, "literal": "^1.2.4", @@ -29302,7 +29302,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 514, "literal": "^1.0.2", @@ -29315,7 +29315,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 515, "literal": "^1.0.3", @@ -29328,7 +29328,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 516, "literal": "^1.0.1", @@ -29341,7 +29341,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 517, "literal": "^1.0.2", @@ -29354,7 +29354,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 518, "literal": "^1.0.3", @@ -29367,7 +29367,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 519, "literal": "^1.0.3", @@ -29380,7 +29380,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 520, "literal": "^2.0.2", @@ -29393,7 +29393,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 521, "literal": "^1.0.7", @@ -29406,7 +29406,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 522, "literal": "^3.0.4", @@ -29419,7 +29419,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 523, "literal": "^1.2.7", @@ -29432,7 +29432,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 524, "literal": "^1.0.1", @@ -29445,7 +29445,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 525, "literal": "^2.0.3", @@ -29458,7 +29458,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 526, "literal": "^1.1.4", @@ -29471,7 +29471,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 527, "literal": "^1.0.3", @@ -29484,7 +29484,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 528, "literal": "^1.0.7", @@ -29497,7 +29497,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 529, "literal": "^1.1.13", @@ -29510,7 +29510,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 530, "literal": "^1.0.2", @@ -29523,7 +29523,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 531, "literal": "^1.13.1", @@ -29536,7 +29536,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 532, "literal": "^1.1.1", @@ -29549,7 +29549,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 533, "literal": "^4.1.5", @@ -29562,7 +29562,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 534, "literal": "^1.5.2", @@ -29575,7 +29575,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 535, "literal": "^1.1.2", @@ -29588,7 +29588,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 536, "literal": "^1.0.3", @@ -29601,7 +29601,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 537, "literal": "^1.2.9", @@ -29614,7 +29614,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 538, "literal": "^1.0.8", @@ -29627,7 +29627,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 539, "literal": "^1.0.8", @@ -29640,7 +29640,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 540, "literal": "^1.0.2", @@ -29653,7 +29653,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 541, "literal": "^1.0.1", @@ -29666,7 +29666,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 542, "literal": "^1.0.2", @@ -29679,7 +29679,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 543, "literal": "^1.0.6", @@ -29692,7 +29692,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 544, "literal": "^1.0.2", @@ -29705,7 +29705,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 545, "literal": "^1.1.15", @@ -29718,7 +29718,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 546, "literal": "^1.0.7", @@ -29731,7 +29731,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 547, "literal": "^1.0.7", @@ -29744,7 +29744,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 548, "literal": "^0.3.3", @@ -29757,7 +29757,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 549, "literal": "^1.0.1", @@ -29770,7 +29770,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 550, "literal": "^1.0.2", @@ -29783,7 +29783,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 551, "literal": "^1.0.3", @@ -29796,7 +29796,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 552, "literal": "^1.1.3", @@ -29809,7 +29809,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 553, "literal": "^1.0.0", @@ -29822,7 +29822,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 554, "literal": "^1.0.2", @@ -29835,7 +29835,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 555, "literal": "^1.0.2", @@ -29848,7 +29848,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 556, "literal": "^1.0.3", @@ -29861,7 +29861,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 557, "literal": "^1.0.2", @@ -29874,7 +29874,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 558, "literal": "^1.0.1", @@ -29887,7 +29887,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 559, "literal": "^1.1.0", @@ -29900,7 +29900,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 560, "literal": "^1.0.4", @@ -29913,7 +29913,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 561, "literal": "^1.0.5", @@ -29926,7 +29926,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 562, "literal": "^1.0.3", @@ -29939,7 +29939,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 563, "literal": "^1.0.2", @@ -29952,7 +29952,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 564, "literal": "^1.0.0", @@ -29965,7 +29965,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 565, "literal": "^1.0.0", @@ -29978,7 +29978,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 566, "literal": "^1.0.2", @@ -29991,7 +29991,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 567, "literal": "^1.0.0", @@ -30004,7 +30004,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 568, "literal": "^1.0.1", @@ -30017,7 +30017,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 569, "literal": "^1.0.7", @@ -30030,7 +30030,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 570, "literal": "^0.3.3", @@ -30043,7 +30043,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 571, "literal": "^1.0.1", @@ -30056,7 +30056,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 572, "literal": "^1.0.3", @@ -30069,7 +30069,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 573, "literal": "^1.1.13", @@ -30082,7 +30082,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 574, "literal": "^1.0.0", @@ -30095,7 +30095,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 575, "literal": "^1.1.14", @@ -30108,7 +30108,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 576, "literal": "^1.0.7", @@ -30121,7 +30121,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 577, "literal": "^1.0.7", @@ -30134,7 +30134,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 578, "literal": "^0.3.3", @@ -30147,7 +30147,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 579, "literal": "^1.0.1", @@ -30160,7 +30160,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 580, "literal": "^1.0.3", @@ -30173,7 +30173,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 581, "literal": "^1.1.13", @@ -30186,7 +30186,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 582, "literal": "^1.0.7", @@ -30199,7 +30199,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 583, "literal": "^0.3.3", @@ -30212,7 +30212,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 584, "literal": "^1.0.1", @@ -30225,7 +30225,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 585, "literal": "^1.0.3", @@ -30238,7 +30238,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 586, "literal": "^1.1.13", @@ -30251,7 +30251,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 587, "literal": "^1.0.7", @@ -30264,7 +30264,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 588, "literal": "^1.3.0", @@ -30277,7 +30277,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 589, "literal": "^1.1.13", @@ -30290,7 +30290,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 590, "literal": "^1.0.7", @@ -30303,7 +30303,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 591, "literal": "^1.2.1", @@ -30316,7 +30316,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 592, "literal": "^1.0.0", @@ -30329,7 +30329,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 593, "literal": "^1.0.7", @@ -30342,7 +30342,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 594, "literal": "^1.2.1", @@ -30355,7 +30355,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 595, "literal": "^1.0.0", @@ -30368,7 +30368,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 596, "literal": "^1.0.7", @@ -30381,7 +30381,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 597, "literal": "^1.2.1", @@ -30394,7 +30394,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 598, "literal": "^1.23.0", @@ -30407,7 +30407,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 599, "literal": "^1.0.0", @@ -30420,7 +30420,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 600, "literal": "^1.0.6", @@ -30433,7 +30433,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 601, "literal": "^1.3.0", @@ -30446,7 +30446,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 602, "literal": "^1.1.4", @@ -30459,7 +30459,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 603, "literal": "^1.0.2", @@ -30472,7 +30472,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 604, "literal": "^1.0.0", @@ -30485,7 +30485,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 605, "literal": "^1.0.7", @@ -30498,7 +30498,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 606, "literal": "^1.2.4", @@ -30511,7 +30511,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 607, "literal": "^1.0.3", @@ -30524,7 +30524,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 608, "literal": "^2.0.5", @@ -30537,7 +30537,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 609, "literal": "^1.0.6", @@ -30550,7 +30550,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 610, "literal": "^1.2.1", @@ -30563,7 +30563,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 611, "literal": "^1.3.0", @@ -30576,7 +30576,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 612, "literal": "^2.0.1", @@ -30589,7 +30589,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 613, "literal": "^1.1.4", @@ -30602,7 +30602,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 614, "literal": "^1.3.0", @@ -30615,7 +30615,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 615, "literal": "^1.2.3", @@ -30628,7 +30628,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 616, "literal": "^1.0.2", @@ -30641,7 +30641,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 617, "literal": "^1.0.5", @@ -30654,7 +30654,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 618, "literal": "^1.2.1", @@ -30667,7 +30667,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 619, "literal": "^1.0.3", @@ -30680,7 +30680,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 620, "literal": "^1.1.1", @@ -30693,7 +30693,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 621, "literal": "^1.0.2", @@ -30706,7 +30706,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 622, "literal": "^1.0.7", @@ -30719,7 +30719,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 623, "literal": "^1.1.13", @@ -30732,7 +30732,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 624, "literal": "^1.0.2", @@ -30745,7 +30745,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 625, "literal": "^1.2.1", @@ -30758,7 +30758,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 626, "literal": "^1.3.0", @@ -30771,7 +30771,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 627, "literal": "^2.0.0", @@ -30784,7 +30784,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 628, "literal": "^1.0.4", @@ -30797,7 +30797,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 629, "literal": "^1.0.7", @@ -30810,7 +30810,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 630, "literal": "^1.3.0", @@ -30823,7 +30823,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 631, "literal": "^1.2.4", @@ -30836,7 +30836,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 632, "literal": "^1.13.1", @@ -30849,7 +30849,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 633, "literal": "^1.2.1", @@ -30862,7 +30862,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 634, "literal": "^1.0.1", @@ -30875,7 +30875,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 635, "literal": "^1.0.5", @@ -30888,7 +30888,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 636, "literal": "^1.3.0", @@ -30901,7 +30901,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 637, "literal": "^1.2.4", @@ -30914,7 +30914,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 638, "literal": "^1.0.2", @@ -30927,7 +30927,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 639, "literal": "^1.2.0", @@ -30940,7 +30940,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 640, "literal": "^1.22.1", @@ -30953,7 +30953,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 641, "literal": "^1.2.3", @@ -30966,7 +30966,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 642, "literal": "^1.1.4", @@ -30979,7 +30979,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 643, "literal": "^1.0.1", @@ -30992,7 +30992,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 644, "literal": "^1.0.2", @@ -31005,7 +31005,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 645, "literal": "^1.0.0", @@ -31018,7 +31018,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 646, "literal": "^1.2.4", @@ -31031,7 +31031,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 647, "literal": "^1.0.2", @@ -31044,7 +31044,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 648, "literal": "^2.0.1", @@ -31057,7 +31057,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 649, "literal": "^1.0.6", @@ -31070,7 +31070,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 650, "literal": "^1.3.0", @@ -31083,7 +31083,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 651, "literal": "^1.0.1", @@ -31096,7 +31096,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 652, "literal": "^1.0.7", @@ -31109,7 +31109,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 653, "literal": "^1.3.0", @@ -31122,7 +31122,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 654, "literal": "^1.0.1", @@ -31135,7 +31135,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 655, "literal": "^1.0.6", @@ -31148,7 +31148,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 656, "literal": "^1.3.0", @@ -31161,7 +31161,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 657, "literal": "^1.0.1", @@ -31174,7 +31174,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 658, "literal": "^1.0.1", @@ -31187,7 +31187,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 659, "literal": "^1.0.5", @@ -31200,7 +31200,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 660, "literal": "^1.2.1", @@ -31213,7 +31213,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 661, "literal": "^1.22.3", @@ -31226,7 +31226,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 662, "literal": "^1.2.1", @@ -31239,7 +31239,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 663, "literal": "^1.2.3", @@ -31252,7 +31252,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 664, "literal": "^3.0.4", @@ -31265,7 +31265,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 665, "literal": "^1.0.2", @@ -31278,7 +31278,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 666, "literal": "^1.0.5", @@ -31291,7 +31291,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 667, "literal": "^3.0.4", @@ -31304,7 +31304,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 668, "literal": "^1.0.7", @@ -31317,7 +31317,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 669, "literal": "^1.2.1", @@ -31330,7 +31330,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 670, "literal": "^1.23.2", @@ -31343,7 +31343,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 671, "literal": "^1.0.0", @@ -31356,7 +31356,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 672, "literal": "^3.2.7", @@ -31369,7 +31369,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 673, "literal": "^2.1.1", @@ -31382,7 +31382,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 674, "literal": "^3.2.7", @@ -31395,7 +31395,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 675, "literal": "^2.13.0", @@ -31408,7 +31408,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 676, "literal": "^1.22.4", @@ -31421,7 +31421,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 677, "literal": "^2.0.2", @@ -31434,7 +31434,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 678, "literal": "^1.0.2", @@ -31447,7 +31447,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 679, "literal": "^1.2.0", @@ -31460,7 +31460,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 680, "literal": "^1.22.1", @@ -31473,7 +31473,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 681, "literal": "^1.0.0", @@ -31486,7 +31486,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 682, "literal": "^2.0.0", @@ -31499,7 +31499,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 683, "literal": "^1.0.2", @@ -31512,7 +31512,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 684, "literal": "^1.2.0", @@ -31525,7 +31525,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 685, "literal": "^1.22.1", @@ -31538,7 +31538,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 686, "literal": "^1.0.0", @@ -31551,7 +31551,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 687, "literal": "^1.0.7", @@ -31564,7 +31564,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 688, "literal": "^1.2.1", @@ -31577,7 +31577,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 689, "literal": "^1.23.2", @@ -31590,7 +31590,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 690, "literal": "^1.3.0", @@ -31603,7 +31603,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 691, "literal": "^1.0.0", @@ -31616,7 +31616,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 692, "literal": "^1.0.2", @@ -31629,7 +31629,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 693, "literal": "^1.0.7", @@ -31642,7 +31642,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 694, "literal": "^1.2.1", @@ -31655,7 +31655,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 695, "literal": "^1.23.2", @@ -31668,7 +31668,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 696, "literal": "^1.0.0", @@ -31681,7 +31681,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 697, "literal": "^1.2.4", @@ -31694,7 +31694,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 698, "literal": "^1.0.7", @@ -31707,7 +31707,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 699, "literal": "^1.0.0", @@ -31720,7 +31720,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 700, "literal": "^4.2.4", @@ -31733,7 +31733,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 701, "literal": "^2.2.0", @@ -31759,7 +31759,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 703, "literal": "^4.3.4", @@ -31772,7 +31772,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 704, "literal": "6.21.0", @@ -31785,7 +31785,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 705, "literal": "6.21.0", @@ -31798,7 +31798,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 706, "literal": "6.21.0", @@ -31811,7 +31811,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 707, "literal": "6.21.0", @@ -31837,7 +31837,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 709, "literal": "^4.3.4", @@ -31850,7 +31850,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 710, "literal": "^11.1.0", @@ -31863,7 +31863,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 711, "literal": "^7.5.4", @@ -31876,7 +31876,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 712, "literal": "^4.0.3", @@ -31889,7 +31889,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 713, "literal": "9.0.3", @@ -31902,7 +31902,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 714, "literal": "^1.0.1", @@ -31915,7 +31915,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 715, "literal": "6.21.0", @@ -31928,7 +31928,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 716, "literal": "6.21.0", @@ -31941,7 +31941,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 717, "literal": "^3.4.1", @@ -31954,7 +31954,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 718, "literal": "6.21.0", @@ -31980,7 +31980,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 720, "literal": "^2.0.1", @@ -31993,7 +31993,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 721, "literal": "^2.1.0", @@ -32006,7 +32006,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 722, "literal": "^3.0.1", @@ -32019,7 +32019,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 723, "literal": "^3.2.9", @@ -32032,7 +32032,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 724, "literal": "^5.2.0", @@ -32045,7 +32045,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 725, "literal": "^1.4.1", @@ -32058,7 +32058,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 726, "literal": "^3.0.0", @@ -32071,7 +32071,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 727, "literal": "^4.0.0", @@ -32084,7 +32084,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 728, "literal": "6.21.0", @@ -32097,7 +32097,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 729, "literal": "6.21.0", @@ -32110,7 +32110,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 730, "literal": "10.3.10", @@ -32123,7 +32123,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 731, "literal": "^7.23.2", @@ -32136,7 +32136,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 732, "literal": "^5.3.0", @@ -32149,7 +32149,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 733, "literal": "^3.1.7", @@ -32162,7 +32162,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 734, "literal": "^1.3.2", @@ -32175,7 +32175,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 735, "literal": "^0.0.8", @@ -32188,7 +32188,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 736, "literal": "=4.7.0", @@ -32201,7 +32201,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 737, "literal": "^3.2.1", @@ -32214,7 +32214,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 738, "literal": "^1.0.8", @@ -32227,7 +32227,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 739, "literal": "^9.2.2", @@ -32240,7 +32240,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 740, "literal": "^1.0.15", @@ -32253,7 +32253,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 741, "literal": "^2.0.0", @@ -32266,7 +32266,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 742, "literal": "^3.3.5", @@ -32279,7 +32279,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 743, "literal": "^1.0.9", @@ -32292,7 +32292,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 744, "literal": "^3.1.2", @@ -32305,7 +32305,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 745, "literal": "^1.1.7", @@ -32318,7 +32318,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 746, "literal": "^2.0.7", @@ -32344,7 +32344,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 748, "literal": "^1.0.7", @@ -32357,7 +32357,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 749, "literal": "^1.2.1", @@ -32370,7 +32370,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 750, "literal": "^1.0.0", @@ -32383,7 +32383,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 751, "literal": "^0.3.20", @@ -32396,7 +32396,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 752, "literal": "^3.1.6", @@ -32409,7 +32409,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 753, "literal": "^1.3.1", @@ -32422,7 +32422,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 754, "literal": "^4.1.4", @@ -32435,7 +32435,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 755, "literal": "^1.1.6", @@ -32448,7 +32448,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 756, "literal": "^1.0.7", @@ -32461,7 +32461,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 757, "literal": "^1.2.1", @@ -32474,7 +32474,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 758, "literal": "^1.23.3", @@ -32487,7 +32487,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 759, "literal": "^1.3.0", @@ -32500,7 +32500,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 760, "literal": "^2.0.3", @@ -32513,7 +32513,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 761, "literal": "^1.1.2", @@ -32526,7 +32526,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 762, "literal": "^1.2.4", @@ -32539,7 +32539,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 763, "literal": "^1.0.3", @@ -32552,7 +32552,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 764, "literal": "^1.0.2", @@ -32565,7 +32565,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 765, "literal": "^1.0.3", @@ -32578,7 +32578,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 766, "literal": "^1.0.3", @@ -32591,7 +32591,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 767, "literal": "^1.0.7", @@ -32604,7 +32604,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 768, "literal": "^1.1.2", @@ -32617,7 +32617,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 769, "literal": "^1.1.2", @@ -32630,7 +32630,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 770, "literal": "^1.2.1", @@ -32643,7 +32643,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 771, "literal": "^1.2.1", @@ -32656,7 +32656,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 772, "literal": "^1.0.3", @@ -32669,7 +32669,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 773, "literal": "^1.0.4", @@ -32682,7 +32682,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 774, "literal": "^2.0.1", @@ -32695,7 +32695,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 775, "literal": "^1.0.7", @@ -32708,7 +32708,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 776, "literal": "^1.2.1", @@ -32721,7 +32721,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 777, "literal": "^1.23.1", @@ -32734,7 +32734,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 778, "literal": "^1.3.0", @@ -32747,7 +32747,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 779, "literal": "^1.2.4", @@ -32760,7 +32760,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 780, "literal": "^1.0.3", @@ -32773,7 +32773,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 781, "literal": "^1.1.3", @@ -32786,7 +32786,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 782, "literal": "^1.1.5", @@ -32799,7 +32799,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 783, "literal": "^1.0.0", @@ -32812,7 +32812,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 784, "literal": "^2.0.0", @@ -32825,7 +32825,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 785, "literal": "^1.0.5", @@ -32838,7 +32838,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 786, "literal": "^1.0.2", @@ -32851,7 +32851,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 787, "literal": "^1.0.10", @@ -32864,7 +32864,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 788, "literal": "^1.1.4", @@ -32877,7 +32877,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 789, "literal": "^1.0.2", @@ -32890,7 +32890,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 790, "literal": "^2.0.5", @@ -32903,7 +32903,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 791, "literal": "^1.0.2", @@ -32916,7 +32916,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 792, "literal": "^1.0.1", @@ -32929,7 +32929,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 793, "literal": "^1.1.9", @@ -32942,7 +32942,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 794, "literal": "^2.0.3", @@ -32955,7 +32955,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 795, "literal": "^2.0.3", @@ -32968,7 +32968,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 796, "literal": "^2.0.2", @@ -32981,7 +32981,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 797, "literal": "^2.0.3", @@ -32994,7 +32994,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 798, "literal": "^1.0.7", @@ -33007,7 +33007,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 799, "literal": "^1.2.4", @@ -33020,7 +33020,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 800, "literal": "^1.0.0", @@ -33033,7 +33033,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 801, "literal": "^1.0.2", @@ -33046,7 +33046,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 802, "literal": "^1.0.0", @@ -33059,7 +33059,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 803, "literal": "^2.0.3", @@ -33072,7 +33072,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 804, "literal": "^2.0.3", @@ -33085,7 +33085,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 805, "literal": "^0.14.0", @@ -33098,7 +33098,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 806, "literal": "^3.1.8", @@ -33111,7 +33111,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 807, "literal": "^1.2.5", @@ -33124,7 +33124,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 808, "literal": "^1.3.2", @@ -33137,7 +33137,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 809, "literal": "^1.1.2", @@ -33150,7 +33150,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 810, "literal": "^1.1.3", @@ -33163,7 +33163,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 811, "literal": "^2.1.0", @@ -33176,7 +33176,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 812, "literal": "^1.0.19", @@ -33189,7 +33189,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 813, "literal": "^5.3.0", @@ -33202,7 +33202,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 814, "literal": "^2.4.1 || ^3.0.0", @@ -33215,7 +33215,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 815, "literal": "^3.1.2", @@ -33228,7 +33228,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 816, "literal": "^1.1.8", @@ -33241,7 +33241,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 817, "literal": "^2.0.8", @@ -33254,7 +33254,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 818, "literal": "^1.1.4", @@ -33267,7 +33267,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 819, "literal": "^1.2.0", @@ -33280,7 +33280,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 820, "literal": "^15.8.1", @@ -33293,7 +33293,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 821, "literal": "^2.0.0-next.5", @@ -33306,7 +33306,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 822, "literal": "^6.3.1", @@ -33319,7 +33319,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 823, "literal": "^4.0.11", @@ -33345,7 +33345,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 825, "literal": "^1.0.7", @@ -33358,7 +33358,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 826, "literal": "^1.2.1", @@ -33371,7 +33371,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 827, "literal": "^1.23.2", @@ -33384,7 +33384,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 828, "literal": "^1.3.0", @@ -33397,7 +33397,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 829, "literal": "^1.0.0", @@ -33410,7 +33410,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 830, "literal": "^1.2.4", @@ -33423,7 +33423,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 831, "literal": "^1.0.1", @@ -33436,7 +33436,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 832, "literal": "^1.0.3", @@ -33449,7 +33449,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 833, "literal": "^1.0.7", @@ -33462,7 +33462,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 834, "literal": "^1.5.2", @@ -33475,7 +33475,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 835, "literal": "^2.0.2", @@ -33488,7 +33488,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 836, "literal": "^1.0.6", @@ -33501,7 +33501,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 837, "literal": "^2.13.0", @@ -33514,7 +33514,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 838, "literal": "^1.0.7", @@ -33527,7 +33527,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 839, "literal": "^1.0.0", @@ -33540,7 +33540,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 840, "literal": "^1.4.0", @@ -33553,7 +33553,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 841, "literal": "^4.1.1", @@ -33566,7 +33566,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 842, "literal": "^16.13.1", @@ -33579,7 +33579,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 843, "literal": "^1.2.1", @@ -33592,7 +33592,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 844, "literal": "^1.23.2", @@ -33605,7 +33605,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 845, "literal": "^1.0.0", @@ -33618,7 +33618,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 846, "literal": "^1.0.7", @@ -33631,7 +33631,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 847, "literal": "^1.2.1", @@ -33644,7 +33644,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 848, "literal": "^1.23.3", @@ -33657,7 +33657,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 849, "literal": "^1.3.0", @@ -33670,7 +33670,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 850, "literal": "^1.0.2", @@ -33683,7 +33683,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 851, "literal": "^1.0.2", @@ -33696,7 +33696,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 852, "literal": "^1.2.0", @@ -33709,7 +33709,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 853, "literal": "^1.22.1", @@ -33722,7 +33722,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 854, "literal": "^1.0.0", @@ -33735,7 +33735,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 855, "literal": "^1.0.7", @@ -33748,7 +33748,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 856, "literal": "^1.2.1", @@ -33761,7 +33761,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 857, "literal": "^1.23.2", @@ -33774,7 +33774,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 858, "literal": "^1.3.0", @@ -33787,7 +33787,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 859, "literal": "^1.0.0", @@ -33800,7 +33800,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 860, "literal": "^1.0.2", @@ -33813,7 +33813,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 861, "literal": "~8.5.10", @@ -33826,7 +33826,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 862, "literal": "~20.12.8", @@ -33839,7 +33839,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 863, "literal": "*", @@ -33852,7 +33852,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 864, "literal": "^4.21.10", @@ -33865,7 +33865,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 865, "literal": "^1.0.30001538", @@ -33878,7 +33878,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 866, "literal": "^4.3.6", @@ -33891,7 +33891,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 867, "literal": "^0.1.2", @@ -33904,7 +33904,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 868, "literal": "^1.0.0", @@ -33917,7 +33917,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 869, "literal": "^4.2.0", @@ -33943,7 +33943,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 871, "literal": "^1.0.30001587", @@ -33956,7 +33956,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 872, "literal": "^1.4.668", @@ -33969,7 +33969,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 873, "literal": "^2.0.14", @@ -33982,7 +33982,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 874, "literal": "^1.0.13", @@ -33995,7 +33995,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 875, "literal": "^3.1.2", @@ -34008,7 +34008,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 876, "literal": "^1.0.1", @@ -34034,7 +34034,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 878, "literal": "*", @@ -34047,7 +34047,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 879, "literal": "*", @@ -34060,7 +34060,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 880, "literal": "*", @@ -34073,7 +34073,7 @@ exports[`next build works: node 1`] = ` }, { "behavior": { - "normal": true, + "prod": true, }, "id": 881, "literal": "^3.0.2", @@ -43376,7 +43376,7 @@ exports[`next build works: node 1`] = ` "package_id": 374, }, "array-includes": { - "id": 806, + "id": 445, "package_id": 384, }, "array-union": { @@ -43396,7 +43396,7 @@ exports[`next build works: node 1`] = ` "package_id": 382, }, "array.prototype.flatmap": { - "id": 808, + "id": 448, "package_id": 380, }, "array.prototype.toreversed": { @@ -43672,11 +43672,11 @@ exports[`next build works: node 1`] = ` "package_id": 316, }, "es-errors": { - "id": 858, + "id": 690, "package_id": 312, }, "es-iterator-helpers": { - "id": 812, + "id": 740, "package_id": 409, }, "es-object-atoms": { @@ -43688,7 +43688,7 @@ exports[`next build works: node 1`] = ` "package_id": 369, }, "es-shim-unscopables": { - "id": 860, + "id": 692, "package_id": 381, }, "es-to-primitive": { @@ -43724,7 +43724,7 @@ exports[`next build works: node 1`] = ` "package_id": 302, }, "eslint-module-utils": { - "id": 452, + "id": 438, "package_id": 376, }, "eslint-plugin-import": { @@ -43768,7 +43768,7 @@ exports[`next build works: node 1`] = ` "package_id": 279, }, "estraverse": { - "id": 432, + "id": 415, "package_id": 166, }, "esutils": { @@ -43852,7 +43852,7 @@ exports[`next build works: node 1`] = ` "package_id": 51, }, "function-bind": { - "id": 761, + "id": 55, "package_id": 21, }, "function.prototype.name": { @@ -44016,7 +44016,7 @@ exports[`next build works: node 1`] = ` "package_id": 329, }, "is-core-module": { - "id": 454, + "id": 675, "package_id": 19, }, "is-data-view": { @@ -44160,7 +44160,7 @@ exports[`next build works: node 1`] = ` "package_id": 174, }, "jsx-ast-utils": { - "id": 814, + "id": 742, "package_id": 408, }, "keyv": { @@ -44284,11 +44284,11 @@ exports[`next build works: node 1`] = ` "package_id": 355, }, "object.entries": { - "id": 816, + "id": 745, "package_id": 405, }, "object.fromentries": { - "id": 817, + "id": 457, "package_id": 375, }, "object.groupby": { @@ -44300,7 +44300,7 @@ exports[`next build works: node 1`] = ` "package_id": 434, }, "object.values": { - "id": 819, + "id": 459, "package_id": 310, }, "once": { @@ -44528,7 +44528,7 @@ exports[`next build works: node 1`] = ` "package_id": 112, }, "semver": { - "id": 822, + "id": 460, "package_id": 309, }, "set-function-length": { @@ -44875,18 +44875,14 @@ exports[`next build works: node 1`] = ` }, { "dependencies": { - "doctrine": { - "id": 811, - "package_id": 379, - }, - "resolve": { - "id": 821, - "package_id": 431, + "debug": { + "id": 674, + "package_id": 377, }, }, "depth": 1, "id": 4, - "path": "node_modules/eslint-plugin-react/node_modules", + "path": "node_modules/eslint-import-resolver-node/node_modules", }, { "dependencies": { @@ -44905,14 +44901,18 @@ exports[`next build works: node 1`] = ` }, { "dependencies": { - "debug": { - "id": 674, - "package_id": 377, + "doctrine": { + "id": 811, + "package_id": 379, + }, + "resolve": { + "id": 821, + "package_id": 431, }, }, "depth": 1, "id": 6, - "path": "node_modules/eslint-import-resolver-node/node_modules", + "path": "node_modules/eslint-plugin-react/node_modules", }, { "dependencies": { @@ -44962,17 +44962,6 @@ exports[`next build works: node 1`] = ` "id": 10, "path": "node_modules/postcss-load-config/node_modules", }, - { - "dependencies": { - "debug": { - "id": 672, - "package_id": 377, - }, - }, - "depth": 1, - "id": 11, - "path": "node_modules/eslint-module-utils/node_modules", - }, { "dependencies": { "minimatch": { @@ -44981,7 +44970,7 @@ exports[`next build works: node 1`] = ` }, }, "depth": 1, - "id": 12, + "id": 11, "path": "node_modules/glob/node_modules", }, { @@ -44996,9 +44985,20 @@ exports[`next build works: node 1`] = ` }, }, "depth": 1, - "id": 13, + "id": 12, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules", }, + { + "dependencies": { + "debug": { + "id": 672, + "package_id": 377, + }, + }, + "depth": 1, + "id": 13, + "path": "node_modules/eslint-module-utils/node_modules", + }, { "dependencies": { "lru-cache": { @@ -45043,17 +45043,6 @@ exports[`next build works: node 1`] = ` "id": 17, "path": "node_modules/path-scurry/node_modules", }, - { - "dependencies": { - "lru-cache": { - "id": 165, - "package_id": 117, - }, - }, - "depth": 2, - "id": 18, - "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", - }, { "dependencies": { "brace-expansion": { @@ -45062,9 +45051,20 @@ exports[`next build works: node 1`] = ` }, }, "depth": 2, - "id": 19, + "id": 18, "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules", }, + { + "dependencies": { + "lru-cache": { + "id": 165, + "package_id": 117, + }, + }, + "depth": 2, + "id": 19, + "path": "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules", + }, { "dependencies": { "@types/node": { diff --git a/test/integration/next-pages/test/dev-server-puppeteer.ts b/test/integration/next-pages/test/dev-server-puppeteer.ts index b529afbf68..06bf56b425 100644 --- a/test/integration/next-pages/test/dev-server-puppeteer.ts +++ b/test/integration/next-pages/test/dev-server-puppeteer.ts @@ -1,8 +1,9 @@ import assert from "assert"; import { copyFileSync } from "fs"; import { join } from "path"; -import { ConsoleMessage, Page, launch } from "puppeteer"; - +import type { ConsoleMessage, Page } from "puppeteer"; +import { launch } from "puppeteer"; +import { which } from "bun"; const root = join(import.meta.dir, "../"); copyFileSync(join(root, "src/Counter1.txt"), join(root, "src/Counter.tsx")); @@ -12,28 +13,38 @@ if (process.argv.length > 2) { url = process.argv[2]; } +const browserPath = which("chromium-browser") || which("chromium") || which("chrome") || undefined; +if (!browserPath) { + console.warn("Since a Chromium browser was not found, it will be downloaded by Puppeteer."); +} + const b = await launch({ - // While puppeteer is migrating to their new headless: `true` mode, - // this causes strange issues on macOS in the cloud (AWS and MacStadium). - // - // There is a GitHub issue, but the discussion is unhelpful: - // https://github.com/puppeteer/puppeteer/issues/10153 - // - // Fixes: 'TargetCloseError: Protocol error (Target.setAutoAttach): Target closed' - headless: "shell", + // On macOS, there are issues using the new headless mode. + // "TargetCloseError: Protocol error (Target.setAutoAttach): Target closed" + headless: process.platform === "darwin" ? "shell" : true, + // Inherit the stdout and stderr of the browser process. dumpio: true, + // Prefer to use a pipe to connect to the browser, instead of a WebSocket. pipe: true, + // Disable timeouts. + timeout: 0, + protocolTimeout: 0, + // Specify that chrome should be used, for consistent test results. + // If a browser path is not found, it will be downloaded. + browser: "chrome", + executablePath: browserPath, args: [ - // Fixes: 'dock_plist is not an NSDictionary' + // On Linux, there are issues with the sandbox, so disable it. + // On macOS, this fixes: "dock_plist is not an NSDictionary" "--no-sandbox", - "--single-process", "--disable-setuid-sandbox", + + // On Docker, the default /dev/shm is too small for Chrome, which causes + // crashes when rendering large pages, so disable it. "--disable-dev-shm-usage", - // Fixes: 'Navigating frame was detached' + + // Fixes: "Navigating frame was detached" "--disable-features=site-per-process", - // Uncomment if you want debug logs from Chromium: - // "--enable-logging=stderr", - // "--v=1", ], }); diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts new file mode 100644 index 0000000000..4129b9dd2e --- /dev/null +++ b/test/internal/bindgen.test.ts @@ -0,0 +1,70 @@ +import { bindgen } from "bun:internal-for-testing"; + +it("bindgen add example", () => { + // Simple cases + expect(bindgen.add(5, 3)).toBe(8); + expect(bindgen.add(-2, 7)).toBe(5); + expect(bindgen.add(0, 0)).toBe(0); + // https://tc39.es/ecma262/multipage/bigint-object.html#sec-tonumber + // 2. If argument is either a Symbol or a BigInt, throw a TypeError exception. + expect(() => bindgen.add(1n, 0)).toThrow("Conversion from 'BigInt' to 'number' is not allowed"); + expect(() => bindgen.add(Symbol("1"), 0)).toThrow("Cannot convert a symbol to a number"); + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber + // 3. If argument is null or false, return +0. + expect(bindgen.add(null, "32")).toBe(32); + expect(bindgen.add(false, "32")).toBe(32); + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber + // 3. If argument is undefined, return NaN. + // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + expect(bindgen.add(undefined, "32")).toBe(32); + expect(bindgen.add(NaN, "32")).toBe(32); + expect(bindgen.add(Infinity, "32")).toBe(32); + expect(bindgen.add(-Infinity, "32")).toBe(32); + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber + // 5. If argument is true, return 1. + expect(bindgen.add(true, "32")).toBe(33); + // https://tc39.es/ecma262/multipage/bigint-object.html#sec-tonumber + // 6. If argument is a String, return StringToNumber(argument). + expect(bindgen.add("1", "1")).toBe(2); + // 8. Let primValue be ? ToPrimitive(argument, number). + // 10. Return ? ToNumber(primValue). + expect(bindgen.add({ [Symbol.toPrimitive]: () => "1" }, "1")).toBe(2); + + expect(bindgen.add(2147483647.9, 0)).toBe(2147483647); + expect(bindgen.add(2147483647.1, 0)).toBe(2147483647); + + // Out of range wrapping behaviors. By adding `0`, this acts as an identity function. + // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + expect(bindgen.add(2147483648, 0)).toBe(-2147483648); + expect(bindgen.add(5555555555, 0)).toBe(1260588259); + expect(bindgen.add(-5555555555, 0)).toBe(-1260588259); + expect(bindgen.add(55555555555, 0)).toBe(-279019293); + expect(bindgen.add(-55555555555, 0)).toBe(279019293); + expect(bindgen.add(555555555555, 0)).toBe(1504774371); + expect(bindgen.add(-555555555555, 0)).toBe(-1504774371); + expect(bindgen.add(5555555555555, 0)).toBe(-2132125469); + expect(bindgen.add(-5555555555555, 0)).toBe(2132125469); + + // Test Zig error handling + expect(() => bindgen.add(2147483647, 1)).toThrow("Integer overflow while adding"); +}); + +it("optional arguments / default arguments", () => { + expect(bindgen.requiredAndOptionalArg(false)).toBe(123498); + expect(bindgen.requiredAndOptionalArg(false, 10)).toBe(52); + expect(bindgen.requiredAndOptionalArg(true, 10)).toBe(-52); + expect(bindgen.requiredAndOptionalArg(1, 10, 5)).toBe(-15); + expect(bindgen.requiredAndOptionalArg("coerce to true", 10, 5)).toBe(-15); + expect(bindgen.requiredAndOptionalArg("", 10, 5)).toBe(15); + expect(bindgen.requiredAndOptionalArg(true, 10, 5, 2)).toBe(-30); + expect(bindgen.requiredAndOptionalArg(true, null, 5, 2)).toBe(123463); +}); + +it("custom enforceRange boundaries", () => { + expect(bindgen.requiredAndOptionalArg(false, 0, 5)).toBe(5); + expect(() => bindgen.requiredAndOptionalArg(false, 0, -1)).toThrow("Value -1 is outside the range [0, 100]"); + expect(() => bindgen.requiredAndOptionalArg(false, 0, 101)).toThrow("Value 101 is outside the range [0, 100]"); + expect(bindgen.requiredAndOptionalArg(false, 0, 100)).toBe(100); + expect(bindgen.requiredAndOptionalArg(false, 0, 0)).toBe(0); +}); diff --git a/test/internal/fifo.test.ts b/test/internal/fifo.test.ts new file mode 100644 index 0000000000..9efd777dc7 --- /dev/null +++ b/test/internal/fifo.test.ts @@ -0,0 +1,245 @@ +import { Dequeue } from "bun:internal-for-testing"; +import { describe, expect, test, it, beforeAll, beforeEach } from "bun:test"; + +/** + * Implements the same API as {@link Dequeue} but uses a simple list as the + * backing store. + * + * Used to check expected behavior. + */ +class DequeueList { + private _list: T[]; + + constructor() { + this._list = []; + } + + size(): number { + return this._list.length; + } + + isEmpty(): boolean { + return this.size() == 0; + } + + isNotEmpty(): boolean { + return this.size() > 0; + } + + shift(): T | undefined { + return this._list.shift(); + } + + peek(): T | undefined { + return this._list[0]; + } + + push(item: T): void { + this._list.push(item); + } + + toArray(fullCopy: boolean): T[] { + return fullCopy ? this._list.slice() : this._list; + } + + clear(): void { + this._list = []; + } +} + +describe("Given an empty queue", () => { + let queue: Dequeue; + + beforeEach(() => { + queue = new Dequeue(); + }); + + it("has a size of 0", () => { + expect(queue.size()).toBe(0); + }); + + it("is empty", () => { + expect(queue.isEmpty()).toBe(true); + expect(queue.isNotEmpty()).toBe(false); + }); + + it("shift() returns undefined", () => { + expect(queue.shift()).toBe(undefined); + expect(queue.size()).toBe(0); + }); + + it("has an initial capacity of 4", () => { + expect(queue._list.length).toBe(4); + expect(queue._capacityMask).toBe(3); + }); + + it("toArray() returns an empty array", () => { + expect(queue.toArray()).toEqual([]); + }); + + describe("When an element is pushed", () => { + beforeEach(() => { + queue.push(42); + }); + + it("has a size of 1", () => { + expect(queue.size()).toBe(1); + }); + + it("can be peeked without removing it", () => { + expect(queue.peek()).toBe(42); + expect(queue.size()).toBe(1); + }); + + it("is not empty", () => { + expect(queue.isEmpty()).toBe(false); + expect(queue.isNotEmpty()).toBe(true); + }); + + it("can be shifted out", () => { + const el = queue.shift(); + expect(el).toBe(42); + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBe(true); + }); + }); // +}); // + +describe("grow boundary conditions", () => { + describe.each([3, 4, 16])("when %d items are pushed", n => { + let queue: Dequeue; + + beforeEach(() => { + queue = new Dequeue(); + for (let i = 0; i < n; i++) { + queue.push(i); + } + }); + + it(`has a size of ${n}`, () => { + expect(queue.size()).toBe(n); + }); + + it("is not empty", () => { + expect(queue.isEmpty()).toBe(false); + expect(queue.isNotEmpty()).toBe(true); + }); + + it(`can shift() ${n} times`, () => { + for (let i = 0; i < n; i++) { + expect(queue.peek()).toBe(i); + expect(queue.shift()).toBe(i); + } + expect(queue.size()).toBe(0); + expect(queue.shift()).toBe(undefined); + }); + + it("toArray() returns [0..n-1]", () => { + // same as repeated push() but only allocates once + var expected = new Array(n); + for (let i = 0; i < n; i++) { + expected[i] = i; + } + expect(queue.toArray()).toEqual(expected); + }); + }); +}); // + +describe("adding and removing items", () => { + let queue: Dequeue; + let expected: DequeueList; + + describe("when 10k items are pushed", () => { + beforeEach(() => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 10_000; i++) { + queue.push(i); + expected.push(i); + } + }); + + it("has a size of 10000", () => { + expect(queue.size()).toBe(10_000); + expect(expected.size()).toBe(10_000); + }); + + describe("when 10 items are shifted", () => { + beforeEach(() => { + for (let i = 0; i < 10; i++) { + expect(queue.shift()).toBe(expected.shift()); + } + }); + + it("has a size of 9990", () => { + expect(queue.size()).toBe(9990); + expect(expected.size()).toBe(9990); + }); + }); + }); // + + describe("when 1k items are pushed, then removed", () => { + beforeEach(() => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 1_000; i++) { + queue.push(i); + expected.push(i); + } + expect(queue.size()).toBe(1_000); + + while (queue.isNotEmpty()) { + expect(queue.shift()).toBe(expected.shift()); + } + }); + + it("is now empty", () => { + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBeTrue(); + expect(queue.isNotEmpty()).toBeFalse(); + }); + + it("when new items are added, the backing list is resized", () => { + for (let i = 0; i < 10_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + }); + }); // + + it("pushing and shifting a lot of items affects the size and backing list correctly", () => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 15_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + + // shift() shrinks the backing array when tail > 10,000 and the list is + // shrunk too far (tail <= list.length >>> 2) + for (let i = 0; i < 10_000; i++) { + expect(queue.shift()).toBe(expected.shift()); + expect(queue.size()).toBe(expected.size()); + } + + for (let i = 0; i < 5_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + }); // +}); // diff --git a/test/internal/highlighter.test.ts b/test/internal/highlighter.test.ts index e45e73ca9f..c1af283f1d 100644 --- a/test/internal/highlighter.test.ts +++ b/test/internal/highlighter.test.ts @@ -1,4 +1,4 @@ -import { quickAndDirtyJavaScriptSyntaxHighlighter as highlighter } from "bun:internal-for-testing"; +import { highlightJavaScript as highlighter } from "bun:internal-for-testing"; import { expect, test } from "bun:test"; test("highlighter", () => { diff --git a/test/js/bun/bun-object/deep-match.spec.ts b/test/js/bun/bun-object/deep-match.spec.ts new file mode 100644 index 0000000000..1a70301937 --- /dev/null +++ b/test/js/bun/bun-object/deep-match.spec.ts @@ -0,0 +1,222 @@ +type TestCase = [a: unknown, b: unknown]; + +// @ts-ignore +if (typeof Bun === "undefined") + [ + // @ts-ignore + (globalThis.Bun = { + deepMatch(a, b) { + try { + expect(b).toMatchObject(a); + return true; + } catch (e) { + if (e instanceof TypeError) throw e; + return false; + } + }, + }), + ]; +describe("Bun.deepMatch", () => { + it.each([ + // force line break + {}, + { a: 1 }, + [[1, 2, 3]], + ] as TestCase[])("returns `true` for referentially equal objects (%p)", obj => { + expect(Bun.deepMatch(obj, obj)).toBe(true); + // expect(Bun.deepMatch(obj, obj)).toBe(true); + }); + + // prettier-ignore + it.each([ + // POJOs + [{}, {}], + [{ a: 1 }, { a: 1 }], + [{ a: Symbol.for("foo") }, { a: Symbol.for("foo") }], + [ + { a: { b: "foo" }, c: true }, + { a: { b: "foo" }, c: true }, + ], + [ + { a: [{ b: [] }, "foo", 0, null] }, + { a: [{ b: [] }, "foo", 0, null] } + ], + [{ }, { a: undefined }], // NOTE: `b` may be a superset of `a`, but not vice-versa + [{ a: { b: "foo" } }, { a: { b: "foo", c: undefined } }], + [{ a: { b: "foo" } }, { a: { b: "foo", c: 1 } }], + + // Arrays + [[], []], + [ + [1, 2, 3], + [1, 2, 3], + ], + [ + [{}, "foo", 1], + [{}, "foo", 1], + ], + + // Maps + [new Map(), new Map()], + [ + new Map([ [1, 2], [2, 3], [3, 4] ]), + new Map([ [1, 2], [2, 3], [3, 4] ]), + ], + [ + new Map([ ["foo", 1] ]), + new Map([ ["foo", 1] ]), + ], + + // Sets + [new Set(), new Set()], + [ + new Set([1, 2, 3]), + new Set([1, 2, 3]), + ], + [ + new Set(["a", "b", "c"]), + new Set(["a", "b", "c"]), + ], + ])("Bun.deepMatch(%p, %p) === true", (a, b) => { + expect(Bun.deepMatch(a, b)).toBe(true); + }); + + // prettier-ignore + it.each([ + // POJOs + [{ a: undefined }, { }], // NOTE: `a` may not be a superset of `b` + [{ a: 1 }, { a: 2 }], + [{ a: 1 }, { b: 1 }], + [{ a: null }, { a: undefined }], + [{ a: { b: "foo" } }, { a: { b: "bar"} }], + [{ a: { b: "foo", c: 1 } }, { a: { b: "foo" } }], + [{ a: Symbol.for("a") }, { a: Symbol.for("b") }], + [{ a: Symbol("a") }, { a: Symbol("a") }], // new symbols are never equal + + // Arrays + [[1, 2, 3], [1, 2]], + [[1, 2, 3], [1, 2, 4]], + [[null], [undefined]], + [[], [undefined]], + [["a", "b", "c"], ["a", "b", "d"]], + + // Maps + // FIXME: I assume this is incorrect but I need confirmation on expected behavior. + // [ + // new Map([ [1, 2], [2, 3], [3, 4] ]), + // new Map([ [1, 2], [2, 3] ]), + // ], + // [ + // new Map([ [1, 2], [2, 3], [3, 4] ]), + // new Map([ [1, 2], [2, 3], [3, 4], [4, 5] ]), + // ], + // [ + // new Map([ [1, 2], [2, 3], [3, 4], [4, 5] ]), + // new Map([ [1, 2], [2, 3], [3, 4] ]), + // ], + + // Sets + // FIXME: I assume this is incorrect but I need confirmation on expected behavior. + // [ + // new Set([1, 2, 3]), + // new Set([4, 5, 6]), + // ], + // [ + // new Set([1, 2, 3]), + // new Set([1, 2]), + // ], + // [ + // new Set([1, 2]), + // new Set([1, 2, 3]), + // ], + // [ + // new Set(["a", "b", "c"]), + // new Set(["a", "b", "d"]), + // ], + ])("Bun.deepMatch(%p, %p) === false", (a, b) => { + expect(Bun.deepMatch(a, b)).toBe(false); + }); + + it("When comparing same-shape objects with different constructors, returns true", () => { + class Foo {} + class Bar {} + + expect(Bun.deepMatch(new Foo(), new Bar())).toBe(true); + }); + + describe("When provided objects with circular references", () => { + let foo: Record; + + const makeCircular = () => { + let foo = { bar: undefined as any }; + let bar = { foo: undefined as any }; + foo.bar = bar; + bar.foo = foo; + return foo; + }; + + beforeEach(() => { + foo = makeCircular(); + }); + + // a, b are ref equal + it("when a and b are _exactly_ the same object, returns true", () => { + expect(Bun.deepMatch(foo, foo)).toBe(true); + }); + + // a, b are not ref equal but their properties are + it("When a and b are different objects whose properties point to the same object, returns true", () => { + const foo2 = { ...foo }; // pointer to bar is copied. + expect(Bun.deepMatch(foo, foo2)).toBe(true); + }); + + // a, b are structurally equal but share no pointers + it.skip("when a and b are structurally equal but share no pointers, returns true", () => { + const bar = makeCircular(); + expect(Bun.deepMatch(foo, bar)).toBe(true); + }); + + // a, b are neither ref or structurally equal + it("when a and b are different, returns false", () => { + const bar = { bar: undefined } as any; + bar.bar = bar; + expect(Bun.deepMatch(foo, bar)).toBe(false); + }); + }); + + describe("array inputs", () => { + it.each([ + // line break + [[1, 2, 3], [1, 2, 3], true], + ] as [any[], any[], boolean][])("Bun.deepMatch(%p, %p) === %p", (a, b, expected) => { + expect(Bun.deepMatch(a, b)).toBe(expected); + }); + }); + + it("does not work on functions", () => { + function foo() {} + function bar() {} + function baz(a) { + return a; + } + expect(Bun.deepMatch(foo, foo)).toBe(true); + expect(Bun.deepMatch(foo, bar)).toBe(true); + // FIXME + // expect(Bun.deepMatch(foo, baz)).toBe(false); + }); + + describe("Invalid arguments", () => { + it.each([ + [null, null], + [undefined, undefined], + [1, 1], + [true, true], + [true, false], + ["a", "a"], + [Symbol.for("a"), Symbol.for("a")], + [Symbol("a"), Symbol("a")], + ])("throws a TypeError for primitives", (a, b) => { + expect(() => Bun.deepMatch(a, b)).toThrow(TypeError); + }); + }); +}); diff --git a/test/js/bun/css/css-fuzz.test.ts b/test/js/bun/css/css-fuzz.test.ts index 6a46bfc16b..6a144bc607 100644 --- a/test/js/bun/css/css-fuzz.test.ts +++ b/test/js/bun/css/css-fuzz.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; -import { isCI } from "harness"; +import { isCI, isDebug } from "harness"; interface InvalidFuzzOptions { maxLength: number; @@ -7,6 +7,9 @@ interface InvalidFuzzOptions { iterations: number; } +const shutup = process.env.CSS_FUZZ_SHUTUP === "1"; +const log = shutup ? () => {} : console.log; + // Collection of invalid CSS generation strategies const invalidGenerators = { // Syntax errors @@ -62,7 +65,7 @@ const invalidGenerators = { // Memory and resource stress memory: { - deepNesting: (depth: number = 1000) => { + deepNesting: (depth: number = 300) => { let css = ""; for (let i = 0; i < depth; i++) { css += "@media screen {"; @@ -111,107 +114,105 @@ function corruptCSS(css: string): string { // TODO: if (!isCI) { // Main fuzzing test suite for invalid inputs - test.each([ - ["syntax", 1000], - ["structure", 1000], - ["encoding", 500], - ["memory", 100], - ])("CSS Parser Invalid Input Fuzzing - %s (%d iterations)", async (strategy, iterations) => { - const options: InvalidFuzzOptions = { - maxLength: 10000, - strategy: strategy as any, - iterations, - }; + test.each( + [["syntax", 1000], ["structure", 1000], ["encoding", 500], !isDebug ? ["memory", 100] : []].filter( + xs => xs.length > 0, + ), + )( + "CSS Parser Invalid Input Fuzzing - %s (%d iterations)", + async (strategy, iterations) => { + const options: InvalidFuzzOptions = { + maxLength: 10000, + strategy: strategy as any, + iterations, + }; - let crashCount = 0; - let errorCount = 0; - const startTime = performance.now(); + let crashCount = 0; + let errorCount = 0; + const startTime = performance.now(); - for (let i = 0; i < options.iterations; i++) { - let invalidCSS = ""; + for (let i = 0; i < options.iterations; i++) { + let invalidCSS = ""; - switch (strategy) { - case "syntax": - invalidCSS = - invalidGenerators.syntax[ - Object.keys(invalidGenerators.syntax)[ - Math.floor(Math.random() * Object.keys(invalidGenerators.syntax).length) - ] - ]()[Math.floor(Math.random() * 5)]; - break; + switch (strategy) { + case "syntax": + invalidCSS = + invalidGenerators.syntax[ + Object.keys(invalidGenerators.syntax)[ + Math.floor(Math.random() * Object.keys(invalidGenerators.syntax).length) + ] + ]()[Math.floor(Math.random() * 5)]; + break; - case "structure": - invalidCSS = - invalidGenerators.structure[ - Object.keys(invalidGenerators.structure)[ - Math.floor(Math.random() * Object.keys(invalidGenerators.structure).length) - ] - ]()[Math.floor(Math.random() * 3)]; - break; + case "structure": + invalidCSS = + invalidGenerators.structure[ + Object.keys(invalidGenerators.structure)[ + Math.floor(Math.random() * Object.keys(invalidGenerators.structure).length) + ] + ]()[Math.floor(Math.random() * 3)]; + break; - case "encoding": - invalidCSS = - invalidGenerators.encoding[ - Object.keys(invalidGenerators.encoding)[ - Math.floor(Math.random() * Object.keys(invalidGenerators.encoding).length) - ] - ]()[0]; - break; + case "encoding": + invalidCSS = + invalidGenerators.encoding[ + Object.keys(invalidGenerators.encoding)[ + Math.floor(Math.random() * Object.keys(invalidGenerators.encoding).length) + ] + ]()[0]; + break; - case "memory": - const memoryFuncs = Object.keys(invalidGenerators.memory); - const selectedFunc = memoryFuncs[Math.floor(Math.random() * memoryFuncs.length)]; - invalidCSS = invalidGenerators.memory[selectedFunc](1000); - break; - } - - // Further corrupt the CSS randomly - if (Math.random() < 0.3) { - invalidCSS = corruptCSS(invalidCSS); - } - - console.log("--- CSS Fuzz ---"); - invalidCSS = invalidCSS + ""; - console.log(JSON.stringify(invalidCSS, null, 2)); - await Bun.write("invalid.css", invalidCSS); - - try { - const result = await Bun.build({ - entrypoints: ["invalid.css"], - experimentalCss: true, - }); - - if (result.logs.length > 0) { - throw new AggregateError("CSS parser returned logs", result.logs); + case "memory": + const memoryFuncs = Object.keys(invalidGenerators.memory); + const selectedFunc = memoryFuncs[Math.floor(Math.random() * memoryFuncs.length)]; + invalidCSS = invalidGenerators.memory[selectedFunc](); + break; } - // We expect the parser to either throw an error or return a valid result - // If it returns undefined/null, that's a potential issue - if (result === undefined || result === null) { - crashCount++; - console.error(`Parser returned ${result} for input:\n${invalidCSS.slice(0, 100)}...`); + // Further corrupt the CSS randomly + if (Math.random() < 0.3) { + invalidCSS = corruptCSS(invalidCSS); } - } catch (error) { - // Expected behavior for invalid CSS - errorCount++; - // Check for specific error types we want to track - if (error instanceof RangeError || error instanceof TypeError) { - console.warn(`Unexpected error type: ${error.constructor.name} for input:\n${invalidCSS.slice(0, 100)}...`); + log("--- CSS Fuzz ---"); + invalidCSS = invalidCSS + ""; + log(JSON.stringify(invalidCSS, null, 2)); + await Bun.write("invalid.css", invalidCSS); + + try { + const result = await Bun.build({ + entrypoints: ["invalid.css"], + experimentalCss: true, + throw: true, + }); + + // We expect the parser to either throw an error or return a valid result + // If it returns undefined/null, that's a potential issue + if (result === undefined || result === null) { + crashCount++; + console.error(`Parser returned ${result} for input:\n${invalidCSS.slice(0, 100)}...`); + } + } catch (error) { + // Expected behavior for invalid CSS + errorCount++; + + // Check for specific error types we want to track + if (error instanceof RangeError || error instanceof TypeError) { + console.warn(`Unexpected error type: ${error.constructor.name} for input:\n${invalidCSS.slice(0, 100)}...`); + } + } + + // Memory check every 100 iterations + if (i % 100 === 0) { + const heapUsed = process.memoryUsage().heapUsed / 1024 / 1024; + expect(heapUsed).toBeLessThan(500); // Alert if memory usage exceeds 500MB } } - // Memory check every 100 iterations - if (i % 100 === 0) { - const heapUsed = process.memoryUsage().heapUsed / 1024 / 1024; - expect(heapUsed).toBeLessThan(500); // Alert if memory usage exceeds 500MB - } - } + const endTime = performance.now(); + const duration = endTime - startTime; - const endTime = performance.now(); - const duration = endTime - startTime; - - console.log(` + console.log(` Strategy: ${strategy} Total iterations: ${iterations} Crashes: ${crashCount} @@ -220,10 +221,12 @@ if (!isCI) { Average time per test: ${(duration / iterations).toFixed(2)}ms `); - // We expect some errors for invalid input, but no crashes - expect(crashCount).toBe(0); - expect(errorCount).toBeGreaterThan(0); - }); + // We expect some errors for invalid input, but no crashes + expect(crashCount).toBe(0); + expect(errorCount).toBeGreaterThan(0); + }, + 10 * 1000, + ); // Additional test for mixed valid/invalid input test("CSS Parser Mixed Input Fuzzing", async () => { diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 842160c315..5c85ae7b07 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -2,12 +2,51 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { describe, expect, test } from "bun:test"; +import { describe, test } from "bun:test"; import "harness"; -import path from "path"; -import { attrTest, cssTest, indoc, indoc, minify_test, minifyTest, prefix_test } from "./util"; +import { attrTest, cssTest, indoc, minify_test, minifyTest, prefix_test } from "./util"; describe("css tests", () => { + describe("pseudo-class edge case", () => { + cssTest( + indoc`[type="file"]::file-selector-button:-moz-any() { + --pico-background-color: var(--pico-primary-hover-background); + --pico-border-color: var(--pico-primary-hover-border); + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 #0000); + --pico-color: var(--pico-primary-inverse); + }`, + indoc`[type="file"]::-webkit-file-upload-button:-webkit-any() { + --pico-background-color: var(--pico-primary-hover-background); + --pico-border-color: var(--pico-primary-hover-border); + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 #0000); + --pico-color: var(--pico-primary-inverse); + } + [type="file"]::file-selector-button:is() { + --pico-background-color: var(--pico-primary-hover-background); + --pico-border-color: var(--pico-primary-hover-border); + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 #0000); + --pico-color: var(--pico-primary-inverse); + }`, + { + chrome: 80 << 16, + edge: 80 << 16, + firefox: 78 << 16, + safari: 14 << 16, + opera: 67 << 16, + }, + ); + }); + + test("calc edge case", () => { + minifyTest( + // Problem: the value is being printed as Infinity in our restrict_prec thing but the internal thing actually wants it as 3.40282e38px + `.rounded-full { + border-radius: calc(infinity * 1px); + width: calc(infinity * -1px); +}`, + indoc`.rounded-full{border-radius:1e999px;width:-1e999px}`, + ); + }); describe("border_spacing", () => { minifyTest( ` @@ -3254,4 +3293,272 @@ describe("css tests", () => { }, ); }); + + describe("linear-gradient", () => { + minifyTest(".foo { background: linear-gradient(yellow, blue) }", ".foo{background:linear-gradient(#ff0,#00f)}"); + minifyTest( + ".foo { background: linear-gradient(to bottom, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(180deg, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(0.5turn, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow 10%, blue 20%) }", + ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue, yellow); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }", + ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue 10px, yellow 20px); }", + ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}", + ); + minifyTest( + ".foo { background: linear-gradient(135deg, yellow, blue); }", + ".foo{background:linear-gradient(135deg,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, blue 20%, #0f0); }", + ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top right, red, white, blue) }", + ".foo{background:linear-gradient(to top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, blue calc(10% * 2), #0f0); }", + ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 20%, blue); }", + ".foo{background:linear-gradient(#ff0,20%,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 50%, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 20px, blue); }", + ".foo{background:linear-gradient(#ff0,20px,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 50px, blue); }", + ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 50px, blue); }", + ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, red 30% 40%, blue); }", + ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, red 30%, red 40%, blue); }", + ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ); + minifyTest(".foo { background: linear-gradient(0, yellow, blue); }", ".foo{background:linear-gradient(#00f,#ff0)}"); + minifyTest( + ".foo { background: -webkit-linear-gradient(yellow, blue) }", + ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-linear-gradient(top right, red, white, blue) }", + ".foo{background:-webkit-linear-gradient(top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: -moz-linear-gradient(yellow, blue) }", + ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-linear-gradient(top right, red, white, blue) }", + ".foo{background:-moz-linear-gradient(top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: -o-linear-gradient(yellow, blue) }", + ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-linear-gradient(top right, red, white, blue) }", + ".foo{background:-o-linear-gradient(top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), to(yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),to(#ff0))}", + ); + minifyTest( + ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), color-stop(50%, red), to(yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}", + ); + minifyTest( + ".foo { background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, blue), color-stop(50%, red), color-stop(100%, yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}", + ); + minifyTest( + ".foo { background: repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest( + ".foo { background: -webkit-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-webkit-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest( + ".foo { background: -moz-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-moz-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest( + ".foo { background: -o-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-o-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest(".foo { background: radial-gradient(yellow, blue) }", ".foo{background:radial-gradient(#ff0,#00f)}"); + minifyTest( + ".foo { background: radial-gradient(at top left, yellow, blue) }", + ".foo{background:radial-gradient(at 0 0,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(5em circle at top left, yellow, blue) }", + ".foo{background:radial-gradient(5em at 0 0,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(circle at 100%, #333, #333 50%, #eee 75%, #333 75%) }", + ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-corner circle at 100% 50%, #333, #333 50%, #eee 75%, #333 75%) }", + ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-corner circle at 50% 50%, #333, #333 50%, #eee 75%, #333 75%) }", + ".foo{background:radial-gradient(circle,#333,#333 50%,#eee 75%,#333 75%)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse at top, #e66465, transparent) }", + ".foo{background:radial-gradient(at top,#e66465,#0000)}", + ); + minifyTest( + ".foo { background: radial-gradient(20px, yellow, blue) }", + ".foo{background:radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(20px 40px, yellow, blue) }", + ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse 20px 40px, yellow, blue) }", + ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse calc(20px + 10px) 40px, yellow, blue) }", + ".foo{background:radial-gradient(30px 40px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(circle farthest-side, yellow, blue) }", + ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-side circle, yellow, blue) }", + ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse farthest-side, yellow, blue) }", + ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-side ellipse, yellow, blue) }", + ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-radial-gradient(yellow, blue) }", + ".foo{background:-webkit-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-radial-gradient(yellow, blue) }", + ".foo{background:-moz-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-radial-gradient(yellow, blue) }", + ".foo{background:-o-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-webkit-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-moz-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-o-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-gradient(radial, center center, 0, center center, 100, from(blue), to(yellow)) }", + ".foo{background:-webkit-gradient(radial,50% 50%,0,50% 50%,100,from(#00f),to(#ff0))}", + ); + minifyTest(".foo { background: conic-gradient(#f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); + minifyTest( + ".foo { background: conic-gradient(at 50% 50%, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest( + ".foo { background: conic-gradient(from 0deg, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest(".foo { background: conic-gradient(from 0, #f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); + minifyTest( + ".foo { background: conic-gradient(from 0deg at center, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest( + ".foo { background: conic-gradient(white -50%, black 150%) }", + ".foo{background:conic-gradient(#fff -50%,#000 150%)}", + ); + minifyTest( + ".foo { background: conic-gradient(white -180deg, black 540deg) }", + ".foo{background:conic-gradient(#fff -180deg,#000 540deg)}", + ); + minifyTest( + ".foo { background: conic-gradient(from 45deg, white, black, white) }", + ".foo{background:conic-gradient(from 45deg,#fff,#000,#fff)}", + ); + minifyTest( + ".foo { background: repeating-conic-gradient(from 45deg, white, black, white) }", + ".foo{background:repeating-conic-gradient(from 45deg,#fff,#000,#fff)}", + ); + minifyTest( + ".foo { background: repeating-conic-gradient(black 0deg 25%, white 0deg 50%) }", + ".foo{background:repeating-conic-gradient(#000 0deg 25%,#fff 0deg 50%)}", + ); + }); }); diff --git a/test/js/bun/css/doesnt_crash.test.ts b/test/js/bun/css/doesnt_crash.test.ts index c418f74e22..64eaa7172d 100644 --- a/test/js/bun/css/doesnt_crash.test.ts +++ b/test/js/bun/css/doesnt_crash.test.ts @@ -15,47 +15,60 @@ describe("doesnt_crash", async () => { files = readdirSync(files_dir).map(file => path.join(files_dir, file)); console.log("Tempdir", temp_dir); - files.map(absolute => { + files.forEach(absolute => { absolute = absolute.replaceAll("\\", "/"); const file = path.basename(absolute); - const outfile1 = path.join(temp_dir, "file-1" + file).replaceAll("\\", "/"); - const outfile2 = path.join(temp_dir, "file-2" + file).replaceAll("\\", "/"); - const outfile3 = path.join(temp_dir, "file-3" + file).replaceAll("\\", "/"); - const outfile4 = path.join(temp_dir, "file-4" + file).replaceAll("\\", "/"); - test(file, async () => { - { - const { stdout, stderr, exitCode } = - await Bun.$`${bunExe()} build --experimental-css ${absolute} --outfile=${outfile1}`.quiet().env(bunEnv); - expect(exitCode).toBe(0); - expect(stdout.toString()).not.toContain("error"); - expect(stderr.toString()).toBeEmpty(); - } + const configs: { target: string; minify: boolean }[] = [ + { target: "bun", minify: false }, + { target: "bun", minify: true }, + { target: "browser", minify: false }, + { target: "browser", minify: true }, + ]; - const { stdout, stderr, exitCode } = - await Bun.$`${bunExe()} build --experimental-css ${outfile1} --outfile=${outfile2}`.quiet().env(bunEnv); - expect(exitCode).toBe(0); - expect(stdout.toString()).not.toContain("error"); - expect(stderr.toString()).toBeEmpty(); - }); + for (const { target, minify } of configs) { + test(`${file} - ${minify ? "minify" : "not minify"}`, async () => { + const timeLog = `Transpiled ${file} - ${minify ? "minify" : "not minify"}`; + console.time(timeLog); + const { logs, outputs } = await Bun.build({ + entrypoints: [absolute], + experimentalCss: true, + minify: minify, + target, + throw: true, + }); + console.timeEnd(timeLog); - test(`(minify) ${file}`, async () => { - { - const { stdout, stderr, exitCode } = - await Bun.$`${bunExe()} build --experimental-css ${absolute} --minify --outfile=${outfile3}` - .quiet() - .env(bunEnv); - expect(exitCode).toBe(0); - expect(stdout.toString()).not.toContain("error"); - expect(stderr.toString()).toBeEmpty(); - } - const { stdout, stderr, exitCode } = - await Bun.$`${bunExe()} build --experimental-css ${outfile3} --minify --outfile=${outfile4}` - .quiet() - .env(bunEnv); - expect(exitCode).toBe(0); - expect(stdout.toString()).not.toContain("error"); - expect(stderr.toString()).toBeEmpty(); - }); + if (logs?.length) { + throw new Error(logs.join("\n")); + } + + expect(outputs.length).toBe(1); + const outfile1 = path.join(temp_dir, "file-1" + file).replaceAll("\\", "/"); + + await Bun.write(outfile1, outputs[0]); + + { + const timeLog = `Re-transpiled ${file} - ${minify ? "minify" : "not minify"}`; + console.time(timeLog); + console.log(" Transpiled file path:", outfile1); + const { logs, outputs } = await Bun.build({ + entrypoints: [outfile1], + experimentalCss: true, + target, + minify: minify, + throw: true, + }); + + if (logs?.length) { + throw new Error(logs.join("\n")); + } + + expect(outputs.length).toBe(1); + expect(await outputs[0].text()).not.toBeEmpty(); + console.timeEnd(timeLog); + } + }); + } }); }); diff --git a/test/js/bun/css/util.ts b/test/js/bun/css/util.ts index 09b4701739..0ec5da219c 100644 --- a/test/js/bun/css/util.ts +++ b/test/js/bun/css/util.ts @@ -30,12 +30,14 @@ export function prefix_test(source: string, expected: string, targets: Browsers) }); } -export function css_test(source: string, expected: string) { - return cssTest(source, expected); +export function css_test(source: string, expected: string, browsers?: Browsers) { + return cssTest(source, expected, browsers); } -export function cssTest(source: string, expected: string) { +export function cssTest(source: string, expected: string, browsers?: Browsers) { test(source, () => { - expect(testWithOptions(source, expected)).toEqualIgnoringWhitespace(expected); + const output = testWithOptions(source, expected, browsers); + console.log("Output", output); + expect(output).toEqualIgnoringWhitespace(expected); }); } diff --git a/test/js/bun/ffi/ffi.test.js b/test/js/bun/ffi/ffi.test.js index 39782240bf..93f1abbfc1 100644 --- a/test/js/bun/ffi/ffi.test.js +++ b/test/js/bun/ffi/ffi.test.js @@ -927,14 +927,6 @@ const libSymbols = { returns: "int", args: ["ptr", "ptr", "usize"], }, - pthread_attr_getguardsize: { - returns: "int", - args: ["ptr", "ptr"], - }, - pthread_attr_setguardsize: { - returns: "int", - args: ["ptr", "usize"], - }, login_tty: { returns: "int", args: ["int"], diff --git a/test/js/bun/glob/match.test.ts b/test/js/bun/glob/match.test.ts index c09f8b7cd0..9a98d44c40 100644 --- a/test/js/bun/glob/match.test.ts +++ b/test/js/bun/glob/match.test.ts @@ -634,6 +634,13 @@ describe("Glob.match", () => { expect(new Glob("[^a-c]*").match("BewAre")).toBeTrue(); }); + test("square braces", () => { + expect(new Glob("src/*.[tj]s").match("src/foo.js")).toBeTrue(); + expect(new Glob("src/*.[tj]s").match("src/foo.ts")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/bar.md")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/baz.md")).toBeTrue(); + }); + test("bash wildmatch", () => { expect(new Glob("a[]-]b").match("aab")).toBeFalse(); expect(new Glob("[ten]").match("ten")).toBeFalse(); diff --git a/test/js/bun/globals.test.js b/test/js/bun/globals.test.js index 554f9d7b42..e109d04fd2 100644 --- a/test/js/bun/globals.test.js +++ b/test/js/bun/globals.test.js @@ -196,3 +196,57 @@ it("errors thrown by native code should be TypeError", async () => { expect(() => Bun.dns.prefetch()).toThrowError(TypeError); expect(async () => await fetch("http://localhost", { body: "123" })).toThrowError(TypeError); }); + +describe("globalThis.gc", () => { + /** + * @param {string} expr + * @param {string[]} args + * @returns {string} + */ + const runAndPrint = (expr, ...args) => { + const result = Bun.spawnSync([bunExe(), ...args, "--print", expr], { + env: bunEnv, + }); + if (!result.success) throw new Error(result.stderr.toString("utf8")); + return result.stdout.toString("utf8").trim(); + }; + + describe("when --expose-gc is not passed", () => { + it("globalThis.gc === undefined", () => { + expect(runAndPrint("typeof globalThis.gc")).toEqual("undefined"); + }); + it(".gc does not take up a property slot", () => { + expect(runAndPrint("'gc' in globalThis")).toEqual("false"); + }); + }); + + describe("when --expose-gc is passed", () => { + it("is a function", () => { + expect(runAndPrint("typeof globalThis.gc", "--expose-gc")).toEqual("function"); + }); + + it("gc is the same as globalThis.gc", () => { + expect(runAndPrint("gc === globalThis.gc", "--expose-gc")).toEqual("true"); + }); + + it("cleans up memory", () => { + const src = /* js */ ` + let arr = [] + for (let i = 0; i < 100; i++) { + arr.push(new Array(100_000)); + } + arr.length = 0; + + const before = process.memoryUsage().heapUsed; + globalThis.gc(); + const after = process.memoryUsage().heapUsed; + return before - after; + `; + const expr = /* js */ `(function() { ${src} })()`; + + const delta = Number.parseInt(runAndPrint(expr, "--expose-gc")); + expect(delta).not.toBeNaN(); + expect(delta).toBeGreaterThanOrEqual(0); + }); + }); +}); diff --git a/test/js/bun/http/async-iterator-throws.fixture.js b/test/js/bun/http/async-iterator-throws.fixture.js index c35b68ecef..2227f9dccd 100644 --- a/test/js/bun/http/async-iterator-throws.fixture.js +++ b/test/js/bun/http/async-iterator-throws.fixture.js @@ -1,5 +1,6 @@ const server = Bun.serve({ port: 0, + idleTimeout: 0, async fetch(req) { return new Response( diff --git a/test/js/bun/http/big-form-data.fixture.js b/test/js/bun/http/big-form-data.fixture.js index b3ff882004..e91de65867 100644 --- a/test/js/bun/http/big-form-data.fixture.js +++ b/test/js/bun/http/big-form-data.fixture.js @@ -3,6 +3,7 @@ const content = Buffer.alloc(3 * 15360000, "Bun").toString(); const server = Bun.serve({ port: 0, + idleTimeout: 0, fetch: async req => { const data = await req.formData(); return new Response(data.get("name") === content ? "OK" : "NO"); diff --git a/test/js/bun/http/body-leak-test-fixture.ts b/test/js/bun/http/body-leak-test-fixture.ts index 1cc5a28298..5dbc7a1ea8 100644 --- a/test/js/bun/http/body-leak-test-fixture.ts +++ b/test/js/bun/http/body-leak-test-fixture.ts @@ -1,5 +1,6 @@ const server = Bun.serve({ port: 0, + idleTimeout: 0, async fetch(req: Request) { const url = req.url; if (url.endsWith("/report")) { diff --git a/test/js/bun/http/bun-connect-x509.test.ts b/test/js/bun/http/bun-connect-x509.test.ts new file mode 100644 index 0000000000..b81b966de7 --- /dev/null +++ b/test/js/bun/http/bun-connect-x509.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test"; +import * as harness from "harness"; +import type { Socket } from "bun"; +describe("bun.connect", () => { + test("should have peer x509 certificate", async () => { + const defer = Promise.withResolvers(); + using socket = await Bun.connect({ + hostname: "example.com", + port: 443, + tls: true, + socket: { + open(socket: Socket) {}, + close() {}, + handshake(socket: Socket) { + defer.resolve(socket); + }, + data() {}, + drain() {}, + }, + }); + await defer.promise; + const x509: import("node:crypto").X509Certificate = socket.getPeerX509Certificate(); + expect(x509.checkHost("example.com")).toBe("example.com"); + }); + + test("should have x509 certificate", async () => { + const defer = Promise.withResolvers(); + const listener = await Bun.listen({ + hostname: "localhost", + port: 0, + tls: harness.tls, + socket: { + open(socket: Socket) {}, + close() {}, + handshake(socket: Socket) { + defer.resolve(socket); + }, + data() {}, + drain() {}, + }, + }); + + const defer2 = Promise.withResolvers(); + await Bun.connect({ + hostname: listener.hostname, + port: listener.port, + tls: harness.tls, + socket: { + open(socket: Socket) {}, + close() {}, + handshake(socket: Socket) { + defer2.resolve(socket); + }, + data() {}, + drain() {}, + }, + }); + using server = await defer.promise; + using client = await defer2.promise; + function check() { + const x509: import("node:crypto").X509Certificate = server.getX509Certificate(); + const peerX509: import("node:crypto").X509Certificate = client.getPeerX509Certificate(); + expect(x509.checkHost("localhost")).toBe("localhost"); + expect(peerX509.checkHost("localhost")).toBe("localhost"); + } + check(); + Bun.gc(true); + + // GC test: + for (let i = 0; i < 1000; i++) { + server.getX509Certificate(); + client.getPeerX509Certificate(); + if (i % 100 === 0 && i > 0) { + Bun.gc(true); + } + } + + Bun.gc(true); + listener.stop(); + }); +}); diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts new file mode 100644 index 0000000000..87bc43095b --- /dev/null +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -0,0 +1,274 @@ +import { Subprocess } from "bun"; +import { test, expect } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { join } from "path"; +test("serve html", async () => { + const dir = tempDirWithFiles("html-css-js", { + "dashboard.html": /*html*/ ` + + + + Dashboard + + + + + +
+

Dashboard

+

This is a separate route to test multiple pages work

+ +

+ Back to Home +
+ + + `, + "dashboard.js": /*js*/ ` + import './script.js'; + // Additional dashboard-specific code could go here + console.log("How...dashing?") + `, + "index.html": /*html*/ ` + + + + Bun HTML Import Test + + + + +
+

Hello from Bun!

+ +
+ + + `, + "script.js": /*js*/ ` + let count = 0; + const button = document.getElementById('counter'); + button.addEventListener('click', () => { + count++; + button.textContent = \`Click me: \${count}\`; + }); + `, + "styles.css": /*css*/ ` + .container { + max-width: 800px; + margin: 2rem auto; + text-align: center; + font-family: system-ui, sans-serif; + } + + button { + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: 0.25rem; + border: 2px solid #000; + background: #fff; + cursor: pointer; + transition: all 0.2s; + } + + button:hover { + background: #000; + color: #fff; + } + `, + }); + + const { subprocess, port, hostname } = await waitForServer(dir, { + "/": join(dir, "index.html"), + "/dashboard": join(dir, "dashboard.html"), + }); + + { + const html = await (await fetch(`http://${hostname}:${port}/`)).text(); + const trimmed = html + .trim() + .split("\n") + .map(a => a.trim()) + .filter(a => a.length > 0) + .join("\n") + .trim() + .replace(/chunk-[a-z0-9]+\.css/g, "chunk-HASH.css") + .replace(/chunk-[a-z0-9]+\.js/g, "chunk-HASH.js"); + + expect(trimmed).toMatchInlineSnapshot(` +" + + +Bun HTML Import Test + + +
+

Hello from Bun!

+ +
+ +" +`); + } + + { + const html = await (await fetch(`http://${hostname}:${port}/dashboard`)).text(); + const jsSrc = new URL( + html.match(/ + +
+

Dashboard

+

This is a separate route to test multiple pages work

+ +

+Back to Home +
+ +" +`); + const response = await fetch(jsSrc!); + const js = await response.text(); + expect( + js + .replace(/# debugId=[a-z0-9A-Z]+/g, "# debugId=") + .replace(/# sourceMappingURL=[^"]+/g, "# sourceMappingURL="), + ).toMatchInlineSnapshot(` +"// script.js +var count = 0; +var button = document.getElementById("counter"); +button.addEventListener("click", () => { + count++; + button.textContent = \`Click me: \${count}\`; +}); + +// dashboard.js +console.log("How...dashing?"); + +//# debugId= +//# sourceMappingURL=" +`); + const sourceMapURL = js.match(/# sourceMappingURL=([^"]+)/)?.[1]; + if (!sourceMapURL) { + throw new Error("No source map URL found"); + } + const sourceMap = await (await fetch(new URL(sourceMapURL, "http://" + hostname + ":" + port))).json(); + sourceMap.sourcesContent = sourceMap.sourcesContent.map(a => a.trim()); + expect(JSON.stringify(sourceMap, null, 2)).toMatchInlineSnapshot(` +"{ + "version": 3, + "sources": [ + "script.js", + "dashboard.js" + ], + "sourcesContent": [ + "let count = 0;\\n const button = document.getElementById('counter');\\n button.addEventListener('click', () => {\\n count++;\\n button.textContent = \`Click me: \${count}\`;\\n });", + "import './script.js';\\n // Additional dashboard-specific code could go here\\n console.log(\\"How...dashing?\\")" + ], + "mappings": ";AACM,IAAI,QAAQ;AACZ,IAAM,SAAS,SAAS,eAAe,SAAS;AAChD,OAAO,iBAAiB,SAAS,MAAM;AACrC;AACA,SAAO,cAAc,aAAa;AAAA,CACnC;;;ACHD,QAAQ,IAAI,gBAAgB;", + "debugId": "0B3DD451DC3D66B564756E2164756E21", + "names": [] +}" +`); + const headers = response.headers.toJSON(); + headers.date = ""; + headers.sourcemap = headers.sourcemap.replace(/chunk-[a-z0-9]+\.js.map/g, "chunk-HASH.js.map"); + expect(headers).toMatchInlineSnapshot(` +{ + "content-length": "316", + "content-type": "text/javascript;charset=utf-8", + "date": "", + "etag": "42b631804ef51c7e", + "sourcemap": "/chunk-HASH.js.map", +} +`); + } + + { + const css = await (await fetch(cssSrc!)).text(); + expect(css).toMatchInlineSnapshot(` +"/* styles.css */ +.container { + text-align: center; + font-family: system-ui, sans-serif; + max-width: 800px; + margin: 2rem auto; +} + +button { + font-size: 1.25rem; + border-radius: .25rem; + border: 2px solid #000; + cursor: pointer; + transition: all .2s; + background: #fff; + padding: .5rem 1rem; +} + +button:hover { + color: #fff; + background: #000; +} +" +`); + } + + expect(await (await fetch(`http://${hostname}:${port}/a-different-url`)).text()).toMatchInlineSnapshot(`"Hello World"`); + + subprocess.kill(); +}); + +async function waitForServer( + dir: string, + entryPoints: Record, +): Promise<{ + subprocess: Subprocess; + port: number; + hostname: string; +}> { + let defer = Promise.withResolvers<{ + subprocess: Subprocess; + port: number; + hostname: string; + }>(); + const process = Bun.spawn({ + cmd: [bunExe(), "--experimental-html", join(import.meta.dir, "bun-serve-static-fixture.js")], + env: { + ...bunEnv, + NODE_ENV: undefined, + }, + cwd: dir, + ipc(message, subprocess) { + subprocess.send({ + files: entryPoints, + }); + defer.resolve({ + subprocess, + port: message.port, + hostname: message.hostname, + }); + }, + }); + return defer.promise; +} diff --git a/test/js/bun/http/bun-serve-static-fixture.js b/test/js/bun/http/bun-serve-static-fixture.js new file mode 100644 index 0000000000..67e499f527 --- /dev/null +++ b/test/js/bun/http/bun-serve-static-fixture.js @@ -0,0 +1,30 @@ +import { serve } from "bun"; + +let server = Bun.serve({ + port: 0, + development: true, + async fetch(req) { + return new Response("Hello World", { + status: 404, + }); + }, +}); + +process.on("message", async message => { + const files = message.files || {}; + const routes = {}; + for (const [key, value] of Object.entries(files)) { + routes[key] = (await import(value)).default; + } + + server.reload({ + // omit "fetch" to check we can do server.reload without passing fetch + static: routes, + development: true, + }); +}); + +process.send({ + port: server.port, + hostname: server.hostname, +}); diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index de430f36e3..f4966cb85b 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -1,6 +1,6 @@ import type { Server, ServerWebSocket, Socket } from "bun"; import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, rejectUnauthorizedScope } from "harness"; +import { bunEnv, bunExe, rejectUnauthorizedScope, tempDirWithFiles } from "harness"; import path from "path"; describe("Server", () => { @@ -317,8 +317,7 @@ describe("Server", () => { } }); - - test('server should return a body for a OPTIONS Request', async () => { + test("server should return a body for a OPTIONS Request", async () => { using server = Bun.serve({ port: 0, fetch(req) { @@ -327,16 +326,17 @@ describe("Server", () => { }); { const url = `http://${server.hostname}:${server.port}/`; - const response = await fetch(new Request(url, { - method: 'OPTIONS', - })); + const response = await fetch( + new Request(url, { + method: "OPTIONS", + }), + ); expect(await response.text()).toBe("Hello World!"); expect(response.status).toBe(200); expect(response.url).toBe(url); } }); - test("abort signal on server with stream", async () => { { let signalOnServer = false; @@ -456,7 +456,7 @@ describe("Server", () => { env: bunEnv, stderr: "pipe", }); - expect(stderr.toString('utf-8')).toBeEmpty(); + expect(stderr.toString("utf-8")).toBeEmpty(); expect(exitCode).toBe(0); }); }); @@ -768,3 +768,336 @@ test.skip("should be able to stream huge amounts of data", async () => { expect(written).toBe(CONTENT_LENGTH); expect(received).toBe(CONTENT_LENGTH); }, 30_000); + +describe("HEAD requests #15355", () => { + test("should be able to make HEAD requests with content-length or transfer-encoding (async)", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + await Bun.sleep(1); + if (req.method === "HEAD") { + if (req.url.endsWith("/content-length")) { + return new Response(null, { + headers: { + "Content-Length": "11", + }, + }); + } + return new Response(null, { + headers: { + "Transfer-Encoding": "chunked", + }, + }); + } + if (req.url.endsWith("/content-length")) { + return new Response("Hello World"); + } + return new Response(async function* () { + yield "Hello"; + await Bun.sleep(1); + yield " "; + await Bun.sleep(1); + yield "World"; + }); + }, + }); + + { + const response = await fetch(server.url + "/content-length"); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + { + const response = await fetch(server.url + "/chunked"); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe("Hello World"); + } + + { + const response = await fetch(server.url + "/content-length", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/chunked", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe(""); + } + }); + + test("should be able to make HEAD requests with content-length or transfer-encoding (sync)", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + if (req.method === "HEAD") { + if (req.url.endsWith("/content-length")) { + return new Response(null, { + headers: { + "Content-Length": "11", + }, + }); + } + return new Response(null, { + headers: { + "Transfer-Encoding": "chunked", + }, + }); + } + if (req.url.endsWith("/content-length")) { + return new Response("Hello World"); + } + return new Response(async function* () { + yield "Hello"; + await Bun.sleep(1); + yield " "; + await Bun.sleep(1); + yield "World"; + }); + }, + }); + + { + const response = await fetch(server.url + "/content-length"); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + { + const response = await fetch(server.url + "/chunked"); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe("Hello World"); + } + + { + const response = await fetch(server.url + "/content-length", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/chunked", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe(""); + } + }); + + test("should fallback to the body if content-length is missing in the headers", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + if (req.url.endsWith("/content-length")) { + return new Response("Hello World", { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }); + } + + if (req.url.endsWith("/chunked")) { + return new Response( + async function* () { + yield "Hello"; + await Bun.sleep(1); + yield " "; + await Bun.sleep(1); + yield "World"; + }, + { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }, + ); + } + + return new Response(null, { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }); + }, + }); + { + const response = await fetch(server.url + "/content-length", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/chunked", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/null", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("0"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + }); + + test("HEAD requests should not have body", async () => { + const dir = tempDirWithFiles("fsr", { + "hello": "Hello World", + }); + + const filename = path.join(dir, "hello"); + using server = Bun.serve({ + port: 0, + fetch(req) { + if (req.url.endsWith("/file")) { + return new Response(Bun.file(filename)); + } + return new Response("Hello World"); + }, + }); + + { + const response = await fetch(server.url); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + { + const response = await fetch(server.url + "/file"); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + + function doHead(server: Server, path: string): Promise<{ headers: string; body: string }> { + const { promise, resolve } = Promise.withResolvers(); + // use node net to make a HEAD request + const net = require("net"); + const url = new URL(server.url); + const socket = net.createConnection(url.port, url.hostname); + socket.write(`HEAD ${path} HTTP/1.1\r\nHost: ${url.hostname}:${url.port}\r\n\r\n`); + let body = ""; + let headers = ""; + socket.on("data", data => { + body += data.toString(); + if (!headers) { + const headerIndex = body.indexOf("\r\n\r\n"); + if (headerIndex !== -1) { + headers = body.slice(0, headerIndex); + body = body.slice(headerIndex + 4); + + setTimeout(() => { + // wait to see if we get extra data + resolve({ headers, body }); + socket.destroy(); + }, 100); + } + } + }); + return promise as Promise<{ headers: string; body: string }>; + } + { + const response = await fetch(server.url, { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/file", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const { headers, body } = await doHead(server, "/"); + expect(headers.toLowerCase()).toContain("content-length: 11"); + expect(body).toBe(""); + } + { + const { headers, body } = await doHead(server, "/file"); + expect(headers.toLowerCase()).toContain("content-length: 11"); + expect(body).toBe(""); + } + }); + + describe("HEAD request should respect status", () => { + test("status only without headers", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + return new Response(null, { status: 404 }); + }, + }); + const response = await fetch(server.url, { method: "HEAD" }); + expect(response.status).toBe(404); + expect(response.headers.get("content-length")).toBe("0"); + }); + test("status only with headers", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + return new Response(null, { + status: 404, + headers: { "X-Bun-Test": "1", "Content-Length": "11" }, + }); + }, + }); + const response = await fetch(server.url, { method: "HEAD" }); + expect(response.status).toBe(404); + expect(response.headers.get("content-length")).toBe("11"); + expect(response.headers.get("x-bun-test")).toBe("1"); + }); + + test("status only with transfer-encoding", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + return new Response(null, { status: 404, headers: { "Transfer-Encoding": "chunked" } }); + }, + }); + const response = await fetch(server.url, { method: "HEAD" }); + expect(response.status).toBe(404); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + }); + + test("status only with body", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + return new Response("Hello World", { status: 404 }); + }, + }); + const response = await fetch(server.url, { method: "HEAD" }); + expect(response.status).toBe(404); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + }); + }); +}); diff --git a/test/js/bun/http/fetch-file-upload.test.ts b/test/js/bun/http/fetch-file-upload.test.ts index ae8a26a870..b779e3b6c5 100644 --- a/test/js/bun/http/fetch-file-upload.test.ts +++ b/test/js/bun/http/fetch-file-upload.test.ts @@ -171,7 +171,7 @@ test("missing file throws the expected error", async () => { proxy: "http://localhost:3000", }); expect(Bun.peek.status(resp)).toBe("rejected"); - expect(async () => await resp).toThrow("No such file or directory"); + expect(async () => await resp).toThrow("no such file or directory"); } }); Bun.gc(true); diff --git a/test/js/bun/http/proxy.test.js b/test/js/bun/http/proxy.test.js index 6faa0f4f07..19ca789e6f 100644 --- a/test/js/bun/http/proxy.test.js +++ b/test/js/bun/http/proxy.test.js @@ -71,9 +71,9 @@ beforeAll(() => { }); afterAll(() => { - server.stop(); - proxy.stop(); - auth_proxy.stop(); + server.stop(true); + proxy.stop(true); + auth_proxy.stop(true); }); const test = process.env.PROXY_URL ? it : it.skip; @@ -178,13 +178,13 @@ it.each([ const path = `${tmpdir()}/bun-test-http-proxy-env-${Date.now()}.ts`; fs.writeFileSync(path, 'await fetch("https://example.com");'); - const { stdout, stderr, exitCode } = Bun.spawnSync({ + const { stderr, exitCode } = Bun.spawnSync({ cmd: [bunExe(), "run", path], env: { http_proxy: http_proxy, https_proxy: https_proxy, }, - stdout: "pipe", + stdout: "inherit", stderr: "pipe", }); diff --git a/test/js/bun/http/readable-stream-throws.fixture.js b/test/js/bun/http/readable-stream-throws.fixture.js index a1d8d4ec06..ff32f94507 100644 --- a/test/js/bun/http/readable-stream-throws.fixture.js +++ b/test/js/bun/http/readable-stream-throws.fixture.js @@ -1,6 +1,6 @@ const server = Bun.serve({ port: 0, - + idleTimeout: 0, error(err) { return new Response("Failed", { status: 555 }); }, diff --git a/test/js/bun/http/rejected-promise-fixture.js b/test/js/bun/http/rejected-promise-fixture.js index f63f774a2a..3b50761f1a 100644 --- a/test/js/bun/http/rejected-promise-fixture.js +++ b/test/js/bun/http/rejected-promise-fixture.js @@ -1,5 +1,6 @@ const server = Bun.serve({ hostname: "localhost", + idleTimeout: 0, async fetch() { throw new Error("Error"); }, diff --git a/test/js/bun/http/serve-body-leak.test.ts b/test/js/bun/http/serve-body-leak.test.ts index 40f260bea5..510a00a078 100644 --- a/test/js/bun/http/serve-body-leak.test.ts +++ b/test/js/bun/http/serve-body-leak.test.ts @@ -1,6 +1,6 @@ import type { Subprocess } from "bun"; import { afterEach, beforeEach, expect, it } from "bun:test"; -import { bunEnv, bunExe, isDebug, isFlaky, isLinux } from "harness"; +import { bunEnv, bunExe, isDebug, isFlaky, isLinux, isWindows } from "harness"; import { join } from "path"; const payload = Buffer.alloc(512 * 1024, "1").toString("utf-8"); // decent size payload to test memory leak @@ -9,15 +9,9 @@ const totalCount = 10_000; const zeroCopyPayload = new Blob([payload]); const zeroCopyJSONPayload = new Blob([JSON.stringify({ bun: payload })]); -let url: URL; -let process: Subprocess<"ignore", "pipe", "inherit"> | null = null; -beforeEach(async () => { - if (process) { - process?.kill(); - } - - let defer = Promise.withResolvers(); - process = Bun.spawn([bunExe(), "--smol", join(import.meta.dirname, "body-leak-test-fixture.ts")], { +async function getURL() { + let defer = Promise.withResolvers(); + const process = Bun.spawn([bunExe(), "--smol", join(import.meta.dirname, "body-leak-test-fixture.ts")], { env: bunEnv, stdout: "inherit", stderr: "inherit", @@ -26,19 +20,17 @@ beforeEach(async () => { defer.resolve(message); }, }); - url = new URL(await defer.promise); + const url: URL = new URL(await defer.promise); process.unref(); - await warmup(); -}); -afterEach(() => { - process?.kill(); -}); + await warmup(url); + return { url, process }; +} -async function getMemoryUsage(): Promise { +async function getMemoryUsage(url: URL): Promise { return (await fetch(`${url.origin}/report`).then(res => res.json())) as number; } -async function warmup() { +async function warmup(url: URL) { var remaining = totalCount; while (remaining > 0) { @@ -54,17 +46,17 @@ async function warmup() { remaining -= batchSize; } // clean up memory before first test - await getMemoryUsage(); + await getMemoryUsage(url); } -async function callBuffering() { +async function callBuffering(url: URL) { const result = await fetch(`${url.origin}/buffering`, { method: "POST", body: zeroCopyPayload, }).then(res => res.text()); expect(result).toBe("Ok"); } -async function callJSONBuffering() { +async function callJSONBuffering(url: URL) { const result = await fetch(`${url.origin}/json-buffering`, { method: "POST", body: zeroCopyJSONPayload, @@ -72,35 +64,35 @@ async function callJSONBuffering() { expect(result).toBe("Ok"); } -async function callBufferingBodyGetter() { +async function callBufferingBodyGetter(url: URL) { const result = await fetch(`${url.origin}/buffering+body-getter`, { method: "POST", body: zeroCopyPayload, }).then(res => res.text()); expect(result).toBe("Ok"); } -async function callStreaming() { +async function callStreaming(url: URL) { const result = await fetch(`${url.origin}/streaming`, { method: "POST", body: zeroCopyPayload, }).then(res => res.text()); expect(result).toBe("Ok"); } -async function callIncompleteStreaming() { +async function callIncompleteStreaming(url: URL) { const result = await fetch(`${url.origin}/incomplete-streaming`, { method: "POST", body: zeroCopyPayload, }).then(res => res.text()); expect(result).toBe("Ok"); } -async function callStreamingEcho() { +async function callStreamingEcho(url: URL) { const result = await fetch(`${url.origin}/streaming-echo`, { method: "POST", body: zeroCopyPayload, }).then(res => res.text()); expect(result).toBe(payload); } -async function callIgnore() { +async function callIgnore(url: URL) { const result = await fetch(url, { method: "POST", body: zeroCopyPayload, @@ -108,8 +100,8 @@ async function callIgnore() { expect(result).toBe("Ok"); } -async function calculateMemoryLeak(fn: () => Promise) { - const start_memory = await getMemoryUsage(); +async function calculateMemoryLeak(fn: (url: URL) => Promise, url: URL) { + const start_memory = await getMemoryUsage(url); const memory_examples: Array = []; let peak_memory = start_memory; @@ -117,14 +109,14 @@ async function calculateMemoryLeak(fn: () => Promise) { while (remaining > 0) { const batch = new Array(batchSize); for (let j = 0; j < batchSize; j++) { - batch[j] = fn(); + batch[j] = fn(url); } await Promise.all(batch); remaining -= batchSize; // garbage collect and check memory usage every 1000 requests if (remaining > 0 && remaining % 1000 === 0) { - const report = await getMemoryUsage(); + const report = await getMemoryUsage(url); if (report > peak_memory) { peak_memory = report; } @@ -133,7 +125,7 @@ async function calculateMemoryLeak(fn: () => Promise) { } // wait for the last memory usage to be stable - const end_memory = await getMemoryUsage(); + const end_memory = await getMemoryUsage(url); if (end_memory > peak_memory) { peak_memory = end_memory; } @@ -157,10 +149,12 @@ for (const test_info of [ ["should not leak memory when streaming the body and echoing it back", callStreamingEcho, false, 64], ] as const) { const [testName, fn, skip, maxMemoryGrowth] = test_info; - it.todoIf(skip)( + it.todoIf(skip || isFlaky && isWindows)( testName, async () => { - const report = await calculateMemoryLeak(fn); + const { url, process } = await getURL(); + await using processHandle = process; + const report = await calculateMemoryLeak(fn, url); // peak memory is too high expect(report.peak_memory).not.toBeGreaterThan(report.start_memory * 2.5); // acceptable memory leak diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index daadea5474..7b83ca75ff 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -1,7 +1,7 @@ import { file, gc, Serve, serve, Server } from "bun"; import { afterAll, afterEach, describe, expect, it, mock } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, dumpStats, isIPv4, isIPv6, isPosix, tls, tmpdirSync } from "harness"; +import { bunEnv, bunExe, dumpStats, isBroken, isIntelMacOS, isIPv4, isIPv6, isPosix, tls, tmpdirSync } from "harness"; import { join, resolve } from "path"; // import { renderToReadableStream } from "react-dom/server"; // import app_jsx from "./app.jsx"; @@ -213,92 +213,95 @@ for (let withDelay of [true, false]) { }); } } -describe("1000 uploads & downloads in batches of 64 do not leak ReadableStream", () => { - for (let isDirect of [true, false] as const) { - it( - isDirect ? "direct" : "default", - async () => { - const blob = new Blob([new Uint8Array(1024 * 768).fill(123)]); - Bun.gc(true); +describe.todoIf(isBroken && isIntelMacOS)( + "1000 uploads & downloads in batches of 64 do not leak ReadableStream", + () => { + for (let isDirect of [true, false] as const) { + it( + isDirect ? "direct" : "default", + async () => { + const blob = new Blob([new Uint8Array(1024 * 768).fill(123)]); + Bun.gc(true); - const expected = Bun.CryptoHasher.hash("sha256", blob, "base64"); - const initialCount = heapStats().objectTypeCounts.ReadableStream || 0; + const expected = Bun.CryptoHasher.hash("sha256", blob, "base64"); + const initialCount = heapStats().objectTypeCounts.ReadableStream || 0; - await runTest( - { - async fetch(req) { - var hasher = new Bun.SHA256(); - for await (const chunk of req.body) { - await Bun.sleep(0); - hasher.update(chunk); + await runTest( + { + async fetch(req) { + var hasher = new Bun.SHA256(); + for await (const chunk of req.body) { + await Bun.sleep(0); + hasher.update(chunk); + } + return new Response( + isDirect + ? new ReadableStream({ + type: "direct", + async pull(controller) { + await Bun.sleep(0); + controller.write(Buffer.from(hasher.digest("base64"))); + await controller.flush(); + controller.close(); + }, + }) + : new ReadableStream({ + async pull(controller) { + await Bun.sleep(0); + controller.enqueue(Buffer.from(hasher.digest("base64"))); + controller.close(); + }, + }), + ); + }, + }, + async server => { + const count = 1000; + async function callback() { + const response = await fetch(server.url, { + body: blob, + method: "POST", + }); + + // We are testing for ReadableStream leaks, so we use the ReadableStream here. + const chunks = []; + for await (const chunk of response.body) { + chunks.push(chunk); + } + + const digest = Buffer.from(Bun.concatArrayBuffers(chunks)).toString(); + + expect(digest).toBe(expected); + Bun.gc(false); } - return new Response( - isDirect - ? new ReadableStream({ - type: "direct", - async pull(controller) { - await Bun.sleep(0); - controller.write(Buffer.from(hasher.digest("base64"))); - await controller.flush(); - controller.close(); - }, - }) - : new ReadableStream({ - async pull(controller) { - await Bun.sleep(0); - controller.enqueue(Buffer.from(hasher.digest("base64"))); - controller.close(); - }, - }), + { + let remaining = count; + + const batchSize = 64; + while (remaining > 0) { + const promises = new Array(count); + for (let i = 0; i < batchSize && remaining > 0; i++) { + promises[i] = callback(); + } + await Promise.all(promises); + remaining -= batchSize; + } + } + + Bun.gc(true); + dumpStats(); + expect(heapStats().objectTypeCounts.ReadableStream).toBeWithin( + Math.max(initialCount - count / 2, 0), + initialCount + count / 2, ); }, - }, - async server => { - const count = 1000; - async function callback() { - const response = await fetch(server.url, { - body: blob, - method: "POST", - }); - - // We are testing for ReadableStream leaks, so we use the ReadableStream here. - const chunks = []; - for await (const chunk of response.body) { - chunks.push(chunk); - } - - const digest = Buffer.from(Bun.concatArrayBuffers(chunks)).toString(); - - expect(digest).toBe(expected); - Bun.gc(false); - } - { - let remaining = count; - - const batchSize = 64; - while (remaining > 0) { - const promises = new Array(count); - for (let i = 0; i < batchSize && remaining > 0; i++) { - promises[i] = callback(); - } - await Promise.all(promises); - remaining -= batchSize; - } - } - - Bun.gc(true); - dumpStats(); - expect(heapStats().objectTypeCounts.ReadableStream).toBeWithin( - Math.max(initialCount - count / 2, 0), - initialCount + count / 2, - ); - }, - ); - }, - 100000, - ); - } -}); + ); + }, + 100000, + ); + } + }, +); [200, 200n, 303, 418, 599, 599n].forEach(statusCode => { it(`should response with HTTP status code (${statusCode})`, async () => { diff --git a/test/js/bun/io/bun-write.test.js b/test/js/bun/io/bun-write.test.js index 18a744c336..926ebba987 100644 --- a/test/js/bun/io/bun-write.test.js +++ b/test/js/bun/io/bun-write.test.js @@ -479,7 +479,7 @@ describe("ENOENT", () => { const file = join(dir, "file"); try { expect(async () => await Bun.write(file, "contents", { createPath: false })).toThrow( - "No such file or directory", + "no such file or directory", ); expect(fs.existsSync(file)).toBe(false); } finally { diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index c60c267cee..693ce9e808 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -220,7 +220,8 @@ it("should reject on connection error, calling both connectError() and rejecting expect(socket).toBeDefined(); expect(socket.data).toBe(data); expect(error).toBeDefined(); - expect(error.name).toBe("ECONNREFUSED"); + expect(error.name).toBe("Error"); + expect(error.code).toBe("ECONNREFUSED"); expect(error.message).toBe("Failed to connect"); }, data() { @@ -246,7 +247,8 @@ it("should reject on connection error, calling both connectError() and rejecting () => done(new Error("Promise should reject instead")), err => { expect(err).toBeDefined(); - expect(err.name).toBe("ECONNREFUSED"); + expect(err.name).toBe("Error"); + expect(err.code).toBe("ECONNREFUSED"); expect(err.message).toBe("Failed to connect"); done(); @@ -293,7 +295,7 @@ it("should handle connection error", done => { expect(socket).toBeDefined(); expect(socket.data).toBe(data); expect(error).toBeDefined(); - expect(error.name).toBe("ECONNREFUSED"); + expect(error.name).toBe("Error"); expect(error.message).toBe("Failed to connect"); expect((error as any).code).toBe("ECONNREFUSED"); done(); @@ -372,7 +374,7 @@ it("should allow large amounts of data to be sent and received", async () => { it("it should not crash when getting a ReferenceError on client socket open", async () => { using server = Bun.serve({ - port: 8080, + port: 0, hostname: "localhost", fetch() { return new Response("Hello World"); @@ -413,7 +415,7 @@ it("it should not crash when getting a ReferenceError on client socket open", as it("it should not crash when returning a Error on client socket open", async () => { using server = Bun.serve({ - port: 8080, + port: 0, hostname: "localhost", fetch() { return new Response("Hello World"); @@ -595,6 +597,7 @@ it("should not call drain before handshake", async () => { }); it("upgradeTLS handles errors", async () => { using server = Bun.serve({ + port: 0, tls, async fetch(req) { return new Response("Hello World"); @@ -699,6 +702,7 @@ it("upgradeTLS handles errors", async () => { }); it("should be able to upgrade to TLS", async () => { using server = Bun.serve({ + port: 0, tls, async fetch(req) { return new Response("Hello World"); diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 5222bc29a6..2da1afa169 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -187,15 +187,14 @@ plugin({ // This is to test that it works when imported from a separate file import "../../third_party/svelte"; import "./module-plugins"; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; -import { filter } from "js/node/test/fixtures/aead-vectors"; +import { render as svelteRender } from "svelte/server"; describe("require", () => { it("SSRs `

Hello world!

` with Svelte", () => { const { default: App } = require("./hello.svelte"); - const { html } = App.render(); + const { body } = svelteRender(App); - expect(html).toBe("

Hello world!

"); + expect(body).toBe("

Hello world!

"); }); it("beep:boop returns 42", () => { @@ -295,9 +294,8 @@ describe("dynamic import", () => { it("SSRs `

Hello world!

` with Svelte", async () => { const { default: App }: any = await import("./hello.svelte"); - const { html } = App.render(); - - expect(html).toBe("

Hello world!

"); + const { body } = svelteRender(App); + expect(body).toBe("

Hello world!

"); }); it("beep:boop returns 42", async () => { @@ -326,9 +324,9 @@ import Hello from ${JSON.stringify(resolve(import.meta.dir, "hello2.svelte"))}; export default Hello; `; const { default: SvelteApp } = await import("delay:hello2.svelte"); - const { html } = SvelteApp.render(); + const { body } = svelteRender(SvelteApp); - expect(html).toBe("

Hello world!

"); + expect(body).toBe("

Hello world!

"); }); }); @@ -478,7 +476,11 @@ describe("errors", () => { return new Response(result); }, }); - const { default: text } = await import(`http://${server.hostname}:${server.port}/hey.txt`); + const sleep = ms => new Promise(res => setTimeout(() => res("timeout"), ms)); + const text = await Promise.race([ + import(`http://${server.hostname}:${server.port}/hey.txt`).then(mod => mod.default) as Promise, + sleep(2_500), + ]); expect(text).toBe(result); }); }); diff --git a/test/js/bun/resolve/bun-lock.test.ts b/test/js/bun/resolve/bun-lock.test.ts new file mode 100644 index 0000000000..251783c00e --- /dev/null +++ b/test/js/bun/resolve/bun-lock.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from "bun:test"; +import { tempDirWithFiles } from "harness"; +import { join } from "path"; + +const lockfile = `{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "something", + "dependencies": { }, + }, + }, + "packages": { }, +}`; + +test("import bun.lock file as json", async () => { + const dir = tempDirWithFiles("bun-lock", { + "bun.lock": lockfile, + "index.ts": ` + import lockfile from './bun.lock'; + const _lockfile = ${lockfile} + if (!Bun.deepEquals(lockfile, _lockfile)) throw new Error('bun.lock wasnt imported as jsonc'); + `, + }); + + expect([join(dir, "index.ts")]).toRun(); +}); diff --git a/test/js/bun/resolve/import-query-fixture.ts b/test/js/bun/resolve/import-query-fixture.ts new file mode 100644 index 0000000000..4651f95b3f --- /dev/null +++ b/test/js/bun/resolve/import-query-fixture.ts @@ -0,0 +1,2 @@ +export const url = import.meta.url; +globalThis.importQueryFixtureOrder.push(import.meta.url); diff --git a/test/js/bun/resolve/import-query.test.ts b/test/js/bun/resolve/import-query.test.ts new file mode 100644 index 0000000000..9d2d9e6be1 --- /dev/null +++ b/test/js/bun/resolve/import-query.test.ts @@ -0,0 +1,44 @@ +import { test, expect, beforeEach, afterEach } from "bun:test"; +globalThis.importQueryFixtureOrder = []; +const resolvedPath = require.resolve("./import-query-fixture.ts"); +const resolvedURL = Bun.pathToFileURL(resolvedPath).href; + +beforeEach(() => { + globalThis.importQueryFixtureOrder = []; + Loader.registry.delete(resolvedPath); + Loader.registry.delete(resolvedPath + "?query"); + Loader.registry.delete(resolvedPath + "?query2"); +}); + +test("[query, no query]", async () => { + const second = await import("./import-query-fixture.ts?query"); + const first = await import("./import-query-fixture.ts"); + expect(second.url).toBe(first.url + "?query"); + expect(globalThis.importQueryFixtureOrder).toEqual([resolvedURL + "?query", resolvedURL]); +}); + +test("[no query, query]", async () => { + const first = await import("./import-query-fixture.ts"); + const second = await import("./import-query-fixture.ts?query"); + expect(second.url).toBe(first.url + "?query"); + expect(globalThis.importQueryFixtureOrder).toEqual([resolvedURL, resolvedURL + "?query"]); +}); + +for (let order of [ + [resolvedPath, resolvedPath + "?query", resolvedPath + "?query2"], + [resolvedPath + "?query", resolvedPath + "?query2", resolvedPath], + [resolvedPath + "?query", resolvedPath, resolvedPath + "?query2"], + [resolvedPath, resolvedPath + "?query2", resolvedPath + "?query"], + [resolvedPath + "?query2", resolvedPath, resolvedPath + "?query"], + [resolvedPath + "?query2", resolvedPath + "?query", resolvedPath], +]) { + test(`[${order.map(url => url.replaceAll(import.meta.dir, "")).join(", ")}]`, async () => { + for (const url of order) { + await import(url); + } + + expect(globalThis.importQueryFixtureOrder).toEqual( + order.map(url => resolvedURL + (url.includes("?") ? "?" + url.split("?")[1] : "")), + ); + }); +} diff --git a/test/js/bun/resolve/jsonc.test.ts b/test/js/bun/resolve/jsonc.test.ts index d2571644c8..bab3ba29ff 100644 --- a/test/js/bun/resolve/jsonc.test.ts +++ b/test/js/bun/resolve/jsonc.test.ts @@ -22,3 +22,19 @@ test("empty jsonc - tsconfig.json", async () => { }); expect([join(dir, "index.ts")]).toRun(); }); + +test("import anything.jsonc as json", async () => { + const jsoncFile = `{ + // comment + "trailingComma": 0, + }`; + const dir = tempDirWithFiles("jsonc", { + "anything.jsonc": jsoncFile, + "index.ts": ` + import file from './anything.jsonc'; + const _file = ${jsoncFile} + if (!Bun.deepEquals(file, _file)) throw new Error('anything.jsonc wasnt imported as jsonc'); + `, + }); + expect([join(dir, "index.ts")]).toRun(); +}); diff --git a/test/js/bun/resolve/resolve.test.ts b/test/js/bun/resolve/resolve.test.ts index d2fa6fbb97..969449ffd0 100644 --- a/test/js/bun/resolve/resolve.test.ts +++ b/test/js/bun/resolve/resolve.test.ts @@ -1,12 +1,8 @@ import { it, expect } from "bun:test"; import { mkdirSync, writeFileSync } from "fs"; -import { join } from "path"; +import { join, sep } from "path"; import { bunExe, bunEnv, tempDirWithFiles, isWindows } from "harness"; import { pathToFileURL } from "bun"; -import { expect, it } from "bun:test"; -import { mkdirSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; -import { join, sep } from "path"; it("spawn test file", () => { writePackageJSONImportsFixture(); diff --git a/test/js/bun/s3/bun-write-leak-fixture.js b/test/js/bun/s3/bun-write-leak-fixture.js new file mode 100644 index 0000000000..0a7dabee81 --- /dev/null +++ b/test/js/bun/s3/bun-write-leak-fixture.js @@ -0,0 +1,31 @@ +// Avoid using String.prototype.repeat in this file because it's very slow in +// debug builds of JavaScriptCore +let MAX_ALLOWED_MEMORY_USAGE = 0; +let MAX_ALLOWED_MEMORY_USAGE_INCREMENT = 15; +const { randomUUID } = require("crypto"); + +const payload = new Buffer(1024 * 1024 * 1, "A".charCodeAt(0)).toString("utf-8"); +async function writeLargeFile() { + const dest = `s3://${randomUUID()}`; + await Bun.write(dest, payload); + await Bun.file(dest).unlink(); +} +async function run() { + { + // base line + await Promise.all(new Array(10).fill(writeLargeFile())); + await Bun.sleep(10); + Bun.gc(true); + } + MAX_ALLOWED_MEMORY_USAGE = ((process.memoryUsage.rss() / 1024 / 1024) | 0) + MAX_ALLOWED_MEMORY_USAGE_INCREMENT; + + { + await Promise.all(new Array(100).fill(writeLargeFile())); + Bun.gc(true); + } + const rss = (process.memoryUsage.rss() / 1024 / 1024) | 0; + if (rss > MAX_ALLOWED_MEMORY_USAGE) { + throw new Error("Memory usage is too high"); + } +} +await run(); diff --git a/test/js/bun/s3/s3-insecure.test.ts b/test/js/bun/s3/s3-insecure.test.ts new file mode 100644 index 0000000000..d757fff77b --- /dev/null +++ b/test/js/bun/s3/s3-insecure.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "bun:test"; +import { S3Client } from "bun"; + +describe("s3", async () => { + it("should not fail to connect when endpoint is http and not https", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response("<>lol!", { + headers: { + "Content-Type": "text/plain", + }, + status: 400, + }); + }, + }); + + const s3 = new S3Client({ + accessKeyId: "test", + secretAccessKey: "test", + endpoint: server.url.href, + bucket: "test", + }); + + const file = s3.file("hello.txt"); + let err; + try { + await file.text(); + } catch (e) { + err = e; + } + // Test we don't get ConnectionRefused + expect(err.code!).toBe("UnknownError"); + }); +}); diff --git a/test/js/bun/s3/s3-stream-leak-fixture.js b/test/js/bun/s3/s3-stream-leak-fixture.js new file mode 100644 index 0000000000..f2ad73edd7 --- /dev/null +++ b/test/js/bun/s3/s3-stream-leak-fixture.js @@ -0,0 +1,40 @@ +// Avoid using String.prototype.repeat in this file because it's very slow in +// debug builds of JavaScriptCore +let MAX_ALLOWED_MEMORY_USAGE = 0; +let MAX_ALLOWED_MEMORY_USAGE_INCREMENT = 15; +const { randomUUID } = require("crypto"); + +const s3Dest = randomUUID() + "-s3-stream-leak-fixture"; + +const s3file = Bun.s3(s3Dest); +async function readLargeFile() { + const stream = Bun.s3(s3Dest).stream(); + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + } +} +async function run(inputType) { + await s3file.write(inputType); + Bun.gc(true); + + { + // base line + await Promise.all(new Array(10).fill(readLargeFile())); + await Bun.sleep(10); + Bun.gc(true); + } + MAX_ALLOWED_MEMORY_USAGE = ((process.memoryUsage.rss() / 1024 / 1024) | 0) + MAX_ALLOWED_MEMORY_USAGE_INCREMENT; + { + await Promise.all(new Array(100).fill(readLargeFile())); + Bun.gc(true); + } + const rss = (process.memoryUsage.rss() / 1024 / 1024) | 0; + if (rss > MAX_ALLOWED_MEMORY_USAGE) { + await s3file.unlink(); + throw new Error("Memory usage is too high"); + } +} +await run(new Buffer(1024 * 1024 * 1, "A".charCodeAt(0)).toString("utf-8")); +await s3file.unlink(); diff --git a/test/js/bun/s3/s3-text-leak-fixture.js b/test/js/bun/s3/s3-text-leak-fixture.js new file mode 100644 index 0000000000..e564a9edb5 --- /dev/null +++ b/test/js/bun/s3/s3-text-leak-fixture.js @@ -0,0 +1,35 @@ +// Avoid using String.prototype.repeat in this file because it's very slow in +// debug builds of JavaScriptCore +let MAX_ALLOWED_MEMORY_USAGE = 0; +let MAX_ALLOWED_MEMORY_USAGE_INCREMENT = 15; +const { randomUUID } = require("crypto"); + +const s3Dest = randomUUID() + "-s3-stream-leak-fixture"; + +const s3file = Bun.s3(s3Dest); +async function readLargeFile() { + await Bun.s3(s3Dest).text(); +} +async function run(inputType) { + await s3file.write(inputType); + Bun.gc(true); + + { + // base line + await Promise.all(new Array(10).fill(readLargeFile())); + await Bun.sleep(10); + Bun.gc(true); + } + MAX_ALLOWED_MEMORY_USAGE = ((process.memoryUsage.rss() / 1024 / 1024) | 0) + MAX_ALLOWED_MEMORY_USAGE_INCREMENT; + { + await Promise.all(new Array(100).fill(readLargeFile())); + Bun.gc(true); + } + const rss = (process.memoryUsage.rss() / 1024 / 1024) | 0; + if (rss > MAX_ALLOWED_MEMORY_USAGE) { + await s3file.unlink(); + throw new Error("Memory usage is too high"); + } +} +await run(new Buffer(1024 * 1024 * 1, "A".charCodeAt(0)).toString("utf-8")); +await s3file.unlink(); diff --git a/test/js/bun/s3/s3-write-leak-fixture.js b/test/js/bun/s3/s3-write-leak-fixture.js new file mode 100644 index 0000000000..019b8121a0 --- /dev/null +++ b/test/js/bun/s3/s3-write-leak-fixture.js @@ -0,0 +1,31 @@ +// Avoid using String.prototype.repeat in this file because it's very slow in +// debug builds of JavaScriptCore +let MAX_ALLOWED_MEMORY_USAGE = 0; +let MAX_ALLOWED_MEMORY_USAGE_INCREMENT = 15; +const dest = process.argv.at(-1); +const { randomUUID } = require("crypto"); +const payload = new Buffer(1024 * 1024 + 1, "A".charCodeAt(0)).toString("utf-8"); +async function writeLargeFile() { + const s3file = Bun.s3(randomUUID()); + await s3file.write(payload); + await s3file.unlink(); +} +async function run() { + { + // base line + await Promise.all(new Array(10).fill(writeLargeFile())); + await Bun.sleep(10); + Bun.gc(true); + } + MAX_ALLOWED_MEMORY_USAGE = ((process.memoryUsage.rss() / 1024 / 1024) | 0) + MAX_ALLOWED_MEMORY_USAGE_INCREMENT; + + { + await Promise.all(new Array(100).fill(writeLargeFile())); + Bun.gc(true); + } + const rss = (process.memoryUsage.rss() / 1024 / 1024) | 0; + if (rss > MAX_ALLOWED_MEMORY_USAGE) { + throw new Error("Memory usage is too high"); + } +} +await run(); diff --git a/test/js/bun/s3/s3-writer-leak-fixture.js b/test/js/bun/s3/s3-writer-leak-fixture.js new file mode 100644 index 0000000000..80a42b3795 --- /dev/null +++ b/test/js/bun/s3/s3-writer-leak-fixture.js @@ -0,0 +1,39 @@ +// Avoid using String.prototype.repeat in this file because it's very slow in +// debug builds of JavaScriptCore +let MAX_ALLOWED_MEMORY_USAGE = 0; +let MAX_ALLOWED_MEMORY_USAGE_INCREMENT = 15; +const dest = process.argv.at(-1); +const { randomUUID } = require("crypto"); +const payload = new Buffer(1024 * 256, "A".charCodeAt(0)).toString("utf-8"); +async function writeLargeFile() { + const s3file = Bun.s3(randomUUID()); + const writer = s3file.writer(); + writer.write(payload); + await Bun.sleep(10); + writer.write(payload); + await Bun.sleep(10); + writer.write(payload); + await Bun.sleep(10); + writer.write(payload); + await writer.end(); + await s3file.unlink(); +} +async function run() { + { + // base line + await Promise.all(new Array(10).fill(writeLargeFile())); + await Bun.sleep(10); + Bun.gc(true); + } + MAX_ALLOWED_MEMORY_USAGE = ((process.memoryUsage.rss() / 1024 / 1024) | 0) + MAX_ALLOWED_MEMORY_USAGE_INCREMENT; + + { + await Promise.all(new Array(100).fill(writeLargeFile())); + Bun.gc(true); + } + const rss = (process.memoryUsage.rss() / 1024 / 1024) | 0; + if (rss > MAX_ALLOWED_MEMORY_USAGE) { + throw new Error("Memory usage is too high"); + } +} +await run(); diff --git a/test/js/bun/s3/s3.leak.test.ts b/test/js/bun/s3/s3.leak.test.ts new file mode 100644 index 0000000000..4f81470722 --- /dev/null +++ b/test/js/bun/s3/s3.leak.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it } from "bun:test"; +import { bunExe, bunEnv, getSecret, tempDirWithFiles } from "harness"; +import type { S3Options } from "bun"; +import path from "path"; +const s3Options: S3Options = { + accessKeyId: getSecret("S3_R2_ACCESS_KEY"), + secretAccessKey: getSecret("S3_R2_SECRET_KEY"), + endpoint: getSecret("S3_R2_ENDPOINT"), +}; + +const S3Bucket = getSecret("S3_R2_BUCKET"); + +describe.skipIf(!s3Options.accessKeyId)("s3", () => { + describe("leak tests", () => { + it( + "s3().stream() should not leak", + async () => { + const dir = tempDirWithFiles("s3-stream-leak-fixture", { + "s3-stream-leak-fixture.js": await Bun.file(path.join(import.meta.dir, "s3-stream-leak-fixture.js")).text(), + "out.bin": "here", + }); + + const dest = path.join(dir, "out.bin"); + + const { exitCode, stderr } = Bun.spawnSync( + [bunExe(), "--smol", path.join(dir, "s3-stream-leak-fixture.js"), dest], + { + env: { + ...bunEnv, + BUN_JSC_gcMaxHeapSize: "503316", + AWS_ACCESS_KEY_ID: s3Options.accessKeyId, + AWS_SECRET_ACCESS_KEY: s3Options.secretAccessKey, + AWS_ENDPOINT: s3Options.endpoint, + AWS_BUCKET: S3Bucket, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "ignore", + }, + ); + expect(exitCode).toBe(0); + expect(stderr.toString()).toBe(""); + }, + 30 * 1000, + ); + it( + "s3().text() should not leak", + async () => { + const dir = tempDirWithFiles("s3-text-leak-fixture", { + "s3-text-leak-fixture.js": await Bun.file(path.join(import.meta.dir, "s3-text-leak-fixture.js")).text(), + "out.bin": "here", + }); + + const dest = path.join(dir, "out.bin"); + + const { exitCode, stderr } = Bun.spawnSync( + [bunExe(), "--smol", path.join(dir, "s3-text-leak-fixture.js"), dest], + { + env: { + ...bunEnv, + BUN_JSC_gcMaxHeapSize: "503316", + AWS_ACCESS_KEY_ID: s3Options.accessKeyId, + AWS_SECRET_ACCESS_KEY: s3Options.secretAccessKey, + AWS_ENDPOINT: s3Options.endpoint, + AWS_BUCKET: S3Bucket, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "ignore", + }, + ); + expect(exitCode).toBe(0); + expect(stderr.toString()).toBe(""); + }, + 30 * 1000, + ); + it( + "s3().writer().write() should not leak", + async () => { + const dir = tempDirWithFiles("s3-writer-leak-fixture", { + "s3-writer-leak-fixture.js": await Bun.file(path.join(import.meta.dir, "s3-writer-leak-fixture.js")).text(), + "out.bin": "here", + }); + + const dest = path.join(dir, "out.bin"); + + const { exitCode, stderr } = Bun.spawnSync( + [bunExe(), "--smol", path.join(dir, "s3-writer-leak-fixture.js"), dest], + { + env: { + ...bunEnv, + BUN_JSC_gcMaxHeapSize: "503316", + AWS_ACCESS_KEY_ID: s3Options.accessKeyId, + AWS_SECRET_ACCESS_KEY: s3Options.secretAccessKey, + AWS_ENDPOINT: s3Options.endpoint, + AWS_BUCKET: S3Bucket, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "ignore", + }, + ); + expect(exitCode).toBe(0); + expect(stderr.toString()).toBe(""); + }, + 30 * 1000, + ); + it( + "s3().write() should not leak", + async () => { + const dir = tempDirWithFiles("s3-write-leak-fixture", { + "s3-write-leak-fixture.js": await Bun.file(path.join(import.meta.dir, "s3-write-leak-fixture.js")).text(), + "out.bin": "here", + }); + + const dest = path.join(dir, "out.bin"); + + const { exitCode, stderr } = Bun.spawnSync( + [bunExe(), "--smol", path.join(dir, "s3-write-leak-fixture.js"), dest], + { + env: { + ...bunEnv, + BUN_JSC_gcMaxHeapSize: "503316", + AWS_ACCESS_KEY_ID: s3Options.accessKeyId, + AWS_SECRET_ACCESS_KEY: s3Options.secretAccessKey, + AWS_ENDPOINT: s3Options.endpoint, + AWS_BUCKET: S3Bucket, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "ignore", + }, + ); + expect(exitCode).toBe(0); + expect(stderr.toString()).toBe(""); + }, + 30 * 1000, + ); + + it( + "Bun.write should not leak", + async () => { + const dir = tempDirWithFiles("bun-write-leak-fixture", { + "bun-write-leak-fixture.js": await Bun.file(path.join(import.meta.dir, "bun-write-leak-fixture.js")).text(), + "out.bin": "here", + }); + + const dest = path.join(dir, "out.bin"); + + const { exitCode, stderr } = Bun.spawnSync( + [bunExe(), "--smol", path.join(dir, "bun-write-leak-fixture.js"), dest], + { + env: { + ...bunEnv, + BUN_JSC_gcMaxHeapSize: "503316", + AWS_ACCESS_KEY_ID: s3Options.accessKeyId, + AWS_SECRET_ACCESS_KEY: s3Options.secretAccessKey, + AWS_ENDPOINT: s3Options.endpoint, + AWS_BUCKET: S3Bucket, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "ignore", + }, + ); + expect(exitCode).toBe(0); + expect(stderr.toString()).toBe(""); + }, + 30 * 1000, + ); + }); +}); diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts new file mode 100644 index 0000000000..99f997ef2d --- /dev/null +++ b/test/js/bun/s3/s3.test.ts @@ -0,0 +1,1064 @@ +import { describe, expect, it, beforeAll, afterAll } from "bun:test"; +import { bunExe, bunEnv, getSecret, tempDirWithFiles, isLinux } from "harness"; +import { randomUUID } from "crypto"; +import { S3Client, s3, file, which } from "bun"; +const S3 = (...args) => new S3Client(...args); +import child_process from "child_process"; +import type { S3Options } from "bun"; +import path from "path"; + +const dockerCLI = which("docker") as string; +function isDockerEnabled(): boolean { + if (!dockerCLI) { + return false; + } + + try { + const info = child_process.execSync(`${dockerCLI} info`, { stdio: ["ignore", "pipe", "inherit"] }); + return info.toString().indexOf("Server Version:") !== -1; + } catch (error) { + return false; + } +} + +const allCredentials = [ + { + accessKeyId: getSecret("S3_R2_ACCESS_KEY"), + secretAccessKey: getSecret("S3_R2_SECRET_KEY"), + endpoint: getSecret("S3_R2_ENDPOINT"), + bucket: getSecret("S3_R2_BUCKET"), + service: "R2" as string, + }, +]; + +if (isDockerEnabled()) { + const minio_dir = tempDirWithFiles("minio", {}); + const result = child_process.spawnSync( + "docker", + [ + "run", + "-d", + "--name", + "minio", + "-p", + "9000:9000", + "-p", + "9001:9001", + "-e", + "MINIO_ROOT_USER=minioadmin", + "-e", + "MINIO_ROOT_PASSWORD=minioadmin", + "-v", + `${minio_dir}:/data`, + "minio/minio", + "server", + "--console-address", + ":9001", + "/data", + ], + { + stdio: ["ignore", "pipe", "pipe"], + }, + ); + + if (result.error) { + if (!result.error.message.includes('The container name "/minio" is already in use by container')) + throw result.error; + } + // wait for minio to be ready + await Bun.sleep(1_000); + + /// create a bucket + child_process.spawnSync(dockerCLI, [`exec`, `minio`, `mc`, `mb`, `data/buntest`], { + stdio: "ignore", + }); + + allCredentials.push({ + endpoint: "http://localhost:9000", // MinIO endpoint + accessKeyId: "minioadmin", + secretAccessKey: "minioadmin", + bucket: "buntest", + service: "MinIO" as string, + }); +} +for (let credentials of allCredentials) { + describe(`${credentials.service}`, () => { + const s3Options: S3Options = { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + endpoint: credentials.endpoint, + }; + + const S3Bucket = credentials.bucket; + + function makePayLoadFrom(text: string, size: number): string { + while (Buffer.byteLength(text) < size) { + text += text; + } + return text.slice(0, size); + } + + // 10 MiB big enough to Multipart upload in more than one part + const bigPayload = makePayLoadFrom("Bun is the best runtime ever", 10 * 1024 * 1024); + const bigishPayload = makePayLoadFrom("Bun is the best runtime ever", 1 * 1024 * 1024); + + describe.skipIf(!s3Options.accessKeyId)("s3", () => { + for (let bucketInName of [true, false]) { + describe("fetch", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + var tmp_filename: string; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + s3: options, + }); + expect(result.status).toBe(200); + }); + + afterAll(async () => { + const result = await fetch(tmp_filename, { + method: "DELETE", + s3: options, + }); + expect(result.status).toBe(204); + }); + + it("should download file via fetch GET", async () => { + const result = await fetch(tmp_filename, { s3: options }); + expect(result.status).toBe(200); + expect(await result.text()).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const result = await fetch(tmp_filename, { + headers: { "range": "bytes=6-10" }, + s3: options, + }); + expect(result.status).toBe(206); + expect(await result.text()).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const result = await fetch(tmp_filename, { + method: "HEAD", + s3: options, + }); + expect(result.status).toBe(200); // 404 if do not exists + expect(result.headers.get("content-length")).toBe("10"); // content-length + }); + + it("should check if a key does not exist", async () => { + const result = await fetch(tmp_filename + "-does-not-exist", { s3: options }); + expect(result.status).toBe(404); + }); + + it("should be able to set content-type", async () => { + { + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + headers: { + "Content-Type": "application/json", + }, + s3: options, + }); + expect(result.status).toBe(200); + const response = await fetch(tmp_filename, { s3: options }); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + { + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + headers: { + "Content-Type": "text/plain", + }, + s3: options, + }); + expect(result.status).toBe(200); + const response = await fetch(tmp_filename, { s3: options }); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + }); + + it("should be able to upload large files", async () => { + // 10 MiB big enough to Multipart upload in more than one part + const buffer = Buffer.alloc(1 * 1024 * 1024, "a"); + { + await fetch(tmp_filename, { + method: "PUT", + body: async function* () { + for (let i = 0; i < 10; i++) { + await Bun.sleep(10); + yield buffer; + } + }, + s3: options, + }).then(res => res.text()); + + const result = await fetch(tmp_filename, { method: "HEAD", s3: options }); + expect(result.status).toBe(200); + expect(result.headers.get("content-length")).toBe((buffer.byteLength * 10).toString()); + } + }, 20_000); + }); + }); + + describe("Bun.S3Client", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; + const options = bucketInName ? null : { bucket: S3Bucket }; + + var bucket = S3(s3Options); + beforeAll(async () => { + const file = bucket.file(tmp_filename, options); + await file.write("Hello Bun!"); + }); + + afterAll(async () => { + const file = bucket.file(tmp_filename, options); + await file.unlink(); + }); + + it("should download file via Bun.s3().text()", async () => { + const file = bucket.file(tmp_filename, options); + const text = await file.text(); + expect(text).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const file = bucket.file(tmp_filename, options); + const text = await file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); + it("should download range with 0 offset", async () => { + const file = bucket.file(tmp_filename, options); + const text = await file.slice(0, 5).text(); + expect(text).toBe("Hello"); + }); + + it("should check if a key exists or content-length", async () => { + const file = bucket.file(tmp_filename, options); + const exists = await file.exists(); + expect(exists).toBe(true); + const stat = await file.stat(); + expect(stat.size).toBe(10); + }); + + it("should check if a key does not exist", async () => { + const file = bucket.file(tmp_filename + "-does-not-exist", options); + const exists = await file.exists(); + expect(exists).toBe(false); + }); + + it("should be able to set content-type", async () => { + { + const s3file = bucket.file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/css" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = bucket.file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + + { + const s3file = bucket.file(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + + { + await bucket.write(tmp_filename, "Hello Bun!", { ...options, type: "application/xml" }); + const response = await fetch(bucket.file(tmp_filename, options).presign()); + expect(response.headers.get("content-type")).toStartWith("application/xml"); + } + }); + + it("should be able to upload large files using bucket.write + readable Request", async () => { + { + await bucket.write( + tmp_filename, + new Request("https://example.com", { + method: "PUT", + body: async function* () { + for (let i = 0; i < 10; i++) { + if (i % 5 === 0) { + await Bun.sleep(10); + } + yield bigishPayload; + } + }, + }), + options, + ); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigishPayload) * 10); + } + }, 10_000); + + it("should be able to upload large files in one go using bucket.write", async () => { + { + await bucket.write(tmp_filename, bigPayload, options); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await bucket.file(tmp_filename, options).text()).toBe(bigPayload); + } + }, 10_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = bucket.file(tmp_filename, options); + await s3File.write(bigPayload); + const stat = await s3File.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + }); + }); + + describe("Bun.file", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + const s3file = file(tmp_filename, options); + await s3file.write("Hello Bun!"); + }); + + afterAll(async () => { + const s3file = file(tmp_filename, options); + await s3file.unlink(); + }); + + it("should download file via Bun.file().text()", async () => { + const s3file = file(tmp_filename, options); + const text = await s3file.text(); + expect(text).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const s3file = file(tmp_filename, options); + const text = await s3file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const s3file = file(tmp_filename, options); + const exists = await s3file.exists(); + expect(exists).toBe(true); + const stat = await s3file.stat(); + expect(stat.size).toBe(10); + }); + + it("should check if a key does not exist", async () => { + const s3file = file(tmp_filename + "-does-not-exist", options); + const exists = await s3file.exists(); + expect(exists).toBe(false); + }); + + it("should be able to set content-type", async () => { + { + const s3file = file(tmp_filename, { ...options, type: "text/css" }); + await s3file.write("Hello Bun!"); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + + { + const s3file = file(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + }); + + it("should be able to upload large files in one go using Bun.write", async () => { + { + await Bun.write(file(tmp_filename, options), bigPayload); + expect(await S3Client.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await file(tmp_filename, options).text()).toEqual(bigPayload); + } + }, 15_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = file(tmp_filename, options); + await s3File.write(bigPayload); + expect(s3File.size).toBeNaN(); + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + }); + }); + + describe("Bun.s3", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + const s3file = s3(tmp_filename, options); + await s3file.write("Hello Bun!"); + }); + + afterAll(async () => { + const s3file = s3(tmp_filename, options); + await s3file.unlink(); + }); + + it("should download file via Bun.s3().text()", async () => { + const s3file = s3(tmp_filename, options); + const text = await s3file.text(); + expect(text).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const s3file = s3(tmp_filename, options); + const text = await s3file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const s3file = s3(tmp_filename, options); + const exists = await s3file.exists(); + expect(exists).toBe(true); + expect(s3file.size).toBeNaN(); + const stat = await s3file.stat(); + expect(stat.size).toBe(10); + expect(stat.etag).toBeDefined(); + + expect(stat.lastModified).toBeDefined(); + }); + + it("should check if a key does not exist", async () => { + const s3file = s3(tmp_filename + "-does-not-exist", options); + const exists = await s3file.exists(); + expect(exists).toBe(false); + }); + + it("presign url", async () => { + const s3file = s3(tmp_filename, options); + const response = await fetch(s3file.presign()); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello Bun!"); + }); + + it("should be able to set content-type", async () => { + { + const s3file = s3(tmp_filename, { ...options, type: "text/css" }); + await s3file.write("Hello Bun!"); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = s3(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + + { + const s3file = s3(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + }); + + it("should be able to upload large files in one go using Bun.write", async () => { + { + const s3file = s3(tmp_filename, options); + await Bun.write(s3file, bigPayload); + const stat = await s3file.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(stat.etag).toBeDefined(); + + expect(stat.lastModified).toBeDefined(); + expect(await s3file.text()).toBe(bigPayload); + } + }, 10_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = s3(tmp_filename, options); + await s3File.write(bigPayload); + const stat = await s3File.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(stat.etag).toBeDefined(); + + expect(stat.lastModified).toBeDefined(); + + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + + describe("readable stream", () => { + afterAll(async () => { + await Promise.all([ + s3(tmp_filename + "-readable-stream", options).unlink(), + s3(tmp_filename + "-readable-stream-big", options).unlink(), + ]); + }); + it("should work with small files", async () => { + const s3file = s3(tmp_filename + "-readable-stream", options); + await s3file.write("Hello Bun!"); + const stream = s3file.stream(); + const reader = stream.getReader(); + let bytes = 0; + let chunks: Array = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + bytes += value?.length ?? 0; + + if (value) chunks.push(value as Buffer); + } + expect(bytes).toBe(10); + expect(Buffer.concat(chunks)).toEqual(Buffer.from("Hello Bun!")); + }); + it("should work with large files ", async () => { + const s3file = s3(tmp_filename + "-readable-stream-big", options); + await s3file.write(bigishPayload); + const stream = s3file.stream(); + const reader = stream.getReader(); + let bytes = 0; + let chunks: Array = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + bytes += value?.length ?? 0; + if (value) chunks.push(value as Buffer); + } + expect(bytes).toBe(Buffer.byteLength(bigishPayload)); + expect(Buffer.concat(chunks).toString()).toBe(bigishPayload); + }, 30_000); + }); + }); + }); + } + describe("special characters", () => { + it("should allow special characters in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`🌈🦄${randomUUID()}.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow forward slashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}/test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow backslashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}\\test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow starting with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + { + const s3file = s3(`/${randomUUID()}test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + { + const s3file = s3(`\\${randomUUID()}test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + expect().pass(); + }); + + it("should allow ending with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + { + const s3file = s3(`${randomUUID()}/`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + { + const s3file = s3(`${randomUUID()}\\`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + expect().pass(); + }); + }); + + describe("static methods", () => { + it("its defined", () => { + expect(S3Client).toBeDefined(); + expect(S3Client.write).toBeDefined(); + expect(S3Client.file).toBeDefined(); + expect(S3Client.stat).toBeDefined(); + expect(S3Client.unlink).toBeDefined(); + expect(S3Client.exists).toBeDefined(); + expect(S3Client.presign).toBeDefined(); + expect(S3Client.size).toBeDefined(); + expect(S3Client.delete).toBeDefined(); + }); + it("should work", async () => { + const filename = randomUUID() + ".txt"; + await S3Client.write(filename, "Hello Bun!", { ...s3Options, bucket: S3Bucket }); + expect(await S3Client.file(filename, { ...s3Options, bucket: S3Bucket }).text()).toBe("Hello Bun!"); + const stat = await S3Client.stat(filename, { ...s3Options, bucket: S3Bucket }); + expect(stat.size).toBe(10); + expect(stat.etag).toBeString(); + expect(stat.lastModified).toBeValidDate(); + expect(stat.type).toBe("text/plain;charset=utf-8"); + const url = S3Client.presign(filename, { ...s3Options, bucket: S3Bucket }); + expect(url).toBeDefined(); + const response = await fetch(url); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello Bun!"); + await S3Client.unlink(filename, { ...s3Options, bucket: S3Bucket }); + expect().pass(); + }); + }); + describe("errors", () => { + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file("./do-not-exist.txt")); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ENOENT"); + expect(e?.path).toBe("./do-not-exist.txt"); + expect(e?.syscall).toBe("open"); + } + }); + + it("Bun.write(s3file, file) should work with empty file", async () => { + const dir = tempDirWithFiles("fsr", { + "hello.txt": "", + }); + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file(path.join(dir, "hello.txt"))); + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: S3Bucket }), + ); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("NoSuchKey"); + expect(e?.path).toBe("do-not-exist.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: "does-not-exists" }), + ); + expect.unreachable(); + } catch (e: any) { + expect(["AccessDenied", "NoSuchBucket"]).toContain(e?.code); + expect(e?.path).toBe("do-not-exist.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + it("should error if bucket is missing", async () => { + try { + await Bun.write(s3("test.txt", s3Options), "Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.name).toBe("S3Error"); + } + }); + + it("should error if bucket is missing on payload", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), s3("test2.txt", s3Options)); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.path).toBe("test2.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + + it("should error when invalid method", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + const s3file = fn("method-test", { + ...s3Options, + bucket: S3Bucket, + }); + + try { + await s3file.presign({ method: "OPTIONS" }); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_METHOD"); + } + }), + ); + }); + + it("should error when path is too long", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + try { + const s3file = fn("test" + "a".repeat(4096), { + ...s3Options, + bucket: S3Bucket, + }); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["ENAMETOOLONG", "ERR_S3_INVALID_PATH"]).toContain(e?.code); + } + }), + ); + }); + }); + describe("credentials", () => { + it("should error with invalid access key id", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + accessKeyId: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["InvalidAccessKeyId", "InvalidArgument"]).toContain(e?.code); + } + }), + ); + }); + it("should error with invalid secret key id", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + secretAccessKey: "invalid", + }); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["SignatureDoesNotMatch", "AccessDenied"]).toContain(e?.code); + } + }), + ); + }); + + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "🙂.🥯", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); + } + }), + ); + }); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, // credentials and endpoint dont match + endpoint: "s3.us-west-1.amazonaws.com", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("PermanentRedirect"); + } + }), + ); + }); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "..asd.@%&&&%%", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); + } + }), + ); + }); + + it("should error with invalid bucket", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + ...s3Options, + bucket: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["AccessDenied", "NoSuchBucket"]).toContain(e?.code); + expect(e?.name).toBe("S3Error"); + } + }), + ); + }); + + it("should error when missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + bucket: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); + }); + it("should error when presign missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + const s3file = fn("method-test", { + bucket: S3Bucket, + }); + + try { + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); + }); + + it("should error when presign with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.endpoint = Buffer.alloc(1024, "a").toString(); + + try { + const s3file = fn(randomUUID(), options); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_ENDPOINT"); + } + }), + ); + }); + it("should error when presign with invalid token", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.sessionToken = Buffer.alloc(4096, "a").toString(); + + try { + const s3file = fn(randomUUID(), options); + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_SESSION_TOKEN"); + } + }), + ); + }); + }); + + describe("S3 static methods", () => { + describe("presign", () => { + it("should work", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("default endpoint and region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = undefined; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-east-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("default endpoint + region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = "us-west-1"; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-west-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("should work with expires", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign({ + expiresIn: 10, + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("should work with acl", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign({ + expiresIn: 10, + acl: "public-read", + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Acl=public-read")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + + it("s3().presign() should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ + expiresIn: 10, + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ + expiresIn: 10, + endpoint: "https://s3.bun.sh", + }); + expect(url).toBeDefined(); + expect(url.includes("https://s3.bun.sh")).toBe(true); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://folder/credentials-test", s3Options).presign({ + expiresIn: 10, + bucket: "my-bucket", + }); + expect(url).toBeDefined(); + expect(url.includes("my-bucket")).toBe(true); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + }); + + it("exists, write, size, unlink should work", async () => { + const fullPath = randomUUID(); + const bucket = S3({ + ...s3Options, + bucket: S3Bucket, + }); + expect(await bucket.exists(fullPath)).toBe(false); + + await bucket.write(fullPath, "bun"); + expect(await bucket.exists(fullPath)).toBe(true); + expect(await bucket.size(fullPath)).toBe(3); + await bucket.unlink(fullPath); + expect(await bucket.exists(fullPath)).toBe(false); + }); + + it("should be able to upload a slice", async () => { + const filename = randomUUID(); + const fullPath = `s3://${S3Bucket}/${filename}`; + const s3file = s3(fullPath, s3Options); + await s3file.write("Hello Bun!"); + const slice = s3file.slice(6, 10); + expect(await slice.text()).toBe("Bun!"); + expect(await s3file.text()).toBe("Hello Bun!"); + + await s3file.write(slice); + const text = await s3file.text(); + expect(text).toBe("Bun!"); + await s3file.unlink(); + }); + }); + }); + }); +} diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 31b59ffa41..8e99647812 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -232,7 +232,7 @@ describe("bunshell", () => { }); } - // funny/crazy edgecases thanks to @paperdave and @Electroid + // funny/crazy edgecases thanks to @paperclover and @Electroid doTest(`echo "$(echo 1; echo 2)"`, "1\n2\n"); doTest(`echo "$(echo "1" ; echo "2")"`, "1\n2\n"); doTest(`echo $(echo 1; echo 2)`, "1 2\n"); @@ -781,7 +781,7 @@ booga" test("error without recursive option", async () => { const { stderr } = await $`rm -v ${temp_dir}`; - expect(stderr.toString()).toEqual(`rm: ${temp_dir}: is a directory\n`); + expect(stderr.toString()).toEqual(`rm: ${temp_dir}: Is a directory\n`); }); test("recursive", async () => { diff --git a/test/js/bun/spawn/fixtures/ipc-child-bun.js b/test/js/bun/spawn/fixtures/ipc-child-bun.js new file mode 100644 index 0000000000..b5e3efed52 --- /dev/null +++ b/test/js/bun/spawn/fixtures/ipc-child-bun.js @@ -0,0 +1,7 @@ +console.log("c start"); +process.on("message", message => { + console.log("c", message); + process.send(message); + process.exit(0); +}); +console.log("c end"); diff --git a/test/js/bun/spawn/fixtures/ipc-child-node.js b/test/js/bun/spawn/fixtures/ipc-child-node.js new file mode 100644 index 0000000000..b5e3efed52 --- /dev/null +++ b/test/js/bun/spawn/fixtures/ipc-child-node.js @@ -0,0 +1,7 @@ +console.log("c start"); +process.on("message", message => { + console.log("c", message); + process.send(message); + process.exit(0); +}); +console.log("c end"); diff --git a/test/js/bun/spawn/fixtures/ipc-parent-bun.js b/test/js/bun/spawn/fixtures/ipc-parent-bun.js new file mode 100644 index 0000000000..dfebf93f69 --- /dev/null +++ b/test/js/bun/spawn/fixtures/ipc-parent-bun.js @@ -0,0 +1,14 @@ +const path = require("node:path"); + +console.log("p start"); +const child = Bun.spawn(["node", path.resolve(import.meta.dir, "ipc-child-node.js")], { + ipc(message) { + console.log("p", message); + process.exit(0); + }, + stdio: ["ignore", "inherit", "inherit"], + serialization: "json", +}); + +child.send("I am your father"); +console.log("p end"); diff --git a/test/js/bun/spawn/fixtures/ipc-parent-node.js b/test/js/bun/spawn/fixtures/ipc-parent-node.js new file mode 100644 index 0000000000..cb9c40b473 --- /dev/null +++ b/test/js/bun/spawn/fixtures/ipc-parent-node.js @@ -0,0 +1,15 @@ +const path = require("node:path"); +const child_process = require("node:child_process"); + +console.log("p start"); + +const child = child_process.spawn(process.argv[2], [path.resolve(__dirname, "ipc-child-bun.js")], { + stdio: ["ignore", "inherit", "inherit", "ipc"], +}); +child.on("message", message => { + console.log("p", message); + process.exit(0); +}); + +child.send("I am your father"); +console.log("p end"); diff --git a/test/js/bun/spawn/spawn-env.test.ts b/test/js/bun/spawn/spawn-env.test.ts new file mode 100644 index 0000000000..5d2e34cc0e --- /dev/null +++ b/test/js/bun/spawn/spawn-env.test.ts @@ -0,0 +1,24 @@ +import { test, expect } from "bun:test"; +import { spawn } from "bun"; +import { bunExe } from "harness"; + +test("spawn env", async () => { + const env = {}; + Object.defineProperty(env, "LOL", { + get() { + throw new Error("Bad!!"); + }, + configurable: false, + enumerable: true, + }); + + // This was the minimum to reliably cause a crash in Bun < v1.1.42 + for (let i = 0; i < 1024 * 10; i++) { + try { + const result = spawn({ + env, + cmd: [bunExe(), "-e", "console.log(process.env.LOL)"], + }); + } catch (e) {} + } +}); diff --git a/test/js/bun/spawn/spawn-path.test.ts b/test/js/bun/spawn/spawn-path.test.ts new file mode 100644 index 0000000000..d47876c33e --- /dev/null +++ b/test/js/bun/spawn/spawn-path.test.ts @@ -0,0 +1,26 @@ +import { test, expect } from "bun:test"; +import { chmodSync } from "fs"; +import { isWindows, tempDirWithFiles, bunEnv } from "harness"; +import path from "path"; + +test.skipIf(isWindows)("spawn uses PATH from env if present", async () => { + const tmpDir = await tempDirWithFiles("spawn-path", { + "test-script": `#!/usr/bin/env bash +echo "hello from script"`, + }); + + chmodSync(path.join(tmpDir, "test-script"), 0o777); + + const proc = Bun.spawn(["test-script"], { + env: { + ...bunEnv, + PATH: tmpDir + ":" + bunEnv.PATH, + }, + }); + + const output = await new Response(proc.stdout).text(); + expect(output.trim()).toBe("hello from script"); + + const status = await proc.exited; + expect(status).toBe(0); +}); diff --git a/test/js/bun/spawn/spawn.ipc.bun-node.test.ts b/test/js/bun/spawn/spawn.ipc.bun-node.test.ts new file mode 100644 index 0000000000..f03212e339 --- /dev/null +++ b/test/js/bun/spawn/spawn.ipc.bun-node.test.ts @@ -0,0 +1,21 @@ +import { spawn } from "bun"; +import { test, expect, it } from "bun:test"; +import { bunExe } from "harness"; +import path from "path"; + +test("ipc with json serialization still works when bun is parent and not the child", async () => { + const child = Bun.spawn([bunExe(), path.resolve(import.meta.dir, "fixtures", "ipc-parent-bun.js")], { + stdio: ["ignore", "pipe", "pipe"], + }); + await child.exited; + expect(await new Response(child.stdout).text()).toEqual( + `p start +p end +c start +c end +c I am your father +p I am your father +`, + ); + expect(await new Response(child.stderr).text()).toEqual(""); +}); diff --git a/test/js/bun/spawn/spawn.ipc.node-bun.test.ts b/test/js/bun/spawn/spawn.ipc.node-bun.test.ts new file mode 100644 index 0000000000..a41cdf8568 --- /dev/null +++ b/test/js/bun/spawn/spawn.ipc.node-bun.test.ts @@ -0,0 +1,22 @@ +import { spawn } from "bun"; +import { test, expect, it } from "bun:test"; +import { bunExe, nodeExe } from "harness"; +import path from "path"; + +test("ipc with json serialization still works when bun is not the parent and the child", async () => { + // prettier-ignore + const child = Bun.spawn(["node", "--no-warnings", path.resolve(import.meta.dir, "fixtures", "ipc-parent-node.js"), bunExe()], { + stdio: ["ignore", "pipe", "pipe"], + }); + await child.exited; + expect(await new Response(child.stderr).text()).toEqual(""); + expect(await new Response(child.stdout).text()).toEqual( + `p start +p end +c start +c end +c I am your father +p I am your father +`, + ); +}); diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index 4f2adf2035..50e2c01426 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -81,7 +81,7 @@ for (let [gcTick, label] of [ cmd: ["node", "-e", "console.log('hi')"], cwd: "./this-should-not-exist", }); - }).toThrow("No such file or directory"); + }).toThrow("no such file or directory"); }); }); @@ -525,7 +525,7 @@ for (let [gcTick, label] of [ cmd: ["node", "-e", "console.log('hi')"], cwd: "./this-should-not-exist", }); - }).toThrow("No such file or directory"); + }).toThrow("no such file or directory"); }); }); }); diff --git a/test/js/bun/sqlite/sqlite.test.js b/test/js/bun/sqlite/sqlite.test.js index ae57c6cad8..28795ec13f 100644 --- a/test/js/bun/sqlite/sqlite.test.js +++ b/test/js/bun/sqlite/sqlite.test.js @@ -1242,27 +1242,33 @@ it("should dispose", () => { it("can continue to use existing statements after database has been GC'd", async () => { let called = false; - const registry = new FinalizationRegistry(() => { - called = true; - }); - function leakTheStatement() { - const db = new Database(":memory:"); - console.log("---"); - db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); - db.exec("INSERT INTO foo (name) VALUES ('foo')"); - const prepared = db.prepare("SELECT * FROM foo"); - registry.register(db); - return prepared; + async function run() { + const registry = new FinalizationRegistry(() => { + called = true; + }); + function leakTheStatement() { + const db = new Database(":memory:"); + console.log("---"); + db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); + db.exec("INSERT INTO foo (name) VALUES ('foo')"); + const prepared = db.prepare("SELECT * FROM foo"); + registry.register(db); + return prepared; + } + + const stmt = leakTheStatement(); + + Bun.gc(true); + await Bun.sleep(1); + Bun.gc(true); + expect(stmt.all()).toEqual([{ id: 1, name: "foo" }]); + stmt.finalize(); + expect(() => stmt.all()).toThrow(); } - - const stmt = leakTheStatement(); - + await run(); Bun.gc(true); await Bun.sleep(1); Bun.gc(true); - expect(stmt.all()).toEqual([{ id: 1, name: "foo" }]); - stmt.finalize(); - expect(() => stmt.all()).toThrow(); if (!isWindows) { // on Windows, FinalizationRegistry is more flaky than on POSIX. expect(called).toBe(true); diff --git a/test/js/bun/symbols.test.ts b/test/js/bun/symbols.test.ts index 67127e3555..e255954ec6 100644 --- a/test/js/bun/symbols.test.ts +++ b/test/js/bun/symbols.test.ts @@ -6,7 +6,7 @@ import { semver } from "bun"; const BUN_EXE = bunExe(); if (process.platform === "linux") { - test("objdump -T does not include symbols from glibc > 2.27", async () => { + test("objdump -T does not include symbols from glibc > 2.26", async () => { const objdump = Bun.which("objdump") || Bun.which("llvm-objdump"); if (!objdump) { throw new Error("objdump executable not found. Please install it."); @@ -22,7 +22,7 @@ if (process.platform === "linux") { if (version.startsWith("2..")) { version = "2." + version.slice(3); } - if (semver.order(version, "2.27.0") >= 0) { + if (semver.order(version, "2.26.0") > 0) { errors.push({ symbol: line.slice(line.lastIndexOf(")") + 1).trim(), "glibc version": version, @@ -31,7 +31,7 @@ if (process.platform === "linux") { } } if (errors.length) { - throw new Error(`Found glibc symbols >= 2.27. This breaks Amazon Linux 2 and Vercel. + throw new Error(`Found glibc symbols > 2.26. This breaks Amazon Linux 2 and Vercel. ${Bun.inspect.table(errors, { colors: true })} To fix this, add it to -Wl,-wrap=symbol in the linker flags and update workaround-missing-symbols.cpp.`); diff --git a/test/js/bun/test/__snapshots__/test-interop.js.snap b/test/js/bun/test/__snapshots__/test-interop.js.snap index c626a5ab56..39cfad68b8 100644 --- a/test/js/bun/test/__snapshots__/test-interop.js.snap +++ b/test/js/bun/test/__snapshots__/test-interop.js.snap @@ -1,3 +1,7 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP exports[`expect() toMatchSnapshot to return undefined 1`] = `"abc"`; + +exports[`expect() toThrowErrorMatchingSnapshot to return undefined 1`] = `undefined`; + +exports[`expect() toThrowErrorMatchingSnapshot to return undefined: undefined 1`] = `undefined`; diff --git a/test/js/bun/test/__snapshots__/test-test.test.ts.snap b/test/js/bun/test/__snapshots__/test-test.test.ts.snap index aad0206a46..d07cc41e5a 100644 --- a/test/js/bun/test/__snapshots__/test-test.test.ts.snap +++ b/test/js/bun/test/__snapshots__/test-test.test.ts.snap @@ -13,7 +13,7 @@ my-test.test.js: 5 | throw new Error('## stage beforeAll ##'); ^ error: ## stage beforeAll ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) ------------------------------- (pass) my-test @@ -35,7 +35,7 @@ my-test.test.js: 5 | throw new Error('## stage beforeEach ##'); ^ error: ## stage beforeEach ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) (fail) my-test 0 pass @@ -58,7 +58,7 @@ my-test.test.js: 5 | throw new Error('## stage afterEach ##'); ^ error: ## stage afterEach ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) ------------------------------- @@ -83,7 +83,7 @@ my-test.test.js: 5 | throw new Error('## stage afterAll ##'); ^ error: ## stage afterAll ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) ------------------------------- @@ -107,7 +107,7 @@ my-test.test.js: 5 | throw new Error('## stage describe ##'); ^ error: ## stage describe ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) at /my-test.test.js:3:1 ------------------------------- diff --git a/test/js/bun/test/expect-extend.test.js b/test/js/bun/test/expect-extend.test.js index ee707d0a78..05e98ae0b1 100644 --- a/test/js/bun/test/expect-extend.test.js +++ b/test/js/bun/test/expect-extend.test.js @@ -354,3 +354,27 @@ it("should support asymmetric matchers", () => { expect({ a: "test" }).not._toCustomEqual({ a: expect.any(Number) }); expect(() => expect(1).not._toCustomEqual(expect.any(Number))).toThrow(); }); + +it("works on prototypes", () => { + const Bar = { + _toBeBar() { + return { pass: true }; + }, + }; + const Foo = Object.create(Bar); + + expect.extend(Foo); + expect(123)._toBeBar(); +}); + +it("works on classes", () => { + class Bar { + _toBeBar() { + return { pass: true }; + } + } + class Foo extends Bar {} + + expect.extend(new Foo()); + expect(123)._toBeBar(); +}); diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 4f5b339ffb..cc3ff40dce 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -4744,7 +4744,7 @@ describe("expect()", () => { expect(expect("abc").toMatch("a")).toBeUndefined(); }); test.todo("toMatchInlineSnapshot to return undefined", () => { - expect(expect("abc").toMatchInlineSnapshot()).toBeUndefined(); + expect(expect("abc").toMatchInlineSnapshot('"abc"')).toBeUndefined(); }); test("toMatchObject to return undefined", () => { expect(expect({}).toMatchObject({})).toBeUndefined(); @@ -4768,11 +4768,19 @@ describe("expect()", () => { }).toThrow(), ).toBeUndefined(); }); - test.todo("toThrowErrorMatchingInlineSnapshot to return undefined", () => { - expect(expect(() => {}).toThrowErrorMatchingInlineSnapshot()).toBeUndefined(); + test("toThrowErrorMatchingInlineSnapshot to return undefined", () => { + expect( + expect(() => { + throw 0; + }).toThrowErrorMatchingInlineSnapshot("undefined"), + ).toBeUndefined(); }); - test.todo("toThrowErrorMatchingSnapshot to return undefined", () => { - expect(expect(() => {}).toThrowErrorMatchingSnapshot()).toBeUndefined(); + test("toThrowErrorMatchingSnapshot to return undefined", () => { + expect( + expect(() => { + throw 0; + }).toThrowErrorMatchingSnapshot("undefined"), + ).toBeUndefined(); }); test(' " " to contain ""', () => { diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index c9c29fd63d..4f813f7ffa 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -100,13 +100,16 @@ describe("mock()", () => { } test("are callable", () => { const fn = jest.fn(() => 42); + expect(fn).not.toHaveBeenCalledOnce(); expect(fn()).toBe(42); + expect(fn).toHaveBeenCalledOnce(); expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalledTimes(1); expect(fn.mock.calls).toHaveLength(1); expect(fn.mock.calls[0]).toBeEmpty(); expect(fn).toHaveBeenLastCalledWith(); expect(fn()).toBe(42); + expect(fn).not.toHaveBeenCalledOnce(); expect(fn).toHaveBeenCalledTimes(2); expect(fn.mock.calls).toHaveLength(2); expect(fn.mock.calls[1]).toBeEmpty(); diff --git a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap index d8f0026646..5effa6968c 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap +++ b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap @@ -551,3 +551,59 @@ exports[\`t2 1\`] = \`"abc\\\`def"\`; exports[\`t3 1\`] = \`"abc def ghi"\`; " `; + +exports[`snapshots property matchers 1`] = ` +"// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[\`abc 1\`] = \` +{ + "createdAt": Any, + "id": Any, + "name": "LeBron James", +} +\`; +" +`; + +exports[`inline snapshots grow file for new snapshot 1`] = ` +" + test("abc", () => { expect("hello").toMatchInlineSnapshot(\`"hello"\`) }); + " +`; + +exports[`inline snapshots backtick in test name 1`] = `"test("\`", () => {expect("abc").toMatchInlineSnapshot(\`"abc"\`);})"`; + +exports[`inline snapshots dollars curly in test name 1`] = `"test("\${}", () => {expect("abc").toMatchInlineSnapshot(\`"abc"\`);})"`; + +exports[`inline snapshots #15283 1`] = ` +"it("Should work", () => { + expect(\`This is \\\`wrong\\\`\`).toMatchInlineSnapshot(\`"This is \\\`wrong\\\`"\`); + });" +`; + +exports[`snapshots unicode surrogate halves 1`] = ` +"// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[\`abc 1\`] = \`"😊abc\\\`\\\${def} �, � "\`; +" +`; + +exports[`error inline snapshots 1`] = `"hello"`; + +exports[`error inline snapshots 2`] = `undefined`; + +exports[`error inline snapshots 3`] = `undefined`; + +exports[`error inline snapshots 4`] = `undefined`; + +exports[`error inline snapshots: hint 1`] = `undefined`; + +exports[`snapshot numbering 1`] = `"item one"`; + +exports[`snapshot numbering 2`] = `"snap"`; + +exports[`snapshot numbering 4`] = `"snap"`; + +exports[`snapshot numbering 6`] = `"hello"`; + +exports[`snapshot numbering: hinted 1`] = `"hello"`; diff --git a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts index 8451d85370..d211fd4c19 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts +++ b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts @@ -1,6 +1,7 @@ -import { $ } from "bun"; +import { $, spawnSync } from "bun"; +import { readFileSync, writeFileSync } from "fs"; import { describe, expect, it, test } from "bun:test"; -import { bunExe, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, DirectoryTree, tempDirWithFiles } from "harness"; function test1000000(arg1: any, arg218718132: any) {} @@ -167,15 +168,15 @@ it("should work with expect.anything()", () => { // expect({ a: 0 }).toMatchSnapshot({ a: expect.anything() }); }); -function defaultWrap(a: string): string { - return `test("abc", () => { expect(${a}).toMatchSnapshot() });`; +function defaultWrap(a: string, b: string = ""): string { + return `test("abc", () => { expect(${a}).toMatchSnapshot(${b}) });`; } class SnapshotTester { dir: string; targetSnapshotContents: string; isFirst: boolean = true; - constructor() { + constructor(public inlineSnapshot: boolean) { this.dir = tempDirWithFiles("snapshotTester", { "snapshot.test.ts": "" }); this.targetSnapshotContents = ""; } @@ -190,6 +191,11 @@ class SnapshotTester { contents: string, opts: { shouldNotError?: boolean; shouldGrow?: boolean; skipSnapshot?: boolean; forceUpdate?: boolean } = {}, ) { + if (this.inlineSnapshot) { + contents = contents.replaceAll("toMatchSnapshot()", "toMatchInlineSnapshot('bad')"); + this.targetSnapshotContents = contents; + } + const isFirst = this.isFirst; this.isFirst = false; await Bun.write(this.dir + "/snapshot.test.ts", contents); @@ -199,9 +205,7 @@ class SnapshotTester { // make sure it fails first: expect((await $`cd ${this.dir} && ${bunExe()} test ./snapshot.test.ts`.nothrow().quiet()).exitCode).not.toBe(0); // make sure the existing snapshot is unchanged: - expect(await Bun.file(this.dir + "/__snapshots__/snapshot.test.ts.snap").text()).toBe( - this.targetSnapshotContents, - ); + expect(await this.getSnapshotContents()).toBe(this.targetSnapshotContents); } // update snapshots now, using -u flag unless this is the first run await $`cd ${this.dir} && ${bunExe()} test ${isFirst && !opts.forceUpdate ? "" : "-u"} ./snapshot.test.ts`.quiet(); @@ -210,116 +214,605 @@ class SnapshotTester { if (!isFirst) { expect(newContents).not.toStartWith(this.targetSnapshotContents); } - if (!opts.skipSnapshot) expect(newContents).toMatchSnapshot(); + if (!opts.skipSnapshot && !this.inlineSnapshot) expect(newContents).toMatchSnapshot(); this.targetSnapshotContents = newContents; } // run, make sure snapshot does not change await $`cd ${this.dir} && ${bunExe()} test ./snapshot.test.ts`.quiet(); if (!opts.shouldGrow) { - expect(await Bun.file(this.dir + "/__snapshots__/snapshot.test.ts.snap").text()).toBe( - this.targetSnapshotContents, - ); + expect(await this.getSnapshotContents()).toBe(this.targetSnapshotContents); } else { this.targetSnapshotContents = await this.getSnapshotContents(); } } async setSnapshotFile(contents: string) { + if (this.inlineSnapshot) throw new Error("not allowed"); await Bun.write(this.dir + "/__snapshots__/snapshot.test.ts.snap", contents); this.isFirst = true; } + async getSrcContents(): Promise { + return await Bun.file(this.dir + "/snapshot.test.ts").text(); + } async getSnapshotContents(): Promise { + if (this.inlineSnapshot) return await this.getSrcContents(); return await Bun.file(this.dir + "/__snapshots__/snapshot.test.ts.snap").text(); } } -describe("snapshots", async () => { - const t = new SnapshotTester(); - await t.update(defaultWrap("''"), { skipSnapshot: true }); +for (const inlineSnapshot of [false, true]) { + describe(inlineSnapshot ? "inline snapshots" : "snapshots", async () => { + const t = new SnapshotTester(inlineSnapshot); + await t.update(defaultWrap("''", inlineSnapshot ? '`""`' : undefined), { skipSnapshot: true }); - t.test("dollars", defaultWrap("`\\$`")); - t.test("backslash", defaultWrap("`\\\\`")); - t.test("dollars curly", defaultWrap("`\\${}`")); - t.test("dollars curly 2", defaultWrap("`\\${`")); - t.test("stuff", defaultWrap(`\`æ™\n\r!!!!*5897yhduN\\"\\'\\\`Il\``)); - t.test("stuff 2", defaultWrap(`\`æ™\n\r!!!!*5897yh!uN\\"\\'\\\`Il\``)); + t.test("dollars", defaultWrap("`\\$`")); + t.test("backslash", defaultWrap("`\\\\`")); + t.test("dollars curly", defaultWrap("`\\${}`")); + t.test("dollars curly 2", defaultWrap("`\\${`")); + t.test("stuff", defaultWrap(`\`æ™\n\r!!!!*5897yhduN\\"\\'\\\`Il\``)); + t.test("stuff 2", defaultWrap(`\`æ™\n\r!!!!*5897yh!uN\\"\\'\\\`Il\``)); - t.test("regexp 1", defaultWrap("/${1..}/")); - t.test("regexp 2", defaultWrap("/${2..}/")); - t.test("string", defaultWrap('"abc"')); - t.test("string with newline", defaultWrap('"qwerty\\nioup"')); + t.test("regexp 1", defaultWrap("/${1..}/")); + t.test("regexp 2", defaultWrap("/${2..}/")); + t.test("string", defaultWrap('"abc"')); + t.test("string with newline", defaultWrap('"qwerty\\nioup"')); - t.test("null byte", defaultWrap('"1 \x00"')); - t.test("null byte 2", defaultWrap('"2 \\x00"')); + if (!inlineSnapshot) + // disabled for inline snapshot because of the bug in CodepointIterator; should be fixed by https://github.com/oven-sh/bun/pull/15163 + t.test("null byte", defaultWrap('"1 \x00"')); + t.test("null byte 2", defaultWrap('"2 \\x00"')); - t.test("backticks", defaultWrap("`This is \\`wrong\\``")); - t.test("unicode", defaultWrap("'😊abc`${def} " + "😊".substring(0, 1) + ", " + "😊".substring(1, 2) + " '")); + t.test("backticks", defaultWrap("`This is \\`wrong\\``")); + if (!inlineSnapshot) + // disabled for inline snapshot because reading the file will have U+FFFD in it rather than surrogate halves + t.test( + "unicode surrogate halves", + defaultWrap("'😊abc`${def} " + "😊".substring(0, 1) + ", " + "😊".substring(1, 2) + " '"), + ); - test("jest newline oddity", async () => { - await t.update(defaultWrap("'\\n'")); - await t.update(defaultWrap("'\\r'"), { shouldNotError: true }); - await t.update(defaultWrap("'\\r\\n'"), { shouldNotError: true }); + if (!inlineSnapshot) + // disabled for inline snapshot because it needs to update the thing + t.test( + "property matchers", + defaultWrap( + '{createdAt: new Date(), id: Math.floor(Math.random() * 20), name: "LeBron James"}', + `{createdAt: expect.any(Date), id: expect.any(Number)}`, + ), + ); + + if (!inlineSnapshot) { + // these other ones are disabled in inline snapshots + + test("jest newline oddity", async () => { + await t.update(defaultWrap("'\\n'")); + await t.update(defaultWrap("'\\r'"), { shouldNotError: true }); + await t.update(defaultWrap("'\\r\\n'"), { shouldNotError: true }); + }); + + test("don't grow file on error", async () => { + await t.setSnapshotFile("exports[`snap 1`] = `hello`goodbye`;"); + try { + await t.update(/*js*/ ` + test("t1", () => {expect("abc def ghi jkl").toMatchSnapshot();}) + test("t2", () => {expect("abc\`def").toMatchSnapshot();}) + test("t3", () => {expect("abc def ghi").toMatchSnapshot();}) + `); + } catch (e) {} + expect(await t.getSnapshotContents()).toBe("exports[`snap 1`] = `hello`goodbye`;"); + }); + + test("replaces file that fails to parse when update flag is used", async () => { + await t.setSnapshotFile("exports[`snap 1`] = `hello`goodbye`;"); + await t.update( + /*js*/ ` + test("t1", () => {expect("abc def ghi jkl").toMatchSnapshot();}) + test("t2", () => {expect("abc\`def").toMatchSnapshot();}) + test("t3", () => {expect("abc def ghi").toMatchSnapshot();}) + `, + { forceUpdate: true }, + ); + expect(await t.getSnapshotContents()).toBe( + '// Bun Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`t1 1`] = `"abc def ghi jkl"`;\n\nexports[`t2 1`] = `"abc\\`def"`;\n\nexports[`t3 1`] = `"abc def ghi"`;\n', + ); + }); + + test("grow file for new snapshot", async () => { + const t4 = new SnapshotTester(inlineSnapshot); + await t4.update(/*js*/ ` + test("abc", () => { expect("hello").toMatchSnapshot() }); + `); + await t4.update( + /*js*/ ` + test("abc", () => { expect("hello").toMatchSnapshot() }); + test("def", () => { expect("goodbye").toMatchSnapshot() }); + `, + { shouldNotError: true, shouldGrow: true }, + ); + await t4.update(/*js*/ ` + test("abc", () => { expect("hello").toMatchSnapshot() }); + test("def", () => { expect("hello").toMatchSnapshot() }); + `); + await t4.update(/*js*/ ` + test("abc", () => { expect("goodbye").toMatchSnapshot() }); + test("def", () => { expect("hello").toMatchSnapshot() }); + `); + }); + + const t2 = new SnapshotTester(inlineSnapshot); + t2.test("backtick in test name", `test("\`", () => {expect("abc").toMatchSnapshot();})`); + const t3 = new SnapshotTester(inlineSnapshot); + t3.test("dollars curly in test name", `test("\${}", () => {expect("abc").toMatchSnapshot();})`); + + const t15283 = new SnapshotTester(inlineSnapshot); + t15283.test( + "#15283", + `it("Should work", () => { + expect(\`This is \\\`wrong\\\`\`).toMatchSnapshot(); + });`, + ); + t15283.test( + "#15283 unicode", + `it("Should work", () => {expect(\`😊This is \\\`wrong\\\`\`).toMatchSnapshot()});`, + ); + } }); +} - test("don't grow file on error", async () => { - await t.setSnapshotFile("exports[`snap 1`] = `hello`goodbye`;"); - try { - await t.update(/*js*/ ` - test("t1", () => {expect("abc def ghi jkl").toMatchSnapshot();}) - test("t2", () => {expect("abc\`def").toMatchSnapshot();}) - test("t3", () => {expect("abc def ghi").toMatchSnapshot();}) - `); - } catch (e) {} - expect(await t.getSnapshotContents()).toBe("exports[`snap 1`] = `hello`goodbye`;"); - }); - - test("replaces file that fails to parse when update flag is used", async () => { - await t.setSnapshotFile("exports[`snap 1`] = `hello`goodbye`;"); - await t.update( - /*js*/ ` - test("t1", () => {expect("abc def ghi jkl").toMatchSnapshot();}) - test("t2", () => {expect("abc\`def").toMatchSnapshot();}) - test("t3", () => {expect("abc def ghi").toMatchSnapshot();}) - `, - { forceUpdate: true }, - ); - expect(await t.getSnapshotContents()).toBe( - '// Bun Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`t1 1`] = `"abc def ghi jkl"`;\n\nexports[`t2 1`] = `"abc\\`def"`;\n\nexports[`t3 1`] = `"abc def ghi"`;\n', - ); - }); - - test("grow file for new snapshot", async () => { - const t4 = new SnapshotTester(); - await t4.update(/*js*/ ` - test("abc", () => { expect("hello").toMatchSnapshot() }); - `); - await t4.update( - /*js*/ ` - test("abc", () => { expect("hello").toMatchSnapshot() }); - test("def", () => { expect("goodbye").toMatchSnapshot() }); - `, - { shouldNotError: true, shouldGrow: true }, - ); - await t4.update(/*js*/ ` - test("abc", () => { expect("hello").toMatchSnapshot() }); - test("def", () => { expect("hello").toMatchSnapshot() }); - `); - await t4.update(/*js*/ ` - test("abc", () => { expect("goodbye").toMatchSnapshot() }); - test("def", () => { expect("hello").toMatchSnapshot() }); - `); - }); - - const t2 = new SnapshotTester(); - t2.test("backtick in test name", `test("\`", () => {expect("abc").toMatchSnapshot();})`); - const t3 = new SnapshotTester(); - t3.test("dollars curly in test name", `test("\${}", () => {expect("abc").toMatchSnapshot();})`); - - const t15283 = new SnapshotTester(); - t15283.test( - "#15283", - `it("Should work", () => { - expect(\`This is \\\`wrong\\\`\`).toMatchSnapshot(); - });`, +test("basic unchanging inline snapshot", () => { + expect("hello").toMatchInlineSnapshot('"hello"'); + expect({ v: new Date() }).toMatchInlineSnapshot( + { v: expect.any(Date) }, + ` +{ + "v": Any, +} +`, ); - t15283.test("#15283 unicode", `it("Should work", () => {expect(\`😊This is \\\`wrong\\\`\`).toMatchSnapshot()});`); +}); + +class InlineSnapshotTester { + tmpdir: string; + tmpid: number; + constructor(tmpfiles: DirectoryTree) { + this.tmpdir = tempDirWithFiles("InlineSnapshotTester", tmpfiles); + this.tmpid = 0; + } + tmpfile(content: string): string { + const filename = "_" + this.tmpid++ + ".test.ts"; + writeFileSync(this.tmpdir + "/" + filename, content); + return filename; + } + readfile(name: string): string { + return readFileSync(this.tmpdir + "/" + name, { encoding: "utf-8" }); + } + + testError(eopts: { update?: boolean; msg: string }, code: string): void { + const thefile = this.tmpfile(code); + + const spawnres = Bun.spawnSync({ + cmd: [bunExe(), "test", ...(eopts.update ? ["-u"] : []), thefile], + env: bunEnv, + cwd: this.tmpdir, + stdio: ["pipe", "pipe", "pipe"], + }); + expect(spawnres.stderr.toString()).toInclude(eopts.msg); + expect(spawnres.exitCode).not.toBe(0); + expect(this.readfile(thefile)).toEqual(code); + } + test(cb: (v: (a: string, b: string, c: string) => string) => string): void { + this.testInternal( + false, + cb((a, b, c) => a), + cb((a, b, c) => c), + ); + this.testInternal( + true, + cb((a, b, c) => b), + cb((a, b, c) => c), + ); + } + testInternal(use_update: boolean, before_value: string, after_value: string): void { + const thefile = this.tmpfile(before_value); + + if (use_update) { + // run without update, expect error + const spawnres = Bun.spawnSync({ + cmd: [bunExe(), "test", thefile], + env: bunEnv, + cwd: this.tmpdir, + stdio: ["pipe", "pipe", "pipe"], + }); + expect(spawnres.stderr.toString()).toInclude("error:"); + expect(spawnres.exitCode).not.toBe(0); + expect(this.readfile(thefile)).toEqual(before_value); + } + + { + const spawnres = Bun.spawnSync({ + cmd: [bunExe(), "test", ...(use_update ? ["-u"] : []), thefile], + env: bunEnv, + cwd: this.tmpdir, + stdio: ["pipe", "pipe", "pipe"], + }); + expect(spawnres.stderr.toString()).not.toInclude("error:"); + expect({ + exitCode: spawnres.exitCode, + content: this.readfile(thefile), + }).toEqual({ + exitCode: 0, + content: after_value, + }); + } + + // run without update, expect pass with no change + { + const spawnres = Bun.spawnSync({ + cmd: [bunExe(), "test", thefile], + env: bunEnv, + cwd: this.tmpdir, + stdio: ["pipe", "pipe", "pipe"], + }); + expect(spawnres.stderr.toString()).not.toInclude("error:"); + expect({ + exitCode: spawnres.exitCode, + content: this.readfile(thefile), + }).toEqual({ + exitCode: 0, + content: after_value, + }); + } + + // update again, expect pass with no change + { + const spawnres = Bun.spawnSync({ + cmd: [bunExe(), "test", "-u", thefile], + env: bunEnv, + cwd: this.tmpdir, + stdio: ["pipe", "pipe", "pipe"], + }); + expect(spawnres.stderr.toString()).not.toInclude("error:"); + expect({ + exitCode: spawnres.exitCode, + content: this.readfile(thefile), + }).toEqual({ + exitCode: 0, + content: after_value, + }); + } + } +} + +describe("inline snapshots", () => { + const bad = '"bad"'; + const helper_js = /*js*/ ` + import {expect} from "bun:test"; + export function wrongFile(value) { + expect(value).toMatchInlineSnapshot(); + } + `; + const tester = new InlineSnapshotTester({ + "helper.js": helper_js, + }); + test("changing inline snapshot", () => { + tester.test( + v => /*js*/ ` + test("inline snapshots", () => { + expect("1").toMatchInlineSnapshot(${v("", bad, '`"1"`')}); + expect("2").toMatchInlineSnapshot( ${v("", bad, '`"2"`')}); + expect("3").toMatchInlineSnapshot( ${v("", bad, '`"3"`')}); + }); + test("m1", () => { + expect("a").toMatchInlineSnapshot(${v("", bad, '`"a"`')}); + expect("b").toMatchInlineSnapshot(${v("", bad, '`"b"`')}); + expect("§<-1l").toMatchInlineSnapshot(${v("", bad, '`"§<-1l"`')}); + expect("𐀁").toMatchInlineSnapshot(${v("", bad, '`"𐀁"`')}); + expect( "m ") . toMatchInlineSnapshot ( ${v("", bad, '`"m "`')}) ; + expect("§§§"). toMatchInlineSnapshot(${v("", bad, '`"§§§"`')}) ; + }); + `, + ); + }); + test("inline snapshot update cases", () => { + tester.test( + v => /*js*/ ` + test("cases", () => { + expect("1").toMatchInlineSnapshot(${v("", bad, '`"1"`')}); + expect("2").toMatchInlineSnapshot( ${v("", bad, '`"2"`')}); + expect("3"). toMatchInlineSnapshot( ${v("", bad, '`"3"`')}); + expect("4") . toMatchInlineSnapshot( ${v("", bad, '`"4"`')}); + expect("5" ) . toMatchInlineSnapshot( ${v("", bad, '`"5"`')}); + expect("6" ) . toMatchInlineSnapshot ( ${v("", bad, '`"6"`')}); + expect("7" ) . toMatchInlineSnapshot ( ${v("", bad, '`"7"`')}); + expect("8" ) . toMatchInlineSnapshot ( ${v("", bad, '`"8"`')}) ; + expect("9" ) . toMatchInlineSnapshot ( \n${v("", bad, '`"9"`')}) ; + expect("10" ) .\ntoMatchInlineSnapshot ( \n${v("", bad, '`"10"`')}) ; + expect("11") + .toMatchInlineSnapshot(${v("", bad, '`"11"`')}) ; + expect("12")\r + .\r + toMatchInlineSnapshot\r + (\r + ${v("", bad, '`"12"`')})\r + ; + expect("13").toMatchInlineSnapshot(${v("", bad, '`"13"`')}); expect("14").toMatchInlineSnapshot(${v("", bad, '`"14"`')}); expect("15").toMatchInlineSnapshot(${v("", bad, '`"15"`')}); + expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)}${v(",", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)\n}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()}).\ntoMatchInlineSnapshot({a: expect.any(Date)\n}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()})\n.\ntoMatchInlineSnapshot({a: expect.any(Date)\n}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()})\n.\ntoMatchInlineSnapshot({a: \nexpect.any(Date)\n}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()})\n.\ntoMatchInlineSnapshot({a: \nexpect.any(\nDate)\n}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()}).toMatchInlineSnapshot( {a: expect.any(Date)} ${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect({a: new Date()}).toMatchInlineSnapshot( {a: expect.any(Date)} ${v(",", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); + expect("😊").toMatchInlineSnapshot(${v("", bad, '`"😊"`')}); + expect("\\r").toMatchInlineSnapshot(${v("", bad, '`\n"\n"\n`')}); + expect("\\r\\n").toMatchInlineSnapshot(${v("", bad, '`\n"\n"\n`')}); + expect("\\n").toMatchInlineSnapshot(${v("", bad, '`\n"\n"\n`')}); + }); + `, + ); + }); + it("should error trying to update outside of a test", () => { + tester.testError( + { msg: "error: Snapshot matchers cannot be used outside of a test" }, + /*js*/ ` + expect("1").toMatchInlineSnapshot(); + `, + ); + }); + it.skip("should pass not needing update outside of a test", () => { + // todo write the test right + tester.test( + v => /*js*/ ` + expect("1").toMatchInlineSnapshot('"1"'); + `, + ); + }); + it("should error trying to update the same line twice", () => { + tester.testError( + { msg: "error: Failed to update inline snapshot: Multiple inline snapshots for the same call are not supported" }, + /*js*/ ` + function oops(a) {expect(a).toMatchInlineSnapshot()} + test("whoops", () => { + oops(1); + oops(2); + }); + `, + ); + + // fun trick: + // function oops(a) {expect(a).toMatchInlineSnapshot('1')} + // now do oops(1); oops(2); + // with `-u` it will toggle between '1' and '2' but won't error + // jest has the same bug so it's fine + }); + + // snapshot in a snapshot + it("should not allow a snapshot in a snapshot", () => { + // this is possible to support, but is not supported + tester.testError( + { msg: "error: Failed to update inline snapshot: Did not advance." }, + ((v: (a: string, b: string, c: string) => string) => /*js*/ ` + test("cases", () => { + expect({a: new Date()}).toMatchInlineSnapshot( + ( expect(2).toMatchInlineSnapshot(${v("", bad, "`2`")}) , {a: expect.any(Date)}) + ${v(",", ', "bad"', ', `\n{\n "a": Any,\n}\n`')} + ); + }); + `)((a, b, c) => a), + ); + }); + + it("requires exactly 'toMatchInlineSnapshot' 1", () => { + tester.testError( + { msg: "error: Failed to update inline snapshot: Could not find 'toMatchInlineSnapshot' here" }, + /*js*/ ` + test("cases", () => { + expect(1)["toMatchInlineSnapshot"](); + }); + `, + ); + }); + it("requires exactly 'toMatchInlineSnapshot' 2", () => { + tester.testError( + { msg: "error: Failed to update inline snapshot: Could not find 'toMatchInlineSnapshot' here" }, + /*js*/ ` + test("cases", () => { + expect(1).t\\u{6f}MatchInlineSnapshot(); + }); + `, + ); + }); + it("only replaces when the argument is a literal string 1", () => { + tester.testError( + { + update: true, + msg: "error: Failed to update inline snapshot: Argument must be a string literal", + }, + /*js*/ ` + test("cases", () => { + const value = "25"; + expect({}).toMatchInlineSnapshot(value); + }); + `, + ); + }); + it("only replaces when the argument is a literal string 2", () => { + tester.testError( + { + update: true, + msg: "error: Failed to update inline snapshot: Argument must be a string literal", + }, + /*js*/ ` + test("cases", () => { + const value = "25"; + expect({}).toMatchInlineSnapshot({}, value); + }); + `, + ); + }); + it("only replaces when the argument is a literal string 3", () => { + tester.testError( + { + update: true, + msg: "error: Failed to update inline snapshot: Argument must be a string literal", + }, + /*js*/ ` + test("cases", () => { + expect({}).toMatchInlineSnapshot({}, {}); + }); + `, + ); + }); + it("only replaces when the argument is a literal string 4", () => { + tester.testError( + { + update: true, + msg: "Matcher error: Expected properties must be an object", + }, + /*js*/ ` + test("cases", () => { + expect({}).toMatchInlineSnapshot("1", {}); + }); + `, + ); + }); + it("does not allow spread 1", () => { + tester.testError( + { + update: true, + msg: "error: Failed to update inline snapshot: Spread is not allowed", + }, + /*js*/ ` + test("cases", () => { + expect({}).toMatchInlineSnapshot(...["1"]); + }); + `, + ); + }); + it("does not allow spread 2", () => { + tester.testError( + { + update: true, + msg: "error: Failed to update inline snapshot: Spread is not allowed", + }, + /*js*/ ` + test("cases", () => { + expect({}).toMatchInlineSnapshot({}, ...["1"]); + }); + `, + ); + }); + it("limit two arguments", () => { + tester.testError( + { + update: true, + msg: "error: Failed to update inline snapshot: Snapshot expects at most two arguments", + }, + /*js*/ ` + test("cases", () => { + expect({}).toMatchInlineSnapshot({}, "1", "hello"); + }); + `, + ); + }); + it("must be in test file", () => { + tester.testError( + { + update: true, + msg: "Inline snapshot matchers must be called from the test file", + }, + /*js*/ ` + import {wrongFile} from "./helper"; + test("cases", () => { + wrongFile("interesting"); + }); + `, + ); + expect(readFileSync(tester.tmpdir + "/helper.js", "utf-8")).toBe(helper_js); + }); + it("is right file", () => { + tester.test( + v => /*js*/ ` + import {wrongFile} from "./helper"; + test("cases", () => { + expect("rightfile").toMatchInlineSnapshot(${v("", '"9"', '`"rightfile"`')}); + expect(wrongFile).toMatchInlineSnapshot(${v("", '"9"', "`[Function: wrongFile]`")}); + }); + `, + ); + }); +}); + +test("error snapshots", () => { + expect(() => { + throw new Error("hello"); + }).toThrowErrorMatchingInlineSnapshot(`"hello"`); + expect(() => { + throw 0; + }).toThrowErrorMatchingInlineSnapshot(`undefined`); + expect(() => { + throw { a: "b" }; + }).toThrowErrorMatchingInlineSnapshot(`undefined`); + expect(() => { + throw undefined; // this one doesn't work in jest because it doesn't think the function threw + }).toThrowErrorMatchingInlineSnapshot(`undefined`); +}); +test("error inline snapshots", () => { + expect(() => { + throw new Error("hello"); + }).toThrowErrorMatchingSnapshot(); + expect(() => { + throw 0; + }).toThrowErrorMatchingSnapshot(); + expect(() => { + throw { a: "b" }; + }).toThrowErrorMatchingSnapshot(); + expect(() => { + throw undefined; + }).toThrowErrorMatchingSnapshot(); + expect(() => { + throw "abcdef"; + }).toThrowErrorMatchingSnapshot("hint"); + expect(() => { + throw new Error("😊"); + }).toThrowErrorMatchingInlineSnapshot(`"😊"`); +}); + +test("snapshot numbering", () => { + function fails() { + throw new Error("snap"); + } + expect("item one").toMatchSnapshot(); + expect(fails).toThrowErrorMatchingSnapshot(); + expect("1").toMatchInlineSnapshot(`"1"`); + expect(fails).toThrowErrorMatchingSnapshot(); + expect(fails).toThrowErrorMatchingInlineSnapshot(`"snap"`); + expect("hello").toMatchSnapshot(); + expect("hello").toMatchSnapshot("hinted"); +}); + +test("write snapshot from filter", async () => { + const sver = (m: string, a: boolean) => /*js*/ ` + test("mysnap", () => { + expect("${m}").toMatchInlineSnapshot(${a ? '`"' + m + '"`' : ""}); + expect(() => {throw new Error("${m}!")}).toThrowErrorMatchingInlineSnapshot(${a ? '`"' + m + '!"`' : ""}); + }) + `; + const dir = tempDirWithFiles("writesnapshotfromfilter", { + "mytests": { + "snap.test.ts": sver("a", false), + "snap2.test.ts": sver("b", false), + "more": { + "testing.test.ts": sver("TEST", false), + }, + }, + }); + await $`cd ${dir} && ${bunExe()} test mytests`; + expect(await Bun.file(dir + "/mytests/snap.test.ts").text()).toBe(sver("a", true)); + expect(await Bun.file(dir + "/mytests/snap2.test.ts").text()).toBe(sver("b", true)); + expect(await Bun.file(dir + "/mytests/more/testing.test.ts").text()).toBe(sver("TEST", true)); + await $`cd ${dir} && ${bunExe()} test mytests`; + expect(await Bun.file(dir + "/mytests/snap.test.ts").text()).toBe(sver("a", true)); + expect(await Bun.file(dir + "/mytests/snap2.test.ts").text()).toBe(sver("b", true)); + expect(await Bun.file(dir + "/mytests/more/testing.test.ts").text()).toBe(sver("TEST", true)); }); diff --git a/test/js/bun/test/stack.test.ts b/test/js/bun/test/stack.test.ts index ac3fde49c0..db623a38ad 100644 --- a/test/js/bun/test/stack.test.ts +++ b/test/js/bun/test/stack.test.ts @@ -93,11 +93,16 @@ test("throwing inside an error suppresses the error and prints the stack", async const { stderr, exitCode } = result; - expect(stderr.toString().trim()).toStartWith( - `error: My custom error message - at http://example.com/test.js:42 - `.trim(), - ); + expect(stderr.toString().trim().split("\n").slice(0, -1).join("\n").trim()).toMatchInlineSnapshot(` +"error: My custom error message +{ + message: "My custom error message", + name: [Getter], + line: 42, + sourceURL: "http://example.com/test.js", +} + at http://example.com/test.js:42" +`); expect(exitCode).toBe(1); }); @@ -108,8 +113,11 @@ test("throwing inside an error suppresses the error and continues printing prope const { stderr, exitCode } = result; - expect(stderr.toString().trim()).toStartWith( - 'ENOENT: No such file or directory\n errno: -2\n syscall: "open"\n path: "this-file-path-is-bad"'.trim(), - ); + expect(stderr.toString().trim()).toStartWith(`ENOENT: no such file or directory, open 'this-file-path-is-bad' + path: "this-file-path-is-bad", + syscall: "open", + errno: -2, + code: "ENOENT" +`); expect(exitCode).toBe(1); }); diff --git a/test/js/bun/udp/udp_socket.test.ts b/test/js/bun/udp/udp_socket.test.ts index 157db230d3..8c8fe1f2c8 100644 --- a/test/js/bun/udp/udp_socket.test.ts +++ b/test/js/bun/udp/udp_socket.test.ts @@ -2,8 +2,16 @@ import { udpSocket } from "bun"; import { describe, expect, test } from "bun:test"; import { disableAggressiveGCScope, randomPort } from "harness"; import { dataCases, dataTypes } from "./testdata"; +import { heapStats } from "bun:jsc"; describe("udpSocket()", () => { + test("connect with invalid hostname rejects", async () => { + expect(async () => + udpSocket({ + connect: { hostname: "example!!!!!.com", port: 443 }, + }), + ).toThrow(); + }); test("can create a socket", async () => { const socket = await udpSocket({}); expect(socket).toBeInstanceOf(Object); diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap index a6a949433d..eff7103964 100644 --- a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -2,7 +2,7 @@ exports[`error.cause 1`] = ` "1 | import { expect, test } from "bun:test"; -2 | +2 | 3 | test("error.cause", () => { 4 | const err = new Error("error 1"); 5 | const err2 = new Error("error 2", { cause: err }); @@ -11,7 +11,7 @@ error: error 2 at [dir]/inspect-error.test.js:5:16 1 | import { expect, test } from "bun:test"; -2 | +2 | 3 | test("error.cause", () => { 4 | const err = new Error("error 1"); ^ @@ -24,7 +24,7 @@ exports[`Error 1`] = ` " 9 | .replaceAll("//", "/"), 10 | ).toMatchSnapshot(); 11 | }); -12 | +12 | 13 | test("Error", () => { 14 | const err = new Error("my message"); ^ @@ -65,7 +65,7 @@ exports[`Error inside minified file (color) 1`] = ` 23 | arguments);c=b;c.s=1;return c.v=g}catch(h){throw g=b,g.s=2,g.v=h,h;}}}; 24 | exports.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var f=C({},a.props),d=a.key,e=a.ref,g=a._owner;if(null!=b){void 0!==b.ref&&(e=b.ref,g=K.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var h=a.type.defaultProps;for(k in b)J.call(b,k)&&!L.hasOwnProperty(k)&&(f[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)f.children=c;else if(1 { // - The test failure message gets a non-sensical error test("error gc test #4", () => { const tmp = tmpdirSync(); - for (let i = 0; i < 1000; i++) { + const base = Buffer.from(join(tmp, "does", "not", "exist").repeat(10)); + + function iterate() { // Use a long-enough string for it to be obvious if we leak memory - let path = join(tmp, join("does", "not", "exist").repeat(10)); + // Use .toString() on the Buffer to ensure we clone the string every time. + let path = base.toString(); try { readFileSync(path); throw new Error("unreachable"); @@ -61,8 +64,14 @@ test("error gc test #4", () => { throw e; } - const inspected = Bun.inspect(e); + path = path.replaceAll("\\", "/"); + if (e.path) { + e.path = e.path.replaceAll("\\", "/"); + } + + let inspected = Bun.inspect(e); Bun.gc(true); + inspected = inspected.replaceAll("\\", "/"); // Deliberately avoid using .toContain() directly to avoid // BunString shenanigins. @@ -80,4 +89,8 @@ test("error gc test #4", () => { Bun.gc(true); } } + + for (let i = 0; i < 1000; i++) { + iterate(); + } }); diff --git a/test/js/bun/util/fileUrl.test.js b/test/js/bun/util/fileUrl.test.js index ecd6ad9c6f..8808a9422e 100644 --- a/test/js/bun/util/fileUrl.test.js +++ b/test/js/bun/util/fileUrl.test.js @@ -9,9 +9,10 @@ describe("pathToFileURL", () => { }); describe("fileURLToPath", () => { + const absoluteErrorMessage = "File URL path must be an absolute"; it("should convert a file url to a path", () => { if (isWindows) { - expect(() => fileURLToPath("file:///path/to/file.js")).toThrow("File URL path must be absolute"); + expect(() => fileURLToPath("file:///path/to/file.js")).toThrow(absoluteErrorMessage); } else { expect(fileURLToPath("file:///path/to/file.js")).toBe("/path/to/file.js"); } @@ -19,7 +20,7 @@ describe("fileURLToPath", () => { it("should convert a URL to a path", () => { if (isWindows) { - expect(() => fileURLToPath(new URL("file:///path/to/file.js"))).toThrow("File URL path must be absolute"); + expect(() => fileURLToPath(new URL("file:///path/to/file.js"))).toThrow(absoluteErrorMessage); } else { expect(fileURLToPath(new URL("file:///path/to/file.js"))).toBe("/path/to/file.js"); } diff --git a/test/js/bun/util/hash.test.js b/test/js/bun/util/hash.test.js index 8401c665ce..e0a2f7eaca 100644 --- a/test/js/bun/util/hash.test.js +++ b/test/js/bun/util/hash.test.js @@ -33,6 +33,24 @@ it(`Bun.hash.cityHash64()`, () => { expect(Bun.hash.cityHash64(new TextEncoder().encode("hello world"))).toBe(0xc7920bbdbecee42fn); gcTick(); }); +it(`Bun.hash.xxHash32()`, () => { + expect(Bun.hash.xxHash32("hello world")).toBe(0xcebb6622); + gcTick(); + expect(Bun.hash.xxHash32(new TextEncoder().encode("hello world"))).toBe(0xcebb6622); + gcTick(); +}); +it(`Bun.hash.xxHash64()`, () => { + expect(Bun.hash.xxHash64("hello world")).toBe(0x45ab6734b21e6968n); + gcTick(); + expect(Bun.hash.xxHash64(new TextEncoder().encode("hello world"))).toBe(0x45ab6734b21e6968n); + gcTick(); +}); +it(`Bun.hash.xxHash3()`, () => { + expect(Bun.hash.xxHash3("hello world")).toBe(0xd447b1ea40e6988bn); + gcTick(); + expect(Bun.hash.xxHash3(new TextEncoder().encode("hello world"))).toBe(0xd447b1ea40e6988bn); + gcTick(); +}); it(`Bun.hash.murmur32v3()`, () => { expect(Bun.hash.murmur32v3("hello world")).toBe(0x5e928f0f); gcTick(); diff --git a/test/js/bun/util/heap-snapshot.test.ts b/test/js/bun/util/heap-snapshot.test.ts new file mode 100644 index 0000000000..2bd5aaa995 --- /dev/null +++ b/test/js/bun/util/heap-snapshot.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from "bun:test"; +import { parseHeapSnapshot, summarizeByType } from "./heap"; +import { estimateShallowMemoryUsageOf } from "bun:jsc"; + +describe("Native types report their size correctly", () => { + it("FormData", () => { + var formData = new FormData(); + globalThis.formData = formData; + let original = estimateShallowMemoryUsageOf(formData); + formData.append("a", Buffer.alloc(1024 * 1024 * 8, "abc").toString()); + const afterBuffer = estimateShallowMemoryUsageOf(formData); + expect(afterBuffer).toBeGreaterThan(original + 1024 * 1024 * 8); + formData.append("a", new Blob([Buffer.alloc(1024 * 1024 * 2, "yooa")])); + const afterBlob = estimateShallowMemoryUsageOf(formData); + expect(afterBlob).toBeGreaterThan(afterBuffer + 1024 * 1024 * 2); + formData.append("a", new Blob([Buffer.alloc(1024 * 1024 * 2, "yooa")])); + const afterBlob2 = estimateShallowMemoryUsageOf(formData); + expect(afterBlob2).toBeGreaterThan(afterBlob + 1024 * 1024 * 2); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("FormData")?.size).toBeGreaterThan( + // Test that FormData includes the size of the strings and the blobs + 1024 * 1024 * 8 + 1024 * 1024 * 2 + 1024 * 1024 * 2, + ); + + delete globalThis.formData; + }); + + it("Request", () => { + var request = new Request("https://example.com", { + body: Buffer.alloc(1024 * 1024 * 2, "yoo"), + }); + globalThis.request = request; + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("Request")?.size).toBeGreaterThan(1024 * 1024 * 2); + expect(summariesMap.get("Request")?.size).toBeLessThan(1024 * 1024 * 4); + + delete globalThis.request; + }); + + it("Response", () => { + var response = new Response(Buffer.alloc(1024 * 1024 * 4, "yoo"), { + headers: { + "Content-Type": "text/plain", + }, + }); + globalThis.response = response; + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("Response")?.size).toBeGreaterThan(1024 * 1024 * 4); + + delete globalThis.response; + }); + + it("URL", () => { + const searchParams = new URLSearchParams(); + for (let i = 0; i < 1000; i++) { + searchParams.set(`a${i}`, `b${i}`); + } + + var url = new URL("https://example.com"); + globalThis.url = url; + url.search = searchParams.toString(); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("URL")?.size).toBeGreaterThan(searchParams.toString().length); + + delete globalThis.url; + }); + + it("URLSearchParams", () => { + const searchParams = new URLSearchParams(); + globalThis.searchParams = searchParams; + const original = estimateShallowMemoryUsageOf(searchParams); + for (let i = 0; i < 1000; i++) { + searchParams.set(`a${i}`, `b${i}`); + } + const after = estimateShallowMemoryUsageOf(searchParams); + expect(after).toBeGreaterThan(original + 1000 * 2); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("URLSearchParams")?.size).toBeGreaterThan( + // toString() is greater because of the "?" and "&" + [...searchParams.keys(), ...searchParams.values()].join("").length, + ); + + delete globalThis.searchParams; + }); + + it("Headers", () => { + const headers = new Headers(); + const original = estimateShallowMemoryUsageOf(headers); + for (let i = 0; i < 1000; i++) { + headers.set(`a${i}`, `b${i}`); + } + const after = estimateShallowMemoryUsageOf(headers); + expect(after).toBeGreaterThan(original + 1000 * 2); + + globalThis.headers = headers; + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + // Test that Headers includes the size of the strings + expect(summariesMap.get("Headers")?.size).toBeGreaterThan([...headers.keys(), ...headers.values()].join("").length); + + delete globalThis.headers; + }); + + it("WebSocket + ServerWebSocket + Request", async () => { + using server = Bun.serve({ + port: 0, + websocket: { + open(ws) {}, + drain(ws) {}, + message(ws, message) { + const before = estimateShallowMemoryUsageOf(ws); + ws.send(message); + const after = estimateShallowMemoryUsageOf(ws); + const bufferedAmount = ws.getBufferedAmount(); + if (bufferedAmount > 0) { + expect(after).toBeGreaterThan(before + bufferedAmount); + } + }, + }, + + fetch(req, server) { + const before = estimateShallowMemoryUsageOf(req); + server.upgrade(req); + const after = estimateShallowMemoryUsageOf(req); + + // We detach the request context from the request object on upgrade. + expect(after).toBeLessThan(before); + + return new Response("hello"); + }, + }); + const ws = new WebSocket(server.url); + const original = estimateShallowMemoryUsageOf(ws); + globalThis.ws = ws; + + const { promise, resolve } = Promise.withResolvers(); + ws.onopen = () => { + // Send more than we can possibly send in a single message + ws.send(Buffer.alloc(1024 * 128, "hello")); + }; + ws.onmessage = event => { + resolve(event.data); + }; + await promise; + + const after = estimateShallowMemoryUsageOf(ws); + expect(after).toBeGreaterThan(original + 1024 * 128); + + const snapshot = Bun.generateHeapSnapshot(); + const parsed = parseHeapSnapshot(snapshot); + const summariesList = Array.from(summarizeByType(parsed)); + const summariesMap = new Map(summariesList.map(summary => [summary.name, summary])); + + expect(summariesMap.get("WebSocket")?.size).toBeGreaterThan(1024 * 128); + + delete globalThis.ws; + }); +}); diff --git a/test/js/bun/util/heap.ts b/test/js/bun/util/heap.ts new file mode 100644 index 0000000000..dd696dc6bb --- /dev/null +++ b/test/js/bun/util/heap.ts @@ -0,0 +1,244 @@ +//! This is a decently effecient heap profiler reader. + +export interface HeapSnapshotData { + nodes: Float64Array; + edges: Float64Array; + nodeClassNames: string[]; + edgeNames: string[]; + edgeTypes: string[]; + type: "Inspector" | "GCDebugging"; +} + +const enum NodeLayout { + ID = 0, + SIZE = 1, + CLASS_NAME_IDX = 2, + FLAGS = 3, + LABEL_IDX = 4, + CELL_ADDR = 5, + WRAPPED_ADDR = 6, + STRIDE_GCDEBUGGING = 7, + STRIDE_INSPECTOR = 4, +} + +const enum EdgeLayout { + FROM_NODE = 0, + TO_NODE = 1, + TYPE = 2, + NAME_OR_INDEX = 3, + STRIDE = 4, +} + +const enum TypeStatsLayout { + NAME = 0, + SIZE = 1, + COUNT = 2, + RETAINED_SIZE = 3, + STRIDE = 4, +} + +export class TypeStats { + constructor(private stats: Array) {} + + [Symbol.iterator]() { + const stats = this.stats; + let i = 0; + var iterator: IterableIterator<{ + name: string; + size: number; + count: number; + retainedSize: number; + }> = { + [Symbol.iterator]() { + return iterator; + }, + next() { + if (i >= stats.length) { + return { done: true, value: undefined }; + } + const name = stats[i++] as string; + const size = stats[i++] as number; + const count = stats[i++] as number; + const retainedSize = stats[i++] as number; + return { + done: false, + value: { name, size, count, retainedSize }, + }; + }, + }; + return iterator; + } +} + +export function parseHeapSnapshot(data: { + nodes: number[]; + edges: number[]; + nodeClassNames: string[]; + edgeNames: string[]; + edgeTypes: string[]; + type: "Inspector" | "GCDebugging"; +}): HeapSnapshotData { + return { + nodes: new Float64Array(data.nodes), + edges: new Float64Array(data.edges), + nodeClassNames: data.nodeClassNames, + edgeNames: data.edgeNames, + edgeTypes: data.edgeTypes, + type: data.type, + }; +} + +function getNodeStride(data: HeapSnapshotData): number { + return data.type === "GCDebugging" ? NodeLayout.STRIDE_GCDEBUGGING : NodeLayout.STRIDE_INSPECTOR; +} + +export function summarizeByType(data: HeapSnapshotData): TypeStats { + const nodeStride = getNodeStride(data); + const statsArray = new Array(data.nodeClassNames.length * TypeStatsLayout.STRIDE); + + // Initialize the stats array + for (let i = 0, nameIdx = 0; nameIdx < data.nodeClassNames.length; nameIdx++) { + statsArray[i++] = data.nodeClassNames[nameIdx]; + statsArray[i++] = 0; // size + statsArray[i++] = 0; // count + statsArray[i++] = 0; // retained size + } + + // Calculate retained sizes + const retainedSizes = computeRetainedSizes(data); + + // Accumulate stats + for (let i = 0, nodeIndex = 0, nodes = data.nodes; i < nodes.length; i += nodeStride, nodeIndex++) { + const classNameIdx = nodes[i + NodeLayout.CLASS_NAME_IDX]; + const size = nodes[i + NodeLayout.SIZE]; + + const statsOffset = classNameIdx * TypeStatsLayout.STRIDE; + statsArray[statsOffset + 1] += size; // Add to size + statsArray[statsOffset + 2] += 1; // Increment count + statsArray[statsOffset + 3] += retainedSizes[nodeIndex]; // Add retained size + } + + return new TypeStats(statsArray); +} + +// TODO: this is wrong. +function computeRetainedSizes(data: HeapSnapshotData): Float64Array { + const nodeStride = getNodeStride(data); + const nodeCount = Math.floor(data.nodes.length / nodeStride); + + // Initialize arrays + const retainedSizes = new Float64Array(nodeCount); + const processedNodes = new Uint8Array(nodeCount); + const incomingEdgeCount = new Uint32Array(nodeCount); + const isRoot = new Uint8Array(nodeCount); + + // Initialize with shallow sizes + for (let i = 0; i < nodeCount; i++) { + const offset = i * nodeStride; + retainedSizes[i] = data.nodes[offset + NodeLayout.SIZE] || 0; + } + + // Mark node 0 as root + isRoot[0] = 1; + + // Build outgoing edges list and count incoming edges + const outgoingEdges = new Array(nodeCount); + for (let i = 0; i < nodeCount; i++) { + outgoingEdges[i] = []; + } + + // First pass - count incoming edges + for (let i = 0; i < data.edges.length; i += EdgeLayout.STRIDE) { + const fromNode = data.edges[i + EdgeLayout.FROM_NODE]; + const toNode = data.edges[i + EdgeLayout.TO_NODE]; + + if (fromNode >= 0 && fromNode < nodeCount && toNode >= 0 && toNode < nodeCount && fromNode !== toNode) { + incomingEdgeCount[toNode]++; + outgoingEdges[fromNode].push(toNode); + } + } + + // Find roots - nodes with no incoming edges + for (let i = 1; i < nodeCount; i++) { + if (incomingEdgeCount[i] === 0) { + isRoot[i] = 1; + } + } + + function computeRetainedSize(nodeIndex: number): number { + if (processedNodes[nodeIndex]) return retainedSizes[nodeIndex]; + processedNodes[nodeIndex] = 1; + + let size = retainedSizes[nodeIndex]; + + // If we're a root, include everything we retain + if (isRoot[nodeIndex]) { + const outgoing = outgoingEdges[nodeIndex]; + for (let i = 0; i < outgoing.length; i++) { + const childIndex = outgoing[i]; + if (childIndex !== nodeIndex) { + size += computeRetainedSize(childIndex); + } + } + } else { + // For non-roots, only include uniquely retained children + const outgoing = outgoingEdges[nodeIndex]; + for (let i = 0; i < outgoing.length; i++) { + const childIndex = outgoing[i]; + if (childIndex !== nodeIndex && incomingEdgeCount[childIndex] === 1) { + size += computeRetainedSize(childIndex); + } + } + } + + retainedSizes[nodeIndex] = size; + return size; + } + + // Process roots first + for (let i = 0; i < nodeCount; i++) { + if (isRoot[i]) { + computeRetainedSize(i); + } + } + + // Process remaining nodes + for (let i = 0; i < nodeCount; i++) { + if (!processedNodes[i]) { + computeRetainedSize(i); + } + } + + return retainedSizes; +} + +if (import.meta.main) { + let json = JSON.parse(require("fs").readFileSync(process.argv[2], "utf-8")); + if (json?.snapshot) { + json = json.snapshot; + } + + const snapshot = parseHeapSnapshot(json); + + const classNames = summarizeByType(snapshot); + const numberFormatter = new Intl.NumberFormat(); + const formatBytes = (bytes: number) => { + if (bytes < 1024) { + return `${bytes} bytes`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(2)} KB`; + } + + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; + }; + + let results = Array.from(classNames).sort((a, b) => b.retainedSize - a.retainedSize); + for (const { name, size, count, retainedSize } of results) { + console.log( + `${name}: ${numberFormatter.format(count)} instances, ${formatBytes( + size, + )} size, ${formatBytes(retainedSize)} retained`, + ); + } +} diff --git a/test/js/bun/util/highlighter.test.ts b/test/js/bun/util/highlighter.test.ts index e45e73ca9f..c1af283f1d 100644 --- a/test/js/bun/util/highlighter.test.ts +++ b/test/js/bun/util/highlighter.test.ts @@ -1,4 +1,4 @@ -import { quickAndDirtyJavaScriptSyntaxHighlighter as highlighter } from "bun:internal-for-testing"; +import { highlightJavaScript as highlighter } from "bun:internal-for-testing"; import { expect, test } from "bun:test"; test("highlighter", () => { diff --git a/test/js/bun/util/inspect-error.test.js b/test/js/bun/util/inspect-error.test.js index a65edd62b3..a439b5c5ad 100644 --- a/test/js/bun/util/inspect-error.test.js +++ b/test/js/bun/util/inspect-error.test.js @@ -1,22 +1,51 @@ -import { expect, test } from "bun:test"; +import { expect, test, describe, jest } from "bun:test"; test("error.cause", () => { const err = new Error("error 1"); const err2 = new Error("error 2", { cause: err }); expect( Bun.inspect(err2) - .replaceAll(import.meta.dir, "[dir]") - .replaceAll("\\", "/"), - ).toMatchSnapshot(); + .replaceAll("\\", "/") + .replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]"), + ).toMatchInlineSnapshot(` +"1 | import { expect, test, describe, jest } from "bun:test"; +2 | +3 | test("error.cause", () => { +4 | const err = new Error("error 1"); +5 | const err2 = new Error("error 2", { cause: err }); + ^ +error: error 2 + at ([dir]/inspect-error.test.js:5:16) + +1 | import { expect, test, describe, jest } from "bun:test"; +2 | +3 | test("error.cause", () => { +4 | const err = new Error("error 1"); + ^ +error: error 1 + at ([dir]/inspect-error.test.js:4:15) +" +`); }); test("Error", () => { const err = new Error("my message"); expect( Bun.inspect(err) - .replaceAll(import.meta.dir, "[dir]") - .replaceAll("\\", "/"), - ).toMatchSnapshot(); + .replaceAll("\\", "/") + .replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]"), + ).toMatchInlineSnapshot(` +"27 | " +28 | \`); +29 | }); +30 | +31 | test("Error", () => { +32 | const err = new Error("my message"); + ^ +error: my message + at ([dir]/inspect-error.test.js:32:15) +" +`); }); test("BuildMessage", async () => { @@ -26,9 +55,19 @@ test("BuildMessage", async () => { } catch (e) { expect( Bun.inspect(e) - .replaceAll(import.meta.dir, "[dir]") - .replaceAll("\\", "/"), - ).toMatchSnapshot(); + .replaceAll("\\", "/") + .replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]"), + ).toMatchInlineSnapshot(` +"2 | const duplicateConstDecl = 456; + ^ +error: "duplicateConstDecl" has already been declared + at [dir]/inspect-error-fixture-bad.js:2:7 + +1 | const duplicateConstDecl = 123; + ^ +note: "duplicateConstDecl" was originally declared here + at [dir]/inspect-error-fixture-bad.js:1:7" +`); } }); @@ -66,11 +105,23 @@ test("Error inside minified file (no color) ", () => { expect( normalizeError( Bun.inspect(e) - .replaceAll(import.meta.dir, "[dir]") .replaceAll("\\", "/") + .replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]") .trim(), ), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` +"21 | exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Z; +22 | exports.cache=function(a){return function(){var b=U.current;if(!b)return a.apply(null,arguments);var c=b.getCacheForType(V);b=c.get(a);void 0===b&&(b=W(),c.set(a,b));c=0;for(var f=arguments.length;c ([dir]/inspect-error-fixture.min.js:26:2846) + at ([dir]/inspect-error-fixture.min.js:26:2890) + at ([dir]/inspect-error.test.js:102:5)" +`); } }); @@ -84,11 +135,67 @@ test("Error inside minified file (color) ", () => { normalizeError( stripANSIColors( Bun.inspect(e, { colors: true }) - .replaceAll(import.meta.dir, "[dir]") .replaceAll("\\", "/") + .replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]") .trim(), ).trim(), ), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` +"21 | exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Z; +22 | exports.cache=function(a){return function(){var b=U.current;if(!b)return a.apply(null,arguments);var c=b.getCacheForType(V);b=c.get(a);void 0===b&&(b=W(),c.set(a,b));c=0;for(var f=arguments.length;c ([dir]/inspect-error-fixture.min.js:26:2846) + at ([dir]/inspect-error-fixture.min.js:26:2890) + at ([dir]/inspect-error.test.js:130:5)" +`); } }); + +test("Inserted originalLine and originalColumn do not appear in node:util.inspect", () => { + const err = new Error("my message"); + expect( + require("util") + .inspect(err) + .replaceAll("\\", "/") + .replaceAll(import.meta.path.replaceAll("\\", "/"), "[file]"), + ).toMatchInlineSnapshot(` +"Error: my message + at ([file]:160:19)" +`); +}); + +describe("observable properties", () => { + for (let property of ["sourceURL", "line", "column"]) { + test(`${property} is observable`, () => { + const mock = jest.fn(); + const err = new Error("my message"); + Object.defineProperty(err, property, { + get: mock, + enumerable: true, + configurable: true, + }); + expect(mock).not.toHaveBeenCalled(); + Bun.inspect(err); + expect(mock).not.toHaveBeenCalled(); + }); + } +}); + +test("error.stack throwing an error doesn't lead to a crash", () => { + const err = new Error("my message"); + Object.defineProperty(err, "stack", { + get: () => { + throw new Error("my message"); + }, + enumerable: true, + configurable: true, + }); + expect(() => { + throw err; + }).toThrow(); +}); diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 6ee2d89b6a..347aaddb33 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -225,7 +225,9 @@ it("TypedArray prints", () => { expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]`); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", ); } } @@ -239,9 +241,11 @@ it("BigIntArray", () => { expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ 1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n ]`); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + - [...buffer.subarray(i)].map(a => a.toString(10) + "n").join(", ") + - " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + + [...buffer.subarray(i)].map(a => a.toString(10) + "n").join(", ") + + " ]", ); } } @@ -255,7 +259,9 @@ for (let TypedArray of [Float32Array, Float64Array]) { expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ ${[Math.fround(42.68)].join(", ")} ]`); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", ); } }); @@ -269,7 +275,9 @@ for (let TypedArray of [Float32Array, Float64Array]) { ); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", ); } }); @@ -553,12 +561,7 @@ describe("console.logging class displays names and extends", async () => { class A {} const cases = [A, class B extends A {}, class extends A {}, class {}]; - const expected_logs = [ - "[class A]", - "[class B extends A]", - "[class (anonymous) extends A]", - "[class (anonymous)]", - ]; + const expected_logs = ["[class A]", "[class B extends A]", "[class (anonymous) extends A]", "[class (anonymous)]"]; for (let i = 0; i < cases.length; i++) { it(expected_logs[i], () => { diff --git a/test/js/bun/util/reportError.test.ts b/test/js/bun/util/reportError.test.ts index 14f0466af3..d2849fe901 100644 --- a/test/js/bun/util/reportError.test.ts +++ b/test/js/bun/util/reportError.test.ts @@ -18,5 +18,49 @@ test("reportError", () => { // remove bun version from output output = output.split("\n").slice(0, -2).join("\n"); - expect(output).toMatchSnapshot(); + expect(output.replaceAll("\\", "/").replaceAll("/reportError.ts", "[file]")).toMatchInlineSnapshot( + ` +"1 | reportError(new Error("reportError Test!")); + ^ +error: reportError Test! + at [file]:1:13 +error: true +true +error: false +false +error: null +null +error: 123 +123 +error: Infinity +Infinity +error: NaN +NaN +error: NaN +NaN +error + +error +Uint8Array(1) [ 0 ] +error +Uint8Array(0) [] +error +ArrayBuffer(0) [] +error +ArrayBuffer(1) [ 0 ] +error: string +string +error +[] +error +[ 123, null ] +error +{} +error +[ + {} +] +" +`, + ); }); diff --git a/test/js/bun/util/v8-heap-snapshot.test.ts b/test/js/bun/util/v8-heap-snapshot.test.ts new file mode 100644 index 0000000000..5125b26667 --- /dev/null +++ b/test/js/bun/util/v8-heap-snapshot.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from "bun:test"; +import { tempDirWithFiles } from "harness"; +import { join } from "node:path"; +import * as v8 from "v8"; +import * as v8HeapSnapshot from "v8-heapsnapshot"; + +test("v8 heap snapshot", async () => { + const snapshot = Bun.generateHeapSnapshot("v8"); + // Sanity check: run the validations from this library + const parsed = await v8HeapSnapshot.parseSnapshot(JSON.parse(snapshot)); + + // Loop over all edges and nodes as another sanity check. + for (const edge of parsed.edges) { + if (!edge.to) { + throw new Error("Edge has no 'to' property"); + } + } + for (const node of parsed.nodes) { + if (!node) { + throw new Error("Node is undefined"); + } + } + + expect(parsed.nodes.length).toBeGreaterThan(0); + expect(parsed.edges.length).toBeGreaterThan(0); +}); + +test("v8.getHeapSnapshot()", async () => { + const snapshot = v8.getHeapSnapshot(); + let chunks = []; + for await (const chunk of snapshot) { + expect(chunk.byteLength).toBeGreaterThan(0); + chunks.push(chunk); + } + expect(chunks.length).toBeGreaterThan(0); +}); + +test("v8.writeHeapSnapshot()", async () => { + const path = v8.writeHeapSnapshot(); + expect(path).toBeDefined(); + expect(path).toContain("Heap-"); + + const snapshot = await Bun.file(path).json(); + expect(await v8HeapSnapshot.parseSnapshot(snapshot)).toBeDefined(); +}); + +test("v8.writeHeapSnapshot() with path", async () => { + const dir = tempDirWithFiles("v8-heap-snapshot", { + "test.heapsnapshot": "", + }); + + const path = join(dir, "test.heapsnapshot"); + v8.writeHeapSnapshot(path); + + const snapshot = await Bun.file(path).json(); + expect(await v8HeapSnapshot.parseSnapshot(snapshot)).toBeDefined(); +}); diff --git a/test/js/bun/wasm/wasi.test.js b/test/js/bun/wasm/wasi.test.js index 9b02de881b..5e6994a504 100644 --- a/test/js/bun/wasm/wasi.test.js +++ b/test/js/bun/wasm/wasi.test.js @@ -3,12 +3,20 @@ import { expect, it } from "bun:test"; import { bunEnv, bunExe } from "harness"; it("Should support printing 'hello world'", () => { - const { stdout, exitCode } = spawnSync({ + const { stdout, stderr, exitCode } = spawnSync({ cmd: [bunExe(), import.meta.dir + "/hello-wasi.wasm"], stdout: "pipe", + stderr: "pipe", env: bunEnv, }); - expect(stdout.toString()).toEqual("hello world\n"); - expect(exitCode).toBe(0); + expect({ + stdout: stdout.toString(), + stderr: stderr.toString(), + exitCode: exitCode, + }).toEqual({ + stdout: "hello world\n", + stderr: "", + exitCode: 0, + }); }); diff --git a/test/js/bun/websocket/websocket-server-fixture.js b/test/js/bun/websocket/websocket-server-fixture.js index 8e140b4b1f..23d5bb55ed 100644 --- a/test/js/bun/websocket/websocket-server-fixture.js +++ b/test/js/bun/websocket/websocket-server-fixture.js @@ -9,6 +9,7 @@ let pending = []; using server = Bun.serve({ port: 0, + idleTimeout: 0, websocket: { open(ws) { globalThis.sockets ??= []; diff --git a/test/js/deno/harness.ts b/test/js/deno/harness.ts index d3a0439e2a..79d84ad867 100644 --- a/test/js/deno/harness.ts +++ b/test/js/deno/harness.ts @@ -130,6 +130,22 @@ export function createDenoTest(path: string, defaultTimeout = 5000) { } }; + const assertGreaterThan = (actual: number, expected: number, message?: string) => { + expect(actual).toBeGreaterThan(expected); + } + + const assertGreaterThanOrEqual = (actual: number, expected: number, message?: string) => { + expect(Math.ceil(actual)).toBeGreaterThanOrEqual(expected); + } + + const assertLessThan = (actual: number, expected: number, message?: string) => { + expect(actual).toBeLessThan(expected); + } + + const assertLessThanOrEqual = (actual: number, expected: number, message?: string) => { + expect(actual).toBeLessThanOrEqual(expected); + } + const assertInstanceOf = (actual: unknown, expected: unknown, message?: string) => { expect(actual).toBeInstanceOf(expected); }; @@ -328,6 +344,10 @@ export function createDenoTest(path: string, defaultTimeout = 5000) { assertStrictEquals, assertNotStrictEquals, assertAlmostEquals, + assertGreaterThan, + assertGreaterThanOrEqual, + assertLessThan, + assertLessThanOrEqual, assertInstanceOf, assertNotInstanceOf, assertStringIncludes, diff --git a/test/js/deno/performance/performance.test.ts b/test/js/deno/performance/performance.test.ts index 8753b774f8..5dba6df82a 100644 --- a/test/js/deno/performance/performance.test.ts +++ b/test/js/deno/performance/performance.test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { createDenoTest } from "deno:harness"; -const { test, assert, assertEquals, assertThrows } = createDenoTest(import.meta.path); +const { test, assert, assertEquals, assertGreaterThanOrEqual, assertThrows } = createDenoTest(import.meta.path); test({ permissions: { hrtime: false } }, async function performanceNow() { const { promise, resolve } = Promise.withResolvers(); @@ -90,7 +90,8 @@ test(function performanceMeasure() { assertEquals(measure2.startTime, 0); assertEquals(mark1.startTime, measure1.startTime); assertEquals(mark1.startTime, measure2.duration); - assert(measure1.duration >= 100, `duration below 100ms: ${measure1.duration}`); + // assert(measure1.duration >= 100, `duration below 100ms: ${measure1.duration}`); + assertGreaterThanOrEqual(measure1.duration, 100, `duration below 100ms: ${measure1.duration}`); assert( measure1.duration < (later - now) * 1.5, `duration exceeds 150% of wallclock time: ${measure1.duration}ms vs ${later - now}ms`, diff --git a/test/js/node/assert/assert-doesNotMatch.test.cjs b/test/js/node/assert/assert-doesNotMatch.test.cjs index 14ffd2eae2..136ea095db 100644 --- a/test/js/node/assert/assert-doesNotMatch.test.cjs +++ b/test/js/node/assert/assert-doesNotMatch.test.cjs @@ -6,7 +6,7 @@ test("doesNotMatch does not throw when not matching", () => { test("doesNotMatch throws when argument is not string", () => { expect(() => assert.doesNotMatch(123, /pass/)).toThrow( - 'The "actual" argument must be of type string. Received type number', + 'The "string" argument must be of type string. Received type number', ); }); diff --git a/test/js/node/assert/assert-match.test.cjs b/test/js/node/assert/assert-match.test.cjs index 4eb097e357..29f2d117eb 100644 --- a/test/js/node/assert/assert-match.test.cjs +++ b/test/js/node/assert/assert-match.test.cjs @@ -5,7 +5,7 @@ test("match does not throw when matching", () => { }); test("match throws when argument is not string", () => { - expect(() => assert.match(123, /pass/)).toThrow('The "actual" argument must be of type string. Received type number'); + expect(() => assert.match(123, /pass/)).toThrow('The "string" argument must be of type string. Received type number'); }); test("match throws when not matching", () => { diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index f5622d093c..08830c4bfc 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -633,6 +633,37 @@ for (let withOverridenBufferWrite of [false, true]) { expect(dot.toString("base64url")).toBe("__4uAA"); }); + describe("writing with offset undefined", () => { + [ + ["writeUInt8", "readUInt8", 8, 1], + ["writeInt8", "readInt8", 8, 1], + ["writeUInt16LE", "readUInt16LE", 8, 2], + ["writeInt16LE", "readInt16LE", 8, 2], + ["writeUInt16BE", "readUInt16BE", 8, 2], + ["writeInt16BE", "readInt16BE", 8, 2], + ["writeUInt32LE", "readUInt32LE", 8, 4], + ["writeInt32LE", "readInt32LE", 8, 4], + ["writeUInt32BE", "readUInt32BE", 8, 4], + ["writeInt32BE", "readInt32BE", 8, 4], + ["writeFloatLE", "readFloatLE", 8, 4], + ["writeFloatBE", "readFloatBE", 8, 4], + ["writeDoubleLE", "readDoubleLE", 8, 8], + ["writeDoubleBE", "readDoubleBE", 8, 8], + ].forEach(([method, read, value, size]) => { + it(`${method} (implicit offset)`, () => { + const b = Buffer.alloc(10, 42); + expect(b[method](value)).toBe(size); + expect(b[read]()).toBe(value); + }); + + it(`${method} (explicit offset)`, () => { + const b = Buffer.alloc(10, 42); + expect(b[method](value, 0)).toBe(size); + expect(b[read]()).toBe(value); + }); + }); + }); + // https://github.com/joyent/node/issues/402 it("writing base64 at a position > 0 should not mangle the result", () => { const segments = ["TWFkbmVzcz8h", "IFRoaXM=", "IGlz", "IG5vZGUuanMh"]; @@ -2940,3 +2971,12 @@ describe("serialization", () => { expect(JSON.parse(string, receiver)).toEqual(buffer); }); }); + +it("should not trim utf-8 start bytes at end of string", () => { + // always worked + const buf1 = Buffer.from("e136e1", "hex"); + expect(buf1.toString("utf-8")).toEqual("\uFFFD6\uFFFD"); + // bugged + const buf2 = Buffer.from("36e1", "hex"); + expect(buf2.toString("utf-8")).toEqual("6\uFFFD"); +}); diff --git a/test/js/node/bunfig.toml b/test/js/node/bunfig.toml new file mode 100644 index 0000000000..946890e448 --- /dev/null +++ b/test/js/node/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./harness.ts", "../../preload.ts"] diff --git a/test/js/node/child_process/child_process-node.test.js b/test/js/node/child_process/child_process-node.test.js index 42931a8dcf..ff4699e1e1 100644 --- a/test/js/node/child_process/child_process-node.test.js +++ b/test/js/node/child_process/child_process-node.test.js @@ -659,7 +659,7 @@ describe("fork", () => { code: "ERR_INVALID_ARG_TYPE", name: "TypeError", message: expect.stringContaining( - `The "modulePath" argument must be of type string, Buffer or URL. Received: `, + `The "modulePath" argument must be of type string, Buffer or URL. Received `, ), }), ); @@ -718,7 +718,7 @@ describe("fork", () => { expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE", name: "TypeError", - message: expect.stringContaining(`The "options" argument must be of type object. Received: `), + message: expect.stringContaining(`The "options" argument must be of type object. Received `), }), ); }); diff --git a/test/js/node/child_process/child_process.test.ts b/test/js/node/child_process/child_process.test.ts index 11bd16de1d..961f9634d3 100644 --- a/test/js/node/child_process/child_process.test.ts +++ b/test/js/node/child_process/child_process.test.ts @@ -96,7 +96,7 @@ describe("spawn()", () => { it("should disallow invalid filename", () => { // @ts-ignore expect(() => spawn(123)).toThrow({ - message: 'The "file" argument must be of type string. Received 123', + message: 'The "file" argument must be of type string. Received type number (123)', code: "ERR_INVALID_ARG_TYPE", }); }); @@ -195,8 +195,8 @@ describe("spawn()", () => { it("should allow us to set env", async () => { async function getChildEnv(env: any): Promise { const result: string = await new Promise(resolve => { - const child = spawn(bunExe(), ["-e", "process.stdout.write(JSON.stringify(process.env))"], { env }); - child.stdout.on("data", data => { + const child = spawn(bunExe(), ["-e", "process.stderr.write(JSON.stringify(process.env))"], { env }); + child.stderr.on("data", data => { resolve(data.toString()); }); }); @@ -231,6 +231,7 @@ describe("spawn()", () => { { argv0: bun, stdio: ["inherit", "pipe", "inherit"], + env: bunEnv, }, ); delete process.env.NO_COLOR; @@ -454,3 +455,13 @@ it.if(!isWindows)("spawnSync correctly reports signal codes", () => { expect(signal).toBe("SIGTRAP"); }); + +it("spawnSync(does-not-exist)", () => { + const x = spawnSync("does-not-exist"); + expect(x.error?.code).toEqual("ENOENT"); + expect(x.error.path).toEqual("does-not-exist"); + expect(x.signal).toEqual(null); + expect(x.output).toEqual([null, null, null]); + expect(x.stdout).toEqual(null); + expect(x.stderr).toEqual(null); +}); diff --git a/test/js/node/cluster/common.ts b/test/js/node/cluster/common.ts deleted file mode 100644 index b99b402c49..0000000000 --- a/test/js/node/cluster/common.ts +++ /dev/null @@ -1,37 +0,0 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import process from "node:process"; -import util from "node:util"; - -export const isWindows = process.platform === "win32"; - -export function tmpdirSync(pattern: string = "bun.test.") { - return fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), pattern)); -} - -export function isAlive(pid) { - try { - process.kill(pid, "SIGCONT"); - return true; - } catch { - return false; - } -} - -export function mustNotCall(msg?) { - return function mustNotCall(...args) { - const argsInfo = args.length > 0 ? `\ncalled with arguments: ${args.map(arg => util.inspect(arg)).join(", ")}` : ""; - assert.fail(`${msg || "function should not have been called"} ` + argsInfo); - }; -} - -export function patchEmitter(emitter: any, prefix: string) { - var oldEmit = emitter.emit; - - emitter.emit = function () { - console.log([prefix, arguments[0]]); - oldEmit.apply(emitter, arguments); - }; -} diff --git a/test/js/node/cluster/test-docs-http-server.ts b/test/js/node/cluster/test-docs-http-server.ts index 91547ed7ae..a72498d227 100644 --- a/test/js/node/cluster/test-docs-http-server.ts +++ b/test/js/node/cluster/test-docs-http-server.ts @@ -1,8 +1,14 @@ +import { isBroken, isWindows } from "harness"; import assert from "node:assert"; import cluster from "node:cluster"; import http from "node:http"; import { availableParallelism } from "node:os"; +if (isWindows && isBroken) { + console.log("Skipping on Windows because it does not work when there are more than 1 CPU"); + process.exit(0); +} + const numCPUs = availableParallelism(); let workers = 0; diff --git a/test/js/node/cluster/test-worker-no-exit-http.ts b/test/js/node/cluster/test-worker-no-exit-http.ts index 661c439861..93156225b9 100644 --- a/test/js/node/cluster/test-worker-no-exit-http.ts +++ b/test/js/node/cluster/test-worker-no-exit-http.ts @@ -1,7 +1,15 @@ const assert = require("assert"); const cluster = require("cluster"); const http = require("http"); -import { patchEmitter } from "./common"; + +function patchEmitter(emitter: any, prefix: string) { + var oldEmit = emitter.emit; + + emitter.emit = function () { + console.log([prefix, arguments[0]]); + oldEmit.apply(emitter, arguments); + }; +} let destroyed; let success; diff --git a/test/js/node/cluster/upstream/common/countdown.js b/test/js/node/cluster/upstream/common/countdown.js deleted file mode 100644 index 9853c9fa47..0000000000 --- a/test/js/node/cluster/upstream/common/countdown.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; - -const assert = require("assert"); -const kLimit = Symbol("limit"); -const kCallback = Symbol("callback"); -const common = require("./"); - -class Countdown { - constructor(limit, cb) { - assert.strictEqual(typeof limit, "number"); - assert.strictEqual(typeof cb, "function"); - this[kLimit] = limit; - this[kCallback] = common.mustCall(cb); - } - - dec() { - assert(this[kLimit] > 0, "Countdown expired"); - if (--this[kLimit] === 0) this[kCallback](); - return this[kLimit]; - } - - get remaining() { - return this[kLimit]; - } -} - -module.exports = Countdown; diff --git a/test/js/node/cluster/upstream/common/index.js b/test/js/node/cluster/upstream/common/index.js deleted file mode 100644 index 46df521726..0000000000 --- a/test/js/node/cluster/upstream/common/index.js +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// /* eslint-disable node-core/crypto-check */ -"use strict"; -const process = global.process; // Some tests tamper with the process global. - -const assert = require("assert"); -const { exec, execSync, spawn, spawnSync } = require("child_process"); -const fs = require("fs"); -const net = require("net"); -// Do not require 'os' until needed so that test-os-checked-function can -// monkey patch it. If 'os' is required here, that test will fail. -const path = require("path"); -const { inspect } = require("util"); -const { isMainThread } = require("worker_threads"); - -// Some tests assume a umask of 0o022 so set that up front. Tests that need a -// different umask will set it themselves. -// -// Workers can read, but not set the umask, so check that this is the main -// thread. -if (isMainThread) process.umask(0o022); - -const noop = () => {}; - -const isWindows = process.platform === "win32"; -const isSunOS = process.platform === "sunos"; -const isFreeBSD = process.platform === "freebsd"; -const isOpenBSD = process.platform === "openbsd"; -const isLinux = process.platform === "linux"; -const isOSX = process.platform === "darwin"; -const isPi = (() => { - try { - // Normal Raspberry Pi detection is to find the `Raspberry Pi` string in - // the contents of `/sys/firmware/devicetree/base/model` but that doesn't - // work inside a container. Match the chipset model number instead. - const cpuinfo = fs.readFileSync("/proc/cpuinfo", { encoding: "utf8" }); - const ok = /^Hardware\s*:\s*(.*)$/im.exec(cpuinfo)?.[1] === "BCM2835"; - /^/.test(""); // Clear RegExp.$_, some tests expect it to be empty. - return ok; - } catch { - return false; - } -})(); - -const isDumbTerminal = process.env.TERM === "dumb"; - -const mustCallChecks = []; - -function runCallChecks(exitCode) { - if (exitCode !== 0) return; - - const failed = mustCallChecks.filter(function (context) { - if ("minimum" in context) { - context.messageSegment = `at least ${context.minimum}`; - return context.actual < context.minimum; - } - context.messageSegment = `exactly ${context.exact}`; - return context.actual !== context.exact; - }); - - failed.forEach(function (context) { - console.log( - "Mismatched %s function calls. Expected %s, actual %d.", - context.name, - context.messageSegment, - context.actual, - ); - console.log(context.stack.split("\n").slice(2).join("\n")); - }); - - if (failed.length) process.exit(1); -} - -function mustCall(fn, exact) { - return _mustCallInner(fn, exact, "exact"); -} - -function mustSucceed(fn, exact) { - return mustCall(function (err, ...args) { - assert.ifError(err); - if (typeof fn === "function") return fn.apply(this, args); - }, exact); -} - -function _mustCallInner(fn, criteria = 1, field) { - if (process._exiting) throw new Error("Cannot use common.mustCall*() in process exit handler"); - if (typeof fn === "number") { - criteria = fn; - fn = noop; - } else if (fn === undefined) { - fn = noop; - } - - if (typeof criteria !== "number") throw new TypeError(`Invalid ${field} value: ${criteria}`); - - const context = { - [field]: criteria, - actual: 0, - stack: inspect(new Error()), - name: fn.name || "", - }; - - // Add the exit listener only once to avoid listener leak warnings - if (mustCallChecks.length === 0) process.on("exit", runCallChecks); - - mustCallChecks.push(context); - - const _return = function () { - // eslint-disable-line func-style - context.actual++; - return fn.apply(this, arguments); - }; - // Function instances have own properties that may be relevant. - // Let's replicate those properties to the returned function. - // Refs: https://tc39.es/ecma262/#sec-function-instances - Object.defineProperties(_return, { - name: { - value: fn.name, - writable: false, - enumerable: false, - configurable: true, - }, - length: { - value: fn.length, - writable: false, - enumerable: false, - configurable: true, - }, - }); - return _return; -} - -function getCallSite(top) { - const originalStackFormatter = Error.prepareStackTrace; - Error.prepareStackTrace = (err, stack) => `${stack[0].getFileName()}:${stack[0].getLineNumber()}`; - const err = new Error(); - Error.captureStackTrace(err, top); - // With the V8 Error API, the stack is not formatted until it is accessed - err.stack; // eslint-disable-line no-unused-expressions - Error.prepareStackTrace = originalStackFormatter; - return err.stack; -} - -function mustNotCall(msg) { - const callSite = getCallSite(mustNotCall); - return function mustNotCall(...args) { - const argsInfo = args.length > 0 ? `\ncalled with arguments: ${args.map(arg => inspect(arg)).join(", ")}` : ""; - assert.fail(`${msg || "function should not have been called"} at ${callSite}` + argsInfo); - }; -} - -function printSkipMessage(msg) { - console.log(`1..0 # Skipped: ${msg}`); -} - -function skip(msg) { - printSkipMessage(msg); - process.exit(0); -} - -function isAlive(pid) { - try { - process.kill(pid, "SIGCONT"); - return true; - } catch { - return false; - } -} - -function skipIf32Bits() { - if (bits < 64) { - skip("The tested feature is not available in 32bit builds"); - } -} - -function skipIfWorker() { - if (!isMainThread) { - skip("This test only works on a main thread"); - } -} - -function skipIfDumbTerminal() { - if (isDumbTerminal) { - skip("skipping - dumb terminal"); - } -} - -const common = { - isAlive, - isDumbTerminal, - isFreeBSD, - isLinux, - isMainThread, - isOpenBSD, - isOSX, - isPi, - isSunOS, - isWindows, - mustCall, - mustNotCall, - mustSucceed, - printSkipMessage, - skip, - skipIf32Bits, - skipIfDumbTerminal, - // On IBMi, process.platform and os.platform() both return 'aix', - // when built with Python versions earlier than 3.9. - // It is not enough to differentiate between IBMi and real AIX system. - get isAIX() { - return require("os").type() === "AIX"; - }, - - get isIBMi() { - return require("os").type() === "OS400"; - }, - - get isLinuxPPCBE() { - return process.platform === "linux" && process.arch === "ppc64" && require("os").endianness() === "BE"; - }, -}; - -const validProperties = new Set(Object.keys(common)); -module.exports = new Proxy(common, { - get(obj, prop) { - if (!validProperties.has(prop)) throw new Error(`Using invalid common property: '${prop}'`); - return obj[prop]; - }, -}); diff --git a/test/js/node/cluster/upstream/common/tmpdir.js b/test/js/node/cluster/upstream/common/tmpdir.js deleted file mode 100644 index 7de0b113a3..0000000000 --- a/test/js/node/cluster/upstream/common/tmpdir.js +++ /dev/null @@ -1,88 +0,0 @@ -"use strict"; - -const { spawnSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); -const { pathToFileURL } = require("url"); -const { isMainThread } = require("worker_threads"); - -function rmSync(pathname, useSpawn) { - if (useSpawn) { - const escapedPath = pathname.replaceAll("\\", "\\\\"); - spawnSync(process.execPath, [ - "-e", - `require("fs").rmSync("${escapedPath}", { maxRetries: 3, recursive: true, force: true });`, - ]); - } else { - fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true }); - } -} - -const testRoot = process.env.NODE_TEST_DIR ? fs.realpathSync(process.env.NODE_TEST_DIR) : path.resolve(__dirname, ".."); - -// Using a `.` prefixed name, which is the convention for "hidden" on POSIX, -// gets tools to ignore it by default or by simple rules, especially eslint. -const tmpdirName = ".tmp." + (process.env.TEST_SERIAL_ID || process.env.TEST_THREAD_ID || "0"); -const tmpPath = path.join(testRoot, tmpdirName); - -let firstRefresh = true; -function refresh(useSpawn = false) { - rmSync(tmpPath, useSpawn); - fs.mkdirSync(tmpPath); - - if (firstRefresh) { - firstRefresh = false; - // Clean only when a test uses refresh. This allows for child processes to - // use the tmpdir and only the parent will clean on exit. - process.on("exit", () => { - return onexit(useSpawn); - }); - } -} - -function onexit(useSpawn) { - // Change directory to avoid possible EBUSY - if (isMainThread) process.chdir(testRoot); - - try { - rmSync(tmpPath, useSpawn); - } catch (e) { - console.error("Can't clean tmpdir:", tmpPath); - - const files = fs.readdirSync(tmpPath); - console.error("Files blocking:", files); - - if (files.some(f => f.startsWith(".nfs"))) { - // Warn about NFS "silly rename" - console.error('Note: ".nfs*" might be files that were open and ' + "unlinked but not closed."); - console.error("See http://nfs.sourceforge.net/#faq_d2 for details."); - } - - console.error(); - throw e; - } -} - -function resolve(...paths) { - return path.resolve(tmpPath, ...paths); -} - -function hasEnoughSpace(size) { - const { bavail, bsize } = fs.statfsSync(tmpPath); - return bavail >= Math.ceil(size / bsize); -} - -function fileURL(...paths) { - // When called without arguments, add explicit trailing slash - const fullPath = path.resolve(tmpPath + path.sep, ...paths); - - return pathToFileURL(fullPath); -} - -module.exports = { - fileURL, - hasEnoughSpace, - path: tmpPath, - refresh, - resolve, -}; diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-advanced-serialization.js b/test/js/node/cluster/upstream/parallel/test-cluster-advanced-serialization.js deleted file mode 100644 index 8a368d44c7..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-advanced-serialization.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isPrimary) { - cluster.settings.serialization = "advanced"; - const worker = cluster.fork(); - const circular = {}; - circular.circular = circular; - - worker.on( - "online", - common.mustCall(() => { - worker.send(circular); - - worker.on( - "message", - common.mustCall(msg => { - assert.deepStrictEqual(msg, circular); - worker.kill(); - }), - ); - }), - ); -} else { - process.on("message", msg => process.send(msg)); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-bind-privileged-port.js b/test/js/node/cluster/upstream/parallel/test-cluster-bind-privileged-port.js deleted file mode 100644 index f3a788984b..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-bind-privileged-port.js +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -if (common.isLinux) return; // TODO: bun -const assert = require("assert"); -const cluster = require("cluster"); -const net = require("net"); -const { readFileSync } = require("fs"); - -if (common.isLinux) { - try { - const unprivilegedPortStart = parseInt(readFileSync("/proc/sys/net/ipv4/ip_unprivileged_port_start")); - if (unprivilegedPortStart <= 42) { - common.skip("Port 42 is unprivileged"); - } - } catch { - // Do nothing, feature doesn't exist, minimum is 1024 so 42 is usable. - // Continue... - } -} - -// Skip on OS X Mojave. https://github.com/nodejs/node/issues/21679 -if (common.isOSX) common.skip("macOS may allow ordinary processes to use any port"); - -if (common.isIBMi) common.skip("IBMi may allow ordinary processes to use any port"); - -if (common.isWindows) common.skip("not reliable on Windows."); - -if (process.getuid() === 0) common.skip("Test is not supposed to be run as root."); - -if (cluster.isPrimary) { - cluster.fork().on( - "exit", - common.mustCall(exitCode => { - assert.strictEqual(exitCode, 0); - }), - ); -} else { - const s = net.createServer(common.mustNotCall()); - s.listen(42, common.mustNotCall("listen should have failed")); - s.on( - "error", - common.mustCall(err => { - assert.strictEqual(err.code, "EACCES"); - process.disconnect(); - }), - ); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-call-and-destroy.js b/test/js/node/cluster/upstream/parallel/test-cluster-call-and-destroy.js deleted file mode 100644 index 6d9ff44e67..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-call-and-destroy.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -const common = require("../common"); -const cluster = require("cluster"); -const assert = require("assert"); - -if (cluster.isPrimary) { - const worker = cluster.fork(); - worker.on( - "disconnect", - common.mustCall(() => { - assert.strictEqual(worker.isConnected(), false); - worker.destroy(); - }), - ); -} else { - assert.strictEqual(cluster.worker.isConnected(), true); - cluster.worker.disconnect(); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-child-index-dgram.js b/test/js/node/cluster/upstream/parallel/test-cluster-child-index-dgram.js deleted file mode 100644 index 426c8de9f4..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-child-index-dgram.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; -const common = require("../common"); -const Countdown = require("../common/countdown"); -if (common.isWindows) common.skip("dgram clustering is currently not supported on Windows."); - -const cluster = require("cluster"); -const dgram = require("dgram"); - -// Test an edge case when using `cluster` and `dgram.Socket.bind()` -// the port of `0`. -const kPort = 0; - -function child() { - const kTime = 2; - const countdown = new Countdown(kTime * 2, () => { - process.exit(0); - }); - for (let i = 0; i < kTime; i += 1) { - const socket = new dgram.Socket("udp4"); - socket.bind( - kPort, - common.mustCall(() => { - // `process.nextTick()` or `socket2.close()` would throw - // ERR_SOCKET_DGRAM_NOT_RUNNING - process.nextTick(() => { - socket.close(countdown.dec()); - const socket2 = new dgram.Socket("udp4"); - socket2.bind( - kPort, - common.mustCall(() => { - process.nextTick(() => { - socket2.close(countdown.dec()); - }); - }), - ); - }); - }), - ); - } -} - -if (cluster.isMaster) cluster.fork(__filename); -else child(); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-child-index-net.js b/test/js/node/cluster/upstream/parallel/test-cluster-child-index-net.js deleted file mode 100644 index 961924a2d6..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-child-index-net.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -const common = require("../common"); -const Countdown = require("../common/countdown"); -const cluster = require("cluster"); -const net = require("net"); - -// Test an edge case when using `cluster` and `net.Server.listen()` to -// the port of `0`. -const kPort = 0; - -function child() { - const kTime = 2; - const countdown = new Countdown(kTime * 2, () => { - process.exit(0); - }); - for (let i = 0; i < kTime; i += 1) { - const server = net.createServer(); - server.listen( - kPort, - common.mustCall(() => { - server.close(countdown.dec()); - const server2 = net.createServer(); - server2.listen( - kPort, - common.mustCall(() => { - server2.close(countdown.dec()); - }), - ); - }), - ); - } -} - -if (cluster.isMaster) cluster.fork(__filename); -else child(); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-concurrent-disconnect.js b/test/js/node/cluster/upstream/parallel/test-cluster-concurrent-disconnect.js deleted file mode 100644 index 1e1daa9ef4..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-concurrent-disconnect.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; - -// Ref: https://github.com/nodejs/node/issues/32106 - -const common = require("../common"); -if (common.isLinux) return; // TODO: bun -if (common.isWindows) return; // TODO: bun - -const assert = require("assert"); -const cluster = require("cluster"); -const os = require("os"); - -if (cluster.isPrimary) { - const workers = []; - const numCPUs = os.availableParallelism(); - let waitOnline = numCPUs; - for (let i = 0; i < numCPUs; i++) { - const worker = cluster.fork(); - workers[i] = worker; - worker.once( - "online", - common.mustCall(() => { - if (--waitOnline === 0) - for (const worker of workers) if (worker.isConnected()) worker.send(i % 2 ? "disconnect" : "destroy"); - }), - ); - - // These errors can occur due to the nature of the test, we might be trying - // to send messages when the worker is disconnecting. - worker.on("error", err => { - assert.strictEqual(err.syscall, "write"); - if (common.isOSX) { - assert(["EPIPE", "ENOTCONN"].includes(err.code), err); - } else { - assert(["EPIPE", "ECONNRESET"].includes(err.code), err); - } - }); - - worker.once( - "disconnect", - common.mustCall(() => { - for (const worker of workers) if (worker.isConnected()) worker.send("disconnect"); - }), - ); - - worker.once( - "exit", - common.mustCall((code, signal) => { - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }), - ); - } -} else { - process.on("message", msg => { - if (cluster.worker.isConnected()) cluster.worker[msg](); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-cwd.js b/test/js/node/cluster/upstream/parallel/test-cluster-cwd.js deleted file mode 100644 index d28bfdf545..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-cwd.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); -const tmpdir = require("../common/tmpdir"); - -if (cluster.isPrimary) { - tmpdir.refresh(); - - assert.strictEqual(cluster.settings.cwd, undefined); - cluster.fork().on( - "message", - common.mustCall(msg => { - assert.strictEqual(msg, process.cwd()); - }), - ); - - cluster.setupPrimary({ cwd: tmpdir.path }); - assert.strictEqual(cluster.settings.cwd, tmpdir.path); - cluster.fork().on( - "message", - common.mustCall(msg => { - assert.strictEqual(msg, tmpdir.path); - }), - ); -} else { - process.send(process.cwd()); - process.disconnect(); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-exitedAfterDisconnect-race.js b/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-exitedAfterDisconnect-race.js deleted file mode 100644 index f461734eb6..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-exitedAfterDisconnect-race.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; - -const common = require("../common"); - -// Test should fail in Node.js 5.4.1 and pass in later versions. - -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isPrimary) { - cluster.on("exit", (worker, code) => { - assert.strictEqual(code, 0, `worker exited with code: ${code}, expected 0`); - }); - - return cluster.fork(); -} - -let eventFired = false; - -cluster.worker.disconnect(); - -process.nextTick( - common.mustCall(() => { - assert.ok(!eventFired, "disconnect event should wait for ack"); - }), -); - -cluster.worker.on( - "disconnect", - common.mustCall(() => { - eventFired = true; - }), -); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-leak.js b/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-leak.js deleted file mode 100644 index d1b4812004..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-leak.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; - -// Test fails in Node v5.4.0 and passes in v5.4.1 and newer. - -const common = require("../common"); -const net = require("net"); -const cluster = require("cluster"); - -cluster.schedulingPolicy = cluster.SCHED_NONE; - -if (cluster.isPrimary) { - const worker = cluster.fork(); - - // This is the important part of the test: Confirm that `disconnect` fires. - worker.on("disconnect", common.mustCall()); - - // These are just some extra stuff we're checking for good measure... - worker.on("exit", common.mustCall()); - cluster.on("exit", common.mustCall()); - - cluster.disconnect(); - return; -} - -const server = net.createServer(); - -server.listen(0); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-fork-env.js b/test/js/node/cluster/upstream/parallel/test-cluster-fork-env.js deleted file mode 100644 index 8bf9ef15d7..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-fork-env.js +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -require("../common"); - -// This test checks that arguments provided to cluster.fork() will create -// new environment variables and override existing environment variables -// in the created worker process. - -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isWorker) { - const result = cluster.worker.send({ - prop: process.env.cluster_test_prop, - overwrite: process.env.cluster_test_overwrite, - }); - - assert.strictEqual(result, true); -} else if (cluster.isPrimary) { - const checks = { - using: false, - overwrite: false, - }; - - // To check that the cluster extend on the process.env we will overwrite a - // property - process.env.cluster_test_overwrite = "old"; - - // Fork worker - const worker = cluster.fork({ - "cluster_test_prop": "custom", - "cluster_test_overwrite": "new", - }); - - // Checks worker env - worker.on("message", function (data) { - checks.using = data.prop === "custom"; - checks.overwrite = data.overwrite === "new"; - process.exit(0); - }); - - process.once("exit", function () { - assert.ok(checks.using, "The worker did not receive the correct env."); - assert.ok(checks.overwrite, "The custom environment did not overwrite the existing environment."); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-fork-windowsHide.js b/test/js/node/cluster/upstream/parallel/test-cluster-fork-windowsHide.js deleted file mode 100644 index 273e8146a7..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-fork-windowsHide.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; -const common = require("../common"); -if (common.isWindows) return; // TODO: bun -const assert = require("assert"); -const child_process = require("child_process"); -const cluster = require("cluster"); - -if (!process.argv[2]) { - // It seems Windows only allocate new console window for - // attaching processes spawned by detached processes. i.e. - // - If process D is spawned by process C with `detached: true`, - // and process W is spawned by process D with `detached: false`, - // W will get a new black console window popped up. - // - If D is spawned by C with `detached: false` or W is spawned - // by D with `detached: true`, no console window will pop up for W. - // - // So, we have to spawn a detached process first to run the actual test. - const primary = child_process.spawn(process.argv[0], [process.argv[1], "--cluster"], { - detached: true, - stdio: ["ignore", "ignore", "ignore", "ipc"], - }); - - const messageHandlers = { - workerOnline: common.mustCall(), - mainWindowHandle: common.mustCall(msg => { - assert.match(msg.value, /0\s*/); - }), - workerExit: common.mustCall(msg => { - assert.strictEqual(msg.code, 0); - assert.strictEqual(msg.signal, null); - }), - }; - - primary.on("message", msg => { - const handler = messageHandlers[msg.type]; - assert.ok(handler); - handler(msg); - }); - - primary.on( - "exit", - common.mustCall((code, signal) => { - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }), - ); -} else if (cluster.isPrimary) { - cluster.setupPrimary({ - silent: true, - windowsHide: true, - }); - - const worker = cluster.fork(); - worker.on("exit", (code, signal) => { - process.send({ type: "workerExit", code: code, signal: signal }); - }); - - worker.on("online", msg => { - process.send({ type: "workerOnline" }); - - let output = "0"; - if (process.platform === "win32") { - output = child_process.execSync( - "powershell -NoProfile -c " + `"(Get-Process -Id ${worker.process.pid}).MainWindowHandle"`, - { windowsHide: true, encoding: "utf8" }, - ); - } - - process.send({ type: "mainWindowHandle", value: output }); - worker.send("shutdown"); - }); -} else { - cluster.worker.on("message", msg => { - cluster.worker.disconnect(); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-invalid-message.js b/test/js/node/cluster/upstream/parallel/test-cluster-invalid-message.js deleted file mode 100644 index fdfe1ada62..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-invalid-message.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isPrimary) { - const worker = cluster.fork(); - - worker.on( - "exit", - common.mustCall((code, signal) => { - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }), - ); - - worker.on("online", () => { - worker.send( - { - cmd: "NODE_CLUSTER", - ack: -1, - }, - () => { - worker.disconnect(); - }, - ); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-kill-disconnect.js b/test/js/node/cluster/upstream/parallel/test-cluster-kill-disconnect.js deleted file mode 100644 index e1c0a313e2..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-kill-disconnect.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -const common = require("../common"); - -// Check that cluster works perfectly for both `kill` and `disconnect` cases. -// Also take into account that the `disconnect` event may be received after the -// `exit` event. -// https://github.com/nodejs/node/issues/3238 - -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isPrimary) { - function forkWorker(action) { - const worker = cluster.fork({ action }); - worker.on( - "disconnect", - common.mustCall(() => { - assert.strictEqual(worker.exitedAfterDisconnect, true); - }), - ); - - worker.on( - "exit", - common.mustCall(() => { - assert.strictEqual(worker.exitedAfterDisconnect, true); - }), - ); - } - - forkWorker("disconnect"); - forkWorker("kill"); -} else { - cluster.worker[process.env.action](); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-kill-infinite-loop.js b/test/js/node/cluster/upstream/parallel/test-cluster-kill-infinite-loop.js deleted file mode 100644 index 837b11f2a1..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-kill-infinite-loop.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -const common = require("../common"); -const cluster = require("cluster"); -const assert = require("assert"); - -if (cluster.isPrimary) { - const worker = cluster.fork(); - - worker.on( - "online", - common.mustCall(() => { - // Use worker.process.kill() instead of worker.kill() because the latter - // waits for a graceful disconnect, which will never happen. - worker.process.kill(); - }), - ); - - worker.on( - "exit", - common.mustCall((code, signal) => { - assert.strictEqual(code, null); - assert.strictEqual(signal, "SIGTERM"); - }), - ); -} else { - while (true); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-listening-port.js b/test/js/node/cluster/upstream/parallel/test-cluster-listening-port.js deleted file mode 100644 index ecf9398cd7..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-listening-port.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); -const net = require("net"); - -if (cluster.isPrimary) { - cluster.fork(); - cluster.on( - "listening", - common.mustCall(function (worker, address) { - const port = address.port; - // Ensure that the port is not 0 or null - assert(port); - // Ensure that the port is numerical - assert.strictEqual(typeof port, "number"); - worker.kill(); - }), - ); -} else { - net.createServer(common.mustNotCall()).listen(0); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-primary-error.js b/test/js/node/cluster/upstream/parallel/test-cluster-primary-error.js deleted file mode 100644 index 763ae3eab3..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-primary-error.js +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -const totalWorkers = 2; - -// Cluster setup -if (cluster.isWorker) { - const http = require("http"); - http.Server(() => {}).listen(0, "127.0.0.1"); -} else if (process.argv[2] === "cluster") { - // Send PID to testcase process - let forkNum = 0; - cluster.on( - "fork", - common.mustCall(function forkEvent(worker) { - // Send PID - process.send({ - cmd: "worker", - workerPID: worker.process.pid, - }); - - // Stop listening when done - if (++forkNum === totalWorkers) { - cluster.removeListener("fork", forkEvent); - } - }, totalWorkers), - ); - - // Throw accidental error when all workers are listening - let listeningNum = 0; - cluster.on( - "listening", - common.mustCall(function listeningEvent() { - // When all workers are listening - if (++listeningNum === totalWorkers) { - // Stop listening - cluster.removeListener("listening", listeningEvent); - - // Throw accidental error - process.nextTick(() => { - throw new Error("accidental error"); - }); - } - }, totalWorkers), - ); - - // Startup a basic cluster - cluster.fork(); - cluster.fork(); -} else { - // This is the testcase - - const fork = require("child_process").fork; - - // List all workers - const workers = []; - - // Spawn a cluster process - const primary = fork(process.argv[1], ["cluster"], { silent: true }); - - // Handle messages from the cluster - primary.on( - "message", - common.mustCall(data => { - // Add worker pid to list and progress tracker - if (data.cmd === "worker") { - workers.push(data.workerPID); - } - }, totalWorkers), - ); - - // When cluster is dead - primary.on( - "exit", - common.mustCall(code => { - // Check that the cluster died accidentally (non-zero exit code) - assert.strictEqual(code, 1); - - // XXX(addaleax): The fact that this uses raw PIDs makes the test inherently - // flaky – another process might end up being started right after the - // workers finished and receive the same PID. - const pollWorkers = () => { - // When primary is dead all workers should be dead too - if (workers.some(pid => common.isAlive(pid))) { - setTimeout(pollWorkers, 50); - } - }; - - // Loop indefinitely until worker exit - pollWorkers(); - }), - ); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-primary-kill.js b/test/js/node/cluster/upstream/parallel/test-cluster-primary-kill.js deleted file mode 100644 index 1a3a26f34d..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-primary-kill.js +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isWorker) { - // Keep the worker alive - const http = require("http"); - http.Server().listen(0, "127.0.0.1"); -} else if (process.argv[2] === "cluster") { - const worker = cluster.fork(); - - // send PID info to testcase process - process.send({ - pid: worker.process.pid, - }); - - // Terminate the cluster process - worker.once( - "listening", - common.mustCall(() => { - setTimeout(() => { - process.exit(0); - }, 1000); - }), - ); -} else { - // This is the testcase - const fork = require("child_process").fork; - - // Spawn a cluster process - const primary = fork(process.argv[1], ["cluster"]); - - // get pid info - let pid = null; - primary.once("message", data => { - pid = data.pid; - }); - - // When primary is dead - let alive = true; - primary.on( - "exit", - common.mustCall(code => { - // Make sure that the primary died on purpose - assert.strictEqual(code, 0); - - // Check worker process status - const pollWorker = () => { - alive = common.isAlive(pid); - if (alive) { - setTimeout(pollWorker, 50); - } - }; - // Loop indefinitely until worker exit. - pollWorker(); - }), - ); - - process.once("exit", () => { - assert.strictEqual(typeof pid, "number", `got ${pid} instead of a worker pid`); - assert.strictEqual(alive, false, `worker was alive after primary died (alive = ${alive})`); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-process-disconnect.js b/test/js/node/cluster/upstream/parallel/test-cluster-process-disconnect.js deleted file mode 100644 index bcaf7df146..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-process-disconnect.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isPrimary) { - const worker = cluster.fork(); - worker.on( - "exit", - common.mustCall((code, signal) => { - assert.strictEqual(code, 0, `Worker did not exit normally with code: ${code}`); - assert.strictEqual(signal, null, `Worker did not exit normally with signal: ${signal}`); - }), - ); -} else { - const net = require("net"); - const server = net.createServer(); - server.listen( - 0, - common.mustCall(() => { - process.disconnect(); - }), - ); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-rr-handle-keep-loop-alive.js b/test/js/node/cluster/upstream/parallel/test-cluster-rr-handle-keep-loop-alive.js deleted file mode 100644 index 8bb183af33..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-rr-handle-keep-loop-alive.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; - -const common = require("../common"); -const cluster = require("cluster"); -const net = require("net"); -const assert = require("assert"); - -cluster.schedulingPolicy = cluster.SCHED_RR; - -if (cluster.isPrimary) { - let exited = false; - const worker = cluster.fork(); - worker.on("exit", () => { - exited = true; - }); - setTimeout(() => { - assert.ok(!exited); - worker.kill(); - }, 3000); -} else { - const server = net.createServer(common.mustNotCall()); - server.listen( - 0, - common.mustCall(() => process.channel.unref()), - ); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-rr-ref.js b/test/js/node/cluster/upstream/parallel/test-cluster-rr-ref.js deleted file mode 100644 index d5f0cbd083..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-rr-ref.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; - -const common = require("../common"); -const cluster = require("cluster"); -const net = require("net"); - -if (cluster.isPrimary) { - cluster.fork().on("message", function (msg) { - if (msg === "done") this.kill(); - }); -} else { - const server = net.createServer(common.mustNotCall()); - server.listen(0, function () { - server.unref(); - server.ref(); - server.close(function () { - process.send("done"); - }); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-send-deadlock.js b/test/js/node/cluster/upstream/parallel/test-cluster-send-deadlock.js deleted file mode 100644 index c5838a666c..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-send-deadlock.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Testing mutual send of handles: from primary to worker, and from worker to -// primary. - -require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); -const net = require("net"); - -if (cluster.isPrimary) { - const worker = cluster.fork(); - worker.on("exit", (code, signal) => { - assert.strictEqual(code, 0, `Worker exited with an error code: ${code}`); - assert(!signal, `Worker exited by a signal: ${signal}`); - server.close(); - }); - - const server = net.createServer(socket => { - worker.send("handle", socket); - }); - - server.listen(0, () => { - worker.send({ message: "listen", port: server.address().port }); - }); -} else { - process.on("message", (msg, handle) => { - if (msg.message && msg.message === "listen") { - assert(msg.port); - const client1 = net.connect( - { - host: "localhost", - port: msg.port, - }, - () => { - const client2 = net.connect( - { - host: "localhost", - port: msg.port, - }, - () => { - client1.on("close", onclose); - client2.on("close", onclose); - client1.end(); - client2.end(); - }, - ); - }, - ); - let waiting = 2; - const onclose = () => { - if (--waiting === 0) cluster.worker.disconnect(); - }; - } else { - process.send("reply", handle); - } - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-argv.js b/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-argv.js deleted file mode 100644 index 8908aa7372..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-argv.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -setTimeout(common.mustNotCall("setup not emitted"), 1000).unref(); - -cluster.on( - "setup", - common.mustCall(function () { - const clusterArgs = cluster.settings.args; - const realArgs = process.argv; - assert.strictEqual(clusterArgs[clusterArgs.length - 1], realArgs[realArgs.length - 1]); - }), -); - -assert.notStrictEqual(process.argv[process.argv.length - 1], "OMG,OMG"); -process.argv.push("OMG,OMG"); -process.argv.push("OMG,OMG"); -cluster.setupPrimary(); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-cumulative.js b/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-cumulative.js deleted file mode 100644 index f9b43121fb..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-cumulative.js +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -assert(cluster.isPrimary); - -// cluster.settings should not be initialized until needed -assert.deepStrictEqual(cluster.settings, {}); - -cluster.setupPrimary(); -assert.deepStrictEqual(cluster.settings, { - args: process.argv.slice(2), - exec: process.argv[1], - execArgv: process.execArgv, - silent: false, -}); -console.log("ok sets defaults"); - -cluster.setupPrimary({ exec: "overridden" }); -assert.strictEqual(cluster.settings.exec, "overridden"); -console.log("ok overrides defaults"); - -cluster.setupPrimary({ args: ["foo", "bar"] }); -assert.strictEqual(cluster.settings.exec, "overridden"); -assert.deepStrictEqual(cluster.settings.args, ["foo", "bar"]); - -cluster.setupPrimary({ execArgv: ["baz", "bang"] }); -assert.strictEqual(cluster.settings.exec, "overridden"); -assert.deepStrictEqual(cluster.settings.args, ["foo", "bar"]); -assert.deepStrictEqual(cluster.settings.execArgv, ["baz", "bang"]); -console.log("ok preserves unchanged settings on repeated calls"); - -cluster.setupPrimary(); -assert.deepStrictEqual(cluster.settings, { - args: ["foo", "bar"], - exec: "overridden", - execArgv: ["baz", "bang"], - silent: false, -}); -console.log("ok preserves current settings"); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-emit.js b/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-emit.js deleted file mode 100644 index 305ebfced2..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-emit.js +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -assert(cluster.isPrimary); - -function emitAndCatch(next) { - cluster.once( - "setup", - common.mustCall(function (settings) { - assert.strictEqual(settings.exec, "new-exec"); - setImmediate(next); - }), - ); - cluster.setupPrimary({ exec: "new-exec" }); -} - -function emitAndCatch2(next) { - cluster.once( - "setup", - common.mustCall(function (settings) { - assert("exec" in settings); - setImmediate(next); - }), - ); - cluster.setupPrimary(); -} - -emitAndCatch( - common.mustCall(function () { - emitAndCatch2(common.mustCall()); - }), -); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary.js b/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary.js deleted file mode 100644 index ccb103cf08..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary.js +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isWorker) { - // Just keep the worker alive - process.send(process.argv[2]); -} else if (cluster.isPrimary) { - const checks = { - args: false, - setupEvent: false, - settingsObject: false, - }; - - const totalWorkers = 2; - let settings; - - // Setup primary - cluster.setupPrimary({ - args: ["custom argument"], - silent: true, - }); - - cluster.once("setup", function () { - checks.setupEvent = true; - - settings = cluster.settings; - if ( - settings && - settings.args && - settings.args[0] === "custom argument" && - settings.silent === true && - settings.exec === process.argv[1] - ) { - checks.settingsObject = true; - } - }); - - let correctInput = 0; - - cluster.on( - "online", - common.mustCall(function listener(worker) { - worker.once("message", function (data) { - correctInput += data === "custom argument" ? 1 : 0; - if (correctInput === totalWorkers) { - checks.args = true; - } - worker.kill(); - }); - }, totalWorkers), - ); - - // Start all workers - cluster.fork(); - cluster.fork(); - - // Check all values - process.once("exit", function () { - const argsMsg = - "Arguments was not send for one or more worker. " + - `${correctInput} workers receive argument, ` + - `but ${totalWorkers} were expected.`; - assert.ok(checks.args, argsMsg); - - assert.ok(checks.setupEvent, "The setup event was never emitted"); - - const settingObjectMsg = "The settingsObject do not have correct " + `properties : ${JSON.stringify(settings)}`; - assert.ok(checks.settingsObject, settingObjectMsg); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-shared-handle-bind-privileged-port.js b/test/js/node/cluster/upstream/parallel/test-cluster-shared-handle-bind-privileged-port.js deleted file mode 100644 index e69c79d697..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-shared-handle-bind-privileged-port.js +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -if (common.isLinux) return; // TODO: bun - -// Skip on OS X Mojave. https://github.com/nodejs/node/issues/21679 -if (common.isOSX) common.skip("macOS may allow ordinary processes to use any port"); - -if (common.isIBMi) common.skip("IBMi may allow ordinary processes to use any port"); - -if (common.isWindows) common.skip("not reliable on Windows"); - -if (process.getuid() === 0) common.skip("as this test should not be run as `root`"); - -const assert = require("assert"); -const cluster = require("cluster"); -const net = require("net"); - -if (cluster.isPrimary) { - // Primary opens and binds the socket and shares it with the worker. - cluster.schedulingPolicy = cluster.SCHED_NONE; - cluster.fork().on( - "exit", - common.mustCall(function (exitCode) { - assert.strictEqual(exitCode, 0); - }), - ); -} else { - const s = net.createServer(common.mustNotCall()); - s.listen(42, common.mustNotCall("listen should have failed")); - s.on( - "error", - common.mustCall(function (err) { - assert.strictEqual(err.code, "EACCES"); - process.disconnect(); - }), - ); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-uncaught-exception.js b/test/js/node/cluster/upstream/parallel/test-cluster-uncaught-exception.js deleted file mode 100644 index ee1dee617e..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-uncaught-exception.js +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Installing a custom uncaughtException handler should override the default -// one that the cluster module installs. -// https://github.com/joyent/node/issues/2556 - -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); -const fork = require("child_process").fork; - -const MAGIC_EXIT_CODE = 42; - -const isTestRunner = process.argv[2] !== "child"; - -if (isTestRunner) { - const primary = fork(__filename, ["child"]); - primary.on( - "exit", - common.mustCall(code => { - assert.strictEqual(code, MAGIC_EXIT_CODE); - }), - ); -} else if (cluster.isPrimary) { - process.on( - "uncaughtException", - common.mustCall(() => { - process.nextTick(() => process.exit(MAGIC_EXIT_CODE)); - }), - ); - cluster.fork(); - throw new Error("kill primary"); -} else { - // worker - process.exit(); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-death.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-death.js deleted file mode 100644 index bab5c8df8a..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-death.js +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (!cluster.isPrimary) { - process.exit(42); -} else { - const worker = cluster.fork(); - worker.on( - "exit", - common.mustCall(function (exitCode, signalCode) { - assert.strictEqual(exitCode, 42); - assert.strictEqual(signalCode, null); - }), - ); - cluster.on( - "exit", - common.mustCall(function (worker_) { - assert.strictEqual(worker_, worker); - }), - ); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-disconnect-on-error.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-disconnect-on-error.js deleted file mode 100644 index f9e3a0de2c..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-disconnect-on-error.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; -const common = require("../common"); -const http = require("http"); -const cluster = require("cluster"); -const assert = require("assert"); - -cluster.schedulingPolicy = cluster.SCHED_NONE; - -const server = http.createServer(); -if (cluster.isPrimary) { - let worker; - - server.listen( - 0, - common.mustSucceed(() => { - assert(worker); - - worker.send({ port: server.address().port }); - }), - ); - - worker = cluster.fork(); - worker.on( - "exit", - common.mustCall(() => { - server.close(); - }), - ); -} else { - process.on( - "message", - common.mustCall(msg => { - assert(msg.port); - - server.listen(msg.port); - server.on( - "error", - common.mustCall(e => { - cluster.worker.disconnect(); - }), - ); - }), - ); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-disconnect.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-disconnect.js deleted file mode 100644 index 35cae334d9..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-disconnect.js +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isWorker) { - const http = require("http"); - http.Server(() => {}).listen(0, "127.0.0.1"); - - cluster.worker.on( - "disconnect", - common.mustCall(() => { - process.exit(42); - }), - ); -} else if (cluster.isPrimary) { - const checks = { - cluster: { - emitDisconnect: false, - emitExit: false, - callback: false, - }, - worker: { - emitDisconnect: false, - emitDisconnectInsideWorker: false, - emitExit: false, - state: false, - voluntaryMode: false, - died: false, - }, - }; - - // start worker - const worker = cluster.fork(); - - // Disconnect worker when it is ready - worker.once( - "listening", - common.mustCall(() => { - const w = worker.disconnect(); - assert.strictEqual(worker, w, `${worker.id} did not return a reference`); - }), - ); - - // Check cluster events - cluster.once( - "disconnect", - common.mustCall(() => { - checks.cluster.emitDisconnect = true; - }), - ); - cluster.once( - "exit", - common.mustCall(() => { - checks.cluster.emitExit = true; - }), - ); - - // Check worker events and properties - worker.once( - "disconnect", - common.mustCall(() => { - checks.worker.emitDisconnect = true; - checks.worker.voluntaryMode = worker.exitedAfterDisconnect; - checks.worker.state = worker.state; - }), - ); - - // Check that the worker died - worker.once( - "exit", - common.mustCall(code => { - checks.worker.emitExit = true; - checks.worker.died = !common.isAlive(worker.process.pid); - checks.worker.emitDisconnectInsideWorker = code === 42; - }), - ); - - process.once("exit", () => { - const w = checks.worker; - const c = checks.cluster; - - // events - assert.ok(w.emitDisconnect, "Disconnect event did not emit"); - assert.ok(w.emitDisconnectInsideWorker, "Disconnect event did not emit inside worker"); - assert.ok(c.emitDisconnect, "Disconnect event did not emit"); - assert.ok(w.emitExit, "Exit event did not emit"); - assert.ok(c.emitExit, "Exit event did not emit"); - - // flags - assert.strictEqual(w.state, "disconnected"); - assert.strictEqual(w.voluntaryMode, true); - - // is process alive - assert.ok(w.died, "The worker did not die"); - }); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-exit.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-exit.js deleted file mode 100644 index e6e61ca604..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-exit.js +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// test-cluster-worker-exit.js -// verifies that, when a child process exits (by calling `process.exit(code)`) -// - the primary receives the proper events in the proper order, no duplicates -// - the exitCode and signalCode are correct in the 'exit' event -// - the worker.exitedAfterDisconnect flag, and worker.state are correct -// - the worker process actually goes away - -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -const EXIT_CODE = 42; - -if (cluster.isWorker) { - const http = require("http"); - const server = http.Server(() => {}); - - server.once( - "listening", - common.mustCall(() => { - process.exit(EXIT_CODE); - }), - ); - server.listen(0, "127.0.0.1"); -} else if (cluster.isPrimary) { - const expected_results = { - cluster_emitDisconnect: [1, "the cluster did not emit 'disconnect'"], - cluster_emitExit: [1, "the cluster did not emit 'exit'"], - cluster_exitCode: [EXIT_CODE, "the cluster exited w/ incorrect exitCode"], - cluster_signalCode: [null, "the cluster exited w/ incorrect signalCode"], - worker_emitDisconnect: [1, "the worker did not emit 'disconnect'"], - worker_emitExit: [1, "the worker did not emit 'exit'"], - worker_state: ["disconnected", "the worker state is incorrect"], - worker_exitedAfterDisconnect: [false, "the .exitedAfterDisconnect flag is incorrect"], - worker_died: [true, "the worker is still running"], - worker_exitCode: [EXIT_CODE, "the worker exited w/ incorrect exitCode"], - worker_signalCode: [null, "the worker exited w/ incorrect signalCode"], - }; - const results = { - cluster_emitDisconnect: 0, - cluster_emitExit: 0, - worker_emitDisconnect: 0, - worker_emitExit: 0, - }; - - // start worker - const worker = cluster.fork(); - - // Check cluster events - cluster.on( - "disconnect", - common.mustCall(() => { - results.cluster_emitDisconnect += 1; - }), - ); - cluster.on( - "exit", - common.mustCall(worker => { - results.cluster_exitCode = worker.process.exitCode; - results.cluster_signalCode = worker.process.signalCode; - results.cluster_emitExit += 1; - }), - ); - - // Check worker events and properties - worker.on( - "disconnect", - common.mustCall(() => { - results.worker_emitDisconnect += 1; - results.worker_exitedAfterDisconnect = worker.exitedAfterDisconnect; - results.worker_state = worker.state; - if (results.worker_emitExit > 0) { - process.nextTick(() => finish_test()); - } - }), - ); - - // Check that the worker died - worker.once( - "exit", - common.mustCall((exitCode, signalCode) => { - results.worker_exitCode = exitCode; - results.worker_signalCode = signalCode; - results.worker_emitExit += 1; - results.worker_died = !common.isAlive(worker.process.pid); - if (results.worker_emitDisconnect > 0) { - process.nextTick(() => finish_test()); - } - }), - ); - - const finish_test = () => { - try { - checkResults(expected_results, results); - } catch (exc) { - if (exc.name !== "AssertionError") { - console.trace(exc); - } - - process.exit(1); - return; - } - process.exit(0); - }; -} - -// Some helper functions ... - -function checkResults(expected_results, results) { - for (const k in expected_results) { - const actual = results[k]; - const expected = expected_results[k]; - - assert.strictEqual( - actual, - expected && expected.length ? expected[0] : expected, - `${expected[1] || ""} [expected: ${expected[0]} / actual: ${actual}]`, - ); - } -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-isdead.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-isdead.js deleted file mode 100644 index 079a154443..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-isdead.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -require("../common"); -const cluster = require("cluster"); -const assert = require("assert"); - -if (cluster.isPrimary) { - const worker = cluster.fork(); - let workerDead = worker.isDead(); - assert.ok( - !workerDead, - `isDead() returned ${workerDead}. isDead() should return ` + "false right after the worker has been created.", - ); - - worker.on("exit", function () { - workerDead = worker.isDead(); - assert.ok( - workerDead, - `isDead() returned ${workerDead}. After an event has been ` + "emitted, isDead should return true", - ); - }); - - worker.on("message", function (msg) { - if (msg === "readyToDie") { - worker.kill(); - } - }); -} else if (cluster.isWorker) { - const workerDead = cluster.worker.isDead(); - assert.ok( - !workerDead, - `isDead() returned ${workerDead}. isDead() should return ` + "false when called from within a worker", - ); - process.send("readyToDie"); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-kill-signal.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-kill-signal.js deleted file mode 100644 index 1562a5e9f3..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-kill-signal.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -// test-cluster-worker-kill-signal.js -// verifies that when we're killing a worker using Worker.prototype.kill -// and the worker's process was killed with the given signal (SIGKILL) - -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isWorker) { - // Make the worker run something - const http = require("http"); - const server = http.Server(() => {}); - - server.once("listening", common.mustCall()); - server.listen(0, "127.0.0.1"); -} else if (cluster.isMaster) { - const KILL_SIGNAL = "SIGKILL"; - - // Start worker - const worker = cluster.fork(); - - // When the worker is up and running, kill it - worker.once( - "listening", - common.mustCall(() => { - worker.kill(KILL_SIGNAL); - }), - ); - - // Check worker events and properties - worker.on( - "disconnect", - common.mustCall(() => { - assert.strictEqual(worker.exitedAfterDisconnect, false); - assert.strictEqual(worker.state, "disconnected"); - }, 1), - ); - - // Check that the worker died - worker.once( - "exit", - common.mustCall((exitCode, signalCode) => { - const isWorkerProcessStillAlive = common.isAlive(worker.process.pid); - const numOfRunningWorkers = Object.keys(cluster.workers).length; - - assert.strictEqual(exitCode, null); - assert.strictEqual(signalCode, KILL_SIGNAL); - assert.strictEqual(isWorkerProcessStillAlive, false); - assert.strictEqual(numOfRunningWorkers, 0); - }, 1), - ); - - // Check if the cluster was killed as well - cluster.on("exit", common.mustCall(1)); -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-kill.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-kill.js deleted file mode 100644 index 1ba588b874..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-kill.js +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// test-cluster-worker-kill.js -// verifies that, when a child process is killed (we use SIGKILL) -// - the primary receives the proper events in the proper order, no duplicates -// - the exitCode and signalCode are correct in the 'exit' event -// - the worker.exitedAfterDisconnect flag, and worker.state are correct -// - the worker process actually goes away - -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); - -if (cluster.isWorker) { - const http = require("http"); - const server = http.Server(() => {}); - server.once("listening", common.mustCall()); - server.listen(0, "127.0.0.1"); -} else if (cluster.isPrimary) { - const KILL_SIGNAL = "SIGKILL"; - const expected_results = { - cluster_emitDisconnect: [1, "the cluster did not emit 'disconnect'"], - cluster_emitExit: [1, "the cluster did not emit 'exit'"], - cluster_exitCode: [null, "the cluster exited w/ incorrect exitCode"], - cluster_signalCode: [KILL_SIGNAL, "the cluster exited w/ incorrect signalCode"], - worker_emitDisconnect: [1, "the worker did not emit 'disconnect'"], - worker_emitExit: [1, "the worker did not emit 'exit'"], - worker_state: ["disconnected", "the worker state is incorrect"], - worker_exitedAfter: [false, "the .exitedAfterDisconnect flag is incorrect"], - worker_died: [true, "the worker is still running"], - worker_exitCode: [null, "the worker exited w/ incorrect exitCode"], - worker_signalCode: [KILL_SIGNAL, "the worker exited w/ incorrect signalCode"], - }; - const results = { - cluster_emitDisconnect: 0, - cluster_emitExit: 0, - worker_emitDisconnect: 0, - worker_emitExit: 0, - }; - - // start worker - const worker = cluster.fork(); - // When the worker is up and running, kill it - worker.once( - "listening", - common.mustCall(() => { - worker.process.kill(KILL_SIGNAL); - }), - ); - - // Check cluster events - cluster.on( - "disconnect", - common.mustCall(() => { - results.cluster_emitDisconnect += 1; - }), - ); - cluster.on( - "exit", - common.mustCall(worker => { - results.cluster_exitCode = worker.process.exitCode; - results.cluster_signalCode = worker.process.signalCode; - results.cluster_emitExit += 1; - }), - ); - - // Check worker events and properties - worker.on( - "disconnect", - common.mustCall(() => { - results.worker_emitDisconnect += 1; - results.worker_exitedAfter = worker.exitedAfterDisconnect; - results.worker_state = worker.state; - }), - ); - - // Check that the worker died - worker.once( - "exit", - common.mustCall((exitCode, signalCode) => { - results.worker_exitCode = exitCode; - results.worker_signalCode = signalCode; - results.worker_emitExit += 1; - results.worker_died = !common.isAlive(worker.process.pid); - }), - ); - - process.on("exit", () => { - checkResults(expected_results, results); - }); -} - -// Some helper functions ... - -function checkResults(expected_results, results) { - for (const k in expected_results) { - const actual = results[k]; - const expected = expected_results[k]; - - assert.strictEqual( - actual, - expected && expected.length ? expected[0] : expected, - `${expected[1] || ""} [expected: ${expected[0]} / actual: ${actual}]`, - ); - } -} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-no-exit.js b/test/js/node/cluster/upstream/parallel/test-cluster-worker-no-exit.js deleted file mode 100644 index 8dcfc45f2c..0000000000 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-no-exit.js +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const common = require("../common"); -if (common.isOSX) return; // TODO: bun -if (common.isLinux) return; // TODO: bun -if (common.isWindows) return; // TODO: bun -const assert = require("assert"); -const cluster = require("cluster"); -const net = require("net"); - -let destroyed; -let success; -let worker; -let server; - -// Workers do not exit on disconnect, they exit under normal node rules: when -// they have nothing keeping their loop alive, like an active connection -// -// test this by: -// -// 1 creating a server, so worker can make a connection to something -// 2 disconnecting worker -// 3 wait to confirm it did not exit -// 4 destroy connection -// 5 confirm it does exit -if (cluster.isPrimary) { - server = net - .createServer(function (conn) { - server.close(); - worker.disconnect(); - worker - .once("disconnect", function () { - setTimeout(function () { - conn.destroy(); - destroyed = true; - }, 1000); - }) - .once("exit", function () { - // Worker should not exit while it has a connection - assert(destroyed, "worker exited before connection destroyed"); - success = true; - }); - }) - .listen(0, function () { - const port = this.address().port; - - worker = cluster.fork().on("online", function () { - this.send({ port }); - }); - }); - process.on("exit", function () { - assert(success); - }); -} else { - process.on("message", function (msg) { - // We shouldn't exit, not while a network connection exists - net.connect(msg.port); - }); -} diff --git a/test/js/node/crypto/node-crypto.test.js b/test/js/node/crypto/node-crypto.test.js index f8cb1fb62f..af41d61407 100644 --- a/test/js/node/crypto/node-crypto.test.js +++ b/test/js/node/crypto/node-crypto.test.js @@ -439,7 +439,7 @@ describe("createHash", () => { it("repeated calls doesnt segfault", () => { function fn() { - crypto.createHash("sha1").update(Math.random(), "ascii").digest("base64"); + crypto.createHash("sha1").update(Math.random().toString(), "ascii").digest("base64"); } for (let i = 0; i < 10; i++) fn(); diff --git a/test/js/node/crypto/pbkdf2.test.ts b/test/js/node/crypto/pbkdf2.test.ts index 5d06fb1db7..585b6554cf 100644 --- a/test/js/node/crypto/pbkdf2.test.ts +++ b/test/js/node/crypto/pbkdf2.test.ts @@ -1,4 +1,5 @@ const crypto = require("crypto"); +const common = require("../test/common"); import { describe, expect, jest, test } from "bun:test"; function testPBKDF2_(password, salt, iterations, keylen, expected) { @@ -72,13 +73,13 @@ describe("invalid inputs", () => { for (let input of ["test", [], true, undefined, null]) { test(`${input} is invalid`, () => { expect(() => crypto.pbkdf2("pass", "salt", input, 8, "sha256")).toThrow( - `The "iteration count" argument must be of type integer. Received ${input}`, + `The "iterations" argument must be of type number.${common.invalidArgTypeHelper(input)}`, ); }); } test(`{} is invalid`, () => { expect(() => crypto.pbkdf2("pass", "salt", {}, 8, "sha256")).toThrow( - `The "iteration count" argument must be of type integer. Received {}`, + `The "iterations" argument must be of type number.${common.invalidArgTypeHelper({})}`, ); }); @@ -97,7 +98,7 @@ describe("invalid inputs", () => { }); expect(() => { crypto.pbkdf2("password", "salt", 1, input, "sha256", outer); - }).toThrow("keylen must be > 0 and < 2147483647"); + }).toThrow(`The value of "keylen" is out of range. It must be >= 0 and <= 2147483647. Received ${input}`); expect(outer).not.toHaveBeenCalled(); }); }); @@ -113,20 +114,22 @@ describe("invalid inputs", () => { thrown = e as Error; } expect(thrown.code).toBe("ERR_CRYPTO_INVALID_DIGEST"); - expect(thrown.message).toBe('Unsupported algorithm "md55"'); + expect(thrown.message).toBe("Invalid digest: md55"); }); }); [Infinity, -Infinity, NaN].forEach(input => { test(`${input} keylen`, () => { expect(() => crypto.pbkdf2("password", "salt", 1, input, "sha256")).toThrow( - `The \"keylen\" argument must be of type integer. Received ${input}`, + `The value of "keylen" is out of range. It must be an integer. Received ${input}`, ); }); }); [-1, 2147483648, 4294967296].forEach(input => { test(`${input} keylen`, () => { - expect(() => crypto.pbkdf2("password", "salt", 1, input, "sha256")).toThrow("keylen must be > 0 and < 2147483647"); + expect(() => crypto.pbkdf2("password", "salt", 1, input, "sha256")).toThrow( + `The value of "keylen" is out of range. It must be >= 0 and <= 2147483647. Received ${input}`, + ); }); }); diff --git a/test/js/node/dns/node-dns.test.js b/test/js/node/dns/node-dns.test.js index ecab13bd3f..44924c3abd 100644 --- a/test/js/node/dns/node-dns.test.js +++ b/test/js/node/dns/node-dns.test.js @@ -220,8 +220,6 @@ test("dns.resolveNs (empty string) ", () => { dns.resolveNs("", (err, results) => { try { expect(err).toBeNull(); - console.log("resolveNs:", results); - expect(results instanceof Array).toBe(true); // root servers expect(results.sort()).toStrictEqual( @@ -254,7 +252,6 @@ test("dns.resolvePtr (ptr.socketify.dev)", () => { dns.resolvePtr("ptr.socketify.dev", (err, results) => { try { expect(err).toBeNull(); - console.log("resolvePtr:", results); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); @@ -270,7 +267,6 @@ test("dns.resolveCname (cname.socketify.dev)", () => { dns.resolveCname("cname.socketify.dev", (err, results) => { try { expect(err).toBeNull(); - console.log("resolveCname:", results); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); @@ -365,7 +361,7 @@ describe("dns.reverse", () => { ["2606:4700:4700::1001", "one.one.one.one"], ["1.1.1.1", "one.one.one.one"], ]; - it.each(inputs)("%s", (ip, expected) => { + it.each(inputs)("%s <- %s", (ip, expected) => { const { promise, resolve, reject } = Promise.withResolvers(); dns.reverse(ip, (err, hostnames) => { try { @@ -427,7 +423,7 @@ describe("test invalid arguments", () => { }).toThrow("Expected address to be a non-empty string for 'lookupService'."); expect(() => { dns.lookupService("google.com", 443, (err, hostname, service) => {}); - }).toThrow("Expected address to be a invalid address for 'lookupService'."); + }).toThrow('The "address" argument is invalid. Received type string ("google.com")'); }); }); @@ -486,7 +482,7 @@ describe("dns.lookupService", () => { ["1.1.1.1", 80, ["one.one.one.one", "http"]], ["1.1.1.1", 443, ["one.one.one.one", "https"]], ])("promises.lookupService(%s, %d)", async (address, port, expected) => { - const [hostname, service] = await dns.promises.lookupService(address, port); + const { hostname, service } = await dns.promises.lookupService(address, port); expect(hostname).toStrictEqual(expected[0]); expect(service).toStrictEqual(expected[1]); }); diff --git a/test/js/node/fs/cp.test.ts b/test/js/node/fs/cp.test.ts index 134dbd016d..4972aac905 100644 --- a/test/js/node/fs/cp.test.ts +++ b/test/js/node/fs/cp.test.ts @@ -1,6 +1,6 @@ import { describe, expect, jest, test } from "bun:test"; import fs from "fs"; -import { tempDirWithFiles } from "harness"; +import { isWindows, tempDirWithFiles } from "harness"; import { join } from "path"; const impls = [ diff --git a/test/js/node/fs/fs-oom.test.ts b/test/js/node/fs/fs-oom.test.ts index a859dc6a89..22749c15aa 100644 --- a/test/js/node/fs/fs-oom.test.ts +++ b/test/js/node/fs/fs-oom.test.ts @@ -7,14 +7,14 @@ setSyntheticAllocationLimitForTesting(128 * 1024 * 1024); // /dev/zero reports a size of 0. So we need a separate test for reDgular files that are huge. if (isPosix) { test("fs.readFileSync(/dev/zero) should throw an OOM without crashing the process.", () => { - expect(() => readFileSync("/dev/zero")).toThrow("Out of memory"); + expect(() => readFileSync("/dev/zero")).toThrow("ENOMEM: not enough memory, read '/dev/zero'"); Bun.gc(true); }); test.each(["utf8", "ucs2", "latin1", "hex", "base64", "base64url"] as const)( "fs.readFileSync(/dev/zero, '%s') should throw an OOM without crashing the process.", encoding => { - expect(() => readFileSync("/dev/zero", encoding)).toThrow("Out of memory"); + expect(() => readFileSync("/dev/zero", encoding)).toThrow("ENOMEM: not enough memory, read '/dev/zero'"); Bun.gc(true); }, ); @@ -29,7 +29,7 @@ if (isLinux) { let buf = new Uint8Array(8 * 1024 * 1024); buf.fill(42); for (let i = 0; i < 1024 * 1024 * 16 + 1; i += buf.byteLength) { - writeSync(memfd, buf, i, buf.byteLength); + writeSync(memfd, buf, 0, buf.byteLength, i); } })(memfd); Bun.gc(true); @@ -37,7 +37,7 @@ if (isLinux) { try { expect(() => (encoding === "buffer" ? readFileSync(memfd) : readFileSync(memfd, encoding))).toThrow( - "Out of memory", + "ENOMEM: not enough memory", ); } finally { Bun.gc(true); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 315bfb8e0b..2bd86c6cad 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, spyOn } from "bun:test"; -import { bunEnv, bunExe, gc, getMaxFD, isIntelMacOS, isWindows, tempDirWithFiles, tmpdirSync } from "harness"; +import { bunEnv, bunExe, gc, getMaxFD, isBroken, isIntelMacOS, isWindows, tempDirWithFiles, tmpdirSync } from "harness"; import { isAscii } from "node:buffer"; import fs, { closeSync, @@ -584,8 +584,8 @@ describe("mkdirSync", () => { }); it("should throw ENOENT for empty string", () => { - expect(() => mkdirSync("", { recursive: true })).toThrow("No such file or directory"); - expect(() => mkdirSync("")).toThrow("No such file or directory"); + expect(() => mkdirSync("", { recursive: true })).toThrow("no such file or directory"); + expect(() => mkdirSync("")).toThrow("no such file or directory"); }); it("throws for invalid options", () => { @@ -597,7 +597,7 @@ describe("mkdirSync", () => { // @ts-expect-error { recursive: "lalala" }, ), - ).toThrow("The \"recursive\" property must be of type boolean, got string"); + ).toThrow('The "recursive" property must be of type boolean, got string'); }); }); @@ -673,7 +673,7 @@ it("promises.readFile", async () => { expect.unreachable(); } catch (e: any) { expect(e).toBeInstanceOf(Error); - expect(e.message).toBe("No such file or directory"); + expect(e.message).toBe("ENOENT: no such file or directory, open '/i-dont-exist'"); expect(e.code).toBe("ENOENT"); expect(e.errno).toBe(-2); expect(e.path).toBe("/i-dont-exist"); @@ -1004,10 +1004,10 @@ it("statSync throwIfNoEntry", () => { it("statSync throwIfNoEntry: true", () => { const path = join(tmpdirSync(), "does", "not", "exist"); - expect(() => statSync(path, { throwIfNoEntry: true })).toThrow("No such file or directory"); - expect(() => statSync(path)).toThrow("No such file or directory"); - expect(() => lstatSync(path, { throwIfNoEntry: true })).toThrow("No such file or directory"); - expect(() => lstatSync(path)).toThrow("No such file or directory"); + expect(() => statSync(path, { throwIfNoEntry: true })).toThrow("no such file or directory"); + expect(() => statSync(path)).toThrow("no such file or directory"); + expect(() => lstatSync(path, { throwIfNoEntry: true })).toThrow("no such file or directory"); + expect(() => lstatSync(path)).toThrow("no such file or directory"); }); it("stat == statSync", async () => { @@ -1115,7 +1115,8 @@ it("readdirSync throws when given a file path", () => { readdirSync(import.meta.path); throw new Error("should not get here"); } catch (exception: any) { - expect(exception.name).toBe("ENOTDIR"); + expect(exception.name).toBe("Error"); + expect(exception.code).toBe("ENOTDIR"); } }); @@ -1126,7 +1127,8 @@ it("readdirSync throws when given a path that doesn't exist", () => { } catch (exception: any) { // the correct error to return in this case is actually ENOENT (which we do on windows), // but on posix we return ENOTDIR - expect(exception.name).toMatch(/ENOTDIR|ENOENT/); + expect(exception.name).toBe("Error"); + expect(exception.code).toMatch(/ENOTDIR|ENOENT/); } }); @@ -1135,7 +1137,8 @@ it("readdirSync throws when given a file path with trailing slash", () => { readdirSync(import.meta.path + "/"); throw new Error("should not get here"); } catch (exception: any) { - expect(exception.name).toBe("ENOTDIR"); + expect(exception.name).toBe("Error"); + expect(exception.code).toBe("ENOTDIR"); } }); @@ -1406,6 +1409,36 @@ describe("readFile", () => { }); }); }); + + it("works with flags", async () => { + const mydir = tempDirWithFiles("fs-read", {}); + console.log(mydir); + + for (const [flag, code] of [ + ["a", "EBADF"], + ["ax", "EBADF"], + ["a+", undefined], + ["as", "EBADF"], + ["as+", undefined], + ["r", "ENOENT"], + ["rs", "ENOENT"], + ["r+", "ENOENT"], + ["rs+", "ENOENT"], + ["w", "EBADF"], + ["wx", "EBADF"], + ["w+", undefined], + ["wx+", undefined], + ]) { + const name = flag!.replace("+", "_plus") + ".txt"; + if (code == null) { + expect(readFileSync(mydir + "/" + name, { encoding: "utf8", flag })).toBe(""); + expect(readFileSync(mydir + "/" + name, { encoding: "utf8" })).toBe(""); + } else { + expect.toThrowWithCode(() => readFileSync(mydir + "/" + name, { encoding: "utf8", flag }), code); + expect.toThrowWithCode(() => readFileSync(mydir + "/" + name, { encoding: "utf8" }), "ENOENT"); + } + } + }); }); describe("writeFileSync", () => { @@ -2195,110 +2228,73 @@ describe("fs.ReadStream", () => { }); describe("createWriteStream", () => { - it("simple write stream finishes", async () => { - const path = `${tmpdir()}/fs.test.ts/${Date.now()}.createWriteStream.txt`; - const stream = createWriteStream(path); + it.todoIf(isBroken && isWindows)("simple write stream finishes", async () => { + const streamPath = join(tmpdirSync(), "create-write-stream.txt"); + const { promise: done, resolve, reject } = Promise.withResolvers(); + + const stream = createWriteStream(streamPath); + stream.on("error", reject); + stream.on("finish", resolve); stream.write("Test file written successfully"); stream.end(); - return await new Promise((resolve, reject) => { - stream.on("error", e => { - reject(e); - }); - - stream.on("finish", () => { - expect(readFileSync(path, "utf8")).toBe("Test file written successfully"); - resolve(true); - }); - }); + await done; + expect(readFileSync(streamPath, "utf8")).toBe("Test file written successfully"); }); it("writing null throws ERR_STREAM_NULL_VALUES", async () => { - const path = `${tmpdir()}/fs.test.ts/${Date.now()}.createWriteStreamNulls.txt`; - const stream = createWriteStream(path); - try { - stream.write(null); - expect(() => {}).toThrow(Error); - } catch (exception: any) { - expect(exception.code).toBe("ERR_STREAM_NULL_VALUES"); - } + const streamPath = join(tmpdirSync(), "create-write-stream-nulls.txt"); + const stream = createWriteStream(streamPath); + expect.toThrowWithCode(() => stream.write(null), "ERR_STREAM_NULL_VALUES"); }); it("writing null throws ERR_STREAM_NULL_VALUES (objectMode: true)", async () => { - const path = `${tmpdir()}/fs.test.ts/${Date.now()}.createWriteStreamNulls.txt`; - const stream = createWriteStream(path, { + const streamPath = join(tmpdirSync(), "create-write-stream-nulls-object-mode.txt"); + const stream = createWriteStream(streamPath, { // @ts-ignore-next-line objectMode: true, }); - try { - stream.write(null); - expect(() => {}).toThrow(Error); - } catch (exception: any) { - expect(exception.code).toBe("ERR_STREAM_NULL_VALUES"); - } + expect.toThrowWithCode(() => stream.write(null), "ERR_STREAM_NULL_VALUES"); }); it("writing false throws ERR_INVALID_ARG_TYPE", async () => { - const path = `${tmpdir()}/fs.test.ts/${Date.now()}.createWriteStreamFalse.txt`; - const stream = createWriteStream(path); - try { - stream.write(false); - expect(() => {}).toThrow(Error); - } catch (exception: any) { - expect(exception.code).toBe("ERR_INVALID_ARG_TYPE"); - } + const streamPath = join(tmpdirSync(), "create-write-stream-false.txt"); + const stream = createWriteStream(streamPath); + expect.toThrowWithCode(() => stream.write(false), "ERR_INVALID_ARG_TYPE"); }); it("writing false throws ERR_INVALID_ARG_TYPE (objectMode: true)", async () => { - const path = `${tmpdir()}/fs.test.ts/${Date.now()}.createWriteStreamFalse.txt`; - const stream = createWriteStream(path, { + const streamPath = join(tmpdirSync(), "create-write-stream-false-object-mode.txt"); + const stream = createWriteStream(streamPath, { // @ts-ignore-next-line objectMode: true, }); - try { - stream.write(false); - expect(() => {}).toThrow(Error); - } catch (exception: any) { - expect(exception.code).toBe("ERR_INVALID_ARG_TYPE"); - } + expect.toThrowWithCode(() => stream.write(false), "ERR_INVALID_ARG_TYPE"); }); it("writing in append mode should not truncate the file", async () => { - const path = `${tmpdir()}/fs.test.ts/${Date.now()}.createWriteStreamAppend.txt`; - const stream = createWriteStream(path, { + const streamPath = join(tmpdirSync(), "create-write-stream-append.txt"); + const stream = createWriteStream(streamPath, { // @ts-ignore-next-line flags: "a", }); + + const { promise: done1, resolve: resolve1, reject: reject1 } = Promise.withResolvers(); + stream.on("error", reject1); + stream.on("finish", resolve1); stream.write("first line\n"); stream.end(); + await done1; - await new Promise((resolve, reject) => { - stream.on("error", e => { - reject(e); - }); - - stream.on("finish", () => { - resolve(true); - }); - }); - - const stream2 = createWriteStream(path, { - // @ts-ignore-next-line - flags: "a", - }); + const { promise: done2, resolve: resolve2, reject: reject2 } = Promise.withResolvers(); + const stream2 = createWriteStream(streamPath, { flags: "a" }); + stream2.on("error", reject2); + stream2.on("finish", resolve2); stream2.write("second line\n"); stream2.end(); + await done2; - return await new Promise((resolve, reject) => { - stream2.on("error", e => { - reject(e); - }); - - stream2.on("finish", () => { - expect(readFileSync(path, "utf8")).toBe("first line\nsecond line\n"); - resolve(true); - }); - }); + expect(readFileSync(streamPath, "utf8")).toBe("first line\nsecond line\n"); }); it("should emit open and call close callback", done => { @@ -2429,9 +2425,9 @@ describe("fs/promises", () => { const text = await new Response(subprocess.stdout).text(); const node = JSON.parse(text); expect(bun.length).toEqual(node.length); - expect([...new Set(node.map(v => v.path))]).toEqual([full]); - expect([...new Set(bun.map(v => v.path))]).toEqual([full]); - expect(bun.map(v => join(v.path, v.name)).sort()).toEqual(node.map(v => join(v.path, v.name)).sort()); + expect([...new Set(node.map(v => v.parentPath))]).toEqual([full]); + expect([...new Set(bun.map(v => v.parentPath))]).toEqual([full]); + expect(bun.map(v => join(v.parentPath, v.name)).sort()).toEqual(node.map(v => join(v.path, v.name)).sort()); }, 100000); it("readdir(path, {withFileTypes: true, recursive: true}) produces the same result as Node.js", async () => { @@ -2699,18 +2695,8 @@ it("fstatSync(decimal)", () => { expect(() => fstatSync(eval("-1.0"))).toThrow(); expect(() => fstatSync(eval("Infinity"))).toThrow(); expect(() => fstatSync(eval("-Infinity"))).toThrow(); - expect(() => - fstatSync( - // > max int32 is not valid in most C APIs still. - 2147483647 + 1, - ), - ).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); - expect(() => - fstatSync( - // max int32 is a valid fd - 2147483647, - ), - ).toThrow(expect.objectContaining({ code: "EBADF" })); + expect(() => fstatSync(2147483647 + 1)).toThrow(expect.objectContaining({ code: "ERR_OUT_OF_RANGE" })); // > max int32 is not valid in most C APIs still. + expect(() => fstatSync(2147483647)).toThrow(expect.objectContaining({ code: "EBADF" })); // max int32 is a valid fd }); it("fstat on a large file", () => { @@ -3293,26 +3279,26 @@ it("new Stats", () => { it("test syscall errno, issue#4198", () => { const path = `${tmpdir()}/non-existent-${Date.now()}.txt`; - expect(() => openSync(path, "r")).toThrow("No such file or directory"); - expect(() => readSync(2147483640, Buffer.alloc(1))).toThrow("Bad file descriptor"); - expect(() => readlinkSync(path)).toThrow("No such file or directory"); - expect(() => realpathSync(path)).toThrow("No such file or directory"); - expect(() => readFileSync(path)).toThrow("No such file or directory"); - expect(() => renameSync(path, `${path}.2`)).toThrow("No such file or directory"); - expect(() => statSync(path)).toThrow("No such file or directory"); - expect(() => unlinkSync(path)).toThrow("No such file or directory"); - expect(() => rmSync(path)).toThrow("No such file or directory"); - expect(() => rmdirSync(path)).toThrow("No such file or directory"); - expect(() => closeSync(2147483640)).toThrow("Bad file descriptor"); + expect(() => openSync(path, "r")).toThrow("no such file or directory"); + expect(() => readSync(2147483640, Buffer.alloc(1))).toThrow("bad file descriptor"); + expect(() => readlinkSync(path)).toThrow("no such file or directory"); + expect(() => realpathSync(path)).toThrow("no such file or directory"); + expect(() => readFileSync(path)).toThrow("no such file or directory"); + expect(() => renameSync(path, `${path}.2`)).toThrow("no such file or directory"); + expect(() => statSync(path)).toThrow("no such file or directory"); + expect(() => unlinkSync(path)).toThrow("no such file or directory"); + expect(() => rmSync(path)).toThrow("no such file or directory"); + expect(() => rmdirSync(path)).toThrow("no such file or directory"); + expect(() => closeSync(2147483640)).toThrow("bad file descriptor"); mkdirSync(path); - expect(() => mkdirSync(path)).toThrow("File or folder exists"); + expect(() => mkdirSync(path)).toThrow("file already exists"); expect(() => unlinkSync(path)).toThrow( ( { - "darwin": "Operation not permitted", - "linux": "Is a directory", - "win32": "Operation not permitted", + "darwin": "operation not permitted", + "linux": "illegal operation on a directory", + "win32": "operation not permitted", } as any )[process.platform], ); @@ -3476,3 +3462,13 @@ it("open mode verification", async () => { RangeError(`The value of "mode" is out of range. It must be an integer. Received 4294967298.5`), ); }); + +it("fs.mkdirSync recursive should not error when the directory already exists, but should error when its a file", () => { + expect(() => mkdirSync(import.meta.dir, { recursive: true })).not.toThrowError(); + expect(() => mkdirSync(import.meta.path, { recursive: true })).toThrowError(); +}); + +it("fs.mkdirSync recursive: false should error when the directory already exists, regardless if its a file or dir", () => { + expect(() => mkdirSync(import.meta.dir, { recursive: false })).toThrowError(); + expect(() => mkdirSync(import.meta.path, { recursive: false })).toThrowError(); +}); diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index f8f20089a1..52bfb3c33a 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -1,4 +1,8 @@ +/** + * @note this file patches `node:test` via the require cache. + */ import { AnyFunction } from "bun"; +import os from "node:os"; import { hideFromStackTrace } from "harness"; import assertNode from "node:assert"; @@ -262,3 +266,126 @@ export function createTest(path: string) { declare namespace Bun { function jest(path: string): typeof import("bun:test"); } + +const normalized = os.platform() === "win32" ? Bun.main.replaceAll("\\", "/") : Bun.main; +if (normalized.includes("node/test/parallel")) { + function createMockNodeTestModule() { + interface TestError extends Error { + testStack: string[]; + } + type Context = { + filename: string; + testStack: string[]; + failures: Error[]; + successes: number; + addFailure(err: unknown): TestError; + recordSuccess(): void; + }; + const contexts: Record = {}; + + // @ts-ignore + let activeSuite: Context = undefined; + + function createContext(key: string): Context { + return { + filename: key, // duplicate for ease-of-use + // entered each time describe, it, etc is called + testStack: [], + failures: [], + successes: 0, + addFailure(err: unknown) { + const error: TestError = (err instanceof Error ? err : new Error(err as any)) as any; + error.testStack = this.testStack; + const testMessage = `Test failed: ${this.testStack.join(" > ")}`; + error.message = testMessage + "\n" + error.message; + this.failures.push(error); + console.error(error); + return error; + }, + recordSuccess() { + const fullname = this.testStack.join(" > "); + console.log("✅ Test passed:", fullname); + this.successes++; + }, + }; + } + + function getContext() { + const key: string = Bun.main; // module.parent?.filename ?? require.main?.filename ?? __filename; + return (activeSuite = contexts[key] ??= createContext(key)); + } + + async function test( + label: string | Function, + optionsOrFn: Record | Function, + fn?: Function | undefined, + ) { + let options = optionsOrFn; + if (arguments.length === 2) { + assertNode.equal(typeof optionsOrFn, "function", "Second argument to test() must be a function."); + fn = optionsOrFn as Function; + options = {}; + } + if (typeof fn !== "function" && typeof label === "function") { + fn = label; + label = fn.name; + options = {}; + } + + const ctx = getContext(); + const { skip } = options; + + if (skip) return; + try { + ctx.testStack.push(label as string); + await fn(); + ctx.recordSuccess(); + } catch (err) { + const error = ctx.addFailure(err); + throw error; + } finally { + ctx.testStack.pop(); + } + } + + function describe(labelOrFn: string | Function, maybeFn?: Function) { + const [label, fn] = typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn]; + if (typeof fn !== "function") throw new TypeError("Second argument to describe() must be a function."); + + getContext().testStack.push(label); + try { + fn(); + } catch (e) { + getContext().addFailure(e); + throw e; + } finally { + getContext().testStack.pop(); + } + + const failures = getContext().failures.length; + const successes = getContext().successes; + console.error(`describe("${label}") finished with ${successes} passed and ${failures} failed tests.`); + if (failures > 0) { + throw new Error(`${failures} tests failed.`); + } + } + + return { + test, + describe, + }; + } + + require.cache["node:test"] ??= { + exports: createMockNodeTestModule(), + loaded: true, + isPreloading: false, + id: "node:test", + parent: require.main, + filename: "node:test", + children: [], + path: "node:test", + paths: [], + require, + }; +} diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index 7400f06eb3..a65e3bbb4a 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -2435,3 +2435,125 @@ it("should work when sending https.request with agent:false", async () => { await promise; }); +it("client should use chunked encoded if more than one write is called", async () => { + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + // Bun.serve is used here until #15576 or similar fix is merged + using server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + fetch(req) { + if (req.headers.get("transfer-encoding") !== "chunked") { + return new Response("should be chunked encoding", { status: 500 }); + } + return new Response(req.body); + }, + }); + + // Options for the HTTP request + const options = { + hostname: "127.0.0.1", // Replace with the target server + port: server.port, + path: "/api/data", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }; + + const { promise, resolve, reject } = Promise.withResolvers(); + + // Create the request + const req = http.request(options, res => { + if (res.statusCode !== 200) { + reject(new Error("Body should be chunked")); + } + const chunks = []; + // Collect the response data + res.on("data", chunk => { + chunks.push(chunk); + }); + + res.on("end", () => { + resolve(chunks); + }); + }); + + // Handle errors + req.on("error", reject); + + // Write chunks to the request body + + for (let i = 0; i < 4; i++) { + req.write("chunk"); + await sleep(50); + req.write(" "); + await sleep(50); + } + req.write("BUN!"); + // End the request and signal no more data will be sent + req.end(); + + const chunks = await promise; + expect(chunks.length).toBeGreaterThan(1); + expect(chunks[chunks.length - 1]?.toString()).toEndWith("BUN!"); + expect(Buffer.concat(chunks).toString()).toBe("chunk ".repeat(4) + "BUN!"); +}); + +it("client should use content-length if only one write is called", async () => { + await using server = http.createServer((req, res) => { + if (req.headers["transfer-encoding"] === "chunked") { + return res.writeHead(500).end(); + } + res.writeHead(200); + req.on("data", data => { + res.write(data); + }); + req.on("end", () => { + res.end(); + }); + }); + + await once(server.listen(0, "127.0.0.1"), "listening"); + + // Options for the HTTP request + const options = { + hostname: "127.0.0.1", // Replace with the target server + port: server.address().port, + path: "/api/data", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }; + + const { promise, resolve, reject } = Promise.withResolvers(); + + // Create the request + const req = http.request(options, res => { + if (res.statusCode !== 200) { + reject(new Error("Body should not be chunked")); + } + const chunks = []; + // Collect the response data + res.on("data", chunk => { + chunks.push(chunk); + }); + + res.on("end", () => { + resolve(chunks); + }); + }); + // Handle errors + req.on("error", reject); + // Write chunks to the request body + req.write("Hello World BUN!"); + // End the request and signal no more data will be sent + req.end(); + + const chunks = await promise; + expect(chunks.length).toBe(1); + expect(chunks[0]?.toString()).toBe("Hello World BUN!"); + expect(Buffer.concat(chunks).toString()).toBe("Hello World BUN!"); +}); diff --git a/test/js/node/http2/node-http2.test.js b/test/js/node/http2/node-http2.test.js index 6d19fe6dd1..403472a040 100644 --- a/test/js/node/http2/node-http2.test.js +++ b/test/js/node/http2/node-http2.test.js @@ -1299,3 +1299,41 @@ for (const nodeExecutable of [nodeExe(), bunExe()]) { }); }); } + +it("sensitive headers should work", async () => { + const server = http2.createServer(); + let client; + try { + const { promise, resolve, reject } = Promise.withResolvers(); + server.on("stream", stream => { + stream.respond({ + ":status": 200, + "content-type": "application/json", + "x-custom-header": "some-value", + + [http2.sensitiveHeaders]: ["x-custom-header"], + }); + + stream.end(JSON.stringify({ message: "Hello from h2c server!" })); + }); + + server.listen(0, () => { + const port = server.address().port; + client = http2.connect(`http://localhost:${port}`); + + client.on("error", reject); + + const req = client.request({ ":path": "/" }); + req.on("response", resolve); + req.on("error", reject); + req.end(); + }); + const res = await promise; + + expect(res["x-custom-header"]).toBe("some-value"); + expect(res[http2.sensitiveHeaders]).toEqual(["x-custom-header"]); + } finally { + server.close(); + client?.close?.(); + } +}); diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index 9c44c9656e..ca76ce322a 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -135,3 +135,7 @@ test("Module._resolveLookupPaths", () => { expect(Module._resolveLookupPaths("./bar", { paths: ["a"] })).toEqual(["."]); expect(Module._resolveLookupPaths("bar", { paths: ["a"] })).toEqual(["a"]); }); + +test("Module.findSourceMap doesn't throw", () => { + expect(Module.findSourceMap("foo")).toEqual(undefined); +}); diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 70034749ed..0572567901 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -285,7 +285,8 @@ describe("net.createServer listen", () => { expect(err).not.toBeNull(); expect(err!.message).toBe("Failed to connect"); - expect(err!.name).toBe("ECONNREFUSED"); + expect(err!.name).toBe("Error"); + expect(err!.code).toBe("ECONNREFUSED"); server.close(); done(); diff --git a/test/js/node/net/node-net.test.ts b/test/js/node/net/node-net.test.ts index ef56deafe9..ebf70d065b 100644 --- a/test/js/node/net/node-net.test.ts +++ b/test/js/node/net/node-net.test.ts @@ -563,6 +563,39 @@ it("should not hang after destroy", async () => { } }); +it("should trigger error when aborted even if connection failed #13126", async () => { + const signal = AbortSignal.timeout(100); + const socket = createConnection({ + host: "example.com", + port: 999, + signal: signal, + }); + const { promise, resolve, reject } = Promise.withResolvers(); + + socket.on("connect", reject); + socket.on("error", resolve); + + const err = (await promise) as Error; + expect(err.name).toBe("TimeoutError"); +}); + +it("should trigger error when aborted even if connection failed, and the signal is already aborted #13126", async () => { + const signal = AbortSignal.timeout(1); + await Bun.sleep(10); + const socket = createConnection({ + host: "example.com", + port: 999, + signal: signal, + }); + const { promise, resolve, reject } = Promise.withResolvers(); + + socket.on("connect", reject); + socket.on("error", resolve); + + const err = (await promise) as Error; + expect(err.name).toBe("TimeoutError"); +}); + it.if(isWindows)( "should work with named pipes", async () => { diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index 469089c2a6..a887b113cd 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -222,3 +222,22 @@ describe("toString works like node", () => { }); } }); + +it("getPriority system error object", () => { + try { + os.getPriority(-1); + expect.unreachable(); + } catch (err) { + expect(err.name).toBe("SystemError"); + expect(err.message).toBe("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"); + expect(err.code).toBe("ERR_SYSTEM_ERROR"); + expect(err.info).toEqual({ + errno: isWindows ? -4040 : -3, + code: "ESRCH", + message: "no such process", + syscall: "uv_os_getpriority", + }); + expect(err.errno).toBe(isWindows ? -4040 : -3); + expect(err.syscall).toBe("uv_os_getpriority"); + } +}); diff --git a/test/js/node/path/15704.test.js b/test/js/node/path/15704.test.js new file mode 100644 index 0000000000..81c12fb9a7 --- /dev/null +++ b/test/js/node/path/15704.test.js @@ -0,0 +1,10 @@ +import path from "path"; +import assert from "assert"; + +test("too-long path names do not crash when joined", () => { + const length = 4096; + const tooLengthyFolderName = Array.from({ length }).fill("b").join(""); + assert.equal(path.join(tooLengthyFolderName), "b".repeat(length)); + assert.equal(path.win32.join(tooLengthyFolderName), "b".repeat(length)); + assert.equal(path.posix.join(tooLengthyFolderName), "b".repeat(length)); +}); diff --git a/test/js/node/path/matches-glob.test.ts b/test/js/node/path/matches-glob.test.ts new file mode 100644 index 0000000000..8802be251b --- /dev/null +++ b/test/js/node/path/matches-glob.test.ts @@ -0,0 +1,78 @@ +import path from "path"; + +describe("path.matchesGlob(path, glob)", () => { + const stringLikeObject = { + toString() { + return "hi"; + }, + }; + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `path` is not a string", (notAString: any) => { + expect(() => path.matchesGlob(notAString, "*")).toThrow(TypeError); + }); + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `glob` is not a string", (notAString: any) => { + expect(() => path.matchesGlob("hi", notAString)).toThrow(TypeError); + }); +}); + +describe("path.posix.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**/*.js"], + ["src/bar/foo.js", "**/*.js"], + ["foo/bar/baz", "foo/[bcr]ar/baz"], + ])("path '%s' matches pattern '%s'", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["src/foo.js", "*.js"], + ["foo.js", "src/*.js"], + ["foo/bar", "*"], + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); + +describe("path.win32.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**/*.js"], + ["foo\\bar\\baz", "foo\\[bcr]ar\\baz"], + ["foo\\bar\\baz", "foo/[bcr]ar/baz"], + ])("path '%s' matches gattern '%s'", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["foo.js", "src\\*.js"], + ["foo/bar", "*"], + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); diff --git a/test/js/node/process-binding.test.ts b/test/js/node/process-binding.test.ts index a0702ea10b..e0eea52644 100644 --- a/test/js/node/process-binding.test.ts +++ b/test/js/node/process-binding.test.ts @@ -25,6 +25,6 @@ describe("process.binding", () => { const map = uv.getErrorMap(); expect(map).toBeDefined(); - expect(map.get(-56)).toEqual(["EISCONN", "socket is already connected"]); + expect(map.get(uv.UV_EISCONN)).toEqual(["EISCONN", "socket is already connected"]); }); }); diff --git a/test/js/node/process/call-constructor.test.js b/test/js/node/process/call-constructor.test.js new file mode 100644 index 0000000000..7522966572 --- /dev/null +++ b/test/js/node/process/call-constructor.test.js @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test"; +import process from "process"; + +test("the constructor of process can be called", () => { + let obj = process.constructor.call({ ...process }); + expect(Object.getPrototypeOf(obj)).toEqual(Object.getPrototypeOf(process)); +}); + +test("#14346", () => { + process.__proto__.constructor.call({}); +}); diff --git a/test/js/node/process/process-on-fixture.ts b/test/js/node/process/process-on-fixture.ts new file mode 100644 index 0000000000..61d57ecad4 --- /dev/null +++ b/test/js/node/process/process-on-fixture.ts @@ -0,0 +1,26 @@ +export function initialize() { + const handler = () => { + console.log("SIGINT"); + }; + + const handler2 = () => { + console.log("SIGTERM"); + }; + + process.on("SIGINT", handler); + process.on("SIGTERM", handler2); + process.off("SIGTERM", handler2); + process.off("SIGINT", handler); + + process.on("SIGINT", handler); + process.on("SIGTERM", handler2); + process.off("SIGTERM", handler2); + process.off("SIGINT", handler); + + process.on("SIGINT", handler); + process.on("SIGTERM", handler2); + process.off("SIGTERM", handler2); + process.off("SIGINT", handler); +} + +initialize(); diff --git a/test/js/node/process/process-on.test.ts b/test/js/node/process/process-on.test.ts new file mode 100644 index 0000000000..092626b233 --- /dev/null +++ b/test/js/node/process/process-on.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "bun:test"; +import { bunEnv, tempDirWithFiles } from "harness"; +import { bunExe } from "harness"; +import path from "path"; + +describe("process.on", () => { + it("when called from the main thread", () => { + const result = Bun.spawnSync({ + cmd: [bunExe(), path.join(__dirname, "process-on-fixture.ts")], + env: bunEnv, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + + expect(result.exitCode).toBe(0); + }); + + it("should work inside --compile", () => { + const dir = tempDirWithFiles("process-on-test", { + "process-on-fixture.ts": require("fs").readFileSync(require.resolve("./process-on-fixture.ts"), "utf-8"), + "package.json": `{ + "name": "process-on-test", + "type": "module", + "scripts": { + "start": "bun run process-on-fixture.ts" + } + }`, + }); + const result1 = Bun.spawnSync({ + cmd: [bunExe(), "build", "--compile", path.join(dir, "./process-on-fixture.ts"), "--outfile=./out"], + env: bunEnv, + cwd: dir, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + + expect(result1.exitCode).toBe(0); + + const result2 = Bun.spawnSync({ + cmd: ["./out"], + env: bunEnv, + cwd: dir, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + expect(result2.exitCode).toBe(0); + }); + + it("should work inside a macro", () => { + const dir = tempDirWithFiles("process-on-test", { + "process-on-fixture.ts": require("fs").readFileSync(require.resolve("./process-on-fixture.ts"), "utf-8"), + "entry.ts": `import { initialize } from "./process-on-fixture.ts" with {type: "macro"}; + initialize();`, + "package.json": `{ + "name": "process-on-test", + "type": "module", + "scripts": { + "start": "bun run entry.ts" + } + }`, + }); + + expect( + Bun.spawnSync({ + cmd: [bunExe(), "build", "--target=bun", path.join(dir, "entry.ts"), "--outfile=./out.ts"], + env: bunEnv, + cwd: dir, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }).exitCode, + ).toBe(0); + + const result2 = Bun.spawnSync({ + cmd: [bunExe(), "run", "./out.ts"], + env: bunEnv, + cwd: dir, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + expect(result2.exitCode).toBe(0); + }); +}); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index a2dac046fa..965105f56b 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -2,7 +2,7 @@ import { spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; -import { basename, join, resolve } from "path"; +import path, { basename, join, resolve } from "path"; import { familySync } from "detect-libc"; expect.extend({ @@ -118,11 +118,15 @@ it("process.chdir() on root dir", () => { } }); -it("process.hrtime()", () => { +it("process.hrtime()", async () => { const start = process.hrtime(); const end = process.hrtime(start); - const end2 = process.hrtime(); expect(end[0]).toBe(0); + + // Flaky on Ubuntu & Windows. + await Bun.sleep(16); + const end2 = process.hrtime(); + expect(end2[1] > start[1]).toBe(true); }); @@ -232,12 +236,16 @@ it("process.uptime()", () => { }); it("process.umask()", () => { - let notNumbers = [265n, "string", true, false, null, {}, [], () => {}, Symbol("symbol"), BigInt(1)]; - for (let notNumber of notNumbers) { - expect(() => { - process.umask(notNumber); - }).toThrow('The "mask" argument must be of type number'); - } + expect(() => process.umask(265n)).toThrow('The "mask" argument must be of type number. Received type bigint (265n)'); + expect(() => process.umask("string")).toThrow(`The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received "string"`); // prettier-ignore + expect(() => process.umask(true)).toThrow('The "mask" argument must be of type number. Received type boolean (true)'); + expect(() => process.umask(false)).toThrow('The "mask" argument must be of type number. Received type boolean (false)'); // prettier-ignore + expect(() => process.umask(null)).toThrow('The "mask" argument must be of type number. Received null'); + expect(() => process.umask({})).toThrow('The "mask" argument must be of type number. Received an instance of Object'); + expect(() => process.umask([])).toThrow('The "mask" argument must be of type number. Received an instance of Array'); + expect(() => process.umask(() => {})).toThrow('The "mask" argument must be of type number. Received function '); + expect(() => process.umask(Symbol("symbol"))).toThrow('The "mask" argument must be of type number. Received type symbol (Symbol(symbol))'); // prettier-ignore + expect(() => process.umask(BigInt(1))).toThrow('The "mask" argument must be of type number. Received type bigint (1n)'); // prettier-ignore let rangeErrors = [NaN, -1.4, Infinity, -Infinity, -1, 1.3, 4294967296]; for (let rangeError of rangeErrors) { @@ -306,20 +314,6 @@ it("process.config", () => { }); }); -it("process.emitWarning", () => { - process.emitWarning("-- Testing process.emitWarning --"); - var called = 0; - process.on("warning", err => { - called++; - expect(err.message).toBe("-- Testing process.on('warning') --"); - }); - process.emitWarning("-- Testing process.on('warning') --"); - expect(called).toBe(1); - expect(process.off("warning")).toBe(process); - process.emitWarning("-- Testing process.on('warning') --"); - expect(called).toBe(1); -}); - it("process.execArgv", () => { expect(process.execArgv instanceof Array).toBe(true); }); @@ -338,11 +332,21 @@ it("process.argv in testing", () => { describe("process.exitCode", () => { it("validates int", () => { - expect(() => (process.exitCode = "potato")).toThrow(`exitCode must be an integer`); - expect(() => (process.exitCode = 1.2)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = NaN)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = Infinity)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = -Infinity)).toThrow("exitCode must be an integer"); + expect(() => (process.exitCode = "potato")).toThrow( + `The "code" argument must be of type number. Received type string ("potato")`, + ); + expect(() => (process.exitCode = 1.2)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received 1.2`, + ); + expect(() => (process.exitCode = NaN)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received NaN`, + ); + expect(() => (process.exitCode = Infinity)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received Infinity`, + ); + expect(() => (process.exitCode = -Infinity)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received -Infinity`, + ); }); it("works with implicit process.exit", () => { @@ -454,13 +458,13 @@ describe("process.cpuUsage", () => { user: -1, system: 100, }), - ).toThrow("The 'user' property must be a number between 0 and 2^53"); + ).toThrow("The property 'prevValue.user' is invalid. Received -1"); expect(() => process.cpuUsage({ user: 100, system: -1, }), - ).toThrow("The 'system' property must be a number between 0 and 2^53"); + ).toThrow("The property 'prevValue.system' is invalid. Received -1"); }); // Skipped on Windows because it seems UV returns { user: 15000, system: 0 } constantly @@ -680,13 +684,7 @@ it("dlopen accepts file: URLs", () => { }); it("process.constrainedMemory()", () => { - if (process.platform === "linux") { - // On Linux, it returns 0 if the kernel doesn't support it - expect(process.constrainedMemory() >= 0).toBe(true); - } else { - // On unsupported platforms, it returns undefined - expect(process.constrainedMemory()).toBeUndefined(); - } + expect(process.constrainedMemory() >= 0).toBe(true); }); it("process.report", () => { @@ -1052,3 +1050,10 @@ describe("process.exitCode", () => { it("process._exiting", () => { expect(process._exiting).toBe(false); }); + +it("process.memoryUsage.arrayBuffers", () => { + const initial = process.memoryUsage().arrayBuffers; + const array = new ArrayBuffer(1024 * 1024 * 16); + array.buffer; + expect(process.memoryUsage().arrayBuffers).toBeGreaterThanOrEqual(initial + 16 * 1024 * 1024); +}); diff --git a/test/js/node/stream/bufferlist.test.ts b/test/js/node/stream/bufferlist.test.ts deleted file mode 100644 index 240c54935d..0000000000 --- a/test/js/node/stream/bufferlist.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { expect, it } from "bun:test"; -import { Readable } from "stream"; - -function makeUint8Array(str: string) { - return new Uint8Array( - [].map.call(str, function (ch: string) { - return ch.charCodeAt(0); - }) as number[], - ); -} - -it("should work with .clear()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push({})).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.push({})).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.clear()).toBeUndefined(); - expect(list.length).toBe(0); -}); - -it("should work with .concat()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push(makeUint8Array("foo"))).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.concat(3)).toEqual(new Uint8Array([102, 111, 111])); - expect(list.push(makeUint8Array("bar"))).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.concat(10)).toEqual(new Uint8Array([102, 111, 111, 98, 97, 114, 0, 0, 0, 0])); -}); - -it("should fail on .concat() with invalid items", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push("foo")).toBeUndefined(); - expect(() => { - list.concat(42); - }).toThrow(TypeError); -}); - -it("should fail on .concat() buffer overflow", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push(makeUint8Array("foo"))).toBeUndefined(); - expect(list.length).toBe(1); - expect(() => { - list.concat(2); - }).toThrow(RangeError); - expect(list.push(makeUint8Array("bar"))).toBeUndefined(); - expect(list.length).toBe(2); - expect(() => { - list.concat(5); - }).toThrow(RangeError); -}); - -it("should work with .consume() on strings", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.consume(42, true)).toBe(""); - expect(list.push("foo")).toBeUndefined(); - expect(list.push("bar")).toBeUndefined(); - expect(list.push("baz")).toBeUndefined(); - expect(list.push("moo")).toBeUndefined(); - expect(list.push("moz")).toBeUndefined(); - expect(list.length).toBe(5); - expect(list.consume(3, true)).toBe("foo"); - expect(list.length).toBe(4); - expect(list.consume(4, true)).toBe("barb"); - expect(list.length).toBe(3); - expect(list.consume(256, true)).toBe("azmoomoz"); - expect(list.length).toBe(0); -}); - -it("should work with .consume() on buffers", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.consume(42, false)).toEqual(new Uint8Array()); - expect(list.push(makeUint8Array("foo"))).toBeUndefined(); - expect(list.push(makeUint8Array("bar"))).toBeUndefined(); - expect(list.push(makeUint8Array("baz"))).toBeUndefined(); - expect(list.push(makeUint8Array("moo"))).toBeUndefined(); - expect(list.push(makeUint8Array("moz"))).toBeUndefined(); - expect(list.length).toBe(5); - expect(list.consume(3, false)).toEqual(makeUint8Array("foo")); - expect(list.length).toBe(4); - expect(list.consume(2, false)).toEqual(makeUint8Array("ba")); - expect(list.length).toBe(4); - expect(list.consume(4, false)).toEqual(makeUint8Array("rbaz")); - expect(list.length).toBe(2); - expect(list.consume(10, false)).toEqual(new Uint8Array([109, 111, 111, 109, 111, 122, 0, 0, 0, 0])); - expect(list.length).toBe(0); -}); - -it("should fail on .consume() with invalid items", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push("foo")).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.consume(0, false)).toEqual(new Uint8Array([])); - expect(() => { - list.consume(1, false); - }).toThrow(TypeError); - expect(list.consume(3, true)).toBe("foo"); - expect(list.length).toBe(0); - expect(list.push(makeUint8Array("bar"))).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.consume(0, true)).toEqual(""); - expect(() => { - list.consume(1, true); - }).toThrow(TypeError); - expect(list.consume(3, false)).toEqual(new Uint8Array([98, 97, 114])); -}); - -it("should work with .first()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.first()).toBeUndefined(); - const item = {}; - expect(list.push(item)).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.first()).toBe(item); -}); - -it("should work with .join()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push(42)).toBeUndefined(); - expect(list.push(null)).toBeUndefined(); - expect(list.push("foo")).toBeUndefined(); - expect(list.push(makeUint8Array("bar"))).toBeUndefined(); - expect(list.length).toBe(4); - expect(list.join("")).toBe("42nullfoo98,97,114"); - expect(list.join(",")).toBe("42,null,foo,98,97,114"); - expect(list.join(" baz ")).toBe("42 baz null baz foo baz 98,97,114"); -}); - -it("should work with .push()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - const item1 = {}; - expect(list.push(item1)).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.first()).toBe(item1); - const item2 = {}; - expect(list.push(item2)).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.shift()).toBe(item1); - expect(list.shift()).toBe(item2); - expect(list.shift()).toBeUndefined(); -}); - -it("should work with .shift()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.shift()).toBeUndefined(); - const item = {}; - expect(list.push(item)).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.shift()).toBe(item); - expect(list.shift()).toBeUndefined(); -}); - -it("should work with .unshift()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - const item1 = {}; - expect(list.unshift(item1)).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.first()).toBe(item1); - const item2 = {}; - expect(list.push(item2)).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.first()).toBe(item1); - const item3 = {}; - expect(list.unshift(item3)).toBeUndefined(); - expect(list.length).toBe(3); - expect(list.shift()).toBe(item3); - expect(list.shift()).toBe(item1); - expect(list.shift()).toBe(item2); - expect(list.shift()).toBeUndefined(); -}); - -it("should work with multiple partial .consume() from buffers", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push(Buffer.from("f000baaa", "hex"))).toBeUndefined(); - expect(list.length).toBe(1); - expect(list.consume(2, undefined)).toEqual(Buffer.from("f000", "hex")); - expect(list.consume(1, undefined)).toEqual(Buffer.from("ba", "hex")); - expect(list.length).toBe(1); -}); - -it("should work with partial .consume() followed by .first()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push("foo")).toBeUndefined(); - expect(list.push("bar")).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.consume(4, true)).toEqual("foob"); - expect(list.length).toBe(1); - expect(list.first()).toEqual("ar"); - expect(list.length).toBe(1); -}); - -it("should work with partial .consume() followed by .shift()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push(makeUint8Array("foo"))).toBeUndefined(); - expect(list.push(makeUint8Array("bar"))).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.consume(4, false)).toEqual(makeUint8Array("foob")); - expect(list.length).toBe(1); - expect(list.shift()).toEqual(makeUint8Array("ar")); - expect(list.length).toBe(0); -}); - -it("should work with partial .consume() followed by .unshift()", () => { - // @ts-ignore - const list = new Readable().readableBuffer; - expect(list.length).toBe(0); - expect(list.push(makeUint8Array("😋😋😋"))).toBeUndefined(); - expect(list.push(makeUint8Array("📋📋📋"))).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.consume(7, false)).toEqual(new Uint8Array([61, 11, 61, 11, 61, 11, 61])); - expect(list.length).toBe(1); - expect(list.unshift(makeUint8Array("👌👌👌"))).toBeUndefined(); - expect(list.length).toBe(2); - expect(list.consume(12, false)).toEqual(new Uint8Array([61, 76, 61, 76, 61, 76, 203, 61, 203, 61, 203, 0])); - expect(list.length).toBe(0); -}); diff --git a/test/js/node/stream/node-stream-uint8array.test.ts b/test/js/node/stream/node-stream-uint8array.test.ts index fd27592240..5072706bd9 100644 --- a/test/js/node/stream/node-stream-uint8array.test.ts +++ b/test/js/node/stream/node-stream-uint8array.test.ts @@ -47,7 +47,7 @@ describe("Writable", () => { expect(chunk instanceof Buffer).toBe(false); expect(chunk instanceof Uint8Array).toBe(true); expect(chunk).toStrictEqual(ABC); - expect(encoding).toBe("utf8"); + expect(encoding).toBeUndefined(); cb(); }, 0), }); diff --git a/test/js/node/stream/node-stream.test.js b/test/js/node/stream/node-stream.test.js index 287aaf8f74..934b4a92bc 100644 --- a/test/js/node/stream/node-stream.test.js +++ b/test/js/node/stream/node-stream.test.js @@ -544,3 +544,26 @@ it("should emit prefinish on current tick", done => { done(); }); }); + +for (const size of [0x10, 0xffff, 0x10000, 0x1f000, 0x20000, 0x20010, 0x7ffff, 0x80000, 0xa0000, 0xa0010]) { + it(`should emit 'readable' with null data and 'close' exactly once each, 0x${size.toString(16)} bytes`, async () => { + const path = `${tmpdir()}/${Date.now()}.readable_and_close.txt`; + writeFileSync(path, new Uint8Array(size)); + const stream = createReadStream(path); + const close_resolvers = Promise.withResolvers(); + const readable_resolvers = Promise.withResolvers(); + + stream.on("close", () => { + close_resolvers.resolve(); + }); + + stream.on("readable", () => { + const data = stream.read(); + if (data === null) { + readable_resolvers.resolve(); + } + }); + + await Promise.all([close_resolvers.promise, readable_resolvers.promise]); + }); +} diff --git a/test/js/node/string_decoder/string-decoder.test.js b/test/js/node/string_decoder/string-decoder.test.js index 5e8463955c..1b4e6ab787 100644 --- a/test/js/node/string_decoder/string-decoder.test.js +++ b/test/js/node/string_decoder/string-decoder.test.js @@ -260,3 +260,21 @@ it("decoding latin1, issue #3738", () => { output += decoder.end(); expect(output).toStrictEqual("ÝYÞ"); }); + +it("invalid utf-8 at end of stream can sometimes produce more than one replacement character", () => { + let decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f59c", "hex"))).toEqual("6"); + expect(decoder.end()).toEqual("\uFFFD\uFFFD"); + decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f5", "hex"))).toEqual("6"); + expect(decoder.end(Buffer.from("9c", "hex"))).toEqual("\uFFFD\uFFFD"); +}); + +it("invalid utf-8 at end of stream can sometimes produce more than one replacement character", () => { + let decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f59c", "hex"))).toEqual("6"); + expect(decoder.end()).toEqual("\uFFFD\uFFFD"); + decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f5", "hex"))).toEqual("6"); + expect(decoder.end(Buffer.from("9c", "hex"))).toEqual("\uFFFD\uFFFD"); +}); diff --git a/test/js/node/test/common/assertSnapshot.js b/test/js/node/test/common/assertSnapshot.js index 88f40281e0..a22455160b 100644 --- a/test/js/node/test/common/assertSnapshot.js +++ b/test/js/node/test/common/assertSnapshot.js @@ -25,7 +25,7 @@ function replaceWindowsPaths(str) { } function replaceFullPaths(str) { - return str.replaceAll(process.cwd(), ''); + return str.replaceAll(path.resolve(__dirname, '../..'), ''); } function transform(...args) { @@ -78,8 +78,11 @@ async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ... return; } const flags = common.parseTestFlags(filename); - const executable = tty ? 'tools/pseudo-tty.py' : process.execPath; - const args = tty ? [process.execPath, ...flags, filename] : [...flags, filename]; + const executable = tty ? (process.env.PYTHON || 'python3') : process.execPath; + const args = + tty ? + [path.join(__dirname, '../..', 'tools/pseudo-tty.py'), process.execPath, ...flags, filename] : + [...flags, filename]; const { stdout, stderr } = await common.spawnPromisified(executable, args, options); await assertSnapshot(transform(`${stdout}${stderr}`), filename); } diff --git a/test/js/node/test/common/dns.js b/test/js/node/test/common/dns.js index d854c73629..8fa264dc2c 100644 --- a/test/js/node/test/common/dns.js +++ b/test/js/node/test/common/dns.js @@ -15,6 +15,7 @@ const types = { TXT: 16, ANY: 255, CAA: 257, + SRV: 33, }; const classes = { diff --git a/test/js/node/test/common/duplexpair.js b/test/js/node/test/common/duplexpair.js deleted file mode 100644 index 1f41ed32f1..0000000000 --- a/test/js/node/test/common/duplexpair.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; -const { Duplex } = require('stream'); -const assert = require('assert'); - -const kCallback = Symbol('Callback'); -const kOtherSide = Symbol('Other'); - -class DuplexSocket extends Duplex { - constructor() { - super(); - this[kCallback] = null; - this[kOtherSide] = null; - } - - _read() { - const callback = this[kCallback]; - if (callback) { - this[kCallback] = null; - callback(); - } - } - - _write(chunk, encoding, callback) { - assert.notStrictEqual(this[kOtherSide], null); - assert.strictEqual(this[kOtherSide][kCallback], null); - if (chunk.length === 0) { - process.nextTick(callback); - } else { - this[kOtherSide].push(chunk); - this[kOtherSide][kCallback] = callback; - } - } - - _final(callback) { - this[kOtherSide].on('end', callback); - this[kOtherSide].push(null); - } -} - -function makeDuplexPair() { - const clientSide = new DuplexSocket(); - const serverSide = new DuplexSocket(); - clientSide[kOtherSide] = serverSide; - serverSide[kOtherSide] = clientSide; - return { clientSide, serverSide }; -} - -module.exports = makeDuplexPair; diff --git a/test/js/node/test/common/gc.js b/test/js/node/test/common/gc.js index 8e2c5ee5da..3774f81cc8 100644 --- a/test/js/node/test/common/gc.js +++ b/test/js/node/test/common/gc.js @@ -120,8 +120,22 @@ async function checkIfCollectableByCounting(fn, ctor, count, waitTime = 20) { throw new Error(`${name} cannot be collected`); } +var finalizationRegistry = new FinalizationRegistry(heldValue => { + heldValue.ongc(); +}) + +function onGC(value, holder) { + if (holder?.ongc) { + + finalizationRegistry.register(value, { ongc: holder.ongc }); + } +} + module.exports = { checkIfCollectable, runAndBreathe, checkIfCollectableByCounting, + onGC, }; + + diff --git a/test/js/node/test/common/globals.js b/test/js/node/test/common/globals.js index 42caece2b8..5d1c4415ee 100644 --- a/test/js/node/test/common/globals.js +++ b/test/js/node/test/common/globals.js @@ -79,7 +79,6 @@ const webIdlExposedWildcard = new Set([ 'TextDecoder', 'AbortController', 'AbortSignal', - 'CustomEvent', 'EventTarget', 'Event', 'URL', @@ -127,7 +126,6 @@ const webIdlExposedWindow = new Set([ 'Response', 'WebSocket', 'EventSource', - 'CloseEvent', ]); const nodeGlobals = new Set([ diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index c67a5b8a81..c07c74094d 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -30,7 +30,7 @@ const net = require('net'); // Do not require 'os' until needed so that test-os-checked-function can // monkey patch it. If 'os' is required here, that test will fail. const path = require('path'); -const { inspect } = require('util'); +const { inspect, getCallSites } = require('util'); const { isMainThread } = require('worker_threads'); const { isModuleNamespaceObject } = require('util/types'); @@ -65,6 +65,9 @@ const opensslVersionNumber = (major = 0, minor = 0, patch = 0) => { return (major << 28) | (minor << 20) | (patch << 4); }; +// https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L21 +const openSSLIsBoringSSL = process.versions.boringssl !== undefined; + let OPENSSL_VERSION_NUMBER; const hasOpenSSL = (major = 0, minor = 0, patch = 0) => { if (!hasCrypto) return false; @@ -107,7 +110,7 @@ function parseTestFlags(filename = process.argv[1]) { // `worker_threads`) and child processes. // If the binary was built without-ssl then the crypto flags are // invalid (bad option). The test itself should handle this case. -if (process.argv.length === 2 && +if ((process.argv.length === 2 || process.argv.length === 3) && !process.env.NODE_SKIP_FLAG_CHECK && isMainThread && hasCrypto && @@ -119,6 +122,14 @@ if (process.argv.length === 2 && // If the binary is build without `intl` the inspect option is // invalid. The test itself should handle this case. (process.features.inspector || !flag.startsWith('--inspect'))) { + if (flag === "--expose-gc" && process.versions.bun) { + globalThis.gc ??= () => Bun.gc(true); + break; + } + if (flag === "--expose-internals" && process.versions.bun) { + process.env.SKIP_FLAG_CHECK = "1"; + break; + } console.log( 'NOTE: The test started as a child_process using these flags:', inspect(flags), @@ -128,7 +139,7 @@ if (process.argv.length === 2 && const options = { encoding: 'utf8', stdio: 'inherit' }; const result = spawnSync(process.execPath, args, options); if (result.signal) { - process.kill(0, result.signal); + process.kill(process.pid, result.signal); } else { process.exit(result.status); } @@ -141,8 +152,10 @@ const isSunOS = process.platform === 'sunos'; const isFreeBSD = process.platform === 'freebsd'; const isOpenBSD = process.platform === 'openbsd'; const isLinux = process.platform === 'linux'; -const isOSX = process.platform === 'darwin'; +const isMacOS = process.platform === 'darwin'; const isASan = process.config.variables.asan === 1; +const isRiscv64 = process.arch === 'riscv64'; +const isDebug = process.features.debug; const isPi = (() => { try { // Normal Raspberry Pi detection is to find the `Raspberry Pi` string in @@ -172,8 +185,7 @@ if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) { const destroydIdsList = {}; const destroyListList = {}; const initHandles = {}; - const { internalBinding } = require('internal/test/binding'); - const async_wrap = internalBinding('async_wrap'); + const async_wrap = process.binding('async_wrap'); process.on('exit', () => { // Iterate through handles to make sure nothing crashes @@ -280,7 +292,7 @@ function platformTimeout(ms) { const multipliers = typeof ms === 'bigint' ? { two: 2n, four: 4n, seven: 7n } : { two: 2, four: 4, seven: 7 }; - if (process.features.debug) + if (isDebug) ms = multipliers.two * ms; if (exports.isAIX || exports.isIBMi) @@ -289,6 +301,10 @@ function platformTimeout(ms) { if (isPi) return multipliers.two * ms; // Raspberry Pi devices + if (isRiscv64) { + return multipliers.four * ms; + } + return ms; } @@ -338,10 +354,9 @@ if (global.structuredClone) { knownGlobals.push(global.structuredClone); } -// BUN:TODO: uncommenting this crashes bun -// if (global.EventSource) { -// knownGlobals.push(EventSource); -// } +if (global.EventSource) { + knownGlobals.push(EventSource); +} if (global.fetch) { knownGlobals.push(fetch); @@ -385,6 +400,57 @@ if (global.Storage) { ); } +if (global.Bun) { + knownGlobals.push( + global.addEventListener, + global.alert, + global.confirm, + global.dispatchEvent, + global.postMessage, + global.prompt, + global.removeEventListener, + global.reportError, + global.Bun, + global.File, + global.process, + global.Blob, + global.Buffer, + global.BuildError, + global.BuildMessage, + global.HTMLRewriter, + global.Request, + global.ResolveError, + global.ResolveMessage, + global.Response, + global.TextDecoder, + global.AbortSignal, + global.BroadcastChannel, + global.CloseEvent, + global.DOMException, + global.ErrorEvent, + global.Event, + global.EventTarget, + global.FormData, + global.Headers, + global.MessageChannel, + global.MessageEvent, + global.MessagePort, + global.PerformanceEntry, + global.PerformanceObserver, + global.PerformanceObserverEntryList, + global.PerformanceResourceTiming, + global.PerformanceServerTiming, + global.PerformanceTiming, + global.TextEncoder, + global.URL, + global.URLSearchParams, + global.WebSocket, + global.Worker, + global.onmessage, + global.onerror + ); +} + function allowGlobals(...allowlist) { knownGlobals = knownGlobals.concat(allowlist); } @@ -510,8 +576,7 @@ function _mustCallInner(fn, criteria = 1, field) { } function hasMultiLocalhost() { - const { internalBinding } = require('internal/test/binding'); - const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap'); + const { TCP, constants: TCPConstants } = process.binding('tcp_wrap'); const t = new TCP(TCPConstants.SOCKET); const ret = t.bind('127.0.0.2', 0); t.close(); @@ -844,6 +909,7 @@ function invalidArgTypeHelper(input) { let inspected = inspect(input, { colors: false }); if (inspected.length > 28) { inspected = `${inspected.slice(inspected, 0, 25)}...`; } + if (inspected.startsWith("'") && inspected.endsWith("'")) inspected = `"${inspected.slice(1, inspected.length - 1)}"`; // BUN: util.inspect uses ' but bun uses " for strings return ` Received type ${typeof input} (${inspected})`; } @@ -921,6 +987,32 @@ function spawnPromisified(...args) { }); } +/** + * Escape values in a string template literal. On Windows, this function + * does not escape anything (which is fine for paths, as `"` is not a valid char + * in a path on Windows), so you should use it only to escape paths – or other + * values on tests which are skipped on Windows. + * This function is meant to be used for tagged template strings. + * @returns {[string, object | undefined]} An array that can be passed as + * arguments to `exec` or `execSync`. + */ +function escapePOSIXShell(cmdParts, ...args) { + if (common.isWindows) { + // On Windows, paths cannot contain `"`, so we can return the string unchanged. + return [String.raw({ raw: cmdParts }, ...args)]; + } + // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. + const env = { ...process.env }; + let cmd = cmdParts[0]; + for (let i = 0; i < args.length; i++) { + const envVarName = `ESCAPED_${i}`; + env[envVarName] = args[i]; + cmd += '${' + envVarName + '}' + cmdParts[i + 1]; + } + + return [cmd, { env }]; +}; + function getPrintedStackTrace(stderr) { const lines = stderr.split('\n'); @@ -967,18 +1059,24 @@ function getPrintedStackTrace(stderr) { * @param {object} mod result returned by require() * @param {object} expectation shape of expected namespace. */ -function expectRequiredModule(mod, expectation) { +function expectRequiredModule(mod, expectation, checkESModule = true) { + const clone = { ...mod }; + if (Object.hasOwn(mod, 'default') && checkESModule) { + assert.strictEqual(mod.__esModule, true); + delete clone.__esModule; + } assert(isModuleNamespaceObject(mod)); - assert.deepStrictEqual({ ...mod }, { ...expectation }); + assert.deepStrictEqual(clone, { ...expectation }); } const common = { - allowGlobals: [], + allowGlobals, buildType, canCreateSymLink, childShouldThrowAndAbort, createZeroFilledFile, defaultAutoSelectFamilyAttemptTimeout, + escapePOSIXShell, expectsError, expectRequiredModule, expectWarning, @@ -996,12 +1094,13 @@ const common = { invalidArgTypeHelper, isAlive, isASan, + isDebug, isDumbTerminal, isFreeBSD, isLinux, isMainThread, isOpenBSD, - isOSX, + isMacOS, isPi, isSunOS, isWindows, @@ -1012,6 +1111,7 @@ const common = { mustNotMutateObjectDeep, mustSucceed, nodeProcessAborted, + openSSLIsBoringSSL, PIPE, parseTestFlags, platformTimeout, @@ -1147,6 +1247,15 @@ const common = { get checkoutEOL() { return fs.readFileSync(__filename).includes('\r\n') ? '\r\n' : '\n'; }, + + get isInsideDirWithUnusualChars() { + return __dirname.includes('%') || + (!isWindows && __dirname.includes('\\')) || + __dirname.includes('$') || + __dirname.includes('\n') || + __dirname.includes('\r') || + __dirname.includes('\t'); + }, }; const validProperties = new Set(Object.keys(common)); diff --git a/test/js/node/test/common/index.mjs b/test/js/node/test/common/index.mjs index 430527faf8..007ce233fb 100644 --- a/test/js/node/test/common/index.mjs +++ b/test/js/node/test/common/index.mjs @@ -30,7 +30,7 @@ const { isLinuxPPCBE, isMainThread, isOpenBSD, - isOSX, + isMacOS, isSunOS, isWindows, localIPv6Hosts, @@ -85,7 +85,7 @@ export { isLinuxPPCBE, isMainThread, isOpenBSD, - isOSX, + isMacOS, isSunOS, isWindows, localIPv6Hosts, diff --git a/test/js/node/test/common/process-exit-code-cases.js b/test/js/node/test/common/process-exit-code-cases.js new file mode 100644 index 0000000000..54cfe2655b --- /dev/null +++ b/test/js/node/test/common/process-exit-code-cases.js @@ -0,0 +1,138 @@ +'use strict'; + +const assert = require('assert'); + +function getTestCases(isWorker = false) { + const cases = []; + function exitsOnExitCodeSet() { + process.exitCode = 42; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 42); + assert.strictEqual(code, 42); + }); + } + cases.push({ func: exitsOnExitCodeSet, result: 42 }); + + function changesCodeViaExit() { + process.exitCode = 99; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 42); + assert.strictEqual(code, 42); + }); + process.exit(42); + } + cases.push({ func: changesCodeViaExit, result: 42 }); + + function changesCodeZeroExit() { + process.exitCode = 99; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 0); + assert.strictEqual(code, 0); + }); + process.exit(0); + } + cases.push({ func: changesCodeZeroExit, result: 0 }); + + function exitWithOneOnUncaught() { + process.exitCode = 99; + process.on('exit', (code) => { + // Cannot use assert because it will be uncaughtException -> 1 exit code + // that will render this test useless + if (code !== 1 || process.exitCode !== 1) { + console.log('wrong code! expected 1 for uncaughtException'); + process.exit(99); + } + }); + throw new Error('ok'); + } + cases.push({ + func: exitWithOneOnUncaught, + result: 1, + error: /^Error: ok$/, + }); + + function changeCodeInsideExit() { + process.exitCode = 95; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 95); + assert.strictEqual(code, 95); + process.exitCode = 99; + }); + } + cases.push({ func: changeCodeInsideExit, result: 99 }); + + function zeroExitWithUncaughtHandler() { + const noop = () => { }; + process.on('exit', (code) => { + process.off('uncaughtException', noop); + assert.strictEqual(process.exitCode, undefined); + assert.strictEqual(code, 0); + }); + process.on('uncaughtException', noop); + throw new Error('ok'); + } + cases.push({ func: zeroExitWithUncaughtHandler, result: 0 }); + + function changeCodeInUncaughtHandler() { + const modifyExitCode = () => { process.exitCode = 97; }; + process.on('exit', (code) => { + process.off('uncaughtException', modifyExitCode); + assert.strictEqual(process.exitCode, 97); + assert.strictEqual(code, 97); + }); + process.on('uncaughtException', modifyExitCode); + throw new Error('ok'); + } + cases.push({ func: changeCodeInUncaughtHandler, result: 97 }); + + function changeCodeInExitWithUncaught() { + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 1); + assert.strictEqual(code, 1); + process.exitCode = 98; + }); + throw new Error('ok'); + } + cases.push({ + func: changeCodeInExitWithUncaught, + result: 98, + error: /^Error: ok$/, + }); + + function exitWithZeroInExitWithUncaught() { + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 1); + assert.strictEqual(code, 1); + process.exitCode = 0; + }); + throw new Error('ok'); + } + cases.push({ + func: exitWithZeroInExitWithUncaught, + result: 0, + error: /^Error: ok$/, + }); + + function exitWithThrowInUncaughtHandler() { + process.on('uncaughtException', () => { + throw new Error('ok'); + }); + throw new Error('bad'); + } + cases.push({ + func: exitWithThrowInUncaughtHandler, + result: isWorker ? 1 : 7, + error: /^Error: ok$/, + }); + + function exitWithUndefinedFatalException() { + process._fatalException = undefined; + throw new Error('ok'); + } + cases.push({ + func: exitWithUndefinedFatalException, + result: 6, + }); + return cases; +} +exports.getTestCases = getTestCases; diff --git a/test/js/node/test/common/sea.js b/test/js/node/test/common/sea.js index 863047ab36..53bfd93d92 100644 --- a/test/js/node/test/common/sea.js +++ b/test/js/node/test/common/sea.js @@ -5,7 +5,7 @@ const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const { inspect } = require('util'); -const { readFileSync, copyFileSync } = require('fs'); +const { readFileSync, copyFileSync, statSync } = require('fs'); const { spawnSyncAndExitWithoutError, } = require('../common/child_process'); @@ -50,12 +50,23 @@ function skipIfSingleExecutableIsNotSupported() { common.skip('UndefinedBehavior Sanitizer is not supported'); } + try { + readFileSync(process.execPath); + } catch (e) { + if (e.code === 'ERR_FS_FILE_TOO_LARGE') { + common.skip('The Node.js binary is too large to be supported by postject'); + } + } + tmpdir.refresh(); // The SEA tests involve making a copy of the executable and writing some fixtures - // to the tmpdir. To be safe, ensure that at least 120MB disk space is available. - if (!tmpdir.hasEnoughSpace(120 * 1024 * 1024)) { - common.skip('Available disk space < 120MB'); + // to the tmpdir. To be safe, ensure that the disk space has at least a copy of the + // executable and some extra space for blobs and configs is available. + const stat = statSync(process.execPath); + const expectedSpace = stat.size + 10 * 1024 * 1024; + if (!tmpdir.hasEnoughSpace(expectedSpace)) { + common.skip(`Available disk space < ${Math.floor(expectedSpace / 1024 / 1024)} MB`); } } diff --git a/test/js/node/test/common/shared-lib-util.js b/test/js/node/test/common/shared-lib-util.js index 3ecd38791c..b5d947a266 100644 --- a/test/js/node/test/common/shared-lib-util.js +++ b/test/js/node/test/common/shared-lib-util.js @@ -22,7 +22,7 @@ function addLibraryPath(env) { env.LIBPATH = (env.LIBPATH ? env.LIBPATH + path.delimiter : '') + kExecPath; - // For Mac OSX. + // For macOS. env.DYLD_LIBRARY_PATH = (env.DYLD_LIBRARY_PATH ? env.DYLD_LIBRARY_PATH + path.delimiter : '') + kExecPath; diff --git a/test/js/node/test/find-new-passes.ts b/test/js/node/test/find-new-passes.ts new file mode 100644 index 0000000000..48c90c33e2 --- /dev/null +++ b/test/js/node/test/find-new-passes.ts @@ -0,0 +1,61 @@ +import path from "path"; +import fs from "fs"; +import { spawn } from "child_process"; + +const localDir = path.resolve(import.meta.dirname, "./parallel"); +const upstreamDir = path.resolve(import.meta.dirname, "../../../node.js/upstream/test/parallel"); + +const localFiles = fs.readdirSync(localDir); +const upstreamFiles = fs.readdirSync(upstreamDir); + +const newFiles = upstreamFiles.filter((file) => !localFiles.includes(file)); + +process.on('SIGTERM', () => { + console.log("SIGTERM received"); +}); +process.on('SIGINT', () => { + console.log("SIGINT received"); +}); + +const stdin = process.stdin; +if (stdin.isTTY) { + stdin.setRawMode(true); + stdin.on('data', (data) => { + if (data[0] === 0x03) { + stdin.setRawMode(false); + console.log("Cancelled"); + process.exit(0); + } + }); +} +process.on('exit', () => { + if (stdin.isTTY) { + stdin.setRawMode(false); + } +}); + +for (const file of newFiles) { + await new Promise((resolve, reject) => { + // Run with a timeout of 5 seconds + const proc = spawn("bun-debug", ["run", path.join(upstreamDir, file)], { + timeout: 5000, + stdio: "inherit", + env: { + ...process.env, + BUN_DEBUG_QUIET_LOGS: "1", + }, + }); + + proc.on("error", (err) => { + console.error(err); + }); + + proc.on("exit", (code) => { + if (code === 0) { + console.log(`New Pass: ${file}`); + fs.appendFileSync("new-passes.txt", file + "\n"); + } + resolve(); + }); + }); +} diff --git a/test/js/node/test/parallel/.gitignore b/test/js/node/test/parallel/.gitignore deleted file mode 100644 index fd3ec92e0c..0000000000 --- a/test/js/node/test/parallel/.gitignore +++ /dev/null @@ -1,67 +0,0 @@ -# Not working yet: -child-process-double-pipe.test.js -child-process-exec-cwd.test.js -child-process-exec-timeout-expire.test.js -child-process-spawn-controller.test.js -child-process-stdio-inherit.test.js -cluster-fork-env.test.js -cluster-kill-infinite-loop.test.js -file-write-stream4.test.js -file-write-stream5.test.js -filehandle-close.test.js -fs-existssync-false.test.js -fs-fmap.test.js -fs-read-stream-fd.test.js -fs-readdir-ucs2.test.js -fs-watch-recursive-add-file-to-new-folder.test.js -fs-watch-recursive-symlink.test.js -http-parser-finish-error.test.js -http-request-agent.test.js -http2-connect-options.test.js -https-server-connections-checking-leak.test.js -module-circular-symlinks.test.js -module-prototype-mutation.test.js -net-listen-error.test.js -net-server-close.test.js -permission-fs-windows-path.test.js -pipe-abstract-socket-http.test.js -pipe-file-to-http.test.js -process-ppid.test.js -require-invalid-package.test.js -require-long-path.test.js -snapshot-dns-lookup-localhost.test.js -trace-events-net-abstract-socket.test.js -trace-events-worker-metadata-with-name.test.js -windows-failed-heap-allocation.test.js -worker-dns-terminate.test.js -worker-esm-exit.test.js -worker-message-port-wasm-module.test.js - -# Failing on Windows: -pipe-head.test.js -http-client-response-domain.test.js -require-extensions-same-filename-as-dir-trailing-slash.test.js -fs-realpath-on-substed-drive.test.js -fs-symlink-dir-junction.test.js - -# macOS not working yet -node-dns.test.js - -# Things we don't support: -repl* -inspector* -npm* -*changelog* -permission* -shadow-realm* -trace-events* -cli-node-options* -corepack* -eslint* -coverage* -buffer-zero-fill-cli* -icu-minimum-version.test.js -release-npm.test.js - -# Bad tests -tls-wrap-no-abort.test.js diff --git a/test/js/node/test/parallel/arm-math-illegal-instruction.test.js b/test/js/node/test/parallel/arm-math-illegal-instruction.test.js deleted file mode 100644 index 58199f8742..0000000000 --- a/test/js/node/test/parallel/arm-math-illegal-instruction.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-arm-math-illegal-instruction.js -//#SHA1: 08aea7234b93dfe296564c6dd21a58bc91acd9dd -//----------------- -"use strict"; - -// This test ensures Math functions don't fail with an "illegal instruction" -// error on ARM devices (primarily on the Raspberry Pi 1) -// See https://github.com/nodejs/node/issues/1376 -// and https://code.google.com/p/v8/issues/detail?id=4019 - -test("Math functions do not fail with illegal instruction on ARM devices", () => { - // Iterate over all Math functions - Object.getOwnPropertyNames(Math).forEach(functionName => { - if (!/[A-Z]/.test(functionName)) { - // The function names don't have capital letters. - expect(() => Math[functionName](-0.5)).not.toThrow(); - } - }); -}); - -//<#END_FILE: test-arm-math-illegal-instruction.js diff --git a/test/js/node/test/parallel/assert-esm-cjs-message-verify.test.js b/test/js/node/test/parallel/assert-esm-cjs-message-verify.test.js deleted file mode 100644 index 93537273a8..0000000000 --- a/test/js/node/test/parallel/assert-esm-cjs-message-verify.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-assert-esm-cjs-message-verify.js -//#SHA1: 3d120c4813c4051523045df80fc501e9921b878f -//----------------- -"use strict"; - -const { spawnPromisified } = require("../common"); -const tmpdir = require("../common/tmpdir"); -const assert = require("assert"); -const { writeFileSync, unlink } = require("fs"); -const { join } = require("path"); - -tmpdir.refresh(); - -const fileImports = { - cjs: 'const assert = require("assert");', - mjs: 'import assert from "assert";', -}; - -const fileNames = []; - -for (const [ext, header] of Object.entries(fileImports)) { - const fileName = `test-file.${ext}`; - // Store the generated filesnames in an array - fileNames.push(join(tmpdir.path, fileName)); - - writeFileSync(tmpdir.resolve(fileName), `${header}\nassert.ok(0 === 2);`); -} - -describe("ensure the assert.ok throwing similar error messages for esm and cjs files", () => { - const nodejsPath = process.execPath; - const errorsMessages = []; - - test("should return code 1 for each command", async () => { - for (const fileName of fileNames) { - const { stderr, code } = await spawnPromisified(nodejsPath, [fileName]); - expect(code).toBe(1); - // For each error message, filter the lines which will starts with AssertionError - errorsMessages.push(stderr.split("\n").find(s => s.startsWith("AssertionError"))); - } - }); - - afterAll(() => { - expect(errorsMessages).toHaveLength(2); - expect(errorsMessages[0]).toEqual(errorsMessages[1]); - - for (const fileName of fileNames) { - unlink(fileName, () => {}); - } - - tmpdir.refresh(); - }); -}); - -//<#END_FILE: test-assert-esm-cjs-message-verify.js diff --git a/test/js/node/test/parallel/assert-strict-exists.test.js b/test/js/node/test/parallel/assert-strict-exists.test.js deleted file mode 100644 index 3595cf38ec..0000000000 --- a/test/js/node/test/parallel/assert-strict-exists.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-assert-strict-exists.js -//#SHA1: 390d3a53b3e79630cbb673eed78ac5857a49352f -//----------------- -"use strict"; - -test("assert/strict is the same as assert.strict", () => { - const assert = require("assert"); - const assertStrict = require("assert/strict"); - - expect(assertStrict).toBe(assert.strict); -}); - -//<#END_FILE: test-assert-strict-exists.js diff --git a/test/js/node/test/parallel/async-hooks-recursive-stack-runinasyncscope.test.js b/test/js/node/test/parallel/async-hooks-recursive-stack-runinasyncscope.test.js deleted file mode 100644 index 30f03f8332..0000000000 --- a/test/js/node/test/parallel/async-hooks-recursive-stack-runinasyncscope.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-async-hooks-recursive-stack-runInAsyncScope.js -//#SHA1: 7258dfd5a442e34e60920fb484336420db8754e2 -//----------------- -"use strict"; - -const async_hooks = require("async_hooks"); - -// This test verifies that the async ID stack can grow indefinitely. - -function recurse(n) { - const a = new async_hooks.AsyncResource("foobar"); - a.runInAsyncScope(() => { - expect(a.asyncId()).toBe(async_hooks.executionAsyncId()); - expect(a.triggerAsyncId()).toBe(async_hooks.triggerAsyncId()); - if (n >= 0) recurse(n - 1); - expect(a.asyncId()).toBe(async_hooks.executionAsyncId()); - expect(a.triggerAsyncId()).toBe(async_hooks.triggerAsyncId()); - }); -} - -test("async ID stack can grow indefinitely", () => { - expect(() => recurse(1000)).not.toThrow(); -}); - -//<#END_FILE: test-async-hooks-recursive-stack-runInAsyncScope.js diff --git a/test/js/node/test/parallel/async-hooks-run-in-async-scope-this-arg.test.js b/test/js/node/test/parallel/async-hooks-run-in-async-scope-this-arg.test.js deleted file mode 100644 index 41479c5c05..0000000000 --- a/test/js/node/test/parallel/async-hooks-run-in-async-scope-this-arg.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-async-hooks-run-in-async-scope-this-arg.js -//#SHA1: a716f9818bdd704e1cf7ca188ffd4ccb9501a8a7 -//----------------- -"use strict"; - -// Test that passing thisArg to runInAsyncScope() works. - -const { AsyncResource } = require("async_hooks"); - -const thisArg = {}; - -const res = new AsyncResource("fhqwhgads"); - -function callback() { - expect(this).toBe(thisArg); -} - -test("runInAsyncScope with thisArg", () => { - const callbackSpy = jest.fn(callback); - res.runInAsyncScope(callbackSpy, thisArg); - expect(callbackSpy).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-async-hooks-run-in-async-scope-this-arg.js diff --git a/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-1.test.js b/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-1.test.js deleted file mode 100644 index c8988c5655..0000000000 --- a/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-1.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-async-hooks-worker-asyncfn-terminate-1.js -//#SHA1: 556f29e8c45107ff3448d713145f6f4e070e8073 -//----------------- -"use strict"; - -const { Worker } = require("worker_threads"); - -test("Worker with async function and async_hooks terminates correctly", () => { - const w = new Worker( - ` - const { createHook } = require('async_hooks'); - - setImmediate(async () => { - createHook({ init() {} }).enable(); - await 0; - process.exit(); - }); - `, - { eval: true }, - ); - - return new Promise(resolve => { - w.on("exit", () => { - expect(true).toBe(true); // Ensures the 'exit' event was called - resolve(); - }); - }); -}); - -//<#END_FILE: test-async-hooks-worker-asyncfn-terminate-1.js diff --git a/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-2.test.js b/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-2.test.js deleted file mode 100644 index 2140f81307..0000000000 --- a/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-2.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-async-hooks-worker-asyncfn-terminate-2.js -//#SHA1: 2329c972e1256f3aadc37e92f0ff1219d8519328 -//----------------- -"use strict"; - -const { Worker } = require("worker_threads"); - -// Like test-async-hooks-worker-promise.js but with the `await` and `createHook` -// lines switched, because that resulted in different assertion failures -// (one a Node.js assertion and one a V8 DCHECK) and it seems prudent to -// cover both of those failures. - -test("Worker with async function and createHook", () => { - const w = new Worker( - ` - const { createHook } = require('async_hooks'); - - setImmediate(async () => { - await 0; - createHook({ init() {} }).enable(); - process.exit(); - }); - `, - { eval: true }, - ); - - w.postMessage({}); - - return new Promise(resolve => { - w.on("exit", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - resolve(); - }); - }); -}); - -//<#END_FILE: test-async-hooks-worker-asyncfn-terminate-2.js diff --git a/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-3.test.js b/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-3.test.js deleted file mode 100644 index ecc905292a..0000000000 --- a/test/js/node/test/parallel/async-hooks-worker-asyncfn-terminate-3.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-async-hooks-worker-asyncfn-terminate-3.js -//#SHA1: bd68d52f5ecd5cb22738f78ee855706ed424cbf0 -//----------------- -"use strict"; - -const { Worker } = require("worker_threads"); - -// Like test-async-hooks-worker-promise.js but with an additional statement -// after the `process.exit()` call, that shouldn't really make a difference -// but apparently does. - -test("Worker with async function and process.exit()", done => { - const w = new Worker( - ` - const { createHook } = require('async_hooks'); - - setImmediate(async () => { - createHook({ init() {} }).enable(); - await 0; - process.exit(); - process._rawDebug('THIS SHOULD NEVER BE REACHED'); - }); - `, - { eval: true }, - ); - - w.on("exit", () => { - expect(true).toBe(true); // Ensure the exit event is called - done(); - }); -}); - -//<#END_FILE: test-async-hooks-worker-asyncfn-terminate-3.js diff --git a/test/js/node/test/parallel/async-local-storage-deep-stack.test.js b/test/js/node/test/parallel/async-local-storage-deep-stack.test.js deleted file mode 100644 index 9c3926b18b..0000000000 --- a/test/js/node/test/parallel/async-local-storage-deep-stack.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-async-local-storage-deep-stack.js -//#SHA1: 305d85dc794f55b19fffebfbb720ba0c83714f63 -//----------------- -"use strict"; - -const { AsyncLocalStorage } = require("async_hooks"); - -// Regression test for: https://github.com/nodejs/node/issues/34556 - -test("AsyncLocalStorage deep stack", () => { - const als = new AsyncLocalStorage(); - - const done = jest.fn(); - - function run(count) { - if (count !== 0) return als.run({}, run, --count); - done(); - } - - run(1000); - - expect(done).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-async-local-storage-deep-stack.js diff --git a/test/js/node/test/parallel/async-local-storage-http-multiclients.test.js b/test/js/node/test/parallel/async-local-storage-http-multiclients.test.js deleted file mode 100644 index b90b19ea6b..0000000000 --- a/test/js/node/test/parallel/async-local-storage-http-multiclients.test.js +++ /dev/null @@ -1,89 +0,0 @@ -//#FILE: test-async-local-storage-http-multiclients.js -//#SHA1: adf8feaec8fa034cbb22fd0f3e2a5ed224c1905e -//----------------- -"use strict"; - -const { AsyncLocalStorage } = require("async_hooks"); -const http = require("http"); - -const NUM_CLIENTS = 10; - -// Run multiple clients that receive data from a server -// in multiple chunks, in a single non-closure function. -// Use the AsyncLocalStorage (ALS) APIs to maintain the context -// and data download. Make sure that individual clients -// receive their respective data, with no conflicts. - -describe("AsyncLocalStorage with multiple HTTP clients", () => { - const cls = new AsyncLocalStorage(); - let server; - let index = 0; - - beforeAll(() => { - // Set up a server that sends large buffers of data, filled - // with cardinal numbers, increasing per request - server = http.createServer((q, r) => { - // Send a large chunk as response, otherwise the data - // may be sent in a single chunk, and the callback in the - // client may be called only once, defeating the purpose of test - r.end((index++ % 10).toString().repeat(1024 * 1024)); - }); - }); - - afterAll(() => { - server.close(); - }); - - it("should handle multiple clients correctly", async () => { - const clientPromises = []; - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - for (let i = 0; i < NUM_CLIENTS; i++) { - clientPromises.push( - new Promise(resolve => { - cls.run(new Map(), () => { - const options = { port: server.address().port }; - const req = http.get(options, res => { - const store = cls.getStore(); - store.set("data", ""); - - // Make ondata and onend non-closure - // functions and fully dependent on ALS - res.setEncoding("utf8"); - res.on("data", ondata); - res.on("end", () => { - onend(); - resolve(); - }); - }); - req.end(); - }); - }), - ); - } - - await Promise.all(clientPromises); - }); - - // Accumulate the current data chunk with the store data - function ondata(d) { - const store = cls.getStore(); - expect(store).not.toBeUndefined(); - let chunk = store.get("data"); - chunk += d; - store.set("data", chunk); - } - - // Retrieve the store data, and test for homogeneity - function onend() { - const store = cls.getStore(); - expect(store).not.toBeUndefined(); - const data = store.get("data"); - expect(data).toBe(data[0].repeat(data.length)); - } -}); - -//<#END_FILE: test-async-local-storage-http-multiclients.js diff --git a/test/js/node/test/parallel/async-local-storage-snapshot.test.js b/test/js/node/test/parallel/async-local-storage-snapshot.test.js deleted file mode 100644 index 8b4d0b80bd..0000000000 --- a/test/js/node/test/parallel/async-local-storage-snapshot.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-async-local-storage-snapshot.js -//#SHA1: f8d967194bfb0b73994d296b03c0c43afa5127e5 -//----------------- -"use strict"; - -const { AsyncLocalStorage } = require("async_hooks"); - -describe("AsyncLocalStorage snapshot", () => { - test("should preserve the original context when using snapshot", () => { - const asyncLocalStorage = new AsyncLocalStorage(); - - const runInAsyncScope = asyncLocalStorage.run(123, () => AsyncLocalStorage.snapshot()); - - const result = asyncLocalStorage.run(321, () => { - return runInAsyncScope(() => { - return asyncLocalStorage.getStore(); - }); - }); - - expect(result).toBe(123); - }); -}); - -//<#END_FILE: test-async-local-storage-snapshot.js diff --git a/test/js/node/test/parallel/atomics-wake.test.js b/test/js/node/test/parallel/atomics-wake.test.js deleted file mode 100644 index 7c690a086a..0000000000 --- a/test/js/node/test/parallel/atomics-wake.test.js +++ /dev/null @@ -1,11 +0,0 @@ -//#FILE: test-atomics-wake.js -//#SHA1: 311b66a7cd5fbc08a20b77de98a66f9cba763f8f -//----------------- -"use strict"; - -// https://github.com/nodejs/node/issues/21219 -test("Atomics.wake should be undefined", () => { - expect(Atomics.wake).toBeUndefined(); -}); - -//<#END_FILE: test-atomics-wake.js diff --git a/test/js/node/test/parallel/bad-unicode.test.js b/test/js/node/test/parallel/bad-unicode.test.js deleted file mode 100644 index 26c70dbf53..0000000000 --- a/test/js/node/test/parallel/bad-unicode.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-bad-unicode.js -//#SHA1: e9b8765f74af6588aff1bd7bfcbc6a19187d100e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("eval with bad unicode throws SyntaxError", () => { - expect(() => { - eval('"\\uc/ef"'); - }).toThrow( - expect.objectContaining({ - name: "SyntaxError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-bad-unicode.js diff --git a/test/js/node/test/parallel/beforeexit-event-exit.test.js b/test/js/node/test/parallel/beforeexit-event-exit.test.js deleted file mode 100644 index c7a0ad2a9c..0000000000 --- a/test/js/node/test/parallel/beforeexit-event-exit.test.js +++ /dev/null @@ -1,44 +0,0 @@ -//#FILE: test-beforeexit-event-exit.js -//#SHA1: 27b6351612c5ec4f51d3317ca1c89511b833eaab -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("beforeExit event should not be called when process.exit() is called", () => { - const mockBeforeExit = jest.fn(); - process.on("beforeExit", mockBeforeExit); - - // Spy on process.exit to prevent actual exit - const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {}); - - process.exit(); - - expect(mockBeforeExit).not.toHaveBeenCalled(); - expect(exitSpy).toHaveBeenCalled(); - - // Clean up - process.removeListener("beforeExit", mockBeforeExit); - exitSpy.mockRestore(); -}); - -//<#END_FILE: test-beforeexit-event-exit.js diff --git a/test/js/node/test/parallel/binding-constants.test.js b/test/js/node/test/parallel/binding-constants.test.js deleted file mode 100644 index e3cabf4e2b..0000000000 --- a/test/js/node/test/parallel/binding-constants.test.js +++ /dev/null @@ -1,44 +0,0 @@ -//#FILE: test-binding-constants.js -//#SHA1: 84b14e2a54ec767074f2a4103eaa0b419655cf8b -//----------------- -"use strict"; - -// Note: This test originally used internal bindings which are not recommended for use in tests. -// The test has been modified to focus on the public API and behavior that can be tested without internals. - -test("constants object structure", () => { - const constants = process.binding("constants"); - - expect(Object.keys(constants).sort()).toEqual(["crypto", "fs", "os", "trace", "zlib"]); - - expect(Object.keys(constants.os).sort()).toEqual(["UV_UDP_REUSEADDR", "dlopen", "errno", "priority", "signals"]); -}); - -test("constants objects do not inherit from Object.prototype", () => { - const constants = process.binding("constants"); - const inheritedProperties = Object.getOwnPropertyNames(Object.prototype); - - function testObject(obj) { - expect(obj).toBeTruthy(); - expect(Object.prototype.toString.call(obj)).toBe("[object Object]"); - expect(Object.getPrototypeOf(obj)).toBeNull(); - - inheritedProperties.forEach(property => { - expect(property in obj).toBe(false); - }); - } - - [ - constants, - constants.crypto, - constants.fs, - constants.os, - constants.trace, - constants.zlib, - constants.os.dlopen, - constants.os.errno, - constants.os.signals, - ].forEach(testObject); -}); - -//<#END_FILE: test-binding-constants.js diff --git a/test/js/node/test/parallel/blob-createobjecturl.test.js b/test/js/node/test/parallel/blob-createobjecturl.test.js deleted file mode 100644 index 9f94ba18f5..0000000000 --- a/test/js/node/test/parallel/blob-createobjecturl.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-blob-createobjecturl.js -//#SHA1: d2030ca0ad6757dd9d338bc2e65cd3ff8917009d -//----------------- -// Flags: --no-warnings -"use strict"; - -// Because registering a Blob URL requires generating a random -// UUID, it can only be done if crypto support is enabled. -if (typeof crypto === "undefined") { - test.skip("missing crypto"); -} - -const { URL } = require("url"); -const { Blob, resolveObjectURL } = require("buffer"); - -test("Blob URL creation and resolution", async () => { - const blob = new Blob(["hello"]); - const id = URL.createObjectURL(blob); - expect(typeof id).toBe("string"); - const otherBlob = resolveObjectURL(id); - expect(otherBlob).toBeInstanceOf(Blob); - expect(otherBlob.constructor).toBe(Blob); - expect(otherBlob.size).toBe(5); - expect(Buffer.from(await otherBlob.arrayBuffer()).toString()).toBe("hello"); - URL.revokeObjectURL(id); - - // should do nothing - URL.revokeObjectURL(id); - - expect(resolveObjectURL(id)).toBeUndefined(); - - // Leaving a Blob registered should not cause an assert - // when Node.js exists - URL.createObjectURL(new Blob()); -}); - -test("resolveObjectURL with invalid inputs", () => { - ["not a url", undefined, 1, "blob:nodedata:1:wrong", {}].forEach(i => { - expect(resolveObjectURL(i)).toBeUndefined(); - }); -}); - -test("createObjectURL with invalid inputs", () => { - [undefined, 1, "", false, {}].forEach(i => { - expect(() => URL.createObjectURL(i)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-blob-createobjecturl.js diff --git a/test/js/node/test/parallel/buffer-arraybuffer.test.js b/test/js/node/test/parallel/buffer-arraybuffer.test.js deleted file mode 100644 index d33487198f..0000000000 --- a/test/js/node/test/parallel/buffer-arraybuffer.test.js +++ /dev/null @@ -1,158 +0,0 @@ -//#FILE: test-buffer-arraybuffer.js -//#SHA1: 2297240ef18399097bd3383db051d8e37339a123 -//----------------- -"use strict"; - -const LENGTH = 16; - -test("Buffer from ArrayBuffer", () => { - const ab = new ArrayBuffer(LENGTH); - const dv = new DataView(ab); - const ui = new Uint8Array(ab); - const buf = Buffer.from(ab); - - expect(buf).toBeInstanceOf(Buffer); - expect(buf.parent).toBe(buf.buffer); - expect(buf.buffer).toBe(ab); - expect(buf.length).toBe(ab.byteLength); - - buf.fill(0xc); - for (let i = 0; i < LENGTH; i++) { - expect(ui[i]).toBe(0xc); - ui[i] = 0xf; - expect(buf[i]).toBe(0xf); - } - - buf.writeUInt32LE(0xf00, 0); - buf.writeUInt32BE(0xb47, 4); - buf.writeDoubleLE(3.1415, 8); - - expect(dv.getUint32(0, true)).toBe(0xf00); - expect(dv.getUint32(4)).toBe(0xb47); - expect(dv.getFloat64(8, true)).toBe(3.1415); -}); - -test.todo("Buffer.from with invalid ArrayBuffer", () => { - expect(() => { - function AB() {} - Object.setPrototypeOf(AB, ArrayBuffer); - Object.setPrototypeOf(AB.prototype, ArrayBuffer.prototype); - Buffer.from(new AB()); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining( - "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object.", - ), - }), - ); -}); - -test("Buffer.from with byteOffset and length arguments", () => { - const ab = new Uint8Array(5); - ab[0] = 1; - ab[1] = 2; - ab[2] = 3; - ab[3] = 4; - ab[4] = 5; - const buf = Buffer.from(ab.buffer, 1, 3); - expect(buf.length).toBe(3); - expect(buf[0]).toBe(2); - expect(buf[1]).toBe(3); - expect(buf[2]).toBe(4); - buf[0] = 9; - expect(ab[1]).toBe(9); - - expect(() => Buffer.from(ab.buffer, 6)).toThrow( - expect.objectContaining({ - name: "RangeError", - // code: "ERR_BUFFER_OUT_OF_BOUNDS", - // message: expect.stringContaining('"offset" is outside of buffer bounds'), - }), - ); - - expect(() => Buffer.from(ab.buffer, 3, 6)).toThrow( - expect.objectContaining({ - name: "RangeError", - // code: "ERR_BUFFER_OUT_OF_BOUNDS", - // message: expect.stringContaining('"length" is outside of buffer bounds'), - }), - ); -}); - -test("Deprecated Buffer() constructor", () => { - const ab = new Uint8Array(5); - ab[0] = 1; - ab[1] = 2; - ab[2] = 3; - ab[3] = 4; - ab[4] = 5; - const buf = Buffer(ab.buffer, 1, 3); - expect(buf.length).toBe(3); - expect(buf[0]).toBe(2); - expect(buf[1]).toBe(3); - expect(buf[2]).toBe(4); - buf[0] = 9; - expect(ab[1]).toBe(9); - - expect(() => Buffer(ab.buffer, 6)).toThrow( - expect.objectContaining({ - name: "RangeError", - // code: "ERR_BUFFER_OUT_OF_BOUNDS", - // message: expect.stringContaining('"offset" is outside of buffer bounds'), - }), - ); - - expect(() => Buffer(ab.buffer, 3, 6)).toThrow( - expect.objectContaining({ - name: "RangeError", - // code: "ERR_BUFFER_OUT_OF_BOUNDS", - // message: expect.stringContaining('"length" is outside of buffer bounds'), - }), - ); -}); - -test("Buffer.from with non-numeric byteOffset", () => { - const ab = new ArrayBuffer(10); - const expected = Buffer.from(ab, 0); - expect(Buffer.from(ab, "fhqwhgads")).toEqual(expected); - expect(Buffer.from(ab, NaN)).toEqual(expected); - expect(Buffer.from(ab, {})).toEqual(expected); - expect(Buffer.from(ab, [])).toEqual(expected); - - expect(Buffer.from(ab, [1])).toEqual(Buffer.from(ab, 1)); - - expect(() => Buffer.from(ab, Infinity)).toThrow( - expect.objectContaining({ - name: "RangeError", - // code: "ERR_BUFFER_OUT_OF_BOUNDS", - // message: expect.stringContaining('"offset" is outside of buffer bounds'), - }), - ); -}); - -test("Buffer.from with non-numeric length", () => { - const ab = new ArrayBuffer(10); - const expected = Buffer.from(ab, 0, 0); - expect(Buffer.from(ab, 0, "fhqwhgads")).toEqual(expected); - expect(Buffer.from(ab, 0, NaN)).toEqual(expected); - expect(Buffer.from(ab, 0, {})).toEqual(expected); - expect(Buffer.from(ab, 0, [])).toEqual(expected); - - expect(Buffer.from(ab, 0, [1])).toEqual(Buffer.from(ab, 0, 1)); - - expect(() => Buffer.from(ab, 0, Infinity)).toThrow( - expect.objectContaining({ - name: "RangeError", - // code: "ERR_BUFFER_OUT_OF_BOUNDS", - // message: expect.stringContaining('"length" is outside of buffer bounds'), - }), - ); -}); - -test("Buffer.from with array-like entry and NaN length", () => { - expect(Buffer.from({ length: NaN })).toEqual(Buffer.alloc(0)); -}); - -//<#END_FILE: test-buffer-arraybuffer.js diff --git a/test/js/node/test/parallel/buffer-bytelength.test.js b/test/js/node/test/parallel/buffer-bytelength.test.js deleted file mode 100644 index 5934db1dc8..0000000000 --- a/test/js/node/test/parallel/buffer-bytelength.test.js +++ /dev/null @@ -1,131 +0,0 @@ -//#FILE: test-buffer-bytelength.js -//#SHA1: bcc75ad2f868ac9414c789c29f23ee9c806c749d -//----------------- -"use strict"; - -const SlowBuffer = require("buffer").SlowBuffer; -const vm = require("vm"); - -test("Buffer.byteLength with invalid arguments", () => { - [[32, "latin1"], [NaN, "utf8"], [{}, "latin1"], []].forEach(args => { - expect(() => Buffer.byteLength(...args)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining( - 'The "string" argument must be of type string or an instance of Buffer or ArrayBuffer.', - ), - }), - ); - }); -}); - -test("ArrayBuffer.isView for various Buffer types", () => { - expect(ArrayBuffer.isView(new Buffer(10))).toBe(true); - expect(ArrayBuffer.isView(new SlowBuffer(10))).toBe(true); - expect(ArrayBuffer.isView(Buffer.alloc(10))).toBe(true); - expect(ArrayBuffer.isView(Buffer.allocUnsafe(10))).toBe(true); - expect(ArrayBuffer.isView(Buffer.allocUnsafeSlow(10))).toBe(true); - expect(ArrayBuffer.isView(Buffer.from(""))).toBe(true); -}); - -test("Buffer.byteLength for various buffer types", () => { - const incomplete = Buffer.from([0xe4, 0xb8, 0xad, 0xe6, 0x96]); - expect(Buffer.byteLength(incomplete)).toBe(5); - - const ascii = Buffer.from("abc"); - expect(Buffer.byteLength(ascii)).toBe(3); - - const buffer = new ArrayBuffer(8); - expect(Buffer.byteLength(buffer)).toBe(8); -}); - -test("Buffer.byteLength for TypedArrays", () => { - expect(Buffer.byteLength(new Int8Array(8))).toBe(8); - expect(Buffer.byteLength(new Uint8Array(8))).toBe(8); - expect(Buffer.byteLength(new Uint8ClampedArray(2))).toBe(2); - expect(Buffer.byteLength(new Int16Array(8))).toBe(16); - expect(Buffer.byteLength(new Uint16Array(8))).toBe(16); - expect(Buffer.byteLength(new Int32Array(8))).toBe(32); - expect(Buffer.byteLength(new Uint32Array(8))).toBe(32); - expect(Buffer.byteLength(new Float32Array(8))).toBe(32); - expect(Buffer.byteLength(new Float64Array(8))).toBe(64); -}); - -test("Buffer.byteLength for DataView", () => { - const dv = new DataView(new ArrayBuffer(2)); - expect(Buffer.byteLength(dv)).toBe(2); -}); - -test("Buffer.byteLength for zero length string", () => { - expect(Buffer.byteLength("", "ascii")).toBe(0); - expect(Buffer.byteLength("", "HeX")).toBe(0); -}); - -test("Buffer.byteLength for utf8", () => { - expect(Buffer.byteLength("∑éllö wørl∂!", "utf-8")).toBe(19); - expect(Buffer.byteLength("κλμνξο", "utf8")).toBe(12); - expect(Buffer.byteLength("挵挶挷挸挹", "utf-8")).toBe(15); - expect(Buffer.byteLength("𠝹𠱓𠱸", "UTF8")).toBe(12); - expect(Buffer.byteLength("hey there")).toBe(9); - expect(Buffer.byteLength("𠱸挶νξ#xx :)")).toBe(17); - expect(Buffer.byteLength("hello world", "")).toBe(11); - expect(Buffer.byteLength("hello world", "abc")).toBe(11); - expect(Buffer.byteLength("ßœ∑≈", "unkn0wn enc0ding")).toBe(10); -}); - -test("Buffer.byteLength for base64", () => { - expect(Buffer.byteLength("aGVsbG8gd29ybGQ=", "base64")).toBe(11); - expect(Buffer.byteLength("aGVsbG8gd29ybGQ=", "BASE64")).toBe(11); - expect(Buffer.byteLength("bm9kZS5qcyByb2NrcyE=", "base64")).toBe(14); - expect(Buffer.byteLength("aGkk", "base64")).toBe(3); - expect(Buffer.byteLength("bHNrZGZsa3NqZmtsc2xrZmFqc2RsZmtqcw==", "base64")).toBe(25); -}); - -test("Buffer.byteLength for base64url", () => { - expect(Buffer.byteLength("aGVsbG8gd29ybGQ", "base64url")).toBe(11); - expect(Buffer.byteLength("aGVsbG8gd29ybGQ", "BASE64URL")).toBe(11); - expect(Buffer.byteLength("bm9kZS5qcyByb2NrcyE", "base64url")).toBe(14); - expect(Buffer.byteLength("aGkk", "base64url")).toBe(3); - expect(Buffer.byteLength("bHNrZGZsa3NqZmtsc2xrZmFqc2RsZmtqcw", "base64url")).toBe(25); -}); - -test("Buffer.byteLength for special padding", () => { - expect(Buffer.byteLength("aaa=", "base64")).toBe(2); - expect(Buffer.byteLength("aaaa==", "base64")).toBe(3); - expect(Buffer.byteLength("aaa=", "base64url")).toBe(2); - expect(Buffer.byteLength("aaaa==", "base64url")).toBe(3); -}); - -test("Buffer.byteLength for various encodings", () => { - expect(Buffer.byteLength("Il était tué")).toBe(14); - expect(Buffer.byteLength("Il était tué", "utf8")).toBe(14); - - ["ascii", "latin1", "binary"] - .reduce((es, e) => es.concat(e, e.toUpperCase()), []) - .forEach(encoding => { - expect(Buffer.byteLength("Il était tué", encoding)).toBe(12); - }); - - ["ucs2", "ucs-2", "utf16le", "utf-16le"] - .reduce((es, e) => es.concat(e, e.toUpperCase()), []) - .forEach(encoding => { - expect(Buffer.byteLength("Il était tué", encoding)).toBe(24); - }); -}); - -test("Buffer.byteLength for ArrayBuffer from different context", () => { - const arrayBuf = vm.runInNewContext("new ArrayBuffer()"); - expect(Buffer.byteLength(arrayBuf)).toBe(0); -}); - -test("Buffer.byteLength for invalid encodings", () => { - for (let i = 1; i < 10; i++) { - const encoding = String(i).repeat(i); - - expect(Buffer.isEncoding(encoding)).toBe(false); - expect(Buffer.byteLength("foo", encoding)).toBe(Buffer.byteLength("foo", "utf8")); - } -}); - -//<#END_FILE: test-buffer-bytelength.js diff --git a/test/js/node/test/parallel/buffer-compare-offset.test.js b/test/js/node/test/parallel/buffer-compare-offset.test.js deleted file mode 100644 index df674d2f59..0000000000 --- a/test/js/node/test/parallel/buffer-compare-offset.test.js +++ /dev/null @@ -1,95 +0,0 @@ -//#FILE: test-buffer-compare-offset.js -//#SHA1: 460e187ac1a40db0dbc00801ad68f1272d27c3cd -//----------------- -"use strict"; - -const assert = require("assert"); - -describe("Buffer.compare with offset", () => { - const a = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]); - const b = Buffer.from([5, 6, 7, 8, 9, 0, 1, 2, 3, 4]); - - test("basic comparison", () => { - expect(a.compare(b)).toBe(-1); - }); - - test("comparison with default arguments", () => { - expect(a.compare(b, 0)).toBe(-1); - expect(() => a.compare(b, "0")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - expect(a.compare(b, undefined)).toBe(-1); - }); - - test("comparison with specified ranges", () => { - expect(a.compare(b, 0, undefined, 0)).toBe(-1); - expect(a.compare(b, 0, 0, 0)).toBe(1); - expect(() => a.compare(b, 0, "0", "0")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - expect(a.compare(b, 6, 10)).toBe(1); - expect(a.compare(b, 6, 10, 0, 0)).toBe(-1); - expect(a.compare(b, 0, 0, 0, 0)).toBe(0); - expect(a.compare(b, 1, 1, 2, 2)).toBe(0); - expect(a.compare(b, 0, 5, 4)).toBe(1); - expect(a.compare(b, 5, undefined, 1)).toBe(1); - expect(a.compare(b, 2, 4, 2)).toBe(-1); - expect(a.compare(b, 0, 7, 4)).toBe(-1); - expect(a.compare(b, 0, 7, 4, 6)).toBe(-1); - }); - - test("invalid arguments", () => { - expect(() => a.compare(b, 0, null)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - expect(() => a.compare(b, 0, { valueOf: () => 5 })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - expect(() => a.compare(b, Infinity, -Infinity)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - }), - ); - expect(a.compare(b, 0xff)).toBe(1); - expect(() => a.compare(b, "0xff")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - expect(() => a.compare(b, 0, "0xff")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - }); - - test("out of range arguments", () => { - const oor = expect.objectContaining({ code: "ERR_OUT_OF_RANGE" }); - expect(() => a.compare(b, 0, 100, 0)).toThrow(oor); - expect(() => a.compare(b, 0, 1, 0, 100)).toThrow(oor); - expect(() => a.compare(b, -1)).toThrow(oor); - expect(() => a.compare(b, 0, Infinity)).toThrow(oor); - expect(() => a.compare(b, 0, 1, -1)).toThrow(oor); - expect(() => a.compare(b, -Infinity, Infinity)).toThrow(oor); - }); - - test("missing target argument", () => { - expect(() => a.compare()).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining('The "target" argument must be an instance of Buffer or Uint8Array'), - }), - ); - }); -}); - -//<#END_FILE: test-buffer-compare-offset.js diff --git a/test/js/node/test/parallel/buffer-compare.test.js b/test/js/node/test/parallel/buffer-compare.test.js deleted file mode 100644 index 9f6d0c70be..0000000000 --- a/test/js/node/test/parallel/buffer-compare.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-buffer-compare.js -//#SHA1: eab68d7262240af3d53eabedb0e7a515b2d84adf -//----------------- -"use strict"; - -test("Buffer compare", () => { - const b = Buffer.alloc(1, "a"); - const c = Buffer.alloc(1, "c"); - const d = Buffer.alloc(2, "aa"); - const e = new Uint8Array([0x61, 0x61]); // ASCII 'aa', same as d - - expect(b.compare(c)).toBe(-1); - expect(c.compare(d)).toBe(1); - expect(d.compare(b)).toBe(1); - expect(d.compare(e)).toBe(0); - expect(b.compare(d)).toBe(-1); - expect(b.compare(b)).toBe(0); - - expect(Buffer.compare(b, c)).toBe(-1); - expect(Buffer.compare(c, d)).toBe(1); - expect(Buffer.compare(d, b)).toBe(1); - expect(Buffer.compare(b, d)).toBe(-1); - expect(Buffer.compare(c, c)).toBe(0); - expect(Buffer.compare(e, e)).toBe(0); - expect(Buffer.compare(d, e)).toBe(0); - expect(Buffer.compare(d, b)).toBe(1); - - expect(Buffer.compare(Buffer.alloc(0), Buffer.alloc(0))).toBe(0); - expect(Buffer.compare(Buffer.alloc(0), Buffer.alloc(1))).toBe(-1); - expect(Buffer.compare(Buffer.alloc(1), Buffer.alloc(0))).toBe(1); - - expect(() => Buffer.compare(Buffer.alloc(1), "abc")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.stringContaining('The "buf2" argument must be an instance of Buffer or Uint8Array.'), - }), - ); - - expect(() => Buffer.compare("abc", Buffer.alloc(1))).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.stringContaining('The "buf1" argument must be an instance of Buffer or Uint8Array.'), - }), - ); - - expect(() => Buffer.alloc(1).compare("abc")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining('The "target" argument must be an instance of Buffer or Uint8Array.'), - }), - ); -}); - -//<#END_FILE: test-buffer-compare.js diff --git a/test/js/node/test/parallel/buffer-constants.test.js b/test/js/node/test/parallel/buffer-constants.test.js deleted file mode 100644 index 5c7c8a18d1..0000000000 --- a/test/js/node/test/parallel/buffer-constants.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-buffer-constants.js -//#SHA1: a5818d34d1588306e48d574ec76b69b2ee4dc51c -//----------------- -"use strict"; - -const { kMaxLength, kStringMaxLength } = require("buffer"); -const { MAX_LENGTH, MAX_STRING_LENGTH } = require("buffer").constants; - -test("Buffer constants", () => { - expect(typeof MAX_LENGTH).toBe("number"); - expect(typeof MAX_STRING_LENGTH).toBe("number"); - expect(MAX_STRING_LENGTH).toBeLessThanOrEqual(MAX_LENGTH); - - expect(() => " ".repeat(MAX_STRING_LENGTH + 1)).toThrow( - expect.objectContaining({ - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => " ".repeat(MAX_STRING_LENGTH)).not.toThrow(); - - // Legacy values match: - expect(kMaxLength).toBe(MAX_LENGTH); - expect(kStringMaxLength).toBe(MAX_STRING_LENGTH); -}); - -//<#END_FILE: test-buffer-constants.js diff --git a/test/js/node/test/parallel/buffer-copy.test.js b/test/js/node/test/parallel/buffer-copy.test.js deleted file mode 100644 index afb49923d2..0000000000 --- a/test/js/node/test/parallel/buffer-copy.test.js +++ /dev/null @@ -1,204 +0,0 @@ -//#FILE: test-buffer-copy.js -//#SHA1: bff8bfe75b7289a279d9fc1a1bf2293257282d27 -//----------------- -"use strict"; - -test("Buffer copy operations", () => { - const b = Buffer.allocUnsafe(1024); - const c = Buffer.allocUnsafe(512); - - let cntr = 0; - - // Copy 512 bytes, from 0 to 512. - b.fill(++cntr); - c.fill(++cntr); - const copied = b.copy(c, 0, 0, 512); - expect(copied).toBe(512); - for (let i = 0; i < c.length; i++) { - expect(c[i]).toBe(b[i]); - } - - // Current behavior is to coerce values to integers. - b.fill(++cntr); - c.fill(++cntr); - const copiedWithStrings = b.copy(c, "0", "0", "512"); - expect(copiedWithStrings).toBe(512); - for (let i = 0; i < c.length; i++) { - expect(c[i]).toBe(b[i]); - } - - // Floats will be converted to integers via `Math.floor` - b.fill(++cntr); - c.fill(++cntr); - const copiedWithFloat = b.copy(c, 0, 0, 512.5); - expect(copiedWithFloat).toBe(512); - for (let i = 0; i < c.length; i++) { - expect(c[i]).toBe(b[i]); - } - - // Copy c into b, without specifying sourceEnd - b.fill(++cntr); - c.fill(++cntr); - const copiedWithoutSourceEnd = c.copy(b, 0, 0); - expect(copiedWithoutSourceEnd).toBe(c.length); - for (let i = 0; i < c.length; i++) { - expect(b[i]).toBe(c[i]); - } - - // Copy c into b, without specifying sourceStart - b.fill(++cntr); - c.fill(++cntr); - const copiedWithoutSourceStart = c.copy(b, 0); - expect(copiedWithoutSourceStart).toBe(c.length); - for (let i = 0; i < c.length; i++) { - expect(b[i]).toBe(c[i]); - } - - // Copied source range greater than source length - b.fill(++cntr); - c.fill(++cntr); - const copiedWithGreaterRange = c.copy(b, 0, 0, c.length + 1); - expect(copiedWithGreaterRange).toBe(c.length); - for (let i = 0; i < c.length; i++) { - expect(b[i]).toBe(c[i]); - } - - // Copy longer buffer b to shorter c without targetStart - b.fill(++cntr); - c.fill(++cntr); - const copiedLongerToShorter = b.copy(c); - expect(copiedLongerToShorter).toBe(c.length); - for (let i = 0; i < c.length; i++) { - expect(c[i]).toBe(b[i]); - } - - // Copy starting near end of b to c - b.fill(++cntr); - c.fill(++cntr); - const copiedNearEnd = b.copy(c, 0, b.length - Math.floor(c.length / 2)); - expect(copiedNearEnd).toBe(Math.floor(c.length / 2)); - for (let i = 0; i < Math.floor(c.length / 2); i++) { - expect(c[i]).toBe(b[b.length - Math.floor(c.length / 2) + i]); - } - for (let i = Math.floor(c.length / 2) + 1; i < c.length; i++) { - expect(c[c.length - 1]).toBe(c[i]); - } - - // Try to copy 513 bytes, and check we don't overrun c - b.fill(++cntr); - c.fill(++cntr); - const copiedOverrun = b.copy(c, 0, 0, 513); - expect(copiedOverrun).toBe(c.length); - for (let i = 0; i < c.length; i++) { - expect(c[i]).toBe(b[i]); - } - - // Copy 768 bytes from b into b - b.fill(++cntr); - b.fill(++cntr, 256); - const copiedIntoSelf = b.copy(b, 0, 256, 1024); - expect(copiedIntoSelf).toBe(768); - for (let i = 0; i < b.length; i++) { - expect(b[i]).toBe(cntr); - } - - // Copy string longer than buffer length (failure will segfault) - const bb = Buffer.allocUnsafe(10); - bb.fill("hello crazy world"); - - // Try to copy from before the beginning of b. Should not throw. - expect(() => b.copy(c, 0, 100, 10)).not.toThrow(); - - // Throw with invalid source type - expect(() => Buffer.prototype.copy.call(0)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", //TODO:"ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - // Copy throws at negative targetStart - expect(() => Buffer.allocUnsafe(10).copy(Buffer.allocUnsafe(5), -1, 0)).toThrow({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: `The value of "targetStart" is out of range. It must be >= 0 and <= 5. Received -1`, - }); - - // Copy throws at negative sourceStart - expect(() => Buffer.allocUnsafe(10).copy(Buffer.allocUnsafe(5), 0, -1)).toThrow({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: `The value of "sourceStart" is out of range. It must be >= 0 and <= 10. Received -1`, - }); - - // Copy throws if sourceStart is greater than length of source - expect(() => Buffer.allocUnsafe(10).copy(Buffer.allocUnsafe(5), 0, 100)).toThrow({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: `The value of "sourceStart" is out of range. It must be >= 0 and <= 10. Received 100`, - }); - - // Check sourceEnd resets to targetEnd if former is greater than the latter - b.fill(++cntr); - c.fill(++cntr); - b.copy(c, 0, 0, 1025); - for (let i = 0; i < c.length; i++) { - expect(c[i]).toBe(b[i]); - } - - // Throw with negative sourceEnd - expect(() => b.copy(c, 0, 0, -1)).toThrow({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: `The value of "sourceEnd" is out of range. It must be >= 0 and <= 1024. Received -1`, - }); - - // When sourceStart is greater than sourceEnd, zero copied - expect(b.copy(c, 0, 100, 10)).toBe(0); - - // When targetStart > targetLength, zero copied - expect(b.copy(c, 512, 0, 10)).toBe(0); - - // Test that the `target` can be a Uint8Array. - const d = new Uint8Array(c); - // copy 512 bytes, from 0 to 512. - b.fill(++cntr); - d.fill(++cntr); - const copiedToUint8Array = b.copy(d, 0, 0, 512); - expect(copiedToUint8Array).toBe(512); - for (let i = 0; i < d.length; i++) { - expect(d[i]).toBe(b[i]); - } - - // Test that the source can be a Uint8Array, too. - const e = new Uint8Array(b); - // copy 512 bytes, from 0 to 512. - e.fill(++cntr); - c.fill(++cntr); - const copiedFromUint8Array = Buffer.prototype.copy.call(e, c, 0, 0, 512); - expect(copiedFromUint8Array).toBe(512); - for (let i = 0; i < c.length; i++) { - expect(c[i]).toBe(e[i]); - } - - // https://github.com/nodejs/node/issues/23668: Do not crash for invalid input. - c.fill("c"); - b.copy(c, "not a valid offset"); - // Make sure this acted like a regular copy with `0` offset. - expect(c).toEqual(b.slice(0, c.length)); - - c.fill("C"); - expect(c.toString()).toBe("C".repeat(c.length)); - expect(() => { - b.copy(c, { - [Symbol.toPrimitive]() { - throw new Error("foo"); - }, - }); - }).toThrow("foo"); - // No copying took place: - expect(c.toString()).toBe("C".repeat(c.length)); -}); - -//<#END_FILE: test-buffer-copy.js diff --git a/test/js/node/test/parallel/buffer-equals.test.js b/test/js/node/test/parallel/buffer-equals.test.js deleted file mode 100644 index 8fbd4c13c4..0000000000 --- a/test/js/node/test/parallel/buffer-equals.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-buffer-equals.js -//#SHA1: 917344b9c4ba47f1e30d02ec6adfad938b2d342a -//----------------- -"use strict"; - -test("Buffer.equals", () => { - const b = Buffer.from("abcdf"); - const c = Buffer.from("abcdf"); - const d = Buffer.from("abcde"); - const e = Buffer.from("abcdef"); - - expect(b.equals(c)).toBe(true); - expect(c.equals(d)).toBe(false); - expect(d.equals(e)).toBe(false); - expect(d.equals(d)).toBe(true); - expect(d.equals(new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]))).toBe(true); - - expect(() => Buffer.alloc(1).equals("abc")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining( - `The "otherBuffer" argument must be an instance of Buffer or Uint8Array. Received`, - ), - }), - ); -}); - -//<#END_FILE: test-buffer-equals.js diff --git a/test/js/node/test/parallel/buffer-failed-alloc-typed-arrays.test.js b/test/js/node/test/parallel/buffer-failed-alloc-typed-arrays.test.js deleted file mode 100644 index 8dfcd1d03a..0000000000 --- a/test/js/node/test/parallel/buffer-failed-alloc-typed-arrays.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-buffer-failed-alloc-typed-arrays.js -//#SHA1: caa3a29c5ca1921e9ab5324d464067a364b8e687 -//----------------- -"use strict"; - -const { Buffer } = require("buffer"); -const SlowBuffer = require("buffer").SlowBuffer; - -// Test failed or zero-sized Buffer allocations not affecting typed arrays. -// This test exists because of a regression that occurred. Because Buffer -// instances are allocated with the same underlying allocator as TypedArrays, -// but Buffer's can optional be non-zero filled, there was a regression that -// occurred when a Buffer allocated failed, the internal flag specifying -// whether or not to zero-fill was not being reset, causing TypedArrays to -// allocate incorrectly. - -test("failed or zero-sized Buffer allocations do not affect typed arrays", () => { - const zeroArray = new Uint32Array(10).fill(0); - const sizes = [1e20, 0, 0.1, -1, "a", undefined, null, NaN]; - const allocators = [Buffer, SlowBuffer, Buffer.alloc, Buffer.allocUnsafe, Buffer.allocUnsafeSlow]; - - for (const allocator of allocators) { - for (const size of sizes) { - try { - // Some of these allocations are known to fail. If they do, - // Uint32Array should still produce a zeroed out result. - allocator(size); - } catch { - expect(new Uint32Array(10)).toEqual(zeroArray); - } - } - } -}); - -//<#END_FILE: test-buffer-failed-alloc-typed-arrays.js diff --git a/test/js/node/test/parallel/buffer-fill.test.js b/test/js/node/test/parallel/buffer-fill.test.js deleted file mode 100644 index f045645d93..0000000000 --- a/test/js/node/test/parallel/buffer-fill.test.js +++ /dev/null @@ -1,428 +0,0 @@ -//#FILE: test-buffer-fill.js -//#SHA1: 983940aa8a47c4d0985c2c4b4d1bc323a4e7d0f5 -//----------------- -"use strict"; - -const SIZE = 28; - -let buf1, buf2; - -beforeEach(() => { - buf1 = Buffer.allocUnsafe(SIZE); - buf2 = Buffer.allocUnsafe(SIZE); -}); - -// Helper functions -function genBuffer(size, args) { - const b = Buffer.allocUnsafe(size); - return b.fill(0).fill.apply(b, args); -} - -function bufReset() { - buf1.fill(0); - buf2.fill(0); -} - -function writeToFill(string, offset, end, encoding) { - if (typeof offset === "string") { - encoding = offset; - offset = 0; - end = buf2.length; - } else if (typeof end === "string") { - encoding = end; - end = buf2.length; - } else if (end === undefined) { - end = buf2.length; - } - - if (offset < 0 || end > buf2.length) throw new RangeError("ERR_OUT_OF_RANGE"); - - if (end <= offset) return buf2; - - offset >>>= 0; - end >>>= 0; - expect(offset).toBeLessThanOrEqual(buf2.length); - - const length = end - offset < 0 ? 0 : end - offset; - - let wasZero = false; - do { - const written = buf2.write(string, offset, length, encoding); - offset += written; - if (written === 0) { - if (wasZero) throw new Error("Could not write all data to Buffer"); - else wasZero = true; - } - } while (offset < buf2.length); - - return buf2; -} - -function testBufs(string, offset, length, encoding) { - bufReset(); - buf1.fill.apply(buf1, arguments); - expect(buf1.fill.apply(buf1, arguments)).toEqual(writeToFill.apply(null, arguments)); -} - -// Tests -test("Default encoding", () => { - testBufs("abc"); - testBufs("\u0222aa"); - testBufs("a\u0234b\u0235c\u0236"); - testBufs("abc", 4); - testBufs("abc", 5); - testBufs("abc", SIZE); - testBufs("\u0222aa", 2); - testBufs("\u0222aa", 8); - testBufs("a\u0234b\u0235c\u0236", 4); - testBufs("a\u0234b\u0235c\u0236", 12); - testBufs("abc", 4, 1); - testBufs("abc", 5, 1); - testBufs("\u0222aa", 8, 1); - testBufs("a\u0234b\u0235c\u0236", 4, 1); - testBufs("a\u0234b\u0235c\u0236", 12, 1); -}); - -test("UTF8 encoding", () => { - testBufs("abc", "utf8"); - testBufs("\u0222aa", "utf8"); - testBufs("a\u0234b\u0235c\u0236", "utf8"); - testBufs("abc", 4, "utf8"); - testBufs("abc", 5, "utf8"); - testBufs("abc", SIZE, "utf8"); - testBufs("\u0222aa", 2, "utf8"); - testBufs("\u0222aa", 8, "utf8"); - testBufs("a\u0234b\u0235c\u0236", 4, "utf8"); - testBufs("a\u0234b\u0235c\u0236", 12, "utf8"); - testBufs("abc", 4, 1, "utf8"); - testBufs("abc", 5, 1, "utf8"); - testBufs("\u0222aa", 8, 1, "utf8"); - testBufs("a\u0234b\u0235c\u0236", 4, 1, "utf8"); - testBufs("a\u0234b\u0235c\u0236", 12, 1, "utf8"); - expect(Buffer.allocUnsafe(1).fill(0).fill("\u0222")[0]).toBe(0xc8); -}); - -test("BINARY encoding", () => { - testBufs("abc", "binary"); - testBufs("\u0222aa", "binary"); - testBufs("a\u0234b\u0235c\u0236", "binary"); - testBufs("abc", 4, "binary"); - testBufs("abc", 5, "binary"); - testBufs("abc", SIZE, "binary"); - testBufs("\u0222aa", 2, "binary"); - testBufs("\u0222aa", 8, "binary"); - testBufs("a\u0234b\u0235c\u0236", 4, "binary"); - testBufs("a\u0234b\u0235c\u0236", 12, "binary"); - testBufs("abc", 4, 1, "binary"); - testBufs("abc", 5, 1, "binary"); - testBufs("\u0222aa", 8, 1, "binary"); - testBufs("a\u0234b\u0235c\u0236", 4, 1, "binary"); - testBufs("a\u0234b\u0235c\u0236", 12, 1, "binary"); -}); - -test("LATIN1 encoding", () => { - testBufs("abc", "latin1"); - testBufs("\u0222aa", "latin1"); - testBufs("a\u0234b\u0235c\u0236", "latin1"); - testBufs("abc", 4, "latin1"); - testBufs("abc", 5, "latin1"); - testBufs("abc", SIZE, "latin1"); - testBufs("\u0222aa", 2, "latin1"); - testBufs("\u0222aa", 8, "latin1"); - testBufs("a\u0234b\u0235c\u0236", 4, "latin1"); - testBufs("a\u0234b\u0235c\u0236", 12, "latin1"); - testBufs("abc", 4, 1, "latin1"); - testBufs("abc", 5, 1, "latin1"); - testBufs("\u0222aa", 8, 1, "latin1"); - testBufs("a\u0234b\u0235c\u0236", 4, 1, "latin1"); - testBufs("a\u0234b\u0235c\u0236", 12, 1, "latin1"); -}); - -test("UCS2 encoding", () => { - testBufs("abc", "ucs2"); - testBufs("\u0222aa", "ucs2"); - testBufs("a\u0234b\u0235c\u0236", "ucs2"); - testBufs("abc", 4, "ucs2"); - testBufs("abc", SIZE, "ucs2"); - testBufs("\u0222aa", 2, "ucs2"); - testBufs("\u0222aa", 8, "ucs2"); - testBufs("a\u0234b\u0235c\u0236", 4, "ucs2"); - testBufs("a\u0234b\u0235c\u0236", 12, "ucs2"); - testBufs("abc", 4, 1, "ucs2"); - testBufs("abc", 5, 1, "ucs2"); - testBufs("\u0222aa", 8, 1, "ucs2"); - testBufs("a\u0234b\u0235c\u0236", 4, 1, "ucs2"); - testBufs("a\u0234b\u0235c\u0236", 12, 1, "ucs2"); - expect(Buffer.allocUnsafe(1).fill("\u0222", "ucs2")[0]).toBe(0x22); -}); - -test("HEX encoding", () => { - testBufs("616263", "hex"); - testBufs("c8a26161", "hex"); - testBufs("61c8b462c8b563c8b6", "hex"); - testBufs("616263", 4, "hex"); - testBufs("616263", 5, "hex"); - testBufs("616263", SIZE, "hex"); - testBufs("c8a26161", 2, "hex"); - testBufs("c8a26161", 8, "hex"); - testBufs("61c8b462c8b563c8b6", 4, "hex"); - testBufs("61c8b462c8b563c8b6", 12, "hex"); - testBufs("616263", 4, 1, "hex"); - testBufs("616263", 5, 1, "hex"); - testBufs("c8a26161", 8, 1, "hex"); - testBufs("61c8b462c8b563c8b6", 4, 1, "hex"); - testBufs("61c8b462c8b563c8b6", 12, 1, "hex"); -}); - -test("Invalid HEX encoding", () => { - expect(() => { - const buf = Buffer.allocUnsafe(SIZE); - buf.fill("yKJh", "hex"); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_VALUE", - name: "TypeError", - }), - ); - - expect(() => { - const buf = Buffer.allocUnsafe(SIZE); - buf.fill("\u0222", "hex"); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_VALUE", - name: "TypeError", - }), - ); -}); - -test("BASE64 encoding", () => { - testBufs("YWJj", "base64"); - testBufs("yKJhYQ==", "base64"); - testBufs("Yci0Ysi1Y8i2", "base64"); - testBufs("YWJj", 4, "base64"); - testBufs("YWJj", SIZE, "base64"); - testBufs("yKJhYQ==", 2, "base64"); - testBufs("yKJhYQ==", 8, "base64"); - testBufs("Yci0Ysi1Y8i2", 4, "base64"); - testBufs("Yci0Ysi1Y8i2", 12, "base64"); - testBufs("YWJj", 4, 1, "base64"); - testBufs("YWJj", 5, 1, "base64"); - testBufs("yKJhYQ==", 8, 1, "base64"); - testBufs("Yci0Ysi1Y8i2", 4, 1, "base64"); - testBufs("Yci0Ysi1Y8i2", 12, 1, "base64"); -}); - -test("BASE64URL encoding", () => { - testBufs("YWJj", "base64url"); - testBufs("yKJhYQ", "base64url"); - testBufs("Yci0Ysi1Y8i2", "base64url"); - testBufs("YWJj", 4, "base64url"); - testBufs("YWJj", SIZE, "base64url"); - testBufs("yKJhYQ", 2, "base64url"); - testBufs("yKJhYQ", 8, "base64url"); - testBufs("Yci0Ysi1Y8i2", 4, "base64url"); - testBufs("Yci0Ysi1Y8i2", 12, "base64url"); - testBufs("YWJj", 4, 1, "base64url"); - testBufs("YWJj", 5, 1, "base64url"); - testBufs("yKJhYQ", 8, 1, "base64url"); - testBufs("Yci0Ysi1Y8i2", 4, 1, "base64url"); - testBufs("Yci0Ysi1Y8i2", 12, 1, "base64url"); -}); - -test("Buffer fill", () => { - function deepStrictEqualValues(buf, arr) { - for (const [index, value] of buf.entries()) { - expect(value).toBe(arr[index]); - } - } - - const buf2Fill = Buffer.allocUnsafe(1).fill(2); - deepStrictEqualValues(genBuffer(4, [buf2Fill]), [2, 2, 2, 2]); - deepStrictEqualValues(genBuffer(4, [buf2Fill, 1]), [0, 2, 2, 2]); - deepStrictEqualValues(genBuffer(4, [buf2Fill, 1, 3]), [0, 2, 2, 0]); - deepStrictEqualValues(genBuffer(4, [buf2Fill, 1, 1]), [0, 0, 0, 0]); - const hexBufFill = Buffer.allocUnsafe(2).fill(0).fill("0102", "hex"); - deepStrictEqualValues(genBuffer(4, [hexBufFill]), [1, 2, 1, 2]); - deepStrictEqualValues(genBuffer(4, [hexBufFill, 1]), [0, 1, 2, 1]); - deepStrictEqualValues(genBuffer(4, [hexBufFill, 1, 3]), [0, 1, 2, 0]); - deepStrictEqualValues(genBuffer(4, [hexBufFill, 1, 1]), [0, 0, 0, 0]); -}); - -test("Check exceptions", () => { - [ - [0, -1], - [0, 0, buf1.length + 1], - ["", -1], - ["", 0, buf1.length + 1], - ["", 1, -1], - ].forEach(args => { - expect(() => buf1.fill(...args)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - }), - ); - }); - - expect(() => buf1.fill("a", 0, buf1.length, "node rocks!")).toThrow( - expect.objectContaining({ - code: "ERR_UNKNOWN_ENCODING", - name: "TypeError", - message: "Unknown encoding: node rocks!", - }), - ); - - [ - ["a", 0, 0, NaN], - ["a", 0, 0, false], - ].forEach(args => { - expect(() => buf1.fill(...args)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.stringContaining('The "encoding" argument must be of type string'), - }), - ); - }); - - expect(() => buf1.fill("a", 0, 0, "foo")).toThrow( - expect.objectContaining({ - code: "ERR_UNKNOWN_ENCODING", - name: "TypeError", - message: "Unknown encoding: foo", - }), - ); -}); - -test("Out of range errors", () => { - expect(() => Buffer.allocUnsafe(8).fill("a", -1)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - }), - ); - expect(() => Buffer.allocUnsafe(8).fill("a", 0, 9)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - }), - ); -}); - -test("Empty fill", () => { - Buffer.allocUnsafe(8).fill(""); - Buffer.alloc(8, ""); -}); - -test("Buffer allocation and fill", () => { - const buf = Buffer.alloc(64, 10); - for (let i = 0; i < buf.length; i++) expect(buf[i]).toBe(10); - - buf.fill(11, 0, buf.length >> 1); - for (let i = 0; i < buf.length >> 1; i++) expect(buf[i]).toBe(11); - for (let i = (buf.length >> 1) + 1; i < buf.length; i++) expect(buf[i]).toBe(10); - - buf.fill("h"); - for (let i = 0; i < buf.length; i++) expect(buf[i]).toBe("h".charCodeAt(0)); - - buf.fill(0); - for (let i = 0; i < buf.length; i++) expect(buf[i]).toBe(0); - - buf.fill(null); - for (let i = 0; i < buf.length; i++) expect(buf[i]).toBe(0); - - buf.fill(1, 16, 32); - for (let i = 0; i < 16; i++) expect(buf[i]).toBe(0); - for (let i = 16; i < 32; i++) expect(buf[i]).toBe(1); - for (let i = 32; i < buf.length; i++) expect(buf[i]).toBe(0); -}); - -test("Buffer fill with string", () => { - const buf = Buffer.alloc(10, "abc"); - expect(buf.toString()).toBe("abcabcabca"); - buf.fill("է"); - expect(buf.toString()).toBe("էէէէէ"); -}); - -test("Buffer fill with invalid end", () => { - expect(() => { - const end = { - [Symbol.toPrimitive]() { - return 1; - }, - }; - Buffer.alloc(1).fill(Buffer.alloc(1), 0, end); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.stringContaining('The "end" argument must be of type number. Received'), - }), - ); -}); - -test.todo("Buffer fill with invalid length", () => { - expect(() => { - const buf = Buffer.from("w00t"); - Object.defineProperty(buf, "length", { - value: 1337, - enumerable: true, - }); - buf.fill(""); - }).toThrow( - expect.objectContaining({ - code: "ERR_BUFFER_OUT_OF_BOUNDS", - name: "RangeError", - message: "Attempt to access memory outside buffer bounds", - }), - ); -}); - -test("Buffer fill with utf16le encoding", () => { - expect(Buffer.allocUnsafeSlow(16).fill("ab", "utf16le")).toEqual( - Buffer.from("61006200610062006100620061006200", "hex"), - ); - - expect(Buffer.allocUnsafeSlow(15).fill("ab", "utf16le")).toEqual( - Buffer.from("610062006100620061006200610062", "hex"), - ); - - expect(Buffer.allocUnsafeSlow(16).fill("ab", "utf16le")).toEqual( - Buffer.from("61006200610062006100620061006200", "hex"), - ); - expect(Buffer.allocUnsafeSlow(16).fill("a", "utf16le")).toEqual( - Buffer.from("61006100610061006100610061006100", "hex"), - ); - - expect(Buffer.allocUnsafeSlow(16).fill("a", "utf16le").toString("utf16le")).toBe("a".repeat(8)); - expect(Buffer.allocUnsafeSlow(16).fill("a", "latin1").toString("latin1")).toBe("a".repeat(16)); - expect(Buffer.allocUnsafeSlow(16).fill("a", "utf8").toString("utf8")).toBe("a".repeat(16)); - - expect(Buffer.allocUnsafeSlow(16).fill("Љ", "utf16le").toString("utf16le")).toBe("Љ".repeat(8)); - expect(Buffer.allocUnsafeSlow(16).fill("Љ", "latin1").toString("latin1")).toBe("\t".repeat(16)); - expect(Buffer.allocUnsafeSlow(16).fill("Љ", "utf8").toString("utf8")).toBe("Љ".repeat(8)); -}); - -test("Buffer fill with invalid hex encoding", () => { - expect(() => { - const buf = Buffer.from("a".repeat(1000)); - buf.fill("This is not correctly encoded", "hex"); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_VALUE", - name: "TypeError", - }), - ); -}); - -test("Buffer fill with empty values", () => { - const bufEmptyString = Buffer.alloc(5, ""); - expect(bufEmptyString.toString()).toBe("\x00\x00\x00\x00\x00"); - - const bufEmptyArray = Buffer.alloc(5, []); - expect(bufEmptyArray.toString()).toBe("\x00\x00\x00\x00\x00"); - - const bufEmptyBuffer = Buffer.alloc(5, Buffer.alloc(5)); - expect(bufEmptyBuffer.toString()).toBe("\x00\x00\x00\x00\x00"); - - const bufZero = Buffer.alloc(5, 0); - expect(bufZero.toString()).toBe("\x00\x00\x00\x00\x00"); -}); - -//<#END_FILE: test-buffer-fill.js diff --git a/test/js/node/test/parallel/buffer-from.test.js b/test/js/node/test/parallel/buffer-from.test.js deleted file mode 100644 index 0d089d4e8c..0000000000 --- a/test/js/node/test/parallel/buffer-from.test.js +++ /dev/null @@ -1,168 +0,0 @@ -//#FILE: test-buffer-from.js -//#SHA1: fdbb08fe98b94d1566ade587f17bb970130e1edd -//----------------- -"use strict"; - -const { runInNewContext } = require("vm"); - -const checkString = "test"; - -const check = Buffer.from(checkString); - -class MyString extends String { - constructor() { - super(checkString); - } -} - -class MyPrimitive { - [Symbol.toPrimitive]() { - return checkString; - } -} - -class MyBadPrimitive { - [Symbol.toPrimitive]() { - return 1; - } -} - -test("Buffer.from with various string-like inputs", () => { - expect(Buffer.from(new String(checkString))).toStrictEqual(check); - expect(Buffer.from(new MyString())).toStrictEqual(check); - expect(Buffer.from(new MyPrimitive())).toStrictEqual(check); - // expect(Buffer.from(runInNewContext("new String(checkString)", { checkString }))).toStrictEqual(check); //TODO: -}); - -describe("Buffer.from with invalid inputs", () => { - const invalidInputs = [ - {}, - new Boolean(true), - { - valueOf() { - return null; - }, - }, - { - valueOf() { - return undefined; - }, - }, - { valueOf: null }, - { __proto__: null }, - new Number(true), - new MyBadPrimitive(), - Symbol(), - 5n, - (one, two, three) => {}, - undefined, - null, - ]; - - for (const input of invalidInputs) { - test(`${Bun.inspect(input)}`, () => { - expect(() => Buffer.from(input)).toThrow( - expect.objectContaining({ - // code: "ERR_INVALID_ARG_TYPE", //TODO: - name: "TypeError", - message: expect.any(String), - }), - ); - expect(() => Buffer.from(input, "hex")).toThrow( - expect.objectContaining({ - // code: "ERR_INVALID_ARG_TYPE", //TODO: - name: "TypeError", - message: expect.any(String), - }), - ); - }); - } -}); - -test("Buffer.allocUnsafe and Buffer.from with valid inputs", () => { - expect(() => Buffer.allocUnsafe(10)).not.toThrow(); - expect(() => Buffer.from("deadbeaf", "hex")).not.toThrow(); -}); - -test("Buffer.copyBytesFrom with Uint16Array", () => { - const u16 = new Uint16Array([0xffff]); - const b16 = Buffer.copyBytesFrom(u16); - u16[0] = 0; - expect(b16.length).toBe(2); - expect(b16[0]).toBe(255); - expect(b16[1]).toBe(255); -}); - -test("Buffer.copyBytesFrom with Uint16Array and offset", () => { - const u16 = new Uint16Array([0, 0xffff]); - const b16 = Buffer.copyBytesFrom(u16, 1, 5); - u16[0] = 0xffff; - u16[1] = 0; - expect(b16.length).toBe(2); - expect(b16[0]).toBe(255); - expect(b16[1]).toBe(255); -}); - -test("Buffer.copyBytesFrom with Uint32Array", () => { - const u32 = new Uint32Array([0xffffffff]); - const b32 = Buffer.copyBytesFrom(u32); - u32[0] = 0; - expect(b32.length).toBe(4); - expect(b32[0]).toBe(255); - expect(b32[1]).toBe(255); - expect(b32[2]).toBe(255); - expect(b32[3]).toBe(255); -}); - -test("Buffer.copyBytesFrom with invalid inputs", () => { - expect(() => Buffer.copyBytesFrom()).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - - const invalidInputs = ["", Symbol(), true, false, {}, [], () => {}, 1, 1n, null, undefined]; - invalidInputs.forEach(notTypedArray => { - expect(() => Buffer.copyBytesFrom(notTypedArray)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - }); - - const invalidSecondArgs = ["", Symbol(), true, false, {}, [], () => {}, 1n]; - invalidSecondArgs.forEach(notANumber => { - expect(() => Buffer.copyBytesFrom(new Uint8Array(1), notANumber)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - }); - - const outOfRangeInputs = [-1, NaN, 1.1, -Infinity]; - outOfRangeInputs.forEach(outOfRange => { - expect(() => Buffer.copyBytesFrom(new Uint8Array(1), outOfRange)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - }), - ); - }); - - invalidSecondArgs.forEach(notANumber => { - expect(() => Buffer.copyBytesFrom(new Uint8Array(1), 0, notANumber)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - }); - - outOfRangeInputs.forEach(outOfRange => { - expect(() => Buffer.copyBytesFrom(new Uint8Array(1), 0, outOfRange)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - }), - ); - }); -}); - -//<#END_FILE: test-buffer-from.js diff --git a/test/js/node/test/parallel/buffer-inheritance.test.js b/test/js/node/test/parallel/buffer-inheritance.test.js deleted file mode 100644 index 5939621c9f..0000000000 --- a/test/js/node/test/parallel/buffer-inheritance.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-buffer-inheritance.js -//#SHA1: 01cba7d2cb76cb1d00fa91b3666dc58333b66e1b -//----------------- -"use strict"; - -test("Buffer inheritance", () => { - function T(n) { - const ui8 = new Uint8Array(n); - Object.setPrototypeOf(ui8, T.prototype); - return ui8; - } - Object.setPrototypeOf(T.prototype, Buffer.prototype); - Object.setPrototypeOf(T, Buffer); - - T.prototype.sum = function sum() { - let cntr = 0; - for (let i = 0; i < this.length; i++) cntr += this[i]; - return cntr; - }; - - const vals = [new T(4), T(4)]; - - vals.forEach(function (t) { - expect(t.constructor).toBe(T); - expect(Object.getPrototypeOf(t)).toBe(T.prototype); - expect(Object.getPrototypeOf(Object.getPrototypeOf(t))).toBe(Buffer.prototype); - - t.fill(5); - let cntr = 0; - for (let i = 0; i < t.length; i++) cntr += t[i]; - expect(cntr).toBe(t.length * 5); - - // Check this does not throw - expect(() => t.toString()).not.toThrow(); - }); -}); - -//<#END_FILE: test-buffer-inheritance.js diff --git a/test/js/node/test/parallel/buffer-inspect.test.js b/test/js/node/test/parallel/buffer-inspect.test.js deleted file mode 100644 index d1ba515755..0000000000 --- a/test/js/node/test/parallel/buffer-inspect.test.js +++ /dev/null @@ -1,98 +0,0 @@ -//#FILE: test-buffer-inspect.js -//#SHA1: 8578a4ec2de348a758e5c4dcbaa13a2ee7005451 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const util = require("util"); -const buffer = require("buffer"); - -describe("Buffer inspect", () => { - beforeEach(() => { - buffer.INSPECT_MAX_BYTES = 2; - }); - - afterEach(() => { - buffer.INSPECT_MAX_BYTES = Infinity; - }); - - test("Buffer and SlowBuffer inspection with INSPECT_MAX_BYTES = 2", () => { - const b = Buffer.allocUnsafe(4); - b.fill("1234"); - - const s = buffer.SlowBuffer(4); - s.fill("1234"); - - const expected = "Buffer(4) [Uint8Array] [ 49, 50, ... 2 more items ]"; - - expect(util.inspect(b)).toBe(expected); - expect(util.inspect(s)).toBe(expected); - }); - - test("Buffer and SlowBuffer inspection with 2 bytes", () => { - const b = Buffer.allocUnsafe(2); - b.fill("12"); - - const s = buffer.SlowBuffer(2); - s.fill("12"); - - const expected = "Buffer(2) [Uint8Array] [ 49, 50 ]"; - - expect(util.inspect(b)).toBe(expected); - expect(util.inspect(s)).toBe(expected); - }); - - test("Buffer and SlowBuffer inspection with INSPECT_MAX_BYTES = Infinity", () => { - const b = Buffer.allocUnsafe(2); - b.fill("12"); - - const s = buffer.SlowBuffer(2); - s.fill("12"); - - const expected = "Buffer(2) [Uint8Array] [ 49, 50 ]"; - - buffer.INSPECT_MAX_BYTES = Infinity; - - expect(util.inspect(b)).toBe(expected); - expect(util.inspect(s)).toBe(expected); - }); - - test("Buffer inspection with custom properties", () => { - const b = Buffer.allocUnsafe(2); - b.fill("12"); - b.inspect = undefined; - b.prop = new Uint8Array(0); - - expect(util.inspect(b)).toBe( - "Buffer(2) [Uint8Array] [\n 49,\n 50,\n inspect: undefined,\n prop: Uint8Array(0) []\n]", - ); - }); - - test("Empty Buffer inspection with custom property", () => { - const b = Buffer.alloc(0); - b.prop = 123; - - expect(util.inspect(b)).toBe("Buffer(0) [Uint8Array] [ prop: 123 ]"); - }); -}); - -//<#END_FILE: test-buffer-inspect.js diff --git a/test/js/node/test/parallel/buffer-isascii.test.js b/test/js/node/test/parallel/buffer-isascii.test.js deleted file mode 100644 index a8fde2110a..0000000000 --- a/test/js/node/test/parallel/buffer-isascii.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-buffer-isascii.js -//#SHA1: e49cbd0752feaa8042a90129dfb38610eb002ee6 -//----------------- -"use strict"; - -const { isAscii, Buffer } = require("buffer"); -const { TextEncoder } = require("util"); - -const encoder = new TextEncoder(); - -test("isAscii function", () => { - expect(isAscii(encoder.encode("hello"))).toBe(true); - expect(isAscii(encoder.encode("ğ"))).toBe(false); - expect(isAscii(Buffer.from([]))).toBe(true); -}); - -test("isAscii with invalid inputs", () => { - const invalidInputs = [undefined, "", "hello", false, true, 0, 1, 0n, 1n, Symbol(), () => {}, {}, [], null]; - - invalidInputs.forEach(input => { - expect(() => isAscii(input)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - }); -}); - -test("isAscii with detached array buffer", () => { - const arrayBuffer = new ArrayBuffer(1024); - structuredClone(arrayBuffer, { transfer: [arrayBuffer] }); - - expect(() => isAscii(arrayBuffer)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_STATE", - }), - ); -}); - -//<#END_FILE: test-buffer-isascii.js diff --git a/test/js/node/test/parallel/buffer-isencoding.test.js b/test/js/node/test/parallel/buffer-isencoding.test.js deleted file mode 100644 index 010d80ca3a..0000000000 --- a/test/js/node/test/parallel/buffer-isencoding.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-buffer-isencoding.js -//#SHA1: 438625bd1ca2a23aa8716bea5334f3ac07eb040f -//----------------- -"use strict"; - -describe("Buffer.isEncoding", () => { - describe("should return true for valid encodings", () => { - const validEncodings = [ - "hex", - "utf8", - "utf-8", - "ascii", - "latin1", - "binary", - "base64", - "base64url", - "ucs2", - "ucs-2", - "utf16le", - "utf-16le", - ]; - - for (const enc of validEncodings) { - test(`${enc}`, () => { - expect(Buffer.isEncoding(enc)).toBe(true); - }); - } - }); - - describe("should return false for invalid encodings", () => { - const invalidEncodings = ["utf9", "utf-7", "Unicode-FTW", "new gnu gun", false, NaN, {}, Infinity, [], 1, 0, -1]; - - for (const enc of invalidEncodings) { - test(`${enc}`, () => { - expect(Buffer.isEncoding(enc)).toBe(false); - }); - } - }); -}); - -//<#END_FILE: test-buffer-isencoding.js diff --git a/test/js/node/test/parallel/buffer-isutf8.test.js b/test/js/node/test/parallel/buffer-isutf8.test.js deleted file mode 100644 index fa55fb4c5b..0000000000 --- a/test/js/node/test/parallel/buffer-isutf8.test.js +++ /dev/null @@ -1,90 +0,0 @@ -//#FILE: test-buffer-isutf8.js -//#SHA1: 47438bb1ade853e71f1266144d24188ebe75e714 -//----------------- -"use strict"; - -const { isUtf8, Buffer } = require("buffer"); -const { TextEncoder } = require("util"); - -const encoder = new TextEncoder(); - -test("isUtf8 validation", () => { - expect(isUtf8(encoder.encode("hello"))).toBe(true); - expect(isUtf8(encoder.encode("ğ"))).toBe(true); - expect(isUtf8(Buffer.from([]))).toBe(true); -}); - -// Taken from test/fixtures/wpt/encoding/textdecoder-fatal.any.js -test("invalid UTF-8 sequences", () => { - const invalidSequences = [ - [0xff], // 'invalid code' - [0xc0], // 'ends early' - [0xe0], // 'ends early 2' - [0xc0, 0x00], // 'invalid trail' - [0xc0, 0xc0], // 'invalid trail 2' - [0xe0, 0x00], // 'invalid trail 3' - [0xe0, 0xc0], // 'invalid trail 4' - [0xe0, 0x80, 0x00], // 'invalid trail 5' - [0xe0, 0x80, 0xc0], // 'invalid trail 6' - [0xfc, 0x80, 0x80, 0x80, 0x80, 0x80], // '> 0x10FFFF' - [0xfe, 0x80, 0x80, 0x80, 0x80, 0x80], // 'obsolete lead byte' - - // Overlong encodings - [0xc0, 0x80], // 'overlong U+0000 - 2 bytes' - [0xe0, 0x80, 0x80], // 'overlong U+0000 - 3 bytes' - [0xf0, 0x80, 0x80, 0x80], // 'overlong U+0000 - 4 bytes' - [0xf8, 0x80, 0x80, 0x80, 0x80], // 'overlong U+0000 - 5 bytes' - [0xfc, 0x80, 0x80, 0x80, 0x80, 0x80], // 'overlong U+0000 - 6 bytes' - - [0xc1, 0xbf], // 'overlong U+007F - 2 bytes' - [0xe0, 0x81, 0xbf], // 'overlong U+007F - 3 bytes' - [0xf0, 0x80, 0x81, 0xbf], // 'overlong U+007F - 4 bytes' - [0xf8, 0x80, 0x80, 0x81, 0xbf], // 'overlong U+007F - 5 bytes' - [0xfc, 0x80, 0x80, 0x80, 0x81, 0xbf], // 'overlong U+007F - 6 bytes' - - [0xe0, 0x9f, 0xbf], // 'overlong U+07FF - 3 bytes' - [0xf0, 0x80, 0x9f, 0xbf], // 'overlong U+07FF - 4 bytes' - [0xf8, 0x80, 0x80, 0x9f, 0xbf], // 'overlong U+07FF - 5 bytes' - [0xfc, 0x80, 0x80, 0x80, 0x9f, 0xbf], // 'overlong U+07FF - 6 bytes' - - [0xf0, 0x8f, 0xbf, 0xbf], // 'overlong U+FFFF - 4 bytes' - [0xf8, 0x80, 0x8f, 0xbf, 0xbf], // 'overlong U+FFFF - 5 bytes' - [0xfc, 0x80, 0x80, 0x8f, 0xbf, 0xbf], // 'overlong U+FFFF - 6 bytes' - - [0xf8, 0x84, 0x8f, 0xbf, 0xbf], // 'overlong U+10FFFF - 5 bytes' - [0xfc, 0x80, 0x84, 0x8f, 0xbf, 0xbf], // 'overlong U+10FFFF - 6 bytes' - - // UTF-16 surrogates encoded as code points in UTF-8 - [0xed, 0xa0, 0x80], // 'lead surrogate' - [0xed, 0xb0, 0x80], // 'trail surrogate' - [0xed, 0xa0, 0x80, 0xed, 0xb0, 0x80], // 'surrogate pair' - ]; - - invalidSequences.forEach(input => { - expect(isUtf8(Buffer.from(input))).toBe(false); - }); -}); - -test("invalid input types", () => { - const invalidInputs = [null, undefined, "hello", true, false]; - - invalidInputs.forEach(input => { - expect(() => isUtf8(input)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - }); -}); - -test("detached array buffer", () => { - const arrayBuffer = new ArrayBuffer(1024); - structuredClone(arrayBuffer, { transfer: [arrayBuffer] }); - expect(() => isUtf8(arrayBuffer)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_STATE", - }), - ); -}); - -//<#END_FILE: test-buffer-isutf8.js diff --git a/test/js/node/test/parallel/buffer-iterator.test.js b/test/js/node/test/parallel/buffer-iterator.test.js deleted file mode 100644 index ca9cacb93d..0000000000 --- a/test/js/node/test/parallel/buffer-iterator.test.js +++ /dev/null @@ -1,72 +0,0 @@ -//#FILE: test-buffer-iterator.js -//#SHA1: cd9bcdf671dc11d86bd194aa3e7500041f03eb4c -//----------------- -"use strict"; - -// Buffers should be iterable -test("Buffers are iterable", () => { - const buffer = Buffer.from([1, 2, 3, 4, 5]); - const arr = []; - - for (const b of buffer) { - arr.push(b); - } - - expect(arr).toEqual([1, 2, 3, 4, 5]); -}); - -// Buffer iterators should be iterable -test("Buffer iterators are iterable", () => { - const buffer = Buffer.from([1, 2, 3, 4, 5]); - const arr = []; - - for (const b of buffer[Symbol.iterator]()) { - arr.push(b); - } - - expect(arr).toEqual([1, 2, 3, 4, 5]); -}); - -// buffer#values() should return iterator for values -test("buffer.values() returns iterator for values", () => { - const buffer = Buffer.from([1, 2, 3, 4, 5]); - const arr = []; - - for (const b of buffer.values()) { - arr.push(b); - } - - expect(arr).toEqual([1, 2, 3, 4, 5]); -}); - -// buffer#keys() should return iterator for keys -test("buffer.keys() returns iterator for keys", () => { - const buffer = Buffer.from([1, 2, 3, 4, 5]); - const arr = []; - - for (const b of buffer.keys()) { - arr.push(b); - } - - expect(arr).toEqual([0, 1, 2, 3, 4]); -}); - -// buffer#entries() should return iterator for entries -test("buffer.entries() returns iterator for entries", () => { - const buffer = Buffer.from([1, 2, 3, 4, 5]); - const arr = []; - - for (const b of buffer.entries()) { - arr.push(b); - } - - expect(arr).toEqual([ - [0, 1], - [1, 2], - [2, 3], - [3, 4], - [4, 5], - ]); -}); - -//<#END_FILE: test-buffer-iterator.js diff --git a/test/js/node/test/parallel/buffer-new.test.js b/test/js/node/test/parallel/buffer-new.test.js deleted file mode 100644 index 7f85579624..0000000000 --- a/test/js/node/test/parallel/buffer-new.test.js +++ /dev/null @@ -1,14 +0,0 @@ -//#FILE: test-buffer-new.js -//#SHA1: 56270fc6342f4ac15433cce1e1b1252ac4dcbb98 -//----------------- -"use strict"; - -test("Buffer constructor with invalid arguments", () => { - expect(() => new Buffer(42, "utf8")).toThrow({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: `The "string" argument must be of type string. Received 42`, - }); -}); - -//<#END_FILE: test-buffer-new.js diff --git a/test/js/node/test/parallel/buffer-no-negative-allocation.test.js b/test/js/node/test/parallel/buffer-no-negative-allocation.test.js deleted file mode 100644 index 2158402336..0000000000 --- a/test/js/node/test/parallel/buffer-no-negative-allocation.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-buffer-no-negative-allocation.js -//#SHA1: c7f13ec857490bc5d1ffbf8da3fff19049c421f8 -//----------------- -"use strict"; - -const { SlowBuffer } = require("buffer"); - -// Test that negative Buffer length inputs throw errors. - -const msg = expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), -}); - -test("Buffer constructor throws on negative or NaN length", () => { - expect(() => Buffer(-Buffer.poolSize)).toThrow(msg); - expect(() => Buffer(-100)).toThrow(msg); - expect(() => Buffer(-1)).toThrow(msg); - expect(() => Buffer(NaN)).toThrow(msg); -}); - -test("Buffer.alloc throws on negative or NaN length", () => { - expect(() => Buffer.alloc(-Buffer.poolSize)).toThrow(msg); - expect(() => Buffer.alloc(-100)).toThrow(msg); - expect(() => Buffer.alloc(-1)).toThrow(msg); - expect(() => Buffer.alloc(NaN)).toThrow(msg); -}); - -test("Buffer.allocUnsafe throws on negative or NaN length", () => { - expect(() => Buffer.allocUnsafe(-Buffer.poolSize)).toThrow(msg); - expect(() => Buffer.allocUnsafe(-100)).toThrow(msg); - expect(() => Buffer.allocUnsafe(-1)).toThrow(msg); - expect(() => Buffer.allocUnsafe(NaN)).toThrow(msg); -}); - -test("Buffer.allocUnsafeSlow throws on negative or NaN length", () => { - expect(() => Buffer.allocUnsafeSlow(-Buffer.poolSize)).toThrow(msg); - expect(() => Buffer.allocUnsafeSlow(-100)).toThrow(msg); - expect(() => Buffer.allocUnsafeSlow(-1)).toThrow(msg); - expect(() => Buffer.allocUnsafeSlow(NaN)).toThrow(msg); -}); - -test("SlowBuffer throws on negative or NaN length", () => { - expect(() => SlowBuffer(-Buffer.poolSize)).toThrow(msg); - expect(() => SlowBuffer(-100)).toThrow(msg); - expect(() => SlowBuffer(-1)).toThrow(msg); - expect(() => SlowBuffer(NaN)).toThrow(msg); -}); - -//<#END_FILE: test-buffer-no-negative-allocation.js diff --git a/test/js/node/test/parallel/buffer-nopendingdep-map.test.js b/test/js/node/test/parallel/buffer-nopendingdep-map.test.js deleted file mode 100644 index 5fc9d4efad..0000000000 --- a/test/js/node/test/parallel/buffer-nopendingdep-map.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-buffer-nopendingdep-map.js -//#SHA1: 908b5747ec3c5873c180b66b6e50221fd29169e3 -//----------------- -// Flags: --no-warnings --pending-deprecation -"use strict"; - -test("Buffer methods should not emit deprecation warnings with --pending-deprecation", () => { - const warningListener = jest.fn(); - process.on("warning", warningListener); - - // With the --pending-deprecation flag, the deprecation warning for - // new Buffer() should not be emitted when Uint8Array methods are called. - - Buffer.from("abc").map(i => i); - Buffer.from("abc").filter(i => i); - Buffer.from("abc").slice(1, 2); - - expect(warningListener).not.toHaveBeenCalled(); - - process.removeListener("warning", warningListener); -}); - -//<#END_FILE: test-buffer-nopendingdep-map.js diff --git a/test/js/node/test/parallel/buffer-of-no-deprecation.test.js b/test/js/node/test/parallel/buffer-of-no-deprecation.test.js deleted file mode 100644 index 53c31f103c..0000000000 --- a/test/js/node/test/parallel/buffer-of-no-deprecation.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-buffer-of-no-deprecation.js -//#SHA1: 7c233f8a82411a5d1c293daecef6494d02d7dabf -//----------------- -"use strict"; - -test("Buffer.of() should not emit deprecation warning", () => { - const warningListener = jest.fn(); - process.on("warning", warningListener); - - Buffer.of(0, 1); - - expect(warningListener).not.toHaveBeenCalled(); - - // Clean up the listener - process.removeListener("warning", warningListener); -}); - -//<#END_FILE: test-buffer-of-no-deprecation.js diff --git a/test/js/node/test/parallel/buffer-over-max-length.test.js b/test/js/node/test/parallel/buffer-over-max-length.test.js deleted file mode 100644 index 5ba6d6af4e..0000000000 --- a/test/js/node/test/parallel/buffer-over-max-length.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-buffer-over-max-length.js -//#SHA1: 797cb237a889a5f09d34b2554a46eb4c545f885e -//----------------- -"use strict"; - -const buffer = require("buffer"); -const SlowBuffer = buffer.SlowBuffer; - -const kMaxLength = buffer.kMaxLength; -const bufferMaxSizeMsg = expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.stringContaining(`The value of "size" is out of range.`), -}); - -test("Buffer creation with over max length", () => { - expect(() => Buffer(kMaxLength + 1)).toThrow(bufferMaxSizeMsg); - expect(() => SlowBuffer(kMaxLength + 1)).toThrow(bufferMaxSizeMsg); - expect(() => Buffer.alloc(kMaxLength + 1)).toThrow(bufferMaxSizeMsg); - expect(() => Buffer.allocUnsafe(kMaxLength + 1)).toThrow(bufferMaxSizeMsg); - expect(() => Buffer.allocUnsafeSlow(kMaxLength + 1)).toThrow(bufferMaxSizeMsg); -}); - -//<#END_FILE: test-buffer-over-max-length.js diff --git a/test/js/node/test/parallel/buffer-parent-property.test.js b/test/js/node/test/parallel/buffer-parent-property.test.js deleted file mode 100644 index ebf02d3652..0000000000 --- a/test/js/node/test/parallel/buffer-parent-property.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-buffer-parent-property.js -//#SHA1: 1496dde41464d188eecd053b64a320c71f62bd7d -//----------------- -"use strict"; - -// Fix for https://github.com/nodejs/node/issues/8266 -// -// Zero length Buffer objects should expose the `buffer` property of the -// TypedArrays, via the `parent` property. - -test("Buffer parent property", () => { - // If the length of the buffer object is zero - expect(Buffer.alloc(0).parent).toBeInstanceOf(ArrayBuffer); - - // If the length of the buffer object is equal to the underlying ArrayBuffer - expect(Buffer.alloc(Buffer.poolSize).parent).toBeInstanceOf(ArrayBuffer); - - // Same as the previous test, but with user created buffer - const arrayBuffer = new ArrayBuffer(0); - expect(Buffer.from(arrayBuffer).parent).toBe(arrayBuffer); - expect(Buffer.from(arrayBuffer).buffer).toBe(arrayBuffer); - expect(Buffer.from(arrayBuffer).parent).toBe(arrayBuffer); - expect(Buffer.from(arrayBuffer).buffer).toBe(arrayBuffer); -}); - -//<#END_FILE: test-buffer-parent-property.js diff --git a/test/js/node/test/parallel/buffer-prototype-inspect.test.js b/test/js/node/test/parallel/buffer-prototype-inspect.test.js deleted file mode 100644 index f6bb9a8915..0000000000 --- a/test/js/node/test/parallel/buffer-prototype-inspect.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-buffer-prototype-inspect.js -//#SHA1: 3809d957d94134495a61469120087c12580fa3f3 -//----------------- -"use strict"; - -// lib/buffer.js defines Buffer.prototype.inspect() to override how buffers are -// presented by util.inspect(). - -const util = require("util"); -const buffer = require("buffer"); -buffer.INSPECT_MAX_BYTES = 50; - -test("Buffer.prototype.inspect() for non-empty buffer", () => { - const buf = Buffer.from("fhqwhgads"); - expect(util.inspect(buf)).toBe("Buffer(9) [Uint8Array] [\n 102, 104, 113, 119,\n 104, 103, 97, 100,\n 115\n]"); -}); - -test("Buffer.prototype.inspect() for empty buffer", () => { - const buf = Buffer.from(""); - expect(util.inspect(buf)).toBe("Buffer(0) [Uint8Array] []"); -}); - -test("Buffer.prototype.inspect() for large buffer", () => { - const buf = Buffer.from("x".repeat(51)); - expect(util.inspect(buf)).toBe( - `Buffer(51) [Uint8Array] [\n` + - ` 120, 120, 120, 120, 120, 120, 120, 120, 120,\n` + - ` 120, 120, 120, 120, 120, 120, 120, 120, 120,\n` + - ` 120, 120, 120, 120, 120, 120, 120, 120, 120,\n` + - ` 120, 120, 120, 120, 120, 120, 120, 120, 120,\n` + - ` 120, 120, 120, 120, 120, 120, 120, 120, 120,\n` + - ` 120, 120, 120, 120, 120,\n` + - ` ... 1 more item\n` + - `]`, - ); -}); - -//<#END_FILE: test-buffer-prototype-inspect.js diff --git a/test/js/node/test/parallel/buffer-safe-unsafe.test.js b/test/js/node/test/parallel/buffer-safe-unsafe.test.js deleted file mode 100644 index 84e1db9096..0000000000 --- a/test/js/node/test/parallel/buffer-safe-unsafe.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-buffer-safe-unsafe.js -//#SHA1: 87831e463ab52a79fca3ac2e28eec57666ea9e5e -//----------------- -"use strict"; - -test("Buffer safe and unsafe allocations", () => { - const safe = Buffer.alloc(10); - - function isZeroFilled(buf) { - for (let n = 0; n < buf.length; n++) if (buf[n] !== 0) return false; - return true; - } - - expect(isZeroFilled(safe)).toBe(true); - - // Test that unsafe allocations doesn't affect subsequent safe allocations - Buffer.allocUnsafe(10); - expect(isZeroFilled(new Float64Array(10))).toBe(true); - - new Buffer(10); - expect(isZeroFilled(new Float64Array(10))).toBe(true); - - Buffer.allocUnsafe(10); - expect(isZeroFilled(Buffer.alloc(10))).toBe(true); -}); - -//<#END_FILE: test-buffer-safe-unsafe.js diff --git a/test/js/node/test/parallel/buffer-set-inspect-max-bytes.test.js b/test/js/node/test/parallel/buffer-set-inspect-max-bytes.test.js deleted file mode 100644 index 306fa0f81b..0000000000 --- a/test/js/node/test/parallel/buffer-set-inspect-max-bytes.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-buffer-set-inspect-max-bytes.js -//#SHA1: de73b2a241585e1cf17a057d21cdbabbadf963bb -//----------------- -"use strict"; - -const buffer = require("buffer"); - -describe("buffer.INSPECT_MAX_BYTES", () => { - const rangeErrorObjs = [NaN, -1]; - const typeErrorObj = "and even this"; - - test.each(rangeErrorObjs)("throws RangeError for invalid value: %p", obj => { - expect(() => { - buffer.INSPECT_MAX_BYTES = obj; - }).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - }); - - test("throws TypeError for invalid type", () => { - expect(() => { - buffer.INSPECT_MAX_BYTES = typeErrorObj; - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-buffer-set-inspect-max-bytes.js diff --git a/test/js/node/test/parallel/buffer-slice.test.js b/test/js/node/test/parallel/buffer-slice.test.js deleted file mode 100644 index 5fde7bdf62..0000000000 --- a/test/js/node/test/parallel/buffer-slice.test.js +++ /dev/null @@ -1,114 +0,0 @@ -//#FILE: test-buffer-slice.js -//#SHA1: 1f7289de3a6dd5167f3659cd5119e6489b059d43 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("Buffer slice operations", () => { - expect(Buffer.from("hello", "utf8").slice(0, 0).length).toBe(0); - expect(Buffer("hello", "utf8").slice(0, 0).length).toBe(0); - - const buf = Buffer.from("0123456789", "utf8"); - const expectedSameBufs = [ - [buf.slice(-10, 10), Buffer.from("0123456789", "utf8")], - [buf.slice(-20, 10), Buffer.from("0123456789", "utf8")], - [buf.slice(-20, -10), Buffer.from("", "utf8")], - [buf.slice(), Buffer.from("0123456789", "utf8")], - [buf.slice(0), Buffer.from("0123456789", "utf8")], - [buf.slice(0, 0), Buffer.from("", "utf8")], - [buf.slice(undefined), Buffer.from("0123456789", "utf8")], - [buf.slice("foobar"), Buffer.from("0123456789", "utf8")], - [buf.slice(undefined, undefined), Buffer.from("0123456789", "utf8")], - [buf.slice(2), Buffer.from("23456789", "utf8")], - [buf.slice(5), Buffer.from("56789", "utf8")], - [buf.slice(10), Buffer.from("", "utf8")], - [buf.slice(5, 8), Buffer.from("567", "utf8")], - [buf.slice(8, -1), Buffer.from("8", "utf8")], - [buf.slice(-10), Buffer.from("0123456789", "utf8")], - [buf.slice(0, -9), Buffer.from("0", "utf8")], - [buf.slice(0, -10), Buffer.from("", "utf8")], - [buf.slice(0, -1), Buffer.from("012345678", "utf8")], - [buf.slice(2, -2), Buffer.from("234567", "utf8")], - [buf.slice(0, 65536), Buffer.from("0123456789", "utf8")], - [buf.slice(65536, 0), Buffer.from("", "utf8")], - [buf.slice(-5, -8), Buffer.from("", "utf8")], - [buf.slice(-5, -3), Buffer.from("56", "utf8")], - [buf.slice(-10, 10), Buffer.from("0123456789", "utf8")], - [buf.slice("0", "1"), Buffer.from("0", "utf8")], - [buf.slice("-5", "10"), Buffer.from("56789", "utf8")], - [buf.slice("-10", "10"), Buffer.from("0123456789", "utf8")], - [buf.slice("-10", "-5"), Buffer.from("01234", "utf8")], - [buf.slice("-10", "-0"), Buffer.from("", "utf8")], - [buf.slice("111"), Buffer.from("", "utf8")], - [buf.slice("0", "-111"), Buffer.from("", "utf8")], - ]; - - for (let i = 0, s = buf.toString(); i < buf.length; ++i) { - expectedSameBufs.push( - [buf.slice(i), Buffer.from(s.slice(i))], - [buf.slice(0, i), Buffer.from(s.slice(0, i))], - [buf.slice(-i), Buffer.from(s.slice(-i))], - [buf.slice(0, -i), Buffer.from(s.slice(0, -i))], - ); - } - - for (const [buf1, buf2] of expectedSameBufs) { - expect(Buffer.compare(buf1, buf2)).toBe(0); - } - - const utf16Buf = Buffer.from("0123456789", "utf16le"); - expect(utf16Buf.slice(0, 6)).toStrictEqual(Buffer.from("012", "utf16le")); - - // Try to slice a zero length Buffer. - // See https://github.com/joyent/node/issues/5881 - expect(Buffer.alloc(0).slice(0, 1).length).toBe(0); - - // Single argument slice - expect(Buffer.from("abcde", "utf8").slice(1).toString("utf8")).toBe("bcde"); - - // slice(0,0).length === 0 - expect(Buffer.from("hello", "utf8").slice(0, 0).length).toBe(0); - - // Regression tests for https://github.com/nodejs/node/issues/9096 - const regressionBuf = Buffer.from("abcd", "utf8"); - expect(regressionBuf.slice(regressionBuf.length / 3).toString("utf8")).toBe("bcd"); - expect(regressionBuf.slice(regressionBuf.length / 3, regressionBuf.length).toString()).toBe("bcd"); - - const largeSliceBuf = Buffer.from("abcdefg", "utf8"); - expect(largeSliceBuf.slice(-(-1 >>> 0) - 1).toString("utf8")).toBe(largeSliceBuf.toString("utf8")); - - const floatSliceBuf = Buffer.from("abc", "utf8"); - expect(floatSliceBuf.slice(-0.5).toString("utf8")).toBe(floatSliceBuf.toString("utf8")); - - const complexBuf = Buffer.from([ - 1, 29, 0, 0, 1, 143, 216, 162, 92, 254, 248, 63, 0, 0, 0, 18, 184, 6, 0, 175, 29, 0, 8, 11, 1, 0, 0, - ]); - const chunk1 = Buffer.from([1, 29, 0, 0, 1, 143, 216, 162, 92, 254, 248, 63, 0]); - const chunk2 = Buffer.from([0, 0, 18, 184, 6, 0, 175, 29, 0, 8, 11, 1, 0, 0]); - const middle = complexBuf.length / 2; - - expect(complexBuf.slice(0, middle)).toStrictEqual(chunk1); - expect(complexBuf.slice(middle)).toStrictEqual(chunk2); -}); - -//<#END_FILE: test-buffer-slice.js diff --git a/test/js/node/test/parallel/buffer-slow.test.js b/test/js/node/test/parallel/buffer-slow.test.js deleted file mode 100644 index 85f35f68e6..0000000000 --- a/test/js/node/test/parallel/buffer-slow.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-buffer-slow.js -//#SHA1: fadf639fe26752f00488a41a29f1977f95fc1c79 -//----------------- -"use strict"; - -const buffer = require("buffer"); -const SlowBuffer = buffer.SlowBuffer; - -const ones = [1, 1, 1, 1]; - -test("SlowBuffer should create a Buffer", () => { - let sb = SlowBuffer(4); - expect(sb).toBeInstanceOf(Buffer); - expect(sb.length).toBe(4); - sb.fill(1); - for (const [key, value] of sb.entries()) { - expect(value).toBe(ones[key]); - } - - // underlying ArrayBuffer should have the same length - expect(sb.buffer.byteLength).toBe(4); -}); - -test("SlowBuffer should work without new", () => { - let sb = SlowBuffer(4); - expect(sb).toBeInstanceOf(Buffer); - expect(sb.length).toBe(4); - sb.fill(1); - for (const [key, value] of sb.entries()) { - expect(value).toBe(ones[key]); - } -}); - -test("SlowBuffer should work with edge cases", () => { - expect(SlowBuffer(0).length).toBe(0); -}); - -test("SlowBuffer should throw with invalid length type", () => { - const bufferInvalidTypeMsg = expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }); - - expect(() => SlowBuffer()).toThrow(bufferInvalidTypeMsg); - expect(() => SlowBuffer({})).toThrow(bufferInvalidTypeMsg); - expect(() => SlowBuffer("6")).toThrow(bufferInvalidTypeMsg); - expect(() => SlowBuffer(true)).toThrow(bufferInvalidTypeMsg); -}); - -test("SlowBuffer should throw with invalid length value", () => { - const bufferMaxSizeMsg = expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }); - - expect(() => SlowBuffer(NaN)).toThrow(bufferMaxSizeMsg); - expect(() => SlowBuffer(Infinity)).toThrow(bufferMaxSizeMsg); - expect(() => SlowBuffer(-1)).toThrow(bufferMaxSizeMsg); - expect(() => SlowBuffer(buffer.kMaxLength + 1)).toThrow(bufferMaxSizeMsg); -}); - -//<#END_FILE: test-buffer-slow.js diff --git a/test/js/node/test/parallel/buffer-swap.test.js b/test/js/node/test/parallel/buffer-swap.test.js deleted file mode 100644 index 89eef58dce..0000000000 --- a/test/js/node/test/parallel/buffer-swap.test.js +++ /dev/null @@ -1,153 +0,0 @@ -//#FILE: test-buffer-swap.js -//#SHA1: 589e4ee82ab5f00e1cffdd4d326e21cc2f06b065 -//----------------- -"use strict"; - -describe("Buffer swap operations", () => { - test("Test buffers small enough to use the JS implementation", () => { - const buf = Buffer.from([ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - ]); - - expect(buf.swap16()).toBe(buf); - expect(buf).toEqual( - Buffer.from([0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x08, 0x07, 0x0a, 0x09, 0x0c, 0x0b, 0x0e, 0x0d, 0x10, 0x0f]), - ); - buf.swap16(); // restore - - expect(buf.swap32()).toBe(buf); - expect(buf).toEqual( - Buffer.from([0x04, 0x03, 0x02, 0x01, 0x08, 0x07, 0x06, 0x05, 0x0c, 0x0b, 0x0a, 0x09, 0x10, 0x0f, 0x0e, 0x0d]), - ); - buf.swap32(); // restore - - expect(buf.swap64()).toBe(buf); - expect(buf).toEqual( - Buffer.from([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09]), - ); - }); - - test("Operates in-place", () => { - const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]); - buf.slice(1, 5).swap32(); - expect(buf).toEqual(Buffer.from([0x1, 0x5, 0x4, 0x3, 0x2, 0x6, 0x7])); - buf.slice(1, 5).swap16(); - expect(buf).toEqual(Buffer.from([0x1, 0x4, 0x5, 0x2, 0x3, 0x6, 0x7])); - - // Length assertions - const re16 = /Buffer size must be a multiple of 16-bits/; - const re32 = /Buffer size must be a multiple of 32-bits/; - const re64 = /Buffer size must be a multiple of 64-bits/; - - expect(() => Buffer.from(buf).swap16()).toThrow(expect.objectContaining({ message: expect.any(String) })); - expect(() => Buffer.alloc(1025).swap16()).toThrow(expect.objectContaining({ message: expect.any(String) })); - expect(() => Buffer.from(buf).swap32()).toThrow(expect.objectContaining({ message: expect.any(String) })); - expect(() => buf.slice(1, 3).swap32()).toThrow(expect.objectContaining({ message: expect.any(String) })); - expect(() => Buffer.alloc(1025).swap32()).toThrow(expect.objectContaining({ message: expect.any(String) })); - expect(() => buf.slice(1, 3).swap64()).toThrow(expect.objectContaining({ message: expect.any(String) })); - expect(() => Buffer.alloc(1025).swap64()).toThrow(expect.objectContaining({ message: expect.any(String) })); - }); - - test("Swap64 on a slice", () => { - const buf = Buffer.from([ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, - 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - ]); - - buf.slice(2, 18).swap64(); - - expect(buf).toEqual( - Buffer.from([ - 0x01, 0x02, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, - 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - ]), - ); - }); - - test("Force use of native code (Buffer size above threshold limit for js impl)", () => { - const bufData = new Uint32Array(256).fill(0x04030201); - const buf = Buffer.from(bufData.buffer, bufData.byteOffset); - const otherBufData = new Uint32Array(256).fill(0x03040102); - const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); - buf.swap16(); - expect(buf).toEqual(otherBuf); - }); - - test("Force use of native code for swap32", () => { - const bufData = new Uint32Array(256).fill(0x04030201); - const buf = Buffer.from(bufData.buffer); - const otherBufData = new Uint32Array(256).fill(0x01020304); - const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); - buf.swap32(); - expect(buf).toEqual(otherBuf); - }); - - test("Force use of native code for swap64", () => { - const bufData = new Uint8Array(256 * 8); - const otherBufData = new Uint8Array(256 * 8); - for (let i = 0; i < bufData.length; i++) { - bufData[i] = i % 8; - otherBufData[otherBufData.length - i - 1] = i % 8; - } - const buf = Buffer.from(bufData.buffer, bufData.byteOffset); - const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); - buf.swap64(); - expect(buf).toEqual(otherBuf); - }); - - test("Test native code with buffers that are not memory-aligned (swap16)", () => { - const bufData = new Uint8Array(256 * 8); - const otherBufData = new Uint8Array(256 * 8 - 2); - for (let i = 0; i < bufData.length; i++) { - bufData[i] = i % 2; - } - for (let i = 1; i < otherBufData.length; i++) { - otherBufData[otherBufData.length - i] = (i + 1) % 2; - } - const buf = Buffer.from(bufData.buffer, bufData.byteOffset); - // 0|1 0|1 0|1... - const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); - // 0|0 1|0 1|0... - - buf.slice(1, buf.length - 1).swap16(); - expect(buf.slice(0, otherBuf.length)).toEqual(otherBuf); - }); - - test("Test native code with buffers that are not memory-aligned (swap32)", () => { - const bufData = new Uint8Array(256 * 8); - const otherBufData = new Uint8Array(256 * 8 - 4); - for (let i = 0; i < bufData.length; i++) { - bufData[i] = i % 4; - } - for (let i = 1; i < otherBufData.length; i++) { - otherBufData[otherBufData.length - i] = (i + 1) % 4; - } - const buf = Buffer.from(bufData.buffer, bufData.byteOffset); - // 0|1 2 3 0|1 2 3... - const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); - // 0|0 3 2 1|0 3 2... - - buf.slice(1, buf.length - 3).swap32(); - expect(buf.slice(0, otherBuf.length)).toEqual(otherBuf); - }); - - test("Test native code with buffers that are not memory-aligned (swap64)", () => { - const bufData = new Uint8Array(256 * 8); - const otherBufData = new Uint8Array(256 * 8 - 8); - for (let i = 0; i < bufData.length; i++) { - bufData[i] = i % 8; - } - for (let i = 1; i < otherBufData.length; i++) { - otherBufData[otherBufData.length - i] = (i + 1) % 8; - } - const buf = Buffer.from(bufData.buffer, bufData.byteOffset); - // 0|1 2 3 4 5 6 7 0|1 2 3 4... - const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); - // 0|0 7 6 5 4 3 2 1|0 7 6 5... - - buf.slice(1, buf.length - 7).swap64(); - expect(buf.slice(0, otherBuf.length)).toEqual(otherBuf); - }); -}); - -//<#END_FILE: test-buffer-swap.js diff --git a/test/js/node/test/parallel/buffer-tostring-range.test.js b/test/js/node/test/parallel/buffer-tostring-range.test.js deleted file mode 100644 index a1e72ba714..0000000000 --- a/test/js/node/test/parallel/buffer-tostring-range.test.js +++ /dev/null @@ -1,115 +0,0 @@ -//#FILE: test-buffer-tostring-range.js -//#SHA1: 2bc09c70e84191e47ae345cc3178f28458b10ec2 -//----------------- -"use strict"; - -const rangeBuffer = Buffer.from("abc"); - -test("Buffer.toString range behavior", () => { - // If start >= buffer's length, empty string will be returned - expect(rangeBuffer.toString("ascii", 3)).toBe(""); - expect(rangeBuffer.toString("ascii", +Infinity)).toBe(""); - expect(rangeBuffer.toString("ascii", 3.14, 3)).toBe(""); - expect(rangeBuffer.toString("ascii", "Infinity", 3)).toBe(""); - - // If end <= 0, empty string will be returned - expect(rangeBuffer.toString("ascii", 1, 0)).toBe(""); - expect(rangeBuffer.toString("ascii", 1, -1.2)).toBe(""); - expect(rangeBuffer.toString("ascii", 1, -100)).toBe(""); - expect(rangeBuffer.toString("ascii", 1, -Infinity)).toBe(""); - - // If start < 0, start will be taken as zero - expect(rangeBuffer.toString("ascii", -1, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", -1.99, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", -Infinity, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", "-1", 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", "-1.99", 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", "-Infinity", 3)).toBe("abc"); - - // If start is an invalid integer, start will be taken as zero - expect(rangeBuffer.toString("ascii", "node.js", 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", {}, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", [], 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", NaN, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", null, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", undefined, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", false, 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", "", 3)).toBe("abc"); - - // But, if start is an integer when coerced, then it will be coerced and used. - expect(rangeBuffer.toString("ascii", "-1", 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", "1", 3)).toBe("bc"); - expect(rangeBuffer.toString("ascii", "-Infinity", 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", "3", 3)).toBe(""); - expect(rangeBuffer.toString("ascii", Number(3), 3)).toBe(""); - expect(rangeBuffer.toString("ascii", "3.14", 3)).toBe(""); - expect(rangeBuffer.toString("ascii", "1.99", 3)).toBe("bc"); - expect(rangeBuffer.toString("ascii", "-1.99", 3)).toBe("abc"); - expect(rangeBuffer.toString("ascii", 1.99, 3)).toBe("bc"); - expect(rangeBuffer.toString("ascii", true, 3)).toBe("bc"); - - // If end > buffer's length, end will be taken as buffer's length - expect(rangeBuffer.toString("ascii", 0, 5)).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, 6.99)).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, Infinity)).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, "5")).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, "6.99")).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, "Infinity")).toBe("abc"); - - // If end is an invalid integer, end will be taken as buffer's length - expect(rangeBuffer.toString("ascii", 0, "node.js")).toBe(""); - expect(rangeBuffer.toString("ascii", 0, {})).toBe(""); - expect(rangeBuffer.toString("ascii", 0, NaN)).toBe(""); - expect(rangeBuffer.toString("ascii", 0, undefined)).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0)).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, null)).toBe(""); - expect(rangeBuffer.toString("ascii", 0, [])).toBe(""); - expect(rangeBuffer.toString("ascii", 0, false)).toBe(""); - expect(rangeBuffer.toString("ascii", 0, "")).toBe(""); - - // But, if end is an integer when coerced, then it will be coerced and used. - expect(rangeBuffer.toString("ascii", 0, "-1")).toBe(""); - expect(rangeBuffer.toString("ascii", 0, "1")).toBe("a"); - expect(rangeBuffer.toString("ascii", 0, "-Infinity")).toBe(""); - expect(rangeBuffer.toString("ascii", 0, "3")).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, Number(3))).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, "3.14")).toBe("abc"); - expect(rangeBuffer.toString("ascii", 0, "1.99")).toBe("a"); - expect(rangeBuffer.toString("ascii", 0, "-1.99")).toBe(""); - expect(rangeBuffer.toString("ascii", 0, 1.99)).toBe("a"); - expect(rangeBuffer.toString("ascii", 0, true)).toBe("a"); -}); - -test("toString() with an object as an encoding", () => { - expect( - rangeBuffer.toString({ - toString: function () { - return "ascii"; - }, - }), - ).toBe("abc"); -}); - -test("toString() with 0 and null as the encoding", () => { - expect(() => { - rangeBuffer.toString(0, 1, 2); - }).toThrow( - expect.objectContaining({ - code: "ERR_UNKNOWN_ENCODING", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => { - rangeBuffer.toString(null, 1, 2); - }).toThrow( - expect.objectContaining({ - code: "ERR_UNKNOWN_ENCODING", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-buffer-tostring-range.js diff --git a/test/js/node/test/parallel/buffer-tostring-rangeerror.test.js b/test/js/node/test/parallel/buffer-tostring-rangeerror.test.js deleted file mode 100644 index 0e88759c45..0000000000 --- a/test/js/node/test/parallel/buffer-tostring-rangeerror.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-buffer-tostring-rangeerror.js -//#SHA1: c5bd04a7b4f3b7ecfb3898262dd73da29a9ad162 -//----------------- -"use strict"; - -// This test ensures that Node.js throws an Error when trying to convert a -// large buffer into a string. -// Regression test for https://github.com/nodejs/node/issues/649. - -const { - SlowBuffer, - constants: { MAX_STRING_LENGTH }, -} = require("buffer"); - -const len = MAX_STRING_LENGTH + 1; -const errorMatcher = expect.objectContaining({ - code: "ERR_STRING_TOO_LONG", - name: "Error", - message: `Cannot create a string longer than 2147483647 characters`, -}); - -test("Buffer toString with large buffer throws RangeError", () => { - expect(() => Buffer(len).toString("utf8")).toThrow(errorMatcher); - expect(() => SlowBuffer(len).toString("utf8")).toThrow(errorMatcher); - expect(() => Buffer.alloc(len).toString("utf8")).toThrow(errorMatcher); - expect(() => Buffer.allocUnsafe(len).toString("utf8")).toThrow(errorMatcher); - expect(() => Buffer.allocUnsafeSlow(len).toString("utf8")).toThrow(errorMatcher); -}); - -//<#END_FILE: test-buffer-tostring-rangeerror.js diff --git a/test/js/node/test/parallel/buffer-tostring.test.js b/test/js/node/test/parallel/buffer-tostring.test.js deleted file mode 100644 index eb48074506..0000000000 --- a/test/js/node/test/parallel/buffer-tostring.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-buffer-tostring.js -//#SHA1: 0a6490b6dd4c343c01828d1c4ff81b745b6b1552 -//----------------- -"use strict"; - -// utf8, ucs2, ascii, latin1, utf16le -const encodings = ["utf8", "utf-8", "ucs2", "ucs-2", "ascii", "latin1", "binary", "utf16le", "utf-16le"]; - -test("Buffer.from().toString() with various encodings", () => { - encodings - .reduce((es, e) => es.concat(e, e.toUpperCase()), []) - .forEach(encoding => { - expect(Buffer.from("foo", encoding).toString(encoding)).toBe("foo"); - }); -}); - -test("Buffer.from().toString() with base64 encoding", () => { - ["base64", "BASE64"].forEach(encoding => { - expect(Buffer.from("Zm9v", encoding).toString(encoding)).toBe("Zm9v"); - }); -}); - -test("Buffer.from().toString() with hex encoding", () => { - ["hex", "HEX"].forEach(encoding => { - expect(Buffer.from("666f6f", encoding).toString(encoding)).toBe("666f6f"); - }); -}); - -test("Buffer.from().toString() with invalid encodings", () => { - for (let i = 1; i < 10; i++) { - const encoding = String(i).repeat(i); - expect(Buffer.isEncoding(encoding)).toBe(false); - expect(() => Buffer.from("foo").toString(encoding)).toThrow( - expect.objectContaining({ - code: "ERR_UNKNOWN_ENCODING", - name: "TypeError", - message: expect.any(String), - }), - ); - } -}); - -//<#END_FILE: test-buffer-tostring.js diff --git a/test/js/node/test/parallel/buffer-write.test.js b/test/js/node/test/parallel/buffer-write.test.js deleted file mode 100644 index ceb7123d5f..0000000000 --- a/test/js/node/test/parallel/buffer-write.test.js +++ /dev/null @@ -1,119 +0,0 @@ -//#FILE: test-buffer-write.js -//#SHA1: 9577e31a533888b164b0abf4ebececbe04e381cb -//----------------- -"use strict"; - -[-1, 10].forEach(offset => { - test(`Buffer.alloc(9).write('foo', ${offset}) throws RangeError`, () => { - expect(() => Buffer.alloc(9).write("foo", offset)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - }); -}); - -const resultMap = new Map([ - ["utf8", Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], - ["ucs2", Buffer.from([102, 0, 111, 0, 111, 0, 0, 0, 0])], - ["ascii", Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], - ["latin1", Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], - ["binary", Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], - ["utf16le", Buffer.from([102, 0, 111, 0, 111, 0, 0, 0, 0])], - ["base64", Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], - ["base64url", Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], - ["hex", Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], -]); - -// utf8, ucs2, ascii, latin1, utf16le -const encodings = ["utf8", "utf-8", "ucs2", "ucs-2", "ascii", "latin1", "binary", "utf16le", "utf-16le"]; - -encodings - .reduce((es, e) => es.concat(e, e.toUpperCase()), []) - .forEach(encoding => { - test(`Buffer.write with encoding ${encoding}`, () => { - const buf = Buffer.alloc(9); - const len = Buffer.byteLength("foo", encoding); - expect(buf.write("foo", 0, len, encoding)).toBe(len); - - if (encoding.includes("-")) encoding = encoding.replace("-", ""); - - expect(buf).toEqual(resultMap.get(encoding.toLowerCase())); - }); - }); - -// base64 -["base64", "BASE64", "base64url", "BASE64URL"].forEach(encoding => { - test(`Buffer.write with encoding ${encoding}`, () => { - const buf = Buffer.alloc(9); - const len = Buffer.byteLength("Zm9v", encoding); - - expect(buf.write("Zm9v", 0, len, encoding)).toBe(len); - expect(buf).toEqual(resultMap.get(encoding.toLowerCase())); - }); -}); - -// hex -["hex", "HEX"].forEach(encoding => { - test(`Buffer.write with encoding ${encoding}`, () => { - const buf = Buffer.alloc(9); - const len = Buffer.byteLength("666f6f", encoding); - - expect(buf.write("666f6f", 0, len, encoding)).toBe(len); - expect(buf).toEqual(resultMap.get(encoding.toLowerCase())); - }); -}); - -// Invalid encodings -for (let i = 1; i < 10; i++) { - const encoding = String(i).repeat(i); - - test(`Invalid encoding ${encoding}`, () => { - expect(Buffer.isEncoding(encoding)).toBe(false); - expect(() => Buffer.alloc(9).write("foo", encoding)).toThrow( - expect.objectContaining({ - code: "ERR_UNKNOWN_ENCODING", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -} - -// UCS-2 overflow CVE-2018-12115 -for (let i = 1; i < 4; i++) { - test(`UCS-2 overflow test ${i}`, () => { - // Allocate two Buffers sequentially off the pool. Run more than once in case - // we hit the end of the pool and don't get sequential allocations - const x = Buffer.allocUnsafe(4).fill(0); - const y = Buffer.allocUnsafe(4).fill(1); - // Should not write anything, pos 3 doesn't have enough room for a 16-bit char - expect(x.write("ыыыыыы", 3, "ucs2")).toBe(0); - // CVE-2018-12115 experienced via buffer overrun to next block in the pool - expect(Buffer.compare(y, Buffer.alloc(4, 1))).toBe(0); - }); -} - -test("Should not write any data when there is no space for 16-bit chars", () => { - const z = Buffer.alloc(4, 0); - expect(z.write("\u0001", 3, "ucs2")).toBe(0); - expect(Buffer.compare(z, Buffer.alloc(4, 0))).toBe(0); - // Make sure longer strings are written up to the buffer end. - expect(z.write("abcd", 2)).toBe(2); - expect([...z]).toEqual([0, 0, 0x61, 0x62]); -}); - -test("Large overrun should not corrupt the process", () => { - expect(Buffer.alloc(4).write("ыыыыыы".repeat(100), 3, "utf16le")).toBe(0); -}); - -test(".write() does not affect the byte after the written-to slice of the Buffer", () => { - // Refs: https://github.com/nodejs/node/issues/26422 - const buf = Buffer.alloc(8); - expect(buf.write("ыы", 1, "utf16le")).toBe(4); - expect([...buf]).toEqual([0, 0x4b, 0x04, 0x4b, 0x04, 0, 0, 0]); -}); - -//<#END_FILE: test-buffer-write.js diff --git a/test/js/node/test/parallel/buffer-zero-fill-reset.test.js b/test/js/node/test/parallel/buffer-zero-fill-reset.test.js deleted file mode 100644 index d4055fb35a..0000000000 --- a/test/js/node/test/parallel/buffer-zero-fill-reset.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-buffer-zero-fill-reset.js -//#SHA1: 2278dd06a2e113e875c1f0f46580c0fcc4fc5254 -//----------------- -"use strict"; - -test("Uint8Array is zero-filled after Buffer.alloc(0)", () => { - function testUint8Array(ui) { - const length = ui.length; - for (let i = 0; i < length; i++) if (ui[i] !== 0) return false; - return true; - } - - for (let i = 0; i < 100; i++) { - Buffer.alloc(0); - const ui = new Uint8Array(65); - expect(testUint8Array(ui)).toBe(true); - } -}); - -//<#END_FILE: test-buffer-zero-fill-reset.js diff --git a/test/js/node/test/parallel/buffer-zero-fill.test.js b/test/js/node/test/parallel/buffer-zero-fill.test.js deleted file mode 100644 index 003b4ffe08..0000000000 --- a/test/js/node/test/parallel/buffer-zero-fill.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-buffer-zero-fill.js -//#SHA1: b710e0c9405c90f7526cf0efabd4c61ede37b1f7 -//----------------- -"use strict"; - -// Tests deprecated Buffer API on purpose -test("Buffer zero-fill", () => { - const buf1 = Buffer(100); - const buf2 = new Buffer(100); - - for (let n = 0; n < buf1.length; n++) { - expect(buf1[n]).toBe(0); - } - - for (let n = 0; n < buf2.length; n++) { - expect(buf2[n]).toBe(0); - } -}); - -//<#END_FILE: test-buffer-zero-fill.js diff --git a/test/js/node/test/parallel/child-process-exec-encoding.test.js b/test/js/node/test/parallel/child-process-exec-encoding.test.js deleted file mode 100644 index 46ae2d5276..0000000000 --- a/test/js/node/test/parallel/child-process-exec-encoding.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-child-process-exec-encoding.js -//#SHA1: 3ad6878126678aa6ad2c38a43264e5684dae6a72 -//----------------- -"use strict"; - -const stdoutData = "foo"; -const stderrData = "bar"; - -if (process.argv[2] === "child") { - // The following console calls are part of the test. - console.log(stdoutData); - console.error(stderrData); -} else { - const cp = require("child_process"); - const expectedStdout = `${stdoutData}\n`; - const expectedStderr = `${stderrData}\n`; - - function run(options) { - const cmd = `"${process.execPath}" "${__filename}" child`; - - return new Promise((resolve, reject) => { - cp.exec(cmd, options, (error, stdout, stderr) => { - if (error) { - reject(error); - } else { - resolve({ stdout, stderr }); - } - }); - }); - } - - test("Test default encoding, which should be utf8", async () => { - const { stdout, stderr } = await run({}); - expect(typeof stdout).toBe("string"); - expect(typeof stderr).toBe("string"); - expect(stdout).toBe(expectedStdout); - expect(stderr).toBe(expectedStderr); - }); - - test("Test explicit utf8 encoding", async () => { - const { stdout, stderr } = await run({ encoding: "utf8" }); - expect(typeof stdout).toBe("string"); - expect(typeof stderr).toBe("string"); - expect(stdout).toBe(expectedStdout); - expect(stderr).toBe(expectedStderr); - }); - - test("Test cases that result in buffer encodings", async () => { - const encodings = [undefined, null, "buffer", "invalid"]; - - for (const encoding of encodings) { - const { stdout, stderr } = await run({ encoding }); - expect(stdout).toBeInstanceOf(Buffer); - expect(stderr).toBeInstanceOf(Buffer); - expect(stdout.toString()).toBe(expectedStdout); - expect(stderr.toString()).toBe(expectedStderr); - } - }); -} - -//<#END_FILE: test-child-process-exec-encoding.js diff --git a/test/js/node/test/parallel/child-process-exec-env.test.js b/test/js/node/test/parallel/child-process-exec-env.test.js deleted file mode 100644 index 1649fad830..0000000000 --- a/test/js/node/test/parallel/child-process-exec-env.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-child-process-exec-env.js -//#SHA1: cba9b5f7a9d1ab7cd674bde00c1c4184470e4899 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { isWindows } = require("../common"); -const { exec } = require("child_process"); -const util = require("util"); - -const execPromise = util.promisify(exec); - -test("exec with custom environment", async () => { - let command, options; - - if (!isWindows) { - command = "/usr/bin/env"; - options = { env: { HELLO: "WORLD" } }; - } else { - command = "set"; - options = { env: { ...process.env, HELLO: "WORLD" } }; - } - - const { stdout, stderr } = await execPromise(command, options); - - expect(stdout).not.toBe(""); - expect(stdout).toContain("HELLO=WORLD"); - expect(stderr).toBe(""); -}); - -//<#END_FILE: test-child-process-exec-env.js diff --git a/test/js/node/test/parallel/child-process-exec-kill-throws.test.js b/test/js/node/test/parallel/child-process-exec-kill-throws.test.js deleted file mode 100644 index b0b8482f24..0000000000 --- a/test/js/node/test/parallel/child-process-exec-kill-throws.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-child-process-exec-kill-throws.js -//#SHA1: 968879ddf3244351dea40c681343ea8defc02a0b -//----------------- -"use strict"; - -const cp = require("child_process"); - -if (process.argv[2] === "child") { - // Since maxBuffer is 0, this should trigger an error. - console.log("foo"); -} else { - const originalKill = cp.ChildProcess.prototype.kill; - - beforeEach(() => { - // Monkey patch ChildProcess#kill() to kill the process and then throw. - cp.ChildProcess.prototype.kill = function () { - originalKill.apply(this, arguments); - throw new Error("mock error"); - }; - }); - - afterEach(() => { - // Restore original kill method - cp.ChildProcess.prototype.kill = originalKill; - }); - - test("ChildProcess#kill() throws error", done => { - const cmd = `"${process.execPath}" "${__filename}" child`; - const options = { maxBuffer: 0, killSignal: "SIGKILL" }; - - const child = cp.exec(cmd, options, (err, stdout, stderr) => { - // Verify that if ChildProcess#kill() throws, the error is reported. - expect(err).toEqual( - expect.objectContaining({ - message: "mock error", - }), - ); - expect(stdout).toBe(""); - expect(stderr).toBe(""); - expect(child.killed).toBe(true); - done(); - }); - }); -} - -//<#END_FILE: test-child-process-exec-kill-throws.js diff --git a/test/js/node/test/parallel/child-process-exec-std-encoding.test.js b/test/js/node/test/parallel/child-process-exec-std-encoding.test.js deleted file mode 100644 index 010bc7097e..0000000000 --- a/test/js/node/test/parallel/child-process-exec-std-encoding.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-child-process-exec-std-encoding.js -//#SHA1: fa74583780f5256e46fd7a5ad02aed4b20bb0b76 -//----------------- -"use strict"; - -const cp = require("child_process"); - -const stdoutData = "foo"; -const stderrData = "bar"; -const expectedStdout = `${stdoutData}\n`; -const expectedStderr = `${stderrData}\n`; - -if (process.argv[2] === "child") { - // The following console calls are part of the test. - console.log(stdoutData); - console.error(stderrData); -} else { - test("child process exec with stdout and stderr encoding", done => { - const cmd = `"${process.execPath}" "${__filename}" child`; - const child = cp.exec(cmd, (error, stdout, stderr) => { - expect(error).toBeNull(); - expect(stdout).toBe(expectedStdout); - expect(stderr).toBe(expectedStderr); - done(); - }); - child.stdout.setEncoding("utf-8"); - child.stderr.setEncoding("utf-8"); - }); -} - -//<#END_FILE: test-child-process-exec-std-encoding.js diff --git a/test/js/node/test/parallel/child-process-exec-stdout-stderr-data-string.test.js b/test/js/node/test/parallel/child-process-exec-stdout-stderr-data-string.test.js deleted file mode 100644 index 42a1a555ab..0000000000 --- a/test/js/node/test/parallel/child-process-exec-stdout-stderr-data-string.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-child-process-exec-stdout-stderr-data-string.js -//#SHA1: 342c40f3dbb506150172c2471ae228fd8632b900 -//----------------- -"use strict"; -// Refs: https://github.com/nodejs/node/issues/7342 -const { exec } = require("child_process"); - -const command = process.platform === "win32" ? "dir" : "ls"; - -test("exec stdout data is called at least once", done => { - const child = exec(command); - const onData = jest.fn(); - child.stdout.on("data", onData); - - child.on("close", () => { - expect(onData).toHaveBeenCalled(); - done(); - }); -}); - -test("exec stderr data is called at least once and receives string", done => { - const child = exec("fhqwhgads"); - const onData = jest.fn(data => { - expect(typeof data).toBe("string"); - }); - child.stderr.on("data", onData); - - child.on("close", () => { - expect(onData).toHaveBeenCalled(); - done(); - }); -}); - -//<#END_FILE: test-child-process-exec-stdout-stderr-data-string.js diff --git a/test/js/node/test/parallel/child-process-exec-timeout-kill.test.js b/test/js/node/test/parallel/child-process-exec-timeout-kill.test.js deleted file mode 100644 index e3bc2b8e9b..0000000000 --- a/test/js/node/test/parallel/child-process-exec-timeout-kill.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-child-process-exec-timeout-kill.js -//#SHA1: 01bc25d258b4d8905a2387e9a08b9ceb8c38c141 -//----------------- -"use strict"; - -// Test exec() with both a timeout and a killSignal. - -const cp = require("child_process"); -const path = require("path"); - -const { kExpiringChildRunTime, kExpiringParentTimer } = require("../common/child_process"); - -const logAfterTime = time => { - setTimeout(() => { - console.log(`Logged after ${time}ms`); - }, time); -}; - -if (process.argv[2] === "child") { - logAfterTime(kExpiringChildRunTime); - process.exit(0); -} - -const cmd = `"${process.execPath}" "${__filename}" child`; - -test("exec with timeout and killSignal", done => { - // Test with a different kill signal. - cp.exec( - cmd, - { - timeout: kExpiringParentTimer, - killSignal: "SIGKILL", - }, - (err, stdout, stderr) => { - console.log("[stdout]", stdout.trim()); - console.log("[stderr]", stderr.trim()); - expect(stdout.trim()).toBe(""); - expect(stderr.trim()).toBe(""); - - expect(err?.killed).toBe(true); - expect(err?.code).toBeNull(); - expect(err?.signal).toBe("SIGKILL"); - expect(err?.cmd).toBe(cmd); - done(); - }, - ); -}); - -//<#END_FILE: test-child-process-exec-timeout-kill.js diff --git a/test/js/node/test/parallel/child-process-execfile-promisified-abortcontroller.test.js b/test/js/node/test/parallel/child-process-execfile-promisified-abortcontroller.test.js deleted file mode 100644 index acebc661e9..0000000000 --- a/test/js/node/test/parallel/child-process-execfile-promisified-abortcontroller.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-child-process-execFile-promisified-abortController.js -//#SHA1: 133445acf9aaafea4be11eb7965f222c5827f2f3 -//----------------- -"use strict"; - -const { promisify } = require("util"); -const execFile = require("child_process").execFile; -const fixtures = require("../common/fixtures"); - -const echoFixture = fixtures.path("echo.js"); -const promisified = promisify(execFile); -const invalidArgTypeError = { - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", -}; - -test("Verify that the signal option works properly", async () => { - const ac = new AbortController(); - const signal = ac.signal; - const promise = promisified(process.execPath, [echoFixture, 0], { signal }); - - ac.abort(); - - await expect(promise).rejects.toThrow( - expect.objectContaining({ - name: "AbortError", - message: expect.any(String), - }), - ); -}); - -test("Verify that the signal option works properly when already aborted", async () => { - const signal = AbortSignal.abort(); - - await expect(promisified(process.execPath, [echoFixture, 0], { signal })).rejects.toThrow( - expect.objectContaining({ - name: "AbortError", - message: expect.any(String), - }), - ); -}); - -test("Verify that if something different than Abortcontroller.signal is passed, ERR_INVALID_ARG_TYPE is thrown", () => { - const signal = {}; - expect(() => { - promisified(process.execPath, [echoFixture, 0], { signal }); - }).toThrow(expect.objectContaining(invalidArgTypeError)); -}); - -test("Verify that if a string is passed as signal, ERR_INVALID_ARG_TYPE is thrown", () => { - const signal = "world!"; - expect(() => { - promisified(process.execPath, [echoFixture, 0], { signal }); - }).toThrow(expect.objectContaining(invalidArgTypeError)); -}); - -//<#END_FILE: test-child-process-execFile-promisified-abortController.js diff --git a/test/js/node/test/parallel/child-process-flush-stdio.test.js b/test/js/node/test/parallel/child-process-flush-stdio.test.js deleted file mode 100644 index 53e4419b66..0000000000 --- a/test/js/node/test/parallel/child-process-flush-stdio.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-child-process-flush-stdio.js -//#SHA1: 42d6ab8508587f13b81d2ec70d28fd88efe8fe05 -//----------------- -"use strict"; - -const cp = require("child_process"); - -// Windows' `echo` command is a built-in shell command and not an external -// executable like on *nix -const opts = { shell: process.platform === "win32" }; - -test("spawn echo without arguments", done => { - const p = cp.spawn("echo", [], opts); - - p.on("close", (code, signal) => { - expect(code).toBe(0); - expect(signal).toBeNull(); - done(); - }); - - p.stdout.read(); -}); - -test("spawn echo with argument", done => { - const buffer = []; - const p = cp.spawn("echo", ["123"], opts); - - p.on("close", (code, signal) => { - expect(code).toBe(0); - expect(signal).toBeNull(); - expect(Buffer.concat(buffer).toString().trim()).toBe("123"); - done(); - }); - - p.stdout.on("readable", () => { - let buf; - while ((buf = p.stdout.read()) !== null) buffer.push(buf); - }); -}); - -//<#END_FILE: test-child-process-flush-stdio.js diff --git a/test/js/node/test/parallel/child-process-fork-abort-signal.test.js b/test/js/node/test/parallel/child-process-fork-abort-signal.test.js deleted file mode 100644 index f3c1dcfe65..0000000000 --- a/test/js/node/test/parallel/child-process-fork-abort-signal.test.js +++ /dev/null @@ -1,115 +0,0 @@ -//#FILE: test-child-process-fork-abort-signal.js -//#SHA1: 4805d5dd4e3cb22ffd5a21fd9d92b6ccd6bc73cf -//----------------- -"use strict"; - -const fixtures = require("../common/fixtures"); -const { fork } = require("child_process"); - -test("aborting a forked child_process after calling fork", done => { - const ac = new AbortController(); - const { signal } = ac; - const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { - signal, - }); - cp.on("exit", (code, killSignal) => { - expect(code).toBeNull(); - expect(killSignal).toBe("SIGTERM"); - done(); - }); - cp.on("error", err => { - expect(err.name).toBe("AbortError"); - done(); - }); - process.nextTick(() => ac.abort()); -}); - -test("aborting with custom error", done => { - const ac = new AbortController(); - const { signal } = ac; - const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { - signal, - }); - cp.on("exit", (code, killSignal) => { - expect(code).toBeNull(); - expect(killSignal).toBe("SIGTERM"); - done(); - }); - cp.on("error", err => { - expect(err.name).toBe("AbortError"); - expect(err.cause.name).toBe("Error"); - expect(err.cause.message).toBe("boom"); - done(); - }); - process.nextTick(() => ac.abort(new Error("boom"))); -}); - -test("passing an already aborted signal to a forked child_process", done => { - const signal = AbortSignal.abort(); - const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { - signal, - }); - cp.on("exit", (code, killSignal) => { - expect(code).toBeNull(); - expect(killSignal).toBe("SIGTERM"); - done(); - }); - cp.on("error", err => { - expect(err.name).toBe("AbortError"); - done(); - }); -}); - -test("passing an aborted signal with custom error to a forked child_process", done => { - const signal = AbortSignal.abort(new Error("boom")); - const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { - signal, - }); - cp.on("exit", (code, killSignal) => { - expect(code).toBeNull(); - expect(killSignal).toBe("SIGTERM"); - done(); - }); - cp.on("error", err => { - expect(err.name).toBe("AbortError"); - expect(err.cause.name).toBe("Error"); - expect(err.cause.message).toBe("boom"); - done(); - }); -}); - -test("passing a different kill signal", done => { - const signal = AbortSignal.abort(); - const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { - signal, - killSignal: "SIGKILL", - }); - cp.on("exit", (code, killSignal) => { - expect(code).toBeNull(); - expect(killSignal).toBe("SIGKILL"); - done(); - }); - cp.on("error", err => { - expect(err.name).toBe("AbortError"); - done(); - }); -}); - -test("aborting a cp before close but after exit", done => { - const ac = new AbortController(); - const { signal } = ac; - const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { - signal, - }); - cp.on("exit", () => { - ac.abort(); - done(); - }); - cp.on("error", () => { - done(new Error("Should not have errored")); - }); - - setTimeout(() => cp.kill(), 1); -}); - -//<#END_FILE: test-child-process-fork-abort-signal.js diff --git a/test/js/node/test/parallel/child-process-fork-args.test.js b/test/js/node/test/parallel/child-process-fork-args.test.js deleted file mode 100644 index f2efa98aa5..0000000000 --- a/test/js/node/test/parallel/child-process-fork-args.test.js +++ /dev/null @@ -1,88 +0,0 @@ -//#FILE: test-child-process-fork-args.js -//#SHA1: 172297ab2ed7887ced1b830b8c36d2d6a508deed -//----------------- -"use strict"; -const fixtures = require("../common/fixtures"); -const { fork } = require("child_process"); - -// This test check the arguments of `fork` method -// Refs: https://github.com/nodejs/node/issues/20749 -const expectedEnv = { foo: "bar" }; - -// Ensure that first argument `modulePath` must be provided -// and be of type string -test("fork modulePath argument must be a string", () => { - const invalidModulePath = [0, true, undefined, null, [], {}, () => {}, Symbol("t")]; - invalidModulePath.forEach(modulePath => { - expect(() => fork(modulePath)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringMatching(/^The "modulePath" argument must be of type string/), - }), - ); - }); -}); - -test("fork with valid modulePath", done => { - const cp = fork(fixtures.path("child-process-echo-options.js")); - cp.on("exit", code => { - expect(code).toBe(0); - done(); - }); -}); - -// Ensure that the second argument of `fork` -// and `fork` should parse options -// correctly if args is undefined or null -test("fork second argument validation", () => { - const invalidSecondArgs = [0, true, () => {}, Symbol("t")]; - invalidSecondArgs.forEach(arg => { - expect(() => { - fork(fixtures.path("child-process-echo-options.js"), arg); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); -}); - -test("fork with valid second argument", async () => { - const argsLists = [undefined, null, []]; - - for (const args of argsLists) { - const cp = fork(fixtures.path("child-process-echo-options.js"), args, { - env: { ...process.env, ...expectedEnv }, - }); - - await new Promise(resolve => { - cp.on("message", ({ env }) => { - expect(env.foo).toBe(expectedEnv.foo); - }); - - cp.on("exit", code => { - expect(code).toBe(0); - resolve(); - }); - }); - } -}); - -// Ensure that the third argument should be type of object if provided -test("fork third argument must be an object if provided", () => { - const invalidThirdArgs = [0, true, () => {}, Symbol("t")]; - invalidThirdArgs.forEach(arg => { - expect(() => { - fork(fixtures.path("child-process-echo-options.js"), [], arg); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); -}); - -//<#END_FILE: test-child-process-fork-args.js diff --git a/test/js/node/test/parallel/child-process-fork-close.test.js b/test/js/node/test/parallel/child-process-fork-close.test.js deleted file mode 100644 index d7dd94759a..0000000000 --- a/test/js/node/test/parallel/child-process-fork-close.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-child-process-fork-close.js -//#SHA1: d196e4b4f06991be7c2a48169cb89b815c0f172b -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { fork } = require("child_process"); -const path = require("path"); - -test("child process fork close events", async () => { - const cp = fork(path.resolve(__dirname, "../fixtures/child-process-message-and-exit.js")); - - let gotMessage = false; - let gotExit = false; - let gotClose = false; - - const messagePromise = new Promise(resolve => { - cp.on("message", message => { - expect(gotMessage).toBe(false); - expect(gotClose).toBe(false); - expect(message).toBe("hello"); - gotMessage = true; - resolve(); - }); - }); - - const exitPromise = new Promise(resolve => { - cp.on("exit", () => { - expect(gotExit).toBe(false); - expect(gotClose).toBe(false); - gotExit = true; - resolve(); - }); - }); - - const closePromise = new Promise(resolve => { - cp.on("close", () => { - expect(gotMessage).toBe(true); - expect(gotExit).toBe(true); - expect(gotClose).toBe(false); - gotClose = true; - resolve(); - }); - }); - - await Promise.all([messagePromise, exitPromise, closePromise]); -}); - -//<#END_FILE: test-child-process-fork-close.js diff --git a/test/js/node/test/parallel/child-process-fork-detached.test.js b/test/js/node/test/parallel/child-process-fork-detached.test.js deleted file mode 100644 index 18c3f6a0cb..0000000000 --- a/test/js/node/test/parallel/child-process-fork-detached.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-child-process-fork-detached.js -//#SHA1: 289d7a5f7c5d58ae50a06e7427730c57d78fe39c -//----------------- -"use strict"; - -const { fork } = require("child_process"); -const path = require("path"); - -const fixturesPath = path.resolve(__dirname, "..", "fixtures"); - -test("fork detached child process", done => { - const nonPersistentNode = fork(path.join(fixturesPath, "parent-process-nonpersistent-fork.js"), [], { silent: true }); - - let childId = -1; - - nonPersistentNode.stdout.on("data", data => { - childId = parseInt(data, 10); - nonPersistentNode.kill(); - }); - - nonPersistentNode.on("exit", () => { - expect(childId).not.toBe(-1); - - // Killing the child process should not throw an error - expect(() => process.kill(childId)).not.toThrow(); - - done(); - }); -}); - -//<#END_FILE: test-child-process-fork-detached.js diff --git a/test/js/node/test/parallel/child-process-fork-ref2.test.js b/test/js/node/test/parallel/child-process-fork-ref2.test.js deleted file mode 100644 index ecea4bfd3a..0000000000 --- a/test/js/node/test/parallel/child-process-fork-ref2.test.js +++ /dev/null @@ -1,69 +0,0 @@ -//#FILE: test-child-process-fork-ref2.js -//#SHA1: 6d8a2bec4198f9d9f71ce9f2cc880cfcd1c9d883 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { fork } = require("child_process"); -const { debuglog } = require("util"); -const debug = debuglog("test"); - -const platformTimeout = ms => ms; - -if (process.argv[2] === "child") { - test("child process", () => { - debug("child -> call disconnect"); - process.disconnect(); - - return new Promise(resolve => { - setTimeout(() => { - debug("child -> will this keep it alive?"); - const mockFn = jest.fn(); - process.on("message", mockFn); - expect(mockFn).not.toHaveBeenCalled(); - resolve(); - }, platformTimeout(400)); - }); - }); -} else { - test("parent process", () => { - return new Promise(resolve => { - const child = fork(__filename, ["child"]); - - const disconnectSpy = jest.fn(() => { - debug("parent -> disconnect"); - }); - child.on("disconnect", disconnectSpy); - - const exitSpy = jest.fn(() => { - debug("parent -> exit"); - expect(disconnectSpy).toHaveBeenCalledTimes(1); - resolve(); - }); - child.once("exit", exitSpy); - - expect(exitSpy).toHaveBeenCalledTimes(0); - }); - }); -} - -//<#END_FILE: test-child-process-fork-ref2.js diff --git a/test/js/node/test/parallel/child-process-fork3.test.js b/test/js/node/test/parallel/child-process-fork3.test.js deleted file mode 100644 index ab5558264a..0000000000 --- a/test/js/node/test/parallel/child-process-fork3.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-child-process-fork3.js -//#SHA1: afa7c58bf22c64585426b2b3ec4358a415f4ff47 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const child_process = require("child_process"); -const path = require("path"); - -test("child_process.fork should not hang", () => { - const emptyScriptPath = path.join(__dirname, "fixtures", "empty.js"); - expect(() => { - child_process.fork(emptyScriptPath); - }).not.toThrow(); -}); - -//<#END_FILE: test-child-process-fork3.js diff --git a/test/js/node/test/parallel/child-process-kill.test.js b/test/js/node/test/parallel/child-process-kill.test.js deleted file mode 100644 index 5a9ece5e38..0000000000 --- a/test/js/node/test/parallel/child-process-kill.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-child-process-kill.js -//#SHA1: 308c453d51a13e0e2c640969456c0092ab00967d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { spawn } = require("child_process"); -const isWindows = process.platform === "win32"; - -test("child process kill behavior", async () => { - const cat = spawn(isWindows ? "cmd" : "cat"); - - const stdoutEndPromise = new Promise(resolve => { - cat.stdout.on("end", resolve); - }); - - const stderrDataPromise = new Promise((resolve, reject) => { - cat.stderr.on("data", () => reject(new Error("stderr should not receive data"))); - cat.stderr.on("end", resolve); - }); - - const exitPromise = new Promise(resolve => { - cat.on("exit", (code, signal) => { - expect(code).toBeNull(); - expect(signal).toBe("SIGTERM"); - expect(cat.signalCode).toBe("SIGTERM"); - resolve(); - }); - }); - - expect(cat.signalCode).toBeNull(); - expect(cat.killed).toBe(false); - cat.kill(); - expect(cat.killed).toBe(true); - - await Promise.all([stdoutEndPromise, stderrDataPromise, exitPromise]); -}); - -//<#END_FILE: test-child-process-kill.js diff --git a/test/js/node/test/parallel/child-process-send-type-error.test.js b/test/js/node/test/parallel/child-process-send-type-error.test.js deleted file mode 100644 index cc4908b7da..0000000000 --- a/test/js/node/test/parallel/child-process-send-type-error.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-child-process-send-type-error.js -//#SHA1: 85b82f9c15ca3d5368e22ccd1e7f44672ce2fb0c -//----------------- -"use strict"; - -const cp = require("child_process"); - -function fail(proc, args) { - expect(() => { - proc.send.apply(proc, args); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); -} - -let target = process; - -if (process.argv[2] !== "child") { - test("child process send type error", () => { - target = cp.fork(__filename, ["child"]); - target.on("exit", (code, signal) => { - expect(code).toBe(0); - expect(signal).toBeNull(); - }); - - fail(target, ["msg", null, null]); - fail(target, ["msg", null, ""]); - fail(target, ["msg", null, "foo"]); - fail(target, ["msg", null, 0]); - fail(target, ["msg", null, NaN]); - fail(target, ["msg", null, 1]); - fail(target, ["msg", null, null, jest.fn()]); - }); -} else { - test("process send type error", () => { - fail(target, ["msg", null, null]); - fail(target, ["msg", null, ""]); - fail(target, ["msg", null, "foo"]); - fail(target, ["msg", null, 0]); - fail(target, ["msg", null, NaN]); - fail(target, ["msg", null, 1]); - fail(target, ["msg", null, null, jest.fn()]); - }); -} - -//<#END_FILE: test-child-process-send-type-error.js diff --git a/test/js/node/test/parallel/child-process-set-blocking.test.js b/test/js/node/test/parallel/child-process-set-blocking.test.js deleted file mode 100644 index a8288fc114..0000000000 --- a/test/js/node/test/parallel/child-process-set-blocking.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-child-process-set-blocking.js -//#SHA1: 2855d3d616c7af61fc6b84705cd05515f02bcb47 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { spawn } = require("child_process"); -const os = require("os"); - -const SIZE = 100000; -const python = process.env.PYTHON || (os.platform() === "win32" ? "python" : "python3"); - -test("child process set blocking", done => { - const cp = spawn(python, ["-c", `print(${SIZE} * "C")`], { - stdio: "inherit", - }); - - cp.on("exit", code => { - expect(code).toBe(0); - done(); - }); -}); - -//<#END_FILE: test-child-process-set-blocking.js diff --git a/test/js/node/test/parallel/child-process-spawnsync-env.test.js b/test/js/node/test/parallel/child-process-spawnsync-env.test.js deleted file mode 100644 index e6368f7e38..0000000000 --- a/test/js/node/test/parallel/child-process-spawnsync-env.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-child-process-spawnsync-env.js -//#SHA1: 21ad31214e1261fb3c2636bd98d36946e5be67de -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const cp = require("child_process"); - -if (process.argv[2] === "child") { - console.log(process.env.foo); -} else { - test("spawnSync with custom environment", () => { - const expected = "bar"; - const child = cp.spawnSync(process.execPath, [__filename, "child"], { - env: Object.assign({}, process.env, { foo: expected }), - }); - - expect(child.stdout.toString().trim()).toBe(expected); - }); -} - -//<#END_FILE: test-child-process-spawnsync-env.js diff --git a/test/js/node/test/parallel/child-process-stdio-big-write-end.test.js b/test/js/node/test/parallel/child-process-stdio-big-write-end.test.js deleted file mode 100644 index ff1ea0cc16..0000000000 --- a/test/js/node/test/parallel/child-process-stdio-big-write-end.test.js +++ /dev/null @@ -1,92 +0,0 @@ -//#FILE: test-child-process-stdio-big-write-end.js -//#SHA1: 728a12ebb5484fcc628e82386c3b521ab95e0456 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { spawn } = require("child_process"); -const assert = require("assert"); -const debug = require("util").debuglog("test"); - -let bufsize = 0; - -function runParent() { - return new Promise(resolve => { - const child = spawn(process.execPath, [__filename, "child"]); - let sent = 0; - - let n = ""; - child.stdout.setEncoding("ascii"); - child.stdout.on("data", c => { - n += c; - }); - child.stdout.on("end", () => { - expect(+n).toBe(sent); - debug("ok"); - resolve(); - }); - - // Write until the buffer fills up. - let buf; - do { - bufsize += 1024; - buf = Buffer.alloc(bufsize, "."); - sent += bufsize; - } while (child.stdin.write(buf)); - - // Then write a bunch more times. - for (let i = 0; i < 100; i++) { - const buf = Buffer.alloc(bufsize, "."); - sent += bufsize; - child.stdin.write(buf); - } - - // Now end, before it's all flushed. - child.stdin.end(); - - // now we wait... - }); -} - -function runChild() { - return new Promise(resolve => { - let received = 0; - process.stdin.on("data", c => { - received += c.length; - }); - process.stdin.on("end", () => { - // This console.log is part of the test. - console.log(received); - resolve(); - }); - }); -} - -if (process.argv[2] === "child") { - runChild(); -} else { - test("child process stdio big write end", async () => { - await runParent(); - }); -} - -//<#END_FILE: test-child-process-stdio-big-write-end.js diff --git a/test/js/node/test/parallel/child-process-stdio-overlapped.test.js b/test/js/node/test/parallel/child-process-stdio-overlapped.test.js deleted file mode 100644 index e37232fce3..0000000000 --- a/test/js/node/test/parallel/child-process-stdio-overlapped.test.js +++ /dev/null @@ -1,91 +0,0 @@ -//#FILE: test-child-process-stdio-overlapped.js -//#SHA1: 20ead82f2ea74983af7bead33605fe8f33fccf6d -//----------------- -// Test for "overlapped" stdio option. This test uses the "overlapped-checker" -// helper program which basically a specialized echo program. -// -// The test has two goals: -// -// - Verify that overlapped I/O works on windows. The test program will deadlock -// if stdin doesn't have the FILE_FLAG_OVERLAPPED flag set on startup (see -// test/overlapped-checker/main_win.c for more details). -// - Verify that "overlapped" stdio option works transparently as a pipe (on -// unix/windows) -// -// This is how the test works: -// -// - This script assumes only numeric strings are written to the test program -// stdout. -// - The test program will be spawned with "overlapped" set on stdin and "pipe" -// set on stdout/stderr and at startup writes a number to its stdout -// - When this script receives some data, it will parse the number, add 50 and -// write to the test program's stdin. -// - The test program will then echo the number back to us which will repeat the -// cycle until the number reaches 200, at which point we send the "exit" -// string, which causes the test program to exit. -// - Extra assertion: Every time the test program writes a string to its stdout, -// it will write the number of bytes written to stderr. -// - If overlapped I/O is not setup correctly, this test is going to hang. -"use strict"; -const path = require("path"); -const child_process = require("child_process"); -const fs = require("fs"); - -const exeExtension = process.platform === "win32" ? ".exe" : ""; -const exe = "overlapped-checker" + exeExtension; -const exePath = path.join(path.dirname(process.execPath), exe); - -test("overlapped stdio option", async () => { - if (!fs.existsSync(exePath)) { - console.log(exe + " binary is not available"); - return; - } - - const child = child_process.spawn(exePath, [], { - stdio: ["overlapped", "pipe", "pipe"], - }); - - child.stdin.setEncoding("utf8"); - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - - function writeNext(n) { - child.stdin.write((n + 50).toString()); - } - - child.stdout.on("data", s => { - const n = Number(s); - if (n >= 200) { - child.stdin.write("exit"); - return; - } - writeNext(n); - }); - - let stderr = ""; - child.stderr.on("data", s => { - stderr += s; - }); - - await new Promise(resolve => { - child.stderr.on("end", resolve); - }); - - // This is the sequence of numbers sent to us: - // - 0 (1 byte written) - // - 50 (2 bytes written) - // - 100 (3 bytes written) - // - 150 (3 bytes written) - // - 200 (3 bytes written) - expect(stderr).toBe("12333"); - - const exitPromise = new Promise(resolve => { - child.on("exit", resolve); - }); - - const status = await exitPromise; - // The test program will return the number of writes as status code. - expect(status).toBe(0); -}); - -//<#END_FILE: test-child-process-stdio-overlapped.js diff --git a/test/js/node/test/parallel/cli-eval-event.test.js b/test/js/node/test/parallel/cli-eval-event.test.js deleted file mode 100644 index 152c3da4dc..0000000000 --- a/test/js/node/test/parallel/cli-eval-event.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-cli-eval-event.js -//#SHA1: d64fa25056a9ecf2e87e46560d477b42d3e32909 -//----------------- -"use strict"; - -const { spawn } = require("child_process"); - -test("CLI eval event", () => { - return new Promise(resolve => { - const child = spawn(process.execPath, [ - "-e", - ` - const server = require('net').createServer().listen(0); - server.once('listening', server.close); - `, - ]); - - child.once("exit", (exitCode, signalCode) => { - expect(exitCode).toBe(0); - expect(signalCode).toBeNull(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-cli-eval-event.js diff --git a/test/js/node/test/parallel/client-request-destroy.test.js b/test/js/node/test/parallel/client-request-destroy.test.js deleted file mode 100644 index 165fbbdd33..0000000000 --- a/test/js/node/test/parallel/client-request-destroy.test.js +++ /dev/null @@ -1,19 +0,0 @@ -//#FILE: test-client-request-destroy.js -//#SHA1: 343919bc022f2956e9aab5c9a215cbadca2364f1 -//----------------- -"use strict"; - -// Test that http.ClientRequest.prototype.destroy() returns `this`. - -const http = require("http"); - -test("http.ClientRequest.prototype.destroy() returns `this`", () => { - const clientRequest = new http.ClientRequest({ createConnection: () => {} }); - - expect(clientRequest.destroyed).toBe(false); - expect(clientRequest.destroy()).toBe(clientRequest); - expect(clientRequest.destroyed).toBe(true); - expect(clientRequest.destroy()).toBe(clientRequest); -}); - -//<#END_FILE: test-client-request-destroy.js diff --git a/test/js/node/test/parallel/cluster-bind-privileged-port.test.js b/test/js/node/test/parallel/cluster-bind-privileged-port.test.js deleted file mode 100644 index c83b370b56..0000000000 --- a/test/js/node/test/parallel/cluster-bind-privileged-port.test.js +++ /dev/null @@ -1,79 +0,0 @@ -//#FILE: test-cluster-bind-privileged-port.js -//#SHA1: f3a12e75717db9c1a1edd4f51fb2985787a50706 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const cluster = require("cluster"); -const net = require("net"); -const { readFileSync } = require("fs"); - -const isLinux = process.platform === "linux"; -const isOSX = process.platform === "darwin"; -const isIBMi = process.platform === "os400"; -const isWindows = process.platform === "win32"; - -const skipTest = () => { - test.skip("Skipping test", () => {}); - process.exit(0); -}; - -if (isLinux) { - try { - const unprivilegedPortStart = parseInt(readFileSync("/proc/sys/net/ipv4/ip_unprivileged_port_start")); - if (unprivilegedPortStart <= 42) { - skipTest(); - } - } catch { - // Do nothing, feature doesn't exist, minimum is 1024 so 42 is usable. - // Continue... - } -} - -// Skip on OS X Mojave. https://github.com/nodejs/node/issues/21679 -if (isOSX) skipTest(); - -if (isIBMi) skipTest(); - -if (isWindows) skipTest(); - -if (process.getuid() === 0) skipTest(); - -if (cluster.isPrimary) { - test("primary cluster process", () => { - const worker = cluster.fork(); - worker.on("exit", exitCode => { - expect(exitCode).toBe(0); - }); - }); -} else { - test("worker cluster process", () => { - const s = net.createServer(jest.fn()); - s.listen(42, jest.fn()); - s.on("error", err => { - expect(err.code).toBe("EACCES"); - process.disconnect(); - }); - }); -} - -//<#END_FILE: test-cluster-bind-privileged-port.js diff --git a/test/js/node/test/parallel/cluster-call-and-destroy.test.js b/test/js/node/test/parallel/cluster-call-and-destroy.test.js deleted file mode 100644 index 1e2914833f..0000000000 --- a/test/js/node/test/parallel/cluster-call-and-destroy.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-cluster-call-and-destroy.js -//#SHA1: 840777cd738f6257dd874035b1c0291ebe16e326 -//----------------- -"use strict"; -const cluster = require("cluster"); - -if (cluster.isPrimary) { - test("worker disconnection and destruction", () => { - const worker = cluster.fork(); - - return new Promise(resolve => { - worker.on("disconnect", () => { - expect(worker.isConnected()).toBe(false); - worker.destroy(); - resolve(); - }); - }); - }); -} else { - test("worker connection in child process", () => { - expect(cluster.worker.isConnected()).toBe(true); - cluster.worker.disconnect(); - }); -} - -//<#END_FILE: test-cluster-call-and-destroy.js diff --git a/test/js/node/test/parallel/cluster-dgram-reuse.test.js b/test/js/node/test/parallel/cluster-dgram-reuse.test.js deleted file mode 100644 index 143db050b6..0000000000 --- a/test/js/node/test/parallel/cluster-dgram-reuse.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-cluster-dgram-reuse.js -//#SHA1: b7bfc0764ebc95fa5ef85ce1d860aebd3f7df539 -//----------------- -"use strict"; - -const cluster = require("cluster"); -const dgram = require("dgram"); - -if (process.platform === "win32") { - test.skip("dgram clustering is currently not supported on windows."); -} else { - if (cluster.isPrimary) { - test("Primary process", () => { - const worker = cluster.fork(); - worker.on("exit", code => { - expect(code).toBe(0); - }); - }); - } else { - test("Worker process", async () => { - let waiting = 2; - function close() { - if (--waiting === 0) cluster.worker.disconnect(); - } - - const options = { type: "udp4", reuseAddr: true }; - const socket1 = dgram.createSocket(options); - const socket2 = dgram.createSocket(options); - - await new Promise(resolve => { - socket1.bind(0, () => { - socket2.bind(socket1.address().port, () => { - // Work around health check issue - process.nextTick(() => { - socket1.close(close); - socket2.close(close); - resolve(); - }); - }); - }); - }); - }); - } -} - -//<#END_FILE: test-cluster-dgram-reuse.js diff --git a/test/js/node/test/parallel/cluster-disconnect-idle-worker.test.js b/test/js/node/test/parallel/cluster-disconnect-idle-worker.test.js deleted file mode 100644 index 217060fd12..0000000000 --- a/test/js/node/test/parallel/cluster-disconnect-idle-worker.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-cluster-disconnect-idle-worker.js -//#SHA1: f559db612db77271be32bab2d7cb9a4e38f28670 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const cluster = require("cluster"); -const fork = cluster.fork; - -if (cluster.isPrimary) { - test("cluster disconnect idle worker", async () => { - fork(); // It is intentionally called `fork` instead of - fork(); // `cluster.fork` to test that `this` is not used - - const disconnectCallback = jest.fn(() => { - expect(Object.keys(cluster.workers)).toEqual([]); - }); - - await new Promise(resolve => { - cluster.disconnect(() => { - disconnectCallback(); - resolve(); - }); - }); - - expect(disconnectCallback).toHaveBeenCalledTimes(1); - }); -} - -//<#END_FILE: test-cluster-disconnect-idle-worker.js diff --git a/test/js/node/test/parallel/cluster-disconnect-with-no-workers.test.js b/test/js/node/test/parallel/cluster-disconnect-with-no-workers.test.js deleted file mode 100644 index 01c67b2877..0000000000 --- a/test/js/node/test/parallel/cluster-disconnect-with-no-workers.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-cluster-disconnect-with-no-workers.js -//#SHA1: 06b4a0a662491bd9593c303307535a635d69d53e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const cluster = require("cluster"); - -let disconnected = false; - -test("cluster.disconnect with no workers", async () => { - const disconnectPromise = new Promise(resolve => { - cluster.disconnect(() => { - disconnected = true; - resolve(); - }); - }); - - // Assert that callback is not sometimes synchronous - expect(disconnected).toBe(false); - - await disconnectPromise; - - // Assert that the callback was called - expect(disconnected).toBe(true); -}); - -//<#END_FILE: test-cluster-disconnect-with-no-workers.js diff --git a/test/js/node/test/parallel/cluster-eaddrinuse.test.js b/test/js/node/test/parallel/cluster-eaddrinuse.test.js deleted file mode 100644 index c72c6d6af5..0000000000 --- a/test/js/node/test/parallel/cluster-eaddrinuse.test.js +++ /dev/null @@ -1,94 +0,0 @@ -//#FILE: test-cluster-eaddrinuse.js -//#SHA1: a17cbbd83e23565c1cb547999357c14f03be6efa -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Check that having a worker bind to a port that's already taken doesn't -// leave the primary process in a confused state. Releasing the port and -// trying again should Just Work[TM]. - -const { fork } = require("child_process"); -const net = require("net"); - -const id = String(process.argv[2]); -const port = String(process.argv[3]); - -if (id === "undefined") { - test("primary process", async () => { - const server = net.createServer(() => { - throw new Error("Server should not receive connections"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const worker = fork(__filename, ["worker", server.address().port]); - worker.on("message", msg => { - if (msg !== "stop-listening") return; - server.close(() => { - worker.send("stopped-listening"); - }); - }); - resolve(); - }); - }); - }); -} else if (id === "worker") { - test("worker process", async () => { - let server = net.createServer(() => { - throw new Error("Server should not receive connections"); - }); - - await expect( - new Promise((resolve, reject) => { - server.listen(port, () => reject(new Error("Server should not listen"))); - server.on("error", resolve); - }), - ).resolves.toMatchObject({ - code: "EADDRINUSE", - message: expect.any(String), - }); - - process.send("stop-listening"); - - await new Promise(resolve => { - process.once("message", msg => { - if (msg !== "stopped-listening") return; - server = net.createServer(() => { - throw new Error("Server should not receive connections"); - }); - server.listen(port, () => { - server.close(); - resolve(); - }); - }); - }); - }); -} else { - test("invalid argument", () => { - expect(() => { - throw new Error("Bad argument"); - }).toThrow("Bad argument"); - }); -} - -//<#END_FILE: test-cluster-eaddrinuse.js diff --git a/test/js/node/test/parallel/cluster-listen-pipe-readable-writable.test.js b/test/js/node/test/parallel/cluster-listen-pipe-readable-writable.test.js deleted file mode 100644 index ea008eda16..0000000000 --- a/test/js/node/test/parallel/cluster-listen-pipe-readable-writable.test.js +++ /dev/null @@ -1,44 +0,0 @@ -//#FILE: test-cluster-listen-pipe-readable-writable.js -//#SHA1: ec8b0021cb41214529af900b088dce4d31db708d -//----------------- -"use strict"; - -const cluster = require("cluster"); -const net = require("net"); -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const PIPE = path.join(os.tmpdir(), "test.sock"); - -if (process.platform === "win32") { - test.skip("skip on Windows", () => {}); -} else { - if (cluster.isPrimary) { - test("cluster worker can listen on pipe with readable and writable permissions", () => { - const worker = cluster.fork(); - worker.on("exit", code => { - expect(code).toBe(0); - }); - }); - } else { - test("server listens on pipe with correct permissions", done => { - const server = net.createServer().listen( - { - path: PIPE, - readableAll: true, - writableAll: true, - }, - () => { - const stat = fs.statSync(PIPE); - expect(stat.mode & 0o777).toBe(0o777); - server.close(); - process.disconnect(); - done(); - }, - ); - }); - } -} - -//<#END_FILE: test-cluster-listen-pipe-readable-writable.js diff --git a/test/js/node/test/parallel/cluster-send-handle-twice.test.js b/test/js/node/test/parallel/cluster-send-handle-twice.test.js deleted file mode 100644 index 06cb47d583..0000000000 --- a/test/js/node/test/parallel/cluster-send-handle-twice.test.js +++ /dev/null @@ -1,75 +0,0 @@ -//#FILE: test-cluster-send-handle-twice.js -//#SHA1: d667e299035da70aa7831cb6964ba4e974c6bdc8 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Testing to send an handle twice to the primary process. - -const cluster = require("cluster"); -const net = require("net"); - -const workers = { - toStart: 1, -}; - -if (cluster.isPrimary) { - test("primary process", () => { - for (let i = 0; i < workers.toStart; ++i) { - const worker = cluster.fork(); - worker.on("exit", (code, signal) => { - expect(code).toBe(0); - expect(signal).toBeNull(); - }); - } - }); -} else { - test("worker process", async () => { - const server = net.createServer(socket => { - process.send("send-handle-1", socket); - process.send("send-handle-2", socket); - }); - - await new Promise((resolve, reject) => { - server.listen(0, () => { - const client = net.connect({ - host: "localhost", - port: server.address().port, - }); - client.on("close", () => { - cluster.worker.disconnect(); - resolve(); - }); - client.on("connect", () => { - client.end(); - }); - }); - - server.on("error", e => { - console.error(e); - reject(new Error("server.listen failed")); - }); - }); - }); -} - -//<#END_FILE: test-cluster-send-handle-twice.js diff --git a/test/js/node/test/parallel/cluster-setup-primary-cumulative.test.js b/test/js/node/test/parallel/cluster-setup-primary-cumulative.test.js deleted file mode 100644 index 178c7f77ab..0000000000 --- a/test/js/node/test/parallel/cluster-setup-primary-cumulative.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-cluster-setup-primary-cumulative.js -//#SHA1: 8a64228ac6d42c930b2426bbc53009f5d8b96a17 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const assert = require("assert"); -const cluster = require("cluster"); - -test("cluster setup primary cumulative", () => { - expect(cluster.isPrimary).toBe(true); - - // cluster.settings should not be initialized until needed - expect(cluster.settings).toEqual({}); - - cluster.setupPrimary(); - expect(cluster.settings).toEqual({ - args: process.argv.slice(2), - exec: process.argv[1], - execArgv: process.execArgv, - silent: false, - }); - - cluster.setupPrimary({ exec: "overridden" }); - expect(cluster.settings.exec).toBe("overridden"); - - cluster.setupPrimary({ args: ["foo", "bar"] }); - expect(cluster.settings.exec).toBe("overridden"); - expect(cluster.settings.args).toEqual(["foo", "bar"]); - - cluster.setupPrimary({ execArgv: ["baz", "bang"] }); - expect(cluster.settings.exec).toBe("overridden"); - expect(cluster.settings.args).toEqual(["foo", "bar"]); - expect(cluster.settings.execArgv).toEqual(["baz", "bang"]); - - cluster.setupPrimary(); - expect(cluster.settings).toEqual({ - args: ["foo", "bar"], - exec: "overridden", - execArgv: ["baz", "bang"], - silent: false, - }); -}); - -//<#END_FILE: test-cluster-setup-primary-cumulative.js diff --git a/test/js/node/test/parallel/cluster-setup-primary-emit.test.js b/test/js/node/test/parallel/cluster-setup-primary-emit.test.js deleted file mode 100644 index dc036034bd..0000000000 --- a/test/js/node/test/parallel/cluster-setup-primary-emit.test.js +++ /dev/null @@ -1,67 +0,0 @@ -//#FILE: test-cluster-setup-primary-emit.js -//#SHA1: 965f86ef2ea557510e956759d3f540edfa7b03de -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const cluster = require("cluster"); - -expect(cluster.isPrimary).toBe(true); - -function emitAndCatch(next) { - return new Promise(resolve => { - cluster.once("setup", settings => { - expect(settings.exec).toBe("new-exec"); - setImmediate(() => { - next(); - resolve(); - }); - }); - cluster.setupPrimary({ exec: "new-exec" }); - }); -} - -function emitAndCatch2(next) { - return new Promise(resolve => { - cluster.once("setup", settings => { - expect(settings).toHaveProperty("exec"); - setImmediate(() => { - next(); - resolve(); - }); - }); - cluster.setupPrimary(); - }); -} - -test("cluster setup primary emit", async () => { - const nextSpy1 = jest.fn(); - const nextSpy2 = jest.fn(); - - await emitAndCatch(nextSpy1); - expect(nextSpy1).toHaveBeenCalledTimes(1); - - await emitAndCatch2(nextSpy2); - expect(nextSpy2).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-cluster-setup-primary-emit.js diff --git a/test/js/node/test/parallel/cluster-setup-primary-multiple.test.js b/test/js/node/test/parallel/cluster-setup-primary-multiple.test.js deleted file mode 100644 index 05a3ca9cf5..0000000000 --- a/test/js/node/test/parallel/cluster-setup-primary-multiple.test.js +++ /dev/null @@ -1,78 +0,0 @@ -//#FILE: test-cluster-setup-primary-multiple.js -//#SHA1: a0b16cb2b01b0265f98508f2a6a9974396b6b03a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const cluster = require("cluster"); -const debug = require("util").debuglog("test"); - -test("cluster setup primary multiple times", async () => { - expect(cluster.isPrimary).toBe(true); - - // The cluster.settings object is cloned even though the current implementation - // makes that unnecessary. This is to make the test less fragile if the - // implementation ever changes such that cluster.settings is mutated instead of - // replaced. - const cheapClone = obj => JSON.parse(JSON.stringify(obj)); - - const configs = []; - - // Capture changes - cluster.on("setup", () => { - debug(`"setup" emitted ${JSON.stringify(cluster.settings)}`); - configs.push(cheapClone(cluster.settings)); - }); - - const execs = ["node-next", "node-next-2", "node-next-3"]; - - // Make changes to cluster settings - for (let i = 0; i < execs.length; i++) { - await new Promise(resolve => { - setTimeout(() => { - cluster.setupPrimary({ exec: execs[i] }); - resolve(); - }, i * 100); - }); - } - - // Cluster emits 'setup' asynchronously, so we must stay alive long - // enough for that to happen - await new Promise(resolve => { - setTimeout( - () => { - debug("cluster setup complete"); - resolve(); - }, - (execs.length + 1) * 100, - ); - }); - - // Tests that "setup" is emitted for every call to setupPrimary - expect(configs.length).toBe(execs.length); - - expect(configs[0].exec).toBe(execs[0]); - expect(configs[1].exec).toBe(execs[1]); - expect(configs[2].exec).toBe(execs[2]); -}); - -//<#END_FILE: test-cluster-setup-primary-multiple.js diff --git a/test/js/node/test/parallel/cluster-worker-constructor.test.js b/test/js/node/test/parallel/cluster-worker-constructor.test.js deleted file mode 100644 index 470481560e..0000000000 --- a/test/js/node/test/parallel/cluster-worker-constructor.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-cluster-worker-constructor.js -//#SHA1: ef3237d09cf6339e487f14c40d4c047c6871ead2 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// test-cluster-worker-constructor.js -// validates correct behavior of the cluster.Worker constructor - -const cluster = require("cluster"); - -describe("cluster.Worker constructor", () => { - test("creates a worker with default values", () => { - const worker = new cluster.Worker(); - expect(worker.exitedAfterDisconnect).toBeUndefined(); - expect(worker.state).toBe("none"); - expect(worker.id).toBe(0); - expect(worker.process).toBeUndefined(); - }); - - test("creates a worker with custom values", () => { - const worker = new cluster.Worker({ - id: 3, - state: "online", - process: process, - }); - expect(worker.exitedAfterDisconnect).toBeUndefined(); - expect(worker.state).toBe("online"); - expect(worker.id).toBe(3); - expect(worker.process).toBe(process); - }); - - test("creates a worker using call method", () => { - const worker = cluster.Worker.call({}, { id: 5 }); - expect(worker).toBeInstanceOf(cluster.Worker); - expect(worker.id).toBe(5); - }); -}); - -//<#END_FILE: test-cluster-worker-constructor.js diff --git a/test/js/node/test/parallel/cluster-worker-destroy.test.js b/test/js/node/test/parallel/cluster-worker-destroy.test.js deleted file mode 100644 index 607cbbb866..0000000000 --- a/test/js/node/test/parallel/cluster-worker-destroy.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-cluster-worker-destroy.js -//#SHA1: 277a85b7c8fdda347d1f753601ac4b843a9a1c8d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// The goal of this test is to cover the Workers' implementation of -// Worker.prototype.destroy. Worker.prototype.destroy is called within -// the worker's context: once when the worker is still connected to the -// primary, and another time when it's not connected to it, so that we cover -// both code paths. - -const assert = require("assert"); -const cluster = require("cluster"); - -let worker1, worker2; - -if (cluster.isPrimary) { - test("primary process", () => { - worker1 = cluster.fork(); - worker2 = cluster.fork(); - - [worker1, worker2].forEach(worker => { - const disconnectSpy = jest.fn(); - const exitSpy = jest.fn(); - - worker.on("disconnect", disconnectSpy); - worker.on("exit", exitSpy); - - worker.on("exit", () => { - expect(disconnectSpy).toHaveBeenCalledTimes(1); - expect(exitSpy).toHaveBeenCalledTimes(1); - }); - }); - }); -} else if (cluster.worker.id === 1) { - test("worker 1: call destroy when worker is disconnected", () => { - // Call destroy when worker is disconnected - cluster.worker.process.on("disconnect", () => { - cluster.worker.destroy(); - }); - - const w = cluster.worker.disconnect(); - expect(w).toBe(cluster.worker); - }); -} else { - test("worker 2: call destroy when worker is not disconnected yet", () => { - // Call destroy when worker is not disconnected yet - cluster.worker.destroy(); - }); -} - -//<#END_FILE: test-cluster-worker-destroy.js diff --git a/test/js/node/test/parallel/cluster-worker-handle-close.test.js b/test/js/node/test/parallel/cluster-worker-handle-close.test.js deleted file mode 100644 index f6f608e0ac..0000000000 --- a/test/js/node/test/parallel/cluster-worker-handle-close.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-cluster-worker-handle-close.js -//#SHA1: 8aa4bcd8641fe9274b97853b80734ea6f18eafbb -//----------------- -"use strict"; -const cluster = require("cluster"); -const net = require("net"); - -if (cluster.isPrimary) { - test("Primary process", () => { - cluster.schedulingPolicy = cluster.SCHED_RR; - expect(cluster.fork).not.toThrow(); - }); -} else { - let server; - - beforeAll(() => { - server = net.createServer(jest.fn()); - }); - - test("Worker process", done => { - const serverListenSpy = jest.spyOn(server, "listen"); - const netConnectSpy = jest.spyOn(net, "connect"); - - server.listen(0, () => { - expect(serverListenSpy).toHaveBeenCalledTimes(1); - expect(netConnectSpy).toHaveBeenCalledWith(server.address().port); - done(); - }); - }); - - test("Internal message handling", done => { - const handleCloseSpy = jest.fn(callback => callback()); - const handle = { close: handleCloseSpy }; - - const messageHandler = (message, messageHandle) => { - if (message.act !== "newconn") { - return; - } - - server.close(); - messageHandle.close = jest.fn(() => { - handleCloseSpy.call(messageHandle, () => { - expect(handleCloseSpy).toHaveBeenCalledTimes(1); - process.exit(); - }); - }); - - expect(messageHandle.close).toHaveBeenCalledTimes(1); - done(); - }; - - process.prependListener("internalMessage", messageHandler); - - // Simulate an internal message - process.emit("internalMessage", { act: "newconn" }, handle); - }); -} - -//<#END_FILE: test-cluster-worker-handle-close.js diff --git a/test/js/node/test/parallel/cluster-worker-isconnected.test.js b/test/js/node/test/parallel/cluster-worker-isconnected.test.js deleted file mode 100644 index 4ca42ebe61..0000000000 --- a/test/js/node/test/parallel/cluster-worker-isconnected.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-cluster-worker-isconnected.js -//#SHA1: cf1e0243c030fe4cf872716099e517daec3efffc -//----------------- -"use strict"; -const cluster = require("cluster"); - -if (cluster.isPrimary) { - test("worker isConnected() in primary", () => { - const worker = cluster.fork(); - - expect(worker.isConnected()).toBe(true); - - worker.on("disconnect", () => { - expect(worker.isConnected()).toBe(false); - }); - - worker.on("message", function (msg) { - if (msg === "readyToDisconnect") { - worker.disconnect(); - } - }); - }); -} else { - test("worker isConnected() in worker", () => { - function assertNotConnected() { - expect(cluster.worker.isConnected()).toBe(false); - } - - expect(cluster.worker.isConnected()).toBe(true); - - cluster.worker.on("disconnect", assertNotConnected); - cluster.worker.process.on("disconnect", assertNotConnected); - - process.send("readyToDisconnect"); - }); -} - -//<#END_FILE: test-cluster-worker-isconnected.js diff --git a/test/js/node/test/parallel/common-countdown.test.js b/test/js/node/test/parallel/common-countdown.test.js deleted file mode 100644 index 24c268588e..0000000000 --- a/test/js/node/test/parallel/common-countdown.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-common-countdown.js -//#SHA1: ba753878e7b8cbeaede6057bc05a7d3b542949a5 -//----------------- -"use strict"; - -const assert = require("assert"); -const Countdown = require("../common/countdown"); -const fixtures = require("../common/fixtures"); -const { execFile } = require("child_process"); - -test("Countdown functionality", () => { - let done = ""; - const countdown = new Countdown(2, () => (done = true)); - expect(countdown.remaining).toBe(2); - countdown.dec(); - expect(countdown.remaining).toBe(1); - countdown.dec(); - expect(countdown.remaining).toBe(0); - expect(done).toBe(true); -}); - -const failFixtures = [ - [fixtures.path("failcounter.js"), "Mismatched function calls. Expected exactly 1, actual 0."], -]; - -test.each(failFixtures)("Fail fixture: %s", async (file, expected) => { - await new Promise(resolve => { - execFile(process.argv[0], [file], (ex, stdout, stderr) => { - expect(ex).toBeTruthy(); - expect(stderr).toBe(""); - const firstLine = stdout.split("\n").shift(); - expect(firstLine).toBe(expected); - resolve(); - }); - }); -}); - -//<#END_FILE: test-common-countdown.js diff --git a/test/js/node/test/parallel/console-assign-undefined.test.js b/test/js/node/test/parallel/console-assign-undefined.test.js deleted file mode 100644 index cc46f41b42..0000000000 --- a/test/js/node/test/parallel/console-assign-undefined.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-console-assign-undefined.js -//#SHA1: ccd5cd3087520e692e5123679c1753d168d310f0 -//----------------- -"use strict"; - -// Patch global.console before importing modules that may modify the console -// object. - -let originalConsole; - -beforeAll(() => { - originalConsole = global.console; - global.console = 42; -}); - -afterAll(() => { - // Reset the console - global.console = originalConsole; -}); - -test("console can be assigned a non-object value", () => { - // Originally the console had a getter. Test twice to verify it had no side - // effect. - expect(global.console).toBe(42); - expect(global.console).toBe(42); - - expect(() => console.log("foo")).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); - - global.console = 1; - expect(global.console).toBe(1); - expect(console).toBe(1); -}); - -test("console can be reset and used", () => { - global.console = originalConsole; - const consoleSpy = jest.spyOn(console, "log"); - console.log("foo"); - expect(consoleSpy).toHaveBeenCalledWith("foo"); - consoleSpy.mockRestore(); -}); - -//<#END_FILE: test-console-assign-undefined.js diff --git a/test/js/node/test/parallel/console-issue-43095.test.js b/test/js/node/test/parallel/console-issue-43095.test.js deleted file mode 100644 index 815c2d86ad..0000000000 --- a/test/js/node/test/parallel/console-issue-43095.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-console-issue-43095.js -//#SHA1: 1c0c5cce62bcee4d50c6b716dd1430db2784c3f4 -//----------------- -"use strict"; - -const { inspect } = require("node:util"); - -test("console output for revoked proxy", () => { - const consoleSpy = { - dir: jest.spyOn(console, "dir").mockImplementation(), - log: jest.spyOn(console, "log").mockImplementation(), - }; - - const r = Proxy.revocable({}, {}); - r.revoke(); - - console.dir(r); - console.dir(r.proxy); - console.log(r.proxy); - console.log(inspect(r.proxy, { showProxy: true })); - - expect(consoleSpy.dir).toHaveBeenCalledTimes(2); - expect(consoleSpy.log).toHaveBeenCalledTimes(2); - - // Check that console.dir was called with the revoked proxy object - expect(consoleSpy.dir.mock.calls[0][0]).toBe(r); - expect(consoleSpy.dir.mock.calls[1][0]).toBe(r.proxy); - - // Check that console.log was called with the revoked proxy - expect(consoleSpy.log.mock.calls[0][0]).toBe(r.proxy); - - // Check that console.log was called with the inspected revoked proxy - expect(consoleSpy.log.mock.calls[1][0]).toBe(inspect(r.proxy, { showProxy: true })); - - // Clean up - consoleSpy.dir.mockRestore(); - consoleSpy.log.mockRestore(); -}); - -//<#END_FILE: test-console-issue-43095.js diff --git a/test/js/node/test/parallel/console-log-stdio-broken-dest.test.js b/test/js/node/test/parallel/console-log-stdio-broken-dest.test.js deleted file mode 100644 index b93dfe3824..0000000000 --- a/test/js/node/test/parallel/console-log-stdio-broken-dest.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-console-log-stdio-broken-dest.js -//#SHA1: c2c2e85eeb28db4ace2c4bb0a86f46f7e7bf2682 -//----------------- -"use strict"; - -const { Writable } = require("stream"); -const { Console } = require("console"); -const { EventEmitter } = require("events"); - -test("Console log with broken destination", done => { - const stream = new Writable({ - write(chunk, enc, cb) { - cb(); - }, - writev(chunks, cb) { - setTimeout(cb, 10, new Error("kaboom")); - }, - }); - const myConsole = new Console(stream, stream); - - const warningListener = jest.fn(); - process.on("warning", warningListener); - - stream.cork(); - for (let i = 0; i < EventEmitter.defaultMaxListeners + 1; i++) { - myConsole.log("a message"); - } - stream.uncork(); - - // We need to wait for the next tick to ensure the error has time to propagate - process.nextTick(() => { - expect(warningListener).not.toHaveBeenCalled(); - process.removeListener("warning", warningListener); - done(); - }); -}); - -//<#END_FILE: test-console-log-stdio-broken-dest.js diff --git a/test/js/node/test/parallel/console-log-throw-primitive.test.js b/test/js/node/test/parallel/console-log-throw-primitive.test.js deleted file mode 100644 index e318eaaf4d..0000000000 --- a/test/js/node/test/parallel/console-log-throw-primitive.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-console-log-throw-primitive.js -//#SHA1: a1889badf1058f6fadc8984a5075f3d048e2948c -//----------------- -"use strict"; - -const { Writable } = require("stream"); -const { Console } = require("console"); - -test("Console.log should not throw when stream throws null", () => { - const stream = new Writable({ - write() { - throw null; // eslint-disable-line no-throw-literal - }, - }); - - const console = new Console({ stdout: stream }); - - // Should not throw - expect(() => console.log("test")).not.toThrow(); -}); - -//<#END_FILE: test-console-log-throw-primitive.js diff --git a/test/js/node/test/parallel/console-not-call-tostring.test.js b/test/js/node/test/parallel/console-not-call-tostring.test.js deleted file mode 100644 index f43e7dbde6..0000000000 --- a/test/js/node/test/parallel/console-not-call-tostring.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-console-not-call-toString.js -//#SHA1: e0bf3a601442b76f12f657e53df54690d2fe21fa -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("util.inspect does not call toString", () => { - function func() {} - let toStringCalled = false; - func.toString = function () { - toStringCalled = true; - }; - - require("util").inspect(func); - - expect(toStringCalled).toBe(false); -}); - -//<#END_FILE: test-console-not-call-toString.js diff --git a/test/js/node/test/parallel/console-self-assign.test.js b/test/js/node/test/parallel/console-self-assign.test.js deleted file mode 100644 index e746e9a47a..0000000000 --- a/test/js/node/test/parallel/console-self-assign.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-console-self-assign.js -//#SHA1: 7ed2fd07a18f0485f2592ada8e97e9c33753e691 -//----------------- -"use strict"; - -// Assigning to itself should not throw. -test("console self-assignment", () => { - expect(() => { - global.console = global.console; // eslint-disable-line no-self-assign - }).not.toThrow(); -}); - -//<#END_FILE: test-console-self-assign.js diff --git a/test/js/node/test/parallel/crypto-dh-shared.test.js b/test/js/node/test/parallel/crypto-dh-shared.test.js deleted file mode 100644 index f811ca4df7..0000000000 --- a/test/js/node/test/parallel/crypto-dh-shared.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-crypto-dh-shared.js -//#SHA1: 8d5e31de4aa93f435c4c6d05d7b394156a38fb8e -//----------------- -"use strict"; - -const crypto = require("crypto"); - -test("Diffie-Hellman shared secret computation", () => { - const alice = crypto.createDiffieHellmanGroup("modp5"); - const bob = crypto.createDiffieHellmanGroup("modp5"); - - alice.generateKeys(); - bob.generateKeys(); - - const aSecret = alice.computeSecret(bob.getPublicKey()).toString("hex"); - const bSecret = bob.computeSecret(alice.getPublicKey()).toString("hex"); - - expect(aSecret).toBe(bSecret); -}); - -//<#END_FILE: test-crypto-dh-shared.js diff --git a/test/js/node/test/parallel/crypto-from-binary.test.js b/test/js/node/test/parallel/crypto-from-binary.test.js deleted file mode 100644 index 3c015ec6b6..0000000000 --- a/test/js/node/test/parallel/crypto-from-binary.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-crypto-from-binary.js -//#SHA1: 2f3b186e9b549c6910a58dea98e6bdeb7c540afa -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// This is the same as test/simple/test-crypto, but from before the shift -// to use buffers by default. - -const crypto = require("crypto"); - -const EXTERN_APEX = 0xfbee9; - -// Manually controlled string for checking binary output -let ucs2_control = "a\u0000"; - -// Grow the strings to proper length -while (ucs2_control.length <= EXTERN_APEX) { - ucs2_control = ucs2_control.repeat(2); -} - -// Check resultant buffer and output string -const b = Buffer.from(ucs2_control + ucs2_control, "ucs2"); - -describe("Crypto from binary", () => { - beforeAll(() => { - if (!crypto) { - return test.skip("missing crypto"); - } - }); - - test("Update from binary data - small slice", () => { - const datum1 = b.slice(700000); - const hash1_converted = crypto.createHash("sha1").update(datum1.toString("base64"), "base64").digest("hex"); - const hash1_direct = crypto.createHash("sha1").update(datum1).digest("hex"); - expect(hash1_direct).toBe(hash1_converted); - }); - - test("Update from binary data - full buffer", () => { - const datum2 = b; - const hash2_converted = crypto.createHash("sha1").update(datum2.toString("base64"), "base64").digest("hex"); - const hash2_direct = crypto.createHash("sha1").update(datum2).digest("hex"); - expect(hash2_direct).toBe(hash2_converted); - }); -}); - -//<#END_FILE: test-crypto-from-binary.js diff --git a/test/js/node/test/parallel/crypto-keygen-async-elliptic-curve-jwk-rsa.test.js b/test/js/node/test/parallel/crypto-keygen-async-elliptic-curve-jwk-rsa.test.js deleted file mode 100644 index f19582afbd..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-async-elliptic-curve-jwk-rsa.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-crypto-keygen-async-elliptic-curve-jwk-rsa.js -//#SHA1: 4337f1e36680f21ed3439d7ab546ea855a0a842e -//----------------- -"use strict"; - -const { generateKeyPair } = require("crypto"); - -// Test async elliptic curve key generation with 'jwk' encoding and RSA. -test("async elliptic curve key generation with jwk encoding and RSA", async () => { - const { publicKey, privateKey } = await new Promise((resolve, reject) => { - generateKeyPair( - "rsa", - { - modulusLength: 1024, - publicKeyEncoding: { - format: "jwk", - }, - privateKeyEncoding: { - format: "jwk", - }, - }, - (err, publicKey, privateKey) => { - if (err) reject(err); - else resolve({ publicKey, privateKey }); - }, - ); - }); - - expect(typeof publicKey).toBe("object"); - expect(typeof privateKey).toBe("object"); - expect(publicKey.kty).toBe("RSA"); - expect(publicKey.kty).toBe(privateKey.kty); - expect(typeof publicKey.n).toBe("string"); - expect(publicKey.n).toBe(privateKey.n); - expect(typeof publicKey.e).toBe("string"); - expect(publicKey.e).toBe(privateKey.e); - expect(typeof privateKey.d).toBe("string"); - expect(typeof privateKey.p).toBe("string"); - expect(typeof privateKey.q).toBe("string"); - expect(typeof privateKey.dp).toBe("string"); - expect(typeof privateKey.dq).toBe("string"); - expect(typeof privateKey.qi).toBe("string"); -}); - -//<#END_FILE: test-crypto-keygen-async-elliptic-curve-jwk-rsa.js diff --git a/test/js/node/test/parallel/crypto-keygen-async-encrypted-private-key-der.test.js b/test/js/node/test/parallel/crypto-keygen-async-encrypted-private-key-der.test.js deleted file mode 100644 index 01e5b30494..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-async-encrypted-private-key-der.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-crypto-keygen-async-encrypted-private-key-der.js -//#SHA1: 30f86c68619f3f24294d5f062eed48b13c116b0c -//----------------- -"use strict"; - -const crypto = require("crypto"); -const { assertApproximateSize, testEncryptDecrypt, testSignVerify } = require("../common/crypto"); - -// Test async RSA key generation with an encrypted private key, but encoded as DER. -test("RSA key generation with encrypted private key (DER)", async () => { - const { publicKey: publicKeyDER, privateKey: privateKeyDER } = await new Promise((resolve, reject) => { - crypto.generateKeyPair( - "rsa", - { - publicExponent: 0x10001, - modulusLength: 512, - publicKeyEncoding: { - type: "pkcs1", - format: "der", - }, - privateKeyEncoding: { - type: "pkcs8", - format: "der", - }, - }, - (err, publicKey, privateKey) => { - if (err) reject(err); - else resolve({ publicKey, privateKey }); - }, - ); - }); - - expect(Buffer.isBuffer(publicKeyDER)).toBe(true); - assertApproximateSize(publicKeyDER, 74); - - expect(Buffer.isBuffer(privateKeyDER)).toBe(true); - - const publicKey = { - key: publicKeyDER, - type: "pkcs1", - format: "der", - }; - const privateKey = { - key: privateKeyDER, - format: "der", - type: "pkcs8", - passphrase: "secret", - }; - - await testEncryptDecrypt(publicKey, privateKey); - await testSignVerify(publicKey, privateKey); -}); - -//<#END_FILE: test-crypto-keygen-async-encrypted-private-key-der.js diff --git a/test/js/node/test/parallel/crypto-keygen-async-explicit-elliptic-curve.test.js b/test/js/node/test/parallel/crypto-keygen-async-explicit-elliptic-curve.test.js deleted file mode 100644 index 4d6f637147..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-async-explicit-elliptic-curve.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-crypto-keygen-async-explicit-elliptic-curve.js -//#SHA1: be1eabf816f52e5f53cb2535d47050b2552d21cd -//----------------- -"use strict"; - -const crypto = require("crypto"); - -// Skip the test if crypto support is not available -if (!crypto.generateKeyPair) { - test.skip("missing crypto support", () => {}); -} else { - const { generateKeyPair } = crypto; - - const { testSignVerify, spkiExp, sec1Exp } = require("../common/crypto"); - - // Test async explicit elliptic curve key generation, e.g. for ECDSA, - // with a SEC1 private key with paramEncoding explicit. - test("async explicit elliptic curve key generation", async () => { - await new Promise((resolve, reject) => { - generateKeyPair( - "ec", - { - namedCurve: "prime256v1", - paramEncoding: "explicit", - publicKeyEncoding: { - type: "spki", - format: "pem", - }, - privateKeyEncoding: { - type: "sec1", - format: "pem", - }, - }, - (err, publicKey, privateKey) => { - if (err) reject(err); - else resolve({ publicKey, privateKey }); - }, - ); - }).then(({ publicKey, privateKey }) => { - expect(typeof publicKey).toBe("string"); - expect(publicKey).toMatch(spkiExp); - expect(typeof privateKey).toBe("string"); - expect(privateKey).toMatch(sec1Exp); - - return testSignVerify(publicKey, privateKey); - }); - }); -} - -//<#END_FILE: test-crypto-keygen-async-explicit-elliptic-curve.js diff --git a/test/js/node/test/parallel/crypto-keygen-async-named-elliptic-curve.test.js b/test/js/node/test/parallel/crypto-keygen-async-named-elliptic-curve.test.js deleted file mode 100644 index 8721a4551f..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-async-named-elliptic-curve.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-crypto-keygen-async-named-elliptic-curve.js -//#SHA1: 77822175b9b2c2206ec4ab8a3e1182e3576b23bd -//----------------- -"use strict"; - -const crypto = require("crypto"); -const { testSignVerify } = require("../common/crypto"); - -if (!crypto.generateKeyPair) { - test.skip("missing crypto.generateKeyPair"); -} - -const spkiExp = - /^-----BEGIN PUBLIC KEY-----\n(?:[A-Za-z0-9+/=]{64}\n)*[A-Za-z0-9+/=]{1,64}\n-----END PUBLIC KEY-----\n$/; -const sec1Exp = - /^-----BEGIN EC PRIVATE KEY-----\n(?:[A-Za-z0-9+/=]{64}\n)*[A-Za-z0-9+/=]{1,64}\n-----END EC PRIVATE KEY-----\n$/; - -// Test async named elliptic curve key generation, e.g. for ECDSA, -// with a SEC1 private key. -test("async named elliptic curve key generation with SEC1 private key", async () => { - const { publicKey, privateKey } = await new Promise((resolve, reject) => { - crypto.generateKeyPair( - "ec", - { - namedCurve: "prime256v1", - paramEncoding: "named", - publicKeyEncoding: { - type: "spki", - format: "pem", - }, - privateKeyEncoding: { - type: "sec1", - format: "pem", - }, - }, - (err, publicKey, privateKey) => { - if (err) reject(err); - else resolve({ publicKey, privateKey }); - }, - ); - }); - - expect(typeof publicKey).toBe("string"); - expect(publicKey).toMatch(spkiExp); - expect(typeof privateKey).toBe("string"); - expect(privateKey).toMatch(sec1Exp); - - await testSignVerify(publicKey, privateKey); -}); - -//<#END_FILE: test-crypto-keygen-async-named-elliptic-curve.js diff --git a/test/js/node/test/parallel/crypto-keygen-empty-passphrase-no-error.test.js b/test/js/node/test/parallel/crypto-keygen-empty-passphrase-no-error.test.js deleted file mode 100644 index ed029ce08d..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-empty-passphrase-no-error.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-crypto-keygen-empty-passphrase-no-error.js -//#SHA1: a949b38385a5dd05507975cbc44b3beba764bd95 -//----------------- -"use strict"; - -const crypto = require("crypto"); - -// Skip the test if crypto is not available -if (typeof crypto.generateKeyPair !== "function") { - test.skip("missing crypto", () => {}); -} else { - test("generateKeyPair with empty passphrase should not throw ERR_OSSL_CRYPTO_MALLOC_FAILURE", async () => { - // Passing an empty passphrase string should not throw ERR_OSSL_CRYPTO_MALLOC_FAILURE even on OpenSSL 3. - // Regression test for https://github.com/nodejs/node/issues/41428. - await expect( - new Promise((resolve, reject) => { - crypto.generateKeyPair( - "rsa", - { - modulusLength: 1024, - publicKeyEncoding: { - type: "spki", - format: "pem", - }, - privateKeyEncoding: { - type: "pkcs8", - format: "pem", - cipher: "aes-256-cbc", - passphrase: "", - }, - }, - (err, publicKey, privateKey) => { - if (err) reject(err); - else resolve({ publicKey, privateKey }); - }, - ); - }), - ).resolves.toEqual( - expect.objectContaining({ - publicKey: expect.any(String), - privateKey: expect.any(String), - }), - ); - }); -} - -//<#END_FILE: test-crypto-keygen-empty-passphrase-no-error.js diff --git a/test/js/node/test/parallel/crypto-keygen-key-object-without-encoding.test.js b/test/js/node/test/parallel/crypto-keygen-key-object-without-encoding.test.js deleted file mode 100644 index 53d01c929f..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-key-object-without-encoding.test.js +++ /dev/null @@ -1,90 +0,0 @@ -//#FILE: test-crypto-keygen-key-object-without-encoding.js -//#SHA1: da408ed128f913dc7343ef88743b362c16420ba0 -//----------------- -"use strict"; - -const crypto = require("crypto"); - -// Skip the test if crypto is not available -if (!crypto.generateKeyPair) { - test.skip("missing crypto"); -} - -const { generateKeyPair } = crypto; - -// Helper functions for testing encryption/decryption and signing/verifying -const testEncryptDecrypt = (publicKey, privateKey) => { - const plaintext = "Hello, World!"; - const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(plaintext)); - const decrypted = crypto.privateDecrypt(privateKey, encrypted); - expect(decrypted.toString()).toBe(plaintext); -}; - -const testSignVerify = (publicKey, privateKey) => { - const data = "Hello, World!"; - const sign = crypto.createSign("SHA256"); - sign.update(data); - const signature = sign.sign(privateKey); - const verify = crypto.createVerify("SHA256"); - verify.update(data); - expect(verify.verify(publicKey, signature)).toBe(true); -}; - -// Tests key objects are returned when key encodings are not specified. -describe("generateKeyPair without encoding", () => { - // If no publicKeyEncoding is specified, a key object should be returned. - test("returns key object for public key when no encoding specified", done => { - generateKeyPair( - "rsa", - { - modulusLength: 1024, - privateKeyEncoding: { - type: "pkcs1", - format: "pem", - }, - }, - (err, publicKey, privateKey) => { - expect(err).toBe(null); - expect(typeof publicKey).toBe("object"); - expect(publicKey.type).toBe("public"); - expect(publicKey.asymmetricKeyType).toBe("rsa"); - - // The private key should still be a string. - expect(typeof privateKey).toBe("string"); - - testEncryptDecrypt(publicKey, privateKey); - testSignVerify(publicKey, privateKey); - done(); - }, - ); - }); - - // If no privateKeyEncoding is specified, a key object should be returned. - test("returns key object for private key when no encoding specified", done => { - generateKeyPair( - "rsa", - { - modulusLength: 1024, - publicKeyEncoding: { - type: "pkcs1", - format: "pem", - }, - }, - (err, publicKey, privateKey) => { - expect(err).toBe(null); - // The public key should still be a string. - expect(typeof publicKey).toBe("string"); - - expect(typeof privateKey).toBe("object"); - expect(privateKey.type).toBe("private"); - expect(privateKey.asymmetricKeyType).toBe("rsa"); - - testEncryptDecrypt(publicKey, privateKey); - testSignVerify(publicKey, privateKey); - done(); - }, - ); - }); -}); - -//<#END_FILE: test-crypto-keygen-key-object-without-encoding.js diff --git a/test/js/node/test/parallel/crypto-keygen-key-objects.test.js b/test/js/node/test/parallel/crypto-keygen-key-objects.test.js deleted file mode 100644 index 29d4bbe5f8..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-key-objects.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-crypto-keygen-key-objects.js -//#SHA1: 7a2dce611ba70e533ebb73d9f281896bdf75051f -//----------------- -"use strict"; - -if (!process.versions.bun) { - const common = require("../common"); - if (!common.hasCrypto) common.skip("missing crypto"); -} - -const { generateKeyPairSync } = require("crypto"); - -// Test sync key generation with key objects. -test("generateKeyPairSync with RSA", () => { - const { publicKey, privateKey } = generateKeyPairSync("rsa", { - modulusLength: 512, - }); - - expect(typeof publicKey).toBe("object"); - expect(publicKey.type).toBe("public"); - expect(publicKey.asymmetricKeyType).toBe("rsa"); - expect(publicKey.asymmetricKeyDetails).toEqual({ - modulusLength: 512, - publicExponent: 65537n, - }); - - expect(typeof privateKey).toBe("object"); - expect(privateKey.type).toBe("private"); - expect(privateKey.asymmetricKeyType).toBe("rsa"); - expect(privateKey.asymmetricKeyDetails).toEqual({ - modulusLength: 512, - publicExponent: 65537n, - }); -}); - -//<#END_FILE: test-crypto-keygen-key-objects.js diff --git a/test/js/node/test/parallel/crypto-keygen-missing-oid.test.js b/test/js/node/test/parallel/crypto-keygen-missing-oid.test.js deleted file mode 100644 index b91248c3a6..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-missing-oid.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-crypto-keygen-missing-oid.js -//#SHA1: ffcbc53b115cce8795ca1a0e73a533b04789a930 -//----------------- -"use strict"; - -const { generateKeyPair, generateKeyPairSync, getCurves } = require("crypto"); - -// This test creates EC key pairs on curves without associated OIDs. -// Specifying a key encoding should not crash. -test("EC key pairs on curves without associated OIDs", () => { - if (process.versions.openssl >= "1.1.1i") { - const curves = ["Oakley-EC2N-3", "Oakley-EC2N-4"]; - const availableCurves = getCurves(); - - for (const namedCurve of curves) { - if (!availableCurves.includes(namedCurve)) continue; - - const expectedErrorCode = process.versions.openssl.startsWith("3.") - ? "ERR_OSSL_MISSING_OID" - : "ERR_OSSL_EC_MISSING_OID"; - - const params = { - namedCurve, - publicKeyEncoding: { - format: "der", - type: "spki", - }, - }; - - expect(() => { - generateKeyPairSync("ec", params); - }).toThrow( - expect.objectContaining({ - code: expectedErrorCode, - message: expect.any(String), - }), - ); - - return new Promise(resolve => { - generateKeyPair("ec", params, err => { - expect(err).toMatchObject({ - code: expectedErrorCode, - message: expect.any(String), - }); - resolve(); - }); - }); - } - } else { - // Skip test if OpenSSL version is less than 1.1.1i - test.skip("OpenSSL version is less than 1.1.1i"); - } -}); - -//<#END_FILE: test-crypto-keygen-missing-oid.js diff --git a/test/js/node/test/parallel/crypto-keygen-non-standard-public-exponent.test.js b/test/js/node/test/parallel/crypto-keygen-non-standard-public-exponent.test.js deleted file mode 100644 index 46fc35ffdb..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-non-standard-public-exponent.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-crypto-keygen-non-standard-public-exponent.js -//#SHA1: 955e956a08102b75fcf0571213a1dd939d1f51ac -//----------------- -"use strict"; - -const crypto = require("crypto"); - -if (!crypto.generateKeyPairSync) { - test.skip("missing crypto.generateKeyPairSync"); -} - -// Test sync key generation with key objects with a non-standard -// publicExponent -test("generateKeyPairSync with non-standard publicExponent", () => { - const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { - publicExponent: 3, - modulusLength: 512, - }); - - expect(typeof publicKey).toBe("object"); - expect(publicKey.type).toBe("public"); - expect(publicKey.asymmetricKeyType).toBe("rsa"); - expect(publicKey.asymmetricKeyDetails).toEqual({ - modulusLength: 512, - publicExponent: 3n, - }); - - expect(typeof privateKey).toBe("object"); - expect(privateKey.type).toBe("private"); - expect(privateKey.asymmetricKeyType).toBe("rsa"); - expect(privateKey.asymmetricKeyDetails).toEqual({ - modulusLength: 512, - publicExponent: 3n, - }); -}); - -//<#END_FILE: test-crypto-keygen-non-standard-public-exponent.js diff --git a/test/js/node/test/parallel/crypto-keygen-promisify.test.js b/test/js/node/test/parallel/crypto-keygen-promisify.test.js deleted file mode 100644 index 845d68a970..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-promisify.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-crypto-keygen-promisify.js -//#SHA1: 70eb7089a04950a2ce28a3af9949105eefd420fa -//----------------- -"use strict"; - -const crypto = require("crypto"); -const util = require("util"); - -const { - assertApproximateSize, - testEncryptDecrypt, - testSignVerify, - pkcs1PubExp, - pkcs1PrivExp, -} = require("../common/crypto"); - -// Skip the test if crypto support is not available -if (!crypto.generateKeyPair) { - test.skip("missing crypto support", () => {}); -} else { - // Test the util.promisified API with async RSA key generation. - test("util.promisified generateKeyPair with RSA", async () => { - const generateKeyPairPromise = util.promisify(crypto.generateKeyPair); - const keys = await generateKeyPairPromise("rsa", { - publicExponent: 0x10001, - modulusLength: 512, - publicKeyEncoding: { - type: "pkcs1", - format: "pem", - }, - privateKeyEncoding: { - type: "pkcs1", - format: "pem", - }, - }); - - const { publicKey, privateKey } = keys; - expect(typeof publicKey).toBe("string"); - expect(publicKey).toMatch(pkcs1PubExp); - assertApproximateSize(publicKey, 180); - - expect(typeof privateKey).toBe("string"); - expect(privateKey).toMatch(pkcs1PrivExp); - assertApproximateSize(privateKey, 512); - - testEncryptDecrypt(publicKey, privateKey); - testSignVerify(publicKey, privateKey); - }); -} - -//<#END_FILE: test-crypto-keygen-promisify.js diff --git a/test/js/node/test/parallel/crypto-keygen-sync.test.js b/test/js/node/test/parallel/crypto-keygen-sync.test.js deleted file mode 100644 index 26d3a218fb..0000000000 --- a/test/js/node/test/parallel/crypto-keygen-sync.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-crypto-keygen-sync.js -//#SHA1: 57749dc903b0d5f9b64a3d61f313be6e5549323f -//----------------- -"use strict"; - -const crypto = require("crypto"); -const { - assertApproximateSize, - testEncryptDecrypt, - testSignVerify, - pkcs1PubExp, - pkcs8Exp, -} = require("../common/crypto"); - -// Skip the test if crypto support is not available -if (typeof crypto.generateKeyPairSync !== "function") { - test.skip("missing crypto support", () => {}); -} else { - // To make the test faster, we will only test sync key generation once and - // with a relatively small key. - test("generateKeyPairSync", () => { - const ret = crypto.generateKeyPairSync("rsa", { - publicExponent: 3, - modulusLength: 512, - publicKeyEncoding: { - type: "pkcs1", - format: "pem", - }, - privateKeyEncoding: { - type: "pkcs8", - format: "pem", - }, - }); - - expect(Object.keys(ret)).toHaveLength(2); - const { publicKey, privateKey } = ret; - - expect(typeof publicKey).toBe("string"); - expect(publicKey).toMatch(pkcs1PubExp); - assertApproximateSize(publicKey, 162); - expect(typeof privateKey).toBe("string"); - expect(privateKey).toMatch(pkcs8Exp); - assertApproximateSize(privateKey, 512); - - testEncryptDecrypt(publicKey, privateKey); - testSignVerify(publicKey, privateKey); - }); -} - -//<#END_FILE: test-crypto-keygen-sync.js diff --git a/test/js/node/test/parallel/crypto-lazy-transform-writable.test.js b/test/js/node/test/parallel/crypto-lazy-transform-writable.test.js deleted file mode 100644 index 6df20ee83f..0000000000 --- a/test/js/node/test/parallel/crypto-lazy-transform-writable.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-crypto-lazy-transform-writable.js -//#SHA1: 29f694c4ea89a94302b3aa84677b5e41c73077d7 -//----------------- -"use strict"; - -const crypto = require("crypto"); -const Stream = require("stream"); - -if (!crypto) it.skip("missing crypto", () => {}); - -test("crypto lazy transform writable", done => { - const hasher1 = crypto.createHash("sha256"); - const hasher2 = crypto.createHash("sha256"); - - // Calculate the expected result. - hasher1.write(Buffer.from("hello world")); - hasher1.end(); - - const expected = hasher1.read().toString("hex"); - - class OldStream extends Stream { - constructor() { - super(); - this.readable = true; - } - } - - const stream = new OldStream(); - - stream.pipe(hasher2).on("finish", () => { - const hash = hasher2.read().toString("hex"); - expect(hash).toBe(expected); - done(); - }); - - stream.emit("data", Buffer.from("hello")); - stream.emit("data", Buffer.from(" world")); - stream.emit("end"); -}); - -//<#END_FILE: test-crypto-lazy-transform-writable.js diff --git a/test/js/node/test/parallel/crypto-padding-aes256.test.js b/test/js/node/test/parallel/crypto-padding-aes256.test.js deleted file mode 100644 index 079e7a15b6..0000000000 --- a/test/js/node/test/parallel/crypto-padding-aes256.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-crypto-padding-aes256.js -//#SHA1: 96fb5beb94bedbc768788ba2726dcd0e61733c5a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const crypto = require("crypto"); - -const iv = Buffer.from("00000000000000000000000000000000", "hex"); -const key = Buffer.from("0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef", "hex"); - -function encrypt(val, pad) { - const c = crypto.createCipheriv("aes256", key, iv); - c.setAutoPadding(pad); - return c.update(val, "utf8", "latin1") + c.final("latin1"); -} - -function decrypt(val, pad) { - const c = crypto.createDecipheriv("aes256", key, iv); - c.setAutoPadding(pad); - return c.update(val, "latin1", "utf8") + c.final("utf8"); -} - -test("AES256 encryption and decryption with no padding (multiple of block size)", () => { - // echo 0123456789abcdef0123456789abcdef \ - // | openssl enc -e -aes256 -nopad -K -iv \ - // | openssl enc -d -aes256 -nopad -K -iv - const plaintext = "0123456789abcdef0123456789abcdef"; // Multiple of block size - const encrypted = encrypt(plaintext, false); - const decrypted = decrypt(encrypted, false); - expect(decrypted).toBe(plaintext); -}); - -test("AES256 encryption and decryption with padding (not a multiple of block size)", () => { - // echo 0123456789abcdef0123456789abcde \ - // | openssl enc -e -aes256 -K -iv \ - // | openssl enc -d -aes256 -K -iv - const plaintext = "0123456789abcdef0123456789abcde"; // not a multiple - const encrypted = encrypt(plaintext, true); - const decrypted = decrypt(encrypted, true); - expect(decrypted).toBe(plaintext); -}); - -//<#END_FILE: test-crypto-padding-aes256.js diff --git a/test/js/node/test/parallel/crypto-randomfillsync-regression.test.js b/test/js/node/test/parallel/crypto-randomfillsync-regression.test.js deleted file mode 100644 index aa57030f2a..0000000000 --- a/test/js/node/test/parallel/crypto-randomfillsync-regression.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-crypto-randomfillsync-regression.js -//#SHA1: f37bc7cc1eab82ab93665b246c0d44e9fce8d112 -//----------------- -"use strict"; - -// Skip the test if crypto is not available -let randomFillSync; -try { - ({ randomFillSync } = require("crypto")); -} catch { - test.skip("missing crypto", () => {}); -} - -if (randomFillSync) { - test("randomFillSync regression test", () => { - const ab = new ArrayBuffer(20); - const buf = Buffer.from(ab, 10); - - const before = buf.toString("hex"); - - randomFillSync(buf); - - const after = buf.toString("hex"); - - expect(before).not.toBe(after); - }); -} - -//<#END_FILE: test-crypto-randomfillsync-regression.js diff --git a/test/js/node/test/parallel/crypto-subtle-zero-length.test.js b/test/js/node/test/parallel/crypto-subtle-zero-length.test.js deleted file mode 100644 index 9113969480..0000000000 --- a/test/js/node/test/parallel/crypto-subtle-zero-length.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-crypto-subtle-zero-length.js -//#SHA1: aa21bbc5fd9db7bc09dad3ec61cd743d655f5e3b -//----------------- -"use strict"; - -// Skip test if crypto is not available -if (typeof crypto === "undefined" || !crypto.subtle) { - test.skip("missing crypto"); -} - -test("SubtleCrypto with zero-length input", async () => { - const { subtle } = globalThis.crypto; - - const k = await subtle.importKey("raw", new Uint8Array(32), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); - expect(k).toBeInstanceOf(CryptoKey); - - const e = await subtle.encrypt( - { - name: "AES-GCM", - iv: new Uint8Array(12), - }, - k, - new Uint8Array(0), - ); - expect(e).toBeInstanceOf(ArrayBuffer); - expect(Buffer.from(e)).toEqual( - Buffer.from([0x53, 0x0f, 0x8a, 0xfb, 0xc7, 0x45, 0x36, 0xb9, 0xa9, 0x63, 0xb4, 0xf1, 0xc4, 0xcb, 0x73, 0x8b]), - ); - - const v = await subtle.decrypt( - { - name: "AES-GCM", - iv: new Uint8Array(12), - }, - k, - e, - ); - expect(v).toBeInstanceOf(ArrayBuffer); - expect(v.byteLength).toBe(0); -}); - -//<#END_FILE: test-crypto-subtle-zero-length.js diff --git a/test/js/node/test/parallel/crypto-update-encoding.test.js b/test/js/node/test/parallel/crypto-update-encoding.test.js deleted file mode 100644 index 1c0d269234..0000000000 --- a/test/js/node/test/parallel/crypto-update-encoding.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-crypto-update-encoding.js -//#SHA1: dfe3c7e71e22a772cf6b2e6a6540be161fda3418 -//----------------- -"use strict"; - -const crypto = require("crypto"); - -const zeros = Buffer.alloc; -const key = zeros(16); -const iv = zeros(16); - -const cipher = () => crypto.createCipheriv("aes-128-cbc", key, iv); -const decipher = () => crypto.createDecipheriv("aes-128-cbc", key, iv); -const hash = () => crypto.createSign("sha256"); -const hmac = () => crypto.createHmac("sha256", key); -const sign = () => crypto.createSign("sha256"); -const verify = () => crypto.createVerify("sha256"); - -test("crypto update ignores inputEncoding for Buffer input", () => { - const functions = [cipher, decipher, hash, hmac, sign, verify]; - const sizes = [15, 16]; - - functions.forEach(f => { - sizes.forEach(n => { - const instance = f(); - expect(() => { - instance.update(zeros(n), "hex"); - }).not.toThrow(); - }); - }); -}); - -//<#END_FILE: test-crypto-update-encoding.js diff --git a/test/js/node/test/parallel/crypto-webcrypto-aes-decrypt-tag-too-small.test.js b/test/js/node/test/parallel/crypto-webcrypto-aes-decrypt-tag-too-small.test.js deleted file mode 100644 index de241c13af..0000000000 --- a/test/js/node/test/parallel/crypto-webcrypto-aes-decrypt-tag-too-small.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-crypto-webcrypto-aes-decrypt-tag-too-small.js -//#SHA1: e58d2e4e7dcfc3a29a6e9acbe177f32a1d6bf280 -//----------------- -"use strict"; - -if (!globalThis.crypto?.subtle) { - test.skip("missing crypto"); -} - -test("AES-GCM decrypt with tag too small", async () => { - const { subtle } = globalThis.crypto; - - const key = await subtle.importKey( - "raw", - new Uint8Array(32), - { - name: "AES-GCM", - }, - false, - ["encrypt", "decrypt"], - ); - - await expect( - subtle.decrypt( - { - name: "AES-GCM", - iv: new Uint8Array(12), - }, - key, - new Uint8Array(0), - ), - ).rejects.toThrow( - expect.objectContaining({ - name: "OperationError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-crypto-webcrypto-aes-decrypt-tag-too-small.js diff --git a/test/js/node/test/parallel/dgram-abort-closed.test.js b/test/js/node/test/parallel/dgram-abort-closed.test.js deleted file mode 100644 index eb09ac67eb..0000000000 --- a/test/js/node/test/parallel/dgram-abort-closed.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-dgram-abort-closed.js -//#SHA1: 8d3ab4d13dda99cdccb6994f165f2ddacf58360c -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("AbortController with closed dgram socket", () => { - const controller = new AbortController(); - const socket = dgram.createSocket({ type: "udp4", signal: controller.signal }); - - socket.close(); - - // This should not throw or cause any issues - expect(() => { - controller.abort(); - }).not.toThrow(); -}); - -//<#END_FILE: test-dgram-abort-closed.js diff --git a/test/js/node/test/parallel/dgram-bind-default-address.test.js b/test/js/node/test/parallel/dgram-bind-default-address.test.js deleted file mode 100644 index 542f335c6e..0000000000 --- a/test/js/node/test/parallel/dgram-bind-default-address.test.js +++ /dev/null @@ -1,82 +0,0 @@ -//#FILE: test-dgram-bind-default-address.js -//#SHA1: f29269b15b1205e37cc43e02b76cc5d8eb3b70be -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -// Skip test in FreeBSD jails since 0.0.0.0 will resolve to default interface -const inFreeBSDJail = process.platform === "freebsd" && process.env.CI === "true"; -if (inFreeBSDJail) { - test.skip("In a FreeBSD jail"); -} - -test("UDP4 socket bind to default address", async () => { - const socket = dgram.createSocket("udp4"); - - await new Promise(resolve => { - socket.bind(0, () => { - const address = socket.address(); - expect(typeof address.port).toBe("number"); - expect(isFinite(address.port)).toBe(true); - expect(address.port).toBeGreaterThan(0); - expect(address.address).toBe("0.0.0.0"); - socket.close(); - resolve(); - }); - }); -}); - -const hasIPv6 = (() => { - try { - const socket = dgram.createSocket("udp6"); - socket.close(); - return true; - } catch { - return false; - } -})(); - -if (!hasIPv6) { - test.skip("udp6 part of test, because no IPv6 support"); -} else { - test("UDP6 socket bind to default address", async () => { - const socket = dgram.createSocket("udp6"); - - await new Promise(resolve => { - socket.bind(0, () => { - const address = socket.address(); - expect(typeof address.port).toBe("number"); - expect(isFinite(address.port)).toBe(true); - expect(address.port).toBeGreaterThan(0); - let addressValue = address.address; - if (addressValue === "::ffff:0.0.0.0") addressValue = "::"; - expect(addressValue).toBe("::"); - socket.close(); - resolve(); - }); - }); - }); -} - -//<#END_FILE: test-dgram-bind-default-address.js diff --git a/test/js/node/test/parallel/dgram-bind.test.js b/test/js/node/test/parallel/dgram-bind.test.js deleted file mode 100644 index 0bf412718a..0000000000 --- a/test/js/node/test/parallel/dgram-bind.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-dgram-bind.js -//#SHA1: 748fcd0fcb3ed5103b9072bba3019e560fb2799b -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -test("dgram socket bind", async () => { - const socket = dgram.createSocket("udp4"); - - await new Promise(resolve => { - socket.on("listening", () => { - expect(() => { - socket.bind(); - }).toThrow( - expect.objectContaining({ - code: "ERR_SOCKET_ALREADY_BOUND", - name: "Error", - message: expect.stringMatching(/^Socket is already bound$/), - }), - ); - - socket.close(); - resolve(); - }); - - const result = socket.bind(); // Should not throw. - - expect(result).toBe(socket); // Should have returned itself. - }); -}); - -//<#END_FILE: test-dgram-bind.js diff --git a/test/js/node/test/parallel/dgram-bytes-length.test.js b/test/js/node/test/parallel/dgram-bytes-length.test.js deleted file mode 100644 index 1c1bd1e219..0000000000 --- a/test/js/node/test/parallel/dgram-bytes-length.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-dgram-bytes-length.js -//#SHA1: f899cc14c13e8c913645e204819cf99b867aec5c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -test("dgram bytes length", async () => { - const message = Buffer.from("Some bytes"); - const client = dgram.createSocket("udp4"); - - await new Promise((resolve, reject) => { - client.send(message, 0, message.length, 41234, "localhost", function (err, bytes) { - if (err) reject(err); - expect(bytes).toBe(message.length); - client.close(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-dgram-bytes-length.js diff --git a/test/js/node/test/parallel/dgram-close-in-listening.test.js b/test/js/node/test/parallel/dgram-close-in-listening.test.js deleted file mode 100644 index e669bea76c..0000000000 --- a/test/js/node/test/parallel/dgram-close-in-listening.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-dgram-close-in-listening.js -//#SHA1: b37e742b092d70824b67c4ad4d3e1bb17a8c5cd5 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram socket closed before sendQueue is drained does not crash", done => { - const buf = Buffer.alloc(1024, 42); - - const socket = dgram.createSocket("udp4"); - - socket.on("listening", function () { - socket.close(); - }); - - // Get a random port for send - const portGetter = dgram.createSocket("udp4").bind(0, "localhost", () => { - // Adds a listener to 'listening' to send the data when - // the socket is available - socket.send(buf, 0, buf.length, portGetter.address().port, portGetter.address().address); - - portGetter.close(); - done(); // Signal test completion - }); -}); - -//<#END_FILE: test-dgram-close-in-listening.js diff --git a/test/js/node/test/parallel/dgram-close.test.js b/test/js/node/test/parallel/dgram-close.test.js deleted file mode 100644 index fe89cc0f66..0000000000 --- a/test/js/node/test/parallel/dgram-close.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-dgram-close.js -//#SHA1: c396ba7a9c9ef45206989b36e4b5db0b95503e38 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// Flags: --expose-internals -"use strict"; -// Ensure that if a dgram socket is closed before the DNS lookup completes, it -// won't crash. - -const dgram = require("dgram"); - -const buf = Buffer.alloc(1024, 42); - -test("dgram socket close before DNS lookup completes", done => { - let socket = dgram.createSocket("udp4"); - - // Get a random port for send - const portGetter = dgram.createSocket("udp4"); - - portGetter.bind(0, "localhost", () => { - socket.send(buf, 0, buf.length, portGetter.address().port, portGetter.address().address); - - expect(socket.close()).toBe(socket); - - socket.on("close", () => { - socket = null; - - // Verify that accessing handle after closure doesn't throw - setImmediate(() => { - setImmediate(() => { - // We can't access internal symbols, so we'll just check if this doesn't throw - expect(() => { - console.log("Handle fd is: ", "placeholder"); - }).not.toThrow(); - - portGetter.close(); - done(); - }); - }); - }); - }); -}); - -//<#END_FILE: test-dgram-close.js diff --git a/test/js/node/test/parallel/dgram-cluster-close-in-listening.test.js b/test/js/node/test/parallel/dgram-cluster-close-in-listening.test.js deleted file mode 100644 index 3a882a49d2..0000000000 --- a/test/js/node/test/parallel/dgram-cluster-close-in-listening.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-dgram-cluster-close-in-listening.js -//#SHA1: f288642fce76ef0138f8e44cd8eb09ded9dc4640 -//----------------- -"use strict"; -// Ensure that closing dgram sockets in 'listening' callbacks of cluster workers -// won't throw errors. - -const dgram = require("dgram"); -const cluster = require("cluster"); - -if (process.platform === "win32") { - it.skip("dgram clustering is currently not supported on windows.", () => {}); -} else { - if (cluster.isPrimary) { - test("Primary cluster forks workers", () => { - for (let i = 0; i < 3; i += 1) { - expect(() => cluster.fork()).not.toThrow(); - } - }); - } else { - test("Worker handles dgram socket lifecycle", done => { - const socket = dgram.createSocket("udp4"); - - socket.on("error", () => { - done(new Error("Error event should not be called")); - }); - - socket.on("listening", () => { - socket.close(); - }); - - socket.on("close", () => { - cluster.worker.disconnect(); - done(); - }); - - socket.bind(0); - }); - } -} - -//<#END_FILE: test-dgram-cluster-close-in-listening.js diff --git a/test/js/node/test/parallel/dgram-connect-send-callback-multi-buffer.test.js b/test/js/node/test/parallel/dgram-connect-send-callback-multi-buffer.test.js deleted file mode 100644 index 2d014e97c6..0000000000 --- a/test/js/node/test/parallel/dgram-connect-send-callback-multi-buffer.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-dgram-connect-send-callback-multi-buffer.js -//#SHA1: f30fbed996bbcd2adb268e9e3412a5f83119f8ae -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram connect send callback multi buffer", done => { - const client = dgram.createSocket("udp4"); - - const messageSent = jest.fn((err, bytes) => { - expect(bytes).toBe(buf1.length + buf2.length); - }); - - const buf1 = Buffer.alloc(256, "x"); - const buf2 = Buffer.alloc(256, "y"); - - client.on("listening", () => { - const port = client.address().port; - client.connect(port, () => { - client.send([buf1, buf2], messageSent); - }); - }); - - client.on("message", (buf, info) => { - const expected = Buffer.concat([buf1, buf2]); - expect(buf.equals(expected)).toBe(true); - client.close(); - expect(messageSent).toHaveBeenCalledTimes(1); - done(); - }); - - client.bind(0); -}); - -//<#END_FILE: test-dgram-connect-send-callback-multi-buffer.js diff --git a/test/js/node/test/parallel/dgram-connect-send-default-host.test.js b/test/js/node/test/parallel/dgram-connect-send-default-host.test.js deleted file mode 100644 index 6e597bae7c..0000000000 --- a/test/js/node/test/parallel/dgram-connect-send-default-host.test.js +++ /dev/null @@ -1,69 +0,0 @@ -//#FILE: test-dgram-connect-send-default-host.js -//#SHA1: 78d734d664f2bf2f6376846bba7c909d8253c4dc -//----------------- -"use strict"; - -const dgram = require("dgram"); - -const toSend = [Buffer.alloc(256, "x"), Buffer.alloc(256, "y"), Buffer.alloc(256, "z"), "hello"]; - -const received = []; - -test("dgram connect and send with default host", async () => { - const client = dgram.createSocket("udp4"); - const server = dgram.createSocket("udp4"); - - const serverListening = new Promise(resolve => { - server.on("listening", resolve); - }); - - server.on("message", (buf, info) => { - received.push(buf.toString()); - - if (received.length === toSend.length * 2) { - // The replies may arrive out of order -> sort them before checking. - received.sort(); - - const expected = toSend.concat(toSend).map(String).sort(); - expect(received).toEqual(expected); - client.close(); - server.close(); - } - }); - - server.bind(0); - - await serverListening; - - const port = server.address().port; - await new Promise((resolve, reject) => { - client.connect(port, err => { - if (err) reject(err); - else resolve(); - }); - }); - - client.send(toSend[0], 0, toSend[0].length); - client.send(toSend[1]); - client.send([toSend[2]]); - client.send(toSend[3], 0, toSend[3].length); - - client.send(new Uint8Array(toSend[0]), 0, toSend[0].length); - client.send(new Uint8Array(toSend[1])); - client.send([new Uint8Array(toSend[2])]); - client.send(new Uint8Array(Buffer.from(toSend[3])), 0, toSend[3].length); - - // Wait for all messages to be received - await new Promise(resolve => { - const checkInterval = setInterval(() => { - if (received.length === toSend.length * 2) { - clearInterval(checkInterval); - resolve(); - } - }, 100); - }); - - expect(received.length).toBe(toSend.length * 2); -}); - -//<#END_FILE: test-dgram-connect-send-default-host.js diff --git a/test/js/node/test/parallel/dgram-connect-send-empty-array.test.js b/test/js/node/test/parallel/dgram-connect-send-empty-array.test.js deleted file mode 100644 index 32b665f932..0000000000 --- a/test/js/node/test/parallel/dgram-connect-send-empty-array.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-dgram-connect-send-empty-array.js -//#SHA1: 81de5b211c0e3be3158d2c06178577f39e62f0d1 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram.connect() and send empty array", () => { - const client = dgram.createSocket("udp4"); - - expect.assertions(1); - - return new Promise(resolve => { - client.on("message", (buf, info) => { - const expected = Buffer.alloc(0); - expect(buf).toEqual(expected); - client.close(); - resolve(); - }); - - client.on("listening", () => { - client.connect(client.address().port, "127.0.0.1", () => client.send([])); - }); - - client.bind(0); - }); -}); - -//<#END_FILE: test-dgram-connect-send-empty-array.js diff --git a/test/js/node/test/parallel/dgram-connect-send-empty-buffer.test.js b/test/js/node/test/parallel/dgram-connect-send-empty-buffer.test.js deleted file mode 100644 index 7c4209dbf1..0000000000 --- a/test/js/node/test/parallel/dgram-connect-send-empty-buffer.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-dgram-connect-send-empty-buffer.js -//#SHA1: 08e8b667af8e6f97e6df2c95360a3a3aec05d435 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram connect and send empty buffer", done => { - const client = dgram.createSocket("udp4"); - - client.bind(0, () => { - const port = client.address().port; - client.connect(port, () => { - const buf = Buffer.alloc(0); - client.send(buf, 0, 0, err => { - expect(err).toBeNull(); - }); - }); - - client.on("message", buffer => { - expect(buffer.length).toBe(0); - client.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-dgram-connect-send-empty-buffer.js diff --git a/test/js/node/test/parallel/dgram-connect-send-empty-packet.test.js b/test/js/node/test/parallel/dgram-connect-send-empty-packet.test.js deleted file mode 100644 index d5f56ddb5d..0000000000 --- a/test/js/node/test/parallel/dgram-connect-send-empty-packet.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-dgram-connect-send-empty-packet.js -//#SHA1: 107d20a1e7a2628097091471ffdad75fc714b1fb -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram connect and send empty packet", done => { - const client = dgram.createSocket("udp4"); - - client.bind(0, () => { - expect.hasAssertions(); - client.connect(client.address().port, () => { - client.on("message", callback); - const buf = Buffer.alloc(1); - - const interval = setInterval(() => { - client.send(buf, 0, 0, callback); - }, 10); - - function callback(firstArg) { - // If client.send() callback, firstArg should be null. - // If client.on('message') listener, firstArg should be a 0-length buffer. - if (firstArg instanceof Buffer) { - expect(firstArg.length).toBe(0); - clearInterval(interval); - client.close(); - done(); - } - } - }); - }); -}); - -//<#END_FILE: test-dgram-connect-send-empty-packet.js diff --git a/test/js/node/test/parallel/dgram-connect-send-multi-string-array.test.js b/test/js/node/test/parallel/dgram-connect-send-multi-string-array.test.js deleted file mode 100644 index 7ae5672c0f..0000000000 --- a/test/js/node/test/parallel/dgram-connect-send-multi-string-array.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-dgram-connect-send-multi-string-array.js -//#SHA1: 611c15bc8089ffcae85adaa91bff5031c776a8ab -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram.createSocket can send multi-string array", done => { - const socket = dgram.createSocket("udp4"); - const data = ["foo", "bar", "baz"]; - - socket.on("message", (msg, rinfo) => { - socket.close(); - expect(msg.toString()).toBe(data.join("")); - done(); - }); - - socket.bind(0, () => { - socket.connect(socket.address().port, () => { - socket.send(data); - }); - }); -}); - -//<#END_FILE: test-dgram-connect-send-multi-string-array.js diff --git a/test/js/node/test/parallel/dgram-implicit-bind.test.js b/test/js/node/test/parallel/dgram-implicit-bind.test.js deleted file mode 100644 index d733e2109f..0000000000 --- a/test/js/node/test/parallel/dgram-implicit-bind.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-dgram-implicit-bind.js -//#SHA1: 3b390facaac3ee5e617c9fdc11acdb9f019fabfa -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -test("dgram implicit bind", async () => { - const source = dgram.createSocket("udp4"); - const target = dgram.createSocket("udp4"); - let messages = 0; - - const messageHandler = jest.fn(buf => { - if (buf.toString() === "abc") ++messages; - if (buf.toString() === "def") ++messages; - if (messages === 2) { - source.close(); - target.close(); - } - }); - - target.on("message", messageHandler); - - await new Promise(resolve => { - target.on("listening", resolve); - target.bind(0); - }); - - // Second .send() call should not throw a bind error. - const port = target.address().port; - source.send(Buffer.from("abc"), 0, 3, port, "127.0.0.1"); - source.send(Buffer.from("def"), 0, 3, port, "127.0.0.1"); - - // Wait for the messages to be processed - await new Promise(resolve => setTimeout(resolve, 100)); - - expect(messageHandler).toHaveBeenCalledTimes(2); - expect(messages).toBe(2); -}); - -//<#END_FILE: test-dgram-implicit-bind.js diff --git a/test/js/node/test/parallel/dgram-listen-after-bind.test.js b/test/js/node/test/parallel/dgram-listen-after-bind.test.js deleted file mode 100644 index fd8faa3fe8..0000000000 --- a/test/js/node/test/parallel/dgram-listen-after-bind.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-dgram-listen-after-bind.js -//#SHA1: c1a91f2b83b502dd1abc4b46f023df6677fdf465 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -test("dgram listen after bind", done => { - const socket = dgram.createSocket("udp4"); - - socket.bind(); - - let fired = false; - const timer = setTimeout(() => { - socket.close(); - }, 100); - - socket.on("listening", () => { - clearTimeout(timer); - fired = true; - socket.close(); - }); - - socket.on("close", () => { - expect(fired).toBe(true); - done(); - }); -}); - -//<#END_FILE: test-dgram-listen-after-bind.js diff --git a/test/js/node/test/parallel/dgram-oob-buffer.test.js b/test/js/node/test/parallel/dgram-oob-buffer.test.js deleted file mode 100644 index 4d1eae8968..0000000000 --- a/test/js/node/test/parallel/dgram-oob-buffer.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-dgram-oob-buffer.js -//#SHA1: a851da9a2178e92ce8315294d7cebf6eb78eb4bd -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Some operating systems report errors when an UDP message is sent to an -// unreachable host. This error can be reported by sendto() and even by -// recvfrom(). Node should not propagate this error to the user. - -const dgram = require("dgram"); - -test("UDP message sent to unreachable host should not propagate error", async () => { - const socket = dgram.createSocket("udp4"); - const buf = Buffer.from([1, 2, 3, 4]); - - const portGetter = dgram.createSocket("udp4"); - - await new Promise(resolve => { - portGetter.bind(0, "localhost", () => { - const { address, port } = portGetter.address(); - - portGetter.close(() => { - const sendCallback = jest.fn(); - - socket.send(buf, 0, 0, port, address, sendCallback); - socket.send(buf, 0, 4, port, address, sendCallback); - socket.send(buf, 1, 3, port, address, sendCallback); - socket.send(buf, 3, 1, port, address, sendCallback); - // Since length of zero means nothing, don't error despite OOB. - socket.send(buf, 4, 0, port, address, sendCallback); - - socket.close(); - - // We expect the sendCallback to not be called - expect(sendCallback).not.toHaveBeenCalled(); - - resolve(); - }); - }); - }); -}); - -//<#END_FILE: test-dgram-oob-buffer.js diff --git a/test/js/node/test/parallel/dgram-ref.test.js b/test/js/node/test/parallel/dgram-ref.test.js deleted file mode 100644 index bbb6602414..0000000000 --- a/test/js/node/test/parallel/dgram-ref.test.js +++ /dev/null @@ -1,44 +0,0 @@ -//#FILE: test-dgram-ref.js -//#SHA1: b1a50859a1784815d575d8203f7da20fe8d07e50 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -test("Should not hang when creating UDP sockets", () => { - // Should not hang, see https://github.com/nodejs/node-v0.x-archive/issues/1282 - expect(() => dgram.createSocket("udp4")).not.toThrow(); - expect(() => dgram.createSocket("udp6")).not.toThrow(); -}); - -test("Test ref() on a closed socket", done => { - // Test the case of ref()'ing a socket with no handle. - const s = dgram.createSocket("udp4"); - - s.close(() => { - expect(() => s.ref()).not.toThrow(); - done(); - }); -}); - -//<#END_FILE: test-dgram-ref.js diff --git a/test/js/node/test/parallel/dgram-send-callback-buffer-empty-address.test.js b/test/js/node/test/parallel/dgram-send-callback-buffer-empty-address.test.js deleted file mode 100644 index de86cfc036..0000000000 --- a/test/js/node/test/parallel/dgram-send-callback-buffer-empty-address.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-dgram-send-callback-buffer-empty-address.js -//#SHA1: 5c76ad150693dcec8921099fa994f61aa783713c -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram send callback with buffer and empty address", done => { - const client = dgram.createSocket("udp4"); - - const buf = Buffer.alloc(256, "x"); - - const onMessage = jest.fn(bytes => { - expect(bytes).toBe(buf.length); - client.close(); - done(); - }); - - client.bind(0, () => { - client.send(buf, client.address().port, error => { - expect(error).toBeFalsy(); - onMessage(buf.length); - }); - }); -}); - -//<#END_FILE: test-dgram-send-callback-buffer-empty-address.js diff --git a/test/js/node/test/parallel/dgram-send-callback-multi-buffer-empty-address.test.js b/test/js/node/test/parallel/dgram-send-callback-multi-buffer-empty-address.test.js deleted file mode 100644 index 429e4cd9eb..0000000000 --- a/test/js/node/test/parallel/dgram-send-callback-multi-buffer-empty-address.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-dgram-send-callback-multi-buffer-empty-address.js -//#SHA1: 61d00ee31b25f144989e0d3ced884a70f4e7d07a -//----------------- -"use strict"; - -const dgram = require("dgram"); - -let client; - -beforeEach(() => { - client = dgram.createSocket("udp4"); -}); - -afterEach(() => { - client.close(); -}); - -test("send callback multi buffer empty address", done => { - const buf1 = Buffer.alloc(256, "x"); - const buf2 = Buffer.alloc(256, "y"); - - client.on("listening", function () { - const port = this.address().port; - client.send([buf1, buf2], port, (err, bytes) => { - expect(err).toBeNull(); - expect(bytes).toBe(buf1.length + buf2.length); - }); - }); - - client.on("message", buf => { - const expected = Buffer.concat([buf1, buf2]); - expect(buf.equals(expected)).toBe(true); - done(); - }); - - client.bind(0); -}); - -//<#END_FILE: test-dgram-send-callback-multi-buffer-empty-address.js diff --git a/test/js/node/test/parallel/dgram-send-callback-multi-buffer.test.js b/test/js/node/test/parallel/dgram-send-callback-multi-buffer.test.js deleted file mode 100644 index 36cf0d1eaa..0000000000 --- a/test/js/node/test/parallel/dgram-send-callback-multi-buffer.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-dgram-send-callback-multi-buffer.js -//#SHA1: 622d513f7897c216601b50a2960a8a36259b2595 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram send callback with multiple buffers", done => { - const client = dgram.createSocket("udp4"); - - const messageSent = jest.fn((err, bytes) => { - expect(err).toBeNull(); - expect(bytes).toBe(buf1.length + buf2.length); - }); - - const buf1 = Buffer.alloc(256, "x"); - const buf2 = Buffer.alloc(256, "y"); - - client.on("listening", () => { - const port = client.address().port; - client.send([buf1, buf2], port, "localhost", messageSent); - }); - - client.on("message", (buf, info) => { - const expected = Buffer.concat([buf1, buf2]); - expect(buf.equals(expected)).toBe(true); - expect(messageSent).toHaveBeenCalledTimes(1); - client.close(); - done(); - }); - - client.bind(0); -}); - -//<#END_FILE: test-dgram-send-callback-multi-buffer.js diff --git a/test/js/node/test/parallel/dgram-send-callback-recursive.test.js b/test/js/node/test/parallel/dgram-send-callback-recursive.test.js deleted file mode 100644 index e42d990c9f..0000000000 --- a/test/js/node/test/parallel/dgram-send-callback-recursive.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-dgram-send-callback-recursive.js -//#SHA1: fac7c8b29bd2122d4de273c54128b5a6100ad437 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -let received = 0; -let sent = 0; -const limit = 10; -let async = false; -let port; -const chunk = "abc"; - -test("dgram send callback recursive", done => { - const client = dgram.createSocket("udp4"); - - function onsend() { - if (sent++ < limit) { - client.send(chunk, 0, chunk.length, port, "127.0.0.1", onsend); - } else { - expect(async).toBe(true); - } - } - - client.on("listening", function () { - port = this.address().port; - - process.nextTick(() => { - async = true; - }); - - onsend(); - }); - - client.on("message", (buf, info) => { - received++; - if (received === limit) { - client.close(); - } - }); - - client.on("close", () => { - expect(received).toBe(limit); - done(); - }); - - client.bind(0); -}); - -//<#END_FILE: test-dgram-send-callback-recursive.js diff --git a/test/js/node/test/parallel/dgram-send-cb-quelches-error.test.js b/test/js/node/test/parallel/dgram-send-cb-quelches-error.test.js deleted file mode 100644 index 96364d73fd..0000000000 --- a/test/js/node/test/parallel/dgram-send-cb-quelches-error.test.js +++ /dev/null @@ -1,44 +0,0 @@ -//#FILE: test-dgram-send-cb-quelches-error.js -//#SHA1: 7525b0a8af0df192c36a848b23332424245d2937 -//----------------- -"use strict"; - -const assert = require("assert"); -const dgram = require("dgram"); -const dns = require("dns"); - -test("dgram send callback quelches error", () => { - const socket = dgram.createSocket("udp4"); - const buffer = Buffer.from("gary busey"); - - dns.setServers([]); - - const onEvent = jest.fn(() => { - throw new Error("Error should not be emitted if there is callback"); - }); - - socket.once("error", onEvent); - - // assert that: - // * callbacks act as "error" listeners if given. - // * error is never emitter for missing dns entries - // if a callback that handles error is present - // * error is emitted if a callback with no argument is passed - socket.send(buffer, 0, buffer.length, 100, "dne.example.com", callbackOnly); - - function callbackOnly(err) { - expect(err).toBeTruthy(); - socket.removeListener("error", onEvent); - socket.on("error", onError); - socket.send(buffer, 0, buffer.length, 100, "dne.invalid"); - } - - function onError(err) { - expect(err).toBeTruthy(); - socket.close(); - } - - expect(onEvent).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-dgram-send-cb-quelches-error.js diff --git a/test/js/node/test/parallel/dgram-send-empty-buffer.test.js b/test/js/node/test/parallel/dgram-send-empty-buffer.test.js deleted file mode 100644 index 3dcd6ffe3b..0000000000 --- a/test/js/node/test/parallel/dgram-send-empty-buffer.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-dgram-send-empty-buffer.js -//#SHA1: ac60fc545252e681b648a7038d1bebe46ffbbac0 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -test("dgram send empty buffer", done => { - const client = dgram.createSocket("udp4"); - - client.bind(0, () => { - const port = client.address().port; - - client.on("message", buffer => { - expect(buffer.length).toBe(0); - clearInterval(interval); - client.close(); - done(); - }); - - const buf = Buffer.alloc(0); - const interval = setInterval(() => { - client.send(buf, 0, 0, port, "127.0.0.1", () => { - // This callback is expected to be called - }); - }, 10); - }); -}); - -//<#END_FILE: test-dgram-send-empty-buffer.js diff --git a/test/js/node/test/parallel/dgram-send-empty-packet.test.js b/test/js/node/test/parallel/dgram-send-empty-packet.test.js deleted file mode 100644 index 07802512c7..0000000000 --- a/test/js/node/test/parallel/dgram-send-empty-packet.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-dgram-send-empty-packet.js -//#SHA1: f39fb8a7245893f0f6f55aeb110d458e2f265013 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("send empty packet", done => { - const client = dgram.createSocket("udp4"); - - client.bind(0, () => { - client.on("message", jest.fn(callback)); - - const port = client.address().port; - const buf = Buffer.alloc(1); - - const interval = setInterval(() => { - client.send(buf, 0, 0, port, "127.0.0.1", jest.fn(callback)); - }, 10); - - function callback(firstArg) { - // If client.send() callback, firstArg should be null. - // If client.on('message') listener, firstArg should be a 0-length buffer. - if (firstArg instanceof Buffer) { - expect(firstArg.length).toBe(0); - clearInterval(interval); - client.close(); - done(); - } - } - }); -}); - -//<#END_FILE: test-dgram-send-empty-packet.js diff --git a/test/js/node/test/parallel/dgram-send-multi-buffer-copy.test.js b/test/js/node/test/parallel/dgram-send-multi-buffer-copy.test.js deleted file mode 100644 index b9baba6443..0000000000 --- a/test/js/node/test/parallel/dgram-send-multi-buffer-copy.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-dgram-send-multi-buffer-copy.js -//#SHA1: 6adf8291a5dd40cb6a71ad3779f0d26d2150249a -//----------------- -"use strict"; - -const dgram = require("dgram"); - -let client; - -beforeEach(() => { - client = dgram.createSocket("udp4"); -}); - -afterEach(() => { - client.close(); -}); - -test("dgram send multi buffer copy", done => { - const onMessage = jest.fn((err, bytes) => { - expect(bytes).toBe(buf1.length + buf2.length); - }); - - const buf1 = Buffer.alloc(256, "x"); - const buf2 = Buffer.alloc(256, "y"); - - client.on("listening", function () { - const toSend = [buf1, buf2]; - client.send(toSend, this.address().port, "127.0.0.1", onMessage); - toSend.splice(0, 2); - }); - - client.on("message", (buf, info) => { - const expected = Buffer.concat([buf1, buf2]); - expect(buf.equals(expected)).toBe(true); - expect(onMessage).toHaveBeenCalledTimes(1); - done(); - }); - - client.bind(0); -}); - -//<#END_FILE: test-dgram-send-multi-buffer-copy.js diff --git a/test/js/node/test/parallel/dgram-send-multi-string-array.test.js b/test/js/node/test/parallel/dgram-send-multi-string-array.test.js deleted file mode 100644 index 804ea7c285..0000000000 --- a/test/js/node/test/parallel/dgram-send-multi-string-array.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-dgram-send-multi-string-array.js -//#SHA1: 8ea2007ac52bfde3742aabe352aab19bf91a4ac2 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -test("dgram send multiple strings as array", done => { - const socket = dgram.createSocket("udp4"); - const data = ["foo", "bar", "baz"]; - - socket.on("message", (msg, rinfo) => { - socket.close(); - expect(msg.toString()).toBe(data.join("")); - done(); - }); - - socket.bind(() => { - socket.send(data, socket.address().port, "localhost"); - }); -}); - -//<#END_FILE: test-dgram-send-multi-string-array.js diff --git a/test/js/node/test/parallel/dgram-sendto.test.js b/test/js/node/test/parallel/dgram-sendto.test.js deleted file mode 100644 index e3b3d88c2a..0000000000 --- a/test/js/node/test/parallel/dgram-sendto.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-dgram-sendto.js -//#SHA1: 8047210c86bed6536f5ff3132e228a2ee9d0bb11 -//----------------- -"use strict"; - -const dgram = require("dgram"); - -describe("dgram.sendto", () => { - let socket; - - beforeEach(() => { - socket = dgram.createSocket("udp4"); - }); - - afterEach(() => { - socket.close(); - }); - - test("throws when called with no arguments", () => { - expect(() => socket.sendto()).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - - test('throws when "length" argument is invalid', () => { - expect(() => socket.sendto("buffer", 1, "offset", "port", "address", "cb")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - - test('throws when "offset" argument is invalid', () => { - expect(() => socket.sendto("buffer", "offset", 1, "port", "address", "cb")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - - test('throws when "address" argument is invalid', () => { - expect(() => socket.sendto("buffer", 1, 1, 10, false, "cb")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - - test('throws when "port" argument is invalid', () => { - expect(() => socket.sendto("buffer", 1, 1, false, "address", "cb")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-dgram-sendto.js diff --git a/test/js/node/test/parallel/dgram-udp4.test.js b/test/js/node/test/parallel/dgram-udp4.test.js deleted file mode 100644 index 27be2e63fc..0000000000 --- a/test/js/node/test/parallel/dgram-udp4.test.js +++ /dev/null @@ -1,81 +0,0 @@ -//#FILE: test-dgram-udp4.js -//#SHA1: 588735591046212fc512f69d9001ccb820c57a71 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -const message_to_send = "A message to send"; -const localhostIPv4 = "127.0.0.1"; - -test("UDP4 server and client communication", async () => { - const server = dgram.createSocket("udp4"); - - const serverMessagePromise = new Promise(resolve => { - server.on("message", (msg, rinfo) => { - expect(rinfo.address).toBe(localhostIPv4); - expect(msg.toString()).toBe(message_to_send); - server.send(msg, 0, msg.length, rinfo.port, rinfo.address); - resolve(); - }); - }); - - const listeningPromise = new Promise(resolve => { - server.on("listening", resolve); - }); - - server.bind(0); - - await listeningPromise; - - const client = dgram.createSocket("udp4"); - const port = server.address().port; - - const clientMessagePromise = new Promise(resolve => { - client.on("message", (msg, rinfo) => { - expect(rinfo.address).toBe(localhostIPv4); - expect(rinfo.port).toBe(port); - expect(msg.toString()).toBe(message_to_send); - resolve(); - }); - }); - - client.send(message_to_send, 0, message_to_send.length, port, "localhost"); - - await Promise.all([serverMessagePromise, clientMessagePromise]); - - const clientClosePromise = new Promise(resolve => { - client.on("close", resolve); - }); - - const serverClosePromise = new Promise(resolve => { - server.on("close", resolve); - }); - - client.close(); - server.close(); - - await Promise.all([clientClosePromise, serverClosePromise]); -}); - -//<#END_FILE: test-dgram-udp4.js diff --git a/test/js/node/test/parallel/dgram-unref-in-cluster.test.js b/test/js/node/test/parallel/dgram-unref-in-cluster.test.js deleted file mode 100644 index dcca2428e3..0000000000 --- a/test/js/node/test/parallel/dgram-unref-in-cluster.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-dgram-unref-in-cluster.js -//#SHA1: eca71c33b1bf5be34a28e6cc82df49c73e775153 -//----------------- -"use strict"; - -const dgram = require("dgram"); -const cluster = require("cluster"); - -if (process.platform === "win32") { - test.skip("dgram clustering is currently not supported on Windows."); -} else { - if (cluster.isPrimary) { - test("dgram unref in cluster", () => { - cluster.fork(); - }); - } else { - test("dgram unref in cluster worker", () => { - const socket = dgram.createSocket("udp4"); - socket.unref(); - socket.bind(); - - return new Promise(resolve => { - socket.on("listening", () => { - const sockets = process.getActiveResourcesInfo().filter(item => { - return item === "UDPWrap"; - }); - expect(sockets.length).toBe(0); - process.disconnect(); - resolve(); - }); - }); - }); - } -} - -//<#END_FILE: test-dgram-unref-in-cluster.js diff --git a/test/js/node/test/parallel/dgram-unref.test.js b/test/js/node/test/parallel/dgram-unref.test.js deleted file mode 100644 index ff11989d7b..0000000000 --- a/test/js/node/test/parallel/dgram-unref.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-dgram-unref.js -//#SHA1: 97b218a9107def7e2cc28e595f84f9a05a606850 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const dgram = require("dgram"); - -test("unref() a socket with a handle", () => { - const s = dgram.createSocket("udp4"); - s.bind(); - s.unref(); - // No assertion needed, just checking that it doesn't throw -}); - -test("unref() a socket with no handle", done => { - const s = dgram.createSocket("udp4"); - s.close(() => { - s.unref(); - done(); - }); -}); - -test("setTimeout should not be called", () => { - const mockCallback = jest.fn(); - setTimeout(mockCallback, 1000).unref(); - expect(mockCallback).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-dgram-unref.js diff --git a/test/js/node/test/parallel/diagnostics-channel-has-subscribers.test.js b/test/js/node/test/parallel/diagnostics-channel-has-subscribers.test.js deleted file mode 100644 index 2e2f119c73..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-has-subscribers.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-diagnostics-channel-has-subscribers.js -//#SHA1: 8040abda8d37916d6f6d5f3966e8c531d1770e1a -//----------------- -"use strict"; - -const { channel, hasSubscribers } = require("diagnostics_channel"); - -describe("diagnostics_channel", () => { - test("hasSubscribers returns correct state", () => { - const dc = channel("test"); - expect(hasSubscribers("test")).toBe(false); - - dc.subscribe(() => {}); - expect(hasSubscribers("test")).toBe(true); - }); -}); - -//<#END_FILE: test-diagnostics-channel-has-subscribers.js diff --git a/test/js/node/test/parallel/diagnostics-channel-http-server-start.test.js b/test/js/node/test/parallel/diagnostics-channel-http-server-start.test.js deleted file mode 100644 index a5446eec21..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-http-server-start.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-diagnostics-channel-http-server-start.js -//#SHA1: 5540b17246152983b832ddafa0be55502ee83a23 -//----------------- -"use strict"; - -const { AsyncLocalStorage } = require("async_hooks"); -const dc = require("diagnostics_channel"); -const http = require("http"); - -const als = new AsyncLocalStorage(); -let context; - -describe("diagnostics_channel http server start", () => { - let server; - let request; - let response; - - beforeAll(() => { - // Bind requests to an AsyncLocalStorage context - dc.subscribe("http.server.request.start", message => { - als.enterWith(message); - context = message; - }); - - // When the request ends, verify the context has been maintained - // and that the messages contain the expected data - dc.subscribe("http.server.response.finish", message => { - const data = { - request, - response, - server, - socket: request.socket, - }; - - // Context is maintained - compare(als.getStore(), context); - - compare(context, data); - compare(message, data); - }); - - server = http.createServer((req, res) => { - request = req; - response = res; - - setTimeout(() => { - res.end("done"); - }, 1); - }); - }); - - afterAll(() => { - server.close(); - }); - - it("should maintain context and contain expected data", done => { - server.listen(() => { - const { port } = server.address(); - http.get(`http://localhost:${port}`, res => { - res.resume(); - res.on("end", () => { - done(); - }); - }); - }); - }); -}); - -function compare(a, b) { - expect(a.request).toBe(b.request); - expect(a.response).toBe(b.response); - expect(a.socket).toBe(b.socket); - expect(a.server).toBe(b.server); -} - -//<#END_FILE: test-diagnostics-channel-http-server-start.js diff --git a/test/js/node/test/parallel/diagnostics-channel-object-channel-pub-sub.test.js b/test/js/node/test/parallel/diagnostics-channel-object-channel-pub-sub.test.js deleted file mode 100644 index ae95ea487b..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-object-channel-pub-sub.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-diagnostics-channel-object-channel-pub-sub.js -//#SHA1: 185a8134da179dfda5f6b2d1a1a2622752431a90 -//----------------- -"use strict"; - -const dc = require("diagnostics_channel"); -const { Channel } = dc; - -const input = { - foo: "bar", -}; - -test("Channel creation and subscription", () => { - // Should not have named channel - expect(dc.hasSubscribers("test")).toBe(false); - - // Individual channel objects can be created to avoid future lookups - const channel = dc.channel("test"); - expect(channel).toBeInstanceOf(Channel); - - // No subscribers yet, should not publish - expect(channel.hasSubscribers).toBe(false); - - const subscriber = jest.fn((message, name) => { - expect(name).toBe(channel.name); - expect(message).toEqual(input); - }); - - // Now there's a subscriber, should publish - channel.subscribe(subscriber); - expect(channel.hasSubscribers).toBe(true); - - // The ActiveChannel prototype swap should not fail instanceof - expect(channel).toBeInstanceOf(Channel); - - // Should trigger the subscriber once - channel.publish(input); - expect(subscriber).toHaveBeenCalledTimes(1); - - // Should not publish after subscriber is unsubscribed - expect(channel.unsubscribe(subscriber)).toBe(true); - expect(channel.hasSubscribers).toBe(false); - - // unsubscribe() should return false when subscriber is not found - expect(channel.unsubscribe(subscriber)).toBe(false); -}); - -test("Invalid subscriber", () => { - const channel = dc.channel("test"); - expect(() => { - channel.subscribe(null); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-diagnostics-channel-object-channel-pub-sub.js diff --git a/test/js/node/test/parallel/diagnostics-channel-pub-sub.test.js b/test/js/node/test/parallel/diagnostics-channel-pub-sub.test.js deleted file mode 100644 index f20d19d136..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-pub-sub.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-diagnostics-channel-pub-sub.js -//#SHA1: cc5eee7f44117c2fd8446e8050ac19f5341f85a5 -//----------------- -"use strict"; - -const dc = require("diagnostics_channel"); -const { Channel } = dc; - -const name = "test"; -const input = { - foo: "bar", -}; - -test("diagnostics_channel pub/sub functionality", () => { - // Individual channel objects can be created to avoid future lookups - const channel = dc.channel(name); - expect(channel).toBeInstanceOf(Channel); - - // No subscribers yet, should not publish - expect(channel.hasSubscribers).toBeFalsy(); - - const subscriber = jest.fn((message, channelName) => { - expect(channelName).toBe(channel.name); - expect(message).toEqual(input); - }); - - // Now there's a subscriber, should publish - dc.subscribe(name, subscriber); - expect(channel.hasSubscribers).toBeTruthy(); - - // The ActiveChannel prototype swap should not fail instanceof - expect(channel).toBeInstanceOf(Channel); - - // Should trigger the subscriber once - channel.publish(input); - expect(subscriber).toHaveBeenCalledTimes(1); - - // Should not publish after subscriber is unsubscribed - expect(dc.unsubscribe(name, subscriber)).toBeTruthy(); - expect(channel.hasSubscribers).toBeFalsy(); - - // unsubscribe() should return false when subscriber is not found - expect(dc.unsubscribe(name, subscriber)).toBeFalsy(); - - expect(() => { - dc.subscribe(name, null); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - - // Reaching zero subscribers should not delete from the channels map as there - // will be no more weakref to incRef if another subscribe happens while the - // channel object itself exists. - channel.subscribe(subscriber); - channel.unsubscribe(subscriber); - channel.subscribe(subscriber); -}); - -//<#END_FILE: test-diagnostics-channel-pub-sub.js diff --git a/test/js/node/test/parallel/diagnostics-channel-symbol-named.test.js b/test/js/node/test/parallel/diagnostics-channel-symbol-named.test.js deleted file mode 100644 index d2d6065687..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-symbol-named.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-diagnostics-channel-symbol-named.js -//#SHA1: e0ae87b803333891439e11fa2306eefd23b507e4 -//----------------- -"use strict"; - -const dc = require("diagnostics_channel"); - -const input = { - foo: "bar", -}; - -const symbol = Symbol("test"); - -// Individual channel objects can be created to avoid future lookups -const channel = dc.channel(symbol); - -test("diagnostics channel with symbol name", () => { - // Expect two successful publishes later - const subscriber = jest.fn((message, name) => { - expect(name).toBe(symbol); - expect(message).toEqual(input); - }); - - channel.subscribe(subscriber); - - channel.publish(input); - - expect(subscriber).toHaveBeenCalledTimes(1); -}); - -test("channel creation with invalid argument", () => { - expect(() => { - dc.channel(null); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-diagnostics-channel-symbol-named.js diff --git a/test/js/node/test/parallel/diagnostics-channel-sync-unsubscribe.test.js b/test/js/node/test/parallel/diagnostics-channel-sync-unsubscribe.test.js deleted file mode 100644 index eb5ceea25b..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-sync-unsubscribe.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-diagnostics-channel-sync-unsubscribe.js -//#SHA1: 85d73bd9ce82a4293daca05617b2aece359745ff -//----------------- -"use strict"; - -const dc = require("node:diagnostics_channel"); - -const channel_name = "test:channel"; -const published_data = "some message"; - -test("diagnostics channel sync unsubscribe", () => { - const onMessageHandler = jest.fn(() => dc.unsubscribe(channel_name, onMessageHandler)); - - dc.subscribe(channel_name, onMessageHandler); - - // This must not throw. - expect(() => { - dc.channel(channel_name).publish(published_data); - }).not.toThrow(); - - expect(onMessageHandler).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-diagnostics-channel-sync-unsubscribe.js diff --git a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-callback-run-stores.test.js b/test/js/node/test/parallel/diagnostics-channel-tracing-channel-callback-run-stores.test.js deleted file mode 100644 index 15fcdedaf9..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-callback-run-stores.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-diagnostics-channel-tracing-channel-callback-run-stores.js -//#SHA1: 8ed3a87eb9d6c1a3a624245ce5f430b7e3730d2d -//----------------- -"use strict"; - -const { AsyncLocalStorage } = require("async_hooks"); -const dc = require("diagnostics_channel"); - -const channel = dc.tracingChannel("test"); -const store = new AsyncLocalStorage(); - -const firstContext = { foo: "bar" }; -const secondContext = { baz: "buz" }; - -test("tracing channel callback run stores", async () => { - const startBindStoreMock = jest.fn(() => firstContext); - const asyncStartBindStoreMock = jest.fn(() => secondContext); - - channel.start.bindStore(store, startBindStoreMock); - channel.asyncStart.bindStore(store, asyncStartBindStoreMock); - - expect(store.getStore()).toBeUndefined(); - - await new Promise(resolve => { - channel.traceCallback( - cb => { - expect(store.getStore()).toEqual(firstContext); - setImmediate(cb); - }, - 0, - {}, - null, - () => { - expect(store.getStore()).toEqual(secondContext); - resolve(); - }, - ); - }); - - expect(store.getStore()).toBeUndefined(); - expect(startBindStoreMock).toHaveBeenCalledTimes(1); - expect(asyncStartBindStoreMock).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-diagnostics-channel-tracing-channel-callback-run-stores.js diff --git a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-promise-run-stores.test.js b/test/js/node/test/parallel/diagnostics-channel-tracing-channel-promise-run-stores.test.js deleted file mode 100644 index 6c75c54789..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-promise-run-stores.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-diagnostics-channel-tracing-channel-promise-run-stores.js -//#SHA1: 4e5abb65d92cb3e649073803971180493c1a30ca -//----------------- -"use strict"; - -const { setTimeout } = require("node:timers/promises"); -const { AsyncLocalStorage } = require("async_hooks"); -const dc = require("diagnostics_channel"); - -const channel = dc.tracingChannel("test"); -const store = new AsyncLocalStorage(); - -const firstContext = { foo: "bar" }; -const secondContext = { baz: "buz" }; - -test("tracingChannel promise run stores", async () => { - const startBindStoreMock = jest.fn(() => firstContext); - const asyncStartBindStoreMock = jest.fn(() => secondContext); - - channel.start.bindStore(store, startBindStoreMock); - channel.asyncStart.bindStore(store, asyncStartBindStoreMock); - - expect(store.getStore()).toBeUndefined(); - - await channel.tracePromise(async () => { - expect(store.getStore()).toEqual(firstContext); - await setTimeout(1); - // Should _not_ switch to second context as promises don't have an "after" - // point at which to do a runStores. - expect(store.getStore()).toEqual(firstContext); - }); - - expect(store.getStore()).toBeUndefined(); - - expect(startBindStoreMock).toHaveBeenCalledTimes(1); - expect(asyncStartBindStoreMock).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-diagnostics-channel-tracing-channel-promise-run-stores.js diff --git a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-promise.test.js b/test/js/node/test/parallel/diagnostics-channel-tracing-channel-promise.test.js deleted file mode 100644 index 032a552cb6..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-promise.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-diagnostics-channel-tracing-channel-promise.js -//#SHA1: 6a692d8400685da6c930b662562adb3e36b2da9a -//----------------- -"use strict"; - -const dc = require("diagnostics_channel"); - -const channel = dc.tracingChannel("test"); - -const expectedResult = { foo: "bar" }; -const input = { foo: "bar" }; -const thisArg = { baz: "buz" }; - -function check(found) { - expect(found).toEqual(input); -} - -function checkAsync(found) { - check(found); - expect(found.error).toBeUndefined(); - expect(found.result).toEqual(expectedResult); -} - -const handlers = { - start: jest.fn(check), - end: jest.fn(check), - asyncStart: jest.fn(checkAsync), - asyncEnd: jest.fn(checkAsync), - error: jest.fn(), -}; - -test("diagnostics_channel tracing channel promise", async () => { - channel.subscribe(handlers); - - await channel.tracePromise( - function (value) { - expect(this).toEqual(thisArg); - return Promise.resolve(value); - }, - input, - thisArg, - expectedResult, - ); - - expect(handlers.start).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.asyncStart).toHaveBeenCalledTimes(1); - expect(handlers.asyncEnd).toHaveBeenCalledTimes(1); - expect(handlers.error).not.toHaveBeenCalled(); - - const value = await channel.tracePromise( - function (value) { - expect(this).toEqual(thisArg); - return Promise.resolve(value); - }, - input, - thisArg, - expectedResult, - ); - - expect(value).toEqual(expectedResult); -}); - -//<#END_FILE: test-diagnostics-channel-tracing-channel-promise.js diff --git a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync-error.test.js b/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync-error.test.js deleted file mode 100644 index 2906e447cf..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync-error.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-diagnostics-channel-tracing-channel-sync-error.js -//#SHA1: faf279d48c76f3c97ddf7e258df9ed49154a4cac -//----------------- -"use strict"; - -const dc = require("diagnostics_channel"); - -const channel = dc.tracingChannel("test"); - -const expectedError = new Error("test"); -const input = { foo: "bar" }; -const thisArg = { baz: "buz" }; - -function check(found) { - expect(found).toEqual(input); -} - -test("traceSync with error", () => { - const handlers = { - start: jest.fn(check), - end: jest.fn(check), - asyncStart: jest.fn(), - asyncEnd: jest.fn(), - error: jest.fn(found => { - check(found); - expect(found.error).toBe(expectedError); - }), - }; - - channel.subscribe(handlers); - - expect(() => { - channel.traceSync( - function (err) { - expect(this).toEqual(thisArg); - expect(err).toBe(expectedError); - throw err; - }, - input, - thisArg, - expectedError, - ); - }).toThrow(expectedError); - - expect(handlers.start).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.asyncStart).not.toHaveBeenCalled(); - expect(handlers.asyncEnd).not.toHaveBeenCalled(); - expect(handlers.error).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-diagnostics-channel-tracing-channel-sync-error.js diff --git a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync-run-stores.test.js b/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync-run-stores.test.js deleted file mode 100644 index ed7212e92d..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync-run-stores.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-diagnostics-channel-tracing-channel-sync-run-stores.js -//#SHA1: 51ffe2c7cb7160b565bfe6bbca0d29005a6bf876 -//----------------- -"use strict"; - -const { AsyncLocalStorage } = require("async_hooks"); -const dc = require("diagnostics_channel"); - -const channel = dc.tracingChannel("test"); -const store = new AsyncLocalStorage(); - -const context = { foo: "bar" }; - -test("diagnostics channel tracing channel sync run stores", () => { - const startCallback = jest.fn(() => context); - channel.start.bindStore(store, startCallback); - - expect(store.getStore()).toBeUndefined(); - - const traceCallback = jest.fn(() => { - expect(store.getStore()).toEqual(context); - }); - - channel.traceSync(traceCallback); - - expect(store.getStore()).toBeUndefined(); - - expect(startCallback).toHaveBeenCalledTimes(1); - expect(traceCallback).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-diagnostics-channel-tracing-channel-sync-run-stores.js diff --git a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync.test.js b/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync.test.js deleted file mode 100644 index f4a4c3b923..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-tracing-channel-sync.test.js +++ /dev/null @@ -1,80 +0,0 @@ -//#FILE: test-diagnostics-channel-tracing-channel-sync.js -//#SHA1: fad9bfb35032ed67643b026f8e79611d8197a131 -//----------------- -"use strict"; - -const dc = require("diagnostics_channel"); - -const channel = dc.tracingChannel("test"); - -const expectedResult = { foo: "bar" }; -const input = { foo: "bar" }; -const thisArg = { baz: "buz" }; -const arg = { baz: "buz" }; - -function check(found) { - expect(found).toBe(input); -} - -describe("diagnostics_channel tracing channel sync", () => { - const handlers = { - start: jest.fn(check), - end: jest.fn(found => { - check(found); - expect(found.result).toBe(expectedResult); - }), - asyncStart: jest.fn(), - asyncEnd: jest.fn(), - error: jest.fn(), - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test("channel subscription and traceSync", () => { - expect(channel.start.hasSubscribers).toBe(false); - channel.subscribe(handlers); - expect(channel.start.hasSubscribers).toBe(true); - - const result1 = channel.traceSync( - function (arg1) { - expect(arg1).toBe(arg); - expect(this).toBe(thisArg); - return expectedResult; - }, - input, - thisArg, - arg, - ); - - expect(result1).toBe(expectedResult); - expect(handlers.start).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.asyncStart).not.toHaveBeenCalled(); - expect(handlers.asyncEnd).not.toHaveBeenCalled(); - expect(handlers.error).not.toHaveBeenCalled(); - }); - - test("channel unsubscription", () => { - channel.unsubscribe(handlers); - expect(channel.start.hasSubscribers).toBe(false); - - const result2 = channel.traceSync( - function (arg1) { - expect(arg1).toBe(arg); - expect(this).toBe(thisArg); - return expectedResult; - }, - input, - thisArg, - arg, - ); - - expect(result2).toBe(expectedResult); - expect(handlers.start).not.toHaveBeenCalled(); - expect(handlers.end).not.toHaveBeenCalled(); - }); -}); - -//<#END_FILE: test-diagnostics-channel-tracing-channel-sync.js diff --git a/test/js/node/test/parallel/diagnostics-channel-udp.test.js b/test/js/node/test/parallel/diagnostics-channel-udp.test.js deleted file mode 100644 index 449df21666..0000000000 --- a/test/js/node/test/parallel/diagnostics-channel-udp.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-diagnostics-channel-udp.js -//#SHA1: 13d46f3f2404ee7cd53a551b90fc1ced191d4a81 -//----------------- -"use strict"; - -const dgram = require("dgram"); -const dc = require("diagnostics_channel"); - -const udpSocketChannel = dc.channel("udp.socket"); - -const isUDPSocket = socket => socket instanceof dgram.Socket; - -test("udp.socket channel emits UDP socket", () => { - const channelCallback = jest.fn(({ socket }) => { - expect(isUDPSocket(socket)).toBe(true); - }); - - udpSocketChannel.subscribe(channelCallback); - - const socket = dgram.createSocket("udp4"); - socket.close(); - - expect(channelCallback).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-diagnostics-channel-udp.js diff --git a/test/js/node/test/parallel/dns-resolve-promises.test.js b/test/js/node/test/parallel/dns-resolve-promises.test.js deleted file mode 100644 index 737ae7e467..0000000000 --- a/test/js/node/test/parallel/dns-resolve-promises.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-dns-resolve-promises.js -//#SHA1: ca56d4fe55a765a3e9db340661c96d8b1fd7a7a9 -//----------------- -"use strict"; - -const dns = require("dns"); - -test("DNS resolve promises error handling", async () => { - // Mock the dns.promises.resolve function to simulate an error - dns.promises.resolve = jest.fn().mockRejectedValue({ - code: "EPERM", - syscall: "queryA", - hostname: "example.org", - }); - - await expect(dns.promises.resolve("example.org")).rejects.toMatchObject({ - code: "EPERM", - syscall: "queryA", - hostname: "example.org", - }); - - expect(dns.promises.resolve).toHaveBeenCalledWith("example.org"); -}); - -//<#END_FILE: test-dns-resolve-promises.js diff --git a/test/js/node/test/parallel/domain-crypto.test.js b/test/js/node/test/parallel/domain-crypto.test.js deleted file mode 100644 index 406da90236..0000000000 --- a/test/js/node/test/parallel/domain-crypto.test.js +++ /dev/null @@ -1,87 +0,0 @@ -//#FILE: test-domain-crypto.js -//#SHA1: d7d6352f0f2684220baef0cfa1029278c3f05f8f -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const crypto = require("crypto"); - -// Pollution of global is intentional as part of test. -// See https://github.com/nodejs/node/commit/d1eff9ab -global.domain = require("domain"); - -describe("domain-crypto", () => { - beforeAll(() => { - if (!crypto) { - test.skip("node compiled without OpenSSL."); - } - }); - - test("crypto.randomBytes should not throw", () => { - expect(() => crypto.randomBytes(8)).not.toThrow(); - }); - - test("crypto.randomBytes with callback should succeed", () => { - return new Promise(resolve => { - crypto.randomBytes(8, (err, buffer) => { - expect(err).toBeNull(); - expect(buffer).toBeInstanceOf(Buffer); - expect(buffer.length).toBe(8); - resolve(); - }); - }); - }); - - test("crypto.randomFillSync should not throw", () => { - const buf = Buffer.alloc(8); - expect(() => crypto.randomFillSync(buf)).not.toThrow(); - }); - - test("crypto.pseudoRandomBytes should not throw", () => { - expect(() => crypto.pseudoRandomBytes(8)).not.toThrow(); - }); - - test("crypto.pseudoRandomBytes with callback should succeed", () => { - return new Promise(resolve => { - crypto.pseudoRandomBytes(8, (err, buffer) => { - expect(err).toBeNull(); - expect(buffer).toBeInstanceOf(Buffer); - expect(buffer.length).toBe(8); - resolve(); - }); - }); - }); - - test("crypto.pbkdf2 should succeed", () => { - return new Promise(resolve => { - crypto.pbkdf2("password", "salt", 8, 8, "sha1", (err, derivedKey) => { - expect(err).toBeNull(); - expect(derivedKey).toBeInstanceOf(Buffer); - expect(derivedKey.length).toBe(8); - resolve(); - }); - }); - }); -}); - -//<#END_FILE: test-domain-crypto.js diff --git a/test/js/node/test/parallel/domain-dep0097.test.js b/test/js/node/test/parallel/domain-dep0097.test.js deleted file mode 100644 index 03a74830e1..0000000000 --- a/test/js/node/test/parallel/domain-dep0097.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-domain-dep0097.js -//#SHA1: d0eeaeed86d045c8deba99d4ca589d35b368f17d -//----------------- -"use strict"; - -// Skip this test if inspector is disabled -if (typeof inspector === "undefined") { - test.skip("Inspector is disabled", () => {}); -} else { - const domain = require("domain"); - const inspector = require("inspector"); - - test("DEP0097 warning is emitted", () => { - const warningHandler = jest.fn(warning => { - expect(warning.code).toBe("DEP0097"); - expect(warning.message).toMatch(/Triggered by calling emit on process/); - }); - - process.on("warning", warningHandler); - - domain.create().run(() => { - inspector.open(0); - }); - - expect(warningHandler).toHaveBeenCalledTimes(1); - - // Clean up - process.removeListener("warning", warningHandler); - }); -} - -//<#END_FILE: test-domain-dep0097.js diff --git a/test/js/node/test/parallel/domain-thrown-error-handler-stack.test.js b/test/js/node/test/parallel/domain-thrown-error-handler-stack.test.js deleted file mode 100644 index f3c8d0b8b7..0000000000 --- a/test/js/node/test/parallel/domain-thrown-error-handler-stack.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-domain-thrown-error-handler-stack.js -//#SHA1: fd9aef2a4c852c0d092708bb6e712ad345cd4d2d -//----------------- -"use strict"; - -const domain = require("domain"); - -// Make sure that when an error is thrown from a nested domain, its error -// handler runs outside of that domain, but within the context of any parent -// domain. - -test("nested domain error handling", () => { - const d = domain.create(); - const d2 = domain.create(); - - d2.on("error", err => { - expect(domain._stack.length).toBe(1); - expect(process.domain).toBe(d); - - process.nextTick(() => { - expect(domain._stack.length).toBe(1); - expect(process.domain).toBe(d); - }); - }); - - expect(() => { - d.run(() => { - d2.run(() => { - throw new Error("oops"); - }); - }); - }).toThrow( - expect.objectContaining({ - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-domain-thrown-error-handler-stack.js diff --git a/test/js/node/test/parallel/domain-vm-promise-isolation.test.js b/test/js/node/test/parallel/domain-vm-promise-isolation.test.js deleted file mode 100644 index fe9ef2198e..0000000000 --- a/test/js/node/test/parallel/domain-vm-promise-isolation.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-domain-vm-promise-isolation.js -//#SHA1: 866cac427030e1696a6583c234815beed16bf5c8 -//----------------- -"use strict"; - -const domain = require("domain"); -const vm = require("vm"); - -// A promise created in a VM should not include a domain field but -// domains should still be able to propagate through them. -// -// See; https://github.com/nodejs/node/issues/40999 - -const context = vm.createContext({}); - -function run(code) { - const d = domain.createDomain(); - d.run(() => { - const p = vm.runInContext(code, context)(); - expect(p.domain).toBeUndefined(); - return p.then(() => { - expect(process.domain).toBe(d); - }); - }); -} - -test("VM promise isolation and domain propagation", async () => { - const runPromises = []; - for (let i = 0; i < 1000; i++) { - runPromises.push(run("async () => null")); - } - await Promise.all(runPromises); -}, 30000); // Increased timeout for multiple iterations - -//<#END_FILE: test-domain-vm-promise-isolation.js diff --git a/test/js/node/test/parallel/double-tls-client.test.js b/test/js/node/test/parallel/double-tls-client.test.js deleted file mode 100644 index 0913a2a5b7..0000000000 --- a/test/js/node/test/parallel/double-tls-client.test.js +++ /dev/null @@ -1,85 +0,0 @@ -//#FILE: test-double-tls-client.js -//#SHA1: f0d01e282b2d7efc1e1770700f742345216a8bb3 -//----------------- -"use strict"; - -const assert = require("assert"); -const fixtures = require("../common/fixtures"); -const tls = require("tls"); - -// In reality, this can be a HTTP CONNECT message, signaling the incoming -// data is TLS encrypted -const HEAD = "XXXX"; - -let subserver; -let server; - -beforeAll(() => { - subserver = tls.createServer({ - key: fixtures.readKey("agent1-key.pem"), - cert: fixtures.readKey("agent1-cert.pem"), - }); - - subserver.on("secureConnection", () => { - process.exit(0); - }); - - server = tls.createServer({ - key: fixtures.readKey("agent1-key.pem"), - cert: fixtures.readKey("agent1-cert.pem"), - }); - - server.on("secureConnection", serverTlsSock => { - serverTlsSock.on("data", chunk => { - expect(chunk.toString()).toBe(HEAD); - subserver.emit("connection", serverTlsSock); - }); - }); -}); - -afterAll(() => { - server.close(); - subserver.close(); -}); - -test("double TLS client", done => { - const onSecureConnect = jest.fn(); - - server.listen(() => { - const down = tls.connect({ - host: "127.0.0.1", - port: server.address().port, - rejectUnauthorized: false, - }); - - down.on("secureConnect", () => { - onSecureConnect(); - down.write(HEAD, err => { - expect(err).toBeFalsy(); - - // Sending tls data on a client TLSSocket with an active write led to a crash: - // - // node[16862]: ../src/crypto/crypto_tls.cc:963:virtual int node::crypto::TLSWrap::DoWrite(node::WriteWrap*, - // uv_buf_t*, size_t, uv_stream_t*): Assertion `!current_write_' failed. - // 1: 0xb090e0 node::Abort() [node] - // 2: 0xb0915e [node] - // 3: 0xca8413 node::crypto::TLSWrap::DoWrite(node::WriteWrap*, uv_buf_t*, unsigned long, uv_stream_s*) [node] - // 4: 0xcaa549 node::StreamBase::Write(uv_buf_t*, unsigned long, uv_stream_s*, v8::Local) [node] - // 5: 0xca88d7 node::crypto::TLSWrap::EncOut() [node] - // 6: 0xd3df3e [node] - // 7: 0xd3f35f v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) [node] - // 8: 0x15d9ef9 [node] - // Aborted - tls.connect({ - socket: down, - rejectUnauthorized: false, - }); - - expect(onSecureConnect).toHaveBeenCalledTimes(1); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-double-tls-client.js diff --git a/test/js/node/test/parallel/error-format-list.test.js b/test/js/node/test/parallel/error-format-list.test.js deleted file mode 100644 index ea5fb2dfc9..0000000000 --- a/test/js/node/test/parallel/error-format-list.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-error-format-list.js -//#SHA1: ed33f55f6c42ff9671add8288b1d2984393bdfc1 -//----------------- -"use strict"; - -if (!Intl) { - test.skip("missing Intl", () => {}); -} else { - test("formatList function", () => { - const and = new Intl.ListFormat("en", { style: "long", type: "conjunction" }); - const or = new Intl.ListFormat("en", { style: "long", type: "disjunction" }); - - const input = ["apple", "banana", "orange", "pear"]; - for (let i = 0; i < input.length; i++) { - const slicedInput = input.slice(0, i); - expect(formatList(slicedInput)).toBe(and.format(slicedInput)); - expect(formatList(slicedInput, "or")).toBe(or.format(slicedInput)); - } - }); -} - -// Helper function to replicate the behavior of internal/errors formatList -function formatList(list, type = "and") { - const formatter = new Intl.ListFormat("en", { - style: "long", - type: type === "and" ? "conjunction" : "disjunction", - }); - return formatter.format(list); -} - -//<#END_FILE: test-error-format-list.js diff --git a/test/js/node/test/parallel/errors-systemerror-stacktracelimit-deleted-and-error-sealed.test.js b/test/js/node/test/parallel/errors-systemerror-stacktracelimit-deleted-and-error-sealed.test.js deleted file mode 100644 index f2b27f0e1f..0000000000 --- a/test/js/node/test/parallel/errors-systemerror-stacktracelimit-deleted-and-error-sealed.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js -//#SHA1: 5d9d37ff8651fd7b7ffd5e9ae1b54e4ebc900355 -//----------------- -"use strict"; - -test("SystemError with deleted stackTraceLimit and sealed Error", () => { - delete Error.stackTraceLimit; - Object.seal(Error); - - const ctx = { - code: "ETEST", - message: "code message", - syscall: "syscall_test", - path: "/str", - dest: "/str2", - }; - - const errorThrowingFunction = () => { - const error = new Error("custom message"); - error.code = "ERR_TEST"; - error.name = "SystemError"; - error.info = ctx; - throw error; - }; - - expect(errorThrowingFunction).toThrow( - expect.objectContaining({ - code: "ERR_TEST", - name: "SystemError", - info: ctx, - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js diff --git a/test/js/node/test/parallel/eval.test.js b/test/js/node/test/parallel/eval.test.js deleted file mode 100644 index 97bc6069a4..0000000000 --- a/test/js/node/test/parallel/eval.test.js +++ /dev/null @@ -1,11 +0,0 @@ -//#FILE: test-eval.js -//#SHA1: 3ea606b33a3ee4b40ef918317b32008e0f5d5e49 -//----------------- -"use strict"; - -// Verify that eval is allowed by default. -test("eval is allowed by default", () => { - expect(eval('"eval"')).toBe("eval"); -}); - -//<#END_FILE: test-eval.js diff --git a/test/js/node/test/parallel/event-emitter-add-listeners.test.js b/test/js/node/test/parallel/event-emitter-add-listeners.test.js deleted file mode 100644 index fec7a8fdd2..0000000000 --- a/test/js/node/test/parallel/event-emitter-add-listeners.test.js +++ /dev/null @@ -1,91 +0,0 @@ -//#FILE: test-event-emitter-add-listeners.js -//#SHA1: 25d8611a8cf3694d26e53bb90f760beeb3bb1946 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const EventEmitter = require("events"); - -test("EventEmitter addListener functionality", () => { - const ee = new EventEmitter(); - const events_new_listener_emitted = []; - const listeners_new_listener_emitted = []; - - // Sanity check - expect(ee.addListener).toBe(ee.on); - - ee.on("newListener", function (event, listener) { - // Don't track newListener listeners. - if (event === "newListener") return; - - events_new_listener_emitted.push(event); - listeners_new_listener_emitted.push(listener); - }); - - const hello = jest.fn((a, b) => { - expect(a).toBe("a"); - expect(b).toBe("b"); - }); - - ee.once("newListener", function (name, listener) { - expect(name).toBe("hello"); - expect(listener).toBe(hello); - expect(this.listeners("hello")).toEqual([]); - }); - - ee.on("hello", hello); - ee.once("foo", () => { - throw new Error("This should not be called"); - }); - expect(events_new_listener_emitted).toEqual(["hello", "foo"]); - expect(listeners_new_listener_emitted).toEqual([hello, expect.any(Function)]); - - ee.emit("hello", "a", "b"); - expect(hello).toHaveBeenCalledTimes(1); -}); - -test("setMaxListeners with 0 does not throw", () => { - const f = new EventEmitter(); - expect(() => { - f.setMaxListeners(0); - }).not.toThrow(); -}); - -test("newListener event and listener order", () => { - const listen1 = () => {}; - const listen2 = () => {}; - const ee = new EventEmitter(); - - ee.once("newListener", function () { - expect(ee.listeners("hello")).toEqual([]); - ee.once("newListener", function () { - expect(ee.listeners("hello")).toEqual([]); - }); - ee.on("hello", listen2); - }); - ee.on("hello", listen1); - // The order of listeners on an event is not always the order in which the - // listeners were added. - expect(ee.listeners("hello")).toEqual([listen2, listen1]); -}); - -//<#END_FILE: test-event-emitter-add-listeners.js diff --git a/test/js/node/test/parallel/event-emitter-check-listener-leaks.test.js b/test/js/node/test/parallel/event-emitter-check-listener-leaks.test.js deleted file mode 100644 index 49c67758b6..0000000000 --- a/test/js/node/test/parallel/event-emitter-check-listener-leaks.test.js +++ /dev/null @@ -1,103 +0,0 @@ -//#FILE: test-event-emitter-check-listener-leaks.js -//#SHA1: c9d313a0879cc331d8ad1afafe5b9f482597bc7c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const events = require("events"); - -test("default", () => { - const e = new events.EventEmitter(); - - for (let i = 0; i < 10; i++) { - e.on("default", jest.fn()); - } - expect(Object.hasOwn(e._events.default, "warned")).toBe(false); - e.on("default", jest.fn()); - expect(e._events.default.warned).toBe(true); - - // symbol - const symbol = Symbol("symbol"); - e.setMaxListeners(1); - e.on(symbol, jest.fn()); - expect(Object.hasOwn(e._events[symbol], "warned")).toBe(false); - e.on(symbol, jest.fn()); - expect(Object.hasOwn(e._events[symbol], "warned")).toBe(true); - - // specific - e.setMaxListeners(5); - for (let i = 0; i < 5; i++) { - e.on("specific", jest.fn()); - } - expect(Object.hasOwn(e._events.specific, "warned")).toBe(false); - e.on("specific", jest.fn()); - expect(e._events.specific.warned).toBe(true); - - // only one - e.setMaxListeners(1); - e.on("only one", jest.fn()); - expect(Object.hasOwn(e._events["only one"], "warned")).toBe(false); - e.on("only one", jest.fn()); - expect(Object.hasOwn(e._events["only one"], "warned")).toBe(true); - - // unlimited - e.setMaxListeners(0); - for (let i = 0; i < 1000; i++) { - e.on("unlimited", jest.fn()); - } - expect(Object.hasOwn(e._events.unlimited, "warned")).toBe(false); -}); - -test("process-wide", () => { - events.EventEmitter.defaultMaxListeners = 42; - const e = new events.EventEmitter(); - - for (let i = 0; i < 42; ++i) { - e.on("fortytwo", jest.fn()); - } - expect(Object.hasOwn(e._events.fortytwo, "warned")).toBe(false); - e.on("fortytwo", jest.fn()); - expect(Object.hasOwn(e._events.fortytwo, "warned")).toBe(true); - delete e._events.fortytwo.warned; - - events.EventEmitter.defaultMaxListeners = 44; - e.on("fortytwo", jest.fn()); - expect(Object.hasOwn(e._events.fortytwo, "warned")).toBe(false); - e.on("fortytwo", jest.fn()); - expect(Object.hasOwn(e._events.fortytwo, "warned")).toBe(true); -}); - -test("_maxListeners precedence over defaultMaxListeners", () => { - events.EventEmitter.defaultMaxListeners = 42; - const e = new events.EventEmitter(); - e.setMaxListeners(1); - e.on("uno", jest.fn()); - expect(Object.hasOwn(e._events.uno, "warned")).toBe(false); - e.on("uno", jest.fn()); - expect(Object.hasOwn(e._events.uno, "warned")).toBe(true); - - // chainable - expect(e.setMaxListeners(1)).toBe(e); -}); - -//<#END_FILE: test-event-emitter-check-listener-leaks.js diff --git a/test/js/node/test/parallel/event-emitter-emit-context.test.js b/test/js/node/test/parallel/event-emitter-emit-context.test.js deleted file mode 100644 index b39c26258e..0000000000 --- a/test/js/node/test/parallel/event-emitter-emit-context.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-event-emitter-emit-context.js -//#SHA1: 66f963c1a1351deff53d036d86f821c5e932c832 -//----------------- -"use strict"; -const EventEmitter = require("events"); - -// Test emit called by other context -const EE = new EventEmitter(); - -// Works as expected if the context has no `constructor.name` -test("emit called with context having no constructor.name", () => { - const ctx = { __proto__: null }; - expect(() => EE.emit.call(ctx, "error", new Error("foo"))).toThrow( - expect.objectContaining({ - name: "Error", - message: expect.any(String), - }), - ); -}); - -test("emit called with empty object context", () => { - expect(EE.emit.call({}, "foo")).toBe(false); -}); - -//<#END_FILE: test-event-emitter-emit-context.js diff --git a/test/js/node/test/parallel/event-emitter-get-max-listeners.test.js b/test/js/node/test/parallel/event-emitter-get-max-listeners.test.js deleted file mode 100644 index 612a3f1323..0000000000 --- a/test/js/node/test/parallel/event-emitter-get-max-listeners.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-event-emitter-get-max-listeners.js -//#SHA1: ff5c2f7b9525ae4137ea8eddd742572bd399c5ce -//----------------- -"use strict"; - -const EventEmitter = require("events"); - -test("EventEmitter getMaxListeners", () => { - const emitter = new EventEmitter(); - - expect(emitter.getMaxListeners()).toBe(EventEmitter.defaultMaxListeners); - - emitter.setMaxListeners(0); - expect(emitter.getMaxListeners()).toBe(0); - - emitter.setMaxListeners(3); - expect(emitter.getMaxListeners()).toBe(3); -}); - -// https://github.com/nodejs/node/issues/523 - second call should not throw. -test("EventEmitter.prototype.on should not throw on second call", () => { - const recv = {}; - expect(() => { - EventEmitter.prototype.on.call(recv, "event", () => {}); - EventEmitter.prototype.on.call(recv, "event", () => {}); - }).not.toThrow(); -}); - -//<#END_FILE: test-event-emitter-get-max-listeners.js diff --git a/test/js/node/test/parallel/event-emitter-listener-count.test.js b/test/js/node/test/parallel/event-emitter-listener-count.test.js deleted file mode 100644 index b9772949ad..0000000000 --- a/test/js/node/test/parallel/event-emitter-listener-count.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-event-emitter-listener-count.js -//#SHA1: 0f4b7f14fe432472b51524b3853db6d8615614f1 -//----------------- -"use strict"; - -const EventEmitter = require("events"); - -describe("EventEmitter.listenerCount and emitter.listenerCount", () => { - let emitter; - - beforeEach(() => { - emitter = new EventEmitter(); - emitter.on("foo", () => {}); - emitter.on("foo", () => {}); - emitter.on("baz", () => {}); - // Allow any type - emitter.on(123, () => {}); - }); - - test("EventEmitter.listenerCount returns correct count", () => { - expect(EventEmitter.listenerCount(emitter, "foo")).toBe(2); - }); - - test("emitter.listenerCount returns correct counts for various events", () => { - expect(emitter.listenerCount("foo")).toBe(2); - expect(emitter.listenerCount("bar")).toBe(0); - expect(emitter.listenerCount("baz")).toBe(1); - expect(emitter.listenerCount(123)).toBe(1); - }); -}); - -//<#END_FILE: test-event-emitter-listener-count.js diff --git a/test/js/node/test/parallel/event-emitter-modify-in-emit.test.js b/test/js/node/test/parallel/event-emitter-modify-in-emit.test.js deleted file mode 100644 index 422054923c..0000000000 --- a/test/js/node/test/parallel/event-emitter-modify-in-emit.test.js +++ /dev/null @@ -1,89 +0,0 @@ -//#FILE: test-event-emitter-modify-in-emit.js -//#SHA1: 6d378f9c7700ce7946f7d59bbc32b7cb82efc836 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const events = require("events"); - -describe("EventEmitter modify in emit", () => { - let callbacks_called; - let e; - - function callback1() { - callbacks_called.push("callback1"); - e.on("foo", callback2); - e.on("foo", callback3); - e.removeListener("foo", callback1); - } - - function callback2() { - callbacks_called.push("callback2"); - e.removeListener("foo", callback2); - } - - function callback3() { - callbacks_called.push("callback3"); - e.removeListener("foo", callback3); - } - - beforeEach(() => { - callbacks_called = []; - e = new events.EventEmitter(); - }); - - test("listeners are modified during emit", () => { - e.on("foo", callback1); - expect(e.listeners("foo").length).toBe(1); - - e.emit("foo"); - expect(e.listeners("foo").length).toBe(2); - expect(callbacks_called).toEqual(["callback1"]); - - e.emit("foo"); - expect(e.listeners("foo").length).toBe(0); - expect(callbacks_called).toEqual(["callback1", "callback2", "callback3"]); - - e.emit("foo"); - expect(e.listeners("foo").length).toBe(0); - expect(callbacks_called).toEqual(["callback1", "callback2", "callback3"]); - }); - - test("removeAllListeners removes all listeners", () => { - e.on("foo", callback1); - e.on("foo", callback2); - expect(e.listeners("foo").length).toBe(2); - e.removeAllListeners("foo"); - expect(e.listeners("foo").length).toBe(0); - }); - - test("removing callbacks during emit allows emits to propagate to all listeners", () => { - e.on("foo", callback2); - e.on("foo", callback3); - expect(e.listeners("foo").length).toBe(2); - e.emit("foo"); - expect(callbacks_called).toEqual(["callback2", "callback3"]); - expect(e.listeners("foo").length).toBe(0); - }); -}); - -//<#END_FILE: test-event-emitter-modify-in-emit.js diff --git a/test/js/node/test/parallel/event-emitter-num-args.test.js b/test/js/node/test/parallel/event-emitter-num-args.test.js deleted file mode 100644 index e295eb022c..0000000000 --- a/test/js/node/test/parallel/event-emitter-num-args.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-event-emitter-num-args.js -//#SHA1: b17b5bfd071180f4c53c8c408dc14ec860fd4225 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const events = require("events"); - -describe("EventEmitter number of arguments", () => { - let e; - let num_args_emitted; - - beforeEach(() => { - e = new events.EventEmitter(); - num_args_emitted = []; - - e.on("numArgs", function () { - const numArgs = arguments.length; - num_args_emitted.push(numArgs); - }); - - e.on("foo", function () { - num_args_emitted.push(arguments.length); - }); - - e.on("foo", function () { - num_args_emitted.push(arguments.length); - }); - }); - - it("should emit correct number of arguments", () => { - e.emit("numArgs"); - e.emit("numArgs", null); - e.emit("numArgs", null, null); - e.emit("numArgs", null, null, null); - e.emit("numArgs", null, null, null, null); - e.emit("numArgs", null, null, null, null, null); - - e.emit("foo", null, null, null, null); - - expect(num_args_emitted).toEqual([0, 1, 2, 3, 4, 5, 4, 4]); - }); -}); - -//<#END_FILE: test-event-emitter-num-args.js diff --git a/test/js/node/test/parallel/event-emitter-prepend.test.js b/test/js/node/test/parallel/event-emitter-prepend.test.js deleted file mode 100644 index f8f7d47b8c..0000000000 --- a/test/js/node/test/parallel/event-emitter-prepend.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-event-emitter-prepend.js -//#SHA1: 9a753f44fc304ff6584a31c20e95037e622579d7 -//----------------- -"use strict"; - -const EventEmitter = require("events"); -const stream = require("stream"); - -describe("EventEmitter prepend", () => { - test("prepend listeners in correct order", () => { - const myEE = new EventEmitter(); - let m = 0; - - // This one comes last. - myEE.on("foo", () => { - expect(m).toBe(2); - }); - - // This one comes second. - myEE.prependListener("foo", () => { - expect(m).toBe(1); - m++; - }); - - // This one comes first. - myEE.prependOnceListener("foo", () => { - expect(m).toBe(0); - m++; - }); - - myEE.emit("foo"); - expect.assertions(3); - }); - - test("fallback if prependListener is undefined", () => { - // Test fallback if prependListener is undefined. - delete EventEmitter.prototype.prependListener; - - function Writable() { - this.writable = true; - stream.Stream.call(this); - } - Object.setPrototypeOf(Writable.prototype, stream.Stream.prototype); - Object.setPrototypeOf(Writable, stream.Stream); - - function Readable() { - this.readable = true; - stream.Stream.call(this); - } - Object.setPrototypeOf(Readable.prototype, stream.Stream.prototype); - Object.setPrototypeOf(Readable, stream.Stream); - - const w = new Writable(); - const r = new Readable(); - r.pipe(w); - - // If we reach this point without throwing, the test passes - expect(true).toBe(true); - }); -}); - -//<#END_FILE: test-event-emitter-prepend.js diff --git a/test/js/node/test/parallel/event-emitter-set-max-listeners-side-effects.test.js b/test/js/node/test/parallel/event-emitter-set-max-listeners-side-effects.test.js deleted file mode 100644 index 101ccfa80b..0000000000 --- a/test/js/node/test/parallel/event-emitter-set-max-listeners-side-effects.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-event-emitter-set-max-listeners-side-effects.js -//#SHA1: 319371e261c5cc46348475cdd789eb9ee56839d8 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const events = require("events"); - -test("EventEmitter setMaxListeners should not have side effects", () => { - const e = new events.EventEmitter(); - - expect(e._events).not.toBeInstanceOf(Object); - expect(Object.keys(e._events)).toEqual([]); - e.setMaxListeners(5); - expect(Object.keys(e._events)).toEqual([]); -}); - -//<#END_FILE: test-event-emitter-set-max-listeners-side-effects.js diff --git a/test/js/node/test/parallel/event-emitter-special-event-names.test.js b/test/js/node/test/parallel/event-emitter-special-event-names.test.js deleted file mode 100644 index 60d8071a33..0000000000 --- a/test/js/node/test/parallel/event-emitter-special-event-names.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-event-emitter-special-event-names.js -//#SHA1: 3ae93e3a9f5cd01560264a5c297a2fae25c5c18f -//----------------- -"use strict"; - -const EventEmitter = require("events"); - -describe("EventEmitter with special event names", () => { - let ee; - - beforeEach(() => { - ee = new EventEmitter(); - }); - - test("initial eventNames() is empty", () => { - expect(ee.eventNames()).toEqual([]); - }); - - test("_events does not have hasOwnProperty or toString", () => { - expect(ee._events.hasOwnProperty).toBeUndefined(); - expect(ee._events.toString).toBeUndefined(); - }); - - test("can add and list special event names", () => { - const handler = jest.fn(); - - ee.on("__proto__", handler); - ee.on("__defineGetter__", handler); - ee.on("toString", handler); - - expect(ee.eventNames()).toEqual(["__proto__", "__defineGetter__", "toString"]); - - expect(ee.listeners("__proto__")).toEqual([handler]); - expect(ee.listeners("__defineGetter__")).toEqual([handler]); - expect(ee.listeners("toString")).toEqual([handler]); - }); - - test("can emit __proto__ event", () => { - const handler = jest.fn(); - ee.on("__proto__", handler); - ee.emit("__proto__", 1); - expect(handler).toHaveBeenCalledWith(1); - }); - - test("process can emit __proto__ event", () => { - const handler = jest.fn(); - process.on("__proto__", handler); - process.emit("__proto__", 1); - expect(handler).toHaveBeenCalledWith(1); - }); -}); - -//<#END_FILE: test-event-emitter-special-event-names.js diff --git a/test/js/node/test/parallel/event-emitter-subclass.test.js b/test/js/node/test/parallel/event-emitter-subclass.test.js deleted file mode 100644 index a48d9e8fdd..0000000000 --- a/test/js/node/test/parallel/event-emitter-subclass.test.js +++ /dev/null @@ -1,81 +0,0 @@ -//#FILE: test-event-emitter-subclass.js -//#SHA1: e7f7e379fcf89318168b9e5d9f38c4af371ada8e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const EventEmitter = require("events").EventEmitter; - -Object.setPrototypeOf(MyEE.prototype, EventEmitter.prototype); -Object.setPrototypeOf(MyEE, EventEmitter); - -function MyEE(cb) { - this.once(1, cb); - this.emit(1); - this.removeAllListeners(); - EventEmitter.call(this); -} - -test("MyEE callback is called", () => { - const callback = jest.fn(); - const myee = new MyEE(callback); - expect(callback).toHaveBeenCalledTimes(1); -}); - -Object.setPrototypeOf(ErrorEE.prototype, EventEmitter.prototype); -Object.setPrototypeOf(ErrorEE, EventEmitter); -function ErrorEE() { - this.emit("error", new Error("blerg")); -} - -test("ErrorEE throws error", () => { - expect(() => { - new ErrorEE(); - }).toThrow( - expect.objectContaining({ - message: expect.any(String), - }), - ); -}); - -test("MyEE _events is empty after removeAllListeners", () => { - const myee = new MyEE(() => {}); - expect(myee._events).not.toBeInstanceOf(Object); - expect(Object.keys(myee._events)).toEqual([]); -}); - -function MyEE2() { - EventEmitter.call(this); -} - -MyEE2.prototype = new EventEmitter(); - -test("MyEE2 instances do not share listeners", () => { - const ee1 = new MyEE2(); - const ee2 = new MyEE2(); - - ee1.on("x", () => {}); - - expect(ee2.listenerCount("x")).toBe(0); -}); - -//<#END_FILE: test-event-emitter-subclass.js diff --git a/test/js/node/test/parallel/event-emitter-symbols.test.js b/test/js/node/test/parallel/event-emitter-symbols.test.js deleted file mode 100644 index bfb0e2b774..0000000000 --- a/test/js/node/test/parallel/event-emitter-symbols.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-event-emitter-symbols.js -//#SHA1: c3fa4a8db31f2a88317d3c60fb52aa2921eaa20d -//----------------- -"use strict"; - -const EventEmitter = require("events"); - -test("EventEmitter with Symbol events", () => { - const ee = new EventEmitter(); - const foo = Symbol("foo"); - const listener = jest.fn(); - - ee.on(foo, listener); - expect(ee.listeners(foo)).toEqual([listener]); - - ee.emit(foo); - expect(listener).toHaveBeenCalledTimes(1); - - ee.removeAllListeners(); - expect(ee.listeners(foo)).toEqual([]); - - ee.on(foo, listener); - expect(ee.listeners(foo)).toEqual([listener]); - - ee.removeListener(foo, listener); - expect(ee.listeners(foo)).toEqual([]); -}); - -//<#END_FILE: test-event-emitter-symbols.js diff --git a/test/js/node/test/parallel/event-target.test.js b/test/js/node/test/parallel/event-target.test.js deleted file mode 100644 index 0b5fc40712..0000000000 --- a/test/js/node/test/parallel/event-target.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-event-target.js -//#SHA1: 3e70912640aa5270e13723e1895636bfd238420a -//----------------- -"use strict"; - -const eventPhases = { - NONE: 0, - CAPTURING_PHASE: 1, - AT_TARGET: 2, - BUBBLING_PHASE: 3, -}; - -describe("Event phases", () => { - test.each(Object.entries(eventPhases))("Event.%s should be %d", (prop, value) => { - // Check if the value of the property matches the expected value - expect(Event[prop]).toBe(value); - - const desc = Object.getOwnPropertyDescriptor(Event, prop); - expect(desc.writable).toBe(false); - expect(desc.configurable).toBe(false); - expect(desc.enumerable).toBe(true); - }); -}); - -//<#END_FILE: test-event-target.js diff --git a/test/js/node/test/parallel/events-list.test.js b/test/js/node/test/parallel/events-list.test.js deleted file mode 100644 index bde2b1a0e6..0000000000 --- a/test/js/node/test/parallel/events-list.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-events-list.js -//#SHA1: 946973d6c9e19c6410b4486cbef3e2ed032715fc -//----------------- -"use strict"; - -const EventEmitter = require("events"); - -test("EventEmitter.eventNames()", () => { - const EE = new EventEmitter(); - const m = () => {}; - - EE.on("foo", () => {}); - expect(EE.eventNames()).toEqual(["foo"]); - - EE.on("bar", m); - expect(EE.eventNames()).toEqual(["foo", "bar"]); - - EE.removeListener("bar", m); - expect(EE.eventNames()).toEqual(["foo"]); - - const s = Symbol("s"); - EE.on(s, m); - expect(EE.eventNames()).toEqual(["foo", s]); - - EE.removeListener(s, m); - expect(EE.eventNames()).toEqual(["foo"]); -}); - -//<#END_FILE: test-events-list.js diff --git a/test/js/node/test/parallel/events-on-async-iterator.test.js b/test/js/node/test/parallel/events-on-async-iterator.test.js deleted file mode 100644 index eb67f58c5b..0000000000 --- a/test/js/node/test/parallel/events-on-async-iterator.test.js +++ /dev/null @@ -1,417 +0,0 @@ -import { test, expect } from "bun:test"; -const common = require("../common"); -const assert = require("assert"); -const { on, EventEmitter, listenerCount } = require("events"); - -test("basic", async () => { - const ee = new EventEmitter(); - process.nextTick(() => { - ee.emit("foo", "bar"); - // 'bar' is a spurious event, we are testing - // that it does not show up in the iterable - ee.emit("bar", 24); - ee.emit("foo", 42); - }); - - const iterable = on(ee, "foo"); - - const expected = [["bar"], [42]]; - - for await (const event of iterable) { - const current = expected.shift(); - - expect(current).toStrictEqual(event); - - if (expected.length === 0) { - break; - } - } - expect(ee.listenerCount("foo")).toBe(0); - expect(ee.listenerCount("error")).toBe(0); -}); - -test("invalidArgType", async () => { - assert.throws( - () => on({}, "foo"), - common.expectsError({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - const ee = new EventEmitter(); - - [1, "hi", null, false, () => {}, Symbol(), 1n].map(options => { - return assert.throws( - () => on(ee, "foo", options), - common.expectsError({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); -}); - -test("error", async () => { - const ee = new EventEmitter(); - const _err = new Error("kaboom"); - process.nextTick(() => { - ee.emit("error", _err); - }); - - const iterable = on(ee, "foo"); - let looped = false; - let thrown = false; - - try { - // eslint-disable-next-line no-unused-vars - for await (const event of iterable) { - looped = true; - } - } catch (err) { - thrown = true; - expect(err).toStrictEqual(_err); - } - expect(thrown).toBe(true); - expect(looped).toBe(false); -}); - -test("errorDelayed", async () => { - const ee = new EventEmitter(); - const _err = new Error("kaboom"); - process.nextTick(() => { - ee.emit("foo", 42); - ee.emit("error", _err); - }); - - const iterable = on(ee, "foo"); - const expected = [[42]]; - let thrown = false; - - try { - for await (const event of iterable) { - const current = expected.shift(); - assert.deepStrictEqual(current, event); - } - } catch (err) { - thrown = true; - expect(err).toStrictEqual(_err); - } - expect(thrown).toBe(true); - expect(ee.listenerCount("foo")).toBe(0); - expect(ee.listenerCount("error")).toBe(0); -}); - -test("throwInLoop", async () => { - const ee = new EventEmitter(); - const _err = new Error("kaboom"); - - process.nextTick(() => { - ee.emit("foo", 42); - }); - - try { - for await (const event of on(ee, "foo")) { - assert.deepStrictEqual(event, [42]); - throw _err; - } - } catch (err) { - expect(err).toStrictEqual(_err); - } - - expect(ee.listenerCount("foo")).toBe(0); - expect(ee.listenerCount("error")).toBe(0); -}); - -test("next", async () => { - const ee = new EventEmitter(); - const iterable = on(ee, "foo"); - - process.nextTick(function () { - ee.emit("foo", "bar"); - ee.emit("foo", 42); - iterable.return(); - }); - - const results = await Promise.all([iterable.next(), iterable.next(), iterable.next()]); - - expect(results).toStrictEqual([ - { - value: ["bar"], - done: false, - }, - { - value: [42], - done: false, - }, - { - value: undefined, - done: true, - }, - ]); - - expect(await iterable.next()).toStrictEqual({ - value: undefined, - done: true, - }); -}); - -test("nextError", async () => { - const ee = new EventEmitter(); - const iterable = on(ee, "foo"); - const _err = new Error("kaboom"); - process.nextTick(function () { - ee.emit("error", _err); - }); - const results = await Promise.allSettled([iterable.next(), iterable.next(), iterable.next()]); - assert.deepStrictEqual(results, [ - { - status: "rejected", - reason: _err, - }, - { - status: "fulfilled", - value: { - value: undefined, - done: true, - }, - }, - { - status: "fulfilled", - value: { - value: undefined, - done: true, - }, - }, - ]); - expect(ee.listeners("error").length).toBe(0); -}); - -test("iterableThrow", async () => { - const ee = new EventEmitter(); - const iterable = on(ee, "foo"); - - process.nextTick(() => { - ee.emit("foo", "bar"); - ee.emit("foo", 42); // lost in the queue - iterable.throw(_err); - }); - - const _err = new Error("kaboom"); - let thrown = false; - - assert.throws( - () => { - // No argument - iterable.throw(); - }, - { - message: 'The "EventEmitter.AsyncIterator" argument must be' + " of type Error. Received: undefined", - name: "TypeError", - }, - ); - - const expected = [["bar"], [42]]; - - try { - for await (const event of iterable) { - assert.deepStrictEqual(event, expected.shift()); - } - } catch (err) { - thrown = true; - assert.strictEqual(err, _err); - } - assert.strictEqual(thrown, true); - assert.strictEqual(expected.length, 0); - assert.strictEqual(ee.listenerCount("foo"), 0); - assert.strictEqual(ee.listenerCount("error"), 0); -}); - -test("eventTarget", async () => { - const et = new EventTarget(); - const tick = () => et.dispatchEvent(new Event("tick")); - const interval = setInterval(tick, 0); - let count = 0; - for await (const [event] of on(et, "tick")) { - count++; - assert.strictEqual(event.type, "tick"); - if (count >= 5) { - break; - } - } - assert.strictEqual(count, 5); - clearInterval(interval); -}); - -test("errorListenerCount", async () => { - const et = new EventEmitter(); - on(et, "foo"); - assert.strictEqual(et.listenerCount("error"), 1); -}); - -test.skip("nodeEventTarget", async () => { - const et = new NodeEventTarget(); - const tick = () => et.dispatchEvent(new Event("tick")); - const interval = setInterval(tick, 0); - let count = 0; - for await (const [event] of on(et, "tick")) { - count++; - assert.strictEqual(event.type, "tick"); - if (count >= 5) { - break; - } - } - assert.strictEqual(count, 5); - clearInterval(interval); -}); - -test("abortableOnBefore", async () => { - const ee = new EventEmitter(); - const abortedSignal = AbortSignal.abort(); - [1, {}, null, false, "hi"].forEach(signal => { - assert.throws(() => on(ee, "foo", { signal }), { - code: "ERR_INVALID_ARG_TYPE", - }); - }); - assert.throws(() => on(ee, "foo", { signal: abortedSignal }), { - name: "AbortError", - }); -}); - -test("eventTargetAbortableOnBefore", async () => { - const et = new EventTarget(); - const abortedSignal = AbortSignal.abort(); - [1, {}, null, false, "hi"].forEach(signal => { - assert.throws(() => on(et, "foo", { signal }), { - code: "ERR_INVALID_ARG_TYPE", - }); - }); - assert.throws(() => on(et, "foo", { signal: abortedSignal }), { - name: "AbortError", - }); -}); - -test("abortableOnAfter", async () => { - const ee = new EventEmitter(); - const ac = new AbortController(); - - const i = setInterval(() => ee.emit("foo", "foo"), 10); - - async function foo() { - for await (const f of on(ee, "foo", { signal: ac.signal })) { - assert.strictEqual(f, "foo"); - } - } - - foo() - .catch( - common.mustCall(error => { - assert.strictEqual(error.name, "AbortError"); - }), - ) - .finally(() => { - clearInterval(i); - }); - - process.nextTick(() => ac.abort()); -}); - -test("eventTargetAbortableOnAfter", async () => { - const et = new EventTarget(); - const ac = new AbortController(); - - const i = setInterval(() => et.dispatchEvent(new Event("foo")), 10); - - async function foo() { - for await (const f of on(et, "foo", { signal: ac.signal })) { - assert(f); - } - } - - foo() - .catch( - common.mustCall(error => { - assert.strictEqual(error.name, "AbortError"); - }), - ) - .finally(() => { - clearInterval(i); - }); - - process.nextTick(() => ac.abort()); -}); - -test("eventTargetAbortableOnAfter2", async () => { - const et = new EventTarget(); - const ac = new AbortController(); - - const i = setInterval(() => et.dispatchEvent(new Event("foo")), 10); - - async function foo() { - for await (const f of on(et, "foo", { signal: ac.signal })) { - assert(f); - // Cancel after a single event has been triggered. - ac.abort(); - } - } - - foo() - .catch( - common.mustCall(error => { - assert.strictEqual(error.name, "AbortError"); - }), - ) - .finally(() => { - clearInterval(i); - }); -}); - -test("abortableOnAfterDone", async () => { - const ee = new EventEmitter(); - const ac = new AbortController(); - - const i = setInterval(() => ee.emit("foo", "foo"), 1); - let count = 0; - - async function foo() { - for await (const f of on(ee, "foo", { signal: ac.signal })) { - assert.strictEqual(f[0], "foo"); - if (++count === 5) break; - } - ac.abort(); // No error will occur - } - - foo().finally(() => { - clearInterval(i); - }); -}); - -test("abortListenerRemovedAfterComplete", async () => { - const ee = new EventEmitter(); - const ac = new AbortController(); - - const i = setInterval(() => ee.emit("foo", "foo"), 1); - try { - // Below: either the kEvents map is empty or the 'abort' listener list is empty - - // Return case - const endedIterator = on(ee, "foo", { signal: ac.signal }); - expect(listenerCount(ac.signal, "abort")).toBeGreaterThan(0); - endedIterator.return(); - expect(listenerCount(ac.signal, "abort")).toBe(0); - - // Throw case - const throwIterator = on(ee, "foo", { signal: ac.signal }); - expect(listenerCount(ac.signal, "abort")).toBeGreaterThan(0); - throwIterator.throw(new Error()); - expect(listenerCount(ac.signal, "abort")).toBe(0); - - // Abort case - on(ee, "foo", { signal: ac.signal }); - expect(listenerCount(ac.signal, "abort")).toBeGreaterThan(0); - ac.abort(new Error()); - expect(listenerCount(ac.signal, "abort")).toBe(0); - } finally { - clearInterval(i); - } -}); diff --git a/test/js/node/test/parallel/events-once.test.js b/test/js/node/test/parallel/events-once.test.js deleted file mode 100644 index 3e0c4c9ecb..0000000000 --- a/test/js/node/test/parallel/events-once.test.js +++ /dev/null @@ -1,254 +0,0 @@ -import { test, expect } from "bun:test"; -const { once, EventEmitter, listenerCount } = require("events"); -const { deepStrictEqual, fail, rejects, strictEqual } = require("assert"); -// const { kEvents } = require("internal/event_target"); - -test("onceAnEvent", async () => { - const ee = new EventEmitter(); - - process.nextTick(() => { - ee.emit("myevent", 42); - }); - - const [value] = await once(ee, "myevent"); - strictEqual(value, 42); - strictEqual(ee.listenerCount("error"), 0); - strictEqual(ee.listenerCount("myevent"), 0); -}); - -test("onceAnEventWithInvalidOptions", async () => { - const ee = new EventEmitter(); - - await Promise.all( - [1, "hi", null, false, () => {}, Symbol(), 1n].map(options => { - expect.toThrowWithCode(() => once(ee, "myevent", options), "ERR_INVALID_ARG_TYPE"); - }), - ); -}); - -test("onceAnEventWithTwoArgs", async () => { - const ee = new EventEmitter(); - - process.nextTick(() => { - ee.emit("myevent", 42, 24); - }); - - const value = await once(ee, "myevent"); - deepStrictEqual(value, [42, 24]); -}); - -test("catchesErrors", async () => { - const ee = new EventEmitter(); - - const expected = new Error("kaboom"); - let err; - process.nextTick(() => { - ee.emit("error", expected); - }); - - try { - await once(ee, "myevent"); - } catch (_e) { - err = _e; - } - strictEqual(err, expected); - strictEqual(ee.listenerCount("error"), 0); - strictEqual(ee.listenerCount("myevent"), 0); -}); - -test("catchesErrorsWithAbortSignal", async () => { - const ee = new EventEmitter(); - const ac = new AbortController(); - const signal = ac.signal; - - const expected = new Error("boom"); - let err; - process.nextTick(() => { - ee.emit("error", expected); - }); - - try { - const promise = once(ee, "myevent", { signal }); - strictEqual(ee.listenerCount("error"), 1); - strictEqual(listenerCount(signal, "abort"), 1); - - await promise; - } catch (e) { - err = e; - } - strictEqual(err, expected); - strictEqual(ee.listenerCount("error"), 0); - strictEqual(ee.listenerCount("myevent"), 0); - strictEqual(listenerCount(signal, "abort"), 0); -}); - -test("stopListeningAfterCatchingError", async () => { - const ee = new EventEmitter(); - - const expected = new Error("kaboom"); - let err; - process.nextTick(() => { - ee.emit("error", expected); - ee.emit("myevent", 42, 24); - }); - - try { - await once(ee, "myevent"); - } catch (_e) { - err = _e; - } - process.removeAllListeners("multipleResolves"); - strictEqual(err, expected); - strictEqual(ee.listenerCount("error"), 0); - strictEqual(ee.listenerCount("myevent"), 0); -}); - -test("onceError", async () => { - const ee = new EventEmitter(); - - const expected = new Error("kaboom"); - process.nextTick(() => { - ee.emit("error", expected); - }); - - const promise = once(ee, "error"); - strictEqual(ee.listenerCount("error"), 1); - const [err] = await promise; - strictEqual(err, expected); - strictEqual(ee.listenerCount("error"), 0); - strictEqual(ee.listenerCount("myevent"), 0); -}); - -test("onceWithEventTarget", async () => { - const et = new EventTarget(); - const event = new Event("myevent"); - process.nextTick(() => { - et.dispatchEvent(event); - }); - const [value] = await once(et, "myevent"); - strictEqual(value, event); -}); - -test("onceWithEventTargetError", async () => { - const et = new EventTarget(); - const error = new Event("error"); - process.nextTick(() => { - et.dispatchEvent(error); - }); - - const [err] = await once(et, "error"); - strictEqual(err, error); -}); - -test("onceWithInvalidEventEmmiter", async () => { - const ac = new AbortController(); - expect.toThrowWithCode(() => once(ac, "myevent"), "ERR_INVALID_ARG_TYPE"); -}); - -test("prioritizesEventEmitter", async () => { - const ee = new EventEmitter(); - ee.addEventListener = fail; - ee.removeAllListeners = fail; - process.nextTick(() => ee.emit("foo")); - await once(ee, "foo"); -}); - -test("abortSignalBefore", async () => { - const ee = new EventEmitter(); - ee.on("error", () => expect(false).toEqual(true)); - const abortedSignal = AbortSignal.abort(); - - await Promise.all( - [1, {}, "hi", null, false].map(signal => { - expect.toThrowWithCode(() => once(ee, "foo", { signal }), "ERR_INVALID_ARG_TYPE"); - }), - ); - - expect(() => once(ee, "foo", { signal: abortedSignal })).toThrow(); -}); - -test("abortSignalAfter", async () => { - const ee = new EventEmitter(); - const ac = new AbortController(); - ee.on("error", () => expect(false).toEqual(true)); - const r = rejects(once(ee, "foo", { signal: ac.signal }), { - name: "AbortError", - }); - process.nextTick(() => ac.abort()); - return r; -}); - -test("abortSignalAfterEvent", async () => { - const ee = new EventEmitter(); - const ac = new AbortController(); - process.nextTick(() => { - ee.emit("foo"); - ac.abort(); - }); - const promise = once(ee, "foo", { signal: ac.signal }); - strictEqual(listenerCount(ac.signal, "abort"), 1); - await promise; - strictEqual(listenerCount(ac.signal, "abort"), 0); -}); - -test("abortSignalRemoveListener", async () => { - const ee = new EventEmitter(); - const ac = new AbortController(); - - try { - process.nextTick(() => ac.abort()); - await once(ee, "test", { signal: ac.signal }); - } catch { - strictEqual(ee.listeners("test").length, 0); - strictEqual(ee.listeners("error").length, 0); - } -}); - -test.skip("eventTargetAbortSignalBefore", async () => { - const et = new EventTarget(); - const abortedSignal = AbortSignal.abort(); - - await Promise.all( - [1, {}, "hi", null, false].map(signal => { - return rejects(once(et, "foo", { signal }), { - code: "ERR_INVALID_ARG_TYPE", - }); - }), - ); - - return rejects(once(et, "foo", { signal: abortedSignal }), { - name: "AbortError", - }); -}); - -test.skip("eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped", async () => { - const et = new EventTarget(); - const ac = new AbortController(); - const { signal } = ac; - signal.addEventListener("abort", e => e.stopImmediatePropagation(), { once: true }); - - process.nextTick(() => ac.abort()); - return rejects(once(et, "foo", { signal }), { - name: "AbortError", - }); -}); - -test("eventTargetAbortSignalAfter", async () => { - const et = new EventTarget(); - const ac = new AbortController(); - const r = rejects(once(et, "foo", { signal: ac.signal }), { - name: "AbortError", - }); - process.nextTick(() => ac.abort()); - return r; -}); - -test("eventTargetAbortSignalAfterEvent", async () => { - const et = new EventTarget(); - const ac = new AbortController(); - process.nextTick(() => { - et.dispatchEvent(new Event("foo")); - ac.abort(); - }); - await once(et, "foo", { signal: ac.signal }); -}); diff --git a/test/js/node/test/parallel/eventsource-disabled.test.js b/test/js/node/test/parallel/eventsource-disabled.test.js deleted file mode 100644 index 0b7c76c1e2..0000000000 --- a/test/js/node/test/parallel/eventsource-disabled.test.js +++ /dev/null @@ -1,10 +0,0 @@ -//#FILE: test-eventsource-disabled.js -//#SHA1: ebb7581b56a8dd86c5385eba1befa9ef984a8065 -//----------------- -"use strict"; - -test("EventSource is undefined", () => { - expect(typeof EventSource).toBe("undefined"); -}); - -//<#END_FILE: test-eventsource-disabled.js diff --git a/test/js/node/test/parallel/eventtarget-once-twice.test.js b/test/js/node/test/parallel/eventtarget-once-twice.test.js deleted file mode 100644 index 74f19e01b0..0000000000 --- a/test/js/node/test/parallel/eventtarget-once-twice.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-eventtarget-once-twice.js -//#SHA1: dd2fea0f3c839d77c64423ed3c2cbdd319365141 -//----------------- -"use strict"; - -const { once } = require("events"); - -test("once can be called twice on EventTarget", async () => { - const et = new EventTarget(); - - const promise = (async () => { - await once(et, "foo"); - await once(et, "foo"); - })(); - - et.dispatchEvent(new Event("foo")); - setImmediate(() => { - et.dispatchEvent(new Event("foo")); - }); - - await expect(promise).resolves.toBeUndefined(); -}); - -//<#END_FILE: test-eventtarget-once-twice.js diff --git a/test/js/node/test/parallel/fetch-mock.test.js b/test/js/node/test/parallel/fetch-mock.test.js deleted file mode 100644 index 84bce7c9a7..0000000000 --- a/test/js/node/test/parallel/fetch-mock.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-fetch-mock.js -//#SHA1: 92268be36b00e63e18a837088b9718344c3bff4f -//----------------- -"use strict"; - -test("should correctly stub globalThis.fetch", async () => { - const customFetch = async url => { - return { - text: async () => "foo", - }; - }; - - const originalFetch = globalThis.fetch; - globalThis.fetch = jest.fn(customFetch); - - const response = await globalThis.fetch("some-url"); - const text = await response.text(); - - expect(text).toBe("foo"); - expect(globalThis.fetch).toHaveBeenCalledWith("some-url"); - - // Restore the original fetch - globalThis.fetch = originalFetch; -}); - -//<#END_FILE: test-fetch-mock.js diff --git a/test/js/node/test/parallel/file-read-noexist.test.js b/test/js/node/test/parallel/file-read-noexist.test.js deleted file mode 100644 index 199afac796..0000000000 --- a/test/js/node/test/parallel/file-read-noexist.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-file-read-noexist.js -//#SHA1: c4cbb5dc6abca53c1dd513a4ece2741c1fc2235a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); -const path = require("path"); - -const filename = path.join(__dirname, "fixtures", "does_not_exist.txt"); - -test("fs.readFile with non-existent file", async () => { - await expect( - new Promise((resolve, reject) => { - fs.readFile(filename, "latin1", (err, content) => { - if (err) reject(err); - else resolve(content); - }); - }), - ).rejects.toMatchObject({ - code: "ENOENT", - message: expect.any(String), - }); -}); - -//<#END_FILE: test-file-read-noexist.js diff --git a/test/js/node/test/parallel/finalization-registry-shutdown.test.js b/test/js/node/test/parallel/finalization-registry-shutdown.test.js deleted file mode 100644 index 9dfd3c99c2..0000000000 --- a/test/js/node/test/parallel/finalization-registry-shutdown.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-finalization-registry-shutdown.js -//#SHA1: c5da440f8ea977c501bb5920f55ce69b1da6e28d -//----------------- -// Flags: --expose-gc -"use strict"; - -// This test verifies that when a V8 FinalizationRegistryCleanupTask is queue -// at the last moment when JavaScript can be executed, the callback of a -// FinalizationRegistry will not be invoked and the process should exit -// normally. - -test("FinalizationRegistry callback should not be called during shutdown", () => { - const mockCallback = jest.fn(); - const reg = new FinalizationRegistry(mockCallback); - - function register() { - // Create a temporary object in a new function scope to allow it to be GC-ed. - reg.register({}); - } - - const exitHandler = () => { - // This is the final chance to execute JavaScript. - register(); - // Queue a FinalizationRegistryCleanupTask by a testing gc request. - global.gc(); - }; - - process.on("exit", exitHandler); - - // Simulate the exit process - exitHandler(); - - // Verify that the callback was not called - expect(mockCallback).not.toHaveBeenCalled(); - - // Clean up - process.removeListener("exit", exitHandler); -}); - -//<#END_FILE: test-finalization-registry-shutdown.js diff --git a/test/js/node/test/parallel/fs-chown-type-check.test.js b/test/js/node/test/parallel/fs-chown-type-check.test.js deleted file mode 100644 index 4a899dd46e..0000000000 --- a/test/js/node/test/parallel/fs-chown-type-check.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-fs-chown-type-check.js -//#SHA1: 6ecbd3ae2da32793768e64e9252d749ddf36c85a -//----------------- -"use strict"; - -const fs = require("fs"); - -describe("fs.chown and fs.chownSync type checks", () => { - test("invalid path argument", () => { - [false, 1, {}, [], null, undefined].forEach(invalidPath => { - expect(() => fs.chown(invalidPath, 1, 1, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - expect(() => fs.chownSync(invalidPath, 1, 1)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); - }); - - test("invalid uid or gid arguments", () => { - [false, "test", {}, [], null, undefined].forEach(invalidId => { - expect(() => fs.chown("not_a_file_that_exists", invalidId, 1, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - expect(() => fs.chown("not_a_file_that_exists", 1, invalidId, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - expect(() => fs.chownSync("not_a_file_that_exists", invalidId, 1)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - expect(() => fs.chownSync("not_a_file_that_exists", 1, invalidId)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); - }); -}); - -//<#END_FILE: test-fs-chown-type-check.js diff --git a/test/js/node/test/parallel/fs-constants.test.js b/test/js/node/test/parallel/fs-constants.test.js deleted file mode 100644 index 0d1332c1bb..0000000000 --- a/test/js/node/test/parallel/fs-constants.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-fs-constants.js -//#SHA1: 6113ac0dd5e6b3d59252a25e6ddae61b589ca362 -//----------------- -"use strict"; - -const fs = require("fs"); - -test("fs constants for Windows chmod() are defined", () => { - expect(fs.constants.S_IRUSR).toBeDefined(); - expect(fs.constants.S_IWUSR).toBeDefined(); -}); - -//<#END_FILE: test-fs-constants.js diff --git a/test/js/node/test/parallel/fs-copyfile-respect-permissions.test.js b/test/js/node/test/parallel/fs-copyfile-respect-permissions.test.js deleted file mode 100644 index 39d72c9cd6..0000000000 --- a/test/js/node/test/parallel/fs-copyfile-respect-permissions.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-fs-copyfile-respect-permissions.js -//#SHA1: 5a5d15dd2a31fab3f8cffa86fcaedc7dab528c9f -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const isWindows = process.platform === 'win32'; -const isIBMi = process.platform === 'os400'; - -if (!isWindows && process.getuid() === 0) { - it.skip('should not run as root', () => {}); -} else if (isIBMi) { - it.skip('IBMi has a different access permission mechanism', () => {}); -} else { - const tmpdir = path.join(os.tmpdir(), 'test-fs-copyfile-respect-permissions'); - - beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - }); - - afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); - }); - - let n = 0; - - function beforeEach() { - n++; - const source = path.join(tmpdir, `source${n}`); - const dest = path.join(tmpdir, `dest${n}`); - fs.writeFileSync(source, 'source'); - fs.writeFileSync(dest, 'dest'); - fs.chmodSync(dest, '444'); - - const check = (err) => { - const expected = ['EACCES', 'EPERM']; - expect(expected).toContain(err.code); - expect(fs.readFileSync(dest, 'utf8')).toBe('dest'); - return true; - }; - - return { source, dest, check }; - } - - test('synchronous API', () => { - const { source, dest, check } = beforeEach(); - expect(() => { fs.copyFileSync(source, dest); }).toThrow(expect.objectContaining({ - message: expect.any(String), - code: expect.stringMatching(/^(EACCES|EPERM)$/) - })); - }); - - test('promises API', async () => { - const { source, dest, check } = beforeEach(); - await expect(fs.promises.copyFile(source, dest)).rejects.toThrow(expect.objectContaining({ - message: expect.any(String), - code: expect.stringMatching(/^(EACCES|EPERM)$/) - })); - }); - - test('callback API', (done) => { - const { source, dest, check } = beforeEach(); - fs.copyFile(source, dest, (err) => { - expect(check(err)).toBe(true); - done(); - }); - }); -} - -//<#END_FILE: test-fs-copyfile-respect-permissions.js diff --git a/test/js/node/test/parallel/fs-empty-readstream.test.js b/test/js/node/test/parallel/fs-empty-readstream.test.js deleted file mode 100644 index cc018a90b7..0000000000 --- a/test/js/node/test/parallel/fs-empty-readstream.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-fs-empty-readStream.js -//#SHA1: 979f558eb3c86e2d897cb766be1f300bbb0cbf8c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; -const fs = require('fs'); -const path = require('path'); - -const emptyFile = path.join(__dirname, '..', 'fixtures', 'empty.txt'); - -test('read stream on empty file should not emit data event', (done) => { - fs.open(emptyFile, 'r', (err, fd) => { - expect(err).toBeNull(); - const read = fs.createReadStream(emptyFile, { fd }); - - const dataHandler = jest.fn(); - read.once('data', dataHandler); - - read.once('end', () => { - expect(dataHandler).not.toHaveBeenCalled(); - done(); - }); - }); -}); - -test('paused read stream on empty file should not emit data or end events', (done) => { - fs.open(emptyFile, 'r', (err, fd) => { - expect(err).toBeNull(); - const read = fs.createReadStream(emptyFile, { fd }); - - read.pause(); - - const dataHandler = jest.fn(); - read.once('data', dataHandler); - - const endHandler = jest.fn(); - read.once('end', endHandler); - - setTimeout(() => { - expect(read.isPaused()).toBe(true); - expect(dataHandler).not.toHaveBeenCalled(); - expect(endHandler).not.toHaveBeenCalled(); - done(); - }, 50); - }); -}); - -//<#END_FILE: test-fs-empty-readStream.js diff --git a/test/js/node/test/parallel/fs-exists.test.js b/test/js/node/test/parallel/fs-exists.test.js deleted file mode 100644 index 3cea91fc1c..0000000000 --- a/test/js/node/test/parallel/fs-exists.test.js +++ /dev/null @@ -1,74 +0,0 @@ -//#FILE: test-fs-exists.js -//#SHA1: b7dcbc226b81e0ae4a111cbab39b87672a02172c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); -const f = __filename; - -test("fs.exists throws with invalid arguments", () => { - expect(() => fs.exists(f)).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); - expect(() => fs.exists()).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); - expect(() => fs.exists(f, {})).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); -}); - -test("fs.exists with valid file", done => { - fs.exists(f, y => { - expect(y).toBe(true); - done(); - }); -}); - -test("fs.exists with non-existent file", done => { - fs.exists(`${f}-NO`, y => { - expect(y).toBe(false); - done(); - }); -}); - -test("fs.exists with invalid path", done => { - fs.exists(new URL("https://foo"), y => { - expect(y).toBe(false); - done(); - }); -}); - -test("fs.exists with object as path", done => { - fs.exists({}, y => { - expect(y).toBe(false); - done(); - }); -}); - -test("fs.existsSync", () => { - expect(fs.existsSync(f)).toBe(true); - expect(fs.existsSync(`${f}-NO`)).toBe(false); -}); - -test("fs.existsSync never throws", () => { - expect(fs.existsSync()).toBe(false); - expect(fs.existsSync({})).toBe(false); - expect(fs.existsSync(new URL("https://foo"))).toBe(false); -}); - -//<#END_FILE: test-fs-exists.js diff --git a/test/js/node/test/parallel/fs-fsync.test.js b/test/js/node/test/parallel/fs-fsync.test.js deleted file mode 100644 index b4d17d9ac9..0000000000 --- a/test/js/node/test/parallel/fs-fsync.test.js +++ /dev/null @@ -1,79 +0,0 @@ -//#FILE: test-fs-fsync.js -//#SHA1: 4225be75eaedfd17c32e0472e6739ba232b7f28e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const fileFixture = path.join(__dirname, '..', 'fixtures', 'a.js'); -const tmpdir = path.join(os.tmpdir(), 'test-fs-fsync'); -const fileTemp = path.join(tmpdir, 'a.js'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - fs.copyFileSync(fileFixture, fileTemp); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('fsync and fdatasync operations', (done) => { - fs.open(fileTemp, 'a', 0o777, (err, fd) => { - expect(err).toBeNull(); - - fs.fdatasyncSync(fd); - fs.fsyncSync(fd); - - fs.fdatasync(fd, (err) => { - expect(err).toBeNull(); - fs.fsync(fd, (err) => { - expect(err).toBeNull(); - fs.closeSync(fd); - done(); - }); - }); - }); -}); - -test('invalid inputs throw TypeError', () => { - const invalidInputs = ['', false, null, undefined, {}, []]; - const errObj = { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError' - }; - - invalidInputs.forEach((input) => { - expect(() => fs.fdatasync(input)).toThrow(expect.objectContaining(errObj)); - expect(() => fs.fdatasyncSync(input)).toThrow(expect.objectContaining(errObj)); - expect(() => fs.fsync(input)).toThrow(expect.objectContaining(errObj)); - expect(() => fs.fsyncSync(input)).toThrow(expect.objectContaining(errObj)); - }); -}); - -//<#END_FILE: test-fs-fsync.js diff --git a/test/js/node/test/parallel/fs-link.test.js b/test/js/node/test/parallel/fs-link.test.js deleted file mode 100644 index 4b6be724f2..0000000000 --- a/test/js/node/test/parallel/fs-link.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-fs-link.js -//#SHA1: 255940f3f953a4bd693b3e475bc466d5f759875f -//----------------- -"use strict"; -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const tmpdir = { - refresh: () => { - // Implement a simple tmpdir.refresh() function - const testDir = path.join(os.tmpdir(), "test-fs-link"); - fs.rmSync(testDir, { recursive: true, force: true }); - fs.mkdirSync(testDir, { recursive: true }); - return testDir; - }, - resolve: filename => path.join(tmpdir.refresh(), filename), -}; - -test("Test creating and reading hard link", done => { - const srcPath = tmpdir.resolve("hardlink-target.txt"); - const dstPath = tmpdir.resolve("link1.js"); - fs.writeFileSync(srcPath, "hello world"); - - fs.link(srcPath, dstPath, err => { - expect(err).toBeFalsy(); - const dstContent = fs.readFileSync(dstPath, "utf8"); - expect(dstContent).toBe("hello world"); - done(); - }); -}); - -test("test error outputs", () => { - [false, 1, [], {}, null, undefined].forEach(i => { - expect(() => fs.link(i, "", () => {})).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - expect(() => fs.link("", i, () => {})).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - expect(() => fs.linkSync(i, "")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - expect(() => fs.linkSync("", i)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); -}); - -//<#END_FILE: test-fs-link.js diff --git a/test/js/node/test/parallel/fs-make-callback.test.js b/test/js/node/test/parallel/fs-make-callback.test.js deleted file mode 100644 index db3860cbd6..0000000000 --- a/test/js/node/test/parallel/fs-make-callback.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-fs-make-callback.js -//#SHA1: 40bbb673a0865f464a2b9e40110e903b6cc9e0d6 -//----------------- -"use strict"; - -const fs = require("fs"); -const { sep } = require("path"); -const tmpdir = require("../common/tmpdir"); - -const callbackThrowValues = [null, true, false, 0, 1, "foo", /foo/, [], {}]; - -beforeEach(() => { - tmpdir.refresh(); -}); - -function testMakeCallback(cb) { - return function () { - // fs.mkdtemp() calls makeCallback() on its third argument - return fs.mkdtemp(`${tmpdir.path}${sep}`, {}, cb); - }; -} - -describe("fs.makeCallback", () => { - test("invalid callbacks throw TypeError", () => { - callbackThrowValues.forEach(value => { - expect(testMakeCallback(value)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - }); -}); - -//<#END_FILE: test-fs-make-callback.js diff --git a/test/js/node/test/parallel/fs-makestatscallback.test.js b/test/js/node/test/parallel/fs-makestatscallback.test.js deleted file mode 100644 index 839e3b4f45..0000000000 --- a/test/js/node/test/parallel/fs-makestatscallback.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-fs-makeStatsCallback.js -//#SHA1: e8c59eddd5ca920ba0a1aaa4dd87c3af879db3b1 -//----------------- -'use strict'; -const fs = require('fs'); - -function testMakeStatsCallback(cb) { - return function() { - // fs.stat() calls makeStatsCallback() on its second argument - fs.stat(__filename, cb); - }; -} - -test('Verify the case where a callback function is provided', (done) => { - testMakeStatsCallback(() => { - done(); - })(); -}); - -test('Invalid callback throws TypeError', () => { - const callbackThrowValues = [null, true, false, 0, 1, 'foo', /foo/, [], {}]; - - callbackThrowValues.forEach((value) => { - expect(testMakeStatsCallback(value)).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); - }); -}); - -//<#END_FILE: test-fs-makeStatsCallback.js diff --git a/test/js/node/test/parallel/fs-mkdir-recursive-eaccess.test.js b/test/js/node/test/parallel/fs-mkdir-recursive-eaccess.test.js deleted file mode 100644 index 570d2617ff..0000000000 --- a/test/js/node/test/parallel/fs-mkdir-recursive-eaccess.test.js +++ /dev/null @@ -1,81 +0,0 @@ -//#FILE: test-fs-mkdir-recursive-eaccess.js -//#SHA1: 1e0e4f480b7573549c130b4177759bd60adc1890 -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); -const os = require('os'); - -const isWindows = process.platform === 'win32'; -const isIBMi = process.platform === 'os400'; - -if (isIBMi) { - console.log('Skipped: IBMi has a different access permission mechanism'); - process.exit(0); -} - -const tmpdir = path.join(os.tmpdir(), 'test-fs-mkdir-recursive-eaccess'); - -let n = 0; - -function makeDirectoryReadOnly(dir) { - let accessErrorCode = 'EACCES'; - if (isWindows) { - accessErrorCode = 'EPERM'; - execSync(`icacls ${dir} /deny "everyone:(OI)(CI)(DE,DC,AD,WD)"`); - } else { - fs.chmodSync(dir, '444'); - } - return accessErrorCode; -} - -function makeDirectoryWritable(dir) { - if (isWindows) { - execSync(`icacls ${dir} /remove:d "everyone"`); - } -} - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('Synchronous API should return an EACCES/EPERM error with path populated', () => { - const dir = path.join(tmpdir, `mkdirp_${n++}`); - fs.mkdirSync(dir); - const codeExpected = makeDirectoryReadOnly(dir); - - expect(() => { - fs.mkdirSync(path.join(dir, '/foo'), { recursive: true }); - }).toThrow(expect.objectContaining({ - code: codeExpected, - path: expect.any(String) - })); - - makeDirectoryWritable(dir); -}); - -test('Asynchronous API should return an EACCES/EPERM error with path populated', (done) => { - const dir = path.join(tmpdir, `mkdirp_${n++}`); - fs.mkdirSync(dir); - const codeExpected = makeDirectoryReadOnly(dir); - - fs.mkdir(path.join(dir, '/bar'), { recursive: true }, (err) => { - makeDirectoryWritable(dir); - expect(err).toEqual(expect.objectContaining({ - code: codeExpected, - path: expect.any(String) - })); - done(); - }); -}); - -//<#END_FILE: test-fs-mkdir-recursive-eaccess.js diff --git a/test/js/node/test/parallel/fs-mkdtemp-prefix-check.test.js b/test/js/node/test/parallel/fs-mkdtemp-prefix-check.test.js deleted file mode 100644 index 81690d9c17..0000000000 --- a/test/js/node/test/parallel/fs-mkdtemp-prefix-check.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-fs-mkdtemp-prefix-check.js -//#SHA1: f17d58f63200ae9b8c855b9def7da223fc5db531 -//----------------- -"use strict"; -const fs = require("fs"); - -const prefixValues = [undefined, null, 0, true, false, 1]; - -describe("fs.mkdtempSync prefix check", () => { - test.each(prefixValues)("should throw for invalid prefix: %p", value => { - expect(() => { - fs.mkdtempSync(value, {}); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -describe("fs.mkdtemp prefix check", () => { - test.each(prefixValues)("should throw for invalid prefix: %p", value => { - expect(() => { - fs.mkdtemp(value, jest.fn()); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-fs-mkdtemp-prefix-check.js diff --git a/test/js/node/test/parallel/fs-non-number-arguments-throw.test.js b/test/js/node/test/parallel/fs-non-number-arguments-throw.test.js deleted file mode 100644 index fa7ff3127d..0000000000 --- a/test/js/node/test/parallel/fs-non-number-arguments-throw.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-fs-non-number-arguments-throw.js -//#SHA1: 65db5c653216831bc16d38c5d659fbffa296d3d8 -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const tmpdir = path.join(os.tmpdir(), 'test-fs-non-number-arguments-throw'); -const tempFile = path.join(tmpdir, 'fs-non-number-arguments-throw'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - fs.writeFileSync(tempFile, 'abc\ndef'); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('createReadStream with valid number arguments', (done) => { - const sanity = 'def'; - const saneEmitter = fs.createReadStream(tempFile, { start: 4, end: 6 }); - - saneEmitter.on('data', (data) => { - expect(data.toString('utf8')).toBe(sanity); - done(); - }); -}); - -test('createReadStream throws with string start argument', () => { - expect(() => { - fs.createReadStream(tempFile, { start: '4', end: 6 }); - }).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); -}); - -test('createReadStream throws with string end argument', () => { - expect(() => { - fs.createReadStream(tempFile, { start: 4, end: '6' }); - }).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); -}); - -test('createWriteStream throws with string start argument', () => { - expect(() => { - fs.createWriteStream(tempFile, { start: '4' }); - }).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); -}); - -//<#END_FILE: test-fs-non-number-arguments-throw.js diff --git a/test/js/node/test/parallel/fs-open-mode-mask.test.js b/test/js/node/test/parallel/fs-open-mode-mask.test.js deleted file mode 100644 index 798005d296..0000000000 --- a/test/js/node/test/parallel/fs-open-mode-mask.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-fs-open-mode-mask.js -//#SHA1: d290e7c1bced1fc3a98f27e2aeb463051581376c -//----------------- -"use strict"; - -// This tests that the lower bits of mode > 0o777 still works in fs.open(). - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const mode = process.platform === "win32" ? 0o444 : 0o644; - -const maskToIgnore = 0o10000; - -const tmpdir = path.join(os.tmpdir(), "test-fs-open-mode-mask"); - -beforeAll(() => { - try { - fs.mkdirSync(tmpdir, { recursive: true }); - } catch (err) { - // Directory might already exist - } -}); - -afterAll(() => { - try { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } catch (err) { - // Ignore errors during cleanup - } -}); - -function test(mode, asString) { - const suffix = asString ? "str" : "num"; - const input = asString ? (mode | maskToIgnore).toString(8) : mode | maskToIgnore; - - it(`should work with ${suffix} input`, () => { - const file = path.join(tmpdir, `openSync-${suffix}.txt`); - const fd = fs.openSync(file, "w+", input); - expect(fs.fstatSync(fd).mode & 0o777).toBe(mode); - fs.closeSync(fd); - expect(fs.statSync(file).mode & 0o777).toBe(mode); - }); - - it(`should work with ${suffix} input using callback`, done => { - const file = path.join(tmpdir, `open-${suffix}.txt`); - fs.open(file, "w+", input, (err, fd) => { - expect(err).toBeNull(); - expect(fs.fstatSync(fd).mode & 0o777).toBe(mode); - fs.closeSync(fd); - expect(fs.statSync(file).mode & 0o777).toBe(mode); - done(); - }); - }); -} - -test(mode, true); -test(mode, false); - -//<#END_FILE: test-fs-open-mode-mask.js diff --git a/test/js/node/test/parallel/fs-open-no-close.test.js b/test/js/node/test/parallel/fs-open-no-close.test.js deleted file mode 100644 index fbd17dc78c..0000000000 --- a/test/js/node/test/parallel/fs-open-no-close.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-fs-open-no-close.js -//#SHA1: 3f09a04c65d9a376e5d9b82882d375ab1dc99ad9 -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const debuglog = (arg) => { - console.log(new Date().toLocaleString(), arg); -}; - -const tmpdir = path.join(os.tmpdir(), 'test-fs-open-no-close'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('fs.open should not keep the event loop open if file is not closed', (done) => { - let openFd; - - fs.open(path.join(tmpdir, 'dummy'), 'wx+', (err, fd) => { - debuglog('fs open() callback'); - expect(err).toBeFalsy(); - openFd = fd; - done(); - }); - - debuglog('waiting for callback'); - - // Simulate process.on('beforeExit') behavior - process.nextTick(() => { - if (openFd) { - fs.closeSync(openFd); - } - }); -}); - -//<#END_FILE: test-fs-open-no-close.js diff --git a/test/js/node/test/parallel/fs-open-numeric-flags.test.js b/test/js/node/test/parallel/fs-open-numeric-flags.test.js deleted file mode 100644 index 4edec0e495..0000000000 --- a/test/js/node/test/parallel/fs-open-numeric-flags.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-fs-open-numeric-flags.js -//#SHA1: 31a49fd78cbd63ab0b41de5f051d029bbe22fded -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// Create a temporary directory for our tests -const tmpdir = path.join(os.tmpdir(), "test-fs-open-numeric-flags"); - -beforeEach(() => { - // Ensure the temporary directory exists and is empty - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterEach(() => { - // Clean up the temporary directory after each test - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } -}); - -test("O_WRONLY without O_CREAT shall fail with ENOENT", () => { - const pathNE = path.join(tmpdir, "file-should-not-exist"); - - expect(() => { - fs.openSync(pathNE, fs.constants.O_WRONLY); - }).toThrow( - expect.objectContaining({ - code: "ENOENT", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-fs-open-numeric-flags.js diff --git a/test/js/node/test/parallel/fs-open.test.js b/test/js/node/test/parallel/fs-open.test.js deleted file mode 100644 index c8c102d7a3..0000000000 --- a/test/js/node/test/parallel/fs-open.test.js +++ /dev/null @@ -1,102 +0,0 @@ -//#FILE: test-fs-open.js -//#SHA1: 0466ad8882a3256fdd8da5fc8da3167f6dde4fd6 -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); - -test('fs.openSync throws ENOENT for non-existent file', () => { - expect(() => { - fs.openSync('/8hvftyuncxrt/path/to/file/that/does/not/exist', 'r'); - }).toThrow(expect.objectContaining({ - code: 'ENOENT', - message: expect.any(String) - })); -}); - -test('fs.openSync succeeds for existing file', () => { - expect(() => fs.openSync(__filename)).not.toThrow(); -}); - -test('fs.open succeeds with various valid arguments', async () => { - await expect(fs.promises.open(__filename)).resolves.toBeDefined(); - await expect(fs.promises.open(__filename, 'r')).resolves.toBeDefined(); - await expect(fs.promises.open(__filename, 'rs')).resolves.toBeDefined(); - await expect(fs.promises.open(__filename, 'r', 0)).resolves.toBeDefined(); - await expect(fs.promises.open(__filename, 'r', null)).resolves.toBeDefined(); -}); - -test('fs.open throws for invalid mode argument', () => { - expect(() => fs.open(__filename, 'r', 'boom', () => {})).toThrow(({ - code: 'ERR_INVALID_ARG_VALUE', - name: 'TypeError', - message: `The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received boom` - })); - expect(() => fs.open(__filename, 'r', 5.5, () => {})).toThrow(({ - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', - message: `The value of "mode" is out of range. It must be an integer. Received 5.5` - })); - expect(() => fs.open(__filename, 'r', -7, () => {})).toThrow(({ - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', - message: `The value of "mode" is out of range. It must be >= 0 and <= 4294967295. Received -7` - })); - expect(() => fs.open(__filename, 'r', 4304967295, () => {})).toThrow(({ - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', - message: `The value of "mode" is out of range. It must be >= 0 and <= 4294967295. Received 4304967295` - })); -}); - -test('fs.open throws for invalid argument combinations', () => { - const invalidArgs = [[], ['r'], ['r', 0], ['r', 0, 'bad callback']]; - invalidArgs.forEach(args => { - expect(() => fs.open(__filename, ...args)).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); - }); -}); - -test('fs functions throw for invalid path types', () => { - const invalidPaths = [false, 1, [], {}, null, undefined]; - invalidPaths.forEach(path => { - expect(() => fs.open(path, 'r', () => {})).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); - expect(() => fs.openSync(path, 'r')).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); - expect(fs.promises.open(path, 'r')).rejects.toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); - }); -}); - -test('fs functions throw for invalid modes', () => { - const invalidModes = [false, [], {}]; - invalidModes.forEach(mode => { - expect(() => fs.open(__filename, 'r', mode, () => {})).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - message: expect.any(String) - })); - expect(() => fs.openSync(__filename, 'r', mode)).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - message: expect.any(String) - })); - expect(fs.promises.open(__filename, 'r', mode)).rejects.toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - message: expect.any(String) - })); - }); -}); - -//<#END_FILE: test-fs-open.js diff --git a/test/js/node/test/parallel/fs-operations-with-surrogate-pairs.test.js b/test/js/node/test/parallel/fs-operations-with-surrogate-pairs.test.js deleted file mode 100644 index 54c9021472..0000000000 --- a/test/js/node/test/parallel/fs-operations-with-surrogate-pairs.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-fs-operations-with-surrogate-pairs.js -//#SHA1: c59fe103e9ec4edee50c9186d341f5fdd32e0af4 -//----------------- -"use strict"; - -const fs = require("node:fs"); -const path = require("node:path"); -const tmpdir = require("../common/tmpdir"); - -tmpdir.refresh(); - -describe("File operations with filenames containing surrogate pairs", () => { - it("should write, read, and delete a file with surrogate pairs in the filename", () => { - // Create a temporary directory - const tempdir = fs.mkdtempSync(tmpdir.resolve("emoji-fruit-🍇 🍈 🍉 🍊 🍋")); - expect(fs.existsSync(tempdir)).toBe(true); - - const filename = "🚀🔥🛸.txt"; - const content = "Test content"; - - // Write content to a file - fs.writeFileSync(path.join(tempdir, filename), content); - - // Read content from the file - const readContent = fs.readFileSync(path.join(tempdir, filename), "utf8"); - - // Check if the content matches - expect(readContent).toBe(content); - }); -}); - -//<#END_FILE: test-fs-operations-with-surrogate-pairs.js diff --git a/test/js/node/test/parallel/fs-options-immutable.test.js b/test/js/node/test/parallel/fs-options-immutable.test.js deleted file mode 100644 index 9beef4c128..0000000000 --- a/test/js/node/test/parallel/fs-options-immutable.test.js +++ /dev/null @@ -1,148 +0,0 @@ -//#FILE: test-fs-options-immutable.js -//#SHA1: 3e986f4e0d29505ada9980c8af5146abd307ddb7 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// These tests make sure that the `options` object passed to these functions are -// never altered. -// -// Refer: https://github.com/nodejs/node/issues/7655 - -const originalOptions = {}; -let options; - -beforeEach(() => { - options = JSON.parse(JSON.stringify(originalOptions)); -}); - -const tmpdir = { - path: path.join(os.tmpdir(), "node-test-fs-options-immutable"), - refresh: () => { - try { - fs.rmSync(tmpdir.path, { recursive: true, force: true }); - } catch (error) { - // Ignore errors - } - fs.mkdirSync(tmpdir.path, { recursive: true }); - }, - resolve: filename => path.join(tmpdir.path, filename), -}; - -tmpdir.refresh(); - -test("fs.readFile", async () => { - await fs.promises.readFile(__filename, options); - expect(options).toEqual(originalOptions); -}); - -test("fs.readFileSync", () => { - fs.readFileSync(__filename, options); - expect(options).toEqual(originalOptions); -}); - -test("fs.readdir", async () => { - await fs.promises.readdir(__dirname, options); - expect(options).toEqual(originalOptions); -}); - -test("fs.readdirSync", () => { - fs.readdirSync(__dirname, options); - expect(options).toEqual(originalOptions); -}); - -test("fs.readlink and fs.readlinkSync", async () => { - const canCreateSymLink = await new Promise(resolve => { - fs.symlink(__filename, "dummy-symlink", err => { - if (err) resolve(false); - fs.unlink("dummy-symlink", () => resolve(true)); - }); - }); - - if (canCreateSymLink) { - const sourceFile = tmpdir.resolve("test-readlink"); - const linkFile = tmpdir.resolve("test-readlink-link"); - - await fs.promises.writeFile(sourceFile, ""); - await fs.promises.symlink(sourceFile, linkFile); - - await fs.promises.readlink(linkFile, options); - expect(options).toEqual(originalOptions); - - fs.readlinkSync(linkFile, options); - expect(options).toEqual(originalOptions); - } else { - test.skip("Symlink tests skipped - cannot create symlinks", () => {}); - } -}); - -test("fs.writeFile and fs.writeFileSync", async () => { - const fileName = tmpdir.resolve("writeFile"); - fs.writeFileSync(fileName, "ABCD", options); - expect(options).toEqual(originalOptions); - - await fs.promises.writeFile(fileName, "ABCD", options); - expect(options).toEqual(originalOptions); -}); - -test("fs.appendFile and fs.appendFileSync", async () => { - const fileName = tmpdir.resolve("appendFile"); - fs.appendFileSync(fileName, "ABCD", options); - expect(options).toEqual(originalOptions); - - await fs.promises.appendFile(fileName, "ABCD", options); - expect(options).toEqual(originalOptions); -}); - -test("fs.watch", () => { - if (process.platform === "os400") { - return test.skip("IBMi does not support fs.watch()"); - } - - const watch = fs.watch(__filename, options, () => {}); - watch.close(); - expect(options).toEqual(originalOptions); -}); - -test("fs.watchFile and fs.unwatchFile", () => { - fs.watchFile(__filename, options, () => {}); - fs.unwatchFile(__filename); - expect(options).toEqual(originalOptions); -}); - -test("fs.realpath and fs.realpathSync", async () => { - fs.realpathSync(__filename, options); - expect(options).toEqual(originalOptions); - - await fs.promises.realpath(__filename, options); - expect(options).toEqual(originalOptions); -}); - -test("fs.mkdtemp and fs.mkdtempSync", async () => { - const tempFileName = tmpdir.resolve("mkdtemp-"); - fs.mkdtempSync(tempFileName, options); - expect(options).toEqual(originalOptions); - - await fs.promises.mkdtemp(tempFileName, options); - expect(options).toEqual(originalOptions); -}); - -test("fs.WriteStream and fs.ReadStream", done => { - const fileName = tmpdir.resolve("streams"); - const writeStream = fs.createWriteStream(fileName, options); - writeStream.once("open", () => { - expect(options).toEqual(originalOptions); - const readStream = fs.createReadStream(fileName, options); - readStream.once("open", () => { - expect(options).toEqual(originalOptions); - readStream.destroy(); - writeStream.end(); - done(); - }); - }); -}); - -//<#END_FILE: test-fs-options-immutable.js diff --git a/test/js/node/test/parallel/fs-promises-exists.test.js b/test/js/node/test/parallel/fs-promises-exists.test.js deleted file mode 100644 index 303718b0ef..0000000000 --- a/test/js/node/test/parallel/fs-promises-exists.test.js +++ /dev/null @@ -1,14 +0,0 @@ -//#FILE: test-fs-promises-exists.js -//#SHA1: 3766c49e29d13338f3124165428e3a8a37d47fab -//----------------- -"use strict"; - -const fs = require("fs"); -const fsPromises = require("fs/promises"); - -test("fs.promises exists and is correctly linked", () => { - expect(fsPromises).toBe(fs.promises); - expect(fsPromises.constants).toBe(fs.constants); -}); - -//<#END_FILE: test-fs-promises-exists.js diff --git a/test/js/node/test/parallel/fs-promises-file-handle-append-file.test.js b/test/js/node/test/parallel/fs-promises-file-handle-append-file.test.js deleted file mode 100644 index 746b7614a8..0000000000 --- a/test/js/node/test/parallel/fs-promises-file-handle-append-file.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-fs-promises-file-handle-append-file.js -//#SHA1: 2a1932450418ea18ef00a890342f29ab307006e7 -//----------------- -'use strict'; - -const fs = require('fs'); -const { open } = fs.promises; -const path = require('path'); -const os = require('os'); - -const tmpDir = path.join(os.tmpdir(), 'test-fs-promises-file-handle-append-file'); - -beforeAll(() => { - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpDir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); -}); - -test('FileHandle.appendFile with buffer', async () => { - const filePath = path.resolve(tmpDir, 'tmp-append-file-buffer.txt'); - const fileHandle = await open(filePath, 'a'); - const buffer = Buffer.from('a&Dp'.repeat(100), 'utf8'); - - await fileHandle.appendFile(buffer); - const appendedFileData = fs.readFileSync(filePath); - expect(appendedFileData).toEqual(buffer); - - await fileHandle.close(); -}); - -test('FileHandle.appendFile with string', async () => { - const filePath = path.resolve(tmpDir, 'tmp-append-file-string.txt'); - const fileHandle = await open(filePath, 'a'); - const string = 'x~yz'.repeat(100); - - await fileHandle.appendFile(string); - const stringAsBuffer = Buffer.from(string, 'utf8'); - const appendedFileData = fs.readFileSync(filePath); - expect(appendedFileData).toEqual(stringAsBuffer); - - await fileHandle.close(); -}); - -//<#END_FILE: test-fs-promises-file-handle-append-file.js diff --git a/test/js/node/test/parallel/fs-promises-file-handle-chmod.test.js b/test/js/node/test/parallel/fs-promises-file-handle-chmod.test.js deleted file mode 100644 index 8bd751f1d1..0000000000 --- a/test/js/node/test/parallel/fs-promises-file-handle-chmod.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-fs-promises-file-handle-chmod.js -//#SHA1: 50a28df8df34deeca4b2f9d7598fb596894d7541 -//----------------- -"use strict"; - -const fs = require("fs"); -const { open } = fs.promises; -const path = require("path"); -const os = require("os"); - -const tmpDir = os.tmpdir(); - -beforeEach(() => { - jest.spyOn(fs, "statSync"); -}); - -afterEach(() => { - jest.restoreAllMocks(); -}); - -test("FileHandle.chmod base functionality", async () => { - const filePath = path.resolve(tmpDir, "tmp-chmod.txt"); - const fileHandle = await open(filePath, "w+", 0o444); - - // File created with r--r--r-- 444 - const statsBeforeMod = fs.statSync(filePath); - expect(statsBeforeMod.mode & 0o444).toBe(0o444); - - let expectedAccess; - const newPermissions = 0o765; - - if (process.platform === "win32") { - // Chmod in Windows will only toggle read only/write access. The - // fs.Stats.mode in Windows is computed using read/write - // bits (not exec). Read-only at best returns 444; r/w 666. - // Refer: /deps/uv/src/win/fs.cfs; - expectedAccess = 0o664; - } else { - expectedAccess = newPermissions; - } - - // Change the permissions to rwxr--r-x - await fileHandle.chmod(newPermissions); - const statsAfterMod = fs.statSync(filePath); - expect(statsAfterMod.mode & expectedAccess).toBe(expectedAccess); - - await fileHandle.close(); -}); - -//<#END_FILE: test-fs-promises-file-handle-chmod.js diff --git a/test/js/node/test/parallel/fs-promises-file-handle-write.test.js b/test/js/node/test/parallel/fs-promises-file-handle-write.test.js deleted file mode 100644 index 1652a75a05..0000000000 --- a/test/js/node/test/parallel/fs-promises-file-handle-write.test.js +++ /dev/null @@ -1,93 +0,0 @@ -//#FILE: test-fs-promises-file-handle-write.js -//#SHA1: 6ca802494e0ce0ee3187b1661322f115cfd7340c -//----------------- -"use strict"; - -const fs = require("fs"); -const { open } = fs.promises; -const path = require("path"); -const os = require("os"); - -const tmpDir = path.join(os.tmpdir(), "test-fs-promises-file-handle-write"); - -beforeAll(() => { - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpDir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); -}); - -test("validateWrite", async () => { - const filePathForHandle = path.resolve(tmpDir, "tmp-write.txt"); - const fileHandle = await open(filePathForHandle, "w+"); - const buffer = Buffer.from("Hello world".repeat(100), "utf8"); - - await fileHandle.write(buffer, 0, buffer.length); - const readFileData = fs.readFileSync(filePathForHandle); - expect(readFileData).toEqual(buffer); - - await fileHandle.close(); -}); - -test("validateEmptyWrite", async () => { - const filePathForHandle = path.resolve(tmpDir, "tmp-empty-write.txt"); - const fileHandle = await open(filePathForHandle, "w+"); - const buffer = Buffer.from(""); // empty buffer - - await fileHandle.write(buffer, 0, buffer.length); - const readFileData = fs.readFileSync(filePathForHandle); - expect(readFileData).toEqual(buffer); - - await fileHandle.close(); -}); - -test("validateNonUint8ArrayWrite", async () => { - const filePathForHandle = path.resolve(tmpDir, "tmp-data-write.txt"); - const fileHandle = await open(filePathForHandle, "w+"); - const buffer = Buffer.from("Hello world", "utf8").toString("base64"); - - await fileHandle.write(buffer, 0, buffer.length); - const readFileData = fs.readFileSync(filePathForHandle); - expect(readFileData).toEqual(Buffer.from(buffer, "utf8")); - - await fileHandle.close(); -}); - -test("validateNonStringValuesWrite", async () => { - const filePathForHandle = path.resolve(tmpDir, "tmp-non-string-write.txt"); - const fileHandle = await open(filePathForHandle, "w+"); - const nonStringValues = [ - 123, - {}, - new Map(), - null, - undefined, - 0n, - () => {}, - Symbol(), - true, - new String("notPrimitive"), - { - toString() { - return "amObject"; - }, - }, - { [Symbol.toPrimitive]: hint => "amObject" }, - ]; - for (const nonStringValue of nonStringValues) { - await expect(fileHandle.write(nonStringValue)).rejects.toThrow( - expect.objectContaining({ - message: expect.stringMatching(/"buffer"/), - code: "ERR_INVALID_ARG_TYPE", - }), - ); - } - - await fileHandle.close(); -}); - -//<#END_FILE: test-fs-promises-file-handle-write.js diff --git a/test/js/node/test/parallel/fs-promises-readfile-empty.test.js b/test/js/node/test/parallel/fs-promises-readfile-empty.test.js deleted file mode 100644 index dc9d291e5f..0000000000 --- a/test/js/node/test/parallel/fs-promises-readfile-empty.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-fs-promises-readfile-empty.js -//#SHA1: 6fcec9b5d3c9617426d46c79fb79244bc236574b -//----------------- -"use strict"; - -const fs = require("fs").promises; -const path = require("path"); - -const fixturesPath = path.resolve(__dirname, "..", "fixtures"); -const fn = path.join(fixturesPath, "empty.txt"); - -test("fs.readFile on empty file", async () => { - const content = await fs.readFile(fn); - expect(content).toBeTruthy(); -}); - -test("fs.readFile on empty file with utf8 encoding", async () => { - const content = await fs.readFile(fn, "utf8"); - expect(content).toBe(""); -}); - -test("fs.readFile on empty file with options object", async () => { - const content = await fs.readFile(fn, { encoding: "utf8" }); - expect(content).toBe(""); -}); - -//<#END_FILE: test-fs-promises-readfile-empty.js diff --git a/test/js/node/test/parallel/fs-promises-readfile-with-fd.test.js b/test/js/node/test/parallel/fs-promises-readfile-with-fd.test.js deleted file mode 100644 index 1f286daab3..0000000000 --- a/test/js/node/test/parallel/fs-promises-readfile-with-fd.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-fs-promises-readfile-with-fd.js -//#SHA1: 041811f02dddcdb9eba7d97e3943e26ec6b881cd -//----------------- -'use strict'; - -const fs = require('fs'); -const fsPromises = require('fs').promises; -const path = require('path'); -const os = require('os'); - -const tmpdir = path.join(os.tmpdir(), 'test-fs-promises-readfile-with-fd'); -const fn = path.join(tmpdir, 'test.txt'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - fs.writeFileSync(fn, 'Hello World'); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('readFile() reads from current position of the file', async () => { - const handle = await fsPromises.open(fn, 'r'); - - // Read only five bytes, so that the position moves to five. - const buf = Buffer.alloc(5); - const { bytesRead } = await handle.read(buf, 0, 5, null); - expect(bytesRead).toBe(5); - expect(buf.toString()).toBe('Hello'); - - // readFile() should read from position five, instead of zero. - expect((await handle.readFile()).toString()).toBe(' World'); - - await handle.close(); -}); - -//<#END_FILE: test-fs-promises-readfile-with-fd.js diff --git a/test/js/node/test/parallel/fs-promises-writefile-typedarray.test.js b/test/js/node/test/parallel/fs-promises-writefile-typedarray.test.js deleted file mode 100644 index bece880bd3..0000000000 --- a/test/js/node/test/parallel/fs-promises-writefile-typedarray.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-fs-promises-writefile-typedarray.js -//#SHA1: 718d3827c56ad0b11c59a801bf9529a1e6e5ab89 -//----------------- -"use strict"; - -const fs = require("fs"); -const fsPromises = fs.promises; -const path = require("path"); -const os = require("os"); - -const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-")); - -beforeAll(() => { - // Ensure the temporary directory is clean - fs.rmSync(tmpDir, { recursive: true, force: true }); - fs.mkdirSync(tmpDir, { recursive: true }); -}); - -afterAll(() => { - // Clean up the temporary directory - fs.rmSync(tmpDir, { recursive: true, force: true }); -}); - -const dest = path.resolve(tmpDir, "tmp.txt"); -// Use a file size larger than `kReadFileMaxChunkSize`. -const buffer = Buffer.from("012".repeat(2 ** 14)); - -test("fsPromises.writeFile with TypedArrays", async () => { - const constructors = [Uint8Array, Uint16Array, Uint32Array]; - - for (const Constructor of constructors) { - const array = new Constructor(buffer.buffer); - await fsPromises.writeFile(dest, array); - const data = await fsPromises.readFile(dest); - expect(data).toEqual(buffer); - } -}); - -//<#END_FILE: test-fs-promises-writefile-typedarray.js diff --git a/test/js/node/test/parallel/fs-promises-writefile-with-fd.test.js b/test/js/node/test/parallel/fs-promises-writefile-with-fd.test.js deleted file mode 100644 index 17182bedb6..0000000000 --- a/test/js/node/test/parallel/fs-promises-writefile-with-fd.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-fs-promises-writefile-with-fd.js -//#SHA1: 55be58e0edcbdc914795c46280459a85071f28eb -//----------------- -"use strict"; - -// This test makes sure that `writeFile()` always writes from the current -// position of the file, instead of truncating the file. - -const fs = require("fs"); -const fsPromises = require("fs").promises; -const path = require("path"); -const os = require("os"); - -let tmpdir; - -beforeEach(() => { - tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test-")); -}); - -afterEach(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test("writeFile() writes from current position", async () => { - const fn = path.join(tmpdir, "test.txt"); - - const handle = await fsPromises.open(fn, "w"); - - /* Write only five bytes, so that the position moves to five. */ - const buf = Buffer.from("Hello"); - const { bytesWritten } = await handle.write(buf, 0, 5, null); - expect(bytesWritten).toBe(5); - - /* Write some more with writeFile(). */ - await handle.writeFile("World"); - - /* New content should be written at position five, instead of zero. */ - expect(fs.readFileSync(fn, "utf8")).toBe("HelloWorld"); - - await handle.close(); -}); - -//<#END_FILE: test-fs-promises-writefile-with-fd.js diff --git a/test/js/node/test/parallel/fs-promisified.test.js b/test/js/node/test/parallel/fs-promisified.test.js deleted file mode 100644 index 3df07cc98e..0000000000 --- a/test/js/node/test/parallel/fs-promisified.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-fs-promisified.js -//#SHA1: 5366497c2a750295d2c5cf65c2938e27f573e8bb -//----------------- -"use strict"; - -const fs = require("fs"); -const { promisify } = require("util"); - -const read = promisify(fs.read); -const write = promisify(fs.write); -const exists = promisify(fs.exists); - -test("promisified fs.read", async () => { - const fd = fs.openSync(__filename, "r"); - const obj = await read(fd, Buffer.alloc(1024), 0, 1024, null); - expect(typeof obj.bytesRead).toBe("number"); - expect(obj.buffer).toBeInstanceOf(Buffer); - fs.closeSync(fd); -}); - -test("promisified fs.write", async () => { - const tmpdir = require("../common/tmpdir"); - tmpdir.refresh(); - const filename = tmpdir.resolve("write-promise.txt"); - const fd = fs.openSync(filename, "w"); - const obj = await write(fd, Buffer.from("foobar")); - expect(typeof obj.bytesWritten).toBe("number"); - expect(obj.buffer.toString()).toBe("foobar"); - fs.closeSync(fd); -}); - -test("promisified fs.exists", async () => { - const result = await exists(__filename); - expect(result).toBe(true); -}); - -//<#END_FILE: test-fs-promisified.js diff --git a/test/js/node/test/parallel/fs-read-empty-buffer.test.js b/test/js/node/test/parallel/fs-read-empty-buffer.test.js deleted file mode 100644 index 04fe94f967..0000000000 --- a/test/js/node/test/parallel/fs-read-empty-buffer.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-fs-read-empty-buffer.js -//#SHA1: a2dc2c25e5a712b62c41298f885df24dd6106646 -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); - -const filepath = path.resolve(__dirname, 'x.txt'); -let fd; - -beforeAll(() => { - // Create a test file - fs.writeFileSync(filepath, 'test content'); - fd = fs.openSync(filepath, 'r'); -}); - -afterAll(() => { - fs.closeSync(fd); - fs.unlinkSync(filepath); -}); - -const buffer = new Uint8Array(); - -test('fs.readSync throws ERR_INVALID_ARG_VALUE for empty buffer', () => { - expect(() => fs.readSync(fd, buffer, 0, 10, 0)).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_VALUE', - message: expect.stringContaining('The argument \'buffer\' is empty and cannot be written') - })); -}); - -test('fs.read throws ERR_INVALID_ARG_VALUE for empty buffer', () => { - expect(() => fs.read(fd, buffer, 0, 1, 0, () => {})).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_VALUE', - message: expect.stringContaining('The argument \'buffer\' is empty and cannot be written') - })); -}); - -test('fsPromises.filehandle.read rejects with ERR_INVALID_ARG_VALUE for empty buffer', async () => { - const filehandle = await fs.promises.open(filepath, 'r'); - await expect(filehandle.read(buffer, 0, 1, 0)).rejects.toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_VALUE', - message: expect.stringContaining('The argument \'buffer\' is empty and cannot be written') - })); - await filehandle.close(); -}); - -//<#END_FILE: test-fs-read-empty-buffer.js diff --git a/test/js/node/test/parallel/fs-read-file-sync-hostname.test.js b/test/js/node/test/parallel/fs-read-file-sync-hostname.test.js deleted file mode 100644 index 9c4eb90ad8..0000000000 --- a/test/js/node/test/parallel/fs-read-file-sync-hostname.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-fs-read-file-sync-hostname.js -//#SHA1: 6e8bd1a34277c7b98b985ba23843555b05f80ccb -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); - -if (process.platform !== "linux") { - test.skip("Test is linux specific.", () => {}); -} else { - test("reading /proc/sys/kernel/hostname", () => { - // Test to make sure reading a file under the /proc directory works. See: - // https://groups.google.com/forum/#!topic/nodejs-dev/rxZ_RoH1Gn0 - const hostname = fs.readFileSync("/proc/sys/kernel/hostname"); - expect(hostname.length).toBeGreaterThan(0); - }); -} - -//<#END_FILE: test-fs-read-file-sync-hostname.js diff --git a/test/js/node/test/parallel/fs-read-optional-params.test.js b/test/js/node/test/parallel/fs-read-optional-params.test.js deleted file mode 100644 index 8abd08c20e..0000000000 --- a/test/js/node/test/parallel/fs-read-optional-params.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-fs-read-optional-params.js -//#SHA1: daea619faa084927d87381fc60aedde3068a13ca -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const filepath = path.join(os.tmpdir(), "x.txt"); -const expected = Buffer.from("xyz\n"); -const defaultBufferAsync = Buffer.alloc(16384); -const bufferAsOption = Buffer.allocUnsafe(expected.byteLength); - -beforeAll(() => { - fs.writeFileSync(filepath, expected); -}); - -afterAll(() => { - fs.unlinkSync(filepath); -}); - -function testValid(message, ...options) { - test(`${message} (as params)`, async () => { - const paramsFilehandle = fs.openSync(filepath, "r"); - await new Promise(resolve => { - fs.read(paramsFilehandle, ...options, (err, bytesRead, buffer) => { - expect(err).toBeNull(); - expect(bytesRead).toBe(expected.byteLength); - expect(buffer.byteLength).toBe(defaultBufferAsync.byteLength); - fs.closeSync(paramsFilehandle); - resolve(); - }); - }); - }); - - test(`${message} (as options)`, async () => { - const optionsFilehandle = fs.openSync(filepath, "r"); - await new Promise(resolve => { - fs.read(optionsFilehandle, bufferAsOption, ...options, (err, bytesRead, buffer) => { - expect(err).toBeNull(); - expect(bytesRead).toBe(expected.byteLength); - expect(buffer.byteLength).toBe(bufferAsOption.byteLength); - fs.closeSync(optionsFilehandle); - resolve(); - }); - }); - }); -} - -testValid("Not passing in any object"); -testValid("Passing in a null", null); -testValid("Passing in an empty object", {}); -testValid("Passing in an object", { - offset: 0, - length: bufferAsOption.byteLength, - position: 0, -}); - -//<#END_FILE: test-fs-read-optional-params.js diff --git a/test/js/node/test/parallel/fs-read-promises-optional-params.test.js b/test/js/node/test/parallel/fs-read-promises-optional-params.test.js deleted file mode 100644 index 60353b9226..0000000000 --- a/test/js/node/test/parallel/fs-read-promises-optional-params.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-fs-read-promises-optional-params.js -//#SHA1: bc986664534329fd86b9aafd4c73a0159f71d388 -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const { promisify } = require('util'); -const read = promisify(fs.read); - -const filepath = path.resolve(__dirname, 'x.txt'); -let fd; - -const expected = Buffer.from('xyz\n'); -const defaultBufferAsync = Buffer.alloc(16384); -const bufferAsOption = Buffer.allocUnsafe(expected.byteLength); - -beforeAll(() => { - // Create the test file - fs.writeFileSync(filepath, expected); - fd = fs.openSync(filepath, 'r'); -}); - -afterAll(() => { - fs.closeSync(fd); - fs.unlinkSync(filepath); -}); - -test('read with empty options object', async () => { - const { bytesRead, buffer } = await read(fd, {}); - expect(bytesRead).toBe(expected.byteLength); - expect(buffer.byteLength).toBe(defaultBufferAsync.byteLength); -}); - -test('read with buffer and position options', async () => { - const { bytesRead, buffer } = await read(fd, bufferAsOption, { position: 0 }); - expect(bytesRead).toBe(expected.byteLength); - expect(buffer.byteLength).toBe(bufferAsOption.byteLength); -}); - -//<#END_FILE: test-fs-read-promises-optional-params.js diff --git a/test/js/node/test/parallel/fs-read-stream-autoclose.test.js b/test/js/node/test/parallel/fs-read-stream-autoclose.test.js deleted file mode 100644 index 36ab013cd8..0000000000 --- a/test/js/node/test/parallel/fs-read-stream-autoclose.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-fs-read-stream-autoClose.js -//#SHA1: 0fbd57ecd5ae02143036c03cdca120bc7c3deea1 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const writeFile = path.join(os.tmpdir(), "write-autoClose.txt"); - -beforeEach(() => { - // Clean up the temporary directory - try { - fs.unlinkSync(writeFile); - } catch (err) { - // Ignore errors if file doesn't exist - } -}); - -test("fs.createWriteStream with autoClose option", done => { - const file = fs.createWriteStream(writeFile, { autoClose: true }); - - file.on("finish", () => { - expect(file.destroyed).toBe(false); - done(); - }); - - file.end("asd"); -}); - -//<#END_FILE: test-fs-read-stream-autoClose.js diff --git a/test/js/node/test/parallel/fs-read-stream-double-close.test.js b/test/js/node/test/parallel/fs-read-stream-double-close.test.js deleted file mode 100644 index 73603d1f1c..0000000000 --- a/test/js/node/test/parallel/fs-read-stream-double-close.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-fs-read-stream-double-close.js -//#SHA1: 066b117ee2b44bedfdce77d06389406b2474eb2f -//----------------- -'use strict'; - -const fs = require('fs'); - -test('double close on ReadStream', (done) => { - const s = fs.createReadStream(__filename); - - let closeCount = 0; - const checkClose = () => { - closeCount++; - if (closeCount === 2) { - done(); - } - }; - - s.close(checkClose); - s.close(checkClose); -}); - -test('double destroy on ReadStream', (done) => { - const s = fs.createReadStream(__filename); - - let destroyCount = 0; - const checkDestroy = () => { - destroyCount++; - if (destroyCount === 2) { - done(); - } - }; - - // This is a private API, but it is worth testing. close calls this - s.destroy(null, checkDestroy); - s.destroy(null, checkDestroy); -}); - -//<#END_FILE: test-fs-read-stream-double-close.js diff --git a/test/js/node/test/parallel/fs-read-stream-fd-leak.test.js b/test/js/node/test/parallel/fs-read-stream-fd-leak.test.js deleted file mode 100644 index 5c4e0dd55b..0000000000 --- a/test/js/node/test/parallel/fs-read-stream-fd-leak.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-fs-read-stream-fd-leak.js -//#SHA1: fc07b42f524d6a2f9743a5a7665c92096f58505b -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); - -let openCount = 0; -const _fsopen = fs.open; -const _fsclose = fs.close; - -const loopCount = 50; -const totalCheck = 50; -const emptyTxt = path.join(__dirname, "../fixtures/empty.txt"); - -fs.open = function () { - openCount++; - return _fsopen.apply(null, arguments); -}; - -fs.close = function () { - openCount--; - return _fsclose.apply(null, arguments); -}; - -function testLeak(endFn) { - return new Promise(resolve => { - console.log(`testing for leaks from fs.createReadStream().${endFn}()...`); - - let i = 0; - let check = 0; - - function checkFunction() { - if (openCount !== 0 && check < totalCheck) { - check++; - setTimeout(checkFunction, 100); - return; - } - - expect(openCount).toBe(0); - openCount = 0; - resolve(); - } - - const interval = setInterval(() => { - const s = fs.createReadStream(emptyTxt); - s[endFn](); - - if (++i === loopCount) { - clearInterval(interval); - setTimeout(checkFunction, 100); - } - }, 2); - }); -} - -test("no leaked file descriptors using close()", async () => { - await testLeak("close"); -}, 10000); - -test("no leaked file descriptors using destroy()", async () => { - await testLeak("destroy"); -}, 10000); - -//<#END_FILE: test-fs-read-stream-fd-leak.js diff --git a/test/js/node/test/parallel/fs-read-stream-pos.test.js b/test/js/node/test/parallel/fs-read-stream-pos.test.js deleted file mode 100644 index bdc551a7c1..0000000000 --- a/test/js/node/test/parallel/fs-read-stream-pos.test.js +++ /dev/null @@ -1,103 +0,0 @@ -//#FILE: test-fs-read-stream-pos.js -//#SHA1: e44b357d8045cfa1e8129a160254dcfb9225d990 -//----------------- -"use strict"; - -// Refs: https://github.com/nodejs/node/issues/33940 - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const tmpdir = { - refresh: () => { - // Implement tmpdir.refresh() if needed - }, - resolve: filename => path.join(os.tmpdir(), filename), -}; - -tmpdir.refresh(); - -const file = tmpdir.resolve("read_stream_pos_test.txt"); - -fs.writeFileSync(file, ""); - -let counter = 0; - -const writeInterval = setInterval(() => { - counter = counter + 1; - const line = `hello at ${counter}\n`; - fs.writeFileSync(file, line, { flag: "a" }); -}, 1); - -const hwm = 10; -let bufs = []; -let isLow = false; -let cur = 0; -let stream; - -const readInterval = setInterval(() => { - if (stream) return; - - stream = fs.createReadStream(file, { - highWaterMark: hwm, - start: cur, - }); - stream.on( - "data", - jest.fn(chunk => { - cur += chunk.length; - bufs.push(chunk); - if (isLow) { - const brokenLines = Buffer.concat(bufs) - .toString() - .split("\n") - .filter(line => { - const s = "hello at".slice(0, line.length); - if (line && !line.startsWith(s)) { - return true; - } - return false; - }); - expect(brokenLines.length).toBe(0); - exitTest(); - return; - } - if (chunk.length !== hwm) { - isLow = true; - } - }), - ); - stream.on("end", () => { - stream = null; - isLow = false; - bufs = []; - }); -}, 10); - -// Time longer than 90 seconds to exit safely -const endTimer = setTimeout(() => { - exitTest(); -}, 90000); - -const exitTest = () => { - clearInterval(readInterval); - clearInterval(writeInterval); - clearTimeout(endTimer); - if (stream && !stream.destroyed) { - stream.on("close", () => { - process.exit(); - }); - stream.destroy(); - } else { - process.exit(); - } -}; - -test("fs read stream position", () => { - // This test is mostly about setting up the environment and running the intervals - // The actual assertions are made within the intervals - expect(true).toBe(true); -}); - -//<#END_FILE: test-fs-read-stream-pos.js diff --git a/test/js/node/test/parallel/fs-readdir-buffer.test.js b/test/js/node/test/parallel/fs-readdir-buffer.test.js deleted file mode 100644 index 4003405487..0000000000 --- a/test/js/node/test/parallel/fs-readdir-buffer.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-fs-readdir-buffer.js -//#SHA1: 333645cb13aa3c15d61428ecfa2794e7393ef91c -//----------------- -"use strict"; - -const fs = require("fs"); - -if (process.platform !== "darwin") { - it("skips test on non-MacOS platforms", () => { - test.skip("this test works only on MacOS"); - }); -} else { - test("readdir with buffer and withFileTypes options on MacOS", () => { - return new Promise(resolve => { - fs.readdir(Buffer.from("/dev"), { withFileTypes: true, encoding: "buffer" }, (err, files) => { - expect(err).toBeNull(); - resolve(); - }); - }); - }); -} - -//<#END_FILE: test-fs-readdir-buffer.js diff --git a/test/js/node/test/parallel/fs-readdir-recursive.test.js b/test/js/node/test/parallel/fs-readdir-recursive.test.js deleted file mode 100644 index 32c284e827..0000000000 --- a/test/js/node/test/parallel/fs-readdir-recursive.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-fs-readdir-recursive.js -//#SHA1: daa6ac8e46cd3d530e4546354a11f87f1b1c092d -//----------------- -"use strict"; - -const fs = require("fs"); -const net = require("net"); -const path = require("path"); -const os = require("os"); - -const tmpdir = path.join(os.tmpdir(), "node-test-fs-readdir-recursive"); - -beforeAll(() => { - // Refresh tmpdir - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterAll(() => { - // Clean up tmpdir - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test("fs.readdirSync with recursive option should not crash", done => { - const server = net.createServer().listen(path.join(tmpdir, "test.sock"), () => { - // The process should not crash - // See https://github.com/nodejs/node/issues/52159 - expect(() => { - fs.readdirSync(tmpdir, { recursive: true }); - }).not.toThrow(); - - server.close(); - done(); - }); -}); - -//<#END_FILE: test-fs-readdir-recursive.js diff --git a/test/js/node/test/parallel/fs-readdir.test.js b/test/js/node/test/parallel/fs-readdir.test.js deleted file mode 100644 index 1e98f2f1b6..0000000000 --- a/test/js/node/test/parallel/fs-readdir.test.js +++ /dev/null @@ -1,90 +0,0 @@ -//#FILE: test-fs-readdir.js -//#SHA1: ce2c5a12cb271c5023f965afe712e78b1a484ad5 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const readdirDir = path.join(os.tmpdir(), "test-fs-readdir"); -const files = ["empty", "files", "for", "just", "testing"]; - -beforeAll(() => { - // Make sure tmp directory is clean - if (fs.existsSync(readdirDir)) { - fs.rmSync(readdirDir, { recursive: true, force: true }); - } - fs.mkdirSync(readdirDir, { recursive: true }); - - // Create the necessary files - files.forEach(currentFile => { - fs.closeSync(fs.openSync(path.join(readdirDir, currentFile), "w")); - }); -}); - -afterAll(() => { - // Clean up - fs.rmSync(readdirDir, { recursive: true, force: true }); -}); - -test("fs.readdirSync returns correct files", () => { - expect(fs.readdirSync(readdirDir).sort()).toEqual(files); -}); - -test("fs.readdir returns correct files", async () => { - await new Promise(resolve => { - fs.readdir(readdirDir, (err, f) => { - expect(err).toBeNull(); - expect(f.sort()).toEqual(files); - resolve(); - }); - }); -}); - -test("fs.readdirSync throws ENOTDIR on file", () => { - expect(() => { - fs.readdirSync(__filename); - }).toThrow( - expect.objectContaining({ - code: "ENOTDIR", - message: expect.any(String), - }), - ); -}); - -test("fs.readdir throws ENOTDIR on file", async () => { - await new Promise(resolve => { - fs.readdir(__filename, e => { - expect(e).toEqual( - expect.objectContaining({ - code: "ENOTDIR", - message: expect.any(String), - }), - ); - resolve(); - }); - }); -}); - -test("fs.readdir and fs.readdirSync throw on invalid input", () => { - [false, 1, [], {}, null, undefined].forEach(i => { - expect(() => fs.readdir(i, () => {})).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => fs.readdirSync(i)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-fs-readdir.js diff --git a/test/js/node/test/parallel/fs-readfile-empty.test.js b/test/js/node/test/parallel/fs-readfile-empty.test.js deleted file mode 100644 index ae4bdb4eb5..0000000000 --- a/test/js/node/test/parallel/fs-readfile-empty.test.js +++ /dev/null @@ -1,67 +0,0 @@ -//#FILE: test-fs-readfile-empty.js -//#SHA1: a78ffc8186bc3e0a7d8d8dcf0f292ef4220817a5 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const fixturesPath = path.join(__dirname, '..', 'fixtures'); -const fn = path.join(fixturesPath, 'empty.txt'); - -test('fs.readFile on an empty file', (done) => { - fs.readFile(fn, (err, data) => { - expect(err).toBeNull(); - expect(data).toBeTruthy(); - done(); - }); -}); - -test('fs.readFile on an empty file with utf8 encoding', (done) => { - fs.readFile(fn, 'utf8', (err, data) => { - expect(err).toBeNull(); - expect(data).toBe(''); - done(); - }); -}); - -test('fs.readFile on an empty file with encoding option', (done) => { - fs.readFile(fn, { encoding: 'utf8' }, (err, data) => { - expect(err).toBeNull(); - expect(data).toBe(''); - done(); - }); -}); - -test('fs.readFileSync on an empty file', () => { - const data = fs.readFileSync(fn); - expect(data).toBeTruthy(); -}); - -test('fs.readFileSync on an empty file with utf8 encoding', () => { - const data = fs.readFileSync(fn, 'utf8'); - expect(data).toBe(''); -}); - -//<#END_FILE: test-fs-readfile-empty.js diff --git a/test/js/node/test/parallel/fs-readfile-eof.test.js b/test/js/node/test/parallel/fs-readfile-eof.test.js deleted file mode 100644 index f1b68ffccf..0000000000 --- a/test/js/node/test/parallel/fs-readfile-eof.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-fs-readfile-eof.js -//#SHA1: 89b7efe6c30d2316249bfae1d01f16f97e32be04 -//----------------- -"use strict"; - -const fs = require("fs/promises"); -const { exec } = require("child_process"); - -const childType = ["child-encoding", "child-non-encoding"]; - -if (process.argv[2] === childType[0]) { - fs.readFile("/dev/stdin", "utf8").then(data => { - process.stdout.write(data); - }); -} else if (process.argv[2] === childType[1]) { - fs.readFile("/dev/stdin").then(data => { - process.stdout.write(data); - }); -} else { - const data1 = "Hello"; - const data2 = "World"; - const expected = `${data1}\n${data2}\n`; - - const f = JSON.stringify(__filename); - const node = JSON.stringify(process.execPath); - - function testReadFile(child) { - return new Promise((resolve, reject) => { - const cmd = `(echo ${data1}; sleep 0.5; echo ${data2}) | ${node} ${f} ${child}`; - exec(cmd, (error, stdout, stderr) => { - if (error) reject(error); - else resolve({ stdout, stderr }); - }); - }); - } - - if (process.platform === "win32" || process.platform === "aix" || process.platform === "os400") { - test.skip(`No /dev/stdin on ${process.platform}.`, () => {}); - } else { - test("readFile with encoding", async () => { - const { stdout, stderr } = await testReadFile(childType[0]); - expect(stdout).toBe(expected); - expect(stderr).toBe(""); - }); - - test("readFile without encoding", async () => { - const { stdout, stderr } = await testReadFile(childType[1]); - expect(stdout).toBe(expected); - expect(stderr).toBe(""); - }); - } -} - -//<#END_FILE: test-fs-readfile-eof.js diff --git a/test/js/node/test/parallel/fs-readfile-fd.test.js b/test/js/node/test/parallel/fs-readfile-fd.test.js deleted file mode 100644 index b62d15b9e6..0000000000 --- a/test/js/node/test/parallel/fs-readfile-fd.test.js +++ /dev/null @@ -1,111 +0,0 @@ -//#FILE: test-fs-readfile-fd.js -//#SHA1: ec2bc78cb0bab7b8e9b23c1c44a77b227294d8b4 -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const tmpdir = path.join(os.tmpdir(), 'test-fs-readfile-fd'); -const emptyFilePath = path.join(tmpdir, 'empty.txt'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - fs.writeFileSync(emptyFilePath, ''); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -function tempFd(callback) { - fs.open(emptyFilePath, 'r', (err, fd) => { - expect(err).toBeFalsy(); - callback(fd, () => { - fs.close(fd, (err) => { - expect(err).toBeFalsy(); - }); - }); - }); -} - -function tempFdSync(callback) { - const fd = fs.openSync(emptyFilePath, 'r'); - callback(fd); - fs.closeSync(fd); -} - -test('fs.readFile with file descriptor', (done) => { - tempFd((fd, close) => { - fs.readFile(fd, (err, data) => { - expect(data).toBeTruthy(); - close(); - done(); - }); - }); -}); - -test('fs.readFile with file descriptor and utf8 encoding', (done) => { - tempFd((fd, close) => { - fs.readFile(fd, 'utf8', (err, data) => { - expect(data).toBe(''); - close(); - done(); - }); - }); -}); - -test('fs.readFileSync with file descriptor', () => { - tempFdSync((fd) => { - expect(fs.readFileSync(fd)).toBeTruthy(); - }); -}); - -test('fs.readFileSync with file descriptor and utf8 encoding', () => { - tempFdSync((fd) => { - expect(fs.readFileSync(fd, 'utf8')).toBe(''); - }); -}); - -test('readFile() reads from current position of file descriptor', (done) => { - const filename = path.join(tmpdir, 'test.txt'); - fs.writeFileSync(filename, 'Hello World'); - - fs.open(filename, 'r', (err, fd) => { - expect(err).toBeFalsy(); - const buf = Buffer.alloc(5); - - fs.read(fd, buf, 0, 5, null, (err, bytes) => { - expect(err).toBeFalsy(); - expect(bytes).toBe(5); - expect(buf.toString()).toBe('Hello'); - - fs.readFile(fd, (err, data) => { - expect(err).toBeFalsy(); - expect(data.toString()).toBe(' World'); - fs.closeSync(fd); - done(); - }); - }); - }); -}); - -test('readFileSync() reads from current position of file descriptor', () => { - const filename = path.join(tmpdir, 'test.txt'); - fs.writeFileSync(filename, 'Hello World'); - - const fd = fs.openSync(filename, 'r'); - - const buf = Buffer.alloc(5); - expect(fs.readSync(fd, buf, 0, 5)).toBe(5); - expect(buf.toString()).toBe('Hello'); - - expect(fs.readFileSync(fd).toString()).toBe(' World'); - - fs.closeSync(fd); -}); - -//<#END_FILE: test-fs-readfile-fd.js diff --git a/test/js/node/test/parallel/fs-readfile-pipe-large.test.js b/test/js/node/test/parallel/fs-readfile-pipe-large.test.js deleted file mode 100644 index 815db4cedd..0000000000 --- a/test/js/node/test/parallel/fs-readfile-pipe-large.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-fs-readfile-pipe-large.js -//#SHA1: 5e2fa068dc742cfe617ccf3f08df6725e92a51f6 -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); -const { exec } = require('child_process'); -const os = require('os'); - -const isWindows = process.platform === 'win32'; -const isAIX = process.platform === 'aix'; -const isIBMi = process.platform === 'os400'; - -const skipPlatforms = ['win32', 'aix', 'os400']; - -// Separate child process logic -if (process.argv[2] === 'child') { - fs.readFile('/dev/stdin', (err, data) => { - if (err) { - console.error(err); - process.exit(1); - } - process.stdout.write(data); - }); -} else { - // Jest test code - describe('fs.readFile pipe large', () => { - const tmpdir = os.tmpdir(); - const filename = path.join(tmpdir, 'readfile_pipe_large_test.txt'); - const dataExpected = 'a'.repeat(999999); - - beforeAll(() => { - if (!skipPlatforms.includes(process.platform)) { - fs.writeFileSync(filename, dataExpected); - } - }); - - afterAll(() => { - if (!skipPlatforms.includes(process.platform)) { - fs.unlinkSync(filename); - } - }); - - test('should read from /dev/stdin and write to stdout', () => { - if (skipPlatforms.includes(process.platform)) { - return test.skip(`No /dev/stdin on ${process.platform}.`); - } - - const f = JSON.stringify(__filename); - const node = JSON.stringify(process.execPath); - const cmd = `cat ${filename} | ${node} ${f} child`; - - return new Promise((resolve, reject) => { - exec(cmd, { maxBuffer: 1000000 }, (error, stdout, stderr) => { - if (error) { - reject(error); - return; - } - - expect(stdout).toBe(dataExpected); - expect(stderr).toBe(''); - resolve(); - }); - }); - }); - }); -} -//<#END_FILE: test-fs-readfile-pipe-large.js diff --git a/test/js/node/test/parallel/fs-readfile-pipe.test.js b/test/js/node/test/parallel/fs-readfile-pipe.test.js deleted file mode 100644 index 9d9cec5cb7..0000000000 --- a/test/js/node/test/parallel/fs-readfile-pipe.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-fs-readfile-pipe.js -//#SHA1: b78e6ea1bbcdaf74b6363f4740bdf2393ed28938 -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); -const { exec } = require('child_process'); - -const isWindows = process.platform === 'win32'; -const isAIX = process.platform === 'aix'; -const isIBMi = process.platform === 'os400'; - -const fixturesPath = path.join(__dirname, '..', 'fixtures'); - -if (isWindows || isAIX || isIBMi) { - test.skip(`No /dev/stdin on ${process.platform}.`, () => {}); -} else { - if (process.argv[2] === 'child') { - fs.readFile('/dev/stdin', (err, data) => { - if (err) { - console.error(err); - process.exit(1); - } - process.stdout.write(data); - }); - } else { - test('readFile pipe test', (done) => { - const filename = path.join(fixturesPath, 'readfile_pipe_test.txt'); - const dataExpected = fs.readFileSync(filename, 'utf8'); - - const f = JSON.stringify(__filename); - const node = JSON.stringify(process.execPath); - const cmd = `cat ${filename} | ${node} ${f} child`; - - exec(cmd, (error, stdout, stderr) => { - if (error) { - done(error); - return; - } - try { - expect(stdout).toBe(dataExpected); - expect(stderr).toBe(''); - done(); - } catch (error) { - done(error); - } - }); - }, 10000); // Increase timeout to 10 seconds - } -} - -//<#END_FILE: test-fs-readfile-pipe.js diff --git a/test/js/node/test/parallel/fs-readfile-unlink.test.js b/test/js/node/test/parallel/fs-readfile-unlink.test.js deleted file mode 100644 index 85475f6488..0000000000 --- a/test/js/node/test/parallel/fs-readfile-unlink.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-fs-readfile-unlink.js -//#SHA1: a7107747d7901dfcc1ffd0e7adbf548412a1016a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// Test that unlink succeeds immediately after readFile completes. - -test("unlink succeeds immediately after readFile completes", async () => { - const tmpdir = path.join(os.tmpdir(), "node-test-fs-readfile-unlink"); - await fs.promises.mkdir(tmpdir, { recursive: true }); - - const fileName = path.join(tmpdir, "test.bin"); - const buf = Buffer.alloc(512 * 1024, 42); - - await fs.promises.writeFile(fileName, buf); - - const data = await fs.promises.readFile(fileName); - - expect(data.length).toBe(buf.length); - expect(data[0]).toBe(42); - - // Unlink should not throw. This is part of the test. It used to throw on - // Windows due to a bug. - await expect(fs.promises.unlink(fileName)).resolves.toBeUndefined(); -}); - -//<#END_FILE: test-fs-readfile-unlink.js diff --git a/test/js/node/test/parallel/fs-readfile-zero-byte-liar.test.js b/test/js/node/test/parallel/fs-readfile-zero-byte-liar.test.js deleted file mode 100644 index 0e3799ec0e..0000000000 --- a/test/js/node/test/parallel/fs-readfile-zero-byte-liar.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-fs-readfile-zero-byte-liar.js -//#SHA1: ddca2f114cf32b03f36405a21c81058e7a1f0c18 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const fs = require("fs"); - -// Test that readFile works even when stat returns size 0. - -const dataExpected = fs.readFileSync(__filename, "utf8"); - -// Sometimes stat returns size=0, but it's a lie. -fs._fstat = fs.fstat; -fs._fstatSync = fs.fstatSync; - -fs.fstat = (fd, cb) => { - fs._fstat(fd, (er, st) => { - if (er) return cb(er); - st.size = 0; - return cb(er, st); - }); -}; - -fs.fstatSync = fd => { - const st = fs._fstatSync(fd); - st.size = 0; - return st; -}; - -test("readFileSync works with zero byte liar", () => { - const d = fs.readFileSync(__filename, "utf8"); - expect(d).toBe(dataExpected); -}); - -test("readFile works with zero byte liar", async () => { - const d = await fs.promises.readFile(__filename, "utf8"); - expect(d).toBe(dataExpected); -}); - -//<#END_FILE: test-fs-readfile-zero-byte-liar.js diff --git a/test/js/node/test/parallel/fs-readfilesync-pipe-large.test.js b/test/js/node/test/parallel/fs-readfilesync-pipe-large.test.js deleted file mode 100644 index 8df6c2e1f0..0000000000 --- a/test/js/node/test/parallel/fs-readfilesync-pipe-large.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-fs-readfilesync-pipe-large.js -//#SHA1: 669e419b344b375a028fa352c7a29eec2d5d52af -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); -const { exec } = require('child_process'); -const os = require('os'); -const { describe, test, expect, beforeAll, afterAll } = require('@jest/globals'); - -const isWindows = process.platform === 'win32'; -const isAIX = process.platform === 'aix'; -const isIBMi = process.platform === 'os400'; - -const shouldSkip = isWindows || isAIX || isIBMi; - -const tmpdir = os.tmpdir(); - -if (process.argv[2] === 'child') { - process.stdout.write(fs.readFileSync('/dev/stdin', 'utf8')); - process.exit(0); -} - -describe('fs.readFileSync pipe large', () => { - const filename = path.join(tmpdir, 'readfilesync_pipe_large_test.txt'); - const dataExpected = 'a'.repeat(999999); - - beforeAll(() => { - if (!shouldSkip) { - fs.writeFileSync(filename, dataExpected); - } - }); - - afterAll(() => { - if (!shouldSkip) { - fs.unlinkSync(filename); - } - }); - - const testFn = shouldSkip ? test.skip : test; - - testFn('should read large file through pipe', (done) => { - const childScriptPath = path.join(__dirname, 'child-script.js'); - fs.writeFileSync(childScriptPath, ` - const fs = require('fs'); - process.stdout.write(fs.readFileSync('/dev/stdin', 'utf8')); - `); - - const cmd = `cat ${filename} | "${process.execPath}" "${childScriptPath}"`; - - exec(cmd, { maxBuffer: 1000000 }, (error, stdout, stderr) => { - try { - expect(error).toBeNull(); - expect(stdout).toBe(dataExpected); - expect(stderr).toBe(''); - fs.unlinkSync(childScriptPath); - done(); - } catch (err) { - fs.unlinkSync(childScriptPath); - done(err); - } - }); - }, 30000); // Increase timeout to 30 seconds -}); - -//<#END_FILE: test-fs-readfilesync-pipe-large.js diff --git a/test/js/node/test/parallel/fs-readlink-type-check.test.js b/test/js/node/test/parallel/fs-readlink-type-check.test.js deleted file mode 100644 index c0ee6e8b00..0000000000 --- a/test/js/node/test/parallel/fs-readlink-type-check.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-fs-readlink-type-check.js -//#SHA1: dd36bda8e12e6c22c342325345dd8d1de2097d9c -//----------------- -"use strict"; - -const fs = require("fs"); - -[false, 1, {}, [], null, undefined].forEach(i => { - test(`fs.readlink throws for invalid input: ${JSON.stringify(i)}`, () => { - expect(() => fs.readlink(i, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - - test(`fs.readlinkSync throws for invalid input: ${JSON.stringify(i)}`, () => { - expect(() => fs.readlinkSync(i)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-fs-readlink-type-check.js diff --git a/test/js/node/test/parallel/fs-readv-promises.test.js b/test/js/node/test/parallel/fs-readv-promises.test.js deleted file mode 100644 index ff66cc9354..0000000000 --- a/test/js/node/test/parallel/fs-readv-promises.test.js +++ /dev/null @@ -1,69 +0,0 @@ -//#FILE: test-fs-readv-promises.js -//#SHA1: 43d801fa8a2eabf438e98f5aa713eb9680fe798b -//----------------- -"use strict"; - -const fs = require("fs").promises; -const path = require("path"); -const os = require("os"); - -const expected = "ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف"; -const expectedBuff = Buffer.from(expected); - -let cnt = 0; -function getFileName() { - return path.join(os.tmpdir(), `readv_promises_${++cnt}.txt`); -} - -const allocateEmptyBuffers = combinedLength => { - const bufferArr = []; - // Allocate two buffers, each half the size of expectedBuff - bufferArr[0] = Buffer.alloc(Math.floor(combinedLength / 2)); - bufferArr[1] = Buffer.alloc(combinedLength - bufferArr[0].length); - - return bufferArr; -}; - -describe("fs.promises.readv", () => { - beforeEach(() => { - cnt = 0; - }); - - test("readv with position", async () => { - const filename = getFileName(); - await fs.writeFile(filename, expectedBuff); - const handle = await fs.open(filename, "r"); - const bufferArr = allocateEmptyBuffers(expectedBuff.length); - const expectedLength = expectedBuff.length; - - let { bytesRead, buffers } = await handle.readv([Buffer.from("")], null); - expect(bytesRead).toBe(0); - expect(buffers).toEqual([Buffer.from("")]); - - ({ bytesRead, buffers } = await handle.readv(bufferArr, null)); - expect(bytesRead).toBe(expectedLength); - expect(buffers).toEqual(bufferArr); - expect(Buffer.concat(bufferArr)).toEqual(await fs.readFile(filename)); - await handle.close(); - }); - - test("readv without position", async () => { - const filename = getFileName(); - await fs.writeFile(filename, expectedBuff); - const handle = await fs.open(filename, "r"); - const bufferArr = allocateEmptyBuffers(expectedBuff.length); - const expectedLength = expectedBuff.length; - - let { bytesRead, buffers } = await handle.readv([Buffer.from("")]); - expect(bytesRead).toBe(0); - expect(buffers).toEqual([Buffer.from("")]); - - ({ bytesRead, buffers } = await handle.readv(bufferArr)); - expect(bytesRead).toBe(expectedLength); - expect(buffers).toEqual(bufferArr); - expect(Buffer.concat(bufferArr)).toEqual(await fs.readFile(filename)); - await handle.close(); - }); -}); - -//<#END_FILE: test-fs-readv-promises.js diff --git a/test/js/node/test/parallel/fs-readv-sync.test.js b/test/js/node/test/parallel/fs-readv-sync.test.js deleted file mode 100644 index f4ab916f05..0000000000 --- a/test/js/node/test/parallel/fs-readv-sync.test.js +++ /dev/null @@ -1,104 +0,0 @@ -//#FILE: test-fs-readv-sync.js -//#SHA1: e9a4527b118e4a814a04c976eaafb5127f7c7c9d -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const expected = "ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف"; - -const exptectedBuff = Buffer.from(expected); -const expectedLength = exptectedBuff.length; - -let filename; -let tmpdir; - -beforeAll(() => { - tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test-fs-readv-sync-")); - filename = path.join(tmpdir, "readv_sync.txt"); - fs.writeFileSync(filename, exptectedBuff); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -const allocateEmptyBuffers = combinedLength => { - const bufferArr = []; - // Allocate two buffers, each half the size of exptectedBuff - bufferArr[0] = Buffer.alloc(Math.floor(combinedLength / 2)); - bufferArr[1] = Buffer.alloc(combinedLength - bufferArr[0].length); - - return bufferArr; -}; - -// fs.readvSync with array of buffers with all parameters -test("fs.readvSync with array of buffers with all parameters", () => { - const fd = fs.openSync(filename, "r"); - - const bufferArr = allocateEmptyBuffers(exptectedBuff.length); - - let read = fs.readvSync(fd, [Buffer.from("")], 0); - expect(read).toBe(0); - - read = fs.readvSync(fd, bufferArr, 0); - expect(read).toBe(expectedLength); - - fs.closeSync(fd); - - expect(Buffer.concat(bufferArr)).toEqual(fs.readFileSync(filename)); -}); - -// fs.readvSync with array of buffers without position -test("fs.readvSync with array of buffers without position", () => { - const fd = fs.openSync(filename, "r"); - - const bufferArr = allocateEmptyBuffers(exptectedBuff.length); - - let read = fs.readvSync(fd, [Buffer.from("")]); - expect(read).toBe(0); - - read = fs.readvSync(fd, bufferArr); - expect(read).toBe(expectedLength); - - fs.closeSync(fd); - - expect(Buffer.concat(bufferArr)).toEqual(fs.readFileSync(filename)); -}); - -/** - * Testing with incorrect arguments - */ -const wrongInputs = [false, "test", {}, [{}], ["sdf"], null, undefined]; - -test("fs.readvSync with incorrect arguments", () => { - const fd = fs.openSync(filename, "r"); - - for (const wrongInput of wrongInputs) { - expect(() => fs.readvSync(fd, wrongInput, null)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - } - - fs.closeSync(fd); -}); - -test("fs.readvSync with wrong fd argument", () => { - for (const wrongInput of wrongInputs) { - expect(() => fs.readvSync(wrongInput)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - } -}); - -//<#END_FILE: test-fs-readv-sync.js diff --git a/test/js/node/test/parallel/fs-readv.test.js b/test/js/node/test/parallel/fs-readv.test.js deleted file mode 100644 index 58f9977c3b..0000000000 --- a/test/js/node/test/parallel/fs-readv.test.js +++ /dev/null @@ -1,110 +0,0 @@ -//#FILE: test-fs-readv.js -//#SHA1: 07d6fe434017163aea491c98db8127bc2c942b96 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const expected = "ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف"; - -let cnt = 0; -const getFileName = () => path.join(os.tmpdir(), `readv_${++cnt}.txt`); -const expectedBuff = Buffer.from(expected); - -const allocateEmptyBuffers = combinedLength => { - const bufferArr = []; - // Allocate two buffers, each half the size of expectedBuff - bufferArr[0] = Buffer.alloc(Math.floor(combinedLength / 2)); - bufferArr[1] = Buffer.alloc(combinedLength - bufferArr[0].length); - - return bufferArr; -}; - -const getCallback = (fd, bufferArr) => { - return (err, bytesRead, buffers) => { - expect(err).toBeNull(); - expect(bufferArr).toEqual(buffers); - const expectedLength = expectedBuff.length; - expect(bytesRead).toBe(expectedLength); - fs.closeSync(fd); - - expect(Buffer.concat(bufferArr).equals(expectedBuff)).toBe(true); - }; -}; - -beforeEach(() => { - jest.spyOn(fs, "writeSync"); - jest.spyOn(fs, "writeFileSync"); - jest.spyOn(fs, "openSync"); - jest.spyOn(fs, "closeSync"); -}); - -afterEach(() => { - jest.restoreAllMocks(); -}); - -test("fs.readv with array of buffers with all parameters", done => { - const filename = getFileName(); - const fd = fs.openSync(filename, "w+"); - fs.writeSync(fd, expectedBuff); - - const bufferArr = allocateEmptyBuffers(expectedBuff.length); - const callback = getCallback(fd, bufferArr); - - fs.readv(fd, bufferArr, 0, (err, bytesRead, buffers) => { - callback(err, bytesRead, buffers); - done(); - }); -}); - -test("fs.readv with array of buffers without position", done => { - const filename = getFileName(); - fs.writeFileSync(filename, expectedBuff); - const fd = fs.openSync(filename, "r"); - - const bufferArr = allocateEmptyBuffers(expectedBuff.length); - const callback = getCallback(fd, bufferArr); - - fs.readv(fd, bufferArr, (err, bytesRead, buffers) => { - callback(err, bytesRead, buffers); - done(); - }); -}); - -describe("Testing with incorrect arguments", () => { - const wrongInputs = [false, "test", {}, [{}], ["sdf"], null, undefined]; - - test("fs.readv with wrong buffers argument", () => { - const filename = getFileName(); - fs.writeFileSync(filename, expectedBuff); - const fd = fs.openSync(filename, "r"); - - for (const wrongInput of wrongInputs) { - expect(() => fs.readv(fd, wrongInput, null, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - } - - fs.closeSync(fd); - }); - - test("fs.readv with wrong fd argument", () => { - for (const wrongInput of wrongInputs) { - expect(() => fs.readv(wrongInput, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - } - }); -}); - -//<#END_FILE: test-fs-readv.js diff --git a/test/js/node/test/parallel/fs-realpath-pipe.test.js b/test/js/node/test/parallel/fs-realpath-pipe.test.js deleted file mode 100644 index 6c461e9d36..0000000000 --- a/test/js/node/test/parallel/fs-realpath-pipe.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-fs-realpath-pipe.js -//#SHA1: 2a876967f5134cd77e2214f2abcbf753d46983cf -//----------------- -"use strict"; - -const { spawnSync } = require("child_process"); - -// Skip test for Windows, AIX, and IBMi -const isSkippedPlatform = ["win32", "aix", "os400"].includes(process.platform); -const testName = `No /dev/stdin on ${process.platform}.`; - -(isSkippedPlatform ? test.skip : test)(testName, () => { - const testCases = [ - `require('fs').realpath('/dev/stdin', (err, resolvedPath) => { - if (err) { - console.error(err); - process.exit(1); - } - if (resolvedPath) { - process.exit(2); - } - });`, - `try { - if (require('fs').realpathSync('/dev/stdin')) { - process.exit(2); - } - } catch (e) { - console.error(e); - process.exit(1); - }`, - ]; - - for (const code of testCases) { - const child = spawnSync(process.execPath, ["-e", code], { - stdio: "pipe", - }); - - if (child.status !== 2) { - console.log(code); - console.log(child.stderr.toString()); - } - - expect(child.status).toBe(2); - } -}); - -//<#END_FILE: test-fs-realpath-pipe.js diff --git a/test/js/node/test/parallel/fs-rmdir-type-check.test.js b/test/js/node/test/parallel/fs-rmdir-type-check.test.js deleted file mode 100644 index 3148f0ba3a..0000000000 --- a/test/js/node/test/parallel/fs-rmdir-type-check.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-fs-rmdir-type-check.js -//#SHA1: 2a00191160af6f0f76a82dcaef31d13c9b223d3b -//----------------- -"use strict"; - -const fs = require("fs"); - -test("fs.rmdir and fs.rmdirSync with invalid arguments", () => { - [false, 1, [], {}, null, undefined].forEach(i => { - expect(() => fs.rmdir(i, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => fs.rmdirSync(i)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-fs-rmdir-type-check.js diff --git a/test/js/node/test/parallel/fs-sir-writes-alot.test.js b/test/js/node/test/parallel/fs-sir-writes-alot.test.js deleted file mode 100644 index b8eefc6642..0000000000 --- a/test/js/node/test/parallel/fs-sir-writes-alot.test.js +++ /dev/null @@ -1,84 +0,0 @@ -//#FILE: test-fs-sir-writes-alot.js -//#SHA1: d6f4574d48b9a85ee1276e4e0499f3fc32096d24 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const tmpdir = path.join(os.tmpdir(), 'test-fs-sir-writes-alot'); -const filename = path.join(tmpdir, 'out.txt'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('multiple async writes to a file', async () => { - const fd = fs.openSync(filename, 'w'); - - const line = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'; - const N = 10240; - let complete = 0; - let bytesChecked = 0; - - function testBuffer(b) { - for (let i = 0; i < b.length; i++) { - bytesChecked++; - if (b[i] !== 'a'.charCodeAt(0) && b[i] !== '\n'.charCodeAt(0)) { - throw new Error(`invalid char ${i},${b[i]}`); - } - } - } - - await new Promise((resolve) => { - for (let i = 0; i < N; i++) { - // Create a new buffer for each write. Before the write is actually - // executed by the thread pool, the buffer will be collected. - const buffer = Buffer.from(line); - fs.write(fd, buffer, 0, buffer.length, null, function(er, written) { - complete++; - if (complete === N) { - fs.closeSync(fd); - const s = fs.createReadStream(filename); - s.on('data', testBuffer); - s.on('end', resolve); - } - }); - } - }); - - // Probably some of the writes are going to overlap, so we can't assume - // that we get (N * line.length). Let's just make sure we've checked a - // few... - expect(bytesChecked).toBeGreaterThan(1000); -}); - -//<#END_FILE: test-fs-sir-writes-alot.js diff --git a/test/js/node/test/parallel/fs-stat-bigint.test.js b/test/js/node/test/parallel/fs-stat-bigint.test.js deleted file mode 100644 index d7eb29c3f0..0000000000 --- a/test/js/node/test/parallel/fs-stat-bigint.test.js +++ /dev/null @@ -1,209 +0,0 @@ -//#FILE: test-fs-stat-bigint.js -//#SHA1: c8ba0bacb927432a68a677cd3a304e8e058fb070 -//----------------- -"use strict"; - -const fs = require("fs"); -const promiseFs = require("fs").promises; -const tmpdir = require("../common/tmpdir"); -const { isDate } = require("util").types; -const { inspect } = require("util"); - -tmpdir.refresh(); - -let testIndex = 0; - -function getFilename() { - const filename = tmpdir.resolve(`test-file-${++testIndex}`); - fs.writeFileSync(filename, "test"); - return filename; -} - -function verifyStats(bigintStats, numStats, allowableDelta) { - // allowableDelta: It's possible that the file stats are updated between the - // two stat() calls so allow for a small difference. - for (const key of Object.keys(numStats)) { - const val = numStats[key]; - if (isDate(val)) { - const time = val.getTime(); - const time2 = bigintStats[key].getTime(); - expect(time - time2).toBeLessThanOrEqual(allowableDelta); - } else if (key === "mode") { - expect(bigintStats[key]).toBe(BigInt(val)); - expect(bigintStats.isBlockDevice()).toBe(numStats.isBlockDevice()); - expect(bigintStats.isCharacterDevice()).toBe(numStats.isCharacterDevice()); - expect(bigintStats.isDirectory()).toBe(numStats.isDirectory()); - expect(bigintStats.isFIFO()).toBe(numStats.isFIFO()); - expect(bigintStats.isFile()).toBe(numStats.isFile()); - expect(bigintStats.isSocket()).toBe(numStats.isSocket()); - expect(bigintStats.isSymbolicLink()).toBe(numStats.isSymbolicLink()); - } else if (key.endsWith("Ms")) { - const nsKey = key.replace("Ms", "Ns"); - const msFromBigInt = bigintStats[key]; - const nsFromBigInt = bigintStats[nsKey]; - const msFromBigIntNs = Number(nsFromBigInt / 10n ** 6n); - const msFromNum = numStats[key]; - - expect(msFromNum - Number(msFromBigInt)).toBeLessThanOrEqual(allowableDelta); - expect(msFromNum - Number(msFromBigIntNs)).toBeLessThanOrEqual(allowableDelta); - } else if (Number.isSafeInteger(val)) { - expect(bigintStats[key]).toBe(BigInt(val)); - } else { - expect(Number(bigintStats[key]) - val).toBeLessThan(1); - } - } -} - -const runSyncTest = (func, arg) => { - const startTime = process.hrtime.bigint(); - const bigintStats = func(arg, { bigint: true }); - const numStats = func(arg); - const endTime = process.hrtime.bigint(); - const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); - verifyStats(bigintStats, numStats, allowableDelta); -}; - -test("fs.statSync", () => { - const filename = getFilename(); - runSyncTest(fs.statSync, filename); -}); - -if (!process.platform.startsWith("win")) { - test("fs.lstatSync", () => { - const filename = getFilename(); - const link = `${filename}-link`; - fs.symlinkSync(filename, link); - runSyncTest(fs.lstatSync, link); - }); -} - -test("fs.fstatSync", () => { - const filename = getFilename(); - const fd = fs.openSync(filename, "r"); - runSyncTest(fs.fstatSync, fd); - fs.closeSync(fd); -}); - -test("fs.statSync with non-existent file", () => { - expect(() => fs.statSync("does_not_exist")).toThrow(expect.objectContaining({ code: "ENOENT" })); - expect(fs.statSync("does_not_exist", { throwIfNoEntry: false })).toBeUndefined(); -}); - -test("fs.lstatSync with non-existent file", () => { - expect(() => fs.lstatSync("does_not_exist")).toThrow(expect.objectContaining({ code: "ENOENT" })); - expect(fs.lstatSync("does_not_exist", { throwIfNoEntry: false })).toBeUndefined(); -}); - -test("fs.fstatSync with invalid file descriptor", () => { - expect(() => fs.fstatSync(9999)).toThrow(expect.objectContaining({ code: "EBADF" })); - expect(() => fs.fstatSync(9999, { throwIfNoEntry: false })).toThrow(expect.objectContaining({ code: "EBADF" })); -}); - -const runCallbackTest = (func, arg) => { - return new Promise(resolve => { - const startTime = process.hrtime.bigint(); - func(arg, { bigint: true }, (err, bigintStats) => { - expect(err).toBeFalsy(); - func(arg, (err, numStats) => { - expect(err).toBeFalsy(); - const endTime = process.hrtime.bigint(); - const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); - verifyStats(bigintStats, numStats, allowableDelta); - resolve(); - }); - }); - }); -}; - -test("fs.stat callback", async () => { - const filename = getFilename(); - await runCallbackTest(fs.stat, filename); -}); - -if (!process.platform.startsWith("win")) { - test("fs.lstat callback", async () => { - const filename = getFilename(); - const link = `${filename}-link`; - fs.symlinkSync(filename, link); - await runCallbackTest(fs.lstat, link); - }); -} - -test("fs.fstat callback", async () => { - const filename = getFilename(); - const fd = fs.openSync(filename, "r"); - await runCallbackTest(fs.fstat, fd); - fs.closeSync(fd); -}); - -const runPromiseTest = async (func, arg) => { - const startTime = process.hrtime.bigint(); - const bigintStats = await func(arg, { bigint: true }); - const numStats = await func(arg); - const endTime = process.hrtime.bigint(); - const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); - verifyStats(bigintStats, numStats, allowableDelta); -}; - -test("promiseFs.stat", async () => { - const filename = getFilename(); - await runPromiseTest(promiseFs.stat, filename); -}); - -if (!process.platform.startsWith("win")) { - test("promiseFs.lstat", async () => { - const filename = getFilename(); - const link = `${filename}-link`; - fs.symlinkSync(filename, link); - await runPromiseTest(promiseFs.lstat, link); - }); -} - -test("promiseFs handle.stat", async () => { - const filename = getFilename(); - const handle = await promiseFs.open(filename, "r"); - const startTime = process.hrtime.bigint(); - const bigintStats = await handle.stat({ bigint: true }); - const numStats = await handle.stat(); - const endTime = process.hrtime.bigint(); - const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); - verifyStats(bigintStats, numStats, allowableDelta); - await handle.close(); -}); - -test("BigIntStats Date properties can be set before reading them", done => { - fs.stat(__filename, { bigint: true }, (err, s) => { - expect(err).toBeFalsy(); - s.atime = 2; - s.mtime = 3; - s.ctime = 4; - s.birthtime = 5; - - expect(s.atime).toBe(2); - expect(s.mtime).toBe(3); - expect(s.ctime).toBe(4); - expect(s.birthtime).toBe(5); - done(); - }); -}); - -test("BigIntStats Date properties can be set after reading them", done => { - fs.stat(__filename, { bigint: true }, (err, s) => { - expect(err).toBeFalsy(); - // eslint-disable-next-line no-unused-expressions - s.atime, s.mtime, s.ctime, s.birthtime; - - s.atime = 2; - s.mtime = 3; - s.ctime = 4; - s.birthtime = 5; - - expect(s.atime).toBe(2); - expect(s.mtime).toBe(3); - expect(s.ctime).toBe(4); - expect(s.birthtime).toBe(5); - done(); - }); -}); - -//<#END_FILE: test-fs-stat-bigint.js diff --git a/test/js/node/test/parallel/fs-stream-double-close.test.js b/test/js/node/test/parallel/fs-stream-double-close.test.js deleted file mode 100644 index 0e89c7bf26..0000000000 --- a/test/js/node/test/parallel/fs-stream-double-close.test.js +++ /dev/null @@ -1,84 +0,0 @@ -//#FILE: test-fs-stream-double-close.js -//#SHA1: 25fa219f7ee462e67611751f996393afc1869490 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const tmpdir = path.join(os.tmpdir(), "node-test-fs-stream-double-close"); -beforeAll(() => { - if (!fs.existsSync(tmpdir)) { - fs.mkdirSync(tmpdir, { recursive: true }); - } -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test("test1 with ReadStream", () => { - test1(fs.createReadStream(__filename)); -}); - -test("test2 with ReadStream", () => { - test2(fs.createReadStream(__filename)); -}); - -test("test3 with ReadStream", () => { - test3(fs.createReadStream(__filename)); -}); - -test("test1 with WriteStream", () => { - test1(fs.createWriteStream(path.join(tmpdir, "dummy1"))); -}); - -test("test2 with WriteStream", () => { - test2(fs.createWriteStream(path.join(tmpdir, "dummy2"))); -}); - -test("test3 with WriteStream", () => { - test3(fs.createWriteStream(path.join(tmpdir, "dummy3"))); -}); - -function test1(stream) { - stream.destroy(); - stream.destroy(); -} - -function test2(stream) { - stream.destroy(); - stream.on("open", jest.fn()); -} - -function test3(stream) { - const openHandler = jest.fn(); - stream.on("open", openHandler); - stream.emit("open"); - expect(openHandler).toHaveBeenCalledTimes(1); - stream.destroy(); - stream.destroy(); -} - -//<#END_FILE: test-fs-stream-double-close.js diff --git a/test/js/node/test/parallel/fs-symlink-buffer-path.test.js b/test/js/node/test/parallel/fs-symlink-buffer-path.test.js deleted file mode 100644 index 17bde4dccb..0000000000 --- a/test/js/node/test/parallel/fs-symlink-buffer-path.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-fs-symlink-buffer-path.js -//#SHA1: 73fa7d9b492bd23730f1a8763caac92a9f4a1896 -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const canCreateSymLink = () => { - try { - fs.symlinkSync('test-file', 'test-symlink'); - fs.unlinkSync('test-symlink'); - return true; - } catch (err) { - return false; - } -}; - -if (!canCreateSymLink()) { - test.skip('insufficient privileges', () => {}); -} else { - const tmpdir = path.join(os.tmpdir(), 'test-fs-symlink-buffer-path'); - const fixturesPath = path.join(__dirname, '..', 'fixtures'); - - beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - }); - - afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); - }); - - test('creating and reading symbolic link', async () => { - const linkData = path.join(fixturesPath, 'cycles', 'root.js'); - const linkPath = path.join(tmpdir, 'symlink1.js'); - - fs.symlinkSync(Buffer.from(linkData), linkPath); - - const linkStats = await fs.promises.lstat(linkPath); - const linkTime = linkStats.mtime.getTime(); - - const fileStats = await fs.promises.stat(linkPath); - const fileTime = fileStats.mtime.getTime(); - - const destination = await fs.promises.readlink(linkPath); - expect(destination).toBe(linkData); - - expect(linkTime).not.toBe(fileTime); - }); -} - -//<#END_FILE: test-fs-symlink-buffer-path.js diff --git a/test/js/node/test/parallel/fs-symlink.test.js b/test/js/node/test/parallel/fs-symlink.test.js deleted file mode 100644 index c4e68336ba..0000000000 --- a/test/js/node/test/parallel/fs-symlink.test.js +++ /dev/null @@ -1,148 +0,0 @@ -//#FILE: test-fs-symlink.js -//#SHA1: 4861a453e314d789a1b933d7179da96b7a35378c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const canCreateSymLink = () => { - try { - fs.symlinkSync("", ""); - fs.unlinkSync(""); - return true; - } catch (e) { - return false; - } -}; - -if (!canCreateSymLink()) { - it.skip("insufficient privileges", () => {}); -} else { - let linkTime; - let fileTime; - const tmpdir = os.tmpdir(); - - beforeEach(() => { - jest.spyOn(fs, "symlink"); - jest.spyOn(fs, "lstat"); - jest.spyOn(fs, "stat"); - jest.spyOn(fs, "readlink"); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - test("Test creating and reading symbolic link", async () => { - const linkData = path.resolve(__dirname, "../fixtures/cycles/root.js"); - const linkPath = path.resolve(tmpdir, "symlink1.js"); - - await new Promise(resolve => { - fs.symlink(linkData, linkPath, resolve); - }); - - expect(fs.symlink).toHaveBeenCalled(); - - await new Promise(resolve => { - fs.lstat(linkPath, (err, stats) => { - expect(err).toBeNull(); - linkTime = stats.mtime.getTime(); - resolve(); - }); - }); - - await new Promise(resolve => { - fs.stat(linkPath, (err, stats) => { - expect(err).toBeNull(); - fileTime = stats.mtime.getTime(); - resolve(); - }); - }); - - await new Promise(resolve => { - fs.readlink(linkPath, (err, destination) => { - expect(err).toBeNull(); - expect(destination).toBe(linkData); - resolve(); - }); - }); - }); - - test("Test invalid symlink", async () => { - const linkData = path.resolve(__dirname, "../fixtures/not/exists/file"); - const linkPath = path.resolve(tmpdir, "symlink2.js"); - - await new Promise(resolve => { - fs.symlink(linkData, linkPath, resolve); - }); - - expect(fs.existsSync(linkPath)).toBe(false); - }); - - test("Test invalid inputs", () => { - const invalidInputs = [false, 1, {}, [], null, undefined]; - const errObj = expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringMatching(/target|path/), - }); - - invalidInputs.forEach(input => { - expect(() => fs.symlink(input, "", () => {})).toThrow(errObj); - expect(() => fs.symlinkSync(input, "")).toThrow(errObj); - - expect(() => fs.symlink("", input, () => {})).toThrow(errObj); - expect(() => fs.symlinkSync("", input)).toThrow(errObj); - }); - }); - - test("Test invalid type inputs", () => { - const errObj = expect.objectContaining({ - code: "ERR_INVALID_ARG_VALUE", - name: "TypeError", - }); - - expect(() => fs.symlink("", "", "🍏", () => {})).toThrow(errObj); - expect(() => fs.symlinkSync("", "", "🍏")).toThrow(errObj); - - expect(() => fs.symlink("", "", "nonExistentType", () => {})).toThrow(errObj); - expect(() => fs.symlinkSync("", "", "nonExistentType")).toThrow(errObj); - expect(fs.promises.symlink("", "", "nonExistentType")).rejects.toMatchObject(errObj); - - expect(() => fs.symlink("", "", false, () => {})).toThrow(errObj); - expect(() => fs.symlinkSync("", "", false)).toThrow(errObj); - expect(fs.promises.symlink("", "", false)).rejects.toMatchObject(errObj); - - expect(() => fs.symlink("", "", {}, () => {})).toThrow(errObj); - expect(() => fs.symlinkSync("", "", {})).toThrow(errObj); - expect(fs.promises.symlink("", "", {})).rejects.toMatchObject(errObj); - }); - - test("Link time should not be equal to file time", () => { - expect(linkTime).not.toBe(fileTime); - }); -} - -//<#END_FILE: test-fs-symlink.js diff --git a/test/js/node/test/parallel/fs-truncate-clear-file-zero.test.js b/test/js/node/test/parallel/fs-truncate-clear-file-zero.test.js deleted file mode 100644 index 8dc9566b1f..0000000000 --- a/test/js/node/test/parallel/fs-truncate-clear-file-zero.test.js +++ /dev/null @@ -1,67 +0,0 @@ -//#FILE: test-fs-truncate-clear-file-zero.js -//#SHA1: 28aa057c9903ea2436c340ccecfec093c647714c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// This test ensures that `fs.truncate` opens the file with `r+` and not `w`, -// which had earlier resulted in the target file's content getting zeroed out. -// https://github.com/nodejs/node-v0.x-archive/issues/6233 - -const filename = path.join(os.tmpdir(), "truncate-file.txt"); - -beforeEach(() => { - // Clean up any existing test file - try { - fs.unlinkSync(filename); - } catch (err) { - if (err.code !== "ENOENT") throw err; - } -}); - -test("fs.truncateSync", () => { - fs.writeFileSync(filename, "0123456789"); - expect(fs.readFileSync(filename, "utf8")).toBe("0123456789"); - fs.truncateSync(filename, 5); - expect(fs.readFileSync(filename, "utf8")).toBe("01234"); -}); - -test("fs.truncate", async () => { - fs.writeFileSync(filename, "0123456789"); - expect(fs.readFileSync(filename, "utf8")).toBe("0123456789"); - - await new Promise((resolve, reject) => { - fs.truncate(filename, 5, err => { - if (err) reject(err); - else resolve(); - }); - }); - - expect(fs.readFileSync(filename, "utf8")).toBe("01234"); -}); - -//<#END_FILE: test-fs-truncate-clear-file-zero.js diff --git a/test/js/node/test/parallel/fs-truncate-sync.test.js b/test/js/node/test/parallel/fs-truncate-sync.test.js deleted file mode 100644 index cdf9a5f739..0000000000 --- a/test/js/node/test/parallel/fs-truncate-sync.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-fs-truncate-sync.js -//#SHA1: 6b4ccbf9b9fab199c6b258374cf0a1665b1c21fe -//----------------- -"use strict"; - -const path = require("path"); -const fs = require("fs"); -const tmpdir = require("../common/tmpdir"); -const tmp = tmpdir.path; - -describe("fs.truncateSync", () => { - beforeEach(() => { - tmpdir.refresh(); - }); - - test("truncates file correctly", () => { - const filename = path.resolve(tmp, "truncate-sync-file.txt"); - - fs.writeFileSync(filename, "hello world", "utf8"); - - const fd = fs.openSync(filename, "r+"); - - fs.truncateSync(fd, 5); - expect(fs.readFileSync(fd)).toEqual(Buffer.from("hello")); - - fs.closeSync(fd); - fs.unlinkSync(filename); - }); -}); - -//<#END_FILE: test-fs-truncate-sync.js diff --git a/test/js/node/test/parallel/fs-unlink-type-check.test.js b/test/js/node/test/parallel/fs-unlink-type-check.test.js deleted file mode 100644 index 4f819feecd..0000000000 --- a/test/js/node/test/parallel/fs-unlink-type-check.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-fs-unlink-type-check.js -//#SHA1: 337e42f3b15589a7652c32c0a1c92292abf098d0 -//----------------- -"use strict"; - -const fs = require("fs"); - -test("fs.unlink and fs.unlinkSync with invalid types", () => { - const invalidTypes = [false, 1, {}, [], null, undefined]; - - invalidTypes.forEach(invalidType => { - expect(() => fs.unlink(invalidType, jest.fn())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => fs.unlinkSync(invalidType)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-fs-unlink-type-check.js diff --git a/test/js/node/test/parallel/fs-util-validateoffsetlength.test.js b/test/js/node/test/parallel/fs-util-validateoffsetlength.test.js deleted file mode 100644 index 1cc49c3be9..0000000000 --- a/test/js/node/test/parallel/fs-util-validateoffsetlength.test.js +++ /dev/null @@ -1,85 +0,0 @@ -//#FILE: test-fs-util-validateoffsetlength.js -//#SHA1: d5c952d2e87072352a6a60351ede415d1925cf21 -//----------------- -'use strict'; - -// Implement the functions we want to test -function validateOffsetLengthRead(offset, length, byteLength) { - if (offset < 0) { - throw new RangeError('The value of "offset" is out of range. ' + - `It must be >= 0. Received ${offset}`); - } - if (length < 0) { - throw new RangeError('The value of "length" is out of range. ' + - `It must be >= 0. Received ${length}`); - } - if (offset + length > byteLength) { - throw new RangeError('The value of "length" is out of range. ' + - `It must be <= ${byteLength - offset}. Received ${length}`); - } -} - -function validateOffsetLengthWrite(offset, length, byteLength) { - if (offset > byteLength) { - throw new RangeError('The value of "offset" is out of range. ' + - `It must be <= ${byteLength}. Received ${offset}`); - } - if (length > byteLength - offset) { - throw new RangeError('The value of "length" is out of range. ' + - `It must be <= ${byteLength - offset}. Received ${length}`); - } -} - -describe('validateOffsetLengthRead', () => { - test('throws RangeError when offset is negative', () => { - const offset = -1; - expect(() => validateOffsetLengthRead(offset, 0, 0)).toThrow(expect.objectContaining({ - name: 'RangeError', - message: expect.stringContaining(`It must be >= 0. Received ${offset}`) - })); - }); - - test('throws RangeError when length is negative', () => { - const length = -1; - expect(() => validateOffsetLengthRead(0, length, 0)).toThrow(expect.objectContaining({ - name: 'RangeError', - message: expect.stringContaining(`It must be >= 0. Received ${length}`) - })); - }); - - test('throws RangeError when length is out of range', () => { - const offset = 1; - const length = 1; - const byteLength = offset + length - 1; - expect(() => validateOffsetLengthRead(offset, length, byteLength)).toThrow(expect.objectContaining({ - name: 'RangeError', - message: expect.stringContaining(`It must be <= ${byteLength - offset}. Received ${length}`) - })); - }); -}); - -describe('validateOffsetLengthWrite', () => { - const kIoMaxLength = 2 ** 31 - 1; - - test('throws RangeError when offset > byteLength', () => { - const offset = 100; - const length = 100; - const byteLength = 50; - expect(() => validateOffsetLengthWrite(offset, length, byteLength)).toThrow(expect.objectContaining({ - name: 'RangeError', - message: expect.stringContaining(`It must be <= ${byteLength}. Received ${offset}`) - })); - }); - - test('throws RangeError when byteLength < kIoMaxLength and length > byteLength - offset', () => { - const offset = kIoMaxLength - 150; - const length = 200; - const byteLength = kIoMaxLength - 100; - expect(() => validateOffsetLengthWrite(offset, length, byteLength)).toThrow(expect.objectContaining({ - name: 'RangeError', - message: expect.stringContaining(`It must be <= ${byteLength - offset}. Received ${length}`) - })); - }); -}); - -//<#END_FILE: test-fs-util-validateoffsetlength.js diff --git a/test/js/node/test/parallel/fs-watch-abort-signal.test.js b/test/js/node/test/parallel/fs-watch-abort-signal.test.js deleted file mode 100644 index 030e33814e..0000000000 --- a/test/js/node/test/parallel/fs-watch-abort-signal.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-fs-watch-abort-signal.js -//#SHA1: 6f0b7fcc2f597faa8e1353559d5d007cd744614a -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const isIBMi = process.platform === 'os400'; - -if (isIBMi) { - test.skip('IBMi does not support `fs.watch()`', () => {}); -} else { - const tmpdir = path.join(os.tmpdir(), 'test-fs-watch-abort-signal'); - const emptyFile = path.join(tmpdir, 'empty.js'); - - beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - fs.writeFileSync(emptyFile, ''); - }); - - afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); - }); - - test('Signal aborted after creating the watcher', (done) => { - const ac = new AbortController(); - const { signal } = ac; - const watcher = fs.watch(emptyFile, { signal }); - watcher.once('close', () => { - done(); - }); - setImmediate(() => ac.abort()); - }); - - test('Signal aborted before creating the watcher', (done) => { - const signal = AbortSignal.abort(); - const watcher = fs.watch(emptyFile, { signal }); - watcher.once('close', () => { - done(); - }); - }); -} - -//<#END_FILE: test-fs-watch-abort-signal.js diff --git a/test/js/node/test/parallel/fs-watch-close-when-destroyed.test.js b/test/js/node/test/parallel/fs-watch-close-when-destroyed.test.js deleted file mode 100644 index 5749125642..0000000000 --- a/test/js/node/test/parallel/fs-watch-close-when-destroyed.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-fs-watch-close-when-destroyed.js -//#SHA1: f062b7243d0c42722a289a6228d4c2c1a503be1b -//----------------- -"use strict"; - -// This tests that closing a watcher when the underlying handle is -// already destroyed will result in a noop instead of a crash. - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// fs-watch on folders have limited capability in AIX. -// The testcase makes use of folder watching, and causes -// hang. This behavior is documented. Skip this for AIX. - -if (process.platform === "aix") { - it.skip("folder watch capability is limited in AIX."); -} else if (process.platform === "os400") { - it.skip("IBMi does not support `fs.watch()`"); -} else { - let root; - - beforeEach(() => { - root = path.join(os.tmpdir(), "watched-directory-" + Math.random().toString(36).slice(2)); - fs.mkdirSync(root); - }); - - afterEach(() => { - try { - fs.rmdirSync(root); - } catch (error) { - // Ignore errors, directory might already be removed - } - }); - - it("should not crash when closing watcher after handle is destroyed", done => { - const watcher = fs.watch(root, { persistent: false, recursive: false }); - - // The following listeners may or may not be invoked. - - watcher.addListener("error", () => { - setTimeout( - () => { - watcher.close(); - }, // Should not crash if it's invoked - 10, - ); - }); - - watcher.addListener("change", () => { - setTimeout(() => { - watcher.close(); - }, 10); - }); - - fs.rmdirSync(root); - // Wait for the listener to hit - setTimeout(done, 100); - }); -} - -//<#END_FILE: test-fs-watch-close-when-destroyed.js diff --git a/test/js/node/test/parallel/fs-watch-encoding.test.js b/test/js/node/test/parallel/fs-watch-encoding.test.js deleted file mode 100644 index 472a77d57a..0000000000 --- a/test/js/node/test/parallel/fs-watch-encoding.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-fs-watch-encoding.js -//#SHA1: 63f7e4008743417c7ee5995bbf16a28ade764e48 -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const tmpdir = path.join(os.tmpdir(), 'test-fs-watch-encoding'); -const fn = '新建文夹件.txt'; -const a = path.join(tmpdir, fn); - -let interval; - -beforeAll(() => { - if (process.platform === 'aix') { - return test.skip('folder watch capability is limited in AIX.'); - } - if (process.platform === 'os400') { - return test.skip('IBMi does not support `fs.watch()`'); - } - - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterAll(() => { - clearInterval(interval); - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -const watcherTests = [ - { - name: 'with hex encoding', - options: { encoding: 'hex' }, - expectedFilenames: ['e696b0e5bbbae69687e5a4b9e4bbb62e747874', null], - }, - { - name: 'without encoding option', - options: {}, - expectedFilenames: [fn, null], - }, - { - name: 'with buffer encoding', - options: { encoding: 'buffer' }, - expectedFilenames: [Buffer.from(fn), null], - }, -]; - -watcherTests.forEach(({ name, options, expectedFilenames }) => { - test(`fs.watch ${name}`, (done) => { - const watcher = fs.watch(tmpdir, options, (event, filename) => { - if (expectedFilenames.some(expected => - expected instanceof Buffer - ? expected.equals(filename) - : expected === filename)) { - watcher.close(); - done(); - } - }); - - // Start the interval after setting up the watcher - if (!interval) { - interval = setInterval(() => { - const fd = fs.openSync(a, 'w+'); - fs.closeSync(fd); - fs.unlinkSync(a); - }, 100); - } - }, 10000); // Increased timeout to allow for file operations -}); - -//<#END_FILE: test-fs-watch-encoding.js diff --git a/test/js/node/test/parallel/fs-watch-file-enoent-after-deletion.test.js b/test/js/node/test/parallel/fs-watch-file-enoent-after-deletion.test.js deleted file mode 100644 index ee439c8272..0000000000 --- a/test/js/node/test/parallel/fs-watch-file-enoent-after-deletion.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-fs-watch-file-enoent-after-deletion.js -//#SHA1: d6c93db608d119bd35fcab0e1e9307bfd6558b68 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// Make sure the deletion event gets reported in the following scenario: -// 1. Watch a file. -// 2. The initial stat() goes okay. -// 3. Something deletes the watched file. -// 4. The second stat() fails with ENOENT. - -// The second stat() translates into the first 'change' event but a logic error -// stopped it from getting emitted. -// https://github.com/nodejs/node-v0.x-archive/issues/4027 - -test("fs.watchFile reports deletion", done => { - const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test-")); - const filename = path.join(tmpdir, "watched"); - fs.writeFileSync(filename, "quis custodiet ipsos custodes"); - - const watcher = jest.fn(); - fs.watchFile(filename, { interval: 50 }, watcher); - - setTimeout(() => { - fs.unlinkSync(filename); - - setTimeout(() => { - expect(watcher).toHaveBeenCalledTimes(1); - const [curr, prev] = watcher.mock.calls[0]; - expect(curr.nlink).toBe(0); - expect(prev.nlink).toBe(1); - - fs.unwatchFile(filename); - fs.rmdirSync(tmpdir); - done(); - }, 100); - }, 100); -}); - -//<#END_FILE: test-fs-watch-file-enoent-after-deletion.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-add-file-to-existing-subfolder.test.js b/test/js/node/test/parallel/fs-watch-recursive-add-file-to-existing-subfolder.test.js deleted file mode 100644 index 3e2c067792..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-add-file-to-existing-subfolder.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-fs-watch-recursive-add-file-to-existing-subfolder.js -//#SHA1: 7d4414be8a9ba35f2ebdb685037951133137b6ef -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const testDir = path.join(os.tmpdir(), 'test-fs-watch-recursive-add-file-to-existing-subfolder'); - -beforeAll(() => { - if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); - } - fs.mkdirSync(testDir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(testDir, { recursive: true, force: true }); -}); - -test('fs.watch detects file added to existing subfolder', (done) => { - const rootDirectory = fs.mkdtempSync(path.join(testDir, 'test-')); - const testDirectory = path.join(rootDirectory, 'test-4'); - fs.mkdirSync(testDirectory); - - const file = 'folder-5'; - const filePath = path.join(testDirectory, file); - fs.mkdirSync(filePath); - - const subfolderPath = path.join(filePath, 'subfolder-6'); - fs.mkdirSync(subfolderPath); - - const childrenFile = 'file-7.txt'; - const childrenAbsolutePath = path.join(subfolderPath, childrenFile); - const relativePath = path.join(file, path.basename(subfolderPath), childrenFile); - - const watcher = fs.watch(testDirectory, { recursive: true }); - let watcherClosed = false; - - watcher.on('change', (event, filename) => { - expect(event).toBe('rename'); - - if (filename === relativePath) { - watcher.close(); - watcherClosed = true; - expect(watcherClosed).toBe(true); - done(); - } - }); - - // Do the write with a delay to ensure that the OS is ready to notify us. - setTimeout(() => { - fs.writeFileSync(childrenAbsolutePath, 'world'); - }, 200); -}, 10000); // Increased timeout to 10 seconds - -//<#END_FILE: test-fs-watch-recursive-add-file-to-existing-subfolder.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-add-file-with-url.test.js b/test/js/node/test/parallel/fs-watch-recursive-add-file-with-url.test.js deleted file mode 100644 index 2131b1dbe5..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-add-file-with-url.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-fs-watch-recursive-add-file-with-url.js -//#SHA1: e6498ea80abdf69cb66d888bbd7d631931970c0a -//----------------- -"use strict"; - -const { setTimeout } = require("timers/promises"); -const path = require("path"); -const fs = require("fs"); -const { pathToFileURL } = require("url"); -const os = require("os"); - -const isIBMi = process.platform === "os400"; -const isAIX = process.platform === "aix"; - -if (isIBMi) { - it.skip("IBMi does not support `fs.watch()`", () => {}); -} else if (isAIX) { - it.skip("folder watch capability is limited in AIX.", () => {}); -} else { - it("should watch for file changes using URL as path", async () => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-")); - - // Add a file to already watching folder, and use URL as the path - const rootDirectory = fs.mkdtempSync(path.join(testDir, path.sep)); - const testDirectory = path.join(rootDirectory, "test-5"); - fs.mkdirSync(testDirectory); - - const filePath = path.join(testDirectory, "file-8.txt"); - const url = pathToFileURL(testDirectory); - - const watcher = fs.watch(url, { recursive: true }); - let watcherClosed = false; - - const watchPromise = new Promise(resolve => { - watcher.on("change", function (event, filename) { - expect(event).toBe("rename"); - - if (filename === path.basename(filePath)) { - watcher.close(); - watcherClosed = true; - resolve(); - } - }); - }); - - await setTimeout(100); - fs.writeFileSync(filePath, "world"); - - await watchPromise; - - expect(watcherClosed).toBe(true); - }, 10000); // Increase timeout to 10 seconds -} - -//<#END_FILE: test-fs-watch-recursive-add-file-with-url.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-add-file.test.js b/test/js/node/test/parallel/fs-watch-recursive-add-file.test.js deleted file mode 100644 index 00c5fcf7e6..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-add-file.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-fs-watch-recursive-add-file.js -//#SHA1: e87d2c9f4789a6e6a83fbdca56e39683625bd0af -//----------------- -"use strict"; - -const path = require("path"); -const fs = require("fs"); -const os = require("os"); - -const isIBMi = os.platform() === "os400"; -const isAIX = os.platform() === "aix"; - -if (isIBMi) { - it.skip("IBMi does not support `fs.watch()`", () => {}); -} else if (isAIX) { - it.skip("folder watch capability is limited in AIX.", () => {}); -} else { - const tmpdir = { - path: path.join(os.tmpdir(), "jest-test-fs-watch-recursive-add-file"), - refresh: () => { - if (fs.existsSync(tmpdir.path)) { - fs.rmSync(tmpdir.path, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir.path, { recursive: true }); - }, - }; - - beforeEach(() => { - tmpdir.refresh(); - }); - - it("should detect file added to already watching folder", done => { - const rootDirectory = fs.mkdtempSync(tmpdir.path + path.sep); - const testDirectory = path.join(rootDirectory, "test-1"); - fs.mkdirSync(testDirectory); - - const testFile = path.join(testDirectory, "file-1.txt"); - - const watcher = fs.watch(testDirectory, { recursive: true }); - let watcherClosed = false; - - watcher.on("change", function (event, filename) { - expect(event).toBe("rename"); - - if (filename === path.basename(testFile)) { - watcher.close(); - watcherClosed = true; - expect(watcherClosed).toBe(true); - done(); - } - }); - - // Do the write with a delay to ensure that the OS is ready to notify us. - setTimeout( - () => { - fs.writeFileSync(testFile, "world"); - }, - process.platform === "win32" ? 200 : 100, - ); - }); -} - -//<#END_FILE: test-fs-watch-recursive-add-file.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-add-folder.test.js b/test/js/node/test/parallel/fs-watch-recursive-add-folder.test.js deleted file mode 100644 index dbe75002f5..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-add-folder.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-fs-watch-recursive-add-folder.js -//#SHA1: 4c2908ccc8502f5f760963a9b9a6db6ddadd4c1c -//----------------- -"use strict"; - -const { setTimeout } = require("timers/promises"); -const assert = require("assert"); -const path = require("path"); -const fs = require("fs"); -const os = require("os"); - -const isIBMi = os.platform() === "os400"; -const isAIX = os.platform() === "aix"; - -if (isIBMi) { - it.skip("IBMi does not support `fs.watch()`", () => {}); -} else if (isAIX) { - it.skip("folder watch capability is limited in AIX.", () => {}); -} else { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-")); - - afterAll(() => { - fs.rmSync(testDir, { recursive: true, force: true }); - }); - - test("Add a folder to already watching folder", async () => { - // Add a folder to already watching folder - - const rootDirectory = fs.mkdtempSync(path.join(testDir, "root-")); - const testDirectory = path.join(rootDirectory, "test-2"); - fs.mkdirSync(testDirectory); - - const testFile = path.join(testDirectory, "folder-2"); - - const watcher = fs.watch(testDirectory, { recursive: true }); - let watcherClosed = false; - - const watchPromise = new Promise(resolve => { - watcher.on("change", function (event, filename) { - expect(event).toBe("rename"); - - if (filename === path.basename(testFile)) { - watcher.close(); - watcherClosed = true; - resolve(); - } - }); - }); - - await setTimeout(100); - fs.mkdirSync(testFile); - - await watchPromise; - - expect(watcherClosed).toBe(true); - }); -} - -//<#END_FILE: test-fs-watch-recursive-add-folder.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-assert-leaks.test.js b/test/js/node/test/parallel/fs-watch-recursive-assert-leaks.test.js deleted file mode 100644 index 69ec1c857c..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-assert-leaks.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-fs-watch-recursive-assert-leaks.js -//#SHA1: 316f1b184840ce5a8f2f92f4ab205038f4acaf9d -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const { setTimeout } = require('timers/promises'); - -const testDir = path.join(os.tmpdir(), 'test-fs-watch-recursive-assert-leaks'); - -beforeAll(() => { - if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); - } - fs.mkdirSync(testDir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(testDir, { recursive: true, force: true }); -}); - -// Skip test for IBMi and AIX -const isIBMi = process.platform === 'os400'; -const isAIX = process.platform === 'aix'; - -if (isIBMi) { - test.skip('IBMi does not support `fs.watch()`', () => {}); -} else if (isAIX) { - test.skip('folder watch capability is limited in AIX', () => {}); -} else { - test('recursive watch does not leak handles', async () => { - const rootDirectory = fs.mkdtempSync(path.join(testDir, 'root-')); - const testDirectory = path.join(rootDirectory, 'test-7'); - const filePath = path.join(testDirectory, 'only-file.txt'); - fs.mkdirSync(testDirectory); - - let watcherClosed = false; - const watcher = fs.watch(testDirectory, { recursive: true }); - - const watchPromise = new Promise((resolve) => { - watcher.on('change', async (event, filename) => { - await setTimeout(100); - if (filename === path.basename(filePath)) { - watcher.close(); - watcherClosed = true; - resolve(); - } - await setTimeout(100); - expect(process._getActiveHandles().some((handle) => handle.constructor.name === 'StatWatcher')).toBe(false); - }); - }); - - // Do the write with a delay to ensure that the OS is ready to notify us. - await setTimeout(200); - fs.writeFileSync(filePath, 'content'); - - await watchPromise; - expect(watcherClosed).toBe(true); - }, 10000); // Increased timeout to 10 seconds -} - -//<#END_FILE: test-fs-watch-recursive-assert-leaks.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-linux-parallel-remove.test.js b/test/js/node/test/parallel/fs-watch-recursive-linux-parallel-remove.test.js deleted file mode 100644 index b109622f57..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-linux-parallel-remove.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-fs-watch-recursive-linux-parallel-remove.js -//#SHA1: ed10536d8d54febe24a3dcf494a26eab06bc4f66 -//----------------- -"use strict"; - -const path = require("node:path"); -const fs = require("node:fs"); -const { spawn } = require("node:child_process"); -const os = require("node:os"); - -// Skip test if not running on Linux -if (os.platform() !== "linux") { - test.skip("This test can run only on Linux", () => {}); -} else { - // Test that the watcher do not crash if the file "disappears" while - // watch is being set up. - - let testDir; - let watcher; - - beforeEach(() => { - testDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-")); - }); - - afterEach(() => { - if (watcher) { - watcher.close(); - } - fs.rmSync(testDir, { recursive: true, force: true }); - }); - - test("fs.watch does not crash on parallel file removal", done => { - watcher = fs.watch(testDir, { recursive: true }); - watcher.on("change", function (event, filename) { - // This console.log makes the error happen - // do not remove - console.log(filename, event); - }); - - const testFile = path.join(testDir, "a"); - const child = spawn( - process.argv[0], - [ - "-e", - `const fs = require('node:fs'); for (let i = 0; i < 10000; i++) { const fd = fs.openSync('${testFile}', 'w'); fs.writeSync(fd, Buffer.from('hello')); fs.rmSync('${testFile}') }`, - ], - { - stdio: "inherit", - }, - ); - - child.on("exit", function () { - watcher.close(); - done(); - }); - }); -} - -//<#END_FILE: test-fs-watch-recursive-linux-parallel-remove.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-sync-write.test.js b/test/js/node/test/parallel/fs-watch-recursive-sync-write.test.js deleted file mode 100644 index fa80c9a8e3..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-sync-write.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-fs-watch-recursive-sync-write.js -//#SHA1: 436087aa83502744252800b9a93dbe88a4ca3822 -//----------------- -'use strict'; - -const fs = require('node:fs'); -const path = require('node:path'); -const os = require('os'); - -const tmpDir = path.join(os.tmpdir(), 'test-fs-watch-recursive-sync-write'); -const filename = path.join(tmpDir, 'test.file'); - -beforeAll(() => { - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpDir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); -}); - -// Skip test for IBMi and AIX -const isIBMi = process.platform === 'os400'; -const isAIX = process.platform === 'aix'; - -if (isIBMi) { - test.skip('IBMi does not support `fs.watch()`', () => {}); -} else if (isAIX) { - test.skip('folder watch capability is limited in AIX', () => {}); -} else { - test('fs.watch detects file creation with recursive option', (done) => { - const timeout = setTimeout(() => { - done(new Error('timed out')); - }, 30000); - - function doWatch() { - const watcher = fs.watch(tmpDir, { recursive: true }, (eventType, _filename) => { - clearTimeout(timeout); - watcher.close(); - expect(eventType).toBe('rename'); - expect(path.join(tmpDir, _filename)).toBe(filename); - done(); - }); - - // Do the write with a delay to ensure that the OS is ready to notify us. - setTimeout(() => { - fs.writeFileSync(filename, 'foobar2'); - }, 200); - } - - if (process.platform === 'darwin') { - // On macOS delay watcher start to avoid leaking previous events. - // Refs: https://github.com/libuv/libuv/pull/4503 - setTimeout(doWatch, 100); - } else { - doWatch(); - } - }, 35000); // Increase timeout to account for the 30 second timeout in the test -} - -//<#END_FILE: test-fs-watch-recursive-sync-write.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-update-file.test.js b/test/js/node/test/parallel/fs-watch-recursive-update-file.test.js deleted file mode 100644 index 331df95da8..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-update-file.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-fs-watch-recursive-update-file.js -//#SHA1: d197449fc5f430b9ce49e7f75b57a44dd4f2259a -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const testDir = path.join(os.tmpdir(), 'test-fs-watch-recursive-update-file'); - -beforeAll(() => { - if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); - } - fs.mkdirSync(testDir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(testDir, { recursive: true, force: true }); -}); - -test('Watch a folder and update an already existing file in it', (done) => { - const rootDirectory = fs.mkdtempSync(path.join(testDir, 'test-')); - const testDirectory = path.join(rootDirectory, 'test-0'); - fs.mkdirSync(testDirectory); - - const testFile = path.join(testDirectory, 'file-1.txt'); - fs.writeFileSync(testFile, 'hello'); - - const watcher = fs.watch(testDirectory, { recursive: true }); - - watcher.on('change', (event, filename) => { - expect(event === 'change' || event === 'rename').toBe(true); - - if (filename === path.basename(testFile)) { - watcher.close(); - done(); - } - }); - - // Do the write with a delay to ensure that the OS is ready to notify us. - setTimeout(() => { - fs.writeFileSync(testFile, 'hello'); - }, 200); -}, 10000); // Increased timeout to allow for file system operations - -//<#END_FILE: test-fs-watch-recursive-update-file.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-validate.test.js b/test/js/node/test/parallel/fs-watch-recursive-validate.test.js deleted file mode 100644 index aeed19b67d..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-validate.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-fs-watch-recursive-validate.js -//#SHA1: eb5d9ff1caac7f9d4acf694c43e4f634f538befb -//----------------- -"use strict"; - -const path = require("path"); -const fs = require("fs"); -const os = require("os"); - -const isIBMi = process.platform === "os400"; -const isAIX = process.platform === "aix"; -const isWindows = process.platform === "win32"; -const isOSX = process.platform === "darwin"; - -if (isIBMi) { - test.skip("IBMi does not support `fs.watch()`", () => {}); -} else if (isAIX) { - test.skip("folder watch capability is limited in AIX.", () => {}); -} else { - const tmpdir = { - path: path.join(os.tmpdir(), "jest-fs-watch-recursive-validate"), - refresh: () => { - if (fs.existsSync(tmpdir.path)) { - fs.rmSync(tmpdir.path, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir.path, { recursive: true }); - }, - }; - - beforeEach(() => { - tmpdir.refresh(); - }); - - test("Handle non-boolean values for options.recursive", async () => { - if (!isWindows && !isOSX) { - expect(() => { - const testsubdir = fs.mkdtempSync(tmpdir.path + path.sep); - fs.watch(testsubdir, { recursive: "1" }); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - } - }); -} - -//<#END_FILE: test-fs-watch-recursive-validate.js diff --git a/test/js/node/test/parallel/fs-watch-recursive-watch-file.test.js b/test/js/node/test/parallel/fs-watch-recursive-watch-file.test.js deleted file mode 100644 index bf7b6e0212..0000000000 --- a/test/js/node/test/parallel/fs-watch-recursive-watch-file.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-fs-watch-recursive-watch-file.js -//#SHA1: 1f06958f6f645cb5c80b424a24b046f107ab83ae -//----------------- -"use strict"; - -const path = require("path"); -const fs = require("fs"); -const os = require("os"); - -const isIBMi = os.platform() === "os400"; -const isAIX = os.platform() === "aix"; - -if (isIBMi) { - test.skip("IBMi does not support `fs.watch()`"); -} - -// fs-watch on folders have limited capability in AIX. -// The testcase makes use of folder watching, and causes -// hang. This behavior is documented. Skip this for AIX. - -if (isAIX) { - test.skip("folder watch capability is limited in AIX."); -} - -const platformTimeout = ms => ms * (process.platform === "win32" ? 2 : 1); - -test("Watch a file (not a folder) using fs.watch", async () => { - // Create a temporary directory for testing - const testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "test-")); - const rootDirectory = await fs.promises.mkdtemp(path.join(testDir, path.sep)); - const testDirectory = path.join(rootDirectory, "test-6"); - await fs.promises.mkdir(testDirectory); - - const filePath = path.join(testDirectory, "only-file.txt"); - await fs.promises.writeFile(filePath, "hello"); - - let watcherClosed = false; - let interval; - - const watcher = fs.watch(filePath, { recursive: true }); - - const watchPromise = new Promise(resolve => { - watcher.on("change", function (event, filename) { - expect(event).toBe("change"); - - if (filename === path.basename(filePath)) { - clearInterval(interval); - interval = null; - watcher.close(); - watcherClosed = true; - resolve(); - } - }); - }); - - interval = setInterval(() => { - fs.writeFileSync(filePath, "world"); - }, platformTimeout(10)); - - await watchPromise; - - expect(watcherClosed).toBe(true); - expect(interval).toBeNull(); -}); - -//<#END_FILE: test-fs-watch-recursive-watch-file.js diff --git a/test/js/node/test/parallel/fs-watch-ref-unref.test.js b/test/js/node/test/parallel/fs-watch-ref-unref.test.js deleted file mode 100644 index c8d92a6eaa..0000000000 --- a/test/js/node/test/parallel/fs-watch-ref-unref.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-fs-watch-ref-unref.js -//#SHA1: ffceabfd7f8fef655b05735b8bba7fb059609980 -//----------------- -"use strict"; - -const fs = require("fs"); - -if (process.platform === "os400") { - test.skip("IBMi does not support `fs.watch()`"); -} - -test("fs.watch() can be unref()ed and ref()ed", () => { - const watcher = fs.watch(__filename, () => { - // This callback should not be called - expect(true).toBe(false); - }); - - watcher.unref(); - - return new Promise(resolve => { - setTimeout( - () => { - watcher.ref(); - watcher.unref(); - resolve(); - }, - process.platform === "win32" ? 100 : 50, - ); - }); -}); - -//<#END_FILE: test-fs-watch-ref-unref.js diff --git a/test/js/node/test/parallel/fs-watch-stop-sync.test.js b/test/js/node/test/parallel/fs-watch-stop-sync.test.js deleted file mode 100644 index fe1eae0f1c..0000000000 --- a/test/js/node/test/parallel/fs-watch-stop-sync.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-fs-watch-stop-sync.js -//#SHA1: 8285d2bd43d2f9be7be525417cf51f9336b2f379 -//----------------- -"use strict"; - -// This test checks that the `stop` event is emitted asynchronously. -// -// If it isn't asynchronous, then the listener will be called during the -// execution of `watch.stop()`. That would be a bug. -// -// If it is asynchronous, then the listener will be removed before the event is -// emitted. - -const fs = require("fs"); - -test("stop event is emitted asynchronously", () => { - const listener = jest.fn(); - - const watch = fs.watchFile(__filename, jest.fn()); - watch.once("stop", listener); - watch.stop(); - watch.removeListener("stop", listener); - - expect(listener).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-fs-watch-stop-sync.js diff --git a/test/js/node/test/parallel/fs-watch.test.js b/test/js/node/test/parallel/fs-watch.test.js deleted file mode 100644 index 117b2611dc..0000000000 --- a/test/js/node/test/parallel/fs-watch.test.js +++ /dev/null @@ -1,122 +0,0 @@ -//#FILE: test-fs-watch.js -//#SHA1: 07373db00b057e796555cac6f75973e9e4358284 -//----------------- -'use strict'; -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const isIBMi = process.platform === 'os400'; -const isLinux = process.platform === 'linux'; -const isMacOS = process.platform === 'darwin'; -const isWindows = process.platform === 'win32'; -const isAIX = process.platform === 'aix'; - -if (isIBMi) { - test.skip('IBMi does not support `fs.watch()`', () => {}); -} else { - const tmpdir = path.join(os.tmpdir(), 'test-fs-watch'); - - class WatchTestCase { - constructor(shouldInclude, dirName, fileName, field) { - this.dirName = dirName; - this.fileName = fileName; - this.field = field; - this.shouldSkip = !shouldInclude; - } - get dirPath() { return path.join(tmpdir, this.dirName); } - get filePath() { return path.join(this.dirPath, this.fileName); } - } - - const cases = [ - new WatchTestCase( - isLinux || isMacOS || isWindows || isAIX, - 'watch1', - 'foo', - 'filePath' - ), - new WatchTestCase( - isLinux || isMacOS || isWindows, - 'watch2', - 'bar', - 'dirPath' - ), - ]; - - beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); - }); - - afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); - }); - - function doWatchTest(testCase) { - return new Promise((resolve, reject) => { - let interval; - const pathToWatch = testCase[testCase.field]; - const watcher = fs.watch(pathToWatch); - - watcher.on('error', (err) => { - if (interval) { - clearInterval(interval); - interval = null; - } - reject(err); - }); - - watcher.on('change', (eventType, argFilename) => { - if (interval) { - clearInterval(interval); - interval = null; - } - if (isMacOS) - expect(['rename', 'change'].includes(eventType)).toBe(true); - else - expect(eventType).toBe('change'); - expect(argFilename).toBe(testCase.fileName); - - watcher.close(); - watcher.close(); // Closing a closed watcher should be a noop - resolve(); - }); - - const content2 = Date.now() + testCase.fileName.toUpperCase().repeat(1e4); - interval = setInterval(() => { - fs.writeFileSync(testCase.filePath, ''); - fs.writeFileSync(testCase.filePath, content2); - }, 100); - }); - } - - test.each(cases.filter(testCase => !testCase.shouldSkip))( - 'Watch test for $dirName', - async (testCase) => { - fs.mkdirSync(testCase.dirPath, { recursive: true }); - const content1 = Date.now() + testCase.fileName.toLowerCase().repeat(1e4); - fs.writeFileSync(testCase.filePath, content1); - - if (isMacOS) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - - await doWatchTest(testCase); - }, - 30000 // Increase timeout to 30 seconds - ); - - test('fs.watch throws for invalid inputs', () => { - [false, 1, {}, [], null, undefined].forEach((input) => { - expect(() => fs.watch(input, () => {})).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: expect.any(String) - })); - }); - }); -} - -//<#END_FILE: test-fs-watch.js diff --git a/test/js/node/test/parallel/fs-watchfile-bigint.test.js b/test/js/node/test/parallel/fs-watchfile-bigint.test.js deleted file mode 100644 index bf4c61d5b9..0000000000 --- a/test/js/node/test/parallel/fs-watchfile-bigint.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-fs-watchfile-bigint.js -//#SHA1: 3b2e1f656e95137ca75dedd42e71ba49e6405441 -//----------------- -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const tmpdir = path.join(os.tmpdir(), 'test-fs-watchfile-bigint'); -const enoentFile = path.join(tmpdir, 'non-existent-file'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('fs.watchFile with bigint option', (done) => { - let fileExists = false; - const options = { interval: 0, bigint: true }; - - const watcher = fs.watchFile(enoentFile, options, (curr, prev) => { - if (!fileExists) { - // If the file does not exist, all the fields should be zero and the date - // fields should be UNIX EPOCH time - expect(curr.ino).toBe(0n); - expect(prev.ino).toBe(0n); - // Create the file now, so that the callback will be called back once the - // event loop notices it. - fs.closeSync(fs.openSync(enoentFile, 'w')); - fileExists = true; - } else { - // If the ino (inode) value is greater than zero, it means that the file - // is present in the filesystem and it has a valid inode number. - expect(curr.ino).toBeGreaterThan(0n); - // As the file just got created, previous ino value should be lesser than - // or equal to zero (non-existent file). - expect(prev.ino).toBeLessThanOrEqual(0n); - // Stop watching the file - fs.unwatchFile(enoentFile); - watcher.stop(); // Stopping a stopped watcher should be a noop - done(); - } - }); - - // 'stop' should only be emitted once - stopping a stopped watcher should - // not trigger a 'stop' event. - watcher.on('stop', jest.fn()); - - // Ensure the test times out if the callback is not called twice - jest.setTimeout(10000); -}); - -//<#END_FILE: test-fs-watchfile-bigint.js diff --git a/test/js/node/test/parallel/fs-write-file-buffer.test.js b/test/js/node/test/parallel/fs-write-file-buffer.test.js deleted file mode 100644 index 774f93cfb8..0000000000 --- a/test/js/node/test/parallel/fs-write-file-buffer.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-fs-write-file-buffer.js -//#SHA1: f721ad0f6969d6cf1ba78f96ccf9600a7f93458d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); -const path = require("path"); - -let data = [ - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcH", - "Bw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/", - "2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e", - "Hh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAQABADASIAAhEBAxEB/8QA", - "HwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF", - "BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK", - "FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1", - "dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG", - "x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEB", - "AQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAEC", - "AxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRom", - "JygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE", - "hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU", - "1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDhfBUFl/wk", - "OmPqKJJZw3aiZFBw4z93jnkkc9u9dj8XLfSI/EBt7DTo7ea2Ox5YXVo5FC7g", - "Tjq24nJPXNVtO0KATRvNHCIg3zoWJWQHqp+o4pun+EtJ0zxBq8mnLJa2d1L5", - "0NvnKRjJBUE5PAx3NYxxUY0pRtvYHSc5Ka2X9d7H/9k=", -]; - -data = data.join("\n"); - -let tmpdir; - -beforeEach(() => { - tmpdir = fs.mkdtempSync(path.join(process.env.TEST_TMPDIR || "/tmp", "test-fs-write-file-buffer-")); -}); - -afterEach(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test("writeFileSync with Buffer", () => { - const buf = Buffer.from(data, "base64"); - const testFile = path.join(tmpdir, "test.jpg"); - - fs.writeFileSync(testFile, buf); - - expect(fs.existsSync(testFile)).toBe(true); - expect(fs.readFileSync(testFile)).toEqual(buf); -}); - -//<#END_FILE: test-fs-write-file-buffer.js diff --git a/test/js/node/test/parallel/fs-write-no-fd.test.js b/test/js/node/test/parallel/fs-write-no-fd.test.js deleted file mode 100644 index 6750c80c46..0000000000 --- a/test/js/node/test/parallel/fs-write-no-fd.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-fs-write-no-fd.js -//#SHA1: eade06241743a0d7e72b5239633e1ddd947f3a28 -//----------------- -"use strict"; -const fs = require("fs"); - -test("fs.write with null fd and Buffer throws TypeError", () => { - expect(() => { - fs.write(null, Buffer.allocUnsafe(1), 0, 1, () => {}); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -test("fs.write with null fd and string throws TypeError", () => { - expect(() => { - fs.write(null, "1", 0, 1, () => {}); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-fs-write-no-fd.js diff --git a/test/js/node/test/parallel/fs-write-stream-close-without-callback.test.js b/test/js/node/test/parallel/fs-write-stream-close-without-callback.test.js deleted file mode 100644 index 969a581bb5..0000000000 --- a/test/js/node/test/parallel/fs-write-stream-close-without-callback.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-fs-write-stream-close-without-callback.js -//#SHA1: 63e0c345b440c8cfb157aa84340f387cf314e20f -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const tmpdir = path.join(os.tmpdir(), "test-fs-write-stream-close-without-callback"); - -beforeEach(() => { - // Create a fresh temporary directory before each test - if (!fs.existsSync(tmpdir)) { - fs.mkdirSync(tmpdir, { recursive: true }); - } -}); - -afterEach(() => { - // Clean up the temporary directory after each test - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } -}); - -test("fs.WriteStream can be closed without a callback", () => { - const filePath = path.join(tmpdir, "nocallback"); - const s = fs.createWriteStream(filePath); - - s.end("hello world"); - s.close(); - - // We don't need to assert anything here as the test is checking - // that the above operations don't throw an error - expect(true).toBe(true); -}); - -//<#END_FILE: test-fs-write-stream-close-without-callback.js diff --git a/test/js/node/test/parallel/fs-write-stream-encoding.test.js b/test/js/node/test/parallel/fs-write-stream-encoding.test.js deleted file mode 100644 index ee0135bd36..0000000000 --- a/test/js/node/test/parallel/fs-write-stream-encoding.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-fs-write-stream-encoding.js -//#SHA1: a2f61bd26151411263b933d254ec75a7ca4056fc -//----------------- -'use strict'; -const fs = require('fs'); -const stream = require('stream'); -const path = require('path'); -const os = require('os'); - -const fixturesPath = path.join(__dirname, '..', 'fixtures'); -const tmpdir = path.join(os.tmpdir(), 'test-fs-write-stream-encoding'); - -const firstEncoding = 'base64'; -const secondEncoding = 'latin1'; - -const examplePath = path.join(fixturesPath, 'x.txt'); -const dummyPath = path.join(tmpdir, 'x.txt'); - -beforeAll(() => { - if (fs.existsSync(tmpdir)) { - fs.rmSync(tmpdir, { recursive: true, force: true }); - } - fs.mkdirSync(tmpdir, { recursive: true }); -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test('write stream encoding', (done) => { - const exampleReadStream = fs.createReadStream(examplePath, { - encoding: firstEncoding - }); - - const dummyWriteStream = fs.createWriteStream(dummyPath, { - encoding: firstEncoding - }); - - exampleReadStream.pipe(dummyWriteStream).on('finish', () => { - const assertWriteStream = new stream.Writable({ - write: function(chunk, enc, next) { - const expected = Buffer.from('xyz\n'); - expect(chunk).toEqual(expected); - next(); - } - }); - assertWriteStream.setDefaultEncoding(secondEncoding); - - fs.createReadStream(dummyPath, { - encoding: secondEncoding - }).pipe(assertWriteStream).on('finish', done); - }); -}); - -//<#END_FILE: test-fs-write-stream-encoding.js diff --git a/test/js/node/test/parallel/fs-write-stream-end.test.js b/test/js/node/test/parallel/fs-write-stream-end.test.js deleted file mode 100644 index a73488cb3b..0000000000 --- a/test/js/node/test/parallel/fs-write-stream-end.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-fs-write-stream-end.js -//#SHA1: a4194cfb1f416f5fddd5edc55b7d867db14a5320 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); -const path = require("path"); - -const tmpdir = path.join(__dirname, "tmp"); - -beforeAll(() => { - if (!fs.existsSync(tmpdir)) { - fs.mkdirSync(tmpdir, { recursive: true }); - } -}); - -afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test("end without data", done => { - const file = path.join(tmpdir, "write-end-test0.txt"); - const stream = fs.createWriteStream(file); - stream.end(); - stream.on("close", () => { - done(); - }); -}); - -test("end with data", done => { - const file = path.join(tmpdir, "write-end-test1.txt"); - const stream = fs.createWriteStream(file); - stream.end("a\n", "utf8"); - stream.on("close", () => { - const content = fs.readFileSync(file, "utf8"); - expect(content).toBe("a\n"); - done(); - }); -}); - -test("end triggers open and finish events", done => { - const file = path.join(tmpdir, "write-end-test2.txt"); - const stream = fs.createWriteStream(file); - stream.end(); - - let calledOpen = false; - stream.on("open", () => { - calledOpen = true; - }); - stream.on("finish", () => { - expect(calledOpen).toBe(true); - done(); - }); -}); - -//<#END_FILE: test-fs-write-stream-end.js diff --git a/test/js/node/test/parallel/fs-write-sync.test.js b/test/js/node/test/parallel/fs-write-sync.test.js deleted file mode 100644 index 5e277e75e5..0000000000 --- a/test/js/node/test/parallel/fs-write-sync.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-fs-write-sync.js -//#SHA1: 4ae5fa7550eefe258b9c1de798f4a4092e9d15d1 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const filename = path.join(os.tmpdir(), "write.txt"); - -beforeEach(() => { - try { - fs.unlinkSync(filename); - } catch (err) { - // Ignore errors if file doesn't exist - } -}); - -test("fs.writeSync with various parameter combinations", () => { - const parameters = [Buffer.from("bár"), 0, Buffer.byteLength("bár")]; - - // The first time fs.writeSync is called with all parameters provided. - // After that, each pop in the cycle removes the final parameter. So: - // - The 2nd time fs.writeSync with a buffer, without the length parameter. - // - The 3rd time fs.writeSync with a buffer, without the offset and length - // parameters. - while (parameters.length > 0) { - const fd = fs.openSync(filename, "w"); - - let written = fs.writeSync(fd, ""); - expect(written).toBe(0); - - fs.writeSync(fd, "foo"); - - written = fs.writeSync(fd, ...parameters); - expect(written).toBeGreaterThan(3); - fs.closeSync(fd); - - expect(fs.readFileSync(filename, "utf-8")).toBe("foobár"); - - parameters.pop(); - } -}); - -//<#END_FILE: test-fs-write-sync.js diff --git a/test/js/node/test/parallel/fs-writestream-open-write.test.js b/test/js/node/test/parallel/fs-writestream-open-write.test.js deleted file mode 100644 index c641c31ce8..0000000000 --- a/test/js/node/test/parallel/fs-writestream-open-write.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-fs-writestream-open-write.js -//#SHA1: a4cb8508ae1f366c94442a43312a817f00b68de6 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// Regression test for https://github.com/nodejs/node/issues/51993 - -let tmpdir; - -beforeEach(() => { - tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test-fs-writestream-open-write-")); -}); - -afterEach(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test("fs.createWriteStream opens and writes correctly", done => { - const file = path.join(tmpdir, "test-fs-writestream-open-write.txt"); - - const w = fs.createWriteStream(file); - - w.on("open", () => { - w.write("hello"); - - process.nextTick(() => { - w.write("world"); - w.end(); - }); - }); - - w.on("close", () => { - expect(fs.readFileSync(file, "utf8")).toBe("helloworld"); - fs.unlinkSync(file); - done(); - }); -}); - -//<#END_FILE: test-fs-writestream-open-write.js diff --git a/test/js/node/test/parallel/global-customevent.test.js b/test/js/node/test/parallel/global-customevent.test.js deleted file mode 100644 index 988e0cd447..0000000000 --- a/test/js/node/test/parallel/global-customevent.test.js +++ /dev/null @@ -1,19 +0,0 @@ -//#FILE: test-global-customevent.js -//#SHA1: 754c1b6babd0e73fa3206c9c9179ff3a034eba9b -//----------------- -"use strict"; - -// Global -test("CustomEvent is defined globally", () => { - expect(CustomEvent).toBeDefined(); -}); - -test("CustomEvent is the same as internal CustomEvent", () => { - // We can't use internal modules in Jest, so we'll skip this test - // and add a comment explaining why. - console.log("Skipping test for internal CustomEvent comparison"); - // The original test was: - // strictEqual(CustomEvent, internalCustomEvent); -}); - -//<#END_FILE: test-global-customevent.js diff --git a/test/js/node/test/parallel/global-domexception.test.js b/test/js/node/test/parallel/global-domexception.test.js deleted file mode 100644 index 2ebf63c2fd..0000000000 --- a/test/js/node/test/parallel/global-domexception.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-global-domexception.js -//#SHA1: 9a8d5eacea5ae98814fa6312b5f10089034c1ef4 -//----------------- -"use strict"; - -// This test checks the global availability and behavior of DOMException - -test("DOMException is a global function", () => { - expect(typeof DOMException).toBe("function"); -}); - -test("atob throws a DOMException for invalid input", () => { - expect(() => { - atob("我要抛错!"); - }).toThrow(DOMException); -}); - -//<#END_FILE: test-global-domexception.js diff --git a/test/js/node/test/parallel/global-encoder.test.js b/test/js/node/test/parallel/global-encoder.test.js deleted file mode 100644 index a9a928fb17..0000000000 --- a/test/js/node/test/parallel/global-encoder.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-global-encoder.js -//#SHA1: 7397937d3493488fc47e8ba6ba8fcb4f5bdd97fa -//----------------- -"use strict"; - -test("TextDecoder and TextEncoder are globally available", () => { - const util = require("util"); - - expect(TextDecoder).toBe(util.TextDecoder); - expect(TextEncoder).toBe(util.TextEncoder); -}); - -//<#END_FILE: test-global-encoder.js diff --git a/test/js/node/test/parallel/global-webcrypto.test.js b/test/js/node/test/parallel/global-webcrypto.test.js deleted file mode 100644 index a936c6b593..0000000000 --- a/test/js/node/test/parallel/global-webcrypto.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-global-webcrypto.js -//#SHA1: 3ae34178f201f6dfeb3ca5ec6e0914e6de63d64b -//----------------- -"use strict"; - -const crypto = require("crypto"); - -// Skip the test if crypto is not available -if (!crypto) { - test.skip("missing crypto", () => {}); -} else { - describe("Global WebCrypto", () => { - test("globalThis.crypto is crypto.webcrypto", () => { - expect(globalThis.crypto).toBe(crypto.webcrypto); - }); - - test("Crypto is the constructor of crypto.webcrypto", () => { - expect(Crypto).toBe(crypto.webcrypto.constructor); - }); - - test("SubtleCrypto is the constructor of crypto.webcrypto.subtle", () => { - expect(SubtleCrypto).toBe(crypto.webcrypto.subtle.constructor); - }); - }); -} - -//<#END_FILE: test-global-webcrypto.js diff --git a/test/js/node/test/parallel/heap-prof-exec-argv.test.js b/test/js/node/test/parallel/heap-prof-exec-argv.test.js deleted file mode 100644 index aa64858dbb..0000000000 --- a/test/js/node/test/parallel/heap-prof-exec-argv.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-heap-prof-exec-argv.js -//#SHA1: 77c3a447116b06f03f52fd56efe928699ba6d60d -//----------------- -"use strict"; - -// Tests --heap-prof generates a heap profile from worker -// when execArgv is set. - -const fixtures = require("../common/fixtures"); -const assert = require("assert"); -const { spawnSync } = require("child_process"); -const tmpdir = require("../common/tmpdir"); -const { getHeapProfiles, verifyFrames } = require("../common/prof"); - -// Skip the test if inspector is disabled -const isInspectorEnabled = process.execArgv.some(arg => arg.startsWith("--inspect")); -if (!isInspectorEnabled) { - test.skip("Inspector is disabled", () => {}); -} else { - test("--heap-prof generates a heap profile from worker when execArgv is set", () => { - tmpdir.refresh(); - const output = spawnSync(process.execPath, [fixtures.path("workload", "allocation-worker-argv.js")], { - cwd: tmpdir.path, - env: { - ...process.env, - HEAP_PROF_INTERVAL: "128", - }, - }); - - if (output.status !== 0) { - console.log(output.stderr.toString()); - } - - expect(output.status).toBe(0); - - const profiles = getHeapProfiles(tmpdir.path); - expect(profiles.length).toBe(1); - - verifyFrames(output, profiles[0], "runAllocation"); - }); -} - -//<#END_FILE: test-heap-prof-exec-argv.js diff --git a/test/js/node/test/parallel/http-abort-before-end.test.js b/test/js/node/test/parallel/http-abort-before-end.test.js deleted file mode 100644 index 0df59e1fcf..0000000000 --- a/test/js/node/test/parallel/http-abort-before-end.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-http-abort-before-end.js -//#SHA1: ccb82c66677f07f3ee815846261393edb8bfe5d4 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("HTTP request abort before end", async () => { - const server = http.createServer(jest.fn()); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const req = http.request({ - method: "GET", - host: "127.0.0.1", - port: server.address().port, - }); - - const abortPromise = new Promise(resolve => { - req.on("abort", resolve); - }); - - req.on("error", jest.fn()); - - req.abort(); - req.end(); - - await abortPromise; - - expect(server.listeners("request")[0]).not.toHaveBeenCalled(); - expect(req.listeners("error")[0]).not.toHaveBeenCalled(); - - await new Promise(resolve => { - server.close(resolve); - }); -}); - -//<#END_FILE: test-http-abort-before-end.js diff --git a/test/js/node/test/parallel/http-abort-queued.test.js b/test/js/node/test/parallel/http-abort-queued.test.js deleted file mode 100644 index d5c7fea669..0000000000 --- a/test/js/node/test/parallel/http-abort-queued.test.js +++ /dev/null @@ -1,102 +0,0 @@ -//#FILE: test-http-abort-queued.js -//#SHA1: e0fcd4a5eb0466a1e218147e8eb53714311a6f42 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -let complete; - -test("http abort queued request", async () => { - const server = http.createServer((req, res) => { - // We should not see the queued /thatotherone request within the server - // as it should be aborted before it is sent. - expect(req.url).toBe("/"); - - res.writeHead(200); - res.write("foo"); - - complete = - complete || - function () { - res.end(); - }; - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const agent = new http.Agent({ maxSockets: 1 }); - expect(Object.keys(agent.sockets)).toHaveLength(0); - - const options = { - hostname: "localhost", - port: server.address().port, - method: "GET", - path: "/", - agent: agent, - }; - - const req1 = http.request(options); - req1.on("response", res1 => { - expect(Object.keys(agent.sockets)).toHaveLength(1); - expect(Object.keys(agent.requests)).toHaveLength(0); - - const req2 = http.request({ - method: "GET", - host: "localhost", - port: server.address().port, - path: "/thatotherone", - agent: agent, - }); - expect(Object.keys(agent.sockets)).toHaveLength(1); - expect(Object.keys(agent.requests)).toHaveLength(1); - - // TODO(jasnell): This event does not appear to currently be triggered. - // is this handler actually required? - req2.on("error", err => { - // This is expected in response to our explicit abort call - expect(err.code).toBe("ECONNRESET"); - }); - - req2.end(); - req2.abort(); - - expect(Object.keys(agent.sockets)).toHaveLength(1); - expect(Object.keys(agent.requests)).toHaveLength(1); - - res1.on("data", chunk => complete()); - - res1.on("end", () => { - setTimeout(() => { - expect(Object.keys(agent.sockets)).toHaveLength(0); - expect(Object.keys(agent.requests)).toHaveLength(0); - - server.close(); - }, 100); - }); - }); - - req1.end(); -}); - -//<#END_FILE: test-http-abort-queued.js diff --git a/test/js/node/test/parallel/http-agent-false.test.js b/test/js/node/test/parallel/http-agent-false.test.js deleted file mode 100644 index c99f236785..0000000000 --- a/test/js/node/test/parallel/http-agent-false.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http-agent-false.js -//#SHA1: ba987050ea069a591615a69e341cf6f9c7298e5a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("http.request with agent: false and port: null", async () => { - // Sending `agent: false` when `port: null` is also passed in (i.e. the result - // of a `url.parse()` call with the default port used, 80 or 443), should not - // result in an assertion error... - const opts = { - host: "127.0.0.1", - port: null, - path: "/", - method: "GET", - agent: false, - }; - - // We just want an "error" (no local HTTP server on port 80) or "response" - // to happen (user happens ot have HTTP server running on port 80). - // As long as the process doesn't crash from a C++ assertion then we're good. - const req = http.request(opts); - - // Will be called by either the response event or error event, not both - const oneResponse = jest.fn(); - req.on("response", oneResponse); - req.on("error", oneResponse); - req.end(); - - // Wait for the request to complete - await new Promise(resolve => { - req.on("response", resolve); - req.on("error", resolve); - }); - - // Check that oneResponse was called exactly once - expect(oneResponse).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-http-agent-false.js diff --git a/test/js/node/test/parallel/http-agent-no-protocol.test.js b/test/js/node/test/parallel/http-agent-no-protocol.test.js deleted file mode 100644 index 99d0acc26f..0000000000 --- a/test/js/node/test/parallel/http-agent-no-protocol.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-http-agent-no-protocol.js -//#SHA1: f1b40623163271a500c87971bf996466e006130e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -test("http agent with no protocol", async () => { - const serverCallback = jest.fn((req, res) => { - res.end(); - }); - - const server = http.createServer(serverCallback); - - await new Promise(resolve => { - server.listen(0, "127.0.0.1", resolve); - }); - - const opts = url.parse(`http://127.0.0.1:${server.address().port}/`); - - // Remove the `protocol` field… the `http` module should fall back - // to "http:", as defined by the global, default `http.Agent` instance. - opts.agent = new http.Agent(); - opts.agent.protocol = null; - - const responseCallback = jest.fn(res => { - res.resume(); - server.close(); - }); - - await new Promise(resolve => { - http.get(opts, res => { - responseCallback(res); - resolve(); - }); - }); - - expect(serverCallback).toHaveBeenCalledTimes(1); - expect(responseCallback).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-http-agent-no-protocol.js diff --git a/test/js/node/test/parallel/http-agent-null.test.js b/test/js/node/test/parallel/http-agent-null.test.js deleted file mode 100644 index c44c03f788..0000000000 --- a/test/js/node/test/parallel/http-agent-null.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-http-agent-null.js -//#SHA1: 65fb22d32bae2a7eecc4242b5b2d2d693849641c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("http.get with null agent", async () => { - const server = http.createServer((req, res) => { - res.end(); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const options = { - agent: null, - port: server.address().port, - }; - - const responsePromise = new Promise(resolve => { - http.get(options, res => { - res.resume(); - resolve(res); - }); - }); - - await expect(responsePromise).resolves.toBeDefined(); - - await new Promise(resolve => { - server.close(resolve); - }); -}); - -//<#END_FILE: test-http-agent-null.js diff --git a/test/js/node/test/parallel/http-agent-uninitialized-with-handle.test.js b/test/js/node/test/parallel/http-agent-uninitialized-with-handle.test.js deleted file mode 100644 index b78aacf5b6..0000000000 --- a/test/js/node/test/parallel/http-agent-uninitialized-with-handle.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-http-agent-uninitialized-with-handle.js -//#SHA1: 828942acbc68f8fd92425ecdf0e754ab13b4baff -//----------------- -"use strict"; - -const http = require("http"); -const net = require("net"); - -test("http agent with uninitialized socket handle", done => { - const agent = new http.Agent({ - keepAlive: true, - }); - const socket = new net.Socket(); - // If _handle exists then internals assume a couple methods exist. - socket._handle = { - ref() {}, - readStart() {}, - }; - - const server = http.createServer((req, res) => { - res.end(); - }); - - server.listen(0, () => { - const req = new http.ClientRequest(`http://localhost:${server.address().port}/`); - - // Manually add the socket without a _handle. - agent.freeSockets[agent.getName(req)] = [socket]; - // Now force the agent to use the socket and check that _handle exists before - // calling asyncReset(). - agent.addRequest(req, {}); - req.on("response", () => { - server.close(); - done(); - }); - req.end(); - }); - - expect(server).toHaveProperty("listen"); - expect(server).toHaveProperty("close"); -}); - -//<#END_FILE: test-http-agent-uninitialized-with-handle.js diff --git a/test/js/node/test/parallel/http-agent-uninitialized.test.js b/test/js/node/test/parallel/http-agent-uninitialized.test.js deleted file mode 100644 index 43e447a063..0000000000 --- a/test/js/node/test/parallel/http-agent-uninitialized.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-http-agent-uninitialized.js -//#SHA1: 00034f4963a5620af8a58e68c262c92ea9ec982b -//----------------- -"use strict"; - -const http = require("http"); -const net = require("net"); - -test("http agent handles uninitialized socket", done => { - const agent = new http.Agent({ - keepAlive: true, - }); - const socket = new net.Socket(); - - const server = http - .createServer((req, res) => { - res.end(); - }) - .listen(0, () => { - const req = new http.ClientRequest(`http://localhost:${server.address().port}/`); - - // Manually add the socket without a _handle. - agent.freeSockets[agent.getName(req)] = [socket]; - // Now force the agent to use the socket and check that _handle exists before - // calling asyncReset(). - agent.addRequest(req, {}); - req.on("response", () => { - server.close(); - done(); - }); - req.end(); - }); - - expect(server).toBeDefined(); -}); - -//<#END_FILE: test-http-agent-uninitialized.js diff --git a/test/js/node/test/parallel/http-agent.test.js b/test/js/node/test/parallel/http-agent.test.js deleted file mode 100644 index 900ad21d17..0000000000 --- a/test/js/node/test/parallel/http-agent.test.js +++ /dev/null @@ -1,97 +0,0 @@ -//#FILE: test-http-agent.js -//#SHA1: c5bb5b1b47100659ac17ae6c4ba084c6974ddaa7 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const N = 4; -const M = 4; - -let server; - -beforeEach(() => { - server = http.Server((req, res) => { - res.writeHead(200); - res.end("hello world\n"); - }); -}); - -afterEach(() => { - server.close(); -}); - -function makeRequests(outCount, inCount, shouldFail) { - return new Promise(resolve => { - const totalRequests = outCount * inCount; - let completedRequests = 0; - - const onRequest = jest.fn(res => { - completedRequests++; - if (completedRequests === totalRequests) { - resolve(); - } - - if (!shouldFail) { - res.resume(); - } - }); - - server.listen(0, () => { - const port = server.address().port; - for (let i = 0; i < outCount; i++) { - setTimeout(() => { - for (let j = 0; j < inCount; j++) { - const req = http.get({ port: port, path: "/" }, onRequest); - if (shouldFail) { - req.on("error", onRequest); - } else { - req.on("error", e => { - throw e; - }); - } - } - }, i); - } - }); - }); -} - -test("makeRequests successful", async () => { - await makeRequests(N, M); - expect(server.listenerCount("request")).toBe(1); -}); - -test("makeRequests with failing requests", async () => { - const originalCreateConnection = http.Agent.prototype.createConnection; - - http.Agent.prototype.createConnection = function createConnection(_, cb) { - process.nextTick(cb, new Error("nothing")); - }; - - await makeRequests(N, M, true); - - http.Agent.prototype.createConnection = originalCreateConnection; -}); - -//<#END_FILE: test-http-agent.js diff --git a/test/js/node/test/parallel/http-bind-twice.test.js b/test/js/node/test/parallel/http-bind-twice.test.js deleted file mode 100644 index 99917ffc27..0000000000 --- a/test/js/node/test/parallel/http-bind-twice.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-http-bind-twice.js -//#SHA1: 71319f0a5445d1ea7644e952ec4048d8996be1bc -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("HTTP server bind twice", async () => { - const server1 = http.createServer(jest.fn()); - const server1ListenPromise = new Promise(resolve => { - server1.listen(0, "127.0.0.1", resolve); - }); - - await server1ListenPromise; - - const server2 = http.createServer(jest.fn()); - const server2ErrorPromise = new Promise(resolve => { - server2.on("error", resolve); - }); - - server2.listen(server1.address().port, "127.0.0.1"); - - const error = await server2ErrorPromise; - - expect(error).toEqual( - expect.objectContaining({ - code: "EADDRINUSE", - message: expect.any(String), - }), - ); - - await new Promise(resolve => server1.close(resolve)); -}); - -//<#END_FILE: test-http-bind-twice.js diff --git a/test/js/node/test/parallel/http-chunked-smuggling.test.js b/test/js/node/test/parallel/http-chunked-smuggling.test.js deleted file mode 100644 index e1ab305b10..0000000000 --- a/test/js/node/test/parallel/http-chunked-smuggling.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http-chunked-smuggling.js -//#SHA1: c146d9dc37a522ac07d943b4c40b3301923659fa -//----------------- -"use strict"; - -const http = require("http"); -const net = require("net"); - -// Verify that invalid chunk extensions cannot be used to perform HTTP request -// smuggling attacks. - -describe("HTTP Chunked Smuggling", () => { - let server; - let serverPort; - - beforeAll(done => { - server = http.createServer((request, response) => { - expect(request.url).not.toBe("/admin"); - response.end("hello world"); - }); - - server.listen(0, () => { - serverPort = server.address().port; - done(); - }); - }); - - afterAll(done => { - server.close(done); - }); - - test("invalid chunk extensions", done => { - const sock = net.connect(serverPort); - - sock.write( - "" + - "GET / HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Transfer-Encoding: chunked\r\n" + - "\r\n" + - "2;\n" + - "xx\r\n" + - "4c\r\n" + - "0\r\n" + - "\r\n" + - "GET /admin HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Transfer-Encoding: chunked\r\n" + - "\r\n" + - "0\r\n" + - "\r\n", - ); - - sock.resume(); - sock.on("end", () => { - done(); - }); - }); -}); - -//<#END_FILE: test-http-chunked-smuggling.js diff --git a/test/js/node/test/parallel/http-client-abort2.test.js b/test/js/node/test/parallel/http-client-abort2.test.js deleted file mode 100644 index 416be11173..0000000000 --- a/test/js/node/test/parallel/http-client-abort2.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-http-client-abort2.js -//#SHA1: 9accf5214e90cab96d06a59931e65718616b85f3 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("http client abort", async () => { - const serverHandler = jest.fn((req, res) => { - res.end("Hello"); - }); - - const server = http.createServer(serverHandler); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const options = { port: server.address().port }; - - const req = http.get(options, res => { - res.on("data", data => { - req.abort(); - server.close(); - }); - }); - - await new Promise(resolve => { - server.on("close", resolve); - }); - - expect(serverHandler).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-http-client-abort2.js diff --git a/test/js/node/test/parallel/http-client-defaults.test.js b/test/js/node/test/parallel/http-client-defaults.test.js deleted file mode 100644 index f527d6ade8..0000000000 --- a/test/js/node/test/parallel/http-client-defaults.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-http-client-defaults.js -//#SHA1: 7209a7752de52cc378c3b29eda88c82d71e6839d -//----------------- -"use strict"; - -const http = require("http"); - -describe("ClientRequest defaults", () => { - test("default path and method", () => { - const req = new http.ClientRequest({ createConnection: () => {} }); - expect(req.path).toBe("/"); - expect(req.method).toBe("GET"); - }); - - test("empty method defaults to GET", () => { - const req = new http.ClientRequest({ method: "", createConnection: () => {} }); - expect(req.path).toBe("/"); - expect(req.method).toBe("GET"); - }); - - test("empty path defaults to /", () => { - const req = new http.ClientRequest({ path: "", createConnection: () => {} }); - expect(req.path).toBe("/"); - expect(req.method).toBe("GET"); - }); -}); - -//<#END_FILE: test-http-client-defaults.js diff --git a/test/js/node/test/parallel/http-client-encoding.test.js b/test/js/node/test/parallel/http-client-encoding.test.js deleted file mode 100644 index b0455d81bb..0000000000 --- a/test/js/node/test/parallel/http-client-encoding.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-http-client-encoding.js -//#SHA1: a3e1a0cc1bf9352602068f323d90071d3c2d5f7d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("HTTP client encoding", async () => { - const server = http.createServer((req, res) => { - res.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const port = server.address().port; - const req = http.request( - { - port: port, - encoding: "utf8", - }, - res => { - let data = ""; - res.on("data", chunk => (data += chunk)); - res.on("end", () => { - expect(data).toBe("ok"); - resolve(); - }); - }, - ); - req.end(); - }); - }); -}); - -//<#END_FILE: test-http-client-encoding.js diff --git a/test/js/node/test/parallel/http-client-get-url.test.js b/test/js/node/test/parallel/http-client-get-url.test.js deleted file mode 100644 index e13f87f7d4..0000000000 --- a/test/js/node/test/parallel/http-client-get-url.test.js +++ /dev/null @@ -1,78 +0,0 @@ -//#FILE: test-http-client-get-url.js -//#SHA1: 0329da4beb5be5da0ab6652b246dd912935e56af -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); -const testPath = "/foo?bar"; - -let server; -let serverAddress; - -beforeAll(async () => { - server = http.createServer((req, res) => { - expect(req.method).toBe("GET"); - expect(req.url).toBe(testPath); - res.writeHead(200, { "Content-Type": "text/plain" }); - res.write("hello\n"); - res.end(); - }); - - await new Promise(resolve => { - server.listen(0, "127.0.0.1", () => { - serverAddress = `http://127.0.0.1:${server.address().port}${testPath}`; - resolve(); - }); - }); -}); - -afterAll(() => { - server.close(); -}); - -test("http.get with string URL", async () => { - await new Promise(resolve => { - http.get(serverAddress, () => { - resolve(); - }); - }); -}); - -test("http.get with parsed URL", async () => { - await new Promise(resolve => { - http.get(url.parse(serverAddress), () => { - resolve(); - }); - }); -}); - -test("http.get with URL object", async () => { - await new Promise(resolve => { - http.get(new URL(serverAddress), () => { - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-client-get-url.js diff --git a/test/js/node/test/parallel/http-client-input-function.test.js b/test/js/node/test/parallel/http-client-input-function.test.js deleted file mode 100644 index e34ee9a0d5..0000000000 --- a/test/js/node/test/parallel/http-client-input-function.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-http-client-input-function.js -//#SHA1: 2ca0147b992331ea69803031f33076c685bce264 -//----------------- -"use strict"; - -const http = require("http"); - -test("http.ClientRequest with server response", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200); - res.end("hello world"); - }); - - await new Promise(resolve => { - server.listen(0, "127.0.0.1", resolve); - }); - - const serverAddress = server.address(); - - const responsePromise = new Promise(resolve => { - const req = new http.ClientRequest(serverAddress, response => { - let body = ""; - response.setEncoding("utf8"); - response.on("data", chunk => { - body += chunk; - }); - - response.on("end", () => { - resolve(body); - }); - }); - - req.end(); - }); - - const body = await responsePromise; - expect(body).toBe("hello world"); - - await new Promise(resolve => server.close(resolve)); -}); - -//<#END_FILE: test-http-client-input-function.js diff --git a/test/js/node/test/parallel/http-client-keep-alive-release-before-finish.test.js b/test/js/node/test/parallel/http-client-keep-alive-release-before-finish.test.js deleted file mode 100644 index 5e7ca84113..0000000000 --- a/test/js/node/test/parallel/http-client-keep-alive-release-before-finish.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-http-client-keep-alive-release-before-finish.js -//#SHA1: 198cd4a6c28c8a7dda45f003305e8fa80f05469d -//----------------- -"use strict"; - -const http = require("http"); - -test("HTTP client keep-alive release before finish", done => { - const server = http.createServer((req, res) => { - res.end(); - }); - - server.listen(0, () => { - const agent = new http.Agent({ - maxSockets: 1, - keepAlive: true, - }); - - const port = server.address().port; - - const post = http.request( - { - agent, - method: "POST", - port, - }, - res => { - res.resume(); - }, - ); - - // What happens here is that the server `end`s the response before we send - // `something`, and the client thought that this is a green light for sending - // next GET request - post.write(Buffer.alloc(16 * 1024, "X")); - setTimeout(() => { - post.end("something"); - }, 100); - - http - .request( - { - agent, - method: "GET", - port, - }, - res => { - server.close(); - res.connection.end(); - done(); - }, - ) - .end(); - }); -}); - -//<#END_FILE: test-http-client-keep-alive-release-before-finish.js diff --git a/test/js/node/test/parallel/http-client-race-2.test.js b/test/js/node/test/parallel/http-client-race-2.test.js deleted file mode 100644 index bc4c83b4f4..0000000000 --- a/test/js/node/test/parallel/http-client-race-2.test.js +++ /dev/null @@ -1,136 +0,0 @@ -//#FILE: test-http-client-race-2.js -//#SHA1: f1e2a4ecdd401cb9fcf615496d1376ce0a94ad73 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -// -// Slight variation on test-http-client-race to test for another race -// condition involving the parsers FreeList used internally by http.Client. -// - -const body1_s = "1111111111111111"; -const body2_s = "22222"; -const body3_s = "3333333333333333333"; - -let server; -let port; - -beforeAll(() => { - return new Promise(resolve => { - server = http.createServer(function (req, res) { - const pathname = url.parse(req.url).pathname; - - let body; - switch (pathname) { - case "/1": - body = body1_s; - break; - case "/2": - body = body2_s; - break; - default: - body = body3_s; - } - - res.writeHead(200, { - "Content-Type": "text/plain", - "Content-Length": body.length, - }); - res.end(body); - }); - - server.listen(0, () => { - port = server.address().port; - resolve(); - }); - }); -}); - -afterAll(() => { - return new Promise(resolve => { - server.close(resolve); - }); -}); - -test("HTTP client race condition", async () => { - let body1 = ""; - let body2 = ""; - let body3 = ""; - - // Client #1 is assigned Parser #1 - const req1 = http.get({ port, path: "/1" }); - await new Promise(resolve => { - req1.on("response", function (res1) { - res1.setEncoding("utf8"); - - res1.on("data", function (chunk) { - body1 += chunk; - }); - - res1.on("end", function () { - // Delay execution a little to allow the 'close' event to be processed - // (required to trigger this bug!) - setTimeout(resolve, 500); - }); - }); - }); - - // The bug would introduce itself here: Client #2 would be allocated the - // parser that previously belonged to Client #1. But we're not finished - // with Client #1 yet! - // - // At this point, the bug would manifest itself and crash because the - // internal state of the parser was no longer valid for use by Client #1 - const req2 = http.get({ port, path: "/2" }); - await new Promise(resolve => { - req2.on("response", function (res2) { - res2.setEncoding("utf8"); - res2.on("data", function (chunk) { - body2 += chunk; - }); - res2.on("end", resolve); - }); - }); - - // Just to be really sure we've covered all our bases, execute a - // request using client2. - const req3 = http.get({ port, path: "/3" }); - await new Promise(resolve => { - req3.on("response", function (res3) { - res3.setEncoding("utf8"); - res3.on("data", function (chunk) { - body3 += chunk; - }); - res3.on("end", resolve); - }); - }); - - expect(body1).toBe(body1_s); - expect(body2).toBe(body2_s); - expect(body3).toBe(body3_s); -}); - -//<#END_FILE: test-http-client-race-2.js diff --git a/test/js/node/test/parallel/http-client-race.test.js b/test/js/node/test/parallel/http-client-race.test.js deleted file mode 100644 index ab780f28d8..0000000000 --- a/test/js/node/test/parallel/http-client-race.test.js +++ /dev/null @@ -1,72 +0,0 @@ -//#FILE: test-http-client-race.js -//#SHA1: 0ad515567d91a194670069b476e166d398543cc0 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -const body1_s = "1111111111111111"; -const body2_s = "22222"; - -test("http client race condition", async () => { - const server = http.createServer((req, res) => { - const body = url.parse(req.url).pathname === "/1" ? body1_s : body2_s; - res.writeHead(200, { - "Content-Type": "text/plain", - "Content-Length": body.length, - }); - res.end(body); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - let body1 = ""; - let body2 = ""; - - const makeRequest = path => { - return new Promise((resolve, reject) => { - const req = http.request({ port: server.address().port, path }); - req.end(); - req.on("response", res => { - res.setEncoding("utf8"); - let body = ""; - res.on("data", chunk => { - body += chunk; - }); - res.on("end", () => resolve(body)); - }); - req.on("error", reject); - }); - }; - - body1 = await makeRequest("/1"); - body2 = await makeRequest("/2"); - - await new Promise(resolve => server.close(resolve)); - - expect(body1).toBe(body1_s); - expect(body2).toBe(body2_s); -}); - -//<#END_FILE: test-http-client-race.js diff --git a/test/js/node/test/parallel/http-client-read-in-error.test.js b/test/js/node/test/parallel/http-client-read-in-error.test.js deleted file mode 100644 index 2dd33c52de..0000000000 --- a/test/js/node/test/parallel/http-client-read-in-error.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-http-client-read-in-error.js -//#SHA1: a7bd75283f46ff8f1246208c72bf0773a27f0fb0 -//----------------- -"use strict"; - -const net = require("net"); -const http = require("http"); - -class Agent extends http.Agent { - createConnection() { - const socket = new net.Socket(); - - socket.on("error", function () { - socket.push("HTTP/1.1 200\r\n\r\n"); - }); - - let onNewListener; - socket.on( - "newListener", - (onNewListener = name => { - if (name !== "error") return; - socket.removeListener("newListener", onNewListener); - - // Let other listeners to be set up too - process.nextTick(() => { - this.breakSocket(socket); - }); - }), - ); - - return socket; - } - - breakSocket(socket) { - socket.emit("error", new Error("Intentional error")); - } -} - -test("http client read in error", () => { - const agent = new Agent(); - const dataHandler = jest.fn(); - - const request = http.request({ agent }); - - request.once("error", function () { - console.log("ignore"); - this.on("data", dataHandler); - }); - - return new Promise(resolve => { - // Give some time for the 'data' event to potentially be called - setTimeout(() => { - expect(dataHandler).not.toHaveBeenCalled(); - resolve(); - }, 100); - }); -}); - -//<#END_FILE: test-http-client-read-in-error.js diff --git a/test/js/node/test/parallel/http-client-res-destroyed.test.js b/test/js/node/test/parallel/http-client-res-destroyed.test.js deleted file mode 100644 index 7b7662ea52..0000000000 --- a/test/js/node/test/parallel/http-client-res-destroyed.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-http-client-res-destroyed.js -//#SHA1: 9a7e890355cecb3eb88b6963b0c37df3f01bc8d7 -//----------------- -"use strict"; - -const http = require("http"); - -describe("HTTP Client Response Destroyed", () => { - test("Response destruction after manually calling destroy()", async () => { - const server = http.createServer((req, res) => { - res.end("asd"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get( - { - port: server.address().port, - }, - res => { - expect(res.destroyed).toBe(false); - res.destroy(); - expect(res.destroyed).toBe(true); - res.on("close", () => { - server.close(resolve); - }); - }, - ); - }); - }); - }); - - test("Response destruction after end of response", async () => { - const server = http.createServer((req, res) => { - res.end("asd"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get( - { - port: server.address().port, - }, - res => { - expect(res.destroyed).toBe(false); - res - .on("close", () => { - expect(res.destroyed).toBe(true); - server.close(resolve); - }) - .resume(); - }, - ); - }); - }); - }); -}); - -//<#END_FILE: test-http-client-res-destroyed.js diff --git a/test/js/node/test/parallel/http-client-timeout-connect-listener.test.js b/test/js/node/test/parallel/http-client-timeout-connect-listener.test.js deleted file mode 100644 index a70f33512b..0000000000 --- a/test/js/node/test/parallel/http-client-timeout-connect-listener.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-http-client-timeout-connect-listener.js -//#SHA1: 4311732db4ce9958ec0ed01be68786e522ed6ca8 -//----------------- -"use strict"; - -// This test ensures that `ClientRequest.prototype.setTimeout()` does -// not add a listener for the `'connect'` event to the socket if the -// socket is already connected. - -const http = require("http"); - -// Maximum allowed value for timeouts. -const timeout = 2 ** 31 - 1; - -let server; -let agent; - -beforeAll(() => { - return new Promise(resolve => { - server = http.createServer((req, res) => { - res.end(); - }); - - server.listen(0, () => { - agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); - resolve(); - }); - }); -}); - -afterAll(() => { - return new Promise(resolve => { - agent.destroy(); - server.close(resolve); - }); -}); - -function doRequest(options) { - return new Promise(resolve => { - const req = http.get(options, res => { - res.on("end", resolve); - res.resume(); - }); - - req.setTimeout(timeout); - return req; - }); -} - -test("ClientRequest.prototype.setTimeout() does not add connect listener to connected socket", async () => { - const options = { port: server.address().port, agent: agent }; - - await doRequest(options); - - const req = http.get(options); - req.setTimeout(timeout); - - await new Promise(resolve => { - req.on("socket", socket => { - expect(socket.listenerCount("connect")).toBe(0); - resolve(); - }); - }); - - await new Promise(resolve => { - req.on("response", res => { - res.on("end", resolve); - res.resume(); - }); - }); -}); - -//<#END_FILE: test-http-client-timeout-connect-listener.js diff --git a/test/js/node/test/parallel/http-client-timeout-event.test.js b/test/js/node/test/parallel/http-client-timeout-event.test.js deleted file mode 100644 index bfff317e47..0000000000 --- a/test/js/node/test/parallel/http-client-timeout-event.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-http-client-timeout-event.js -//#SHA1: b4aeb9d5d97b5ffa46c8c281fbc04d052857b08f -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const options = { - method: "GET", - port: undefined, - host: "127.0.0.1", - path: "/", -}; - -test("http client timeout event", async () => { - const server = http.createServer(); - - await new Promise(resolve => { - server.listen(0, options.host, () => { - options.port = server.address().port; - const req = http.request(options); - - req.on("error", () => { - // This space is intentionally left blank - }); - - req.on("close", () => { - expect(req.destroyed).toBe(true); - server.close(); - resolve(); - }); - - req.setTimeout(1); - req.on("timeout", () => { - req.end(() => { - setTimeout(() => { - req.destroy(); - }, 100); - }); - }); - }); - }); -}); - -//<#END_FILE: test-http-client-timeout-event.js diff --git a/test/js/node/test/parallel/http-client-timeout.test.js b/test/js/node/test/parallel/http-client-timeout.test.js deleted file mode 100644 index 557b469490..0000000000 --- a/test/js/node/test/parallel/http-client-timeout.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-http-client-timeout.js -//#SHA1: f99a3189acb9c566e378f9fa48c66dd503034d0d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const options = { - method: "GET", - port: undefined, - host: "127.0.0.1", - path: "/", -}; - -test("http client timeout", done => { - const server = http.createServer((req, res) => { - // This space intentionally left blank - }); - - server.listen(0, options.host, () => { - options.port = server.address().port; - const req = http.request(options, res => { - // This space intentionally left blank - }); - req.on("close", () => { - expect(req.destroyed).toBe(true); - server.close(); - done(); - }); - function destroy() { - req.destroy(); - } - const s = req.setTimeout(1, destroy); - expect(s).toBeInstanceOf(http.ClientRequest); - req.on("error", destroy); - req.end(); - }); -}); - -//<#END_FILE: test-http-client-timeout.js diff --git a/test/js/node/test/parallel/http-client-upload-buf.test.js b/test/js/node/test/parallel/http-client-upload-buf.test.js deleted file mode 100644 index 9891a442dc..0000000000 --- a/test/js/node/test/parallel/http-client-upload-buf.test.js +++ /dev/null @@ -1,77 +0,0 @@ -//#FILE: test-http-client-upload-buf.js -//#SHA1: bbfd7c52e710f53683f5f9a4578f34e451db4eb0 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const assert = require("assert"); -const http = require("http"); - -const N = 1024; - -test("HTTP client upload buffer", async () => { - const server = http.createServer((req, res) => { - expect(req.method).toBe("POST"); - let bytesReceived = 0; - - req.on("data", chunk => { - bytesReceived += chunk.length; - }); - - req.on("end", () => { - expect(bytesReceived).toBe(N); - console.log("request complete from server"); - res.writeHead(200, { "Content-Type": "text/plain" }); - res.write("hello\n"); - res.end(); - }); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const { port } = server.address(); - - const responsePromise = new Promise(resolve => { - const req = http.request( - { - port, - method: "POST", - path: "/", - }, - res => { - res.setEncoding("utf8"); - res.on("data", chunk => { - console.log(chunk); - }); - res.on("end", resolve); - }, - ); - - req.write(Buffer.allocUnsafe(N)); - req.end(); - }); - - await responsePromise; - await new Promise(resolve => server.close(resolve)); -}); - -//<#END_FILE: test-http-client-upload-buf.js diff --git a/test/js/node/test/parallel/http-client-upload.test.js b/test/js/node/test/parallel/http-client-upload.test.js deleted file mode 100644 index 6adc480232..0000000000 --- a/test/js/node/test/parallel/http-client-upload.test.js +++ /dev/null @@ -1,84 +0,0 @@ -//#FILE: test-http-client-upload.js -//#SHA1: 328a7e9989cc28daa5996c83fe9e3cfcb0893e01 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("HTTP client upload", async () => { - const serverHandler = jest.fn((req, res) => { - expect(req.method).toBe("POST"); - req.setEncoding("utf8"); - - let sent_body = ""; - - req.on("data", chunk => { - console.log(`server got: ${JSON.stringify(chunk)}`); - sent_body += chunk; - }); - - req.on("end", () => { - expect(sent_body).toBe("1\n2\n3\n"); - console.log("request complete from server"); - res.writeHead(200, { "Content-Type": "text/plain" }); - res.write("hello\n"); - res.end(); - }); - }); - - const server = http.createServer(serverHandler); - await new Promise(resolve => server.listen(0, resolve)); - - const { port } = server.address(); - - const clientHandler = jest.fn(res => { - res.setEncoding("utf8"); - res.on("data", chunk => { - console.log(chunk); - }); - res.on("end", () => { - server.close(); - }); - }); - - const req = http.request( - { - port, - method: "POST", - path: "/", - }, - clientHandler, - ); - - req.write("1\n"); - req.write("2\n"); - req.write("3\n"); - req.end(); - - await new Promise(resolve => server.on("close", resolve)); - - expect(serverHandler).toHaveBeenCalledTimes(1); - expect(clientHandler).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-http-client-upload.js diff --git a/test/js/node/test/parallel/http-contentlength0.test.js b/test/js/node/test/parallel/http-contentlength0.test.js deleted file mode 100644 index 4a6a7fec72..0000000000 --- a/test/js/node/test/parallel/http-contentlength0.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-http-contentLength0.js -//#SHA1: d85b0cc3dcfcff522ffbeddacf89111897b80c02 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -// Simple test of Node's HTTP Client choking on a response -// with a 'Content-Length: 0 ' response header. -// I.E. a space character after the 'Content-Length' throws an `error` event. - -test("HTTP Client handles Content-Length: 0 with space", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200, { "Content-Length": "0 " }); - res.end(); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const { port } = server.address(); - - await new Promise(resolve => { - const request = http.request({ port }, response => { - expect(response.statusCode).toBe(200); - server.close(); - response.resume(); - resolve(); - }); - - request.end(); - }); -}); - -//<#END_FILE: test-http-contentLength0.js diff --git a/test/js/node/test/parallel/http-date-header.test.js b/test/js/node/test/parallel/http-date-header.test.js deleted file mode 100644 index b47a7163d9..0000000000 --- a/test/js/node/test/parallel/http-date-header.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-http-date-header.js -//#SHA1: e4d2a00dad7c6483d9ed328731bb04f5f431afb4 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const testResBody = "other stuff!\n"; - -test("HTTP Date header", async () => { - const server = http.createServer((req, res) => { - expect(req.headers).not.toHaveProperty("date"); - res.writeHead(200, { - "Content-Type": "text/plain", - }); - res.end(testResBody); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const { port } = server.address(); - - const options = { - port, - path: "/", - method: "GET", - }; - - const responsePromise = new Promise((resolve, reject) => { - const req = http.request(options, res => { - expect(res.headers).toHaveProperty("date"); - res.resume(); - res.on("end", resolve); - }); - req.on("error", reject); - req.end(); - }); - - await responsePromise; - await new Promise(resolve => server.close(resolve)); -}); - -//<#END_FILE: test-http-date-header.js diff --git a/test/js/node/test/parallel/http-decoded-auth.test.js b/test/js/node/test/parallel/http-decoded-auth.test.js deleted file mode 100644 index ed117d1954..0000000000 --- a/test/js/node/test/parallel/http-decoded-auth.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-http-decoded-auth.js -//#SHA1: 70ba85653c7479ce80cf528a07aa85f598f85ef8 -//----------------- -"use strict"; - -const http = require("http"); - -const testCases = [ - { - username: 'test@test"', - password: "123456^", - expected: "dGVzdEB0ZXN0IjoxMjM0NTZe", - }, - { - username: "test%40test", - password: "123456", - expected: "dGVzdEB0ZXN0OjEyMzQ1Ng==", - }, - { - username: "not%3Agood", - password: "god", - expected: "bm90Omdvb2Q6Z29k", - }, - { - username: "not%22good", - password: "g%5Eod", - expected: "bm90Imdvb2Q6Z15vZA==", - }, - { - username: "test1234::::", - password: "mypass", - expected: "dGVzdDEyMzQ6Ojo6Om15cGFzcw==", - }, -]; - -testCases.forEach((testCase, index) => { - test(`HTTP decoded auth - case ${index + 1}`, async () => { - const server = http.createServer((request, response) => { - // The correct authorization header is be passed - expect(request.headers.authorization).toBe(`Basic ${testCase.expected}`); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - // make the request - const url = new URL(`http://${testCase.username}:${testCase.password}@localhost:${server.address().port}`); - http.request(url).end(); - resolve(); - }); - }); - }); -}); - -//<#END_FILE: test-http-decoded-auth.js diff --git a/test/js/node/test/parallel/http-default-encoding.test.js b/test/js/node/test/parallel/http-default-encoding.test.js deleted file mode 100644 index 30fc083b29..0000000000 --- a/test/js/node/test/parallel/http-default-encoding.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-http-default-encoding.js -//#SHA1: f5dfdba00ec21efec894e5edf97583c77334a2c3 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const expected = "This is a unicode text: سلام"; - -test("HTTP server with default encoding", async () => { - let result = ""; - - const server = http.Server((req, res) => { - req.setEncoding("utf8"); - req - .on("data", chunk => { - result += chunk; - }) - .on("end", () => { - res.writeHead(200); - res.end("hello world\n"); - server.close(); - }); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const req = http.request( - { - port: server.address().port, - path: "/", - method: "POST", - }, - res => { - expect(res.statusCode).toBe(200); - res.resume(); - resolve(); - }, - ); - - req.on("error", e => { - console.log(e.message); - process.exit(1); - }); - - req.end(expected); - }); - }); - - expect(result).toBe(expected); -}); - -//<#END_FILE: test-http-default-encoding.js diff --git a/test/js/node/test/parallel/http-eof-on-connect.test.js b/test/js/node/test/parallel/http-eof-on-connect.test.js deleted file mode 100644 index 1161c1f40c..0000000000 --- a/test/js/node/test/parallel/http-eof-on-connect.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-http-eof-on-connect.js -//#SHA1: c243d7ad215d84d88b20dfeea40976155f00a2bb -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const net = require("net"); -const http = require("http"); - -// This is a regression test for https://github.com/joyent/node/issues/44 -// It is separate from test-http-malformed-request.js because it is only -// reproducible on the first packet on the first connection to a server. - -test("EOF on connect", async () => { - const server = http.createServer(jest.fn()); - server.listen(0); - - await new Promise(resolve => { - server.on("listening", () => { - const client = net.createConnection(server.address().port, "127.0.0.1"); - - client.on("connect", () => { - client.destroy(); - }); - - client.on("close", () => { - server.close(resolve); - }); - }); - }); - - expect(server.listeners("request")[0]).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-http-eof-on-connect.js diff --git a/test/js/node/test/parallel/http-extra-response.test.js b/test/js/node/test/parallel/http-extra-response.test.js deleted file mode 100644 index b3e73a341a..0000000000 --- a/test/js/node/test/parallel/http-extra-response.test.js +++ /dev/null @@ -1,98 +0,0 @@ -//#FILE: test-http-extra-response.js -//#SHA1: 0d2dfa2459e54fa9f1b90a609c328afd478f2793 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const net = require("net"); - -// If an HTTP server is broken and sends data after the end of the response, -// node should ignore it and drop the connection. -// Demos this bug: https://github.com/joyent/node/issues/680 - -const body = "hello world\r\n"; -const fullResponse = - "HTTP/1.1 500 Internal Server Error\r\n" + - `Content-Length: ${body.length}\r\n` + - "Content-Type: text/plain\r\n" + - "Date: Fri + 18 Feb 2011 06:22:45 GMT\r\n" + - "Host: 10.20.149.2\r\n" + - "Access-Control-Allow-Credentials: true\r\n" + - "Server: badly broken/0.1 (OS NAME)\r\n" + - "\r\n" + - body; - -test("HTTP server sending data after response end", async () => { - const server = net.createServer(socket => { - let postBody = ""; - - socket.setEncoding("utf8"); - - socket.on("data", chunk => { - postBody += chunk; - - if (postBody.includes("\r\n")) { - socket.write(fullResponse); - socket.end(fullResponse); - } - }); - - socket.on("error", err => { - expect(err.code).toBe("ECONNRESET"); - }); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const { port } = server.address(); - - const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); - - await new Promise(resolve => { - http.get({ port }, res => { - let buffer = ""; - console.log(`Got res code: ${res.statusCode}`); - - res.setEncoding("utf8"); - res.on("data", chunk => { - buffer += chunk; - }); - - res.on("end", () => { - console.log(`Response ended, read ${buffer.length} bytes`); - expect(buffer).toBe(body); - resolve(); - }); - }); - }); - - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Got res code: \d+/)); - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Response ended, read \d+ bytes/)); - - consoleLogSpy.mockRestore(); - await new Promise(resolve => server.close(resolve)); -}); - -//<#END_FILE: test-http-extra-response.js diff --git a/test/js/node/test/parallel/http-full-response.test.js b/test/js/node/test/parallel/http-full-response.test.js deleted file mode 100644 index 8e0a8fcc3b..0000000000 --- a/test/js/node/test/parallel/http-full-response.test.js +++ /dev/null @@ -1,104 +0,0 @@ -//#FILE: test-http-full-response.js -//#SHA1: 3494c79026bf858a01bb497a50a8f2fd3166e62d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// This test requires the program 'ab' -const http = require("http"); -const { exec } = require("child_process"); - -const bodyLength = 12345; - -const body = "c".repeat(bodyLength); - -let server; - -beforeAll(() => { - return new Promise(resolve => { - server = http.createServer((req, res) => { - res.writeHead(200, { - "Content-Length": bodyLength, - "Content-Type": "text/plain", - }); - res.end(body); - }); - - server.listen(0, () => { - resolve(); - }); - }); -}); - -afterAll(() => { - return new Promise(resolve => { - server.close(() => { - resolve(); - }); - }); -}); - -function runAb(opts) { - return new Promise((resolve, reject) => { - const command = `ab ${opts} http://127.0.0.1:${server.address().port}/`; - exec(command, (err, stdout, stderr) => { - if (err) { - if (/ab|apr/i.test(stderr)) { - console.log(`Skipping: problem spawning \`ab\`.\n${stderr}`); - return resolve(); - } - return reject(err); - } - - let m = /Document Length:\s*(\d+) bytes/i.exec(stdout); - const documentLength = parseInt(m[1]); - - m = /Complete requests:\s*(\d+)/i.exec(stdout); - const completeRequests = parseInt(m[1]); - - m = /HTML transferred:\s*(\d+) bytes/i.exec(stdout); - const htmlTransferred = parseInt(m[1]); - - expect(documentLength).toBe(bodyLength); - expect(htmlTransferred).toBe(completeRequests * documentLength); - - resolve(); - }); - }); -} - -test("-c 1 -n 10", async () => { - await runAb("-c 1 -n 10"); - console.log("-c 1 -n 10 okay"); -}); - -test("-c 1 -n 100", async () => { - await runAb("-c 1 -n 100"); - console.log("-c 1 -n 100 okay"); -}); - -test("-c 1 -n 1000", async () => { - await runAb("-c 1 -n 1000"); - console.log("-c 1 -n 1000 okay"); -}); - -//<#END_FILE: test-http-full-response.js diff --git a/test/js/node/test/parallel/http-get-pipeline-problem.test.js b/test/js/node/test/parallel/http-get-pipeline-problem.test.js deleted file mode 100644 index ec0750f018..0000000000 --- a/test/js/node/test/parallel/http-get-pipeline-problem.test.js +++ /dev/null @@ -1,98 +0,0 @@ -//#FILE: test-http-get-pipeline-problem.js -//#SHA1: 422a6c8ae8350bb70627efee734652421322b1bd -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// In previous versions of Node.js (e.g., 0.6.0), this sort of thing would halt -// after http.globalAgent.maxSockets number of files. -// See https://groups.google.com/forum/#!topic/nodejs-dev/V5fB69hFa9o -const fixtures = require("../common/fixtures"); -const http = require("http"); -const fs = require("fs"); -const tmpdir = require("../common/tmpdir"); - -http.globalAgent.maxSockets = 1; - -tmpdir.refresh(); - -const image = fixtures.readSync("/person.jpg"); - -console.log(`image.length = ${image.length}`); - -const total = 10; - -test("HTTP GET pipeline problem", async () => { - const server = http.createServer((req, res) => { - setTimeout(() => { - res.writeHead(200, { - "content-type": "image/jpeg", - connection: "close", - "content-length": image.length, - }); - res.end(image); - }, 1); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const serverAddress = server.address(); - - const requests = Array.from({ length: total }, (_, i) => { - return new Promise((resolve, reject) => { - const opts = { - port: serverAddress.port, - headers: { connection: "close" }, - }; - - http - .get(opts, res => { - console.error(`recv ${i}`); - const s = fs.createWriteStream(`${tmpdir.path}/${i}.jpg`); - res.pipe(s); - - s.on("finish", () => { - console.error(`done ${i}`); - resolve(); - }); - }) - .on("error", reject); - }); - }); - - await Promise.all(requests); - - // Check files - const files = fs.readdirSync(tmpdir.path); - expect(files.length).toBeGreaterThanOrEqual(total); - - for (let i = 0; i < total; i++) { - const fn = `${i}.jpg`; - expect(files).toContain(fn); - const stat = fs.statSync(`${tmpdir.path}/${fn}`); - expect(stat.size).toBe(image.length); - } - - server.close(); -}); - -//<#END_FILE: test-http-get-pipeline-problem.js diff --git a/test/js/node/test/parallel/http-head-request.test.js b/test/js/node/test/parallel/http-head-request.test.js deleted file mode 100644 index b2f86c5ac2..0000000000 --- a/test/js/node/test/parallel/http-head-request.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-http-head-request.js -//#SHA1: ab54d1748aa92e4fa61cf4994e83ddf5e00bf874 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const body = "hello world\n"; - -async function runTest(headers) { - return new Promise((resolve, reject) => { - const server = http.createServer((req, res) => { - console.error("req: %s headers: %j", req.method, headers); - res.writeHead(200, headers); - res.end(); - server.close(); - }); - - server.listen(0, () => { - const request = http.request( - { - port: server.address().port, - method: "HEAD", - path: "/", - }, - response => { - console.error("response start"); - response.on("end", () => { - console.error("response end"); - resolve(); - }); - response.resume(); - }, - ); - request.end(); - }); - }); -} - -test("HEAD request with Transfer-Encoding: chunked", async () => { - await expect( - runTest({ - "Transfer-Encoding": "chunked", - }), - ).resolves.toBeUndefined(); -}); - -test("HEAD request with Content-Length", async () => { - await expect( - runTest({ - "Content-Length": body.length, - }), - ).resolves.toBeUndefined(); -}); - -//<#END_FILE: test-http-head-request.js diff --git a/test/js/node/test/parallel/http-head-response-has-no-body-end-implicit-headers.test.js b/test/js/node/test/parallel/http-head-response-has-no-body-end-implicit-headers.test.js deleted file mode 100644 index 46f4f2269d..0000000000 --- a/test/js/node/test/parallel/http-head-response-has-no-body-end-implicit-headers.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-http-head-response-has-no-body-end-implicit-headers.js -//#SHA1: e2f884b0a99ba30e0e8065596d00af1ed99b4791 -//----------------- -"use strict"; -const http = require("http"); - -// This test is to make sure that when the HTTP server -// responds to a HEAD request with data to res.end, -// it does not send any body but the response is sent -// anyway. - -test("HTTP HEAD response has no body, end implicit headers", done => { - const server = http.createServer((req, res) => { - res.end("FAIL"); // broken: sends FAIL from hot path. - }); - - server.listen(0, () => { - const req = http.request( - { - port: server.address().port, - method: "HEAD", - path: "/", - }, - res => { - res.on("end", () => { - server.close(); - done(); - }); - res.resume(); - }, - ); - req.end(); - }); -}); - -//<#END_FILE: test-http-head-response-has-no-body-end-implicit-headers.js diff --git a/test/js/node/test/parallel/http-head-response-has-no-body-end.test.js b/test/js/node/test/parallel/http-head-response-has-no-body-end.test.js deleted file mode 100644 index 5d18311eb5..0000000000 --- a/test/js/node/test/parallel/http-head-response-has-no-body-end.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-http-head-response-has-no-body-end.js -//#SHA1: 64091937f68588f23597f106fa906d27380be005 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -// This test is to make sure that when the HTTP server -// responds to a HEAD request with data to res.end, -// it does not send any body. - -test("HTTP server responds to HEAD request without sending body", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200); - res.end("FAIL"); // broken: sends FAIL from hot path. - }); - - await new Promise(resolve => { - server.listen(0, () => { - const req = http.request( - { - port: server.address().port, - method: "HEAD", - path: "/", - }, - res => { - const onEnd = jest.fn(); - res.on("end", onEnd); - res.resume(); - - res.on("end", () => { - expect(onEnd).toHaveBeenCalledTimes(1); - server.close(resolve); - }); - }, - ); - req.end(); - }); - }); -}); - -//<#END_FILE: test-http-head-response-has-no-body-end.js diff --git a/test/js/node/test/parallel/http-head-response-has-no-body.test.js b/test/js/node/test/parallel/http-head-response-has-no-body.test.js deleted file mode 100644 index f87fece95a..0000000000 --- a/test/js/node/test/parallel/http-head-response-has-no-body.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-http-head-response-has-no-body.js -//#SHA1: f7df6559885b0465d43994e773c961b525b195a9 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -// This test is to make sure that when the HTTP server -// responds to a HEAD request, it does not send any body. -// In this case it was sending '0\r\n\r\n' - -test("HTTP server responds to HEAD request without body", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200); // broken: defaults to TE chunked - res.end(); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const { port } = server.address(); - - await new Promise((resolve, reject) => { - const req = http.request( - { - port, - method: "HEAD", - path: "/", - }, - res => { - res.on("end", () => { - server.close(() => { - resolve(); - }); - }); - res.resume(); - }, - ); - req.on("error", reject); - req.end(); - }); -}); - -//<#END_FILE: test-http-head-response-has-no-body.js diff --git a/test/js/node/test/parallel/http-header-obstext.test.js b/test/js/node/test/parallel/http-header-obstext.test.js deleted file mode 100644 index 55248d91c4..0000000000 --- a/test/js/node/test/parallel/http-header-obstext.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-http-header-obstext.js -//#SHA1: 031a5230bc91c831407772f2b8cbeba3559ed1d2 -//----------------- -"use strict"; - -// This test ensures that the http-parser can handle UTF-8 characters -// in the http header. - -const http = require("http"); - -test("http-parser can handle UTF-8 characters in http header", async () => { - const server = http.createServer((req, res) => { - res.end("ok"); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const { port } = server.address(); - - const response = await new Promise(resolve => { - http.get( - { - port, - headers: { Test: "Düsseldorf" }, - }, - resolve, - ); - }); - - expect(response.statusCode).toBe(200); - - await new Promise(resolve => server.close(resolve)); -}); - -//<#END_FILE: test-http-header-obstext.js diff --git a/test/js/node/test/parallel/http-header-owstext.test.js b/test/js/node/test/parallel/http-header-owstext.test.js deleted file mode 100644 index cfab935f17..0000000000 --- a/test/js/node/test/parallel/http-header-owstext.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-http-header-owstext.js -//#SHA1: 339bfcf13a4cc9caa39940de3854eeda01b4500c -//----------------- -"use strict"; - -const http = require("http"); -const net = require("net"); - -// This test ensures that the http-parser strips leading and trailing OWS from -// header values. It sends the header values in chunks to force the parser to -// build the string up through multiple calls to on_header_value(). - -function check(hdr, snd, rcv) { - return new Promise(resolve => { - const server = http.createServer((req, res) => { - expect(req.headers[hdr]).toBe(rcv); - req.pipe(res); - }); - - server.listen(0, function () { - const client = net.connect(this.address().port, start); - function start() { - client.write("GET / HTTP/1.1\r\n" + hdr + ":", drain); - } - - function drain() { - if (snd.length === 0) { - return client.write("\r\nConnection: close\r\n\r\n"); - } - client.write(snd.shift(), drain); - } - - const bufs = []; - client.on("data", function (chunk) { - bufs.push(chunk); - }); - client.on("end", function () { - const head = Buffer.concat(bufs).toString("latin1").split("\r\n")[0]; - expect(head).toBe("HTTP/1.1 200 OK"); - server.close(); - resolve(); - }); - }); - }); -} - -test("http header OWS text parsing", async () => { - await check("host", [" \t foo.com\t"], "foo.com"); - await check("host", [" \t foo\tcom\t"], "foo\tcom"); - await check("host", [" \t", " ", " foo.com\t", "\t "], "foo.com"); - await check("host", [" \t", " \t".repeat(100), "\t "], ""); - await check("host", [" \t", " - - - - ", "\t "], "- - - -"); -}); - -//<#END_FILE: test-http-header-owstext.js diff --git a/test/js/node/test/parallel/http-host-headers.test.js b/test/js/node/test/parallel/http-host-headers.test.js deleted file mode 100644 index cb50ca9b04..0000000000 --- a/test/js/node/test/parallel/http-host-headers.test.js +++ /dev/null @@ -1,77 +0,0 @@ -//#FILE: test-http-host-headers.js -//#SHA1: 256e8b55e2c545a9f9df89607600f18a93c1c67a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -function reqHandler(req, res) { - if (req.url === "/setHostFalse5") { - expect(req.headers.host).toBeUndefined(); - } else { - expect(req.headers.host).toBe(`localhost:${this.address().port}`); - } - res.writeHead(200, {}); - res.end("ok"); -} - -const httpServer = http.createServer(reqHandler); - -test("HTTP host headers", async () => { - await new Promise(resolve => { - httpServer.listen(0, async () => { - const port = httpServer.address().port; - const makeRequest = (method, path) => { - return new Promise(resolve => { - const req = http.request( - { - method, - path, - host: "localhost", - port, - rejectUnauthorized: false, - }, - res => { - res.resume(); - resolve(); - }, - ); - req.on("error", () => { - throw new Error("Request should not fail"); - }); - req.end(); - }); - }; - - await makeRequest("GET", "/0"); - await makeRequest("GET", "/1"); - await makeRequest("POST", "/2"); - await makeRequest("PUT", "/3"); - await makeRequest("DELETE", "/4"); - - httpServer.close(resolve); - }); - }); -}); - -//<#END_FILE: test-http-host-headers.js diff --git a/test/js/node/test/parallel/http-keep-alive-timeout-custom.test.js b/test/js/node/test/parallel/http-keep-alive-timeout-custom.test.js deleted file mode 100644 index 9d0e874e81..0000000000 --- a/test/js/node/test/parallel/http-keep-alive-timeout-custom.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-http-keep-alive-timeout-custom.js -//#SHA1: 4f7c5a20da7b46bea9198b3854aed7c2042a8691 -//----------------- -"use strict"; - -const http = require("http"); - -test("HTTP Keep-Alive timeout custom", async () => { - const server = http.createServer((req, res) => { - const body = "hello world\n"; - - res.writeHead(200, { - "Content-Length": body.length, - "Keep-Alive": "timeout=50", - }); - res.write(body); - res.end(); - }); - server.keepAliveTimeout = 12010; - - const agent = new http.Agent({ maxSockets: 1, keepAlive: true }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get( - { - path: "/", - port: server.address().port, - agent: agent, - }, - response => { - response.resume(); - expect(response.headers["keep-alive"]).toBe("timeout=50"); - server.close(); - agent.destroy(); - resolve(); - }, - ); - }); - }); -}); - -//<#END_FILE: test-http-keep-alive-timeout-custom.js diff --git a/test/js/node/test/parallel/http-many-ended-pipelines.test.js b/test/js/node/test/parallel/http-many-ended-pipelines.test.js deleted file mode 100644 index c142011e0d..0000000000 --- a/test/js/node/test/parallel/http-many-ended-pipelines.test.js +++ /dev/null @@ -1,82 +0,0 @@ -//#FILE: test-http-many-ended-pipelines.js -//#SHA1: 930bb6dc614c68f965c7b31e9a1223386234e389 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const net = require("net"); - -const numRequests = 20; -let first = false; - -test("HTTP server handles many ended pipelines", async () => { - const server = http.createServer((req, res) => { - if (!first) { - first = true; - req.socket.on("close", () => { - server.close(); - }); - } - - res.end("ok"); - // Oh no! The connection died! - req.socket.destroy(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const client = net.connect({ - port: server.address().port, - allowHalfOpen: true, - }); - - client.on("error", err => { - // The socket might be destroyed by the other peer while data is still - // being written. The `'EPIPE'` and `'ECONNABORTED'` codes might also be - // valid but they have not been seen yet. - expect(err.code).toBe("ECONNRESET"); - }); - - for (let i = 0; i < numRequests; i++) { - client.write("GET / HTTP/1.1\r\n" + "Host: some.host.name\r\n" + "\r\n\r\n"); - } - client.end(); - client.pipe(process.stdout); - - resolve(); - }); - }); -}); - -const mockWarning = jest.spyOn(process, "emit"); -mockWarning.mockImplementation((event, ...args) => { - if (event === "warning") return; - return process.emit.apply(process, [event, ...args]); -}); - -afterAll(() => { - expect(mockWarning).not.toHaveBeenCalledWith("warning", expect.anything()); - mockWarning.mockRestore(); -}); - -//<#END_FILE: test-http-many-ended-pipelines.js diff --git a/test/js/node/test/parallel/http-missing-header-separator-cr.test.js b/test/js/node/test/parallel/http-missing-header-separator-cr.test.js deleted file mode 100644 index 952d726eed..0000000000 --- a/test/js/node/test/parallel/http-missing-header-separator-cr.test.js +++ /dev/null @@ -1,89 +0,0 @@ -//#FILE: test-http-missing-header-separator-cr.js -//#SHA1: 6e213764778e9edddd0fc6a43c9a3183507054c6 -//----------------- -"use strict"; - -const http = require("http"); -const net = require("net"); - -function serverHandler(server, msg) { - const client = net.connect(server.address().port, "localhost"); - - let response = ""; - - client.on("data", chunk => { - response += chunk; - }); - - client.setEncoding("utf8"); - client.on("error", () => { - throw new Error("Client error should not occur"); - }); - client.on("end", () => { - expect(response).toBe("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"); - server.close(); - }); - client.write(msg); - client.resume(); -} - -test("GET request with invalid header", async () => { - const msg = [ - "GET / HTTP/1.1", - "Host: localhost", - "Dummy: x\nContent-Length: 23", - "", - "GET / HTTP/1.1", - "Dummy: GET /admin HTTP/1.1", - "Host: localhost", - "", - "", - ].join("\r\n"); - - const server = http.createServer(() => { - throw new Error("Server should not be called"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - serverHandler(server, msg); - resolve(); - }); - }); -}); - -test("POST request with invalid Transfer-Encoding header", async () => { - const msg = ["POST / HTTP/1.1", "Host: localhost", "x:x\nTransfer-Encoding: chunked", "", "1", "A", "0", "", ""].join( - "\r\n", - ); - - const server = http.createServer(() => { - throw new Error("Server should not be called"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - serverHandler(server, msg); - resolve(); - }); - }); -}); - -test("POST request with invalid header and Transfer-Encoding", async () => { - const msg = ["POST / HTTP/1.1", "Host: localhost", "x:\nTransfer-Encoding: chunked", "", "1", "A", "0", "", ""].join( - "\r\n", - ); - - const server = http.createServer(() => { - throw new Error("Server should not be called"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - serverHandler(server, msg); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-missing-header-separator-cr.js diff --git a/test/js/node/test/parallel/http-no-read-no-dump.test.js b/test/js/node/test/parallel/http-no-read-no-dump.test.js deleted file mode 100644 index 2085ea1981..0000000000 --- a/test/js/node/test/parallel/http-no-read-no-dump.test.js +++ /dev/null @@ -1,86 +0,0 @@ -//#FILE: test-http-no-read-no-dump.js -//#SHA1: 8548eb47a6eb8ec151b9c60e74b026d983145d26 -//----------------- -"use strict"; - -const http = require("http"); - -let onPause = null; - -describe("HTTP no read no dump", () => { - let server; - let port; - - beforeAll(done => { - server = http - .createServer((req, res) => { - if (req.method === "GET") return res.end(); - - res.writeHead(200); - res.flushHeaders(); - - req.on("close", () => { - expect(() => { - req.on("end", () => {}); - }).not.toThrow(); - }); - - req.connection.on("pause", () => { - res.end(); - onPause(); - }); - }) - .listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(done => { - server.close(done); - }); - - test("should handle POST and GET requests correctly", done => { - const agent = new http.Agent({ - maxSockets: 1, - keepAlive: true, - }); - - const post = http.request( - { - agent, - method: "POST", - port, - }, - res => { - res.resume(); - - post.write(Buffer.alloc(64 * 1024).fill("X")); - onPause = () => { - post.end("something"); - }; - }, - ); - - // What happens here is that the server `end`s the response before we send - // `something`, and the client thought that this is a green light for sending - // next GET request - post.write("initial"); - - http - .request( - { - agent, - method: "GET", - port, - }, - res => { - res.connection.end(); - done(); - }, - ) - .end(); - }); -}); - -//<#END_FILE: test-http-no-read-no-dump.js diff --git a/test/js/node/test/parallel/http-outgoing-finish.test.js b/test/js/node/test/parallel/http-outgoing-finish.test.js deleted file mode 100644 index fa7aab9a23..0000000000 --- a/test/js/node/test/parallel/http-outgoing-finish.test.js +++ /dev/null @@ -1,86 +0,0 @@ -//#FILE: test-http-outgoing-finish.js -//#SHA1: cd9dbce2b1b26369349c30bcd94979b354316128 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const http = require("http"); - -test("http outgoing finish", async () => { - const server = http.createServer((req, res) => { - req.resume(); - req.on("end", () => { - write(res); - }); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const req = http.request({ - port: server.address().port, - method: "PUT", - }); - write(req); - req.on("response", res => { - res.resume(); - }); - resolve(); - }); - }); -}); - -const buf = Buffer.alloc(1024 * 16, "x"); -function write(out) { - const name = out.constructor.name; - let finishEvent = false; - let endCb = false; - - // First, write until it gets some backpressure - while (out.write(buf)); - - // Now end, and make sure that we don't get the 'finish' event - // before the tick where the cb gets called. We give it until - // nextTick because this is added as a listener before the endcb - // is registered. The order is not what we're testing here, just - // that 'finish' isn't emitted until the stream is fully flushed. - out.on("finish", () => { - finishEvent = true; - console.error(`${name} finish event`); - process.nextTick(() => { - expect(endCb).toBe(true); - console.log(`ok - ${name} finishEvent`); - }); - }); - - out.end(buf, () => { - endCb = true; - console.error(`${name} endCb`); - process.nextTick(() => { - expect(finishEvent).toBe(true); - console.log(`ok - ${name} endCb`); - }); - }); -} - -//<#END_FILE: test-http-outgoing-finish.js diff --git a/test/js/node/test/parallel/http-outgoing-finished.test.js b/test/js/node/test/parallel/http-outgoing-finished.test.js deleted file mode 100644 index 96363b2382..0000000000 --- a/test/js/node/test/parallel/http-outgoing-finished.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-http-outgoing-finished.js -//#SHA1: 9c1ce8205b113dbb5b4ddfd06c0c90017b344e15 -//----------------- -"use strict"; - -const http = require("http"); -const { finished } = require("stream"); - -let server; - -beforeAll(() => { - return new Promise(resolve => { - server = http - .createServer((req, res) => { - let closed = false; - res - .on("close", () => { - closed = true; - finished(res, () => { - server.close(); - }); - }) - .end(); - finished(res, () => { - expect(closed).toBe(true); - }); - }) - .listen(0, () => { - resolve(); - }); - }); -}); - -afterAll(() => { - return new Promise(resolve => { - server.close(() => { - resolve(); - }); - }); -}); - -test("HTTP outgoing finished", done => { - const closeHandler = jest.fn(); - const finishedHandler = jest.fn(); - - server.on("request", (req, res) => { - res.on("close", closeHandler); - finished(res, finishedHandler); - }); - - http - .request({ - port: server.address().port, - method: "GET", - }) - .on("response", res => { - res.resume(); - }) - .end(); - - setTimeout(() => { - expect(closeHandler).toHaveBeenCalledTimes(1); - expect(finishedHandler).toHaveBeenCalledTimes(1); - done(); - }, 1000); -}); - -//<#END_FILE: test-http-outgoing-finished.js diff --git a/test/js/node/test/parallel/http-outgoing-writablefinished.test.js b/test/js/node/test/parallel/http-outgoing-writablefinished.test.js deleted file mode 100644 index 2788589d87..0000000000 --- a/test/js/node/test/parallel/http-outgoing-writablefinished.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-http-outgoing-writableFinished.js -//#SHA1: f3fbc0d89cd03168f3ee92ed586b62dd5e3b8edb -//----------------- -"use strict"; - -const http = require("http"); - -test("HTTP server response writableFinished", async () => { - const server = http.createServer((req, res) => { - expect(res.writableFinished).toBe(false); - res.on("finish", () => { - expect(res.writableFinished).toBe(true); - server.close(); - }); - res.end(); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const port = server.address().port; - - const clientRequest = http.request({ - port, - method: "GET", - path: "/", - }); - - expect(clientRequest.writableFinished).toBe(false); - - await new Promise(resolve => { - clientRequest.on("finish", () => { - expect(clientRequest.writableFinished).toBe(true); - resolve(); - }); - clientRequest.end(); - expect(clientRequest.writableFinished).toBe(false); - }); -}); - -//<#END_FILE: test-http-outgoing-writableFinished.js diff --git a/test/js/node/test/parallel/http-outgoing-write-types.test.js b/test/js/node/test/parallel/http-outgoing-write-types.test.js deleted file mode 100644 index 6870939a90..0000000000 --- a/test/js/node/test/parallel/http-outgoing-write-types.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-http-outgoing-write-types.js -//#SHA1: bdeac2ab8008bea1c7e0b22f8744176dea0410e2 -//----------------- -"use strict"; - -const http = require("http"); - -test("HTTP outgoing write types", async () => { - const httpServer = http.createServer((req, res) => { - httpServer.close(); - - expect(() => { - res.write(["Throws."]); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); - - // should not throw - expect(() => res.write("1a2b3c")).not.toThrow(); - - // should not throw - expect(() => res.write(new Uint8Array(1024))).not.toThrow(); - - // should not throw - expect(() => res.write(Buffer.from("1".repeat(1024)))).not.toThrow(); - - res.end(); - }); - - await new Promise(resolve => { - httpServer.listen(0, () => { - http.get({ port: httpServer.address().port }, resolve); - }); - }); -}); - -//<#END_FILE: test-http-outgoing-write-types.js diff --git a/test/js/node/test/parallel/http-pause-no-dump.test.js b/test/js/node/test/parallel/http-pause-no-dump.test.js deleted file mode 100644 index 1f32f2b0a6..0000000000 --- a/test/js/node/test/parallel/http-pause-no-dump.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-http-pause-no-dump.js -//#SHA1: 30c3bd27f5edd0ba060a0d6833061d1ce6379cd5 -//----------------- -"use strict"; - -const http = require("http"); - -test("HTTP pause should not dump", done => { - const server = http.createServer((req, res) => { - req.once("data", () => { - req.pause(); - res.writeHead(200); - res.end(); - res.on("finish", () => { - expect(req._dumped).toBeFalsy(); - }); - }); - }); - - server.listen(0, () => { - const req = http.request( - { - port: server.address().port, - method: "POST", - path: "/", - }, - res => { - expect(res.statusCode).toBe(200); - res.resume(); - res.on("end", () => { - server.close(); - done(); - }); - }, - ); - - req.end(Buffer.allocUnsafe(1024)); - }); -}); - -//<#END_FILE: test-http-pause-no-dump.js diff --git a/test/js/node/test/parallel/http-pause-resume-one-end.test.js b/test/js/node/test/parallel/http-pause-resume-one-end.test.js deleted file mode 100644 index 52198ee8a3..0000000000 --- a/test/js/node/test/parallel/http-pause-resume-one-end.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-http-pause-resume-one-end.js -//#SHA1: 69f25ca624d470d640d6366b6df27eba31668e96 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("HTTP server pause and resume", async () => { - const server = http.Server(function (req, res) { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("Hello World\n"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const opts = { - port: server.address().port, - headers: { connection: "close" }, - }; - - await new Promise(resolve => { - http.get(opts, res => { - res.on( - "data", - jest.fn().mockImplementation(() => { - res.pause(); - setImmediate(() => { - res.resume(); - }); - }), - ); - - res.on("end", () => { - expect(res.destroyed).toBe(false); - }); - - expect(res.destroyed).toBe(false); - - res.on("close", () => { - expect(res.destroyed).toBe(true); - resolve(); - }); - }); - }); -}); - -//<#END_FILE: test-http-pause-resume-one-end.js diff --git a/test/js/node/test/parallel/http-pause.test.js b/test/js/node/test/parallel/http-pause.test.js deleted file mode 100644 index 74bca8bc34..0000000000 --- a/test/js/node/test/parallel/http-pause.test.js +++ /dev/null @@ -1,87 +0,0 @@ -//#FILE: test-http-pause.js -//#SHA1: d7712077ebe0493c27ffd7180e73fdd409041bf7 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const expectedServer = "Request Body from Client"; -let resultServer = ""; -const expectedClient = "Response Body from Server"; -let resultClient = ""; - -test("HTTP pause and resume", async () => { - const server = http.createServer((req, res) => { - console.error("pause server request"); - req.pause(); - setTimeout(() => { - console.error("resume server request"); - req.resume(); - req.setEncoding("utf8"); - req.on("data", chunk => { - resultServer += chunk; - }); - req.on("end", () => { - console.error(resultServer); - res.writeHead(200); - res.end(expectedClient); - }); - }, 100); - }); - - await new Promise(resolve => { - server.listen(0, function () { - // Anonymous function rather than arrow function to test `this` value. - expect(this).toBe(server); - const req = http.request( - { - port: this.address().port, - path: "/", - method: "POST", - }, - res => { - console.error("pause client response"); - res.pause(); - setTimeout(() => { - console.error("resume client response"); - res.resume(); - res.on("data", chunk => { - resultClient += chunk; - }); - res.on("end", () => { - console.error(resultClient); - server.close(); - resolve(); - }); - }, 100); - }, - ); - req.end(expectedServer); - }); - }); - - expect(resultServer).toBe(expectedServer); - expect(resultClient).toBe(expectedClient); -}); - -//<#END_FILE: test-http-pause.js diff --git a/test/js/node/test/parallel/http-pipe-fs.test.js b/test/js/node/test/parallel/http-pipe-fs.test.js deleted file mode 100644 index 8db6bebdd3..0000000000 --- a/test/js/node/test/parallel/http-pipe-fs.test.js +++ /dev/null @@ -1,90 +0,0 @@ -//#FILE: test-http-pipe-fs.js -//#SHA1: eb13abd37a9e18b0b28077247a7d336b92b79fbc -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const NUMBER_OF_STREAMS = 2; - -const tmpdir = path.join(os.tmpdir(), "node-test-http-pipe-fs"); -fs.mkdirSync(tmpdir, { recursive: true }); - -const file = path.join(tmpdir, "http-pipe-fs-test.txt"); - -describe("HTTP pipe to fs", () => { - let server; - - beforeAll(() => { - server = http.createServer((req, res) => { - const stream = fs.createWriteStream(file); - req.pipe(stream); - stream.on("close", () => { - res.writeHead(200); - res.end(); - }); - }); - }); - - afterAll(() => { - return new Promise(resolve => server.close(resolve)); - }); - - it("should handle multiple concurrent requests", async () => { - await new Promise(resolve => server.listen(0, resolve)); - - const port = server.address().port; - http.globalAgent.maxSockets = 1; - - const makeRequest = () => { - return new Promise(resolve => { - const req = http.request( - { - port: port, - method: "POST", - headers: { - "Content-Length": 5, - }, - }, - res => { - res.on("end", resolve); - res.resume(); - }, - ); - - req.end("12345"); - }); - }; - - const requests = Array(NUMBER_OF_STREAMS).fill().map(makeRequest); - await Promise.all(requests); - - expect.assertions(1); - expect(true).toBe(true); // Dummy assertion to ensure the test ran - }); -}); - -//<#END_FILE: test-http-pipe-fs.js diff --git a/test/js/node/test/parallel/http-proxy.test.js b/test/js/node/test/parallel/http-proxy.test.js deleted file mode 100644 index dbb116434c..0000000000 --- a/test/js/node/test/parallel/http-proxy.test.js +++ /dev/null @@ -1,118 +0,0 @@ -//#FILE: test-http-proxy.js -//#SHA1: 7b000418a5941e059d64a57b2f0b4fdfb43eb71d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -const cookies = [ - "session_token=; path=/; expires=Sun, 15-Sep-2030 13:48:52 GMT", - "prefers_open_id=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", -]; - -const headers = { "content-type": "text/plain", "set-cookie": cookies, hello: "world" }; - -test("HTTP proxy server", async () => { - const backend = http.createServer((req, res) => { - console.error("backend request"); - res.writeHead(200, headers); - res.write("hello world\n"); - res.end(); - }); - - const proxy = http.createServer((req, res) => { - console.error(`proxy req headers: ${JSON.stringify(req.headers)}`); - http.get( - { - port: backend.address().port, - path: url.parse(req.url).pathname, - }, - proxy_res => { - console.error(`proxy res headers: ${JSON.stringify(proxy_res.headers)}`); - - expect(proxy_res.headers.hello).toBe("world"); - expect(proxy_res.headers["content-type"]).toBe("text/plain"); - expect(proxy_res.headers["set-cookie"]).toEqual(cookies); - - res.writeHead(proxy_res.statusCode, proxy_res.headers); - - proxy_res.on("data", chunk => { - res.write(chunk); - }); - - proxy_res.on("end", () => { - res.end(); - console.error("proxy res"); - }); - }, - ); - }); - - let body = ""; - - await new Promise(resolve => { - let nlistening = 0; - function startReq() { - nlistening++; - if (nlistening < 2) return; - - http.get( - { - port: proxy.address().port, - path: "/test", - }, - res => { - console.error("got res"); - expect(res.statusCode).toBe(200); - - expect(res.headers.hello).toBe("world"); - expect(res.headers["content-type"]).toBe("text/plain"); - expect(res.headers["set-cookie"]).toEqual(cookies); - - res.setEncoding("utf8"); - res.on("data", chunk => { - body += chunk; - }); - res.on("end", () => { - proxy.close(); - backend.close(); - console.error("closed both"); - resolve(); - }); - }, - ); - console.error("client req"); - } - - console.error("listen proxy"); - proxy.listen(0, startReq); - - console.error("listen backend"); - backend.listen(0, startReq); - }); - - expect(body).toBe("hello world\n"); -}); - -//<#END_FILE: test-http-proxy.js diff --git a/test/js/node/test/parallel/http-request-arguments.test.js b/test/js/node/test/parallel/http-request-arguments.test.js deleted file mode 100644 index d202ccc5af..0000000000 --- a/test/js/node/test/parallel/http-request-arguments.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-http-request-arguments.js -//#SHA1: c02b492e2dbf5fa6ffcda8a80c3e4ad41bb0c9e5 -//----------------- -"use strict"; - -const http = require("http"); - -// Test providing both a url and options, with the options partially -// replacing address and port portions of the URL provided. -test("http.get with url and options", done => { - const server = http.createServer((req, res) => { - expect(req.url).toBe("/testpath"); - res.end(); - server.close(); - }); - - server.listen(0, () => { - const port = server.address().port; - http.get("http://example.com/testpath", { hostname: "localhost", port }, res => { - res.resume(); - done(); - }); - }); -}); - -//<#END_FILE: test-http-request-arguments.js diff --git a/test/js/node/test/parallel/http-request-end-twice.test.js b/test/js/node/test/parallel/http-request-end-twice.test.js deleted file mode 100644 index b1d2c8a209..0000000000 --- a/test/js/node/test/parallel/http-request-end-twice.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-http-request-end-twice.js -//#SHA1: c8c502b3bf8a681a7acb9afa603a13cebaf1d00e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("http request end twice", async () => { - const server = http.Server((req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("hello world\n"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const req = http.get({ port: server.address().port }, res => { - res.on("end", () => { - expect(req.end()).toBe(req); - server.close(resolve); - }); - res.resume(); - }); - }); - }); -}); - -//<#END_FILE: test-http-request-end-twice.js diff --git a/test/js/node/test/parallel/http-request-end.test.js b/test/js/node/test/parallel/http-request-end.test.js deleted file mode 100644 index 9ed911d1ad..0000000000 --- a/test/js/node/test/parallel/http-request-end.test.js +++ /dev/null @@ -1,74 +0,0 @@ -//#FILE: test-http-request-end.js -//#SHA1: e86769441d8d182d32d60d4631e32fbcc2675ed9 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -const expected = "Post Body For Test"; - -test("http request end", async () => { - const server = http.Server((req, res) => { - let result = ""; - - req.setEncoding("utf8"); - req.on("data", chunk => { - result += chunk; - }); - - req.on("end", () => { - expect(result).toBe(expected); - res.writeHead(200); - res.end("hello world\n"); - server.close(); - }); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const req = http - .request( - { - port: server.address().port, - path: "/", - method: "POST", - }, - res => { - expect(res.statusCode).toBe(200); - res.resume(); - resolve(); - }, - ) - .on("error", e => { - console.error(e.message); - process.exit(1); - }); - - const result = req.end(expected); - - expect(req).toBe(result); - }); - }); -}); - -//<#END_FILE: test-http-request-end.js diff --git a/test/js/node/test/parallel/http-request-large-payload.test.js b/test/js/node/test/parallel/http-request-large-payload.test.js deleted file mode 100644 index ea69bdc95a..0000000000 --- a/test/js/node/test/parallel/http-request-large-payload.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-http-request-large-payload.js -//#SHA1: 236870617a867c47c0767e351433c5deb7c87120 -//----------------- -"use strict"; - -// This test ensures Node.js doesn't throw an error when making requests with -// the payload 16kb or more in size. -// https://github.com/nodejs/node/issues/2821 - -const http = require("http"); - -test("HTTP request with large payload", done => { - const server = http.createServer((req, res) => { - res.writeHead(200); - res.end(); - - server.close(); - done(); - }); - - server.listen(0, function () { - const req = http.request({ - method: "POST", - port: this.address().port, - }); - - const payload = Buffer.alloc(16390, "Й"); - req.write(payload); - req.end(); - }); -}); - -//<#END_FILE: test-http-request-large-payload.js diff --git a/test/js/node/test/parallel/http-res-write-after-end.test.js b/test/js/node/test/parallel/http-res-write-after-end.test.js deleted file mode 100644 index 45a17ed5e7..0000000000 --- a/test/js/node/test/parallel/http-res-write-after-end.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-http-res-write-after-end.js -//#SHA1: e579896871bf375190397b4f7e99f37cc156e7c9 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("HTTP response write after end", async () => { - const server = http.Server((req, res) => { - res.on("error", error => { - expect(error).toMatchObject({ - code: "ERR_STREAM_WRITE_AFTER_END", - name: "Error", - message: expect.any(String), - }); - }); - - res.write("This should write."); - res.end(); - - const r = res.write("This should raise an error."); - // Write after end should return false - expect(r).toBe(false); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - server.close(resolve); - }); - }); - }); -}); - -//<#END_FILE: test-http-res-write-after-end.js diff --git a/test/js/node/test/parallel/http-response-readable.test.js b/test/js/node/test/parallel/http-response-readable.test.js deleted file mode 100644 index 4af51a42e5..0000000000 --- a/test/js/node/test/parallel/http-response-readable.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-http-response-readable.js -//#SHA1: bfdd12475c68879668c3019c685001244559fb20 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("HTTP response readable state", async () => { - const testServer = new http.Server((req, res) => { - res.writeHead(200); - res.end("Hello world"); - }); - - await new Promise(resolve => { - testServer.listen(0, () => { - const port = testServer.address().port; - http.get({ port }, res => { - expect(res.readable).toBe(true); - res.on("end", () => { - expect(res.readable).toBe(false); - testServer.close(resolve); - }); - res.resume(); - }); - }); - }); -}); - -//<#END_FILE: test-http-response-readable.js diff --git a/test/js/node/test/parallel/http-response-writehead-returns-this.test.js b/test/js/node/test/parallel/http-response-writehead-returns-this.test.js deleted file mode 100644 index 9418c9e6b0..0000000000 --- a/test/js/node/test/parallel/http-response-writehead-returns-this.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-http-response-writehead-returns-this.js -//#SHA1: 8a079a3635356290e98a1e7c4eb89b97680b3889 -//----------------- -"use strict"; - -const http = require("http"); - -test("http.ServerResponse.writeHead() returns this", done => { - const server = http.createServer((req, res) => { - res.writeHead(200, { "a-header": "a-header-value" }).end("abc"); - }); - - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - expect(res.headers["a-header"]).toBe("a-header-value"); - - const chunks = []; - - res.on("data", chunk => chunks.push(chunk)); - res.on("end", () => { - expect(Buffer.concat(chunks).toString()).toBe("abc"); - server.close(); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-http-response-writehead-returns-this.js diff --git a/test/js/node/test/parallel/http-server-close-idle-wait-response.test.js b/test/js/node/test/parallel/http-server-close-idle-wait-response.test.js deleted file mode 100644 index 415d32e729..0000000000 --- a/test/js/node/test/parallel/http-server-close-idle-wait-response.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-http-server-close-idle-wait-response.js -//#SHA1: 04c4c10103faabfd084635c9280824668eb0ba18 -//----------------- -"use strict"; - -const { createServer, get } = require("http"); - -test("HTTP server close idle connections after response", async () => { - const server = createServer( - jest.fn((req, res) => { - req.resume(); - - setTimeout(() => { - res.writeHead(204, { Connection: "keep-alive", "Keep-Alive": "timeout=1" }); - res.end(); - }, 1000); - }), - ); - - await new Promise(resolve => { - server.listen(0, () => { - const port = server.address().port; - - get(`http://localhost:${port}`, res => { - server.close(); - }).on("finish", () => { - setTimeout(() => { - server.closeIdleConnections(); - resolve(); - }, 500); - }); - }); - }); - - expect(server.listeners("request")[0]).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-http-server-close-idle-wait-response.js diff --git a/test/js/node/test/parallel/http-server-delete-parser.test.js b/test/js/node/test/parallel/http-server-delete-parser.test.js deleted file mode 100644 index f16d12c848..0000000000 --- a/test/js/node/test/parallel/http-server-delete-parser.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-http-server-delete-parser.js -//#SHA1: 49465ae50d9dac34e834dcb19c02e75b284acdc2 -//----------------- -"use strict"; - -const http = require("http"); - -test("HTTP server deletes parser after write", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.write("okay", () => { - delete res.socket.parser; - }); - res.end(); - }); - - await new Promise(resolve => { - server.listen(0, "127.0.0.1", resolve); - }); - - const { port } = server.address(); - - const req = http.request({ - port, - host: "127.0.0.1", - method: "GET", - }); - - await new Promise(resolve => { - req.end(resolve); - }); - - server.close(); -}); - -//<#END_FILE: test-http-server-delete-parser.js diff --git a/test/js/node/test/parallel/http-server-multiheaders.test.js b/test/js/node/test/parallel/http-server-multiheaders.test.js deleted file mode 100644 index f0ba4b4d61..0000000000 --- a/test/js/node/test/parallel/http-server-multiheaders.test.js +++ /dev/null @@ -1,87 +0,0 @@ -//#FILE: test-http-server-multiheaders.js -//#SHA1: 48e657fa74fb8aeeb2d04661ac760ff0cf5bf12a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Verify that the HTTP server implementation handles multiple instances -// of the same header as per RFC2616: joining the handful of fields by ', ' -// that support it, and dropping duplicates for other fields. - -const http = require("http"); - -test("HTTP server handles multiple instances of the same header correctly", async () => { - const server = http.createServer((req, res) => { - expect(req.headers.accept).toBe("abc, def, ghijklmnopqrst"); - expect(req.headers.host).toBe("foo"); - expect(req.headers["www-authenticate"]).toBe("foo, bar, baz"); - expect(req.headers["proxy-authenticate"]).toBe("foo, bar, baz"); - expect(req.headers["x-foo"]).toBe("bingo"); - expect(req.headers["x-bar"]).toBe("banjo, bango"); - expect(req.headers["sec-websocket-protocol"]).toBe("chat, share"); - expect(req.headers["sec-websocket-extensions"]).toBe("foo; 1, bar; 2, baz"); - expect(req.headers.constructor).toBe("foo, bar, baz"); - - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("EOF"); - - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get({ - host: "localhost", - port: server.address().port, - path: "/", - headers: [ - ["accept", "abc"], - ["accept", "def"], - ["Accept", "ghijklmnopqrst"], - ["host", "foo"], - ["Host", "bar"], - ["hOst", "baz"], - ["www-authenticate", "foo"], - ["WWW-Authenticate", "bar"], - ["WWW-AUTHENTICATE", "baz"], - ["proxy-authenticate", "foo"], - ["Proxy-Authenticate", "bar"], - ["PROXY-AUTHENTICATE", "baz"], - ["x-foo", "bingo"], - ["x-bar", "banjo"], - ["x-bar", "bango"], - ["sec-websocket-protocol", "chat"], - ["sec-websocket-protocol", "share"], - ["sec-websocket-extensions", "foo; 1"], - ["sec-websocket-extensions", "bar; 2"], - ["sec-websocket-extensions", "baz"], - ["constructor", "foo"], - ["constructor", "bar"], - ["constructor", "baz"], - ], - }); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-server-multiheaders.js diff --git a/test/js/node/test/parallel/http-server-non-utf8-header.test.js b/test/js/node/test/parallel/http-server-non-utf8-header.test.js deleted file mode 100644 index de10907234..0000000000 --- a/test/js/node/test/parallel/http-server-non-utf8-header.test.js +++ /dev/null @@ -1,67 +0,0 @@ -//#FILE: test-http-server-non-utf8-header.js -//#SHA1: bc84accb29cf80323d0fb55455a596f36a7933b2 -//----------------- -"use strict"; -const http = require("http"); - -const nonUtf8Header = "bår"; -const nonUtf8ToLatin1 = Buffer.from(nonUtf8Header).toString("latin1"); - -test("HTTP server with non-UTF8 header", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200, ["content-disposition", Buffer.from(nonUtf8Header).toString("binary")]); - res.end("hello"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - expect(res.statusCode).toBe(200); - expect(res.headers["content-disposition"]).toBe(nonUtf8ToLatin1); - res.resume().on("end", () => { - server.close(resolve); - }); - }); - }); - }); -}); - -test("HTTP server with multi-value non-UTF8 header", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200, ["content-disposition", [Buffer.from(nonUtf8Header).toString("binary")]]); - res.end("hello"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - expect(res.statusCode).toBe(200); - expect(res.headers["content-disposition"]).toBe(nonUtf8ToLatin1); - res.resume().on("end", () => { - server.close(resolve); - }); - }); - }); - }); -}); - -test("HTTP server with non-UTF8 header and Content-Length", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200, ["Content-Length", "5", "content-disposition", Buffer.from(nonUtf8Header).toString("binary")]); - res.end("hello"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - expect(res.statusCode).toBe(200); - expect(res.headers["content-disposition"]).toBe(nonUtf8ToLatin1); - res.resume().on("end", () => { - server.close(resolve); - }); - }); - }); - }); -}); - -//<#END_FILE: test-http-server-non-utf8-header.js diff --git a/test/js/node/test/parallel/http-server-options-incoming-message.test.js b/test/js/node/test/parallel/http-server-options-incoming-message.test.js deleted file mode 100644 index e8c595a1aa..0000000000 --- a/test/js/node/test/parallel/http-server-options-incoming-message.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-http-server-options-incoming-message.js -//#SHA1: 5d553fff4a2a29f67836269914e5f33b7e91b64e -//----------------- -"use strict"; - -/** - * This test covers http.Server({ IncomingMessage }) option: - * With IncomingMessage option the server should use - * the new class for creating req Object instead of the default - * http.IncomingMessage. - */ -const http = require("http"); - -class MyIncomingMessage extends http.IncomingMessage { - getUserAgent() { - return this.headers["user-agent"] || "unknown"; - } -} - -test("http.Server with custom IncomingMessage", done => { - const server = http.createServer( - { - IncomingMessage: MyIncomingMessage, - }, - (req, res) => { - expect(req.getUserAgent()).toBe("node-test"); - res.statusCode = 200; - res.end(); - }, - ); - - server.listen(() => { - const { port } = server.address(); - - http.get( - { - port, - headers: { - "User-Agent": "node-test", - }, - }, - res => { - expect(res.statusCode).toBe(200); - res.on("end", () => { - server.close(); - done(); - }); - res.resume(); - }, - ); - }); -}); - -//<#END_FILE: test-http-server-options-incoming-message.js diff --git a/test/js/node/test/parallel/http-server-options-server-response.test.js b/test/js/node/test/parallel/http-server-options-server-response.test.js deleted file mode 100644 index 67155a18ac..0000000000 --- a/test/js/node/test/parallel/http-server-options-server-response.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-http-server-options-server-response.js -//#SHA1: ae3128a67e671596c2470bb973747640620b807a -//----------------- -"use strict"; - -/** - * This test covers http.Server({ ServerResponse }) option: - * With ServerResponse option the server should use - * the new class for creating res Object instead of the default - * http.ServerResponse. - */ -const http = require("http"); - -class MyServerResponse extends http.ServerResponse { - status(code) { - return this.writeHead(code, { "Content-Type": "text/plain" }); - } -} - -test("http.Server with custom ServerResponse", done => { - const server = http.Server( - { - ServerResponse: MyServerResponse, - }, - jest.fn((req, res) => { - res.status(200); - res.end(); - }), - ); - - server.listen(() => { - const port = server.address().port; - - http.get({ port }, res => { - expect(res.statusCode).toBe(200); - res.on("end", () => { - server.close(); - done(); - }); - res.resume(); - }); - }); - - server.on("close", () => { - expect(server.listeners("request")[0]).toHaveBeenCalledTimes(1); - }); -}); - -//<#END_FILE: test-http-server-options-server-response.js diff --git a/test/js/node/test/parallel/http-server-reject-chunked-with-content-length.test.js b/test/js/node/test/parallel/http-server-reject-chunked-with-content-length.test.js deleted file mode 100644 index 9bd2f8b82b..0000000000 --- a/test/js/node/test/parallel/http-server-reject-chunked-with-content-length.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-http-server-reject-chunked-with-content-length.js -//#SHA1: e94d6c381c99ba72c2cc2bcbc4c6474a7c63819a -//----------------- -"use strict"; - -const http = require("http"); -const net = require("net"); - -const reqstr = "POST / HTTP/1.1\r\n" + "Content-Length: 1\r\n" + "Transfer-Encoding: chunked\r\n\r\n"; - -test("HTTP server rejects chunked with content length", done => { - const server = http.createServer(expect.any(Function)); - - server.on("clientError", err => { - expect(err.message).toMatch(/^Parse Error/); - expect(err.code).toBe("HPE_INVALID_TRANSFER_ENCODING"); - server.close(); - }); - - server.listen(0, () => { - const client = net.connect({ port: server.address().port }, () => { - client.write(reqstr); - client.end(); - }); - - client.on("data", () => { - // Should not get to this point because the server should simply - // close the connection without returning any data. - throw new Error("no data should be returned by the server"); - }); - - client.on("end", () => { - done(); - }); - }); -}); - -//<#END_FILE: test-http-server-reject-chunked-with-content-length.js diff --git a/test/js/node/test/parallel/http-server-stale-close.test.js b/test/js/node/test/parallel/http-server-stale-close.test.js deleted file mode 100644 index ea0f99dcf0..0000000000 --- a/test/js/node/test/parallel/http-server-stale-close.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-http-server-stale-close.js -//#SHA1: 5c246ffb442bd9ff61779bc300db12d2f3394be4 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const fork = require("child_process").fork; - -if (process.env.NODE_TEST_FORK_PORT) { - const req = http.request( - { - headers: { "Content-Length": "42" }, - method: "POST", - host: "127.0.0.1", - port: +process.env.NODE_TEST_FORK_PORT, - }, - process.exit, - ); - req.write("BAM"); - req.end(); -} else { - test("HTTP server stale close", async () => { - const server = http.createServer((req, res) => { - res.writeHead(200, { "Content-Length": "42" }); - req.pipe(res); - expect(req.destroyed).toBe(false); - req.on("close", () => { - expect(req.destroyed).toBe(true); - server.close(); - res.end(); - }); - }); - - await new Promise(resolve => { - server.listen(0, function () { - fork(__filename, { - env: { ...process.env, NODE_TEST_FORK_PORT: this.address().port }, - }); - resolve(); - }); - }); - }); -} - -//<#END_FILE: test-http-server-stale-close.js diff --git a/test/js/node/test/parallel/http-server-write-after-end.test.js b/test/js/node/test/parallel/http-server-write-after-end.test.js deleted file mode 100644 index c842524c4b..0000000000 --- a/test/js/node/test/parallel/http-server-write-after-end.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-http-server-write-after-end.js -//#SHA1: cacf983393f707ddefc829a25ce16a5bf6f41c19 -//----------------- -"use strict"; - -const http = require("http"); - -// Fix for https://github.com/nodejs/node/issues/14368 - -test("HTTP server write after end", done => { - const server = http.createServer(handle); - - function handle(req, res) { - res.on("error", jest.fn()); - - res.write("hello"); - res.end(); - - setImmediate(() => { - res.write("world", err => { - expect(err).toEqual( - expect.objectContaining({ - code: "ERR_STREAM_WRITE_AFTER_END", - name: "Error", - message: expect.any(String), - }), - ); - server.close(); - done(); - }); - }); - } - - server.listen(0, () => { - http.get(`http://localhost:${server.address().port}`); - }); -}); - -//<#END_FILE: test-http-server-write-after-end.js diff --git a/test/js/node/test/parallel/http-server-write-end-after-end.test.js b/test/js/node/test/parallel/http-server-write-end-after-end.test.js deleted file mode 100644 index 836b835e1f..0000000000 --- a/test/js/node/test/parallel/http-server-write-end-after-end.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-http-server-write-end-after-end.js -//#SHA1: 5b7550b3241cd6b99e607419c3b81d2df519b641 -//----------------- -"use strict"; - -const http = require("http"); - -let server; - -beforeAll(() => { - server = http.createServer(handle); -}); - -afterAll(() => { - server.close(); -}); - -function handle(req, res) { - res.on("error", jest.fn()); - - res.write("hello"); - res.end(); - - setImmediate(() => { - res.end("world"); - process.nextTick(() => { - server.close(); - }); - res.write("world", err => { - expect(err).toMatchObject({ - code: "ERR_STREAM_WRITE_AFTER_END", - name: "Error", - message: expect.any(String), - }); - server.close(); - }); - }); -} - -test("http server write end after end", done => { - server.listen(0, () => { - http.get(`http://localhost:${server.address().port}`); - done(); - }); -}); - -//<#END_FILE: test-http-server-write-end-after-end.js diff --git a/test/js/node/test/parallel/http-set-header-chain.test.js b/test/js/node/test/parallel/http-set-header-chain.test.js deleted file mode 100644 index 85aac8f9e1..0000000000 --- a/test/js/node/test/parallel/http-set-header-chain.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-http-set-header-chain.js -//#SHA1: e009f5ffdce12a659bd2d3402449cd0095d79aa2 -//----------------- -"use strict"; - -const http = require("http"); - -const expected = { - __proto__: null, - testheader1: "foo", - testheader2: "bar", - testheader3: "xyz", -}; - -test("HTTP setHeader chaining", async () => { - const server = http.createServer((req, res) => { - let retval = res.setHeader("testheader1", "foo"); - - // Test that the setHeader returns the same response object. - expect(retval).toBe(res); - - retval = res.setHeader("testheader2", "bar").setHeader("testheader3", "xyz"); - // Test that chaining works for setHeader. - expect(res.getHeaders()).toEqual(expected); - res.end("ok"); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - res.on("data", () => {}); - res.on("end", () => { - server.close(resolve); - }); - }); - }); - }); -}); - -//<#END_FILE: test-http-set-header-chain.js diff --git a/test/js/node/test/parallel/http-upgrade-reconsume-stream.test.js b/test/js/node/test/parallel/http-upgrade-reconsume-stream.test.js deleted file mode 100644 index 57d72bf41d..0000000000 --- a/test/js/node/test/parallel/http-upgrade-reconsume-stream.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-http-upgrade-reconsume-stream.js -//#SHA1: 4117d0b2212d192173b5bd6bf2ef7fe82f627079 -//----------------- -"use strict"; - -const tls = require("tls"); -const http = require("http"); - -// Tests that, after the HTTP parser stopped owning a socket that emits an -// 'upgrade' event, another C++ stream can start owning it (e.g. a TLSSocket). - -test("HTTP upgrade and TLSSocket creation", done => { - const server = http.createServer(expect.any(Function)); - - server.on("upgrade", (request, socket, head) => { - // This should not crash. - new tls.TLSSocket(socket); - server.close(); - socket.destroy(); - done(); - }); - - server.listen(0, () => { - http - .get({ - port: server.address().port, - headers: { - "Connection": "Upgrade", - "Upgrade": "websocket", - }, - }) - .on("error", () => {}); - }); -}); - -//<#END_FILE: test-http-upgrade-reconsume-stream.js diff --git a/test/js/node/test/parallel/http-url.parse-auth-with-header-in-request.test.js b/test/js/node/test/parallel/http-url.parse-auth-with-header-in-request.test.js deleted file mode 100644 index a87bfbd3ac..0000000000 --- a/test/js/node/test/parallel/http-url.parse-auth-with-header-in-request.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-http-url.parse-auth-with-header-in-request.js -//#SHA1: 396adc5e441a57d24b11a42513c834b6b11ea7ff -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -test("HTTP request with authorization header in request", async () => { - function check(request) { - // The correct authorization header is be passed - expect(request.headers.authorization).toBe("NoAuthForYOU"); - } - - const server = http.createServer((request, response) => { - // Run the check function - check(request); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const testURL = url.parse(`http://asdf:qwer@localhost:${server.address().port}`); - // The test here is if you set a specific authorization header in the - // request we should not override that with basic auth - testURL.headers = { - Authorization: "NoAuthForYOU", - }; - - // make the request - http.request(testURL).end(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-url.parse-auth-with-header-in-request.js diff --git a/test/js/node/test/parallel/http-url.parse-auth.test.js b/test/js/node/test/parallel/http-url.parse-auth.test.js deleted file mode 100644 index 439d699393..0000000000 --- a/test/js/node/test/parallel/http-url.parse-auth.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-http-url.parse-auth.js -//#SHA1: 97f9b1c737c705489b2d6402750034291a9f6f63 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -test("http url parse auth", async () => { - function check(request) { - // The correct authorization header is be passed - expect(request.headers.authorization).toBe("Basic dXNlcjpwYXNzOg=="); - } - - const server = http.createServer((request, response) => { - // Run the check function - check(request); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const port = server.address().port; - // username = "user", password = "pass:" - const testURL = url.parse(`http://user:pass%3A@localhost:${port}`); - - // make the request - http.request(testURL).end(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-url.parse-auth.js diff --git a/test/js/node/test/parallel/http-url.parse-basic.test.js b/test/js/node/test/parallel/http-url.parse-basic.test.js deleted file mode 100644 index 9d2d329d3f..0000000000 --- a/test/js/node/test/parallel/http-url.parse-basic.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-http-url.parse-basic.js -//#SHA1: f2f2841de1c82e38067e73196926090f350d89c6 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -let testURL; - -// Make sure the basics work -function check(request) { - // Default method should still be 'GET' - expect(request.method).toBe("GET"); - // There are no URL params, so you should not see any - expect(request.url).toBe("/"); - // The host header should use the url.parse.hostname - expect(request.headers.host).toBe(`${testURL.hostname}:${testURL.port}`); -} - -test("HTTP URL parsing basics", async () => { - const server = http.createServer((request, response) => { - // Run the check function - check(request); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - testURL = url.parse(`http://localhost:${server.address().port}`); - - // make the request - const clientRequest = http.request(testURL); - // Since there is a little magic with the agent - // make sure that an http request uses the http.Agent - expect(clientRequest.agent).toBeInstanceOf(http.Agent); - clientRequest.end(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-url.parse-basic.js diff --git a/test/js/node/test/parallel/http-url.parse-https.request.test.js b/test/js/node/test/parallel/http-url.parse-https.request.test.js deleted file mode 100644 index 6b8a992569..0000000000 --- a/test/js/node/test/parallel/http-url.parse-https.request.test.js +++ /dev/null @@ -1,81 +0,0 @@ -//#FILE: test-http-url.parse-https.request.js -//#SHA1: e9b9e39f28d5d2633f9444150977b748bc8995cb -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const https = require("https"); -const url = require("url"); -const { readKey } = require("../common/fixtures"); - -let common; -try { - common = require("../common"); -} catch (e) { - // For Bun compatibility - common = { - hasCrypto: true, - skip: console.log, - }; -} - -if (!common.hasCrypto) { - common.skip("missing crypto"); - process.exit(0); -} - -// https options -const httpsOptions = { - key: readKey("agent1-key.pem"), - cert: readKey("agent1-cert.pem"), -}; - -function check(request) { - // Assert that I'm https - expect(request.socket._secureEstablished).toBeTruthy(); -} - -test("HTTPS request with URL object", done => { - const server = https.createServer(httpsOptions, function (request, response) { - // Run the check function - check(request); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - server.listen(0, function () { - const testURL = url.parse(`https://localhost:${this.address().port}`); - testURL.rejectUnauthorized = false; - - // make the request - const clientRequest = https.request(testURL); - // Since there is a little magic with the agent - // make sure that the request uses the https.Agent - expect(clientRequest.agent).toBeInstanceOf(https.Agent); - clientRequest.end(); - done(); - }); -}); - -//<#END_FILE: test-http-url.parse-https.request.js diff --git a/test/js/node/test/parallel/http-url.parse-only-support-http-https-protocol.test.js b/test/js/node/test/parallel/http-url.parse-only-support-http-https-protocol.test.js deleted file mode 100644 index 4f3c5dd20a..0000000000 --- a/test/js/node/test/parallel/http-url.parse-only-support-http-https-protocol.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-http-url.parse-only-support-http-https-protocol.js -//#SHA1: 924c029f73164388b765c128401affa763af7b56 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -const invalidUrls = [ - "file:///whatever", - "mailto:asdf@asdf.com", - "ftp://www.example.com", - "javascript:alert('hello');", - "xmpp:foo@bar.com", - "f://some.host/path", -]; - -describe("http.request with invalid protocols", () => { - test.each(invalidUrls)("throws for invalid URL: %s", invalid => { - expect(() => { - http.request(url.parse(invalid)); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_PROTOCOL", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-http-url.parse-only-support-http-https-protocol.js diff --git a/test/js/node/test/parallel/http-url.parse-path.test.js b/test/js/node/test/parallel/http-url.parse-path.test.js deleted file mode 100644 index 21b456b66f..0000000000 --- a/test/js/node/test/parallel/http-url.parse-path.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-http-url.parse-path.js -//#SHA1: 9eb246a6c09b70b76260a83bec4bb25452d38b7d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -test("http request url parsing", async () => { - function check(request) { - // A path should come over - expect(request.url).toBe("/asdf"); - } - - const server = http.createServer((request, response) => { - // Run the check function - check(request); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const testURL = url.parse(`http://localhost:${server.address().port}/asdf`); - - // make the request - http.request(testURL).end(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-url.parse-path.js diff --git a/test/js/node/test/parallel/http-url.parse-post.test.js b/test/js/node/test/parallel/http-url.parse-post.test.js deleted file mode 100644 index 800cde5e39..0000000000 --- a/test/js/node/test/parallel/http-url.parse-post.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http-url.parse-post.js -//#SHA1: e0e7f97c725fb9eaa6058365bef5021e9710e857 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -let testURL; - -function check(request) { - // url.parse should not mess with the method - expect(request.method).toBe("POST"); - // Everything else should be right - expect(request.url).toBe("/asdf?qwer=zxcv"); - // The host header should use the url.parse.hostname - expect(request.headers.host).toBe(`${testURL.hostname}:${testURL.port}`); -} - -test("http url parse post", async () => { - const server = http.createServer((request, response) => { - // Run the check function - check(request); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - testURL = url.parse(`http://localhost:${server.address().port}/asdf?qwer=zxcv`); - testURL.method = "POST"; - - // make the request - http.request(testURL).end(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-url.parse-post.js diff --git a/test/js/node/test/parallel/http-url.parse-search.test.js b/test/js/node/test/parallel/http-url.parse-search.test.js deleted file mode 100644 index fe07df2f63..0000000000 --- a/test/js/node/test/parallel/http-url.parse-search.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-http-url.parse-search.js -//#SHA1: 11d08b9c62625b7b554d5fb46d63c4aaa77c1a7c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); -const url = require("url"); - -test("HTTP request URL parsing with search params", async () => { - function check(request) { - // A path should come over with params - expect(request.url).toBe("/asdf?qwer=zxcv"); - } - - const server = http.createServer((request, response) => { - // Run the check function - check(request); - response.writeHead(200, {}); - response.end("ok"); - server.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const port = server.address().port; - const testURL = url.parse(`http://localhost:${port}/asdf?qwer=zxcv`); - - // make the request - http.request(testURL).end(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-http-url.parse-search.js diff --git a/test/js/node/test/parallel/http-write-empty-string.test.js b/test/js/node/test/parallel/http-write-empty-string.test.js deleted file mode 100644 index 16e1b0def3..0000000000 --- a/test/js/node/test/parallel/http-write-empty-string.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http-write-empty-string.js -//#SHA1: 779199784d3142e353324041eeb30924c7e4d5b1 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const http = require("http"); - -test("http write empty string", async () => { - const server = http.createServer(function (request, response) { - console.log(`responding to ${request.url}`); - - response.writeHead(200, { "Content-Type": "text/plain" }); - response.write("1\n"); - response.write(""); - response.write("2\n"); - response.write(""); - response.end("3\n"); - - this.close(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - let response = ""; - - expect(res.statusCode).toBe(200); - res.setEncoding("ascii"); - res.on("data", chunk => { - response += chunk; - }); - res.on("end", () => { - expect(response).toBe("1\n2\n3\n"); - resolve(); - }); - }); - }); - }); -}); - -//<#END_FILE: test-http-write-empty-string.js diff --git a/test/js/node/test/parallel/http-zerolengthbuffer.test.js b/test/js/node/test/parallel/http-zerolengthbuffer.test.js deleted file mode 100644 index 8c82c7a82f..0000000000 --- a/test/js/node/test/parallel/http-zerolengthbuffer.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-http-zerolengthbuffer.js -//#SHA1: 28fff143238744f829f63936c8902047ad2c2fc5 -//----------------- -"use strict"; -// Serving up a zero-length buffer should work. - -const http = require("http"); - -test("Serve zero-length buffer", done => { - const server = http.createServer((req, res) => { - const buffer = Buffer.alloc(0); - res.writeHead(200, { "Content-Type": "text/html", "Content-Length": buffer.length }); - res.end(buffer); - }); - - server.listen(0, () => { - http.get({ port: server.address().port }, res => { - const dataHandler = jest.fn(); - res.on("data", dataHandler); - - res.on("end", () => { - expect(dataHandler).not.toHaveBeenCalled(); - server.close(); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-http-zerolengthbuffer.js diff --git a/test/js/node/test/parallel/http2-binding.test.js b/test/js/node/test/parallel/http2-binding.test.js deleted file mode 100644 index a7ea7fb939..0000000000 --- a/test/js/node/test/parallel/http2-binding.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-http2-binding.js -//#SHA1: 73c6e6b3c2f9b4c9c06183713dbf28454185a1a0 -//----------------- -const http2 = require('http2'); - -describe('HTTP/2 Binding', () => { - beforeAll(() => { - // Skip all tests in this file - jest.spyOn(console, 'log').mockImplementation(() => {}); - console.log('Skipping HTTP/2 binding tests - internal bindings not available'); - }); - - test('SKIP: HTTP/2 binding tests', () => { - expect(true).toBe(true); - }); -}); - -//<#END_FILE: test-http2-binding.test.js diff --git a/test/js/node/test/parallel/http2-client-priority-before-connect.test.js b/test/js/node/test/parallel/http2-client-priority-before-connect.test.js deleted file mode 100644 index 273aa7bf44..0000000000 --- a/test/js/node/test/parallel/http2-client-priority-before-connect.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-http2-client-priority-before-connect.js -//#SHA1: bc94924856dc82c18ccf699d467d63c28fed0d13 -//----------------- -'use strict'; - -const h2 = require('http2'); - -let server; -let port; - -beforeAll(async () => { - // Check if crypto is available - try { - require('crypto'); - } catch (err) { - return test.skip('missing crypto'); - } -}); - -afterAll(() => { - if (server) { - server.close(); - } -}); - -test('HTTP2 client priority before connect', (done) => { - server = h2.createServer(); - - // We use the lower-level API here - server.on('stream', (stream) => { - stream.respond(); - stream.end('ok'); - }); - - server.listen(0, () => { - port = server.address().port; - const client = h2.connect(`http://localhost:${port}`); - const req = client.request(); - req.priority({}); - - req.on('response', () => { - // Response received - }); - - req.resume(); - - req.on('end', () => { - // Request ended - }); - - req.on('close', () => { - client.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-http2-client-priority-before-connect.js diff --git a/test/js/node/test/parallel/http2-client-request-listeners-warning.test.js b/test/js/node/test/parallel/http2-client-request-listeners-warning.test.js deleted file mode 100644 index a560ec53ad..0000000000 --- a/test/js/node/test/parallel/http2-client-request-listeners-warning.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-http2-client-request-listeners-warning.js -//#SHA1: cb4f9a71d1f670a78f989caed948e88fa5dbd681 -//----------------- -"use strict"; -const http2 = require("http2"); -const EventEmitter = require("events"); - -// Skip the test if crypto is not available -let hasCrypto; -try { - require("crypto"); - hasCrypto = true; -} catch (err) { - hasCrypto = false; -} - -(hasCrypto ? describe : describe.skip)("HTTP2 client request listeners warning", () => { - let server; - let port; - - beforeAll(done => { - server = http2.createServer(); - server.on("stream", stream => { - stream.respond(); - stream.end(); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("should not emit MaxListenersExceededWarning", done => { - const warningListener = jest.fn(); - process.on("warning", warningListener); - - const client = http2.connect(`http://localhost:${port}`); - - function request() { - return new Promise((resolve, reject) => { - const stream = client.request(); - stream.on("error", reject); - stream.on("response", resolve); - stream.end(); - }); - } - - const requests = []; - for (let i = 0; i < EventEmitter.defaultMaxListeners + 1; i++) { - requests.push(request()); - } - - Promise.all(requests) - .then(() => { - expect(warningListener).not.toHaveBeenCalled(); - }) - .finally(() => { - process.removeListener("warning", warningListener); - client.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-http2-client-request-listeners-warning.js diff --git a/test/js/node/test/parallel/http2-client-shutdown-before-connect.test.js b/test/js/node/test/parallel/http2-client-shutdown-before-connect.test.js deleted file mode 100644 index 18091d3a31..0000000000 --- a/test/js/node/test/parallel/http2-client-shutdown-before-connect.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-http2-client-shutdown-before-connect.js -//#SHA1: 75a343e9d8b577911242f867708310346fe9ddce -//----------------- -'use strict'; - -const h2 = require('http2'); - -// Skip test if crypto is not available -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -if (!hasCrypto) { - test.skip('missing crypto', () => {}); -} else { - test('HTTP/2 client shutdown before connect', (done) => { - const server = h2.createServer(); - - // We use the lower-level API here - server.on('stream', () => { - throw new Error('Stream should not be created'); - }); - - server.listen(0, () => { - const client = h2.connect(`http://localhost:${server.address().port}`); - client.close(() => { - server.close(() => { - done(); - }); - }); - }); - }); -} - -//<#END_FILE: test-http2-client-shutdown-before-connect.js diff --git a/test/js/node/test/parallel/http2-client-write-before-connect.test.js b/test/js/node/test/parallel/http2-client-write-before-connect.test.js deleted file mode 100644 index b245680da9..0000000000 --- a/test/js/node/test/parallel/http2-client-write-before-connect.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-http2-client-write-before-connect.js -//#SHA1: f38213aa6b5fb615d5b80f0213022ea06e2705cc -//----------------- -'use strict'; - -const h2 = require('http2'); - -let server; -let client; - -beforeAll(() => { - if (!process.versions.openssl) { - test.skip('missing crypto'); - return; - } -}); - -afterEach(() => { - if (client) { - client.close(); - } - if (server) { - server.close(); - } -}); - -test('HTTP/2 client write before connect', (done) => { - server = h2.createServer(); - - server.on('stream', (stream, headers, flags) => { - let data = ''; - stream.setEncoding('utf8'); - stream.on('data', (chunk) => data += chunk); - stream.on('end', () => { - expect(data).toBe('some data more data'); - }); - stream.respond(); - stream.end('ok'); - }); - - server.listen(0, () => { - const port = server.address().port; - client = h2.connect(`http://localhost:${port}`); - - const req = client.request({ ':method': 'POST' }); - req.write('some data '); - req.end('more data'); - - req.on('response', () => {}); - req.resume(); - req.on('end', () => {}); - req.on('close', () => { - done(); - }); - }); -}); - -//<#END_FILE: test-http2-client-write-before-connect.js diff --git a/test/js/node/test/parallel/http2-client-write-empty-string.test.js b/test/js/node/test/parallel/http2-client-write-empty-string.test.js deleted file mode 100644 index daf8182df6..0000000000 --- a/test/js/node/test/parallel/http2-client-write-empty-string.test.js +++ /dev/null @@ -1,74 +0,0 @@ -//#FILE: test-http2-client-write-empty-string.js -//#SHA1: d4371ceba660942fe3c398bbb3144ce691054cec -//----------------- -'use strict'; - -const http2 = require('http2'); - -const runTest = async (chunkSequence) => { - return new Promise((resolve, reject) => { - const server = http2.createServer(); - server.on('stream', (stream, headers, flags) => { - stream.respond({ 'content-type': 'text/html' }); - - let data = ''; - stream.on('data', (chunk) => { - data += chunk.toString(); - }); - stream.on('end', () => { - stream.end(`"${data}"`); - }); - }); - - server.listen(0, async () => { - const port = server.address().port; - const client = http2.connect(`http://localhost:${port}`); - - const req = client.request({ - ':method': 'POST', - ':path': '/' - }); - - req.on('response', (headers) => { - expect(headers[':status']).toBe(200); - expect(headers['content-type']).toBe('text/html'); - }); - - let data = ''; - req.setEncoding('utf8'); - req.on('data', (d) => data += d); - req.on('end', () => { - expect(data).toBe('""'); - server.close(); - client.close(); - resolve(); - }); - - for (const chunk of chunkSequence) { - req.write(chunk); - } - req.end(); - }); - }); -}; - -const testCases = [ - [''], - ['', ''] -]; - -describe('http2 client write empty string', () => { - beforeAll(() => { - if (typeof http2 === 'undefined') { - return test.skip('http2 module not available'); - } - }); - - testCases.forEach((chunkSequence, index) => { - it(`should handle chunk sequence ${index + 1}`, async () => { - await runTest(chunkSequence); - }); - }); -}); - -//<#END_FILE: test-http2-client-write-empty-string.js diff --git a/test/js/node/test/parallel/http2-compat-aborted.test.js b/test/js/node/test/parallel/http2-compat-aborted.test.js deleted file mode 100644 index b304d69e16..0000000000 --- a/test/js/node/test/parallel/http2-compat-aborted.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-http2-compat-aborted.js -//#SHA1: 2aaf11840d98c2b8f4387473180ec86626ac48d1 -//----------------- -"use strict"; - -const h2 = require("http2"); - -let server; -let port; - -beforeAll(done => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } - server = h2.createServer((req, res) => { - req.on("aborted", () => { - expect(req.aborted).toBe(true); - expect(req.complete).toBe(true); - }); - expect(req.aborted).toBe(false); - expect(req.complete).toBe(false); - res.write("hello"); - server.close(); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll(() => { - if (server) { - server.close(); - } -}); - -test("HTTP/2 compat aborted", done => { - const url = `http://localhost:${port}`; - const client = h2.connect(url, () => { - const request = client.request(); - request.on("data", chunk => { - client.destroy(); - }); - request.on("end", () => { - done(); - }); - }); - - client.on("error", err => { - // Ignore client errors as we're forcibly destroying the connection - }); -}); - -//<#END_FILE: test-http2-compat-aborted.js diff --git a/test/js/node/test/parallel/http2-compat-client-upload-reject.test.js b/test/js/node/test/parallel/http2-compat-client-upload-reject.test.js deleted file mode 100644 index a9e085022b..0000000000 --- a/test/js/node/test/parallel/http2-compat-client-upload-reject.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-http2-compat-client-upload-reject.js -//#SHA1: 4dff98612ac613af951070f79f07f5c1750045da -//----------------- -'use strict'; - -const http2 = require('http2'); -const fs = require('fs'); -const path = require('path'); - -const fixturesPath = path.resolve(__dirname, '..', 'fixtures'); -const loc = path.join(fixturesPath, 'person-large.jpg'); - -let server; -let client; - -beforeAll(() => { - if (!process.versions.openssl) { - return test.skip('missing crypto'); - } -}); - -afterEach(() => { - if (server) server.close(); - if (client) client.close(); -}); - -test('HTTP/2 client upload reject', (done) => { - expect(fs.existsSync(loc)).toBe(true); - - fs.readFile(loc, (err, data) => { - expect(err).toBeNull(); - - server = http2.createServer((req, res) => { - setImmediate(() => { - res.writeHead(400); - res.end(); - }); - }); - - server.listen(0, () => { - const port = server.address().port; - client = http2.connect(`http://localhost:${port}`); - - const req = client.request({ ':method': 'POST' }); - req.on('response', (headers) => { - expect(headers[':status']).toBe(400); - }); - - req.resume(); - req.on('end', () => { - server.close(); - client.close(); - done(); - }); - - const str = fs.createReadStream(loc); - str.pipe(req); - }); - }); -}); - -//<#END_FILE: test-http2-compat-client-upload-reject.js diff --git a/test/js/node/test/parallel/http2-compat-errors.test.js b/test/js/node/test/parallel/http2-compat-errors.test.js deleted file mode 100644 index e326447865..0000000000 --- a/test/js/node/test/parallel/http2-compat-errors.test.js +++ /dev/null @@ -1,67 +0,0 @@ -//#FILE: test-http2-compat-errors.js -//#SHA1: 3a958d2216c02d05272fbc89bd09a532419876a4 -//----------------- -'use strict'; - -const h2 = require('http2'); - -// Simulate crypto check -const hasCrypto = true; -if (!hasCrypto) { - test.skip('missing crypto', () => {}); -} else { - let expected = null; - - describe('http2 compat errors', () => { - let server; - let url; - - beforeAll((done) => { - server = h2.createServer((req, res) => { - const resStreamErrorHandler = jest.fn(); - const reqErrorHandler = jest.fn(); - const resErrorHandler = jest.fn(); - const reqAbortedHandler = jest.fn(); - const resAbortedHandler = jest.fn(); - - res.stream.on('error', resStreamErrorHandler); - req.on('error', reqErrorHandler); - res.on('error', resErrorHandler); - req.on('aborted', reqAbortedHandler); - res.on('aborted', resAbortedHandler); - - res.write('hello'); - - expected = new Error('kaboom'); - res.stream.destroy(expected); - - // Use setImmediate to allow event handlers to be called - setImmediate(() => { - expect(resStreamErrorHandler).toHaveBeenCalled(); - expect(reqErrorHandler).not.toHaveBeenCalled(); - expect(resErrorHandler).not.toHaveBeenCalled(); - expect(reqAbortedHandler).toHaveBeenCalled(); - expect(resAbortedHandler).not.toHaveBeenCalled(); - server.close(done); - }); - }); - - server.listen(0, () => { - url = `http://localhost:${server.address().port}`; - done(); - }); - }); - - test('should handle errors correctly', (done) => { - const client = h2.connect(url, () => { - const request = client.request(); - request.on('data', (chunk) => { - client.destroy(); - done(); - }); - }); - }); - }); -} - -//<#END_FILE: test-http2-compat-errors.js diff --git a/test/js/node/test/parallel/http2-compat-expect-continue-check.test.js b/test/js/node/test/parallel/http2-compat-expect-continue-check.test.js deleted file mode 100644 index 8ee10f45fd..0000000000 --- a/test/js/node/test/parallel/http2-compat-expect-continue-check.test.js +++ /dev/null @@ -1,77 +0,0 @@ -//#FILE: test-http2-compat-expect-continue-check.js -//#SHA1: cfaba2929ccb61aa085572010d7730ceef07859e -//----------------- -'use strict'; - -const http2 = require('http2'); - -const testResBody = 'other stuff!\n'; - -describe('HTTP/2 100-continue flow', () => { - let server; - - beforeAll(() => { - if (!process.versions.openssl) { - return test.skip('missing crypto'); - } - }); - - afterEach(() => { - if (server) { - server.close(); - } - }); - - test('Full 100-continue flow', (done) => { - server = http2.createServer(); - const fullRequestHandler = jest.fn(); - server.on('request', fullRequestHandler); - - server.on('checkContinue', (req, res) => { - res.writeContinue(); - res.writeHead(200, {}); - res.end(testResBody); - - expect(res.writeContinue()).toBe(false); - - res.on('finish', () => { - process.nextTick(() => { - expect(res.writeContinue()).toBe(false); - }); - }); - }); - - server.listen(0, () => { - let body = ''; - - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ - ':method': 'POST', - 'expect': '100-continue' - }); - - let gotContinue = false; - req.on('continue', () => { - gotContinue = true; - }); - - req.on('response', (headers) => { - expect(gotContinue).toBe(true); - expect(headers[':status']).toBe(200); - req.end(); - }); - - req.setEncoding('utf-8'); - req.on('data', (chunk) => { body += chunk; }); - - req.on('end', () => { - expect(body).toBe(testResBody); - expect(fullRequestHandler).not.toHaveBeenCalled(); - client.close(); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-http2-compat-expect-continue-check.js diff --git a/test/js/node/test/parallel/http2-compat-expect-continue.test.js b/test/js/node/test/parallel/http2-compat-expect-continue.test.js deleted file mode 100644 index b2e98efb5d..0000000000 --- a/test/js/node/test/parallel/http2-compat-expect-continue.test.js +++ /dev/null @@ -1,98 +0,0 @@ -//#FILE: test-http2-compat-expect-continue.js -//#SHA1: 3c95de1bb9a0bf620945ec5fc39ba3a515dfe5fd -//----------------- -'use strict'; - -const http2 = require('http2'); - -// Skip the test if crypto is not available -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -if (!hasCrypto) { - test.skip('missing crypto', () => {}); -} else { - describe('HTTP/2 100-continue flow', () => { - test('full 100-continue flow with response', (done) => { - const testResBody = 'other stuff!\n'; - const server = http2.createServer(); - let sentResponse = false; - - server.on('request', (req, res) => { - res.end(testResBody); - sentResponse = true; - }); - - server.listen(0, () => { - let body = ''; - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ - ':method': 'POST', - 'expect': '100-continue' - }); - - let gotContinue = false; - req.on('continue', () => { - gotContinue = true; - }); - - req.on('response', (headers) => { - expect(gotContinue).toBe(true); - expect(sentResponse).toBe(true); - expect(headers[':status']).toBe(200); - req.end(); - }); - - req.setEncoding('utf8'); - req.on('data', (chunk) => { body += chunk; }); - req.on('end', () => { - expect(body).toBe(testResBody); - client.close(); - server.close(done); - }); - }); - }); - - test('100-continue flow with immediate response', (done) => { - const server = http2.createServer(); - - server.on('request', (req, res) => { - res.end(); - }); - - server.listen(0, () => { - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ - ':path': '/', - 'expect': '100-continue' - }); - - let gotContinue = false; - req.on('continue', () => { - gotContinue = true; - }); - - let gotResponse = false; - req.on('response', () => { - gotResponse = true; - }); - - req.setEncoding('utf8'); - req.on('end', () => { - expect(gotContinue).toBe(true); - expect(gotResponse).toBe(true); - client.close(); - server.close(done); - }); - }); - }); - }); -} - -//<#END_FILE: test-http2-compat-expect-continue.js diff --git a/test/js/node/test/parallel/http2-compat-expect-handling.test.js b/test/js/node/test/parallel/http2-compat-expect-handling.test.js deleted file mode 100644 index 2a1940ae23..0000000000 --- a/test/js/node/test/parallel/http2-compat-expect-handling.test.js +++ /dev/null @@ -1,96 +0,0 @@ -//#FILE: test-http2-compat-expect-handling.js -//#SHA1: 015a7b40547c969f4d631e7e743f5293d9e8f843 -//----------------- -"use strict"; - -const http2 = require("http2"); - -const hasCrypto = (() => { - try { - require("crypto"); - return true; - } catch (err) { - return false; - } -})(); - -const expectValue = "meoww"; - -describe("HTTP/2 Expect Header Handling", () => { - let server; - let port; - - beforeAll(done => { - server = http2.createServer(); - server.listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("server should not call request handler", () => { - const requestHandler = jest.fn(); - server.on("request", requestHandler); - - return new Promise(resolve => { - server.once("checkExpectation", (req, res) => { - expect(req.headers.expect).toBe(expectValue); - res.statusCode = 417; - res.end(); - expect(requestHandler).not.toHaveBeenCalled(); - resolve(); - }); - - const client = http2.connect(`http://localhost:${port}`); - const req = client.request({ - ":path": "/", - ":method": "GET", - ":scheme": "http", - ":authority": `localhost:${port}`, - "expect": expectValue, - }); - - req.on("response", headers => { - expect(headers[":status"]).toBe(417); - req.resume(); - }); - - req.on("end", () => { - client.close(); - }); - }); - }); - - test("client should receive 417 status", () => { - return new Promise(resolve => { - const client = http2.connect(`http://localhost:${port}`); - const req = client.request({ - ":path": "/", - ":method": "GET", - ":scheme": "http", - ":authority": `localhost:${port}`, - "expect": expectValue, - }); - - req.on("response", headers => { - expect(headers[":status"]).toBe(417); - req.resume(); - }); - - req.on("end", () => { - client.close(); - resolve(); - }); - }); - }); -}); - -if (!hasCrypto) { - test.skip("skipping HTTP/2 tests due to missing crypto support", () => {}); -} - -//<#END_FILE: test-http2-compat-expect-handling.js diff --git a/test/js/node/test/parallel/http2-compat-serverrequest-pause.test.js b/test/js/node/test/parallel/http2-compat-serverrequest-pause.test.js deleted file mode 100644 index a42d021210..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverrequest-pause.test.js +++ /dev/null @@ -1,75 +0,0 @@ -//#FILE: test-http2-compat-serverrequest-pause.js -//#SHA1: 3f3eff95f840e6321b0d25211ef5116304049dc7 -//----------------- -'use strict'; - -const h2 = require('http2'); - -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -if (!hasCrypto) { - test.skip('missing crypto', () => {}); -} else { - const testStr = 'Request Body from Client'; - let server; - let client; - - beforeAll(() => { - server = h2.createServer(); - }); - - afterAll(() => { - if (client) client.close(); - if (server) server.close(); - }); - - test('pause & resume work as expected with Http2ServerRequest', (done) => { - const requestHandler = jest.fn((req, res) => { - let data = ''; - req.pause(); - req.setEncoding('utf8'); - req.on('data', jest.fn((chunk) => (data += chunk))); - setTimeout(() => { - expect(data).toBe(''); - req.resume(); - }, 100); - req.on('end', () => { - expect(data).toBe(testStr); - res.end(); - }); - - res.on('finish', () => process.nextTick(() => { - req.pause(); - req.resume(); - })); - }); - - server.on('request', requestHandler); - - server.listen(0, () => { - const port = server.address().port; - - client = h2.connect(`http://localhost:${port}`); - const request = client.request({ - ':path': '/foobar', - ':method': 'POST', - ':scheme': 'http', - ':authority': `localhost:${port}` - }); - request.resume(); - request.end(testStr); - request.on('end', () => { - expect(requestHandler).toHaveBeenCalled(); - done(); - }); - }); - }); -} -//<#END_FILE: test-http2-compat-serverrequest-pause.js diff --git a/test/js/node/test/parallel/http2-compat-serverrequest-pipe.test.js b/test/js/node/test/parallel/http2-compat-serverrequest-pipe.test.js deleted file mode 100644 index 47ed561685..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverrequest-pipe.test.js +++ /dev/null @@ -1,69 +0,0 @@ -//#FILE: test-http2-compat-serverrequest-pipe.js -//#SHA1: c4254ac88df3334dccc8adb4b60856193a6e644e -//----------------- -"use strict"; - -const http2 = require("http2"); -const fs = require("fs"); -const path = require("path"); -const os = require("os"); -const { isWindows } = require("harness"); - -const fixtures = path.join(__dirname, "..", "fixtures"); -const tmpdir = os.tmpdir(); - -let server; -let client; -let port; - -beforeAll(async () => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } - - await fs.promises.mkdir(tmpdir, { recursive: true }); -}); - -afterAll(async () => { - if (server) server.close(); - if (client) client.close(); -}); - -test.todoIf(isWindows)("HTTP/2 server request pipe", done => { - const loc = path.join(fixtures, "person-large.jpg"); - const fn = path.join(tmpdir, "http2-url-tests.js"); - - server = http2.createServer(); - - server.on("request", (req, res) => { - const dest = req.pipe(fs.createWriteStream(fn)); - dest.on("finish", () => { - expect(req.complete).toBe(true); - expect(fs.readFileSync(loc).length).toBe(fs.readFileSync(fn).length); - fs.unlinkSync(fn); - res.end(); - }); - }); - - server.listen(0, () => { - port = server.address().port; - client = http2.connect(`http://localhost:${port}`); - - let remaining = 2; - function maybeClose() { - if (--remaining === 0) { - done(); - } - } - - const req = client.request({ ":method": "POST" }); - req.on("response", () => {}); - req.resume(); - req.on("end", maybeClose); - const str = fs.createReadStream(loc); - str.on("end", maybeClose); - str.pipe(req); - }); -}); - -//<#END_FILE: test-http2-compat-serverrequest-pipe.js diff --git a/test/js/node/test/parallel/http2-compat-serverrequest.test.js b/test/js/node/test/parallel/http2-compat-serverrequest.test.js deleted file mode 100644 index 2349965420..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverrequest.test.js +++ /dev/null @@ -1,69 +0,0 @@ -//#FILE: test-http2-compat-serverrequest.js -//#SHA1: f661c6c9249c0cdc770439f7498943fc5edbf86b -//----------------- -"use strict"; - -const h2 = require("http2"); -const net = require("net"); - -let server; -let port; - -beforeAll(done => { - server = h2.createServer(); - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll(done => { - server.close(done); -}); - -// today we deatch the socket earlier -test.todo("Http2ServerRequest should expose convenience properties", done => { - expect.assertions(7); - - server.once("request", (request, response) => { - const expected = { - version: "2.0", - httpVersionMajor: 2, - httpVersionMinor: 0, - }; - - expect(request.httpVersion).toBe(expected.version); - expect(request.httpVersionMajor).toBe(expected.httpVersionMajor); - expect(request.httpVersionMinor).toBe(expected.httpVersionMinor); - - expect(request.socket).toBeInstanceOf(net.Socket); - expect(request.connection).toBeInstanceOf(net.Socket); - expect(request.socket).toBe(request.connection); - - response.on("finish", () => { - process.nextTick(() => { - expect(request.socket).toBeTruthy(); - done(); - }); - }); - response.end(); - }); - - const url = `http://localhost:${port}`; - const client = h2.connect(url, () => { - const headers = { - ":path": "/foobar", - ":method": "GET", - ":scheme": "http", - ":authority": `localhost:${port}`, - }; - const request = client.request(headers); - request.on("end", () => { - client.close(); - }); - request.end(); - request.resume(); - }); -}); - -//<#END_FILE: test-http2-compat-serverrequest.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-close.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-close.test.js deleted file mode 100644 index 6ae966fc55..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-close.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-close.js -//#SHA1: 6b61a9cea948447ae33843472678ffbed0b47c9a -//----------------- -"use strict"; - -const h2 = require("http2"); - -// Skip the test if crypto is not available -let hasCrypto; -try { - require("crypto"); - hasCrypto = true; -} catch (err) { - hasCrypto = false; -} - -(hasCrypto ? describe : describe.skip)("HTTP/2 server response close", () => { - let server; - let url; - - beforeAll(done => { - server = h2.createServer((req, res) => { - res.writeHead(200); - res.write("a"); - - const reqCloseMock = jest.fn(); - const resCloseMock = jest.fn(); - const reqErrorMock = jest.fn(); - - req.on("close", reqCloseMock); - res.on("close", resCloseMock); - req.on("error", reqErrorMock); - - // Use Jest's fake timers to ensure the test doesn't hang - setTimeout(() => { - expect(reqCloseMock).toHaveBeenCalled(); - expect(resCloseMock).toHaveBeenCalled(); - expect(reqErrorMock).not.toHaveBeenCalled(); - done(); - }, 1000); - }); - - server.listen(0, () => { - url = `http://localhost:${server.address().port}`; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("Server request and response should receive close event if connection terminated before response.end", done => { - const client = h2.connect(url, () => { - const request = client.request(); - request.on("data", chunk => { - client.destroy(); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-http2-compat-serverresponse-close.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-drain.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-drain.test.js deleted file mode 100644 index 4976ad2284..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-drain.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-drain.js -//#SHA1: 4ec55745f622a31b4729fcb9daf9bfd707a3bdb3 -//----------------- -'use strict'; - -const h2 = require('http2'); - -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -const testString = 'tests'; - -test('HTTP/2 server response drain event', async () => { - if (!hasCrypto) { - test.skip('missing crypto'); - return; - } - - const server = h2.createServer(); - - const requestHandler = jest.fn((req, res) => { - res.stream._writableState.highWaterMark = testString.length; - expect(res.write(testString)).toBe(false); - res.on('drain', jest.fn(() => res.end(testString))); - }); - - server.on('request', requestHandler); - - await new Promise(resolve => server.listen(0, resolve)); - const port = server.address().port; - - const client = h2.connect(`http://localhost:${port}`); - const request = client.request({ - ':path': '/foobar', - ':method': 'POST', - ':scheme': 'http', - ':authority': `localhost:${port}` - }); - request.resume(); - request.end(); - - let data = ''; - request.setEncoding('utf8'); - request.on('data', (chunk) => (data += chunk)); - - await new Promise(resolve => request.on('end', resolve)); - - expect(data).toBe(testString.repeat(2)); - expect(requestHandler).toHaveBeenCalled(); - - client.close(); - await new Promise(resolve => server.close(resolve)); -}); - -//<#END_FILE: test-http2-compat-serverresponse-drain.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-end-after-statuses-without-body.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-end-after-statuses-without-body.test.js deleted file mode 100644 index 2dd0f00dd3..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-end-after-statuses-without-body.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-end-after-statuses-without-body.js -//#SHA1: c4a4b76e1b04b7e6779f80f7077758dfab0e8b80 -//----------------- -"use strict"; - -const h2 = require("http2"); - -const { HTTP_STATUS_NO_CONTENT, HTTP_STATUS_RESET_CONTENT, HTTP_STATUS_NOT_MODIFIED } = h2.constants; - -const statusWithoutBody = [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_RESET_CONTENT, HTTP_STATUS_NOT_MODIFIED]; -const STATUS_CODES_COUNT = statusWithoutBody.length; - -describe("HTTP/2 server response end after statuses without body", () => { - let server; - let url; - - beforeAll(done => { - server = h2.createServer((req, res) => { - res.writeHead(statusWithoutBody.pop()); - res.end(); - }); - - server.listen(0, () => { - url = `http://localhost:${server.address().port}`; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - it("should handle end() after sending statuses without body", done => { - const client = h2.connect(url, () => { - let responseCount = 0; - const closeAfterResponse = () => { - if (STATUS_CODES_COUNT === ++responseCount) { - client.destroy(); - done(); - } - }; - - for (let i = 0; i < STATUS_CODES_COUNT; i++) { - const request = client.request(); - request.on("response", closeAfterResponse); - } - }); - }); -}); - -//<#END_FILE: test-http2-compat-serverresponse-end-after-statuses-without-body.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-end.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-end.test.js deleted file mode 100644 index 27b1f393db..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-end.test.js +++ /dev/null @@ -1,80 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-end.js -//#SHA1: 672da69abcb0b86d5234556e692949ac36ef6395 -//----------------- -'use strict'; - -const http2 = require('http2'); -const { promisify } = require('util'); - -// Mock the common module functions -const mustCall = (fn) => jest.fn(fn); -const mustNotCall = () => jest.fn().mockImplementation(() => { - throw new Error('This function should not have been called'); -}); - -const { - HTTP2_HEADER_STATUS, - HTTP_STATUS_OK -} = http2.constants; - -// Helper function to create a server and get its port -const createServerAndGetPort = async (requestListener) => { - const server = http2.createServer(requestListener); - await promisify(server.listen.bind(server))(0); - const { port } = server.address(); - return { server, port }; -}; - -// Helper function to create a client -const createClient = (port) => { - const url = `http://localhost:${port}`; - return http2.connect(url); -}; - -describe('Http2ServerResponse.end', () => { - test('accepts chunk, encoding, cb as args and can be called multiple times', async () => { - const { server, port } = await createServerAndGetPort((request, response) => { - const endCallback = jest.fn(() => { - response.end(jest.fn()); - process.nextTick(() => { - response.end(jest.fn()); - server.close(); - }); - }); - - response.end('end', 'utf8', endCallback); - response.on('finish', () => { - response.end(jest.fn()); - }); - response.end(jest.fn()); - }); - - const client = createClient(port); - const headers = { - ':path': '/', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - - let data = ''; - const request = client.request(headers); - request.setEncoding('utf8'); - request.on('data', (chunk) => (data += chunk)); - await new Promise(resolve => { - request.on('end', () => { - expect(data).toBe('end'); - client.close(); - resolve(); - }); - request.end(); - request.resume(); - }); - }); - - // Add more tests here... -}); - -// More test blocks for other scenarios... - -//<#END_FILE: test-http2-compat-serverresponse-end.test.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-finished.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-finished.test.js deleted file mode 100644 index fb6f9c2b52..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-finished.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-finished.js -//#SHA1: 6ef7a05f30923975d7a267cee54aafae1bfdbc7d -//----------------- -'use strict'; - -const h2 = require('http2'); -const net = require('net'); - -let server; - -beforeAll(() => { - // Skip the test if crypto is not available - if (!process.versions.openssl) { - return test.skip('missing crypto'); - } -}); - -afterEach(() => { - if (server) { - server.close(); - } -}); - -test('Http2ServerResponse.finished', (done) => { - server = h2.createServer(); - server.listen(0, () => { - const port = server.address().port; - - server.once('request', (request, response) => { - expect(response.socket).toBeInstanceOf(net.Socket); - expect(response.connection).toBeInstanceOf(net.Socket); - expect(response.socket).toBe(response.connection); - - response.on('finish', () => { - expect(response.socket).toBeUndefined(); - expect(response.connection).toBeUndefined(); - process.nextTick(() => { - expect(response.stream).toBeDefined(); - done(); - }); - }); - - expect(response.finished).toBe(false); - expect(response.writableEnded).toBe(false); - response.end(); - expect(response.finished).toBe(true); - expect(response.writableEnded).toBe(true); - }); - - const url = `http://localhost:${port}`; - const client = h2.connect(url, () => { - const headers = { - ':path': '/', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - const request = client.request(headers); - request.on('end', () => { - client.close(); - }); - request.end(); - request.resume(); - }); - }); -}); - -//<#END_FILE: test-http2-compat-serverresponse-finished.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-flushheaders.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-flushheaders.test.js deleted file mode 100644 index 6d0864b507..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-flushheaders.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-flushheaders.js -//#SHA1: ea772e05a29f43bd7b61e4d70f24b94c1e1e201c -//----------------- -"use strict"; - -const h2 = require("http2"); - -let server; -let serverResponse; - -beforeAll(done => { - server = h2.createServer(); - server.listen(0, () => { - done(); - }); -}); - -afterAll(() => { - server.close(); -}); - -test("Http2ServerResponse.flushHeaders", done => { - const port = server.address().port; - - server.once("request", (request, response) => { - expect(response.headersSent).toBe(false); - expect(response._header).toBe(false); // Alias for headersSent - response.flushHeaders(); - expect(response.headersSent).toBe(true); - expect(response._header).toBe(true); - response.flushHeaders(); // Idempotent - - expect(() => { - response.writeHead(400, { "foo-bar": "abc123" }); - }).toThrow( - expect.objectContaining({ - code: "ERR_HTTP2_HEADERS_SENT", - }), - ); - response.on("finish", () => { - process.nextTick(() => { - response.flushHeaders(); // Idempotent - done(); - }); - }); - serverResponse = response; - }); - - const url = `http://localhost:${port}`; - const client = h2.connect(url, () => { - const headers = { - ":path": "/", - ":method": "GET", - ":scheme": "http", - ":authority": `localhost:${port}`, - }; - const request = client.request(headers); - request.on("response", (headers, flags) => { - expect(headers["foo-bar"]).toBeUndefined(); - expect(headers[":status"]).toBe(200); - serverResponse.end(); - }); - request.on("end", () => { - client.close(); - }); - request.end(); - request.resume(); - }); -}); - -//<#END_FILE: test-http2-compat-serverresponse-flushheaders.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-headers-send-date.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-headers-send-date.test.js deleted file mode 100644 index 6f410d12f1..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-headers-send-date.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-headers-send-date.js -//#SHA1: 1ed6319986a3bb9bf58709d9577d03407fdde3f2 -//----------------- -"use strict"; -const http2 = require("http2"); - -let server; -let port; - -beforeAll(done => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } - - server = http2.createServer((request, response) => { - response.sendDate = false; - response.writeHead(200); - response.end(); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll(() => { - server.close(); -}); - -test("HTTP/2 server response should not send Date header when sendDate is false", done => { - const session = http2.connect(`http://localhost:${port}`); - const req = session.request(); - - req.on("response", (headers, flags) => { - expect(headers).not.toHaveProperty("Date"); - expect(headers).not.toHaveProperty("date"); - }); - - req.on("end", () => { - session.close(); - done(); - }); - - req.end(); -}); - -//<#END_FILE: test-http2-compat-serverresponse-headers-send-date.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-settimeout.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-settimeout.test.js deleted file mode 100644 index 305f398176..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-settimeout.test.js +++ /dev/null @@ -1,78 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-settimeout.js -//#SHA1: fe2e0371e885463968a268362464724494b758a6 -//----------------- -"use strict"; - -const http2 = require("http2"); - -const msecs = 1000; // Assuming a reasonable timeout for all platforms - -let server; -let client; - -beforeAll(done => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } - server = http2.createServer(); - server.listen(0, () => { - done(); - }); -}); - -afterAll(() => { - if (client) { - client.close(); - } - if (server) { - server.close(); - } -}); - -test("HTTP2 ServerResponse setTimeout", done => { - const timeoutCallback = jest.fn(); - const onTimeout = jest.fn(); - const onFinish = jest.fn(); - - server.on("request", (req, res) => { - res.setTimeout(msecs, timeoutCallback); - res.on("timeout", onTimeout); - res.on("finish", () => { - onFinish(); - res.setTimeout(msecs, jest.fn()); - process.nextTick(() => { - res.setTimeout(msecs, jest.fn()); - }); - }); - - // Explicitly end the response after a short delay - setTimeout(() => { - res.end(); - }, 100); - }); - - const port = server.address().port; - client = http2.connect(`http://localhost:${port}`); - const req = client.request({ - ":path": "/", - ":method": "GET", - ":scheme": "http", - ":authority": `localhost:${port}`, - }); - - req.on("end", () => { - client.close(); - - // Move assertions here to ensure they run after the response has finished - expect(timeoutCallback).not.toHaveBeenCalled(); - expect(onTimeout).not.toHaveBeenCalled(); - expect(onFinish).toHaveBeenCalledTimes(1); - - done(); - }); - - req.resume(); - req.end(); -}, 10000); // Increase the timeout to 10 seconds - -//<#END_FILE: test-http2-compat-serverresponse-settimeout.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-statuscode.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-statuscode.test.js deleted file mode 100644 index 8845f6c532..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-statuscode.test.js +++ /dev/null @@ -1,95 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-statuscode.js -//#SHA1: 10cb487c1fd9e256f807319b84c426b356be443f -//----------------- -"use strict"; - -const h2 = require("http2"); - -let server; -let port; - -beforeAll(async () => { - server = h2.createServer(); - await new Promise(resolve => server.listen(0, resolve)); - port = server.address().port; -}); - -afterAll(async () => { - server.close(); -}); - -test("Http2ServerResponse should have a statusCode property", async () => { - const responsePromise = new Promise(resolve => { - server.once("request", (request, response) => { - const expectedDefaultStatusCode = 200; - const realStatusCodes = { - continue: 100, - ok: 200, - multipleChoices: 300, - badRequest: 400, - internalServerError: 500, - }; - const fakeStatusCodes = { - tooLow: 99, - tooHigh: 600, - }; - - expect(response.statusCode).toBe(expectedDefaultStatusCode); - - // Setting the response.statusCode should not throw. - response.statusCode = realStatusCodes.ok; - response.statusCode = realStatusCodes.multipleChoices; - response.statusCode = realStatusCodes.badRequest; - response.statusCode = realStatusCodes.internalServerError; - - expect(() => { - response.statusCode = realStatusCodes.continue; - }).toThrow( - expect.objectContaining({ - code: "ERR_HTTP2_INFO_STATUS_NOT_ALLOWED", - name: "RangeError", - }), - ); - - expect(() => { - response.statusCode = fakeStatusCodes.tooLow; - }).toThrow( - expect.objectContaining({ - code: "ERR_HTTP2_STATUS_INVALID", - name: "RangeError", - }), - ); - - expect(() => { - response.statusCode = fakeStatusCodes.tooHigh; - }).toThrow( - expect.objectContaining({ - code: "ERR_HTTP2_STATUS_INVALID", - name: "RangeError", - }), - ); - - response.on("finish", resolve); - response.end(); - }); - }); - - const url = `http://localhost:${port}`; - const client = h2.connect(url); - - const headers = { - ":path": "/", - ":method": "GET", - ":scheme": "http", - ":authority": `localhost:${port}`, - }; - - const request = client.request(headers); - request.end(); - await new Promise(resolve => request.resume().on("end", resolve)); - - await responsePromise; - client.close(); -}); - -//<#END_FILE: test-http2-compat-serverresponse-statuscode.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-writehead-array.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-writehead-array.test.js deleted file mode 100644 index 2b1ca358a9..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-writehead-array.test.js +++ /dev/null @@ -1,114 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-writehead-array.js -//#SHA1: e43a5a9f99ddad68b313e15fbb69839cca6d0775 -//----------------- -'use strict'; - -const http2 = require('http2'); - -// Skip the test if crypto is not available -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -if (!hasCrypto) { - test.skip('missing crypto', () => {}); -} else { - describe('Http2ServerResponse.writeHead with arrays', () => { - test('should support nested arrays', (done) => { - const server = http2.createServer(); - server.listen(0, () => { - const port = server.address().port; - - server.once('request', (request, response) => { - const returnVal = response.writeHead(200, [ - ['foo', 'bar'], - ['foo', 'baz'], - ['ABC', 123], - ]); - expect(returnVal).toBe(response); - response.end(() => { server.close(); }); - }); - - const client = http2.connect(`http://localhost:${port}`, () => { - const request = client.request(); - - request.on('response', (headers) => { - expect(headers.foo).toBe('bar, baz'); - expect(headers.abc).toBe('123'); - expect(headers[':status']).toBe(200); - }); - request.on('end', () => { - client.close(); - done(); - }); - request.end(); - request.resume(); - }); - }); - }); - - test('should support flat arrays', (done) => { - const server = http2.createServer(); - server.listen(0, () => { - const port = server.address().port; - - server.once('request', (request, response) => { - const returnVal = response.writeHead(200, ['foo', 'bar', 'foo', 'baz', 'ABC', 123]); - expect(returnVal).toBe(response); - response.end(() => { server.close(); }); - }); - - const client = http2.connect(`http://localhost:${port}`, () => { - const request = client.request(); - - request.on('response', (headers) => { - expect(headers.foo).toBe('bar, baz'); - expect(headers.abc).toBe('123'); - expect(headers[':status']).toBe(200); - }); - request.on('end', () => { - client.close(); - done(); - }); - request.end(); - request.resume(); - }); - }); - }); - - test('should throw ERR_INVALID_ARG_VALUE for invalid array', (done) => { - const server = http2.createServer(); - server.listen(0, () => { - const port = server.address().port; - - server.once('request', (request, response) => { - expect(() => { - response.writeHead(200, ['foo', 'bar', 'ABC', 123, 'extra']); - }).toThrow(expect.objectContaining({ - code: 'ERR_INVALID_ARG_VALUE' - })); - - response.end(() => { server.close(); }); - }); - - const client = http2.connect(`http://localhost:${port}`, () => { - const request = client.request(); - - request.on('end', () => { - client.close(); - done(); - }); - request.end(); - request.resume(); - }); - }); - }); - }); -} - -//<#END_FILE: test-http2-compat-serverresponse-writehead-array.js diff --git a/test/js/node/test/parallel/http2-compat-serverresponse-writehead.test.js b/test/js/node/test/parallel/http2-compat-serverresponse-writehead.test.js deleted file mode 100644 index 296a1e1a73..0000000000 --- a/test/js/node/test/parallel/http2-compat-serverresponse-writehead.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-http2-compat-serverresponse-writehead.js -//#SHA1: fa267d5108f95ba69583bc709a82185ee9d18e76 -//----------------- -'use strict'; - -const h2 = require('http2'); - -// Http2ServerResponse.writeHead should override previous headers - -test('Http2ServerResponse.writeHead overrides previous headers', (done) => { - const server = h2.createServer(); - server.listen(0, () => { - const port = server.address().port; - server.once('request', (request, response) => { - response.setHeader('foo-bar', 'def456'); - - // Override - const returnVal = response.writeHead(418, { 'foo-bar': 'abc123' }); - - expect(returnVal).toBe(response); - - expect(() => { response.writeHead(300); }).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_HEADERS_SENT' - })); - - response.on('finish', () => { - server.close(); - process.nextTick(() => { - // The stream is invalid at this point, - // and this line verifies this does not throw. - response.writeHead(300); - done(); - }); - }); - response.end(); - }); - - const url = `http://localhost:${port}`; - const client = h2.connect(url, () => { - const headers = { - ':path': '/', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - const request = client.request(headers); - request.on('response', (headers) => { - expect(headers['foo-bar']).toBe('abc123'); - expect(headers[':status']).toBe(418); - }); - request.on('end', () => { - client.close(); - }); - request.end(); - request.resume(); - }); - }); -}); - -// Skip the test if crypto is not available -if (!process.versions.openssl) { - test.skip('missing crypto', () => {}); -} - -//<#END_FILE: test-http2-compat-serverresponse-writehead.js diff --git a/test/js/node/test/parallel/http2-compat-socket-destroy-delayed.test.js b/test/js/node/test/parallel/http2-compat-socket-destroy-delayed.test.js deleted file mode 100644 index 10e6afe2bc..0000000000 --- a/test/js/node/test/parallel/http2-compat-socket-destroy-delayed.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-http2-compat-socket-destroy-delayed.js -//#SHA1: c7b5b8b5de4667a89e0e261e36098f617d411ed2 -//----------------- -"use strict"; - -const http2 = require("http2"); - -const { HTTP2_HEADER_PATH, HTTP2_HEADER_METHOD } = http2.constants; - -// Skip the test if crypto is not available -if (!process.versions.openssl) { - test.skip("missing crypto", () => {}); -} else { - test("HTTP/2 socket destroy delayed", done => { - const app = http2.createServer((req, res) => { - res.end("hello"); - setImmediate(() => req.socket?.destroy()); - }); - - app.listen(0, () => { - const session = http2.connect(`http://localhost:${app.address().port}`); - const request = session.request({ - [HTTP2_HEADER_PATH]: "/", - [HTTP2_HEADER_METHOD]: "get", - }); - request.once("response", (headers, flags) => { - let data = ""; - request.on("data", chunk => { - data += chunk; - }); - request.on("end", () => { - expect(data).toBe("hello"); - session.close(); - app.close(); - done(); - }); - }); - request.end(); - }); - }); -} - -// This tests verifies that calling `req.socket.destroy()` via -// setImmediate does not crash. -// Fixes https://github.com/nodejs/node/issues/22855. - -//<#END_FILE: test-http2-compat-socket-destroy-delayed.js diff --git a/test/js/node/test/parallel/http2-compat-write-early-hints-invalid-argument-type.test.js b/test/js/node/test/parallel/http2-compat-write-early-hints-invalid-argument-type.test.js deleted file mode 100644 index 0ab3a588a3..0000000000 --- a/test/js/node/test/parallel/http2-compat-write-early-hints-invalid-argument-type.test.js +++ /dev/null @@ -1,72 +0,0 @@ -//#FILE: test-http2-compat-write-early-hints-invalid-argument-type.js -//#SHA1: 8ae2eba59668a38b039a100d3ad26f88e54be806 -//----------------- -"use strict"; - -const http2 = require("node:http2"); -const util = require("node:util"); -const debug = util.debuglog("test"); - -const testResBody = "response content"; - -// Check if crypto is available -let hasCrypto = false; -try { - require("crypto"); - hasCrypto = true; -} catch (err) { - // crypto not available -} - -(hasCrypto ? describe : describe.skip)("HTTP2 compat writeEarlyHints invalid argument type", () => { - let server; - let client; - - beforeAll(done => { - server = http2.createServer(); - server.listen(0, () => { - done(); - }); - }); - - afterAll(() => { - if (client) { - client.close(); - } - server.close(); - }); - - test("should throw ERR_INVALID_ARG_TYPE for invalid object value", done => { - server.on("request", (req, res) => { - debug("Server sending early hints..."); - expect(() => { - res.writeEarlyHints("this should not be here"); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - debug("Server sending full response..."); - res.end(testResBody); - }); - - client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - - debug("Client sending request..."); - - req.on("headers", () => { - done(new Error("Should not receive headers")); - }); - - req.on("response", () => { - done(); - }); - - req.end(); - }); -}); - -//<#END_FILE: test-http2-compat-write-early-hints-invalid-argument-type.js diff --git a/test/js/node/test/parallel/http2-compat-write-early-hints.test.js b/test/js/node/test/parallel/http2-compat-write-early-hints.test.js deleted file mode 100644 index c3d8fb4e15..0000000000 --- a/test/js/node/test/parallel/http2-compat-write-early-hints.test.js +++ /dev/null @@ -1,146 +0,0 @@ -//#FILE: test-http2-compat-write-early-hints.js -//#SHA1: 0ed18263958421cde07c37b8ec353005b7477499 -//----------------- -'use strict'; - -const http2 = require('node:http2'); -const util = require('node:util'); -const debug = util.debuglog('test'); - -const testResBody = 'response content'; - -describe('HTTP/2 Early Hints', () => { - test('Happy flow - string argument', async () => { - const server = http2.createServer(); - - server.on('request', (req, res) => { - debug('Server sending early hints...'); - res.writeEarlyHints({ - link: '; rel=preload; as=style' - }); - - debug('Server sending full response...'); - res.end(testResBody); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - - debug('Client sending request...'); - - await new Promise(resolve => { - req.on('headers', (headers) => { - expect(headers).toBeDefined(); - expect(headers[':status']).toBe(103); - expect(headers.link).toBe('; rel=preload; as=style'); - }); - - req.on('response', (headers) => { - expect(headers[':status']).toBe(200); - }); - - let data = ''; - req.on('data', (d) => data += d); - - req.on('end', () => { - debug('Got full response.'); - expect(data).toBe(testResBody); - client.close(); - server.close(resolve); - }); - }); - }); - - test('Happy flow - array argument', async () => { - const server = http2.createServer(); - - server.on('request', (req, res) => { - debug('Server sending early hints...'); - res.writeEarlyHints({ - link: [ - '; rel=preload; as=style', - '; rel=preload; as=script', - ] - }); - - debug('Server sending full response...'); - res.end(testResBody); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - - debug('Client sending request...'); - - await new Promise(resolve => { - req.on('headers', (headers) => { - expect(headers).toBeDefined(); - expect(headers[':status']).toBe(103); - expect(headers.link).toBe( - '; rel=preload; as=style, ; rel=preload; as=script' - ); - }); - - req.on('response', (headers) => { - expect(headers[':status']).toBe(200); - }); - - let data = ''; - req.on('data', (d) => data += d); - - req.on('end', () => { - debug('Got full response.'); - expect(data).toBe(testResBody); - client.close(); - server.close(resolve); - }); - }); - }); - - test('Happy flow - empty array', async () => { - const server = http2.createServer(); - - server.on('request', (req, res) => { - debug('Server sending early hints...'); - res.writeEarlyHints({ - link: [] - }); - - debug('Server sending full response...'); - res.end(testResBody); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - - debug('Client sending request...'); - - await new Promise(resolve => { - const headersListener = jest.fn(); - req.on('headers', headersListener); - - req.on('response', (headers) => { - expect(headers[':status']).toBe(200); - expect(headersListener).not.toHaveBeenCalled(); - }); - - let data = ''; - req.on('data', (d) => data += d); - - req.on('end', () => { - debug('Got full response.'); - expect(data).toBe(testResBody); - client.close(); - server.close(resolve); - }); - }); - }); -}); - -//<#END_FILE: test-http2-compat-write-early-hints.js diff --git a/test/js/node/test/parallel/http2-compat-write-head-destroyed.test.js b/test/js/node/test/parallel/http2-compat-write-head-destroyed.test.js deleted file mode 100644 index 601f47928e..0000000000 --- a/test/js/node/test/parallel/http2-compat-write-head-destroyed.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-http2-compat-write-head-destroyed.js -//#SHA1: 29f693f49912d4621c1a19ab7412b1b318d55d8e -//----------------- -"use strict"; - -const http2 = require("http2"); - -let server; -let port; - -beforeAll(done => { - if (!process.versions.openssl) { - done(); - return; - } - - server = http2.createServer((req, res) => { - // Destroy the stream first - req.stream.destroy(); - - res.writeHead(200); - res.write("hello "); - res.end("world"); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll(() => { - if (server) { - server.close(); - } -}); - -test("writeHead, write and end do not crash in compatibility mode", done => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } - - const client = http2.connect(`http://localhost:${port}`); - - const req = client.request(); - - req.on("response", () => { - done.fail("Should not receive response"); - }); - - req.on("close", () => { - client.close(); - done(); - }); - - req.resume(); -}); - -//<#END_FILE: test-http2-compat-write-head-destroyed.js diff --git a/test/js/node/test/parallel/http2-connect-tls-with-delay.test.js b/test/js/node/test/parallel/http2-connect-tls-with-delay.test.js deleted file mode 100644 index 1161272cab..0000000000 --- a/test/js/node/test/parallel/http2-connect-tls-with-delay.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-http2-connect-tls-with-delay.js -//#SHA1: 8c5489e025ec14c2cc53788b27fde11a11990e42 -//----------------- -"use strict"; - -const http2 = require("http2"); -const tls = require("tls"); -const fs = require("fs"); -const path = require("path"); - -const serverOptions = { - key: fs.readFileSync(path.join(__dirname, "..", "fixtures", "keys", "agent1-key.pem")), - cert: fs.readFileSync(path.join(__dirname, "..", "fixtures", "keys", "agent1-cert.pem")), -}; - -let server; - -beforeAll(done => { - server = http2.createSecureServer(serverOptions, (req, res) => { - res.end(); - }); - - server.listen(0, "127.0.0.1", done); -}); - -afterAll(() => { - server.close(); -}); - -test("HTTP/2 connect with TLS and delay", done => { - const options = { - ALPNProtocols: ["h2"], - host: "127.0.0.1", - servername: "localhost", - port: server.address().port, - rejectUnauthorized: false, - }; - - const socket = tls.connect(options, async () => { - socket.once("readable", () => { - const client = http2.connect("https://localhost:" + server.address().port, { - ...options, - createConnection: () => socket, - }); - - client.once("remoteSettings", () => { - const req = client.request({ - ":path": "/", - }); - req.on("data", () => req.resume()); - req.on("end", () => { - client.close(); - req.close(); - done(); - }); - req.end(); - }); - }); - }); -}); - -//<#END_FILE: test-http2-connect-tls-with-delay.js diff --git a/test/js/node/test/parallel/http2-cookies.test.js b/test/js/node/test/parallel/http2-cookies.test.js deleted file mode 100644 index c906992d71..0000000000 --- a/test/js/node/test/parallel/http2-cookies.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-http2-cookies.js -//#SHA1: 91bdbacba9eb8ebd9dddd43327aa2271dc00c271 -//----------------- -'use strict'; - -const h2 = require('http2'); - -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -if (!hasCrypto) { - test.skip('missing crypto', () => {}); -} else { - test('HTTP/2 cookies', async () => { - const server = h2.createServer(); - - const setCookie = [ - 'a=b', - 'c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly', - 'e=f', - ]; - - server.on('stream', (stream, headers) => { - expect(typeof headers.abc).toBe('string'); - expect(headers.abc).toBe('1, 2, 3'); - expect(typeof headers.cookie).toBe('string'); - expect(headers.cookie).toBe('a=b; c=d; e=f'); - - stream.respond({ - 'content-type': 'text/html', - ':status': 200, - 'set-cookie': setCookie - }); - - stream.end('hello world'); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - const client = h2.connect(`http://localhost:${server.address().port}`); - - const req = client.request({ - ':path': '/', - 'abc': [1, 2, 3], - 'cookie': ['a=b', 'c=d', 'e=f'], - }); - - await new Promise((resolve, reject) => { - req.on('response', (headers) => { - expect(Array.isArray(headers['set-cookie'])).toBe(true); - expect(headers['set-cookie']).toEqual(setCookie); - }); - - req.on('end', resolve); - req.on('error', reject); - req.end(); - req.resume(); - }); - - server.close(); - client.close(); - }); -} - -//<#END_FILE: test-http2-cookies.js diff --git a/test/js/node/test/parallel/http2-createwritereq.test.js b/test/js/node/test/parallel/http2-createwritereq.test.js deleted file mode 100644 index 2c768f880a..0000000000 --- a/test/js/node/test/parallel/http2-createwritereq.test.js +++ /dev/null @@ -1,88 +0,0 @@ -//#FILE: test-http2-createwritereq.js -//#SHA1: 8b0d2399fb8a26ce6cc76b9f338be37a7ff08ca5 -//----------------- -"use strict"; - -const http2 = require("http2"); - -// Mock the gc function -global.gc = jest.fn(); - -const testString = "a\u00A1\u0100\uD83D\uDE00"; - -const encodings = { - // "buffer": "utf8", - "ascii": "ascii", - // "latin1": "latin1", - // "binary": "latin1", - // "utf8": "utf8", - // "utf-8": "utf8", - // "ucs2": "ucs2", - // "ucs-2": "ucs2", - // "utf16le": "ucs2", - // "utf-16le": "ucs2", - // "UTF8": "utf8", -}; - -describe("http2 createWriteReq", () => { - let server; - let serverAddress; - - beforeAll(done => { - server = http2.createServer((req, res) => { - const testEncoding = encodings[req.url.slice(1)]; - - req.on("data", chunk => { - // console.error(testEncoding, chunk, Buffer.from(testString, testEncoding)); - expect(Buffer.from(testString, testEncoding).equals(chunk)).toBe(true); - }); - - req.on("end", () => res.end()); - }); - - server.listen(0, () => { - serverAddress = `http://localhost:${server.address().port}`; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - Object.keys(encodings).forEach(writeEncoding => { - test(`should handle ${writeEncoding} encoding`, done => { - const client = http2.connect(serverAddress); - const req = client.request({ - ":path": `/${writeEncoding}`, - ":method": "POST", - }); - - expect(req._writableState.decodeStrings).toBe(false); - - req.write( - writeEncoding !== "buffer" ? testString : Buffer.from(testString), - writeEncoding !== "buffer" ? writeEncoding : undefined, - ); - req.resume(); - - req.on("end", () => { - client.close(); - done(); - }); - - // Ref: https://github.com/nodejs/node/issues/17840 - const origDestroy = req.destroy; - req.destroy = function (...args) { - // Schedule a garbage collection event at the end of the current - // MakeCallback() run. - process.nextTick(global.gc); - return origDestroy.call(this, ...args); - }; - - req.end(); - }); - }); -}); - -//<#END_FILE: test-http2-createwritereq.test.js diff --git a/test/js/node/test/parallel/http2-destroy-after-write.test.js b/test/js/node/test/parallel/http2-destroy-after-write.test.js deleted file mode 100644 index c3303887ac..0000000000 --- a/test/js/node/test/parallel/http2-destroy-after-write.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-http2-destroy-after-write.js -//#SHA1: 193688397df0b891b9286ff825ca873935d30e04 -//----------------- -"use strict"; - -const http2 = require("http2"); - -let server; -let port; - -beforeAll(done => { - server = http2.createServer(); - - server.on("session", session => { - session.on("stream", stream => { - stream.on("end", function () { - this.respond({ - ":status": 200, - }); - this.write("foo"); - this.destroy(); - }); - stream.resume(); - }); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll(() => { - server.close(); -}); - -test("http2 destroy after write", done => { - const client = http2.connect(`http://localhost:${port}`); - const stream = client.request({ ":method": "POST" }); - - stream.on("response", headers => { - expect(headers[":status"]).toBe(200); - }); - - stream.on("close", () => { - client.close(); - done(); - }); - - stream.resume(); - stream.end(); -}); - -//<#END_FILE: test-http2-destroy-after-write.js diff --git a/test/js/node/test/parallel/http2-dont-override.test.js b/test/js/node/test/parallel/http2-dont-override.test.js deleted file mode 100644 index ea465da5a3..0000000000 --- a/test/js/node/test/parallel/http2-dont-override.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-http2-dont-override.js -//#SHA1: d295b8c4823cc34c03773eb08bf0393fca541694 -//----------------- -'use strict'; - -const http2 = require('http2'); - -// Skip test if crypto is not available -if (!process.versions.openssl) { - test.skip('missing crypto', () => {}); -} else { - test('http2 should not override options', (done) => { - const options = {}; - - const server = http2.createServer(options); - - // Options are defaulted but the options are not modified - expect(Object.keys(options)).toEqual([]); - - server.on('stream', (stream) => { - const headers = {}; - const options = {}; - stream.respond(headers, options); - - // The headers are defaulted but the original object is not modified - expect(Object.keys(headers)).toEqual([]); - - // Options are defaulted but the original object is not modified - expect(Object.keys(options)).toEqual([]); - - stream.end(); - }); - - server.listen(0, () => { - const client = http2.connect(`http://localhost:${server.address().port}`); - - const headers = {}; - const options = {}; - - const req = client.request(headers, options); - - // The headers are defaulted but the original object is not modified - expect(Object.keys(headers)).toEqual([]); - - // Options are defaulted but the original object is not modified - expect(Object.keys(options)).toEqual([]); - - req.resume(); - req.on('end', () => { - server.close(); - client.close(); - done(); - }); - }); - }); -} - -//<#END_FILE: test-http2-dont-override.js diff --git a/test/js/node/test/parallel/http2-forget-closed-streams.test.js b/test/js/node/test/parallel/http2-forget-closed-streams.test.js deleted file mode 100644 index b21280b343..0000000000 --- a/test/js/node/test/parallel/http2-forget-closed-streams.test.js +++ /dev/null @@ -1,85 +0,0 @@ -//#FILE: test-http2-forget-closed-streams.js -//#SHA1: 2f917924c763cc220e68ce2b829c63dc03a836ab -//----------------- -"use strict"; -const http2 = require("http2"); - -// Skip test if crypto is not available -const hasCrypto = (() => { - try { - require("crypto"); - return true; - } catch (err) { - return false; - } -})(); - -(hasCrypto ? describe : describe.skip)("http2 forget closed streams", () => { - let server; - - beforeAll(() => { - server = http2.createServer({ maxSessionMemory: 1 }); - - server.on("session", session => { - session.on("stream", stream => { - stream.on("end", () => { - stream.respond( - { - ":status": 200, - }, - { - endStream: true, - }, - ); - }); - stream.resume(); - }); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("should handle 10000 requests without memory issues", done => { - const listenPromise = new Promise(resolve => { - server.listen(0, () => { - resolve(server.address().port); - }); - }); - - listenPromise.then(port => { - const client = http2.connect(`http://localhost:${port}`); - - function makeRequest(i) { - return new Promise(resolve => { - const stream = client.request({ ":method": "POST" }); - stream.on("response", headers => { - expect(headers[":status"]).toBe(200); - stream.on("close", resolve); - }); - stream.end(); - }); - } - - async function runRequests() { - for (let i = 0; i < 10000; i++) { - await makeRequest(i); - } - client.close(); - } - - runRequests() - .then(() => { - // If we've reached here without errors, the test has passed - expect(true).toBe(true); - done(); - }) - .catch(err => { - done(err); - }); - }); - }, 30000); // Increase timeout to 30 seconds -}); - -//<#END_FILE: test-http2-forget-closed-streams.js diff --git a/test/js/node/test/parallel/http2-goaway-opaquedata.test.js b/test/js/node/test/parallel/http2-goaway-opaquedata.test.js deleted file mode 100644 index 7de3263266..0000000000 --- a/test/js/node/test/parallel/http2-goaway-opaquedata.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-http2-goaway-opaquedata.js -//#SHA1: 5ad5b6a64cb0e7419753dcd88d59692eb97973ed -//----------------- -'use strict'; - -const http2 = require('http2'); - -let server; -let serverPort; - -beforeAll((done) => { - server = http2.createServer(); - server.listen(0, () => { - serverPort = server.address().port; - done(); - }); -}); - -afterAll((done) => { - server.close(done); -}); - -test('HTTP/2 GOAWAY with opaque data', (done) => { - const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); - let session; - - server.once('stream', (stream) => { - session = stream.session; - session.on('close', () => { - expect(true).toBe(true); // Session closed - }); - session.goaway(0, 0, data); - stream.respond(); - stream.end(); - }); - - const client = http2.connect(`http://localhost:${serverPort}`); - client.once('goaway', (code, lastStreamID, buf) => { - expect(code).toBe(0); - expect(lastStreamID).toBe(1); - expect(buf).toEqual(data); - session.close(); - client.close(); - done(); - }); - - const req = client.request(); - req.resume(); - req.on('end', () => { - expect(true).toBe(true); // Request ended - }); - req.on('close', () => { - expect(true).toBe(true); // Request closed - }); - req.end(); -}); - -//<#END_FILE: test-http2-goaway-opaquedata.js diff --git a/test/js/node/test/parallel/http2-large-write-close.test.js b/test/js/node/test/parallel/http2-large-write-close.test.js deleted file mode 100644 index f50a3b581f..0000000000 --- a/test/js/node/test/parallel/http2-large-write-close.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-http2-large-write-close.js -//#SHA1: 66ad4345c0888700887c23af455fdd9ff49721d9 -//----------------- -"use strict"; -const fixtures = require("../common/fixtures"); -const http2 = require("http2"); - -const { beforeEach, afterEach, test, expect } = require("bun:test"); -const { isWindows } = require("harness"); -const content = Buffer.alloc(1e5, 0x44); - -let server; -let port; - -beforeEach(done => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } - - server = http2.createSecureServer({ - key: fixtures.readKey("agent1-key.pem"), - cert: fixtures.readKey("agent1-cert.pem"), - }); - - server.on("stream", stream => { - stream.respond({ - "Content-Type": "application/octet-stream", - "Content-Length": content.byteLength.toString() * 2, - "Vary": "Accept-Encoding", - }); - - stream.write(content); - stream.write(content); - stream.end(); - stream.close(); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterEach(() => { - server.close(); -}); - -test.todoIf(isWindows)( - "HTTP/2 large write and close", - done => { - const client = http2.connect(`https://localhost:${port}`, { rejectUnauthorized: false }); - - const req = client.request({ ":path": "/" }); - req.end(); - - let receivedBufferLength = 0; - req.on("data", buf => { - receivedBufferLength += buf.byteLength; - }); - - req.on("close", () => { - expect(receivedBufferLength).toBe(content.byteLength * 2); - client.close(); - done(); - }); - }, - 5000, -); - -//<#END_FILE: test-http2-large-write-close.js diff --git a/test/js/node/test/parallel/http2-large-write-destroy.test.js b/test/js/node/test/parallel/http2-large-write-destroy.test.js deleted file mode 100644 index b9d7679961..0000000000 --- a/test/js/node/test/parallel/http2-large-write-destroy.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-http2-large-write-destroy.js -//#SHA1: 0c76344570b21b6ed78f12185ddefde59a9b2914 -//----------------- -'use strict'; - -const http2 = require('http2'); - -const content = Buffer.alloc(60000, 0x44); - -let server; - -afterEach(() => { - if (server) { - server.close(); - } -}); - -test('HTTP/2 large write and destroy', (done) => { - server = http2.createServer(); - - server.on('stream', (stream) => { - stream.respond({ - 'Content-Type': 'application/octet-stream', - 'Content-Length': (content.length.toString() * 2), - 'Vary': 'Accept-Encoding' - }, { waitForTrailers: true }); - - stream.write(content); - stream.destroy(); - }); - - server.listen(0, () => { - const client = http2.connect(`http://localhost:${server.address().port}`); - - const req = client.request({ ':path': '/' }); - req.end(); - req.resume(); // Otherwise close won't be emitted if there's pending data. - - req.on('close', () => { - client.close(); - done(); - }); - - req.on('error', (err) => { - // We expect an error due to the stream being destroyed - expect(err.code).toBe('ECONNRESET'); - client.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-http2-large-write-destroy.js diff --git a/test/js/node/test/parallel/http2-large-writes-session-memory-leak.test.js b/test/js/node/test/parallel/http2-large-writes-session-memory-leak.test.js deleted file mode 100644 index e718f292ff..0000000000 --- a/test/js/node/test/parallel/http2-large-writes-session-memory-leak.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-http2-large-writes-session-memory-leak.js -//#SHA1: 8f39b92e38fac58143b4d50534b2f6a171fb1a1b -//----------------- -'use strict'; - -const http2 = require('http2'); - -test.skip('HTTP/2 large writes should not cause session memory leak', () => { - console.log('This test is skipped because it requires specific Node.js internals and fixtures.'); - console.log('Original test description:'); - console.log('Regression test for https://github.com/nodejs/node/issues/29223.'); - console.log('There was a "leak" in the accounting of session memory leading'); - console.log('to streams eventually failing with NGHTTP2_ENHANCE_YOUR_CALM.'); - - // Original test logic preserved for reference: - /* - const server = http2.createSecureServer({ - key: fixtures.readKey('agent2-key.pem'), - cert: fixtures.readKey('agent2-cert.pem'), - }); - - const data200k = 'a'.repeat(200 * 1024); - server.on('stream', (stream) => { - stream.write(data200k); - stream.end(); - }); - - server.listen(0, () => { - const client = http2.connect(`https://localhost:${server.address().port}`, { - ca: fixtures.readKey('agent2-cert.pem'), - servername: 'agent2', - maxSessionMemory: 1 - }); - - let streamsLeft = 50; - function newStream() { - const stream = client.request({ ':path': '/' }); - stream.on('data', () => { }); - stream.on('close', () => { - if (streamsLeft-- > 0) { - newStream(); - } else { - client.destroy(); - server.close(); - } - }); - } - newStream(); - }); - */ -}); - -//<#END_FILE: test-http2-large-writes-session-memory-leak.js diff --git a/test/js/node/test/parallel/http2-many-writes-and-destroy.test.js b/test/js/node/test/parallel/http2-many-writes-and-destroy.test.js deleted file mode 100644 index 503419d879..0000000000 --- a/test/js/node/test/parallel/http2-many-writes-and-destroy.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-http2-many-writes-and-destroy.js -//#SHA1: b4a66fa27d761038f79e0eb3562f521724887db4 -//----------------- -"use strict"; - -const http2 = require("http2"); - -// Skip the test if crypto is not available -let hasCrypto; -try { - require("crypto"); - hasCrypto = true; -} catch (err) { - hasCrypto = false; -} - -(hasCrypto ? describe : describe.skip)("HTTP/2 many writes and destroy", () => { - let server; - let url; - - beforeAll(done => { - server = http2.createServer((req, res) => { - req.pipe(res); - }); - - server.listen(0, () => { - url = `http://localhost:${server.address().port}`; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("should handle many writes and destroy", done => { - const client = http2.connect(url); - const req = client.request({ ":method": "POST" }); - - for (let i = 0; i < 4000; i++) { - req.write(Buffer.alloc(6)); - } - - req.on("close", () => { - console.log("(req onclose)"); - client.close(); - done(); - }); - - req.once("data", () => { - req.destroy(); - }); - }); -}); - -//<#END_FILE: test-http2-many-writes-and-destroy.js diff --git a/test/js/node/test/parallel/http2-misc-util.test.js b/test/js/node/test/parallel/http2-misc-util.test.js deleted file mode 100644 index 0af25ec564..0000000000 --- a/test/js/node/test/parallel/http2-misc-util.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-http2-misc-util.js -//#SHA1: 0fa21e185faeff6ee5b1d703d9a998bf98d6b229 -//----------------- -const http2 = require("http2"); - -describe("HTTP/2 Misc Util", () => { - test("HTTP2 constants are defined", () => { - expect(http2.constants).toBeDefined(); - expect(http2.constants.NGHTTP2_SESSION_SERVER).toBe(0); - expect(http2.constants.NGHTTP2_SESSION_CLIENT).toBe(1); - }); - // make it not fail after re-enabling push - test.todo("HTTP2 default settings are within valid ranges", () => { - const defaultSettings = http2.getDefaultSettings(); - expect(defaultSettings).toBeDefined(); - expect(defaultSettings.headerTableSize).toBeGreaterThanOrEqual(0); - expect(defaultSettings.enablePush).toBe(true); // push is disabled because is not implemented yet - expect(defaultSettings.initialWindowSize).toBeGreaterThanOrEqual(0); - expect(defaultSettings.maxFrameSize).toBeGreaterThanOrEqual(16384); - expect(defaultSettings.maxConcurrentStreams).toBeGreaterThanOrEqual(0); - expect(defaultSettings.maxHeaderListSize).toBeGreaterThanOrEqual(0); - }); - - test("HTTP2 getPackedSettings and getUnpackedSettings", () => { - const settings = { - headerTableSize: 4096, - enablePush: true, - initialWindowSize: 65535, - maxFrameSize: 16384, - }; - const packed = http2.getPackedSettings(settings); - expect(packed).toBeInstanceOf(Buffer); - - const unpacked = http2.getUnpackedSettings(packed); - expect(unpacked).toEqual(expect.objectContaining(settings)); - }); -}); - -//<#END_FILE: test-http2-misc-util.js diff --git a/test/js/node/test/parallel/http2-multistream-destroy-on-read-tls.test.js b/test/js/node/test/parallel/http2-multistream-destroy-on-read-tls.test.js deleted file mode 100644 index 5e27b6472c..0000000000 --- a/test/js/node/test/parallel/http2-multistream-destroy-on-read-tls.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-http2-multistream-destroy-on-read-tls.js -//#SHA1: bf3869a9f8884210710d41c0fb1f54d2112e9af5 -//----------------- -"use strict"; -const http2 = require("http2"); - -describe("HTTP2 multistream destroy on read", () => { - let server; - const filenames = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; - - beforeAll(done => { - server = http2.createServer(); - - server.on("stream", stream => { - function write() { - stream.write("a".repeat(10240)); - stream.once("drain", write); - } - write(); - }); - - server.listen(0, done); - }); - - afterAll(() => { - if (server) { - server.close(); - } else { - done(); - } - }); - - test("should handle multiple stream destructions", done => { - const client = http2.connect(`http://localhost:${server.address().port}`); - - let destroyed = 0; - for (const entry of filenames) { - const stream = client.request({ - ":path": `/${entry}`, - }); - stream.once("data", () => { - stream.destroy(); - - if (++destroyed === filenames.length) { - client.close(); - done(); - } - }); - } - }); -}); - -//<#END_FILE: test-http2-multistream-destroy-on-read-tls.js diff --git a/test/js/node/test/parallel/http2-no-wanttrailers-listener.test.js b/test/js/node/test/parallel/http2-no-wanttrailers-listener.test.js deleted file mode 100644 index b7aa239af9..0000000000 --- a/test/js/node/test/parallel/http2-no-wanttrailers-listener.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-http2-no-wanttrailers-listener.js -//#SHA1: a5297c0a1ed58f7d2d0a13bc4eaaa198a7ab160e -//----------------- -"use strict"; - -const h2 = require("http2"); - -let server; -let client; - -beforeAll(() => { - // Check if crypto is available - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } -}); - -afterEach(() => { - if (client) { - client.close(); - } - if (server) { - server.close(); - } -}); - -test("HTTP/2 server should not hang without wantTrailers listener", done => { - server = h2.createServer(); - - server.on("stream", (stream, headers, flags) => { - stream.respond(undefined, { waitForTrailers: true }); - stream.end("ok"); - }); - - server.listen(0, () => { - const port = server.address().port; - client = h2.connect(`http://localhost:${port}`); - const req = client.request(); - req.resume(); - - req.on("trailers", () => { - throw new Error("Unexpected trailers event"); - }); - - req.on("close", () => { - done(); - }); - }); -}); - -//<#END_FILE: test-http2-no-wanttrailers-listener.js diff --git a/test/js/node/test/parallel/http2-options-server-response.test.js b/test/js/node/test/parallel/http2-options-server-response.test.js deleted file mode 100644 index 4ad8e33898..0000000000 --- a/test/js/node/test/parallel/http2-options-server-response.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-http2-options-server-response.js -//#SHA1: 66736f340efdbdf2e20a79a3dffe75f499e65d89 -//----------------- -'use strict'; - -const h2 = require('http2'); - -class MyServerResponse extends h2.Http2ServerResponse { - status(code) { - return this.writeHead(code, { 'Content-Type': 'text/plain' }); - } -} - -let server; -let client; - -beforeAll(() => { - if (!process.versions.openssl) { - return test.skip('missing crypto'); - } -}); - -afterAll(() => { - if (server) server.close(); - if (client) client.destroy(); -}); - -test('http2 server with custom ServerResponse', (done) => { - server = h2.createServer({ - Http2ServerResponse: MyServerResponse - }, (req, res) => { - res.status(200); - res.end(); - }); - - server.listen(0, () => { - const port = server.address().port; - client = h2.connect(`http://localhost:${port}`); - const req = client.request({ ':path': '/' }); - - const responseHandler = jest.fn(); - req.on('response', responseHandler); - - const endHandler = jest.fn(() => { - expect(responseHandler).toHaveBeenCalled(); - done(); - }); - - req.resume(); - req.on('end', endHandler); - }); -}); - -//<#END_FILE: test-http2-options-server-response.js diff --git a/test/js/node/test/parallel/http2-perf_hooks.test.js b/test/js/node/test/parallel/http2-perf_hooks.test.js deleted file mode 100644 index b45b8d48c7..0000000000 --- a/test/js/node/test/parallel/http2-perf_hooks.test.js +++ /dev/null @@ -1,124 +0,0 @@ -//#FILE: test-http2-perf_hooks.js -//#SHA1: a759a55527c8587bdf272da00c6597d93aa36da0 -//----------------- -'use strict'; - -const h2 = require('http2'); -const { PerformanceObserver } = require('perf_hooks'); - -let server; -let client; - -beforeAll(() => { - if (!process.versions.openssl) { - return test.skip('missing crypto'); - } -}); - -afterEach(() => { - if (client) client.close(); - if (server) server.close(); -}); - -test('HTTP/2 performance hooks', (done) => { - const obs = new PerformanceObserver((items) => { - const entry = items.getEntries()[0]; - expect(entry.entryType).toBe('http2'); - expect(typeof entry.startTime).toBe('number'); - expect(typeof entry.duration).toBe('number'); - - switch (entry.name) { - case 'Http2Session': - expect(typeof entry.pingRTT).toBe('number'); - expect(typeof entry.streamAverageDuration).toBe('number'); - expect(typeof entry.streamCount).toBe('number'); - expect(typeof entry.framesReceived).toBe('number'); - expect(typeof entry.framesSent).toBe('number'); - expect(typeof entry.bytesWritten).toBe('number'); - expect(typeof entry.bytesRead).toBe('number'); - expect(typeof entry.maxConcurrentStreams).toBe('number'); - expect(typeof entry.detail.pingRTT).toBe('number'); - expect(typeof entry.detail.streamAverageDuration).toBe('number'); - expect(typeof entry.detail.streamCount).toBe('number'); - expect(typeof entry.detail.framesReceived).toBe('number'); - expect(typeof entry.detail.framesSent).toBe('number'); - expect(typeof entry.detail.bytesWritten).toBe('number'); - expect(typeof entry.detail.bytesRead).toBe('number'); - expect(typeof entry.detail.maxConcurrentStreams).toBe('number'); - switch (entry.type) { - case 'server': - expect(entry.detail.streamCount).toBe(1); - expect(entry.detail.framesReceived).toBeGreaterThanOrEqual(3); - break; - case 'client': - expect(entry.detail.streamCount).toBe(1); - expect(entry.detail.framesReceived).toBe(7); - break; - default: - fail('invalid Http2Session type'); - } - break; - case 'Http2Stream': - expect(typeof entry.timeToFirstByte).toBe('number'); - expect(typeof entry.timeToFirstByteSent).toBe('number'); - expect(typeof entry.timeToFirstHeader).toBe('number'); - expect(typeof entry.bytesWritten).toBe('number'); - expect(typeof entry.bytesRead).toBe('number'); - expect(typeof entry.detail.timeToFirstByte).toBe('number'); - expect(typeof entry.detail.timeToFirstByteSent).toBe('number'); - expect(typeof entry.detail.timeToFirstHeader).toBe('number'); - expect(typeof entry.detail.bytesWritten).toBe('number'); - expect(typeof entry.detail.bytesRead).toBe('number'); - break; - default: - fail('invalid entry name'); - } - }); - - obs.observe({ type: 'http2' }); - - const body = '

this is some data

'; - - server = h2.createServer(); - - server.on('stream', (stream, headers, flags) => { - expect(headers[':scheme']).toBe('http'); - expect(headers[':authority']).toBeTruthy(); - expect(headers[':method']).toBe('GET'); - expect(flags).toBe(5); - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); - stream.write(body.slice(0, 20)); - stream.end(body.slice(20)); - }); - - server.on('session', (session) => { - session.ping(jest.fn()); - }); - - server.listen(0, () => { - client = h2.connect(`http://localhost:${server.address().port}`); - - client.on('connect', () => { - client.ping(jest.fn()); - }); - - const req = client.request(); - - req.on('response', jest.fn()); - - let data = ''; - req.setEncoding('utf8'); - req.on('data', (d) => data += d); - req.on('end', () => { - expect(body).toBe(data); - }); - req.on('close', () => { - obs.disconnect(); - done(); - }); - }); -}); -//<#END_FILE: test-http2-perf_hooks.js diff --git a/test/js/node/test/parallel/http2-pipe.test.js b/test/js/node/test/parallel/http2-pipe.test.js deleted file mode 100644 index 0f852cef61..0000000000 --- a/test/js/node/test/parallel/http2-pipe.test.js +++ /dev/null @@ -1,83 +0,0 @@ -//#FILE: test-http2-pipe.js -//#SHA1: bb970b612d495580b8c216a1b202037e5eb0721e -//----------------- -"use strict"; - -import { afterEach, beforeEach, test, expect, describe, mock } from "bun:test"; - -const http2 = require("http2"); -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -// Skip the test if crypto is not available -let hasCrypto; -try { - require("crypto"); - hasCrypto = true; -} catch (err) { - hasCrypto = false; -} - -const testIfCrypto = hasCrypto ? test : test.skip; - -describe("HTTP2 Pipe", () => { - let server; - let serverPort; - let tmpdir; - const fixturesDir = path.join(__dirname, "..", "fixtures"); - const loc = path.join(fixturesDir, "person-large.jpg"); - let fn; - - beforeEach(() => { - tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "http2-test-")); - fn = path.join(tmpdir, "http2-url-tests.js"); - }); - - afterEach(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); - }); - - testIfCrypto("Piping should work as expected with createWriteStream", done => { - server = http2.createServer(); - - server.on("stream", stream => { - const dest = stream.pipe(fs.createWriteStream(fn)); - - dest.on("finish", () => { - expect(fs.readFileSync(loc).length).toBe(fs.readFileSync(fn).length); - }); - stream.respond(); - stream.end(); - }); - - server.listen(0, () => { - serverPort = server.address().port; - const client = http2.connect(`http://localhost:${serverPort}`); - - const req = client.request({ ":method": "POST" }); - - const responseHandler = mock(() => {}); - req.on("response", responseHandler); - req.resume(); - - req.on("close", () => { - expect(responseHandler).toHaveBeenCalled(); - server.close(); - client.close(); - done(); - }); - - const str = fs.createReadStream(loc); - const strEndHandler = mock(() => {}); - str.on("end", strEndHandler); - str.pipe(req); - - req.on("finish", () => { - expect(strEndHandler).toHaveBeenCalled(); - }); - }); - }); -}); - -//<#END_FILE: test-http2-pipe.js diff --git a/test/js/node/test/parallel/http2-priority-cycle-.test.js b/test/js/node/test/parallel/http2-priority-cycle-.test.js deleted file mode 100644 index 61bab1f9cd..0000000000 --- a/test/js/node/test/parallel/http2-priority-cycle-.test.js +++ /dev/null @@ -1,84 +0,0 @@ -//#FILE: test-http2-priority-cycle-.js -//#SHA1: 32c70d0d1e4be42834f071fa3d9bb529aa4ea1c1 -//----------------- -'use strict'; - -const http2 = require('http2'); - -const largeBuffer = Buffer.alloc(1e4); - -class Countdown { - constructor(count, done) { - this.count = count; - this.done = done; - } - - dec() { - this.count--; - if (this.count === 0) this.done(); - } -} - -test('HTTP/2 priority cycle', (done) => { - const server = http2.createServer(); - - server.on('stream', (stream) => { - stream.respond(); - setImmediate(() => { - stream.end(largeBuffer); - }); - }); - - server.on('session', (session) => { - session.on('priority', (id, parent, weight, exclusive) => { - expect(weight).toBe(16); - expect(exclusive).toBe(false); - switch (id) { - case 1: - expect(parent).toBe(5); - break; - case 3: - expect(parent).toBe(1); - break; - case 5: - expect(parent).toBe(3); - break; - default: - fail('should not happen'); - } - }); - }); - - server.listen(0, () => { - const client = http2.connect(`http://localhost:${server.address().port}`); - - const countdown = new Countdown(3, () => { - client.close(); - server.close(); - done(); - }); - - { - const req = client.request(); - req.priority({ parent: 5 }); - req.resume(); - req.on('close', () => countdown.dec()); - } - - { - const req = client.request(); - req.priority({ parent: 1 }); - req.resume(); - req.on('close', () => countdown.dec()); - } - - { - const req = client.request(); - req.priority({ parent: 3 }); - req.resume(); - req.on('close', () => countdown.dec()); - } - }); -}); - -//<#END_FILE: test-http2-priority-cycle-.js diff --git a/test/js/node/test/parallel/http2-removed-header-stays-removed.test.js b/test/js/node/test/parallel/http2-removed-header-stays-removed.test.js deleted file mode 100644 index a996aabc1c..0000000000 --- a/test/js/node/test/parallel/http2-removed-header-stays-removed.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-http2-removed-header-stays-removed.js -//#SHA1: f8bc3d1be9927b83a02492d9cb44c803c337e3c1 -//----------------- -"use strict"; -const http2 = require("http2"); - -let server; -let port; - -beforeAll(done => { - server = http2.createServer((request, response) => { - response.setHeader("date", "snacks o clock"); - response.end(); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll(() => { - server.close(); -}); - -test("HTTP/2 removed header stays removed", done => { - const session = http2.connect(`http://localhost:${port}`); - const req = session.request(); - - req.on("response", (headers, flags) => { - expect(headers.date).toBe("snacks o clock"); - }); - - req.on("end", () => { - session.close(); - done(); - }); -}); - -// Conditional skip if crypto is not available -try { - require("crypto"); -} catch (err) { - test.skip("missing crypto", () => {}); -} - -//<#END_FILE: test-http2-removed-header-stays-removed.js diff --git a/test/js/node/test/parallel/http2-request-remove-connect-listener.test.js b/test/js/node/test/parallel/http2-request-remove-connect-listener.test.js deleted file mode 100644 index 85bcbf502c..0000000000 --- a/test/js/node/test/parallel/http2-request-remove-connect-listener.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-http2-request-remove-connect-listener.js -//#SHA1: 28cbc334f4429a878522e1e78eac56d13fb0c916 -//----------------- -'use strict'; - -const http2 = require('http2'); - -// Skip the test if crypto is not available -let cryptoAvailable = true; -try { - require('crypto'); -} catch (err) { - cryptoAvailable = false; -} - -test('HTTP/2 request removes connect listener', (done) => { - if (!cryptoAvailable) { - console.log('Skipping test: missing crypto'); - return done(); - } - - const server = http2.createServer(); - const streamHandler = jest.fn((stream) => { - stream.respond(); - stream.end(); - }); - server.on('stream', streamHandler); - - server.listen(0, () => { - const client = http2.connect(`http://localhost:${server.address().port}`); - const connectHandler = jest.fn(); - client.once('connect', connectHandler); - - const req = client.request(); - - req.on('response', () => { - expect(client.listenerCount('connect')).toBe(0); - expect(streamHandler).toHaveBeenCalled(); - expect(connectHandler).toHaveBeenCalled(); - }); - - req.on('close', () => { - server.close(); - client.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-http2-request-remove-connect-listener.js diff --git a/test/js/node/test/parallel/http2-request-response-proto.test.js b/test/js/node/test/parallel/http2-request-response-proto.test.js deleted file mode 100644 index 5ed889e51a..0000000000 --- a/test/js/node/test/parallel/http2-request-response-proto.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-http2-request-response-proto.js -//#SHA1: ffffac0d4d11b6a77ddbfce366c206de8db99446 -//----------------- -'use strict'; - -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -let http2; - -if (!hasCrypto) { - test.skip('missing crypto', () => {}); -} else { - http2 = require('http2'); - - const { - Http2ServerRequest, - Http2ServerResponse, - } = http2; - - describe('Http2ServerRequest and Http2ServerResponse prototypes', () => { - test('protoRequest should be instance of Http2ServerRequest', () => { - const protoRequest = { __proto__: Http2ServerRequest.prototype }; - expect(protoRequest instanceof Http2ServerRequest).toBe(true); - }); - - test('protoResponse should be instance of Http2ServerResponse', () => { - const protoResponse = { __proto__: Http2ServerResponse.prototype }; - expect(protoResponse instanceof Http2ServerResponse).toBe(true); - }); - }); -} - -//<#END_FILE: test-http2-request-response-proto.js diff --git a/test/js/node/test/parallel/http2-res-corked.test.js b/test/js/node/test/parallel/http2-res-corked.test.js deleted file mode 100644 index 0da21d6cc4..0000000000 --- a/test/js/node/test/parallel/http2-res-corked.test.js +++ /dev/null @@ -1,79 +0,0 @@ -//#FILE: test-http2-res-corked.js -//#SHA1: a6c5da9f22eae611c043c6d177d63c0eaca6e02e -//----------------- -"use strict"; -const http2 = require("http2"); - -// Skip the test if crypto is not available -let hasCrypto = false; -try { - require("crypto"); - hasCrypto = true; -} catch (err) { - // crypto not available -} - -(hasCrypto ? describe : describe.skip)("Http2ServerResponse#[writableCorked,cork,uncork]", () => { - let server; - let client; - let corksLeft = 0; - - beforeAll(done => { - server = http2.createServer((req, res) => { - expect(res.writableCorked).toBe(corksLeft); - res.write(Buffer.from("1".repeat(1024))); - res.cork(); - corksLeft++; - expect(res.writableCorked).toBe(corksLeft); - res.write(Buffer.from("1".repeat(1024))); - res.cork(); - corksLeft++; - expect(res.writableCorked).toBe(corksLeft); - res.write(Buffer.from("1".repeat(1024))); - res.cork(); - corksLeft++; - expect(res.writableCorked).toBe(corksLeft); - res.write(Buffer.from("1".repeat(1024))); - res.cork(); - corksLeft++; - expect(res.writableCorked).toBe(corksLeft); - res.uncork(); - corksLeft--; - expect(res.writableCorked).toBe(corksLeft); - res.uncork(); - corksLeft--; - expect(res.writableCorked).toBe(corksLeft); - res.uncork(); - corksLeft--; - expect(res.writableCorked).toBe(corksLeft); - res.uncork(); - corksLeft--; - expect(res.writableCorked).toBe(corksLeft); - res.end(); - }); - - server.listen(0, () => { - const port = server.address().port; - client = http2.connect(`http://localhost:${port}`); - done(); - }); - }); - - afterAll(() => { - client.close(); - server.close(); - }); - - test("cork and uncork operations", done => { - const req = client.request(); - let dataCallCount = 0; - req.on("data", () => { - dataCallCount++; - }); - req.on("end", () => { - expect(dataCallCount).toBe(2); - done(); - }); - }); -}); -//<#END_FILE: test-http2-res-corked.js diff --git a/test/js/node/test/parallel/http2-respond-file-compat.test.js b/test/js/node/test/parallel/http2-respond-file-compat.test.js deleted file mode 100644 index 7d05c6e8f0..0000000000 --- a/test/js/node/test/parallel/http2-respond-file-compat.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-http2-respond-file-compat.js -//#SHA1: fac1eb9c2e4f7a75e9c7605abc64fc9c6e6f7f14 -//----------------- -'use strict'; - -const http2 = require('http2'); -const fs = require('fs'); -const path = require('path'); - -const hasCrypto = (() => { - try { - require('crypto'); - return true; - } catch (err) { - return false; - } -})(); - -const fname = path.join(__dirname, '..', 'fixtures', 'elipses.txt'); - -describe('HTTP/2 respondWithFile', () => { - let server; - - beforeAll(() => { - if (!hasCrypto) { - return; - } - // Ensure the file exists - if (!fs.existsSync(fname)) { - fs.writeFileSync(fname, '...'); - } - }); - - afterAll(() => { - if (server) { - server.close(); - } - }); - - test('should respond with file', (done) => { - if (!hasCrypto) { - done(); - return; - } - - const requestHandler = jest.fn((request, response) => { - response.stream.respondWithFile(fname); - }); - - server = http2.createServer(requestHandler); - server.listen(0, () => { - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - - const responseHandler = jest.fn(); - req.on('response', responseHandler); - - req.on('end', () => { - expect(requestHandler).toHaveBeenCalled(); - expect(responseHandler).toHaveBeenCalled(); - client.close(); - server.close(() => { - done(); - }); - }); - - req.end(); - req.resume(); - }); - }); -}); - -//<#END_FILE: test-http2-respond-file-compat.js diff --git a/test/js/node/test/parallel/http2-respond-file-error-dir.test.js b/test/js/node/test/parallel/http2-respond-file-error-dir.test.js deleted file mode 100644 index b3b9e7a592..0000000000 --- a/test/js/node/test/parallel/http2-respond-file-error-dir.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-http2-respond-file-error-dir.js -//#SHA1: 61f98e2ad2c69302fe84383e1dec1118edaa70e1 -//----------------- -'use strict'; - -const http2 = require('http2'); -const path = require('path'); - -let server; -let client; - -beforeAll(() => { - if (!process.versions.openssl) { - test.skip('missing crypto'); - } -}); - -afterEach(() => { - if (client) { - client.close(); - } - if (server) { - server.close(); - } -}); - -test('http2 respondWithFile with directory should fail', (done) => { - server = http2.createServer(); - server.on('stream', (stream) => { - stream.respondWithFile(process.cwd(), { - 'content-type': 'text/plain' - }, { - onError(err) { - expect(err).toMatchObject({ - code: 'ERR_HTTP2_SEND_FILE', - name: 'Error', - message: 'Directories cannot be sent' - }); - - stream.respond({ ':status': 404 }); - stream.end(); - }, - statCheck: jest.fn() - }); - }); - - server.listen(0, () => { - const port = server.address().port; - client = http2.connect(`http://localhost:${port}`); - const req = client.request(); - - const responseHandler = jest.fn((headers) => { - expect(headers[':status']).toBe(404); - }); - - const dataHandler = jest.fn(); - const endHandler = jest.fn(() => { - expect(responseHandler).toHaveBeenCalled(); - expect(dataHandler).not.toHaveBeenCalled(); - done(); - }); - - req.on('response', responseHandler); - req.on('data', dataHandler); - req.on('end', endHandler); - req.end(); - }); -}); - -//<#END_FILE: test-http2-respond-file-error-dir.js diff --git a/test/js/node/test/parallel/http2-respond-file-filehandle.test.js b/test/js/node/test/parallel/http2-respond-file-filehandle.test.js deleted file mode 100644 index 297f53d852..0000000000 --- a/test/js/node/test/parallel/http2-respond-file-filehandle.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-http2-respond-file-filehandle.js -//#SHA1: c80cf9e1a4a879a73d275616e0604e56ac7756bb -//----------------- -'use strict'; - -const http2 = require('http2'); -const fs = require('fs'); -const path = require('path'); - -const { - HTTP2_HEADER_CONTENT_TYPE, - HTTP2_HEADER_CONTENT_LENGTH -} = http2.constants; - -const fixturesPath = path.join(__dirname, '..', 'fixtures'); -const fname = path.join(fixturesPath, 'elipses.txt'); - -test('http2 respond with file handle', async () => { - // Skip test if running in Bun - if (process.versions.bun) { - return; - } - - const data = await fs.promises.readFile(fname); - const stat = await fs.promises.stat(fname); - - const fileHandle = await fs.promises.open(fname, 'r'); - - const server = http2.createServer(); - server.on('stream', (stream) => { - stream.respondWithFD(fileHandle, { - [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', - [HTTP2_HEADER_CONTENT_LENGTH]: stat.size, - }); - }); - - const serverCloseHandler = jest.fn(); - server.on('close', serverCloseHandler); - - await new Promise(resolve => server.listen(0, resolve)); - - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - - const responseHandler = jest.fn((headers) => { - expect(headers[HTTP2_HEADER_CONTENT_TYPE]).toBe('text/plain'); - expect(Number(headers[HTTP2_HEADER_CONTENT_LENGTH])).toBe(data.length); - }); - req.on('response', responseHandler); - - req.setEncoding('utf8'); - let check = ''; - req.on('data', (chunk) => check += chunk); - - await new Promise(resolve => { - req.on('end', () => { - expect(check).toBe(data.toString('utf8')); - client.close(); - server.close(); - resolve(); - }); - req.end(); - }); - - await new Promise(resolve => server.on('close', resolve)); - - expect(responseHandler).toHaveBeenCalled(); - expect(serverCloseHandler).toHaveBeenCalled(); - - await fileHandle.close(); -}); - -//<#END_FILE: test-http2-respond-file-filehandle.js diff --git a/test/js/node/test/parallel/http2-sent-headers.test.js b/test/js/node/test/parallel/http2-sent-headers.test.js deleted file mode 100644 index 21a5c36ad1..0000000000 --- a/test/js/node/test/parallel/http2-sent-headers.test.js +++ /dev/null @@ -1,74 +0,0 @@ -//#FILE: test-http2-sent-headers.js -//#SHA1: cbc2db06925ef62397fd91d70872b787363cd96c -//----------------- -"use strict"; - -const h2 = require("http2"); - -const hasCrypto = (() => { - try { - require("crypto"); - return true; - } catch (err) { - return false; - } -})(); - -(hasCrypto ? describe : describe.skip)("http2 sent headers", () => { - let server; - let client; - let port; - - beforeAll(done => { - server = h2.createServer(); - - server.on("stream", stream => { - stream.additionalHeaders({ ":status": 102 }); - expect(stream.sentInfoHeaders[0][":status"]).toBe(102); - - stream.respond({ abc: "xyz" }, { waitForTrailers: true }); - stream.on("wantTrailers", () => { - stream.sendTrailers({ xyz: "abc" }); - }); - expect(stream.sentHeaders.abc).toBe("xyz"); - expect(stream.sentHeaders[":status"]).toBe(200); - expect(stream.sentHeaders.date).toBeDefined(); - stream.end(); - stream.on("close", () => { - expect(stream.sentTrailers.xyz).toBe("abc"); - }); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("client request headers", done => { - client = h2.connect(`http://localhost:${port}`); - const req = client.request(); - - req.on("headers", (headers, flags) => { - expect(headers[":status"]).toBe(102); - expect(typeof flags).toBe("number"); - }); - - expect(req.sentHeaders[":method"]).toBe("GET"); - expect(req.sentHeaders[":authority"]).toBe(`localhost:${port}`); - expect(req.sentHeaders[":scheme"]).toBe("http"); - expect(req.sentHeaders[":path"]).toBe("/"); - - req.resume(); - req.on("close", () => { - client.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-http2-sent-headers.js diff --git a/test/js/node/test/parallel/http2-server-async-dispose.test.js b/test/js/node/test/parallel/http2-server-async-dispose.test.js deleted file mode 100644 index bdf5282129..0000000000 --- a/test/js/node/test/parallel/http2-server-async-dispose.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-http2-server-async-dispose.js -//#SHA1: 3f26a183d15534b5f04c61836e718ede1726834f -//----------------- -'use strict'; - -const http2 = require('http2'); - -// Check if crypto is available -let hasCrypto = false; -try { - require('crypto'); - hasCrypto = true; -} catch (err) { - // crypto is not available -} - -(hasCrypto ? test : test.skip)('http2 server async close', (done) => { - const server = http2.createServer(); - - const closeHandler = jest.fn(); - server.on('close', closeHandler); - - server.listen(0, () => { - // Use the close method instead of Symbol.asyncDispose - server.close(() => { - expect(closeHandler).toHaveBeenCalled(); - done(); - }); - }); -}, 10000); // Increase timeout to 10 seconds - -//<#END_FILE: test-http2-server-async-dispose.js diff --git a/test/js/node/test/parallel/http2-server-push-stream-errors.test.js b/test/js/node/test/parallel/http2-server-push-stream-errors.test.js deleted file mode 100644 index 7c9aa34bcc..0000000000 --- a/test/js/node/test/parallel/http2-server-push-stream-errors.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-http2-server-push-stream-errors.js -//#SHA1: e0c43917d2cc3edee06a7d89fb1cbeff9c81fb08 -//----------------- -'use strict'; - -test.skip('HTTP/2 server push stream errors', () => { - console.log('This test is skipped because it relies on Node.js internals that are not accessible in Jest.'); -}); - -// Original test code (commented out for reference) -/* -const http2 = require('http2'); -const { internalBinding } = require('internal/test/binding'); -const { - constants, - Http2Stream, - nghttp2ErrorString -} = internalBinding('http2'); -const { NghttpError } = require('internal/http2/util'); - -// ... rest of the original test code ... -*/ - -//<#END_FILE: test-http2-server-push-stream-errors.test.js diff --git a/test/js/node/test/parallel/http2-server-rst-before-respond.test.js b/test/js/node/test/parallel/http2-server-rst-before-respond.test.js deleted file mode 100644 index 9280ea17eb..0000000000 --- a/test/js/node/test/parallel/http2-server-rst-before-respond.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-http2-server-rst-before-respond.js -//#SHA1: 67d0d7c2fdd32d5eb050bf8473a767dbf24d158a -//----------------- -'use strict'; - -const h2 = require('http2'); - -let server; -let client; - -beforeEach(() => { - server = h2.createServer(); -}); - -afterEach(() => { - if (server) server.close(); - if (client) client.close(); -}); - -test('HTTP/2 server reset stream before respond', (done) => { - if (!process.versions.openssl) { - test.skip('missing crypto'); - return; - } - - const onStream = jest.fn((stream, headers, flags) => { - stream.close(); - - expect(() => { - stream.additionalHeaders({ - ':status': 123, - 'abc': 123 - }); - }).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_INVALID_STREAM' - })); - }); - - server.on('stream', onStream); - - server.listen(0, () => { - const port = server.address().port; - client = h2.connect(`http://localhost:${port}`); - const req = client.request(); - - const onHeaders = jest.fn(); - req.on('headers', onHeaders); - - const onResponse = jest.fn(); - req.on('response', onResponse); - - req.on('close', () => { - expect(req.rstCode).toBe(h2.constants.NGHTTP2_NO_ERROR); - expect(onStream).toHaveBeenCalledTimes(1); - expect(onHeaders).not.toHaveBeenCalled(); - expect(onResponse).not.toHaveBeenCalled(); - done(); - }); - }); -}); - -//<#END_FILE: test-http2-server-rst-before-respond.js diff --git a/test/js/node/test/parallel/http2-server-set-header.test.js b/test/js/node/test/parallel/http2-server-set-header.test.js deleted file mode 100644 index 8f63781248..0000000000 --- a/test/js/node/test/parallel/http2-server-set-header.test.js +++ /dev/null @@ -1,77 +0,0 @@ -//#FILE: test-http2-server-set-header.js -//#SHA1: d4ba0042eab7b4ef4927f3aa3e344f4b5e04f935 -//----------------- -'use strict'; - -const http2 = require('http2'); - -const body = '

this is some data

'; - -let server; -let port; - -beforeAll((done) => { - server = http2.createServer((req, res) => { - res.setHeader('foobar', 'baz'); - res.setHeader('X-POWERED-BY', 'node-test'); - res.setHeader('connection', 'connection-test'); - res.end(body); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll((done) => { - server.close(done); -}); - -test('HTTP/2 server set header', (done) => { - const client = http2.connect(`http://localhost:${port}`); - const headers = { ':path': '/' }; - const req = client.request(headers); - req.setEncoding('utf8'); - - req.on('response', (headers) => { - expect(headers.foobar).toBe('baz'); - expect(headers['x-powered-by']).toBe('node-test'); - // The 'connection' header should not be present in HTTP/2 - expect(headers.connection).toBeUndefined(); - }); - - let data = ''; - req.on('data', (d) => data += d); - req.on('end', () => { - expect(data).toBe(body); - client.close(); - done(); - }); - req.end(); -}); - -test('Setting connection header should not throw', () => { - const res = { - setHeader: jest.fn() - }; - - expect(() => { - res.setHeader('connection', 'test'); - }).not.toThrow(); - - expect(res.setHeader).toHaveBeenCalledWith('connection', 'test'); -}); - -test('Server should not emit error', (done) => { - const errorListener = jest.fn(); - server.on('error', errorListener); - - setTimeout(() => { - expect(errorListener).not.toHaveBeenCalled(); - server.removeListener('error', errorListener); - done(); - }, 100); -}); - -//<#END_FILE: test-http2-server-set-header.js diff --git a/test/js/node/test/parallel/http2-session-timeout.test.js b/test/js/node/test/parallel/http2-session-timeout.test.js deleted file mode 100644 index 08b4a07c34..0000000000 --- a/test/js/node/test/parallel/http2-session-timeout.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http2-session-timeout.js -//#SHA1: 8a03d5dc642f9d07faac7b4a44caa0e02b625339 -//----------------- -'use strict'; - -const http2 = require('http2'); -const { hrtime } = process; -const NS_PER_MS = 1_000_000n; - -let requests = 0; - -test('HTTP/2 session timeout', (done) => { - const server = http2.createServer(); - server.timeout = 0n; - - server.on('request', (req, res) => res.end()); - server.on('timeout', () => { - throw new Error(`Timeout after ${requests} request(s)`); - }); - - server.listen(0, () => { - const port = server.address().port; - const url = `http://localhost:${port}`; - const client = http2.connect(url); - let startTime = hrtime.bigint(); - - function makeReq() { - const request = client.request({ - ':path': '/foobar', - ':method': 'GET', - ':scheme': 'http', - ':authority': `localhost:${port}`, - }); - request.resume(); - request.end(); - - requests += 1; - - request.on('end', () => { - const diff = hrtime.bigint() - startTime; - const milliseconds = diff / NS_PER_MS; - if (server.timeout === 0n) { - server.timeout = milliseconds * 2n; - startTime = hrtime.bigint(); - makeReq(); - } else if (milliseconds < server.timeout * 2n) { - makeReq(); - } else { - server.close(); - client.close(); - expect(requests).toBeGreaterThan(1); - done(); - } - }); - } - - makeReq(); - }); -}); - -//<#END_FILE: test-http2-session-timeout.js diff --git a/test/js/node/test/parallel/http2-socket-proxy.test.js b/test/js/node/test/parallel/http2-socket-proxy.test.js deleted file mode 100644 index 3e6122df11..0000000000 --- a/test/js/node/test/parallel/http2-socket-proxy.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http2-socket-proxy.js -//#SHA1: c5158fe06db7a7572dc5f7a52c23f019d16fb8ce -//----------------- -'use strict'; - -const h2 = require('http2'); -const net = require('net'); - -let server; -let port; - -beforeAll(async () => { - server = h2.createServer(); - await new Promise(resolve => server.listen(0, () => { - port = server.address().port; - resolve(); - })); -}); - -afterAll(async () => { - await new Promise(resolve => server.close(resolve)); -}); - -describe('HTTP/2 Socket Proxy', () => { - test('Socket behavior on Http2Session', async () => { - expect.assertions(5); - - server.once('stream', (stream, headers) => { - const socket = stream.session.socket; - const session = stream.session; - - expect(socket).toBeInstanceOf(net.Socket); - expect(socket.writable).toBe(true); - expect(socket.readable).toBe(true); - expect(typeof socket.address()).toBe('object'); - - // Test that setting a property on socket affects the session - const fn = jest.fn(); - socket.setTimeout = fn; - expect(session.setTimeout).toBe(fn); - - stream.respond({ ':status': 200 }); - stream.end('OK'); - }); - - const client = h2.connect(`http://localhost:${port}`); - const req = client.request({ ':path': '/' }); - - await new Promise(resolve => { - req.on('response', () => { - req.on('data', () => {}); - req.on('end', () => { - client.close(); - resolve(); - }); - }); - }); - }, 10000); // Increase timeout to 10 seconds -}); - -//<#END_FILE: test-http2-socket-proxy.js diff --git a/test/js/node/test/parallel/http2-status-code.test.js b/test/js/node/test/parallel/http2-status-code.test.js deleted file mode 100644 index ec02531975..0000000000 --- a/test/js/node/test/parallel/http2-status-code.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-http2-status-code.js -//#SHA1: 53911ac66c46f57bca1d56cdaf76e46d61c957d8 -//----------------- -"use strict"; - -const http2 = require("http2"); - -const codes = [200, 202, 300, 400, 404, 451, 500]; -let server; -let client; - -beforeAll(done => { - server = http2.createServer(); - - let testIndex = 0; - server.on("stream", stream => { - const status = codes[testIndex++]; - stream.respond({ ":status": status }, { endStream: true }); - }); - - server.listen(0, () => { - done(); - }); -}); - -afterAll(() => { - client.close(); - server.close(); -}); - -test("HTTP/2 status codes", done => { - const port = server.address().port; - client = http2.connect(`http://localhost:${port}`); - - let remaining = codes.length; - function maybeClose() { - if (--remaining === 0) { - done(); - } - } - - function doTest(expected) { - return new Promise(resolve => { - const req = client.request(); - req.on("response", headers => { - expect(headers[":status"]).toBe(expected); - }); - req.resume(); - req.on("end", () => { - maybeClose(); - resolve(); - }); - }); - } - - Promise.all(codes.map(doTest)).then(() => { - // All tests completed - }); -}); - -//<#END_FILE: test-http2-status-code.js diff --git a/test/js/node/test/parallel/http2-tls-disconnect.test.js b/test/js/node/test/parallel/http2-tls-disconnect.test.js deleted file mode 100644 index f26b325906..0000000000 --- a/test/js/node/test/parallel/http2-tls-disconnect.test.js +++ /dev/null @@ -1,17 +0,0 @@ -//#FILE: test-http2-tls-disconnect.js -//#SHA1: 0673265638d2f040031cb9fbc7a1fefda23ba0e1 -//----------------- -'use strict'; - -test.skip('http2 TLS disconnect', () => { - console.log('This test is skipped because:'); - console.log('1. It requires specific SSL certificate files (agent8-key.pem and agent8-cert.pem) which are not available in the current test environment.'); - console.log('2. It relies on an external tool (h2load) which may not be installed on all systems.'); - console.log('3. The test involves creating a real HTTPS server and spawning a child process, which is not ideal for unit testing.'); - console.log('To properly test this functionality, consider:'); - console.log('- Mocking the SSL certificates and http2 server creation'); - console.log('- Replacing the h2load functionality with a simulated load using pure JavaScript'); - console.log('- Focusing on testing the specific behavior (TLS disconnect handling) without relying on external tools'); -}); - -//<#END_FILE: test-http2-tls-disconnect.js diff --git a/test/js/node/test/parallel/http2-trailers.test.js b/test/js/node/test/parallel/http2-trailers.test.js deleted file mode 100644 index 63666b1966..0000000000 --- a/test/js/node/test/parallel/http2-trailers.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-http2-trailers.js -//#SHA1: 1e3d42d5008cf87fa8bf557b38f4fd00b4dbd712 -//----------------- -'use strict'; - -const h2 = require('http2'); - -const body = - '

this is some data

'; -const trailerKey = 'test-trailer'; -const trailerValue = 'testing'; - -let server; - -beforeAll(() => { - server = h2.createServer(); - server.on('stream', onStream); -}); - -afterAll(() => { - server.close(); -}); - -function onStream(stream, headers, flags) { - stream.on('trailers', (headers) => { - expect(headers[trailerKey]).toBe(trailerValue); - stream.end(body); - }); - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }, { waitForTrailers: true }); - stream.on('wantTrailers', () => { - stream.sendTrailers({ [trailerKey]: trailerValue }); - expect(() => stream.sendTrailers({})).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_TRAILERS_ALREADY_SENT', - name: 'Error' - })); - }); - - expect(() => stream.sendTrailers({})).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_TRAILERS_NOT_READY', - name: 'Error' - })); -} - -test('HTTP/2 trailers', (done) => { - server.listen(0, () => { - const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ ':path': '/', ':method': 'POST' }, - { waitForTrailers: true }); - req.on('wantTrailers', () => { - req.sendTrailers({ [trailerKey]: trailerValue }); - }); - req.on('data', () => {}); - req.on('trailers', (headers) => { - expect(headers[trailerKey]).toBe(trailerValue); - }); - req.on('close', () => { - expect(() => req.sendTrailers({})).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_INVALID_STREAM', - name: 'Error' - })); - client.close(); - done(); - }); - req.end('data'); - }); -}); - -//<#END_FILE: test-http2-trailers.js diff --git a/test/js/node/test/parallel/http2-unbound-socket-proxy.test.js b/test/js/node/test/parallel/http2-unbound-socket-proxy.test.js deleted file mode 100644 index c4c0635240..0000000000 --- a/test/js/node/test/parallel/http2-unbound-socket-proxy.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-http2-unbound-socket-proxy.js -//#SHA1: bcb8a31b2f29926a8e8d9a3bb5f23d09bfa5e805 -//----------------- -'use strict'; - -const http2 = require('http2'); -const net = require('net'); - -let server; -let client; - -beforeAll(() => { - if (!process.versions.openssl) { - return test.skip('missing crypto'); - } -}); - -afterEach(() => { - if (client) { - client.close(); - } - if (server) { - server.close(); - } -}); - -test('http2 unbound socket proxy', (done) => { - server = http2.createServer(); - const streamHandler = jest.fn((stream) => { - stream.respond(); - stream.end('ok'); - }); - server.on('stream', streamHandler); - - server.listen(0, () => { - client = http2.connect(`http://localhost:${server.address().port}`); - const socket = client.socket; - const req = client.request(); - req.resume(); - req.on('close', () => { - client.close(); - server.close(); - - // Tests to make sure accessing the socket proxy fails with an - // informative error. - setImmediate(() => { - expect(() => { - socket.example; // eslint-disable-line no-unused-expressions - }).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_SOCKET_UNBOUND' - })); - - expect(() => { - socket.example = 1; - }).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_SOCKET_UNBOUND' - })); - - expect(() => { - // eslint-disable-next-line no-unused-expressions - socket instanceof net.Socket; - }).toThrow(expect.objectContaining({ - code: 'ERR_HTTP2_SOCKET_UNBOUND' - })); - - expect(streamHandler).toHaveBeenCalled(); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-http2-unbound-socket-proxy.js diff --git a/test/js/node/test/parallel/http2-util-assert-valid-pseudoheader.test.js b/test/js/node/test/parallel/http2-util-assert-valid-pseudoheader.test.js deleted file mode 100644 index 42f0ccf3c2..0000000000 --- a/test/js/node/test/parallel/http2-util-assert-valid-pseudoheader.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-http2-util-assert-valid-pseudoheader.js -//#SHA1: 765cdbf9a64c432ef1706fb7b24ab35d926cda3b -//----------------- -'use strict'; - -let mapToHeaders; - -beforeAll(() => { - try { - // Try to require the internal module - ({ mapToHeaders } = require('internal/http2/util')); - } catch (error) { - // If the internal module is not available, mock it - mapToHeaders = jest.fn((headers) => { - const validPseudoHeaders = [':status', ':path', ':authority', ':scheme', ':method']; - for (const key in headers) { - if (key.startsWith(':') && !validPseudoHeaders.includes(key)) { - throw new TypeError(`"${key}" is an invalid pseudoheader or is used incorrectly`); - } - } - }); - } -}); - -describe('HTTP/2 Util - assertValidPseudoHeader', () => { - test('should not throw for valid pseudo-headers', () => { - expect(() => mapToHeaders({ ':status': 'a' })).not.toThrow(); - expect(() => mapToHeaders({ ':path': 'a' })).not.toThrow(); - expect(() => mapToHeaders({ ':authority': 'a' })).not.toThrow(); - expect(() => mapToHeaders({ ':scheme': 'a' })).not.toThrow(); - expect(() => mapToHeaders({ ':method': 'a' })).not.toThrow(); - }); - - test('should throw for invalid pseudo-headers', () => { - expect(() => mapToHeaders({ ':foo': 'a' })).toThrow(expect.objectContaining({ - name: 'TypeError', - message: expect.stringContaining('is an invalid pseudoheader or is used incorrectly') - })); - }); -}); - -//<#END_FILE: test-http2-util-assert-valid-pseudoheader.js diff --git a/test/js/node/test/parallel/http2-util-update-options-buffer.test.js b/test/js/node/test/parallel/http2-util-update-options-buffer.test.js deleted file mode 100644 index d83855aa28..0000000000 --- a/test/js/node/test/parallel/http2-util-update-options-buffer.test.js +++ /dev/null @@ -1,11 +0,0 @@ -//#FILE: test-http2-util-update-options-buffer.js -//#SHA1: f1d75eaca8be74152cd7eafc114815b5d59d7f0c -//----------------- -'use strict'; - -test('Skip: HTTP/2 util update options buffer test', () => { - console.log('This test is skipped because it relies on Node.js internals that are not easily accessible in a Jest environment.'); - expect(true).toBe(true); -}); - -//<#END_FILE: test-http2-util-update-options-buffer.js diff --git a/test/js/node/test/parallel/http2-write-callbacks.test.js b/test/js/node/test/parallel/http2-write-callbacks.test.js deleted file mode 100644 index 2aa826a373..0000000000 --- a/test/js/node/test/parallel/http2-write-callbacks.test.js +++ /dev/null @@ -1,72 +0,0 @@ -//#FILE: test-http2-write-callbacks.js -//#SHA1: 4ad84acd162dcde6c2fbe344e6da2a3ec225edc1 -//----------------- -"use strict"; - -const http2 = require("http2"); - -// Mock for common.mustCall -const mustCall = fn => { - const wrappedFn = jest.fn(fn); - return wrappedFn; -}; - -describe("HTTP/2 write callbacks", () => { - let server; - let client; - let port; - - beforeAll(done => { - server = http2.createServer(); - server.listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("write callbacks are called", done => { - const serverWriteCallback = mustCall(() => {}); - const clientWriteCallback = mustCall(() => {}); - - server.once("stream", stream => { - stream.write("abc", serverWriteCallback); - stream.end("xyz"); - - let actual = ""; - stream.setEncoding("utf8"); - stream.on("data", chunk => (actual += chunk)); - stream.on("end", () => { - expect(actual).toBe("abcxyz"); - }); - }); - - client = http2.connect(`http://localhost:${port}`); - const req = client.request({ ":method": "POST" }); - - req.write("abc", clientWriteCallback); - req.end("xyz"); - - let actual = ""; - req.setEncoding("utf8"); - req.on("data", chunk => (actual += chunk)); - req.on("end", () => { - expect(actual).toBe("abcxyz"); - }); - - req.on("close", () => { - client.close(); - - // Check if callbacks were called - expect(serverWriteCallback).toHaveBeenCalled(); - expect(clientWriteCallback).toHaveBeenCalled(); - - done(); - }); - }); -}); - -//<#END_FILE: test-http2-write-callbacks.js diff --git a/test/js/node/test/parallel/http2-write-empty-string.test.js b/test/js/node/test/parallel/http2-write-empty-string.test.js deleted file mode 100644 index ca1e65b234..0000000000 --- a/test/js/node/test/parallel/http2-write-empty-string.test.js +++ /dev/null @@ -1,69 +0,0 @@ -//#FILE: test-http2-write-empty-string.js -//#SHA1: 59ba4a8a3c63aad827770d96f668922107ed2f2f -//----------------- -'use strict'; - -const http2 = require('http2'); - -// Skip the test if crypto is not available -let http2Server; -beforeAll(() => { - if (!process.versions.openssl) { - test.skip('missing crypto'); - } -}); - -afterAll(() => { - if (http2Server) { - http2Server.close(); - } -}); - -test('HTTP/2 server writes empty strings correctly', async () => { - http2Server = http2.createServer((request, response) => { - response.writeHead(200, { 'Content-Type': 'text/plain' }); - response.write('1\n'); - response.write(''); - response.write('2\n'); - response.write(''); - response.end('3\n'); - }); - - await new Promise(resolve => { - http2Server.listen(0, resolve); - }); - - const port = http2Server.address().port; - const client = http2.connect(`http://localhost:${port}`); - const headers = { ':path': '/' }; - - const responsePromise = new Promise((resolve, reject) => { - const req = client.request(headers); - - let res = ''; - req.setEncoding('ascii'); - - req.on('response', (headers) => { - expect(headers[':status']).toBe(200); - }); - - req.on('data', (chunk) => { - res += chunk; - }); - - req.on('end', () => { - resolve(res); - }); - - req.on('error', reject); - - req.end(); - }); - - const response = await responsePromise; - expect(response).toBe('1\n2\n3\n'); - - await new Promise(resolve => client.close(resolve)); -}); - -//<#END_FILE: test-http2-write-empty-string.js diff --git a/test/js/node/test/parallel/http2-zero-length-header.test.js b/test/js/node/test/parallel/http2-zero-length-header.test.js deleted file mode 100644 index aef1d62dbf..0000000000 --- a/test/js/node/test/parallel/http2-zero-length-header.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-http2-zero-length-header.js -//#SHA1: 65bd4ca954be7761c2876b26c6ac5d3f0e5c98e4 -//----------------- -"use strict"; -const http2 = require("http2"); - -// Skip test if crypto is not available -const hasCrypto = (() => { - try { - require("crypto"); - return true; - } catch (err) { - return false; - } -})(); - -(hasCrypto ? describe : describe.skip)("http2 zero length header", () => { - let server; - let port; - - beforeAll(async () => { - server = http2.createServer(); - await new Promise(resolve => server.listen(0, resolve)); - port = server.address().port; - }); - - afterAll(() => { - server.close(); - }); - - test("server receives correct headers", async () => { - const serverPromise = new Promise(resolve => { - server.once("stream", (stream, headers) => { - expect(headers).toEqual({ - ":scheme": "http", - ":authority": `localhost:${port}`, - ":method": "GET", - ":path": "/", - "bar": "", - "__proto__": null, - [http2.sensitiveHeaders]: [], - }); - stream.session.destroy(); - resolve(); - }); - }); - - const client = http2.connect(`http://localhost:${port}/`); - client.request({ ":path": "/", "": "foo", "bar": "" }).end(); - - await serverPromise; - client.close(); - }); -}); - -//<#END_FILE: test-http2-zero-length-header.js diff --git a/test/js/node/test/parallel/http2-zero-length-write.test.js b/test/js/node/test/parallel/http2-zero-length-write.test.js deleted file mode 100644 index dbd25616c5..0000000000 --- a/test/js/node/test/parallel/http2-zero-length-write.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-http2-zero-length-write.js -//#SHA1: a948a83af3675490313ff7b33a36d2c12cdd2837 -//----------------- -"use strict"; - -const http2 = require("http2"); -const { Readable } = require("stream"); - -function getSrc() { - const chunks = ["", "asdf", "", "foo", "", "bar", ""]; - return new Readable({ - read() { - const chunk = chunks.shift(); - if (chunk !== undefined) this.push(chunk); - else this.push(null); - }, - }); -} - -const expectedOutput = "asdffoobar"; - -let server; -let client; - -beforeAll(() => { - if (!process.versions.openssl) { - test.skip("missing crypto"); - } -}); - -afterEach(() => { - if (client) client.close(); - if (server) server.close(); -}); - -test("HTTP/2 zero length write", async () => { - return new Promise((resolve, reject) => { - server = http2.createServer(); - server.on("stream", stream => { - let actual = ""; - stream.respond(); - stream.resume(); - stream.setEncoding("utf8"); - stream.on("data", chunk => (actual += chunk)); - stream.on("end", () => { - getSrc().pipe(stream); - expect(actual).toBe(expectedOutput); - }); - }); - - server.listen(0, () => { - client = http2.connect(`http://localhost:${server.address().port}`); - let actual = ""; - const req = client.request({ ":method": "POST" }); - req.on("response", () => {}); - req.setEncoding("utf8"); - req.on("data", chunk => (actual += chunk)); - - req.on("end", () => { - expect(actual).toBe(expectedOutput); - resolve(); - }); - getSrc().pipe(req); - }); - }); -}, 10000); // Increase timeout to 10 seconds - -//<#END_FILE: test-http2-zero-length-write.js diff --git a/test/js/node/test/parallel/https-agent-constructor.test.js b/test/js/node/test/parallel/https-agent-constructor.test.js deleted file mode 100644 index dc3079eddd..0000000000 --- a/test/js/node/test/parallel/https-agent-constructor.test.js +++ /dev/null @@ -1,17 +0,0 @@ -//#FILE: test-https-agent-constructor.js -//#SHA1: 6b63dcb4d1a1a60f19fbb26cb555013821af5791 -//----------------- -"use strict"; - -if (!process.versions.openssl) { - test.skip("missing crypto"); -} - -const https = require("https"); - -test("https.Agent constructor", () => { - expect(new https.Agent()).toBeInstanceOf(https.Agent); - expect(https.Agent()).toBeInstanceOf(https.Agent); -}); - -//<#END_FILE: test-https-agent-constructor.js diff --git a/test/js/node/test/parallel/https-agent.test.js b/test/js/node/test/parallel/https-agent.test.js deleted file mode 100644 index 31b1e0ee25..0000000000 --- a/test/js/node/test/parallel/https-agent.test.js +++ /dev/null @@ -1,106 +0,0 @@ -//#FILE: test-https-agent.js -//#SHA1: 1348abc863ae99725dd893838c95b42c5120a052 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const https = require("https"); -const { readKey } = require("../common/fixtures"); - -const options = { - key: readKey("agent1-key.pem"), - cert: readKey("agent1-cert.pem"), -}; - -const N = 4; -const M = 4; - -let server; -let responses = 0; - -beforeAll(() => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } -}); - -beforeEach(() => { - return new Promise(resolve => { - server = https.createServer(options, (req, res) => { - res.writeHead(200); - res.end("hello world\n"); - }); - - server.listen(0, () => { - resolve(); - }); - }); -}); - -afterEach(() => { - return new Promise(resolve => { - server.close(() => { - resolve(); - }); - }); -}); - -test("HTTPS Agent handles multiple concurrent requests", async () => { - const makeRequests = i => { - return new Promise(resolve => { - setTimeout(() => { - const requests = Array.from( - { length: M }, - () => - new Promise(innerResolve => { - https - .get( - { - path: "/", - port: server.address().port, - rejectUnauthorized: false, - }, - function (res) { - res.resume(); - expect(res.statusCode).toBe(200); - responses++; - innerResolve(); - }, - ) - .on("error", e => { - throw e; - }); - }), - ); - Promise.all(requests).then(resolve); - }, i); - }); - }; - - const allRequests = Array.from({ length: N }, (_, i) => makeRequests(i)); - await Promise.all(allRequests); - - expect(responses).toBe(N * M); -}); - -//<#END_FILE: test-https-agent.js diff --git a/test/js/node/test/parallel/https-byteswritten.test.js b/test/js/node/test/parallel/https-byteswritten.test.js deleted file mode 100644 index c695fa7027..0000000000 --- a/test/js/node/test/parallel/https-byteswritten.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-https-byteswritten.js -//#SHA1: 8b808da3e55de553190426095aee298ec7d5df36 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fixtures = require("../common/fixtures"); -const https = require("https"); - -const options = { - key: fixtures.readKey("agent1-key.pem"), - cert: fixtures.readKey("agent1-cert.pem"), -}; - -const body = "hello world\n"; - -test("HTTPS server bytesWritten", async () => { - const httpsServer = https.createServer(options, (req, res) => { - res.on("finish", () => { - expect(typeof req.connection.bytesWritten).toBe("number"); - expect(req.connection.bytesWritten).toBeGreaterThan(0); - httpsServer.close(); - }); - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end(body); - }); - - await new Promise(resolve => { - httpsServer.listen(0, () => { - https.get({ - port: httpsServer.address().port, - rejectUnauthorized: false, - }); - resolve(); - }); - }); -}); - -//<#END_FILE: test-https-byteswritten.js diff --git a/test/js/node/test/parallel/https-foafssl.test.js b/test/js/node/test/parallel/https-foafssl.test.js deleted file mode 100644 index aac467af04..0000000000 --- a/test/js/node/test/parallel/https-foafssl.test.js +++ /dev/null @@ -1,114 +0,0 @@ -//#FILE: test-https-foafssl.js -//#SHA1: 07ac711f5948207540af7366d06803f2675f04c7 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const { skip } = require("../common"); -const fixtures = require("../common/fixtures"); -const https = require("https"); -const { spawn } = require("child_process"); - -if (!process.versions.openssl) { - skip("missing crypto"); -} - -if (!process.env.NODE_OPENSSL_CERT) { - skip("node compiled without OpenSSL CLI."); -} - -const options = { - key: fixtures.readKey("rsa_private.pem"), - cert: fixtures.readKey("rsa_cert.crt"), - requestCert: true, - rejectUnauthorized: false, -}; - -const webIdUrl = "URI:http://example.com/#me"; -const modulus = fixtures.readKey("rsa_cert_foafssl_b.modulus", "ascii").replace(/\n/g, ""); -const exponent = fixtures.readKey("rsa_cert_foafssl_b.exponent", "ascii").replace(/\n/g, ""); - -const CRLF = "\r\n"; -const body = "hello world\n"; -let cert; - -test("HTTPS FOAFSSL", async () => { - const serverHandler = jest.fn((req, res) => { - console.log("got request"); - - cert = req.connection.getPeerCertificate(); - - expect(cert.subjectaltname).toBe(webIdUrl); - expect(cert.exponent).toBe(exponent); - expect(cert.modulus).toBe(modulus); - res.writeHead(200, { "content-type": "text/plain" }); - res.end(body, () => { - console.log("stream finished"); - }); - console.log("sent response"); - }); - - const server = https.createServer(options, serverHandler); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const { port } = server.address(); - - const args = [ - "s_client", - "-quiet", - "-connect", - `127.0.0.1:${port}`, - "-cert", - fixtures.path("keys/rsa_cert_foafssl_b.crt"), - "-key", - fixtures.path("keys/rsa_private_b.pem"), - ]; - - const client = spawn(process.env.NODE_OPENSSL_CERT, args); - - client.stdout.on("data", data => { - console.log("response received"); - const message = data.toString(); - const contents = message.split(CRLF + CRLF).pop(); - expect(contents).toBe(body); - server.close(e => { - expect(e).toBeFalsy(); - console.log("server closed"); - }); - console.log("server.close() called"); - }); - - client.stdin.write("GET /\r\n\r\n"); - - await new Promise((resolve, reject) => { - client.on("error", reject); - client.on("close", resolve); - }); - - expect(serverHandler).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-https-foafssl.js diff --git a/test/js/node/test/parallel/https-socket-options.test.js b/test/js/node/test/parallel/https-socket-options.test.js deleted file mode 100644 index 5e325acc5d..0000000000 --- a/test/js/node/test/parallel/https-socket-options.test.js +++ /dev/null @@ -1,102 +0,0 @@ -//#FILE: test-https-socket-options.js -//#SHA1: 8f63b3c65f69e8b766b159d148e681984c134477 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const fixtures = require("../common/fixtures"); -const https = require("https"); -const http = require("http"); - -const options = { - key: fixtures.readKey("agent1-key.pem"), - cert: fixtures.readKey("agent1-cert.pem"), -}; - -const body = "hello world\n"; - -test("HTTP server socket options", async () => { - const server_http = http.createServer((req, res) => { - console.log("got HTTP request"); - res.writeHead(200, { "content-type": "text/plain" }); - res.end(body); - }); - - await new Promise(resolve => { - server_http.listen(0, () => { - const req = http.request( - { - port: server_http.address().port, - rejectUnauthorized: false, - }, - res => { - server_http.close(); - res.resume(); - resolve(); - }, - ); - // These methods should exist on the request and get passed down to the socket - expect(req.setNoDelay).toBeDefined(); - expect(req.setTimeout).toBeDefined(); - expect(req.setSocketKeepAlive).toBeDefined(); - req.setNoDelay(true); - req.setTimeout(1000, () => {}); - req.setSocketKeepAlive(true, 1000); - req.end(); - }); - }); -}); - -test("HTTPS server socket options", async () => { - const server_https = https.createServer(options, (req, res) => { - console.log("got HTTPS request"); - res.writeHead(200, { "content-type": "text/plain" }); - res.end(body); - }); - - await new Promise(resolve => { - server_https.listen(0, () => { - const req = https.request( - { - port: server_https.address().port, - rejectUnauthorized: false, - }, - res => { - server_https.close(); - res.resume(); - resolve(); - }, - ); - // These methods should exist on the request and get passed down to the socket - expect(req.setNoDelay).toBeDefined(); - expect(req.setTimeout).toBeDefined(); - expect(req.setSocketKeepAlive).toBeDefined(); - req.setNoDelay(true); - req.setTimeout(1000, () => {}); - req.setSocketKeepAlive(true, 1000); - req.end(); - }); - }); -}); - -//<#END_FILE: test-https-socket-options.js diff --git a/test/js/node/test/parallel/inspect-publish-uid.test.js b/test/js/node/test/parallel/inspect-publish-uid.test.js deleted file mode 100644 index 09ec36dcd3..0000000000 --- a/test/js/node/test/parallel/inspect-publish-uid.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-inspect-publish-uid.js -//#SHA1: cd6577ea81261e5e89b5ec6272e27f5a0614ffcf -//----------------- -"use strict"; - -const { spawnSync } = require("child_process"); -const inspector = require("inspector"); -const http = require("http"); -const url = require("url"); - -// Skip the test if inspector is disabled -if (!inspector.url()) { - test.skip("Inspector is disabled", () => {}); -} else { - test("Checks stderr", async () => { - await testArg("stderr"); - }); - - test("Checks http", async () => { - await testArg("http"); - }); - - test("Checks http,stderr", async () => { - await testArg("http,stderr"); - }); -} - -async function testArg(argValue) { - console.log("Checks " + argValue + ".."); - const hasHttp = argValue.split(",").includes("http"); - const hasStderr = argValue.split(",").includes("stderr"); - - const nodeProcess = spawnSync(process.execPath, [ - "--inspect=0", - `--inspect-publish-uid=${argValue}`, - "-e", - `(${scriptMain.toString()})(${hasHttp ? 200 : 404})`, - ]); - const hasWebSocketInStderr = checkStdError(nodeProcess.stderr.toString("utf8")); - expect(hasWebSocketInStderr).toBe(hasStderr); -} - -function checkStdError(data) { - const matches = data.toString("utf8").match(/ws:\/\/.+:(\d+)\/.+/); - return !!matches; -} - -function scriptMain(code) { - const inspectorUrl = inspector.url(); - const { host } = url.parse(inspectorUrl); - http.get("http://" + host + "/json/list", response => { - expect(response.statusCode).toBe(code); - response.destroy(); - }); -} - -//<#END_FILE: test-inspect-publish-uid.js diff --git a/test/js/node/test/parallel/inspect-support-for-node_options.test.js b/test/js/node/test/parallel/inspect-support-for-node_options.test.js deleted file mode 100644 index 77187f7aa6..0000000000 --- a/test/js/node/test/parallel/inspect-support-for-node_options.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-inspect-support-for-node_options.js -//#SHA1: 622a8cb07922833373b0d88c42c2a7bbfdc7d58e -//----------------- -"use strict"; - -const cluster = require("cluster"); - -// Skip if inspector is disabled -if (process.config.variables.v8_enable_inspector === 0) { - test.skip("Inspector is disabled", () => {}); -} else { - checkForInspectSupport("--inspect"); -} - -function checkForInspectSupport(flag) { - const nodeOptions = JSON.stringify(flag); - const numWorkers = 2; - process.env.NODE_OPTIONS = flag; - - test(`Cluster support for NODE_OPTIONS ${nodeOptions}`, () => { - if (cluster.isPrimary) { - const workerExitPromises = []; - - for (let i = 0; i < numWorkers; i++) { - const worker = cluster.fork(); - - worker.on("online", () => { - worker.disconnect(); - }); - - workerExitPromises.push( - new Promise(resolve => { - worker.on("exit", (code, signal) => { - expect(worker.exitedAfterDisconnect).toBe(true); - resolve(); - }); - }), - ); - } - - return Promise.all(workerExitPromises); - } - }); -} - -//<#END_FILE: test-inspect-support-for-node_options.js diff --git a/test/js/node/test/parallel/internal-fs.test.js b/test/js/node/test/parallel/internal-fs.test.js deleted file mode 100644 index 7b84ec5156..0000000000 --- a/test/js/node/test/parallel/internal-fs.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-internal-fs.js -//#SHA1: 47b2f898d6c0cdfba71a1f82b7617f466eb475c9 -//----------------- -'use strict'; - -const assert = require('assert'); -const fs = require('fs'); -const path = require('path'); - -// We can't use internal modules in this test, so we'll mock the necessary functions -const mockFs = { - assertEncoding: (encoding) => { - if (encoding && !['utf8', 'utf-8', 'ascii', 'utf16le', 'ucs2', 'ucs-2', 'base64', 'base64url', 'latin1', 'binary', 'hex'].includes(encoding.toLowerCase())) { - throw new TypeError('ERR_INVALID_ARG_VALUE'); - } - }, - preprocessSymlinkDestination: (pathString, type, linkPathString) => { - if (process.platform === 'win32' && type === 'junction') { - return path.join('\\\\?\\', pathString); - } - return pathString; - } -}; - -test('assertEncoding should not throw for valid encodings', () => { - expect(() => mockFs.assertEncoding()).not.toThrow(); - expect(() => mockFs.assertEncoding('utf8')).not.toThrow(); -}); - -test('assertEncoding should throw for invalid encodings', () => { - expect(() => mockFs.assertEncoding('foo')).toThrow(expect.objectContaining({ - name: 'TypeError', - message: expect.stringContaining('ERR_INVALID_ARG_VALUE') - })); -}); - -test('preprocessSymlinkDestination for junction symlinks', () => { - const pathString = 'c:\\test1'; - const linkPathString = '\\test2'; - - const preprocessSymlinkDestination = mockFs.preprocessSymlinkDestination( - pathString, - 'junction', - linkPathString - ); - - if (process.platform === 'win32') { - expect(preprocessSymlinkDestination).toMatch(/^\\\\\?\\/); - } else { - expect(preprocessSymlinkDestination).toBe(pathString); - } -}); - -test('preprocessSymlinkDestination for non-junction symlinks', () => { - const pathString = 'c:\\test1'; - const linkPathString = '\\test2'; - - const preprocessSymlinkDestination = mockFs.preprocessSymlinkDestination( - pathString, - undefined, - linkPathString - ); - - if (process.platform === 'win32') { - expect(preprocessSymlinkDestination).not.toMatch(/\//); - } else { - expect(preprocessSymlinkDestination).toBe(pathString); - } -}); - -//<#END_FILE: test-internal-fs.js diff --git a/test/js/node/test/parallel/internal-process-binding.test.js b/test/js/node/test/parallel/internal-process-binding.test.js deleted file mode 100644 index e3b90d28a4..0000000000 --- a/test/js/node/test/parallel/internal-process-binding.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-internal-process-binding.js -//#SHA1: e14c48cb6cd21ab499bd5d72cf8c8d0cddccf767 -//----------------- -"use strict"; - -test("process internal binding", () => { - expect(process._internalBinding).toBeUndefined(); - expect(process.internalBinding).toBeUndefined(); - expect(() => { - process.binding("module_wrap"); - }).toThrow( - expect.objectContaining({ - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-internal-process-binding.js diff --git a/test/js/node/test/parallel/intl-v8breakiterator.test.js b/test/js/node/test/parallel/intl-v8breakiterator.test.js deleted file mode 100644 index 7bf86915d2..0000000000 --- a/test/js/node/test/parallel/intl-v8breakiterator.test.js +++ /dev/null @@ -1,19 +0,0 @@ -//#FILE: test-intl-v8BreakIterator.js -//#SHA1: c1592e4a1a3d4971e70d4b2bc30e31bb157f8646 -//----------------- -"use strict"; - -if (!globalThis.Intl) { - test.skip("missing Intl"); -} - -test("v8BreakIterator is not in Intl", () => { - expect("v8BreakIterator" in Intl).toBe(false); -}); - -test("v8BreakIterator is not in Intl in a new context", () => { - const vm = require("vm"); - expect(vm.runInNewContext('"v8BreakIterator" in Intl')).toBe(false); -}); - -//<#END_FILE: test-intl-v8BreakIterator.js diff --git a/test/js/node/test/parallel/jest.config.js b/test/js/node/test/parallel/jest.config.js deleted file mode 100644 index 7f2c94ff64..0000000000 --- a/test/js/node/test/parallel/jest.config.js +++ /dev/null @@ -1,2 +0,0 @@ -// So jest doesn't try to look up. -module.exports = {}; diff --git a/test/js/node/test/parallel/js-stream-call-properties.test.js b/test/js/node/test/parallel/js-stream-call-properties.test.js deleted file mode 100644 index 070bd47e99..0000000000 --- a/test/js/node/test/parallel/js-stream-call-properties.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-js-stream-call-properties.js -//#SHA1: 491c3447495fadda8e713d00b9621ea31d8fb27d -//----------------- -"use strict"; - -test("JSStream properties can be inspected", () => { - // We can't use internal bindings in Jest, so we'll mock the JSStream - class MockJSStream { - constructor() { - // Add some properties that might be inspected - this.readableFlowing = null; - this.writableFinished = false; - // Add more properties as needed - } - } - - // Mock util.inspect to ensure it's called - const mockInspect = jest.fn(); - jest.spyOn(console, "log").mockImplementation(mockInspect); - - // Create an instance of our mock JSStream - const jsStream = new MockJSStream(); - - // Call console.log, which will internally call util.inspect - console.log(jsStream); - - // Verify that inspect was called - expect(mockInspect).toHaveBeenCalledTimes(1); - expect(mockInspect).toHaveBeenCalledWith(expect.any(MockJSStream)); - - // Clean up - console.log.mockRestore(); -}); - -//<#END_FILE: test-js-stream-call-properties.js diff --git a/test/js/node/test/parallel/kill-segfault-freebsd.test.js b/test/js/node/test/parallel/kill-segfault-freebsd.test.js deleted file mode 100644 index 786d3f4dbf..0000000000 --- a/test/js/node/test/parallel/kill-segfault-freebsd.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-kill-segfault-freebsd.js -//#SHA1: ac3b65d8e5e92ffb9714307d2249b4c3b9240ed9 -//----------------- -"use strict"; - -// This test ensures Node.js doesn't crash on hitting Ctrl+C in order to -// terminate the currently running process (especially on FreeBSD). -// https://github.com/nodejs/node-v0.x-archive/issues/9326 - -const child_process = require("child_process"); - -test("Node.js doesn't crash on SIGINT (FreeBSD issue)", done => { - // NOTE: Was crashing on FreeBSD - const cp = child_process.spawn(process.execPath, ["-e", 'process.kill(process.pid, "SIGINT")']); - - cp.on("exit", function (code) { - expect(code).not.toBe(0); - done(); - }); -}); - -//<#END_FILE: test-kill-segfault-freebsd.js diff --git a/test/js/node/test/parallel/listen-fd-detached-inherit.test.js b/test/js/node/test/parallel/listen-fd-detached-inherit.test.js deleted file mode 100644 index ba5323fbdc..0000000000 --- a/test/js/node/test/parallel/listen-fd-detached-inherit.test.js +++ /dev/null @@ -1,137 +0,0 @@ -//#FILE: test-listen-fd-detached-inherit.js -//#SHA1: 4aab69e9262c853cc790bd9de1fc3cf9529baa01 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const http = require("http"); -const net = require("net"); -const { spawn } = require("child_process"); - -if (process.platform === "win32") { - test.skip("This test is disabled on windows."); -} - -switch (process.argv[2]) { - case "child": - child(); - break; - case "parent": - parent(); - break; - default: - runTest(); -} - -// Spawn the parent, and listen for it to tell us the pid of the child. -// WARNING: This is an example of listening on some arbitrary FD number -// that has already been bound elsewhere in advance. However, binding -// server handles to stdio fd's is NOT a good or reliable way to do -// concurrency in HTTP servers! Use the cluster module, or if you want -// a more low-level approach, use child process IPC manually. -async function runTest() { - const parent = spawn(process.execPath, [__filename, "parent"], { - stdio: [0, "pipe", 2], - }); - - let json = ""; - for await (const chunk of parent.stdout) { - json += chunk.toString(); - if (json.includes("\n")) break; - } - - console.error("output from parent = %s", json); - const child = JSON.parse(json); - - // Now make sure that we can request to the subprocess, then kill it. - const response = await new Promise(resolve => { - http.get( - { - hostname: "localhost", - port: child.port, - path: "/", - }, - resolve, - ); - }); - - let responseBody = ""; - for await (const chunk of response) { - responseBody += chunk.toString(); - } - - // Kill the subprocess before we start doing asserts. - // It's really annoying when tests leave orphans! - process.kill(child.pid, "SIGKILL"); - try { - parent.kill(); - } catch { - // Continue regardless of error. - } - - expect(responseBody).toBe("hello from child\n"); - expect(response.statusCode).toBe(200); -} - -// Listen on port, and then pass the handle to the detached child. -// Then output the child's pid, and immediately exit. -function parent() { - const server = net - .createServer(conn => { - conn.end("HTTP/1.1 403 Forbidden\r\n\r\nI got problems.\r\n"); - throw new Error("Should not see connections on parent"); - }) - .listen(0, function () { - console.error("server listening on %d", this.address().port); - - const child = spawn(process.execPath, [__filename, "child"], { - stdio: [0, 1, 2, server._handle], - detached: true, - }); - - console.log("%j\n", { pid: child.pid, port: this.address().port }); - - // Now close the parent, so that the child is the only thing - // referencing that handle. Note that connections will still - // be accepted, because the child has the fd open, but the parent - // will exit gracefully. - server.close(); - child.unref(); - }); -} - -// Run as a child of the parent() mode. -function child() { - // Start a server on fd=3 - http - .createServer((req, res) => { - console.error("request on child"); - console.error("%s %s", req.method, req.url, req.headers); - res.end("hello from child\n"); - }) - .listen({ fd: 3 }, () => { - console.error("child listening on fd=3"); - }); -} - -//<#END_FILE: test-listen-fd-detached-inherit.js diff --git a/test/js/node/test/parallel/memory-usage-emfile.test.js b/test/js/node/test/parallel/memory-usage-emfile.test.js deleted file mode 100644 index 6e8cb22c8d..0000000000 --- a/test/js/node/test/parallel/memory-usage-emfile.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-memory-usage-emfile.js -//#SHA1: 062c0483d1da90d9e08bab6a7d006d2da5861bd9 -//----------------- -"use strict"; - -// On IBMi, the rss memory always returns zero -if (process.platform === "os400") { - test.skip("On IBMi, the rss memory always returns zero", () => {}); -} else { - const fs = require("fs"); - - test("memory usage with many open files", () => { - const files = []; - - while (files.length < 256) files.push(fs.openSync(__filename, "r")); - - const r = process.memoryUsage.rss(); - expect(r).toBeGreaterThan(0); - - // Clean up opened files - files.forEach(fd => fs.closeSync(fd)); - }); -} - -//<#END_FILE: test-memory-usage-emfile.js diff --git a/test/js/node/test/parallel/memory-usage.test.js b/test/js/node/test/parallel/memory-usage.test.js deleted file mode 100644 index 74c73896df..0000000000 --- a/test/js/node/test/parallel/memory-usage.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-memory-usage.js -//#SHA1: fffba1b4ff9ad7092d9a8f51b2799a0606d769eb -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// Flags: --predictable-gc-schedule -"use strict"; - -const isIBMi = process.platform === "aix" && process.execPath.includes("powerpc"); - -test("memory usage", () => { - const r = process.memoryUsage(); - // On IBMi, the rss memory always returns zero - if (!isIBMi) { - expect(r.rss).toBeGreaterThan(0); - expect(process.memoryUsage.rss()).toBeGreaterThan(0); - } - - expect(r.heapTotal).toBeGreaterThan(0); - expect(r.heapUsed).toBeGreaterThan(0); - expect(r.external).toBeGreaterThan(0); - - expect(typeof r.arrayBuffers).toBe("number"); - if (r.arrayBuffers > 0) { - const size = 10 * 1024 * 1024; - // eslint-disable-next-line no-unused-vars - const ab = new ArrayBuffer(size); - - const after = process.memoryUsage(); - expect(after.external - r.external).toBeGreaterThanOrEqual(size); - expect(after.arrayBuffers - r.arrayBuffers).toBe(size); - } -}); - -//<#END_FILE: test-memory-usage.js diff --git a/test/js/node/test/parallel/messageevent-brandcheck.test.js b/test/js/node/test/parallel/messageevent-brandcheck.test.js deleted file mode 100644 index ba2dd9c11a..0000000000 --- a/test/js/node/test/parallel/messageevent-brandcheck.test.js +++ /dev/null @@ -1,17 +0,0 @@ -//#FILE: test-messageevent-brandcheck.js -//#SHA1: 1b04c8b7c45fe0f2fe12018ca10137eefa892b4c -//----------------- -"use strict"; - -test("MessageEvent brand checks", () => { - ["data", "origin", "lastEventId", "source", "ports"].forEach(prop => { - expect(() => Reflect.get(MessageEvent.prototype, prop, {})).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-messageevent-brandcheck.js diff --git a/test/js/node/test/parallel/microtask-queue-integration.test.js b/test/js/node/test/parallel/microtask-queue-integration.test.js deleted file mode 100644 index 7d0dc16d64..0000000000 --- a/test/js/node/test/parallel/microtask-queue-integration.test.js +++ /dev/null @@ -1,81 +0,0 @@ -//#FILE: test-microtask-queue-integration.js -//#SHA1: bc144f7d64d2ed682489718745be5883122cf323 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const implementations = [ - function (fn) { - Promise.resolve().then(fn); - }, -]; - -let expected = 0; -let done = 0; - -afterAll(() => { - expect(done).toBe(expected); -}); - -function testMicrotask(scheduleMicrotask) { - return new Promise(resolve => { - let nextTickCalled = false; - expected++; - - scheduleMicrotask(() => { - process.nextTick(() => { - nextTickCalled = true; - }); - - setTimeout(() => { - expect(nextTickCalled).toBe(true); - done++; - resolve(); - }, 0); - }); - }); -} - -test("first tick case", async () => { - await Promise.all(implementations.map(testMicrotask)); -}); - -test("tick callback case", async () => { - await new Promise(resolve => { - setTimeout(async () => { - await Promise.all( - implementations.map( - impl => - new Promise(resolve => { - process.nextTick(() => { - testMicrotask(impl).then(resolve); - }); - }), - ), - ); - resolve(); - }, 0); - }); -}); - -//<#END_FILE: test-microtask-queue-integration.js diff --git a/test/js/node/test/parallel/microtask-queue-run-immediate.test.js b/test/js/node/test/parallel/microtask-queue-run-immediate.test.js deleted file mode 100644 index e64e7022b7..0000000000 --- a/test/js/node/test/parallel/microtask-queue-run-immediate.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-microtask-queue-run-immediate.js -//#SHA1: 49e5d82cc3467e4e12d0e93629607cd48b3548e4 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -function enqueueMicrotask(fn) { - Promise.resolve().then(fn); -} - -test("microtask queue runs in setImmediate", done => { - let microtaskExecuted = false; - - setImmediate(() => { - enqueueMicrotask(() => { - microtaskExecuted = true; - expect(microtaskExecuted).toBe(true); - done(); - }); - }); -}); - -test("microtask with nextTick runs before next setImmediate", done => { - let nextTickCalled = false; - - setImmediate(() => { - enqueueMicrotask(() => { - process.nextTick(() => { - nextTickCalled = true; - }); - }); - - setImmediate(() => { - expect(nextTickCalled).toBe(true); - done(); - }); - }); -}); - -//<#END_FILE: test-microtask-queue-run-immediate.js diff --git a/test/js/node/test/parallel/microtask-queue-run.test.js b/test/js/node/test/parallel/microtask-queue-run.test.js deleted file mode 100644 index 8e25e318d2..0000000000 --- a/test/js/node/test/parallel/microtask-queue-run.test.js +++ /dev/null @@ -1,67 +0,0 @@ -//#FILE: test-microtask-queue-run.js -//#SHA1: caf14b2c15bbc84816eb2ce6b9bdb073c009b447 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -function enqueueMicrotask(fn) { - Promise.resolve().then(fn); -} - -test("microtask queue runs correctly", async () => { - let done = 0; - - // No nextTick, microtask - await new Promise(resolve => { - setTimeout(() => { - enqueueMicrotask(() => { - done++; - resolve(); - }); - }, 0); - }); - - // No nextTick, microtask with nextTick - await new Promise(resolve => { - setTimeout(() => { - let called = false; - - enqueueMicrotask(() => { - process.nextTick(() => { - called = true; - }); - }); - - setTimeout(() => { - if (called) { - done++; - } - resolve(); - }, 0); - }, 0); - }); - - expect(done).toBe(2); -}); - -//<#END_FILE: test-microtask-queue-run.js diff --git a/test/js/node/test/parallel/module-builtin.test.js b/test/js/node/test/parallel/module-builtin.test.js deleted file mode 100644 index cc9f208d38..0000000000 --- a/test/js/node/test/parallel/module-builtin.test.js +++ /dev/null @@ -1,17 +0,0 @@ -//#FILE: test-module-builtin.js -//#SHA1: 18114886f66eccc937942a815feca25d9b324a37 -//----------------- -"use strict"; - -test("builtinModules", () => { - const { builtinModules } = require("module"); - - // Includes modules in lib/ (even deprecated ones) - expect(builtinModules).toContain("http"); - expect(builtinModules).toContain("sys"); - - // Does not include internal modules - expect(builtinModules.filter(mod => mod.startsWith("internal/"))).toEqual([]); -}); - -//<#END_FILE: test-module-builtin.js diff --git a/test/js/node/test/parallel/module-cache.test.js b/test/js/node/test/parallel/module-cache.test.js deleted file mode 100644 index 605b768808..0000000000 --- a/test/js/node/test/parallel/module-cache.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-module-cache.js -//#SHA1: ff0f4c6ca37e23c009f98bba966e9daee2dcaef6 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -let tmpdir; - -beforeEach(() => { - tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test-module-cache-")); -}); - -afterEach(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); -}); - -test("throws MODULE_NOT_FOUND when file does not exist", () => { - const filePath = path.join(tmpdir, "test-module-cache.json"); - expect(() => require(filePath)).toThrow( - expect.objectContaining({ - code: "MODULE_NOT_FOUND", - message: expect.any(String), - }), - ); -}); - -test("requires JSON file successfully after creation", () => { - const filePath = path.join(tmpdir, "test-module-cache.json"); - fs.writeFileSync(filePath, "[]"); - - const content = require(filePath); - expect(Array.isArray(content)).toBe(true); - expect(content.length).toBe(0); -}); - -//<#END_FILE: test-module-cache.js diff --git a/test/js/node/test/parallel/module-main-extension-lookup.test.js b/test/js/node/test/parallel/module-main-extension-lookup.test.js deleted file mode 100644 index 741f721bc6..0000000000 --- a/test/js/node/test/parallel/module-main-extension-lookup.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-module-main-extension-lookup.js -//#SHA1: d50be34ba21e1e14de5225ac5d93b6fe20505014 -//----------------- -"use strict"; - -const path = require("path"); -const { execFileSync } = require("child_process"); - -const node = process.argv[0]; - -test("ES modules extension lookup", () => { - const fixturesPath = path.resolve(__dirname, "..", "fixtures"); - - expect(() => { - execFileSync(node, [path.join(fixturesPath, "es-modules", "test-esm-ok.mjs")]); - }).not.toThrow(); - - expect(() => { - execFileSync(node, [path.join(fixturesPath, "es-modules", "noext")]); - }).not.toThrow(); -}); - -//<#END_FILE: test-module-main-extension-lookup.js diff --git a/test/js/node/test/parallel/needs-test/README.md b/test/js/node/test/parallel/needs-test/README.md new file mode 100644 index 0000000000..821ae16ee3 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/README.md @@ -0,0 +1,8 @@ +A good deal of parallel test cases can be run directly via `bun `. +However, some newer cases use `node:test`. + +Files in this directory need to be run with `bun test `. The +`node:test` module is shimmed via a require cache hack in +`test/js/node/harness.js` to use `bun:test`. Note that our test runner +(`scripts/runner.node.mjs`) checks for `needs-test` in the names of test files, +so don't rename this folder without updating that code. diff --git a/test/js/node/test/parallel/needs-test/test-assert.js b/test/js/node/test/parallel/needs-test/test-assert.js new file mode 100644 index 0000000000..d16194b57f --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert.js @@ -0,0 +1,1601 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const assert = require('node:assert'); + +require('../../../harness'); + +const {invalidArgTypeHelper} = require('../../common'); +const {inspect} = require('util'); +const {test} = require('node:test'); +const vm = require('vm'); +// const { createTest } = require('node-harness'); +// const { test } = createTest(__filename); + +// Disable colored output to prevent color codes from breaking assertion +// message comparisons. This should only be an issue when process.stdout +// is a TTY. +if (process.stdout.isTTY) { + process.env.NODE_DISABLE_COLORS = '1'; +} + +const strictEqualMessageStart = 'Expected values to be strictly equal:\n'; +const start = 'Expected values to be strictly deep-equal:'; +const actExp = '+ actual - expected'; + +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-restricted-properties */ + +test('some basics', () => { + assert.ok(assert.AssertionError.prototype instanceof Error, + 'assert.AssertionError instanceof Error'); + + assert.throws(() => assert(false), assert.AssertionError, 'ok(false)'); + assert.throws(() => assert.ok(false), assert.AssertionError, 'ok(false)'); + assert(true); + assert('test', 'ok(\'test\')'); + assert.ok(true); + assert.ok('test'); + assert.throws(() => assert.equal(true, false), + assert.AssertionError, 'equal(true, false)'); + assert.equal(null, null); + assert.equal(undefined, undefined); + assert.equal(null, undefined); + assert.equal(true, true); + assert.equal(2, '2'); + assert.notEqual(true, false); + assert.notStrictEqual(2, '2'); +}); + +test('Throw message if the message is instanceof Error', () => { + let threw = false; + try { + assert.ok(false, new Error('ok(false)')); + } catch (e) { + threw = true; + assert.ok(e instanceof Error); + } + assert.ok(threw, 'Error: ok(false)'); +}); + +test('Errors created in different contexts are handled as any other custom error', () => { + assert('createContext' in vm, 'vm.createContext is available'); + const context = vm.createContext(); + const error = vm.runInContext('new SyntaxError("custom error")', context); + + assert.throws(() => assert(false, error), { + message: 'custom error', + name: 'SyntaxError' + }); +}); + +test('assert.throws()', () => { + assert.throws(() => assert.notEqual(true, true), + assert.AssertionError, 'notEqual(true, true)'); + + assert.throws(() => assert.strictEqual(2, '2'), + assert.AssertionError, 'strictEqual(2, \'2\')'); + + assert.throws(() => assert.strictEqual(null, undefined), + assert.AssertionError, 'strictEqual(null, undefined)'); + + assert.throws( + () => assert.notStrictEqual(2, 2), + { + message: 'Expected "actual" to be strictly unequal to: 2', + name: 'AssertionError' + } + ); + + assert.throws( + () => assert.notStrictEqual('a '.repeat(30), 'a '.repeat(30)), + { + message: 'Expected "actual" to be strictly unequal to:\n\n' + + `'${'a '.repeat(30)}'`, + name: 'AssertionError' + } + ); + + assert.throws( + () => assert.notEqual(1, 1), + { + message: '1 != 1', + operator: '!=' + } + ); + + // Testing the throwing. + function thrower(errorConstructor) { + throw new errorConstructor({}); + } + + // The basic calls work. + assert.throws(() => thrower(assert.AssertionError), assert.AssertionError, 'message'); + assert.throws(() => thrower(assert.AssertionError), assert.AssertionError); + assert.throws(() => thrower(assert.AssertionError)); + + // If not passing an error, catch all. + assert.throws(() => thrower(TypeError)); + + // When passing a type, only catch errors of the appropriate type. + assert.throws( + () => assert.throws(() => thrower(TypeError), assert.AssertionError), + { + generatedMessage: true, + actual: new TypeError({}), + expected: assert.AssertionError, + code: 'ERR_ASSERTION', + name: 'AssertionError', + operator: 'throws', + message: 'The error is expected to be an instance of "AssertionError". ' + + 'Received "TypeError"\n\nError message:\n\n[object Object]' + } + ); + + // doesNotThrow should pass through all errors. + { + let threw = false; + try { + assert.doesNotThrow(() => thrower(TypeError), assert.AssertionError); + } catch (e) { + threw = true; + assert.ok(e instanceof TypeError); + } + assert(threw, 'assert.doesNotThrow with an explicit error is eating extra errors'); + } + + // Key difference is that throwing our correct error makes an assertion error. + { + let threw = false; + try { + assert.doesNotThrow(() => thrower(TypeError), TypeError); + } catch (e) { + threw = true; + assert.ok(e instanceof assert.AssertionError); + assert.ok(!e.stack.includes('at Function.doesNotThrow')); + } + assert.ok(threw, 'assert.doesNotThrow is not catching type matching errors'); + } + + assert.throws( + () => assert.doesNotThrow(() => thrower(Error), 'user message'), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + operator: 'doesNotThrow', + message: 'Got unwanted exception: user message\n' + + 'Actual message: "[object Object]"' + } + ); + + assert.throws( + () => assert.doesNotThrow(() => thrower(Error)), + { + code: 'ERR_ASSERTION', + message: 'Got unwanted exception.\nActual message: "[object Object]"' + } + ); + + assert.throws( + () => assert.doesNotThrow(() => thrower(Error), /\[[a-z]{6}\s[A-z]{6}\]/g, 'user message'), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + operator: 'doesNotThrow', + message: 'Got unwanted exception: user message\n' + + 'Actual message: "[object Object]"' + } + ); + + // Make sure that validating using constructor really works. + { + let threw = false; + try { + assert.throws( + () => { + throw ({}); // eslint-disable-line no-throw-literal + }, + Array + ); + } catch { + threw = true; + } + assert.ok(threw, 'wrong constructor validation'); + } + + // Use a RegExp to validate the error message. + { + assert.throws(() => thrower(TypeError), /\[object Object\]/); + + const symbol = Symbol('foo'); + assert.throws(() => { + throw symbol; + }, /foo/); + + assert.throws(() => { + assert.throws(() => { + throw symbol; + }, /abc/); + }, { + message: 'The input did not match the regular expression /abc/. ' + + "Input:\n\n'Symbol(foo)'\n", + code: 'ERR_ASSERTION', + operator: 'throws', + actual: symbol, + expected: /abc/ + }); + } + + // Use a fn to validate the error object. + assert.throws(() => thrower(TypeError), (err) => { + if ((err instanceof TypeError) && /\[object Object\]/.test(err)) { + return true; + } + }); + + // https://github.com/nodejs/node/issues/3188 + { + let actual; + assert.throws( + () => { + const ES6Error = class extends Error {}; + const AnotherErrorType = class extends Error {}; + + assert.throws(() => { + actual = new AnotherErrorType('foo'); + throw actual; + }, ES6Error); + }, + (err) => { + assert.strictEqual( + err.message, + 'The error is expected to be an instance of "ES6Error". ' + + 'Received "AnotherErrorType"\n\nError message:\n\nfoo' + ); + assert.strictEqual(err.actual, actual); + return true; + } + ); + } + + assert.throws( + () => assert.strictEqual(new Error('foo'), new Error('foobar')), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected "actual" to be reference-equal to "expected":\n' + + '+ actual - expected\n' + + '\n' + + '+ [Error: foo]\n' + + '- [Error: foobar]\n' + } + ); +}); + +test('Check messages from assert.throws()', () => { + const noop = () => {}; + assert.throws( + () => {assert.throws((noop));}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception.', + operator: 'throws', + actual: undefined, + expected: undefined + }); + + assert.throws( + () => {assert.throws(noop, TypeError);}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception (TypeError).', + actual: undefined, + expected: TypeError + }); + + assert.throws( + () => {assert.throws(noop, 'fhqwhgads');}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception: fhqwhgads', + actual: undefined, + expected: undefined + }); + + assert.throws( + () => {assert.throws(noop, TypeError, 'fhqwhgads');}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception (TypeError): fhqwhgads', + actual: undefined, + expected: TypeError + }); + + let threw = false; + try { + assert.throws(noop); + } catch (e) { + threw = true; + assert.ok(e instanceof assert.AssertionError); + assert.ok(!e.stack.includes('at Function.throws')); + } + assert.ok(threw); +}); + +test('Test assertion messages', () => { + const circular = {y: 1}; + circular.x = circular; + + function testAssertionMessage(actual, expected, msg) { + assert.throws( + () => assert.strictEqual(actual, ''), + { + generatedMessage: true, + message: msg || `Expected values to be strictly equal:\n\n${expected} !== ''\n` + } + ); + } + + function testLongAssertionMessage(actual, expected) { + testAssertionMessage(actual, expected, 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + `+ ${expected}\n` + + "- ''\n"); + } + + function testShortAssertionMessage(actual, expected) { + testAssertionMessage(actual, expected, strictEqualMessageStart + `\n${inspect(actual)} !== ''\n`); + } + + testShortAssertionMessage(null, 'null'); + testShortAssertionMessage(true, 'true'); + testShortAssertionMessage(false, 'false'); + testShortAssertionMessage(100, '100'); + testShortAssertionMessage(NaN, 'NaN'); + testShortAssertionMessage(Infinity, 'Infinity'); + testShortAssertionMessage('a', '\'a\''); + testShortAssertionMessage('foo', '\'foo\''); + testShortAssertionMessage(0, '0'); + testShortAssertionMessage(Symbol(), 'Symbol()'); + testShortAssertionMessage(undefined, 'undefined'); + testShortAssertionMessage(-Infinity, '-Infinity'); + testShortAssertionMessage([], '[]'); + testShortAssertionMessage({}, '{}'); + testAssertionMessage(/a/, '/a/'); + testAssertionMessage(/abc/gim, '/abc/gim'); + testLongAssertionMessage(function f() {}, '[Function: f]'); + testLongAssertionMessage(function () {}, '[Function (anonymous)]'); + + assert.throws( + () => assert.strictEqual([1, 2, 3], ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ [\n' + + '+ 1,\n' + + '+ 2,\n' + + '+ 3\n' + + '+ ]\n' + + "- ''\n", + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual(circular, ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ {\n' + + '+ x: [Circular *1],\n' + + '+ y: 1\n' + + '+ }\n' + + "- ''\n", + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual({a: undefined, b: null}, ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ {\n' + + '+ a: undefined,\n' + + '+ b: null\n' + + '+ }\n' + + "- ''\n", + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual({a: NaN, b: Infinity, c: -Infinity}, ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ {\n' + + '+ a: NaN,\n' + + '+ b: Infinity,\n' + + '+ c: -Infinity\n' + + '+ }\n' + + "- ''\n", + generatedMessage: true + } + ); + + // https://github.com/nodejs/node-v0.x-archive/issues/5292 + assert.throws( + () => assert.strictEqual(1, 2), + { + message: 'Expected values to be strictly equal:\n\n1 !== 2\n', + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual(1, 2, 'oh no'), + { + message: 'oh no\n\n1 !== 2\n', + generatedMessage: false + } + ); +}); + +test('Custom errors', () => { + let threw = false; + const rangeError = new RangeError('my range'); + + // Verify custom errors. + try { + assert.strictEqual(1, 2, rangeError); + } catch (e) { + assert.strictEqual(e, rangeError); + threw = true; + assert.ok(e instanceof RangeError, 'Incorrect error type thrown'); + } + assert.ok(threw); + threw = false; + + // Verify AssertionError is the result from doesNotThrow with custom Error. + try { + assert.doesNotThrow(() => { + throw new TypeError('wrong type'); + }, TypeError, rangeError); + } catch (e) { + threw = true; + // assert.ok(e.message.includes(rangeError.message)); + assert.ok(e.actual instanceof TypeError); + assert.equal(e.expected, TypeError); + assert.ok(e instanceof assert.AssertionError); + assert.ok(!e.stack.includes('doesNotThrow'), e); + } + assert.ok(threw); +}); + +test('Verify that throws() and doesNotThrow() throw on non-functions', () => { + const testBlockTypeError = (method, fn) => { + assert.throws( + () => method(fn), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "fn" argument must be of type function.' + + // invalidArgTypeHelper(fn) + } + ); + }; + + testBlockTypeError(assert.throws, 'string'); + testBlockTypeError(assert.doesNotThrow, 'string'); + testBlockTypeError(assert.throws, 1); + testBlockTypeError(assert.doesNotThrow, 1); + testBlockTypeError(assert.throws, true); + testBlockTypeError(assert.doesNotThrow, true); + testBlockTypeError(assert.throws, false); + testBlockTypeError(assert.doesNotThrow, false); + testBlockTypeError(assert.throws, []); + testBlockTypeError(assert.doesNotThrow, []); + testBlockTypeError(assert.throws, {}); + testBlockTypeError(assert.doesNotThrow, {}); + testBlockTypeError(assert.throws, /foo/); + testBlockTypeError(assert.doesNotThrow, /foo/); + testBlockTypeError(assert.throws, null); + testBlockTypeError(assert.doesNotThrow, null); + testBlockTypeError(assert.throws, undefined); + testBlockTypeError(assert.doesNotThrow, undefined); +}); + +test('https://github.com/nodejs/node/issues/3275', () => { + // eslint-disable-next-line no-throw-literal + assert.throws(() => {throw 'error';}, (err) => err === 'error'); + assert.throws(() => {throw new Error();}, (err) => err instanceof Error); +}); + +test('Long values should be truncated for display', () => { + assert.throws(() => { + assert.strictEqual('A'.repeat(1000), ''); + }, (err) => { + assert.strictEqual(err.code, 'ERR_ASSERTION'); + assert.strictEqual(err.message, + `${strictEqualMessageStart}+ actual - expected\n\n` + + `+ '${'A'.repeat(1000)}'\n- ''\n`); + assert.strictEqual(err.actual.length, 1000); + assert.ok(inspect(err).includes(`actual: '${'A'.repeat(488)}...'`)); + return true; + }); +}); + +test('Output that extends beyond 10 lines should also be truncated for display', () => { + const multilineString = 'fhqwhgads\n'.repeat(15); + assert.throws(() => { + assert.strictEqual(multilineString, ''); + }, (err) => { + assert.strictEqual(err.code, 'ERR_ASSERTION'); + assert.strictEqual(err.message.split('\n').length, 21); + assert.strictEqual(err.actual.split('\n').length, 16); + assert.ok(inspect(err).includes( + "actual: 'fhqwhgads\\n' +\n" + + " 'fhqwhgads\\n' +\n".repeat(9) + + " '...'")); + return true; + }); +}); + +test('Bad args to AssertionError constructor should throw TypeError.', () => { + const args = [1, true, false, '', null, Infinity, Symbol('test'), undefined]; + for (const input of args) { + assert.throws( + () => new assert.AssertionError(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + invalidArgTypeHelper(input) + }); + } +}); + +test('NaN is handled correctly', () => { + assert.equal(NaN, NaN); + assert.throws( + () => assert.notEqual(NaN, NaN), + assert.AssertionError + ); +}); + +test('Test strict assert', () => { + const {strict} = require('assert'); + + strict.throws(() => strict.equal(1, true), strict.AssertionError); + strict.notEqual(0, false); + strict.throws(() => strict.deepEqual(1, true), strict.AssertionError); + strict.notDeepEqual(0, false); + strict.equal(strict.strict, strict.strict.strict); + strict.equal(strict.equal, strict.strictEqual); + strict.equal(strict.deepEqual, strict.deepStrictEqual); + strict.equal(strict.notEqual, strict.notStrictEqual); + strict.equal(strict.notDeepEqual, strict.notDeepStrictEqual); + strict.equal(Object.keys(strict).length, Object.keys(assert).length); + strict(7); + strict.throws( + () => strict(...[]), + { + message: 'No value argument passed to `assert.ok()`', + name: 'AssertionError', + generatedMessage: true + } + ); + strict.throws( + () => assert(), + { + message: 'No value argument passed to `assert.ok()`', + name: 'AssertionError' + } + ); + + // Test setting the limit to zero and that assert.strict works properly. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + strict.throws( + () => { + strict.ok( + typeof 123 === 'string' + ); + }, + { + code: 'ERR_ASSERTION', + constructor: strict.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // "strict.ok(\n typeof 123 === 'string'\n )\n" + } + ); + Error.stackTraceLimit = tmpLimit; + + // Test error diffs. + let message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' [\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + '+ 3\n' + + "- '3'\n" + + ' ]\n' + + ' ],\n' + + ' 4,\n' + + ' 5\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]), + {message}); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '... Skipped lines\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 1,\n' + + ' 1,\n' + + ' 0,\n' + + '...\n' + + ' 1,\n' + + '+ 1\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual( + [1, 1, 1, 0, 1, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1]), + {message}); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + ' 3,\n' + + ' 4,\n' + + ' 5,\n' + + '+ 6,\n' + + '- 9,\n' + + ' 7\n' + + ' ]\n'; + + assert.throws( + () => assert.deepStrictEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]), + {message} + ); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + ' 3,\n' + + ' 4,\n' + + ' 5,\n' + + ' 6,\n' + + '+ 7,\n' + + '- 9,\n' + + ' 8\n' + + ' ]\n'; + + assert.throws( + () => assert.deepStrictEqual([1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4, 5, 6, 9, 8]), + {message} + ); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '... Skipped lines\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + ' 3,\n' + + ' 4,\n' + + '...\n' + + ' 7,\n' + + '+ 8,\n' + + '- 0,\n' + + ' 9\n' + + ' ]\n'; + + assert.throws( + () => assert.deepStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 0, 9]), + {message} + ); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' 1,\n' + + '+ 2,\n' + + ' 1,\n' + + ' 1,\n' + + '- 1,\n' + + ' 0,\n' + + ' 1,\n' + + '+ 1\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual( + [1, 2, 1, 1, 0, 1, 1], + [1, 1, 1, 1, 0, 1]), + {message}); + + message = [ + start, + actExp, + '', + '+ [', + '+ 1,', + '+ 2,', + '+ 1', + '+ ]', + '- undefined\n', + ].join('\n'); + strict.throws( + () => strict.deepEqual([1, 2, 1], undefined), + {message}); + + message = [ + start, + actExp, + '', + ' [', + '+ 1,', + ' 2,', + ' 1', + ' ]\n', + ].join('\n'); + strict.throws( + () => strict.deepEqual([1, 2, 1], [2, 1]), + {message}); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + '+ 1,\n'.repeat(10) + + '+ 3\n' + + '- 2,\n'.repeat(11) + + '- 4,\n' + + '- 4,\n' + + '- 4\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4]), + {message}); + + const obj1 = {}; + const obj2 = {loop: 'forever'}; + obj2[inspect.custom] = () => '{}'; + // No infinite loop and no custom inspect. + strict.throws(() => strict.deepEqual(obj1, obj2), { + message: `${start}\n` + + `${actExp}\n` + + '\n' + + '+ {}\n' + + '- {\n' + + '- [Symbol(nodejs.util.inspect.custom)]: [Function (anonymous)],\n' + + "- loop: 'forever'\n" + + '- }\n' + }); + + // notDeepEqual tests + strict.throws( + () => strict.notDeepEqual([1], [1]), + { + message: 'Expected "actual" not to be strictly deep-equal to:\n\n' + + '[\n 1\n]\n' + } + ); + + message = 'Expected "actual" not to be strictly deep-equal to:' + + `\n\n[${'\n 1,'.repeat(45)}\n...\n`; + const data = Array(51).fill(1); + strict.throws( + () => strict.notDeepEqual(data, data), + {message}); + +}); + +test('Additional asserts', () => { + assert.throws( + () => assert.ok(null), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok(null)\n' + } + ); + assert.throws( + () => { + // This test case checks if `try` left brace without a line break + // before the assertion causes any wrong assertion message. + // Therefore, don't reformat the following code. + // Refs: https://github.com/nodejs/node/issues/30872 + try { + assert.ok(0); // eslint-disable-line no-useless-catch, @stylistic/js/brace-style + } catch (err) { + throw err; + } + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok(0)\n' + } + ); + assert.throws( + () => { + try { + throw new Error(); + // This test case checks if `catch` left brace without a line break + // before the assertion causes any wrong assertion message. + // Therefore, don't reformat the following code. + // Refs: https://github.com/nodejs/node/issues/30872 + } catch (err) {assert.ok(0);} // eslint-disable-line no-unused-vars + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + message: '0 == true' + } + ); + assert.throws( + () => { + // This test case checks if `function` left brace without a line break + // before the assertion causes any wrong assertion message. + // Therefore, don't reformat the following code. + // Refs: https://github.com/nodejs/node/issues/30872 + function test() { + assert.ok(0); // eslint-disable-line @stylistic/js/brace-style + } + test(); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok(0)\n' + } + ); + assert.throws( + () => assert(typeof 123n === 'string'), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // "assert(typeof 123n === 'string')\n" + } + ); + + assert.throws( + () => assert(false, Symbol('foo')), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: false, + message: 'Symbol(foo)' + } + ); + + assert.throws( + () => { + assert.strictEqual((() => 'string')(), 123 instanceof + Buffer); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'Expected values to be strictly equal:\n\n\'string\' !== false\n' + } + ); + + assert.throws( + () => { + assert.strictEqual((() => 'string')(), 123 instanceof + Buffer); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'Expected values to be strictly equal:\n\n\'string\' !== false\n' + } + ); + + /* eslint-disable @stylistic/js/indent */ + assert.throws(() => { + assert.strictEqual(( + () => 'string')(), 123 instanceof + Buffer); + }, { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'Expected values to be strictly equal:\n\n\'string\' !== false\n' + } + ); + /* eslint-enable @stylistic/js/indent */ + + assert.throws( + () => { + assert(true); assert(null, undefined); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert(null, undefined)\n' + } + ); + + assert.throws( + () => { + assert + .ok(null, undefined); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'ok(null, undefined)\n' + } + ); + + assert.throws( + // eslint-disable-next-line dot-notation, @stylistic/js/quotes + () => assert['ok']["apply"](null, [0]), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert[\'ok\']["apply"](null, [0])\n' + } + ); + + assert.throws( + () => { + const wrapper = (fn, value) => fn(value); + wrapper(assert, false); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n fn(value)\n' + } + ); + + assert.throws( + () => assert.ok.call(null, 0), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok.call(null, 0)\n', + generatedMessage: true + } + ); + + assert.throws( + () => assert.ok.call(null, 0, 'test'), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'test', + generatedMessage: false + } + ); + + // Works in eval. + assert.throws( + () => new Function('assert', 'assert(1 === 2);')(assert), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'false == true' + } + ); + assert.throws( + () => eval('console.log("FOO");\nassert.ok(1 === 2);'), + { + code: 'ERR_ASSERTION', + message: 'false == true' + } + ); + + assert.throws( + () => assert.throws(() => {}, 'Error message', 'message'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "error" argument must be of type Object, Error, Function or RegExp. Received: "Error message"', + message: 'The "error" argument must be of type Object, Error, Function or RegExp.' + invalidArgTypeHelper('Error message'), + } + ); + + const inputs = [1, false, Symbol()]; + for (const input of inputs) { + assert.throws( + () => assert.throws(() => {}, input), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "error" argument must be of type Object, Error, Function or RegExp.' + invalidArgTypeHelper(input) + + } + ); + } +}); + +test('Throws accepts objects', () => { + assert.throws(() => { + // eslint-disable-next-line no-constant-binary-expression + assert.ok((() => Boolean('' === false))()); + }, { + code: 'ERR_ASSERTION', + // message: 'The expression evaluated to a falsy value:\n\n' + + // " assert.ok((() => Boolean('\\u0001' === false))())\n" + }); + + const errFn = () => { + const err = new TypeError('Wrong value'); + err.code = 404; + throw err; + }; + const errObj = { + name: 'TypeError', + message: 'Wrong value' + }; + assert.throws(errFn, errObj); + + errObj.code = 404; + assert.throws(errFn, errObj); + + // Fail in case a expected property is undefined and not existent on the + // error. + errObj.foo = undefined; + assert.throws( + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + ' code: 404,\n' + + '- foo: undefined,\n' + + " message: 'Wrong value',\n" + + " name: 'TypeError'\n" + + ' }\n' + } + ); + + // Show multiple wrong properties at the same time. + errObj.code = '404'; + assert.throws( + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + '+ code: 404,\n' + + "- code: '404',\n" + + '- foo: undefined,\n' + + " message: 'Wrong value',\n" + + " name: 'TypeError'\n" + + ' }\n' + } + ); + + assert.throws( + () => assert.throws(() => {throw new Error();}, {foo: 'bar'}, 'foobar'), + { + constructor: assert.AssertionError, + code: 'ERR_ASSERTION', + message: 'foobar' + } + ); + + assert.throws( + () => assert.doesNotThrow(() => {throw new Error();}, {foo: 'bar'}), + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "expected" argument must be of type Function or ' + + 'RegExp.' + invalidArgTypeHelper({foo: 'bar'}) + } + ); + + assert.throws(() => {throw new Error('e');}, new Error('e')); + assert.throws( + () => assert.throws(() => {throw new TypeError('e');}, new Error('e')), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + " message: 'e',\n" + + "+ name: 'TypeError'\n" + + "- name: 'Error'\n" + + ' }\n' + } + ); + assert.throws( + () => assert.throws(() => {throw new Error('foo');}, new Error('')), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + generatedMessage: true, + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + "+ message: 'foo',\n" + + "- message: '',\n" + + " name: 'Error'\n" + + ' }\n' + } + ); + + // eslint-disable-next-line no-throw-literal + assert.throws(() => {throw undefined;}, /undefined/); + assert.throws( + // eslint-disable-next-line no-throw-literal + () => assert.doesNotThrow(() => {throw undefined;}), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + message: 'Got unwanted exception.\nActual message: "undefined"' + } + ); +}); + +test('Additional assert', () => { + assert.throws( + () => assert.throws(() => {throw new Error();}, {}), + { + message: "The argument 'error' may not be an empty object. Received {}", + code: 'ERR_INVALID_ARG_VALUE' + } + ); + + assert.throws( + () => assert.throws( + // eslint-disable-next-line no-throw-literal + () => {throw 'foo';}, + 'foo' + ), + { + code: 'ERR_AMBIGUOUS_ARGUMENT', + message: 'The "error/message" argument is ambiguous. ' + + 'The error "foo" is identical to the message.' + } + ); + + assert.throws( + () => assert.throws( + () => {throw new TypeError('foo');}, + 'foo' + ), + { + code: 'ERR_AMBIGUOUS_ARGUMENT', + message: 'The "error/message" argument is ambiguous. ' + + 'The error message "foo" is identical to the message.' + } + ); + + // Should not throw. + assert.throws(() => {throw null;}, 'foo'); // eslint-disable-line no-throw-literal + + assert.throws( + () => assert.strictEqual([], []), + { + message: 'Values have same structure but are not reference-equal:\n\n[]\n' + } + ); + + { + const args = (function () {return arguments;})('a'); + assert.throws( + () => assert.strictEqual(args, {0: 'a'}), + { + message: 'Expected "actual" to be reference-equal to "expected":\n' + + '+ actual - expected\n\n' + + "+ [Arguments] {\n- {\n '0': 'a'\n }\n" + } + ); + } + + assert.throws( + () => {throw new TypeError('foobar');}, + { + message: /foo/, + name: /^TypeError$/ + } + ); + + assert.throws( + () => assert.throws( + () => {throw new TypeError('foobar');}, + { + message: /fooa/, + name: /^TypeError$/ + } + ), + { + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + "+ message: 'foobar',\n" + + '- message: /fooa/,\n' + + " name: 'TypeError'\n" + + ' }\n' + } + ); + + { + let actual = null; + const expected = {message: 'foo'}; + assert.throws( + () => assert.throws( + () => {throw actual;}, + expected + ), + { + operator: 'throws', + actual, + expected, + generatedMessage: true, + message: `${start}\n${actExp}\n\n` + + '+ null\n' + + '- {\n' + + "- message: 'foo'\n" + + '- }\n' + } + ); + + actual = 'foobar'; + const message = 'message'; + assert.throws( + () => assert.throws( + () => {throw actual;}, + {message: 'foobar'}, + message + ), + { + actual, + message: "message\n+ actual - expected\n\n+ 'foobar'\n- {\n- message: 'foobar'\n- }\n", + operator: 'throws', + generatedMessage: false + } + ); + } + + // Indicate where the strings diverge. + assert.throws( + () => assert.strictEqual('test test', 'test foobar'), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + "+ 'test test'\n" + + "- 'test foobar'\n" + + ' ^\n', + } + ); + + // Check for reference-equal objects in `notStrictEqual()` + assert.throws( + () => { + const obj = {}; + assert.notStrictEqual(obj, obj); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected "actual" not to be reference-equal to "expected": {}' + } + ); + + assert.throws( + () => { + const obj = {a: true}; + assert.notStrictEqual(obj, obj); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected "actual" not to be reference-equal to "expected":\n\n' + + '{\n a: true\n}\n' + } + ); + + assert.throws( + () => { + assert.deepStrictEqual({a: true}, {a: false}, 'custom message'); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' + } + ); + + { + let threw = false; + try { + assert.deepStrictEqual(Array(100).fill(1), 'foobar'); + } catch (err) { + threw = true; + assert.match(inspect(err), /actual: \[Array],\n {2}expected: 'foobar',/); + } + assert(threw); + } + + assert.throws( + () => assert.equal(1), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.deepEqual(/a/), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notEqual(null), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notDeepEqual('test'), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.strictEqual({}), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.deepStrictEqual(Symbol()), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notStrictEqual(5n), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notDeepStrictEqual(undefined), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.strictEqual(), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.deepStrictEqual(), + {code: 'ERR_MISSING_ARGS'} + ); + + // Verify that `stackStartFunction` works as alternative to `stackStartFn`. + { + (function hidden() { + const err = new assert.AssertionError({ + actual: 'foo', + operator: 'strictEqual', + stackStartFunction: hidden + }); + const err2 = new assert.AssertionError({ + actual: 'foo', + operator: 'strictEqual', + stackStartFn: hidden + }); + assert(!err.stack.includes('hidden')); + assert(!err2.stack.includes('hidden')); + })(); + } + + assert.throws( + () => assert.throws(() => {throw Symbol('foo');}, RangeError), + { + message: 'The error is expected to be an instance of "RangeError". ' + + 'Received "Symbol(foo)"' + } + ); + + assert.throws( + // eslint-disable-next-line no-throw-literal + () => assert.throws(() => {throw [1, 2];}, RangeError), + { + message: 'The error is expected to be an instance of "RangeError". ' + + 'Received "[Array]"' + } + ); + + { + const err = new TypeError('foo'); + const validate = (() => () => ({a: true, b: [1, 2, 3]}))(); + assert.throws( + () => assert.throws(() => {throw err;}, validate), + { + message: 'The validation function is expected to ' + + `return "true". Received ${inspect(validate())}\n\nCaught ` + + `error:\n\n${err}`, + code: 'ERR_ASSERTION', + actual: err, + expected: validate, + name: 'AssertionError', + operator: 'throws', + } + ); + } + + assert.throws( + () => { + const script = new vm.Script('new RangeError("foobar");'); + const context = vm.createContext(); + const err = script.runInContext(context); + assert.throws(() => {throw err;}, RangeError); + }, + { + message: 'The error is expected to be an instance of "RangeError". ' + + 'Received an error with identical name but a different ' + + 'prototype.\n\nError message:\n\nfoobar' + } + ); + + // Multiple assert.match() tests. + { + assert.throws( + () => assert.match(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be of type RegExp.' + + invalidArgTypeHelper('string') + } + ); + assert.throws( + () => assert.match('string', /abc/), + { + actual: 'string', + expected: /abc/, + operator: 'match', + message: 'The input did not match the regular expression /abc/. ' + + "Input:\n\n'string'\n", + generatedMessage: true + } + ); + assert.throws( + () => assert.match('string', /abc/, 'foobar'), + { + actual: 'string', + expected: /abc/, + operator: 'match', + message: 'foobar', + generatedMessage: false + } + ); + const errorMessage = new RangeError('foobar'); + assert.throws( + () => assert.match('string', /abc/, errorMessage), + errorMessage + ); + assert.throws( + () => assert.match({abc: 123}, /abc/), + { + actual: {abc: 123}, + expected: /abc/, + operator: 'match', + message: 'The "string" argument must be of type string. ' + + // NOTE: invalidArgTypeHelper just says "received instanceof Object", + // as does message formatters in ErrorCode.cpp. we may want to change that in the future. + // invalidArgTypeHelper({abc: 123}), + 'Received type object ({ abc: 123 })', + generatedMessage: true + } + ); + assert.match('I will pass', /pass$/); + } + + // Multiple assert.doesNotMatch() tests. + { + assert.throws( + () => assert.doesNotMatch(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be of type RegExp.' + + invalidArgTypeHelper('string') + } + ); + assert.throws( + () => assert.doesNotMatch('string', /string/), + { + actual: 'string', + expected: /string/, + operator: 'doesNotMatch', + message: 'The input was expected to not match the regular expression ' + + "/string/. Input:\n\n'string'\n", + generatedMessage: true + } + ); + assert.throws( + () => assert.doesNotMatch('string', /string/, 'foobar'), + { + actual: 'string', + expected: /string/, + operator: 'doesNotMatch', + message: 'foobar', + generatedMessage: false + } + ); + const errorMessage = new RangeError('foobar'); + assert.throws( + () => assert.doesNotMatch('string', /string/, errorMessage), + errorMessage + ); + assert.throws( + () => assert.doesNotMatch({abc: 123}, /abc/), + { + actual: {abc: 123}, + expected: /abc/, + operator: 'doesNotMatch', + message: 'The "string" argument must be of type string. ' + + 'Received type object ({ abc: 123 })', + generatedMessage: true + } + ); + assert.doesNotMatch('I will pass', /different$/); + } +}); + +test('assert/strict exists', () => { + assert.strictEqual(require('assert/strict'), assert.strict); +}); + +/* eslint-enable no-restricted-syntax */ +/* eslint-enable no-restricted-properties */ + + diff --git a/test/js/node/test/parallel/needs-test/test-url-fileurltopath.js b/test/js/node/test/parallel/needs-test/test-url-fileurltopath.js new file mode 100644 index 0000000000..3b08d06730 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-url-fileurltopath.js @@ -0,0 +1,177 @@ +'use strict'; +const { isWindows } = require('../../common'); + +const { test } = require('node:test'); +const assert = require('node:assert'); +const url = require('node:url'); + +test('invalid arguments', () => { + for (const arg of [null, undefined, 1, {}, true]) { + assert.throws(() => url.fileURLToPath(arg), { + code: 'ERR_INVALID_ARG_TYPE' + }); + } +}); + +test('input must be a file URL', () => { + assert.throws(() => url.fileURLToPath('https://a/b/c'), { + code: 'ERR_INVALID_URL_SCHEME' + }); +}); + +test('fileURLToPath with host', () => { + const withHost = new URL('file://host/a'); + + if (isWindows) { + assert.strictEqual(url.fileURLToPath(withHost), '\\\\host\\a'); + } else { + assert.throws(() => url.fileURLToPath(withHost), { + code: 'ERR_INVALID_FILE_URL_HOST' + }); + } +}); + +test('fileURLToPath with invalid path', () => { + if (isWindows) { + assert.throws(() => url.fileURLToPath('file:///C:/a%2F/'), { + code: 'ERR_INVALID_FILE_URL_PATH' + }); + assert.throws(() => url.fileURLToPath('file:///C:/a%5C/'), { + code: 'ERR_INVALID_FILE_URL_PATH' + }); + assert.throws(() => url.fileURLToPath('file:///?:/'), { + code: 'ERR_INVALID_FILE_URL_PATH' + }); + } else { + assert.throws(() => url.fileURLToPath('file:///a%2F/'), { + code: 'ERR_INVALID_FILE_URL_PATH' + }); + } +}); + +const windowsTestCases = [ + // Lowercase ascii alpha + { path: 'C:\\foo', fileURL: 'file:///C:/foo' }, + // Uppercase ascii alpha + { path: 'C:\\FOO', fileURL: 'file:///C:/FOO' }, + // dir + { path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' }, + // trailing separator + { path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' }, + // dot + { path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' }, + // space + { path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' }, + // question mark + { path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' }, + // number sign + { path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' }, + // ampersand + { path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' }, + // equals + { path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' }, + // colon + { path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' }, + // semicolon + { path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' }, + // percent + { path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' }, + // backslash + { path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' }, + // backspace + { path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' }, + // tab + { path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' }, + // newline + { path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' }, + // carriage return + { path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' }, + // latin1 + { path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' }, + // Euro sign (BMP code point) + { path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' }, + // Rocket emoji (non-BMP code point) + { path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' }, + // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows) + { path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' }, +]; + +const posixTestCases = [ + // Lowercase ascii alpha + { path: '/foo', fileURL: 'file:///foo' }, + // Uppercase ascii alpha + { path: '/FOO', fileURL: 'file:///FOO' }, + // dir + { path: '/dir/foo', fileURL: 'file:///dir/foo' }, + // trailing separator + { path: '/dir/', fileURL: 'file:///dir/' }, + // dot + { path: '/foo.mjs', fileURL: 'file:///foo.mjs' }, + // space + { path: '/foo bar', fileURL: 'file:///foo%20bar' }, + // question mark + { path: '/foo?bar', fileURL: 'file:///foo%3Fbar' }, + // number sign + { path: '/foo#bar', fileURL: 'file:///foo%23bar' }, + // ampersand + { path: '/foo&bar', fileURL: 'file:///foo&bar' }, + // equals + { path: '/foo=bar', fileURL: 'file:///foo=bar' }, + // colon + { path: '/foo:bar', fileURL: 'file:///foo:bar' }, + // semicolon + { path: '/foo;bar', fileURL: 'file:///foo;bar' }, + // percent + { path: '/foo%bar', fileURL: 'file:///foo%25bar' }, + // backslash + { path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' }, + // backspace + { path: '/foo\bbar', fileURL: 'file:///foo%08bar' }, + // tab + { path: '/foo\tbar', fileURL: 'file:///foo%09bar' }, + // newline + { path: '/foo\nbar', fileURL: 'file:///foo%0Abar' }, + // carriage return + { path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' }, + // latin1 + { path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' }, + // Euro sign (BMP code point) + { path: '/€', fileURL: 'file:///%E2%82%AC' }, + // Rocket emoji (non-BMP code point) + { path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' }, +]; + +test('fileURLToPath with windows path', { skip: !isWindows }, () => { + + for (const { path, fileURL } of windowsTestCases) { + const fromString = url.fileURLToPath(fileURL, { windows: true }); + assert.strictEqual(fromString, path); + const fromURL = url.fileURLToPath(new URL(fileURL), { windows: true }); + assert.strictEqual(fromURL, path); + } +}); + +test('fileURLToPath with posix path', { skip: isWindows }, () => { + for (const { path, fileURL } of posixTestCases) { + const fromString = url.fileURLToPath(fileURL, { windows: false }); + assert.strictEqual(fromString, path); + const fromURL = url.fileURLToPath(new URL(fileURL), { windows: false }); + assert.strictEqual(fromURL, path); + } +}); + +const defaultTestCases = isWindows ? windowsTestCases : posixTestCases; + +test('options is null', () => { + const whenNullActual = url.fileURLToPath(new URL(defaultTestCases[0].fileURL), null); + assert.strictEqual(whenNullActual, defaultTestCases[0].path); +}); + +test('defaultTestCases', () => { + for (const { path, fileURL } of defaultTestCases) { + const fromString = url.fileURLToPath(fileURL); + assert.strictEqual(fromString, path); + const fromURL = url.fileURLToPath(new URL(fileURL)); + assert.strictEqual(fromURL, path); + } +}); diff --git a/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js b/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js new file mode 100644 index 0000000000..7ccb472a8d --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js @@ -0,0 +1,32 @@ +'use strict'; + +require('../../common'); + +const assert = require('node:assert'); +const url = require('node:url'); +const { test } = require('node:test'); + +test('format invalid input', () => { + const throwsObjsAndReportTypes = [ + undefined, + null, + true, + false, + 0, + function() {}, + Symbol('foo'), + ]; + + for (const urlObject of throwsObjsAndReportTypes) { + console.log(urlObject) + assert.throws(function runFormat() { + url.format(urlObject); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } + assert.strictEqual(url.format(''), ''); + assert.strictEqual(url.format({}), ''); +}); + diff --git a/test/js/node/test/parallel/net-after-close.test.js b/test/js/node/test/parallel/net-after-close.test.js deleted file mode 100644 index 5d2248cc5e..0000000000 --- a/test/js/node/test/parallel/net-after-close.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-net-after-close.js -//#SHA1: 5b16857d2580262739b7c74c87a520ee6fc974c9 -//----------------- -"use strict"; -const net = require("net"); - -let server; -let serverPort; - -beforeAll(done => { - server = net.createServer(s => { - s.end(); - }); - - server.listen(0, () => { - serverPort = server.address().port; - done(); - }); -}); - -afterAll(done => { - server.close(done); -}); - -test("net socket behavior after close", done => { - const c = net.createConnection(serverPort); - - c.on("close", () => { - expect(c._handle).toBeNull(); - - // Calling functions / accessing properties of a closed socket should not throw. - expect(() => { - c.setNoDelay(); - c.setKeepAlive(); - c.bufferSize; - c.pause(); - c.resume(); - c.address(); - c.remoteAddress; - c.remotePort; - }).not.toThrow(); - - done(); - }); -}); - -//<#END_FILE: test-net-after-close.js diff --git a/test/js/node/test/parallel/net-allow-half-open.test.js b/test/js/node/test/parallel/net-allow-half-open.test.js deleted file mode 100644 index 0b05942eeb..0000000000 --- a/test/js/node/test/parallel/net-allow-half-open.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-net-allow-half-open.js -//#SHA1: 713191e6681104ac9709a51cbe5dc881f7a7fa89 -//----------------- -'use strict'; - -const net = require('net'); - -describe('Net allow half open', () => { - test('Socket not destroyed immediately after end', (done) => { - const server = net.createServer((socket) => { - socket.end(Buffer.alloc(1024)); - }); - - server.listen(0, () => { - const socket = net.connect(server.address().port); - expect(socket.allowHalfOpen).toBe(false); - socket.resume(); - - socket.on('end', () => { - process.nextTick(() => { - // Ensure socket is not destroyed straight away - // without proper shutdown. - expect(socket.destroyed).toBe(false); - server.close(); - done(); - }); - }); - - socket.on('finish', () => { - expect(socket.destroyed).toBe(false); - }); - - socket.on('close', () => {}); - }); - }); - - test('Socket not destroyed after end and write', (done) => { - const server = net.createServer((socket) => { - socket.end(Buffer.alloc(1024)); - }); - - server.listen(0, () => { - const socket = net.connect(server.address().port); - expect(socket.allowHalfOpen).toBe(false); - socket.resume(); - - socket.on('end', () => { - expect(socket.destroyed).toBe(false); - }); - - socket.end('asd'); - - socket.on('finish', () => { - expect(socket.destroyed).toBe(false); - }); - - socket.on('close', () => { - server.close(); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-net-allow-half-open.js diff --git a/test/js/node/test/parallel/net-autoselectfamily-attempt-timeout-default-value.test.js b/test/js/node/test/parallel/net-autoselectfamily-attempt-timeout-default-value.test.js deleted file mode 100644 index 5b33636b4d..0000000000 --- a/test/js/node/test/parallel/net-autoselectfamily-attempt-timeout-default-value.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-net-autoselectfamily-attempt-timeout-default-value.js -//#SHA1: 028b16515c47d987e68ca138e753ed4d255f179c -//----------------- -"use strict"; - -const { platformTimeout } = require("../common"); -const { getDefaultAutoSelectFamilyAttemptTimeout } = require("net"); - -test("getDefaultAutoSelectFamilyAttemptTimeout returns the correct default value", () => { - expect(getDefaultAutoSelectFamilyAttemptTimeout()).toBe(platformTimeout(2500)); -}); - -//<#END_FILE: test-net-autoselectfamily-attempt-timeout-default-value.js diff --git a/test/js/node/test/parallel/net-bind-twice-exclusive.test.js b/test/js/node/test/parallel/net-bind-twice-exclusive.test.js deleted file mode 100644 index 9854878479..0000000000 --- a/test/js/node/test/parallel/net-bind-twice-exclusive.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-net-bind-twice.js -//#SHA1: 432eb9529d0affc39c8af9ebc1147528d96305c9 -//----------------- -"use strict"; -const net = require("net"); - -test("net.Server should not allow binding to the same port twice", done => { - const server1 = net.createServer(() => { - throw new Error("Server1 should not receive connections"); - }); - - server1.listen( - { - exclusive: true, - port: 0, - host: "127.0.0.1", - }, - () => { - const server2 = net.createServer(() => { - throw new Error("Server2 should not receive connections"); - }); - - const port = server1.address().port; - server2.listen(port, "127.0.0.1", () => { - throw new Error("Server2 should not be able to listen"); - }); - - server2.on("error", e => { - console.error(e); - expect(e.code).toBe("EADDRINUSE"); - server1.close(() => { - done(); - }); - }); - }, - ); -}); - -//<#END_FILE: test-net-bind-twice.js diff --git a/test/js/node/test/parallel/net-bind-twice-reuseport.test.js b/test/js/node/test/parallel/net-bind-twice-reuseport.test.js deleted file mode 100644 index 55067ce9d6..0000000000 --- a/test/js/node/test/parallel/net-bind-twice-reuseport.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-net-bind-twice.js -//#SHA1: 432eb9529d0affc39c8af9ebc1147528d96305c9 -//----------------- -"use strict"; - -import { test } from "bun:test"; -import net from "node:net"; -import { isWindows } from "harness"; - -test.skipIf(isWindows)("net.Server should not allow binding to the same port twice", done => { - const server1 = net.createServer(() => { - throw new Error("Server1 should not receive connections"); - }); - - const options = { - reusePort: true, - port: 0, - host: "127.0.0.1", - }; - server1.listen(options, () => { - const server2 = net.createServer(() => { - throw new Error("Server2 should not receive connections"); - }); - - const port = server1.address().port; - server2.listen({ ...options, port }, () => { - server2.close(() => { - server1.close(() => { - done(); - }); - }); - }); - - server2.on("error", e => { - server1.close(() => { - done(e); - }); - }); - }); -}); - -//<#END_FILE: test-net-bind-twice.js diff --git a/test/js/node/test/parallel/net-bind-twice.test.js b/test/js/node/test/parallel/net-bind-twice.test.js deleted file mode 100644 index 56454d2aab..0000000000 --- a/test/js/node/test/parallel/net-bind-twice.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-net-bind-twice.js -//#SHA1: 432eb9529d0affc39c8af9ebc1147528d96305c9 -//----------------- -"use strict"; -const net = require("net"); - -test("net.Server should not allow binding to the same port twice", done => { - const server1 = net.createServer(() => { - throw new Error("Server1 should not receive connections"); - }); - - server1.listen(0, "127.0.0.1", () => { - const server2 = net.createServer(() => { - throw new Error("Server2 should not receive connections"); - }); - - const port = server1.address().port; - server2.listen(port, "127.0.0.1", () => { - throw new Error("Server2 should not be able to listen"); - }); - - server2.on("error", e => { - expect(e.code).toBe("EADDRINUSE"); - server1.close(() => { - done(); - }); - }); - }); -}); - -//<#END_FILE: test-net-bind-twice.js diff --git a/test/js/node/test/parallel/net-buffersize.test.js b/test/js/node/test/parallel/net-buffersize.test.js deleted file mode 100644 index 5701356648..0000000000 --- a/test/js/node/test/parallel/net-buffersize.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-net-buffersize.js -//#SHA1: b6b1298dc9f836252e5fcdcee680116d50da7651 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -const iter = 10; - -test("net buffer size", async () => { - const server = net.createServer(socket => { - socket.on("readable", () => { - socket.read(); - }); - - socket.on("end", () => { - server.close(); - }); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const client = net.connect(server.address().port); - - client.on("finish", () => { - expect(client.bufferSize).toBe(0); - resolve(); - }); - - for (let i = 1; i < iter; i++) { - client.write("a"); - expect(client.bufferSize).toBe(i); - } - - client.end(); - }); - }); -}); - -//<#END_FILE: test-net-buffersize.js diff --git a/test/js/node/test/parallel/net-bytes-written-large.test.js b/test/js/node/test/parallel/net-bytes-written-large.test.js deleted file mode 100644 index 715af6ecc7..0000000000 --- a/test/js/node/test/parallel/net-bytes-written-large.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-net-bytes-written-large.js -//#SHA1: 9005801147f80a8058f1b2126d772e52abd1f237 -//----------------- -"use strict"; -const net = require("net"); - -const N = 10000000; - -describe("Net bytes written large", () => { - test("Write a Buffer", done => { - const server = net - .createServer(socket => { - socket.end(Buffer.alloc(N), () => { - expect(socket.bytesWritten).toBe(N); - }); - expect(socket.bytesWritten).toBe(N); - }) - .listen(0, () => { - const client = net.connect(server.address().port); - client.resume(); - client.on("close", () => { - expect(client.bytesRead).toBe(N); - server.close(); - done(); - }); - }); - }); - - test("Write a string", done => { - const server = net - .createServer(socket => { - socket.end("a".repeat(N), () => { - expect(socket.bytesWritten).toBe(N); - }); - expect(socket.bytesWritten).toBe(N); - }) - .listen(0, () => { - const client = net.connect(server.address().port); - client.resume(); - client.on("close", () => { - expect(client.bytesRead).toBe(N); - server.close(); - done(); - }); - }); - }); - - test("writev() with mixed data", done => { - const server = net - .createServer(socket => { - socket.cork(); - socket.write("a".repeat(N)); - expect(socket.bytesWritten).toBe(N); - socket.write(Buffer.alloc(N)); - expect(socket.bytesWritten).toBe(2 * N); - socket.end("", () => { - expect(socket.bytesWritten).toBe(2 * N); - }); - socket.uncork(); - }) - .listen(0, () => { - const client = net.connect(server.address().port); - client.resume(); - client.on("close", () => { - expect(client.bytesRead).toBe(2 * N); - server.close(); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-net-bytes-written-large.js diff --git a/test/js/node/test/parallel/net-can-reset-timeout.test.js b/test/js/node/test/parallel/net-can-reset-timeout.test.js deleted file mode 100644 index 1bb7e8e6a8..0000000000 --- a/test/js/node/test/parallel/net-can-reset-timeout.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-net-can-reset-timeout.js -//#SHA1: 871319149db929419e14ba7f08e5d0c878222a93 -//----------------- -'use strict'; - -const net = require('net'); - -describe('Net can reset timeout', () => { - let server; - let port; - - beforeAll((done) => { - server = net.createServer((stream) => { - stream.setTimeout(100); - - stream.resume(); - - stream.once('timeout', () => { - console.log('timeout'); - // Try to reset the timeout. - stream.write('WHAT.'); - }); - - stream.on('end', () => { - console.log('server side end'); - stream.end(); - }); - }); - - server.listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test('should handle timeout and reset', (done) => { - const c = net.createConnection(port, "127.0.0.1"); - - c.on('data', () => { - c.end(); - }); - - c.on('end', () => { - console.log('client side end'); - done(); - }); - }); -}); - -//<#END_FILE: test-net-can-reset-timeout.js diff --git a/test/js/node/test/parallel/net-connect-after-destroy.test.js b/test/js/node/test/parallel/net-connect-after-destroy.test.js deleted file mode 100644 index 013f7cd0da..0000000000 --- a/test/js/node/test/parallel/net-connect-after-destroy.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-net-connect-after-destroy.js -//#SHA1: 9341bea710601b5a3a8e823f4847396b210a855c -//----------------- -'use strict'; - -const net = require('net'); - -test('net.createConnection after destroy', () => { - // Connect to something that we need to DNS resolve - const c = net.createConnection(80, 'google.com'); - - // The test passes if this doesn't throw an error - expect(() => { - c.destroy(); - }).not.toThrow(); -}); - -//<#END_FILE: test-net-connect-after-destroy.js diff --git a/test/js/node/test/parallel/net-connect-call-socket-connect.test.js b/test/js/node/test/parallel/net-connect-call-socket-connect.test.js deleted file mode 100644 index 8a74641b74..0000000000 --- a/test/js/node/test/parallel/net-connect-call-socket-connect.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-net-connect-call-socket-connect.js -//#SHA1: 953a427c411633a029dcf978ea07fb701ae5ed9a -//----------------- -"use strict"; - -const net = require("net"); -const Socket = net.Socket; - -// This test checks that calling `net.connect` internally calls -// `Socket.prototype.connect`. -// -// This is important for people who monkey-patch `Socket.prototype.connect` -// since it's not possible to monkey-patch `net.connect` directly (as the core -// `connect` function is called internally in Node instead of calling the -// `exports.connect` function). -// -// Monkey-patching of `Socket.prototype.connect` is done by - among others - -// most APM vendors, the async-listener module and the -// continuation-local-storage module. -// -// Related: -// - https://github.com/nodejs/node/pull/12342 -// - https://github.com/nodejs/node/pull/12852 - -test("net.connect calls Socket.prototype.connect", async () => { - // Monkey patch Socket.prototype.connect to check that it's called. - const orig = Socket.prototype.connect; - const connectMock = jest.fn(function () { - return orig.apply(this, arguments); - }); - Socket.prototype.connect = connectMock; - - const server = net.createServer(); - - await new Promise(resolve => { - server.listen(() => { - const port = server.address().port; - const client = net.connect({ port }, () => { - client.end(); - }); - client.on("end", () => { - server.close(resolve); - }); - }); - }); - - expect(connectMock).toHaveBeenCalledTimes(1); - - // Restore original Socket.prototype.connect - Socket.prototype.connect = orig; -}); - -//<#END_FILE: test-net-connect-call-socket-connect.js diff --git a/test/js/node/test/parallel/net-connect-destroy.test.js b/test/js/node/test/parallel/net-connect-destroy.test.js deleted file mode 100644 index 358d9495a9..0000000000 --- a/test/js/node/test/parallel/net-connect-destroy.test.js +++ /dev/null @@ -1,19 +0,0 @@ -//#FILE: test-net-connect-destroy.js -//#SHA1: a185f5169d7b2988a09b74d9524743beda08dcff -//----------------- -'use strict'; -const net = require('net'); - -test('Socket is destroyed and emits close event', (done) => { - const socket = new net.Socket(); - - socket.on('close', () => { - // The close event was emitted - expect(true).toBe(true); - done(); - }); - - socket.destroy(); -}); - -//<#END_FILE: test-net-connect-destroy.js diff --git a/test/js/node/test/parallel/net-connect-immediate-destroy.test.js b/test/js/node/test/parallel/net-connect-immediate-destroy.test.js deleted file mode 100644 index 055b18bae4..0000000000 --- a/test/js/node/test/parallel/net-connect-immediate-destroy.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-net-connect-immediate-destroy.js -//#SHA1: 28ba78fafba37cb07a2ec4b18e3e35bbb78ef699 -//----------------- -"use strict"; -const net = require("net"); - -test("net.connect immediate destroy", done => { - const server = net.createServer(); - server.listen(0, () => { - const port = server.address().port; - const socket = net.connect(port, "127.0.0.1"); - - socket.on("connect", () => { - throw new Error("Socket should not connect"); - }); - - socket.on("error", () => { - throw new Error("Socket should not emit error"); - }); - - server.close(() => { - socket.destroy(); - done(); - }); - }); -}); - -//<#END_FILE: test-net-connect-immediate-destroy.js diff --git a/test/js/node/test/parallel/net-connect-options-allowhalfopen.test.js b/test/js/node/test/parallel/net-connect-options-allowhalfopen.test.js deleted file mode 100644 index e0cdeb1803..0000000000 --- a/test/js/node/test/parallel/net-connect-options-allowhalfopen.test.js +++ /dev/null @@ -1,112 +0,0 @@ -//#FILE: test-net-connect-options-allowhalfopen.js -//#SHA1: 9ba18563d747b3ebfa63f8f54468b62526224ec6 -//----------------- -"use strict"; -const net = require("net"); - -describe("Net connect options allowHalfOpen", () => { - let server; - let clientReceivedFIN = 0; - let serverConnections = 0; - let clientSentFIN = 0; - let serverReceivedFIN = 0; - const host = "127.0.0.1"; - const CLIENT_VARIANTS = 6; - - function serverOnConnection(socket) { - console.log(`'connection' ${++serverConnections} emitted on server`); - const srvConn = serverConnections; - socket.resume(); - socket.on("data", data => { - socket.clientId = data.toString(); - console.log(`server connection ${srvConn} is started by client ${socket.clientId}`); - }); - - socket.on("end", () => { - console.log(`Server received FIN sent by client ${socket.clientId}`); - if (++serverReceivedFIN < CLIENT_VARIANTS) return; - setTimeout(() => { - server.close(); - console.log( - `connection ${socket.clientId} is closing the server: - FIN ${serverReceivedFIN} received by server, - FIN ${clientReceivedFIN} received by client - FIN ${clientSentFIN} sent by client, - FIN ${serverConnections} sent by server`.replace(/ {3,}/g, ""), - ); - }, 50); - }); - socket.end(); - console.log(`Server has sent ${serverConnections} FIN`); - } - - function serverOnClose() { - console.log( - `Server has been closed: - FIN ${serverReceivedFIN} received by server - FIN ${clientReceivedFIN} received by client - FIN ${clientSentFIN} sent by client - FIN ${serverConnections} sent by server`.replace(/ {3,}/g, ""), - ); - } - - beforeAll(done => { - server = net - .createServer({ allowHalfOpen: true }) - .on("connection", serverOnConnection) - .on("close", serverOnClose) - .listen(0, host, () => { - console.log(`Server started listening at ${host}:${server.address().port}`); - done(); - }); - }); - - afterAll(() => { - if (server) { - server.close(); - } else { - done(); - } - }); - - test("should handle allowHalfOpen connections correctly", done => { - function clientOnConnect(index) { - return function clientOnConnectInner() { - const client = this; - console.log(`'connect' emitted on Client ${index}`); - client.resume(); - client.on("end", () => { - setTimeout(() => { - console.log(`client ${index} received FIN`); - expect(client.readable).toBe(false); - expect(client.writable).toBe(true); - expect(client.write(String(index))).toBeTruthy(); - client.end(); - clientSentFIN++; - console.log(`client ${index} sent FIN, ${clientSentFIN} have been sent`); - }, 50); - }); - client.on("close", () => { - clientReceivedFIN++; - console.log( - `connection ${index} has been closed by both sides,` + ` ${clientReceivedFIN} clients have closed`, - ); - if (clientReceivedFIN === CLIENT_VARIANTS) { - done(); - } - }); - }; - } - - const port = server.address().port; - const opts = { allowHalfOpen: true, host, port }; - net.connect(opts, clientOnConnect(1)); - net.connect(opts).on("connect", clientOnConnect(2)); - net.createConnection(opts, clientOnConnect(3)); - net.createConnection(opts).on("connect", clientOnConnect(4)); - new net.Socket(opts).connect(opts, clientOnConnect(5)); - new net.Socket(opts).connect(opts).on("connect", clientOnConnect(6)); - }); -}); - -//<#END_FILE: test-net-connect-options-allowhalfopen.js diff --git a/test/js/node/test/parallel/net-connect-options-fd.test.js b/test/js/node/test/parallel/net-connect-options-fd.test.js deleted file mode 100644 index a685b4a0e6..0000000000 --- a/test/js/node/test/parallel/net-connect-options-fd.test.js +++ /dev/null @@ -1,12 +0,0 @@ -//#FILE: test-net-connect-options-fd.js -//#SHA1: 3933f2a09469bfaad999b5ba483bde9c6255cb35 -//----------------- -'use strict'; - -// This test requires internal Node.js modules and cannot be run in a standard Jest environment -test('net connect options fd', () => { - console.log('This test requires internal Node.js modules and cannot be run in a standard Jest environment'); - expect(true).toBe(true); -}); - -//<#END_FILE: test-net-connect-options-fd.js diff --git a/test/js/node/test/parallel/net-connect-options-path.test.js b/test/js/node/test/parallel/net-connect-options-path.test.js deleted file mode 100644 index 446200036b..0000000000 --- a/test/js/node/test/parallel/net-connect-options-path.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-net-connect-options-path.js -//#SHA1: 03b1a7de04f689c6429298b553a49478321b4adb -//----------------- -'use strict'; -const net = require('net'); -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -const CLIENT_VARIANTS = 12; - -describe('net.connect options path', () => { - let serverPath; - let server; - - beforeAll(() => { - const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'net-connect-options-path-')); - serverPath = path.join(tmpdir, 'server'); - }); - - afterAll(() => { - fs.rmdirSync(path.dirname(serverPath), { recursive: true }); - }); - - test('connect with various options', (done) => { - let connectionsCount = 0; - - server = net.createServer((socket) => { - socket.end('ok'); - }); - - server.listen(serverPath, () => { - const connectAndTest = (connectFn) => { - return new Promise((resolve) => { - const socket = connectFn(); - socket.on('data', (data) => { - expect(data.toString()).toBe('ok'); - socket.end(); - }); - socket.on('end', () => { - connectionsCount++; - resolve(); - }); - }); - }; - - const connectPromises = [ - () => net.connect(serverPath), - () => net.createConnection(serverPath), - () => new net.Socket().connect(serverPath), - () => net.connect({ path: serverPath }), - () => net.createConnection({ path: serverPath }), - () => new net.Socket().connect({ path: serverPath }) - ]; - - Promise.all(connectPromises.map(connectAndTest)) - .then(() => { - expect(connectionsCount).toBe(CLIENT_VARIANTS / 2); // We're testing 6 variants instead of 12 - server.close(() => { - done(); - }); - }) - .catch((err) => { - done(err); - }); - }); - }); -}); - -//<#END_FILE: test-net-connect-options-path.js diff --git a/test/js/node/test/parallel/net-connect-paused-connection.test.js b/test/js/node/test/parallel/net-connect-paused-connection.test.js deleted file mode 100644 index 6bfc5f554a..0000000000 --- a/test/js/node/test/parallel/net-connect-paused-connection.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-net-connect-paused-connection.js -//#SHA1: ab2fae629f3abb5fc4d5e59dd7d1dd2e09b9eb48 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -test("net connect with paused connection", done => { - const server = net.createServer(conn => { - conn.unref(); - }); - - server.listen(0, () => { - const connection = net.connect(server.address().port, "localhost"); - connection.pause(); - - const timeoutId = setTimeout(() => { - done.fail("Should not have called timeout"); - }, 1000); - - // Unref the timeout to allow the process to exit - timeoutId.unref(); - - // Allow some time for the test to potentially fail - setTimeout(() => { - server.close(); - done(); - }, 500); - }); - - server.unref(); -}); - -//<#END_FILE: test-net-connect-paused-connection.js diff --git a/test/js/node/test/parallel/net-connect-reset-until-connected.test.js b/test/js/node/test/parallel/net-connect-reset-until-connected.test.js deleted file mode 100644 index 7e2e77698b..0000000000 --- a/test/js/node/test/parallel/net-connect-reset-until-connected.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-net-connect-reset-until-connected.js -//#SHA1: b0170103868d4f693e9afda7923e021758393a39 -//----------------- -"use strict"; - -const net = require("net"); - -function barrier(count, cb) { - return function () { - if (--count === 0) cb(); - }; -} - -test("net connection reset until connected", done => { - const server = net.createServer(); - server.listen(0, () => { - const port = server.address().port; - const conn = net.createConnection(port); - const connok = barrier(2, () => conn.resetAndDestroy()); - - conn.on("close", () => { - expect(true).toBe(true); // Ensure 'close' event is called - }); - - server.on("connection", socket => { - connok(); - socket.on("error", err => { - expect(err).toEqual( - expect.objectContaining({ - code: "ECONNRESET", - name: "Error", - message: expect.any(String), - }), - ); - }); - server.close(); - }); - - conn.on("connect", connok); - }); - - // Ensure the test completes - setTimeout(() => { - done(); - }, 1000); -}); - -//<#END_FILE: test-net-connect-reset-until-connected.js diff --git a/test/js/node/test/parallel/net-dns-lookup-skip.test.js b/test/js/node/test/parallel/net-dns-lookup-skip.test.js deleted file mode 100644 index b75771a6cf..0000000000 --- a/test/js/node/test/parallel/net-dns-lookup-skip.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-net-dns-lookup-skip.js -//#SHA1: 023bfbaa998480ab732d83d4bf8efb68ad4fe5db -//----------------- -'use strict'; -const net = require('net'); - -async function checkDnsLookupSkip(addressType) { - return new Promise((resolve, reject) => { - const server = net.createServer((client) => { - client.end(); - server.close(); - }); - - const address = addressType === 4 ? '127.0.0.1' : '::1'; - const lookupSpy = jest.fn(); - - server.listen(0, address, () => { - net.connect(server.address().port, address) - .on('lookup', lookupSpy) - .on('connect', () => { - expect(lookupSpy).not.toHaveBeenCalled(); - resolve(); - }) - .on('error', reject); - }); - }); -} - -test('DNS lookup should be skipped for IPv4', async () => { - await checkDnsLookupSkip(4); -}); - -// Check if the environment supports IPv6 -const hasIPv6 = (() => { - try { - net.createServer().listen(0, '::1').close(); - return true; - } catch { - return false; - } -})(); - -(hasIPv6 ? test : test.skip)('DNS lookup should be skipped for IPv6', async () => { - await checkDnsLookupSkip(6); -}); - -//<#END_FILE: test-net-dns-lookup-skip.js diff --git a/test/js/node/test/parallel/net-during-close.test.js b/test/js/node/test/parallel/net-during-close.test.js deleted file mode 100644 index 9edb7efc92..0000000000 --- a/test/js/node/test/parallel/net-during-close.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-net-during-close.js -//#SHA1: c5fc5c85760b2c68679f7041ebf737e8204ca8c5 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -test("accessing client properties during server close", done => { - const server = net.createServer(socket => { - socket.end(); - }); - - server.listen(0, () => { - const client = net.createConnection(server.address().port); - server.close(); - - // Server connection event has not yet fired client is still attempting to - // connect. Accessing properties should not throw in this case. - expect(() => { - /* eslint-disable no-unused-expressions */ - client.remoteAddress; - client.remoteFamily; - client.remotePort; - /* eslint-enable no-unused-expressions */ - }).not.toThrow(); - - // Exit now, do not wait for the client error event. - done(); - }); -}); - -//<#END_FILE: test-net-during-close.js diff --git a/test/js/node/test/parallel/net-end-close.test.js b/test/js/node/test/parallel/net-end-close.test.js deleted file mode 100644 index 10d17c8c07..0000000000 --- a/test/js/node/test/parallel/net-end-close.test.js +++ /dev/null @@ -1,12 +0,0 @@ -//#FILE: test-net-end-close.js -//#SHA1: 01ac4a26e7cb4d477e547f9e6bd2f52a3b0d9277 -//----------------- -"use strict"; - -test.skip("net Socket end and close events", () => { - console.log( - "This test relies on internal Node.js APIs and cannot be accurately replicated in a cross-platform manner.", - ); -}); - -//<#END_FILE: test-net-end-close.js diff --git a/test/js/node/test/parallel/net-end-destroyed.test.js b/test/js/node/test/parallel/net-end-destroyed.test.js deleted file mode 100644 index 0ad62bbf17..0000000000 --- a/test/js/node/test/parallel/net-end-destroyed.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-net-end-destroyed.js -//#SHA1: cf219496c5dd2cb11f3d01750692b61791c7e2f9 -//----------------- -"use strict"; - -const net = require("net"); - -test('socket is not destroyed when the "end" event is emitted', done => { - const server = net.createServer(); - - server.on("connection", () => { - // Connection event handler - }); - - // Ensure that the socket is not destroyed when the 'end' event is emitted. - - server.listen(() => { - const socket = net.createConnection({ - port: server.address().port, - }); - - socket.on("connect", () => { - socket.on("end", () => { - expect(socket.destroyed).toBe(false); - server.close(); - done(); - }); - - socket.end(); - }); - }); -}); - -//<#END_FILE: test-net-end-destroyed.js diff --git a/test/js/node/test/parallel/net-end-without-connect.test.js b/test/js/node/test/parallel/net-end-without-connect.test.js deleted file mode 100644 index ffdd987eb0..0000000000 --- a/test/js/node/test/parallel/net-end-without-connect.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-net-end-without-connect.js -//#SHA1: d13d4a7117c5625fec0c619acc024e705dfb4212 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -test("Socket.end() without connect", done => { - const sock = new net.Socket(); - sock.end(() => { - expect(sock.writable).toBe(false); - done(); - }); -}); - -//<#END_FILE: test-net-end-without-connect.js diff --git a/test/js/node/test/parallel/net-isip.test.js b/test/js/node/test/parallel/net-isip.test.js deleted file mode 100644 index d30b722c22..0000000000 --- a/test/js/node/test/parallel/net-isip.test.js +++ /dev/null @@ -1,107 +0,0 @@ -//#FILE: test-net-isip.js -//#SHA1: 5fb15ec330f4e7489c3e7eb2a74547c44aa5a4dc -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -test("net.isIP", () => { - expect(net.isIP("127.0.0.1")).toBe(4); - expect(net.isIP("x127.0.0.1")).toBe(0); - expect(net.isIP("example.com")).toBe(0); - expect(net.isIP("0000:0000:0000:0000:0000:0000:0000:0000")).toBe(6); - expect(net.isIP("0000:0000:0000:0000:0000:0000:0000:0000::0000")).toBe(0); - expect(net.isIP("1050:0:0:0:5:600:300c:326b")).toBe(6); - expect(net.isIP("2001:252:0:1::2008:6")).toBe(6); - expect(net.isIP("2001:dead:beef:1::2008:6")).toBe(6); - expect(net.isIP("2001::")).toBe(6); - expect(net.isIP("2001:dead::")).toBe(6); - expect(net.isIP("2001:dead:beef::")).toBe(6); - expect(net.isIP("2001:dead:beef:1::")).toBe(6); - expect(net.isIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")).toBe(6); - expect(net.isIP(":2001:252:0:1::2008:6:")).toBe(0); - expect(net.isIP(":2001:252:0:1::2008:6")).toBe(0); - expect(net.isIP("2001:252:0:1::2008:6:")).toBe(0); - expect(net.isIP("2001:252::1::2008:6")).toBe(0); - expect(net.isIP("::2001:252:1:2008:6")).toBe(6); - expect(net.isIP("::2001:252:1:1.1.1.1")).toBe(6); - expect(net.isIP("::2001:252:1:255.255.255.255")).toBe(6); - expect(net.isIP("::2001:252:1:255.255.255.255.76")).toBe(0); - expect(net.isIP("fe80::2008%eth0")).toBe(6); - expect(net.isIP("fe80::2008%eth0.0")).toBe(6); - expect(net.isIP("fe80::2008%eth0@1")).toBe(0); - expect(net.isIP("::anything")).toBe(0); - expect(net.isIP("::1")).toBe(6); - expect(net.isIP("::")).toBe(6); - expect(net.isIP("0000:0000:0000:0000:0000:0000:12345:0000")).toBe(0); - expect(net.isIP("0")).toBe(0); - expect(net.isIP()).toBe(0); - expect(net.isIP("")).toBe(0); - expect(net.isIP(null)).toBe(0); - expect(net.isIP(123)).toBe(0); - expect(net.isIP(true)).toBe(0); - expect(net.isIP({})).toBe(0); - expect(net.isIP({ toString: () => "::2001:252:1:255.255.255.255" })).toBe(6); - expect(net.isIP({ toString: () => "127.0.0.1" })).toBe(4); - expect(net.isIP({ toString: () => "bla" })).toBe(0); -}); - -test("net.isIPv4", () => { - expect(net.isIPv4("127.0.0.1")).toBe(true); - expect(net.isIPv4("example.com")).toBe(false); - expect(net.isIPv4("2001:252:0:1::2008:6")).toBe(false); - expect(net.isIPv4()).toBe(false); - expect(net.isIPv4("")).toBe(false); - expect(net.isIPv4(null)).toBe(false); - expect(net.isIPv4(123)).toBe(false); - expect(net.isIPv4(true)).toBe(false); - expect(net.isIPv4({})).toBe(false); - expect( - net.isIPv4({ - toString: () => "::2001:252:1:255.255.255.255", - }), - ).toBe(false); - expect(net.isIPv4({ toString: () => "127.0.0.1" })).toBe(true); - expect(net.isIPv4({ toString: () => "bla" })).toBe(false); -}); - -test("net.isIPv6", () => { - expect(net.isIPv6("127.0.0.1")).toBe(false); - expect(net.isIPv6("example.com")).toBe(false); - expect(net.isIPv6("2001:252:0:1::2008:6")).toBe(true); - expect(net.isIPv6()).toBe(false); - expect(net.isIPv6("")).toBe(false); - expect(net.isIPv6(null)).toBe(false); - expect(net.isIPv6(123)).toBe(false); - expect(net.isIPv6(true)).toBe(false); - expect(net.isIPv6({})).toBe(false); - expect( - net.isIPv6({ - toString: () => "::2001:252:1:255.255.255.255", - }), - ).toBe(true); - expect(net.isIPv6({ toString: () => "127.0.0.1" })).toBe(false); - expect(net.isIPv6({ toString: () => "bla" })).toBe(false); -}); - -//<#END_FILE: test-net-isip.js diff --git a/test/js/node/test/parallel/net-isipv4.test.js b/test/js/node/test/parallel/net-isipv4.test.js deleted file mode 100644 index d76f97e740..0000000000 --- a/test/js/node/test/parallel/net-isipv4.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-net-isipv4.js -//#SHA1: 2f26a4e5d952b01d6fe2143c1e8f65a19997c974 -//----------------- -"use strict"; - -const net = require("net"); - -const v4 = [ - "0.0.0.0", - "8.8.8.8", - "127.0.0.1", - "100.100.100.100", - "192.168.0.1", - "18.101.25.153", - "123.23.34.2", - "172.26.168.134", - "212.58.241.131", - "128.0.0.0", - "23.71.254.72", - "223.255.255.255", - "192.0.2.235", - "99.198.122.146", - "46.51.197.88", - "173.194.34.134", -]; - -const v4not = [ - ".100.100.100.100", - "100..100.100.100.", - "100.100.100.100.", - "999.999.999.999", - "256.256.256.256", - "256.100.100.100.100", - "123.123.123", - "http://123.123.123", - "1000.2.3.4", - "999.2.3.4", - "0000000192.168.0.200", - "192.168.0.2000000000", -]; - -test("net.isIPv4 correctly identifies valid IPv4 addresses", () => { - for (const ip of v4) { - expect(net.isIPv4(ip)).toBe(true); - } -}); - -test("net.isIPv4 correctly identifies invalid IPv4 addresses", () => { - for (const ip of v4not) { - expect(net.isIPv4(ip)).toBe(false); - } -}); - -//<#END_FILE: test-net-isipv4.js diff --git a/test/js/node/test/parallel/net-isipv6.test.js b/test/js/node/test/parallel/net-isipv6.test.js deleted file mode 100644 index 62d06daf6b..0000000000 --- a/test/js/node/test/parallel/net-isipv6.test.js +++ /dev/null @@ -1,254 +0,0 @@ -//#FILE: test-net-isipv6.js -//#SHA1: 4c6eea8203876ea65c4496732ee169a8fe0eb574 -//----------------- -"use strict"; - -const net = require("net"); - -const v6 = [ - "::", - "1::", - "::1", - "1::8", - "1::7:8", - "1:2:3:4:5:6:7:8", - "1:2:3:4:5:6::8", - "1:2:3:4:5:6:7::", - "1:2:3:4:5::7:8", - "1:2:3:4:5::8", - "1:2:3::8", - "1::4:5:6:7:8", - "1::6:7:8", - "1::3:4:5:6:7:8", - "1:2:3:4::6:7:8", - "1:2::4:5:6:7:8", - "::2:3:4:5:6:7:8", - "1:2::8", - "2001:0000:1234:0000:0000:C1C0:ABCD:0876", - "3ffe:0b00:0000:0000:0001:0000:0000:000a", - "FF02:0000:0000:0000:0000:0000:0000:0001", - "0000:0000:0000:0000:0000:0000:0000:0001", - "0000:0000:0000:0000:0000:0000:0000:0000", - "::ffff:192.168.1.26", - "2::10", - "ff02::1", - "fe80::", - "2002::", - "2001:db8::", - "2001:0db8:1234::", - "::ffff:0:0", - "::ffff:192.168.1.1", - "1:2:3:4::8", - "1::2:3:4:5:6:7", - "1::2:3:4:5:6", - "1::2:3:4:5", - "1::2:3:4", - "1::2:3", - "::2:3:4:5:6:7", - "::2:3:4:5:6", - "::2:3:4:5", - "::2:3:4", - "::2:3", - "::8", - "1:2:3:4:5:6::", - "1:2:3:4:5::", - "1:2:3:4::", - "1:2:3::", - "1:2::", - "1:2:3:4::7:8", - "1:2:3::7:8", - "1:2::7:8", - "1:2:3:4:5:6:1.2.3.4", - "1:2:3:4:5::1.2.3.4", - "1:2:3:4::1.2.3.4", - "1:2:3::1.2.3.4", - "1:2::1.2.3.4", - "1::1.2.3.4", - "1:2:3:4::5:1.2.3.4", - "1:2:3::5:1.2.3.4", - "1:2::5:1.2.3.4", - "1::5:1.2.3.4", - "1::5:11.22.33.44", - "fe80::217:f2ff:254.7.237.98", - "fe80::217:f2ff:fe07:ed62", - "2001:DB8:0:0:8:800:200C:417A", - "FF01:0:0:0:0:0:0:101", - "0:0:0:0:0:0:0:1", - "0:0:0:0:0:0:0:0", - "2001:DB8::8:800:200C:417A", - "FF01::101", - "0:0:0:0:0:0:13.1.68.3", - "0:0:0:0:0:FFFF:129.144.52.38", - "::13.1.68.3", - "::FFFF:129.144.52.38", - "fe80:0000:0000:0000:0204:61ff:fe9d:f156", - "fe80:0:0:0:204:61ff:fe9d:f156", - "fe80::204:61ff:fe9d:f156", - "fe80:0:0:0:204:61ff:254.157.241.86", - "fe80::204:61ff:254.157.241.86", - "fe80::1", - "2001:0db8:85a3:0000:0000:8a2e:0370:7334", - "2001:db8:85a3:0:0:8a2e:370:7334", - "2001:db8:85a3::8a2e:370:7334", - "2001:0db8:0000:0000:0000:0000:1428:57ab", - "2001:0db8:0000:0000:0000::1428:57ab", - "2001:0db8:0:0:0:0:1428:57ab", - "2001:0db8:0:0::1428:57ab", - "2001:0db8::1428:57ab", - "2001:db8::1428:57ab", - "::ffff:12.34.56.78", - "::ffff:0c22:384e", - "2001:0db8:1234:0000:0000:0000:0000:0000", - "2001:0db8:1234:ffff:ffff:ffff:ffff:ffff", - "2001:db8:a::123", - "::ffff:192.0.2.128", - "::ffff:c000:280", - "a:b:c:d:e:f:f1:f2", - "a:b:c::d:e:f:f1", - "a:b:c::d:e:f", - "a:b:c::d:e", - "a:b:c::d", - "::a", - "::a:b:c", - "::a:b:c:d:e:f:f1", - "a::", - "a:b:c::", - "a:b:c:d:e:f:f1::", - "a:bb:ccc:dddd:000e:00f:0f::", - "0:a:0:a:0:0:0:a", - "0:a:0:0:a:0:0:a", - "2001:db8:1:1:1:1:0:0", - "2001:db8:1:1:1:0:0:0", - "2001:db8:1:1:0:0:0:0", - "2001:db8:1:0:0:0:0:0", - "2001:db8:0:0:0:0:0:0", - "2001:0:0:0:0:0:0:0", - "A:BB:CCC:DDDD:000E:00F:0F::", - "0:0:0:0:0:0:0:a", - "0:0:0:0:a:0:0:0", - "0:0:0:a:0:0:0:0", - "a:0:0:a:0:0:a:a", - "a:0:0:a:0:0:0:a", - "a:0:0:0:a:0:0:a", - "a:0:0:0:a:0:0:0", - "a:0:0:0:0:0:0:0", - "fe80::7:8%eth0", - "fe80::7:8%1", -]; - -const v6not = [ - "", - "1:", - ":1", - "11:36:12", - "02001:0000:1234:0000:0000:C1C0:ABCD:0876", - "2001:0000:1234:0000:00001:C1C0:ABCD:0876", - "2001:0000:1234: 0000:0000:C1C0:ABCD:0876", - "2001:1:1:1:1:1:255Z255X255Y255", - "3ffe:0b00:0000:0001:0000:0000:000a", - "FF02:0000:0000:0000:0000:0000:0000:0000:0001", - "3ffe:b00::1::a", - "::1111:2222:3333:4444:5555:6666::", - "1:2:3::4:5::7:8", - "12345::6:7:8", - "1::5:400.2.3.4", - "1::5:260.2.3.4", - "1::5:256.2.3.4", - "1::5:1.256.3.4", - "1::5:1.2.256.4", - "1::5:1.2.3.256", - "1::5:300.2.3.4", - "1::5:1.300.3.4", - "1::5:1.2.300.4", - "1::5:1.2.3.300", - "1::5:900.2.3.4", - "1::5:1.900.3.4", - "1::5:1.2.900.4", - "1::5:1.2.3.900", - "1::5:300.300.300.300", - "1::5:3000.30.30.30", - "1::400.2.3.4", - "1::260.2.3.4", - "1::256.2.3.4", - "1::1.256.3.4", - "1::1.2.256.4", - "1::1.2.3.256", - "1::300.2.3.4", - "1::1.300.3.4", - "1::1.2.300.4", - "1::1.2.3.300", - "1::900.2.3.4", - "1::1.900.3.4", - "1::1.2.900.4", - "1::1.2.3.900", - "1::300.300.300.300", - "1::3000.30.30.30", - "::400.2.3.4", - "::260.2.3.4", - "::256.2.3.4", - "::1.256.3.4", - "::1.2.256.4", - "::1.2.3.256", - "::300.2.3.4", - "::1.300.3.4", - "::1.2.300.4", - "::1.2.3.300", - "::900.2.3.4", - "::1.900.3.4", - "::1.2.900.4", - "::1.2.3.900", - "::300.300.300.300", - "::3000.30.30.30", - "2001:DB8:0:0:8:800:200C:417A:221", - "FF01::101::2", - "1111:2222:3333:4444::5555:", - "1111:2222:3333::5555:", - "1111:2222::5555:", - "1111::5555:", - "::5555:", - ":::", - "1111:", - ":", - ":1111:2222:3333:4444::5555", - ":1111:2222:3333::5555", - ":1111:2222::5555", - ":1111::5555", - ":::5555", - "1.2.3.4:1111:2222:3333:4444::5555", - "1.2.3.4:1111:2222:3333::5555", - "1.2.3.4:1111:2222::5555", - "1.2.3.4:1111::5555", - "1.2.3.4::5555", - "1.2.3.4::", - "fe80:0000:0000:0000:0204:61ff:254.157.241.086", - "123", - "ldkfj", - "2001::FFD3::57ab", - "2001:db8:85a3::8a2e:37023:7334", - "2001:db8:85a3::8a2e:370k:7334", - "1:2:3:4:5:6:7:8:9", - "1::2::3", - "1:::3:4:5", - "1:2:3::4:5:6:7:8:9", - "::ffff:2.3.4", - "::ffff:257.1.2.3", - "::ffff:12345678901234567890.1.26", - "2001:0000:1234:0000:0000:C1C0:ABCD:0876 0", - "02001:0000:1234:0000:0000:C1C0:ABCD:0876", -]; - -describe("net.isIPv6", () => { - test("valid IPv6 addresses", () => { - for (const ip of v6) { - expect(net.isIPv6(ip)).toBe(true); - } - }); - - test("invalid IPv6 addresses", () => { - for (const ip of v6not) { - expect(net.isIPv6(ip)).toBe(false); - } - }); -}); - -//<#END_FILE: test-net-isipv6.js diff --git a/test/js/node/test/parallel/net-keepalive.test.js b/test/js/node/test/parallel/net-keepalive.test.js deleted file mode 100644 index 2b875ceb20..0000000000 --- a/test/js/node/test/parallel/net-keepalive.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-net-keepalive.js -//#SHA1: 822f2eb57a17abc64e2664803a4ac69430e5b035 -//----------------- -"use strict"; - -const net = require("net"); - -describe("net keepalive", () => { - test("should maintain connection", async () => { - let serverConnection; - let clientConnection; - - const { promise, resolve, reject } = Promise.withResolvers(); - function done(err) { - clientConnection.destroy(); - echoServer.close(); - if (err) reject(err); - else resolve(); - } - - const echoServer = net.createServer(connection => { - serverConnection = connection; - connection.setTimeout(0); - try { - expect(connection.setKeepAlive).toBeDefined(); - } catch (err) { - done(err); - return; - } - connection.setKeepAlive(true, 50); - connection.on("end", () => { - connection.end(); - }); - }); - - echoServer.listen(0, () => { - clientConnection = net.createConnection(echoServer.address().port, "127.0.0.1"); - clientConnection.setTimeout(0); - clientConnection.on("connect", () => { - setTimeout(() => { - try { - expect(serverConnection.readyState).toBe("open"); - expect(clientConnection.readyState).toBe("open"); - done(); - } catch (err) { - done(err); - } - }, 100); - }); - }); - - await promise; - }); -}); - -//<#END_FILE: test-net-keepalive.js diff --git a/test/js/node/test/parallel/net-large-string.test.js b/test/js/node/test/parallel/net-large-string.test.js deleted file mode 100644 index e69dd073d4..0000000000 --- a/test/js/node/test/parallel/net-large-string.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-net-large-string.js -//#SHA1: d823932009345f5d651ca02b7ddbba67057a423b -//----------------- -"use strict"; -const net = require("net"); - -const kPoolSize = 40 * 1024; -const data = "あ".repeat(kPoolSize); -const encoding = "UTF-8"; - -test("net large string", done => { - const server = net.createServer(socket => { - let receivedSize = 0; - socket.setEncoding(encoding); - socket.on("data", chunk => { - receivedSize += chunk.length; - }); - socket.on("end", () => { - expect(receivedSize).toBe(kPoolSize); - socket.end(); - }); - }); - - server.listen(0, () => { - // we connect to the server using 127.0.0.1 to avoid happy eyeballs - const client = net.createConnection(server.address().port, "127.0.0.1"); - client.on("end", () => { - server.close(); - done(); - }); - client.write(data, encoding); - client.end(); - }); -}); - -//<#END_FILE: test-net-large-string.js diff --git a/test/js/node/test/parallel/net-listen-after-destroying-stdin.test.js b/test/js/node/test/parallel/net-listen-after-destroying-stdin.test.js deleted file mode 100644 index 3084568eda..0000000000 --- a/test/js/node/test/parallel/net-listen-after-destroying-stdin.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-net-listen-after-destroying-stdin.js -//#SHA1: 933b6b80e7babac7189e16f85967b75836efea74 -//----------------- -"use strict"; - -// Just test that destroying stdin doesn't mess up listening on a server. -// This is a regression test for -// https://github.com/nodejs/node-v0.x-archive/issues/746. - -const net = require("net"); - -test("destroying stdin does not affect server listening", done => { - process.stdin.destroy(); - - const server = net.createServer(socket => { - console.log("accepted..."); - socket.end(() => { - console.log("finished..."); - expect(true).toBe(true); // Ensure this callback is called - }); - server.close(() => { - console.log("closed"); - expect(true).toBe(true); // Ensure this callback is called - done(); - }); - }); - - server.listen(0, () => { - console.log("listening..."); - expect(server.address().port).toBeGreaterThan(0); - - net.createConnection(server.address().port); - }); -}); - -//<#END_FILE: test-net-listen-after-destroying-stdin.js diff --git a/test/js/node/test/parallel/net-listen-close-server.test.js b/test/js/node/test/parallel/net-listen-close-server.test.js deleted file mode 100644 index 519ae0a2cb..0000000000 --- a/test/js/node/test/parallel/net-listen-close-server.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-net-listen-close-server.js -//#SHA1: e39989e43d691f9cc91a02156112ba681366601b -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -test("server listen and close without calling callbacks", () => { - const server = net.createServer(() => {}); - const listenSpy = jest.fn(); - const errorSpy = jest.fn(); - - server.listen(0, listenSpy); - server.on("error", errorSpy); - server.close(); - - expect(listenSpy).not.toHaveBeenCalled(); - expect(errorSpy).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-net-listen-close-server.js diff --git a/test/js/node/test/parallel/net-listen-exclusive-random-ports.test.js b/test/js/node/test/parallel/net-listen-exclusive-random-ports.test.js deleted file mode 100644 index 01f8e25506..0000000000 --- a/test/js/node/test/parallel/net-listen-exclusive-random-ports.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-net-listen-exclusive-random-ports.js -//#SHA1: d125e8ff5fd688b5638099581c08c78d91460c59 -//----------------- -'use strict'; - -const net = require('net'); - -describe('Net listen exclusive random ports', () => { - test('should listen on different ports for different servers', async () => { - const createServer = () => { - return new Promise((resolve, reject) => { - const server = net.createServer(() => {}); - server.listen({ - port: 0, - exclusive: true - }, () => { - const port = server.address().port; - resolve({ server, port }); - }); - server.on('error', reject); - }); - }; - - const { server: server1, port: port1 } = await createServer(); - const { server: server2, port: port2 } = await createServer(); - - expect(port1).toBe(port1 | 0); - expect(port2).toBe(port2 | 0); - expect(port1).not.toBe(port2); - - server1.close(); - server2.close(); - }); -}); - -//<#END_FILE: test-net-listen-exclusive-random-ports.js diff --git a/test/js/node/test/parallel/net-listen-handle-in-cluster-2.test.js b/test/js/node/test/parallel/net-listen-handle-in-cluster-2.test.js deleted file mode 100644 index ac5017b087..0000000000 --- a/test/js/node/test/parallel/net-listen-handle-in-cluster-2.test.js +++ /dev/null @@ -1,10 +0,0 @@ -//#FILE: test-net-listen-handle-in-cluster-2.js -//#SHA1: 1902a830aa4f12e7049fc0383e9a919b46aa79dc -//----------------- -'use strict'; - -test.skip('net.listen with handle in cluster (worker)', () => { - console.log('This test is skipped because it relies on Node.js internals and cluster functionality that cannot be accurately replicated in a Jest environment.'); -}); - -//<#END_FILE: test-net-listen-handle-in-cluster-2.js diff --git a/test/js/node/test/parallel/net-listening.test.js b/test/js/node/test/parallel/net-listening.test.js deleted file mode 100644 index ea0b14ab53..0000000000 --- a/test/js/node/test/parallel/net-listening.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-net-listening.js -//#SHA1: 3c2824f7bd90fec69702e096faba0b4f07353a20 -//----------------- -"use strict"; -const net = require("net"); - -test("Server listening state", done => { - const server = net.createServer(); - - expect(server.listening).toBe(false); - - server.listen(0, () => { - expect(server.listening).toBe(true); - - server.close(() => { - expect(server.listening).toBe(false); - done(); - }); - }); -}); - -//<#END_FILE: test-net-listening.js diff --git a/test/js/node/test/parallel/net-local-address-port.test.js b/test/js/node/test/parallel/net-local-address-port.test.js deleted file mode 100644 index a41661e52b..0000000000 --- a/test/js/node/test/parallel/net-local-address-port.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-net-local-address-port.js -//#SHA1: 9fdb2786eb87ca722138e027be5ee72f04b9909c -//----------------- -"use strict"; -const net = require("net"); - -const localhostIPv4 = "127.0.0.1"; - -describe("Net local address and port", () => { - let server; - let client; - - afterEach(() => { - if (client) { - client.destroy(); - } - if (server && server.listening) { - server.close(); - } - }); - - test("should have correct local address, port, and family", done => { - server = net.createServer(socket => { - expect(socket.localAddress).toBe(localhostIPv4); - expect(socket.localPort).toBe(server.address().port); - expect(socket.localFamily).toBe(server.address().family); - - socket.resume(); - }); - - server.listen(0, localhostIPv4, () => { - client = net.createConnection(server.address().port, localhostIPv4); - client.on("connect", () => { - client.end(); - // We'll end the test here instead of waiting for the server to close - done(); - }); - }); - }); -}); - -//<#END_FILE: test-net-local-address-port.js diff --git a/test/js/node/test/parallel/net-persistent-keepalive.test.js b/test/js/node/test/parallel/net-persistent-keepalive.test.js deleted file mode 100644 index 86b5fbc054..0000000000 --- a/test/js/node/test/parallel/net-persistent-keepalive.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-net-persistent-keepalive.js -//#SHA1: 1428cedddea85130590caec6c04b1939c1f614d4 -//----------------- -"use strict"; -const net = require("net"); - -let serverConnection; -let clientConnection; -let echoServer; -let serverPort; - -beforeAll((done) => { - echoServer = net.createServer((connection) => { - serverConnection = connection; - connection.setTimeout(0); - expect(typeof connection.setKeepAlive).toBe("function"); - connection.on("end", () => { - connection.end(); - }); - }); - - echoServer.listen(0, () => { - serverPort = echoServer.address().port; - done(); - }); -}); - -afterAll((done) => { - if (echoServer) { - echoServer.close(done); - } else { - done(); - } -}); - -test("persistent keepalive", (done) => { - clientConnection = new net.Socket(); - // Send a keepalive packet after 400 ms and make sure it persists - const s = clientConnection.setKeepAlive(true, 400); - expect(s).toBeInstanceOf(net.Socket); - - clientConnection.connect(serverPort, "127.0.0.1"); - clientConnection.setTimeout(0); - - setTimeout(() => { - // Make sure both connections are still open - expect(serverConnection.readyState).toBe("open"); - expect(clientConnection.readyState).toBe("open"); - - serverConnection.end(); - clientConnection.end(); - done(); - }, 600); -}); - -//<#END_FILE: test-net-persistent-keepalive.js diff --git a/test/js/node/test/parallel/net-persistent-nodelay.test.js b/test/js/node/test/parallel/net-persistent-nodelay.test.js deleted file mode 100644 index 87e9db3e82..0000000000 --- a/test/js/node/test/parallel/net-persistent-nodelay.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-net-persistent-nodelay.js -//#SHA1: 48ab228711df09f547aa2ef4abaac2735ef4625b -//----------------- -"use strict"; - -const net = require("net"); - -describe("TCP setNoDelay persistence", () => { - let echoServer; - let originalSetNoDelay; - let callCount; - - beforeAll(() => { - echoServer = net.createServer(connection => { - connection.end(); - }); - }); - - afterAll(() => { - echoServer.close(); - }); - - beforeEach(() => { - callCount = 0; - originalSetNoDelay = net.Socket.prototype.setNoDelay; - net.Socket.prototype.setNoDelay = function (enable) { - const result = originalSetNoDelay.call(this, enable); - callCount++; - return result; - }; - }); - - afterEach(() => { - net.Socket.prototype.setNoDelay = originalSetNoDelay; - }); - - test("setNoDelay is called once when connecting", done => { - echoServer.listen(0, () => { - const sock1 = new net.Socket(); - - // setNoDelay before the handle is created - const s = sock1.setNoDelay(); - expect(s).toBeInstanceOf(net.Socket); - - sock1.connect(echoServer.address().port); - sock1.on("end", () => { - expect(callCount).toBe(1); - done(); - }); - }); - }); -}); - -//<#END_FILE: test-net-persistent-nodelay.js diff --git a/test/js/node/test/parallel/net-persistent-ref-unref.test.js b/test/js/node/test/parallel/net-persistent-ref-unref.test.js deleted file mode 100644 index 58c2a799bc..0000000000 --- a/test/js/node/test/parallel/net-persistent-ref-unref.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-net-persistent-ref-unref.js -//#SHA1: 630ad893713b3c13100743b5e5ae46453adc523e -//----------------- -'use strict'; -const net = require('net'); - -// Mock TCPWrap -const TCPWrap = { - prototype: { - ref: jest.fn(), - unref: jest.fn(), - }, -}; - -let refCount = 0; - -describe('Net persistent ref/unref', () => { - let echoServer; - - beforeAll((done) => { - echoServer = net.createServer((conn) => { - conn.end(); - }); - - TCPWrap.prototype.ref = jest.fn().mockImplementation(function() { - TCPWrap.prototype.ref.mockOriginal.call(this); - refCount++; - expect(refCount).toBe(0); - }); - - TCPWrap.prototype.unref = jest.fn().mockImplementation(function() { - TCPWrap.prototype.unref.mockOriginal.call(this); - refCount--; - expect(refCount).toBe(-1); - }); - - echoServer.listen(0, done); - }); - - afterAll((done) => { - echoServer.close(done); - }); - - test('should maintain correct ref count', (done) => { - const sock = new net.Socket(); - sock.unref(); - sock.ref(); - sock.connect(echoServer.address().port); - sock.on('end', () => { - expect(refCount).toBe(0); - done(); - }); - }); -}); - -//<#END_FILE: test-net-persistent-ref-unref.js diff --git a/test/js/node/test/parallel/net-remote-address.test.js b/test/js/node/test/parallel/net-remote-address.test.js deleted file mode 100644 index ea145b43d3..0000000000 --- a/test/js/node/test/parallel/net-remote-address.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-net-remote-address.js -//#SHA1: a4c7e915b2d6465060b3d283c3cc3906ad629531 -//----------------- -"use strict"; - -const net = require("net"); - -test("remote address behavior", done => { - const server = net.createServer(); - - server.listen(() => { - const socket = net.connect({ port: server.address().port }); - - expect(socket.connecting).toBe(true); - expect(socket.remoteAddress).toBeUndefined(); - - socket.on("connect", () => { - expect(socket.remoteAddress).toBeDefined(); - socket.end(); - }); - - socket.on("end", () => { - server.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-net-remote-address.js diff --git a/test/js/node/test/parallel/net-server-close-before-calling-lookup-callback.test.js b/test/js/node/test/parallel/net-server-close-before-calling-lookup-callback.test.js deleted file mode 100644 index dd3c3b91d0..0000000000 --- a/test/js/node/test/parallel/net-server-close-before-calling-lookup-callback.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-net-server-close-before-calling-lookup-callback.js -//#SHA1: 42a269f691f19c53994f939bacf7e0451f065107 -//----------------- -"use strict"; - -const net = require("net"); - -test("server closes before calling lookup callback", () => { - // Process should exit because it does not create a real TCP server. - // Pass localhost to ensure create TCP handle asynchronously because It causes DNS resolution. - const server = net.createServer(); - - expect(() => { - server.listen(0, "localhost", () => { - throw new Error("This callback should not be called"); - }); - server.close(); - }).not.toThrow(); -}); - -//<#END_FILE: test-net-server-close-before-calling-lookup-callback.js diff --git a/test/js/node/test/parallel/net-server-close-before-ipc-response.test.js b/test/js/node/test/parallel/net-server-close-before-ipc-response.test.js deleted file mode 100644 index 95bba271d2..0000000000 --- a/test/js/node/test/parallel/net-server-close-before-ipc-response.test.js +++ /dev/null @@ -1,16 +0,0 @@ -//#FILE: test-net-server-close-before-ipc-response.js -//#SHA1: 540c9049f49219e9dbcbbd053be54cc2cbd332a0 -//----------------- -'use strict'; - -const net = require('net'); - -describe('Net server close before IPC response', () => { - test.skip('Process should exit', () => { - console.log('This test is skipped because it requires a complex cluster and IPC setup that is difficult to simulate in a Jest environment.'); - console.log('The original test verified that the process exits correctly when a server is closed before an IPC response is received.'); - console.log('To properly test this, we would need to set up a real cluster environment or use a more sophisticated mocking approach.'); - }); -}); - -//<#END_FILE: test-net-server-close-before-ipc-response.js diff --git a/test/js/node/test/parallel/net-server-listen-remove-callback.test.js b/test/js/node/test/parallel/net-server-listen-remove-callback.test.js deleted file mode 100644 index 0aaff47a52..0000000000 --- a/test/js/node/test/parallel/net-server-listen-remove-callback.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-net-server-listen-remove-callback.js -//#SHA1: 031a06bd108815e34b9ebbc3019044daeb8cf8c8 -//----------------- -'use strict'; - -const net = require('net'); - -let server; - -beforeEach(() => { - server = net.createServer(); -}); - -afterEach((done) => { - if (server.listening) { - server.close(done); - } else { - done(); - } -}); - -test('Server should only fire listen callback once', (done) => { - server.on('close', () => { - const listeners = server.listeners('listening'); - console.log('Closed, listeners:', listeners.length); - expect(listeners.length).toBe(0); - }); - - server.listen(0, () => { - server.close(); - }); - - server.once('close', () => { - server.listen(0, () => { - server.close(done); - }); - }); -}); - -//<#END_FILE: test-net-server-listen-remove-callback.js diff --git a/test/js/node/test/parallel/net-server-try-ports.test.js b/test/js/node/test/parallel/net-server-try-ports.test.js deleted file mode 100644 index 67668a69bc..0000000000 --- a/test/js/node/test/parallel/net-server-try-ports.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-net-server-try-ports.js -//#SHA1: 8f3f2a7c0fcc9b76f2aaf8ac2bb00c81e6a752fa -//----------------- -"use strict"; - -const net = require("net"); - -test("Server should handle EADDRINUSE and bind to another port", done => { - const server1 = new net.Server(); - const server2 = new net.Server(); - - server2.on("error", e => { - expect(e.code).toBe("EADDRINUSE"); - - server2.listen(0, () => { - server1.close(); - server2.close(); - done(); - }); - }); - - server1.listen(0, () => { - // This should make server2 emit EADDRINUSE - server2.listen(server1.address().port); - }); -}); - -//<#END_FILE: test-net-server-try-ports.js diff --git a/test/js/node/test/parallel/net-server-unref-persistent.test.js b/test/js/node/test/parallel/net-server-unref-persistent.test.js deleted file mode 100644 index add3449f2b..0000000000 --- a/test/js/node/test/parallel/net-server-unref-persistent.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-net-server-unref-persistent.js -//#SHA1: 4b518c58827ac05dd5c3746c8a0811181184b945 -//----------------- -'use strict'; -const net = require('net'); - -test.skip('net server unref should be persistent', () => { - // This test is skipped in Jest because it relies on Node.js-specific event loop behavior - // that can't be accurately simulated in a Jest environment. - // The original test should be kept in Node.js's test suite. -}); - -//<#END_FILE: test-net-server-unref-persistent.js diff --git a/test/js/node/test/parallel/net-server-unref.test.js b/test/js/node/test/parallel/net-server-unref.test.js deleted file mode 100644 index 2067d6cbb9..0000000000 --- a/test/js/node/test/parallel/net-server-unref.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-net-server-unref.js -//#SHA1: bb2f989bf01182d804d6a8a0d0f33950f357c617 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -test("net server unref", () => { - const s = net.createServer(); - s.listen(0); - s.unref(); - - const mockCallback = jest.fn(); - setTimeout(mockCallback, 1000).unref(); - - expect(mockCallback).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-net-server-unref.js diff --git a/test/js/node/test/parallel/net-settimeout.test.js b/test/js/node/test/parallel/net-settimeout.test.js deleted file mode 100644 index b766196ac8..0000000000 --- a/test/js/node/test/parallel/net-settimeout.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-net-settimeout.js -//#SHA1: 24fde10dfba0d555d2a61853374866b370e40edf -//----------------- -'use strict'; - -const net = require('net'); - -const T = 100; - -let server; -let serverPort; - -beforeAll((done) => { - server = net.createServer((c) => { - c.write('hello'); - }); - - server.listen(0, () => { - serverPort = server.address().port; - done(); - }); -}); - -afterAll((done) => { - server.close(done); -}); - -test('setTimeout and immediate clearTimeout', (done) => { - const socket = net.createConnection(serverPort, 'localhost'); - - const timeoutCallback = jest.fn(); - const s = socket.setTimeout(T, timeoutCallback); - expect(s).toBeInstanceOf(net.Socket); - - socket.on('data', () => { - setTimeout(() => { - socket.destroy(); - expect(timeoutCallback).not.toHaveBeenCalled(); - done(); - }, T * 2); - }); - - socket.setTimeout(0); -}); - -//<#END_FILE: test-net-settimeout.js diff --git a/test/js/node/test/parallel/net-socket-close-after-end.test.js b/test/js/node/test/parallel/net-socket-close-after-end.test.js deleted file mode 100644 index d0d744a6e3..0000000000 --- a/test/js/node/test/parallel/net-socket-close-after-end.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-net-socket-close-after-end.js -//#SHA1: d3abfad3599a4245fb35f5589c55bb56a43ca3f7 -//----------------- -"use strict"; - -const net = require("net"); - -test('socket emits "end" before "close"', done => { - const server = net.createServer(); - - server.on("connection", socket => { - let endEmitted = false; - - socket.once("readable", () => { - setTimeout(() => { - socket.read(); - }, 100); - }); - - socket.on("end", () => { - endEmitted = true; - }); - - socket.on("close", () => { - expect(endEmitted).toBe(true); - server.close(); - done(); - }); - - socket.end("foo"); - }); - - server.listen(() => { - const socket = net.createConnection(server.address().port, () => { - socket.end("foo"); - }); - }); -}); - -//<#END_FILE: test-net-socket-close-after-end.js diff --git a/test/js/node/test/parallel/net-socket-connect-without-cb.test.js b/test/js/node/test/parallel/net-socket-connect-without-cb.test.js deleted file mode 100644 index 3f47475afc..0000000000 --- a/test/js/node/test/parallel/net-socket-connect-without-cb.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-net-socket-connect-without-cb.js -//#SHA1: 2441c4dfe4351f2e9a02cd08df36e4703096864a -//----------------- -"use strict"; - -const net = require("net"); - -// This test ensures that socket.connect can be called without callback -// which is optional. - -test("socket.connect without callback", done => { - const server = net - .createServer(conn => { - conn.end(); - server.close(); - }) - .listen(0, () => { - const client = new net.Socket(); - - client.on("connect", () => { - client.end(); - done(); - }); - - const address = server.address(); - if (process.version.startsWith("v") && !process.versions.bun && address.family === "IPv6") { - // Necessary to pass CI running inside containers. - client.connect(address.port); - } else { - client.connect(address); - } - }); - - expect(server).toBeDefined(); -}); - -//<#END_FILE: test-net-socket-connect-without-cb.js diff --git a/test/js/node/test/parallel/net-socket-destroy-twice.test.js b/test/js/node/test/parallel/net-socket-destroy-twice.test.js deleted file mode 100644 index cc8a7ecaf2..0000000000 --- a/test/js/node/test/parallel/net-socket-destroy-twice.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-net-socket-destroy-twice.js -//#SHA1: b9066749198a610e24f0b75c017f00abb3c70bfc -//----------------- -"use strict"; - -const net = require("net"); - -describe("Net socket destroy twice", () => { - let server; - let port; - - beforeAll((done) => { - server = net.createServer(); - server.listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("should handle destroying a socket twice", (done) => { - const conn = net.createConnection(port, "127.0.0.1"); - - let errorCalled = 0; - conn.on("error", () => { - errorCalled++; - conn.destroy(); - }); - - conn.on("close", () => { - expect(errorCalled).toBe(1); - done(); - }); - - // Trigger an error by closing the server - server.close(); - }); -}); - -//<#END_FILE: test-net-socket-destroy-twice.js diff --git a/test/js/node/test/parallel/net-socket-end-before-connect.test.js b/test/js/node/test/parallel/net-socket-end-before-connect.test.js deleted file mode 100644 index d27dfd7d46..0000000000 --- a/test/js/node/test/parallel/net-socket-end-before-connect.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-net-socket-end-before-connect.js -//#SHA1: e09a7492b07dfa5467171563408395f653e9b032 -//----------------- -'use strict'; - -const net = require('net'); - -test('Socket ends before connect', (done) => { - const server = net.createServer(); - - server.listen(() => { - const socket = net.createConnection(server.address().port, "127.0.0.1"); - - const closeHandler = function() { - server.close(); - done(); - } - socket.on('close', closeHandler); - socket.end(); - }); -}); - -//<#END_FILE: test-net-socket-end-before-connect.js diff --git a/test/js/node/test/parallel/net-socket-ready-without-cb.test.js b/test/js/node/test/parallel/net-socket-ready-without-cb.test.js deleted file mode 100644 index d22eac4d22..0000000000 --- a/test/js/node/test/parallel/net-socket-ready-without-cb.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-net-socket-ready-without-cb.js -//#SHA1: 2f6be9472163372bcd602f547bd709b27a2baad6 -//----------------- -'use strict'; - -const net = require('net'); - -test('socket.connect can be called without callback', (done) => { - const server = net.createServer((conn) => { - conn.end(); - server.close(); - }); - - server.listen(0, 'localhost', () => { - const client = new net.Socket(); - - client.on('ready', () => { - client.end(); - done(); - }); - - client.connect(server.address()); - }); -}); - -//<#END_FILE: test-net-socket-ready-without-cb.js diff --git a/test/js/node/test/parallel/net-socket-reset-twice.test.js b/test/js/node/test/parallel/net-socket-reset-twice.test.js deleted file mode 100644 index 10adfdc49d..0000000000 --- a/test/js/node/test/parallel/net-socket-reset-twice.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-net-socket-reset-twice.js -//#SHA1: 70cb2037a6385ada696f8b9f8fa66a0b111275c4 -//----------------- -"use strict"; -const net = require("net"); - -let server; -let port; - -beforeAll((done) => { - server = net.createServer(); - server.listen(0, () => { - port = server.address().port; - done(); - }); -}); - -afterAll(() => { - server.close(); -}); - -test("net socket reset twice", (done) => { - const conn = net.createConnection(port, "127.0.0.1"); - - const errorHandler = jest.fn(() => { - conn.resetAndDestroy(); - }); - - conn.on("error", errorHandler); - - const closeHandler = jest.fn(() => { - expect(errorHandler).toHaveBeenCalled(); - expect(closeHandler).toHaveBeenCalled(); - done(); - }); - - conn.on("close", closeHandler); - - // Trigger the error event - server.close(); -}); - -//<#END_FILE: test-net-socket-reset-twice.js diff --git a/test/js/node/test/parallel/net-socket-timeout-unref.test.js b/test/js/node/test/parallel/net-socket-timeout-unref.test.js deleted file mode 100644 index 5e05c5cf1c..0000000000 --- a/test/js/node/test/parallel/net-socket-timeout-unref.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-net-socket-timeout-unref.js -//#SHA1: 1583fd33473989bba11fead2493c70a79d9ff48e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// Test that unref'ed sockets with timeouts do not prevent exit. - -const net = require("net"); - -test("unref'ed sockets with timeouts do not prevent exit", () => { - const server = net.createServer(c => { - c.write("hello"); - c.unref(); - }); - server.listen(0); - server.unref(); - - let connections = 0; - const sockets = []; - const delays = [8, 5, 3, 6, 2, 4]; - - delays.forEach(T => { - const socket = net.createConnection(server.address().port, "localhost"); - socket.on("connect", () => { - if (++connections === delays.length) { - sockets.forEach(s => { - s.socket.setTimeout(s.timeout, () => { - s.socket.destroy(); - throw new Error("socket timed out unexpectedly"); - }); - - s.socket.unref(); - }); - } - }); - - sockets.push({ socket: socket, timeout: T * 1000 }); - }); - - // We don't need to explicitly assert anything here. - // The test will pass if the process exits without throwing an error. -}); - -//<#END_FILE: test-net-socket-timeout-unref.js diff --git a/test/js/node/test/parallel/net-socket-write-error.test.js b/test/js/node/test/parallel/net-socket-write-error.test.js deleted file mode 100644 index 56b8b5634f..0000000000 --- a/test/js/node/test/parallel/net-socket-write-error.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-net-socket-write-error.js -//#SHA1: a69bb02fc98fc265ad23ff03e7ae16e9c984202d -//----------------- -"use strict"; - -const net = require("net"); - -describe("Net Socket Write Error", () => { - let server; - - beforeAll(done => { - server = net.createServer().listen(0, () => { - done(); - }); - }); - - afterAll(() => { - server.close(); - }); - - test("should throw TypeError when writing non-string/buffer", done => { - const client = net.createConnection(server.address().port, () => { - client.on("error", () => { - done.fail("Client should not emit error"); - }); - - expect(() => { - client.write(1337); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - - client.destroy(); - done(); - }); - - client.on("close", () => { - // This ensures the server closes after the client disconnects - server.close(); - }); - }); -}); - -//<#END_FILE: test-net-socket-write-error.js diff --git a/test/js/node/test/parallel/net-stream.test.js b/test/js/node/test/parallel/net-stream.test.js deleted file mode 100644 index bbfca1ad3e..0000000000 --- a/test/js/node/test/parallel/net-stream.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-net-stream.js -//#SHA1: 3682dee1fcd1fea4f59bbad200ab1476e0f49bda -//----------------- -"use strict"; - -const net = require("net"); -const { once } = require("events"); -const SIZE = 2e6; -const N = 10; -const buf = Buffer.alloc(SIZE, "a"); -//TODO: need to check how to handle error on close events properly -test.skip("net stream behavior", async () => { - let server; - try { - const { promise, resolve: done } = Promise.withResolvers(); - - server = net.createServer(socket => { - socket.setNoDelay(); - - let onErrorCalls = 0; - let onCloseCalls = 0; - socket - .on("error", () => { - onErrorCalls++; - socket.destroy(); - }) - .on("close", () => { - onCloseCalls++; - done({ onErrorCalls, onCloseCalls }); - }); - - for (let i = 0; i < N; ++i) { - socket.write(buf, () => {}); - } - - socket.end(); - }); - await once(server.listen(0), "listening"); - - const conn = net.connect(server.address().port, "127.0.0.1"); - const { promise: dataPromise, resolve: dataResolve } = Promise.withResolvers(); - conn.on("data", buf => { - dataResolve(conn.pause()); - setTimeout(() => { - conn.destroy(); - }, 20); - }); - expect(await dataPromise).toBe(conn); - - const { onCloseCalls, onErrorCalls } = await promise; - expect(onErrorCalls).toBeGreaterThan(0); - expect(onCloseCalls).toBeGreaterThan(0); - } finally { - server.close(); - } -}); - -//<#END_FILE: test-net-stream.js diff --git a/test/js/node/test/parallel/net-sync-cork.test.js b/test/js/node/test/parallel/net-sync-cork.test.js deleted file mode 100644 index bc0c4524fd..0000000000 --- a/test/js/node/test/parallel/net-sync-cork.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-net-sync-cork.js -//#SHA1: baf95df782bcb1c53ea0118e8e47e93d63cf4262 -//----------------- -"use strict"; - -const net = require("net"); - -const N = 100; -const buf = Buffer.alloc(2, "a"); - -let server; - -beforeAll(done => { - server = net.createServer(handle); - server.listen(0, done); -}); - -afterAll(() => { - server.close(); -}); - -test("net sync cork", done => { - const conn = net.connect(server.address().port); - - conn.on("connect", () => { - let res = true; - let i = 0; - for (; i < N && res; i++) { - conn.cork(); - conn.write(buf); - res = conn.write(buf); - conn.uncork(); - } - expect(i).toBe(N); - conn.end(); - }); - - conn.on("close", done); -}); - -function handle(socket) { - socket.resume(); - socket.on("error", () => { - throw new Error("Socket error should not occur"); - }); - socket.on("close", () => { - // This is called when the connection is closed - }); -} - -//<#END_FILE: test-net-sync-cork.js diff --git a/test/js/node/test/parallel/net-throttle.test.js b/test/js/node/test/parallel/net-throttle.test.js deleted file mode 100644 index b33fc01bea..0000000000 --- a/test/js/node/test/parallel/net-throttle.test.js +++ /dev/null @@ -1,78 +0,0 @@ -//#FILE: test-net-throttle.js -//#SHA1: 5c09d0b1c174ba1f88acae8d731c039ae7c3fc99 -//----------------- -"use strict"; - -const net = require("net"); -const { debuglog } = require("util"); - -const debug = debuglog("test"); - -let chars_recved = 0; -let npauses = 0; -let totalLength = 0; -let server; - -beforeAll(done => { - server = net.createServer(connection => { - const body = "C".repeat(1024); - let n = 1; - debug("starting write loop"); - while (connection.write(body)) { - n++; - } - debug("ended write loop"); - // Now that we're throttled, do some more writes to make sure the data isn't - // lost. - connection.write(body); - connection.write(body); - n += 2; - totalLength = n * body.length; - expect(connection.bufferSize).toBeGreaterThanOrEqual(0); - expect(connection.writableLength).toBeLessThanOrEqual(totalLength); - connection.end(); - }); - - server.listen(0, () => { - debug(`server started on port ${server.address().port}`); - done(); - }); -}); - -afterAll(done => { - server.close(done); -}); - -test("net throttle", done => { - const port = server.address().port; - let paused = false; - const client = net.createConnection(port, "127.0.0.1"); - client.setEncoding("ascii"); - - client.on("data", d => { - chars_recved += d.length; - debug(`got ${chars_recved}`); - if (!paused) { - client.pause(); - npauses += 1; - paused = true; - debug("pause"); - const x = chars_recved; - setTimeout(() => { - expect(chars_recved).toBe(x); - client.resume(); - debug("resume"); - paused = false; - }, 100); - } - }); - - client.on("end", () => { - client.end(); - expect(chars_recved).toBe(totalLength); - expect(npauses).toBeGreaterThan(1); - done(); - }); -}); - -//<#END_FILE: test-net-throttle.js diff --git a/test/js/node/test/parallel/net-writable.test.js b/test/js/node/test/parallel/net-writable.test.js deleted file mode 100644 index 60f10d743b..0000000000 --- a/test/js/node/test/parallel/net-writable.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-net-writable.js -//#SHA1: dfbbbc883e83311b16b93fc9e06d214552cb6448 -//----------------- -"use strict"; - -const net = require("net"); - -test("net writable after end event", done => { - const server = net.createServer(s => { - server.close(); - s.end(); - }); - - server.listen(0, "127.0.0.1", () => { - const socket = net.connect(server.address().port, "127.0.0.1"); - socket.on("end", () => { - expect(socket.writable).toBe(true); - socket.write("hello world"); - done(); - }); - }); - - expect.assertions(1); -}); - -//<#END_FILE: test-net-writable.js diff --git a/test/js/node/test/parallel/net-write-after-close.test.js b/test/js/node/test/parallel/net-write-after-close.test.js deleted file mode 100644 index 8aacf621b9..0000000000 --- a/test/js/node/test/parallel/net-write-after-close.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-net-write-after-close.js -//#SHA1: fe97d63608f4e6651247e83071c81800a6de2ee6 -//----------------- -"use strict"; - -const net = require("net"); - -test("write after close", async () => { - const { promise, resolve } = Promise.withResolvers(); - const { promise: writePromise, resolve: writeResolve } = Promise.withResolvers(); - let server; - try { - server = net.createServer(socket => { - socket.on("end", () => resolve(socket)); - socket.resume(); - socket.on("error", error => { - throw new Error("Server socket should not emit error"); - }); - }); - - server.listen(0, () => { - const client = net.connect(server.address().port, "127.0.0.1", () => { - client.end(); - }); - }); - (await promise).write("test", writeResolve); - const err = await writePromise; - expect(err).toBeTruthy(); - } finally { - server.close(); - } -}); - -//<#END_FILE: test-net-write-after-close.js diff --git a/test/js/node/test/parallel/net-write-after-end-nt.test.js b/test/js/node/test/parallel/net-write-after-end-nt.test.js deleted file mode 100644 index b3f2e81936..0000000000 --- a/test/js/node/test/parallel/net-write-after-end-nt.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-net-write-after-end-nt.js -//#SHA1: 086a5699d5eff4953af4e9f19757b8489e915579 -//----------------- -"use strict"; -const net = require("net"); - -describe("net.Socket.write() after end", () => { - let server; - let port; - - beforeAll(done => { - server = net - .createServer(socket => { - socket.end(); - }) - .listen(0, () => { - port = server.address().port; - done(); - }); - }); - - afterAll(done => { - server.close(done); - }); - - test("error is emitted in the next tick", done => { - const client = net.connect(port, "127.0.0.1", () => { - let hasError = false; - - client.on("error", err => { - hasError = true; - expect(err).toEqual( - expect.objectContaining({ - code: "EPIPE", - message: "This socket has been ended by the other party", - name: "Error", - }), - ); - done(); - }); - - client.on("end", () => { - const ret = client.write("hello"); - expect(ret).toBe(false); - expect(hasError).toBe(false); - process.nextTick(() => { - expect(hasError).toBe(true); - }); - }); - - client.end(); - }); - }); -}); - -//<#END_FILE: test-net-write-after-end-nt.js diff --git a/test/js/node/test/parallel/net-write-arguments.test.js b/test/js/node/test/parallel/net-write-arguments.test.js deleted file mode 100644 index 347230744d..0000000000 --- a/test/js/node/test/parallel/net-write-arguments.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-net-write-arguments.js -//#SHA1: 2a9ed0086e2675e0e31ef15c1e86b15c47c10c5b -//----------------- -"use strict"; -const net = require("net"); - -test("net.Stream write arguments", () => { - const socket = net.Stream({ highWaterMark: 0 }); - - // Make sure that anything besides a buffer or a string throws. - socket.on("error", jest.fn()); - expect(() => { - socket.write(null); - }).toThrow( - expect.objectContaining({ - code: "ERR_STREAM_NULL_VALUES", - name: "TypeError", - message: expect.any(String), - }), - ); - - [true, false, undefined, 1, 1.0, +Infinity, -Infinity, [], {}].forEach(value => { - const socket = net.Stream({ highWaterMark: 0 }); - // We need to check the callback since 'error' will only - // be emitted once per instance. - expect(() => { - socket.write(value); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-net-write-arguments.js diff --git a/test/js/node/test/parallel/net-write-cb-on-destroy-before-connect.test.js b/test/js/node/test/parallel/net-write-cb-on-destroy-before-connect.test.js deleted file mode 100644 index 5a6b245ff6..0000000000 --- a/test/js/node/test/parallel/net-write-cb-on-destroy-before-connect.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-net-write-cb-on-destroy-before-connect.js -//#SHA1: 49dc0c1780402ca7bc3648f52f821b0ba89eff32 -//----------------- -'use strict'; - -const net = require('net'); - -let server; - -beforeAll((done) => { - server = net.createServer(); - server.listen(0, () => { - done(); - }); -}); - -afterAll((done) => { - server.close(done); -}); - -test('write callback on destroy before connect', (done) => { - const socket = new net.Socket(); - - socket.on('connect', () => { - done('Socket should not connect'); - }); - - socket.connect({ - port: server.address().port, - }, "127.0.0.1"); - - expect(socket.connecting).toBe(true); - - socket.write('foo', (err) => { - expect(err).toEqual(expect.objectContaining({ - code: 'ERR_SOCKET_CLOSED_BEFORE_CONNECTION', - name: 'Error' - })); - done(); - }); - - socket.destroy(); -}); - -//<#END_FILE: test-net-write-cb-on-destroy-before-connect.js diff --git a/test/js/node/test/parallel/net-write-connect-write.test.js b/test/js/node/test/parallel/net-write-connect-write.test.js deleted file mode 100644 index bc959534eb..0000000000 --- a/test/js/node/test/parallel/net-write-connect-write.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-net-write-connect-write.js -//#SHA1: 8d6e9a30cc58bee105db15dc48c8a13c451629be -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const net = require("net"); - -test("net write connect write", async () => { - const server = net.createServer(socket => { - socket.pipe(socket); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const conn = net.connect(server.address().port); - let received = ""; - - conn.setEncoding("utf8"); - conn.write("before"); - - await new Promise(resolve => { - conn.on("connect", () => { - conn.write(" after"); - resolve(); - }); - }); - - await new Promise(resolve => { - conn.on("data", buf => { - received += buf; - conn.end(); - }); - - conn.on("end", () => { - server.close(); - expect(received).toBe("before after"); - resolve(); - }); - }); -}); - -//<#END_FILE: test-net-write-connect-write.js diff --git a/test/js/node/test/parallel/net-write-fully-async-buffer.test.js b/test/js/node/test/parallel/net-write-fully-async-buffer.test.js deleted file mode 100644 index acd0eeb23c..0000000000 --- a/test/js/node/test/parallel/net-write-fully-async-buffer.test.js +++ /dev/null @@ -1,55 +0,0 @@ -//#FILE: test-net-write-fully-async-buffer.js -//#SHA1: b26773ed4c8c5bafaaa8a4513b25d1806a72ae5f -//----------------- -"use strict"; - -const net = require("net"); - -// Note: This test assumes that the --expose-gc flag is available. -// In a Jest environment, you might need to configure this separately. - -const data = Buffer.alloc(1000000); - -let server; - -beforeAll(done => { - server = net - .createServer(conn => { - conn.resume(); - }) - .listen(0, () => { - done(); - }); -}); - -afterAll(() => { - server.close(); -}); - -test("net write fully async buffer", done => { - const conn = net.createConnection(server.address().port, () => { - let count = 0; - - function writeLoop() { - if (count++ === 200) { - conn.destroy(); - done(); - return; - } - - while (conn.write(Buffer.from(data))); - - // Note: global.gc() is not available in standard Jest environments. - // You might need to configure Jest to run with the --expose-gc flag. - // For this test, we'll comment it out, but in a real scenario, you'd need to ensure it's available. - // global.gc({ type: 'minor' }); - // The buffer allocated above should still be alive. - } - - conn.on("drain", writeLoop); - - writeLoop(); - }); -}); - -//<#END_FILE: test-net-write-fully-async-buffer.js diff --git a/test/js/node/test/parallel/net-write-fully-async-hex-string.test.js b/test/js/node/test/parallel/net-write-fully-async-hex-string.test.js deleted file mode 100644 index 64b79e17ed..0000000000 --- a/test/js/node/test/parallel/net-write-fully-async-hex-string.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-net-write-fully-async-hex-string.js -//#SHA1: e5b365bb794f38e7153fc41ebfaf991031f85423 -//----------------- -"use strict"; - -const net = require("net"); - -let server; - -afterAll(() => { - if (server) { - server.close(); - } -}); - -test("net write fully async hex string", done => { - const data = Buffer.alloc(1000000).toString("hex"); - - server = net.createServer(conn => { - conn.resume(); - }); - - server.listen(0, () => { - const conn = net.createConnection(server.address().port, () => { - let count = 0; - - function writeLoop() { - if (count++ === 20) { - conn.destroy(); - done(); - return; - } - while (conn.write(data, "hex")); - // Note: We can't use global.gc in Jest, so we'll skip this part - // global.gc({ type: 'minor' }); - // The buffer allocated inside the .write() call should still be alive. - - // Use setImmediate to allow other operations to occur - setImmediate(writeLoop); - } - - conn.on("drain", writeLoop); - - writeLoop(); - }); - }); -}); - -//<#END_FILE: test-net-write-fully-async-hex-string.js diff --git a/test/js/node/test/parallel/net-write-slow.test.js b/test/js/node/test/parallel/net-write-slow.test.js deleted file mode 100644 index 9ce97d8d39..0000000000 --- a/test/js/node/test/parallel/net-write-slow.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-net-write-slow.js -//#SHA1: ef646d024e2dfcfb07b99fcdfb9ccf2bfbcb6487 -//----------------- -'use strict'; -const net = require('net'); - -const SIZE = 2E5; -const N = 10; -let flushed = 0; -let received = 0; -const buf = Buffer.alloc(SIZE, 'a'); - -let server; - -beforeAll(() => { - return new Promise((resolve) => { - server = net.createServer((socket) => { - socket.setNoDelay(); - socket.setTimeout(9999); - socket.on('timeout', () => { - throw new Error(`flushed: ${flushed}, received: ${received}/${SIZE * N}`); - }); - - for (let i = 0; i < N; ++i) { - socket.write(buf, () => { - ++flushed; - if (flushed === N) { - socket.setTimeout(0); - } - }); - } - socket.end(); - }).listen(0, () => { - resolve(); - }); - }); -}); - -afterAll(() => { - return new Promise((resolve) => { - server.close(resolve); - }); -}); - -test('net write slow', (done) => { - const conn = net.connect(server.address().port); - - conn.on('data', (buf) => { - received += buf.length; - conn.pause(); - setTimeout(() => { - conn.resume(); - }, 20); - }); - - conn.on('end', () => { - expect(received).toBe(SIZE * N); - done(); - }); -}); - -//<#END_FILE: test-net-write-slow.js diff --git a/test/js/node/test/parallel/next-tick-doesnt-hang.test.js b/test/js/node/test/parallel/next-tick-doesnt-hang.test.js deleted file mode 100644 index 23fa936de0..0000000000 --- a/test/js/node/test/parallel/next-tick-doesnt-hang.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-next-tick-doesnt-hang.js -//#SHA1: 6812bb4cd77cd15dd04c4409e34e0c5b605bbb88 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// This test verifies that having a single nextTick statement and nothing else -// does not hang the event loop. If this test times out it has failed. - -test("nextTick does not hang", done => { - process.nextTick(() => { - // Nothing - done(); - }); -}); - -//<#END_FILE: test-next-tick-doesnt-hang.js diff --git a/test/js/node/test/parallel/next-tick-domain.test.js b/test/js/node/test/parallel/next-tick-domain.test.js deleted file mode 100644 index d975718da7..0000000000 --- a/test/js/node/test/parallel/next-tick-domain.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-next-tick-domain.js -//#SHA1: 75db0e440cbc2eb2fc89b4486eb10e398042a88b -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("requiring domain should not change process.nextTick", () => { - const origNextTick = process.nextTick; - - require("domain"); - - // Requiring domain should not change nextTick. - expect(process.nextTick).toBe(origNextTick); -}); - -//<#END_FILE: test-next-tick-domain.js diff --git a/test/js/node/test/parallel/next-tick-ordering.test.js b/test/js/node/test/parallel/next-tick-ordering.test.js deleted file mode 100644 index 3790be15ab..0000000000 --- a/test/js/node/test/parallel/next-tick-ordering.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-next-tick-ordering.js -//#SHA1: 224023554e0ccd7644d70c5acef5bf3a094409ac -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("next tick ordering", async () => { - const N = 30; - const done = []; - - function get_printer(timeout) { - return function () { - console.log(`Running from setTimeout ${timeout}`); - done.push(timeout); - }; - } - - process.nextTick(function () { - console.log("Running from nextTick"); - done.push("nextTick"); - }); - - for (let i = 0; i < N; i += 1) { - setTimeout(get_printer(i), i); - } - - console.log("Running from main."); - - // Wait for all timeouts to complete - await new Promise(resolve => setTimeout(resolve, N + 100)); - - expect(done[0]).toBe("nextTick"); - // Disabling this test. I don't think we can ensure the order - // for (let i = 0; i < N; i += 1) { - // expect(done[i + 1]).toBe(i); - // } -}); - -//<#END_FILE: test-next-tick-ordering.js diff --git a/test/js/node/test/parallel/next-tick-ordering2.test.js b/test/js/node/test/parallel/next-tick-ordering2.test.js deleted file mode 100644 index 233df7aed3..0000000000 --- a/test/js/node/test/parallel/next-tick-ordering2.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-next-tick-ordering2.js -//#SHA1: 6aa2e4c01b6ff008c2c2c844cb50a75d45af481d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("nextTick and setTimeout ordering", async () => { - const order = []; - - await new Promise(resolve => { - process.nextTick(() => { - setTimeout(() => { - order.push("setTimeout"); - resolve(); - }, 0); - - process.nextTick(() => { - order.push("nextTick"); - }); - }); - }); - - expect(order).toEqual(["nextTick", "setTimeout"]); -}); - -//<#END_FILE: test-next-tick-ordering2.js diff --git a/test/js/node/test/parallel/next-tick-when-exiting.test.js b/test/js/node/test/parallel/next-tick-when-exiting.test.js deleted file mode 100644 index 1684eebc47..0000000000 --- a/test/js/node/test/parallel/next-tick-when-exiting.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-next-tick-when-exiting.js -//#SHA1: fcca0c205805ee8c1d9e994013d5e3738e3ef6e4 -//----------------- -"use strict"; - -test("process.nextTick should not be called when exiting", () => { - const exitHandler = jest.fn(() => { - expect(process._exiting).toBe(true); - - process.nextTick(jest.fn().mockName("process is exiting, should not be called")); - }); - - process.on("exit", exitHandler); - - process.exit(); - - expect(exitHandler).toHaveBeenCalledTimes(1); - expect(exitHandler.mock.calls[0][0]).toBe(undefined); -}); - -//<#END_FILE: test-next-tick-when-exiting.js diff --git a/test/js/node/test/parallel/no-node-snapshot.test.js b/test/js/node/test/parallel/no-node-snapshot.test.js deleted file mode 100644 index bebaabb1c5..0000000000 --- a/test/js/node/test/parallel/no-node-snapshot.test.js +++ /dev/null @@ -1,14 +0,0 @@ -//#FILE: test-no-node-snapshot.js -//#SHA1: e8e4ecdb2aa4c064a55e1c7b076424d0b0d0a007 -//----------------- -"use strict"; - -// Flags: --no-node-snapshot - -test("--no-node-snapshot flag", () => { - // This test doesn't actually assert anything. - // It's merely checking if the script runs without errors when the flag is set. - expect(true).toBe(true); -}); - -//<#END_FILE: test-no-node-snapshot.js diff --git a/test/js/node/test/parallel/outgoing-message-destroy.test.js b/test/js/node/test/parallel/outgoing-message-destroy.test.js deleted file mode 100644 index 6f61032083..0000000000 --- a/test/js/node/test/parallel/outgoing-message-destroy.test.js +++ /dev/null @@ -1,19 +0,0 @@ -//#FILE: test-outgoing-message-destroy.js -//#SHA1: 64f5438a6e8b8315e79f25d8f9e40b7dde6e3c19 -//----------------- -"use strict"; - -// Test that http.OutgoingMessage,prototype.destroy() returns `this`. - -const http = require("http"); - -test("http.OutgoingMessage.prototype.destroy() returns `this`", () => { - const outgoingMessage = new http.OutgoingMessage(); - - expect(outgoingMessage.destroyed).toBe(false); - expect(outgoingMessage.destroy()).toBe(outgoingMessage); - expect(outgoingMessage.destroyed).toBe(true); - expect(outgoingMessage.destroy()).toBe(outgoingMessage); -}); - -//<#END_FILE: test-outgoing-message-destroy.js diff --git a/test/js/node/test/parallel/path-extname.test.js b/test/js/node/test/parallel/path-extname.test.js deleted file mode 100644 index e8a74c6b2e..0000000000 --- a/test/js/node/test/parallel/path-extname.test.js +++ /dev/null @@ -1,115 +0,0 @@ -//#FILE: test-path-extname.js -//#SHA1: 29d676d507ef80d7e5795db0f2a0265dbc7baf1e -//----------------- -"use strict"; -const path = require("path"); - -const slashRE = /\//g; - -const testPaths = [ - [__filename, ".js"], - ["", ""], - ["/path/to/file", ""], - ["/path/to/file.ext", ".ext"], - ["/path.to/file.ext", ".ext"], - ["/path.to/file", ""], - ["/path.to/.file", ""], - ["/path.to/.file.ext", ".ext"], - ["/path/to/f.ext", ".ext"], - ["/path/to/..ext", ".ext"], - ["/path/to/..", ""], - ["file", ""], - ["file.ext", ".ext"], - [".file", ""], - [".file.ext", ".ext"], - ["/file", ""], - ["/file.ext", ".ext"], - ["/.file", ""], - ["/.file.ext", ".ext"], - [".path/file.ext", ".ext"], - ["file.ext.ext", ".ext"], - ["file.", "."], - [".", ""], - ["./", ""], - [".file.ext", ".ext"], - [".file", ""], - [".file.", "."], - [".file..", "."], - ["..", ""], - ["../", ""], - ["..file.ext", ".ext"], - ["..file", ".file"], - ["..file.", "."], - ["..file..", "."], - ["...", "."], - ["...ext", ".ext"], - ["....", "."], - ["file.ext/", ".ext"], - ["file.ext//", ".ext"], - ["file/", ""], - ["file//", ""], - ["file./", "."], - ["file.//", "."], -]; - -describe("path.extname", () => { - test("should return correct extensions for various paths", () => { - const failures = []; - - for (const testPath of testPaths) { - const expected = testPath[1]; - const extNames = [path.posix.extname, path.win32.extname]; - for (const extname of extNames) { - let input = testPath[0]; - let os; - if (extname === path.win32.extname) { - input = input.replace(slashRE, "\\"); - os = "win32"; - } else { - os = "posix"; - } - const actual = extname(input); - const message = `path.${os}.extname(${JSON.stringify(input)})\n expect=${JSON.stringify( - expected, - )}\n actual=${JSON.stringify(actual)}`; - if (actual !== expected) failures.push(`\n${message}`); - } - const input = `C:${testPath[0].replace(slashRE, "\\")}`; - const actual = path.win32.extname(input); - const message = `path.win32.extname(${JSON.stringify(input)})\n expect=${JSON.stringify( - expected, - )}\n actual=${JSON.stringify(actual)}`; - if (actual !== expected) failures.push(`\n${message}`); - } - - expect(failures).toHaveLength(0); - }); - - describe("Windows-specific behavior", () => { - test("backslash is a path separator", () => { - expect(path.win32.extname(".\\").toString()).toBe(""); - expect(path.win32.extname("..\\").toString()).toBe(""); - expect(path.win32.extname("file.ext\\").toString()).toBe(".ext"); - expect(path.win32.extname("file.ext\\\\").toString()).toBe(".ext"); - expect(path.win32.extname("file\\").toString()).toBe(""); - expect(path.win32.extname("file\\\\").toString()).toBe(""); - expect(path.win32.extname("file.\\").toString()).toBe("."); - expect(path.win32.extname("file.\\\\").toString()).toBe("."); - }); - }); - - describe("POSIX-specific behavior", () => { - test("backslash is a valid name component", () => { - expect(path.posix.extname(".\\").toString()).toBe(""); - expect(path.posix.extname("..\\").toString()).toBe(".\\"); - expect(path.posix.extname("file.ext\\").toString()).toBe(".ext\\"); - expect(path.posix.extname("file.ext\\\\").toString()).toBe(".ext\\\\"); - expect(path.posix.extname("file\\").toString()).toBe(""); - expect(path.posix.extname("file\\\\").toString()).toBe(""); - expect(path.posix.extname("file.\\").toString()).toBe(".\\"); - expect(path.posix.extname("file.\\\\").toString()).toBe(".\\\\"); - }); - }); -}); - -//<#END_FILE: test-path-extname.js diff --git a/test/js/node/test/parallel/path-isabsolute.test.js b/test/js/node/test/parallel/path-isabsolute.test.js deleted file mode 100644 index 53604b0d51..0000000000 --- a/test/js/node/test/parallel/path-isabsolute.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-path-isabsolute.js -//#SHA1: d0ff051a7934f18aed9c435a823ff688e5f782c1 -//----------------- -"use strict"; - -const path = require("path"); - -test("path.win32.isAbsolute", () => { - expect(path.win32.isAbsolute("/")).toBe(true); - expect(path.win32.isAbsolute("//")).toBe(true); - expect(path.win32.isAbsolute("//server")).toBe(true); - expect(path.win32.isAbsolute("//server/file")).toBe(true); - expect(path.win32.isAbsolute("\\\\server\\file")).toBe(true); - expect(path.win32.isAbsolute("\\\\server")).toBe(true); - expect(path.win32.isAbsolute("\\\\")).toBe(true); - expect(path.win32.isAbsolute("c")).toBe(false); - expect(path.win32.isAbsolute("c:")).toBe(false); - expect(path.win32.isAbsolute("c:\\")).toBe(true); - expect(path.win32.isAbsolute("c:/")).toBe(true); - expect(path.win32.isAbsolute("c://")).toBe(true); - expect(path.win32.isAbsolute("C:/Users/")).toBe(true); - expect(path.win32.isAbsolute("C:\\Users\\")).toBe(true); - expect(path.win32.isAbsolute("C:cwd/another")).toBe(false); - expect(path.win32.isAbsolute("C:cwd\\another")).toBe(false); - expect(path.win32.isAbsolute("directory/directory")).toBe(false); - expect(path.win32.isAbsolute("directory\\directory")).toBe(false); -}); - -test("path.posix.isAbsolute", () => { - expect(path.posix.isAbsolute("/home/foo")).toBe(true); - expect(path.posix.isAbsolute("/home/foo/..")).toBe(true); - expect(path.posix.isAbsolute("bar/")).toBe(false); - expect(path.posix.isAbsolute("./baz")).toBe(false); -}); - -//<#END_FILE: test-path-isabsolute.js diff --git a/test/js/node/test/parallel/path-normalize.test.js b/test/js/node/test/parallel/path-normalize.test.js deleted file mode 100644 index c1a4bee899..0000000000 --- a/test/js/node/test/parallel/path-normalize.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-path-normalize.js -//#SHA1: 94c9aec4a962fc0737d7a88610d3c3e17a3b96b5 -//----------------- -"use strict"; - -const path = require("path"); - -describe("path.normalize", () => { - describe("win32", () => { - test("normalizes various paths correctly", () => { - expect(path.win32.normalize("./fixtures///b/../b/c.js")).toBe("fixtures\\b\\c.js"); - expect(path.win32.normalize("/foo/../../../bar")).toBe("\\bar"); - expect(path.win32.normalize("a//b//../b")).toBe("a\\b"); - expect(path.win32.normalize("a//b//./c")).toBe("a\\b\\c"); - expect(path.win32.normalize("a//b//.")).toBe("a\\b"); - expect(path.win32.normalize("//server/share/dir/file.ext")).toBe("\\\\server\\share\\dir\\file.ext"); - expect(path.win32.normalize("/a/b/c/../../../x/y/z")).toBe("\\x\\y\\z"); - expect(path.win32.normalize("C:")).toBe("C:."); - expect(path.win32.normalize("C:..\\abc")).toBe("C:..\\abc"); - expect(path.win32.normalize("C:..\\..\\abc\\..\\def")).toBe("C:..\\..\\def"); - expect(path.win32.normalize("C:\\.")).toBe("C:\\"); - expect(path.win32.normalize("file:stream")).toBe("file:stream"); - expect(path.win32.normalize("bar\\foo..\\..\\")).toBe("bar\\"); - expect(path.win32.normalize("bar\\foo..\\..")).toBe("bar"); - expect(path.win32.normalize("bar\\foo..\\..\\baz")).toBe("bar\\baz"); - expect(path.win32.normalize("bar\\foo..\\")).toBe("bar\\foo..\\"); - expect(path.win32.normalize("bar\\foo..")).toBe("bar\\foo.."); - expect(path.win32.normalize("..\\foo..\\..\\..\\bar")).toBe("..\\..\\bar"); - expect(path.win32.normalize("..\\...\\..\\.\\...\\..\\..\\bar")).toBe("..\\..\\bar"); - expect(path.win32.normalize("../../../foo/../../../bar")).toBe("..\\..\\..\\..\\..\\bar"); - expect(path.win32.normalize("../../../foo/../../../bar/../../")).toBe("..\\..\\..\\..\\..\\..\\"); - expect(path.win32.normalize("../foobar/barfoo/foo/../../../bar/../../")).toBe("..\\..\\"); - expect(path.win32.normalize("../.../../foobar/../../../bar/../../baz")).toBe("..\\..\\..\\..\\baz"); - expect(path.win32.normalize("foo/bar\\baz")).toBe("foo\\bar\\baz"); - }); - }); - - describe("posix", () => { - test("normalizes various paths correctly", () => { - expect(path.posix.normalize("./fixtures///b/../b/c.js")).toBe("fixtures/b/c.js"); - expect(path.posix.normalize("/foo/../../../bar")).toBe("/bar"); - expect(path.posix.normalize("a//b//../b")).toBe("a/b"); - expect(path.posix.normalize("a//b//./c")).toBe("a/b/c"); - expect(path.posix.normalize("a//b//.")).toBe("a/b"); - expect(path.posix.normalize("/a/b/c/../../../x/y/z")).toBe("/x/y/z"); - expect(path.posix.normalize("///..//./foo/.//bar")).toBe("/foo/bar"); - expect(path.posix.normalize("bar/foo../../")).toBe("bar/"); - expect(path.posix.normalize("bar/foo../..")).toBe("bar"); - expect(path.posix.normalize("bar/foo../../baz")).toBe("bar/baz"); - expect(path.posix.normalize("bar/foo../")).toBe("bar/foo../"); - expect(path.posix.normalize("bar/foo..")).toBe("bar/foo.."); - expect(path.posix.normalize("../foo../../../bar")).toBe("../../bar"); - expect(path.posix.normalize("../.../.././.../../../bar")).toBe("../../bar"); - expect(path.posix.normalize("../../../foo/../../../bar")).toBe("../../../../../bar"); - expect(path.posix.normalize("../../../foo/../../../bar/../../")).toBe("../../../../../../"); - expect(path.posix.normalize("../foobar/barfoo/foo/../../../bar/../../")).toBe("../../"); - expect(path.posix.normalize("../.../../foobar/../../../bar/../../baz")).toBe("../../../../baz"); - expect(path.posix.normalize("foo/bar\\baz")).toBe("foo/bar\\baz"); - }); - }); -}); - -//<#END_FILE: test-path-normalize.js diff --git a/test/js/node/test/parallel/path-posix-exists.test.js b/test/js/node/test/parallel/path-posix-exists.test.js deleted file mode 100644 index 8dfb86ea81..0000000000 --- a/test/js/node/test/parallel/path-posix-exists.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-path-posix-exists.js -//#SHA1: 4bd4c9ef3ffd03623fefbdedd28732c21fd10956 -//----------------- -"use strict"; - -// The original test file used Node.js specific modules and assertions. -// We'll convert this to use Jest's testing framework while maintaining -// the same behavior and allowing it to run in both Node.js and Bun. - -test("path/posix module exists and is identical to path.posix", () => { - // In Jest, we don't need to explicitly require assert - // We'll use Jest's expect API instead - - // We still need to require the path module - const path = require("path"); - - // Check if the path/posix module is the same as path.posix - expect(require("path/posix")).toBe(path.posix); -}); - -//<#END_FILE: test-path-posix-exists.js diff --git a/test/js/node/test/parallel/path-posix-relative-on-windows.test.js b/test/js/node/test/parallel/path-posix-relative-on-windows.test.js deleted file mode 100644 index c1a4e9ea2c..0000000000 --- a/test/js/node/test/parallel/path-posix-relative-on-windows.test.js +++ /dev/null @@ -1,15 +0,0 @@ -//#FILE: test-path-posix-relative-on-windows.js -//#SHA1: 2fb8d86d02eea6a077fbca45828aa2433d6a49e6 -//----------------- -"use strict"; - -const path = require("path"); - -// Refs: https://github.com/nodejs/node/issues/13683 - -test("path.posix.relative on Windows", () => { - const relativePath = path.posix.relative("a/b/c", "../../x"); - expect(relativePath).toMatch(/^(\.\.\/){3,5}x$/); -}); - -//<#END_FILE: test-path-posix-relative-on-windows.js diff --git a/test/js/node/test/parallel/path-relative.test.js b/test/js/node/test/parallel/path-relative.test.js deleted file mode 100644 index 86c50968de..0000000000 --- a/test/js/node/test/parallel/path-relative.test.js +++ /dev/null @@ -1,75 +0,0 @@ -//#FILE: test-path-relative.js -//#SHA1: 9f0d03bf451853a369a3b31b94b902ee4f607e51 -//----------------- -"use strict"; - -const path = require("path"); - -describe("path.relative", () => { - const relativeTests = [ - [ - path.win32.relative, - // Arguments result - [ - ["c:/blah\\blah", "d:/games", "d:\\games"], - ["c:/aaaa/bbbb", "c:/aaaa", ".."], - ["c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"], - ["c:/aaaa/bbbb", "c:/aaaa/bbbb", ""], - ["c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"], - ["c:/aaaa/", "c:/aaaa/cccc", "cccc"], - ["c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"], - ["c:/aaaa/bbbb", "d:\\", "d:\\"], - ["c:/AaAa/bbbb", "c:/aaaa/bbbb", ""], - ["c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"], - ["C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."], - ["C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"], - ["C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"], - ["C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"], - ["\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"], - ["\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."], - ["\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"], - ["\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"], - ["C:\\baz-quux", "C:\\baz", "..\\baz"], - ["C:\\baz", "C:\\baz-quux", "..\\baz-quux"], - ["\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"], - ["\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"], - ["C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"], - ["\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"], - ], - ], - [ - path.posix.relative, - // Arguments result - [ - ["/var/lib", "/var", ".."], - ["/var/lib", "/bin", "../../bin"], - ["/var/lib", "/var/lib", ""], - ["/var/lib", "/var/apache", "../apache"], - ["/var/", "/var/lib", "lib"], - ["/", "/var/lib", "var/lib"], - ["/foo/test", "/foo/test/bar/package.json", "bar/package.json"], - ["/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."], - ["/foo/bar/baz-quux", "/foo/bar/baz", "../baz"], - ["/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"], - ["/baz-quux", "/baz", "../baz"], - ["/baz", "/baz-quux", "../baz-quux"], - ["/page1/page2/foo", "/", "../../.."], - ], - ], - ]; - - relativeTests.forEach(test => { - const relative = test[0]; - const os = relative === path.win32.relative ? "win32" : "posix"; - - test[1].forEach(testCase => { - it(`path.${os}.relative(${JSON.stringify(testCase[0])}, ${JSON.stringify(testCase[1])})`, () => { - const actual = relative(testCase[0], testCase[1]); - const expected = testCase[2]; - expect(actual).toBe(expected); - }); - }); - }); -}); - -//<#END_FILE: test-path-relative.js diff --git a/test/js/node/test/parallel/path-win32-exists.test.js b/test/js/node/test/parallel/path-win32-exists.test.js deleted file mode 100644 index b3aae0de87..0000000000 --- a/test/js/node/test/parallel/path-win32-exists.test.js +++ /dev/null @@ -1,10 +0,0 @@ -//#FILE: test-path-win32-exists.js -//#SHA1: 7d5cfa1f0fc5a13f9878eddd3b119b9c488fecc5 -//----------------- -"use strict"; - -test("path/win32 exists and is the same as path.win32", () => { - expect(require("path/win32")).toBe(require("path").win32); -}); - -//<#END_FILE: test-path-win32-exists.js diff --git a/test/js/node/test/parallel/path-zero-length-strings.test.js b/test/js/node/test/parallel/path-zero-length-strings.test.js deleted file mode 100644 index 0c4766ddd6..0000000000 --- a/test/js/node/test/parallel/path-zero-length-strings.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-path-zero-length-strings.js -//#SHA1: 2f55f68499f5dcd0b2cbb43e7793c0f45175402f -//----------------- -"use strict"; - -// These testcases are specific to one uncommon behavior in path module. Few -// of the functions in path module, treat '' strings as current working -// directory. This test makes sure that the behavior is intact between commits. -// See: https://github.com/nodejs/node/pull/2106 - -const path = require("path"); -const pwd = process.cwd(); - -describe("Path module zero-length strings behavior", () => { - test("Join with zero-length strings", () => { - expect(path.posix.join("")).toBe("."); - expect(path.posix.join("", "")).toBe("."); - expect(path.win32.join("")).toBe("."); - expect(path.win32.join("", "")).toBe("."); - expect(path.join(pwd)).toBe(pwd); - expect(path.join(pwd, "")).toBe(pwd); - }); - - test("Normalize with zero-length strings", () => { - expect(path.posix.normalize("")).toBe("."); - expect(path.win32.normalize("")).toBe("."); - expect(path.normalize(pwd)).toBe(pwd); - }); - - test("isAbsolute with zero-length strings", () => { - expect(path.posix.isAbsolute("")).toBe(false); - expect(path.win32.isAbsolute("")).toBe(false); - }); - - test("Resolve with zero-length strings", () => { - expect(path.resolve("")).toBe(pwd); - expect(path.resolve("", "")).toBe(pwd); - }); - - test("Relative with zero-length strings", () => { - expect(path.relative("", pwd)).toBe(""); - expect(path.relative(pwd, "")).toBe(""); - expect(path.relative(pwd, pwd)).toBe(""); - }); -}); - -//<#END_FILE: test-path-zero-length-strings.js diff --git a/test/js/node/test/parallel/path.test.js b/test/js/node/test/parallel/path.test.js deleted file mode 100644 index f358ae1a27..0000000000 --- a/test/js/node/test/parallel/path.test.js +++ /dev/null @@ -1,88 +0,0 @@ -//#FILE: test-path.js -//#SHA1: da9d0113e57d9da3983b555973f7835c17657326 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const path = require("path"); - -// Test thrown TypeErrors -const typeErrorTests = [true, false, 7, null, {}, undefined, [], NaN]; - -function fail(fn) { - const args = Array.from(arguments).slice(1); - - expect(() => { - fn.apply(null, args); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); -} - -test("Invalid argument types", () => { - for (const test of typeErrorTests) { - for (const namespace of [path.posix, path.win32]) { - fail(namespace.join, test); - fail(namespace.resolve, test); - fail(namespace.normalize, test); - fail(namespace.isAbsolute, test); - fail(namespace.relative, test, "foo"); - fail(namespace.relative, "foo", test); - fail(namespace.parse, test); - fail(namespace.dirname, test); - fail(namespace.basename, test); - fail(namespace.extname, test); - - // Undefined is a valid value as the second argument to basename - if (test !== undefined) { - fail(namespace.basename, "foo", test); - } - } - } -}); - -test("path.sep tests", () => { - // windows - expect(path.win32.sep).toBe("\\"); - // posix - expect(path.posix.sep).toBe("/"); -}); - -test("path.delimiter tests", () => { - // windows - expect(path.win32.delimiter).toBe(";"); - // posix - expect(path.posix.delimiter).toBe(":"); -}); - -test("path module", () => { - if (process.platform === "win32") { - expect(path).toBe(path.win32); - } else { - expect(path).toBe(path.posix); - } -}); - -//<#END_FILE: test-path.js diff --git a/test/js/node/test/parallel/perf-gc-crash.test.js b/test/js/node/test/parallel/perf-gc-crash.test.js deleted file mode 100644 index 059bff9b3c..0000000000 --- a/test/js/node/test/parallel/perf-gc-crash.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-perf-gc-crash.js -//#SHA1: 376f73db023a0e22edee3e715b8b5999213b5c93 -//----------------- -"use strict"; - -// Refers to https://github.com/nodejs/node/issues/39548 - -// The test fails if this crashes. If it closes normally, -// then all is good. - -const { PerformanceObserver } = require("perf_hooks"); - -test("PerformanceObserver does not crash on multiple observe and disconnect calls", () => { - // We don't actually care if the observer callback is called here. - const gcObserver = new PerformanceObserver(() => {}); - - expect(() => { - gcObserver.observe({ entryTypes: ["gc"] }); - gcObserver.disconnect(); - }).not.toThrow(); - - const gcObserver2 = new PerformanceObserver(() => {}); - - expect(() => { - gcObserver2.observe({ entryTypes: ["gc"] }); - gcObserver2.disconnect(); - }).not.toThrow(); -}); - -//<#END_FILE: test-perf-gc-crash.js diff --git a/test/js/node/test/parallel/performance-measure.test.js b/test/js/node/test/parallel/performance-measure.test.js deleted file mode 100644 index eff0e0d818..0000000000 --- a/test/js/node/test/parallel/performance-measure.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-performance-measure.js -//#SHA1: f049b29e11ba7864ddf502609caf424eccee7ca5 -//----------------- -"use strict"; - -const { PerformanceObserver, performance } = require("perf_hooks"); - -const DELAY = 1000; -const ALLOWED_MARGIN = 10; - -test("performance measures", done => { - const expected = ["Start to Now", "A to Now", "A to B"]; - const obs = new PerformanceObserver(items => { - items.getEntries().forEach(({ name, duration }) => { - expect(duration).toBeGreaterThan(DELAY - ALLOWED_MARGIN); - expect(expected.shift()).toBe(name); - }); - if (expected.length === 0) { - done(); - } - }); - obs.observe({ entryTypes: ["measure"] }); - - performance.mark("A"); - setTimeout(() => { - performance.measure("Start to Now"); - performance.measure("A to Now", "A"); - - performance.mark("B"); - performance.measure("A to B", "A", "B"); - }, DELAY); -}); - -//<#END_FILE: test-performance-measure.js diff --git a/test/js/node/test/parallel/performance-nodetiming.test.js b/test/js/node/test/parallel/performance-nodetiming.test.js deleted file mode 100644 index 9146bc5b05..0000000000 --- a/test/js/node/test/parallel/performance-nodetiming.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-performance-nodetiming.js -//#SHA1: 7b861bd4f2035688c2cb23ff54525d8e039c2d23 -//----------------- -"use strict"; - -const { performance } = require("perf_hooks"); -const { isMainThread } = require("worker_threads"); - -describe("performance.nodeTiming", () => { - const { nodeTiming } = performance; - - test("basic properties", () => { - expect(nodeTiming.name).toBe("node"); - expect(nodeTiming.entryType).toBe("node"); - }); - - test("timing values", () => { - expect(nodeTiming.startTime).toBe(0); - const now = performance.now(); - expect(nodeTiming.duration).toBeGreaterThanOrEqual(now); - }); - - test("milestone values order", () => { - const keys = ["nodeStart", "v8Start", "environment", "bootstrapComplete"]; - for (let idx = 0; idx < keys.length; idx++) { - if (idx === 0) { - expect(nodeTiming[keys[idx]]).toBeGreaterThanOrEqual(0); - continue; - } - expect(nodeTiming[keys[idx]]).toBeGreaterThan(nodeTiming[keys[idx - 1]]); - } - }); - - test("loop milestones", () => { - expect(nodeTiming.idleTime).toBe(0); - if (isMainThread) { - expect(nodeTiming.loopStart).toBe(-1); - } else { - expect(nodeTiming.loopStart).toBeGreaterThanOrEqual(nodeTiming.bootstrapComplete); - } - expect(nodeTiming.loopExit).toBe(-1); - }); - - test("idle time and loop exit", done => { - setTimeout(() => { - expect(nodeTiming.idleTime).toBeGreaterThanOrEqual(0); - expect(nodeTiming.idleTime + nodeTiming.loopExit).toBeLessThanOrEqual(nodeTiming.duration); - expect(nodeTiming.loopStart).toBeGreaterThanOrEqual(nodeTiming.bootstrapComplete); - done(); - }, 1); - }); - - test("loop exit on process exit", done => { - process.on("exit", () => { - expect(nodeTiming.loopExit).toBeGreaterThan(0); - done(); - }); - // Trigger process exit - process.exit(); - }); -}); - -//<#END_FILE: test-performance-nodetiming.js diff --git a/test/js/node/test/parallel/performanceobserver-gc.test.js b/test/js/node/test/parallel/performanceobserver-gc.test.js deleted file mode 100644 index b5e6c913d0..0000000000 --- a/test/js/node/test/parallel/performanceobserver-gc.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-performanceobserver-gc.js -//#SHA1: 2a18df2fa465d96ec5c9ee5d42052c732c777e2b -//----------------- -"use strict"; - -// Verifies that setting up two observers to listen -// to gc performance does not crash. - -const { PerformanceObserver } = require("perf_hooks"); - -test("Setting up two observers to listen to gc performance does not crash", () => { - // We don't actually care if the callback is ever invoked in this test - const obs = new PerformanceObserver(() => {}); - const obs2 = new PerformanceObserver(() => {}); - - expect(() => { - obs.observe({ type: "gc" }); - obs2.observe({ type: "gc" }); - }).not.toThrow(); -}); - -//<#END_FILE: test-performanceobserver-gc.js diff --git a/test/js/node/test/parallel/pipe-abstract-socket.test.js b/test/js/node/test/parallel/pipe-abstract-socket.test.js deleted file mode 100644 index 934108f4c5..0000000000 --- a/test/js/node/test/parallel/pipe-abstract-socket.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-pipe-abstract-socket.js -//#SHA1: 085e51018c26c846b8ea9a2b23da4f45f37fe82f -//----------------- -"use strict"; - -const net = require("net"); - -const isLinux = process.platform === "linux"; - -if (!isLinux) { - it.skip("Skipping test on non-Linux platforms", () => {}); -} else { - describe("Abstract Unix socket tests", () => { - const path = "\0abstract"; - const expectedErrorMessage = "The argument 'options' can not set readableAll or writableAll to true when path is abstract unix socket. Received"; - - test("throws when setting readableAll to true", () => { - const options = { - path, - readableAll: true, - }; - - expect(() => { - const server = net.createServer(jest.fn()); - server.listen(options); - }).toThrow( - expect.objectContaining({ - message: `${expectedErrorMessage} ${JSON.stringify(options)}`, - code: "ERR_INVALID_ARG_VALUE", - }), - ); - }); - - test("throws when setting writableAll to true", () => { - const options = { - path, - writableAll: true, - } ; - - expect(() => { - const server = net.createServer(jest.fn()); - server.listen(options); - }).toThrow( - expect.objectContaining({ - message: `${expectedErrorMessage} ${JSON.stringify(options)}`, - code: "ERR_INVALID_ARG_VALUE", - }), - ); - }); - - test("throws when setting both readableAll and writableAll to true", () => { - const options = { - path, - readableAll: true, - writableAll: true, - }; - - expect(() => { - const server = net.createServer(jest.fn()); - server.listen(options); - }).toThrow( - expect.objectContaining({ - message: `${expectedErrorMessage} ${JSON.stringify(options)}`, - code: "ERR_INVALID_ARG_VALUE", - }), - ); - }); - }); -} - -//<#END_FILE: test-pipe-abstract-socket.js diff --git a/test/js/node/test/parallel/pipe-return-val.test.js b/test/js/node/test/parallel/pipe-return-val.test.js deleted file mode 100644 index 5027349c9c..0000000000 --- a/test/js/node/test/parallel/pipe-return-val.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-pipe-return-val.js -//#SHA1: 63e55c62d40a6ea0c652d66b609d89f9f5324ea1 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// This test ensures SourceStream.pipe(DestStream) returns DestStream - -const Stream = require("stream").Stream; - -test("SourceStream.pipe(DestStream) returns DestStream", () => { - const sourceStream = new Stream(); - const destStream = new Stream(); - const result = sourceStream.pipe(destStream); - - expect(result).toBe(destStream); -}); - -//<#END_FILE: test-pipe-return-val.js diff --git a/test/js/node/test/parallel/preload-print-process-argv.test.js b/test/js/node/test/parallel/preload-print-process-argv.test.js deleted file mode 100644 index 9f26ef5044..0000000000 --- a/test/js/node/test/parallel/preload-print-process-argv.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-preload-print-process-argv.js -//#SHA1: 071fd007af8342d4f1924da004a6dcc7d419cf9f -//----------------- -"use strict"; - -// This tests that process.argv is the same in the preloaded module -// and the user module. - -const tmpdir = require("../common/tmpdir"); -const { spawnSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); - -beforeAll(() => { - tmpdir.refresh(); -}); - -test("process.argv is the same in preloaded and user module", () => { - const preloadPath = path.join(tmpdir.path, "preload.js"); - const mainPath = path.join(tmpdir.path, "main.js"); - - fs.writeFileSync(preloadPath, "console.log(JSON.stringify(process.argv));", "utf-8"); - - fs.writeFileSync(mainPath, "console.log(JSON.stringify(process.argv));", "utf-8"); - - const child = spawnSync(process.execPath, ["-r", "./preload.js", "main.js"], { cwd: tmpdir.path }); - - expect(child.status).toBe(0); - - if (child.status !== 0) { - console.log(child.stderr.toString()); - } - - const lines = child.stdout.toString().trim().split("\n"); - expect(JSON.parse(lines[0])).toEqual(JSON.parse(lines[1])); -}); - -//<#END_FILE: test-preload-print-process-argv.js diff --git a/test/js/node/test/parallel/preload-self-referential.test.js b/test/js/node/test/parallel/preload-self-referential.test.js deleted file mode 100644 index b050248bd6..0000000000 --- a/test/js/node/test/parallel/preload-self-referential.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-preload-self-referential.js -//#SHA1: 24bb3def0bae68082aee5280abc5116ebd81c972 -//----------------- -"use strict"; - -const path = require("path"); -const { exec } = require("child_process"); - -const nodeBinary = process.argv[0]; - -// Skip test if not in main thread -if (typeof Worker !== "undefined") { - test.skip("process.chdir is not available in Workers", () => {}); -} else { - test("self-referential module preload", done => { - const selfRefModule = path.join(__dirname, "..", "fixtures", "self_ref_module"); - const fixtureA = path.join(__dirname, "..", "fixtures", "printA.js"); - - exec(`"${nodeBinary}" -r self_ref "${fixtureA}"`, { cwd: selfRefModule }, (err, stdout, stderr) => { - expect(err).toBeFalsy(); - expect(stdout).toBe("A\n"); - done(); - }); - }); -} - -//<#END_FILE: test-preload-self-referential.js diff --git a/test/js/node/test/parallel/process-abort.test.js b/test/js/node/test/parallel/process-abort.test.js deleted file mode 100644 index 24731b9cc3..0000000000 --- a/test/js/node/test/parallel/process-abort.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-process-abort.js -//#SHA1: ca6e85cb79ad3e78182547bd6be24625268aced4 -//----------------- -"use strict"; - -// Skip this test in Workers as process.abort() is not available -if (typeof Worker !== "undefined") { - test.skip("process.abort() is not available in Workers", () => {}); -} else { - describe("process.abort", () => { - test("should not have a prototype", () => { - expect(process.abort.prototype).toBeUndefined(); - }); - - test("should throw TypeError when instantiated", () => { - expect(() => new process.abort()).toThrow(TypeError); - }); - }); -} - -//<#END_FILE: test-process-abort.js diff --git a/test/js/node/test/parallel/process-argv-0.test.js b/test/js/node/test/parallel/process-argv-0.test.js deleted file mode 100644 index 7dee3674eb..0000000000 --- a/test/js/node/test/parallel/process-argv-0.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-process-argv-0.js -//#SHA1: 849d017b5c8496f1927b0618a55a688f4a7aa982 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const path = require("path"); -const { spawn } = require("child_process"); - -if (process.argv[2] !== "child") { - test("process.argv[0] in child process", async () => { - const child = spawn(process.execPath, [__filename, "child"], { - cwd: path.dirname(process.execPath), - }); - - let childArgv0 = ""; - for await (const chunk of child.stdout) { - childArgv0 += chunk; - } - - expect(childArgv0).toBe(process.execPath); - }); -} else { - process.stdout.write(process.argv[0]); -} - -//<#END_FILE: test-process-argv-0.js diff --git a/test/js/node/test/parallel/process-chdir-errormessage.test.js b/test/js/node/test/parallel/process-chdir-errormessage.test.js deleted file mode 100644 index 3703d4e1b9..0000000000 --- a/test/js/node/test/parallel/process-chdir-errormessage.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-process-chdir-errormessage.js -//#SHA1: d0eee0a43892b20221d341b892fa425fe207c506 -//----------------- -"use strict"; - -// Skip test in workers where process.chdir is not available -if (typeof Worker !== "undefined") { - test.skip("process.chdir is not available in Workers"); -} else { - test("process.chdir throws correct error for non-existent directory", () => { - expect(() => { - process.chdir("does-not-exist"); - }).toThrow( - expect.objectContaining({ - name: "Error", - code: "ENOENT", - message: expect.stringMatching(/ENOENT: no such file or directory, chdir .+ -> 'does-not-exist'/), - path: process.cwd(), - syscall: "chdir", - dest: "does-not-exist", - }), - ); - }); -} - -//<#END_FILE: test-process-chdir-errormessage.js diff --git a/test/js/node/test/parallel/process-constants-noatime.test.js b/test/js/node/test/parallel/process-constants-noatime.test.js deleted file mode 100644 index 319d5ff97a..0000000000 --- a/test/js/node/test/parallel/process-constants-noatime.test.js +++ /dev/null @@ -1,17 +0,0 @@ -//#FILE: test-process-constants-noatime.js -//#SHA1: cc1fb622e4cb1e217a3e7a0662db5050dc2562c2 -//----------------- -"use strict"; - -const fs = require("fs"); - -test("O_NOATIME constant", () => { - if (process.platform === "linux") { - expect(fs.constants).toHaveProperty("O_NOATIME"); - expect(fs.constants.O_NOATIME).toBe(0x40000); - } else { - expect(fs.constants).not.toHaveProperty("O_NOATIME"); - } -}); - -//<#END_FILE: test-process-constants-noatime.js diff --git a/test/js/node/test/parallel/process-emit.test.js b/test/js/node/test/parallel/process-emit.test.js deleted file mode 100644 index ca5b2c353e..0000000000 --- a/test/js/node/test/parallel/process-emit.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-process-emit.js -//#SHA1: a019bda4bcc14ef2e20bad3cc89cf8676ed5bc49 -//----------------- -"use strict"; - -const sym = Symbol(); - -test("process.emit for normal event", () => { - const listener = jest.fn(); - process.on("normal", listener); - - process.emit("normal", "normalData"); - - expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith("normalData"); - - process.removeListener("normal", listener); -}); - -test("process.emit for symbol event", () => { - const listener = jest.fn(); - process.on(sym, listener); - - process.emit(sym, "symbolData"); - - expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith("symbolData"); - - process.removeListener(sym, listener); -}); - -test("process.emit for SIGPIPE signal", () => { - const listener = jest.fn(); - process.on("SIGPIPE", listener); - - process.emit("SIGPIPE", "signalData"); - - expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith("signalData"); - - process.removeListener("SIGPIPE", listener); -}); - -test("process._eventsCount is not NaN", () => { - expect(Number.isNaN(process._eventsCount)).toBe(false); -}); - -//<#END_FILE: test-process-emit.js diff --git a/test/js/node/test/parallel/process-env-windows-error-reset.test.js b/test/js/node/test/parallel/process-env-windows-error-reset.test.js deleted file mode 100644 index 57c703c828..0000000000 --- a/test/js/node/test/parallel/process-env-windows-error-reset.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-process-env-windows-error-reset.js -//#SHA1: f7e32cc8da8c33ecfa3cddeb13d3dd8689d6af64 -//----------------- -"use strict"; - -// This checks that after accessing a missing env var, a subsequent -// env read will succeed even for empty variables. - -test("empty env var after accessing missing env var", () => { - process.env.FOO = ""; - process.env.NONEXISTENT_ENV_VAR; // eslint-disable-line no-unused-expressions - const foo = process.env.FOO; - - expect(foo).toBe(""); -}); - -test("env var existence after accessing missing env var", () => { - process.env.FOO = ""; - process.env.NONEXISTENT_ENV_VAR; // eslint-disable-line no-unused-expressions - const hasFoo = "FOO" in process.env; - - expect(hasFoo).toBe(true); -}); - -//<#END_FILE: test-process-env-windows-error-reset.js diff --git a/test/js/node/test/parallel/process-exit.test.js b/test/js/node/test/parallel/process-exit.test.js deleted file mode 100644 index 53ad68de05..0000000000 --- a/test/js/node/test/parallel/process-exit.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-process-exit.js -//#SHA1: 6b66cdd7fd70fedb0fab288294724b6ffe7df8c9 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test('Calling .exit() from within "exit" should not overflow the call stack', () => { - let nexits = 0; - - const exitHandler = jest.fn(code => { - expect(nexits++).toBe(0); - expect(code).toBe(0); - process.exit(); - }); - - process.on("exit", exitHandler); - - // Simulate process exit - process.emit("exit", 0); - - expect(exitHandler).toHaveBeenCalledTimes(1); -}); - -// "exit" should be emitted unprovoked - -//<#END_FILE: test-process-exit.js diff --git a/test/js/node/test/parallel/process-features.test.js b/test/js/node/test/parallel/process-features.test.js deleted file mode 100644 index ee32335bf1..0000000000 --- a/test/js/node/test/parallel/process-features.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-process-features.js -//#SHA1: 18e2385d3d69890cd826120906ce495a0bc4ba85 -//----------------- -"use strict"; - -test("process.features", () => { - const keys = new Set(Object.keys(process.features)); - - expect(keys).toEqual( - new Set(["inspector", "debug", "uv", "ipv6", "tls_alpn", "tls_sni", "tls_ocsp", "tls", "cached_builtins"]), - ); - - for (const key of keys) { - expect(typeof process.features[key]).toBe("boolean"); - } -}); - -//<#END_FILE: test-process-features.js diff --git a/test/js/node/test/parallel/process-hrtime-bigint.test.js b/test/js/node/test/parallel/process-hrtime-bigint.test.js deleted file mode 100644 index 3e8aa3d6fb..0000000000 --- a/test/js/node/test/parallel/process-hrtime-bigint.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-process-hrtime-bigint.js -//#SHA1: 2fbd7286e22ca2f6d3155f829032524692e4d77c -//----------------- -"use strict"; - -// Tests that process.hrtime.bigint() works. - -test("process.hrtime.bigint() works", () => { - const start = process.hrtime.bigint(); - expect(typeof start).toBe("bigint"); - - const end = process.hrtime.bigint(); - expect(typeof end).toBe("bigint"); - - expect(end - start).toBeGreaterThanOrEqual(0n); -}); - -//<#END_FILE: test-process-hrtime-bigint.js diff --git a/test/js/node/test/parallel/process-initgroups.test.js b/test/js/node/test/parallel/process-initgroups.test.js deleted file mode 100644 index 8dac517f1e..0000000000 --- a/test/js/node/test/parallel/process-initgroups.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-process-initgroups.js -//#SHA1: e7321b3005c066a0b2edbe457e695622b9f2b8e9 -//----------------- -"use strict"; - -if (process.platform === "win32") { - test("process.initgroups is undefined on Windows", () => { - expect(process.initgroups).toBeUndefined(); - }); -} else if (typeof process.initgroups !== "undefined") { - describe("process.initgroups", () => { - test("throws TypeError for invalid user argument", () => { - [undefined, null, true, {}, [], () => {}].forEach(val => { - expect(() => { - process.initgroups(val); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining('The "user" argument must be one of type number or string.'), - }), - ); - }); - }); - - test("throws TypeError for invalid extraGroup argument", () => { - [undefined, null, true, {}, [], () => {}].forEach(val => { - expect(() => { - process.initgroups("foo", val); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining('The "extraGroup" argument must be one of type number or string.'), - }), - ); - }); - }); - - test("throws ERR_UNKNOWN_CREDENTIAL for non-existent group", () => { - expect(() => { - process.initgroups("fhqwhgadshgnsdhjsdbkhsdabkfabkveyb", "fhqwhgadshgnsdhjsdbkhsdabkfabkveyb"); - }).toThrow( - expect.objectContaining({ - code: "ERR_UNKNOWN_CREDENTIAL", - message: expect.stringContaining("Group identifier does not exist"), - }), - ); - }); - }); -} - -//<#END_FILE: test-process-initgroups.js diff --git a/test/js/node/test/parallel/process-uptime.test.js b/test/js/node/test/parallel/process-uptime.test.js deleted file mode 100644 index f776e4d2a0..0000000000 --- a/test/js/node/test/parallel/process-uptime.test.js +++ /dev/null @@ -1,44 +0,0 @@ -//#FILE: test-process-uptime.js -//#SHA1: 98140b3c8b495ef62c519ca900eeb15f1ef5b5aa -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("process.uptime() returns a reasonable value", () => { - console.error(process.uptime()); - // Add some wiggle room for different platforms. - // Verify that the returned value is in seconds - - // 15 seconds should be a good estimate. - expect(process.uptime()).toBeLessThanOrEqual(15); -}); - -test("process.uptime() increases over time", async () => { - const original = process.uptime(); - - await new Promise(resolve => setTimeout(resolve, 10)); - - const uptime = process.uptime(); - expect(uptime).toBeGreaterThan(original); -}); - -//<#END_FILE: test-process-uptime.js diff --git a/test/js/node/test/parallel/promise-unhandled-issue-43655.test.js b/test/js/node/test/parallel/promise-unhandled-issue-43655.test.js deleted file mode 100644 index cdaa2e24a9..0000000000 --- a/test/js/node/test/parallel/promise-unhandled-issue-43655.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-promise-unhandled-issue-43655.js -//#SHA1: d0726f3e05d7aba39fc84013c63399d915956e30 -//----------------- -"use strict"; - -function delay(time) { - return new Promise(resolve => { - setTimeout(resolve, time); - }); -} - -test("Promise unhandled rejection performance", async () => { - for (let i = 0; i < 100000; i++) { - await new Promise((resolve, reject) => { - reject("value"); - }).then( - () => {}, - () => {}, - ); - } - - const time0 = Date.now(); - await delay(0); - - const diff = Date.now() - time0; - expect(diff).toBeLessThan(500); -}, 10000); // Increased timeout to 10 seconds to ensure enough time for the test - -//<#END_FILE: test-promise-unhandled-issue-43655.js diff --git a/test/js/node/test/parallel/querystring-maxkeys-non-finite.test.js b/test/js/node/test/parallel/querystring-maxkeys-non-finite.test.js deleted file mode 100644 index 10116e91b3..0000000000 --- a/test/js/node/test/parallel/querystring-maxkeys-non-finite.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-querystring-maxKeys-non-finite.js -//#SHA1: a1b76b45daad6e46e5504d52e5931d9e4a6d745d -//----------------- -"use strict"; - -// This test was originally written to test a regression -// that was introduced by -// https://github.com/nodejs/node/pull/2288#issuecomment-179543894 - -const querystring = require("querystring"); - -// Taken from express-js/body-parser -// https://github.com/expressjs/body-parser/blob/ed25264fb494cf0c8bc992b8257092cd4f694d5e/test/urlencoded.js#L636-L651 -function createManyParams(count) { - let str = ""; - - if (count === 0) { - return str; - } - - str += "0=0"; - - for (let i = 1; i < count; i++) { - const n = i.toString(36); - str += `&${n}=${n}`; - } - - return str; -} - -const count = 10000; -const originalMaxLength = 1000; -const params = createManyParams(count); - -// thealphanerd -// 27def4f introduced a change to parse that would cause Infinity -// to be passed to String.prototype.split as an argument for limit -// In this instance split will always return an empty array -// this test confirms that the output of parse is the expected length -// when passed Infinity as the argument for maxKeys -describe("querystring.parse with non-finite maxKeys", () => { - test("Infinity maxKeys should return the length of input", () => { - const resultInfinity = querystring.parse(params, undefined, undefined, { - maxKeys: Infinity, - }); - expect(Object.keys(resultInfinity)).toHaveLength(count); - }); - - test("NaN maxKeys should return the length of input", () => { - const resultNaN = querystring.parse(params, undefined, undefined, { - maxKeys: NaN, - }); - expect(Object.keys(resultNaN)).toHaveLength(count); - }); - - test('String "Infinity" maxKeys should return the maxLength defined by parse internals', () => { - const resultInfinityString = querystring.parse(params, undefined, undefined, { - maxKeys: "Infinity", - }); - expect(Object.keys(resultInfinityString)).toHaveLength(originalMaxLength); - }); - - test('String "NaN" maxKeys should return the maxLength defined by parse internals', () => { - const resultNaNString = querystring.parse(params, undefined, undefined, { - maxKeys: "NaN", - }); - expect(Object.keys(resultNaNString)).toHaveLength(originalMaxLength); - }); -}); - -//<#END_FILE: test-querystring-maxKeys-non-finite.js diff --git a/test/js/node/test/parallel/querystring-multichar-separator.test.js b/test/js/node/test/parallel/querystring-multichar-separator.test.js deleted file mode 100644 index cba18fc69a..0000000000 --- a/test/js/node/test/parallel/querystring-multichar-separator.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-querystring-multichar-separator.js -//#SHA1: 22b484432502e6f32fc4517ea91060b983c7be25 -//----------------- -"use strict"; - -const qs = require("querystring"); - -function check(actual, expected) { - expect(actual).not.toBeInstanceOf(Object); - expect(Object.keys(actual).sort()).toEqual(Object.keys(expected).sort()); - Object.keys(expected).forEach(function (key) { - expect(actual[key]).toEqual(expected[key]); - }); -} - -test("qs.parse with multi-character separator", () => { - check(qs.parse("foo=>bar&&bar=>baz", "&&", "=>"), { foo: "bar", bar: "baz" }); -}); - -test("qs.stringify with multi-character separator", () => { - check(qs.stringify({ foo: "bar", bar: "baz" }, "&&", "=>"), "foo=>bar&&bar=>baz"); -}); - -test("qs.parse with different multi-character separators", () => { - check(qs.parse("foo==>bar, bar==>baz", ", ", "==>"), { foo: "bar", bar: "baz" }); -}); - -test("qs.stringify with different multi-character separators", () => { - check(qs.stringify({ foo: "bar", bar: "baz" }, ", ", "==>"), "foo==>bar, bar==>baz"); -}); - -//<#END_FILE: test-querystring-multichar-separator.js diff --git a/test/js/node/test/parallel/quic-internal-endpoint-options.test.js b/test/js/node/test/parallel/quic-internal-endpoint-options.test.js deleted file mode 100644 index 9a5694dcfd..0000000000 --- a/test/js/node/test/parallel/quic-internal-endpoint-options.test.js +++ /dev/null @@ -1,192 +0,0 @@ -//#FILE: test-quic-internal-endpoint-options.js -//#SHA1: 089ba4358a2a9ed3c5463e59d205ee7854f26f30 -//----------------- -// Flags: --expose-internals -"use strict"; - -const common = require("../common"); -if (!common.hasQuic) common.skip("missing quic"); - -const { internalBinding } = require("internal/test/binding"); -const quic = internalBinding("quic"); - -quic.setCallbacks({ - onEndpointClose() {}, - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, -}); - -test("Invalid Endpoint constructor arguments", () => { - expect(() => new quic.Endpoint()).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - - expect(() => new quic.Endpoint("a")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - - expect(() => new quic.Endpoint(null)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - - expect(() => new quic.Endpoint(false)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); -}); - -test("Default options work", () => { - expect(() => new quic.Endpoint({})).not.toThrow(); -}); - -const cases = [ - { - key: "retryTokenExpiration", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "tokenExpiration", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "maxConnectionsPerHost", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "maxConnectionsTotal", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "maxStatelessResetsPerHost", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "addressLRUSize", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "maxRetries", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "maxPayloadSize", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "unacknowledgedPacketThreshold", - valid: [1, 10, 100, 1000, 10000, 10000n], - invalid: [-1, -1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "validateAddress", - valid: [true, false, 0, 1, "a"], - invalid: [], - }, - { - key: "disableStatelessReset", - valid: [true, false, 0, 1, "a"], - invalid: [], - }, - { - key: "ipv6Only", - valid: [true, false, 0, 1, "a"], - invalid: [], - }, - { - key: "cc", - valid: [ - quic.CC_ALGO_RENO, - quic.CC_ALGO_CUBIC, - quic.CC_ALGO_BBR, - quic.CC_ALGO_BBR2, - quic.CC_ALGO_RENO_STR, - quic.CC_ALGO_CUBIC_STR, - quic.CC_ALGO_BBR_STR, - quic.CC_ALGO_BBR2_STR, - ], - invalid: [-1, 4, 1n, "a", null, false, true, {}, [], () => {}], - }, - { - key: "udpReceiveBufferSize", - valid: [0, 1, 2, 3, 4, 1000], - invalid: [-1, "a", null, false, true, {}, [], () => {}], - }, - { - key: "udpSendBufferSize", - valid: [0, 1, 2, 3, 4, 1000], - invalid: [-1, "a", null, false, true, {}, [], () => {}], - }, - { - key: "udpTTL", - valid: [0, 1, 2, 3, 4, 255], - invalid: [-1, 256, "a", null, false, true, {}, [], () => {}], - }, - { - key: "resetTokenSecret", - valid: [new Uint8Array(16), new Uint16Array(8), new Uint32Array(4)], - invalid: ["a", null, false, true, {}, [], () => {}, new Uint8Array(15), new Uint8Array(17), new ArrayBuffer(16)], - }, - { - key: "tokenSecret", - valid: [new Uint8Array(16), new Uint16Array(8), new Uint32Array(4)], - invalid: ["a", null, false, true, {}, [], () => {}, new Uint8Array(15), new Uint8Array(17), new ArrayBuffer(16)], - }, - { - // Unknown options are ignored entirely for any value type - key: "ignored", - valid: ["a", null, false, true, {}, [], () => {}], - invalid: [], - }, -]; - -for (const { key, valid, invalid } of cases) { - describe(`Endpoint option: ${key}`, () => { - test.each(valid)("valid value: %p", value => { - const options = { [key]: value }; - expect(() => new quic.Endpoint(options)).not.toThrow(); - }); - - test.each(invalid)("invalid value: %p", value => { - const options = { [key]: value }; - expect(() => new quic.Endpoint(options)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_VALUE", - message: expect.any(String), - }), - ); - }); - }); -} - -//<#END_FILE: test-quic-internal-endpoint-options.js diff --git a/test/js/node/test/parallel/readable-large-hwm.test.js b/test/js/node/test/parallel/readable-large-hwm.test.js deleted file mode 100644 index 2fc8b24615..0000000000 --- a/test/js/node/test/parallel/readable-large-hwm.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-readable-large-hwm.js -//#SHA1: 1f1184c10e91262eb541830677abcd3e759d304e -//----------------- -"use strict"; -const { Readable } = require("stream"); - -// Make sure that readable completes -// even when reading larger buffer. -test("readable completes when reading larger buffer", done => { - const bufferSize = 10 * 1024 * 1024; - let n = 0; - const r = new Readable({ - read() { - // Try to fill readable buffer piece by piece. - r.push(Buffer.alloc(bufferSize / 10)); - - if (n++ > 10) { - r.push(null); - } - }, - }); - - r.on("readable", () => { - while (true) { - const ret = r.read(bufferSize); - if (ret === null) break; - } - }); - - r.on("end", () => { - done(); - }); -}); - -//<#END_FILE: test-readable-large-hwm.js diff --git a/test/js/node/test/parallel/readable-single-end.test.js b/test/js/node/test/parallel/readable-single-end.test.js deleted file mode 100644 index f8d6813997..0000000000 --- a/test/js/node/test/parallel/readable-single-end.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-readable-single-end.js -//#SHA1: eb85fac7020fa4b5bc4a0f17ba287e8ab3c9dd2f -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -// This test ensures that there will not be an additional empty 'readable' -// event when stream has ended (only 1 event signalling about end) - -test("Readable stream emits only one event when ended", () => { - const r = new Readable({ - read: () => {}, - }); - - r.push(null); - - const readableSpy = jest.fn(); - const endSpy = jest.fn(); - - r.on("readable", readableSpy); - r.on("end", endSpy); - - return new Promise(resolve => { - setTimeout(() => { - expect(readableSpy).toHaveBeenCalledTimes(1); - expect(endSpy).toHaveBeenCalledTimes(1); - resolve(); - }, 0); - }); -}); - -//<#END_FILE: test-readable-single-end.js diff --git a/test/js/node/test/parallel/readline-async-iterators-destroy.test.js b/test/js/node/test/parallel/readline-async-iterators-destroy.test.js deleted file mode 100644 index 1680fec8d8..0000000000 --- a/test/js/node/test/parallel/readline-async-iterators-destroy.test.js +++ /dev/null @@ -1,99 +0,0 @@ -//#FILE: test-readline-async-iterators-destroy.js -//#SHA1: c082e44d93a4bc199683c7cad216da7bc3f0dc7b -//----------------- -"use strict"; - -const fs = require("fs"); -const { once } = require("events"); -const readline = require("readline"); -const path = require("path"); -const os = require("os"); - -const tmpdir = os.tmpdir(); - -const filename = path.join(tmpdir, "test.txt"); - -const testContents = [ - "", - "\n", - "line 1", - "line 1\nline 2 南越国是前203年至前111年存在于岭南地区的一个国家\nline 3\ntrailing", - "line 1\nline 2\nline 3 ends with newline\n", -]; - -async function testSimpleDestroy() { - for (const fileContent of testContents) { - fs.writeFileSync(filename, fileContent); - - const readable = fs.createReadStream(filename); - const rli = readline.createInterface({ - input: readable, - crlfDelay: Infinity, - }); - - const iteratedLines = []; - for await (const k of rli) { - iteratedLines.push(k); - break; - } - - const expectedLines = fileContent.split("\n"); - if (expectedLines[expectedLines.length - 1] === "") { - expectedLines.pop(); - } - expectedLines.splice(1); - - expect(iteratedLines).toEqual(expectedLines); - - rli.close(); - readable.destroy(); - - await once(readable, "close"); - } -} - -async function testMutualDestroy() { - for (const fileContent of testContents) { - fs.writeFileSync(filename, fileContent); - - const readable = fs.createReadStream(filename); - const rli = readline.createInterface({ - input: readable, - crlfDelay: Infinity, - }); - - const expectedLines = fileContent.split("\n"); - if (expectedLines[expectedLines.length - 1] === "") { - expectedLines.pop(); - } - expectedLines.splice(2); - - const iteratedLines = []; - for await (const k of rli) { - iteratedLines.push(k); - for await (const l of rli) { - iteratedLines.push(l); - break; - } - expect(iteratedLines).toEqual(expectedLines); - break; - } - - expect(iteratedLines).toEqual(expectedLines); - - rli.close(); - readable.destroy(); - - await once(readable, "close"); - } -} - -test("Simple destroy", async () => { - await testSimpleDestroy(); -}); - -test("Mutual destroy", async () => { - await testMutualDestroy(); -}); - -//<#END_FILE: test-readline-async-iterators-destroy.js diff --git a/test/js/node/test/parallel/readline-csi.test.js b/test/js/node/test/parallel/readline-csi.test.js deleted file mode 100644 index e22cdf3b53..0000000000 --- a/test/js/node/test/parallel/readline-csi.test.js +++ /dev/null @@ -1,211 +0,0 @@ -//#FILE: test-readline-csi.js -//#SHA1: 6c80ba1b15c53086d80064d93c6f4cf56d1056d6 -//----------------- -"use strict"; - -const readline = require("readline"); -const { Writable } = require("stream"); - -// Mock the CSI object -const CSI = { - kClearToLineBeginning: "\x1b[1K", - kClearToLineEnd: "\x1b[0K", - kClearLine: "\x1b[2K", - kClearScreenDown: "\x1b[0J", -}; - -test("CSI constants", () => { - expect(CSI).toBeDefined(); - expect(CSI.kClearToLineBeginning).toBe("\x1b[1K"); - expect(CSI.kClearToLineEnd).toBe("\x1b[0K"); - expect(CSI.kClearLine).toBe("\x1b[2K"); - expect(CSI.kClearScreenDown).toBe("\x1b[0J"); -}); - -class TestWritable extends Writable { - constructor() { - super(); - this.data = ""; - } - _write(chunk, encoding, callback) { - this.data += chunk.toString(); - callback(); - } -} - -let writable; - -beforeEach(() => { - writable = new TestWritable(); -}); - -test("clearScreenDown", () => { - expect(readline.clearScreenDown(writable)).toBe(true); - expect(writable.data).toBe(CSI.kClearScreenDown); - - writable.data = ""; - expect(readline.clearScreenDown(writable, jest.fn())).toBe(true); - - expect(() => { - readline.clearScreenDown(writable, null); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_TYPE", - }), - ); - - expect(readline.clearScreenDown(null, jest.fn())).toBe(true); - expect(readline.clearScreenDown(undefined, jest.fn())).toBe(true); -}); - -test("clearLine", () => { - expect(readline.clearLine(writable, -1)).toBe(true); - expect(writable.data).toBe(CSI.kClearToLineBeginning); - - writable.data = ""; - expect(readline.clearLine(writable, 1)).toBe(true); - expect(writable.data).toBe(CSI.kClearToLineEnd); - - writable.data = ""; - expect(readline.clearLine(writable, 0)).toBe(true); - expect(writable.data).toBe(CSI.kClearLine); - - writable.data = ""; - expect(readline.clearLine(writable, -1, jest.fn())).toBe(true); - expect(writable.data).toBe(CSI.kClearToLineBeginning); - - expect(() => { - readline.clearLine(writable, 0, null); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_TYPE", - }), - ); - - expect(readline.clearLine(null, 0)).toBe(true); - expect(readline.clearLine(undefined, 0)).toBe(true); - expect(readline.clearLine(null, 0, jest.fn())).toBe(true); - expect(readline.clearLine(undefined, 0, jest.fn())).toBe(true); -}); - -test("moveCursor", () => { - const testCases = [ - [0, 0, ""], - [1, 0, "\x1b[1C"], - [-1, 0, "\x1b[1D"], - [0, 1, "\x1b[1B"], - [0, -1, "\x1b[1A"], - [1, 1, "\x1b[1C\x1b[1B"], - [-1, 1, "\x1b[1D\x1b[1B"], - [-1, -1, "\x1b[1D\x1b[1A"], - [1, -1, "\x1b[1C\x1b[1A"], - ]; - - testCases.forEach(([dx, dy, expected]) => { - writable.data = ""; - expect(readline.moveCursor(writable, dx, dy)).toBe(true); - expect(writable.data).toBe(expected); - - writable.data = ""; - expect(readline.moveCursor(writable, dx, dy, jest.fn())).toBe(true); - expect(writable.data).toBe(expected); - }); - - expect(() => { - readline.moveCursor(writable, 1, 1, null); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_TYPE", - }), - ); - - expect(readline.moveCursor(null, 1, 1)).toBe(true); - expect(readline.moveCursor(undefined, 1, 1)).toBe(true); - expect(readline.moveCursor(null, 1, 1, jest.fn())).toBe(true); - expect(readline.moveCursor(undefined, 1, 1, jest.fn())).toBe(true); -}); - -test("cursorTo", () => { - expect(readline.cursorTo(null)).toBe(true); - expect(readline.cursorTo()).toBe(true); - expect(readline.cursorTo(null, 1, 1, jest.fn())).toBe(true); - expect(readline.cursorTo(undefined, 1, 1, jest.fn())).toBe(true); - - expect(readline.cursorTo(writable, "a")).toBe(true); - expect(writable.data).toBe(""); - - writable.data = ""; - expect(readline.cursorTo(writable, "a", "b")).toBe(true); - expect(writable.data).toBe(""); - - writable.data = ""; - expect(() => readline.cursorTo(writable, "a", 1)).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_CURSOR_POS", - message: "Cannot set cursor row without setting its column", - }), - ); - expect(writable.data).toBe(""); - - writable.data = ""; - expect(readline.cursorTo(writable, 1, "a")).toBe(true); - expect(writable.data).toBe("\x1b[2G"); - - writable.data = ""; - expect(readline.cursorTo(writable, 1)).toBe(true); - expect(writable.data).toBe("\x1b[2G"); - - writable.data = ""; - expect(readline.cursorTo(writable, 1, 2)).toBe(true); - expect(writable.data).toBe("\x1b[3;2H"); - - writable.data = ""; - expect(readline.cursorTo(writable, 1, 2, jest.fn())).toBe(true); - expect(writable.data).toBe("\x1b[3;2H"); - - writable.data = ""; - expect(readline.cursorTo(writable, 1, jest.fn())).toBe(true); - expect(writable.data).toBe("\x1b[2G"); - - expect(() => { - readline.cursorTo(writable, 1, 1, null); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_TYPE", - }), - ); - - expect(() => { - readline.cursorTo(writable, NaN); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_VALUE", - }), - ); - - expect(() => { - readline.cursorTo(writable, 1, NaN); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_VALUE", - }), - ); - - expect(() => { - readline.cursorTo(writable, NaN, NaN); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_VALUE", - }), - ); -}); - -//<#END_FILE: test-readline-csi.js diff --git a/test/js/node/test/parallel/readline-emit-keypress-events.test.js b/test/js/node/test/parallel/readline-emit-keypress-events.test.js deleted file mode 100644 index 34eaeaa63b..0000000000 --- a/test/js/node/test/parallel/readline-emit-keypress-events.test.js +++ /dev/null @@ -1,75 +0,0 @@ -//#FILE: test-readline-emit-keypress-events.js -//#SHA1: 79b97832d1108222b690320e06d4028f73910125 -//----------------- -"use strict"; -// emitKeypressEvents is thoroughly tested in test-readline-keys.js. -// However, that test calls it implicitly. This is just a quick sanity check -// to verify that it works when called explicitly. - -const readline = require("readline"); -const { PassThrough } = require("stream"); - -const expectedSequence = ["f", "o", "o"]; -const expectedKeys = [ - { sequence: "f", name: "f", ctrl: false, meta: false, shift: false }, - { sequence: "o", name: "o", ctrl: false, meta: false, shift: false }, - { sequence: "o", name: "o", ctrl: false, meta: false, shift: false }, -]; - -test("emitKeypressEvents with stream", () => { - const stream = new PassThrough(); - const sequence = []; - const keys = []; - - readline.emitKeypressEvents(stream); - stream.on("keypress", (s, k) => { - sequence.push(s); - keys.push(k); - }); - stream.write("foo"); - - expect(sequence).toEqual(expectedSequence); - expect(keys).toEqual(expectedKeys); -}); - -test("emitKeypressEvents after attaching listener", () => { - const stream = new PassThrough(); - const sequence = []; - const keys = []; - - stream.on("keypress", (s, k) => { - sequence.push(s); - keys.push(k); - }); - readline.emitKeypressEvents(stream); - stream.write("foo"); - - expect(sequence).toEqual(expectedSequence); - expect(keys).toEqual(expectedKeys); -}); - -test("emitKeypressEvents with listener removal", () => { - const stream = new PassThrough(); - const sequence = []; - const keys = []; - const keypressListener = (s, k) => { - sequence.push(s); - keys.push(k); - }; - - stream.on("keypress", keypressListener); - readline.emitKeypressEvents(stream); - stream.removeListener("keypress", keypressListener); - stream.write("foo"); - - expect(sequence).toEqual([]); - expect(keys).toEqual([]); - - stream.on("keypress", keypressListener); - stream.write("foo"); - - expect(sequence).toEqual(expectedSequence); - expect(keys).toEqual(expectedKeys); -}); - -//<#END_FILE: test-readline-emit-keypress-events.js diff --git a/test/js/node/test/parallel/readline-interface-escapecodetimeout.test.js b/test/js/node/test/parallel/readline-interface-escapecodetimeout.test.js deleted file mode 100644 index c11f399b5a..0000000000 --- a/test/js/node/test/parallel/readline-interface-escapecodetimeout.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-readline-interface-escapecodetimeout.js -//#SHA1: 6d32b42ce02228999a37e3f7017ddd747b346d5c -//----------------- -"use strict"; - -const readline = require("readline"); -const EventEmitter = require("events").EventEmitter; - -// This test ensures that the escapeCodeTimeout option set correctly - -class FakeInput extends EventEmitter { - resume() {} - pause() {} - write() {} - end() {} -} - -test("escapeCodeTimeout option is set correctly", () => { - const fi = new FakeInput(); - const rli = new readline.Interface({ - input: fi, - output: fi, - escapeCodeTimeout: 50, - }); - expect(rli.escapeCodeTimeout).toBe(50); - rli.close(); -}); - -test.each([null, {}, NaN, "50"])("invalid escapeCodeTimeout input throws TypeError", invalidInput => { - const fi = new FakeInput(); - expect(() => { - const rli = new readline.Interface({ - input: fi, - output: fi, - escapeCodeTimeout: invalidInput, - }); - rli.close(); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_VALUE", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-readline-interface-escapecodetimeout.js diff --git a/test/js/node/test/parallel/readline-position.test.js b/test/js/node/test/parallel/readline-position.test.js deleted file mode 100644 index 967f5ba830..0000000000 --- a/test/js/node/test/parallel/readline-position.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-readline-position.js -//#SHA1: 652d50a766a0728968f155ca650ff2ba9f5c32a8 -//----------------- -"use strict"; -const { PassThrough } = require("stream"); -const readline = require("readline"); - -const ctrlU = { ctrl: true, name: "u" }; - -// Skip test if running in a dumb terminal -const isDumbTerminal = process.env.TERM === "dumb"; -if (isDumbTerminal) { - test.skip("Skipping test in dumb terminal", () => {}); -} else { - describe("readline position", () => { - let input; - let rl; - - beforeEach(() => { - input = new PassThrough(); - rl = readline.createInterface({ - terminal: true, - input: input, - prompt: "", - }); - }); - - afterEach(() => { - rl.close(); - }); - - test.each([ - [1, "a"], - [2, "ab"], - [2, "丁"], - [0, "\u0301"], // COMBINING ACUTE ACCENT - [1, "a\u0301"], // á - [0, "\u20DD"], // COMBINING ENCLOSING CIRCLE - [2, "a\u20DDb"], // a⃝b - [0, "\u200E"], // LEFT-TO-RIGHT MARK - ])('cursor position for "%s" should be %i', (expectedCursor, string) => { - rl.write(string); - expect(rl.getCursorPos().cols).toBe(expectedCursor); - rl.write(null, ctrlU); - }); - }); -} - -//<#END_FILE: test-readline-position.js diff --git a/test/js/node/test/parallel/readline-reopen.test.js b/test/js/node/test/parallel/readline-reopen.test.js deleted file mode 100644 index 490034547d..0000000000 --- a/test/js/node/test/parallel/readline-reopen.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-readline-reopen.js -//#SHA1: 39894fe94eb8e03222c86db2daa35a7b449447eb -//----------------- -"use strict"; - -// Regression test for https://github.com/nodejs/node/issues/13557 -// Tests that multiple subsequent readline instances can re-use an input stream. - -const readline = require("readline"); -const { PassThrough } = require("stream"); - -test("multiple readline instances can re-use an input stream", async () => { - const input = new PassThrough(); - const output = new PassThrough(); - - const rl1 = readline.createInterface({ - input, - output, - terminal: true, - }); - - const rl1LinePromise = new Promise(resolve => { - rl1.once("line", line => { - expect(line).toBe("foo"); - resolve(); - }); - }); - - // Write a line plus the first byte of a UTF-8 multibyte character to make sure - // that it doesn't get lost when closing the readline instance. - input.write( - Buffer.concat([ - Buffer.from("foo\n"), - Buffer.from([0xe2]), // Exactly one third of a ☃ snowman. - ]), - ); - - await rl1LinePromise; - rl1.close(); - - const rl2 = readline.createInterface({ - input, - output, - terminal: true, - }); - - const rl2LinePromise = new Promise(resolve => { - rl2.once("line", line => { - expect(line).toBe("☃bar"); - resolve(); - }); - }); - - input.write(Buffer.from([0x98, 0x83])); // The rest of the ☃ snowman. - input.write("bar\n"); - - await rl2LinePromise; - rl2.close(); -}); - -//<#END_FILE: test-readline-reopen.js diff --git a/test/js/node/test/parallel/ref-unref-return.test.js b/test/js/node/test/parallel/ref-unref-return.test.js deleted file mode 100644 index c17af7034a..0000000000 --- a/test/js/node/test/parallel/ref-unref-return.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-ref-unref-return.js -//#SHA1: c7275fa0ca17f1cee96244c48f1044ce4d2e67c1 -//----------------- -"use strict"; - -const net = require("net"); -const dgram = require("dgram"); - -test("ref and unref methods return the same instance", () => { - expect(new net.Server().ref()).toBeInstanceOf(net.Server); - expect(new net.Server().unref()).toBeInstanceOf(net.Server); - expect(new net.Socket().ref()).toBeInstanceOf(net.Socket); - expect(new net.Socket().unref()).toBeInstanceOf(net.Socket); - expect(new dgram.Socket("udp4").ref()).toBeInstanceOf(dgram.Socket); - expect(new dgram.Socket("udp6").unref()).toBeInstanceOf(dgram.Socket); -}); - -//<#END_FILE: test-ref-unref-return.js diff --git a/test/js/node/test/parallel/regression-object-prototype.test.js b/test/js/node/test/parallel/regression-object-prototype.test.js deleted file mode 100644 index aae31cdf28..0000000000 --- a/test/js/node/test/parallel/regression-object-prototype.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-regression-object-prototype.js -//#SHA1: 198fbbc217f8034fb1b81ce137b6b09a19acd0de -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -/* eslint-disable node-core/require-common-first, node-core/required-modules */ -"use strict"; - -test("Object prototype modification regression", () => { - const consoleSpy = jest.spyOn(console, "log"); - - Object.prototype.xadsadsdasasdxx = function () {}; - - expect(consoleSpy).not.toHaveBeenCalled(); - - console.log("puts after"); - - expect(consoleSpy).toHaveBeenCalledWith("puts after"); - expect(consoleSpy).toHaveBeenCalledTimes(1); - - consoleSpy.mockRestore(); -}); - -//<#END_FILE: test-regression-object-prototype.js diff --git a/test/js/node/test/parallel/require-empty-main.test.js b/test/js/node/test/parallel/require-empty-main.test.js deleted file mode 100644 index 93e53d4aed..0000000000 --- a/test/js/node/test/parallel/require-empty-main.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-require-empty-main.js -//#SHA1: 1c03cef0482df2bd119e42f418f54123675b532d -//----------------- -"use strict"; - -const path = require("path"); -const fixtures = require("../common/fixtures"); - -const where = fixtures.path("require-empty-main"); -const expected = path.join(where, "index.js"); - -const testRequireResolve = () => { - expect(require.resolve(where)).toBe(expected); - expect(require(where)).toBe(42); - expect(require.resolve(where)).toBe(expected); -}; - -test('A package.json with an empty "main" property should use index.js if present', testRequireResolve); - -test("require.resolve() should resolve to index.js for the same reason", testRequireResolve); - -test('Any "main" property that doesn\'t resolve to a file should result in index.js being used', testRequireResolve); - -test("Asynchronous test execution", done => { - setImmediate(() => { - testRequireResolve(); - done(); - }); -}); - -//<#END_FILE: test-require-empty-main.js diff --git a/test/js/node/test/parallel/require-extensions-main.test.js b/test/js/node/test/parallel/require-extensions-main.test.js deleted file mode 100644 index 47dee39446..0000000000 --- a/test/js/node/test/parallel/require-extensions-main.test.js +++ /dev/null @@ -1,15 +0,0 @@ -//#FILE: test-require-extensions-main.js -//#SHA1: c3dd50393bbc3eb542e40c67611fc48707ad3cba -//----------------- -"use strict"; - -const path = require("path"); - -test("require extensions main", () => { - const fixturesPath = path.join(__dirname, "..", "fixtures"); - const fixturesRequire = require(path.join(fixturesPath, "require-bin", "bin", "req.js")); - - expect(fixturesRequire).toBe(""); -}); - -//<#END_FILE: test-require-extensions-main.js diff --git a/test/js/node/test/parallel/require-process.test.js b/test/js/node/test/parallel/require-process.test.js deleted file mode 100644 index b6efc34c07..0000000000 --- a/test/js/node/test/parallel/require-process.test.js +++ /dev/null @@ -1,11 +0,0 @@ -//#FILE: test-require-process.js -//#SHA1: 699b499b3f906d140de0d550c310085aa0791c95 -//----------------- -"use strict"; - -test('require("process") should return global process reference', () => { - const nativeProcess = require("process"); - expect(nativeProcess).toBe(process); -}); - -//<#END_FILE: test-require-process.js diff --git a/test/js/node/test/parallel/require-unicode.test.js b/test/js/node/test/parallel/require-unicode.test.js deleted file mode 100644 index b4fa1f1f01..0000000000 --- a/test/js/node/test/parallel/require-unicode.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-require-unicode.js -//#SHA1: 3101d8c9e69745baeed9e6f09f32ca9ab31684a8 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); - -const tmpdir = require("../common/tmpdir"); - -test("require with unicode path", () => { - tmpdir.refresh(); - - const dirname = tmpdir.resolve("\u4e2d\u6587\u76ee\u5f55"); - fs.mkdirSync(dirname); - fs.writeFileSync(path.join(dirname, "file.js"), "module.exports = 42;"); - fs.writeFileSync(path.join(dirname, "package.json"), JSON.stringify({ name: "test", main: "file.js" })); - - expect(require(dirname)).toBe(42); - expect(require(path.join(dirname, "file.js"))).toBe(42); -}); - -//<#END_FILE: test-require-unicode.js diff --git a/test/js/node/test/parallel/runner-filter-warning.test.js b/test/js/node/test/parallel/runner-filter-warning.test.js deleted file mode 100644 index 56e4817871..0000000000 --- a/test/js/node/test/parallel/runner-filter-warning.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-runner-filter-warning.js -//#SHA1: c0887965f213c569d83684255054bf9e4bc27c29 -//----------------- -// Flags: --test-only -"use strict"; - -const { defaultMaxListeners } = require("node:events"); - -// Remove the process.on('warning') listener as it's not needed in Jest - -for (let i = 0; i < defaultMaxListeners + 1; ++i) { - test(`test ${i + 1}`, () => { - // Empty test body, just to create the specified number of tests - }); -} - -// Add a test to ensure no warnings are emitted -test("no warnings should be emitted", () => { - const warningListener = jest.fn(); - process.on("warning", warningListener); - - // Run all tests - return new Promise(resolve => { - setTimeout(() => { - expect(warningListener).not.toHaveBeenCalled(); - process.removeListener("warning", warningListener); - resolve(); - }, 100); // Wait a short time to ensure all tests have run - }); -}); - -//<#END_FILE: test-runner-filter-warning.js diff --git a/test/js/node/test/parallel/runner-root-after-with-refed-handles.test.js b/test/js/node/test/parallel/runner-root-after-with-refed-handles.test.js deleted file mode 100644 index 7656ca6ca0..0000000000 --- a/test/js/node/test/parallel/runner-root-after-with-refed-handles.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-runner-root-after-with-refed-handles.js -//#SHA1: cdfe0c601f7139167bfdc5fb5ab39ecf7b403a10 -//----------------- -"use strict"; - -const { createServer } = require("node:http"); - -let server; - -beforeAll(() => { - return new Promise((resolve, reject) => { - server = createServer(); - server.listen(0, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -}); - -afterAll(() => { - return new Promise(resolve => { - server.close(() => { - resolve(); - }); - }); -}); - -test("placeholder test", () => { - // This is a placeholder test to ensure the test suite runs - expect(true).toBe(true); -}); - -//<#END_FILE: test-runner-root-after-with-refed-handles.js diff --git a/test/js/node/test/parallel/safe-get-env.test.js b/test/js/node/test/parallel/safe-get-env.test.js deleted file mode 100644 index 118585eeab..0000000000 --- a/test/js/node/test/parallel/safe-get-env.test.js +++ /dev/null @@ -1,44 +0,0 @@ -//#FILE: test-safe-get-env.js -//#SHA1: 4d2553ac6bc24242b872b748a62aca05ca6fbcc8 -//----------------- -"use strict"; - -// This test has been converted to use Jest's API and removed dependencies on internal bindings. -// The original functionality of testing safeGetenv is preserved by mocking process.env. - -describe("safeGetenv", () => { - let originalEnv; - - beforeEach(() => { - originalEnv = { ...process.env }; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - test("should return the same values as process.env", () => { - // Mock some environment variables - process.env = { - TEST_VAR1: "value1", - TEST_VAR2: "value2", - TEST_VAR3: "value3", - }; - - // In a real scenario, we would use the actual safeGetenv function. - // For this test, we'll simulate its behavior by directly accessing process.env - const safeGetenv = key => process.env[key]; - - for (const oneEnv in process.env) { - expect(safeGetenv(oneEnv)).toBe(process.env[oneEnv]); - } - }); - - // Note: The following comment is preserved from the original test file - // FIXME(joyeecheung): this test is not entirely useful. To properly - // test this we could create a mismatch between the effective/real - // group/user id of a Node.js process and see if the environment variables - // are no longer available - but that might be tricky to set up reliably. -}); - -//<#END_FILE: test-safe-get-env.js diff --git a/test/js/node/test/parallel/sigint-infinite-loop.test.js b/test/js/node/test/parallel/sigint-infinite-loop.test.js deleted file mode 100644 index 8c511f99d9..0000000000 --- a/test/js/node/test/parallel/sigint-infinite-loop.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-sigint-infinite-loop.js -//#SHA1: 9c59ace1c99605bce14768e06d62a29e254e6116 -//----------------- -"use strict"; -// This test is to assert that we can SIGINT a script which loops forever. -// Ref(http): -// groups.google.com/group/nodejs-dev/browse_thread/thread/e20f2f8df0296d3f -const { spawn } = require("child_process"); - -test("SIGINT can kill an infinite loop", async () => { - console.log("start"); - - const c = spawn(process.execPath, ["-e", 'while(true) { console.log("hi"); }']); - - let sentKill = false; - - c.stdout.on("data", function (s) { - // Prevent race condition: - // Wait for the first bit of output from the child process - // so that we're sure that it's in the V8 event loop and not - // just in the startup phase of execution. - if (!sentKill) { - c.kill("SIGINT"); - console.log("SIGINT infinite-loop.js"); - sentKill = true; - } - }); - - await new Promise(resolve => { - c.on("exit", code => { - expect(code).not.toBe(0); - console.log("killed infinite-loop.js"); - resolve(); - }); - }); - - expect(sentKill).toBe(true); -}); - -//<#END_FILE: test-sigint-infinite-loop.js diff --git a/test/js/node/test/parallel/signal-safety.test.js b/test/js/node/test/parallel/signal-safety.test.js deleted file mode 100644 index 51f6c19fe8..0000000000 --- a/test/js/node/test/parallel/signal-safety.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-signal-safety.js -//#SHA1: 49090f0605b0ba01c323138ac8e94d423925cbf2 -//----------------- -"use strict"; - -test("Signal `this` safety", () => { - // We cannot use internal bindings in Jest, so we'll mock the Signal class - class Signal { - start() { - // This method should be called with the correct 'this' context - if (!(this instanceof Signal)) { - throw new TypeError("Illegal invocation"); - } - } - } - - const s = new Signal(); - const nots = { start: s.start }; - - expect(() => { - nots.start(9); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-signal-safety.js diff --git a/test/js/node/test/parallel/signal-unregister.test.js b/test/js/node/test/parallel/signal-unregister.test.js deleted file mode 100644 index 0f7b55ce66..0000000000 --- a/test/js/node/test/parallel/signal-unregister.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-signal-unregister.js -//#SHA1: 44f7b12e1cb3d5e2bc94cb244f52d5a6a591d8f4 -//----------------- -"use strict"; - -const { spawn } = require("child_process"); -const path = require("path"); - -const fixturesPath = path.resolve(__dirname, "..", "fixtures"); - -test("Child process exits on SIGINT", () => { - const child = spawn(process.argv[0], [path.join(fixturesPath, "should_exit.js")]); - - return new Promise(resolve => { - child.stdout.once("data", () => { - child.kill("SIGINT"); - }); - - child.on("exit", (exitCode, signalCode) => { - expect(exitCode).toBeNull(); - expect(signalCode).toBe("SIGINT"); - resolve(); - }); - }); -}); - -//<#END_FILE: test-signal-unregister.js diff --git a/test/js/node/test/parallel/stdin-from-file-spawn.test.js b/test/js/node/test/parallel/stdin-from-file-spawn.test.js deleted file mode 100644 index aad7445891..0000000000 --- a/test/js/node/test/parallel/stdin-from-file-spawn.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-stdin-from-file-spawn.js -//#SHA1: 1f8f432985d08b841ebb2c8142b865ae417737a1 -//----------------- -"use strict"; - -const process = require("process"); -const { execSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -let defaultShell; -if (process.platform === "linux" || process.platform === "darwin") { - defaultShell = "/bin/sh"; -} else if (process.platform === "win32") { - defaultShell = "cmd.exe"; -} else { - it.skip("This test exists only on Linux/Win32/OSX", () => {}); -} - -if (defaultShell) { - test("stdin from file spawn", () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-")); - const tmpCmdFile = path.join(tmpDir, "test-stdin-from-file-spawn-cmd"); - const tmpJsFile = path.join(tmpDir, "test-stdin-from-file-spawn.js"); - - fs.writeFileSync(tmpCmdFile, "echo hello"); - fs.writeFileSync( - tmpJsFile, - ` - 'use strict'; - const { spawn } = require('child_process'); - // Reference the object to invoke the getter - process.stdin; - setTimeout(() => { - let ok = false; - const child = spawn(process.env.SHELL || '${defaultShell}', - [], { stdio: ['inherit', 'pipe'] }); - child.stdout.on('data', () => { - ok = true; - }); - child.on('close', () => { - process.exit(ok ? 0 : -1); - }); - }, 100); - `, - ); - - expect(() => { - execSync(`${process.argv[0]} ${tmpJsFile} < ${tmpCmdFile}`); - }).not.toThrow(); - - // Clean up - fs.unlinkSync(tmpCmdFile); - fs.unlinkSync(tmpJsFile); - fs.rmdirSync(tmpDir); - }); -} - -//<#END_FILE: test-stdin-from-file-spawn.js diff --git a/test/js/node/test/parallel/stdin-hang.test.js b/test/js/node/test/parallel/stdin-hang.test.js deleted file mode 100644 index b9f9dd71a9..0000000000 --- a/test/js/node/test/parallel/stdin-hang.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-stdin-hang.js -//#SHA1: f28512893c8d9cd6500247aa87881347638eb616 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// This test *only* verifies that invoking the stdin getter does not -// cause node to hang indefinitely. -// If it does, then the test-runner will nuke it. - -test("process.stdin getter does not cause indefinite hang", () => { - // invoke the getter. - process.stdin; // eslint-disable-line no-unused-expressions - - // If we reach this point, it means the process didn't hang - expect(true).toBe(true); -}); - -test("Console output", () => { - const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); - - console.error("Should exit normally now."); - - expect(consoleErrorSpy).toHaveBeenCalledWith("Should exit normally now."); - - consoleErrorSpy.mockRestore(); -}); - -//<#END_FILE: test-stdin-hang.js diff --git a/test/js/node/test/parallel/stdin-pause-resume-sync.test.js b/test/js/node/test/parallel/stdin-pause-resume-sync.test.js deleted file mode 100644 index ed69ac4afe..0000000000 --- a/test/js/node/test/parallel/stdin-pause-resume-sync.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-stdin-pause-resume-sync.js -//#SHA1: 2256a91336e221a1770b59ebebb4afb406a770dd -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("stdin pause and resume", () => { - const consoleSpy = jest.spyOn(console, "error"); - const stdinResumeSpy = jest.spyOn(process.stdin, "resume"); - const stdinPauseSpy = jest.spyOn(process.stdin, "pause"); - - console.error("before opening stdin"); - process.stdin.resume(); - console.error("stdin opened"); - console.error("pausing stdin"); - process.stdin.pause(); - console.error("opening again"); - process.stdin.resume(); - console.error("pausing again"); - process.stdin.pause(); - console.error("should exit now"); - - expect(consoleSpy).toHaveBeenCalledTimes(6); - expect(consoleSpy).toHaveBeenNthCalledWith(1, "before opening stdin"); - expect(consoleSpy).toHaveBeenNthCalledWith(2, "stdin opened"); - expect(consoleSpy).toHaveBeenNthCalledWith(3, "pausing stdin"); - expect(consoleSpy).toHaveBeenNthCalledWith(4, "opening again"); - expect(consoleSpy).toHaveBeenNthCalledWith(5, "pausing again"); - expect(consoleSpy).toHaveBeenNthCalledWith(6, "should exit now"); - - expect(stdinResumeSpy).toHaveBeenCalledTimes(2); - expect(stdinPauseSpy).toHaveBeenCalledTimes(2); - - consoleSpy.mockRestore(); - stdinResumeSpy.mockRestore(); - stdinPauseSpy.mockRestore(); -}); - -//<#END_FILE: test-stdin-pause-resume-sync.js diff --git a/test/js/node/test/parallel/stdin-pause-resume.test.js b/test/js/node/test/parallel/stdin-pause-resume.test.js deleted file mode 100644 index f172615922..0000000000 --- a/test/js/node/test/parallel/stdin-pause-resume.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-stdin-pause-resume.js -//#SHA1: 941cff7d9e52f178538b4fdd09458bb2fc6a12b7 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("stdin pause and resume", async () => { - const consoleSpy = jest.spyOn(console, "error"); - const stdinResumeSpy = jest.spyOn(process.stdin, "resume"); - const stdinPauseSpy = jest.spyOn(process.stdin, "pause"); - - console.error("before opening stdin"); - process.stdin.resume(); - console.error("stdin opened"); - - await new Promise(resolve => setTimeout(resolve, 1)); - - console.error("pausing stdin"); - process.stdin.pause(); - - await new Promise(resolve => setTimeout(resolve, 1)); - - console.error("opening again"); - process.stdin.resume(); - - await new Promise(resolve => setTimeout(resolve, 1)); - - console.error("pausing again"); - process.stdin.pause(); - console.error("should exit now"); - - expect(consoleSpy).toHaveBeenCalledTimes(6); - expect(consoleSpy).toHaveBeenNthCalledWith(1, "before opening stdin"); - expect(consoleSpy).toHaveBeenNthCalledWith(2, "stdin opened"); - expect(consoleSpy).toHaveBeenNthCalledWith(3, "pausing stdin"); - expect(consoleSpy).toHaveBeenNthCalledWith(4, "opening again"); - expect(consoleSpy).toHaveBeenNthCalledWith(5, "pausing again"); - expect(consoleSpy).toHaveBeenNthCalledWith(6, "should exit now"); - - expect(stdinResumeSpy).toHaveBeenCalledTimes(2); - expect(stdinPauseSpy).toHaveBeenCalledTimes(2); - - consoleSpy.mockRestore(); - stdinResumeSpy.mockRestore(); - stdinPauseSpy.mockRestore(); -}); - -//<#END_FILE: test-stdin-pause-resume.js diff --git a/test/js/node/test/parallel/stdin-pipe-resume.test.js b/test/js/node/test/parallel/stdin-pipe-resume.test.js deleted file mode 100644 index 7a38ca23c3..0000000000 --- a/test/js/node/test/parallel/stdin-pipe-resume.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-stdin-pipe-resume.js -//#SHA1: 6775f16e6a971590e3a5308d4e3678029be47411 -//----------------- -"use strict"; - -// This tests that piping stdin will cause it to resume() as well. - -const { spawn } = require("child_process"); - -if (process.argv[2] === "child") { - process.stdin.pipe(process.stdout); -} else { - test("piping stdin causes it to resume", done => { - const buffers = []; - const child = spawn(process.execPath, [__filename, "child"]); - - child.stdout.on("data", c => { - buffers.push(c); - }); - - child.stdout.on("close", () => { - const b = Buffer.concat(buffers).toString(); - expect(b).toBe("Hello, world\n"); - done(); - }); - - child.stdin.write("Hel"); - child.stdin.write("lo,"); - child.stdin.write(" wo"); - - setTimeout(() => { - child.stdin.write("rld\n"); - child.stdin.end(); - }, 10); - }); -} - -//<#END_FILE: test-stdin-pipe-resume.js diff --git a/test/js/node/test/parallel/stdin-resume-pause.test.js b/test/js/node/test/parallel/stdin-resume-pause.test.js deleted file mode 100644 index b455110d35..0000000000 --- a/test/js/node/test/parallel/stdin-resume-pause.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-stdin-resume-pause.js -//#SHA1: ef96cec1b4e29ec6b96da37fd44e3bbdb50e1393 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -test("process.stdin can be resumed and paused", () => { - const originalResume = process.stdin.resume; - const originalPause = process.stdin.pause; - - const mockResume = jest.fn(); - const mockPause = jest.fn(); - - process.stdin.resume = mockResume; - process.stdin.pause = mockPause; - - process.stdin.resume(); - process.stdin.pause(); - - expect(mockResume).toHaveBeenCalledTimes(1); - expect(mockPause).toHaveBeenCalledTimes(1); - - // Restore original methods - process.stdin.resume = originalResume; - process.stdin.pause = originalPause; -}); - -//<#END_FILE: test-stdin-resume-pause.js diff --git a/test/js/node/test/parallel/stdin-script-child-option.test.js b/test/js/node/test/parallel/stdin-script-child-option.test.js deleted file mode 100644 index 3dc4f26ce6..0000000000 --- a/test/js/node/test/parallel/stdin-script-child-option.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-stdin-script-child-option.js -//#SHA1: 80d38c88249e1ceb4ef9b029e6910c91dd08ccc5 -//----------------- -"use strict"; - -const { spawn } = require("child_process"); - -test("child process receives option from command line", done => { - const expected = "--option-to-be-seen-on-child"; - const child = spawn(process.execPath, ["-", expected], { stdio: "pipe" }); - - child.stdin.end("console.log(process.argv[2])"); - - let actual = ""; - child.stdout.setEncoding("utf8"); - child.stdout.on("data", chunk => (actual += chunk)); - child.stdout.on("end", () => { - expect(actual.trim()).toBe(expected); - done(); - }); -}); - -//<#END_FILE: test-stdin-script-child-option.js diff --git a/test/js/node/test/parallel/stdio-pipe-access.test.js b/test/js/node/test/parallel/stdio-pipe-access.test.js deleted file mode 100644 index 026d7ae891..0000000000 --- a/test/js/node/test/parallel/stdio-pipe-access.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-stdio-pipe-access.js -//#SHA1: a1f6e6c04c96ad54cbbd4270958bcb2934a3ce6b -//----------------- -"use strict"; - -// Test if Node handles accessing process.stdin if it is a redirected -// pipe without deadlocking -const { spawn, spawnSync } = require("child_process"); - -const numTries = 5; -const who = process.argv.length <= 2 ? "runner" : process.argv[2]; - -// Skip test for Workers as they don't have process-like stdio -if (typeof Worker !== "undefined") { - test.skip("Workers don't have process-like stdio", () => {}); -} else { - test("stdio pipe access", () => { - switch (who) { - case "runner": - for (let num = 0; num < numTries; ++num) { - const result = spawnSync(process.argv0, [process.argv[1], "parent"], { stdio: "inherit" }); - expect(result.status).toBe(0); - } - break; - case "parent": { - const middle = spawn(process.argv0, [process.argv[1], "middle"], { stdio: "pipe" }); - middle.stdout.on("data", () => {}); - break; - } - case "middle": - spawn(process.argv0, [process.argv[1], "bottom"], { stdio: [process.stdin, process.stdout, process.stderr] }); - break; - case "bottom": - process.stdin; // eslint-disable-line no-unused-expressions - break; - } - }); -} - -//<#END_FILE: test-stdio-pipe-access.js diff --git a/test/js/node/test/parallel/stdio-pipe-stderr.test.js b/test/js/node/test/parallel/stdio-pipe-stderr.test.js deleted file mode 100644 index eaae7ef20c..0000000000 --- a/test/js/node/test/parallel/stdio-pipe-stderr.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-stdio-pipe-stderr.js -//#SHA1: 5a30748a31ac72c12cd7438b96a8e09c7c8f07f7 -//----------------- -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const { spawnSync } = require("child_process"); - -// Test that invoking node with require, and piping stderr to file, -// does not result in exception, -// see: https://github.com/nodejs/node/issues/11257 - -describe("stdio pipe stderr", () => { - const tmpdir = path.join(__dirname, "tmp"); - const fakeModulePath = path.join(tmpdir, "batman.js"); - const stderrOutputPath = path.join(tmpdir, "stderr-output.txt"); - - beforeAll(() => { - if (!fs.existsSync(tmpdir)) { - fs.mkdirSync(tmpdir, { recursive: true }); - } - }); - - afterAll(() => { - fs.rmSync(tmpdir, { recursive: true, force: true }); - }); - - test("piping stderr to file should not result in exception", done => { - // We need to redirect stderr to a file to produce #11257 - const stream = fs.createWriteStream(stderrOutputPath); - - // The error described in #11257 only happens when we require a - // non-built-in module. - fs.writeFileSync(fakeModulePath, "", "utf8"); - - stream.on("open", () => { - spawnSync(process.execPath, { - input: `require(${JSON.stringify(fakeModulePath)})`, - stdio: ["pipe", "pipe", stream], - }); - - const stderr = fs.readFileSync(stderrOutputPath, "utf8").trim(); - expect(stderr).toBe(""); - - stream.end(); - fs.unlinkSync(stderrOutputPath); - fs.unlinkSync(fakeModulePath); - done(); - }); - }); -}); - -//<#END_FILE: test-stdio-pipe-stderr.js diff --git a/test/js/node/test/parallel/stdout-cannot-be-closed-child-process-pipe.test.js b/test/js/node/test/parallel/stdout-cannot-be-closed-child-process-pipe.test.js deleted file mode 100644 index 1626f01a67..0000000000 --- a/test/js/node/test/parallel/stdout-cannot-be-closed-child-process-pipe.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-stdout-cannot-be-closed-child-process-pipe.js -//#SHA1: 405380c20ca8313c3f58109a4928d90eae9b79b9 -//----------------- -"use strict"; - -const { spawn } = require("child_process"); - -if (process.argv[2] === "child") { - process.stdout.end("foo"); -} else { - test("stdout cannot be closed in child process pipe", done => { - const child = spawn(process.execPath, [__filename, "child"]); - let out = ""; - let err = ""; - - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - - child.stdout.on("data", c => { - out += c; - }); - child.stderr.on("data", c => { - err += c; - }); - - child.on("close", (code, signal) => { - expect(code).toBe(0); - expect(err).toBe(""); - expect(out).toBe("foo"); - console.log("ok"); - done(); - }); - }); -} - -//<#END_FILE: test-stdout-cannot-be-closed-child-process-pipe.js diff --git a/test/js/node/test/parallel/stdout-stderr-write.test.js b/test/js/node/test/parallel/stdout-stderr-write.test.js deleted file mode 100644 index 3ee9dba103..0000000000 --- a/test/js/node/test/parallel/stdout-stderr-write.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-stdout-stderr-write.js -//#SHA1: 8811b3e776c91c3300ee6eb32b7d1ccb4d3d5d6a -//----------------- -"use strict"; - -// This test checks if process.stderr.write() and process.stdout.write() return true - -test("process.stderr.write() and process.stdout.write() return true", () => { - expect(process.stderr.write("asd")).toBe(true); - expect(process.stdout.write("asd")).toBe(true); -}); - -//<#END_FILE: test-stdout-stderr-write.js diff --git a/test/js/node/test/parallel/stream-add-abort-signal.test.js b/test/js/node/test/parallel/stream-add-abort-signal.test.js deleted file mode 100644 index 1a704c5d5b..0000000000 --- a/test/js/node/test/parallel/stream-add-abort-signal.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-stream-add-abort-signal.js -//#SHA1: 8caf14dd370aac5a01fad14026a78e1994dd3e4e -//----------------- -"use strict"; - -const { addAbortSignal, Readable } = require("stream"); - -describe("addAbortSignal", () => { - test("throws error for invalid signal", () => { - expect(() => { - addAbortSignal("INVALID_SIGNAL"); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - }); - - test("throws error for invalid stream", () => { - const ac = new AbortController(); - expect(() => { - addAbortSignal(ac.signal, "INVALID_STREAM"); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - }); -}); - -describe("addAbortSignalNoValidate", () => { - test("returns the same readable stream", () => { - const r = new Readable({ - read: () => {}, - }); - - // Since addAbortSignalNoValidate is an internal function, - // we'll skip this test in the Jest version. - // In a real-world scenario, we'd need to mock or implement this function. - expect(true).toBe(true); - }); -}); - -//<#END_FILE: test-stream-add-abort-signal.js diff --git a/test/js/node/test/parallel/stream-base-prototype-accessors-enumerability.test.js b/test/js/node/test/parallel/stream-base-prototype-accessors-enumerability.test.js deleted file mode 100644 index d1776dd952..0000000000 --- a/test/js/node/test/parallel/stream-base-prototype-accessors-enumerability.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-stream-base-prototype-accessors-enumerability.js -//#SHA1: a5423c2b42bae0fbdd1530553de4d40143c010cf -//----------------- -"use strict"; - -// This tests that the prototype accessors added by StreamBase::AddMethods -// are not enumerable. They could be enumerated when inspecting the prototype -// with util.inspect or the inspector protocol. - -// Or anything that calls StreamBase::AddMethods when setting up its prototype -const TTY = process.binding("tty_wrap").TTY; - -test("StreamBase prototype accessors are not enumerable", () => { - const ttyIsEnumerable = Object.prototype.propertyIsEnumerable.bind(TTY); - expect(ttyIsEnumerable("bytesRead")).toBe(false); - expect(ttyIsEnumerable("fd")).toBe(false); - expect(ttyIsEnumerable("_externalStream")).toBe(false); -}); - -//<#END_FILE: test-stream-base-prototype-accessors-enumerability.js diff --git a/test/js/node/test/parallel/stream-big-packet.test.js b/test/js/node/test/parallel/stream-big-packet.test.js deleted file mode 100644 index 01b34975c9..0000000000 --- a/test/js/node/test/parallel/stream-big-packet.test.js +++ /dev/null @@ -1,73 +0,0 @@ -//#FILE: test-stream-big-packet.js -//#SHA1: ce0f56afc4946041321028a7128eca47765fd53d -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const stream = require("stream"); - -let passed = false; - -class TestStream extends stream.Transform { - _transform(chunk, encoding, done) { - if (!passed) { - // Char 'a' only exists in the last write - passed = chunk.toString().includes("a"); - } - done(); - } -} - -test("Large buffer is handled properly by Writable Stream", done => { - const s1 = new stream.Transform({ - transform(chunk, encoding, cb) { - process.nextTick(cb, null, chunk); - }, - }); - const s2 = new stream.PassThrough(); - const s3 = new TestStream(); - s1.pipe(s3); - // Don't let s2 auto close which may close s3 - s2.pipe(s3, { end: false }); - - // We must write a buffer larger than highWaterMark - const big = Buffer.alloc(s1.writableHighWaterMark + 1, "x"); - - // Since big is larger than highWaterMark, it will be buffered internally. - expect(s1.write(big)).toBe(false); - // 'tiny' is small enough to pass through internal buffer. - expect(s2.write("tiny")).toBe(true); - - // Write some small data in next IO loop, which will never be written to s3 - // Because 'drain' event is not emitted from s1 and s1 is still paused - setImmediate(s1.write.bind(s1), "later"); - - // Assert after two IO loops when all operations have been done. - setImmediate(() => { - setImmediate(() => { - expect(passed).toBe(true); - done(); - }); - }); -}); - -//<#END_FILE: test-stream-big-packet.js diff --git a/test/js/node/test/parallel/stream-big-push.test.js b/test/js/node/test/parallel/stream-big-push.test.js deleted file mode 100644 index 664dc59bea..0000000000 --- a/test/js/node/test/parallel/stream-big-push.test.js +++ /dev/null @@ -1,87 +0,0 @@ -//#FILE: test-stream-big-push.js -//#SHA1: 833718bae7463fa469ed5acc9a1c69aa321785b7 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const stream = require("stream"); -const str = "asdfasdfasdfasdfasdf"; - -test("stream big push", async () => { - const r = new stream.Readable({ - highWaterMark: 5, - encoding: "utf8", - }); - - let reads = 0; - - function _read() { - if (reads === 0) { - setTimeout(() => { - r.push(str); - }, 1); - reads++; - } else if (reads === 1) { - const ret = r.push(str); - expect(ret).toBe(false); - reads++; - } else { - r.push(null); - } - } - - r._read = jest.fn(_read); - - const endPromise = new Promise(resolve => { - r.on("end", resolve); - }); - - // Push some data in to start. - // We've never gotten any read event at this point. - const ret = r.push(str); - // Should be false. > hwm - expect(ret).toBe(false); - let chunk = r.read(); - expect(chunk).toBe(str); - chunk = r.read(); - expect(chunk).toBeNull(); - - await new Promise(resolve => { - r.once("readable", () => { - // This time, we'll get *all* the remaining data, because - // it's been added synchronously, as the read WOULD take - // us below the hwm, and so it triggered a _read() again, - // which synchronously added more, which we then return. - chunk = r.read(); - expect(chunk).toBe(str + str); - - chunk = r.read(); - expect(chunk).toBeNull(); - resolve(); - }); - }); - - await endPromise; - expect(r._read).toHaveBeenCalledTimes(3); -}); - -//<#END_FILE: test-stream-big-push.js diff --git a/test/js/node/test/parallel/stream-decoder-objectmode.test.js b/test/js/node/test/parallel/stream-decoder-objectmode.test.js deleted file mode 100644 index a78775aa5d..0000000000 --- a/test/js/node/test/parallel/stream-decoder-objectmode.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-stream-decoder-objectmode.js -//#SHA1: 373c0c494e625b8264fae296b46f16a5cd5a9ef8 -//----------------- -"use strict"; - -const stream = require("stream"); - -test("stream.Readable with objectMode and utf16le encoding", () => { - const readable = new stream.Readable({ - read: () => {}, - encoding: "utf16le", - objectMode: true, - }); - - readable.push(Buffer.from("abc", "utf16le")); - readable.push(Buffer.from("def", "utf16le")); - readable.push(null); - - // Without object mode, these would be concatenated into a single chunk. - expect(readable.read()).toBe("abc"); - expect(readable.read()).toBe("def"); - expect(readable.read()).toBeNull(); -}); - -//<#END_FILE: test-stream-decoder-objectmode.js diff --git a/test/js/node/test/parallel/stream-destroy-event-order.test.js b/test/js/node/test/parallel/stream-destroy-event-order.test.js deleted file mode 100644 index 480b80267d..0000000000 --- a/test/js/node/test/parallel/stream-destroy-event-order.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-stream-destroy-event-order.js -//#SHA1: 0d5e12d85e093a1d7c118a2e15cf0c38c1ab96f6 -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -test("Readable stream destroy event order", () => { - const rs = new Readable({ - read() {}, - }); - - let closed = false; - let errored = false; - - rs.on("close", () => { - closed = true; - expect(errored).toBe(true); - }); - - rs.on("error", () => { - errored = true; - expect(closed).toBe(false); - }); - - rs.destroy(new Error("kaboom")); - - return new Promise(resolve => { - rs.on("close", () => { - expect(closed).toBe(true); - expect(errored).toBe(true); - resolve(); - }); - }); -}); - -//<#END_FILE: test-stream-destroy-event-order.js diff --git a/test/js/node/test/parallel/stream-duplex-props.test.js b/test/js/node/test/parallel/stream-duplex-props.test.js deleted file mode 100644 index 8fbc7f3cff..0000000000 --- a/test/js/node/test/parallel/stream-duplex-props.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-stream-duplex-props.js -//#SHA1: ae1e09a8b6631f457ad8587544a2a245f3c2ef04 -//----------------- -"use strict"; - -const { Duplex } = require("stream"); - -test("Duplex stream with same object mode and high water mark for readable and writable", () => { - const d = new Duplex({ - objectMode: true, - highWaterMark: 100, - }); - - expect(d.writableObjectMode).toBe(true); - expect(d.writableHighWaterMark).toBe(100); - expect(d.readableObjectMode).toBe(true); - expect(d.readableHighWaterMark).toBe(100); -}); - -test("Duplex stream with different object mode and high water mark for readable and writable", () => { - const d = new Duplex({ - readableObjectMode: false, - readableHighWaterMark: 10, - writableObjectMode: true, - writableHighWaterMark: 100, - }); - - expect(d.writableObjectMode).toBe(true); - expect(d.writableHighWaterMark).toBe(100); - expect(d.readableObjectMode).toBe(false); - expect(d.readableHighWaterMark).toBe(10); -}); - -//<#END_FILE: test-stream-duplex-props.js diff --git a/test/js/node/test/parallel/stream-duplex-readable-end.test.js b/test/js/node/test/parallel/stream-duplex-readable-end.test.js deleted file mode 100644 index 7ccfe779b6..0000000000 --- a/test/js/node/test/parallel/stream-duplex-readable-end.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-stream-duplex-readable-end.js -//#SHA1: 8cecff6703e081aeb94abe2b37332bc7aef28b1d -//----------------- -"use strict"; - -// https://github.com/nodejs/node/issues/35926 -const stream = require("stream"); - -test("stream duplex readable end", done => { - let loops = 5; - - const src = new stream.Readable({ - highWaterMark: 16 * 1024, - read() { - if (loops--) this.push(Buffer.alloc(20000)); - }, - }); - - const dst = new stream.Transform({ - highWaterMark: 16 * 1024, - transform(chunk, output, fn) { - this.push(null); - fn(); - }, - }); - - src.pipe(dst); - - dst.on("data", () => {}); - dst.on("end", () => { - expect(loops).toBe(3); - expect(src.isPaused()).toBe(true); - done(); - }); -}); - -//<#END_FILE: test-stream-duplex-readable-end.js diff --git a/test/js/node/test/parallel/stream-duplex-readable-writable.test.js b/test/js/node/test/parallel/stream-duplex-readable-writable.test.js deleted file mode 100644 index c37a46d6dd..0000000000 --- a/test/js/node/test/parallel/stream-duplex-readable-writable.test.js +++ /dev/null @@ -1,95 +0,0 @@ -//#FILE: test-stream-duplex-readable-writable.js -//#SHA1: d56d29f5fbb8adc3d61708839985f0ea7ffb9b5c -//----------------- -"use strict"; - -const { Duplex } = require("stream"); - -test("Duplex with readable false", () => { - const duplex = new Duplex({ - readable: false, - }); - expect(duplex.readable).toBe(false); - duplex.push("asd"); - - const errorHandler = jest.fn(); - duplex.on("error", errorHandler); - - const dataHandler = jest.fn(); - duplex.on("data", dataHandler); - - const endHandler = jest.fn(); - duplex.on("end", endHandler); - - return new Promise(resolve => { - setImmediate(() => { - expect(errorHandler).toHaveBeenCalledTimes(1); - expect(errorHandler).toHaveBeenCalledWith( - expect.objectContaining({ - code: "ERR_STREAM_PUSH_AFTER_EOF", - message: expect.any(String), - }), - ); - expect(dataHandler).not.toHaveBeenCalled(); - expect(endHandler).not.toHaveBeenCalled(); - resolve(); - }); - }); -}); - -test("Duplex with writable false", () => { - const writeSpy = jest.fn(); - const duplex = new Duplex({ - writable: false, - write: writeSpy, - }); - expect(duplex.writable).toBe(false); - duplex.write("asd"); - - const errorHandler = jest.fn(); - duplex.on("error", errorHandler); - - const finishHandler = jest.fn(); - duplex.on("finish", finishHandler); - - return new Promise(resolve => { - setImmediate(() => { - expect(errorHandler).toHaveBeenCalledTimes(1); - expect(errorHandler).toHaveBeenCalledWith( - expect.objectContaining({ - code: "ERR_STREAM_WRITE_AFTER_END", - message: expect.any(String), - }), - ); - expect(writeSpy).not.toHaveBeenCalled(); - expect(finishHandler).not.toHaveBeenCalled(); - resolve(); - }); - }); -}); - -test("Duplex with readable false and async iteration", async () => { - const duplex = new Duplex({ - readable: false, - }); - expect(duplex.readable).toBe(false); - - const dataHandler = jest.fn(); - duplex.on("data", dataHandler); - - const endHandler = jest.fn(); - duplex.on("end", endHandler); - - async function run() { - for await (const chunk of duplex) { - expect(chunk).toBeFalsy(); // This should never be reached - } - } - - await run(); - - expect(dataHandler).not.toHaveBeenCalled(); - expect(endHandler).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-stream-duplex-readable-writable.js diff --git a/test/js/node/test/parallel/stream-duplex-writable-finished.test.js b/test/js/node/test/parallel/stream-duplex-writable-finished.test.js deleted file mode 100644 index 0a2400bb78..0000000000 --- a/test/js/node/test/parallel/stream-duplex-writable-finished.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-stream-duplex-writable-finished.js -//#SHA1: ba8c61c576c3a900076baa134c8a0d6876e84db5 -//----------------- -"use strict"; - -const { Duplex } = require("stream"); - -// basic -test("Duplex.prototype has writableFinished", () => { - expect(Object.hasOwn(Duplex.prototype, "writableFinished")).toBe(true); -}); - -// event -test("writableFinished state changes correctly", done => { - const duplex = new Duplex(); - - duplex._write = (chunk, encoding, cb) => { - // The state finished should start in false. - expect(duplex.writableFinished).toBe(false); - cb(); - }; - - duplex.on("finish", () => { - expect(duplex.writableFinished).toBe(true); - }); - - duplex.end("testing finished state", () => { - expect(duplex.writableFinished).toBe(true); - done(); - }); -}); - -//<#END_FILE: test-stream-duplex-writable-finished.js diff --git a/test/js/node/test/parallel/stream-end-of-streams.test.js b/test/js/node/test/parallel/stream-end-of-streams.test.js deleted file mode 100644 index af768bc70e..0000000000 --- a/test/js/node/test/parallel/stream-end-of-streams.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-stream-end-of-streams.js -//#SHA1: 3fa13e31cef06059026b0fcf90c151a8a975752c -//----------------- -"use strict"; - -const { Duplex, finished } = require("stream"); - -test("finished function with invalid stream", () => { - // Passing empty object to mock invalid stream - // should throw error - expect(() => { - finished({}, () => {}); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); -}); - -test("finished function with valid stream", () => { - const streamObj = new Duplex(); - streamObj.end(); - // Below code should not throw any errors as the - // streamObj is `Stream` - expect(() => { - finished(streamObj, () => {}); - }).not.toThrow(); -}); - -//<#END_FILE: test-stream-end-of-streams.js diff --git a/test/js/node/test/parallel/stream-end-paused.test.js b/test/js/node/test/parallel/stream-end-paused.test.js deleted file mode 100644 index 1022f980ef..0000000000 --- a/test/js/node/test/parallel/stream-end-paused.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-stream-end-paused.js -//#SHA1: 4ebe901f30ba0469bb75c7f9f7ba5316ceb271a5 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { Readable } = require("stream"); - -test("end event for paused 0-length streams", done => { - // Make sure we don't miss the end event for paused 0-length streams - - const stream = new Readable(); - let calledRead = false; - stream._read = function () { - expect(calledRead).toBe(false); - calledRead = true; - this.push(null); - }; - - stream.on("data", () => { - throw new Error("should not ever get data"); - }); - stream.pause(); - - setTimeout(() => { - stream.on("end", () => { - expect(calledRead).toBe(true); - done(); - }); - stream.resume(); - }, 1); -}); - -//<#END_FILE: test-stream-end-paused.js diff --git a/test/js/node/test/parallel/stream-events-prepend.test.js b/test/js/node/test/parallel/stream-events-prepend.test.js deleted file mode 100644 index 590421f79d..0000000000 --- a/test/js/node/test/parallel/stream-events-prepend.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-stream-events-prepend.js -//#SHA1: db830318b8c5a2e990a75320319c8fcd96fa760a -//----------------- -"use strict"; -const stream = require("stream"); - -class Writable extends stream.Writable { - constructor() { - super(); - this.prependListener = undefined; - } - - _write(chunk, end, cb) { - cb(); - } -} - -class Readable extends stream.Readable { - _read() { - this.push(null); - } -} - -test("pipe event is emitted even when prependListener is undefined", () => { - const w = new Writable(); - const pipeSpy = jest.fn(); - w.on("pipe", pipeSpy); - - const r = new Readable(); - r.pipe(w); - - expect(pipeSpy).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-stream-events-prepend.js diff --git a/test/js/node/test/parallel/stream-once-readable-pipe.test.js b/test/js/node/test/parallel/stream-once-readable-pipe.test.js deleted file mode 100644 index 71b0c32698..0000000000 --- a/test/js/node/test/parallel/stream-once-readable-pipe.test.js +++ /dev/null @@ -1,75 +0,0 @@ -//#FILE: test-stream-once-readable-pipe.js -//#SHA1: 4f12e7a8a1c06ba3cef54605469eb67d23306aa3 -//----------------- -"use strict"; - -const { Readable, Writable } = require("stream"); - -// This test ensures that if have 'readable' listener -// on Readable instance it will not disrupt the pipe. - -test("readable listener before pipe", () => { - let receivedData = ""; - const w = new Writable({ - write: (chunk, env, callback) => { - receivedData += chunk; - callback(); - }, - }); - - const data = ["foo", "bar", "baz"]; - const r = new Readable({ - read: () => {}, - }); - - const readableSpy = jest.fn(); - r.once("readable", readableSpy); - - r.pipe(w); - r.push(data[0]); - r.push(data[1]); - r.push(data[2]); - r.push(null); - - return new Promise(resolve => { - w.on("finish", () => { - expect(receivedData).toBe(data.join("")); - expect(readableSpy).toHaveBeenCalledTimes(1); - resolve(); - }); - }); -}); - -test("readable listener after pipe", () => { - let receivedData = ""; - const w = new Writable({ - write: (chunk, env, callback) => { - receivedData += chunk; - callback(); - }, - }); - - const data = ["foo", "bar", "baz"]; - const r = new Readable({ - read: () => {}, - }); - - r.pipe(w); - r.push(data[0]); - r.push(data[1]); - r.push(data[2]); - r.push(null); - - const readableSpy = jest.fn(); - r.once("readable", readableSpy); - - return new Promise(resolve => { - w.on("finish", () => { - expect(receivedData).toBe(data.join("")); - expect(readableSpy).toHaveBeenCalledTimes(1); - resolve(); - }); - }); -}); - -//<#END_FILE: test-stream-once-readable-pipe.js diff --git a/test/js/node/test/parallel/stream-passthrough-drain.test.js b/test/js/node/test/parallel/stream-passthrough-drain.test.js deleted file mode 100644 index b77fa6daf3..0000000000 --- a/test/js/node/test/parallel/stream-passthrough-drain.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-stream-passthrough-drain.js -//#SHA1: c17561a8fc9a14d7abc05af3528d0ead32502a57 -//----------------- -"use strict"; -const { PassThrough } = require("stream"); - -test("PassThrough stream emits drain event when buffer is emptied", () => { - const pt = new PassThrough({ highWaterMark: 0 }); - - const drainHandler = jest.fn(); - pt.on("drain", drainHandler); - - expect(pt.write("hello1")).toBe(false); - - pt.read(); - pt.read(); - - // Use process.nextTick to ensure the drain event has a chance to fire - return new Promise(resolve => { - process.nextTick(() => { - expect(drainHandler).toHaveBeenCalledTimes(1); - resolve(); - }); - }); -}); - -//<#END_FILE: test-stream-passthrough-drain.js diff --git a/test/js/node/test/parallel/stream-pipe-event.test.js b/test/js/node/test/parallel/stream-pipe-event.test.js deleted file mode 100644 index 5c7c0626f9..0000000000 --- a/test/js/node/test/parallel/stream-pipe-event.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-stream-pipe-event.js -//#SHA1: 63887b8cce85a4c7cfa27c8111edd14330a2078f -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const stream = require("stream"); - -function Writable() { - this.writable = true; - stream.Stream.call(this); -} -Object.setPrototypeOf(Writable.prototype, stream.Stream.prototype); -Object.setPrototypeOf(Writable, stream.Stream); - -function Readable() { - this.readable = true; - stream.Stream.call(this); -} -Object.setPrototypeOf(Readable.prototype, stream.Stream.prototype); -Object.setPrototypeOf(Readable, stream.Stream); - -test("pipe event is emitted", () => { - let passed = false; - - const w = new Writable(); - w.on("pipe", function (src) { - passed = true; - }); - - const r = new Readable(); - r.pipe(w); - - expect(passed).toBe(true); -}); - -//<#END_FILE: test-stream-pipe-event.js diff --git a/test/js/node/test/parallel/stream-pipe-needdrain.test.js b/test/js/node/test/parallel/stream-pipe-needdrain.test.js deleted file mode 100644 index 42e4b2e9f3..0000000000 --- a/test/js/node/test/parallel/stream-pipe-needdrain.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-stream-pipe-needDrain.js -//#SHA1: 5c524a253a770fcbb95365aea011a38a665c61da -//----------------- -"use strict"; - -const { Readable, Writable } = require("stream"); - -// Pipe should pause temporarily if writable needs drain. -test("Pipe pauses when writable needs drain", done => { - const w = new Writable({ - write(buf, encoding, callback) { - process.nextTick(callback); - }, - highWaterMark: 1, - }); - - while (w.write("asd")); - - expect(w.writableNeedDrain).toBe(true); - - const r = new Readable({ - read() { - this.push("asd"); - this.push(null); - }, - }); - - const pauseSpy = jest.fn(); - r.on("pause", pauseSpy); - - const endSpy = jest.fn().mockImplementation(() => { - expect(pauseSpy).toHaveBeenCalledTimes(2); - done(); - }); - r.on("end", endSpy); - - r.pipe(w); -}); - -//<#END_FILE: test-stream-pipe-needDrain.js diff --git a/test/js/node/test/parallel/stream-preprocess.test.js b/test/js/node/test/parallel/stream-preprocess.test.js deleted file mode 100644 index 8c5dd3cc8a..0000000000 --- a/test/js/node/test/parallel/stream-preprocess.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-stream-preprocess.js -//#SHA1: 4061428f95671f257c4a57a92d77c0dc63a1394a -//----------------- -"use strict"; - -const fs = require("fs"); -const rl = require("readline"); -const fixtures = require("../common/fixtures"); - -const BOM = "\uFEFF"; - -// Get the data using a non-stream way to compare with the streamed data. -const modelData = fixtures.readSync("file-to-read-without-bom.txt", "utf8"); -const modelDataFirstCharacter = modelData[0]; - -// Detect the number of forthcoming 'line' events for mustCall() 'expected' arg. -const lineCount = modelData.match(/\n/g).length; - -test("Ensure both without-bom and with-bom test files are textwise equal", () => { - expect(fixtures.readSync("file-to-read-with-bom.txt", "utf8")).toBe(`${BOM}${modelData}`); -}); - -test("An unjustified BOM stripping with a non-BOM character unshifted to a stream", done => { - const inputWithoutBOM = fs.createReadStream(fixtures.path("file-to-read-without-bom.txt"), "utf8"); - - inputWithoutBOM.once("readable", () => { - const maybeBOM = inputWithoutBOM.read(1); - expect(maybeBOM).toBe(modelDataFirstCharacter); - expect(maybeBOM).not.toBe(BOM); - - inputWithoutBOM.unshift(maybeBOM); - - let streamedData = ""; - rl.createInterface({ - input: inputWithoutBOM, - }) - .on("line", line => { - streamedData += `${line}\n`; - }) - .on("close", () => { - expect(streamedData).toBe(modelData); - done(); - }); - }); -}); - -test("A justified BOM stripping", done => { - const inputWithBOM = fs.createReadStream(fixtures.path("file-to-read-with-bom.txt"), "utf8"); - - inputWithBOM.once("readable", () => { - const maybeBOM = inputWithBOM.read(1); - expect(maybeBOM).toBe(BOM); - - let streamedData = ""; - rl.createInterface({ - input: inputWithBOM, - }) - .on("line", line => { - streamedData += `${line}\n`; - }) - .on("close", () => { - expect(streamedData).toBe(modelData); - done(); - }); - }); -}); - -//<#END_FILE: test-stream-preprocess.js diff --git a/test/js/node/test/parallel/stream-push-strings.test.js b/test/js/node/test/parallel/stream-push-strings.test.js deleted file mode 100644 index fecd87ceaa..0000000000 --- a/test/js/node/test/parallel/stream-push-strings.test.js +++ /dev/null @@ -1,72 +0,0 @@ -//#FILE: test-stream-push-strings.js -//#SHA1: d2da34fc74795ea8cc46460ce443d0b30b0e98d8 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const { Readable } = require("stream"); - -class MyStream extends Readable { - constructor(options) { - super(options); - this._chunks = 3; - } - - _read(n) { - switch (this._chunks--) { - case 0: - return this.push(null); - case 1: - return setTimeout(() => { - this.push("last chunk"); - }, 100); - case 2: - return this.push("second to last chunk"); - case 3: - return process.nextTick(() => { - this.push("first chunk"); - }); - default: - throw new Error("?"); - } - } -} - -test("MyStream pushes strings correctly", done => { - const ms = new MyStream(); - const results = []; - ms.on("readable", function () { - let chunk; - while (null !== (chunk = ms.read())) results.push(String(chunk)); - }); - - const expected = ["first chunksecond to last chunk", "last chunk"]; - - ms.on("end", () => { - expect(ms._chunks).toBe(-1); - expect(results).toEqual(expected); - done(); - }); -}); - -//<#END_FILE: test-stream-push-strings.js diff --git a/test/js/node/test/parallel/stream-readable-aborted.test.js b/test/js/node/test/parallel/stream-readable-aborted.test.js deleted file mode 100644 index 3d005e5901..0000000000 --- a/test/js/node/test/parallel/stream-readable-aborted.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-stream-readable-aborted.js -//#SHA1: b4d59c7cd8eda084bae2d2ff603dd153aff79f98 -//----------------- -"use strict"; - -const { Readable, Duplex } = require("stream"); - -test("Readable stream aborted state", () => { - const readable = new Readable({ - read() {}, - }); - expect(readable.readableAborted).toBe(false); - readable.destroy(); - expect(readable.readableAborted).toBe(true); -}); - -test("Readable stream aborted state after push(null)", () => { - const readable = new Readable({ - read() {}, - }); - expect(readable.readableAborted).toBe(false); - readable.push(null); - readable.destroy(); - expect(readable.readableAborted).toBe(true); -}); - -test("Readable stream aborted state after push(data)", () => { - const readable = new Readable({ - read() {}, - }); - expect(readable.readableAborted).toBe(false); - readable.push("asd"); - readable.destroy(); - expect(readable.readableAborted).toBe(true); -}); - -test("Readable stream aborted state after end event", done => { - const readable = new Readable({ - read() {}, - }); - expect(readable.readableAborted).toBe(false); - readable.push("asd"); - readable.push(null); - expect(readable.readableAborted).toBe(false); - readable.on("end", () => { - expect(readable.readableAborted).toBe(false); - readable.destroy(); - expect(readable.readableAborted).toBe(false); - queueMicrotask(() => { - expect(readable.readableAborted).toBe(false); - done(); - }); - }); - readable.resume(); -}); - -test("Duplex stream with readable false", () => { - const duplex = new Duplex({ - readable: false, - write() {}, - }); - duplex.destroy(); - expect(duplex.readableAborted).toBe(false); -}); - -//<#END_FILE: test-stream-readable-aborted.js diff --git a/test/js/node/test/parallel/stream-readable-add-chunk-during-data.test.js b/test/js/node/test/parallel/stream-readable-add-chunk-during-data.test.js deleted file mode 100644 index 2a75e47cbf..0000000000 --- a/test/js/node/test/parallel/stream-readable-add-chunk-during-data.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-stream-readable-add-chunk-during-data.js -//#SHA1: f34525f5ea022c837ba1e40e98b1b41da5df50b2 -//----------------- -"use strict"; -const { Readable } = require("stream"); - -// Verify that .push() and .unshift() can be called from 'data' listeners. - -["push", "unshift"].forEach(method => { - test(`Readable ${method} can be called from 'data' listeners`, done => { - const r = new Readable({ read() {} }); - - r.once("data", chunk => { - expect(r.readableLength).toBe(0); - r[method](chunk); - expect(r.readableLength).toBe(chunk.length); - - r.on("data", newChunk => { - expect(newChunk.toString()).toBe("Hello, world"); - done(); - }); - }); - - r.push("Hello, world"); - }); -}); - -//<#END_FILE: test-stream-readable-add-chunk-during-data.js diff --git a/test/js/node/test/parallel/stream-readable-data.test.js b/test/js/node/test/parallel/stream-readable-data.test.js deleted file mode 100644 index b654147c26..0000000000 --- a/test/js/node/test/parallel/stream-readable-data.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-stream-readable-data.js -//#SHA1: c679a0f31d84cd65d9e85ab0617abfd65a053afc -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -test("Readable stream emits data event after removing readable listener", done => { - const readable = new Readable({ - read() {}, - }); - - function read() {} - - readable.setEncoding("utf8"); - readable.on("readable", read); - readable.removeListener("readable", read); - - process.nextTick(() => { - const dataHandler = jest.fn(); - readable.on("data", dataHandler); - readable.push("hello"); - - // Use setImmediate to ensure the data event has time to be emitted - setImmediate(() => { - expect(dataHandler).toHaveBeenCalledTimes(1); - done(); - }); - }); -}); - -//<#END_FILE: test-stream-readable-data.js diff --git a/test/js/node/test/parallel/stream-readable-emittedreadable.test.js b/test/js/node/test/parallel/stream-readable-emittedreadable.test.js deleted file mode 100644 index 744dba984c..0000000000 --- a/test/js/node/test/parallel/stream-readable-emittedreadable.test.js +++ /dev/null @@ -1,111 +0,0 @@ -//#FILE: test-stream-readable-emittedReadable.js -//#SHA1: 1c9463d9bb0e7927d8d18949aae849a04cc034fe -//----------------- -"use strict"; -const { Readable } = require("stream"); - -describe("Readable Stream emittedReadable", () => { - test("emittedReadable state changes correctly", () => { - const readable = new Readable({ - read: () => {}, - }); - - // Initialized to false. - expect(readable._readableState.emittedReadable).toBe(false); - - const expected = [Buffer.from("foobar"), Buffer.from("quo"), null]; - const readableSpy = jest.fn(() => { - // emittedReadable should be true when the readable event is emitted - expect(readable._readableState.emittedReadable).toBe(true); - expect(readable.read()).toEqual(expected.shift()); - // emittedReadable is reset to false during read() - expect(readable._readableState.emittedReadable).toBe(false); - }); - - readable.on("readable", readableSpy); - - // When the first readable listener is just attached, - // emittedReadable should be false - expect(readable._readableState.emittedReadable).toBe(false); - - // These trigger a single 'readable', as things are batched up - process.nextTick(() => { - readable.push("foo"); - }); - process.nextTick(() => { - readable.push("bar"); - }); - - // These triggers two readable events - setImmediate(() => { - readable.push("quo"); - process.nextTick(() => { - readable.push(null); - }); - }); - - return new Promise(resolve => { - setTimeout(() => { - expect(readableSpy).toHaveBeenCalledTimes(3); - resolve(); - }, 100); - }); - }); - - test("emittedReadable with read(0)", () => { - const noRead = new Readable({ - read: () => {}, - }); - - const readableSpy = jest.fn(() => { - // emittedReadable should be true when the readable event is emitted - expect(noRead._readableState.emittedReadable).toBe(true); - noRead.read(0); - // emittedReadable is not reset during read(0) - expect(noRead._readableState.emittedReadable).toBe(true); - }); - - noRead.on("readable", readableSpy); - - noRead.push("foo"); - noRead.push(null); - - return new Promise(resolve => { - setTimeout(() => { - expect(readableSpy).toHaveBeenCalledTimes(1); - resolve(); - }, 100); - }); - }); - - test("emittedReadable in flowing mode", () => { - const flowing = new Readable({ - read: () => {}, - }); - - const dataSpy = jest.fn(() => { - // When in flowing mode, emittedReadable is always false. - expect(flowing._readableState.emittedReadable).toBe(false); - flowing.read(); - expect(flowing._readableState.emittedReadable).toBe(false); - }); - - flowing.on("data", dataSpy); - - flowing.push("foooo"); - flowing.push("bar"); - flowing.push("quo"); - process.nextTick(() => { - flowing.push(null); - }); - - return new Promise(resolve => { - setTimeout(() => { - expect(dataSpy).toHaveBeenCalledTimes(3); - resolve(); - }, 100); - }); - }); -}); - -//<#END_FILE: test-stream-readable-emittedReadable.js diff --git a/test/js/node/test/parallel/stream-readable-end-destroyed.test.js b/test/js/node/test/parallel/stream-readable-end-destroyed.test.js deleted file mode 100644 index c8b98db765..0000000000 --- a/test/js/node/test/parallel/stream-readable-end-destroyed.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-stream-readable-end-destroyed.js -//#SHA1: 20c8bb870db11018d2eaa1c9e4dece071917d03d -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -test("Don't emit 'end' after 'close'", () => { - const r = new Readable(); - - const endListener = jest.fn(); - r.on("end", endListener); - r.resume(); - r.destroy(); - - return new Promise(resolve => { - r.on("close", () => { - r.push(null); - expect(endListener).not.toHaveBeenCalled(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-stream-readable-end-destroyed.js diff --git a/test/js/node/test/parallel/stream-readable-ended.test.js b/test/js/node/test/parallel/stream-readable-ended.test.js deleted file mode 100644 index fb6a3c3b64..0000000000 --- a/test/js/node/test/parallel/stream-readable-ended.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-stream-readable-ended.js -//#SHA1: 93aa267630bb32f94783c5bbdeb8e345c0acd94a -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -// basic -test("Readable.prototype has readableEnded property", () => { - expect(Object.hasOwn(Readable.prototype, "readableEnded")).toBe(true); -}); - -// event -test("readableEnded state changes correctly", done => { - const readable = new Readable(); - - readable._read = () => { - // The state ended should start in false. - expect(readable.readableEnded).toBe(false); - readable.push("asd"); - expect(readable.readableEnded).toBe(false); - readable.push(null); - expect(readable.readableEnded).toBe(false); - }; - - readable.on("end", () => { - expect(readable.readableEnded).toBe(true); - done(); - }); - - readable.on("data", () => { - expect(readable.readableEnded).toBe(false); - }); -}); - -// Verifies no `error` triggered on multiple .push(null) invocations -test("No error triggered on multiple .push(null) invocations", done => { - const readable = new Readable(); - - readable.on("readable", () => { - readable.read(); - }); - - const errorHandler = jest.fn(); - readable.on("error", errorHandler); - - readable.on("end", () => { - expect(errorHandler).not.toHaveBeenCalled(); - done(); - }); - - readable.push("a"); - readable.push(null); - readable.push(null); -}); - -//<#END_FILE: test-stream-readable-ended.js diff --git a/test/js/node/test/parallel/stream-readable-event.test.js b/test/js/node/test/parallel/stream-readable-event.test.js deleted file mode 100644 index 688f207d1b..0000000000 --- a/test/js/node/test/parallel/stream-readable-event.test.js +++ /dev/null @@ -1,146 +0,0 @@ -//#FILE: test-stream-readable-event.js -//#SHA1: 8a3da958252097730dcd22e82d325d106d5512a5 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const { Readable } = require("stream"); - -test("not reading when the readable is added", done => { - const r = new Readable({ - highWaterMark: 3, - }); - - r._read = jest.fn(); - - // This triggers a 'readable' event, which is lost. - r.push(Buffer.from("blerg")); - - setTimeout(() => { - // We're testing what we think we are - expect(r._readableState.reading).toBe(false); - const readableSpy = jest.fn(); - r.on("readable", readableSpy); - - // Allow time for the 'readable' event to potentially fire - setTimeout(() => { - expect(readableSpy).toHaveBeenCalled(); - expect(r._read).not.toHaveBeenCalled(); - done(); - }, 10); - }, 1); -}); - -test("readable is re-emitted if there's already a length, while it IS reading", done => { - const r = new Readable({ - highWaterMark: 3, - }); - - r._read = jest.fn(); - - // This triggers a 'readable' event, which is lost. - r.push(Buffer.from("bl")); - - setTimeout(() => { - // Assert we're testing what we think we are - expect(r._readableState.reading).toBe(true); - const readableSpy = jest.fn(); - r.on("readable", readableSpy); - - // Allow time for the 'readable' event to potentially fire - setTimeout(() => { - expect(readableSpy).toHaveBeenCalled(); - expect(r._read).toHaveBeenCalled(); - done(); - }, 10); - }, 1); -}); - -test("not reading when the stream has not passed the highWaterMark but has reached EOF", done => { - const r = new Readable({ - highWaterMark: 30, - }); - - r._read = jest.fn(); - - // This triggers a 'readable' event, which is lost. - r.push(Buffer.from("blerg")); - r.push(null); - - setTimeout(() => { - // Assert we're testing what we think we are - expect(r._readableState.reading).toBe(false); - const readableSpy = jest.fn(); - r.on("readable", readableSpy); - - // Allow time for the 'readable' event to potentially fire - setTimeout(() => { - expect(readableSpy).toHaveBeenCalled(); - expect(r._read).not.toHaveBeenCalled(); - done(); - }, 10); - }, 1); -}); - -test("Pushing an empty string in non-objectMode should trigger next `read()`", done => { - const underlyingData = ["", "x", "y", "", "z"]; - const expected = underlyingData.filter(data => data); - const result = []; - - const r = new Readable({ - encoding: "utf8", - }); - r._read = function () { - process.nextTick(() => { - if (!underlyingData.length) { - this.push(null); - } else { - this.push(underlyingData.shift()); - } - }); - }; - - r.on("readable", () => { - const data = r.read(); - if (data !== null) result.push(data); - }); - - r.on("end", () => { - expect(result).toEqual(expected); - done(); - }); -}); - -test("#20923 - removeAllListeners should clear all event listeners", () => { - const r = new Readable(); - r._read = function () { - // Actually doing thing here - }; - r.on("data", function () {}); - - r.removeAllListeners(); - - expect(r.eventNames().length).toBe(0); -}); - -//<#END_FILE: test-stream-readable-event.js diff --git a/test/js/node/test/parallel/stream-readable-hwm-0-async.test.js b/test/js/node/test/parallel/stream-readable-hwm-0-async.test.js deleted file mode 100644 index 0d2388c333..0000000000 --- a/test/js/node/test/parallel/stream-readable-hwm-0-async.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-stream-readable-hwm-0-async.js -//#SHA1: ddaa3718bf6d6ae9258293494ac3449800169768 -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -// This test ensures that Readable stream will continue to call _read -// for streams with highWaterMark === 0 once the stream returns data -// by calling push() asynchronously. - -test("Readable stream with highWaterMark 0 and async push", async () => { - let count = 5; - const readMock = jest.fn(() => { - process.nextTick(() => { - if (count--) { - r.push("a"); - } else { - r.push(null); - } - }); - }); - - const r = new Readable({ - read: readMock, - highWaterMark: 0, - }); - - const dataHandler = jest.fn(); - const endHandler = jest.fn(); - - r.on("data", dataHandler); - r.on("end", endHandler); - - // Consume the stream - for await (const chunk of r) { - // This loop will iterate 5 times - } - - // Called 6 times: First 5 return data, last one signals end of stream. - expect(readMock).toHaveBeenCalledTimes(6); - expect(dataHandler).toHaveBeenCalledTimes(5); - expect(endHandler).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-stream-readable-hwm-0-async.js diff --git a/test/js/node/test/parallel/stream-readable-hwm-0.test.js b/test/js/node/test/parallel/stream-readable-hwm-0.test.js deleted file mode 100644 index 3d8266581d..0000000000 --- a/test/js/node/test/parallel/stream-readable-hwm-0.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-stream-readable-hwm-0.js -//#SHA1: 986085294672eff1ba0a13f99633edd30c3fb54a -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -// This test ensures that Readable stream will call _read() for streams -// with highWaterMark === 0 upon .read(0) instead of just trying to -// emit 'readable' event. - -test("Readable stream with highWaterMark 0 calls _read()", () => { - const mockRead = jest.fn(); - - const r = new Readable({ - // Must be called only once upon setting 'readable' listener - read: mockRead, - highWaterMark: 0, - }); - - let pushedNull = false; - - // This will trigger read(0) but must only be called after push(null) - // because we haven't pushed any data - r.on( - "readable", - jest.fn(() => { - expect(r.read()).toBeNull(); - expect(pushedNull).toBe(true); - }), - ); - - const endHandler = jest.fn(); - r.on("end", endHandler); - - return new Promise(resolve => { - process.nextTick(() => { - expect(r.read()).toBeNull(); - pushedNull = true; - r.push(null); - - // Use setImmediate to ensure all events have been processed - setImmediate(() => { - expect(mockRead).toHaveBeenCalledTimes(1); - expect(endHandler).toHaveBeenCalledTimes(1); - resolve(); - }); - }); - }); -}); - -//<#END_FILE: test-stream-readable-hwm-0.js diff --git a/test/js/node/test/parallel/stream-readable-needreadable.test.js b/test/js/node/test/parallel/stream-readable-needreadable.test.js deleted file mode 100644 index 171eeee0df..0000000000 --- a/test/js/node/test/parallel/stream-readable-needreadable.test.js +++ /dev/null @@ -1,131 +0,0 @@ -//#FILE: test-stream-readable-needReadable.js -//#SHA1: 301ca49c86e59196821c0fcd419c71f5ffd4a94d -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -describe("Readable stream needReadable property", () => { - test("Initial state and readable event", () => { - const readable = new Readable({ - read: () => {}, - }); - - // Initialized to false. - expect(readable._readableState.needReadable).toBe(false); - - readable.on("readable", () => { - // When the readable event fires, needReadable is reset. - expect(readable._readableState.needReadable).toBe(false); - readable.read(); - }); - - // If a readable listener is attached, then a readable event is needed. - expect(readable._readableState.needReadable).toBe(true); - - readable.push("foo"); - readable.push(null); - - return new Promise(resolve => { - readable.on("end", () => { - // No need to emit readable anymore when the stream ends. - expect(readable._readableState.needReadable).toBe(false); - resolve(); - }); - }); - }); - - test("Async readable stream", done => { - const asyncReadable = new Readable({ - read: () => {}, - }); - - let readableCallCount = 0; - asyncReadable.on("readable", () => { - if (asyncReadable.read() !== null) { - // After each read(), the buffer is empty. - // If the stream doesn't end now, - // then we need to notify the reader on future changes. - expect(asyncReadable._readableState.needReadable).toBe(true); - } - readableCallCount++; - if (readableCallCount === 2) { - done(); - } - }); - - process.nextTick(() => { - asyncReadable.push("foooo"); - }); - process.nextTick(() => { - asyncReadable.push("bar"); - }); - setImmediate(() => { - asyncReadable.push(null); - expect(asyncReadable._readableState.needReadable).toBe(false); - }); - }); - - test("Flowing mode", done => { - const flowing = new Readable({ - read: () => {}, - }); - - // Notice this must be above the on('data') call. - flowing.push("foooo"); - flowing.push("bar"); - flowing.push("quo"); - process.nextTick(() => { - flowing.push(null); - }); - - let dataCallCount = 0; - // When the buffer already has enough data, and the stream is - // in flowing mode, there is no need for the readable event. - flowing.on("data", data => { - expect(flowing._readableState.needReadable).toBe(false); - dataCallCount++; - if (dataCallCount === 3) { - done(); - } - }); - }); - - test("Slow producer", done => { - const slowProducer = new Readable({ - read: () => {}, - }); - - let readableCallCount = 0; - slowProducer.on("readable", () => { - const chunk = slowProducer.read(8); - const state = slowProducer._readableState; - if (chunk === null) { - // The buffer doesn't have enough data, and the stream is not end, - // we need to notify the reader when data arrives. - expect(state.needReadable).toBe(true); - } else { - expect(state.needReadable).toBe(false); - } - readableCallCount++; - if (readableCallCount === 4) { - done(); - } - }); - - process.nextTick(() => { - slowProducer.push("foo"); - process.nextTick(() => { - slowProducer.push("foo"); - process.nextTick(() => { - slowProducer.push("foo"); - process.nextTick(() => { - slowProducer.push(null); - }); - }); - }); - }); - }); -}); - -//<#END_FILE: test-stream-readable-needReadable.js diff --git a/test/js/node/test/parallel/stream-readable-next-no-null.test.js b/test/js/node/test/parallel/stream-readable-next-no-null.test.js deleted file mode 100644 index c0129802e9..0000000000 --- a/test/js/node/test/parallel/stream-readable-next-no-null.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-stream-readable-next-no-null.js -//#SHA1: b3362d071d6f13ce317db2b897ff1146441d1bea -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -describe("Readable.from with null value", () => { - it("should throw ERR_STREAM_NULL_VALUES error", async () => { - async function* generate() { - yield null; - } - - const stream = Readable.from(generate()); - - const errorPromise = new Promise(resolve => { - stream.on("error", error => { - resolve(error); - }); - }); - - const dataPromise = new Promise(resolve => { - stream.on("data", () => { - resolve("data"); - }); - }); - - const endPromise = new Promise(resolve => { - stream.on("end", () => { - resolve("end"); - }); - }); - - await expect(errorPromise).resolves.toEqual( - expect.objectContaining({ - code: "ERR_STREAM_NULL_VALUES", - name: "TypeError", - message: expect.any(String), - }), - ); - - await expect(Promise.race([dataPromise, endPromise, errorPromise])).resolves.not.toBe("data"); - await expect(Promise.race([dataPromise, endPromise, errorPromise])).resolves.not.toBe("end"); - }); -}); - -//<#END_FILE: test-stream-readable-next-no-null.js diff --git a/test/js/node/test/parallel/stream-readable-readable-then-resume.test.js b/test/js/node/test/parallel/stream-readable-readable-then-resume.test.js deleted file mode 100644 index e542119c61..0000000000 --- a/test/js/node/test/parallel/stream-readable-readable-then-resume.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-stream-readable-readable-then-resume.js -//#SHA1: 79790a4cd766421a6891704bf157072eaef2d5b4 -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -// This test verifies that a stream could be resumed after -// removing the readable event in the same tick - -function check(s) { - const readableListener = jest.fn(); - s.on("readable", readableListener); - s.on("end", jest.fn()); - expect(s.removeListener).toBe(s.off); - s.removeListener("readable", readableListener); - s.resume(); - - expect(readableListener).not.toHaveBeenCalled(); -} - -test("Readable stream can be resumed after removing readable event", () => { - const s = new Readable({ - objectMode: true, - highWaterMark: 1, - read() { - if (!this.first) { - this.push("hello"); - this.first = true; - return; - } - - this.push(null); - }, - }); - - check(s); -}); - -//<#END_FILE: test-stream-readable-readable-then-resume.js diff --git a/test/js/node/test/parallel/stream-readable-readable.test.js b/test/js/node/test/parallel/stream-readable-readable.test.js deleted file mode 100644 index cb0b9c2dba..0000000000 --- a/test/js/node/test/parallel/stream-readable-readable.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-stream-readable-readable.js -//#SHA1: f2d897473803968b7ee8efb20a8f1f374987980b -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -describe("Readable.readable", () => { - test("readable property is set correctly", () => { - const r = new Readable({ - read() {}, - }); - expect(r.readable).toBe(true); - r.destroy(); - expect(r.readable).toBe(false); - }); - - test("readable property remains true until end event", () => { - const r = new Readable({ - read() {}, - }); - expect(r.readable).toBe(true); - - const endHandler = jest.fn(); - r.on("end", endHandler); - r.resume(); - r.push(null); - expect(r.readable).toBe(true); - r.off("end", endHandler); - - return new Promise(resolve => { - r.on("end", () => { - expect(r.readable).toBe(false); - resolve(); - }); - }); - }); - - test("readable property becomes false on error", () => { - const r = new Readable({ - read: jest.fn(() => { - process.nextTick(() => { - r.destroy(new Error()); - expect(r.readable).toBe(false); - }); - }), - }); - r.resume(); - - return new Promise(resolve => { - r.on("error", () => { - expect(r.readable).toBe(false); - resolve(); - }); - }); - }); -}); - -//<#END_FILE: test-stream-readable-readable.js diff --git a/test/js/node/test/parallel/stream-readable-resume-hwm.test.js b/test/js/node/test/parallel/stream-readable-resume-hwm.test.js deleted file mode 100644 index 7e18b7f9a0..0000000000 --- a/test/js/node/test/parallel/stream-readable-resume-hwm.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-stream-readable-resume-hwm.js -//#SHA1: 8149b27327258da89c087856f54e7e7584ddf1e5 -//----------------- -"use strict"; -const { Readable } = require("stream"); - -// readable.resume() should not lead to a ._read() call being scheduled -// when we exceed the high water mark already. - -test("readable.resume() should not call _read() when exceeding highWaterMark", () => { - const mockRead = jest.fn(); - const readable = new Readable({ - read: mockRead, - highWaterMark: 100, - }); - - // Fill up the internal buffer so that we definitely exceed the HWM: - for (let i = 0; i < 10; i++) readable.push("a".repeat(200)); - - // Call resume, and pause after one chunk. - // The .pause() is just so that we don't empty the buffer fully, which would - // be a valid reason to call ._read(). - readable.resume(); - - return new Promise(resolve => { - readable.once("data", () => { - readable.pause(); - expect(mockRead).not.toHaveBeenCalled(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-stream-readable-resume-hwm.js diff --git a/test/js/node/test/parallel/stream-readable-resumescheduled.test.js b/test/js/node/test/parallel/stream-readable-resumescheduled.test.js deleted file mode 100644 index 499d82925f..0000000000 --- a/test/js/node/test/parallel/stream-readable-resumescheduled.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-stream-readable-resumeScheduled.js -//#SHA1: 3327b31acfd00e4df0bac4e89d7a764c4de6cb4b -//----------------- -"use strict"; - -const { Readable, Writable } = require("stream"); - -// Testing Readable Stream resumeScheduled state - -describe("Readable Stream resumeScheduled state", () => { - test("pipe() test case", done => { - const r = new Readable({ read() {} }); - const w = new Writable(); - - // resumeScheduled should start = `false`. - expect(r._readableState.resumeScheduled).toBe(false); - - // Calling pipe() should change the state value = true. - r.pipe(w); - expect(r._readableState.resumeScheduled).toBe(true); - - process.nextTick(() => { - expect(r._readableState.resumeScheduled).toBe(false); - done(); - }); - }); - - test("data listener test case", done => { - const r = new Readable({ read() {} }); - - // resumeScheduled should start = `false`. - expect(r._readableState.resumeScheduled).toBe(false); - - r.push(Buffer.from([1, 2, 3])); - - // Adding 'data' listener should change the state value - r.on( - "data", - jest.fn(() => { - expect(r._readableState.resumeScheduled).toBe(false); - }), - ); - expect(r._readableState.resumeScheduled).toBe(true); - - process.nextTick(() => { - expect(r._readableState.resumeScheduled).toBe(false); - done(); - }); - }); - - test("resume() test case", done => { - const r = new Readable({ read() {} }); - - // resumeScheduled should start = `false`. - expect(r._readableState.resumeScheduled).toBe(false); - - // Calling resume() should change the state value. - r.resume(); - expect(r._readableState.resumeScheduled).toBe(true); - - const resumeHandler = jest.fn(() => { - // The state value should be `false` again - expect(r._readableState.resumeScheduled).toBe(false); - }); - - r.on("resume", resumeHandler); - - process.nextTick(() => { - expect(r._readableState.resumeScheduled).toBe(false); - expect(resumeHandler).toHaveBeenCalledTimes(1); - done(); - }); - }); -}); - -//<#END_FILE: test-stream-readable-resumeScheduled.js diff --git a/test/js/node/test/parallel/stream-readable-setencoding-existing-buffers.test.js b/test/js/node/test/parallel/stream-readable-setencoding-existing-buffers.test.js deleted file mode 100644 index f79124ae7f..0000000000 --- a/test/js/node/test/parallel/stream-readable-setencoding-existing-buffers.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-stream-readable-setEncoding-existing-buffers.js -//#SHA1: 1b54f93d0be77b949ce81135243cc9ab3318db5b -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -test("Call .setEncoding() while there are bytes already in the buffer", done => { - const r = new Readable({ read() {} }); - - r.push(Buffer.from("a")); - r.push(Buffer.from("b")); - - r.setEncoding("utf8"); - const chunks = []; - r.on("data", chunk => chunks.push(chunk)); - - process.nextTick(() => { - expect(chunks).toEqual(["ab"]); - done(); - }); -}); - -test("Call .setEncoding() while the buffer contains a complete, but chunked character", done => { - const r = new Readable({ read() {} }); - - r.push(Buffer.from([0xf0])); - r.push(Buffer.from([0x9f])); - r.push(Buffer.from([0x8e])); - r.push(Buffer.from([0x89])); - - r.setEncoding("utf8"); - const chunks = []; - r.on("data", chunk => chunks.push(chunk)); - - process.nextTick(() => { - expect(chunks).toEqual(["🎉"]); - done(); - }); -}); - -test("Call .setEncoding() while the buffer contains an incomplete character, and finish the character later", done => { - const r = new Readable({ read() {} }); - - r.push(Buffer.from([0xf0])); - r.push(Buffer.from([0x9f])); - - r.setEncoding("utf8"); - - r.push(Buffer.from([0x8e])); - r.push(Buffer.from([0x89])); - - const chunks = []; - r.on("data", chunk => chunks.push(chunk)); - - process.nextTick(() => { - expect(chunks).toEqual(["🎉"]); - done(); - }); -}); - -//<#END_FILE: test-stream-readable-setEncoding-existing-buffers.js diff --git a/test/js/node/test/parallel/stream-readable-with-unimplemented-_read.test.js b/test/js/node/test/parallel/stream-readable-with-unimplemented-_read.test.js deleted file mode 100644 index 4160e39091..0000000000 --- a/test/js/node/test/parallel/stream-readable-with-unimplemented-_read.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-stream-readable-with-unimplemented-_read.js -//#SHA1: ba318da76b4c594580f62650bad861933a59c215 -//----------------- -"use strict"; -const { Readable } = require("stream"); - -test("Readable stream with unimplemented _read method", done => { - const readable = new Readable(); - - readable.read(); - readable.on("error", error => { - expect(error).toEqual( - expect.objectContaining({ - code: "ERR_METHOD_NOT_IMPLEMENTED", - name: "Error", - message: expect.any(String), - }), - ); - }); - - readable.on("close", () => { - done(); - }); -}); - -//<#END_FILE: test-stream-readable-with-unimplemented-_read.js diff --git a/test/js/node/test/parallel/stream-set-default-hwm.test.js b/test/js/node/test/parallel/stream-set-default-hwm.test.js deleted file mode 100644 index 6bd5bbee52..0000000000 --- a/test/js/node/test/parallel/stream-set-default-hwm.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-stream-set-default-hwm.js -//#SHA1: bc1189f9270a4b5463d8421ef234fc7baaad667f -//----------------- -"use strict"; - -const { setDefaultHighWaterMark, getDefaultHighWaterMark, Writable, Readable, Transform } = require("stream"); - -test("setDefaultHighWaterMark and getDefaultHighWaterMark for object mode", () => { - expect(getDefaultHighWaterMark(false)).not.toBe(32 * 1000); - setDefaultHighWaterMark(false, 32 * 1000); - expect(getDefaultHighWaterMark(false)).toBe(32 * 1000); -}); - -test("setDefaultHighWaterMark and getDefaultHighWaterMark for non-object mode", () => { - expect(getDefaultHighWaterMark(true)).not.toBe(32); - setDefaultHighWaterMark(true, 32); - expect(getDefaultHighWaterMark(true)).toBe(32); -}); - -test("Writable stream uses new default high water mark", () => { - const w = new Writable({ - write() {}, - }); - expect(w.writableHighWaterMark).toBe(32 * 1000); -}); - -test("Readable stream uses new default high water mark", () => { - const r = new Readable({ - read() {}, - }); - expect(r.readableHighWaterMark).toBe(32 * 1000); -}); - -test("Transform stream uses new default high water mark for both readable and writable", () => { - const t = new Transform({ - transform() {}, - }); - expect(t.writableHighWaterMark).toBe(32 * 1000); - expect(t.readableHighWaterMark).toBe(32 * 1000); -}); - -//<#END_FILE: test-stream-set-default-hwm.js diff --git a/test/js/node/test/parallel/stream-transform-callback-twice.test.js b/test/js/node/test/parallel/stream-transform-callback-twice.test.js deleted file mode 100644 index 3be1a53af3..0000000000 --- a/test/js/node/test/parallel/stream-transform-callback-twice.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-stream-transform-callback-twice.js -//#SHA1: f2ad2048f83461d93a84b8b5696230beb4dba9f2 -//----------------- -"use strict"; - -const { Transform } = require("stream"); - -test("Transform stream callback called twice", done => { - const stream = new Transform({ - transform(chunk, enc, cb) { - cb(); - cb(); - }, - }); - - stream.on("error", error => { - expect(error).toMatchObject({ - name: "Error", - code: "ERR_MULTIPLE_CALLBACK", - message: expect.any(String), - }); - done(); - }); - - stream.write("foo"); -}); - -//<#END_FILE: test-stream-transform-callback-twice.js diff --git a/test/js/node/test/parallel/stream-transform-constructor-set-methods.test.js b/test/js/node/test/parallel/stream-transform-constructor-set-methods.test.js deleted file mode 100644 index ec66e53654..0000000000 --- a/test/js/node/test/parallel/stream-transform-constructor-set-methods.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-stream-transform-constructor-set-methods.js -//#SHA1: a827edab0555cd9f8bd240738812b4d6a48b4e7d -//----------------- -"use strict"; - -const { Transform } = require("stream"); - -test("Transform constructor throws when _transform is not implemented", () => { - const t = new Transform(); - - expect(() => { - t.end(Buffer.from("blerg")); - }).toThrow( - expect.objectContaining({ - name: "Error", - code: "ERR_METHOD_NOT_IMPLEMENTED", - message: expect.any(String), - }), - ); -}); - -test("Transform constructor sets methods correctly", () => { - const _transform = jest.fn((chunk, _, next) => { - next(); - }); - - const _final = jest.fn(next => { - next(); - }); - - const _flush = jest.fn(next => { - next(); - }); - - const t2 = new Transform({ - transform: _transform, - flush: _flush, - final: _final, - }); - - expect(t2._transform).toBe(_transform); - expect(t2._flush).toBe(_flush); - expect(t2._final).toBe(_final); - - t2.end(Buffer.from("blerg")); - t2.resume(); - - expect(_transform).toHaveBeenCalled(); - expect(_final).toHaveBeenCalled(); - expect(_flush).toHaveBeenCalled(); -}); - -//<#END_FILE: test-stream-transform-constructor-set-methods.js diff --git a/test/js/node/test/parallel/stream-transform-flush-data.test.js b/test/js/node/test/parallel/stream-transform-flush-data.test.js deleted file mode 100644 index 6bd8e013c0..0000000000 --- a/test/js/node/test/parallel/stream-transform-flush-data.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-stream-transform-flush-data.js -//#SHA1: 04d0db2bec19ea79b0914db96fd2c996ab8d67bd -//----------------- -"use strict"; - -const { Transform } = require("stream"); - -test("Transform flush should emit expected data", done => { - const expected = "asdf"; - - function _transform(d, e, n) { - n(); - } - - function _flush(n) { - n(null, expected); - } - - const t = new Transform({ - transform: _transform, - flush: _flush, - }); - - t.end(Buffer.from("blerg")); - t.on("data", data => { - expect(data.toString()).toBe(expected); - done(); - }); -}); - -//<#END_FILE: test-stream-transform-flush-data.js diff --git a/test/js/node/test/parallel/stream-unpipe-event.test.js b/test/js/node/test/parallel/stream-unpipe-event.test.js deleted file mode 100644 index 7ad2afc486..0000000000 --- a/test/js/node/test/parallel/stream-unpipe-event.test.js +++ /dev/null @@ -1,140 +0,0 @@ -//#FILE: test-stream-unpipe-event.js -//#SHA1: 17303ffe85d8760f81f6294d9004c98638985bd6 -//----------------- -"use strict"; - -const { Writable, Readable } = require("stream"); - -class NullWriteable extends Writable { - _write(chunk, encoding, callback) { - return callback(); - } -} - -class QuickEndReadable extends Readable { - _read() { - this.push(null); - } -} - -class NeverEndReadable extends Readable { - _read() {} -} - -test("QuickEndReadable pipes and unpipes", done => { - const dest = new NullWriteable(); - const src = new QuickEndReadable(); - const pipeSpy = jest.fn(); - const unpipeSpy = jest.fn(); - - dest.on("pipe", pipeSpy); - dest.on("unpipe", unpipeSpy); - - src.pipe(dest); - - setImmediate(() => { - expect(pipeSpy).toHaveBeenCalledTimes(1); - expect(unpipeSpy).toHaveBeenCalledTimes(1); - expect(src._readableState.pipes.length).toBe(0); - done(); - }); -}); - -test("NeverEndReadable pipes but does not unpipe", done => { - const dest = new NullWriteable(); - const src = new NeverEndReadable(); - const pipeSpy = jest.fn(); - const unpipeSpy = jest.fn(); - - dest.on("pipe", pipeSpy); - dest.on("unpipe", unpipeSpy); - - src.pipe(dest); - - setImmediate(() => { - expect(pipeSpy).toHaveBeenCalledTimes(1); - expect(unpipeSpy).not.toHaveBeenCalled(); - expect(src._readableState.pipes.length).toBe(1); - done(); - }); -}); - -test("NeverEndReadable pipes and manually unpipes", done => { - const dest = new NullWriteable(); - const src = new NeverEndReadable(); - const pipeSpy = jest.fn(); - const unpipeSpy = jest.fn(); - - dest.on("pipe", pipeSpy); - dest.on("unpipe", unpipeSpy); - - src.pipe(dest); - src.unpipe(dest); - - setImmediate(() => { - expect(pipeSpy).toHaveBeenCalledTimes(1); - expect(unpipeSpy).toHaveBeenCalledTimes(1); - expect(src._readableState.pipes.length).toBe(0); - done(); - }); -}); - -test("QuickEndReadable pipes and unpipes with end: false", done => { - const dest = new NullWriteable(); - const src = new QuickEndReadable(); - const pipeSpy = jest.fn(); - const unpipeSpy = jest.fn(); - - dest.on("pipe", pipeSpy); - dest.on("unpipe", unpipeSpy); - - src.pipe(dest, { end: false }); - - setImmediate(() => { - expect(pipeSpy).toHaveBeenCalledTimes(1); - expect(unpipeSpy).toHaveBeenCalledTimes(1); - expect(src._readableState.pipes.length).toBe(0); - done(); - }); -}); - -test("NeverEndReadable pipes but does not unpipe with end: false", done => { - const dest = new NullWriteable(); - const src = new NeverEndReadable(); - const pipeSpy = jest.fn(); - const unpipeSpy = jest.fn(); - - dest.on("pipe", pipeSpy); - dest.on("unpipe", unpipeSpy); - - src.pipe(dest, { end: false }); - - setImmediate(() => { - expect(pipeSpy).toHaveBeenCalledTimes(1); - expect(unpipeSpy).not.toHaveBeenCalled(); - expect(src._readableState.pipes.length).toBe(1); - done(); - }); -}); - -test("NeverEndReadable pipes and manually unpipes with end: false", done => { - const dest = new NullWriteable(); - const src = new NeverEndReadable(); - const pipeSpy = jest.fn(); - const unpipeSpy = jest.fn(); - - dest.on("pipe", pipeSpy); - dest.on("unpipe", unpipeSpy); - - src.pipe(dest, { end: false }); - src.unpipe(dest); - - setImmediate(() => { - expect(pipeSpy).toHaveBeenCalledTimes(1); - expect(unpipeSpy).toHaveBeenCalledTimes(1); - expect(src._readableState.pipes.length).toBe(0); - done(); - }); -}); - -//<#END_FILE: test-stream-unpipe-event.js diff --git a/test/js/node/test/parallel/stream-unshift-read-race.test.js b/test/js/node/test/parallel/stream-unshift-read-race.test.js deleted file mode 100644 index 9a03dbb0f0..0000000000 --- a/test/js/node/test/parallel/stream-unshift-read-race.test.js +++ /dev/null @@ -1,141 +0,0 @@ -//#FILE: test-stream-unshift-read-race.js -//#SHA1: 3b6a1e1b0ae4b58251211a49cd8171569cfd86ed -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const stream = require("stream"); - -// This test verifies that: -// 1. unshift() does not cause colliding _read() calls. -// 2. unshift() after the 'end' event is an error, but after the EOF -// signalling null, it is ok, and just creates a new readable chunk. -// 3. push() after the EOF signaling null is an error. -// 4. _read() is not called after pushing the EOF null chunk. - -test("stream unshift read race", done => { - const hwm = 10; - const r = stream.Readable({ highWaterMark: hwm, autoDestroy: false }); - const chunks = 10; - - const data = Buffer.allocUnsafe(chunks * hwm + Math.ceil(hwm / 2)); - for (let i = 0; i < data.length; i++) { - const c = "asdf".charCodeAt(i % 4); - data[i] = c; - } - - let pos = 0; - let pushedNull = false; - r._read = function (n) { - expect(pushedNull).toBe(false); - - // Every third chunk is fast - push(!(chunks % 3)); - - function push(fast) { - expect(pushedNull).toBe(false); - const c = pos >= data.length ? null : data.slice(pos, pos + n); - pushedNull = c === null; - if (fast) { - pos += n; - r.push(c); - if (c === null) pushError(); - } else { - setTimeout(function () { - pos += n; - r.push(c); - if (c === null) pushError(); - }, 1); - } - } - }; - - function pushError() { - r.unshift(Buffer.allocUnsafe(1)); - w.end(); - - expect(() => { - r.push(Buffer.allocUnsafe(1)); - }).toThrow( - expect.objectContaining({ - code: "ERR_STREAM_PUSH_AFTER_EOF", - name: "Error", - message: expect.any(String), - }), - ); - } - - const w = stream.Writable(); - const written = []; - w._write = function (chunk, encoding, cb) { - written.push(chunk.toString()); - cb(); - }; - - r.on("end", () => { - throw new Error("end event should not be emitted"); - }); - - r.on("readable", function () { - let chunk; - while (null !== (chunk = r.read(10))) { - w.write(chunk); - if (chunk.length > 4) r.unshift(Buffer.from("1234")); - } - }); - - w.on("finish", () => { - // Each chunk should start with 1234, and then be asfdasdfasdf... - // The first got pulled out before the first unshift('1234'), so it's - // lacking that piece. - expect(written[0]).toBe("asdfasdfas"); - let asdf = "d"; - console.error(`0: ${written[0]}`); - for (let i = 1; i < written.length; i++) { - console.error(`${i.toString(32)}: ${written[i]}`); - expect(written[i].slice(0, 4)).toBe("1234"); - for (let j = 4; j < written[i].length; j++) { - const c = written[i].charAt(j); - expect(c).toBe(asdf); - switch (asdf) { - case "a": - asdf = "s"; - break; - case "s": - asdf = "d"; - break; - case "d": - asdf = "f"; - break; - case "f": - asdf = "a"; - break; - } - } - } - expect(written).toHaveLength(18); - console.log("ok"); - done(); - }); -}); - -//<#END_FILE: test-stream-unshift-read-race.js diff --git a/test/js/node/test/parallel/stream-wrap.test.js b/test/js/node/test/parallel/stream-wrap.test.js deleted file mode 100644 index ab020ef2b1..0000000000 --- a/test/js/node/test/parallel/stream-wrap.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-stream-wrap.js -//#SHA1: 675a22f043e5a5a38cdd7fff376f269f8d341aab -//----------------- -"use strict"; - -const { Duplex } = require("stream"); - -// Remove internal bindings and StreamWrap import as they're not accessible in a normal environment -// We'll mock the necessary functionality instead - -test("StreamWrap shutdown behavior", done => { - const stream = new Duplex({ - read: function () {}, - write: function () {}, - }); - - // Mock StreamWrap - class MockStreamWrap { - constructor(stream) { - this.stream = stream; - this._handle = { - shutdown: jest.fn(req => { - // Simulate async completion with error - process.nextTick(() => { - req.oncomplete(-1); - }); - }), - }; - } - - destroy() { - // Simulate handle closure - } - } - - // Mock ShutdownWrap - class MockShutdownWrap { - oncomplete = null; - } - - function testShutdown(callback) { - const wrap = new MockStreamWrap(stream); - - const req = new MockShutdownWrap(); - req.oncomplete = function (code) { - expect(code).toBeLessThan(0); - callback(); - }; - req.handle = wrap._handle; - - // Close the handle to simulate - wrap.destroy(); - req.handle.shutdown(req); - } - - testShutdown(done); -}); - -//<#END_FILE: test-stream-wrap.js diff --git a/test/js/node/test/parallel/stream-writable-aborted.test.js b/test/js/node/test/parallel/stream-writable-aborted.test.js deleted file mode 100644 index 82c4be4233..0000000000 --- a/test/js/node/test/parallel/stream-writable-aborted.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-stream-writable-aborted.js -//#SHA1: be315bbc27ad16f13bb6b3022e864c8902265391 -//----------------- -"use strict"; - -const { Writable } = require("stream"); - -describe("Writable stream aborted property", () => { - test("writableAborted is false initially and true after destroy", () => { - const writable = new Writable({ - write() {}, - }); - expect(writable.writableAborted).toBe(false); - writable.destroy(); - expect(writable.writableAborted).toBe(true); - }); - - test("writableAborted is false initially and true after end and destroy", () => { - const writable = new Writable({ - write() {}, - }); - expect(writable.writableAborted).toBe(false); - writable.end(); - writable.destroy(); - expect(writable.writableAborted).toBe(true); - }); -}); - -//<#END_FILE: test-stream-writable-aborted.js diff --git a/test/js/node/test/parallel/stream-writable-change-default-encoding.test.js b/test/js/node/test/parallel/stream-writable-change-default-encoding.test.js deleted file mode 100644 index 0ccbd29ac5..0000000000 --- a/test/js/node/test/parallel/stream-writable-change-default-encoding.test.js +++ /dev/null @@ -1,91 +0,0 @@ -//#FILE: test-stream-writable-change-default-encoding.js -//#SHA1: 7a4e7c22888d4901899fe5a0ed4604c319f73ff9 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const stream = require("stream"); - -class MyWritable extends stream.Writable { - constructor(fn, options) { - super(options); - this.fn = fn; - } - - _write(chunk, encoding, callback) { - this.fn(Buffer.isBuffer(chunk), typeof chunk, encoding); - callback(); - } -} - -test("default encoding is utf8", () => { - const m = new MyWritable( - (isBuffer, type, enc) => { - expect(enc).toBe("utf8"); - }, - { decodeStrings: false }, - ); - m.write("foo"); - m.end(); -}); - -test("change default encoding to ascii", () => { - const m = new MyWritable( - (isBuffer, type, enc) => { - expect(enc).toBe("ascii"); - }, - { decodeStrings: false }, - ); - m.setDefaultEncoding("ascii"); - m.write("bar"); - m.end(); -}); - -test("change default encoding to invalid value", () => { - const m = new MyWritable((isBuffer, type, enc) => {}, { decodeStrings: false }); - - expect(() => { - m.setDefaultEncoding({}); - m.write("bar"); - m.end(); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_UNKNOWN_ENCODING", - message: expect.any(String), - }), - ); -}); - -test("check variable case encoding", () => { - const m = new MyWritable( - (isBuffer, type, enc) => { - expect(enc).toBe("ascii"); - }, - { decodeStrings: false }, - ); - m.setDefaultEncoding("AsCii"); - m.write("bar"); - m.end(); -}); - -//<#END_FILE: test-stream-writable-change-default-encoding.js diff --git a/test/js/node/test/parallel/stream-writable-clear-buffer.test.js b/test/js/node/test/parallel/stream-writable-clear-buffer.test.js deleted file mode 100644 index 329e26d512..0000000000 --- a/test/js/node/test/parallel/stream-writable-clear-buffer.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-stream-writable-clear-buffer.js -//#SHA1: 0088292e626fb952c1777f191acee9a6d92d3f4d -//----------------- -"use strict"; - -const Stream = require("stream"); - -class StreamWritable extends Stream.Writable { - constructor() { - super({ objectMode: true }); - } - - // Refs: https://github.com/nodejs/node/issues/6758 - // We need a timer like on the original issue thread. - // Otherwise the code will never reach our test case. - _write(chunk, encoding, cb) { - setImmediate(cb); - } -} - -test("StreamWritable bufferedRequestCount matches actual buffered request count", done => { - const testStream = new StreamWritable(); - testStream.cork(); - - const writeOperations = 5; - let completedWrites = 0; - - for (let i = 1; i <= writeOperations; i++) { - testStream.write(i, () => { - expect(testStream._writableState.bufferedRequestCount).toBe(testStream._writableState.getBuffer().length); - completedWrites++; - - if (completedWrites === writeOperations) { - done(); - } - }); - } - - testStream.end(); -}); - -//<#END_FILE: test-stream-writable-clear-buffer.js diff --git a/test/js/node/test/parallel/stream-writable-constructor-set-methods.test.js b/test/js/node/test/parallel/stream-writable-constructor-set-methods.test.js deleted file mode 100644 index 932e14afb6..0000000000 --- a/test/js/node/test/parallel/stream-writable-constructor-set-methods.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-stream-writable-constructor-set-methods.js -//#SHA1: 5610c9523a02a55898e40f7c72a09973affd133f -//----------------- -"use strict"; - -const { Writable } = require("stream"); - -const bufferBlerg = Buffer.from("blerg"); - -test("Writable without _write method throws", () => { - const w = new Writable(); - - expect(() => { - w.end(bufferBlerg); - }).toThrow( - expect.objectContaining({ - name: "Error", - code: "ERR_METHOD_NOT_IMPLEMENTED", - message: expect.any(String), - }), - ); -}); - -test("Writable with custom write and writev methods", () => { - const _write = jest.fn((chunk, _, next) => { - next(); - }); - - const _writev = jest.fn((chunks, next) => { - expect(chunks.length).toBe(2); - next(); - }); - - const w2 = new Writable({ write: _write, writev: _writev }); - - expect(w2._write).toBe(_write); - expect(w2._writev).toBe(_writev); - - w2.write(bufferBlerg); - - w2.cork(); - w2.write(bufferBlerg); - w2.write(bufferBlerg); - - w2.end(); - - expect(_write).toHaveBeenCalledTimes(1); - expect(_writev).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-stream-writable-constructor-set-methods.js diff --git a/test/js/node/test/parallel/stream-writable-end-multiple.test.js b/test/js/node/test/parallel/stream-writable-end-multiple.test.js deleted file mode 100644 index 71b55c8cc7..0000000000 --- a/test/js/node/test/parallel/stream-writable-end-multiple.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-stream-writable-end-multiple.js -//#SHA1: db58e265a3eb8bdf7dba1cb959979059ba686f6d -//----------------- -"use strict"; - -const stream = require("stream"); - -test("stream writable end multiple", async () => { - const writable = new stream.Writable(); - writable._write = (chunk, encoding, cb) => { - setTimeout(() => cb(), 10); - }; - - const endCallback1 = jest.fn(); - const endCallback2 = jest.fn(); - const finishCallback = jest.fn(); - const endCallback3 = jest.fn(); - - writable.end("testing ended state", endCallback1); - writable.end(endCallback2); - - writable.on("finish", finishCallback); - - await new Promise(resolve => setTimeout(resolve, 20)); - - expect(endCallback1).toHaveBeenCalledTimes(1); - expect(endCallback2).toHaveBeenCalledTimes(1); - expect(finishCallback).toHaveBeenCalledTimes(1); - - let ticked = false; - writable.end(endCallback3); - ticked = true; - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(endCallback3).toHaveBeenCalledTimes(1); - expect(endCallback3).toHaveBeenCalledWith( - expect.objectContaining({ - code: "ERR_STREAM_ALREADY_FINISHED", - message: expect.any(String), - }), - ); - expect(ticked).toBe(true); -}); - -//<#END_FILE: test-stream-writable-end-multiple.js diff --git a/test/js/node/test/parallel/stream-writable-ended-state.test.js b/test/js/node/test/parallel/stream-writable-ended-state.test.js deleted file mode 100644 index fb6c3d10c6..0000000000 --- a/test/js/node/test/parallel/stream-writable-ended-state.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-stream-writable-ended-state.js -//#SHA1: e6a35fad059c742def91bd4cab4786faffa26f5b -//----------------- -"use strict"; - -const stream = require("stream"); - -describe("Stream Writable Ended State", () => { - let writable; - - beforeEach(() => { - writable = new stream.Writable(); - - writable._write = (chunk, encoding, cb) => { - expect(writable._writableState.ended).toBe(false); - expect(writable._writableState.writable).toBeUndefined(); - expect(writable.writableEnded).toBe(false); - cb(); - }; - }); - - test("initial state", () => { - expect(writable._writableState.ended).toBe(false); - expect(writable._writableState.writable).toBeUndefined(); - expect(writable.writable).toBe(true); - expect(writable.writableEnded).toBe(false); - }); - - test("ended state after end() call", done => { - writable.end("testing ended state", () => { - expect(writable._writableState.ended).toBe(true); - expect(writable._writableState.writable).toBeUndefined(); - expect(writable.writable).toBe(false); - expect(writable.writableEnded).toBe(true); - done(); - }); - - expect(writable._writableState.ended).toBe(true); - expect(writable._writableState.writable).toBeUndefined(); - expect(writable.writable).toBe(false); - expect(writable.writableEnded).toBe(true); - }); -}); - -//<#END_FILE: test-stream-writable-ended-state.js diff --git a/test/js/node/test/parallel/stream-writable-final-destroy.test.js b/test/js/node/test/parallel/stream-writable-final-destroy.test.js deleted file mode 100644 index 0d3b853b3b..0000000000 --- a/test/js/node/test/parallel/stream-writable-final-destroy.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-stream-writable-final-destroy.js -//#SHA1: 4213d1382f0e5b950211e183a94adc5f3e7a1468 -//----------------- -"use strict"; - -const { Writable } = require("stream"); - -test("Writable stream with final and destroy", () => { - const w = new Writable({ - write(chunk, encoding, callback) { - callback(null); - }, - final(callback) { - queueMicrotask(callback); - }, - }); - - w.end(); - w.destroy(); - - const prefinishSpy = jest.fn(); - const finishSpy = jest.fn(); - const closeSpy = jest.fn(); - - w.on("prefinish", prefinishSpy); - w.on("finish", finishSpy); - w.on("close", closeSpy); - - return new Promise(resolve => { - // Use setImmediate to ensure all microtasks have been processed - setImmediate(() => { - expect(prefinishSpy).not.toHaveBeenCalled(); - expect(finishSpy).not.toHaveBeenCalled(); - expect(closeSpy).toHaveBeenCalledTimes(1); - resolve(); - }); - }); -}); - -//<#END_FILE: test-stream-writable-final-destroy.js diff --git a/test/js/node/test/parallel/stream-writable-finished-state.test.js b/test/js/node/test/parallel/stream-writable-finished-state.test.js deleted file mode 100644 index c2d8e8ab49..0000000000 --- a/test/js/node/test/parallel/stream-writable-finished-state.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-stream-writable-finished-state.js -//#SHA1: e9ea6f7cc3e0262bf187b9cf08e9a054c93d7b5f -//----------------- -"use strict"; - -const stream = require("stream"); - -test("Writable stream finished state", done => { - const writable = new stream.Writable(); - - writable._write = (chunk, encoding, cb) => { - // The state finished should start in false. - expect(writable._writableState.finished).toBe(false); - cb(); - }; - - writable.on("finish", () => { - expect(writable._writableState.finished).toBe(true); - }); - - writable.end("testing finished state", () => { - expect(writable._writableState.finished).toBe(true); - done(); - }); -}); - -//<#END_FILE: test-stream-writable-finished-state.js diff --git a/test/js/node/test/parallel/stream-writable-finished.test.js b/test/js/node/test/parallel/stream-writable-finished.test.js deleted file mode 100644 index d7db2acf1d..0000000000 --- a/test/js/node/test/parallel/stream-writable-finished.test.js +++ /dev/null @@ -1,97 +0,0 @@ -//#FILE: test-stream-writable-finished.js -//#SHA1: 20d27885cc10a6787d3e6c6fb877c0aba2310f93 -//----------------- -"use strict"; - -const { Writable } = require("stream"); - -// basic -test("Writable.prototype has writableFinished", () => { - expect(Object.hasOwn(Writable.prototype, "writableFinished")).toBe(true); -}); - -// event -test("writableFinished state changes correctly", done => { - const writable = new Writable(); - - writable._write = (chunk, encoding, cb) => { - // The state finished should start in false. - expect(writable.writableFinished).toBe(false); - cb(); - }; - - writable.on("finish", () => { - expect(writable.writableFinished).toBe(true); - done(); - }); - - writable.end("testing finished state", () => { - expect(writable.writableFinished).toBe(true); - }); -}); - -test("Emit finish asynchronously", done => { - const w = new Writable({ - write(chunk, encoding, cb) { - cb(); - }, - }); - - w.end(); - w.on("finish", done); -}); - -test("Emit prefinish synchronously", () => { - const w = new Writable({ - write(chunk, encoding, cb) { - cb(); - }, - }); - - let sync = true; - w.on("prefinish", () => { - expect(sync).toBe(true); - }); - w.end(); - sync = false; -}); - -test("Emit prefinish synchronously w/ final", () => { - const w = new Writable({ - write(chunk, encoding, cb) { - cb(); - }, - final(cb) { - cb(); - }, - }); - - let sync = true; - w.on("prefinish", () => { - expect(sync).toBe(true); - }); - w.end(); - sync = false; -}); - -test("Call _final synchronously", () => { - let sync = true; - const finalMock = jest.fn(cb => { - expect(sync).toBe(true); - cb(); - }); - - const w = new Writable({ - write(chunk, encoding, cb) { - cb(); - }, - final: finalMock, - }); - - w.end(); - sync = false; - - expect(finalMock).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-stream-writable-finished.js diff --git a/test/js/node/test/parallel/stream-writable-invalid-chunk.test.js b/test/js/node/test/parallel/stream-writable-invalid-chunk.test.js deleted file mode 100644 index 259e993173..0000000000 --- a/test/js/node/test/parallel/stream-writable-invalid-chunk.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-stream-writable-invalid-chunk.js -//#SHA1: 8febb7872aa1c8bfb7ebcb33db7a5fcd2903b2bd -//----------------- -"use strict"; - -const stream = require("stream"); - -function testWriteType(val, objectMode, code) { - const writable = new stream.Writable({ - objectMode, - write: () => {}, - }); - - const writeOperation = () => writable.write(val); - - if (code) { - expect(writeOperation).toThrow( - expect.objectContaining({ - code, - message: expect.any(String), - }), - ); - } else { - expect(writeOperation).not.toThrow(); - } -} - -describe("Writable stream invalid chunk tests", () => { - test("non-object mode invalid types", () => { - testWriteType([], false, "ERR_INVALID_ARG_TYPE"); - testWriteType({}, false, "ERR_INVALID_ARG_TYPE"); - testWriteType(0, false, "ERR_INVALID_ARG_TYPE"); - testWriteType(true, false, "ERR_INVALID_ARG_TYPE"); - testWriteType(0.0, false, "ERR_INVALID_ARG_TYPE"); - testWriteType(undefined, false, "ERR_INVALID_ARG_TYPE"); - testWriteType(null, false, "ERR_STREAM_NULL_VALUES"); - }); - - test("object mode valid types", () => { - testWriteType([], true); - testWriteType({}, true); - testWriteType(0, true); - testWriteType(true, true); - testWriteType(0.0, true); - testWriteType(undefined, true); - }); - - test("object mode null value", () => { - testWriteType(null, true, "ERR_STREAM_NULL_VALUES"); - }); -}); - -//<#END_FILE: test-stream-writable-invalid-chunk.js diff --git a/test/js/node/test/parallel/stream-writable-needdrain-state.test.js b/test/js/node/test/parallel/stream-writable-needdrain-state.test.js deleted file mode 100644 index 3cac2829fa..0000000000 --- a/test/js/node/test/parallel/stream-writable-needdrain-state.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-stream-writable-needdrain-state.js -//#SHA1: c73d65b940e3ea2fe9c94d9c9d0d4ffe36c47397 -//----------------- -"use strict"; - -const stream = require("stream"); - -test("Transform stream needDrain state", done => { - const transform = new stream.Transform({ - transform: _transform, - highWaterMark: 1, - }); - - function _transform(chunk, encoding, cb) { - process.nextTick(() => { - expect(transform._writableState.needDrain).toBe(true); - cb(); - }); - } - - expect(transform._writableState.needDrain).toBe(false); - - transform.write("asdasd", () => { - expect(transform._writableState.needDrain).toBe(false); - done(); - }); - - expect(transform._writableState.needDrain).toBe(true); -}); - -//<#END_FILE: test-stream-writable-needdrain-state.js diff --git a/test/js/node/test/parallel/stream-writable-null.test.js b/test/js/node/test/parallel/stream-writable-null.test.js deleted file mode 100644 index 6f747df705..0000000000 --- a/test/js/node/test/parallel/stream-writable-null.test.js +++ /dev/null @@ -1,58 +0,0 @@ -//#FILE: test-stream-writable-null.js -//#SHA1: 5a080b117b05a98d0b7bf6895b554892c2690ed8 -//----------------- -"use strict"; - -const stream = require("stream"); - -class MyWritable extends stream.Writable { - constructor(options) { - super({ autoDestroy: false, ...options }); - } - _write(chunk, encoding, callback) { - expect(chunk).not.toBe(null); - callback(); - } -} - -test("MyWritable throws on null in object mode", () => { - const m = new MyWritable({ objectMode: true }); - expect(() => { - m.write(null); - }).toThrow( - expect.objectContaining({ - code: "ERR_STREAM_NULL_VALUES", - }), - ); -}); - -test("MyWritable throws on false in non-object mode", () => { - const m = new MyWritable(); - expect(() => { - m.write(false); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - }), - ); -}); - -test("MyWritable should not throw on false in object mode", done => { - const m = new MyWritable({ objectMode: true }); - m.write(false, err => { - expect(err).toBeFalsy(); - done(); - }); -}); - -test("MyWritable should not throw or emit error on false in object mode", done => { - const m = new MyWritable({ objectMode: true }).on("error", e => { - done(e || new Error("should not get here")); - }); - m.write(false, err => { - expect(err).toBeFalsy(); - done(); - }); -}); - -//<#END_FILE: test-stream-writable-null.js diff --git a/test/js/node/test/parallel/stream-writable-properties.test.js b/test/js/node/test/parallel/stream-writable-properties.test.js deleted file mode 100644 index 86bf40656b..0000000000 --- a/test/js/node/test/parallel/stream-writable-properties.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-stream-writable-properties.js -//#SHA1: 3af7ed348fc81b0a70901cb29735a8778d0a8875 -//----------------- -"use strict"; - -const { Writable } = require("stream"); - -describe("Writable stream properties", () => { - test("writableCorked property", () => { - const w = new Writable(); - - expect(w.writableCorked).toBe(0); - - w.uncork(); - expect(w.writableCorked).toBe(0); - - w.cork(); - expect(w.writableCorked).toBe(1); - - w.cork(); - expect(w.writableCorked).toBe(2); - - w.uncork(); - expect(w.writableCorked).toBe(1); - - w.uncork(); - expect(w.writableCorked).toBe(0); - - w.uncork(); - expect(w.writableCorked).toBe(0); - }); -}); - -//<#END_FILE: test-stream-writable-properties.js diff --git a/test/js/node/test/parallel/stream-writable-write-error.test.js b/test/js/node/test/parallel/stream-writable-write-error.test.js deleted file mode 100644 index 6c4a7d0b51..0000000000 --- a/test/js/node/test/parallel/stream-writable-write-error.test.js +++ /dev/null @@ -1,88 +0,0 @@ -//#FILE: test-stream-writable-write-error.js -//#SHA1: 16053b21f2a6c80ae69ae55424550e7213f1f868 -//----------------- -"use strict"; - -const { Writable } = require("stream"); - -function expectError(w, args, code, sync) { - if (sync) { - if (code) { - expect(() => w.write(...args)).toThrow( - expect.objectContaining({ - code, - message: expect.any(String), - }), - ); - } else { - w.write(...args); - } - } else { - let errorCalled = false; - let ticked = false; - w.write(...args, err => { - expect(ticked).toBe(true); - expect(errorCalled).toBe(false); - expect(err.code).toBe(code); - }); - ticked = true; - w.on("error", err => { - errorCalled = true; - expect(err.code).toBe(code); - }); - } -} - -function runTest(autoDestroy) { - test("write after end", () => { - const w = new Writable({ - autoDestroy, - write() {}, - }); - w.end(); - expectError(w, ["asd"], "ERR_STREAM_WRITE_AFTER_END"); - }); - - test("write after destroy", () => { - const w = new Writable({ - autoDestroy, - write() {}, - }); - w.destroy(); - }); - - test("write null values", () => { - const w = new Writable({ - autoDestroy, - write() {}, - }); - expectError(w, [null], "ERR_STREAM_NULL_VALUES", true); - }); - - test("write invalid arg type", () => { - const w = new Writable({ - autoDestroy, - write() {}, - }); - expectError(w, [{}], "ERR_INVALID_ARG_TYPE", true); - }); - - test("write with unknown encoding", () => { - const w = new Writable({ - decodeStrings: false, - autoDestroy, - write() {}, - }); - expectError(w, ["asd", "noencoding"], "ERR_UNKNOWN_ENCODING", true); - }); -} - -describe("Writable stream write errors (autoDestroy: false)", () => { - runTest(false); -}); - -describe("Writable stream write errors (autoDestroy: true)", () => { - runTest(true); -}); - -//<#END_FILE: test-stream-writable-write-error.js diff --git a/test/js/node/test/parallel/stream-writablestate-ending.test.js b/test/js/node/test/parallel/stream-writablestate-ending.test.js deleted file mode 100644 index 43063afed8..0000000000 --- a/test/js/node/test/parallel/stream-writablestate-ending.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-stream-writableState-ending.js -//#SHA1: 97f5685bff2d1c4507caed842006d83c7317e0c0 -//----------------- -"use strict"; - -const stream = require("stream"); - -describe("Writable Stream State", () => { - let writable; - - beforeEach(() => { - writable = new stream.Writable(); - }); - - function testStates(ending, finished, ended) { - expect(writable._writableState.ending).toBe(ending); - expect(writable._writableState.finished).toBe(finished); - expect(writable._writableState.ended).toBe(ended); - } - - test("Writable state transitions", done => { - writable._write = (chunk, encoding, cb) => { - // Ending, finished, ended start in false. - testStates(false, false, false); - cb(); - }; - - writable.on("finish", () => { - // Ending, finished, ended = true. - testStates(true, true, true); - done(); - }); - - const result = writable.end("testing function end()", () => { - // Ending, finished, ended = true. - testStates(true, true, true); - }); - - // End returns the writable instance - expect(result).toBe(writable); - - // Ending, ended = true. - // finished = false. - testStates(true, false, true); - }); -}); - -//<#END_FILE: test-stream-writableState-ending.js diff --git a/test/js/node/test/parallel/stream-writablestate-uncorked-bufferedrequestcount.test.js b/test/js/node/test/parallel/stream-writablestate-uncorked-bufferedrequestcount.test.js deleted file mode 100644 index 7a9e5b4db1..0000000000 --- a/test/js/node/test/parallel/stream-writablestate-uncorked-bufferedrequestcount.test.js +++ /dev/null @@ -1,71 +0,0 @@ -//#FILE: test-stream-writableState-uncorked-bufferedRequestCount.js -//#SHA1: 39a95157551d47517d4c7aa46d1806b5dbccebcf -//----------------- -"use strict"; - -const stream = require("stream"); - -describe("Writable stream corking and uncorking", () => { - let writable; - - beforeEach(() => { - writable = new stream.Writable(); - - writable._writev = jest.fn((chunks, cb) => { - expect(chunks.length).toBe(2); - cb(); - }); - - writable._write = jest.fn((chunk, encoding, cb) => { - cb(); - }); - }); - - test("corking and uncorking behavior", done => { - // first cork - writable.cork(); - expect(writable._writableState.corked).toBe(1); - expect(writable._writableState.bufferedRequestCount).toBe(0); - - // cork again - writable.cork(); - expect(writable._writableState.corked).toBe(2); - - // The first chunk is buffered - writable.write("first chunk"); - expect(writable._writableState.bufferedRequestCount).toBe(1); - - // First uncork does nothing - writable.uncork(); - expect(writable._writableState.corked).toBe(1); - expect(writable._writableState.bufferedRequestCount).toBe(1); - - process.nextTick(() => { - // The second chunk is buffered, because we uncork at the end of tick - writable.write("second chunk"); - expect(writable._writableState.corked).toBe(1); - expect(writable._writableState.bufferedRequestCount).toBe(2); - - // Second uncork flushes the buffer - writable.uncork(); - expect(writable._writableState.corked).toBe(0); - expect(writable._writableState.bufferedRequestCount).toBe(0); - - // Verify that end() uncorks correctly - writable.cork(); - writable.write("third chunk"); - writable.end(); - - // End causes an uncork() as well - expect(writable._writableState.corked).toBe(0); - expect(writable._writableState.bufferedRequestCount).toBe(0); - - expect(writable._writev).toHaveBeenCalledTimes(1); - expect(writable._write).toHaveBeenCalledTimes(1); - - done(); - }); - }); -}); - -//<#END_FILE: test-stream-writableState-uncorked-bufferedRequestCount.js diff --git a/test/js/node/test/parallel/stream-write-destroy.test.js b/test/js/node/test/parallel/stream-write-destroy.test.js deleted file mode 100644 index 4fd760ef04..0000000000 --- a/test/js/node/test/parallel/stream-write-destroy.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-stream-write-destroy.js -//#SHA1: d39354900702b56f19b37407f2e8459ca063fbd6 -//----------------- -"use strict"; - -const { Writable } = require("stream"); - -// Test interaction between calling .destroy() on a writable and pending -// writes. - -describe("Stream write and destroy interaction", () => { - for (const withPendingData of [false, true]) { - for (const useEnd of [false, true]) { - test(`withPendingData: ${withPendingData}, useEnd: ${useEnd}`, () => { - const callbacks = []; - - const w = new Writable({ - write(data, enc, cb) { - callbacks.push(cb); - }, - // Effectively disable the HWM to observe 'drain' events more easily. - highWaterMark: 1, - }); - - let chunksWritten = 0; - let drains = 0; - w.on("drain", () => drains++); - - function onWrite(err) { - if (err) { - expect(w.destroyed).toBe(true); - expect(err.code).toBe("ERR_STREAM_DESTROYED"); - } else { - chunksWritten++; - } - } - - w.write("abc", onWrite); - expect(chunksWritten).toBe(0); - expect(drains).toBe(0); - callbacks.shift()(); - expect(chunksWritten).toBe(1); - expect(drains).toBe(1); - - if (withPendingData) { - // Test 2 cases: There either is or is not data still in the write queue. - // (The second write will never actually get executed either way.) - w.write("def", onWrite); - } - if (useEnd) { - // Again, test 2 cases: Either we indicate that we want to end the - // writable or not. - w.end("ghi", onWrite); - } else { - w.write("ghi", onWrite); - } - - expect(chunksWritten).toBe(1); - w.destroy(); - expect(chunksWritten).toBe(1); - callbacks.shift()(); - expect(chunksWritten).toBe(useEnd && !withPendingData ? 1 : 2); - expect(callbacks.length).toBe(0); - expect(drains).toBe(1); - }); - } - } -}); - -//<#END_FILE: test-stream-write-destroy.js diff --git a/test/js/node/test/parallel/stream-write-drain.test.js b/test/js/node/test/parallel/stream-write-drain.test.js deleted file mode 100644 index d0c89e4302..0000000000 --- a/test/js/node/test/parallel/stream-write-drain.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-stream-write-drain.js -//#SHA1: 893708699284e105a409388953fae28a836370b2 -//----------------- -"use strict"; -const { Writable } = require("stream"); - -// Don't emit 'drain' if ended - -test("Writable stream should not emit 'drain' if ended", done => { - const w = new Writable({ - write(data, enc, cb) { - process.nextTick(cb); - }, - highWaterMark: 1, - }); - - const drainSpy = jest.fn(); - w.on("drain", drainSpy); - - w.write("asd"); - w.end(); - - // Use process.nextTick to ensure that any potential 'drain' event would have been emitted - process.nextTick(() => { - expect(drainSpy).not.toHaveBeenCalled(); - done(); - }); -}); - -//<#END_FILE: test-stream-write-drain.js diff --git a/test/js/node/test/parallel/stream2-base64-single-char-read-end.test.js b/test/js/node/test/parallel/stream2-base64-single-char-read-end.test.js deleted file mode 100644 index bcaae8e2bc..0000000000 --- a/test/js/node/test/parallel/stream2-base64-single-char-read-end.test.js +++ /dev/null @@ -1,65 +0,0 @@ -//#FILE: test-stream2-base64-single-char-read-end.js -//#SHA1: 7d3b8e9ad3f47e915bc265658c0b5639fa4a68cd -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { Readable: R, Writable: W } = require("stream"); - -test("stream2 base64 single char read end", async () => { - const src = new R({ encoding: "base64" }); - const dst = new W(); - let hasRead = false; - const accum = []; - - src._read = function (n) { - if (!hasRead) { - hasRead = true; - process.nextTick(function () { - src.push(Buffer.from("1")); - src.push(null); - }); - } - }; - - dst._write = function (chunk, enc, cb) { - accum.push(chunk); - cb(); - }; - - const endPromise = new Promise(resolve => { - src.on("end", resolve); - }); - - src.pipe(dst); - - await Promise.race([ - endPromise, - new Promise((_, reject) => { - setTimeout(() => reject(new Error("timed out waiting for _write")), 100); - }), - ]); - - expect(String(Buffer.concat(accum))).toBe("MQ=="); -}); - -//<#END_FILE: test-stream2-base64-single-char-read-end.js diff --git a/test/js/node/test/parallel/stream2-compatibility.test.js b/test/js/node/test/parallel/stream2-compatibility.test.js deleted file mode 100644 index a06395efa9..0000000000 --- a/test/js/node/test/parallel/stream2-compatibility.test.js +++ /dev/null @@ -1,81 +0,0 @@ -//#FILE: test-stream2-compatibility.js -//#SHA1: 4767fa4655235101bee847c06850d6db3b71d1cd -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { Readable: R, Writable: W } = require("stream"); - -let ondataCalled = 0; - -class TestReader extends R { - constructor() { - super(); - this._buffer = Buffer.alloc(100, "x"); - - this.on("data", () => { - ondataCalled++; - }); - } - - _read(n) { - this.push(this._buffer); - this._buffer = Buffer.alloc(0); - } -} - -class TestWriter extends W { - constructor() { - super(); - this.write("foo"); - this.end(); - } - - _write(chunk, enc, cb) { - cb(); - } -} - -test("TestReader emits data once", done => { - const reader = new TestReader(); - setImmediate(() => { - expect(ondataCalled).toBe(1); - reader.push(null); - done(); - }); -}); - -test("TestWriter ends correctly", () => { - const writer = new TestWriter(); - expect(writer.writable).toBe(false); -}); - -test("TestReader becomes unreadable", done => { - const reader = new TestReader(); - reader.on("end", () => { - expect(reader.readable).toBe(false); - done(); - }); - reader.push(null); -}); - -//<#END_FILE: test-stream2-compatibility.js diff --git a/test/js/node/test/parallel/stream2-decode-partial.test.js b/test/js/node/test/parallel/stream2-decode-partial.test.js deleted file mode 100644 index 47cbf45918..0000000000 --- a/test/js/node/test/parallel/stream2-decode-partial.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-stream2-decode-partial.js -//#SHA1: bc4bec1c0be7857c86b9cd75dbb76b939d9619ab -//----------------- -"use strict"; - -const { Readable } = require("stream"); - -let buf = ""; -const euro = Buffer.from([0xe2, 0x82, 0xac]); -const cent = Buffer.from([0xc2, 0xa2]); -const source = Buffer.concat([euro, cent]); - -test("Readable stream decodes partial UTF-8 characters correctly", done => { - const readable = Readable({ encoding: "utf8" }); - readable.push(source.slice(0, 2)); - readable.push(source.slice(2, 4)); - readable.push(source.slice(4, 6)); - readable.push(null); - - readable.on("data", function (data) { - buf += data; - }); - - readable.on("end", function () { - expect(buf).toBe("€¢"); - done(); - }); -}); - -//<#END_FILE: test-stream2-decode-partial.js diff --git a/test/js/node/test/parallel/stream2-finish-pipe.test.js b/test/js/node/test/parallel/stream2-finish-pipe.test.js deleted file mode 100644 index db9e55b8f6..0000000000 --- a/test/js/node/test/parallel/stream2-finish-pipe.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-stream2-finish-pipe.js -//#SHA1: dfe174476d312542a54b9574955bddf0ad35aa9e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const stream = require("stream"); - -test("stream2 finish pipe", done => { - const r = new stream.Readable(); - r._read = function (size) { - r.push(Buffer.allocUnsafe(size)); - }; - - const w = new stream.Writable(); - w._write = function (data, encoding, cb) { - process.nextTick(cb, null); - }; - - r.pipe(w); - - // end() must be called in nextTick or a WRITE_AFTER_END error occurs. - process.nextTick(() => { - // This might sound unrealistic, but it happens in net.js. When - // socket.allowHalfOpen === false, EOF will cause .destroySoon() call which - // ends the writable side of net.Socket. - w.end(); - // We need to wait for the 'finish' event to ensure the test completes successfully - w.on("finish", () => { - done(); - }); - }); -}); - -//<#END_FILE: test-stream2-finish-pipe.js diff --git a/test/js/node/test/parallel/stream2-large-read-stall.test.js b/test/js/node/test/parallel/stream2-large-read-stall.test.js deleted file mode 100644 index 9754a67b0a..0000000000 --- a/test/js/node/test/parallel/stream2-large-read-stall.test.js +++ /dev/null @@ -1,78 +0,0 @@ -//#FILE: test-stream2-large-read-stall.js -//#SHA1: c83f78a1c91b12eedd6262c4691bb2e9118c90ae -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// If everything aligns so that you do a read(n) of exactly the -// remaining buffer, then make sure that 'end' still emits. - -const READSIZE = 100; -const PUSHSIZE = 20; -const PUSHCOUNT = 1000; -const HWM = 50; - -const { Readable } = require("stream"); - -test("large read stall", async () => { - const r = new Readable({ - highWaterMark: HWM, - }); - const rs = r._readableState; - - r._read = push; - - let pushes = 0; - function push() { - if (pushes > PUSHCOUNT) return; - - if (pushes++ === PUSHCOUNT) { - console.error(" push(EOF)"); - return r.push(null); - } - - console.error(` push #${pushes}`); - if (r.push(Buffer.allocUnsafe(PUSHSIZE))) setTimeout(push, 1); - } - - r.on("readable", function () { - console.error(">> readable"); - let ret; - do { - console.error(` > read(${READSIZE})`); - ret = r.read(READSIZE); - console.error(` < ${ret && ret.length} (${rs.length} remain)`); - } while (ret && ret.length === READSIZE); - - console.error("<< after read()", ret && ret.length, rs.needReadable, rs.length); - }); - - await new Promise(resolve => { - r.on("end", () => { - expect(pushes).toBe(PUSHCOUNT + 1); - resolve(); - }); - }); -}); - -//<#END_FILE: test-stream2-large-read-stall.js diff --git a/test/js/node/test/parallel/stream2-objects.test.js b/test/js/node/test/parallel/stream2-objects.test.js deleted file mode 100644 index 847802f2db..0000000000 --- a/test/js/node/test/parallel/stream2-objects.test.js +++ /dev/null @@ -1,298 +0,0 @@ -//#FILE: test-stream2-objects.js -//#SHA1: e9aa308270bcb656e33df7deb63c8ce8739c0f35 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const { Readable, Writable } = require("stream"); - -function toArray(callback) { - const stream = new Writable({ objectMode: true }); - const list = []; - stream.write = function (chunk) { - list.push(chunk); - }; - - stream.end = function () { - callback(list); - }; - - return stream; -} - -function fromArray(list) { - const r = new Readable({ objectMode: true }); - r._read = jest.fn(); - list.forEach(function (chunk) { - r.push(chunk); - }); - r.push(null); - - return r; -} - -test("Verify that objects can be read from the stream", () => { - const r = fromArray([{ one: "1" }, { two: "2" }]); - - const v1 = r.read(); - const v2 = r.read(); - const v3 = r.read(); - - expect(v1).toEqual({ one: "1" }); - expect(v2).toEqual({ two: "2" }); - expect(v3).toBeNull(); -}); - -test("Verify that objects can be piped into the stream", done => { - const r = fromArray([{ one: "1" }, { two: "2" }]); - - r.pipe( - toArray(list => { - expect(list).toEqual([{ one: "1" }, { two: "2" }]); - done(); - }), - ); -}); - -test("Verify that read(n) is ignored", () => { - const r = fromArray([{ one: "1" }, { two: "2" }]); - const value = r.read(2); - - expect(value).toEqual({ one: "1" }); -}); - -test("Verify that objects can be synchronously read", done => { - const r = new Readable({ objectMode: true }); - const list = [{ one: "1" }, { two: "2" }]; - r._read = function (n) { - const item = list.shift(); - r.push(item || null); - }; - - r.pipe( - toArray(list => { - expect(list).toEqual([{ one: "1" }, { two: "2" }]); - done(); - }), - ); -}); - -test("Verify that objects can be asynchronously read", done => { - const r = new Readable({ objectMode: true }); - const list = [{ one: "1" }, { two: "2" }]; - r._read = function (n) { - const item = list.shift(); - process.nextTick(function () { - r.push(item || null); - }); - }; - - r.pipe( - toArray(list => { - expect(list).toEqual([{ one: "1" }, { two: "2" }]); - done(); - }), - ); -}); - -test("Verify that strings can be read as objects", done => { - const r = new Readable({ - objectMode: true, - }); - r._read = jest.fn(); - const list = ["one", "two", "three"]; - list.forEach(function (str) { - r.push(str); - }); - r.push(null); - - r.pipe( - toArray(array => { - expect(array).toEqual(list); - done(); - }), - ); -}); - -test("Verify read(0) behavior for object streams", done => { - const r = new Readable({ - objectMode: true, - }); - r._read = jest.fn(); - - r.push("foobar"); - r.push(null); - - r.pipe( - toArray(array => { - expect(array).toEqual(["foobar"]); - done(); - }), - ); -}); - -test("Verify the behavior of pushing falsey values", done => { - const r = new Readable({ - objectMode: true, - }); - r._read = jest.fn(); - - r.push(false); - r.push(0); - r.push(""); - r.push(null); - - r.pipe( - toArray(array => { - expect(array).toEqual([false, 0, ""]); - done(); - }), - ); -}); - -test("Verify high watermark _read() behavior", () => { - const r = new Readable({ - highWaterMark: 6, - objectMode: true, - }); - let calls = 0; - const list = ["1", "2", "3", "4", "5", "6", "7", "8"]; - - r._read = function (n) { - calls++; - }; - - list.forEach(function (c) { - r.push(c); - }); - - const v = r.read(); - - expect(calls).toBe(0); - expect(v).toBe("1"); - - const v2 = r.read(); - expect(v2).toBe("2"); - - const v3 = r.read(); - expect(v3).toBe("3"); - - expect(calls).toBe(1); -}); - -test("Verify high watermark push behavior", () => { - const r = new Readable({ - highWaterMark: 6, - objectMode: true, - }); - r._read = jest.fn(); - for (let i = 0; i < 6; i++) { - const bool = r.push(i); - expect(bool).toBe(i !== 5); - } -}); - -test("Verify that objects can be written to stream", done => { - const w = new Writable({ objectMode: true }); - - w._write = function (chunk, encoding, cb) { - expect(chunk).toEqual({ foo: "bar" }); - cb(); - }; - - w.on("finish", done); - w.write({ foo: "bar" }); - w.end(); -}); - -test("Verify that multiple objects can be written to stream", done => { - const w = new Writable({ objectMode: true }); - const list = []; - - w._write = function (chunk, encoding, cb) { - list.push(chunk); - cb(); - }; - - w.on("finish", () => { - expect(list).toEqual([0, 1, 2, 3, 4]); - done(); - }); - - w.write(0); - w.write(1); - w.write(2); - w.write(3); - w.write(4); - w.end(); -}); - -test("Verify that strings can be written as objects", done => { - const w = new Writable({ - objectMode: true, - }); - const list = []; - - w._write = function (chunk, encoding, cb) { - list.push(chunk); - process.nextTick(cb); - }; - - w.on("finish", () => { - expect(list).toEqual(["0", "1", "2", "3", "4"]); - done(); - }); - - w.write("0"); - w.write("1"); - w.write("2"); - w.write("3"); - w.write("4"); - w.end(); -}); - -test("Verify that stream buffers finish until callback is called", done => { - const w = new Writable({ - objectMode: true, - }); - let called = false; - - w._write = function (chunk, encoding, cb) { - expect(chunk).toBe("foo"); - - process.nextTick(function () { - called = true; - cb(); - }); - }; - - w.on("finish", () => { - expect(called).toBe(true); - done(); - }); - - w.write("foo"); - w.end(); -}); - -//<#END_FILE: test-stream2-objects.js diff --git a/test/js/node/test/parallel/stream2-pipe-error-handling.test.js b/test/js/node/test/parallel/stream2-pipe-error-handling.test.js deleted file mode 100644 index 4a482b234a..0000000000 --- a/test/js/node/test/parallel/stream2-pipe-error-handling.test.js +++ /dev/null @@ -1,110 +0,0 @@ -//#FILE: test-stream2-pipe-error-handling.js -//#SHA1: c5e7ad139c64f22b16e8fff8a62f6f91067087c8 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const stream = require("stream"); - -test("stream pipe error handling with autoDestroy true", () => { - let count = 1000; - - const source = new stream.Readable(); - source._read = function (n) { - n = Math.min(count, n); - count -= n; - source.push(Buffer.allocUnsafe(n)); - }; - - let unpipedDest; - source.unpipe = function (dest) { - unpipedDest = dest; - stream.Readable.prototype.unpipe.call(this, dest); - }; - - const dest = new stream.Writable(); - dest._write = function (chunk, encoding, cb) { - cb(); - }; - - source.pipe(dest); - - let gotErr = null; - dest.on("error", function (err) { - gotErr = err; - }); - - let unpipedSource; - dest.on("unpipe", function (src) { - unpipedSource = src; - }); - - const err = new Error("This stream turned into bacon."); - dest.emit("error", err); - expect(gotErr).toBe(err); - expect(unpipedSource).toBe(source); - expect(unpipedDest).toBe(dest); -}); - -test("stream pipe error handling with autoDestroy false", () => { - let count = 1000; - - const source = new stream.Readable(); - source._read = function (n) { - n = Math.min(count, n); - count -= n; - source.push(Buffer.allocUnsafe(n)); - }; - - let unpipedDest; - source.unpipe = function (dest) { - unpipedDest = dest; - stream.Readable.prototype.unpipe.call(this, dest); - }; - - const dest = new stream.Writable({ autoDestroy: false }); - dest._write = function (chunk, encoding, cb) { - cb(); - }; - - source.pipe(dest); - - let unpipedSource; - dest.on("unpipe", function (src) { - unpipedSource = src; - }); - - const err = new Error("This stream turned into bacon."); - - let gotErr = null; - expect(() => { - dest.emit("error", err); - }).toThrow( - expect.objectContaining({ - message: expect.any(String), - }), - ); - expect(unpipedSource).toBe(source); - expect(unpipedDest).toBe(dest); -}); - -//<#END_FILE: test-stream2-pipe-error-handling.js diff --git a/test/js/node/test/parallel/stream2-pipe-error-once-listener.test.js b/test/js/node/test/parallel/stream2-pipe-error-once-listener.test.js deleted file mode 100644 index 4879a847e0..0000000000 --- a/test/js/node/test/parallel/stream2-pipe-error-once-listener.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-stream2-pipe-error-once-listener.js -//#SHA1: a0bd981aa626f937edb6779bcf0e4dc49b82e69e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const stream = require("stream"); - -class Read extends stream.Readable { - _read(size) { - this.push("x"); - this.push(null); - } -} - -class Write extends stream.Writable { - _write(buffer, encoding, cb) { - this.emit("error", new Error("boom")); - this.emit("alldone"); - } -} - -test("stream2 pipe error once listener", done => { - const read = new Read(); - const write = new Write(); - - write.once("error", () => {}); - write.once("alldone", () => { - console.log("ok"); - done(); - }); - - const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {}); - - read.pipe(write); - - process.nextTick(() => { - expect(exitSpy).not.toHaveBeenCalled(); - exitSpy.mockRestore(); - }); -}); - -//<#END_FILE: test-stream2-pipe-error-once-listener.js diff --git a/test/js/node/test/parallel/stream2-push.test.js b/test/js/node/test/parallel/stream2-push.test.js deleted file mode 100644 index d623f82a98..0000000000 --- a/test/js/node/test/parallel/stream2-push.test.js +++ /dev/null @@ -1,139 +0,0 @@ -//#FILE: test-stream2-push.js -//#SHA1: 9b6aded0c40321f8d2c15dceab98b26033f23adf -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { Readable, Writable } = require("stream"); -const EE = require("events").EventEmitter; - -test("Stream2 push behavior", async () => { - const stream = new Readable({ - highWaterMark: 16, - encoding: "utf8", - }); - - const source = new EE(); - - stream._read = function () { - console.error("stream._read"); - readStart(); - }; - - let ended = false; - stream.on("end", function () { - ended = true; - }); - - source.on("data", function (chunk) { - const ret = stream.push(chunk); - console.error("data", stream.readableLength); - if (!ret) readStop(); - }); - - source.on("end", function () { - stream.push(null); - }); - - let reading = false; - - function readStart() { - console.error("readStart"); - reading = true; - } - - function readStop() { - console.error("readStop"); - reading = false; - process.nextTick(function () { - const r = stream.read(); - if (r !== null) writer.write(r); - }); - } - - const writer = new Writable({ - decodeStrings: false, - }); - - const written = []; - - const expectWritten = [ - "asdfgasdfgasdfgasdfg", - "asdfgasdfgasdfgasdfg", - "asdfgasdfgasdfgasdfg", - "asdfgasdfgasdfgasdfg", - "asdfgasdfgasdfgasdfg", - "asdfgasdfgasdfgasdfg", - ]; - - writer._write = function (chunk, encoding, cb) { - console.error(`WRITE ${chunk}`); - written.push(chunk); - process.nextTick(cb); - }; - - const finishPromise = new Promise(resolve => { - writer.on("finish", () => { - console.error("finish"); - expect(written).toEqual(expectWritten); - console.log("ok"); - resolve(); - }); - }); - - // Now emit some chunks. - const chunk = "asdfg"; - - let set = 0; - readStart(); - - function data() { - expect(reading).toBe(true); - source.emit("data", chunk); - expect(reading).toBe(true); - source.emit("data", chunk); - expect(reading).toBe(true); - source.emit("data", chunk); - expect(reading).toBe(true); - source.emit("data", chunk); - expect(reading).toBe(false); - if (set++ < 5) return new Promise(resolve => setTimeout(() => resolve(data()), 10)); - else return end(); - } - - function end() { - source.emit("end"); - expect(reading).toBe(false); - writer.end(stream.read()); - return new Promise(resolve => - setImmediate(() => { - expect(ended).toBe(true); - resolve(); - }), - ); - } - - await data(); - await finishPromise; -}); - -//<#END_FILE: test-stream2-push.js diff --git a/test/js/node/test/parallel/stream2-readable-empty-buffer-no-eof.test.js b/test/js/node/test/parallel/stream2-readable-empty-buffer-no-eof.test.js deleted file mode 100644 index 5aad62d568..0000000000 --- a/test/js/node/test/parallel/stream2-readable-empty-buffer-no-eof.test.js +++ /dev/null @@ -1,107 +0,0 @@ -//#FILE: test-stream2-readable-empty-buffer-no-eof.js -//#SHA1: 70ef48637116477747867b03a60462fb64331087 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const { Readable } = require("stream"); - -test("test1", done => { - const r = new Readable(); - - // Should not end when we get a Buffer.alloc(0) or '' as the _read - // result that just means that there is *temporarily* no data, but to - // go ahead and try again later. - // - // note that this is very unusual. it only works for crypto streams - // because the other side of the stream will call read(0) to cycle - // data through openssl. that's why setImmediate() is used to call - // r.read(0) again later, otherwise there is no more work being done - // and the process just exits. - - const buf = Buffer.alloc(5, "x"); - let reads = 5; - r._read = function (n) { - switch (reads--) { - case 5: - return setImmediate(() => { - return r.push(buf); - }); - case 4: - setImmediate(() => { - return r.push(Buffer.alloc(0)); - }); - return setImmediate(r.read.bind(r, 0)); - case 3: - setImmediate(r.read.bind(r, 0)); - return process.nextTick(() => { - return r.push(Buffer.alloc(0)); - }); - case 2: - setImmediate(r.read.bind(r, 0)); - return r.push(Buffer.alloc(0)); // Not-EOF! - case 1: - return r.push(buf); - case 0: - return r.push(null); // EOF - default: - throw new Error("unreachable"); - } - }; - - const results = []; - function flow() { - let chunk; - while (null !== (chunk = r.read())) results.push(String(chunk)); - } - r.on("readable", flow); - r.on("end", () => { - results.push("EOF"); - expect(results).toEqual(["xxxxx", "xxxxx", "EOF"]); - done(); - }); - flow(); -}); - -test("test2", done => { - const r = new Readable({ encoding: "base64" }); - let reads = 5; - r._read = function (n) { - if (!reads--) return r.push(null); // EOF - return r.push(Buffer.from("x")); - }; - - const results = []; - function flow() { - let chunk; - while (null !== (chunk = r.read())) results.push(String(chunk)); - } - r.on("readable", flow); - r.on("end", () => { - results.push("EOF"); - expect(results).toEqual(["eHh4", "eHg=", "EOF"]); - done(); - }); - flow(); -}); - -//<#END_FILE: test-stream2-readable-empty-buffer-no-eof.js diff --git a/test/js/node/test/parallel/stream2-readable-legacy-drain.test.js b/test/js/node/test/parallel/stream2-readable-legacy-drain.test.js deleted file mode 100644 index 137ee20ab6..0000000000 --- a/test/js/node/test/parallel/stream2-readable-legacy-drain.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-stream2-readable-legacy-drain.js -//#SHA1: 8182fd1e12ce8538106404d39102ea69eee2e467 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const Stream = require("stream"); -const Readable = Stream.Readable; - -test("Readable stream with legacy drain", done => { - const r = new Readable(); - const N = 256; - let reads = 0; - r._read = function (n) { - return r.push(++reads === N ? null : Buffer.allocUnsafe(1)); - }; - - const onEnd = jest.fn(); - r.on("end", onEnd); - - const w = new Stream(); - w.writable = true; - let buffered = 0; - w.write = function (c) { - buffered += c.length; - process.nextTick(drain); - return false; - }; - - function drain() { - expect(buffered).toBeLessThanOrEqual(3); - buffered = 0; - w.emit("drain"); - } - - const endSpy = jest.fn(); - w.end = endSpy; - - r.pipe(w); - - // Wait for the 'end' event to be emitted - r.on("end", () => { - expect(onEnd).toHaveBeenCalledTimes(1); - expect(endSpy).toHaveBeenCalledTimes(1); - done(); - }); -}); - -//<#END_FILE: test-stream2-readable-legacy-drain.js diff --git a/test/js/node/test/parallel/stream2-readable-wrap-destroy.test.js b/test/js/node/test/parallel/stream2-readable-wrap-destroy.test.js deleted file mode 100644 index b001ee2a9b..0000000000 --- a/test/js/node/test/parallel/stream2-readable-wrap-destroy.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-stream2-readable-wrap-destroy.js -//#SHA1: 632a198f6b4fc882942984df461383047f6b78a6 -//----------------- -"use strict"; - -const { Readable } = require("stream"); -const EventEmitter = require("events"); - -test('Readable.wrap should call destroy on "destroy" event', () => { - const oldStream = new EventEmitter(); - oldStream.pause = jest.fn(); - oldStream.resume = jest.fn(); - - const destroyMock = jest.fn(); - - const readable = new Readable({ - autoDestroy: false, - destroy: destroyMock, - }); - - readable.wrap(oldStream); - oldStream.emit("destroy"); - - expect(destroyMock).toHaveBeenCalledTimes(1); -}); - -test('Readable.wrap should call destroy on "close" event', () => { - const oldStream = new EventEmitter(); - oldStream.pause = jest.fn(); - oldStream.resume = jest.fn(); - - const destroyMock = jest.fn(); - - const readable = new Readable({ - autoDestroy: false, - destroy: destroyMock, - }); - - readable.wrap(oldStream); - oldStream.emit("close"); - - expect(destroyMock).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-stream2-readable-wrap-destroy.js diff --git a/test/js/node/test/parallel/stream2-readable-wrap-empty.test.js b/test/js/node/test/parallel/stream2-readable-wrap-empty.test.js deleted file mode 100644 index 9735960e5b..0000000000 --- a/test/js/node/test/parallel/stream2-readable-wrap-empty.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-stream2-readable-wrap-empty.js -//#SHA1: aaac82ec7df0743321f2aaacd9512ecf1b932ad6 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const { Readable } = require("stream"); -const EventEmitter = require("events"); - -test("Readable.wrap with empty stream", done => { - const oldStream = new EventEmitter(); - oldStream.pause = jest.fn(); - oldStream.resume = jest.fn(); - - const newStream = new Readable().wrap(oldStream); - - newStream - .on("readable", () => {}) - .on("end", () => { - done(); - }); - - oldStream.emit("end"); -}); - -//<#END_FILE: test-stream2-readable-wrap-empty.js diff --git a/test/js/node/test/parallel/stream2-unpipe-drain.test.js b/test/js/node/test/parallel/stream2-unpipe-drain.test.js deleted file mode 100644 index a2ea536ef6..0000000000 --- a/test/js/node/test/parallel/stream2-unpipe-drain.test.js +++ /dev/null @@ -1,75 +0,0 @@ -//#FILE: test-stream2-unpipe-drain.js -//#SHA1: b04d9c383281786f45989d8d7f85f6f1a620bde2 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const stream = require("stream"); - -class TestWriter extends stream.Writable { - _write(buffer, encoding, callback) { - console.log("write called"); - // Super slow write stream (callback never called) - } -} - -class TestReader extends stream.Readable { - constructor() { - super(); - this.reads = 0; - } - - _read(size) { - this.reads += 1; - this.push(Buffer.alloc(size)); - } -} - -test("stream2 unpipe drain", done => { - const dest = new TestWriter(); - const src1 = new TestReader(); - const src2 = new TestReader(); - - src1.pipe(dest); - - src1.once("readable", () => { - process.nextTick(() => { - src2.pipe(dest); - - src2.once("readable", () => { - process.nextTick(() => { - src1.unpipe(dest); - - // Use setImmediate to ensure all microtasks have been processed - setImmediate(() => { - expect(src1.reads).toBe(2); - expect(src2.reads).toBe(2); - done(); - }); - }); - }); - }); - }); -}); - -//<#END_FILE: test-stream2-unpipe-drain.js diff --git a/test/js/node/test/parallel/stream3-cork-end.test.js b/test/js/node/test/parallel/stream3-cork-end.test.js deleted file mode 100644 index 974e12ce1a..0000000000 --- a/test/js/node/test/parallel/stream3-cork-end.test.js +++ /dev/null @@ -1,98 +0,0 @@ -//#FILE: test-stream3-cork-end.js -//#SHA1: 1ac6a2589bee41bc1e9e08ef308bcae3cd999106 -//----------------- -"use strict"; - -const stream = require("stream"); -const Writable = stream.Writable; - -// Test the buffering behavior of Writable streams. -// -// The call to cork() triggers storing chunks which are flushed -// on calling end() and the stream subsequently ended. -// -// node version target: 0.12 - -test("Writable stream buffering behavior with cork() and end()", done => { - const expectedChunks = ["please", "buffer", "me", "kindly"]; - const inputChunks = expectedChunks.slice(0); - let seenChunks = []; - let seenEnd = false; - - const w = new Writable(); - // Let's arrange to store the chunks. - w._write = function (chunk, encoding, cb) { - // Stream end event is not seen before the last write. - expect(seenEnd).toBe(false); - // Default encoding given none was specified. - expect(encoding).toBe("buffer"); - - seenChunks.push(chunk); - cb(); - }; - // Let's record the stream end event. - w.on("finish", () => { - seenEnd = true; - }); - - function writeChunks(remainingChunks, callback) { - const writeChunk = remainingChunks.shift(); - let writeState; - - if (writeChunk) { - setImmediate(() => { - writeState = w.write(writeChunk); - // We were not told to stop writing. - expect(writeState).toBe(true); - - writeChunks(remainingChunks, callback); - }); - } else { - callback(); - } - } - - // Do an initial write. - w.write("stuff"); - // The write was immediate. - expect(seenChunks.length).toBe(1); - // Reset the seen chunks. - seenChunks = []; - - // Trigger stream buffering. - w.cork(); - - // Write the bufferedChunks. - writeChunks(inputChunks, () => { - // Should not have seen anything yet. - expect(seenChunks.length).toBe(0); - - // Trigger flush and ending the stream. - w.end(); - - // Stream should not ended in current tick. - expect(seenEnd).toBe(false); - - // Buffered bytes should be seen in current tick. - expect(seenChunks.length).toBe(4); - - // Did the chunks match. - for (let i = 0, l = expectedChunks.length; i < l; i++) { - const seen = seenChunks[i]; - // There was a chunk. - expect(seen).toBeTruthy(); - - const expected = Buffer.from(expectedChunks[i]); - // It was what we expected. - expect(seen.equals(expected)).toBe(true); - } - - setImmediate(() => { - // Stream should have ended in next tick. - expect(seenEnd).toBe(true); - done(); - }); - }); -}); - -//<#END_FILE: test-stream3-cork-end.js diff --git a/test/js/node/test/parallel/stream3-cork-uncork.test.js b/test/js/node/test/parallel/stream3-cork-uncork.test.js deleted file mode 100644 index 85aa626dec..0000000000 --- a/test/js/node/test/parallel/stream3-cork-uncork.test.js +++ /dev/null @@ -1,104 +0,0 @@ -//#FILE: test-stream3-cork-uncork.js -//#SHA1: d1cc0d9e9be4ae657ab2db8e02589ac485268c63 -//----------------- -"use strict"; - -const stream = require("stream"); -const Writable = stream.Writable; - -// Test the buffering behavior of Writable streams. -// -// The call to cork() triggers storing chunks which are flushed -// on calling uncork() in the same tick. -// -// node version target: 0.12 - -describe("Writable stream cork and uncork", () => { - const expectedChunks = ["please", "buffer", "me", "kindly"]; - let inputChunks; - let seenChunks; - let seenEnd; - let w; - - beforeEach(() => { - inputChunks = expectedChunks.slice(0); - seenChunks = []; - seenEnd = false; - - w = new Writable(); - // Let's arrange to store the chunks. - w._write = function (chunk, encoding, cb) { - // Default encoding given none was specified. - expect(encoding).toBe("buffer"); - - seenChunks.push(chunk); - cb(); - }; - // Let's record the stream end event. - w.on("finish", () => { - seenEnd = true; - }); - }); - - function writeChunks(remainingChunks) { - return new Promise(resolve => { - function write() { - const writeChunk = remainingChunks.shift(); - if (writeChunk) { - setImmediate(() => { - const writeState = w.write(writeChunk); - // We were not told to stop writing. - expect(writeState).toBe(true); - write(); - }); - } else { - resolve(); - } - } - write(); - }); - } - - test("initial write is immediate", () => { - w.write("stuff"); - // The write was immediate. - expect(seenChunks.length).toBe(1); - }); - - test("cork buffers writes and uncork flushes", async () => { - // Reset the chunks seen so far. - seenChunks = []; - - // Trigger stream buffering. - w.cork(); - - // Write the bufferedChunks. - await writeChunks(inputChunks); - - // Should not have seen anything yet. - expect(seenChunks.length).toBe(0); - - // Trigger writing out the buffer. - w.uncork(); - - // Buffered bytes should be seen in current tick. - expect(seenChunks.length).toBe(4); - - // Did the chunks match. - for (let i = 0, l = expectedChunks.length; i < l; i++) { - const seen = seenChunks[i]; - // There was a chunk. - expect(seen).toBeTruthy(); - - const expected = Buffer.from(expectedChunks[i]); - // It was what we expected. - expect(seen.equals(expected)).toBe(true); - } - - await new Promise(resolve => setImmediate(resolve)); - // The stream should not have been ended. - expect(seenEnd).toBe(false); - }); -}); - -//<#END_FILE: test-stream3-cork-uncork.js diff --git a/test/js/node/test/parallel/stream3-pause-then-read.test.js b/test/js/node/test/parallel/stream3-pause-then-read.test.js deleted file mode 100644 index f7df831524..0000000000 --- a/test/js/node/test/parallel/stream3-pause-then-read.test.js +++ /dev/null @@ -1,187 +0,0 @@ -//#FILE: test-stream3-pause-then-read.js -//#SHA1: bd44dc04c63140e4b65c0755eec67a55eaf48158 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const stream = require("stream"); -const Readable = stream.Readable; -const Writable = stream.Writable; - -const totalChunks = 100; -const chunkSize = 99; -const expectTotalData = totalChunks * chunkSize; -let expectEndingData = expectTotalData; - -let r, totalPushed; - -beforeEach(() => { - r = new Readable({ highWaterMark: 1000 }); - let chunks = totalChunks; - r._read = function (n) { - console.log("_read called", chunks); - if (!(chunks % 2)) setImmediate(push); - else if (!(chunks % 3)) process.nextTick(push); - else push(); - }; - - totalPushed = 0; - function push() { - const chunk = chunks-- > 0 ? Buffer.alloc(chunkSize, "x") : null; - if (chunk) { - totalPushed += chunk.length; - } - console.log("chunks", chunks); - r.push(chunk); - } -}); - -test("stream3 pause then read", async () => { - await read100(); - await new Promise(resolve => setImmediate(resolve)); - await pipeLittle(); - await read1234(); - await resumePause(); - await pipe(); -}); - -// First we read 100 bytes. -async function read100() { - await readn(100); -} - -async function readn(n) { - console.error(`read ${n}`); - expectEndingData -= n; - return new Promise(resolve => { - function read() { - const c = r.read(n); - console.error("c", c); - if (!c) r.once("readable", read); - else { - expect(c.length).toBe(n); - expect(r.readableFlowing).toBeFalsy(); - resolve(); - } - } - read(); - }); -} - -// Then we listen to some data events. -function onData() { - return new Promise(resolve => { - expectEndingData -= 100; - console.error("onData"); - let seen = 0; - r.on("data", function od(c) { - seen += c.length; - if (seen >= 100) { - // Seen enough - r.removeListener("data", od); - r.pause(); - if (seen > 100) { - // Oh no, seen too much! - // Put the extra back. - const diff = seen - 100; - r.unshift(c.slice(c.length - diff)); - console.error("seen too much", seen, diff); - } - resolve(); - } - }); - }); -} - -// Just pipe 200 bytes, then unshift the extra and unpipe. -async function pipeLittle() { - expectEndingData -= 200; - console.error("pipe a little"); - const w = new Writable(); - let written = 0; - await new Promise(resolve => { - w.on("finish", () => { - expect(written).toBe(200); - resolve(); - }); - w._write = function (chunk, encoding, cb) { - written += chunk.length; - if (written >= 200) { - r.unpipe(w); - w.end(); - cb(); - if (written > 200) { - const diff = written - 200; - written -= diff; - r.unshift(chunk.slice(chunk.length - diff)); - } - } else { - setImmediate(cb); - } - }; - r.pipe(w); - }); -} - -// Now read 1234 more bytes. -async function read1234() { - await readn(1234); -} - -function resumePause() { - console.error("resumePause"); - // Don't read anything, just resume and re-pause a whole bunch. - r.resume(); - r.pause(); - r.resume(); - r.pause(); - r.resume(); - r.pause(); - r.resume(); - r.pause(); - r.resume(); - r.pause(); - return new Promise(resolve => setImmediate(resolve)); -} - -function pipe() { - console.error("pipe the rest"); - const w = new Writable(); - let written = 0; - w._write = function (chunk, encoding, cb) { - written += chunk.length; - cb(); - }; - return new Promise(resolve => { - w.on("finish", () => { - console.error("written", written, totalPushed); - expect(written).toBe(expectEndingData); - expect(totalPushed).toBe(expectTotalData); - console.log("ok"); - resolve(); - }); - r.pipe(w); - }); -} - -//<#END_FILE: test-stream3-pause-then-read.js diff --git a/test/js/node/test/parallel/stream3-pipeline-async-iterator.test.js b/test/js/node/test/parallel/stream3-pipeline-async-iterator.test.js deleted file mode 100644 index d84b763fcc..0000000000 --- a/test/js/node/test/parallel/stream3-pipeline-async-iterator.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-stream3-pipeline-async-iterator.js -//#SHA1: db2d5b4cb6c502fdccdfa1ed9384d6baa70b1e0b -//----------------- -/* eslint-disable node-core/require-common-first, require-yield */ -"use strict"; -const { pipeline } = require("node:stream/promises"); - -test("async iterators can act as readable and writable streams", async () => { - // Ensure that async iterators can act as readable and writable streams - async function* myCustomReadable() { - yield "Hello"; - yield "World"; - } - - const messages = []; - async function* myCustomWritable(stream) { - for await (const chunk of stream) { - messages.push(chunk); - } - } - - await pipeline(myCustomReadable, myCustomWritable); - - expect(messages).toEqual(["Hello", "World"]); -}); - -//<#END_FILE: test-stream3-pipeline-async-iterator.js diff --git a/test/js/node/test/parallel/string-decoder-end.test.js b/test/js/node/test/parallel/string-decoder-end.test.js deleted file mode 100644 index fc12a567f7..0000000000 --- a/test/js/node/test/parallel/string-decoder-end.test.js +++ /dev/null @@ -1,124 +0,0 @@ -//#FILE: test-string-decoder-end.js -//#SHA1: 9b9cd65cf41dc419c54b8c47317aef7fcb251c5c -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Verify that the string decoder works getting 1 byte at a time, -// the whole buffer at once, and that both match the .toString(enc) -// result of the entire buffer. - -const assert = require("assert"); -const SD = require("string_decoder").StringDecoder; -const encodings = ["base64", "base64url", "hex", "utf8", "utf16le", "ucs2"]; - -const bufs = ["☃💩", "asdf"].map(b => Buffer.from(b)); - -// Also test just arbitrary bytes from 0-15. -for (let i = 1; i <= 16; i++) { - const bytes = "." - .repeat(i - 1) - .split(".") - .map((_, j) => j + 0x78); - bufs.push(Buffer.from(bytes)); -} - -encodings.forEach(testEncoding); - -function testEncoding(encoding) { - bufs.forEach(buf => { - testBuf(encoding, buf); - }); -} - -function testBuf(encoding, buf) { - test(`StringDecoder ${encoding} - ${buf.toString()}`, () => { - // Write one byte at a time. - let s = new SD(encoding); - let res1 = ""; - for (let i = 0; i < buf.length; i++) { - res1 += s.write(buf.slice(i, i + 1)); - } - res1 += s.end(); - - // Write the whole buffer at once. - let res2 = ""; - s = new SD(encoding); - res2 += s.write(buf); - res2 += s.end(); - - // .toString() on the buffer - const res3 = buf.toString(encoding); - - // One byte at a time should match toString - expect(res1).toBe(res3); - // All bytes at once should match toString - expect(res2).toBe(res3); - }); -} - -function testEnd(encoding, incomplete, next, expected) { - test(`StringDecoder ${encoding} end - ${incomplete.toString("hex")} + ${next.toString("hex")}`, () => { - let res = ""; - const s = new SD(encoding); - res += s.write(incomplete); - res += s.end(); - res += s.write(next); - res += s.end(); - - expect(res).toBe(expected); - }); -} - -testEnd("utf8", Buffer.of(0xe2), Buffer.of(0x61), "\uFFFDa"); -testEnd("utf8", Buffer.of(0xe2), Buffer.of(0x82), "\uFFFD\uFFFD"); -testEnd("utf8", Buffer.of(0xe2), Buffer.of(0xe2), "\uFFFD\uFFFD"); -testEnd("utf8", Buffer.of(0xe2, 0x82), Buffer.of(0x61), "\uFFFDa"); -testEnd("utf8", Buffer.of(0xe2, 0x82), Buffer.of(0xac), "\uFFFD\uFFFD"); -testEnd("utf8", Buffer.of(0xe2, 0x82), Buffer.of(0xe2), "\uFFFD\uFFFD"); -testEnd("utf8", Buffer.of(0xe2, 0x82, 0xac), Buffer.of(0x61), "€a"); - -testEnd("utf16le", Buffer.of(0x3d), Buffer.of(0x61, 0x00), "a"); -testEnd("utf16le", Buffer.of(0x3d), Buffer.of(0xd8, 0x4d, 0xdc), "\u4DD8"); -testEnd("utf16le", Buffer.of(0x3d, 0xd8), Buffer.of(), "\uD83D"); -testEnd("utf16le", Buffer.of(0x3d, 0xd8), Buffer.of(0x61, 0x00), "\uD83Da"); -testEnd("utf16le", Buffer.of(0x3d, 0xd8), Buffer.of(0x4d, 0xdc), "\uD83D\uDC4D"); -testEnd("utf16le", Buffer.of(0x3d, 0xd8, 0x4d), Buffer.of(), "\uD83D"); -testEnd("utf16le", Buffer.of(0x3d, 0xd8, 0x4d), Buffer.of(0x61, 0x00), "\uD83Da"); -testEnd("utf16le", Buffer.of(0x3d, 0xd8, 0x4d), Buffer.of(0xdc), "\uD83D"); -testEnd("utf16le", Buffer.of(0x3d, 0xd8, 0x4d, 0xdc), Buffer.of(0x61, 0x00), "👍a"); - -testEnd("base64", Buffer.of(0x61), Buffer.of(), "YQ=="); -testEnd("base64", Buffer.of(0x61), Buffer.of(0x61), "YQ==YQ=="); -testEnd("base64", Buffer.of(0x61, 0x61), Buffer.of(), "YWE="); -testEnd("base64", Buffer.of(0x61, 0x61), Buffer.of(0x61), "YWE=YQ=="); -testEnd("base64", Buffer.of(0x61, 0x61, 0x61), Buffer.of(), "YWFh"); -testEnd("base64", Buffer.of(0x61, 0x61, 0x61), Buffer.of(0x61), "YWFhYQ=="); - -testEnd("base64url", Buffer.of(0x61), Buffer.of(), "YQ"); -testEnd("base64url", Buffer.of(0x61), Buffer.of(0x61), "YQYQ"); -testEnd("base64url", Buffer.of(0x61, 0x61), Buffer.of(), "YWE"); -testEnd("base64url", Buffer.of(0x61, 0x61), Buffer.of(0x61), "YWEYQ"); -testEnd("base64url", Buffer.of(0x61, 0x61, 0x61), Buffer.of(), "YWFh"); -testEnd("base64url", Buffer.of(0x61, 0x61, 0x61), Buffer.of(0x61), "YWFhYQ"); - -//<#END_FILE: test-string-decoder-end.js diff --git a/test/js/node/test/parallel/sys.test.js b/test/js/node/test/parallel/sys.test.js deleted file mode 100644 index eb25bc0689..0000000000 --- a/test/js/node/test/parallel/sys.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-sys.js -//#SHA1: a7732f65863d5e2856179378dc44f09f2b315650 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const sys = require("sys"); // eslint-disable-line no-restricted-modules -const util = require("util"); - -test("sys module is identical to util module", () => { - expect(sys).toBe(util); -}); - -//<#END_FILE: test-sys.js diff --git a/test/js/node/test/parallel/tcp-wrap-connect.test.js b/test/js/node/test/parallel/tcp-wrap-connect.test.js deleted file mode 100644 index 819fd28e18..0000000000 --- a/test/js/node/test/parallel/tcp-wrap-connect.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-tcp-wrap-connect.js -//#SHA1: cc302b52d997beac187400587ce2dffc0978a7da -//----------------- -"use strict"; - -const net = require("net"); - -let connectCount = 0; -let endCount = 0; -let shutdownCount = 0; - -function makeConnection(server) { - return new Promise((resolve, reject) => { - const client = new net.Socket(); - - client.connect(server.address().port, "127.0.0.1", () => { - expect(client.readable).toBe(true); - expect(client.writable).toBe(true); - - client.end(() => { - shutdownCount++; - client.destroy(); - resolve(); - }); - }); - - client.on("error", reject); - }); -} - -test("TCP connection and shutdown", async () => { - const server = net.createServer(socket => { - connectCount++; - socket.resume(); - socket.on("end", () => { - endCount++; - socket.destroy(); - server.close(); - }); - }); - - await new Promise(resolve => server.listen(0, resolve)); - - await makeConnection(server); - - await new Promise(resolve => server.on("close", resolve)); - - expect(shutdownCount).toBe(1); - expect(connectCount).toBe(1); - expect(endCount).toBe(1); -}); - -//<#END_FILE: test-tcp-wrap-connect.js diff --git a/test/js/node/test/parallel/test-arm-math-illegal-instruction.js b/test/js/node/test/parallel/test-arm-math-illegal-instruction.js new file mode 100644 index 0000000000..4bf881d1b3 --- /dev/null +++ b/test/js/node/test/parallel/test-arm-math-illegal-instruction.js @@ -0,0 +1,15 @@ +'use strict'; +require('../common'); + +// This test ensures Math functions don't fail with an "illegal instruction" +// error on ARM devices (primarily on the Raspberry Pi 1) +// See https://github.com/nodejs/node/issues/1376 +// and https://code.google.com/p/v8/issues/detail?id=4019 + +// Iterate over all Math functions +Object.getOwnPropertyNames(Math).forEach((functionName) => { + if (!/[A-Z]/.test(functionName)) { + // The function names don't have capital letters. + Math[functionName](-0.5); + } +}); diff --git a/test/js/node/test/parallel/test-assert-builtins-not-read-from-filesystem.js b/test/js/node/test/parallel/test-assert-builtins-not-read-from-filesystem.js new file mode 100644 index 0000000000..7a713a2ea4 --- /dev/null +++ b/test/js/node/test/parallel/test-assert-builtins-not-read-from-filesystem.js @@ -0,0 +1,48 @@ +'use strict'; + +// Do not read filesystem when creating AssertionError messages for code in +// builtin modules. + +require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); +const e = new EventEmitter(); +e.on('hello', assert); + +if (process.argv[2] !== 'child') { + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + const { spawnSync } = require('child_process'); + + let threw = false; + try { + e.emit('hello', false); + } catch (err) { + const frames = err.stack.split('\n'); + const [, filename, line, column] = frames[1].match(/\((.+):(\d+):(\d+)\)/); + // Spawn a child process to avoid the error having been cached in the assert + // module's `errorCache` Map. + + const { output, status, error } = + spawnSync(process.execPath, + [process.argv[1], 'child', filename, line, column], + { cwd: tmpdir.path, env: process.env }); + assert.ifError(error); + assert.strictEqual(status, 0, `Exit code: ${status}\n${output}`); + threw = true; + } + assert.ok(threw); +} else { + const { writeFileSync } = require('fs'); + const [, , , filename, line, column] = process.argv; + const data = `${'\n'.repeat(line - 1)}${' '.repeat(column - 1)}` + + 'ok(failed(badly));'; + + writeFileSync(filename, data); + assert.throws( + () => e.emit('hello', false), + { + message: 'false == true' + } + ); +} diff --git a/test/js/node/test/parallel/test-assert-strict-exists.js b/test/js/node/test/parallel/test-assert-strict-exists.js new file mode 100644 index 0000000000..50cd8a49a7 --- /dev/null +++ b/test/js/node/test/parallel/test-assert-strict-exists.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(require('assert/strict'), assert.strict); diff --git a/test/js/node/test/parallel/test-async-hooks-asyncresource-constructor.js b/test/js/node/test/parallel/test-async-hooks-asyncresource-constructor.js new file mode 100644 index 0000000000..8b504aa7a7 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-asyncresource-constructor.js @@ -0,0 +1,41 @@ +'use strict'; + +// This tests that AsyncResource throws an error if bad parameters are passed + +require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { AsyncResource } = async_hooks; + +// Setup init hook such parameters are validated +async_hooks.createHook({ + init() {} +}).enable(); + +assert.throws(() => { + return new AsyncResource(); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', +}); + +assert.throws(() => { + new AsyncResource(''); +}, { + code: 'ERR_ASYNC_TYPE', + name: 'TypeError', +}); + +assert.throws(() => { + new AsyncResource('type', -4); +}, { + code: 'ERR_INVALID_ASYNC_ID', + name: 'RangeError', +}); + +assert.throws(() => { + new AsyncResource('type', Math.PI); +}, { + code: 'ERR_INVALID_ASYNC_ID', + name: 'RangeError', +}); diff --git a/test/js/node/test/parallel/test-async-hooks-constructor.js b/test/js/node/test/parallel/test-async-hooks-constructor.js new file mode 100644 index 0000000000..62ec854108 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-constructor.js @@ -0,0 +1,21 @@ +'use strict'; + +// This tests that AsyncHooks throws an error if bad parameters are passed. + +require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const nonFunctionArray = [null, -1, 1, {}, []]; + +['init', 'before', 'after', 'destroy', 'promiseResolve'].forEach( + (functionName) => { + nonFunctionArray.forEach((nonFunction) => { + assert.throws(() => { + async_hooks.createHook({ [functionName]: nonFunction }); + }, { + code: 'ERR_ASYNC_CALLBACK', + name: 'TypeError', + message: `hook.${functionName} must be a function`, + }); + }); + }); diff --git a/test/js/node/test/parallel/test-async-hooks-recursive-stack-runInAsyncScope.js b/test/js/node/test/parallel/test-async-hooks-recursive-stack-runInAsyncScope.js new file mode 100644 index 0000000000..bc4ac86e7f --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-recursive-stack-runInAsyncScope.js @@ -0,0 +1,20 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); + +// This test verifies that the async ID stack can grow indefinitely. + +function recurse(n) { + const a = new async_hooks.AsyncResource('foobar'); + a.runInAsyncScope(() => { + assert.strictEqual(a.asyncId(), async_hooks.executionAsyncId()); + assert.strictEqual(a.triggerAsyncId(), async_hooks.triggerAsyncId()); + if (n >= 0) + recurse(n - 1); + assert.strictEqual(a.asyncId(), async_hooks.executionAsyncId()); + assert.strictEqual(a.triggerAsyncId(), async_hooks.triggerAsyncId()); + }); +} + +recurse(1000); diff --git a/test/js/node/test/parallel/test-async-hooks-run-in-async-scope-caught-exception.js b/test/js/node/test/parallel/test-async-hooks-run-in-async-scope-caught-exception.js new file mode 100644 index 0000000000..e38cefd20e --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-run-in-async-scope-caught-exception.js @@ -0,0 +1,11 @@ +'use strict'; + +require('../common'); +const { AsyncResource } = require('async_hooks'); + +try { + new AsyncResource('foo').runInAsyncScope(() => { throw new Error('bar'); }); +} catch { + // Continue regardless of error. +} +// Should abort (fail the case) if async id is not matching. diff --git a/test/js/node/test/parallel/test-async-hooks-run-in-async-scope-this-arg.js b/test/js/node/test/parallel/test-async-hooks-run-in-async-scope-this-arg.js new file mode 100644 index 0000000000..a5016da9d5 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-run-in-async-scope-this-arg.js @@ -0,0 +1,17 @@ +'use strict'; + +// Test that passing thisArg to runInAsyncScope() works. + +const common = require('../common'); +const assert = require('assert'); +const { AsyncResource } = require('async_hooks'); + +const thisArg = {}; + +const res = new AsyncResource('fhqwhgads'); + +function callback() { + assert.strictEqual(this, thisArg); +} + +res.runInAsyncScope(common.mustCall(callback), thisArg); diff --git a/test/js/node/test/parallel/test-async-hooks-vm-gc.js b/test/js/node/test/parallel/test-async-hooks-vm-gc.js new file mode 100644 index 0000000000..da95e3579d --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-vm-gc.js @@ -0,0 +1,15 @@ +// Flags: --expose-gc +'use strict'; + +require('../common'); +const asyncHooks = require('async_hooks'); +const vm = require('vm'); + +// This is a regression test for https://github.com/nodejs/node/issues/39019 +// +// It should not segfault. + +const hook = asyncHooks.createHook({ init() {} }).enable(); +vm.createContext(); +globalThis.gc(); +hook.disable(); diff --git a/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-1.js b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-1.js new file mode 100644 index 0000000000..eb16645913 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-1.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +const w = new Worker(` +const { createHook } = require('async_hooks'); + +setImmediate(async () => { + createHook({ init() {} }).enable(); + await 0; + process.exit(); +}); +`, { eval: true }); + +w.on('exit', common.mustCall()); diff --git a/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-2.js b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-2.js new file mode 100644 index 0000000000..049264d3e8 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-2.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// Like test-async-hooks-worker-promise.js but with the `await` and `createHook` +// lines switched, because that resulted in different assertion failures +// (one a Node.js assertion and one a V8 DCHECK) and it seems prudent to +// cover both of those failures. + +const w = new Worker(` +const { createHook } = require('async_hooks'); + +setImmediate(async () => { + await 0; + createHook({ init() {} }).enable(); + process.exit(); +}); +`, { eval: true }); + +w.postMessage({}); +w.on('exit', common.mustCall()); diff --git a/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-3.js b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-3.js new file mode 100644 index 0000000000..40c7d85835 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-3.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// Like test-async-hooks-worker-promise.js but with an additional statement +// after the `process.exit()` call, that shouldn’t really make a difference +// but apparently does. + +const w = new Worker(` +const { createHook } = require('async_hooks'); + +setImmediate(async () => { + createHook({ init() {} }).enable(); + await 0; + process.exit(); + process._rawDebug('THIS SHOULD NEVER BE REACHED'); +}); +`, { eval: true }); + +w.on('exit', common.mustCall()); diff --git a/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-4.js b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-4.js new file mode 100644 index 0000000000..c522091006 --- /dev/null +++ b/test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-4.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Like test-async-hooks-worker-promise.js but doing a trivial counter increase +// after process.exit(). This should not make a difference, but apparently it +// does. This is *also* different from test-async-hooks-worker-promise-3.js, +// in that the statement is an ArrayBuffer access rather than a full method, +// which *also* makes a difference even though it shouldn’t. + +const workerData = new Int32Array(new SharedArrayBuffer(4)); +const w = new Worker(` +const { createHook } = require('async_hooks'); +const { workerData } = require('worker_threads'); + +setImmediate(async () => { + createHook({ init() {} }).enable(); + await 0; + process.exit(); + workerData[0]++; +}); +`, { eval: true, workerData }); + +w.on('exit', common.mustCall(() => assert.strictEqual(workerData[0], 0))); diff --git a/test/js/node/test/parallel/test-async-local-storage-bind.js b/test/js/node/test/parallel/test-async-local-storage-bind.js new file mode 100644 index 0000000000..d8d4c45998 --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-bind.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +[1, false, '', {}, []].forEach((i) => { + assert.throws(() => AsyncLocalStorage.bind(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +const fn = common.mustCall(AsyncLocalStorage.bind(() => 123)); +assert.strictEqual(fn(), 123); + +const fn2 = AsyncLocalStorage.bind(common.mustCall((arg) => assert.strictEqual(arg, 'test'))); +fn2('test'); diff --git a/test/js/node/test/parallel/test-async-local-storage-contexts.js b/test/js/node/test/parallel/test-async-local-storage-contexts.js new file mode 100644 index 0000000000..9a63271337 --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-contexts.js @@ -0,0 +1,35 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { AsyncLocalStorage } = require('async_hooks'); + +// Regression test for https://github.com/nodejs/node/issues/38781 + +const context = vm.createContext({ + AsyncLocalStorage, + assert +}); + +vm.runInContext(` + const storage = new AsyncLocalStorage() + async function test() { + return storage.run({ test: 'vm' }, async () => { + assert.strictEqual(storage.getStore().test, 'vm'); + await 42; + assert.strictEqual(storage.getStore().test, 'vm'); + }); + } + test() +`, context); + +const storage = new AsyncLocalStorage(); +async function test() { + return storage.run({ test: 'main context' }, async () => { + assert.strictEqual(storage.getStore().test, 'main context'); + await 42; + assert.strictEqual(storage.getStore().test, 'main context'); + }); +} +test(); diff --git a/test/js/node/test/parallel/test-async-local-storage-deep-stack.js b/test/js/node/test/parallel/test-async-local-storage-deep-stack.js new file mode 100644 index 0000000000..b5e1048d94 --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-deep-stack.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); + +// Regression test for: https://github.com/nodejs/node/issues/34556 + +const als = new AsyncLocalStorage(); + +const done = common.mustCall(); + +function run(count) { + if (count !== 0) return als.run({}, run, --count); + done(); +} +run(1000); diff --git a/test/js/node/test/parallel/test-async-local-storage-exit-does-not-leak.js b/test/js/node/test/parallel/test-async-local-storage-exit-does-not-leak.js new file mode 100644 index 0000000000..61a3397240 --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-exit-does-not-leak.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const als = new AsyncLocalStorage(); + +// The _propagate function only exists on the old JavaScript implementation. +if (typeof als._propagate === 'function') { + // The als instance should be getting removed from the storageList in + // lib/async_hooks.js when exit(...) is called, therefore when the nested runs + // are called there should be no copy of the als in the storageList to run the + // _propagate method on. + als._propagate = common.mustNotCall('_propagate() should not be called'); +} + +const done = common.mustCall(); + +const data = true; + +function run(count) { + if (count === 0) return done(); + assert.notStrictEqual(als.getStore(), data); + als.run(data, () => { + als.exit(run, --count); + }); +} +run(100); diff --git a/test/js/node/test/parallel/test-async-local-storage-http-multiclients.js b/test/js/node/test/parallel/test-async-local-storage-http-multiclients.js new file mode 100644 index 0000000000..1903d5825d --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-http-multiclients.js @@ -0,0 +1,65 @@ +'use strict'; +const common = require('../common'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); +const http = require('http'); +const cls = new AsyncLocalStorage(); +const NUM_CLIENTS = 10; + +// Run multiple clients that receive data from a server +// in multiple chunks, in a single non-closure function. +// Use the AsyncLocalStorage (ALS) APIs to maintain the context +// and data download. Make sure that individual clients +// receive their respective data, with no conflicts. + +// Set up a server that sends large buffers of data, filled +// with cardinal numbers, increasing per request +let index = 0; +const server = http.createServer((q, r) => { + // Send a large chunk as response, otherwise the data + // may be sent in a single chunk, and the callback in the + // client may be called only once, defeating the purpose of test + r.end((index++ % 10).toString().repeat(1024 * 1024)); +}); + +const countdown = new Countdown(NUM_CLIENTS, () => { + server.close(); +}); + +server.listen(0, common.mustCall(() => { + for (let i = 0; i < NUM_CLIENTS; i++) { + cls.run(new Map(), common.mustCall(() => { + const options = { port: server.address().port }; + const req = http.get(options, common.mustCall((res) => { + const store = cls.getStore(); + store.set('data', ''); + + // Make ondata and onend non-closure + // functions and fully dependent on ALS + res.setEncoding('utf8'); + res.on('data', ondata); + res.on('end', common.mustCall(onend)); + })); + req.end(); + })); + } +})); + +// Accumulate the current data chunk with the store data +function ondata(d) { + const store = cls.getStore(); + assert.notStrictEqual(store, undefined); + let chunk = store.get('data'); + chunk += d; + store.set('data', chunk); +} + +// Retrieve the store data, and test for homogeneity +function onend() { + const store = cls.getStore(); + assert.notStrictEqual(store, undefined); + const data = store.get('data'); + assert.strictEqual(data, data[0].repeat(data.length)); + countdown.dec(); +} diff --git a/test/js/node/test/parallel/test-async-local-storage-snapshot.js b/test/js/node/test/parallel/test-async-local-storage-snapshot.js new file mode 100644 index 0000000000..63e47ba3ce --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-snapshot.js @@ -0,0 +1,16 @@ +'use strict'; + +const common = require('../common'); +const { strictEqual } = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const runInAsyncScope = + asyncLocalStorage.run(123, common.mustCall(() => AsyncLocalStorage.snapshot())); +const result = + asyncLocalStorage.run(321, common.mustCall(() => { + return runInAsyncScope(() => { + return asyncLocalStorage.getStore(); + }); + })); +strictEqual(result, 123); diff --git a/test/js/node/test/parallel/test-async-wrap-constructor.js b/test/js/node/test/parallel/test-async-wrap-constructor.js new file mode 100644 index 0000000000..853898aa0a --- /dev/null +++ b/test/js/node/test/parallel/test-async-wrap-constructor.js @@ -0,0 +1,21 @@ +'use strict'; + +// This tests that using falsy values in createHook throws an error. + +require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); + +const falsyValues = [0, 1, false, true, null, 'hello']; +for (const badArg of falsyValues) { + const hookNames = ['init', 'before', 'after', 'destroy', 'promiseResolve']; + for (const hookName of hookNames) { + assert.throws(() => { + async_hooks.createHook({ [hookName]: badArg }); + }, { + code: 'ERR_ASYNC_CALLBACK', + name: 'TypeError', + message: `hook.${hookName} must be a function` + }); + } +} diff --git a/test/js/node/test/parallel/test-atomics-wake.js b/test/js/node/test/parallel/test-atomics-wake.js new file mode 100644 index 0000000000..0f38700176 --- /dev/null +++ b/test/js/node/test/parallel/test-atomics-wake.js @@ -0,0 +1,7 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// https://github.com/nodejs/node/issues/21219 +assert.strictEqual(Atomics.wake, undefined); diff --git a/test/js/node/test/parallel/test-bad-unicode.js b/test/js/node/test/parallel/test-bad-unicode.js new file mode 100644 index 0000000000..b4fccc0644 --- /dev/null +++ b/test/js/node/test/parallel/test-bad-unicode.js @@ -0,0 +1,33 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +let exception = null; + +try { + eval('"\\uc/ef"'); +} catch (e) { + exception = e; +} + +assert(exception instanceof SyntaxError); diff --git a/test/js/node/test/parallel/test-beforeexit-event-exit.js b/test/js/node/test/parallel/test-beforeexit-event-exit.js new file mode 100644 index 0000000000..4210ad04b6 --- /dev/null +++ b/test/js/node/test/parallel/test-beforeexit-event-exit.js @@ -0,0 +1,27 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { mustNotCall } = require('../common'); + +process.on('beforeExit', mustNotCall('exit should not allow this to occur')); + +process.exit(); diff --git a/test/js/node/test/parallel/test-binding-constants.js b/test/js/node/test/parallel/test-binding-constants.js new file mode 100644 index 0000000000..bd6e533816 --- /dev/null +++ b/test/js/node/test/parallel/test-binding-constants.js @@ -0,0 +1,32 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const constants = process.binding('constants'); +const assert = require('assert'); + +assert.deepStrictEqual( + Object.keys(constants).sort(), ['crypto', 'fs', 'os', 'trace', 'zlib'] +); + +assert.deepStrictEqual( + Object.keys(constants.os).sort(), ['UV_UDP_REUSEADDR', 'dlopen', 'errno', + 'priority', 'signals'] +); + +// Make sure all the constants objects don't inherit from Object.prototype +const inheritedProperties = Object.getOwnPropertyNames(Object.prototype); +function test(obj) { + assert(obj); + assert.strictEqual(Object.prototype.toString.call(obj), '[object Object]'); + assert.strictEqual(Object.getPrototypeOf(obj), null); + + inheritedProperties.forEach((property) => { + assert.strictEqual(property in obj, false); + }); +} + +[ + constants, constants.crypto, constants.fs, constants.os, constants.trace, + constants.zlib, constants.os.dlopen, constants.os.errno, constants.os.signals, +].forEach(test); diff --git a/test/js/node/test/parallel/test-blob-createobjecturl.js b/test/js/node/test/parallel/test-blob-createobjecturl.js new file mode 100644 index 0000000000..614b8ae4a6 --- /dev/null +++ b/test/js/node/test/parallel/test-blob-createobjecturl.js @@ -0,0 +1,54 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); + +// Because registering a Blob URL requires generating a random +// UUID, it can only be done if crypto support is enabled. +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { + URL, +} = require('url'); + +const { + Blob, + resolveObjectURL, +} = require('buffer'); + +const assert = require('assert'); + +(async () => { + const blob = new Blob(['hello']); + const id = URL.createObjectURL(blob); + assert.strictEqual(typeof id, 'string'); + const otherBlob = resolveObjectURL(id); + assert.ok(otherBlob instanceof Blob); + assert.strictEqual(otherBlob.constructor, Blob); + assert.strictEqual(otherBlob.size, 5); + assert.strictEqual( + Buffer.from(await otherBlob.arrayBuffer()).toString(), + 'hello'); + URL.revokeObjectURL(id); + + // should do nothing + URL.revokeObjectURL(id); + + assert.strictEqual(resolveObjectURL(id), undefined); + + // Leaving a Blob registered should not cause an assert + // when Node.js exists + URL.createObjectURL(new Blob()); + +})().then(common.mustCall()); + +['not a url', undefined, 1, 'blob:nodedata:1:wrong', {}].forEach((i) => { + assert.strictEqual(resolveObjectURL(i), undefined); +}); + +[undefined, 1, '', false, {}].forEach((i) => { + assert.throws(() => URL.createObjectURL(i), { + code: 'ERR_INVALID_ARG_TYPE', + }); +}); diff --git a/test/js/node/test/parallel/test-btoa-atob.js b/test/js/node/test/parallel/test-btoa-atob.js new file mode 100644 index 0000000000..abf05adeef --- /dev/null +++ b/test/js/node/test/parallel/test-btoa-atob.js @@ -0,0 +1,39 @@ +'use strict'; + +require('../common'); + +const { strictEqual, throws } = require('assert'); +const buffer = require('buffer'); + +// Exported on the global object +strictEqual(globalThis.atob, buffer.atob); +strictEqual(globalThis.btoa, buffer.btoa); + +// Throws type error on no argument passed +throws(() => buffer.atob(), /TypeError/); +throws(() => buffer.btoa(), /TypeError/); + +strictEqual(atob(' '), ''); +strictEqual(atob(' Y\fW\tJ\njZ A=\r= '), 'abcd'); + +strictEqual(atob(null), '\x9Eée'); +strictEqual(atob(NaN), '5£'); +strictEqual(atob(Infinity), '"wâ\x9E+r'); +strictEqual(atob(true), '¶»\x9E'); +strictEqual(atob(1234), '×mø'); +strictEqual(atob([]), ''); +strictEqual(atob({ toString: () => '' }), ''); +strictEqual(atob({ [Symbol.toPrimitive]: () => '' }), ''); + +throws(() => atob(Symbol()), /TypeError/); +[ + undefined, false, () => {}, {}, [1], + 0, 1, 0n, 1n, -Infinity, + 'a', 'a\n\n\n', '\ra\r\r', ' a ', '\t\t\ta', 'a\f\f\f', '\ta\r \n\f', +].forEach((value) => + // See #2 - https://html.spec.whatwg.org/multipage/webappapis.html#dom-atob + throws(() => atob(value), { + constructor: DOMException, + name: 'InvalidCharacterError', + code: 5, + })); diff --git a/test/js/node/test/parallel/test-buffer-ascii.js b/test/js/node/test/parallel/test-buffer-ascii.js new file mode 100644 index 0000000000..afedb7252c --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-ascii.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// ASCII conversion in node.js simply masks off the high bits, +// it doesn't do transliteration. +assert.strictEqual(Buffer.from('hérité').toString('ascii'), 'hC)ritC)'); + +// 71 characters, 78 bytes. The ’ character is a triple-byte sequence. +const input = 'C’est, graphiquement, la réunion d’un accent aigu ' + + 'et d’un accent grave.'; + +const expected = 'Cb\u0000\u0019est, graphiquement, la rC)union ' + + 'db\u0000\u0019un accent aigu et db\u0000\u0019un ' + + 'accent grave.'; + +const buf = Buffer.from(input); + +for (let i = 0; i < expected.length; ++i) { + assert.strictEqual(buf.slice(i).toString('ascii'), expected.slice(i)); + + // Skip remainder of multi-byte sequence. + if (input.charCodeAt(i) > 65535) ++i; + if (input.charCodeAt(i) > 127) ++i; +} diff --git a/test/js/node/test/parallel/test-buffer-compare-offset.js b/test/js/node/test/parallel/test-buffer-compare-offset.js new file mode 100644 index 0000000000..9f6f733547 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-compare-offset.js @@ -0,0 +1,94 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const a = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]); +const b = Buffer.from([5, 6, 7, 8, 9, 0, 1, 2, 3, 4]); + +assert.strictEqual(a.compare(b), -1); + +// Equivalent to a.compare(b). +assert.strictEqual(a.compare(b, 0), -1); +assert.throws(() => a.compare(b, '0'), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.strictEqual(a.compare(b, undefined), -1); + +// Equivalent to a.compare(b). +assert.strictEqual(a.compare(b, 0, undefined, 0), -1); + +// Zero-length target, return 1 +assert.strictEqual(a.compare(b, 0, 0, 0), 1); +assert.throws( + () => a.compare(b, 0, '0', '0'), + { code: 'ERR_INVALID_ARG_TYPE' } +); + +// Equivalent to Buffer.compare(a, b.slice(6, 10)) +assert.strictEqual(a.compare(b, 6, 10), 1); + +// Zero-length source, return -1 +assert.strictEqual(a.compare(b, 6, 10, 0, 0), -1); + +// Zero-length source and target, return 0 +assert.strictEqual(a.compare(b, 0, 0, 0, 0), 0); +assert.strictEqual(a.compare(b, 1, 1, 2, 2), 0); + +// Equivalent to Buffer.compare(a.slice(4), b.slice(0, 5)) +assert.strictEqual(a.compare(b, 0, 5, 4), 1); + +// Equivalent to Buffer.compare(a.slice(1), b.slice(5)) +assert.strictEqual(a.compare(b, 5, undefined, 1), 1); + +// Equivalent to Buffer.compare(a.slice(2), b.slice(2, 4)) +assert.strictEqual(a.compare(b, 2, 4, 2), -1); + +// Equivalent to Buffer.compare(a.slice(4), b.slice(0, 7)) +assert.strictEqual(a.compare(b, 0, 7, 4), -1); + +// Equivalent to Buffer.compare(a.slice(4, 6), b.slice(0, 7)); +assert.strictEqual(a.compare(b, 0, 7, 4, 6), -1); + +// Null is ambiguous. +assert.throws( + () => a.compare(b, 0, null), + { code: 'ERR_INVALID_ARG_TYPE' } +); + +// Values do not get coerced. +assert.throws( + () => a.compare(b, 0, { valueOf: () => 5 }), + { code: 'ERR_INVALID_ARG_TYPE' } +); + +// Infinity should not be coerced. +assert.throws( + () => a.compare(b, Infinity, -Infinity), + { code: 'ERR_OUT_OF_RANGE' } +); + +// Zero length target because default for targetEnd <= targetSource +assert.strictEqual(a.compare(b, 0xff), 1); + +assert.throws( + () => a.compare(b, '0xff'), + { code: 'ERR_INVALID_ARG_TYPE' } +); +assert.throws( + () => a.compare(b, 0, '0xff'), + { code: 'ERR_INVALID_ARG_TYPE' } +); + +const oor = { code: 'ERR_OUT_OF_RANGE' }; + +assert.throws(() => a.compare(b, 0, 100, 0), oor); +assert.throws(() => a.compare(b, 0, 1, 0, 100), oor); +assert.throws(() => a.compare(b, -1), oor); +assert.throws(() => a.compare(b, 0, Infinity), oor); +assert.throws(() => a.compare(b, 0, 1, -1), oor); +assert.throws(() => a.compare(b, -Infinity, Infinity), oor); +assert.throws(() => a.compare(), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "target" argument must be an instance of ' + + 'Buffer or Uint8Array. Received undefined' +}); diff --git a/test/js/node/test/parallel/test-buffer-failed-alloc-typed-arrays.js b/test/js/node/test/parallel/test-buffer-failed-alloc-typed-arrays.js new file mode 100644 index 0000000000..699475ad0a --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-failed-alloc-typed-arrays.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const SlowBuffer = require('buffer').SlowBuffer; + +// Test failed or zero-sized Buffer allocations not affecting typed arrays. +// This test exists because of a regression that occurred. Because Buffer +// instances are allocated with the same underlying allocator as TypedArrays, +// but Buffer's can optional be non-zero filled, there was a regression that +// occurred when a Buffer allocated failed, the internal flag specifying +// whether or not to zero-fill was not being reset, causing TypedArrays to +// allocate incorrectly. +const zeroArray = new Uint32Array(10).fill(0); +const sizes = [1e20, 0, 0.1, -1, 'a', undefined, null, NaN]; +const allocators = [ + Buffer, + SlowBuffer, + Buffer.alloc, + Buffer.allocUnsafe, + Buffer.allocUnsafeSlow, +]; +for (const allocator of allocators) { + for (const size of sizes) { + try { + // Some of these allocations are known to fail. If they do, + // Uint32Array should still produce a zeroed out result. + allocator(size); + } catch { + assert.deepStrictEqual(zeroArray, new Uint32Array(10)); + } + } +} diff --git a/test/js/node/test/parallel/test-buffer-fakes.js b/test/js/node/test/parallel/test-buffer-fakes.js new file mode 100644 index 0000000000..da78fe0895 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-fakes.js @@ -0,0 +1,54 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +function FakeBuffer() { } +Object.setPrototypeOf(FakeBuffer, Buffer); +Object.setPrototypeOf(FakeBuffer.prototype, Buffer.prototype); + +const fb = new FakeBuffer(); + +assert.throws(function() { + Buffer.from(fb); +}, TypeError); + +assert.throws(function() { + +Buffer.prototype; // eslint-disable-line no-unused-expressions +}, TypeError); + +assert.throws(function() { + Buffer.compare(fb, Buffer.alloc(0)); +}, TypeError); + +assert.throws(function() { + fb.write('foo'); +}, TypeError); + +assert.throws(function() { + Buffer.concat([fb, fb]); +}, TypeError); + +assert.throws(function() { + fb.toString(); +}, TypeError); + +assert.throws(function() { + fb.equals(Buffer.alloc(0)); +}, TypeError); + +assert.throws(function() { + fb.indexOf(5); +}, TypeError); + +assert.throws(function() { + fb.readFloatLE(0); +}, TypeError); + +assert.throws(function() { + fb.writeFloatLE(0); +}, TypeError); + +assert.throws(function() { + fb.fill(0); +}, TypeError); diff --git a/test/js/node/test/parallel/test-buffer-inheritance.js b/test/js/node/test/parallel/test-buffer-inheritance.js new file mode 100644 index 0000000000..4794f56717 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-inheritance.js @@ -0,0 +1,39 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + + +function T(n) { + const ui8 = new Uint8Array(n); + Object.setPrototypeOf(ui8, T.prototype); + return ui8; +} +Object.setPrototypeOf(T.prototype, Buffer.prototype); +Object.setPrototypeOf(T, Buffer); + +T.prototype.sum = function sum() { + let cntr = 0; + for (let i = 0; i < this.length; i++) + cntr += this[i]; + return cntr; +}; + + +const vals = [new T(4), T(4)]; + +vals.forEach(function(t) { + assert.strictEqual(t.constructor, T); + assert.strictEqual(Object.getPrototypeOf(t), T.prototype); + assert.strictEqual(Object.getPrototypeOf(Object.getPrototypeOf(t)), + Buffer.prototype); + + t.fill(5); + let cntr = 0; + for (let i = 0; i < t.length; i++) + cntr += t[i]; + assert.strictEqual(cntr, t.length * 5); + + // Check this does not throw + t.toString(); +}); diff --git a/test/js/node/test/parallel/test-buffer-isascii.js b/test/js/node/test/parallel/test-buffer-isascii.js new file mode 100644 index 0000000000..b9468ca133 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-isascii.js @@ -0,0 +1,42 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { isAscii, Buffer } = require('buffer'); +const { TextEncoder } = require('util'); + +const encoder = new TextEncoder(); + +assert.strictEqual(isAscii(encoder.encode('hello')), true); +assert.strictEqual(isAscii(encoder.encode('ğ')), false); +assert.strictEqual(isAscii(Buffer.from([])), true); + +[ + undefined, + '', 'hello', + false, true, + 0, 1, + 0n, 1n, + Symbol(), + () => {}, + {}, [], null, +].forEach((input) => { + assert.throws( + () => { isAscii(input); }, + { + code: 'ERR_INVALID_ARG_TYPE', + }, + ); +}); + +{ + // Test with detached array buffers + const arrayBuffer = new ArrayBuffer(1024); + structuredClone(arrayBuffer, { transfer: [arrayBuffer] }); + assert.throws( + () => { isAscii(arrayBuffer); }, + { + code: 'ERR_INVALID_STATE' + } + ); +} diff --git a/test/js/node/test/parallel/test-buffer-isencoding.js b/test/js/node/test/parallel/test-buffer-isencoding.js new file mode 100644 index 0000000000..f9150055cc --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-isencoding.js @@ -0,0 +1,38 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +[ + 'hex', + 'utf8', + 'utf-8', + 'ascii', + 'latin1', + 'binary', + 'base64', + 'base64url', + 'ucs2', + 'ucs-2', + 'utf16le', + 'utf-16le', +].forEach((enc) => { + assert.strictEqual(Buffer.isEncoding(enc), true); +}); + +[ + 'utf9', + 'utf-7', + 'Unicode-FTW', + 'new gnu gun', + false, + NaN, + {}, + Infinity, + [], + 1, + 0, + -1, +].forEach((enc) => { + assert.strictEqual(Buffer.isEncoding(enc), false); +}); diff --git a/test/js/node/test/parallel/test-buffer-isutf8.js b/test/js/node/test/parallel/test-buffer-isutf8.js new file mode 100644 index 0000000000..204db3e6a5 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-isutf8.js @@ -0,0 +1,86 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { isUtf8, Buffer } = require('buffer'); +const { TextEncoder } = require('util'); + +const encoder = new TextEncoder(); + +assert.strictEqual(isUtf8(encoder.encode('hello')), true); +assert.strictEqual(isUtf8(encoder.encode('ğ')), true); +assert.strictEqual(isUtf8(Buffer.from([])), true); + +// Taken from test/fixtures/wpt/encoding/textdecoder-fatal.any.js +[ + [0xFF], // 'invalid code' + [0xC0], // 'ends early' + [0xE0], // 'ends early 2' + [0xC0, 0x00], // 'invalid trail' + [0xC0, 0xC0], // 'invalid trail 2' + [0xE0, 0x00], // 'invalid trail 3' + [0xE0, 0xC0], // 'invalid trail 4' + [0xE0, 0x80, 0x00], // 'invalid trail 5' + [0xE0, 0x80, 0xC0], // 'invalid trail 6' + [0xFC, 0x80, 0x80, 0x80, 0x80, 0x80], // '> 0x10FFFF' + [0xFE, 0x80, 0x80, 0x80, 0x80, 0x80], // 'obsolete lead byte' + + // Overlong encodings + [0xC0, 0x80], // 'overlong U+0000 - 2 bytes' + [0xE0, 0x80, 0x80], // 'overlong U+0000 - 3 bytes' + [0xF0, 0x80, 0x80, 0x80], // 'overlong U+0000 - 4 bytes' + [0xF8, 0x80, 0x80, 0x80, 0x80], // 'overlong U+0000 - 5 bytes' + [0xFC, 0x80, 0x80, 0x80, 0x80, 0x80], // 'overlong U+0000 - 6 bytes' + + [0xC1, 0xBF], // 'overlong U+007F - 2 bytes' + [0xE0, 0x81, 0xBF], // 'overlong U+007F - 3 bytes' + [0xF0, 0x80, 0x81, 0xBF], // 'overlong U+007F - 4 bytes' + [0xF8, 0x80, 0x80, 0x81, 0xBF], // 'overlong U+007F - 5 bytes' + [0xFC, 0x80, 0x80, 0x80, 0x81, 0xBF], // 'overlong U+007F - 6 bytes' + + [0xE0, 0x9F, 0xBF], // 'overlong U+07FF - 3 bytes' + [0xF0, 0x80, 0x9F, 0xBF], // 'overlong U+07FF - 4 bytes' + [0xF8, 0x80, 0x80, 0x9F, 0xBF], // 'overlong U+07FF - 5 bytes' + [0xFC, 0x80, 0x80, 0x80, 0x9F, 0xBF], // 'overlong U+07FF - 6 bytes' + + [0xF0, 0x8F, 0xBF, 0xBF], // 'overlong U+FFFF - 4 bytes' + [0xF8, 0x80, 0x8F, 0xBF, 0xBF], // 'overlong U+FFFF - 5 bytes' + [0xFC, 0x80, 0x80, 0x8F, 0xBF, 0xBF], // 'overlong U+FFFF - 6 bytes' + + [0xF8, 0x84, 0x8F, 0xBF, 0xBF], // 'overlong U+10FFFF - 5 bytes' + [0xFC, 0x80, 0x84, 0x8F, 0xBF, 0xBF], // 'overlong U+10FFFF - 6 bytes' + + // UTF-16 surrogates encoded as code points in UTF-8 + [0xED, 0xA0, 0x80], // 'lead surrogate' + [0xED, 0xB0, 0x80], // 'trail surrogate' + [0xED, 0xA0, 0x80, 0xED, 0xB0, 0x80], // 'surrogate pair' +].forEach((input) => { + assert.strictEqual(isUtf8(Buffer.from(input)), false); +}); + +[ + null, + undefined, + 'hello', + true, + false, +].forEach((input) => { + assert.throws( + () => { isUtf8(input); }, + { + code: 'ERR_INVALID_ARG_TYPE', + }, + ); +}); + +{ + // Test with detached array buffers + const arrayBuffer = new ArrayBuffer(1024); + structuredClone(arrayBuffer, { transfer: [arrayBuffer] }); + assert.throws( + () => { isUtf8(arrayBuffer); }, + { + code: 'ERR_INVALID_STATE' + } + ); +} diff --git a/test/js/node/test/parallel/test-buffer-iterator.js b/test/js/node/test/parallel/test-buffer-iterator.js new file mode 100644 index 0000000000..6cf64712c0 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-iterator.js @@ -0,0 +1,62 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const buffer = Buffer.from([1, 2, 3, 4, 5]); +let arr; +let b; + +// Buffers should be iterable + +arr = []; + +for (b of buffer) + arr.push(b); + +assert.deepStrictEqual(arr, [1, 2, 3, 4, 5]); + + +// Buffer iterators should be iterable + +arr = []; + +for (b of buffer[Symbol.iterator]()) + arr.push(b); + +assert.deepStrictEqual(arr, [1, 2, 3, 4, 5]); + + +// buffer#values() should return iterator for values + +arr = []; + +for (b of buffer.values()) + arr.push(b); + +assert.deepStrictEqual(arr, [1, 2, 3, 4, 5]); + + +// buffer#keys() should return iterator for keys + +arr = []; + +for (b of buffer.keys()) + arr.push(b); + +assert.deepStrictEqual(arr, [0, 1, 2, 3, 4]); + + +// buffer#entries() should return iterator for entries + +arr = []; + +for (b of buffer.entries()) + arr.push(b); + +assert.deepStrictEqual(arr, [ + [0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], +]); diff --git a/test/js/node/test/parallel/test-buffer-no-negative-allocation.js b/test/js/node/test/parallel/test-buffer-no-negative-allocation.js new file mode 100644 index 0000000000..055e2d5dc6 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-no-negative-allocation.js @@ -0,0 +1,37 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { SlowBuffer } = require('buffer'); + +const msg = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}; + +// Test that negative Buffer length inputs throw errors. + +assert.throws(() => Buffer(-Buffer.poolSize), msg); +assert.throws(() => Buffer(-100), msg); +assert.throws(() => Buffer(-1), msg); +assert.throws(() => Buffer(NaN), msg); + +assert.throws(() => Buffer.alloc(-Buffer.poolSize), msg); +assert.throws(() => Buffer.alloc(-100), msg); +assert.throws(() => Buffer.alloc(-1), msg); +assert.throws(() => Buffer.alloc(NaN), msg); + +assert.throws(() => Buffer.allocUnsafe(-Buffer.poolSize), msg); +assert.throws(() => Buffer.allocUnsafe(-100), msg); +assert.throws(() => Buffer.allocUnsafe(-1), msg); +assert.throws(() => Buffer.allocUnsafe(NaN), msg); + +assert.throws(() => Buffer.allocUnsafeSlow(-Buffer.poolSize), msg); +assert.throws(() => Buffer.allocUnsafeSlow(-100), msg); +assert.throws(() => Buffer.allocUnsafeSlow(-1), msg); +assert.throws(() => Buffer.allocUnsafeSlow(NaN), msg); + +assert.throws(() => SlowBuffer(-Buffer.poolSize), msg); +assert.throws(() => SlowBuffer(-100), msg); +assert.throws(() => SlowBuffer(-1), msg); +assert.throws(() => SlowBuffer(NaN), msg); diff --git a/test/js/node/test/parallel/test-buffer-nopendingdep-map.js b/test/js/node/test/parallel/test-buffer-nopendingdep-map.js new file mode 100644 index 0000000000..c85d184fbc --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-nopendingdep-map.js @@ -0,0 +1,13 @@ +// Flags: --no-warnings --pending-deprecation +'use strict'; + +const common = require('../common'); + +process.on('warning', common.mustNotCall('A warning should not be emitted')); + +// With the --pending-deprecation flag, the deprecation warning for +// new Buffer() should not be emitted when Uint8Array methods are called. + +Buffer.from('abc').map((i) => i); +Buffer.from('abc').filter((i) => i); +Buffer.from('abc').slice(1, 2); diff --git a/test/js/node/test/parallel/test-buffer-of-no-deprecation.js b/test/js/node/test/parallel/test-buffer-of-no-deprecation.js new file mode 100644 index 0000000000..d0ac75b4a3 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-of-no-deprecation.js @@ -0,0 +1,7 @@ +'use strict'; + +const common = require('../common'); + +process.on('warning', common.mustNotCall()); + +Buffer.of(0, 1); diff --git a/test/js/node/test/parallel/test-buffer-over-max-length.js b/test/js/node/test/parallel/test-buffer-over-max-length.js new file mode 100644 index 0000000000..f29d6b62d4 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-over-max-length.js @@ -0,0 +1,19 @@ +'use strict'; +require('../common'); + +const assert = require('assert'); + +const buffer = require('buffer'); +const SlowBuffer = buffer.SlowBuffer; + +const kMaxLength = buffer.kMaxLength; +const bufferMaxSizeMsg = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}; + +assert.throws(() => Buffer(kMaxLength + 1), bufferMaxSizeMsg); +assert.throws(() => SlowBuffer(kMaxLength + 1), bufferMaxSizeMsg); +assert.throws(() => Buffer.alloc(kMaxLength + 1), bufferMaxSizeMsg); +assert.throws(() => Buffer.allocUnsafe(kMaxLength + 1), bufferMaxSizeMsg); +assert.throws(() => Buffer.allocUnsafeSlow(kMaxLength + 1), bufferMaxSizeMsg); diff --git a/test/js/node/test/parallel/test-buffer-parent-property.js b/test/js/node/test/parallel/test-buffer-parent-property.js new file mode 100644 index 0000000000..24cdaade43 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-parent-property.js @@ -0,0 +1,21 @@ +'use strict'; + +// Fix for https://github.com/nodejs/node/issues/8266 +// +// Zero length Buffer objects should expose the `buffer` property of the +// TypedArrays, via the `parent` property. +require('../common'); +const assert = require('assert'); + +// If the length of the buffer object is zero +assert((new Buffer(0)).parent instanceof ArrayBuffer); + +// If the length of the buffer object is equal to the underlying ArrayBuffer +assert((new Buffer(Buffer.poolSize)).parent instanceof ArrayBuffer); + +// Same as the previous test, but with user created buffer +const arrayBuffer = new ArrayBuffer(0); +assert.strictEqual(new Buffer(arrayBuffer).parent, arrayBuffer); +assert.strictEqual(new Buffer(arrayBuffer).buffer, arrayBuffer); +assert.strictEqual(Buffer.from(arrayBuffer).parent, arrayBuffer); +assert.strictEqual(Buffer.from(arrayBuffer).buffer, arrayBuffer); diff --git a/test/js/node/test/parallel/test-buffer-safe-unsafe.js b/test/js/node/test/parallel/test-buffer-safe-unsafe.js new file mode 100644 index 0000000000..9f8b6b7410 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-safe-unsafe.js @@ -0,0 +1,24 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const safe = Buffer.alloc(10); + +function isZeroFilled(buf) { + for (let n = 0; n < buf.length; n++) + if (buf[n] !== 0) return false; + return true; +} + +assert(isZeroFilled(safe)); + +// Test that unsafe allocations doesn't affect subsequent safe allocations +Buffer.allocUnsafe(10); +assert(isZeroFilled(new Float64Array(10))); + +new Buffer(10); +assert(isZeroFilled(new Float64Array(10))); + +Buffer.allocUnsafe(10); +assert(isZeroFilled(Buffer.alloc(10))); diff --git a/test/js/node/test/parallel/test-buffer-set-inspect-max-bytes.js b/test/js/node/test/parallel/test-buffer-set-inspect-max-bytes.js new file mode 100644 index 0000000000..975c828111 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-set-inspect-max-bytes.js @@ -0,0 +1,34 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const buffer = require('buffer'); + +const rangeErrorObjs = [NaN, -1]; +const typeErrorObj = 'and even this'; + +for (const obj of rangeErrorObjs) { + assert.throws( + () => buffer.INSPECT_MAX_BYTES = obj, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } + ); + + assert.throws( + () => buffer.INSPECT_MAX_BYTES = obj, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } + ); +} + +assert.throws( + () => buffer.INSPECT_MAX_BYTES = typeErrorObj, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } +); diff --git a/test/js/node/test/parallel/test-buffer-slice.js b/test/js/node/test/parallel/test-buffer-slice.js new file mode 100644 index 0000000000..52720bb87b --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-slice.js @@ -0,0 +1,129 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(Buffer.from('hello', 'utf8').slice(0, 0).length, 0); +assert.strictEqual(Buffer('hello', 'utf8').slice(0, 0).length, 0); + +const buf = Buffer.from('0123456789', 'utf8'); +const expectedSameBufs = [ + [buf.slice(-10, 10), Buffer.from('0123456789', 'utf8')], + [buf.slice(-20, 10), Buffer.from('0123456789', 'utf8')], + [buf.slice(-20, -10), Buffer.from('', 'utf8')], + [buf.slice(), Buffer.from('0123456789', 'utf8')], + [buf.slice(0), Buffer.from('0123456789', 'utf8')], + [buf.slice(0, 0), Buffer.from('', 'utf8')], + [buf.slice(undefined), Buffer.from('0123456789', 'utf8')], + [buf.slice('foobar'), Buffer.from('0123456789', 'utf8')], + [buf.slice(undefined, undefined), Buffer.from('0123456789', 'utf8')], + [buf.slice(2), Buffer.from('23456789', 'utf8')], + [buf.slice(5), Buffer.from('56789', 'utf8')], + [buf.slice(10), Buffer.from('', 'utf8')], + [buf.slice(5, 8), Buffer.from('567', 'utf8')], + [buf.slice(8, -1), Buffer.from('8', 'utf8')], + [buf.slice(-10), Buffer.from('0123456789', 'utf8')], + [buf.slice(0, -9), Buffer.from('0', 'utf8')], + [buf.slice(0, -10), Buffer.from('', 'utf8')], + [buf.slice(0, -1), Buffer.from('012345678', 'utf8')], + [buf.slice(2, -2), Buffer.from('234567', 'utf8')], + [buf.slice(0, 65536), Buffer.from('0123456789', 'utf8')], + [buf.slice(65536, 0), Buffer.from('', 'utf8')], + [buf.slice(-5, -8), Buffer.from('', 'utf8')], + [buf.slice(-5, -3), Buffer.from('56', 'utf8')], + [buf.slice(-10, 10), Buffer.from('0123456789', 'utf8')], + [buf.slice('0', '1'), Buffer.from('0', 'utf8')], + [buf.slice('-5', '10'), Buffer.from('56789', 'utf8')], + [buf.slice('-10', '10'), Buffer.from('0123456789', 'utf8')], + [buf.slice('-10', '-5'), Buffer.from('01234', 'utf8')], + [buf.slice('-10', '-0'), Buffer.from('', 'utf8')], + [buf.slice('111'), Buffer.from('', 'utf8')], + [buf.slice('0', '-111'), Buffer.from('', 'utf8')], +]; + +for (let i = 0, s = buf.toString(); i < buf.length; ++i) { + expectedSameBufs.push( + [buf.slice(i), Buffer.from(s.slice(i))], + [buf.slice(0, i), Buffer.from(s.slice(0, i))], + [buf.slice(-i), Buffer.from(s.slice(-i))], + [buf.slice(0, -i), Buffer.from(s.slice(0, -i))] + ); +} + +for (const [buf1, buf2] of expectedSameBufs) { + assert.strictEqual(Buffer.compare(buf1, buf2), 0); +} + +const utf16Buf = Buffer.from('0123456789', 'utf16le'); +assert.deepStrictEqual(utf16Buf.slice(0, 6), Buffer.from('012', 'utf16le')); +// Try to slice a zero length Buffer. +// See https://github.com/joyent/node/issues/5881 +assert.strictEqual(Buffer.alloc(0).slice(0, 1).length, 0); + +{ + // Single argument slice + assert.strictEqual(Buffer.from('abcde', 'utf8').slice(1).toString('utf8'), + 'bcde'); +} + +// slice(0,0).length === 0 +assert.strictEqual(Buffer.from('hello', 'utf8').slice(0, 0).length, 0); + +{ + // Regression tests for https://github.com/nodejs/node/issues/9096 + const buf = Buffer.from('abcd', 'utf8'); + assert.strictEqual(buf.slice(buf.length / 3).toString('utf8'), 'bcd'); + assert.strictEqual( + buf.slice(buf.length / 3, buf.length).toString(), + 'bcd' + ); +} + +{ + const buf = Buffer.from('abcdefg', 'utf8'); + assert.strictEqual(buf.slice(-(-1 >>> 0) - 1).toString('utf8'), + buf.toString('utf8')); +} + +{ + const buf = Buffer.from('abc', 'utf8'); + assert.strictEqual(buf.slice(-0.5).toString('utf8'), buf.toString('utf8')); +} + +{ + const buf = Buffer.from([ + 1, 29, 0, 0, 1, 143, 216, 162, 92, 254, 248, 63, 0, + 0, 0, 18, 184, 6, 0, 175, 29, 0, 8, 11, 1, 0, 0, + ]); + const chunk1 = Buffer.from([ + 1, 29, 0, 0, 1, 143, 216, 162, 92, 254, 248, 63, 0, + ]); + const chunk2 = Buffer.from([ + 0, 0, 18, 184, 6, 0, 175, 29, 0, 8, 11, 1, 0, 0, + ]); + const middle = buf.length / 2; + + assert.deepStrictEqual(buf.slice(0, middle), chunk1); + assert.deepStrictEqual(buf.slice(middle), chunk2); +} diff --git a/test/js/node/test/parallel/test-buffer-slow.js b/test/js/node/test/parallel/test-buffer-slow.js new file mode 100644 index 0000000000..07138d5db0 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-slow.js @@ -0,0 +1,53 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const buffer = require('buffer'); +const SlowBuffer = buffer.SlowBuffer; + +const ones = [1, 1, 1, 1]; + +// Should create a Buffer +let sb = SlowBuffer(4); +assert(sb instanceof Buffer); +assert.strictEqual(sb.length, 4); +sb.fill(1); +for (const [key, value] of sb.entries()) { + assert.deepStrictEqual(value, ones[key]); +} + +// underlying ArrayBuffer should have the same length +assert.strictEqual(sb.buffer.byteLength, 4); + +// Should work without new +sb = SlowBuffer(4); +assert(sb instanceof Buffer); +assert.strictEqual(sb.length, 4); +sb.fill(1); +for (const [key, value] of sb.entries()) { + assert.deepStrictEqual(value, ones[key]); +} + +// Should work with edge cases +assert.strictEqual(SlowBuffer(0).length, 0); + +// Should throw with invalid length type +const bufferInvalidTypeMsg = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "size" argument must be of type number/, +}; +assert.throws(() => SlowBuffer(), bufferInvalidTypeMsg); +assert.throws(() => SlowBuffer({}), bufferInvalidTypeMsg); +assert.throws(() => SlowBuffer('6'), bufferInvalidTypeMsg); +assert.throws(() => SlowBuffer(true), bufferInvalidTypeMsg); + +// Should throw with invalid length value +const bufferMaxSizeMsg = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}; +assert.throws(() => SlowBuffer(NaN), bufferMaxSizeMsg); +assert.throws(() => SlowBuffer(Infinity), bufferMaxSizeMsg); +assert.throws(() => SlowBuffer(-1), bufferMaxSizeMsg); +assert.throws(() => SlowBuffer(buffer.kMaxLength + 1), bufferMaxSizeMsg); diff --git a/test/js/node/test/parallel/test-buffer-swap.js b/test/js/node/test/parallel/test-buffer-swap.js new file mode 100644 index 0000000000..82b550790e --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-swap.js @@ -0,0 +1,152 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// Test buffers small enough to use the JS implementation +{ + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10]); + + assert.strictEqual(buf, buf.swap16()); + assert.deepStrictEqual(buf, Buffer.from([0x02, 0x01, 0x04, 0x03, 0x06, 0x05, + 0x08, 0x07, 0x0a, 0x09, 0x0c, 0x0b, + 0x0e, 0x0d, 0x10, 0x0f])); + buf.swap16(); // restore + + assert.strictEqual(buf, buf.swap32()); + assert.deepStrictEqual(buf, Buffer.from([0x04, 0x03, 0x02, 0x01, 0x08, 0x07, + 0x06, 0x05, 0x0c, 0x0b, 0x0a, 0x09, + 0x10, 0x0f, 0x0e, 0x0d])); + buf.swap32(); // restore + + assert.strictEqual(buf, buf.swap64()); + assert.deepStrictEqual(buf, Buffer.from([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, + 0x02, 0x01, 0x10, 0x0f, 0x0e, 0x0d, + 0x0c, 0x0b, 0x0a, 0x09])); +} + +// Operates in-place +{ + const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]); + buf.slice(1, 5).swap32(); + assert.deepStrictEqual(buf, Buffer.from([0x1, 0x5, 0x4, 0x3, 0x2, 0x6, 0x7])); + buf.slice(1, 5).swap16(); + assert.deepStrictEqual(buf, Buffer.from([0x1, 0x4, 0x5, 0x2, 0x3, 0x6, 0x7])); + + // Length assertions + const re16 = /Buffer size must be a multiple of 16-bits/; + const re32 = /Buffer size must be a multiple of 32-bits/; + const re64 = /Buffer size must be a multiple of 64-bits/; + + assert.throws(() => Buffer.from(buf).swap16(), re16); + assert.throws(() => Buffer.alloc(1025).swap16(), re16); + assert.throws(() => Buffer.from(buf).swap32(), re32); + assert.throws(() => buf.slice(1, 3).swap32(), re32); + assert.throws(() => Buffer.alloc(1025).swap32(), re32); + assert.throws(() => buf.slice(1, 3).swap64(), re64); + assert.throws(() => Buffer.alloc(1025).swap64(), re64); +} + +{ + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10]); + + buf.slice(2, 18).swap64(); + + assert.deepStrictEqual(buf, Buffer.from([0x01, 0x02, 0x0a, 0x09, 0x08, 0x07, + 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, + 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10])); +} + +// Force use of native code (Buffer size above threshold limit for js impl) +{ + const bufData = new Uint32Array(256).fill(0x04030201); + const buf = Buffer.from(bufData.buffer, bufData.byteOffset); + const otherBufData = new Uint32Array(256).fill(0x03040102); + const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); + buf.swap16(); + assert.deepStrictEqual(buf, otherBuf); +} + +{ + const bufData = new Uint32Array(256).fill(0x04030201); + const buf = Buffer.from(bufData.buffer); + const otherBufData = new Uint32Array(256).fill(0x01020304); + const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); + buf.swap32(); + assert.deepStrictEqual(buf, otherBuf); +} + +{ + const bufData = new Uint8Array(256 * 8); + const otherBufData = new Uint8Array(256 * 8); + for (let i = 0; i < bufData.length; i++) { + bufData[i] = i % 8; + otherBufData[otherBufData.length - i - 1] = i % 8; + } + const buf = Buffer.from(bufData.buffer, bufData.byteOffset); + const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); + buf.swap64(); + assert.deepStrictEqual(buf, otherBuf); +} + +// Test native code with buffers that are not memory-aligned +{ + const bufData = new Uint8Array(256 * 8); + const otherBufData = new Uint8Array(256 * 8 - 2); + for (let i = 0; i < bufData.length; i++) { + bufData[i] = i % 2; + } + for (let i = 1; i < otherBufData.length; i++) { + otherBufData[otherBufData.length - i] = (i + 1) % 2; + } + const buf = Buffer.from(bufData.buffer, bufData.byteOffset); + // 0|1 0|1 0|1... + const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); + // 0|0 1|0 1|0... + + buf.slice(1, buf.length - 1).swap16(); + assert.deepStrictEqual(buf.slice(0, otherBuf.length), otherBuf); +} + +{ + const bufData = new Uint8Array(256 * 8); + const otherBufData = new Uint8Array(256 * 8 - 4); + for (let i = 0; i < bufData.length; i++) { + bufData[i] = i % 4; + } + for (let i = 1; i < otherBufData.length; i++) { + otherBufData[otherBufData.length - i] = (i + 1) % 4; + } + const buf = Buffer.from(bufData.buffer, bufData.byteOffset); + // 0|1 2 3 0|1 2 3... + const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); + // 0|0 3 2 1|0 3 2... + + buf.slice(1, buf.length - 3).swap32(); + assert.deepStrictEqual(buf.slice(0, otherBuf.length), otherBuf); +} + +{ + const bufData = new Uint8Array(256 * 8); + const otherBufData = new Uint8Array(256 * 8 - 8); + for (let i = 0; i < bufData.length; i++) { + bufData[i] = i % 8; + } + for (let i = 1; i < otherBufData.length; i++) { + otherBufData[otherBufData.length - i] = (i + 1) % 8; + } + const buf = Buffer.from(bufData.buffer, bufData.byteOffset); + // 0|1 2 3 4 5 6 7 0|1 2 3 4... + const otherBuf = Buffer.from(otherBufData.buffer, otherBufData.byteOffset); + // 0|0 7 6 5 4 3 2 1|0 7 6 5... + + buf.slice(1, buf.length - 7).swap64(); + assert.deepStrictEqual(buf.slice(0, otherBuf.length), otherBuf); +} diff --git a/test/js/node/test/parallel/test-buffer-tojson.js b/test/js/node/test/parallel/test-buffer-tojson.js new file mode 100644 index 0000000000..d9a4a85e81 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-tojson.js @@ -0,0 +1,35 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +{ + assert.strictEqual(JSON.stringify(Buffer.alloc(0)), + '{"type":"Buffer","data":[]}'); + assert.strictEqual(JSON.stringify(Buffer.from([1, 2, 3, 4])), + '{"type":"Buffer","data":[1,2,3,4]}'); +} + +// issue GH-7849 +{ + const buf = Buffer.from('test'); + const json = JSON.stringify(buf); + const obj = JSON.parse(json); + const copy = Buffer.from(obj); + + assert.deepStrictEqual(buf, copy); +} + +// GH-5110 +{ + const buffer = Buffer.from('test'); + const string = JSON.stringify(buffer); + + assert.strictEqual(string, '{"type":"Buffer","data":[116,101,115,116]}'); + + function receiver(key, value) { + return value && value.type === 'Buffer' ? Buffer.from(value.data) : value; + } + + assert.deepStrictEqual(buffer, JSON.parse(string, receiver)); +} diff --git a/test/js/node/test/parallel/test-buffer-tostring-range.js b/test/js/node/test/parallel/test-buffer-tostring-range.js new file mode 100644 index 0000000000..f4adf64c8d --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-tostring-range.js @@ -0,0 +1,100 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const rangeBuffer = Buffer.from('abc'); + +// If start >= buffer's length, empty string will be returned +assert.strictEqual(rangeBuffer.toString('ascii', 3), ''); +assert.strictEqual(rangeBuffer.toString('ascii', +Infinity), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 3.14, 3), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 'Infinity', 3), ''); + +// If end <= 0, empty string will be returned +assert.strictEqual(rangeBuffer.toString('ascii', 1, 0), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 1, -1.2), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 1, -100), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 1, -Infinity), ''); + +// If start < 0, start will be taken as zero +assert.strictEqual(rangeBuffer.toString('ascii', -1, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', -1.99, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', -Infinity, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', '-1', 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', '-1.99', 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', '-Infinity', 3), 'abc'); + +// If start is an invalid integer, start will be taken as zero +assert.strictEqual(rangeBuffer.toString('ascii', 'node.js', 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', {}, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', [], 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', NaN, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', null, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', undefined, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', false, 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', '', 3), 'abc'); + +// But, if start is an integer when coerced, then it will be coerced and used. +assert.strictEqual(rangeBuffer.toString('ascii', '-1', 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', '1', 3), 'bc'); +assert.strictEqual(rangeBuffer.toString('ascii', '-Infinity', 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', '3', 3), ''); +assert.strictEqual(rangeBuffer.toString('ascii', Number(3), 3), ''); +assert.strictEqual(rangeBuffer.toString('ascii', '3.14', 3), ''); +assert.strictEqual(rangeBuffer.toString('ascii', '1.99', 3), 'bc'); +assert.strictEqual(rangeBuffer.toString('ascii', '-1.99', 3), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 1.99, 3), 'bc'); +assert.strictEqual(rangeBuffer.toString('ascii', true, 3), 'bc'); + +// If end > buffer's length, end will be taken as buffer's length +assert.strictEqual(rangeBuffer.toString('ascii', 0, 5), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, 6.99), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, Infinity), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '5'), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '6.99'), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, 'Infinity'), 'abc'); + +// If end is an invalid integer, end will be taken as buffer's length +assert.strictEqual(rangeBuffer.toString('ascii', 0, 'node.js'), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, {}), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, NaN), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, undefined), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, null), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, []), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, false), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, ''), ''); + +// But, if end is an integer when coerced, then it will be coerced and used. +assert.strictEqual(rangeBuffer.toString('ascii', 0, '-1'), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '1'), 'a'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '-Infinity'), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '3'), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, Number(3)), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '3.14'), 'abc'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '1.99'), 'a'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, '-1.99'), ''); +assert.strictEqual(rangeBuffer.toString('ascii', 0, 1.99), 'a'); +assert.strictEqual(rangeBuffer.toString('ascii', 0, true), 'a'); + +// Try toString() with an object as an encoding +assert.strictEqual(rangeBuffer.toString({ toString: function() { + return 'ascii'; +} }), 'abc'); + +// Try toString() with 0 and null as the encoding +assert.throws(() => { + rangeBuffer.toString(0, 1, 2); +}, { + code: 'ERR_UNKNOWN_ENCODING', + name: 'TypeError', + message: 'Unknown encoding: 0' +}); +assert.throws(() => { + rangeBuffer.toString(null, 1, 2); +}, { + code: 'ERR_UNKNOWN_ENCODING', + name: 'TypeError', + message: 'Unknown encoding: null' +}); diff --git a/test/js/node/test/parallel/test-buffer-tostring-rangeerror.js b/test/js/node/test/parallel/test-buffer-tostring-rangeerror.js new file mode 100644 index 0000000000..0ebea759b5 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-tostring-rangeerror.js @@ -0,0 +1,25 @@ +'use strict'; +require('../common'); + +// This test ensures that Node.js throws an Error when trying to convert a +// large buffer into a string. +// Regression test for https://github.com/nodejs/node/issues/649. + +const assert = require('assert'); +const { + SlowBuffer, + constants: { + MAX_STRING_LENGTH, + }, +} = require('buffer'); + +const len = MAX_STRING_LENGTH + 1; +const message = { + code: 'ERR_STRING_TOO_LONG', + name: 'Error', +}; +assert.throws(() => Buffer(len).toString('utf8'), message); +assert.throws(() => SlowBuffer(len).toString('utf8'), message); +assert.throws(() => Buffer.alloc(len).toString('utf8'), message); +assert.throws(() => Buffer.allocUnsafe(len).toString('utf8'), message); +assert.throws(() => Buffer.allocUnsafeSlow(len).toString('utf8'), message); diff --git a/test/js/node/test/parallel/test-buffer-tostring.js b/test/js/node/test/parallel/test-buffer-tostring.js new file mode 100644 index 0000000000..4219649fa3 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-tostring.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +// utf8, ucs2, ascii, latin1, utf16le +const encodings = ['utf8', 'utf-8', 'ucs2', 'ucs-2', 'ascii', 'latin1', + 'binary', 'utf16le', 'utf-16le']; + +encodings + .reduce((es, e) => es.concat(e, e.toUpperCase()), []) + .forEach((encoding) => { + assert.strictEqual(Buffer.from('foo', encoding).toString(encoding), 'foo'); + }); + +// base64 +['base64', 'BASE64'].forEach((encoding) => { + assert.strictEqual(Buffer.from('Zm9v', encoding).toString(encoding), 'Zm9v'); +}); + +// hex +['hex', 'HEX'].forEach((encoding) => { + assert.strictEqual(Buffer.from('666f6f', encoding).toString(encoding), + '666f6f'); +}); + +// Invalid encodings +for (let i = 1; i < 10; i++) { + const encoding = String(i).repeat(i); + const error = common.expectsError({ + code: 'ERR_UNKNOWN_ENCODING', + name: 'TypeError', + message: `Unknown encoding: ${encoding}` + }); + assert.ok(!Buffer.isEncoding(encoding)); + assert.throws(() => Buffer.from('foo').toString(encoding), error); +} diff --git a/test/js/node/test/parallel/test-buffer-zero-fill-reset.js b/test/js/node/test/parallel/test-buffer-zero-fill-reset.js new file mode 100644 index 0000000000..334ee1b618 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-zero-fill-reset.js @@ -0,0 +1,19 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + + +function testUint8Array(ui) { + const length = ui.length; + for (let i = 0; i < length; i++) + if (ui[i] !== 0) return false; + return true; +} + + +for (let i = 0; i < 100; i++) { + Buffer.alloc(0); + const ui = new Uint8Array(65); + assert.ok(testUint8Array(ui), `Uint8Array is not zero-filled: ${ui}`); +} diff --git a/test/js/node/test/parallel/test-buffer-zero-fill.js b/test/js/node/test/parallel/test-buffer-zero-fill.js new file mode 100644 index 0000000000..7a9f0c1250 --- /dev/null +++ b/test/js/node/test/parallel/test-buffer-zero-fill.js @@ -0,0 +1,14 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// Tests deprecated Buffer API on purpose +const buf1 = Buffer(100); +const buf2 = new Buffer(100); + +for (let n = 0; n < buf1.length; n++) + assert.strictEqual(buf1[n], 0); + +for (let n = 0; n < buf2.length; n++) + assert.strictEqual(buf2[n], 0); diff --git a/test/js/node/test/parallel/test-child-process-can-write-to-stdout.js b/test/js/node/test/parallel/test-child-process-can-write-to-stdout.js new file mode 100644 index 0000000000..069e07fb16 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-can-write-to-stdout.js @@ -0,0 +1,22 @@ +'use strict'; +// Tests that a spawned child process can write to stdout without throwing. +// See https://github.com/nodejs/node-v0.x-archive/issues/1899. + +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +const child = spawn(process.argv[0], [ + fixtures.path('GH-1899-output.js'), +]); +let output = ''; + +child.stdout.on('data', function(data) { + output += data; +}); + +child.on('exit', function(code, signal) { + assert.strictEqual(code, 0); + assert.strictEqual(output, 'hello, world!\n'); +}); diff --git a/test/js/node/test/parallel/test-child-process-default-options.js b/test/js/node/test/parallel/test-child-process-default-options.js new file mode 100644 index 0000000000..39f90deaeb --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-default-options.js @@ -0,0 +1,51 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { isWindows } = require('../common'); +const assert = require('assert'); + +const spawn = require('child_process').spawn; +const debug = require('util').debuglog('test'); + +process.env.HELLO = 'WORLD'; + +let child; +if (isWindows) { + child = spawn('cmd.exe', ['/c', 'set'], {}); +} else { + child = spawn('/usr/bin/env', [], {}); +} + +let response = ''; + +child.stdout.setEncoding('utf8'); + +child.stdout.on('data', function(chunk) { + debug(`stdout: ${chunk}`); + response += chunk; +}); + +process.on('exit', function() { + assert.ok(response.includes('HELLO=WORLD'), + 'spawn did not use process.env as default ' + + `(process.env.HELLO = ${process.env.HELLO})`); +}); diff --git a/test/js/node/test/parallel/test-child-process-destroy.js b/test/js/node/test/parallel/test-child-process-destroy.js new file mode 100644 index 0000000000..50763bb031 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-destroy.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const cat = spawn(common.isWindows ? 'cmd' : 'cat'); + +cat.stdout.on('end', common.mustCall()); +cat.stderr.on('data', common.mustNotCall()); +cat.stderr.on('end', common.mustCall()); + +cat.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, null); + assert.strictEqual(signal, 'SIGTERM'); + assert.strictEqual(cat.signalCode, 'SIGTERM'); +})); +cat.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, null); + assert.strictEqual(signal, 'SIGTERM'); + assert.strictEqual(cat.signalCode, 'SIGTERM'); +})); + +assert.strictEqual(cat.signalCode, null); +assert.strictEqual(cat.killed, false); +cat[Symbol.dispose](); +assert.strictEqual(cat.killed, true); diff --git a/test/js/node/test/parallel/test-child-process-double-pipe.js b/test/js/node/test/parallel/test-child-process-double-pipe.js new file mode 100644 index 0000000000..7a432d3892 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-double-pipe.js @@ -0,0 +1,122 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { + isWindows, + mustCall, + mustCallAtLeast, +} = require('../common'); +const assert = require('assert'); +const os = require('os'); +const spawn = require('child_process').spawn; +const debug = require('util').debuglog('test'); + +// We're trying to reproduce: +// $ echo "hello\nnode\nand\nworld" | grep o | sed s/o/a/ + +let grep, sed, echo; + +if (isWindows) { + grep = spawn('grep', ['--binary', 'o']); + sed = spawn('sed', ['--binary', 's/o/O/']); + echo = spawn('cmd.exe', + ['/c', 'echo', 'hello&&', 'echo', + 'node&&', 'echo', 'and&&', 'echo', 'world']); +} else { + grep = spawn('grep', ['o']); + sed = spawn('sed', ['s/o/O/']); + echo = spawn('echo', ['hello\nnode\nand\nworld\n']); +} + +// If the spawn function leaks file descriptors to subprocesses, grep and sed +// hang. +// This happens when calling pipe(2) and then forgetting to set the +// FD_CLOEXEC flag on the resulting file descriptors. +// +// This test checks child processes exit, meaning they don't hang like +// explained above. + + +// pipe echo | grep +echo.stdout.on('data', mustCallAtLeast((data) => { + debug(`grep stdin write ${data.length}`); + if (!grep.stdin.write(data)) { + echo.stdout.pause(); + } +})); + +// TODO(@jasnell): This does not appear to ever be +// emitted. It's not clear if it is necessary. +grep.stdin.on('drain', (data) => { + echo.stdout.resume(); +}); + +// Propagate end from echo to grep +echo.stdout.on('end', mustCall((code) => { + grep.stdin.end(); +})); + +echo.on('exit', mustCall(() => { + debug('echo exit'); +})); + +grep.on('exit', mustCall(() => { + debug('grep exit'); +})); + +sed.on('exit', mustCall(() => { + debug('sed exit'); +})); + + +// pipe grep | sed +grep.stdout.on('data', mustCallAtLeast((data) => { + debug(`grep stdout ${data.length}`); + if (!sed.stdin.write(data)) { + grep.stdout.pause(); + } +})); + +// TODO(@jasnell): This does not appear to ever be +// emitted. It's not clear if it is necessary. +sed.stdin.on('drain', (data) => { + grep.stdout.resume(); +}); + +// Propagate end from grep to sed +grep.stdout.on('end', mustCall((code) => { + debug('grep stdout end'); + sed.stdin.end(); +})); + + +let result = ''; + +// print sed's output +sed.stdout.on('data', mustCallAtLeast((data) => { + result += data.toString('utf8', 0, data.length); + debug(data); +})); + +sed.stdout.on('end', mustCall((code) => { + assert.strictEqual(result, `hellO${os.EOL}nOde${os.EOL}wOrld${os.EOL}`); +})); diff --git a/test/js/node/test/parallel/test-child-process-env.js b/test/js/node/test/parallel/test-child-process-env.js new file mode 100644 index 0000000000..f9815ff015 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-env.js @@ -0,0 +1,77 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { + isWindows, + mustCall, + mustCallAtLeast, +} = require('../common'); +const assert = require('assert'); +const os = require('os'); +const debug = require('util').debuglog('test'); + +const spawn = require('child_process').spawn; + +const env = { + ...process.env, + 'HELLO': 'WORLD', + 'UNDEFINED': undefined, + 'NULL': null, + 'EMPTY': '', + 'duplicate': 'lowercase', + 'DUPLICATE': 'uppercase', +}; +Object.setPrototypeOf(env, { + 'FOO': 'BAR' +}); + +let child; +if (isWindows) { + child = spawn('cmd.exe', ['/c', 'set'], { env }); +} else { + child = spawn('/usr/bin/env', [], { env }); +} + + +let response = ''; + +child.stdout.setEncoding('utf8'); + +child.stdout.on('data', mustCallAtLeast((chunk) => { + debug(`stdout: ${chunk}`); + response += chunk; +})); + +child.stdout.on('end', mustCall(() => { + assert.ok(response.includes('HELLO=WORLD')); + assert.ok(response.includes('FOO=BAR')); + assert.ok(!response.includes('UNDEFINED=undefined')); + assert.ok(response.includes('NULL=null')); + assert.ok(response.includes(`EMPTY=${os.EOL}`)); + if (isWindows) { + assert.ok(response.includes('DUPLICATE=uppercase')); + assert.ok(!response.includes('duplicate=lowercase')); + } else { + assert.ok(response.includes('DUPLICATE=uppercase')); + assert.ok(response.includes('duplicate=lowercase')); + } +})); diff --git a/test/js/node/test/parallel/test-child-process-exec-any-shells-windows.js b/test/js/node/test/parallel/test-child-process-exec-any-shells-windows.js new file mode 100644 index 0000000000..5c34bc7730 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-any-shells-windows.js @@ -0,0 +1,67 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +// This test is only relevant on Windows. +if (!common.isWindows) + common.skip('Windows specific test.'); + +// This test ensures that child_process.exec can work with any shells. + +tmpdir.refresh(); +const tmpPath = `${tmpdir.path}\\path with spaces`; +fs.mkdirSync(tmpPath); + +const test = (shell) => { + cp.exec('echo foo bar', { shell: shell }, + common.mustSucceed((stdout, stderror) => { + assert.ok(!stderror); + assert.ok(stdout.includes('foo') && stdout.includes('bar')); + })); +}; +const testCopy = (shellName, shellPath) => { + // Symlink the executable to a path with spaces, to ensure there are no issues + // related to quoting of argv0 + const copyPath = `${tmpPath}\\${shellName}`; + fs.symlinkSync(shellPath, copyPath); + test(copyPath); +}; + +const system32 = `${process.env.SystemRoot}\\System32`; + +// Test CMD +test(true); +test('cmd'); +testCopy('cmd.exe', `${system32}\\cmd.exe`); +test('cmd.exe'); +test('CMD'); + +// Test PowerShell +test('powershell'); +testCopy('powershell.exe', + `${system32}\\WindowsPowerShell\\v1.0\\powershell.exe`); +fs.writeFile(`${tmpPath}\\test file`, 'Test', common.mustSucceed(() => { + cp.exec(`Get-ChildItem "${tmpPath}" | Select-Object -Property Name`, + { shell: 'PowerShell' }, + common.mustSucceed((stdout, stderror) => { + assert.ok(!stderror); + assert.ok(stdout.includes( + 'test file')); + })); +})); + +// Test Bash (from WSL and Git), if available +cp.exec('where bash', common.mustCall((error, stdout) => { + if (error) { + return; + } + const lines = stdout.trim().split(/[\r\n]+/g); + for (let i = 0; i < lines.length; ++i) { + const bashPath = lines[i].trim(); + test(bashPath); + testCopy(`bash_${i}.exe`, bashPath); + } +})); diff --git a/test/js/node/test/parallel/test-child-process-exec-cwd.js b/test/js/node/test/parallel/test-child-process-exec-cwd.js new file mode 100644 index 0000000000..49e56ef551 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-cwd.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const exec = require('child_process').exec; + +let pwdcommand, dir; + +if (common.isWindows) { + pwdcommand = 'echo %cd%'; + dir = 'c:\\windows'; +} else { + pwdcommand = 'pwd'; + dir = '/dev'; +} + +exec(pwdcommand, { cwd: dir }, common.mustSucceed((stdout, stderr) => { + assert(stdout.toLowerCase().startsWith(dir)); +})); diff --git a/test/js/node/test/parallel/test-child-process-exec-encoding.js b/test/js/node/test/parallel/test-child-process-exec-encoding.js new file mode 100644 index 0000000000..0c3178e3f2 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-encoding.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../common'); +const stdoutData = 'foo'; +const stderrData = 'bar'; + +if (process.argv[2] === 'child') { + // The following console calls are part of the test. + console.log(stdoutData); + console.error(stderrData); +} else { + const assert = require('assert'); + const cp = require('child_process'); + const expectedStdout = `${stdoutData}\n`; + const expectedStderr = `${stderrData}\n`; + function run(options, callback) { + const cmd = `"${process.execPath}" "${__filename}" child`; + + cp.exec(cmd, options, common.mustSucceed((stdout, stderr) => { + callback(stdout, stderr); + })); + } + + // Test default encoding, which should be utf8. + run({}, (stdout, stderr) => { + assert.strictEqual(typeof stdout, 'string'); + assert.strictEqual(typeof stderr, 'string'); + assert.strictEqual(stdout, expectedStdout); + assert.strictEqual(stderr, expectedStderr); + }); + + // Test explicit utf8 encoding. + run({ encoding: 'utf8' }, (stdout, stderr) => { + assert.strictEqual(typeof stdout, 'string'); + assert.strictEqual(typeof stderr, 'string'); + assert.strictEqual(stdout, expectedStdout); + assert.strictEqual(stderr, expectedStderr); + }); + + // Test cases that result in buffer encodings. + [undefined, null, 'buffer', 'invalid'].forEach((encoding) => { + run({ encoding }, (stdout, stderr) => { + assert(stdout instanceof Buffer); + assert(stdout instanceof Buffer); + assert.strictEqual(stdout.toString(), expectedStdout); + assert.strictEqual(stderr.toString(), expectedStderr); + }); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-exec-env.js b/test/js/node/test/parallel/test-child-process-exec-env.js new file mode 100644 index 0000000000..f515643619 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-env.js @@ -0,0 +1,64 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { isWindows } = require('../common'); +const assert = require('assert'); +const exec = require('child_process').exec; +const debug = require('util').debuglog('test'); + +let success_count = 0; +let error_count = 0; +let response = ''; +let child; + +function after(err, stdout, stderr) { + if (err) { + error_count++; + debug(`error!: ${err.code}`); + debug(`stdout: ${JSON.stringify(stdout)}`); + debug(`stderr: ${JSON.stringify(stderr)}`); + assert.strictEqual(err.killed, false); + } else { + success_count++; + assert.notStrictEqual(stdout, ''); + } +} + +if (!isWindows) { + child = exec('/usr/bin/env', { env: { 'HELLO': 'WORLD' } }, after); +} else { + child = exec('set', + { env: { ...process.env, 'HELLO': 'WORLD' } }, + after); +} + +child.stdout.setEncoding('utf8'); +child.stdout.on('data', function(chunk) { + response += chunk; +}); + +process.on('exit', function() { + debug('response: ', response); + assert.strictEqual(success_count, 1); + assert.strictEqual(error_count, 0); + assert.ok(response.includes('HELLO=WORLD')); +}); diff --git a/test/js/node/test/parallel/test-child-process-exec-std-encoding.js b/test/js/node/test/parallel/test-child-process-exec-std-encoding.js new file mode 100644 index 0000000000..0818731672 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-std-encoding.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +const stdoutData = 'foo'; +const stderrData = 'bar'; +const expectedStdout = `${stdoutData}\n`; +const expectedStderr = `${stderrData}\n`; + +if (process.argv[2] === 'child') { + // The following console calls are part of the test. + console.log(stdoutData); + console.error(stderrData); +} else { + const cmd = `"${process.execPath}" "${__filename}" child`; + const child = cp.exec(cmd, common.mustSucceed((stdout, stderr) => { + assert.strictEqual(stdout, expectedStdout); + assert.strictEqual(stderr, expectedStderr); + })); + child.stdout.setEncoding('utf-8'); + child.stderr.setEncoding('utf-8'); +} diff --git a/test/js/node/test/parallel/test-child-process-exec-stdout-stderr-data-string.js b/test/js/node/test/parallel/test-child-process-exec-stdout-stderr-data-string.js new file mode 100644 index 0000000000..1fbdfbf8e4 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-stdout-stderr-data-string.js @@ -0,0 +1,13 @@ +'use strict'; +// Refs: https://github.com/nodejs/node/issues/7342 +const common = require('../common'); +const assert = require('assert'); +const exec = require('child_process').exec; + +const command = common.isWindows ? 'dir' : 'ls'; + +exec(command).stdout.on('data', common.mustCallAtLeast()); + +exec('fhqwhgads').stderr.on('data', common.mustCallAtLeast((data) => { + assert.strictEqual(typeof data, 'string'); +})); diff --git a/test/js/node/test/parallel/test-child-process-exec-timeout-expire.js b/test/js/node/test/parallel/test-child-process-exec-timeout-expire.js new file mode 100644 index 0000000000..6b62d131cb --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-timeout-expire.js @@ -0,0 +1,50 @@ +'use strict'; + +// Test exec() with a timeout that expires. + +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +const { + cleanupStaleProcess, + logAfterTime, + kExpiringChildRunTime, + kExpiringParentTimer +} = require('../common/child_process'); + +if (process.argv[2] === 'child') { + logAfterTime(kExpiringChildRunTime); + return; +} + +const cmd = `"${process.execPath}" "${__filename}" child`; + +cp.exec(cmd, { + timeout: kExpiringParentTimer, +}, common.mustCall((err, stdout, stderr) => { + console.log('[stdout]', stdout.trim()); + console.log('[stderr]', stderr.trim()); + + let sigterm = 'SIGTERM'; + assert.strictEqual(err.killed, true); + // TODO OpenBSD returns a null signal and 143 for code + if (common.isOpenBSD) { + assert.strictEqual(err.code, 143); + sigterm = null; + } else { + assert.strictEqual(err.code, null); + } + // At least starting with Darwin Kernel Version 16.4.0, sending a SIGTERM to a + // process that is still starting up kills it with SIGKILL instead of SIGTERM. + // See: https://github.com/libuv/libuv/issues/1226 + if (common.isMacOS) + assert.ok(err.signal === 'SIGTERM' || err.signal === 'SIGKILL'); + else + assert.strictEqual(err.signal, sigterm); + assert.strictEqual(err.cmd, cmd); + assert.strictEqual(stdout.trim(), ''); + assert.strictEqual(stderr.trim(), ''); +})); + +cleanupStaleProcess(__filename); diff --git a/test/js/node/test/parallel/test-child-process-exec-timeout-kill.js b/test/js/node/test/parallel/test-child-process-exec-timeout-kill.js new file mode 100644 index 0000000000..845fd1eaec --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-timeout-kill.js @@ -0,0 +1,39 @@ +'use strict'; + +// Test exec() with both a timeout and a killSignal. + +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +const { + cleanupStaleProcess, + logAfterTime, + kExpiringChildRunTime, + kExpiringParentTimer, +} = require('../common/child_process'); + +if (process.argv[2] === 'child') { + logAfterTime(kExpiringChildRunTime); + return; +} + +const cmd = `"${process.execPath}" "${__filename}" child`; + +// Test with a different kill signal. +cp.exec(cmd, { + timeout: kExpiringParentTimer, + killSignal: 'SIGKILL' +}, common.mustCall((err, stdout, stderr) => { + console.log('[stdout]', stdout.trim()); + console.log('[stderr]', stderr.trim()); + + assert.strictEqual(err.killed, true); + assert.strictEqual(err.code, null); + assert.strictEqual(err.signal, 'SIGKILL'); + assert.strictEqual(err.cmd, cmd); + assert.strictEqual(stdout.trim(), ''); + assert.strictEqual(stderr.trim(), ''); +})); + +cleanupStaleProcess(__filename); diff --git a/test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js b/test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js new file mode 100644 index 0000000000..7c8dd3661a --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js @@ -0,0 +1,34 @@ +'use strict'; + +// Test exec() when a timeout is set, but not expired. + +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +const { + cleanupStaleProcess, + logAfterTime +} = require('../common/child_process'); + +const kTimeoutNotSupposedToExpire = 2 ** 30; +const childRunTime = common.platformTimeout(100); + +// The time spent in the child should be smaller than the timeout below. +assert(childRunTime < kTimeoutNotSupposedToExpire); + +if (process.argv[2] === 'child') { + logAfterTime(childRunTime); + return; +} + +const cmd = `"${process.execPath}" "${__filename}" child`; + +cp.exec(cmd, { + timeout: kTimeoutNotSupposedToExpire +}, common.mustSucceed((stdout, stderr) => { + assert.strict(stdout.trim().includes('child stdout')); + assert.strict(stderr.trim().includes('child stderr')); +})); + +cleanupStaleProcess(__filename); diff --git a/test/js/node/test/parallel/test-child-process-execFile-promisified-abortController.js b/test/js/node/test/parallel/test-child-process-execFile-promisified-abortController.js new file mode 100644 index 0000000000..38c177eb33 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-execFile-promisified-abortController.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); +const execFile = require('child_process').execFile; +const fixtures = require('../common/fixtures'); + +const echoFixture = fixtures.path('echo.js'); +const promisified = promisify(execFile); +const invalidArgTypeError = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}; + +{ + // Verify that the signal option works properly + const ac = new AbortController(); + const signal = ac.signal; + const promise = promisified(process.execPath, [echoFixture, 0], { signal }); + + ac.abort(); + + assert.rejects( + promise, + { name: 'AbortError' } + ).then(common.mustCall()); +} + +{ + // Verify that the signal option works properly when already aborted + const signal = AbortSignal.abort(); + + assert.rejects( + promisified(process.execPath, [echoFixture, 0], { signal }), + { name: 'AbortError' } + ).then(common.mustCall()); +} + +{ + // Verify that if something different than Abortcontroller.signal + // is passed, ERR_INVALID_ARG_TYPE is thrown + const signal = {}; + assert.throws(() => { + promisified(process.execPath, [echoFixture, 0], { signal }); + }, invalidArgTypeError); +} + +{ + // Verify that if something different than Abortcontroller.signal + // is passed, ERR_INVALID_ARG_TYPE is thrown + const signal = 'world!'; + assert.throws(() => { + promisified(process.execPath, [echoFixture, 0], { signal }); + }, invalidArgTypeError); +} diff --git a/test/js/node/test/parallel/test-child-process-exit-code.js b/test/js/node/test/parallel/test-child-process-exit-code.js new file mode 100644 index 0000000000..7f5e54be42 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exit-code.js @@ -0,0 +1,41 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const fixtures = require('../common/fixtures'); + +const exitScript = fixtures.path('exit.js'); +const exitChild = spawn(process.argv[0], [exitScript, 23]); +exitChild.on('exit', common.mustCall(function(code, signal) { + assert.strictEqual(code, 23); + assert.strictEqual(signal, null); +})); + + +const errorScript = fixtures.path('child_process_should_emit_error.js'); +const errorChild = spawn(process.argv[0], [errorScript]); +errorChild.on('exit', common.mustCall(function(code, signal) { + assert.ok(code !== 0); + assert.strictEqual(signal, null); +})); diff --git a/test/js/node/test/parallel/test-child-process-flush-stdio.js b/test/js/node/test/parallel/test-child-process-flush-stdio.js new file mode 100644 index 0000000000..7b9a2a049c --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-flush-stdio.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +const cp = require('child_process'); +const assert = require('assert'); + +// Windows' `echo` command is a built-in shell command and not an external +// executable like on *nix +const opts = { shell: common.isWindows }; + +const p = cp.spawn('echo', [], opts); + +p.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + spawnWithReadable(); +})); + +p.stdout.read(); + +const spawnWithReadable = () => { + const buffer = []; + const p = cp.spawn('echo', ['123'], opts); + p.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + assert.strictEqual(Buffer.concat(buffer).toString().trim(), '123'); + })); + p.stdout.on('readable', () => { + let buf; + while ((buf = p.stdout.read()) !== null) + buffer.push(buf); + }); +}; diff --git a/test/js/node/test/parallel/test-child-process-fork-abort-signal.js b/test/js/node/test/parallel/test-child-process-fork-abort-signal.js new file mode 100644 index 0000000000..b963306fb1 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-abort-signal.js @@ -0,0 +1,105 @@ +'use strict'; + +const { mustCall, mustNotCall } = require('../common'); +const { strictEqual } = require('assert'); +const fixtures = require('../common/fixtures'); +const { fork } = require('child_process'); + +{ + // Test aborting a forked child_process after calling fork + const ac = new AbortController(); + const { signal } = ac; + const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), { + signal + }); + cp.on('exit', mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, 'SIGTERM'); + })); + cp.on('error', mustCall((err) => { + strictEqual(err.name, 'AbortError'); + })); + process.nextTick(() => ac.abort()); +} + +{ + // Test aborting with custom error + const ac = new AbortController(); + const { signal } = ac; + const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), { + signal + }); + cp.on('exit', mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, 'SIGTERM'); + })); + cp.on('error', mustCall((err) => { + strictEqual(err.name, 'AbortError'); + strictEqual(err.cause.name, 'Error'); + strictEqual(err.cause.message, 'boom'); + })); + process.nextTick(() => ac.abort(new Error('boom'))); +} + +{ + // Test passing an already aborted signal to a forked child_process + const signal = AbortSignal.abort(); + const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), { + signal + }); + cp.on('exit', mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, 'SIGTERM'); + })); + cp.on('error', mustCall((err) => { + strictEqual(err.name, 'AbortError'); + })); +} + +{ + // Test passing an aborted signal with custom error to a forked child_process + const signal = AbortSignal.abort(new Error('boom')); + const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), { + signal + }); + cp.on('exit', mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, 'SIGTERM'); + })); + cp.on('error', mustCall((err) => { + strictEqual(err.name, 'AbortError'); + strictEqual(err.cause.name, 'Error'); + strictEqual(err.cause.message, 'boom'); + })); +} + +{ + // Test passing a different kill signal + const signal = AbortSignal.abort(); + const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), { + signal, + killSignal: 'SIGKILL', + }); + cp.on('exit', mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, 'SIGKILL'); + })); + cp.on('error', mustCall((err) => { + strictEqual(err.name, 'AbortError'); + })); +} + +{ + // Test aborting a cp before close but after exit + const ac = new AbortController(); + const { signal } = ac; + const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), { + signal + }); + cp.on('exit', mustCall(() => { + ac.abort(); + })); + cp.on('error', mustNotCall()); + + setTimeout(() => cp.kill(), 1); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-and-spawn.js b/test/js/node/test/parallel/test-child-process-fork-and-spawn.js new file mode 100644 index 0000000000..88e634ba8b --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-and-spawn.js @@ -0,0 +1,44 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { fork, spawn } = require('child_process'); + +// Fork, then spawn. The spawned process should not hang. +switch (process.argv[2] || '') { + case '': + fork(__filename, ['fork']).on('exit', common.mustCall(checkExit)); + break; + case 'fork': + spawn(process.execPath, [__filename, 'spawn']) + .on('exit', common.mustCall(checkExit)); + break; + case 'spawn': + break; + default: + assert.fail(); +} + +function checkExit(statusCode) { + assert.strictEqual(statusCode, 0); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-args.js b/test/js/node/test/parallel/test-child-process-fork-args.js new file mode 100644 index 0000000000..2ed31fa4e9 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-args.js @@ -0,0 +1,105 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { fork } = require('child_process'); + +// This test check the arguments of `fork` method +// Refs: https://github.com/nodejs/node/issues/20749 +const expectedEnv = { foo: 'bar' }; + +// Ensure that first argument `modulePath` must be provided +// and be of type string +{ + const invalidModulePath = [ + 0, + true, + undefined, + null, + [], + {}, + () => {}, + Symbol('t'), + ]; + invalidModulePath.forEach((modulePath) => { + assert.throws(() => fork(modulePath), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "modulePath" argument must be of type string/ + }); + }); + + const cp = fork(fixtures.path('child-process-echo-options.js')); + cp.on( + 'exit', + common.mustCall((code) => { + assert.strictEqual(code, 0); + }) + ); +} + +// Ensure that the second argument of `fork` +// and `fork` should parse options +// correctly if args is undefined or null +{ + const invalidSecondArgs = [ + 0, + true, + () => {}, + Symbol('t'), + ]; + invalidSecondArgs.forEach((arg) => { + assert.throws( + () => { + fork(fixtures.path('child-process-echo-options.js'), arg); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); + + const argsLists = [undefined, null, []]; + + argsLists.forEach((args) => { + const cp = fork(fixtures.path('child-process-echo-options.js'), args, { + env: { ...process.env, ...expectedEnv } + }); + + cp.on( + 'message', + common.mustCall(({ env }) => { + assert.strictEqual(env.foo, expectedEnv.foo); + }) + ); + + cp.on( + 'exit', + common.mustCall((code) => { + assert.strictEqual(code, 0); + }) + ); + }); +} + +// Ensure that the third argument should be type of object if provided +{ + const invalidThirdArgs = [ + 0, + true, + () => {}, + Symbol('t'), + ]; + invalidThirdArgs.forEach((arg) => { + assert.throws( + () => { + fork(fixtures.path('child-process-echo-options.js'), [], arg); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-close.js b/test/js/node/test/parallel/test-child-process-fork-close.js new file mode 100644 index 0000000000..bfaabd3d2f --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-close.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fork = require('child_process').fork; +const fixtures = require('../common/fixtures'); + +const cp = fork(fixtures.path('child-process-message-and-exit.js')); + +let gotMessage = false; +let gotExit = false; +let gotClose = false; + +cp.on('message', common.mustCall(function(message) { + assert(!gotMessage); + assert(!gotClose); + assert.strictEqual(message, 'hello'); + gotMessage = true; +})); + +cp.on('exit', common.mustCall(function() { + assert(!gotExit); + assert(!gotClose); + gotExit = true; +})); + +cp.on('close', common.mustCall(function() { + assert(gotMessage); + assert(gotExit); + assert(!gotClose); + gotClose = true; +})); diff --git a/test/js/node/test/parallel/test-child-process-fork-detached.js b/test/js/node/test/parallel/test-child-process-fork-detached.js new file mode 100644 index 0000000000..87c1517328 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-detached.js @@ -0,0 +1,23 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fork = require('child_process').fork; +const fixtures = require('../common/fixtures'); + +const nonPersistentNode = fork( + fixtures.path('parent-process-nonpersistent-fork.js'), + [], + { silent: true }); + +let childId = -1; + +nonPersistentNode.stdout.on('data', (data) => { + childId = parseInt(data, 10); + nonPersistentNode.kill(); +}); + +process.on('exit', () => { + assert.notStrictEqual(childId, -1); + // Killing the child process should not throw an error + process.kill(childId); +}); diff --git a/test/js/node/test/parallel/test-child-process-fork-exec-path.js b/test/js/node/test/parallel/test-child-process-fork-exec-path.js new file mode 100644 index 0000000000..3417a102c6 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-exec-path.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Test that `fork()` respects the `execPath` option. + +const tmpdir = require('../common/tmpdir'); +const { addLibraryPath } = require('../common/shared-lib-util'); +const assert = require('assert'); +const fs = require('fs'); +const { fork } = require('child_process'); + +const msg = { test: 'this' }; +const nodePath = process.execPath; +const copyPath = tmpdir.resolve('node-copy.exe'); + +addLibraryPath(process.env); + +// Child +if (process.env.FORK) { + assert.strictEqual(process.execPath, copyPath); + assert.ok(process.send); + process.send(msg); + return process.exit(); +} + +// Parent +tmpdir.refresh(); +assert.strictEqual(fs.existsSync(copyPath), false); +fs.copyFileSync(nodePath, copyPath, fs.constants.COPYFILE_FICLONE); +fs.chmodSync(copyPath, '0755'); + +const envCopy = { ...process.env, FORK: 'true' }; +const child = fork(__filename, { execPath: copyPath, env: envCopy }); +child.on('message', common.mustCall(function(recv) { + assert.deepStrictEqual(recv, msg); +})); +child.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); +})); diff --git a/test/js/node/test/parallel/test-child-process-fork-no-shell.js b/test/js/node/test/parallel/test-child-process-fork-no-shell.js new file mode 100644 index 0000000000..81ceab61fd --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-no-shell.js @@ -0,0 +1,20 @@ +'use strict'; +// This test verifies that the shell option is not supported by fork(). +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +const expected = common.isWindows ? '%foo%' : '$foo'; + +if (process.argv[2] === undefined) { + const child = cp.fork(__filename, [expected], { + shell: true, + env: { ...process.env, foo: 'bar' } + }); + + child.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); +} else { + assert.strictEqual(process.argv[2], expected); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-stdio-string-variant.js b/test/js/node/test/parallel/test-child-process-fork-stdio-string-variant.js new file mode 100644 index 0000000000..6a396b51d9 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-stdio-string-variant.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); + +// Ensures that child_process.fork can accept string +// variant of stdio parameter in options object and +// throws a TypeError when given an unexpected string + +const assert = require('assert'); +const fork = require('child_process').fork; +const fixtures = require('../common/fixtures'); + +const childScript = fixtures.path('child-process-spawn-node'); +const malFormedOpts = { stdio: '33' }; +const payload = { hello: 'world' }; + +assert.throws( + () => fork(childScript, malFormedOpts), + { code: 'ERR_INVALID_ARG_VALUE', name: 'TypeError' }); + +function test(stringVariant) { + const child = fork(childScript, { stdio: stringVariant }); + + child.on('message', common.mustCall((message) => { + assert.deepStrictEqual(message, { foo: 'bar' }); + })); + + child.send(payload); + + child.on('exit', common.mustCall((code) => assert.strictEqual(code, 0))); +} + +['pipe', 'inherit', 'ignore'].forEach(test); diff --git a/test/js/node/test/parallel/test-child-process-fork-url.mjs b/test/js/node/test/parallel/test-child-process-fork-url.mjs new file mode 100644 index 0000000000..9261b87563 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-url.mjs @@ -0,0 +1,11 @@ +import { mustCall } from '../common/index.mjs'; +import { fork } from 'child_process'; + +if (process.argv[2] === 'child') { + process.disconnect(); +} else { + const child = fork(new URL(import.meta.url), ['child']); + + child.on('disconnect', mustCall()); + child.once('exit', mustCall()); +} diff --git a/test/js/node/test/parallel/test-child-process-fork3.js b/test/js/node/test/parallel/test-child-process-fork3.js new file mode 100644 index 0000000000..735a441950 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork3.js @@ -0,0 +1,27 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const child_process = require('child_process'); +const fixtures = require('../common/fixtures'); + +child_process.fork(fixtures.path('empty.js')); // should not hang diff --git a/test/js/node/test/parallel/test-child-process-ipc.js b/test/js/node/test/parallel/test-child-process-ipc.js new file mode 100644 index 0000000000..d776f9594b --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-ipc.js @@ -0,0 +1,63 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + mustCall, + mustNotCall, +} = require('../common'); +const assert = require('assert'); +const debug = require('util').debuglog('test'); + +const { spawn } = require('child_process'); +const fixtures = require('../common/fixtures'); + +const sub = fixtures.path('echo.js'); + +const child = spawn(process.argv[0], [sub]); + +child.stderr.on('data', mustNotCall()); + +child.stdout.setEncoding('utf8'); + +const messages = [ + 'hello world\r\n', + 'echo me\r\n', +]; + +child.stdout.on('data', mustCall((data) => { + debug(`child said: ${JSON.stringify(data)}`); + const test = messages.shift(); + debug(`testing for '${test}'`); + assert.strictEqual(data, test); + if (messages.length) { + debug(`writing '${messages[0]}'`); + child.stdin.write(messages[0]); + } else { + assert.strictEqual(messages.length, 0); + child.stdin.end(); + } +}, messages.length)); + +child.stdout.on('end', mustCall((data) => { + debug('child end'); +})); diff --git a/test/js/node/test/parallel/test-child-process-kill.js b/test/js/node/test/parallel/test-child-process-kill.js new file mode 100644 index 0000000000..1025c69ba1 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-kill.js @@ -0,0 +1,41 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const cat = spawn(common.isWindows ? 'cmd' : 'cat'); + +cat.stdout.on('end', common.mustCall()); +cat.stderr.on('data', common.mustNotCall()); +cat.stderr.on('end', common.mustCall()); + +cat.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, null); + assert.strictEqual(signal, 'SIGTERM'); + assert.strictEqual(cat.signalCode, 'SIGTERM'); +})); + +assert.strictEqual(cat.signalCode, null); +assert.strictEqual(cat.killed, false); +cat.kill(); +assert.strictEqual(cat.killed, true); diff --git a/test/js/node/test/parallel/test-child-process-no-deprecation.js b/test/js/node/test/parallel/test-child-process-no-deprecation.js new file mode 100644 index 0000000000..d12e5b882f --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-no-deprecation.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +process.noDeprecation = true; + +if (process.argv[2] === 'child') { + process.emitWarning('Something else is deprecated.', 'DeprecationWarning'); +} else { + // parent process + const spawn = require('child_process').spawn; + + // spawn self as child + const child = spawn(process.execPath, [process.argv[1], 'child']); + + child.stderr.on('data', common.mustNotCall()); +} diff --git a/test/js/node/test/parallel/test-child-process-send-type-error.js b/test/js/node/test/parallel/test-child-process-send-type-error.js new file mode 100644 index 0000000000..65c620dd29 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-send-type-error.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const cp = require('child_process'); + +function fail(proc, args) { + assert.throws(() => { + proc.send.apply(proc, args); + }, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }); +} + +let target = process; + +if (process.argv[2] !== 'child') { + target = cp.fork(__filename, ['child']); + target.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); +} + +fail(target, ['msg', null, null]); +fail(target, ['msg', null, '']); +fail(target, ['msg', null, 'foo']); +fail(target, ['msg', null, 0]); +fail(target, ['msg', null, NaN]); +fail(target, ['msg', null, 1]); +fail(target, ['msg', null, null, common.mustNotCall()]); diff --git a/test/js/node/test/parallel/test-child-process-set-blocking.js b/test/js/node/test/parallel/test-child-process-set-blocking.js new file mode 100644 index 0000000000..6dcfa93141 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-set-blocking.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const ch = require('child_process'); + +const SIZE = 100000; +const python = process.env.PYTHON || (common.isWindows ? 'python' : 'python3'); + +const cp = ch.spawn(python, ['-c', `print(${SIZE} * "C")`], { + stdio: 'inherit' +}); + +cp.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); +})); diff --git a/test/js/node/test/parallel/test-child-process-silent.js b/test/js/node/test/parallel/test-child-process-silent.js new file mode 100644 index 0000000000..892c4527c9 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-silent.js @@ -0,0 +1,106 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const childProcess = require('child_process'); + +// Child pipe test +if (process.argv[2] === 'pipe') { + process.stdout.write('stdout message'); + process.stderr.write('stderr message'); + +} else if (process.argv[2] === 'ipc') { + // Child IPC test + process.send('message from child'); + process.on('message', function() { + process.send('got message from primary'); + }); + +} else if (process.argv[2] === 'primary') { + // Primary | start child pipe test + + const child = childProcess.fork(process.argv[1], ['pipe'], { silent: true }); + + // Allow child process to self terminate + child.disconnect(); + + child.on('exit', function() { + process.exit(0); + }); + +} else { + // Testcase | start primary && child IPC test + + // testing: is stderr and stdout piped to primary + const args = [process.argv[1], 'primary']; + const primary = childProcess.spawn(process.execPath, args); + + // Got any stderr or std data + let stdoutData = false; + primary.stdout.on('data', function() { + stdoutData = true; + }); + let stderrData = false; + primary.stderr.on('data', function() { + stderrData = true; + }); + + // testing: do message system work when using silent + const child = childProcess.fork(process.argv[1], ['ipc'], { silent: true }); + + // Manual pipe so we will get errors + child.stderr.pipe(process.stderr, { end: false }); + child.stdout.pipe(process.stdout, { end: false }); + + let childSending = false; + let childReceiving = false; + child.on('message', function(message) { + if (childSending === false) { + childSending = (message === 'message from child'); + } + + if (childReceiving === false) { + childReceiving = (message === 'got message from primary'); + } + + if (childReceiving === true) { + child.kill(); + } + }); + child.send('message to child'); + + // Check all values + process.on('exit', function() { + // clean up + child.kill(); + primary.kill(); + + // Check std(out|err) pipes + assert.ok(!stdoutData); + assert.ok(!stderrData); + + // Check message system + assert.ok(childSending); + assert.ok(childReceiving); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-spawn-args.js b/test/js/node/test/parallel/test-child-process-spawn-args.js new file mode 100644 index 0000000000..ec56f409fa --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawn-args.js @@ -0,0 +1,55 @@ +'use strict'; + +// This test confirms that `undefined`, `null`, and `[]` +// can be used as a placeholder for the second argument (`args`) of `spawn()`. +// Previously, there was a bug where using `undefined` for the second argument +// caused the third argument (`options`) to be ignored. +// See https://github.com/nodejs/node/issues/24912. + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +const assert = require('assert'); +const { spawn } = require('child_process'); + +tmpdir.refresh(); + +const command = common.isWindows ? 'cd' : 'pwd'; +const options = { cwd: tmpdir.path }; + +if (common.isWindows) { + // This test is not the case for Windows based systems + // unless the `shell` options equals to `true` + + options.shell = true; +} + +const testCases = [ + undefined, + null, + [], +]; + +const expectedResult = tmpdir.path.trim().toLowerCase(); + +(async () => { + const results = await Promise.all( + testCases.map((testCase) => { + return new Promise((resolve) => { + const subprocess = spawn(command, testCase, options); + + let accumulatedData = Buffer.alloc(0); + + subprocess.stdout.on('data', common.mustCall((data) => { + accumulatedData = Buffer.concat([accumulatedData, data]); + })); + + subprocess.stdout.on('end', () => { + resolve(accumulatedData.toString().trim().toLowerCase()); + }); + }); + }) + ); + + assert.deepStrictEqual([...new Set(results)], [expectedResult]); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-child-process-spawn-controller.js b/test/js/node/test/parallel/test-child-process-spawn-controller.js new file mode 100644 index 0000000000..20facb09b3 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawn-controller.js @@ -0,0 +1,183 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); +const fixtures = require('../common/fixtures'); + +const aliveScript = fixtures.path('child-process-stay-alive-forever.js'); +{ + // Verify that passing an AbortSignal works + const controller = new AbortController(); + const { signal } = controller; + + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGTERM'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + })); + + controller.abort(); +} + +{ + // Verify that passing an AbortSignal with custom abort error works + const controller = new AbortController(); + const { signal } = controller; + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGTERM'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(e.cause.name, 'Error'); + assert.strictEqual(e.cause.message, 'boom'); + })); + + controller.abort(new Error('boom')); +} + +{ + const controller = new AbortController(); + const { signal } = controller; + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGTERM'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(e.cause, 'boom'); + })); + + controller.abort('boom'); +} + +{ + // Verify that passing an already-aborted signal works. + const signal = AbortSignal.abort(); + + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGTERM'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + })); +} + +{ + // Verify that passing an already-aborted signal with custom abort error + // works. + const signal = AbortSignal.abort(new Error('boom')); + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGTERM'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(e.cause.name, 'Error'); + assert.strictEqual(e.cause.message, 'boom'); + })); +} + +{ + const signal = AbortSignal.abort('boom'); + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGTERM'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(e.cause, 'boom'); + })); +} + +{ + // Verify that waiting a bit and closing works + const controller = new AbortController(); + const { signal } = controller; + + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGTERM'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + })); + + setTimeout(() => controller.abort(), 1); +} + +{ + // Test passing a different killSignal + const controller = new AbortController(); + const { signal } = controller; + + const cp = spawn(process.execPath, [aliveScript], { + signal, + killSignal: 'SIGKILL', + }); + + cp.on('exit', common.mustCall((code, killSignal) => { + assert.strictEqual(code, null); + assert.strictEqual(killSignal, 'SIGKILL'); + })); + + cp.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + })); + + setTimeout(() => controller.abort(), 1); +} + +{ + // Test aborting a cp before close but after exit + const controller = new AbortController(); + const { signal } = controller; + + const cp = spawn(process.execPath, [aliveScript], { + signal, + }); + + cp.on('exit', common.mustCall(() => { + controller.abort(); + })); + + cp.on('error', common.mustNotCall()); + + setTimeout(() => cp.kill(), 1); +} diff --git a/test/js/node/test/parallel/test-child-process-spawn-event.js b/test/js/node/test/parallel/test-child-process-spawn-event.js new file mode 100644 index 0000000000..c025d86286 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawn-event.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const spawn = require('child_process').spawn; +const assert = require('assert'); + +const subprocess = spawn('echo', ['ok']); + +let didSpawn = false; +subprocess.on('spawn', function() { + didSpawn = true; +}); +function mustCallAfterSpawn() { + return common.mustCall(function() { + assert.ok(didSpawn); + }); +} + +subprocess.on('error', common.mustNotCall()); +subprocess.on('spawn', common.mustCall()); +subprocess.stdout.on('data', mustCallAfterSpawn()); +subprocess.stdout.on('end', mustCallAfterSpawn()); +subprocess.stdout.on('close', mustCallAfterSpawn()); +subprocess.stderr.on('data', common.mustNotCall()); +subprocess.stderr.on('end', mustCallAfterSpawn()); +subprocess.stderr.on('close', mustCallAfterSpawn()); +subprocess.on('exit', mustCallAfterSpawn()); +subprocess.on('close', mustCallAfterSpawn()); diff --git a/test/js/node/test/parallel/test-child-process-spawnsync-args.js b/test/js/node/test/parallel/test-child-process-spawnsync-args.js new file mode 100644 index 0000000000..4e65b22f6e --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawnsync-args.js @@ -0,0 +1,48 @@ +'use strict'; + +// This test confirms that `undefined`, `null`, and `[]` can be used +// as a placeholder for the second argument (`args`) of `spawnSync()`. +// Previously, there was a bug where using `undefined` for the second argument +// caused the third argument (`options`) to be ignored. +// See https://github.com/nodejs/node/issues/24912. + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +const assert = require('assert'); +const { spawnSync } = require('child_process'); + +const command = common.isWindows ? 'cd' : 'pwd'; +const options = { cwd: tmpdir.path }; + +tmpdir.refresh(); + +if (common.isWindows) { + // This test is not the case for Windows based systems + // unless the `shell` options equals to `true` + + options.shell = true; +} + +const testCases = [ + undefined, + null, + [], +]; + +const expectedResult = tmpdir.path.trim().toLowerCase(); + +const results = testCases.map((testCase) => { + const { stdout, stderr, error } = spawnSync( + command, + testCase, + options + ); + + assert.ifError(error); + assert.deepStrictEqual(stderr, Buffer.alloc(0)); + + return stdout.toString().trim().toLowerCase(); +}); + +assert.deepStrictEqual([...new Set(results)], [expectedResult]); diff --git a/test/js/node/test/parallel/test-child-process-spawnsync-env.js b/test/js/node/test/parallel/test-child-process-spawnsync-env.js new file mode 100644 index 0000000000..c8e11b5067 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawnsync-env.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +if (process.argv[2] === 'child') { + console.log(process.env.foo); +} else { + const expected = 'bar'; + const child = cp.spawnSync(process.execPath, [__filename, 'child'], { + env: Object.assign(process.env, { foo: expected }) + }); + + assert.strictEqual(child.stdout.toString().trim(), expected); +} diff --git a/test/js/node/test/parallel/test-child-process-spawnsync-input.js b/test/js/node/test/parallel/test-child-process-spawnsync-input.js new file mode 100644 index 0000000000..4b4549ff55 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawnsync-input.js @@ -0,0 +1,127 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); + +const spawnSync = require('child_process').spawnSync; + +const msgOut = 'this is stdout'; +const msgErr = 'this is stderr'; + +// This is actually not os.EOL? +const msgOutBuf = Buffer.from(`${msgOut}\n`); +const msgErrBuf = Buffer.from(`${msgErr}\n`); + +const args = [ + '-e', + `console.log("${msgOut}"); console.error("${msgErr}");`, +]; + +let ret; + + +function checkSpawnSyncRet(ret) { + assert.strictEqual(ret.status, 0); + assert.strictEqual(ret.error, undefined); +} + +function verifyBufOutput(ret) { + checkSpawnSyncRet(ret); + assert.deepStrictEqual(ret.stdout.toString('utf8'), msgOutBuf.toString('utf8')); + assert.deepStrictEqual(ret.stdout, msgOutBuf); + assert.deepStrictEqual(ret.stderr.toString('utf8'), msgErrBuf.toString('utf8')); + assert.deepStrictEqual(ret.stderr, msgErrBuf); +} + +if (process.argv.includes('spawnchild')) { + switch (process.argv[3]) { + case '1': + ret = spawnSync(process.execPath, args, { stdio: 'inherit' }); + checkSpawnSyncRet(ret); + break; + case '2': + ret = spawnSync(process.execPath, args, { + stdio: ['inherit', 'inherit', 'inherit'] + }); + checkSpawnSyncRet(ret); + break; + } + process.exit(0); + return; +} + +verifyBufOutput(spawnSync(process.execPath, [__filename, 'spawnchild', 1])); +verifyBufOutput(spawnSync(process.execPath, [__filename, 'spawnchild', 2])); + +let options = { + input: 1234 +}; + +assert.throws( + () => spawnSync('cat', [], options), + { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }); + +options = { + input: 'hello world' +}; + +ret = spawnSync('cat', [], options); + +checkSpawnSyncRet(ret); +assert.strictEqual(ret.stdout.toString('utf8'), options.input); +assert.strictEqual(ret.stderr.toString('utf8'), ''); + +options = { + input: Buffer.from('hello world') +}; + +ret = spawnSync('cat', [], options); + +checkSpawnSyncRet(ret); +assert.deepStrictEqual(ret.stdout, options.input); +assert.deepStrictEqual(ret.stderr, Buffer.from('')); + +// common.getArrayBufferViews expects a buffer +// with length an multiple of 8 +const msgBuf = Buffer.from('hello world'.repeat(8)); +for (const arrayBufferView of common.getArrayBufferViews(msgBuf)) { + options = { + input: arrayBufferView + }; + + ret = spawnSync('cat', [], options); + + checkSpawnSyncRet(ret); + + assert.deepStrictEqual(ret.stdout, msgBuf); + assert.deepStrictEqual(ret.stderr, Buffer.from('')); +} + +verifyBufOutput(spawnSync(process.execPath, args)); + +ret = spawnSync(process.execPath, args, { encoding: 'utf8' }); + +checkSpawnSyncRet(ret); +assert.strictEqual(ret.stdout, `${msgOut}\n`); +assert.strictEqual(ret.stderr, `${msgErr}\n`); diff --git a/test/js/node/test/parallel/test-child-process-stdin-ipc.js b/test/js/node/test/parallel/test-child-process-stdin-ipc.js new file mode 100644 index 0000000000..945960b99b --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdin-ipc.js @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const spawn = require('child_process').spawn; + +if (process.argv[2] === 'child') { + // Just reference stdin, it should start it + process.stdin; // eslint-disable-line no-unused-expressions + return; +} + +const proc = spawn(process.execPath, [__filename, 'child'], { + stdio: ['ipc', 'inherit', 'inherit'] +}); + +proc.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); +})); diff --git a/test/js/node/test/parallel/test-child-process-stdio-big-write-end.js b/test/js/node/test/parallel/test-child-process-stdio-big-write-end.js new file mode 100644 index 0000000000..85e6a8b321 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio-big-write-end.js @@ -0,0 +1,86 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { + mustCall, + mustCallAtLeast, +} = require('../common'); +const assert = require('assert'); +const debug = require('util').debuglog('test'); + +let bufsize = 0; + +switch (process.argv[2]) { + case undefined: + return parent(); + case 'child': + return child(); + default: + throw new Error('invalid'); +} + +function parent() { + const spawn = require('child_process').spawn; + const child = spawn(process.execPath, [__filename, 'child']); + let sent = 0; + + let n = ''; + child.stdout.setEncoding('ascii'); + child.stdout.on('data', mustCallAtLeast((c) => { + n += c; + })); + child.stdout.on('end', mustCall(() => { + assert.strictEqual(+n, sent); + debug('ok'); + })); + + // Write until the buffer fills up. + let buf; + do { + bufsize += 1024; + buf = Buffer.alloc(bufsize, '.'); + sent += bufsize; + } while (child.stdin.write(buf)); + + // Then write a bunch more times. + for (let i = 0; i < 100; i++) { + const buf = Buffer.alloc(bufsize, '.'); + sent += bufsize; + child.stdin.write(buf); + } + + // Now end, before it's all flushed. + child.stdin.end(); + + // now we wait... +} + +function child() { + let received = 0; + process.stdin.on('data', mustCallAtLeast((c) => { + received += c.length; + })); + process.stdin.on('end', mustCall(() => { + // This console.log is part of the test. + console.log(received); + })); +} diff --git a/test/js/node/test/parallel/test-child-process-stdio-inherit.js b/test/js/node/test/parallel/test-child-process-stdio-inherit.js new file mode 100644 index 0000000000..034a077016 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio-inherit.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +if (process.argv[2] === 'parent') + parent(); +else + grandparent(); + +function grandparent() { + const child = spawn(process.execPath, [__filename, 'parent']); + child.stderr.pipe(process.stderr); + let output = ''; + const input = 'asdfasdf'; + + child.stdout.on('data', function(chunk) { + output += chunk; + }); + child.stdout.setEncoding('utf8'); + + child.stdin.end(input); + + child.on('close', function(code, signal) { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + // 'cat' on windows adds a \r\n at the end. + assert.strictEqual(output.trim(), input.trim()); + }); +} + +function parent() { + // Should not immediately exit. + spawn('cat', [], { stdio: 'inherit' }); +} diff --git a/test/js/node/test/parallel/test-child-process-stdio-overlapped.js b/test/js/node/test/parallel/test-child-process-stdio-overlapped.js new file mode 100644 index 0000000000..5c48e7ee10 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio-overlapped.js @@ -0,0 +1,79 @@ +// Test for "overlapped" stdio option. This test uses the "overlapped-checker" +// helper program which basically a specialized echo program. +// +// The test has two goals: +// +// - Verify that overlapped I/O works on windows. The test program will deadlock +// if stdin doesn't have the FILE_FLAG_OVERLAPPED flag set on startup (see +// test/overlapped-checker/main_win.c for more details). +// - Verify that "overlapped" stdio option works transparently as a pipe (on +// unix/windows) +// +// This is how the test works: +// +// - This script assumes only numeric strings are written to the test program +// stdout. +// - The test program will be spawned with "overlapped" set on stdin and "pipe" +// set on stdout/stderr and at startup writes a number to its stdout +// - When this script receives some data, it will parse the number, add 50 and +// write to the test program's stdin. +// - The test program will then echo the number back to us which will repeat the +// cycle until the number reaches 200, at which point we send the "exit" +// string, which causes the test program to exit. +// - Extra assertion: Every time the test program writes a string to its stdout, +// it will write the number of bytes written to stderr. +// - If overlapped I/O is not setup correctly, this test is going to hang. +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const child_process = require('child_process'); + +const exeExtension = process.platform === 'win32' ? '.exe' : ''; +const exe = 'overlapped-checker' + exeExtension; +const exePath = path.join(path.dirname(process.execPath), exe); + +if (!require('fs').existsSync(exePath)) { + common.skip(exe + ' binary is not available'); +} + +const child = child_process.spawn(exePath, [], { + stdio: ['overlapped', 'pipe', 'pipe'] +}); + +child.stdin.setEncoding('utf8'); +child.stdout.setEncoding('utf8'); +child.stderr.setEncoding('utf8'); + +function writeNext(n) { + child.stdin.write((n + 50).toString()); +} + +child.stdout.on('data', (s) => { + const n = Number(s); + if (n >= 200) { + child.stdin.write('exit'); + return; + } + writeNext(n); +}); + +let stderr = ''; +child.stderr.on('data', (s) => { + stderr += s; +}); + +child.stderr.on('end', common.mustCall(() => { + // This is the sequence of numbers sent to us: + // - 0 (1 byte written) + // - 50 (2 bytes written) + // - 100 (3 bytes written) + // - 150 (3 bytes written) + // - 200 (3 bytes written) + assert.strictEqual(stderr, '12333'); +})); + +child.on('exit', common.mustCall((status) => { + // The test program will return the number of writes as status code. + assert.strictEqual(status, 0); +})); diff --git a/test/js/node/test/parallel/test-child-process-stdio.js b/test/js/node/test/parallel/test-child-process-stdio.js new file mode 100644 index 0000000000..15c2770aa2 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio.js @@ -0,0 +1,77 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +// Test stdio piping. +{ + const child = spawn(...common.pwdCommand, { stdio: ['pipe'] }); + assert.notStrictEqual(child.stdout, null); + assert.notStrictEqual(child.stderr, null); +} + +// Test stdio ignoring. +{ + const child = spawn(...common.pwdCommand, { stdio: 'ignore' }); + assert.strictEqual(child.stdout, null); + assert.strictEqual(child.stderr, null); +} + +// Asset options invariance. +{ + const options = { stdio: 'ignore' }; + spawn(...common.pwdCommand, options); + assert.deepStrictEqual(options, { stdio: 'ignore' }); +} + +// Test stdout buffering. +{ + let output = ''; + const child = spawn(...common.pwdCommand); + + child.stdout.setEncoding('utf8'); + child.stdout.on('data', function(s) { + output += s; + }); + + child.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); + })); + + child.on('close', common.mustCall(function() { + assert.strictEqual(output.length > 1, true); + assert.strictEqual(output[output.length - 1], '\n'); + })); +} + +// Assert only one IPC pipe allowed. +assert.throws( + () => { + spawn( + ...common.pwdCommand, + { stdio: ['pipe', 'pipe', 'pipe', 'ipc', 'ipc'] } + ); + }, + { code: 'ERR_IPC_ONE_PIPE', name: 'Error' } +); diff --git a/test/js/node/test/parallel/test-child-process-stdout-flush-exit.js b/test/js/node/test/parallel/test-child-process-stdout-flush-exit.js new file mode 100644 index 0000000000..3c5f00d9bb --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdout-flush-exit.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// If child process output to console and exit +// The console.log statements here are part of the test. +if (process.argv[2] === 'child') { + console.log('hello'); + for (let i = 0; i < 200; i++) { + console.log('filler'); + } + console.log('goodbye'); + process.exit(0); +} else { + // parent process + const spawn = require('child_process').spawn; + + // spawn self as child + const child = spawn(process.argv[0], [process.argv[1], 'child']); + + let stdout = ''; + + child.stderr.on('data', common.mustNotCall()); + + // Check if we receive both 'hello' at start and 'goodbye' at end + child.stdout.setEncoding('utf8'); + child.stdout.on('data', common.mustCallAtLeast((data) => { + stdout += data; + })); + + child.on('close', common.mustCall(() => { + assert.strictEqual(stdout.slice(0, 6), 'hello\n'); + assert.strictEqual(stdout.slice(stdout.length - 8), 'goodbye\n'); + })); +} diff --git a/test/js/node/test/parallel/test-child-process-stdout-flush.js b/test/js/node/test/parallel/test-child-process-stdout-flush.js new file mode 100644 index 0000000000..bc549fb6f3 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdout-flush.js @@ -0,0 +1,48 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const fixtures = require('../common/fixtures'); + +const sub = fixtures.path('print-chars.js'); + +const n = 500000; + +const child = spawn(process.argv[0], [sub, n]); + +let count = 0; + +child.stderr.setEncoding('utf8'); +child.stderr.on('data', common.mustNotCall()); + +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (data) => { + count += data.length; +}); + +child.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + assert.strictEqual(n, count); +})); diff --git a/test/js/node/test/parallel/test-cli-eval-event.js b/test/js/node/test/parallel/test-cli-eval-event.js new file mode 100644 index 0000000000..df356e50d3 --- /dev/null +++ b/test/js/node/test/parallel/test-cli-eval-event.js @@ -0,0 +1,15 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +const child = spawn(process.execPath, ['-e', ` + const server = require('net').createServer().listen(0); + server.once('listening', server.close); +`]); + +child.once('exit', common.mustCall(function(exitCode, signalCode) { + assert.strictEqual(exitCode, 0); + assert.strictEqual(signalCode, null); +})); diff --git a/test/js/node/test/parallel/test-client-request-destroy.js b/test/js/node/test/parallel/test-client-request-destroy.js new file mode 100644 index 0000000000..2f3efcf812 --- /dev/null +++ b/test/js/node/test/parallel/test-client-request-destroy.js @@ -0,0 +1,13 @@ +'use strict'; + +// Test that http.ClientRequest,prototype.destroy() returns `this`. +require('../common'); + +const assert = require('assert'); +const http = require('http'); +const clientRequest = new http.ClientRequest({ createConnection: () => {} }); + +assert.strictEqual(clientRequest.destroyed, false); +assert.strictEqual(clientRequest.destroy(), clientRequest); +assert.strictEqual(clientRequest.destroyed, true); +assert.strictEqual(clientRequest.destroy(), clientRequest); diff --git a/test/js/node/test/parallel/test-cluster-advanced-serialization.js b/test/js/node/test/parallel/test-cluster-advanced-serialization.js new file mode 100644 index 0000000000..ffca3a8f97 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-advanced-serialization.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isPrimary) { + cluster.settings.serialization = 'advanced'; + const worker = cluster.fork(); + const circular = {}; + circular.circular = circular; + + worker.on('online', common.mustCall(() => { + worker.send(circular); + + worker.on('message', common.mustCall((msg) => { + assert.deepStrictEqual(msg, circular); + worker.kill(); + })); + })); +} else { + process.on('message', (msg) => process.send(msg)); +} diff --git a/test/js/node/test/parallel/test-cluster-bind-privileged-port.js b/test/js/node/test/parallel/test-cluster-bind-privileged-port.js new file mode 100644 index 0000000000..43f6f20158 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-bind-privileged-port.js @@ -0,0 +1,66 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isLinux) return; // TODO: BUN +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); +const { readFileSync } = require('fs'); + +if (common.isLinux) { + try { + const unprivilegedPortStart = parseInt(readFileSync('/proc/sys/net/ipv4/ip_unprivileged_port_start')); + if (unprivilegedPortStart <= 42) { + common.skip('Port 42 is unprivileged'); + } + } catch { + // Do nothing, feature doesn't exist, minimum is 1024 so 42 is usable. + // Continue... + } +} + +// Skip on macOS Mojave. https://github.com/nodejs/node/issues/21679 +if (common.isMacOS) + common.skip('macOS may allow ordinary processes to use any port'); + +if (common.isIBMi) + common.skip('IBMi may allow ordinary processes to use any port'); + +if (common.isWindows) + common.skip('not reliable on Windows.'); + +if (process.getuid() === 0) + common.skip('Test is not supposed to be run as root.'); + +if (cluster.isPrimary) { + cluster.fork().on('exit', common.mustCall((exitCode) => { + assert.strictEqual(exitCode, 0); + })); +} else { + const s = net.createServer(common.mustNotCall()); + s.listen(42, common.mustNotCall('listen should have failed')); + s.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'EACCES'); + process.disconnect(); + })); +} diff --git a/test/js/node/test/parallel/test-cluster-call-and-destroy.js b/test/js/node/test/parallel/test-cluster-call-and-destroy.js new file mode 100644 index 0000000000..2ff20cf03a --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-call-and-destroy.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const cluster = require('cluster'); +const assert = require('assert'); + +if (cluster.isPrimary) { + const worker = cluster.fork(); + worker.on('disconnect', common.mustCall(() => { + assert.strictEqual(worker.isConnected(), false); + worker.destroy(); + })); +} else { + assert.strictEqual(cluster.worker.isConnected(), true); + cluster.worker.disconnect(); +} diff --git a/test/js/node/test/parallel/test-cluster-child-index-dgram.js b/test/js/node/test/parallel/test-cluster-child-index-dgram.js new file mode 100644 index 0000000000..0df7bc175b --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-child-index-dgram.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common'); +const Countdown = require('../common/countdown'); +if (common.isWindows) + common.skip('dgram clustering is currently not supported on Windows.'); + +const cluster = require('cluster'); +const dgram = require('dgram'); + +// Test an edge case when using `cluster` and `dgram.Socket.bind()` +// the port of `0`. +const kPort = 0; + +function child() { + const kTime = 2; + const countdown = new Countdown(kTime * 2, () => { + process.exit(0); + }); + for (let i = 0; i < kTime; i += 1) { + const socket = new dgram.Socket('udp4'); + socket.bind(kPort, common.mustCall(() => { + // `process.nextTick()` or `socket2.close()` would throw + // ERR_SOCKET_DGRAM_NOT_RUNNING + process.nextTick(() => { + socket.close(countdown.dec()); + const socket2 = new dgram.Socket('udp4'); + socket2.bind(kPort, common.mustCall(() => { + process.nextTick(() => { + socket2.close(countdown.dec()); + }); + })); + }); + })); + } +} + +if (cluster.isMaster) + cluster.fork(__filename); +else + child(); diff --git a/test/js/node/test/parallel/test-cluster-child-index-net.js b/test/js/node/test/parallel/test-cluster-child-index-net.js new file mode 100644 index 0000000000..d8c3166d1b --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-child-index-net.js @@ -0,0 +1,31 @@ +'use strict'; +const common = require('../common'); +const Countdown = require('../common/countdown'); +const cluster = require('cluster'); +const net = require('net'); + +// Test an edge case when using `cluster` and `net.Server.listen()` to +// the port of `0`. +const kPort = 0; + +function child() { + const kTime = 2; + const countdown = new Countdown(kTime * 2, () => { + process.exit(0); + }); + for (let i = 0; i < kTime; i += 1) { + const server = net.createServer(); + server.listen(kPort, common.mustCall(() => { + server.close(countdown.dec()); + const server2 = net.createServer(); + server2.listen(kPort, common.mustCall(() => { + server2.close(countdown.dec()); + })); + })); + } +} + +if (cluster.isMaster) + cluster.fork(__filename); +else + child(); diff --git a/test/js/node/test/parallel/test-cluster-concurrent-disconnect.js b/test/js/node/test/parallel/test-cluster-concurrent-disconnect.js new file mode 100644 index 0000000000..b754fa221a --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-concurrent-disconnect.js @@ -0,0 +1,52 @@ +'use strict'; + +// Ref: https://github.com/nodejs/node/issues/32106 + +const common = require('../common'); + +const assert = require('assert'); +const cluster = require('cluster'); +const os = require('os'); + +if (cluster.isPrimary) { + const workers = []; + const numCPUs = os.availableParallelism(); + let waitOnline = numCPUs; + for (let i = 0; i < numCPUs; i++) { + const worker = cluster.fork(); + workers[i] = worker; + worker.once('online', common.mustCall(() => { + if (--waitOnline === 0) + for (const worker of workers) + if (worker.isConnected()) + worker.send(i % 2 ? 'disconnect' : 'destroy'); + })); + + // These errors can occur due to the nature of the test, we might be trying + // to send messages when the worker is disconnecting. + worker.on('error', (err) => { + assert.strictEqual(err.syscall, 'write'); + if (common.isMacOS) { + assert(['EPIPE', 'ENOTCONN'].includes(err.code), err); + } else { + assert(['EPIPE', 'ECONNRESET'].includes(err.code), err); + } + }); + + worker.once('disconnect', common.mustCall(() => { + for (const worker of workers) + if (worker.isConnected()) + worker.send('disconnect'); + })); + + worker.once('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); + } +} else { + process.on('message', (msg) => { + if (cluster.worker.isConnected()) + cluster.worker[msg](); + }); +} diff --git a/test/js/node/test/parallel/test-cluster-cwd.js b/test/js/node/test/parallel/test-cluster-cwd.js new file mode 100644 index 0000000000..c5d47e7301 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-cwd.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const tmpdir = require('../common/tmpdir'); + +if (cluster.isPrimary) { + tmpdir.refresh(); + + assert.strictEqual(cluster.settings.cwd, undefined); + cluster.fork().on('message', common.mustCall((msg) => { + assert.strictEqual(msg, process.cwd()); + })); + + cluster.setupPrimary({ cwd: tmpdir.path }); + assert.strictEqual(cluster.settings.cwd, tmpdir.path); + cluster.fork().on('message', common.mustCall((msg) => { + assert.strictEqual(msg, tmpdir.path); + })); +} else { + process.send(process.cwd()); + process.disconnect(); +} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-before-exit.js b/test/js/node/test/parallel/test-cluster-disconnect-before-exit.js similarity index 87% rename from test/js/node/cluster/upstream/parallel/test-cluster-disconnect-before-exit.js rename to test/js/node/test/parallel/test-cluster-disconnect-before-exit.js index bb5dda7aa7..f95f1384d5 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-before-exit.js +++ b/test/js/node/test/parallel/test-cluster-disconnect-before-exit.js @@ -19,12 +19,12 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; -const common = require("../common"); -const cluster = require("cluster"); +'use strict'; +const common = require('../common'); +const cluster = require('cluster'); if (cluster.isPrimary) { - const worker = cluster.fork().on("online", common.mustCall(disconnect)); + const worker = cluster.fork().on('online', common.mustCall(disconnect)); function disconnect() { worker.disconnect(); @@ -32,6 +32,6 @@ if (cluster.isPrimary) { // Disconnect is supposed to disconnect all workers, but not workers that // are already disconnected, since calling disconnect() on an already // disconnected worker would error. - worker.on("disconnect", common.mustCall(cluster.disconnect)); + worker.on('disconnect', common.mustCall(cluster.disconnect)); } } diff --git a/test/js/node/test/parallel/test-cluster-disconnect-exitedAfterDisconnect-race.js b/test/js/node/test/parallel/test-cluster-disconnect-exitedAfterDisconnect-race.js new file mode 100644 index 0000000000..f1a8dea0a9 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-disconnect-exitedAfterDisconnect-race.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); + +// Test should fail in Node.js 5.4.1 and pass in later versions. + +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isPrimary) { + cluster.on('exit', (worker, code) => { + assert.strictEqual(code, 0, `worker exited with code: ${code}, expected 0`); + }); + + return cluster.fork(); +} + +let eventFired = false; + +cluster.worker.disconnect(); + +process.nextTick(common.mustCall(() => { + assert.ok(!eventFired, 'disconnect event should wait for ack'); +})); + +cluster.worker.on('disconnect', common.mustCall(() => { + eventFired = true; +})); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-idle-worker.js b/test/js/node/test/parallel/test-cluster-disconnect-idle-worker.js similarity index 84% rename from test/js/node/cluster/upstream/parallel/test-cluster-disconnect-idle-worker.js rename to test/js/node/test/parallel/test-cluster-disconnect-idle-worker.js index f20bacdede..566f631312 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-idle-worker.js +++ b/test/js/node/test/parallel/test-cluster-disconnect-idle-worker.js @@ -19,18 +19,16 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); const fork = cluster.fork; if (cluster.isPrimary) { fork(); // It is intentionally called `fork` instead of fork(); // `cluster.fork` to test that `this` is not used - cluster.disconnect( - common.mustCall(() => { - assert.deepStrictEqual(Object.keys(cluster.workers), []); - }), - ); + cluster.disconnect(common.mustCall(() => { + assert.deepStrictEqual(Object.keys(cluster.workers), []); + })); } diff --git a/test/js/node/test/parallel/test-cluster-disconnect-leak.js b/test/js/node/test/parallel/test-cluster-disconnect-leak.js new file mode 100644 index 0000000000..e2a417e5cd --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-disconnect-leak.js @@ -0,0 +1,27 @@ +'use strict'; + +// Test fails in Node v5.4.0 and passes in v5.4.1 and newer. + +const common = require('../common'); +const net = require('net'); +const cluster = require('cluster'); + +cluster.schedulingPolicy = cluster.SCHED_NONE; + +if (cluster.isPrimary) { + const worker = cluster.fork(); + + // This is the important part of the test: Confirm that `disconnect` fires. + worker.on('disconnect', common.mustCall()); + + // These are just some extra stuff we're checking for good measure... + worker.on('exit', common.mustCall()); + cluster.on('exit', common.mustCall()); + + cluster.disconnect(); + return; +} + +const server = net.createServer(); + +server.listen(0); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-with-no-workers.js b/test/js/node/test/parallel/test-cluster-disconnect-with-no-workers.js similarity index 88% rename from test/js/node/cluster/upstream/parallel/test-cluster-disconnect-with-no-workers.js rename to test/js/node/test/parallel/test-cluster-disconnect-with-no-workers.js index 865dd25f3f..a34e04973c 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-disconnect-with-no-workers.js +++ b/test/js/node/test/parallel/test-cluster-disconnect-with-no-workers.js @@ -19,18 +19,18 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; -require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); +'use strict'; +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); let disconnected; -process.on("exit", function () { +process.on('exit', function() { assert(disconnected); }); -cluster.disconnect(function () { +cluster.disconnect(function() { disconnected = true; }); diff --git a/test/js/node/test/parallel/test-cluster-fork-env.js b/test/js/node/test/parallel/test-cluster-fork-env.js new file mode 100644 index 0000000000..90b456b196 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-fork-env.js @@ -0,0 +1,70 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test checks that arguments provided to cluster.fork() will create +// new environment variables and override existing environment variables +// in the created worker process. + +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isWorker) { + const result = cluster.worker.send({ + prop: process.env.cluster_test_prop, + overwrite: process.env.cluster_test_overwrite + }); + + assert.strictEqual(result, true); +} else if (cluster.isPrimary) { + + const checks = { + using: false, + overwrite: false + }; + + // To check that the cluster extend on the process.env we will overwrite a + // property + process.env.cluster_test_overwrite = 'old'; + + // Fork worker + const worker = cluster.fork({ + 'cluster_test_prop': 'custom', + 'cluster_test_overwrite': 'new' + }); + + // Checks worker env + worker.on('message', function(data) { + checks.using = (data.prop === 'custom'); + checks.overwrite = (data.overwrite === 'new'); + process.exit(0); + }); + + process.once('exit', function() { + assert.ok(checks.using, 'The worker did not receive the correct env.'); + assert.ok( + checks.overwrite, + 'The custom environment did not overwrite the existing environment.'); + }); + +} diff --git a/test/js/node/test/parallel/test-cluster-fork-windowsHide.js b/test/js/node/test/parallel/test-cluster-fork-windowsHide.js new file mode 100644 index 0000000000..2b90713ceb --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-fork-windowsHide.js @@ -0,0 +1,74 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const child_process = require('child_process'); +const cluster = require('cluster'); + +if (!process.argv[2]) { + // It seems Windows only allocate new console window for + // attaching processes spawned by detached processes. i.e. + // - If process D is spawned by process C with `detached: true`, + // and process W is spawned by process D with `detached: false`, + // W will get a new black console window popped up. + // - If D is spawned by C with `detached: false` or W is spawned + // by D with `detached: true`, no console window will pop up for W. + // + // So, we have to spawn a detached process first to run the actual test. + const primary = child_process.spawn( + process.argv[0], + [process.argv[1], '--cluster'], + { detached: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }); + + const messageHandlers = { + workerOnline: common.mustCall(), + mainWindowHandle: common.mustCall((msg) => { + assert.match(msg.value, /0\s*/); + }), + workerExit: common.mustCall((msg) => { + assert.strictEqual(msg.code, 0); + assert.strictEqual(msg.signal, null); + }) + }; + + primary.on('message', (msg) => { + const handler = messageHandlers[msg.type]; + assert.ok(handler); + handler(msg); + }); + + primary.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); + +} else if (cluster.isPrimary) { + cluster.setupPrimary({ + silent: true, + windowsHide: true + }); + + const worker = cluster.fork(); + worker.on('exit', (code, signal) => { + process.send({ type: 'workerExit', code: code, signal: signal }); + }); + + worker.on('online', (msg) => { + process.send({ type: 'workerOnline' }); + + let output = '0'; + if (process.platform === 'win32') { + output = child_process.execSync( + 'powershell -NoProfile -c ' + + `"(Get-Process -Id ${worker.process.pid}).MainWindowHandle"`, + { windowsHide: true, encoding: 'utf8' }); + } + + process.send({ type: 'mainWindowHandle', value: output }); + worker.send('shutdown'); + }); + +} else { + cluster.worker.on('message', (msg) => { + cluster.worker.disconnect(); + }); +} diff --git a/test/js/node/test/parallel/test-cluster-http-pipe.js b/test/js/node/test/parallel/test-cluster-http-pipe.js new file mode 100644 index 0000000000..bdd8fe8c4f --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-http-pipe.js @@ -0,0 +1,59 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +if (common.isWindows) { + common.skip( + 'It is not possible to send pipe handles over the IPC pipe on Windows'); +} + +const assert = require('assert'); +const cluster = require('cluster'); +const http = require('http'); + +if (cluster.isPrimary) { + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + const worker = cluster.fork(); + worker.on('message', common.mustCall((msg) => { + assert.strictEqual(msg, 'DONE'); + })); + worker.on('exit', common.mustCall()); + return; +} + +http.createServer(common.mustCall((req, res) => { + assert.strictEqual(req.connection.remoteAddress, undefined); + assert.strictEqual(req.connection.localAddress, undefined); + + res.writeHead(200); + res.end('OK'); +})).listen(common.PIPE, common.mustCall(() => { + http.get({ socketPath: common.PIPE, path: '/' }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustSucceed(() => { + process.send('DONE'); + process.exit(); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-cluster-invalid-message.js b/test/js/node/test/parallel/test-cluster-invalid-message.js new file mode 100644 index 0000000000..a42f5284db --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-invalid-message.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isPrimary) { + const worker = cluster.fork(); + + worker.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); + + worker.on('online', () => { + worker.send({ + cmd: 'NODE_CLUSTER', + ack: -1 + }, () => { + worker.disconnect(); + }); + }); +} diff --git a/test/js/node/test/parallel/test-cluster-ipc-throw.js b/test/js/node/test/parallel/test-cluster-ipc-throw.js new file mode 100644 index 0000000000..c9640a23fc --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-ipc-throw.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const cluster = require('cluster'); +const assert = require('assert'); + +cluster.schedulingPolicy = cluster.SCHED_RR; + +const server = http.createServer(); + +if (cluster.isPrimary) { + server.listen({ port: 0 }, common.mustCall(() => { + const worker = cluster.fork({ PORT: server.address().port }); + worker.on('exit', common.mustCall(() => { + server.close(); + })); + })); +} else { + assert(process.env.PORT); + process.on('uncaughtException', common.mustCall()); + server.listen(process.env.PORT); + server.on('error', common.mustCall((e) => { + cluster.worker.disconnect(); + throw e; + })); +} diff --git a/test/js/node/test/parallel/test-cluster-kill-disconnect.js b/test/js/node/test/parallel/test-cluster-kill-disconnect.js new file mode 100644 index 0000000000..3e1f2f0841 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-kill-disconnect.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); + +// Check that cluster works perfectly for both `kill` and `disconnect` cases. +// Also take into account that the `disconnect` event may be received after the +// `exit` event. +// https://github.com/nodejs/node/issues/3238 + +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isPrimary) { + function forkWorker(action) { + const worker = cluster.fork({ action }); + worker.on('disconnect', common.mustCall(() => { + assert.strictEqual(worker.exitedAfterDisconnect, true); + })); + + worker.on('exit', common.mustCall(() => { + assert.strictEqual(worker.exitedAfterDisconnect, true); + })); + } + + forkWorker('disconnect'); + forkWorker('kill'); +} else { + cluster.worker[process.env.action](); +} diff --git a/test/js/node/test/parallel/test-cluster-kill-infinite-loop.js b/test/js/node/test/parallel/test-cluster-kill-infinite-loop.js new file mode 100644 index 0000000000..57781b6972 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-kill-infinite-loop.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const cluster = require('cluster'); +const assert = require('assert'); + +if (cluster.isPrimary) { + const worker = cluster.fork(); + + worker.on('online', common.mustCall(() => { + // Use worker.process.kill() instead of worker.kill() because the latter + // waits for a graceful disconnect, which will never happen. + worker.process.kill(); + })); + + worker.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, null); + assert.strictEqual(signal, 'SIGTERM'); + })); +} else { + while (true); +} diff --git a/test/js/node/test/parallel/test-cluster-listening-port.js b/test/js/node/test/parallel/test-cluster-listening-port.js new file mode 100644 index 0000000000..c09134e1de --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-listening-port.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); + +if (cluster.isPrimary) { + cluster.fork(); + cluster.on('listening', common.mustCall(function(worker, address) { + const port = address.port; + // Ensure that the port is not 0 or null + assert(port); + // Ensure that the port is numerical + assert.strictEqual(typeof port, 'number'); + worker.kill(); + })); +} else { + net.createServer(common.mustNotCall()).listen(0); +} diff --git a/test/js/node/test/parallel/test-cluster-net-listen.js b/test/js/node/test/parallel/test-cluster-net-listen.js new file mode 100644 index 0000000000..9fa975aaaf --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-net-listen.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); + +if (cluster.isPrimary) { + // Ensure that the worker exits peacefully + cluster.fork().on('exit', common.mustCall(function(statusCode) { + assert.strictEqual(statusCode, 0); + })); +} else { + // listen() without port should not trigger a libuv assert + net.createServer(common.mustNotCall()).listen(process.exit); +} diff --git a/test/js/node/test/parallel/test-cluster-primary-error.js b/test/js/node/test/parallel/test-cluster-primary-error.js new file mode 100644 index 0000000000..f48682da4e --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-primary-error.js @@ -0,0 +1,104 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +const totalWorkers = 2; + +// Cluster setup +if (cluster.isWorker) { + const http = require('http'); + http.Server(() => {}).listen(0, '127.0.0.1'); +} else if (process.argv[2] === 'cluster') { + // Send PID to testcase process + let forkNum = 0; + cluster.on('fork', common.mustCall(function forkEvent(worker) { + // Send PID + process.send({ + cmd: 'worker', + workerPID: worker.process.pid + }); + + // Stop listening when done + if (++forkNum === totalWorkers) { + cluster.removeListener('fork', forkEvent); + } + }, totalWorkers)); + + // Throw accidental error when all workers are listening + let listeningNum = 0; + cluster.on('listening', common.mustCall(function listeningEvent() { + // When all workers are listening + if (++listeningNum === totalWorkers) { + // Stop listening + cluster.removeListener('listening', listeningEvent); + + // Throw accidental error + process.nextTick(() => { + throw new Error('accidental error'); + }); + } + }, totalWorkers)); + + // Startup a basic cluster + cluster.fork(); + cluster.fork(); +} else { + // This is the testcase + + const fork = require('child_process').fork; + + // List all workers + const workers = []; + + // Spawn a cluster process + const primary = fork(process.argv[1], ['cluster'], { silent: true }); + + // Handle messages from the cluster + primary.on('message', common.mustCall((data) => { + // Add worker pid to list and progress tracker + if (data.cmd === 'worker') { + workers.push(data.workerPID); + } + }, totalWorkers)); + + // When cluster is dead + primary.on('exit', common.mustCall((code) => { + // Check that the cluster died accidentally (non-zero exit code) + assert.strictEqual(code, 1); + + // XXX(addaleax): The fact that this uses raw PIDs makes the test inherently + // flaky – another process might end up being started right after the + // workers finished and receive the same PID. + const pollWorkers = () => { + // When primary is dead all workers should be dead too + if (workers.some((pid) => common.isAlive(pid))) { + setTimeout(pollWorkers, 50); + } + }; + + // Loop indefinitely until worker exit + pollWorkers(); + })); +} diff --git a/test/js/node/test/parallel/test-cluster-primary-kill.js b/test/js/node/test/parallel/test-cluster-primary-kill.js new file mode 100644 index 0000000000..08c7809603 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-primary-kill.js @@ -0,0 +1,89 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isWorker) { + + // Keep the worker alive + const http = require('http'); + http.Server().listen(0, '127.0.0.1'); + +} else if (process.argv[2] === 'cluster') { + + const worker = cluster.fork(); + + // send PID info to testcase process + process.send({ + pid: worker.process.pid + }); + + // Terminate the cluster process + worker.once('listening', common.mustCall(() => { + setTimeout(() => { + process.exit(0); + }, 1000); + })); + +} else { + + // This is the testcase + const fork = require('child_process').fork; + + // Spawn a cluster process + const primary = fork(process.argv[1], ['cluster']); + + // get pid info + let pid = null; + primary.once('message', (data) => { + pid = data.pid; + }); + + // When primary is dead + let alive = true; + primary.on('exit', common.mustCall((code) => { + + // Make sure that the primary died on purpose + assert.strictEqual(code, 0); + + // Check worker process status + const pollWorker = () => { + alive = common.isAlive(pid); + if (alive) { + setTimeout(pollWorker, 50); + } + }; + // Loop indefinitely until worker exit. + pollWorker(); + })); + + process.once('exit', () => { + assert.strictEqual(typeof pid, 'number', + `got ${pid} instead of a worker pid`); + assert.strictEqual(alive, false, + `worker was alive after primary died (alive = ${alive})` + ); + }); + +} diff --git a/test/js/node/test/parallel/test-cluster-process-disconnect.js b/test/js/node/test/parallel/test-cluster-process-disconnect.js new file mode 100644 index 0000000000..378c4ef24d --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-process-disconnect.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isPrimary) { + const worker = cluster.fork(); + worker.on('exit', common.mustCall((code, signal) => { + assert.strictEqual( + code, + 0, + `Worker did not exit normally with code: ${code}` + ); + assert.strictEqual( + signal, + null, + `Worker did not exit normally with signal: ${signal}` + ); + })); +} else { + const net = require('net'); + const server = net.createServer(); + server.listen(0, common.mustCall(() => { + process.disconnect(); + })); +} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-rr-domain-listen.js b/test/js/node/test/parallel/test-cluster-rr-domain-listen.js similarity index 85% rename from test/js/node/cluster/upstream/parallel/test-cluster-rr-domain-listen.js rename to test/js/node/test/parallel/test-cluster-rr-domain-listen.js index c48ed0c55c..6043535d3b 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-rr-domain-listen.js +++ b/test/js/node/test/parallel/test-cluster-rr-domain-listen.js @@ -19,10 +19,10 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; -require("../common"); -const cluster = require("cluster"); -const domain = require("domain"); +'use strict'; +require('../common'); +const cluster = require('cluster'); +const domain = require('domain'); // RR is the default for v0.11.9+ so the following line is redundant: // cluster.schedulingPolicy = cluster.SCHED_RR; @@ -31,16 +31,18 @@ if (cluster.isWorker) { const d = domain.create(); d.run(() => {}); - const http = require("http"); - http.Server(() => {}).listen(0, "127.0.0.1"); + const http = require('http'); + http.Server(() => {}).listen(0, '127.0.0.1'); + } else if (cluster.isPrimary) { + // Kill worker when listening - cluster.on("listening", function () { + cluster.on('listening', function() { worker.kill(); }); // Kill process when worker is killed - cluster.on("exit", function () { + cluster.on('exit', function() { process.exit(0); }); diff --git a/test/js/node/test/parallel/test-cluster-rr-handle-keep-loop-alive.js b/test/js/node/test/parallel/test-cluster-rr-handle-keep-loop-alive.js new file mode 100644 index 0000000000..0b18408a19 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-rr-handle-keep-loop-alive.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const cluster = require('cluster'); +const net = require('net'); +const assert = require('assert'); + +cluster.schedulingPolicy = cluster.SCHED_RR; + +if (cluster.isPrimary) { + let exited = false; + const worker = cluster.fork(); + worker.on('exit', () => { + exited = true; + }); + setTimeout(() => { + assert.ok(!exited); + worker.kill(); + }, 3000); +} else { + const server = net.createServer(common.mustNotCall()); + server.listen(0, common.mustCall(() => process.channel.unref())); +} diff --git a/test/js/node/test/parallel/test-cluster-rr-ref.js b/test/js/node/test/parallel/test-cluster-rr-ref.js new file mode 100644 index 0000000000..92bb673ccd --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-rr-ref.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const cluster = require('cluster'); +const net = require('net'); + +if (cluster.isPrimary) { + cluster.fork().on('message', function(msg) { + if (msg === 'done') this.kill(); + }); +} else { + const server = net.createServer(common.mustNotCall()); + server.listen(0, function() { + server.unref(); + server.ref(); + server.close(function() { + process.send('done'); + }); + }); +} diff --git a/test/js/node/test/parallel/test-cluster-send-deadlock.js b/test/js/node/test/parallel/test-cluster-send-deadlock.js new file mode 100644 index 0000000000..8ddc40c252 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-send-deadlock.js @@ -0,0 +1,73 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Testing mutual send of handles: from primary to worker, and from worker to +// primary. + +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); + +if (cluster.isPrimary) { + const worker = cluster.fork(); + worker.on('exit', (code, signal) => { + assert.strictEqual(code, 0, `Worker exited with an error code: ${code}`); + assert(!signal, `Worker exited by a signal: ${signal}`); + server.close(); + }); + + const server = net.createServer((socket) => { + worker.send('handle', socket); + }); + + server.listen(0, () => { + worker.send({ message: 'listen', port: server.address().port }); + }); +} else { + process.on('message', (msg, handle) => { + if (msg.message && msg.message === 'listen') { + assert(msg.port); + const client1 = net.connect({ + host: 'localhost', + port: msg.port + }, () => { + const client2 = net.connect({ + host: 'localhost', + port: msg.port + }, () => { + client1.on('close', onclose); + client2.on('close', onclose); + client1.end(); + client2.end(); + }); + }); + let waiting = 2; + const onclose = () => { + if (--waiting === 0) + cluster.worker.disconnect(); + }; + } else { + process.send('reply', handle); + } + }); +} diff --git a/test/js/node/test/parallel/test-cluster-setup-primary-argv.js b/test/js/node/test/parallel/test-cluster-setup-primary-argv.js new file mode 100644 index 0000000000..4c465bb72a --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-setup-primary-argv.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +setTimeout(common.mustNotCall('setup not emitted'), 1000).unref(); + +cluster.on('setup', common.mustCall(function() { + const clusterArgs = cluster.settings.args; + const realArgs = process.argv; + assert.strictEqual(clusterArgs[clusterArgs.length - 1], + realArgs[realArgs.length - 1]); +})); + +assert.notStrictEqual(process.argv[process.argv.length - 1], 'OMG,OMG'); +process.argv.push('OMG,OMG'); +process.argv.push('OMG,OMG'); +cluster.setupPrimary(); diff --git a/test/js/node/test/parallel/test-cluster-setup-primary-cumulative.js b/test/js/node/test/parallel/test-cluster-setup-primary-cumulative.js new file mode 100644 index 0000000000..cf62291e9d --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-setup-primary-cumulative.js @@ -0,0 +1,62 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +assert(cluster.isPrimary); + +// cluster.settings should not be initialized until needed +assert.deepStrictEqual(cluster.settings, {}); + +cluster.setupPrimary(); +assert.deepStrictEqual(cluster.settings, { + args: process.argv.slice(2), + exec: process.argv[1], + execArgv: process.execArgv, + silent: false, +}); +console.log('ok sets defaults'); + +cluster.setupPrimary({ exec: 'overridden' }); +assert.strictEqual(cluster.settings.exec, 'overridden'); +console.log('ok overrides defaults'); + +cluster.setupPrimary({ args: ['foo', 'bar'] }); +assert.strictEqual(cluster.settings.exec, 'overridden'); +assert.deepStrictEqual(cluster.settings.args, ['foo', 'bar']); + +cluster.setupPrimary({ execArgv: ['baz', 'bang'] }); +assert.strictEqual(cluster.settings.exec, 'overridden'); +assert.deepStrictEqual(cluster.settings.args, ['foo', 'bar']); +assert.deepStrictEqual(cluster.settings.execArgv, ['baz', 'bang']); +console.log('ok preserves unchanged settings on repeated calls'); + +cluster.setupPrimary(); +assert.deepStrictEqual(cluster.settings, { + args: ['foo', 'bar'], + exec: 'overridden', + execArgv: ['baz', 'bang'], + silent: false, +}); +console.log('ok preserves current settings'); diff --git a/test/js/node/test/parallel/test-cluster-setup-primary-emit.js b/test/js/node/test/parallel/test-cluster-setup-primary-emit.js new file mode 100644 index 0000000000..08414d5b21 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-setup-primary-emit.js @@ -0,0 +1,47 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +assert(cluster.isPrimary); + +function emitAndCatch(next) { + cluster.once('setup', common.mustCall(function(settings) { + assert.strictEqual(settings.exec, 'new-exec'); + setImmediate(next); + })); + cluster.setupPrimary({ exec: 'new-exec' }); +} + +function emitAndCatch2(next) { + cluster.once('setup', common.mustCall(function(settings) { + assert('exec' in settings); + setImmediate(next); + })); + cluster.setupPrimary(); +} + +emitAndCatch(common.mustCall(function() { + emitAndCatch2(common.mustCall()); +})); diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-multiple.js b/test/js/node/test/parallel/test-cluster-setup-primary-multiple.js similarity index 83% rename from test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-multiple.js rename to test/js/node/test/parallel/test-cluster-setup-primary-multiple.js index 381642cf58..0fd8c0943c 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-setup-primary-multiple.js +++ b/test/js/node/test/parallel/test-cluster-setup-primary-multiple.js @@ -19,11 +19,11 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; -require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); -const debug = require("util").debuglog("test"); +'use strict'; +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const debug = require('util').debuglog('test'); assert(cluster.isPrimary); @@ -31,19 +31,23 @@ assert(cluster.isPrimary); // makes that unnecessary. This is to make the test less fragile if the // implementation ever changes such that cluster.settings is mutated instead of // replaced. -const cheapClone = obj => JSON.parse(JSON.stringify(obj)); +const cheapClone = (obj) => JSON.parse(JSON.stringify(obj)); const configs = []; // Capture changes -cluster.on("setup", () => { +cluster.on('setup', () => { debug(`"setup" emitted ${JSON.stringify(cluster.settings)}`); configs.push(cheapClone(cluster.settings)); }); -const execs = ["node-next", "node-next-2", "node-next-3"]; +const execs = [ + 'node-next', + 'node-next-2', + 'node-next-3', +]; -process.on("exit", () => { +process.on('exit', () => { // Tests that "setup" is emitted for every call to setupPrimary assert.strictEqual(configs.length, execs.length); @@ -61,9 +65,6 @@ execs.forEach((v, i) => { // Cluster emits 'setup' asynchronously, so we must stay alive long // enough for that to happen -setTimeout( - () => { - debug("cluster setup complete"); - }, - (execs.length + 1) * 100, -); +setTimeout(() => { + debug('cluster setup complete'); +}, (execs.length + 1) * 100); diff --git a/test/js/node/test/parallel/test-cluster-setup-primary.js b/test/js/node/test/parallel/test-cluster-setup-primary.js new file mode 100644 index 0000000000..efba017fd7 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-setup-primary.js @@ -0,0 +1,93 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isWorker) { + + // Just keep the worker alive + process.send(process.argv[2]); + +} else if (cluster.isPrimary) { + + const checks = { + args: false, + setupEvent: false, + settingsObject: false + }; + + const totalWorkers = 2; + let settings; + + // Setup primary + cluster.setupPrimary({ + args: ['custom argument'], + silent: true + }); + + cluster.once('setup', function() { + checks.setupEvent = true; + + settings = cluster.settings; + if (settings && + settings.args && settings.args[0] === 'custom argument' && + settings.silent === true && + settings.exec === process.argv[1]) { + checks.settingsObject = true; + } + }); + + let correctInput = 0; + + cluster.on('online', common.mustCall(function listener(worker) { + + worker.once('message', function(data) { + correctInput += (data === 'custom argument' ? 1 : 0); + if (correctInput === totalWorkers) { + checks.args = true; + } + worker.kill(); + }); + + }, totalWorkers)); + + // Start all workers + cluster.fork(); + cluster.fork(); + + // Check all values + process.once('exit', function() { + const argsMsg = 'Arguments was not send for one or more worker. ' + + `${correctInput} workers receive argument, ` + + `but ${totalWorkers} were expected.`; + assert.ok(checks.args, argsMsg); + + assert.ok(checks.setupEvent, 'The setup event was never emitted'); + + const settingObjectMsg = 'The settingsObject do not have correct ' + + `properties : ${JSON.stringify(settings)}`; + assert.ok(checks.settingsObject, settingObjectMsg); + }); + +} diff --git a/test/js/node/test/parallel/test-cluster-shared-handle-bind-privileged-port.js b/test/js/node/test/parallel/test-cluster-shared-handle-bind-privileged-port.js new file mode 100644 index 0000000000..edc522fd2d --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-shared-handle-bind-privileged-port.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isLinux) return; // TODO: BUN + +// Skip on macOS Mojave. https://github.com/nodejs/node/issues/21679 +if (common.isMacOS) + common.skip('macOS may allow ordinary processes to use any port'); + +if (common.isIBMi) + common.skip('IBMi may allow ordinary processes to use any port'); + +if (common.isWindows) + common.skip('not reliable on Windows'); + +if (process.getuid() === 0) + common.skip('as this test should not be run as `root`'); + +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); + +if (cluster.isPrimary) { + // Primary opens and binds the socket and shares it with the worker. + cluster.schedulingPolicy = cluster.SCHED_NONE; + cluster.fork().on('exit', common.mustCall(function(exitCode) { + assert.strictEqual(exitCode, 0); + })); +} else { + const s = net.createServer(common.mustNotCall()); + s.listen(42, common.mustNotCall('listen should have failed')); + s.on('error', common.mustCall(function(err) { + assert.strictEqual(err.code, 'EACCES'); + process.disconnect(); + })); +} diff --git a/test/js/node/test/parallel/test-cluster-uncaught-exception.js b/test/js/node/test/parallel/test-cluster-uncaught-exception.js new file mode 100644 index 0000000000..80d1ec6118 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-uncaught-exception.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Installing a custom uncaughtException handler should override the default +// one that the cluster module installs. +// https://github.com/joyent/node/issues/2556 + +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const fork = require('child_process').fork; + +const MAGIC_EXIT_CODE = 42; + +const isTestRunner = process.argv[2] !== 'child'; + +if (isTestRunner) { + const primary = fork(__filename, ['child']); + primary.on('exit', common.mustCall((code) => { + assert.strictEqual(code, MAGIC_EXIT_CODE); + })); +} else if (cluster.isPrimary) { + process.on('uncaughtException', common.mustCall(() => { + process.nextTick(() => process.exit(MAGIC_EXIT_CODE)); + })); + cluster.fork(); + throw new Error('kill primary'); +} else { // worker + process.exit(); +} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-constructor.js b/test/js/node/test/parallel/test-cluster-worker-constructor.js similarity index 88% rename from test/js/node/cluster/upstream/parallel/test-cluster-worker-constructor.js rename to test/js/node/test/parallel/test-cluster-worker-constructor.js index 904eb2e2ab..c116e622e5 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-constructor.js +++ b/test/js/node/test/parallel/test-cluster-worker-constructor.js @@ -19,28 +19,28 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; +'use strict'; // test-cluster-worker-constructor.js // validates correct behavior of the cluster.Worker constructor -require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); let worker; worker = new cluster.Worker(); assert.strictEqual(worker.exitedAfterDisconnect, undefined); -assert.strictEqual(worker.state, "none"); +assert.strictEqual(worker.state, 'none'); assert.strictEqual(worker.id, 0); assert.strictEqual(worker.process, undefined); worker = new cluster.Worker({ id: 3, - state: "online", - process: process, + state: 'online', + process: process }); assert.strictEqual(worker.exitedAfterDisconnect, undefined); -assert.strictEqual(worker.state, "online"); +assert.strictEqual(worker.state, 'online'); assert.strictEqual(worker.id, 3); assert.strictEqual(worker.process, process); diff --git a/test/js/node/test/parallel/test-cluster-worker-death.js b/test/js/node/test/parallel/test-cluster-worker-death.js new file mode 100644 index 0000000000..700cae7c52 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-death.js @@ -0,0 +1,38 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (!cluster.isPrimary) { + process.exit(42); +} else { + const worker = cluster.fork(); + worker.on('exit', common.mustCall(function(exitCode, signalCode) { + assert.strictEqual(exitCode, 42); + assert.strictEqual(signalCode, null); + })); + cluster.on('exit', common.mustCall(function(worker_) { + assert.strictEqual(worker_, worker); + })); +} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-destroy.js b/test/js/node/test/parallel/test-cluster-worker-destroy.js similarity index 85% rename from test/js/node/cluster/upstream/parallel/test-cluster-worker-destroy.js rename to test/js/node/test/parallel/test-cluster-worker-destroy.js index ebffb4fb04..91eee51a5a 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-destroy.js +++ b/test/js/node/test/parallel/test-cluster-worker-destroy.js @@ -19,7 +19,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; +'use strict'; // The goal of this test is to cover the Workers' implementation of // Worker.prototype.destroy. Worker.prototype.destroy is called within @@ -27,22 +27,22 @@ // primary, and another time when it's not connected to it, so that we cover // both code paths. -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); let worker1, worker2; if (cluster.isPrimary) { worker1 = cluster.fork(); worker2 = cluster.fork(); - [worker1, worker2].forEach(function (worker) { - worker.on("disconnect", common.mustCall()); - worker.on("exit", common.mustCall()); + [worker1, worker2].forEach(function(worker) { + worker.on('disconnect', common.mustCall()); + worker.on('exit', common.mustCall()); }); } else if (cluster.worker.id === 1) { // Call destroy when worker is disconnected - cluster.worker.process.on("disconnect", function () { + cluster.worker.process.on('disconnect', function() { cluster.worker.destroy(); }); diff --git a/test/js/node/test/parallel/test-cluster-worker-disconnect-on-error.js b/test/js/node/test/parallel/test-cluster-worker-disconnect-on-error.js new file mode 100644 index 0000000000..122d31a62e --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-disconnect-on-error.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const cluster = require('cluster'); +const assert = require('assert'); + +cluster.schedulingPolicy = cluster.SCHED_NONE; + +const server = http.createServer(); +if (cluster.isPrimary) { + let worker; + + server.listen(0, common.mustSucceed(() => { + assert(worker); + + worker.send({ port: server.address().port }); + })); + + worker = cluster.fork(); + worker.on('exit', common.mustCall(() => { + server.close(); + })); +} else { + process.on('message', common.mustCall((msg) => { + assert(msg.port); + + server.listen(msg.port); + server.on('error', common.mustCall((e) => { + cluster.worker.disconnect(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-cluster-worker-disconnect.js b/test/js/node/test/parallel/test-cluster-worker-disconnect.js new file mode 100644 index 0000000000..b28c0fbd8f --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-disconnect.js @@ -0,0 +1,106 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isWorker) { + const http = require('http'); + http.Server(() => { + + }).listen(0, '127.0.0.1'); + + cluster.worker.on('disconnect', common.mustCall(() => { + process.exit(42); + })); + +} else if (cluster.isPrimary) { + + const checks = { + cluster: { + emitDisconnect: false, + emitExit: false, + callback: false + }, + worker: { + emitDisconnect: false, + emitDisconnectInsideWorker: false, + emitExit: false, + state: false, + voluntaryMode: false, + died: false + } + }; + + // start worker + const worker = cluster.fork(); + + // Disconnect worker when it is ready + worker.once('listening', common.mustCall(() => { + const w = worker.disconnect(); + assert.strictEqual(worker, w, `${worker.id} did not return a reference`); + })); + + // Check cluster events + cluster.once('disconnect', common.mustCall(() => { + checks.cluster.emitDisconnect = true; + })); + cluster.once('exit', common.mustCall(() => { + checks.cluster.emitExit = true; + })); + + // Check worker events and properties + worker.once('disconnect', common.mustCall(() => { + checks.worker.emitDisconnect = true; + checks.worker.voluntaryMode = worker.exitedAfterDisconnect; + checks.worker.state = worker.state; + })); + + // Check that the worker died + worker.once('exit', common.mustCall((code) => { + checks.worker.emitExit = true; + checks.worker.died = !common.isAlive(worker.process.pid); + checks.worker.emitDisconnectInsideWorker = code === 42; + })); + + process.once('exit', () => { + + const w = checks.worker; + const c = checks.cluster; + + // events + assert.ok(w.emitDisconnect, 'Disconnect event did not emit'); + assert.ok(w.emitDisconnectInsideWorker, + 'Disconnect event did not emit inside worker'); + assert.ok(c.emitDisconnect, 'Disconnect event did not emit'); + assert.ok(w.emitExit, 'Exit event did not emit'); + assert.ok(c.emitExit, 'Exit event did not emit'); + + // flags + assert.strictEqual(w.state, 'disconnected'); + assert.strictEqual(w.voluntaryMode, true); + + // is process alive + assert.ok(w.died, 'The worker did not die'); + }); +} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-events.js b/test/js/node/test/parallel/test-cluster-worker-events.js similarity index 82% rename from test/js/node/cluster/upstream/parallel/test-cluster-worker-events.js rename to test/js/node/test/parallel/test-cluster-worker-events.js index aaf355a581..6c044ace8d 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-events.js +++ b/test/js/node/test/parallel/test-cluster-worker-events.js @@ -19,22 +19,23 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; -require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); +'use strict'; +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); const OK = 2; if (cluster.isPrimary) { + const worker = cluster.fork(); - worker.on("exit", code => { + worker.on('exit', (code) => { assert.strictEqual(code, OK); process.exit(0); }); - const result = worker.send("SOME MESSAGE"); + const result = worker.send('SOME MESSAGE'); assert.strictEqual(result, true); return; @@ -50,28 +51,28 @@ let sawWorker; const messages = []; -const check = m => { +const check = (m) => { messages.push(m); if (messages.length < 2) return; assert.deepStrictEqual(messages[0], messages[1]); - cluster.worker.once("error", e => { - assert.strictEqual(e, "HI"); + cluster.worker.once('error', (e) => { + assert.strictEqual(e, 'HI'); process.exit(OK); }); - process.emit("error", "HI"); + process.emit('error', 'HI'); }; -process.on("message", m => { +process.on('message', (m) => { assert(!sawProcess); sawProcess = true; check(m); }); -cluster.worker.on("message", m => { +cluster.worker.on('message', (m) => { assert(!sawWorker); sawWorker = true; check(m); diff --git a/test/js/node/test/parallel/test-cluster-worker-exit.js b/test/js/node/test/parallel/test-cluster-worker-exit.js new file mode 100644 index 0000000000..09e2a83701 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-exit.js @@ -0,0 +1,130 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// test-cluster-worker-exit.js +// verifies that, when a child process exits (by calling `process.exit(code)`) +// - the primary receives the proper events in the proper order, no duplicates +// - the exitCode and signalCode are correct in the 'exit' event +// - the worker.exitedAfterDisconnect flag, and worker.state are correct +// - the worker process actually goes away + +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +const EXIT_CODE = 42; + +if (cluster.isWorker) { + const http = require('http'); + const server = http.Server(() => { }); + + server.once('listening', common.mustCall(() => { + process.exit(EXIT_CODE); + })); + server.listen(0, '127.0.0.1'); + +} else if (cluster.isPrimary) { + + const expected_results = { + cluster_emitDisconnect: [1, "the cluster did not emit 'disconnect'"], + cluster_emitExit: [1, "the cluster did not emit 'exit'"], + cluster_exitCode: [EXIT_CODE, 'the cluster exited w/ incorrect exitCode'], + cluster_signalCode: [null, 'the cluster exited w/ incorrect signalCode'], + worker_emitDisconnect: [1, "the worker did not emit 'disconnect'"], + worker_emitExit: [1, "the worker did not emit 'exit'"], + worker_state: ['disconnected', 'the worker state is incorrect'], + worker_exitedAfterDisconnect: [ + false, 'the .exitedAfterDisconnect flag is incorrect', + ], + worker_died: [true, 'the worker is still running'], + worker_exitCode: [EXIT_CODE, 'the worker exited w/ incorrect exitCode'], + worker_signalCode: [null, 'the worker exited w/ incorrect signalCode'] + }; + const results = { + cluster_emitDisconnect: 0, + cluster_emitExit: 0, + worker_emitDisconnect: 0, + worker_emitExit: 0 + }; + + + // start worker + const worker = cluster.fork(); + + // Check cluster events + cluster.on('disconnect', common.mustCall(() => { + results.cluster_emitDisconnect += 1; + })); + cluster.on('exit', common.mustCall((worker) => { + results.cluster_exitCode = worker.process.exitCode; + results.cluster_signalCode = worker.process.signalCode; + results.cluster_emitExit += 1; + })); + + // Check worker events and properties + worker.on('disconnect', common.mustCall(() => { + results.worker_emitDisconnect += 1; + results.worker_exitedAfterDisconnect = worker.exitedAfterDisconnect; + results.worker_state = worker.state; + if (results.worker_emitExit > 0) { + process.nextTick(() => finish_test()); + } + })); + + // Check that the worker died + worker.once('exit', common.mustCall((exitCode, signalCode) => { + results.worker_exitCode = exitCode; + results.worker_signalCode = signalCode; + results.worker_emitExit += 1; + results.worker_died = !common.isAlive(worker.process.pid); + if (results.worker_emitDisconnect > 0) { + process.nextTick(() => finish_test()); + } + })); + + const finish_test = () => { + try { + checkResults(expected_results, results); + } catch (exc) { + if (exc.name !== 'AssertionError') { + console.trace(exc); + } + + process.exit(1); + return; + } + process.exit(0); + }; +} + +// Some helper functions ... + +function checkResults(expected_results, results) { + for (const k in expected_results) { + const actual = results[k]; + const expected = expected_results[k]; + + assert.strictEqual( + actual, expected && expected.length ? expected[0] : expected, + `${expected[1] || ''} [expected: ${expected[0]} / actual: ${actual}]`); + } +} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-forced-exit.js b/test/js/node/test/parallel/test-cluster-worker-forced-exit.js similarity index 76% rename from test/js/node/cluster/upstream/parallel/test-cluster-worker-forced-exit.js rename to test/js/node/test/parallel/test-cluster-worker-forced-exit.js index 901868973d..6d2bf4f537 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-forced-exit.js +++ b/test/js/node/test/parallel/test-cluster-worker-forced-exit.js @@ -19,10 +19,10 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); const SENTINEL = 42; @@ -37,7 +37,7 @@ const SENTINEL = 42; // 3 disconnect worker with child_process's disconnect, confirm // no sentinel value if (cluster.isWorker) { - process.on("disconnect", msg => { + process.on('disconnect', (msg) => { setTimeout(() => process.exit(SENTINEL), 10); }); return; @@ -49,27 +49,15 @@ checkForced(); function checkUnforced() { const worker = cluster.fork(); worker - .on( - "online", - common.mustCall(() => worker.disconnect()), - ) - .on( - "exit", - common.mustCall(status => { - assert.strictEqual(status, SENTINEL); - }), - ); + .on('online', common.mustCall(() => worker.disconnect())) + .on('exit', common.mustCall((status) => { + assert.strictEqual(status, SENTINEL); + })); } function checkForced() { const worker = cluster.fork(); worker - .on( - "online", - common.mustCall(() => worker.process.disconnect()), - ) - .on( - "exit", - common.mustCall(status => assert.strictEqual(status, 0)), - ); + .on('online', common.mustCall(() => worker.process.disconnect())) + .on('exit', common.mustCall((status) => assert.strictEqual(status, 0))); } diff --git a/test/js/node/test/parallel/test-cluster-worker-handle-close.js b/test/js/node/test/parallel/test-cluster-worker-handle-close.js new file mode 100644 index 0000000000..47a80ef1cd --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-handle-close.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const cluster = require('cluster'); +const net = require('net'); + +if (cluster.isPrimary) { + cluster.schedulingPolicy = cluster.SCHED_RR; + cluster.fork(); +} else { + const server = net.createServer(common.mustNotCall()); + server.listen(0, common.mustCall(() => { + net.connect(server.address().port); + })); + process.prependListener('internalMessage', common.mustCallAtLeast((message, handle) => { + if (message.act !== 'newconn') { + return; + } + // Make the worker drops the connection, see `rr` and `onconnection` in child.js + server.close(); + const close = handle.close; + handle.close = common.mustCall(() => { + close.call(handle, common.mustCall(() => { + process.exit(); + })); + }); + })); +} diff --git a/test/js/node/cluster/upstream/parallel/test-cluster-worker-init.js b/test/js/node/test/parallel/test-cluster-worker-init.js similarity index 78% rename from test/js/node/cluster/upstream/parallel/test-cluster-worker-init.js rename to test/js/node/test/parallel/test-cluster-worker-init.js index f2dafdf66d..2acc55c148 100644 --- a/test/js/node/cluster/upstream/parallel/test-cluster-worker-init.js +++ b/test/js/node/test/parallel/test-cluster-worker-init.js @@ -19,34 +19,31 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -"use strict"; +'use strict'; // test-cluster-worker-init.js // verifies that, when a child process is forked, the cluster.worker // object can receive messages as expected -const common = require("../common"); -const assert = require("assert"); -const cluster = require("cluster"); -const msg = "foo"; +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const msg = 'foo'; if (cluster.isPrimary) { const worker = cluster.fork(); - worker.on( - "message", - common.mustCall(message => { - assert.strictEqual(message, true); - const w = worker.disconnect(); - assert.strictEqual(worker, w); - }), - ); + worker.on('message', common.mustCall((message) => { + assert.strictEqual(message, true); + const w = worker.disconnect(); + assert.strictEqual(worker, w); + })); - worker.on("online", () => { + worker.on('online', () => { worker.send(msg); }); } else { // https://github.com/nodejs/node-v0.x-archive/issues/7998 - cluster.worker.on("message", message => { + cluster.worker.on('message', (message) => { process.send(message === msg); }); } diff --git a/test/js/node/test/parallel/test-cluster-worker-isdead.js b/test/js/node/test/parallel/test-cluster-worker-isdead.js new file mode 100644 index 0000000000..6f2aa3c52e --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-isdead.js @@ -0,0 +1,32 @@ +'use strict'; +require('../common'); +const cluster = require('cluster'); +const assert = require('assert'); + +if (cluster.isPrimary) { + const worker = cluster.fork(); + let workerDead = worker.isDead(); + assert.ok(!workerDead, + `isDead() returned ${workerDead}. isDead() should return ` + + 'false right after the worker has been created.'); + + worker.on('exit', function() { + workerDead = worker.isDead(); + assert.ok(workerDead, + `isDead() returned ${workerDead}. After an event has been ` + + 'emitted, isDead should return true'); + }); + + worker.on('message', function(msg) { + if (msg === 'readyToDie') { + worker.kill(); + } + }); + +} else if (cluster.isWorker) { + const workerDead = cluster.worker.isDead(); + assert.ok(!workerDead, + `isDead() returned ${workerDead}. isDead() should return ` + + 'false when called from within a worker'); + process.send('readyToDie'); +} diff --git a/test/js/node/test/parallel/test-cluster-worker-kill-signal.js b/test/js/node/test/parallel/test-cluster-worker-kill-signal.js new file mode 100644 index 0000000000..53e3739eba --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-kill-signal.js @@ -0,0 +1,49 @@ +'use strict'; +// test-cluster-worker-kill-signal.js +// verifies that when we're killing a worker using Worker.prototype.kill +// and the worker's process was killed with the given signal (SIGKILL) + + +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isWorker) { + // Make the worker run something + const http = require('http'); + const server = http.Server(() => { }); + + server.once('listening', common.mustCall()); + server.listen(0, '127.0.0.1'); + +} else if (cluster.isMaster) { + const KILL_SIGNAL = 'SIGKILL'; + + // Start worker + const worker = cluster.fork(); + + // When the worker is up and running, kill it + worker.once('listening', common.mustCall(() => { + worker.kill(KILL_SIGNAL); + })); + + // Check worker events and properties + worker.on('disconnect', common.mustCall(() => { + assert.strictEqual(worker.exitedAfterDisconnect, false); + assert.strictEqual(worker.state, 'disconnected'); + }, 1)); + + // Check that the worker died + worker.once('exit', common.mustCall((exitCode, signalCode) => { + const isWorkerProcessStillAlive = common.isAlive(worker.process.pid); + const numOfRunningWorkers = Object.keys(cluster.workers).length; + + assert.strictEqual(exitCode, null); + assert.strictEqual(signalCode, KILL_SIGNAL); + assert.strictEqual(isWorkerProcessStillAlive, false); + assert.strictEqual(numOfRunningWorkers, 0); + }, 1)); + + // Check if the cluster was killed as well + cluster.on('exit', common.mustCall(1)); +} diff --git a/test/js/node/test/parallel/test-cluster-worker-kill.js b/test/js/node/test/parallel/test-cluster-worker-kill.js new file mode 100644 index 0000000000..7307a93e1b --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-kill.js @@ -0,0 +1,117 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// test-cluster-worker-kill.js +// verifies that, when a child process is killed (we use SIGKILL) +// - the primary receives the proper events in the proper order, no duplicates +// - the exitCode and signalCode are correct in the 'exit' event +// - the worker.exitedAfterDisconnect flag, and worker.state are correct +// - the worker process actually goes away + +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); + +if (cluster.isWorker) { + const http = require('http'); + const server = http.Server(() => { }); + + server.once('listening', common.mustCall()); + server.listen(0, '127.0.0.1'); + +} else if (cluster.isPrimary) { + + const KILL_SIGNAL = 'SIGKILL'; + const expected_results = { + cluster_emitDisconnect: [1, "the cluster did not emit 'disconnect'"], + cluster_emitExit: [1, "the cluster did not emit 'exit'"], + cluster_exitCode: [null, 'the cluster exited w/ incorrect exitCode'], + cluster_signalCode: [KILL_SIGNAL, + 'the cluster exited w/ incorrect signalCode'], + worker_emitDisconnect: [1, "the worker did not emit 'disconnect'"], + worker_emitExit: [1, "the worker did not emit 'exit'"], + worker_state: ['disconnected', 'the worker state is incorrect'], + worker_exitedAfter: [false, 'the .exitedAfterDisconnect flag is incorrect'], + worker_died: [true, 'the worker is still running'], + worker_exitCode: [null, 'the worker exited w/ incorrect exitCode'], + worker_signalCode: [KILL_SIGNAL, + 'the worker exited w/ incorrect signalCode'] + }; + const results = { + cluster_emitDisconnect: 0, + cluster_emitExit: 0, + worker_emitDisconnect: 0, + worker_emitExit: 0 + }; + + + // start worker + const worker = cluster.fork(); + + // When the worker is up and running, kill it + worker.once('listening', common.mustCall(() => { + worker.process.kill(KILL_SIGNAL); + })); + + + // Check cluster events + cluster.on('disconnect', common.mustCall(() => { + results.cluster_emitDisconnect += 1; + })); + cluster.on('exit', common.mustCall((worker) => { + results.cluster_exitCode = worker.process.exitCode; + results.cluster_signalCode = worker.process.signalCode; + results.cluster_emitExit += 1; + })); + + // Check worker events and properties + worker.on('disconnect', common.mustCall(() => { + results.worker_emitDisconnect += 1; + results.worker_exitedAfter = worker.exitedAfterDisconnect; + results.worker_state = worker.state; + })); + + // Check that the worker died + worker.once('exit', common.mustCall((exitCode, signalCode) => { + results.worker_exitCode = exitCode; + results.worker_signalCode = signalCode; + results.worker_emitExit += 1; + results.worker_died = !common.isAlive(worker.process.pid); + })); + + process.on('exit', () => { + checkResults(expected_results, results); + }); +} + +// Some helper functions ... + +function checkResults(expected_results, results) { + for (const k in expected_results) { + const actual = results[k]; + const expected = expected_results[k]; + + assert.strictEqual( + actual, expected && expected.length ? expected[0] : expected, + `${expected[1] || ''} [expected: ${expected[0]} / actual: ${actual}]`); + } +} diff --git a/test/js/node/test/parallel/test-cluster-worker-no-exit.js b/test/js/node/test/parallel/test-cluster-worker-no-exit.js new file mode 100644 index 0000000000..e4694a4a3a --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-no-exit.js @@ -0,0 +1,74 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); + +let destroyed; +let success; +let worker; +let server; + +// Workers do not exit on disconnect, they exit under normal node rules: when +// they have nothing keeping their loop alive, like an active connection +// +// test this by: +// +// 1 creating a server, so worker can make a connection to something +// 2 disconnecting worker +// 3 wait to confirm it did not exit +// 4 destroy connection +// 5 confirm it does exit +if (cluster.isPrimary) { + server = net.createServer(function(conn) { + server.close(); + worker.disconnect(); + worker.once('disconnect', function() { + setTimeout(function() { + conn.destroy(); + destroyed = true; + }, 1000); + }).once('exit', function() { + // Worker should not exit while it has a connection + assert(destroyed, 'worker exited before connection destroyed'); + success = true; + }); + + }).listen(0, function() { + const port = this.address().port; + + worker = cluster.fork() + .on('online', function() { + this.send({ port }); + }); + }); + process.on('exit', function() { + assert(success); + }); +} else { + process.on('message', function(msg) { + // We shouldn't exit, not while a network connection exists + net.connect(msg.port); + }); +} diff --git a/test/js/node/test/parallel/test-cluster-worker-wait-server-close.js b/test/js/node/test/parallel/test-cluster-worker-wait-server-close.js new file mode 100644 index 0000000000..71a8cacb52 --- /dev/null +++ b/test/js/node/test/parallel/test-cluster-worker-wait-server-close.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); + +let serverClosed = false; + +if (cluster.isWorker) { + const server = net.createServer(function(socket) { + // Wait for any data, then close connection + socket.write('.'); + socket.on('data', () => {}); + }).listen(0, common.localhostIPv4); + + server.once('close', function() { + serverClosed = true; + }); + + // Although not typical, the worker process can exit before the disconnect + // event fires. Use this to keep the process open until the event has fired. + const keepOpen = setInterval(() => {}, 9999); + + // Check worker events and properties + process.once('disconnect', function() { + // Disconnect should occur after socket close + assert(serverClosed); + clearInterval(keepOpen); + }); +} else if (cluster.isPrimary) { + // start worker + const worker = cluster.fork(); + + // Disconnect worker when it is ready + worker.once('listening', function(address) { + const socket = net.createConnection(address.port, common.localhostIPv4); + + socket.on('connect', function() { + socket.on('data', function() { + console.log('got data from client'); + // Socket definitely connected to worker if we got data + worker.disconnect(); + socket.end(); + }); + }); + }); +} diff --git a/test/js/node/test/parallel/test-common-countdown.js b/test/js/node/test/parallel/test-common-countdown.js new file mode 100644 index 0000000000..d3c0daf599 --- /dev/null +++ b/test/js/node/test/parallel/test-common-countdown.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const Countdown = require('../common/countdown'); +const fixtures = require('../common/fixtures'); +const { execFile } = require('child_process'); + +let done = ''; +const countdown = new Countdown(2, () => done = true); +assert.strictEqual(countdown.remaining, 2); +countdown.dec(); +assert.strictEqual(countdown.remaining, 1); +countdown.dec(); +assert.strictEqual(countdown.remaining, 0); +assert.strictEqual(done, true); + +const failFixtures = [ + [ + fixtures.path('failcounter.js'), + 'Mismatched function calls. Expected exactly 1, actual 0.', + ], +]; + +for (const p of failFixtures) { + const [file, expected] = p; + execFile(process.argv[0], [file], common.mustCall((ex, stdout, stderr) => { + assert.ok(ex); + assert.strictEqual(stderr, ''); + const firstLine = stdout.split('\n').shift(); + assert.strictEqual(firstLine, expected); + })); +} diff --git a/test/js/node/test/parallel/test-common-must-not-call.js b/test/js/node/test/parallel/test-common-must-not-call.js new file mode 100644 index 0000000000..b3c94a2390 --- /dev/null +++ b/test/js/node/test/parallel/test-common-must-not-call.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const util = require('util'); + +const message = 'message'; +const testFunction1 = common.mustNotCall(message); + +const testFunction2 = common.mustNotCall(message); + +const createValidate = (line, args = []) => common.mustCall((e) => { + const prefix = `${message} at `; + assert.ok(e.message.startsWith(prefix)); + if (process.platform === 'win32') { + e.message = e.message.substring(2); // remove 'C:' + } + const msg = e.message.substring(prefix.length); + const firstColon = msg.indexOf(':'); + const fileName = msg.substring(0, firstColon); + const rest = msg.substring(firstColon + 1); + assert.strictEqual(path.basename(fileName), 'test-common-must-not-call.js'); + const argsInfo = args.length > 0 ? + `\ncalled with arguments: ${args.map(util.inspect).join(', ')}` : ''; + assert.strictEqual(rest, line + argsInfo); +}); + +const validate1 = createValidate('9'); +try { + testFunction1(); +} catch (e) { + validate1(e); +} + +const validate2 = createValidate('11', ['hello', 42]); +try { + testFunction2('hello', 42); +} catch (e) { + validate2(e); +} + +assert.throws( + () => new Proxy({ prop: Symbol() }, { get: common.mustNotCall() }).prop, + { code: 'ERR_ASSERTION' } +); + +{ + const { inspect } = util; + delete util.inspect; + assert.throws( + () => common.mustNotCall()(null), + { code: 'ERR_ASSERTION' } + ); + util.inspect = inspect; +} diff --git a/test/js/node/test/parallel/test-console-assign-undefined.js b/test/js/node/test/parallel/test-console-assign-undefined.js new file mode 100644 index 0000000000..1021307b3c --- /dev/null +++ b/test/js/node/test/parallel/test-console-assign-undefined.js @@ -0,0 +1,28 @@ +'use strict'; + +// Patch global.console before importing modules that may modify the console +// object. + +const tmp = global.console; +global.console = 42; + +require('../common'); +const assert = require('assert'); + +// Originally the console had a getter. Test twice to verify it had no side +// effect. +assert.strictEqual(global.console, 42); +assert.strictEqual(global.console, 42); + +assert.throws( + () => console.log('foo'), + { name: 'TypeError' } +); + +global.console = 1; +assert.strictEqual(global.console, 1); +assert.strictEqual(console, 1); + +// Reset the console +global.console = tmp; +console.log('foo'); diff --git a/test/js/node/test/parallel/test-console-async-write-error.js b/test/js/node/test/parallel/test-console-async-write-error.js new file mode 100644 index 0000000000..be76c89832 --- /dev/null +++ b/test/js/node/test/parallel/test-console-async-write-error.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const { Console } = require('console'); +const { Writable } = require('stream'); + +for (const method of ['dir', 'log', 'warn']) { + const out = new Writable({ + write: common.mustCall((chunk, enc, callback) => { + process.nextTick(callback, new Error('foobar')); + }) + }); + + const c = new Console(out, out, true); + c[method]('abc'); // Should not throw. +} diff --git a/test/js/node/test/parallel/test-console-group.js b/test/js/node/test/parallel/test-console-group.js new file mode 100644 index 0000000000..3870619f36 --- /dev/null +++ b/test/js/node/test/parallel/test-console-group.js @@ -0,0 +1,241 @@ +'use strict'; +require('../common'); +const { + hijackStdout, + hijackStderr, + restoreStdout, + restoreStderr +} = require('../common/hijackstdio'); + +const assert = require('assert'); +const Console = require('console').Console; + +let c, stdout, stderr; + +function setup(groupIndentation) { + stdout = ''; + hijackStdout(function(data) { + stdout += data; + }); + + stderr = ''; + hijackStderr(function(data) { + stderr += data; + }); + + c = new Console({ stdout: process.stdout, + stderr: process.stderr, + colorMode: false, + groupIndentation: groupIndentation }); +} + +function teardown() { + restoreStdout(); + restoreStderr(); +} + +// Basic group() functionality +{ + setup(); + const expectedOut = 'This is the outer level\n' + + ' Level 2\n' + + ' Level 3\n' + + ' Back to level 2\n' + + 'Back to the outer level\n' + + 'Still at the outer level\n'; + + + const expectedErr = ' More of level 3\n'; + + c.log('This is the outer level'); + c.group(); + c.log('Level 2'); + c.group(); + c.log('Level 3'); + c.warn('More of level 3'); + c.groupEnd(); + c.log('Back to level 2'); + c.groupEnd(); + c.log('Back to the outer level'); + c.groupEnd(); + c.log('Still at the outer level'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Group indentation is tracked per Console instance. +{ + setup(); + const expectedOut = 'No indentation\n' + + 'None here either\n' + + ' Now the first console is indenting\n' + + 'But the second one does not\n'; + const expectedErr = ''; + + const c2 = new Console(process.stdout, process.stderr); + c.log('No indentation'); + c2.log('None here either'); + c.group(); + c.log('Now the first console is indenting'); + c2.log('But the second one does not'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Make sure labels work. +{ + setup(); + const expectedOut = 'This is a label\n' + + ' And this is the data for that label\n'; + const expectedErr = ''; + + c.group('This is a label'); + c.log('And this is the data for that label'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check that console.groupCollapsed() is an alias of console.group() +{ + setup(); + const expectedOut = 'Label\n' + + ' Level 2\n' + + ' Level 3\n'; + const expectedErr = ''; + + c.groupCollapsed('Label'); + c.log('Level 2'); + c.groupCollapsed(); + c.log('Level 3'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check that multiline strings and object output are indented properly. +{ + setup(); + const expectedOut = 'not indented\n' + + ' indented\n' + + ' also indented\n' + + ' {\n' + + " also: 'a',\n" + + " multiline: 'object',\n" + + " should: 'be',\n" + + " indented: 'properly',\n" + + " kthx: 'bai'\n" + + ' }\n'; + const expectedErr = ''; + + c.log('not indented'); + c.group(); + c.log('indented\nalso indented'); + c.log({ also: 'a', + multiline: 'object', + should: 'be', + indented: 'properly', + kthx: 'bai' }); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check that the kGroupIndent symbol property is not enumerable +{ + const keys = Reflect.ownKeys(console) + .filter((val) => Object.prototype.propertyIsEnumerable.call(console, val)) + .map((val) => val.toString()); + assert(!keys.includes('Symbol(groupIndent)'), + 'groupIndent should not be enumerable'); +} + +// Check custom groupIndentation. +{ + setup(3); + const expectedOut = 'Set the groupIndentation parameter to 3\n' + + 'This is the outer level\n' + + ' Level 2\n' + + ' Level 3\n' + + ' Back to level 2\n' + + 'Back to the outer level\n' + + 'Still at the outer level\n'; + + + const expectedErr = ' More of level 3\n'; + + c.log('Set the groupIndentation parameter to 3'); + c.log('This is the outer level'); + c.group(); + c.log('Level 2'); + c.group(); + c.log('Level 3'); + c.warn('More of level 3'); + c.groupEnd(); + c.log('Back to level 2'); + c.groupEnd(); + c.log('Back to the outer level'); + c.groupEnd(); + c.log('Still at the outer level'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check the correctness of the groupIndentation parameter. +{ + // TypeError + [null, 'str', [], false, true, {}].forEach((e) => { + assert.throws( + () => { + new Console({ stdout: process.stdout, + stderr: process.stderr, + groupIndentation: e }); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); + + // RangeError for integer + [NaN, 1.01].forEach((e) => { + assert.throws( + () => { + new Console({ stdout: process.stdout, + stderr: process.stderr, + groupIndentation: e }); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: /an integer/, + } + ); + }); + + // RangeError + [-1, 1001].forEach((e) => { + assert.throws( + () => { + new Console({ stdout: process.stdout, + stderr: process.stderr, + groupIndentation: e }); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: />= 0 and <= 1000/, + } + ); + }); +} diff --git a/test/js/node/test/parallel/test-console-instance.js b/test/js/node/test/parallel/test-console-instance.js new file mode 100644 index 0000000000..9821afeabd --- /dev/null +++ b/test/js/node/test/parallel/test-console-instance.js @@ -0,0 +1,146 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const Stream = require('stream'); +const requiredConsole = require('console'); +const Console = requiredConsole.Console; + +const out = new Stream(); +const err = new Stream(); + +// Ensure the Console instance doesn't write to the +// process' "stdout" or "stderr" streams. +process.stdout.write = process.stderr.write = common.mustNotCall(); + +// Make sure that the "Console" function exists. +assert.strictEqual(typeof Console, 'function'); + +assert.strictEqual(requiredConsole, global.console); +// Make sure the custom instanceof of Console works +assert.ok(global.console instanceof Console); +assert.ok(!({} instanceof Console)); + +// Make sure that the Console constructor throws +// when not given a writable stream instance. +assert.throws( + () => { new Console(); }, + { + code: 'ERR_CONSOLE_WRITABLE_STREAM', + name: 'TypeError', + message: /stdout/ + } +); + +// Console constructor should throw if stderr exists but is not writable. +assert.throws( + () => { + out.write = () => {}; + err.write = undefined; + new Console(out, err); + }, + { + code: 'ERR_CONSOLE_WRITABLE_STREAM', + name: 'TypeError', + message: /stderr/ + } +); + +out.write = err.write = (d) => {}; + +{ + const c = new Console(out, err); + assert.ok(c instanceof Console); + + out.write = err.write = common.mustCall((d) => { + assert.strictEqual(d, 'test\n'); + }, 2); + + c.log('test'); + c.error('test'); + + out.write = common.mustCall((d) => { + assert.strictEqual(d, '{ foo: 1 }\n'); + }); + + c.dir({ foo: 1 }); + + // Ensure that the console functions are bound to the console instance. + let called = 0; + out.write = common.mustCall((d) => { + called++; + assert.strictEqual(d, `${called} ${called - 1} [ 1, 2, 3 ]\n`); + }, 3); + + [1, 2, 3].forEach(c.log); +} + +// Test calling Console without the `new` keyword. +{ + const withoutNew = Console(out, err); + assert.ok(withoutNew instanceof Console); +} + +// Test extending Console +{ + class MyConsole extends Console { + hello() {} + // See if the methods on Console.prototype are overridable. + log() { return 'overridden'; } + } + const myConsole = new MyConsole(process.stdout); + assert.strictEqual(typeof myConsole.hello, 'function'); + assert.ok(myConsole instanceof Console); + assert.strictEqual(myConsole.log(), 'overridden'); + + const log = myConsole.log; + assert.strictEqual(log(), 'overridden'); +} + +// Instance that does not ignore the stream errors. +{ + const c2 = new Console(out, err, false); + + out.write = () => { throw new Error('out'); }; + err.write = () => { throw new Error('err'); }; + + assert.throws(() => c2.log('foo'), /^Error: out$/); + assert.throws(() => c2.warn('foo'), /^Error: err$/); + assert.throws(() => c2.dir('foo'), /^Error: out$/); +} + +// Console constructor throws if inspectOptions is not an object. +[null, true, false, 'foo', 5, Symbol()].forEach((inspectOptions) => { + assert.throws( + () => { + new Console({ + stdout: out, + stderr: err, + inspectOptions + }); + }, + { + code: 'ERR_INVALID_ARG_TYPE' + } + ); +}); diff --git a/test/js/node/test/parallel/test-console-issue-43095.js b/test/js/node/test/parallel/test-console-issue-43095.js new file mode 100644 index 0000000000..647f4af2df --- /dev/null +++ b/test/js/node/test/parallel/test-console-issue-43095.js @@ -0,0 +1,12 @@ +'use strict'; + +require('../common'); +const { inspect } = require('node:util'); + +const r = Proxy.revocable({}, {}); +r.revoke(); + +console.dir(r); +console.dir(r.proxy); +console.log(r.proxy); +console.log(inspect(r.proxy, { showProxy: true })); diff --git a/test/js/node/test/parallel/test-console-log-stdio-broken-dest.js b/test/js/node/test/parallel/test-console-log-stdio-broken-dest.js new file mode 100644 index 0000000000..bdd1b79427 --- /dev/null +++ b/test/js/node/test/parallel/test-console-log-stdio-broken-dest.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const { Writable } = require('stream'); +const { Console } = require('console'); +const { EventEmitter } = require('events'); + +const stream = new Writable({ + write(chunk, enc, cb) { + cb(); + }, + writev(chunks, cb) { + setTimeout(cb, 10, new Error('kaboom')); + } +}); +const myConsole = new Console(stream, stream); + +process.on('warning', common.mustNotCall()); + +stream.cork(); +for (let i = 0; i < EventEmitter.defaultMaxListeners + 1; i++) { + myConsole.log('a message'); +} +stream.uncork(); diff --git a/test/js/node/test/parallel/test-console-log-throw-primitive.js b/test/js/node/test/parallel/test-console-log-throw-primitive.js new file mode 100644 index 0000000000..a1a9ca2572 --- /dev/null +++ b/test/js/node/test/parallel/test-console-log-throw-primitive.js @@ -0,0 +1,14 @@ +'use strict'; +require('../common'); +const { Writable } = require('stream'); +const { Console } = require('console'); + +const stream = new Writable({ + write() { + throw null; // eslint-disable-line no-throw-literal + } +}); + +const console = new Console({ stdout: stream }); + +console.log('test'); // Should not throw diff --git a/test/js/node/test/parallel/test-console-methods.js b/test/js/node/test/parallel/test-console-methods.js new file mode 100644 index 0000000000..d338cc1f80 --- /dev/null +++ b/test/js/node/test/parallel/test-console-methods.js @@ -0,0 +1,63 @@ +'use strict'; +require('../common'); + +// This test ensures that console methods cannot be invoked as constructors and +// that their name is always correct. + +const assert = require('assert'); + +const { Console } = console; +const newInstance = new Console(process.stdout); +const err = TypeError; + +const methods = [ + 'log', + 'warn', + 'dir', + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'assert', + 'clear', + 'count', + 'countReset', + 'group', + 'groupEnd', + 'table', + 'debug', + 'info', + 'dirxml', + 'error', + 'groupCollapsed', +]; + +const alternateNames = { + debug: 'log', + info: 'log', + dirxml: 'log', + error: 'warn', + groupCollapsed: 'group' +}; + +function assertEqualName(method) { + try { + assert.strictEqual(console[method].name, method); + } catch { + assert.strictEqual(console[method].name, alternateNames[method]); + } + try { + assert.strictEqual(newInstance[method].name, method); + } catch { + assert.strictEqual(newInstance[method].name, alternateNames[method]); + } +} + +for (const method of methods) { + assertEqualName(method); + + assert.throws(() => new console[method](), err); + assert.throws(() => new newInstance[method](), err); + assert.throws(() => Reflect.construct({}, [], console[method]), err); + assert.throws(() => Reflect.construct({}, [], newInstance[method]), err); +} diff --git a/test/js/node/test/parallel/test-console-no-swallow-stack-overflow.js b/test/js/node/test/parallel/test-console-no-swallow-stack-overflow.js new file mode 100644 index 0000000000..8510f99488 --- /dev/null +++ b/test/js/node/test/parallel/test-console-no-swallow-stack-overflow.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Console } = require('console'); +const { Writable } = require('stream'); + +for (const method of ['dir', 'log', 'warn']) { + assert.throws(() => { + const out = new Writable({ + write: common.mustCall(function write(...args) { + // Exceeds call stack. + // call twice to prevent jsc tail call optimization + write(...args); + return write(...args); + }), + }); + const c = new Console(out, out, true); + + c[method]('Hello, world!'); + }, { name: 'RangeError' }); +} diff --git a/test/js/node/test/parallel/test-console-not-call-toString.js b/test/js/node/test/parallel/test-console-not-call-toString.js new file mode 100644 index 0000000000..0f6f2624c5 --- /dev/null +++ b/test/js/node/test/parallel/test-console-not-call-toString.js @@ -0,0 +1,34 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +function func() {} +let toStringCalled = false; +func.toString = function() { + toStringCalled = true; +}; + +require('util').inspect(func); + +assert.ok(!toStringCalled); diff --git a/test/js/node/test/parallel/test-console-self-assign.js b/test/js/node/test/parallel/test-console-self-assign.js new file mode 100644 index 0000000000..53c54ab9a3 --- /dev/null +++ b/test/js/node/test/parallel/test-console-self-assign.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); + +// Assigning to itself should not throw. +global.console = global.console; // eslint-disable-line no-self-assign diff --git a/test/js/node/test/parallel/test-console-sync-write-error.js b/test/js/node/test/parallel/test-console-sync-write-error.js new file mode 100644 index 0000000000..bf916ff5b8 --- /dev/null +++ b/test/js/node/test/parallel/test-console-sync-write-error.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common'); +const { Console } = require('console'); +const { Writable } = require('stream'); + +for (const method of ['dir', 'log', 'warn']) { + { + const out = new Writable({ + write: common.mustCall((chunk, enc, callback) => { + callback(new Error('foobar')); + }) + }); + + const c = new Console(out, out, true); + c[method]('abc'); // Should not throw. + } + + { + const out = new Writable({ + write: common.mustCall((chunk, enc, callback) => { + throw new Error('foobar'); + }) + }); + + const c = new Console(out, out, true); + c[method]('abc'); // Should not throw. + } + + { + const out = new Writable({ + write: common.mustCall((chunk, enc, callback) => { + setImmediate(() => callback(new Error('foobar'))); + }) + }); + + const c = new Console(out, out, true); + c[method]('abc'); // Should not throw. + } +} diff --git a/test/js/node/test/parallel/test-console-tty-colors.js b/test/js/node/test/parallel/test-console-tty-colors.js new file mode 100644 index 0000000000..63ff42935b --- /dev/null +++ b/test/js/node/test/parallel/test-console-tty-colors.js @@ -0,0 +1,113 @@ +'use strict'; +// ci sets process.env["FORCE_COLOR"], which makes the test fail in both node and bun +delete process.env["FORCE_COLOR"]; + +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); +const { Writable } = require('stream'); +const { Console } = require('console'); + +function check(isTTY, colorMode, expectedColorMode, inspectOptions) { + const items = [ + 1, + { a: 2 }, + [ 'foo' ], + { '\\a': '\\bar' }, + ]; + + let i = 0; + const stream = new Writable({ + write: common.mustCall((chunk, enc, cb) => { + console.log("testing case", isTTY, colorMode, expectedColorMode, inspectOptions); + assert.strictEqual(chunk.trim(), + util.inspect(items[i++], { + colors: expectedColorMode, + ...inspectOptions + })); + cb(); + }, items.length), + decodeStrings: false + }); + stream.isTTY = isTTY; + + // Set ignoreErrors to `false` here so that we see assertion failures + // from the `write()` call happen. + const testConsole = new Console({ + stdout: stream, + ignoreErrors: false, + colorMode, + inspectOptions + }); + for (const item of items) { + testConsole.log(item); + } +} + +check(true, 'auto', true); +check(false, 'auto', false); +check(false, undefined, true, { colors: true, compact: false }); +check(true, 'auto', true, { compact: false }); +check(true, undefined, false, { colors: false }); +check(true, true, true); +check(false, true, true); +check(true, false, false); +check(false, false, false); + +// Check invalid options. +{ + const stream = new Writable({ + write: common.mustNotCall() + }); + + assert.throws( + () => { + new Console({ + stdout: stream, + ignoreErrors: false, + colorMode: 'true' + }); + }, + { + message: `The argument 'colorMode' must be one of: 'auto', true, false. Received "true"`, + code: 'ERR_INVALID_ARG_VALUE' + } + ); + + [0, null, {}, [], () => {}].forEach((colorMode) => { + const received = util.inspect(colorMode); + assert.throws( + () => { + new Console({ + stdout: stream, + ignoreErrors: false, + colorMode: colorMode + }); + }, + { + message: `The argument 'colorMode' must be one of: 'auto', true, false. Received ${received}`, + code: 'ERR_INVALID_ARG_VALUE' + } + ); + }); + + [true, false, 'auto'].forEach((colorMode) => { + assert.throws( + () => { + new Console({ + stdout: stream, + ignoreErrors: false, + colorMode: colorMode, + inspectOptions: { + colors: false + } + }); + }, + { + message: 'Option "options.inspectOptions.color" cannot be used in ' + + 'combination with option "colorMode"', + code: 'ERR_INCOMPATIBLE_OPTION_PAIR' + } + ); + }); +} diff --git a/test/js/node/test/parallel/test-console-with-frozen-intrinsics.js b/test/js/node/test/parallel/test-console-with-frozen-intrinsics.js new file mode 100644 index 0000000000..1da2a6a5fb --- /dev/null +++ b/test/js/node/test/parallel/test-console-with-frozen-intrinsics.js @@ -0,0 +1,30 @@ +// flags: --frozen-intrinsics +'use strict'; +require('../common'); +console.clear(); + +const consoleMethods = ['log', 'info', 'warn', 'error', 'debug', 'trace']; + +for (const method of consoleMethods) { + console[method]('foo'); + console[method]('foo', 'bar'); + console[method]('%s %s', 'foo', 'bar', 'hop'); +} + +console.dir({ slashes: '\\\\' }); +console.dirxml({ slashes: '\\\\' }); + +console.time('label'); +console.timeLog('label', 'hi'); +console.timeEnd('label'); + +console.assert(true, 'true'); + +console.count('label'); +console.countReset('label'); + +console.group('label'); +console.groupCollapsed('label'); +console.groupEnd(); + +console.table([{ a: 1, b: 2 }, { a: 'foo', b: 'bar' }]); diff --git a/test/js/node/test/parallel/test-coverage-with-inspector-disabled.js b/test/js/node/test/parallel/test-coverage-with-inspector-disabled.js new file mode 100644 index 0000000000..f2ba070859 --- /dev/null +++ b/test/js/node/test/parallel/test-coverage-with-inspector-disabled.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +if (process.features.inspector) { + common.skip('V8 inspector is enabled'); +} + +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const env = { ...process.env, NODE_V8_COVERAGE: '/foo/bar' }; +const childPath = fixtures.path('v8-coverage/subprocess'); +const { status, stderr } = spawnSync( + process.execPath, + [childPath], + { env } +); + +const warningMessage = 'The inspector is disabled, ' + + 'coverage could not be collected'; + +assert.strictEqual(status, 0); +assert.strictEqual( + stderr.toString().includes(`Warning: ${warningMessage}`), + true +); diff --git a/test/js/node/test/parallel/test-crypto-aes-wrap.js b/test/js/node/test/parallel/test-crypto-aes-wrap.js new file mode 100644 index 0000000000..9fe1b02eb2 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-aes-wrap.js @@ -0,0 +1,68 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L10 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const test = [ + { + algorithm: 'aes128-wrap', + key: 'b26f309fbe57e9b3bb6ae5ef31d54450', + iv: '3fd838af4093d749', + text: '12345678123456781234567812345678' + }, + { + algorithm: 'id-aes128-wrap-pad', + key: 'b26f309fbe57e9b3bb6ae5ef31d54450', + iv: '3fd838af', + text: '12345678123456781234567812345678123' + }, + { + algorithm: 'aes192-wrap', + key: '40978085d68091f7dfca0d7dfc7a5ee76d2cc7f2f345a304', + iv: '3fd838af4093d749', + text: '12345678123456781234567812345678' + }, + { + algorithm: 'id-aes192-wrap-pad', + key: '40978085d68091f7dfca0d7dfc7a5ee76d2cc7f2f345a304', + iv: '3fd838af', + text: '12345678123456781234567812345678123' + }, + { + algorithm: 'aes256-wrap', + key: '29c9eab5ed5ad44134a1437fe2e673b4d88a5b7c72e68454fea08721392b7323', + iv: '3fd838af4093d749', + text: '12345678123456781234567812345678' + }, + { + algorithm: 'id-aes256-wrap-pad', + key: '29c9eab5ed5ad44134a1437fe2e673b4d88a5b7c72e68454fea08721392b7323', + iv: '3fd838af', + text: '12345678123456781234567812345678123' + }, +]; + +test.forEach((data) => { + const cipher = crypto.createCipheriv( + data.algorithm, + Buffer.from(data.key, 'hex'), + Buffer.from(data.iv, 'hex')); + const ciphertext = cipher.update(data.text, 'utf8'); + + const decipher = crypto.createDecipheriv( + data.algorithm, + Buffer.from(data.key, 'hex'), + Buffer.from(data.iv, 'hex')); + const msg = decipher.update(ciphertext, 'buffer', 'utf8'); + + assert.strictEqual(msg, data.text, `${data.algorithm} test case failed`); +}); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-authenticated-stream.js b/test/js/node/test/parallel/test-crypto-authenticated-stream.js new file mode 100644 index 0000000000..815d18abe9 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-authenticated-stream.js @@ -0,0 +1,147 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L12 + +'use strict'; +// Refs: https://github.com/nodejs/node/issues/31733 +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const fs = require('fs'); +const stream = require('stream'); +const tmpdir = require('../common/tmpdir'); + +class Sink extends stream.Writable { + constructor() { + super(); + this.chunks = []; + } + + _write(chunk, encoding, cb) { + this.chunks.push(chunk); + cb(); + } +} + +function direct(config) { + const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; + const expected = Buffer.alloc(plaintextLength); + + const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); + c.setAAD(aad, { plaintextLength }); + const ciphertext = Buffer.concat([c.update(expected), c.final()]); + + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + const actual = Buffer.concat([d.update(ciphertext), d.final()]); + + assert.deepStrictEqual(expected, actual); +} + +function mstream(config) { + const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; + const expected = Buffer.alloc(plaintextLength); + + const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); + c.setAAD(aad, { plaintextLength }); + + const plain = new stream.PassThrough(); + const crypt = new Sink(); + const chunks = crypt.chunks; + plain.pipe(c).pipe(crypt); + plain.end(expected); + + crypt.on('close', common.mustCall(() => { + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + + const crypt = new stream.PassThrough(); + const plain = new Sink(); + crypt.pipe(d).pipe(plain); + for (const chunk of chunks) crypt.write(chunk); + crypt.end(); + + plain.on('close', common.mustCall(() => { + const actual = Buffer.concat(plain.chunks); + assert.deepStrictEqual(expected, actual); + })); + })); +} + +function fstream(config) { + const count = fstream.count++; + const filename = (name) => tmpdir.resolve(`${name}${count}`); + + const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; + const expected = Buffer.alloc(plaintextLength); + fs.writeFileSync(filename('a'), expected); + + const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); + c.setAAD(aad, { plaintextLength }); + + const plain = fs.createReadStream(filename('a')); + const crypt = fs.createWriteStream(filename('b')); + plain.pipe(c).pipe(crypt); + + // Observation: 'close' comes before 'end' on |c|, which definitely feels + // wrong. Switching to `c.on('end', ...)` doesn't fix the test though. + crypt.on('close', common.mustCall(() => { + // Just to drive home the point that decryption does actually work: + // reading the file synchronously, then decrypting it, works. + { + const ciphertext = fs.readFileSync(filename('b')); + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + const actual = Buffer.concat([d.update(ciphertext), d.final()]); + assert.deepStrictEqual(expected, actual); + } + + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + + const crypt = fs.createReadStream(filename('b')); + const plain = fs.createWriteStream(filename('c')); + crypt.pipe(d).pipe(plain); + + plain.on('close', common.mustCall(() => { + const actual = fs.readFileSync(filename('c')); + assert.deepStrictEqual(expected, actual); + })); + })); +} +fstream.count = 0; + +function test(config) { + direct(config); + mstream(config); + fstream(config); +} + +tmpdir.refresh(); + +test({ + cipher: 'aes-128-ccm', + aad: Buffer.alloc(1), + iv: Buffer.alloc(8), + key: Buffer.alloc(16), + authTagLength: 16, + plaintextLength: 32768, +}); + +test({ + cipher: 'aes-128-ccm', + aad: Buffer.alloc(1), + iv: Buffer.alloc(8), + key: Buffer.alloc(16), + authTagLength: 16, + plaintextLength: 32769, +}); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-authenticated.js b/test/js/node/test/parallel/test-crypto-authenticated.js new file mode 100644 index 0000000000..b318e4cacd --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-authenticated.js @@ -0,0 +1,709 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L11 + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { inspect } = require('util'); +const fixtures = require('../common/fixtures'); + +// +// Test authenticated encryption modes. +// +// !NEVER USE STATIC IVs IN REAL LIFE! +// + +const TEST_CASES = require(fixtures.path('aead-vectors.js')); + +const errMessages = { + auth: / auth/, + state: / state/, + FIPS: /not supported in FIPS mode/, + length: /Invalid initialization vector/, + authTagLength: /Invalid authentication tag length/ +}; + +const ciphers = crypto.getCiphers(); + +for (const test of TEST_CASES) { + if (!ciphers.includes(test.algo)) { + common.printSkipMessage(`unsupported ${test.algo} test`); + continue; + } + + if (common.hasFipsCrypto && test.iv.length < 24) { + common.printSkipMessage('IV len < 12 bytes unsupported in FIPS mode'); + continue; + } + + const isCCM = /^aes-(128|192|256)-ccm$/.test(test.algo); + const isOCB = /^aes-(128|192|256)-ocb$/.test(test.algo); + + let options; + if (isCCM || isOCB) + options = { authTagLength: test.tag.length / 2 }; + + const inputEncoding = test.plainIsHex ? 'hex' : 'ascii'; + + let aadOptions; + if (isCCM) { + aadOptions = { + plaintextLength: Buffer.from(test.plain, inputEncoding).length + }; + } + + { + const encrypt = crypto.createCipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + + if (test.aad) + encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); + + let hex = encrypt.update(test.plain, inputEncoding, 'hex'); + hex += encrypt.final('hex'); + + const auth_tag = encrypt.getAuthTag(); + // Only test basic encryption run if output is marked as tampered. + if (!test.tampered) { + assert.strictEqual(hex, test.ct); + assert.strictEqual(auth_tag.toString('hex'), test.tag); + } + } + + { + if (isCCM && common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + }, errMessages.FIPS); + } else { + const decrypt = crypto.createDecipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); + if (test.aad) + decrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); + + const outputEncoding = test.plainIsHex ? 'hex' : 'ascii'; + + let msg = decrypt.update(test.ct, 'hex', outputEncoding); + if (!test.tampered) { + msg += decrypt.final(outputEncoding); + assert.strictEqual(msg, test.plain); + } else { + // Assert that final throws if input data could not be verified! + assert.throws(function() { decrypt.final('hex'); }, errMessages.auth); + } + } + } + + { + // Trying to get tag before inputting all data: + const encrypt = crypto.createCipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + encrypt.update('blah', 'ascii'); + assert.throws(function() { encrypt.getAuthTag(); }, errMessages.state); + } + + { + // Trying to create cipher with incorrect IV length + assert.throws(function() { + crypto.createCipheriv( + test.algo, + Buffer.from(test.key, 'hex'), + Buffer.alloc(0) + ); + }, errMessages.length); + } +} + +// Non-authenticating mode: +{ + const encrypt = + crypto.createCipheriv('aes-128-cbc', + 'ipxp9a6i1Mb4USb4', + '6fKjEjR3Vl30EUYC'); + encrypt.update('blah', 'ascii'); + encrypt.final(); + assert.throws(() => encrypt.getAuthTag(), errMessages.state); + assert.throws(() => encrypt.setAAD(Buffer.from('123', 'ascii')), + errMessages.state); +} + +// GCM only supports specific authentication tag lengths, invalid lengths should +// throw. +{ + for (const length of [0, 1, 2, 6, 9, 10, 11, 17]) { + assert.throws(() => { + const decrypt = crypto.createDecipheriv('aes-128-gcm', + 'FxLKsqdmv0E9xrQh', + 'qkuZpJWCewa6Szih'); + decrypt.setAuthTag(Buffer.from('1'.repeat(length))); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + + assert.throws(() => { + crypto.createCipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', + { + authTagLength: length + }); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + + assert.throws(() => { + crypto.createDecipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', + { + authTagLength: length + }); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + } +} + +// Test that GCM can produce shorter authentication tags than 16 bytes. +{ + const fullTag = '1debb47b2c91ba2cea16fad021703070'; + for (const [authTagLength, e] of [[undefined, 16], [12, 12], [4, 4]]) { + const cipher = crypto.createCipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', { + authTagLength + }); + cipher.setAAD(Buffer.from('abcd')); + cipher.update('01234567', 'hex'); + cipher.final(); + const tag = cipher.getAuthTag(); + assert.strictEqual(tag.toString('hex'), fullTag.slice(0, 2 * e)); + } +} + +// Test that users can manually restrict the GCM tag length to a single value. +{ + const decipher = crypto.createDecipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', { + authTagLength: 8 + }); + + assert.throws(() => { + // This tag would normally be allowed. + decipher.setAuthTag(Buffer.from('1'.repeat(12))); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + + // The Decipher object should be left intact. + decipher.setAuthTag(Buffer.from('445352d3ff85cf94', 'hex')); + const text = Buffer.concat([ + decipher.update('3a2a3647', 'hex'), + decipher.final(), + ]); + assert.strictEqual(text.toString('utf8'), 'node'); +} + +// Test that create(De|C)ipher(iv)? throws if the mode is CCM and an invalid +// authentication tag length has been specified. +{ + for (const authTagLength of [-1, true, false, NaN, 5.5]) { + assert.throws(() => { + crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.authTagLength' is invalid. " + + `Received ${inspect(authTagLength)}` + }); + + assert.throws(() => { + crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.authTagLength' is invalid. " + + `Received ${inspect(authTagLength)}` + }); + } + + // The following values will not be caught by the JS layer and thus will not + // use the default error codes. + for (const authTagLength of [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 18]) { + assert.throws(() => { + crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, errMessages.authTagLength); + + if (!common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, errMessages.authTagLength); + } + } +} + +// Test that create(De|C)ipher(iv)? throws if the mode is CCM or OCB and no +// authentication tag has been specified. +{ + for (const mode of ['ccm', 'ocb']) { + assert.throws(() => { + crypto.createCipheriv(`aes-256-${mode}`, + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S'); + }, { + message: `authTagLength required for aes-256-${mode}` + }); + + // CCM decryption and create(De|C)ipher are unsupported in FIPS mode. + if (!common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv(`aes-256-${mode}`, + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S'); + }, { + message: `authTagLength required for aes-256-${mode}` + }); + } + } +} + +// Test that setAAD throws if an invalid plaintext length has been specified. +{ + const cipher = crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + + for (const plaintextLength of [-1, true, false, NaN, 5.5]) { + assert.throws(() => { + cipher.setAAD(Buffer.from('0123456789', 'hex'), { plaintextLength }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.plaintextLength' is invalid. " + + `Received ${inspect(plaintextLength)}` + }); + } +} + +// Test that setAAD and update throw if the plaintext is too long. +{ + for (const ivLength of [13, 12]) { + const maxMessageSize = (1 << (8 * (15 - ivLength))) - 1; + const key = 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8'; + const cipher = () => crypto.createCipheriv('aes-256-ccm', key, + '0'.repeat(ivLength), + { + authTagLength: 10 + }); + + assert.throws(() => { + cipher().setAAD(Buffer.alloc(0), { + plaintextLength: maxMessageSize + 1 + }); + }, /Invalid message length$/); + + const msg = Buffer.alloc(maxMessageSize + 1); + assert.throws(() => { + cipher().update(msg); + }, /Invalid message length/); + + const c = cipher(); + c.setAAD(Buffer.alloc(0), { + plaintextLength: maxMessageSize + }); + c.update(msg.slice(1)); + } +} + +// Test that setAAD throws if the mode is CCM and the plaintext length has not +// been specified. +{ + assert.throws(() => { + const cipher = crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + cipher.setAAD(Buffer.from('0123456789', 'hex')); + }, /options\.plaintextLength required for CCM mode with AAD/); + + if (!common.hasFipsCrypto) { + assert.throws(() => { + const cipher = crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + cipher.setAAD(Buffer.from('0123456789', 'hex')); + }, /options\.plaintextLength required for CCM mode with AAD/); + } +} + +// Test that final() throws in CCM mode when no authentication tag is provided. +{ + if (!common.hasFipsCrypto) { + const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); + const iv = Buffer.from('7305220bca40d4c90e1791e9', 'hex'); + const ct = Buffer.from('8beba09d4d4d861f957d51c0794f4abf8030848e', 'hex'); + const decrypt = crypto.createDecipheriv('aes-128-ccm', key, iv, { + authTagLength: 10 + }); + // Normally, we would do this: + // decrypt.setAuthTag(Buffer.from('0d9bcd142a94caf3d1dd', 'hex')); + assert.throws(() => { + decrypt.setAAD(Buffer.from('63616c76696e', 'hex'), { + plaintextLength: ct.length + }); + decrypt.update(ct); + decrypt.final(); + }, errMessages.state); + } +} + +// Test that setAuthTag does not throw in GCM mode when called after setAAD. +{ + const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); + const iv = Buffer.from('579d9dfde9cd93d743da1ceaeebb86e4', 'hex'); + const decrypt = crypto.createDecipheriv('aes-128-gcm', key, iv); + decrypt.setAAD(Buffer.from('0123456789', 'hex')); + decrypt.setAuthTag(Buffer.from('1bb9253e250b8069cde97151d7ef32d9', 'hex')); + assert.strictEqual(decrypt.update('807022', 'hex', 'hex'), 'abcdef'); + assert.strictEqual(decrypt.final('hex'), ''); +} + +// Test that an IV length of 11 does not overflow max_message_size_. +{ + const key = 'x'.repeat(16); + const iv = Buffer.from('112233445566778899aabb', 'hex'); + const options = { authTagLength: 8 }; + const encrypt = crypto.createCipheriv('aes-128-ccm', key, iv, options); + encrypt.update('boom'); // Should not throw 'Message exceeds maximum size'. + encrypt.final(); +} + +// Test that the authentication tag can be set at any point before calling +// final() in GCM or OCB mode. +{ + const plain = Buffer.from('Hello world', 'utf8'); + const key = Buffer.from('0123456789abcdef', 'utf8'); + const iv = Buffer.from('0123456789ab', 'utf8'); + + for (const mode of ['gcm', 'ocb']) { + for (const authTagLength of mode === 'gcm' ? [undefined, 8] : [8]) { + const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv, { + authTagLength + }); + const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); + const authTag = cipher.getAuthTag(); + + for (const authTagBeforeUpdate of [true, false]) { + const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, { + authTagLength + }); + if (authTagBeforeUpdate) { + decipher.setAuthTag(authTag); + } + const resultUpdate = decipher.update(ciphertext); + if (!authTagBeforeUpdate) { + decipher.setAuthTag(authTag); + } + const resultFinal = decipher.final(); + const result = Buffer.concat([resultUpdate, resultFinal]); + assert(result.equals(plain)); + } + } + } +} + +// Test that setAuthTag can only be called once. +{ + const plain = Buffer.from('Hello world', 'utf8'); + const key = Buffer.from('0123456789abcdef', 'utf8'); + const iv = Buffer.from('0123456789ab', 'utf8'); + const opts = { authTagLength: 8 }; + + for (const mode of ['gcm', 'ccm', 'ocb']) { + const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv, opts); + const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); + const tag = cipher.getAuthTag(); + + const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, opts); + decipher.setAuthTag(tag); + assert.throws(() => { + decipher.setAuthTag(tag); + }, errMessages.state); + // Decryption should still work. + const plaintext = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + assert(plain.equals(plaintext)); + } +} + + +// Test chacha20-poly1305 rejects invalid IV lengths of 13, 14, 15, and 16 (a +// length of 17 or greater was already rejected). +// - https://www.openssl.org/news/secadv/20190306.txt +{ + // Valid extracted from TEST_CASES, check that it detects IV tampering. + const valid = { + algo: 'chacha20-poly1305', + key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', + iv: '070000004041424344454647', + plain: '4c616469657320616e642047656e746c656d656e206f662074686520636c6173' + + '73206f66202739393a204966204920636f756c64206f6666657220796f75206f' + + '6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73' + + '637265656e20776f756c642062652069742e', + plainIsHex: true, + aad: '50515253c0c1c2c3c4c5c6c7', + ct: 'd31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5' + + 'a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e06' + + '0b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fa' + + 'b324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d265' + + '86cec64b6116', + tag: '1ae10b594f09e26a7e902ecbd0600691', + tampered: false, + }; + + // Invalid IV lengths should be detected: + // - 12 and below are valid. + // - 13-16 are not detected as invalid by some OpenSSL versions. + check(13); + check(14); + check(15); + check(16); + // - 17 and above were always detected as invalid by OpenSSL. + check(17); + + function check(ivLength) { + const prefix = ivLength - valid.iv.length / 2; + assert.throws(() => crypto.createCipheriv( + valid.algo, + Buffer.from(valid.key, 'hex'), + Buffer.from(H(prefix) + valid.iv, 'hex') + ), errMessages.length, `iv length ${ivLength} was not rejected`); + + function H(length) { return '00'.repeat(length); } + } +} + +{ + // CCM cipher without data should not crash, see https://github.com/nodejs/node/issues/38035. + const algo = 'aes-128-ccm'; + const key = Buffer.alloc(16); + const iv = Buffer.alloc(12); + const opts = { authTagLength: 10 }; + + for (const cipher of [ + crypto.createCipheriv(algo, key, iv, opts), + ]) { + assert.throws(() => { + cipher.final(); + }, common.hasOpenSSL3 ? { + code: 'ERR_OSSL_TAG_NOT_SET' + } : { + message: /Unsupported state/ + }); + } +} + +{ + const key = Buffer.alloc(32); + const iv = Buffer.alloc(12); + + for (const authTagLength of [0, 17]) { + assert.throws(() => { + crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength }); + }, { + code: 'ERR_CRYPTO_INVALID_AUTH_TAG', + message: errMessages.authTagLength + }); + } +} + +// ChaCha20-Poly1305 should respect the authTagLength option and should not +// require the authentication tag before calls to update() during decryption. +{ + const key = Buffer.alloc(32); + const iv = Buffer.alloc(12); + + for (let authTagLength = 1; authTagLength <= 16; authTagLength++) { + const cipher = + crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength }); + const ciphertext = Buffer.concat([cipher.update('foo'), cipher.final()]); + const authTag = cipher.getAuthTag(); + assert.strictEqual(authTag.length, authTagLength); + + // The decipher operation should reject all authentication tags other than + // that of the expected length. + for (let other = 1; other <= 16; other++) { + const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, { + authTagLength: other + }); + // ChaCha20 is a stream cipher so we do not need to call final() to obtain + // the full plaintext. + const plaintext = decipher.update(ciphertext); + assert.strictEqual(plaintext.toString(), 'foo'); + if (other === authTagLength) { + // The authentication tag length is as expected and the tag itself is + // correct, so this should work. + decipher.setAuthTag(authTag); + decipher.final(); + } else { + // The authentication tag that we are going to pass to setAuthTag is + // either too short or too long. If other < authTagLength, the + // authentication tag is still correct, but it should still be rejected + // because its security assurance is lower than expected. + assert.throws(() => { + decipher.setAuthTag(authTag); + }, { + code: 'ERR_CRYPTO_INVALID_AUTH_TAG', + message: `Invalid authentication tag length: ${authTagLength}` + }); + } + } + } +} + +// ChaCha20-Poly1305 should default to an authTagLength of 16. When encrypting, +// this matches the behavior of GCM ciphers. When decrypting, however, it is +// stricter than GCM in that it only allows authentication tags that are exactly +// 16 bytes long, whereas, when no authTagLength was specified, GCM would accept +// shorter tags as long as their length was valid according to NIST SP 800-38D. +// For ChaCha20-Poly1305, we intentionally deviate from that because there are +// no recommended or approved authentication tag lengths below 16 bytes. +{ + const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { + return algo === 'chacha20-poly1305' && tampered === false; + }); + assert.strictEqual(rfcTestCases.length, 1); + + const [testCase] = rfcTestCases; + const key = Buffer.from(testCase.key, 'hex'); + const iv = Buffer.from(testCase.iv, 'hex'); + const aad = Buffer.from(testCase.aad, 'hex'); + + for (const opt of [ + undefined, + { authTagLength: undefined }, + { authTagLength: 16 }, + ]) { + const cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, opt); + const ciphertext = Buffer.concat([ + cipher.setAAD(aad).update(testCase.plain, 'hex'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + + assert.strictEqual(ciphertext.toString('hex'), testCase.ct); + assert.strictEqual(authTag.toString('hex'), testCase.tag); + + const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, opt); + const plaintext = Buffer.concat([ + decipher.setAAD(aad).update(ciphertext), + decipher.setAuthTag(authTag).final(), + ]); + + assert.strictEqual(plaintext.toString('hex'), testCase.plain); + } +} + +// https://github.com/nodejs/node/issues/45874 +{ + const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { + return algo === 'chacha20-poly1305' && tampered === false; + }); + assert.strictEqual(rfcTestCases.length, 1); + + const [testCase] = rfcTestCases; + const key = Buffer.from(testCase.key, 'hex'); + const iv = Buffer.from(testCase.iv, 'hex'); + const aad = Buffer.from(testCase.aad, 'hex'); + const opt = { authTagLength: 16 }; + + const cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, opt); + const ciphertext = Buffer.concat([ + cipher.setAAD(aad).update(testCase.plain, 'hex'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + + assert.strictEqual(ciphertext.toString('hex'), testCase.ct); + assert.strictEqual(authTag.toString('hex'), testCase.tag); + + const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, opt); + decipher.setAAD(aad).update(ciphertext); + + assert.throws(() => { + decipher.final(); + }, /Unsupported state or unable to authenticate data/); +} + +*/ diff --git a/test/js/node/test/parallel/test-crypto-certificate.js b/test/js/node/test/parallel/test-crypto-certificate.js new file mode 100644 index 0000000000..da932c608d --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-certificate.js @@ -0,0 +1,127 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { Certificate } = crypto; +const fixtures = require('../common/fixtures'); + +// Test Certificates +const spkacValid = fixtures.readKey('rsa_spkac.spkac'); +const spkacChallenge = 'this-is-a-challenge'; +const spkacFail = fixtures.readKey('rsa_spkac_invalid.spkac'); +const spkacPublicPem = fixtures.readKey('rsa_public.pem'); + +function copyArrayBuffer(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +function checkMethods(certificate) { + + /* spkacValid has a md5 based signature which is not allowed in boringssl + https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L77 + assert.strictEqual(certificate.verifySpkac(spkacValid), true); + assert.strictEqual(certificate.verifySpkac(spkacFail), false); + */ + + assert.strictEqual( + stripLineEndings(certificate.exportPublicKey(spkacValid).toString('utf8')), + stripLineEndings(spkacPublicPem.toString('utf8')) + ); + assert.strictEqual(certificate.exportPublicKey(spkacFail), ''); + + assert.strictEqual( + certificate.exportChallenge(spkacValid).toString('utf8'), + spkacChallenge + ); + assert.strictEqual(certificate.exportChallenge(spkacFail), ''); + + /* spkacValid has a md5 based signature which is not allowed in boringssl + https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L88 + const ab = copyArrayBuffer(spkacValid); + assert.strictEqual(certificate.verifySpkac(ab), true); + assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), true); + assert.strictEqual(certificate.verifySpkac(new DataView(ab)), true); + */ +} + +{ + // Test maximum size of input buffer + let buf; + let skip = false; + try { + buf = Buffer.alloc(2 ** 31); + } catch { + // The allocation may fail on some systems. That is expected due + // to architecture and memory constraints. If it does, go ahead + // and skip this test. + skip = true; + } + if (!skip) { + assert.throws( + () => Certificate.verifySpkac(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportChallenge(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportPublicKey(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + } +} + +{ + // Test instance methods + checkMethods(new Certificate()); +} + +{ + // Test static methods + checkMethods(Certificate); +} + +function stripLineEndings(obj) { + return obj.replace(/\n/g, ''); +} + +// Direct call Certificate() should return instance +assert(Certificate() instanceof Certificate); + +[1, {}, [], Infinity, true, undefined, null].forEach((val) => { + assert.throws( + () => Certificate.verifySpkac(val), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +}); + +[1, {}, [], Infinity, true, undefined, null].forEach((val) => { + const errObj = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => Certificate.exportPublicKey(val), errObj); + assert.throws(() => Certificate.exportChallenge(val), errObj); +}); diff --git a/test/js/node/test/parallel/test-crypto-des3-wrap.js b/test/js/node/test/parallel/test-crypto-des3-wrap.js new file mode 100644 index 0000000000..6648881bf6 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-des3-wrap.js @@ -0,0 +1,31 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L13 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Test case for des-ede3 wrap/unwrap. des3-wrap needs extra 2x blocksize +// then plaintext to store ciphertext. +const test = { + key: Buffer.from('3c08e25be22352910671cfe4ba3652b1220a8a7769b490ba', 'hex'), + iv: Buffer.alloc(0), + plaintext: '32|RmVZZkFUVmpRRkp0TmJaUm56ZU9qcnJkaXNNWVNpTTU*|iXmckfRWZBG' + + 'WWELweCBsThSsfUHLeRe0KCsK8ooHgxie0zOINpXxfZi/oNG7uq9JWFVCk70gfzQH8ZU' + + 'JjAfaFg**' +}; + +const cipher = crypto.createCipheriv('des3-wrap', test.key, test.iv); +const ciphertext = cipher.update(test.plaintext, 'utf8'); + +const decipher = crypto.createDecipheriv('des3-wrap', test.key, test.iv); +const msg = decipher.update(ciphertext, 'buffer', 'utf8'); + +assert.strictEqual(msg, test.plaintext); + +*/ diff --git a/test/js/node/test/parallel/test-crypto-dh-curves.js b/test/js/node/test/parallel/test-crypto-dh-curves.js new file mode 100644 index 0000000000..81a469c226 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-curves.js @@ -0,0 +1,191 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Second OAKLEY group, see +// https://github.com/nodejs/node-v0.x-archive/issues/2338 and +// https://xml2rfc.tools.ietf.org/public/rfc/html/rfc2412.html#anchor49 +const p = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' + + '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' + + '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' + + 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF'; +crypto.createDiffieHellman(p, 'hex'); + +// Confirm DH_check() results are exposed for optional examination. +const bad_dh = crypto.createDiffieHellman('02', 'hex'); +assert.notStrictEqual(bad_dh.verifyError, 0); + +const availableCurves = new Set(crypto.getCurves()); +const availableHashes = new Set(crypto.getHashes()); + +// Oakley curves do not clean up ERR stack, it was causing unexpected failure +// when accessing other OpenSSL APIs afterwards. +if (availableCurves.has('Oakley-EC2N-3')) { + crypto.createECDH('Oakley-EC2N-3'); + crypto.createHash('sha256'); +} + +// Test ECDH +if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) { + const ecdh1 = crypto.createECDH('prime256v1'); + const ecdh2 = crypto.createECDH('prime256v1'); + const key1 = ecdh1.generateKeys(); + const key2 = ecdh2.generateKeys('hex'); + const secret1 = ecdh1.computeSecret(key2, 'hex', 'base64'); + const secret2 = ecdh2.computeSecret(key1, 'latin1', 'buffer'); + + assert.strictEqual(secret1, secret2.toString('base64')); + + // Point formats + assert.strictEqual(ecdh1.getPublicKey('buffer', 'uncompressed')[0], 4); + let firstByte = ecdh1.getPublicKey('buffer', 'compressed')[0]; + assert(firstByte === 2 || firstByte === 3); + firstByte = ecdh1.getPublicKey('buffer', 'hybrid')[0]; + assert(firstByte === 6 || firstByte === 7); + // Format value should be string + + assert.throws( + () => ecdh1.getPublicKey('buffer', 10), + { + code: 'ERR_CRYPTO_ECDH_INVALID_FORMAT', + name: 'TypeError', + message: 'Invalid ECDH format: 10' + }); + + // ECDH should check that point is on curve + const ecdh3 = crypto.createECDH('secp256k1'); + const key3 = ecdh3.generateKeys(); + + assert.throws( + () => ecdh2.computeSecret(key3, 'latin1', 'buffer'), + { + code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + name: 'Error', + message: 'Public key is not valid for specified curve' + }); + + // ECDH should allow .setPrivateKey()/.setPublicKey() + const ecdh4 = crypto.createECDH('prime256v1'); + + ecdh4.setPrivateKey(ecdh1.getPrivateKey()); + ecdh4.setPublicKey(ecdh1.getPublicKey()); + + assert.throws(() => { + ecdh4.setPublicKey(ecdh3.getPublicKey()); + }, { message: 'Failed to convert Buffer to EC_POINT' }); + + // Verify that we can use ECDH without having to use newly generated keys. + const ecdh5 = crypto.createECDH('secp256k1'); + + // Verify errors are thrown when retrieving keys from an uninitialized object. + assert.throws(() => { + ecdh5.getPublicKey(); + }, /^Error: Failed to get ECDH public key$/); + + assert.throws(() => { + ecdh5.getPrivateKey(); + }, /^Error: Failed to get ECDH private key$/); + + // A valid private key for the secp256k1 curve. + const cafebabeKey = 'cafebabe'.repeat(8); + // Associated compressed and uncompressed public keys (points). + const cafebabePubPtComp = + '03672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3'; + const cafebabePubPtUnComp = + '04672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3' + + '2e02c7f93d13dc2732b760ca377a5897b9dd41a1c1b29dc0442fdce6d0a04d1d'; + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + // Show that the public point (key) is generated while setting the + // private key. + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Compressed and uncompressed public points/keys for other party's + // private key. + // 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF + const peerPubPtComp = + '02c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae'; + const peerPubPtUnComp = + '04c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae' + + 'b651944a574a362082a77e3f2b5d9223eb54d7f2f76846522bf75f3bedb8178e'; + + const sharedSecret = + '1da220b5329bbe8bfd19ceef5a5898593f411a6f12ea40f2a8eead9a5cf59970'; + + assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), + sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'), + sharedSecret); + + // Verify that we still have the same key pair as before the computation. + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Verify setting and getting compressed and non-compressed serializations. + ecdh5.setPublicKey(cafebabePubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + ecdh5.setPublicKey(cafebabePubPtUnComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + + // Show why allowing the public key to be set on this type + // does not make sense. + ecdh5.setPublicKey(peerPubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), peerPubPtUnComp); + assert.throws(() => { + // Error because the public key does not match the private key anymore. + ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'); + }, /Invalid key pair/); + + // Set to a valid key to show that later attempts to set an invalid key are + // rejected. + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + + // Some invalid private keys for the secp256k1 curve. + const errMessage = /Private key is not valid for specified curve/; + ['0000000000000000000000000000000000000000000000000000000000000000', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + ].forEach((element) => { + assert.throws(() => { + ecdh5.setPrivateKey(element, 'hex'); + }, errMessage); + // Verify object state did not change. + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + }); +} + +// Use of invalid keys was not cleaning up ERR stack, and was causing +// unexpected failure in subsequent signing operations. +if (availableCurves.has('prime256v1') && availableHashes.has('sha256')) { + const curve = crypto.createECDH('prime256v1'); + const invalidKey = Buffer.alloc(65); + invalidKey.fill('\0'); + curve.generateKeys(); + assert.throws( + () => curve.computeSecret(invalidKey), + { + code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + name: 'Error', + message: 'Public key is not valid for specified curve' + }); + // Check that signing operations are not impacted by the above error. + const ecPrivateKey = + '-----BEGIN EC PRIVATE KEY-----\n' + + 'MHcCAQEEIF+jnWY1D5kbVYDNvxxo/Y+ku2uJPDwS0r/VuPZQrjjVoAoGCCqGSM49\n' + + 'AwEHoUQDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNhB8i3mXyIMq704m2m52FdfKZ2\n' + + 'pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' + + '-----END EC PRIVATE KEY-----'; + crypto.createSign('SHA256').sign(ecPrivateKey); +} diff --git a/test/js/node/test/parallel/test-crypto-dh-group-setters.js b/test/js/node/test/parallel/test-crypto-dh-group-setters.js new file mode 100644 index 0000000000..55086e293e --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-group-setters.js @@ -0,0 +1,19 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L14 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Unlike DiffieHellman, DiffieHellmanGroup does not have any setters. +const dhg = crypto.getDiffieHellman('modp1'); +assert.strictEqual(dhg.constructor, crypto.DiffieHellmanGroup); +assert.strictEqual(dhg.setPrivateKey, undefined); +assert.strictEqual(dhg.setPublicKey, undefined); + +// */ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-dh-modp2-views.js b/test/js/node/test/parallel/test-crypto-dh-modp2-views.js new file mode 100644 index 0000000000..382d575a8a --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-modp2-views.js @@ -0,0 +1,30 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L16 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { modp2buf } = require('../common/crypto'); + +const modp2 = crypto.createDiffieHellmanGroup('modp2'); + +const views = common.getArrayBufferViews(modp2buf); +for (const buf of [modp2buf, ...views]) { + // Ensure specific generator (string with encoding) works as expected with + // any ArrayBufferViews as the first argument to createDiffieHellman(). + const exmodp2 = crypto.createDiffieHellman(buf, '02', 'hex'); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +*/ diff --git a/test/js/node/test/parallel/test-crypto-dh-modp2.js b/test/js/node/test/parallel/test-crypto-dh-modp2.js new file mode 100644 index 0000000000..c60f6aa456 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-modp2.js @@ -0,0 +1,49 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L15 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { modp2buf } = require('../common/crypto'); +const modp2 = crypto.createDiffieHellmanGroup('modp2'); + +{ + // Ensure specific generator (buffer) works as expected. + const exmodp2 = crypto.createDiffieHellman(modp2buf, Buffer.from([2])); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +{ + // Ensure specific generator (string without encoding) works as expected. + const exmodp2 = crypto.createDiffieHellman(modp2buf, '\x02'); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +{ + // Ensure specific generator (numeric) works as expected. + const exmodp2 = crypto.createDiffieHellman(modp2buf, 2); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +*/ diff --git a/test/js/node/test/parallel/test-crypto-dh-odd-key.js b/test/js/node/test/parallel/test-crypto-dh-odd-key.js new file mode 100644 index 0000000000..69a1eb56c8 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-odd-key.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +function test() { + const odd = Buffer.alloc(39, 'A'); + + const c = crypto.createDiffieHellman(common.hasOpenSSL3 ? 1024 : 32); + c.setPrivateKey(odd); + c.generateKeys(); +} + +// FIPS requires a length of at least 1024 +if (!common.hasFipsCrypto) { + test(); +} else { + assert.throws(function() { test(); }, /key size too small/); +} diff --git a/test/js/node/test/parallel/test-crypto-dh-shared.js b/test/js/node/test/parallel/test-crypto-dh-shared.js new file mode 100644 index 0000000000..515405034d --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-shared.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const alice = crypto.createDiffieHellmanGroup('modp5'); +const bob = crypto.createDiffieHellmanGroup('modp5'); +alice.generateKeys(); +bob.generateKeys(); +const aSecret = alice.computeSecret(bob.getPublicKey()).toString('hex'); +const bSecret = bob.computeSecret(alice.getPublicKey()).toString('hex'); +assert.strictEqual(aSecret, bSecret); diff --git a/test/js/node/test/parallel/test-crypto-domain.js b/test/js/node/test/parallel/test-crypto-domain.js new file mode 100644 index 0000000000..62e2be4c0f --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-domain.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const domain = require('domain'); + +const test = (fn) => { + const ex = new Error('BAM'); + const d = domain.create(); + d.on('error', common.mustCall(function(err) { + assert.strictEqual(err, ex); + })); + const cb = common.mustCall(function() { + throw ex; + }); + d.run(cb); +}; + +test(function(cb) { + crypto.pbkdf2('password', 'salt', 1, 8, cb); +}); + +test(function(cb) { + crypto.randomBytes(32, cb); +}); diff --git a/test/js/node/test/parallel/test-crypto-ecb.js b/test/js/node/test/parallel/test-crypto-ecb.js new file mode 100644 index 0000000000..aeb569cbfc --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-ecb.js @@ -0,0 +1,60 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L17 + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (common.hasFipsCrypto) + common.skip('BF-ECB is not FIPS 140-2 compatible'); + +if (common.hasOpenSSL3) + common.skip('Blowfish is only available with the legacy provider in ' + + 'OpenSSl 3.x'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Testing whether EVP_CipherInit_ex is functioning correctly. +// Reference: bug#1997 + +{ + const encrypt = + crypto.createCipheriv('BF-ECB', 'SomeRandomBlahz0c5GZVnR', ''); + let hex = encrypt.update('Hello World!', 'ascii', 'hex'); + hex += encrypt.final('hex'); + assert.strictEqual(hex.toUpperCase(), '6D385F424AAB0CFBF0BB86E07FFB7D71'); +} + +{ + const decrypt = + crypto.createDecipheriv('BF-ECB', 'SomeRandomBlahz0c5GZVnR', ''); + let msg = decrypt.update('6D385F424AAB0CFBF0BB86E07FFB7D71', 'hex', 'ascii'); + msg += decrypt.final('ascii'); + assert.strictEqual(msg, 'Hello World!'); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-ecdh-convert-key.js b/test/js/node/test/parallel/test-crypto-ecdh-convert-key.js new file mode 100644 index 0000000000..c0046099df --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-ecdh-convert-key.js @@ -0,0 +1,125 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const { ECDH, createSign, getCurves } = require('crypto'); + +// A valid private key for the secp256k1 curve. +const cafebabeKey = 'cafebabe'.repeat(8); +// Associated compressed and uncompressed public keys (points). +const cafebabePubPtComp = + '03672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3'; +const cafebabePubPtUnComp = + '04672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3' + + '2e02c7f93d13dc2732b760ca377a5897b9dd41a1c1b29dc0442fdce6d0a04d1d'; + +// Invalid test: key argument is undefined. +assert.throws( + () => ECDH.convertKey(), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + +// Invalid test: curve argument is undefined. +assert.throws( + () => ECDH.convertKey(cafebabePubPtComp), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + +// Invalid test: curve argument is invalid. +assert.throws( + () => ECDH.convertKey(cafebabePubPtComp, 'badcurve'), + { + name: 'TypeError', + message: 'Invalid EC curve name' + }); + +if (getCurves().includes('secp256k1')) { + // Invalid test: format argument is undefined. + assert.throws( + () => ECDH.convertKey(cafebabePubPtComp, 'secp256k1', 'hex', 'hex', 10), + { + code: 'ERR_CRYPTO_ECDH_INVALID_FORMAT', + name: 'TypeError', + message: 'Invalid ECDH format: 10' + }); + + // Point formats. + let uncompressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'buffer', + 'uncompressed'); + let compressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'buffer', + 'compressed'); + let hybrid = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'buffer', + 'hybrid'); + assert.strictEqual(uncompressed[0], 4); + let firstByte = compressed[0]; + assert(firstByte === 2 || firstByte === 3); + firstByte = hybrid[0]; + assert(firstByte === 6 || firstByte === 7); + + // Format conversion from hex to hex + uncompressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'hex', + 'uncompressed'); + compressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'hex', + 'compressed'); + hybrid = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'hex', + 'hybrid'); + assert.strictEqual(uncompressed, cafebabePubPtUnComp); + assert.strictEqual(compressed, cafebabePubPtComp); + + // Compare to getPublicKey. + const ecdh1 = ECDH('secp256k1'); + ecdh1.generateKeys(); + ecdh1.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh1.getPublicKey('hex', 'uncompressed'), uncompressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'compressed'), compressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'hybrid'), hybrid); +} + +// See https://github.com/nodejs/node/issues/26133, failed ConvertKey +// operations should not leave errors on OpenSSL's error stack because +// that's observable by subsequent operations. +{ + const privateKey = + '-----BEGIN EC PRIVATE KEY-----\n' + + 'MHcCAQEEIF+jnWY1D5kbVYDNvxxo/Y+ku2uJPDwS0r/VuPZQrjjVoAoGCCqGSM49\n' + + 'AwEHoUQDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNhB8i3mXyIMq704m2m52FdfKZ2\n' + + 'pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' + + '-----END EC PRIVATE KEY-----'; + + const sign = createSign('sha256').update('plaintext'); + + // TODO(bnoordhuis) This should really bubble up the specific OpenSSL error + // rather than Node's generic error message. + const badKey = 'f'.repeat(128); + assert.throws( + () => ECDH.convertKey(badKey, 'secp521r1', 'hex', 'hex', 'compressed'), + /Failed to convert Buffer to EC_POINT/); + + // Next statement should not throw an exception. + sign.sign(privateKey); +} diff --git a/test/js/node/test/parallel/test-crypto-fips.js b/test/js/node/test/parallel/test-crypto-fips.js new file mode 100644 index 0000000000..c862b617b2 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-fips.js @@ -0,0 +1,285 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L19 + +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const spawnSync = require('child_process').spawnSync; +const path = require('path'); +const fixtures = require('../common/fixtures'); +const { internalBinding } = require('internal/test/binding'); +const { testFipsCrypto } = internalBinding('crypto'); + +const FIPS_ENABLED = 1; +const FIPS_DISABLED = 0; +const FIPS_ERROR_STRING2 = + 'Error [ERR_CRYPTO_FIPS_FORCED]: Cannot set FIPS mode, it was forced with ' + + '--force-fips at startup.'; +const FIPS_UNSUPPORTED_ERROR_STRING = 'fips mode not supported'; +const FIPS_ENABLE_ERROR_STRING = 'OpenSSL error when trying to enable FIPS:'; + +const CNF_FIPS_ON = fixtures.path('openssl_fips_enabled.cnf'); +const CNF_FIPS_OFF = fixtures.path('openssl_fips_disabled.cnf'); + +let num_children_ok = 0; + +function sharedOpenSSL() { + return process.config.variables.node_shared_openssl; +} + +function testHelper(stream, args, expectedOutput, cmd, env) { + const fullArgs = args.concat(['-e', `console.log(${cmd})`]); + const child = spawnSync(process.execPath, fullArgs, { + cwd: path.dirname(process.execPath), + env: env + }); + + console.error( + `Spawned child [pid:${child.pid}] with cmd '${cmd}' expect %j with args '${ + args}' OPENSSL_CONF=%j`, expectedOutput, env.OPENSSL_CONF); + + function childOk(child) { + console.error(`Child #${++num_children_ok} [pid:${child.pid}] OK.`); + } + + function responseHandler(buffer, expectedOutput) { + const response = buffer.toString(); + assert.notStrictEqual(response.length, 0); + if (FIPS_ENABLED !== expectedOutput && FIPS_DISABLED !== expectedOutput) { + // In the case of expected errors just look for a substring. + assert.ok(response.includes(expectedOutput)); + } else { + const getFipsValue = Number(response); + if (!Number.isNaN(getFipsValue)) + // Normal path where we expect either FIPS enabled or disabled. + assert.strictEqual(getFipsValue, expectedOutput); + } + childOk(child); + } + + responseHandler(child[stream], expectedOutput); +} + +// --enable-fips should raise an error if OpenSSL is not FIPS enabled. +testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_ENABLE_ERROR_STRING, + 'process.versions', + process.env); + +// --force-fips should raise an error if OpenSSL is not FIPS enabled. +testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_ENABLE_ERROR_STRING, + 'process.versions', + process.env); + +// By default FIPS should be off in both FIPS and non-FIPS builds +// unless Node.js was configured using --shared-openssl in +// which case it may be enabled by the system. +if (!sharedOpenSSL()) { + testHelper( + 'stdout', + [], + FIPS_DISABLED, + 'require("crypto").getFips()', + { ...process.env, 'OPENSSL_CONF': ' ' }); +} + +// Toggling fips with setFips should not be allowed from a worker thread +testHelper( + 'stderr', + [], + 'Calling crypto.setFips() is not supported in workers', + 'new worker_threads.Worker(\'require("crypto").setFips(true);\', { eval: true })', + process.env); + +// This should succeed for both FIPS and non-FIPS builds in combination with +// OpenSSL 1.1.1 or OpenSSL 3.0 +const test_result = testFipsCrypto(); +assert.ok(test_result === 1 || test_result === 0); + +// If Node was configured using --shared-openssl fips support might be +// available depending on how OpenSSL was built. If fips support is +// available the tests that toggle the fips_mode on/off using the config +// file option will succeed and return 1 instead of 0. +// +// Note that this case is different from when calling the fips setter as the +// configuration file is handled by OpenSSL, so it is not possible for us +// to try to call the fips setter, to try to detect this situation, as +// that would throw an error: +// ("Error: Cannot set FIPS mode in a non-FIPS build."). +// Due to this uncertainty the following tests are skipped when configured +// with --shared-openssl. +if (!sharedOpenSSL() && !common.hasOpenSSL3) { + // OpenSSL config file should be able to turn on FIPS mode + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_ON}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_DISABLED, + 'require("crypto").getFips()', + process.env); + + // OPENSSL_CONF should be able to turn on FIPS mode + testHelper( + 'stdout', + [], + testFipsCrypto() ? FIPS_ENABLED : FIPS_DISABLED, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_ON })); + + // --openssl-config option should override OPENSSL_CONF + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_ON}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_DISABLED, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_OFF })); +} + +// OpenSSL 3.x has changed the configuration files so the following tests +// will not work as expected with that version. +// TODO(danbev) Revisit these test once FIPS support is available in +// OpenSSL 3.x. +if (!common.hasOpenSSL3) { + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_OFF}`], + FIPS_DISABLED, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_ON })); + + // --enable-fips should take precedence over OpenSSL config file + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips', `--openssl-config=${CNF_FIPS_OFF}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + // --force-fips should take precedence over OpenSSL config file + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips', `--openssl-config=${CNF_FIPS_OFF}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + // --enable-fips should turn FIPS mode on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + + // --force-fips should turn FIPS mode on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + + // OPENSSL_CONF should _not_ make a difference to --enable-fips + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_OFF })); + + // Using OPENSSL_CONF should not make a difference to --force-fips + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_OFF })); + + // setFipsCrypto should be able to turn FIPS mode on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + [], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").getFips())', + process.env); + + // setFipsCrypto should be able to turn FIPS mode on and off + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + [], + testFipsCrypto() ? FIPS_DISABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").setFips(false),' + + 'require("crypto").getFips())', + process.env); + + // setFipsCrypto takes precedence over OpenSSL config file, FIPS on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + [`--openssl-config=${CNF_FIPS_OFF}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").getFips())', + process.env); + + // setFipsCrypto takes precedence over OpenSSL config file, FIPS off + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_ON}`], + FIPS_DISABLED, + '(require("crypto").setFips(false),' + + 'require("crypto").getFips())', + process.env); + + // --enable-fips does not prevent use of setFipsCrypto API + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_DISABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(false),' + + 'require("crypto").getFips())', + process.env); + + // --force-fips prevents use of setFipsCrypto API + testHelper( + 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ERROR_STRING2 : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").setFips(false)', + process.env); + + // --force-fips makes setFipsCrypto enable a no-op (FIPS stays on) + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").getFips())', + process.env); + + // --force-fips and --enable-fips order does not matter + testHelper( + 'stderr', + ['--force-fips', '--enable-fips'], + testFipsCrypto() ? FIPS_ERROR_STRING2 : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").setFips(false)', + process.env); + + // --enable-fips and --force-fips order does not matter + testHelper( + 'stderr', + ['--enable-fips', '--force-fips'], + testFipsCrypto() ? FIPS_ERROR_STRING2 : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").setFips(false)', + process.env); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-from-binary.js b/test/js/node/test/parallel/test-crypto-from-binary.js new file mode 100644 index 0000000000..3b3c6a81ef --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-from-binary.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// This is the same as test/simple/test-crypto, but from before the shift +// to use buffers by default. + + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const EXTERN_APEX = 0xFBEE9; + +// Manually controlled string for checking binary output +let ucs2_control = 'a\u0000'; + +// Grow the strings to proper length +while (ucs2_control.length <= EXTERN_APEX) { + ucs2_control = ucs2_control.repeat(2); +} + + +// Check resultant buffer and output string +const b = Buffer.from(ucs2_control + ucs2_control, 'ucs2'); + +// +// Test updating from birant data +// +{ + const datum1 = b.slice(700000); + const hash1_converted = crypto.createHash('sha1') + .update(datum1.toString('base64'), 'base64') + .digest('hex'); + const hash1_direct = crypto.createHash('sha1').update(datum1).digest('hex'); + assert.strictEqual(hash1_direct, hash1_converted); + + const datum2 = b; + const hash2_converted = crypto.createHash('sha1') + .update(datum2.toString('base64'), 'base64') + .digest('hex'); + const hash2_direct = crypto.createHash('sha1').update(datum2).digest('hex'); + assert.strictEqual(hash2_direct, hash2_converted); +} diff --git a/test/js/node/test/parallel/test-crypto-getcipherinfo.js b/test/js/node/test/parallel/test-crypto-getcipherinfo.js new file mode 100644 index 0000000000..fd41073b66 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-getcipherinfo.js @@ -0,0 +1,74 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { + getCiphers, + getCipherInfo +} = require('crypto'); + +const assert = require('assert'); + +const ciphers = getCiphers(); + +assert.strictEqual(getCipherInfo(-1), undefined); +assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined); + +for (const cipher of ciphers) { + const info = getCipherInfo(cipher); + assert(info); + const info2 = getCipherInfo(info.nid); + assert.deepStrictEqual(info, info2); +} + +const info = getCipherInfo('aes-128-cbc'); +assert.strictEqual(info.name, 'AES-128-CBC'); +assert.strictEqual(info.nid, 419); +assert.strictEqual(info.blockSize, 16); +assert.strictEqual(info.ivLength, 16); +assert.strictEqual(info.keyLength, 16); +assert.strictEqual(info.mode, 'cbc'); + +[null, undefined, [], {}].forEach((arg) => { + assert.throws(() => getCipherInfo(arg), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[null, '', 1, true].forEach((options) => { + assert.throws( + () => getCipherInfo('aes-192-cbc', options), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[null, '', {}, [], true].forEach((len) => { + assert.throws( + () => getCipherInfo('aes-192-cbc', { keyLength: len }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws( + () => getCipherInfo('aes-192-cbc', { ivLength: len }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +assert(!getCipherInfo('aes-128-cbc', { keyLength: 12 })); +assert(getCipherInfo('aes-128-cbc', { keyLength: 16 })); +assert(!getCipherInfo('aes-128-cbc', { ivLength: 12 })); +assert(getCipherInfo('aes-128-cbc', { ivLength: 16 })); + +assert(!getCipherInfo('aes-128-ccm', { ivLength: 1 })); +assert(!getCipherInfo('aes-128-ccm', { ivLength: 14 })); +if (!common.openSSLIsBoringSSL) { + for (let n = 7; n <= 13; n++) + assert(getCipherInfo('aes-128-ccm', { ivLength: n })); +} + +assert(!getCipherInfo('aes-128-ocb', { ivLength: 16 })); +if (!common.openSSLIsBoringSSL) { +for (let n = 1; n < 16; n++) + assert(getCipherInfo('aes-128-ocb', { ivLength: n })); +} diff --git a/test/js/node/test/parallel/test-crypto-key-objects.js b/test/js/node/test/parallel/test-crypto-key-objects.js new file mode 100644 index 0000000000..b63b1c9054 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-key-objects.js @@ -0,0 +1,894 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L20 + +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createCipheriv, + createDecipheriv, + createSign, + createVerify, + createSecretKey, + createPublicKey, + createPrivateKey, + KeyObject, + randomBytes, + publicDecrypt, + publicEncrypt, + privateDecrypt, + privateEncrypt, + getCurves, + generateKeySync, + generateKeyPairSync, +} = require('crypto'); + +const fixtures = require('../common/fixtures'); + +const publicPem = fixtures.readKey('rsa_public.pem', 'ascii'); +const privatePem = fixtures.readKey('rsa_private.pem', 'ascii'); + +const publicDsa = fixtures.readKey('dsa_public_1025.pem', 'ascii'); +const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + 'ascii'); + +{ + // Attempting to create a key of a wrong type should throw + const TYPE = 'wrong_type'; + + assert.throws(() => new KeyObject(TYPE), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: `The argument 'type' is invalid. Received '${TYPE}'` + }); +} + +{ + // Attempting to create a key with non-object handle should throw + assert.throws(() => new KeyObject('secret', ''), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "handle" argument must be of type object. Received type ' + + "string ('')" + }); +} + +{ + assert.throws(() => KeyObject.from('invalid_key'), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "key" argument must be an instance of CryptoKey. Received type ' + + "string ('invalid_key')" + }); +} + +{ + const keybuf = randomBytes(32); + const key = createSecretKey(keybuf); + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.toString(), '[object KeyObject]'); + assert.strictEqual(key.symmetricKeySize, 32); + assert.strictEqual(key.asymmetricKeyType, undefined); + assert.strictEqual(key.asymmetricKeyDetails, undefined); + + const exportedKey = key.export(); + assert(keybuf.equals(exportedKey)); + + const plaintext = Buffer.from('Hello world', 'utf8'); + + const cipher = createCipheriv('aes-256-ecb', key, null); + const ciphertext = Buffer.concat([ + cipher.update(plaintext), cipher.final(), + ]); + + const decipher = createDecipheriv('aes-256-ecb', key, null); + const deciphered = Buffer.concat([ + decipher.update(ciphertext), decipher.final(), + ]); + + assert(plaintext.equals(deciphered)); +} + +{ + // Passing an existing public key object to createPublicKey should throw. + const publicKey = createPublicKey(publicPem); + assert.throws(() => createPublicKey(publicKey), { + name: 'TypeError', + code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + message: 'Invalid key object type public, expected private.' + }); + + // Constructing a private key from a public key should be impossible, even + // if the public key was derived from a private key. + assert.throws(() => createPrivateKey(createPublicKey(privatePem)), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + + // Similarly, passing an existing private key object to createPrivateKey + // should throw. + const privateKey = createPrivateKey(privatePem); + assert.throws(() => createPrivateKey(privateKey), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +{ + const jwk = { + e: 'AQAB', + n: 't9xYiIonscC3vz_A2ceR7KhZZlDu_5bye53nCVTcKnWd2seY6UAdKersX6njr83Dd5OVe' + + '1BW_wJvp5EjWTAGYbFswlNmeD44edEGM939B6Lq-_8iBkrTi8mGN4YCytivE24YI0D4XZ' + + 'MPfkLSpab2y_Hy4DjQKBq1ThZ0UBnK-9IhX37Ju_ZoGYSlTIGIhzyaiYBh7wrZBoPczIE' + + 'u6et_kN2VnnbRUtkYTF97ggcv5h-hDpUQjQW0ZgOMcTc8n-RkGpIt0_iM_bTjI3Tz_gsF' + + 'di6hHcpZgbopPL630296iByyigQCPJVzdusFrQN5DeC-zT_nGypQkZanLb4ZspSx9Q', + d: 'ktnq2LvIMqBj4txP82IEOorIRQGVsw1khbm8A-cEpuEkgM71Yi_0WzupKktucUeevQ5i0' + + 'Yh8w9e1SJiTLDRAlJz66kdky9uejiWWl6zR4dyNZVMFYRM43ijLC-P8rPne9Fz16IqHFW' + + '5VbJqA1xCBhKmuPMsD71RNxZ4Hrsa7Kt_xglQTYsLbdGIwDmcZihId9VGXRzvmCPsDRf2' + + 'fCkAj7HDeRxpUdEiEDpajADc-PWikra3r3b40tVHKWm8wxJLivOIN7GiYXKQIW6RhZgH-' + + 'Rk45JIRNKxNagxdeXUqqyhnwhbTo1Hite0iBDexN9tgoZk0XmdYWBn6ElXHRZ7VCDQ', + p: '8UovlB4nrBm7xH-u7XXBMbqxADQm5vaEZxw9eluc-tP7cIAI4sglMIvL_FMpbd2pEeP_B' + + 'kR76NTDzzDuPAZvUGRavgEjy0O9j2NAs_WPK4tZF-vFdunhnSh4EHAF4Ij9kbsUi90NOp' + + 'bGfVqPdOaHqzgHKoR23Cuusk9wFQ2XTV8', + q: 'wxHdEYT9xrpfrHPqSBQPpO0dWGKJEkrWOb-76rSfuL8wGR4OBNmQdhLuU9zTIh22pog-X' + + 'PnLPAecC-4yu_wtJ2SPCKiKDbJBre0CKPyRfGqzvA3njXwMxXazU4kGs-2Fg-xu_iKbaI' + + 'jxXrclBLhkxhBtySrwAFhxxOk6fFcPLSs', + dp: 'qS_Mdr5CMRGGMH0bKhPUWEtAixUGZhJaunX5wY71Xoc_Gh4cnO-b7BNJ_-5L8WZog0vr' + + '6PgiLhrqBaCYm2wjpyoG2o2wDHm-NAlzN_wp3G2EFhrSxdOux-S1c0kpRcyoiAO2n29rN' + + 'Da-jOzwBBcU8ACEPdLOCQl0IEFFJO33tl8', + dq: 'WAziKpxLKL7LnL4dzDcx8JIPIuwnTxh0plCDdCffyLaT8WJ9lXbXHFTjOvt8WfPrlDP_' + + 'Ylxmfkw5BbGZOP1VLGjZn2DkH9aMiwNmbDXFPdG0G3hzQovx_9fajiRV4DWghLHeT9wzJ' + + 'fZabRRiI0VQR472300AVEeX4vgbrDBn600', + qi: 'k7czBCT9rHn_PNwCa17hlTy88C4vXkwbz83Oa-aX5L4e5gw5lhcR2ZuZHLb2r6oMt9rl' + + 'D7EIDItSs-u21LOXWPTAlazdnpYUyw_CzogM_PN-qNwMRXn5uXFFhmlP2mVg2EdELTahX' + + 'ch8kWqHaCSX53yvqCtRKu_j76V31TfQZGM', + kty: 'RSA', + }; + const publicJwk = { kty: jwk.kty, e: jwk.e, n: jwk.n }; + + const publicKey = createPublicKey(publicPem); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.toString(), '[object KeyObject]'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(publicKey.symmetricKeySize, undefined); + + const privateKey = createPrivateKey(privatePem); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.toString(), '[object KeyObject]'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(privateKey.symmetricKeySize, undefined); + + // It should be possible to derive a public key from a private key. + const derivedPublicKey = createPublicKey(privateKey); + assert.strictEqual(derivedPublicKey.type, 'public'); + assert.strictEqual(derivedPublicKey.toString(), '[object KeyObject]'); + assert.strictEqual(derivedPublicKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(derivedPublicKey.symmetricKeySize, undefined); + + const publicKeyFromJwk = createPublicKey({ key: publicJwk, format: 'jwk' }); + assert.strictEqual(publicKeyFromJwk.type, 'public'); + assert.strictEqual(publicKeyFromJwk.toString(), '[object KeyObject]'); + assert.strictEqual(publicKeyFromJwk.asymmetricKeyType, 'rsa'); + assert.strictEqual(publicKeyFromJwk.symmetricKeySize, undefined); + + const privateKeyFromJwk = createPrivateKey({ key: jwk, format: 'jwk' }); + assert.strictEqual(privateKeyFromJwk.type, 'private'); + assert.strictEqual(privateKeyFromJwk.toString(), '[object KeyObject]'); + assert.strictEqual(privateKeyFromJwk.asymmetricKeyType, 'rsa'); + assert.strictEqual(privateKeyFromJwk.symmetricKeySize, undefined); + + // It should also be possible to import an encrypted private key as a public + // key. + const decryptedKey = createPublicKey({ + key: privateKey.export({ + type: 'pkcs8', + format: 'pem', + passphrase: '123', + cipher: 'aes-128-cbc' + }), + format: 'pem', + passphrase: '123' + }); + assert.strictEqual(decryptedKey.type, 'public'); + assert.strictEqual(decryptedKey.asymmetricKeyType, 'rsa'); + + // Test exporting with an invalid options object, this should throw. + for (const opt of [undefined, null, 'foo', 0, NaN]) { + assert.throws(() => publicKey.export(opt), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "options" argument must be of type object/ + }); + } + + for (const keyObject of [publicKey, derivedPublicKey, publicKeyFromJwk]) { + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + { kty: 'RSA', n: jwk.n, e: jwk.e } + ); + } + + for (const keyObject of [privateKey, privateKeyFromJwk]) { + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + jwk + ); + } + + // Exporting the key using JWK should not work since this format does not + // support key encryption + assert.throws(() => { + privateKey.export({ format: 'jwk', passphrase: 'secret' }); + }, { + message: 'The selected key encoding jwk does not support encryption.', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' + }); + + const publicDER = publicKey.export({ + format: 'der', + type: 'pkcs1' + }); + + const privateDER = privateKey.export({ + format: 'der', + type: 'pkcs1' + }); + + assert(Buffer.isBuffer(publicDER)); + assert(Buffer.isBuffer(privateDER)); + + const plaintext = Buffer.from('Hello world', 'utf8'); + const testDecryption = (fn, ciphertexts, decryptionKeys) => { + for (const ciphertext of ciphertexts) { + for (const key of decryptionKeys) { + const deciphered = fn(key, ciphertext); + assert.deepStrictEqual(deciphered, plaintext); + } + } + }; + + testDecryption(privateDecrypt, [ + // Encrypt using the public key. + publicEncrypt(publicKey, plaintext), + publicEncrypt({ key: publicKey }, plaintext), + publicEncrypt({ key: publicJwk, format: 'jwk' }, plaintext), + + // Encrypt using the private key. + publicEncrypt(privateKey, plaintext), + publicEncrypt({ key: privateKey }, plaintext), + publicEncrypt({ key: jwk, format: 'jwk' }, plaintext), + + // Encrypt using a public key derived from the private key. + publicEncrypt(derivedPublicKey, plaintext), + publicEncrypt({ key: derivedPublicKey }, plaintext), + + // Test distinguishing PKCS#1 public and private keys based on the + // DER-encoded data only. + publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext), + publicEncrypt({ format: 'der', type: 'pkcs1', key: privateDER }, plaintext), + ], [ + privateKey, + { format: 'pem', key: privatePem }, + { format: 'der', type: 'pkcs1', key: privateDER }, + { key: jwk, format: 'jwk' }, + ]); + + testDecryption(publicDecrypt, [ + privateEncrypt(privateKey, plaintext), + ], [ + // Decrypt using the public key. + publicKey, + { format: 'pem', key: publicPem }, + { format: 'der', type: 'pkcs1', key: publicDER }, + { key: publicJwk, format: 'jwk' }, + + // Decrypt using the private key. + privateKey, + { format: 'pem', key: privatePem }, + { format: 'der', type: 'pkcs1', key: privateDER }, + { key: jwk, format: 'jwk' }, + ]); +} + +{ + // This should not cause a crash: https://github.com/nodejs/node/issues/25247 + assert.throws(() => { + createPrivateKey({ key: '' }); + }, common.hasOpenSSL3 ? { + message: 'error:1E08010C:DECODER routines::unsupported', + } : { + message: 'error:0909006C:PEM routines:get_name:no start line', + code: 'ERR_OSSL_PEM_NO_START_LINE', + reason: 'no start line', + library: 'PEM routines', + function: 'get_name', + }); + + // This should not abort either: https://github.com/nodejs/node/issues/29904 + assert.throws(() => { + createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'spki' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.type' is invalid. Received 'spki'" + }); + + // Unlike SPKI, PKCS#1 is a valid encoding for private keys (and public keys), + // so it should be accepted by createPrivateKey, but OpenSSL won't parse it. + assert.throws(() => { + const key = createPublicKey(publicPem).export({ + format: 'der', + type: 'pkcs1' + }); + createPrivateKey({ key, format: 'der', type: 'pkcs1' }); + }, common.hasOpenSSL3 ? { + message: /error:1E08010C:DECODER routines::unsupported/, + library: 'DECODER routines' + } : { + message: /asn1 encoding/, + library: 'asn1 encoding routines' + }); +} + +[ + { private: fixtures.readKey('ed25519_private.pem', 'ascii'), + public: fixtures.readKey('ed25519_public.pem', 'ascii'), + keyType: 'ed25519', + jwk: { + crv: 'Ed25519', + x: 'K1wIouqnuiA04b3WrMa-xKIKIpfHetNZRv3h9fBf768', + d: 'wVK6M3SMhQh3NK-7GRrSV-BVWQx1FO5pW8hhQeu_NdA', + kty: 'OKP' + } }, + { private: fixtures.readKey('ed448_private.pem', 'ascii'), + public: fixtures.readKey('ed448_public.pem', 'ascii'), + keyType: 'ed448', + jwk: { + crv: 'Ed448', + x: 'oX_ee5-jlcU53-BbGRsGIzly0V-SZtJ_oGXY0udf84q2hTW2RdstLktvwpkVJOoNb7o' + + 'Dgc2V5ZUA', + d: '060Ke71sN0GpIc01nnGgMDkp0sFNQ09woVo4AM1ffax1-mjnakK0-p-S7-Xf859QewX' + + 'jcR9mxppY', + kty: 'OKP' + } }, + { private: fixtures.readKey('x25519_private.pem', 'ascii'), + public: fixtures.readKey('x25519_public.pem', 'ascii'), + keyType: 'x25519', + jwk: { + crv: 'X25519', + x: 'aSb8Q-RndwfNnPeOYGYPDUN3uhAPnMLzXyfi-mqfhig', + d: 'mL_IWm55RrALUGRfJYzw40gEYWMvtRkesP9mj8o8Omc', + kty: 'OKP' + } }, + { private: fixtures.readKey('x448_private.pem', 'ascii'), + public: fixtures.readKey('x448_public.pem', 'ascii'), + keyType: 'x448', + jwk: { + crv: 'X448', + x: 'ioHSHVpTs6hMvghosEJDIR7ceFiE3-Xccxati64oOVJ7NWjfozE7ae31PXIUFq6cVYg' + + 'vSKsDFPA', + d: 'tMNtrO_q8dlY6Y4NDeSTxNQ5CACkHiPvmukidPnNIuX_EkcryLEXt_7i6j6YZMKsrWy' + + 'S0jlSYJk', + kty: 'OKP' + } }, +].forEach((info) => { + const keyType = info.keyType; + + { + const key = createPrivateKey(info.private); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + for (const input of [ + info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const key = createPublicKey(input); + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'spki', format: 'pem' }), info.public); + const jwk = { ...info.jwk }; + delete jwk.d; + assert.deepStrictEqual( + key.export({ format: 'jwk' }), jwk); + } + } +}); + +[ + { private: fixtures.readKey('ec_p256_private.pem', 'ascii'), + public: fixtures.readKey('ec_p256_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'prime256v1', + jwk: { + crv: 'P-256', + d: 'DxBsPQPIgMuMyQbxzbb9toew6Ev6e9O6ZhpxLNgmAEo', + kty: 'EC', + x: 'X0mMYR_uleZSIPjNztIkAS3_ud5LhNpbiIFp6fNf2Gs', + y: 'UbJuPy2Xi0lW7UYTBxPK3yGgDu9EAKYIecjkHX5s2lI' + } }, + { private: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), + public: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp256k1', + jwk: { + crv: 'secp256k1', + d: 'c34ocwTwpFa9NZZh3l88qXyrkoYSxvC0FEsU5v1v4IM', + kty: 'EC', + x: 'cOzhFSpWxhalCbWNdP2H_yUkdC81C9T2deDpfxK7owA', + y: '-A3DAZTk9IPppN-f03JydgHaFvL1fAHaoXf4SX4NXyo' + } }, + { private: fixtures.readKey('ec_p384_private.pem', 'ascii'), + public: fixtures.readKey('ec_p384_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp384r1', + jwk: { + crv: 'P-384', + d: 'dwfuHuAtTlMRn7ZBCBm_0grpc1D_4hPeNAgevgelljuC0--k_LDFosDgBlLLmZsi', + kty: 'EC', + x: 'hON3nzGJgv-08fdHpQxgRJFZzlK-GZDGa5f3KnvM31cvvjJmsj4UeOgIdy3rDAjV', + y: 'fidHhtecNCGCfLqmrLjDena1NSzWzWH1u_oUdMKGo5XSabxzD7-8JZxjpc8sR9cl' + } }, + { private: fixtures.readKey('ec_p521_private.pem', 'ascii'), + public: fixtures.readKey('ec_p521_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp521r1', + jwk: { + crv: 'P-521', + d: 'ABIIbmn3Gm_Y11uIDkC3g2ijpRxIrJEBY4i_JJYo5OougzTl3BX2ifRluPJMaaHcNer' + + 'bQH_WdVkLLX86ShlHrRyJ', + kty: 'EC', + x: 'AaLFgjwZtznM3N7qsfb86awVXe6c6djUYOob1FN-kllekv0KEXV0bwcDjPGQz5f6MxL' + + 'CbhMeHRavUS6P10rsTtBn', + y: 'Ad3flexBeAfXceNzRBH128kFbOWD6W41NjwKRqqIF26vmgW_8COldGKZjFkOSEASxPB' + + 'cvA2iFJRUyQ3whC00j0Np' + } }, +].forEach((info) => { + const { keyType, namedCurve } = info; + + { + const key = createPrivateKey(info.private); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + for (const input of [ + info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const key = createPublicKey(input); + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'spki', format: 'pem' }), info.public); + const jwk = { ...info.jwk }; + delete jwk.d; + assert.deepStrictEqual( + key.export({ format: 'jwk' }), jwk); + } + } +}); + +{ + // Reading an encrypted key without a passphrase should fail. + assert.throws(() => createPrivateKey(privateDsa), common.hasOpenSSL3 ? { + name: 'Error', + message: 'error:07880109:common libcrypto routines::interrupted or ' + + 'cancelled', + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + + // Reading an encrypted key with a passphrase that exceeds OpenSSL's buffer + // size limit should fail with an appropriate error code. + assert.throws(() => createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: Buffer.alloc(1025, 'a') + }), common.hasOpenSSL3 ? { name: 'Error' } : { + code: 'ERR_OSSL_PEM_BAD_PASSWORD_READ', + name: 'Error' + }); + + // The buffer has a size of 1024 bytes, so this passphrase should be permitted + // (but will fail decryption). + assert.throws(() => createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: Buffer.alloc(1024, 'a') + }), { + message: /bad decrypt/ + }); + + const publicKey = createPublicKey(publicDsa); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'dsa'); + assert.strictEqual(publicKey.symmetricKeySize, undefined); + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); + + const privateKey = createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: 'secret' + }); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'dsa'); + assert.strictEqual(privateKey.symmetricKeySize, undefined); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); +} + +{ + // Test RSA-PSS. + { + // This key pair does not restrict the message digest algorithm or salt + // length. + const publicPem = fixtures.readKey('rsa_pss_public_2048.pem'); + const privatePem = fixtures.readKey('rsa_pss_private_2048.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Because no RSASSA-PSS-params appears in the PEM, no defaults should be + // added for the PSS parameters. This is different from an empty + // RSASSA-PSS-params sequence (see test below). + const expectedKeyDetails = { + modulusLength: 2048, + publicExponent: 65537n + }; + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); + + for (const key of [privatePem, privateKey]) { + // Any algorithm should work. + for (const algo of ['sha1', 'sha256']) { + // Any salt length should work. + for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { + const signature = createSign(algo) + .update('foo') + .sign({ key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify(algo) + .update('foo') + .verify({ key: pkey, saltLength }, signature); + + assert.ok(okay); + } + } + } + } + + // Exporting the key using PKCS#1 should not work since this would discard + // any algorithm restrictions. + assert.throws(() => { + publicKey.export({ format: 'pem', type: 'pkcs1' }); + }, { + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' + }); + } + + { + // This key pair enforces sha1 as the message digest and the MGF1 + // message digest and a salt length of 20 bytes. + + const publicPem = fixtures.readKey('rsa_pss_public_2048_sha1_sha1_20.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha1_sha1_20.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Unlike the previous key pair, this key pair contains an RSASSA-PSS-params + // sequence. However, because all values in the RSASSA-PSS-params are set to + // their defaults (see RFC 3447), the ASN.1 structure contains an empty + // sequence. Node.js should add the default values to the key details. + const expectedKeyDetails = { + modulusLength: 2048, + publicExponent: 65537n, + hashAlgorithm: 'sha1', + mgf1HashAlgorithm: 'sha1', + saltLength: 20 + }; + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, expectedKeyDetails); + } + + { + // This key pair enforces sha256 as the message digest and the MGF1 + // message digest and a salt length of at least 16 bytes. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha256_sha256_16.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + + for (const key of [privatePem, privateKey]) { + // Signing with anything other than sha256 should fail. + assert.throws(() => { + createSign('sha1').sign(key); + }, /digest not allowed/); + + // Signing with salt lengths less than 16 bytes should fail. + for (const saltLength of [8, 10, 12]) { + assert.throws(() => { + createSign('sha1').sign({ key, saltLength }); + }, /pss saltlen too small/); + } + + // Signing with sha256 and appropriate salt lengths should work. + for (const saltLength of [undefined, 16, 18, 20]) { + const signature = createSign('sha256') + .update('foo') + .sign({ key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify('sha256') + .update('foo') + .verify({ key: pkey, saltLength }, signature); + + assert.ok(okay); + } + } + } + } + + { + // This key enforces sha512 as the message digest and sha256 as the MGF1 + // message digest. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha512_sha256_20.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha512_sha256_20.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + const expectedKeyDetails = { + modulusLength: 2048, + publicExponent: 65537n, + hashAlgorithm: 'sha512', + mgf1HashAlgorithm: 'sha256', + saltLength: 20 + }; + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, expectedKeyDetails); + + // Node.js usually uses the same hash function for the message and for MGF1. + // However, when a different MGF1 message digest algorithm has been + // specified as part of the key, it should automatically switch to that. + // This behavior is required by sections 3.1 and 3.3 of RFC4055. + for (const key of [privatePem, privateKey]) { + // sha256 matches the MGF1 hash function and should be used internally, + // but it should not be permitted as the main message digest algorithm. + for (const algo of ['sha1', 'sha256']) { + assert.throws(() => { + createSign(algo).sign(key); + }, /digest not allowed/); + } + + // sha512 should produce a valid signature. + const signature = createSign('sha512') + .update('foo') + .sign(key); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify('sha512') + .update('foo') + .verify(pkey, signature); + + assert.ok(okay); + } + } + } +} + +{ + // Exporting an encrypted private key requires a cipher + const privateKey = createPrivateKey(privatePem); + assert.throws(() => { + privateKey.export({ + format: 'pem', type: 'pkcs8', passphrase: 'super-secret' + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.cipher' is invalid. Received undefined" + }); +} + +{ + // SecretKeyObject export buffer format (default) + const buffer = Buffer.from('Hello World'); + const keyObject = createSecretKey(buffer); + assert.deepStrictEqual(keyObject.export(), buffer); + assert.deepStrictEqual(keyObject.export({}), buffer); + assert.deepStrictEqual(keyObject.export({ format: 'buffer' }), buffer); + assert.deepStrictEqual(keyObject.export({ format: undefined }), buffer); +} + +{ + // Exporting an "oct" JWK from a SecretKeyObject + const buffer = Buffer.from('Hello World'); + const keyObject = createSecretKey(buffer); + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + { kty: 'oct', k: 'SGVsbG8gV29ybGQ' } + ); +} + +{ + // Exporting a JWK unsupported curve EC key + const supported = ['prime256v1', 'secp256k1', 'secp384r1', 'secp521r1']; + // Find an unsupported curve regardless of whether a FIPS compliant crypto + // provider is currently in use. + const namedCurve = getCurves().find((curve) => !supported.includes(curve)); + assert(namedCurve); + const keyPair = generateKeyPairSync('ec', { namedCurve }); + const { publicKey, privateKey } = keyPair; + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); +} + +{ + const first = Buffer.from('Hello'); + const second = Buffer.from('World'); + const keyObject = createSecretKey(first); + assert(createSecretKey(first).equals(createSecretKey(first))); + assert(!createSecretKey(first).equals(createSecretKey(second))); + + assert.throws(() => keyObject.equals(0), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "otherKeyObject" argument must be an instance of KeyObject. Received type number (0)' + }); + + assert(keyObject.equals(keyObject)); + assert(!keyObject.equals(createPublicKey(publicPem))); + assert(!keyObject.equals(createPrivateKey(privatePem))); +} + +{ + const first = generateKeyPairSync('ed25519'); + const second = generateKeyPairSync('ed25519'); + const secret = generateKeySync('aes', { length: 128 }); + + assert(first.publicKey.equals(first.publicKey)); + assert(first.publicKey.equals(createPublicKey( + first.publicKey.export({ format: 'pem', type: 'spki' })))); + assert(!first.publicKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.privateKey)); + assert(!first.publicKey.equals(secret)); + + assert(first.privateKey.equals(first.privateKey)); + assert(first.privateKey.equals(createPrivateKey( + first.privateKey.export({ format: 'pem', type: 'pkcs8' })))); + assert(!first.privateKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.publicKey)); + assert(!first.privateKey.equals(secret)); +} + +{ + const first = generateKeyPairSync('ed25519'); + const second = generateKeyPairSync('ed448'); + + assert(!first.publicKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.publicKey)); +} + +{ + const first = createSecretKey(Buffer.alloc(0)); + const second = createSecretKey(new ArrayBuffer(0)); + const third = createSecretKey(Buffer.alloc(1)); + assert(first.equals(first)); + assert(first.equals(second)); + assert(!first.equals(third)); + assert(!third.equals(first)); +} + +{ + // This should not cause a crash: https://github.com/nodejs/node/issues/44471 + for (const key of ['', 'foo', null, undefined, true, Boolean]) { + assert.throws(() => { + createPublicKey({ key, format: 'jwk' }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + assert.throws(() => { + createPrivateKey({ key, format: 'jwk' }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + } +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js b/test/js/node/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js new file mode 100644 index 0000000000..449d1a97f9 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); + +// Test async elliptic curve key generation with 'jwk' encoding and RSA. +{ + generateKeyPair('rsa', { + modulusLength: 1024, + publicKeyEncoding: { + format: 'jwk' + }, + privateKeyEncoding: { + format: 'jwk' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'object'); + assert.strictEqual(typeof privateKey, 'object'); + assert.strictEqual(publicKey.kty, 'RSA'); + assert.strictEqual(publicKey.kty, privateKey.kty); + assert.strictEqual(typeof publicKey.n, 'string'); + assert.strictEqual(publicKey.n, privateKey.n); + assert.strictEqual(typeof publicKey.e, 'string'); + assert.strictEqual(publicKey.e, privateKey.e); + assert.strictEqual(typeof privateKey.d, 'string'); + assert.strictEqual(typeof privateKey.p, 'string'); + assert.strictEqual(typeof privateKey.q, 'string'); + assert.strictEqual(typeof privateKey.dp, 'string'); + assert.strictEqual(typeof privateKey.dq, 'string'); + assert.strictEqual(typeof privateKey.qi, 'string'); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js b/test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js new file mode 100644 index 0000000000..3203dfe16e --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + assertApproximateSize, + testEncryptDecrypt, + testSignVerify, +} = require('../common/crypto'); + +// Test async RSA key generation with an encrypted private key, but encoded as DER. +{ + generateKeyPair('rsa', { + publicExponent: 0x10001, + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'der' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'der' + } + }, common.mustSucceed((publicKeyDER, privateKeyDER) => { + assert(Buffer.isBuffer(publicKeyDER)); + assertApproximateSize(publicKeyDER, 74); + + assert(Buffer.isBuffer(privateKeyDER)); + + const publicKey = { + key: publicKeyDER, + type: 'pkcs1', + format: 'der', + }; + const privateKey = { + key: privateKeyDER, + format: 'der', + type: 'pkcs8', + passphrase: 'secret' + }; + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js b/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js new file mode 100644 index 0000000000..46223f08d7 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + testSignVerify, + spkiExp, + sec1Exp, +} = require('../common/crypto'); + +// Test async explicit elliptic curve key generation, e.g. for ECDSA, +// with a SEC1 private key with paramEncoding explicit. +{ + generateKeyPair('ec', { + namedCurve: 'prime256v1', + paramEncoding: 'explicit', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'sec1', + format: 'pem' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, spkiExp); + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, sec1Exp); + + testSignVerify(publicKey, privateKey); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve.js b/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve.js new file mode 100644 index 0000000000..a1dfdbce1f --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + testSignVerify, + spkiExp, + sec1Exp, +} = require('../common/crypto'); + +// Test async named elliptic curve key generation, e.g. for ECDSA, +// with a SEC1 private key. +{ + generateKeyPair('ec', { + namedCurve: 'prime256v1', + paramEncoding: 'named', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'sec1', + format: 'pem' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, spkiExp); + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, sec1Exp); + + testSignVerify(publicKey, privateKey); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-deprecation.js b/test/js/node/test/parallel/test-crypto-keygen-deprecation.js new file mode 100644 index 0000000000..6aaf148d0e --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-deprecation.js @@ -0,0 +1,55 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L22 + +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const DeprecationWarning = []; +DeprecationWarning.push([ + '"options.hash" is deprecated, use "options.hashAlgorithm" instead.', + 'DEP0154']); +DeprecationWarning.push([ + '"options.mgf1Hash" is deprecated, use "options.mgf1HashAlgorithm" instead.', + 'DEP0154']); + +common.expectWarning({ DeprecationWarning }); + +const assert = require('assert'); +const { generateKeyPair } = require('crypto'); + +{ + // This test makes sure deprecated options still work as intended + + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hash: 'sha256', + mgf1Hash: 'sha256' + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 65537n, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256', + saltLength: 16 + }); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 65537n, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256', + saltLength: 16 + }); + })); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-keygen-eddsa.js b/test/js/node/test/parallel/test-crypto-keygen-eddsa.js new file mode 100644 index 0000000000..5a097c2524 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-eddsa.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); + +// Test EdDSA key generation. +{ + if (!/^1\.1\.0/.test(process.versions.openssl)) { + ['ed25519', 'ed448', 'x25519', 'x448'].forEach((keyType) => { + generateKeyPair(keyType, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, keyType); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, keyType); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, {}); + })); + }); + } +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js b/test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js new file mode 100644 index 0000000000..6c7938f99e --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); + +// Passing an empty passphrase string should not throw ERR_OSSL_CRYPTO_MALLOC_FAILURE even on OpenSSL 3. +// Regression test for https://github.com/nodejs/node/issues/41428. +generateKeyPair('rsa', { + modulusLength: 1024, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: '' + } +}, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'string'); + assert.strictEqual(typeof privateKey, 'string'); +})); diff --git a/test/js/node/test/parallel/test-crypto-keygen-key-object-without-encoding.js b/test/js/node/test/parallel/test-crypto-keygen-key-object-without-encoding.js new file mode 100644 index 0000000000..abcd282871 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-key-object-without-encoding.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + testEncryptDecrypt, + testSignVerify, +} = require('../common/crypto'); + +// Tests key objects are returned when key encodings are not specified. +{ + // If no publicKeyEncoding is specified, a key object should be returned. + generateKeyPair('rsa', { + modulusLength: 1024, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'object'); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + + // The private key should still be a string. + assert.strictEqual(typeof privateKey, 'string'); + + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); + })); + + // If no privateKeyEncoding is specified, a key object should be returned. + generateKeyPair('rsa', { + modulusLength: 1024, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }, common.mustSucceed((publicKey, privateKey) => { + // The public key should still be a string. + assert.strictEqual(typeof publicKey, 'string'); + + assert.strictEqual(typeof privateKey, 'object'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-key-objects.js b/test/js/node/test/parallel/test-crypto-keygen-key-objects.js new file mode 100644 index 0000000000..a0f1bdf2bc --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-key-objects.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPairSync, +} = require('crypto'); + +// Test sync key generation with key objects. +{ + const { publicKey, privateKey } = generateKeyPairSync('rsa', { + modulusLength: 512 + }); + + assert.strictEqual(typeof publicKey, 'object'); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 65537n + }); + + assert.strictEqual(typeof privateKey, 'object'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 65537n + }); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-missing-oid.js b/test/js/node/test/parallel/test-crypto-keygen-missing-oid.js new file mode 100644 index 0000000000..f7fefe1384 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-missing-oid.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, + generateKeyPairSync, + getCurves, +} = require('crypto'); + +// This test creates EC key pairs on curves without associated OIDs. +// Specifying a key encoding should not crash. +{ + if (process.versions.openssl >= '1.1.1i') { + for (const namedCurve of ['Oakley-EC2N-3', 'Oakley-EC2N-4']) { + if (!getCurves().includes(namedCurve)) + continue; + + const expectedErrorCode = + common.hasOpenSSL3 ? 'ERR_OSSL_MISSING_OID' : 'ERR_OSSL_EC_MISSING_OID'; + const params = { + namedCurve, + publicKeyEncoding: { + format: 'der', + type: 'spki' + } + }; + + assert.throws(() => { + generateKeyPairSync('ec', params); + }, { + code: expectedErrorCode + }); + + generateKeyPair('ec', params, common.mustCall((err) => { + assert.strictEqual(err.code, expectedErrorCode); + })); + } + } +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-non-standard-public-exponent.js b/test/js/node/test/parallel/test-crypto-keygen-non-standard-public-exponent.js new file mode 100644 index 0000000000..f54a9e8a6d --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-non-standard-public-exponent.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPairSync, +} = require('crypto'); + +// Test sync key generation with key objects with a non-standard +// publicExponent +{ + const { publicKey, privateKey } = generateKeyPairSync('rsa', { + publicExponent: 3, + modulusLength: 512 + }); + + assert.strictEqual(typeof publicKey, 'object'); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 3n + }); + + assert.strictEqual(typeof privateKey, 'object'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 3n + }); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-promisify.js b/test/js/node/test/parallel/test-crypto-keygen-promisify.js new file mode 100644 index 0000000000..cd6ca7d6e3 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-promisify.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + assertApproximateSize, + testEncryptDecrypt, + testSignVerify, + pkcs1PubExp, + pkcs1PrivExp, +} = require('../common/crypto'); +const { promisify } = require('util'); + +// Test the util.promisified API with async RSA key generation. +{ + promisify(generateKeyPair)('rsa', { + publicExponent: 0x10001, + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }).then(common.mustCall((keys) => { + const { publicKey, privateKey } = keys; + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, pkcs1PubExp); + assertApproximateSize(publicKey, 180); + + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, pkcs1PrivExp); + assertApproximateSize(privateKey, 512); + + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-sync.js b/test/js/node/test/parallel/test-crypto-keygen-sync.js new file mode 100644 index 0000000000..a100379e21 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-sync.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPairSync, +} = require('crypto'); +const { + assertApproximateSize, + testEncryptDecrypt, + testSignVerify, + pkcs1PubExp, + pkcs8Exp, +} = require('../common/crypto'); + +// To make the test faster, we will only test sync key generation once and +// with a relatively small key. +{ + const ret = generateKeyPairSync('rsa', { + publicExponent: 3, + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }); + + assert.strictEqual(Object.keys(ret).length, 2); + const { publicKey, privateKey } = ret; + + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, pkcs1PubExp); + assertApproximateSize(publicKey, 162); + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, pkcs8Exp); + assertApproximateSize(privateKey, 512); + + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen.js b/test/js/node/test/parallel/test-crypto-keygen.js new file mode 100644 index 0000000000..1c32e0738a --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen.js @@ -0,0 +1,826 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L21 + +'use strict'; + +// This tests early errors for invalid encodings. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const { + generateKeyPair, + generateKeyPairSync, +} = require('crypto'); +const { inspect } = require('util'); + + +// Test invalid parameter encoding. +{ + assert.throws(() => generateKeyPairSync('ec', { + namedCurve: 'P-256', + paramEncoding: 'otherEncoding', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase: 'top secret' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.paramEncoding' is invalid. " + + "Received 'otherEncoding'" + }); +} + +{ + // Test invalid key types. + for (const type of [undefined, null, 0]) { + assert.throws(() => generateKeyPairSync(type, {}), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "type" argument must be of type string.' + + common.invalidArgTypeHelper(type) + }); + } + + assert.throws(() => generateKeyPairSync('rsa2', {}), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The argument 'type' must be a supported key type. Received 'rsa2'" + }); +} + +{ + // Test keygen without options object. + assert.throws(() => generateKeyPair('rsa', common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. ' + + 'Received undefined' + }); + + // Even if no options are required, it should be impossible to pass anything + // but an object (or undefined). + assert.throws(() => generateKeyPair('ed448', 0, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. ' + + 'Received type number (0)' + }); +} + +{ + // Invalid publicKeyEncoding. + for (const enc of [0, 'a', true]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: enc, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding' is invalid. " + + `Received ${inspect(enc)}` + }); + } + + // Missing publicKeyEncoding.type. + for (const type of [undefined, null, 0, true, {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type, + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Missing / invalid publicKeyEncoding.format. + for (const format of [undefined, null, 0, false, 'a', {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding.format' is invalid. " + + `Received ${inspect(format)}` + }); + } + + // Invalid privateKeyEncoding. + for (const enc of [0, 'a', true]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: enc + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding' is invalid. " + + `Received ${inspect(enc)}` + }); + } + + // Missing / invalid privateKeyEncoding.type. + for (const type of [undefined, null, 0, true, {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type, + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Missing / invalid privateKeyEncoding.format. + for (const format of [undefined, null, 0, false, 'a', {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.format' is invalid. " + + `Received ${inspect(format)}` + }); + } + + // Cipher of invalid type. + for (const cipher of [0, true, {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + cipher + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.cipher' is invalid. " + + `Received ${inspect(cipher)}` + }); + } + + // Invalid cipher. + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'foo', + passphrase: 'secret' + } + }), { + name: 'Error', + code: 'ERR_CRYPTO_UNKNOWN_CIPHER', + message: 'Unknown cipher' + }); + + // Cipher, but no valid passphrase. + for (const passphrase of [undefined, null, 5, false, true]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.passphrase' " + + `is invalid. Received ${inspect(passphrase)}` + }); + } + + // Test invalid callbacks. + for (const cb of [undefined, null, 0, {}]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 512, + publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' } + }, cb), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); + } +} + +// Test RSA parameters. +{ + // Test invalid modulus lengths. (non-number) + for (const modulusLength of [undefined, null, 'a', true, {}, []]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.modulusLength" property must be of type number.' + + common.invalidArgTypeHelper(modulusLength) + }); + } + + // Test invalid modulus lengths. (non-integer) + for (const modulusLength of [512.1, 1.3, 1.1, 5000.9, 100.5]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.modulusLength" is out of range. ' + + 'It must be an integer. ' + + `Received ${inspect(modulusLength)}` + }); + } + + // Test invalid modulus lengths. (out of range) + for (const modulusLength of [-1, -9, 4294967297]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid exponents. (non-number) + for (const publicExponent of ['a', true, {}, []]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.publicExponent" property must be of type number.' + + common.invalidArgTypeHelper(publicExponent) + }); + } + + // Test invalid exponents. (non-integer) + for (const publicExponent of [3.5, 1.1, 50.5, 510.5]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.publicExponent" is out of range. ' + + 'It must be an integer. ' + + `Received ${inspect(publicExponent)}` + }); + } + + // Test invalid exponents. (out of range) + for (const publicExponent of [-5, -3, 4294967297]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid exponents. (caught by OpenSSL) + for (const publicExponent of [1, 1 + 0x10001]) { + generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustCall((err) => { + assert.strictEqual(err.name, 'Error'); + assert.match(err.message, common.hasOpenSSL3 ? /exponent/ : /bad e value/); + })); + } +} + +// Test DSA parameters. +{ + // Test invalid modulus lengths. (non-number) + for (const modulusLength of [undefined, null, 'a', true, {}, []]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.modulusLength" property must be of type number.' + + common.invalidArgTypeHelper(modulusLength) + }); + } + + // Test invalid modulus lengths. (non-integer) + for (const modulusLength of [512.1, 1.3, 1.1, 5000.9, 100.5]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid modulus lengths. (out of range) + for (const modulusLength of [-1, -9, 4294967297]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid divisor lengths. (non-number) + for (const divisorLength of ['a', true, {}, []]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength: 2048, + divisorLength + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.divisorLength" property must be of type number.' + + common.invalidArgTypeHelper(divisorLength) + }); + } + + // Test invalid divisor lengths. (non-integer) + for (const divisorLength of [4096.1, 5.1, 6.9, 9.5]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength: 2048, + divisorLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.divisorLength" is out of range. ' + + 'It must be an integer. ' + + `Received ${inspect(divisorLength)}` + }); + } + + // Test invalid divisor lengths. (out of range) + for (const divisorLength of [-1, -6, -9, 2147483648]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength: 2048, + divisorLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.divisorLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + `Received ${inspect(divisorLength)}` + }); + } +} + +// Test EC parameters. +{ + // Test invalid curves. + assert.throws(() => { + generateKeyPairSync('ec', { + namedCurve: 'abcdef', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'sec1', format: 'pem' } + }); + }, { + name: 'TypeError', + message: 'Invalid EC curve name' + }); + + // Test error type when curve is not a string + for (const namedCurve of [true, {}, [], 123]) { + assert.throws(() => { + generateKeyPairSync('ec', { + namedCurve, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'sec1', format: 'pem' } + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.namedCurve" property must be of type string.' + + common.invalidArgTypeHelper(namedCurve) + }); + } + + // It should recognize both NIST and standard curve names. + generateKeyPair('ec', { + namedCurve: 'P-256', + }, common.mustSucceed((publicKey, privateKey) => { + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + namedCurve: 'prime256v1' + }); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + namedCurve: 'prime256v1' + }); + })); + + generateKeyPair('ec', { + namedCurve: 'secp256k1', + }, common.mustSucceed((publicKey, privateKey) => { + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + })); +} + +{ + assert.throws(() => { + generateKeyPair('dh', common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. Received undefined' + }); + + assert.throws(() => { + generateKeyPair('dh', {}, common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_MISSING_OPTION', + message: 'At least one of the group, prime, or primeLength options is ' + + 'required' + }); + + assert.throws(() => { + generateKeyPair('dh', { + group: 'modp0' + }, common.mustNotCall()); + }, { + name: 'Error', + code: 'ERR_CRYPTO_UNKNOWN_DH_GROUP', + message: 'Unknown DH group' + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: 2147483648 + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.primeLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received 2147483648', + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: -1 + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.primeLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received -1', + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: 2, + generator: 2147483648, + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.generator" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received 2147483648', + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: 2, + generator: -1, + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.generator" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received -1', + }); + + // Test incompatible options. + const allOpts = { + group: 'modp5', + prime: Buffer.alloc(0), + primeLength: 1024, + generator: 2 + }; + const incompatible = [ + ['group', 'prime'], + ['group', 'primeLength'], + ['group', 'generator'], + ['prime', 'primeLength'], + ]; + for (const [opt1, opt2] of incompatible) { + assert.throws(() => { + generateKeyPairSync('dh', { + [opt1]: allOpts[opt1], + [opt2]: allOpts[opt2] + }); + }, { + name: 'TypeError', + code: 'ERR_INCOMPATIBLE_OPTION_PAIR', + message: `Option "${opt1}" cannot be used in combination with option ` + + `"${opt2}"` + }); + } +} + +// Test invalid key encoding types. +{ + // Invalid public key type. + for (const type of ['foo', 'pkcs8', 'sec1']) { + assert.throws(() => { + generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { type, format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Invalid hash value. + for (const hashValue of [123, true, {}, []]) { + assert.throws(() => { + generateKeyPairSync('rsa-pss', { + modulusLength: 4096, + hashAlgorithm: hashValue + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.hashAlgorithm" property must be of type string.' + + common.invalidArgTypeHelper(hashValue) + }); + } + + // too long salt length + assert.throws(() => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 2147483648, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256' + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.saltLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received 2147483648' + }); + + assert.throws(() => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: -1, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256' + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.saltLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received -1' + }); + + // Invalid private key type. + for (const type of ['foo', 'spki']) { + assert.throws(() => { + generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type, format: 'pem' } + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Key encoding doesn't match key type. + for (const type of ['dsa', 'ec']) { + assert.throws(() => { + generateKeyPairSync(type, { + modulusLength: 4096, + namedCurve: 'P-256', + publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: 'The selected key encoding pkcs1 can only be used for RSA keys.' + }); + + assert.throws(() => { + generateKeyPairSync(type, { + modulusLength: 4096, + namedCurve: 'P-256', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: 'The selected key encoding pkcs1 can only be used for RSA keys.' + }); + } + + for (const type of ['rsa', 'dsa']) { + assert.throws(() => { + generateKeyPairSync(type, { + modulusLength: 4096, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'sec1', format: 'pem' } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: 'The selected key encoding sec1 can only be used for EC keys.' + }); + } + + // Attempting to encrypt a DER-encoded, non-PKCS#8 key. + for (const type of ['pkcs1', 'sec1']) { + assert.throws(() => { + generateKeyPairSync(type === 'pkcs1' ? 'rsa' : 'ec', { + modulusLength: 4096, + namedCurve: 'P-256', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { + type, + format: 'der', + cipher: 'aes-128-cbc', + passphrase: 'hello' + } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: `The selected key encoding ${type} does not support encryption.` + }); + } +} + +{ + // Test RSA-PSS. + assert.throws( + () => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: undefined + }); + }, + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + } + ); + + for (const mgf1HashAlgorithm of [null, 0, false, {}, []]) { + assert.throws( + () => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm + }, common.mustNotCall()); + }, + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.mgf1HashAlgorithm" property must be of type string.' + + common.invalidArgTypeHelper(mgf1HashAlgorithm) + + } + ); + } + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + hashAlgorithm: 'sha2', + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_CRYPTO_INVALID_DIGEST', + message: 'Invalid digest: sha2' + }); + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + mgf1HashAlgorithm: 'sha2', + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_CRYPTO_INVALID_DIGEST', + message: 'Invalid MGF1 digest: sha2' + }); +} + +{ + // This test makes sure deprecated and new options must + // be the same value. + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + mgf1Hash: 'sha256', + mgf1HashAlgorithm: 'sha1' + }, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hash: 'sha256', + hashAlgorithm: 'sha1' + }, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-lazy-transform-writable.js b/test/js/node/test/parallel/test-crypto-lazy-transform-writable.js new file mode 100644 index 0000000000..000c6693c8 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-lazy-transform-writable.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const Stream = require('stream'); + +const hasher1 = crypto.createHash('sha256'); +const hasher2 = crypto.createHash('sha256'); + +// Calculate the expected result. +hasher1.write(Buffer.from('hello world')); +hasher1.end(); + +const expected = hasher1.read().toString('hex'); + +class OldStream extends Stream { + constructor() { + super(); + this.readable = true; + } +} + +const stream = new OldStream(); + +stream.pipe(hasher2).on('finish', common.mustCall(function() { + const hash = hasher2.read().toString('hex'); + assert.strictEqual(hash, expected); +})); + +stream.emit('data', Buffer.from('hello')); +stream.emit('data', Buffer.from(' world')); +stream.emit('end'); diff --git a/test/js/node/test/parallel/test-crypto-no-algorithm.js b/test/js/node/test/parallel/test-crypto-no-algorithm.js new file mode 100644 index 0000000000..37db38cf61 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-no-algorithm.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.hasOpenSSL3) + common.skip('this test requires OpenSSL 3.x'); + +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); + +if (common.isMainThread) { + // TODO(richardlau): Decide if `crypto.setFips` should error if the + // provider named "fips" is not available. + crypto.setFips(1); + crypto.randomBytes(20, common.mustCall((err) => { + // crypto.randomBytes should either succeed or fail but not hang. + if (err) { + assert.match(err.message, /digital envelope routines::unsupported/); + const expected = /random number generator::unable to fetch drbg/; + assert(err.opensslErrorStack.some((msg) => expected.test(msg)), + `did not find ${expected} in ${err.opensslErrorStack}`); + } + })); +} + +{ + // Startup test. Should not hang. + const { path } = require('../common/fixtures'); + const { spawnSync } = require('node:child_process'); + const baseConf = path('openssl3-conf', 'base_only.cnf'); + const cp = spawnSync(process.execPath, + [ `--openssl-config=${baseConf}`, '-p', '"hello"' ], + { encoding: 'utf8' }); + assert(common.nodeProcessAborted(cp.status, cp.signal), + `process did not abort, code:${cp.status} signal:${cp.signal}`); +} diff --git a/test/js/node/test/parallel/test-crypto-op-during-process-exit.js b/test/js/node/test/parallel/test-crypto-op-during-process-exit.js new file mode 100644 index 0000000000..a9a70c39e0 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-op-during-process-exit.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) { common.skip('missing crypto'); } +const assert = require('assert'); +const { generateKeyPair } = require('crypto'); + +if (common.isWindows) { + // Remove this conditional once the libuv change is in Node.js. + common.skip('crashing due to https://github.com/libuv/libuv/pull/2983'); +} + +// Regression test for a race condition: process.exit() might lead to OpenSSL +// cleaning up state from the exit() call via calling its destructor, but +// running OpenSSL operations on another thread might lead to them attempting +// to initialize OpenSSL, leading to a crash. +// This test crashed consistently on x64 Linux on Node v14.9.0. + +generateKeyPair('rsa', { + modulusLength: 2048, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } +}, (err/* , publicKey, privateKey */) => { + assert.ifError(err); +}); + +setTimeout(() => process.exit(), common.platformTimeout(10)); diff --git a/test/js/node/test/parallel/test-crypto-padding-aes256.js b/test/js/node/test/parallel/test-crypto-padding-aes256.js new file mode 100644 index 0000000000..14d853bdfd --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-padding-aes256.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const iv = Buffer.from('00000000000000000000000000000000', 'hex'); +const key = Buffer.from('0123456789abcdef0123456789abcdef' + + '0123456789abcdef0123456789abcdef', 'hex'); + +function encrypt(val, pad) { + const c = crypto.createCipheriv('aes256', key, iv); + c.setAutoPadding(pad); + return c.update(val, 'utf8', 'latin1') + c.final('latin1'); +} + +function decrypt(val, pad) { + const c = crypto.createDecipheriv('aes256', key, iv); + c.setAutoPadding(pad); + return c.update(val, 'latin1', 'utf8') + c.final('utf8'); +} + +// echo 0123456789abcdef0123456789abcdef \ +// | openssl enc -e -aes256 -nopad -K -iv \ +// | openssl enc -d -aes256 -nopad -K -iv +let plaintext = '0123456789abcdef0123456789abcdef'; // Multiple of block size +let encrypted = encrypt(plaintext, false); +let decrypted = decrypt(encrypted, false); +assert.strictEqual(decrypted, plaintext); + +// echo 0123456789abcdef0123456789abcde \ +// | openssl enc -e -aes256 -K -iv \ +// | openssl enc -d -aes256 -K -iv +plaintext = '0123456789abcdef0123456789abcde'; // not a multiple +encrypted = encrypt(plaintext, true); +decrypted = decrypt(encrypted, true); +assert.strictEqual(decrypted, plaintext); diff --git a/test/js/node/test/parallel/test-crypto-pbkdf2.js b/test/js/node/test/parallel/test-crypto-pbkdf2.js new file mode 100644 index 0000000000..dac8aed95d --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-pbkdf2.js @@ -0,0 +1,241 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +function runPBKDF2(password, salt, iterations, keylen, hash) { + const syncResult = + crypto.pbkdf2Sync(password, salt, iterations, keylen, hash); + + crypto.pbkdf2(password, salt, iterations, keylen, hash, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(asyncResult, syncResult); + })); + + return syncResult; +} + +function testPBKDF2(password, salt, iterations, keylen, expected, encoding) { + const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256'); + assert.strictEqual(actual.toString(encoding || 'latin1'), expected); +} + +// +// Test PBKDF2 with RFC 6070 test vectors (except #4) +// + +testPBKDF2('password', 'salt', 1, 20, + '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' + + '\x56\xc4\xf8\x37\xa8\x65\x48\xc9'); + +testPBKDF2('password', 'salt', 2, 20, + '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' + + '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e'); + +testPBKDF2('password', 'salt', 4096, 20, + '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' + + '\x84\x5c\x4c\x8d\x96\x28\x93\xa0'); + +testPBKDF2('passwordPASSWORDpassword', + 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, + 25, + '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' + + '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c'); + +testPBKDF2('pass\0word', 'sa\0lt', 4096, 16, + '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' + + '\x0a\x86\x87'); + +testPBKDF2('password', 'salt', 32, 32, + '64c486c55d30d4c5a079b8823b7d7cb37ff0556f537da8410233bcec330ed956', + 'hex'); + +// Error path should not leak memory (check with valgrind). +assert.throws( + () => crypto.pbkdf2('password', 'salt', 1, 20, 'sha1'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } +); + +for (const iterations of [-1, 0, 2147483648]) { + assert.throws( + () => crypto.pbkdf2Sync('password', 'salt', iterations, 20, 'sha1'), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } + ); +} + +['str', null, undefined, [], {}].forEach((notNumber) => { + assert.throws( + () => { + crypto.pbkdf2Sync('password', 'salt', 1, notNumber, 'sha256'); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "keylen" argument must be of type number.' + + `${common.invalidArgTypeHelper(notNumber)}` + }); +}); + +[Infinity, -Infinity, NaN].forEach((input) => { + assert.throws( + () => { + crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "keylen" is out of range. It ' + + `must be an integer. Received ${input}` + }); +}); + +[-1, 2147483648, 4294967296].forEach((input) => { + assert.throws( + () => { + crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); +}); + +// Should not get FATAL ERROR with empty password and salt +// https://github.com/nodejs/node/issues/8571 +crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed()); + +assert.throws( + () => crypto.pbkdf2('password', 'salt', 8, 8, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "digest" argument must be of type string. ' + + 'Received undefined' + }); + +assert.throws( + () => crypto.pbkdf2Sync('password', 'salt', 8, 8), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "digest" argument must be of type string. ' + + 'Received undefined' + }); + +assert.throws( + () => crypto.pbkdf2Sync('password', 'salt', 8, 8, null), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "digest" argument must be of type string. ' + + 'Received null' + }); +[1, {}, [], true, undefined, null].forEach((input) => { + assert.throws( + () => crypto.pbkdf2(input, 'salt', 8, 8, 'sha256', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); + + assert.throws( + () => crypto.pbkdf2('pass', input, 8, 8, 'sha256', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); + + assert.throws( + () => crypto.pbkdf2Sync(input, 'salt', 8, 8, 'sha256'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); + + assert.throws( + () => crypto.pbkdf2Sync('pass', input, 8, 8, 'sha256'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); +}); + +['test', {}, [], true, undefined, null].forEach((i) => { + const received = common.invalidArgTypeHelper(i); + assert.throws( + () => crypto.pbkdf2('pass', 'salt', i, 8, 'sha256', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "iterations" argument must be of type number.${received}` + } + ); + + assert.throws( + () => crypto.pbkdf2Sync('pass', 'salt', i, 8, 'sha256'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "iterations" argument must be of type number.${received}` + } + ); +}); + +// Any TypedArray should work for password and salt. +for (const SomeArray of [Uint8Array, Uint16Array, Uint32Array, Float32Array, + Float64Array, ArrayBuffer, SharedArrayBuffer]) { + runPBKDF2(new SomeArray(10), 'salt', 8, 8, 'sha256'); + runPBKDF2('pass', new SomeArray(10), 8, 8, 'sha256'); +} + +assert.throws( + () => crypto.pbkdf2('pass', 'salt', 8, 8, 'md55', common.mustNotCall()), + { + code: 'ERR_CRYPTO_INVALID_DIGEST', + name: 'TypeError', + message: 'Invalid digest: md55' + } +); + +assert.throws( + () => crypto.pbkdf2Sync('pass', 'salt', 8, 8, 'md55'), + { + code: 'ERR_CRYPTO_INVALID_DIGEST', + name: 'TypeError', + message: 'Invalid digest: md55' + } +); + +if (!common.openSSLIsBoringSSL) { + const kNotPBKDF2Supported = ['shake128', 'shake256']; + crypto.getHashes() + .filter((hash) => !kNotPBKDF2Supported.includes(hash)) + .forEach((hash) => { + runPBKDF2(new Uint8Array(10), 'salt', 8, 8, hash); + }); +} + +{ + // This should not crash. + assert.throws( + () => crypto.pbkdf2Sync('1', '2', 1, 1, '%'), + { + code: 'ERR_CRYPTO_INVALID_DIGEST', + name: 'TypeError', + message: 'Invalid digest: %' + } + ); +} diff --git a/test/js/node/test/parallel/test-crypto-psychic-signatures.js b/test/js/node/test/parallel/test-crypto-psychic-signatures.js new file mode 100644 index 0000000000..e8228b26be --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-psychic-signatures.js @@ -0,0 +1,100 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const crypto = require('crypto'); + +// Tests for CVE-2022-21449 +// https://neilmadden.blog/2022/04/19/psychic-signatures-in-java/ +// Dubbed "Psychic Signatures", these signatures bypassed the ECDSA signature +// verification implementation in Java in 15, 16, 17, and 18. OpenSSL is not +// (and was not) vulnerable so these are a precaution. + +const vectors = { + 'ieee-p1363': [ + Buffer.from('0000000000000000000000000000000000000000000000000000000000000000' + + '0000000000000000000000000000000000000000000000000000000000000000', 'hex'), + Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551' + + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex'), + ], + 'der': [ + Buffer.from('3046022100' + + '0000000000000000000000000000000000000000000000000000000000000000' + + '022100' + + '0000000000000000000000000000000000000000000000000000000000000000', 'hex'), + Buffer.from('3046022100' + + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551' + + '022100' + + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex'), + ], +}; + +const keyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'P-256', + publicKeyEncoding: { + format: 'der', + type: 'spki' + }, +}); + +const data = Buffer.from('Hello!'); + +for (const [encoding, signatures] of Object.entries(vectors)) { + for (const signature of signatures) { + const key = { + key: keyPair.publicKey, + format: 'der', + type: 'spki', + dsaEncoding: encoding, + }; + + // one-shot sync + assert.strictEqual( + crypto.verify( + 'sha256', + data, + key, + signature, + ), + false, + ); + + // one-shot async + crypto.verify( + 'sha256', + data, + key, + signature, + common.mustSucceed((verified) => assert.strictEqual(verified, false)), + ); + + // stream + assert.strictEqual( + crypto.createVerify('sha256') + .update(data) + .verify(key, signature), + false, + ); + + // webcrypto + globalThis.crypto.subtle.importKey( + 'spki', + keyPair.publicKey, + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['verify'], + ).then((publicKey) => { + return globalThis.crypto.subtle.verify( + { name: 'ECDSA', hash: 'SHA-256' }, + publicKey, + signature, + data, + ); + }).then(common.mustCall((verified) => { + assert.strictEqual(verified, false); + })); + } +} diff --git a/test/js/node/test/parallel/test-crypto-publicDecrypt-fails-first-time.js b/test/js/node/test/parallel/test-crypto-publicDecrypt-fails-first-time.js new file mode 100644 index 0000000000..a60b87dbf6 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-publicDecrypt-fails-first-time.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); + +// Test for https://github.com/nodejs/node/issues/40814 + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.hasOpenSSL3) + common.skip('only openssl3'); // https://github.com/nodejs/node/pull/42793#issuecomment-1107491901 + +const assert = require('assert'); +const crypto = require('crypto'); + +const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-128-ecb', + passphrase: 'abcdef' + } +}); +assert.notStrictEqual(privateKey.toString(), ''); + +const msg = 'The quick brown fox jumps over the lazy dog'; + +const encryptedString = crypto.privateEncrypt({ + key: privateKey, + passphrase: 'abcdef' +}, Buffer.from(msg)).toString('base64'); +const decryptedString = crypto.publicDecrypt(publicKey, Buffer.from(encryptedString, 'base64')).toString(); +console.log(`Encrypted: ${encryptedString}`); +console.log(`Decrypted: ${decryptedString}`); + +assert.notStrictEqual(encryptedString, ''); +assert.strictEqual(decryptedString, msg); diff --git a/test/js/node/test/parallel/test-crypto-randomfillsync-regression.js b/test/js/node/test/parallel/test-crypto-randomfillsync-regression.js new file mode 100644 index 0000000000..7a37864258 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-randomfillsync-regression.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { randomFillSync } = require('crypto'); +const { notStrictEqual } = require('assert'); + +const ab = new ArrayBuffer(20); +const buf = Buffer.from(ab, 10); + +const before = buf.toString('hex'); + +randomFillSync(buf); + +const after = buf.toString('hex'); + +notStrictEqual(before, after); diff --git a/test/js/node/test/parallel/test-crypto-rsa-dsa.js b/test/js/node/test/parallel/test-crypto-rsa-dsa.js new file mode 100644 index 0000000000..84df67ce5b --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-rsa-dsa.js @@ -0,0 +1,549 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L24 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const constants = crypto.constants; + +const fixtures = require('../common/fixtures'); + +// Test certificates +const certPem = fixtures.readKey('rsa_cert.crt'); +const keyPem = fixtures.readKey('rsa_private.pem'); +const rsaKeySize = 2048; +const rsaPubPem = fixtures.readKey('rsa_public.pem', 'ascii'); +const rsaKeyPem = fixtures.readKey('rsa_private.pem', 'ascii'); +const rsaKeyPemEncrypted = fixtures.readKey('rsa_private_encrypted.pem', + 'ascii'); +const dsaPubPem = fixtures.readKey('dsa_public.pem', 'ascii'); +const dsaKeyPem = fixtures.readKey('dsa_private.pem', 'ascii'); +const dsaKeyPemEncrypted = fixtures.readKey('dsa_private_encrypted.pem', + 'ascii'); +const rsaPkcs8KeyPem = fixtures.readKey('rsa_private_pkcs8.pem'); +const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem'); + +const ec = new TextEncoder(); + +const openssl1DecryptError = { + message: 'error:06065064:digital envelope routines:EVP_DecryptFinal_ex:' + + 'bad decrypt', + code: 'ERR_OSSL_EVP_BAD_DECRYPT', + reason: 'bad decrypt', + function: 'EVP_DecryptFinal_ex', + library: 'digital envelope routines', +}; + +const decryptError = common.hasOpenSSL3 ? + { message: 'error:1C800064:Provider routines::bad decrypt' } : + openssl1DecryptError; + +const decryptPrivateKeyError = common.hasOpenSSL3 ? { + message: 'error:1C800064:Provider routines::bad decrypt', +} : openssl1DecryptError; + +function getBufferCopy(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +// Test RSA encryption/decryption +{ + const input = 'I AM THE WALRUS'; + const bufferToEncrypt = Buffer.from(input); + const bufferPassword = Buffer.from('password'); + + let encryptedBuffer = crypto.publicEncrypt(rsaPubPem, bufferToEncrypt); + + // Test other input types + let otherEncrypted; + { + const ab = getBufferCopy(ec.encode(rsaPubPem)); + const ab2enc = getBufferCopy(bufferToEncrypt); + + crypto.publicEncrypt(ab, ab2enc); + crypto.publicEncrypt(new Uint8Array(ab), new Uint8Array(ab2enc)); + crypto.publicEncrypt(new DataView(ab), new DataView(ab2enc)); + otherEncrypted = crypto.publicEncrypt({ + key: Buffer.from(ab).toString('hex'), + encoding: 'hex' + }, Buffer.from(ab2enc).toString('hex')); + } + + let decryptedBuffer = crypto.privateDecrypt(rsaKeyPem, encryptedBuffer); + const otherDecrypted = crypto.privateDecrypt(rsaKeyPem, otherEncrypted); + assert.strictEqual(decryptedBuffer.toString(), input); + assert.strictEqual(otherDecrypted.toString(), input); + + decryptedBuffer = crypto.privateDecrypt(rsaPkcs8KeyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + let decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + + const otherDecryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: ec.encode('password') + }, encryptedBuffer); + + assert.strictEqual( + otherDecryptedBufferWithPassword.toString(), + decryptedBufferWithPassword.toString()); + + decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + encryptedBuffer = crypto.publicEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, bufferToEncrypt); + + decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + encryptedBuffer = crypto.privateEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, bufferToEncrypt); + + decryptedBufferWithPassword = crypto.publicDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + // Now with explicit RSA_PKCS1_PADDING. + encryptedBuffer = crypto.privateEncrypt({ + padding: crypto.constants.RSA_PKCS1_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, bufferToEncrypt); + + decryptedBufferWithPassword = crypto.publicDecrypt({ + padding: crypto.constants.RSA_PKCS1_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + // Omitting padding should be okay because RSA_PKCS1_PADDING is the default. + decryptedBufferWithPassword = crypto.publicDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + // Now with RSA_NO_PADDING. Plaintext needs to match key size. + // OpenSSL 3.x has a rsa_check_padding that will cause an error if + // RSA_NO_PADDING is used. + if (!common.hasOpenSSL3) { + { + const plaintext = 'x'.repeat(rsaKeySize / 8); + encryptedBuffer = crypto.privateEncrypt({ + padding: crypto.constants.RSA_NO_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, Buffer.from(plaintext)); + + decryptedBufferWithPassword = crypto.publicDecrypt({ + padding: crypto.constants.RSA_NO_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), plaintext); + } + } + + encryptedBuffer = crypto.publicEncrypt(certPem, bufferToEncrypt); + + decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + encryptedBuffer = crypto.publicEncrypt(keyPem, bufferToEncrypt); + + decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + encryptedBuffer = crypto.privateEncrypt(keyPem, bufferToEncrypt); + + decryptedBuffer = crypto.publicDecrypt(keyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'wrong' + }, bufferToEncrypt); + }, decryptError); + + assert.throws(() => { + crypto.publicEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'wrong' + }, encryptedBuffer); + }, decryptError); + + encryptedBuffer = crypto.privateEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: Buffer.from('password') + }, bufferToEncrypt); + + assert.throws(() => { + crypto.publicDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: Buffer.from('wrong') + }, encryptedBuffer); + }, decryptError); +} + +function test_rsa(padding, encryptOaepHash, decryptOaepHash) { + const size = (padding === 'RSA_NO_PADDING') ? rsaKeySize / 8 : 32; + const input = Buffer.allocUnsafe(size); + for (let i = 0; i < input.length; i++) + input[i] = (i * 7 + 11) & 0xff; + const bufferToEncrypt = Buffer.from(input); + + padding = constants[padding]; + + const encryptedBuffer = crypto.publicEncrypt({ + key: rsaPubPem, + padding: padding, + oaepHash: encryptOaepHash + }, bufferToEncrypt); + + + if (padding === constants.RSA_PKCS1_PADDING) { + if (!process.config.variables.node_shared_openssl) { + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + } else { + // The version of a linked against OpenSSL. May + // or may not support implicit rejection. Figuring + // this out in the test is not feasible but we + // require that it pass based on one of the two + // cases of supporting it or not. + try { + // The expected exceptions should be thrown if implicit rejection + // is not supported + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + } catch (e) { + if (e.toString() === + 'AssertionError [ERR_ASSERTION]: Missing expected exception.') { + // Implicit rejection must be supported since + // we did not get the exceptions that are thrown + // when it is not, we should be able to decrypt + let decryptedBuffer = crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + + decryptedBuffer = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + } else { + // There was an exception but it is not the one we expect if implicit + // rejection is not supported so there was some other failure, + // re-throw it so the test fails + throw e; + } + } + } + } else { + let decryptedBuffer = crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + + decryptedBuffer = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + } +} + +test_rsa('RSA_NO_PADDING'); +test_rsa('RSA_PKCS1_PADDING'); +test_rsa('RSA_PKCS1_OAEP_PADDING'); + +// Test OAEP with different hash functions. +test_rsa('RSA_PKCS1_OAEP_PADDING', undefined, 'sha1'); +test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha1', undefined); +test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha256'); +test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha512', 'sha512'); +assert.throws(() => { + test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha512'); +}, { + code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR' +}); + +// The following RSA-OAEP test cases were created using the WebCrypto API to +// ensure compatibility when using non-SHA1 hash functions. +{ + const { decryptionTests } = + JSON.parse(fixtures.readSync('rsa-oaep-test-vectors.js', 'utf8')); + + for (const { ct, oaepHash, oaepLabel } of decryptionTests) { + const label = oaepLabel ? Buffer.from(oaepLabel, 'hex') : undefined; + const copiedLabel = oaepLabel ? getBufferCopy(label) : undefined; + + const decrypted = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + oaepHash, + oaepLabel: oaepLabel ? label : undefined + }, Buffer.from(ct, 'hex')); + + assert.strictEqual(decrypted.toString('utf8'), 'Hello Node.js'); + + const otherDecrypted = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + oaepHash, + oaepLabel: copiedLabel + }, Buffer.from(ct, 'hex')); + + assert.strictEqual(otherDecrypted.toString('utf8'), 'Hello Node.js'); + } +} + +// Test invalid oaepHash and oaepLabel options. +for (const fn of [crypto.publicEncrypt, crypto.privateDecrypt]) { + assert.throws(() => { + fn({ + key: rsaPubPem, + oaepHash: 'Hello world' + }, Buffer.alloc(10)); + }, { + code: 'ERR_OSSL_EVP_INVALID_DIGEST' + }); + + for (const oaepHash of [0, false, null, Symbol(), () => {}]) { + assert.throws(() => { + fn({ + key: rsaPubPem, + oaepHash + }, Buffer.alloc(10)); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + } + + for (const oaepLabel of [0, false, null, Symbol(), () => {}, {}]) { + assert.throws(() => { + fn({ + key: rsaPubPem, + oaepLabel + }, Buffer.alloc(10)); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + } +} + +// Test RSA key signing/verification +let rsaSign = crypto.createSign('SHA1'); +let rsaVerify = crypto.createVerify('SHA1'); +assert.ok(rsaSign); +assert.ok(rsaVerify); + +const expectedSignature = fixtures.readKey( + 'rsa_public_sha1_signature_signedby_rsa_private_pkcs8.sha1', + 'hex' +); + +rsaSign.update(rsaPubPem); +let rsaSignature = rsaSign.sign(rsaKeyPem, 'hex'); +assert.strictEqual(rsaSignature, expectedSignature); + +rsaVerify.update(rsaPubPem); +assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); + +// Test RSA PKCS#8 key signing/verification +rsaSign = crypto.createSign('SHA1'); +rsaSign.update(rsaPubPem); +rsaSignature = rsaSign.sign(rsaPkcs8KeyPem, 'hex'); +assert.strictEqual(rsaSignature, expectedSignature); + +rsaVerify = crypto.createVerify('SHA1'); +rsaVerify.update(rsaPubPem); +assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); + +// Test RSA key signing/verification with encrypted key +rsaSign = crypto.createSign('SHA1'); +rsaSign.update(rsaPubPem); +const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'password' }; +rsaSignature = rsaSign.sign(signOptions, 'hex'); +assert.strictEqual(rsaSignature, expectedSignature); + +rsaVerify = crypto.createVerify('SHA1'); +rsaVerify.update(rsaPubPem); +assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); + +rsaSign = crypto.createSign('SHA1'); +rsaSign.update(rsaPubPem); +assert.throws(() => { + const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'wrong' }; + rsaSign.sign(signOptions, 'hex'); +}, decryptPrivateKeyError); + +// +// Test RSA signing and verification +// +{ + const privateKey = fixtures.readKey('rsa_private_b.pem'); + const publicKey = fixtures.readKey('rsa_public_b.pem'); + + const input = 'I AM THE WALRUS'; + + const signature = fixtures.readKey( + 'I_AM_THE_WALRUS_sha256_signature_signedby_rsa_private_b.sha256', + 'hex' + ); + + const sign = crypto.createSign('SHA256'); + sign.update(input); + + const output = sign.sign(privateKey, 'hex'); + assert.strictEqual(output, signature); + + const verify = crypto.createVerify('SHA256'); + verify.update(input); + + assert.strictEqual(verify.verify(publicKey, signature, 'hex'), true); + + // Test the legacy signature algorithm name. + const sign2 = crypto.createSign('RSA-SHA256'); + sign2.update(input); + + const output2 = sign2.sign(privateKey, 'hex'); + assert.strictEqual(output2, signature); + + const verify2 = crypto.createVerify('SHA256'); + verify2.update(input); + + assert.strictEqual(verify2.verify(publicKey, signature, 'hex'), true); +} + + +// +// Test DSA signing and verification +// +{ + const input = 'I AM THE WALRUS'; + + // DSA signatures vary across runs so there is no static string to verify + // against. + const sign = crypto.createSign('SHA1'); + sign.update(input); + const signature = sign.sign(dsaKeyPem, 'hex'); + + const verify = crypto.createVerify('SHA1'); + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); + + // Test the legacy 'DSS1' name. + const sign2 = crypto.createSign('DSS1'); + sign2.update(input); + const signature2 = sign2.sign(dsaKeyPem, 'hex'); + + const verify2 = crypto.createVerify('DSS1'); + verify2.update(input); + + assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); +} + + +// +// Test DSA signing and verification with PKCS#8 private key +// +{ + const input = 'I AM THE WALRUS'; + + // DSA signatures vary across runs so there is no static string to verify + // against. + const sign = crypto.createSign('SHA1'); + sign.update(input); + const signature = sign.sign(dsaPkcs8KeyPem, 'hex'); + + const verify = crypto.createVerify('SHA1'); + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); +} + + +// +// Test DSA signing and verification with encrypted key +// +const input = 'I AM THE WALRUS'; + +{ + const sign = crypto.createSign('SHA1'); + sign.update(input); + assert.throws(() => { + sign.sign({ key: dsaKeyPemEncrypted, passphrase: 'wrong' }, 'hex'); + }, decryptPrivateKeyError); +} + +{ + // DSA signatures vary across runs so there is no static string to verify + // against. + const sign = crypto.createSign('SHA1'); + sign.update(input); + const signOptions = { key: dsaKeyPemEncrypted, passphrase: 'password' }; + const signature = sign.sign(signOptions, 'hex'); + + const verify = crypto.createVerify('SHA1'); + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-secure-heap.js b/test/js/node/test/parallel/test-crypto-secure-heap.js new file mode 100644 index 0000000000..9e19181e40 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-secure-heap.js @@ -0,0 +1,82 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L26 + +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (common.isWindows) + common.skip('Not supported on Windows'); + +if (common.isASan) + common.skip('ASan does not play well with secure heap allocations'); + +const assert = require('assert'); +const { fork } = require('child_process'); +const fixtures = require('../common/fixtures'); +const { + secureHeapUsed, + createDiffieHellman, +} = require('crypto'); + +if (process.argv[2] === 'child') { + + const a = secureHeapUsed(); + + assert(a); + assert.strictEqual(typeof a, 'object'); + assert.strictEqual(a.total, 65536); + assert.strictEqual(a.min, 4); + assert.strictEqual(a.used, 0); + + { + const size = common.hasFipsCrypto || common.hasOpenSSL3 ? 1024 : 256; + const dh1 = createDiffieHellman(size); + const p1 = dh1.getPrime('buffer'); + const dh2 = createDiffieHellman(p1, 'buffer'); + const key1 = dh1.generateKeys(); + const key2 = dh2.generateKeys('hex'); + dh1.computeSecret(key2, 'hex', 'base64'); + dh2.computeSecret(key1, 'latin1', 'buffer'); + + const b = secureHeapUsed(); + assert(b); + assert.strictEqual(typeof b, 'object'); + assert.strictEqual(b.total, 65536); + assert.strictEqual(b.min, 4); + // The amount used can vary on a number of factors + assert(b.used > 0); + assert(b.utilization > 0.0); + } + + return; +} + +const child = fork( + process.argv[1], + ['child'], + { execArgv: ['--secure-heap=65536', '--secure-heap-min=4'] }); + +child.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); +})); + +{ + const child = fork(fixtures.path('a.js'), { + execArgv: ['--secure-heap=3', '--secure-heap-min=3'], + stdio: 'pipe' + }); + let res = ''; + child.on('exit', common.mustCall((code) => { + assert.notStrictEqual(code, 0); + assert.match(res, /--secure-heap must be a power of 2/); + assert.match(res, /--secure-heap-min must be a power of 2/); + })); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (chunk) => res += chunk); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-subtle-zero-length.js b/test/js/node/test/parallel/test-crypto-subtle-zero-length.js new file mode 100644 index 0000000000..f5a84727b0 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-subtle-zero-length.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const k = await subtle.importKey( + 'raw', + new Uint8Array(32), + { name: 'AES-GCM' }, + false, + [ 'encrypt', 'decrypt' ]); + assert(k instanceof CryptoKey); + + const e = await subtle.encrypt({ + name: 'AES-GCM', + iv: new Uint8Array(12), + }, k, new Uint8Array(0)); + assert(e instanceof ArrayBuffer); + assert.deepStrictEqual( + Buffer.from(e), + Buffer.from([ + 0x53, 0x0f, 0x8a, 0xfb, 0xc7, 0x45, 0x36, 0xb9, + 0xa9, 0x63, 0xb4, 0xf1, 0xc4, 0xcb, 0x73, 0x8b ])); + + const v = await subtle.decrypt({ + name: 'AES-GCM', + iv: new Uint8Array(12), + }, k, e); + assert(v instanceof ArrayBuffer); + assert.strictEqual(v.byteLength, 0); +})().then(common.mustCall()).catch((e) => { + assert.ifError(e); +}); diff --git a/test/js/node/test/parallel/test-crypto-update-encoding.js b/test/js/node/test/parallel/test-crypto-update-encoding.js new file mode 100644 index 0000000000..e1e6d029aa --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-update-encoding.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const crypto = require('crypto'); + +const zeros = Buffer.alloc; +const key = zeros(16); +const iv = zeros(16); + +const cipher = () => crypto.createCipheriv('aes-128-cbc', key, iv); +const decipher = () => crypto.createDecipheriv('aes-128-cbc', key, iv); +const hash = () => crypto.createSign('sha256'); +const hmac = () => crypto.createHmac('sha256', key); +const sign = () => crypto.createSign('sha256'); +const verify = () => crypto.createVerify('sha256'); + +for (const f of [cipher, decipher, hash, hmac, sign, verify]) + for (const n of [15, 16]) + f().update(zeros(n), 'hex'); // Should ignore inputEncoding. diff --git a/test/js/node/test/parallel/test-crypto-verify-failure.js b/test/js/node/test/parallel/test-crypto-verify-failure.js new file mode 100644 index 0000000000..ad7d5d4f86 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-verify-failure.js @@ -0,0 +1,67 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const crypto = require('crypto'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const certPem = fixtures.readKey('rsa_cert.crt'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = tls.Server(options, (socket) => { + setImmediate(() => { + verify(); + setImmediate(() => { + socket.destroy(); + }); + }); +}); + +function verify() { + crypto.createVerify('SHA1') + .update('Test') + .verify(certPem, 'asdfasdfas', 'base64'); +} + +server.listen(0, common.mustCall(() => { + tls.connect({ + port: server.address().port, + rejectUnauthorized: false + }, common.mustCall(() => { + verify(); + })) + .on('error', common.mustNotCall()) + .on('close', common.mustCall(() => { + server.close(); + })).resume(); +})); + +server.unref(); diff --git a/test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js b/test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js new file mode 100644 index 0000000000..589a2f91a1 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +subtle.importKey( + 'raw', + new Uint8Array(32), + { + name: 'AES-GCM' + }, + false, + [ 'encrypt', 'decrypt' ]) + .then((k) => + assert.rejects(() => { + return subtle.decrypt({ + name: 'AES-GCM', + iv: new Uint8Array(12), + }, k, new Uint8Array(0)); + }, { + name: 'OperationError', + message: /The provided data is too small/, + }) + ).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-crypto-x509.js b/test/js/node/test/parallel/test-crypto-x509.js new file mode 100644 index 0000000000..2530569bfb --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-x509.js @@ -0,0 +1,446 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { + X509Certificate, + createPrivateKey, + generateKeyPairSync, + createSign, +} = require('crypto'); + +// const { +// isX509Certificate +// } = require('internal/crypto/x509'); + +const { isX509Certificate } = process.binding("crypto/x509"); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { readFileSync } = require('fs'); + +const cert = readFileSync(fixtures.path('keys', 'agent1-cert.pem')); +const key = readFileSync(fixtures.path('keys', 'agent1-key.pem')); +const ca = readFileSync(fixtures.path('keys', 'ca1-cert.pem')); + +const privateKey = createPrivateKey(key); + +[1, {}, false, null].forEach((i) => { + assert.throws(() => new X509Certificate(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +const subjectCheck = `C=US +ST=CA +L=SF +O=Joyent +OU=Node.js +CN=agent1 +emailAddress=ry@tinyclouds.org`; + +const issuerCheck = `C=US +ST=CA +L=SF +O=Joyent +OU=Node.js +CN=ca1 +emailAddress=ry@tinyclouds.org`; + +let infoAccessCheck = `OCSP - URI:http://ocsp.nodejs.org/ +CA Issuers - URI:http://ca.nodejs.org/ca.cert`; +if (!common.hasOpenSSL3) + infoAccessCheck += '\n'; + +const der = Buffer.from( + '308203e8308202d0a0030201020214147d36c1c2f74206de9fab5f2226d78adb00a42630' + + '0d06092a864886f70d01010b0500307a310b3009060355040613025553310b3009060355' + + '04080c024341310b300906035504070c025346310f300d060355040a0c064a6f79656e74' + + '3110300e060355040b0c074e6f64652e6a73310c300a06035504030c036361313120301e' + + '06092a864886f70d010901161172794074696e79636c6f7564732e6f72673020170d3232' + + '303930333231343033375a180f32323936303631373231343033375a307d310b30090603' + + '55040613025553310b300906035504080c024341310b300906035504070c025346310f30' + + '0d060355040a0c064a6f79656e743110300e060355040b0c074e6f64652e6a73310f300d' + + '06035504030c066167656e74313120301e06092a864886f70d010901161172794074696e' + + '79636c6f7564732e6f726730820122300d06092a864886f70d01010105000382010f0030' + + '82010a0282010100d456320afb20d3827093dc2c4284ed04dfbabd56e1ddae529e28b790' + + 'cd4256db273349f3735ffd337c7a6363ecca5a27b7f73dc7089a96c6d886db0c62388f1c' + + 'dd6a963afcd599d5800e587a11f908960f84ed50ba25a28303ecda6e684fbe7baedc9ce8' + + '801327b1697af25097cee3f175e400984c0db6a8eb87be03b4cf94774ba56fffc8c63c68' + + 'd6adeb60abbe69a7b14ab6a6b9e7baa89b5adab8eb07897c07f6d4fa3d660dff574107d2' + + '8e8f63467a788624c574197693e959cea1362ffae1bba10c8c0d88840abfef103631b2e8' + + 'f5c39b5548a7ea57e8a39f89291813f45a76c448033a2b7ed8403f4baa147cf35e2d2554' + + 'aa65ce49695797095bf4dc6b0203010001a361305f305d06082b06010505070101045130' + + '4f302306082b060105050730018617687474703a2f2f6f6373702e6e6f64656a732e6f72' + + '672f302806082b06010505073002861c687474703a2f2f63612e6e6f64656a732e6f7267' + + '2f63612e63657274300d06092a864886f70d01010b05000382010100c3349810632ccb7d' + + 'a585de3ed51e34ed154f0f7215608cf2701c00eda444dc2427072c8aca4da6472c1d9e68' + + 'f177f99a90a8b5dbf3884586d61cb1c14ea7016c8d38b70d1b46b42947db30edc1e9961e' + + 'd46c0f0e35da427bfbe52900771817e733b371adf19e12137235141a34347db0dfc05579' + + '8b1f269f3bdf5e30ce35d1339d56bb3c570de9096215433047f87ca42447b44e7e6b5d0e' + + '48f7894ab186f85b6b1a74561b520952fea888617f32f582afce1111581cd63efcc68986' + + '00d248bb684dedb9c3d6710c38de9e9bc21f9c3394b729d5f707d64ea890603e5989f8fa' + + '59c19ad1a00732e7adc851b89487cc00799dde068aa64b3b8fd976e8bc113ef2', + 'hex'); + +{ + const x509 = new X509Certificate(cert); + + assert(isX509Certificate(x509)); + + assert(!x509.ca); + assert.strictEqual(x509.subject, subjectCheck); + assert.strictEqual(x509.subjectAltName, undefined); + assert.strictEqual(x509.issuer, issuerCheck); + assert.strictEqual(x509.infoAccess, infoAccessCheck); + assert.strictEqual(x509.validFrom, 'Sep 3 21:40:37 2022 GMT'); + assert.strictEqual(x509.validTo, 'Jun 17 21:40:37 2296 GMT'); + assert.deepStrictEqual(x509.validFromDate, new Date('2022-09-03T21:40:37Z')); + assert.deepStrictEqual(x509.validToDate, new Date('2296-06-17T21:40:37Z')); + assert.strictEqual( + x509.fingerprint, + '8B:89:16:C4:99:87:D2:13:1A:64:94:36:38:A5:32:01:F0:95:3B:53'); + assert.strictEqual( + x509.fingerprint256, + '2C:62:59:16:91:89:AB:90:6A:3E:98:88:A6:D3:C5:58:58:6C:AE:FF:9C:33:' + + '22:7C:B6:77:D3:34:E7:53:4B:05' + ); + assert.strictEqual( + x509.fingerprint512, + '0B:6F:D0:4D:6B:22:53:99:66:62:51:2D:2C:96:F2:58:3F:95:1C:CC:4C:44:' + + '9D:B5:59:AA:AD:A8:F6:2A:24:8A:BB:06:A5:26:42:52:30:A3:37:61:30:A9:' + + '5A:42:63:E0:21:2F:D6:70:63:07:96:6F:27:A7:78:12:08:02:7A:8B' + ); + assert.strictEqual(x509.keyUsage, undefined); + assert.strictEqual(x509.serialNumber.toUpperCase(), '147D36C1C2F74206DE9FAB5F2226D78ADB00A426'); + + assert.deepStrictEqual(x509.raw, der); + + assert(x509.publicKey); + assert.strictEqual(x509.publicKey.type, 'public'); + + assert.strictEqual(x509.toString().replaceAll('\r\n', '\n'), + cert.toString().replaceAll('\r\n', '\n')); + assert.strictEqual(x509.toJSON(), x509.toString()); + + assert(x509.checkPrivateKey(privateKey)); + assert.throws(() => x509.checkPrivateKey(x509.publicKey), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + assert.strictEqual(x509.checkIP('127.0.0.1'), undefined); + assert.strictEqual(x509.checkIP('::'), undefined); + assert.strictEqual(x509.checkHost('agent1'), 'agent1'); + assert.strictEqual(x509.checkHost('agent2'), undefined); + assert.strictEqual(x509.checkEmail('ry@tinyclouds.org'), 'ry@tinyclouds.org'); + assert.strictEqual(x509.checkEmail('sally@example.com'), undefined); + assert.throws(() => x509.checkHost('agent\x001'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + assert.throws(() => x509.checkIP('[::]'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + assert.throws(() => x509.checkEmail('not\x00hing'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + [1, false, null].forEach((i) => { + assert.throws(() => x509.checkHost('agent1', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.checkHost('agent1', { subject: i }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [ + 'wildcards', + 'partialWildcards', + 'multiLabelWildcards', + 'singleLabelSubdomains', + ].forEach((key) => { + [1, '', null, {}].forEach((i) => { + assert.throws(() => x509.checkHost('agent1', { [key]: i }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + }); + + const ca_cert = new X509Certificate(ca); + + assert(x509.checkIssued(ca_cert)); + assert(!x509.checkIssued(x509)); + assert(x509.verify(ca_cert.publicKey)); + assert(!x509.verify(x509.publicKey)); + + assert.throws(() => x509.checkIssued({}), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.checkIssued(''), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.verify({}), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.verify(''), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.verify(privateKey), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + { + // https://github.com/nodejs/node/issues/45377 + // https://github.com/nodejs/node/issues/45485 + // Confirm failures of + // X509Certificate:verify() + // X509Certificate:CheckPrivateKey() + // X509Certificate:CheckCA() + // X509Certificate:CheckIssued() + // X509Certificate:ToLegacy() + // do not affect other functions that use OpenSSL. + // Subsequent calls to e.g. createPrivateKey should not throw. + const keyPair = generateKeyPairSync('ed25519'); + assert(!x509.verify(keyPair.publicKey)); + createPrivateKey(key); + assert(!x509.checkPrivateKey(keyPair.privateKey)); + createPrivateKey(key); + const certPem = ` +-----BEGIN CERTIFICATE----- +MIID6zCCAtOgAwIBAgIUTUREAaNcNL0zPkxAlMX0GJtJ/FcwDQYJKoZIhvcNAQEN +BQAwgYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMREwDwYDVQQH +DAhDYXJsc2JhZDEPMA0GA1UECgwGVmlhc2F0MR0wGwYDVQQLDBRWaWFzYXQgU2Vj +dXJlIE1vYmlsZTEiMCAGA1UEAwwZSGFja2VyT25lIHJlcG9ydCAjMTgwODU5NjAi +GA8yMDIyMTIxNjAwMDAwMFoYDzIwMjMxMjE1MjM1OTU5WjCBiTELMAkGA1UEBhMC +VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExETAPBgNVBAcMCENhcmxzYmFkMQ8wDQYD +VQQKDAZWaWFzYXQxHTAbBgNVBAsMFFZpYXNhdCBTZWN1cmUgTW9iaWxlMSIwIAYD +VQQDDBlIYWNrZXJPbmUgcmVwb3J0ICMxODA4NTk2MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA6I7RBPm4E/9rIrCHV5lfsHI/yYzXtACJmoyP8OMkjbeB +h21oSJJF9FEnbivk6bYaHZIPasa+lSAydRM2rbbmfhF+jQoWYCIbV2ztrbFR70S1 +wAuJrlYYm+8u+1HUru5UBZWUr/p1gFtv3QjpA8+43iwE4pXytTBKPXFo1f5iZwGI +D5Bz6DohT7Tyb8cpQ1uMCMCT0EJJ4n8wUrvfBgwBO94O4qlhs9vYgnDKepJDjptc +uSuEpvHALO8+EYkQ7nkM4Xzl/WK1yFtxxE93Jvd1OvViDGVrRVfsq+xYTKknGLX0 +QIeoDDnIr0OjlYPd/cqyEgMcFyFxwDSzSc1esxdCpQIDAQABo0UwQzAdBgNVHQ4E +FgQUurygsEKdtQk0T+sjM0gEURdveRUwEgYDVR0TAQH/BAgwBgEB/wIB/zAOBgNV +HQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQENBQADggEBAH7mIIXiQsQ4/QGNNFOQzTgP +/bUbMSZJsY5TPAvS9rF9yQVzs4dJZnQk5kEb/qrDQSe27oP0L0hfFm1wTGy+aKfa +BVGHdRmmvHtDUPLA9URCFShqKuS+GXp+6zt7dyZPRrPmiZaciiCMPHOnx59xSdPm +AZG8cD3fmK2ThC4FAMyvRb0qeobka3s22xTQ2kjwJO5gykTkZ+BR6SzRHQTjYMuT +iry9Bu8Kvbzu3r5n+/bmNz+xRNmEeehgT2qsHjA5b2YBVTr9MdN9Ro3H3saA3upr +oans248kpal88CGqsN2so/wZKxVnpiXlPHMdiNL7hRSUqlHkUi07FrP2Htg8kjI= +-----END CERTIFICATE-----`.trim(); + const c = new X509Certificate(certPem); + assert(!c.ca); + const signer = createSign('SHA256'); + assert(signer.sign(key, 'hex')); + + const c1 = new X509Certificate(certPem); + assert(!c1.checkIssued(c1)); + const signer1 = createSign('SHA256'); + assert(signer1.sign(key, 'hex')); + + const c2 = new X509Certificate(certPem); + assert(c2.toLegacyObject()); + const signer2 = createSign('SHA256'); + assert(signer2.sign(key, 'hex')); + } + + // X509Certificate can be cloned via MessageChannel/MessagePort + const mc = new MessageChannel(); + mc.port1.onmessage = common.mustCall(({ data }) => { + assert(isX509Certificate(data)); + assert.deepStrictEqual(data.raw, x509.raw); + mc.port1.close(); + }); + mc.port2.postMessage(x509); + + const modulusOSSL = 'D456320AFB20D3827093DC2C4284ED04DFBABD56E1DDAE529E28B790CD42' + + '56DB273349F3735FFD337C7A6363ECCA5A27B7F73DC7089A96C6D886DB0C' + + '62388F1CDD6A963AFCD599D5800E587A11F908960F84ED50BA25A28303EC' + + 'DA6E684FBE7BAEDC9CE8801327B1697AF25097CEE3F175E400984C0DB6A8' + + 'EB87BE03B4CF94774BA56FFFC8C63C68D6ADEB60ABBE69A7B14AB6A6B9E7' + + 'BAA89B5ADAB8EB07897C07F6D4FA3D660DFF574107D28E8F63467A788624' + + 'C574197693E959CEA1362FFAE1BBA10C8C0D88840ABFEF103631B2E8F5C3' + + '9B5548A7EA57E8A39F89291813F45A76C448033A2B7ED8403F4BAA147CF3' + + '5E2D2554AA65CE49695797095BF4DC6B'; + + // Verify that legacy encoding works + const legacyObjectCheck = { + subject: Object.assign({ __proto__: null }, { + C: 'US', + ST: 'CA', + L: 'SF', + O: 'Joyent', + OU: 'Node.js', + CN: 'agent1', + emailAddress: 'ry@tinyclouds.org', + }), + issuer: Object.assign({ __proto__: null }, { + C: 'US', + ST: 'CA', + L: 'SF', + O: 'Joyent', + OU: 'Node.js', + CN: 'ca1', + emailAddress: 'ry@tinyclouds.org', + }), + infoAccess: Object.assign({ __proto__: null }, { + 'OCSP - URI': ['http://ocsp.nodejs.org/'], + 'CA Issuers - URI': ['http://ca.nodejs.org/ca.cert'] + }), + modulusPattern: new RegExp(`^${modulusOSSL}$`, 'i'), + bits: 2048, + exponent: '0x10001', + valid_from: 'Sep 3 21:40:37 2022 GMT', + valid_to: 'Jun 17 21:40:37 2296 GMT', + fingerprint: '8B:89:16:C4:99:87:D2:13:1A:64:94:36:38:A5:32:01:F0:95:3B:53', + fingerprint256: + '2C:62:59:16:91:89:AB:90:6A:3E:98:88:A6:D3:C5:58:58:6C:AE:FF:9C:33:' + + '22:7C:B6:77:D3:34:E7:53:4B:05', + fingerprint512: + '51:62:18:39:E2:E2:77:F5:86:11:E8:C0:CA:54:43:7C:76:83:19:05:D0:03:' + + '24:21:B8:EB:14:61:FB:24:16:EB:BD:51:1A:17:91:04:30:03:EB:68:5F:DC:' + + '86:E1:D1:7C:FB:AF:78:ED:63:5F:29:9C:32:AF:A1:8E:22:96:D1:02', + serialNumberPattern: /^147D36C1C2F74206DE9FAB5F2226D78ADB00A426$/i + }; + + const legacyObject = x509.toLegacyObject(); + + assert.deepStrictEqual(legacyObject.raw, x509.raw); + assert.deepStrictEqual(legacyObject.subject, legacyObjectCheck.subject); + assert.deepStrictEqual(legacyObject.issuer, legacyObjectCheck.issuer); + assert.deepStrictEqual(legacyObject.infoAccess, legacyObjectCheck.infoAccess); + assert.match(legacyObject.modulus, legacyObjectCheck.modulusPattern); + assert.strictEqual(legacyObject.bits, legacyObjectCheck.bits); + assert.strictEqual(legacyObject.exponent, legacyObjectCheck.exponent); + assert.strictEqual(legacyObject.valid_from, legacyObjectCheck.valid_from); + assert.strictEqual(legacyObject.valid_to, legacyObjectCheck.valid_to); + assert.strictEqual(legacyObject.fingerprint, legacyObjectCheck.fingerprint); + assert.strictEqual( + legacyObject.fingerprint256, + legacyObjectCheck.fingerprint256); + assert.match( + legacyObject.serialNumber, + legacyObjectCheck.serialNumberPattern); +} + + +/* +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L549 +{ + // This X.509 Certificate can be parsed by OpenSSL because it contains a + // structurally sound TBSCertificate structure. However, the SPKI field of the + // TBSCertificate contains the subjectPublicKey as a BIT STRING, and this bit + // sequence is not a valid public key. Ensure that X509Certificate.publicKey + // does not abort in this case. + + const certPem = `-----BEGIN CERTIFICATE----- +MIIDpDCCAw0CFEc1OZ8g17q+PZnna3iQ/gfoZ7f3MA0GCSqGSIb3DQEBBQUAMIHX +MRMwEQYLKwYBBAGCNzwCAQMTAkdJMR0wGwYDVQQPExRQcml2YXRlIE9yZ2FuaXph +dGlvbjEOMAwGA1UEBRMFOTkxOTExCzAJBgNVBAYTAkdJMRIwEAYDVQQIFAlHaWJy +YWx0YXIxEjAQBgNVBAcUCUdpYnJhbHRhcjEgMB4GA1UEChQXV0hHIChJbnRlcm5h +dGlvbmFsKSBMdGQxHDAaBgNVBAsUE0ludGVyYWN0aXZlIEJldHRpbmcxHDAaBgNV +BAMUE3d3dy53aWxsaWFtaGlsbC5jb20wIhgPMjAxNDAyMDcwMDAwMDBaGA8yMDE1 +MDIyMTIzNTk1OVowgbAxCzAJBgNVBAYTAklUMQ0wCwYDVQQIEwRSb21lMRAwDgYD +VQQHEwdQb21lemlhMRYwFAYDVQQKEw1UZWxlY29taXRhbGlhMRIwEAYDVQQrEwlB +RE0uQVAuUE0xHTAbBgNVBAMTFHd3dy50ZWxlY29taXRhbGlhLml0MTUwMwYJKoZI +hvcNAQkBFiZ2YXNlc2VyY2l6aW9wb3J0YWxpY29AdGVsZWNvbWl0YWxpYS5pdDCB +nzANBgkqhkiG9w0BAQEFAAOBjQA4gYkCgYEA5m/Vf7PevH+inMfUJOc8GeR7WVhM +CQwcMM5k46MSZo7kCk7VZuaq5G2JHGAGnLPaPUkeXlrf5qLpTxXXxHNtz+WrDlFt +boAdnTcqpX3+72uBGOaT6Wi/9YRKuCs5D5/cAxAc3XjHfpRXMoXObj9Vy7mLndfV +/wsnTfU9QVeBkgsCAwEAAaOBkjCBjzAdBgNVHQ4EFgQUfLjAjEiC83A+NupGrx5+ +Qe6nhRMwbgYIKwYBBQUHAQwEYjBgoV6gXDBaMFgwVhYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUS2u5KJYGDLvQUjibKaxLB4shBRgwJhYkaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nbzEuZ2lmMA0GCSqGSIb3DQEBBQUAA4GBALLiAMX0cIMp ++V/JgMRhMEUKbrt5lYKfv9dil/f22ezZaFafb070jGMMPVy9O3/PavDOkHtTv3vd +tAt3hIKFD1bJt6c6WtMH2Su3syosWxmdmGk5ihslB00lvLpfj/wed8i3bkcB1doq +UcXd/5qu2GhokrKU2cPttU+XAN2Om6a0 +-----END CERTIFICATE-----`; + + const cert = new X509Certificate(certPem); + assert.throws(() => cert.publicKey, { + message: common.hasOpenSSL3 ? /decode error/ : /wrong tag/, + name: 'Error' + }); + + assert.strictEqual(cert.checkIssued(cert), false); +} +*/ + +{ + // Test date parsing of `validFromDate` and `validToDate` fields, according to RFC 5280. + + // Validity dates up until the year 2049 are encoded as UTCTime. + // The formatting of UTCTime changes from the year ~1949 to 1950~. + const certPemUTCTime = `-----BEGIN CERTIFICATE----- +MIIE/TCCAuWgAwIBAgIUHbXPaFnjeBehMvdHkXZ+E3a78QswDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEBhMCS1IwIBgPMTk0OTEyMjUyMzU5NThaFw01MDAxMDEyMzU5 +NThaMA0xCzAJBgNVBAYTAktSMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAtFfV2DB2dZFFaR1PPZMmyo0mSDAxGReoixxlhQTFZZymU71emWV/6gR8MxAE +L5+uzpgBvOZWgEbELWeV/gzZGU/x1Cki0dSJ0B8Qwr5HvKX6oOZrJ8t+wn4SRceq +r6MRPskDpTjnvelt+VURGmawtKKHll5fSqfjRWkQC8WQHdogXylRjd3oIh9p1D5P +hphK/jKddxsRkLhJKQWqTjAy2v8hsJAxvpCPnlqMCXxjbQV41UTY8+kY3RPG3d6c +yHBGM7dzM7XWVc79V9z/rjdRcxE2eBqrJT/yR3Cok8wWVVfQEgBfpolHUZxA8K4N +tubTez9zsJy7xUG7udf91wXWVHMBHXg6m/u5nIW0fAXGMtnG/H6FMyyBDbJoUlqm +VRTG71DzvBXpd/qx2P5LkU1JjWY3U8HSn6Q1DJzMIrbOmWpdlFYXxzLlXU2vG8Q3 +PmdAHDDYW3M2YBVCdKqOtsuL2dMDuqRWdi3iCCPSR2UCm4HzAVYSe2FP8SPcY3xs +1NX+oDSpTxXruJYHGUp10/pXoqMrGT1IBgv2Dhsm3jcfRLSXkaBDJIKLO6dXmLBt +rlxM0DphiKnP6lDjpv7EDMdwsakz0zib3JrTmSLSbwZXR4abITmtbYbTpY3XAq7c +adO8YCMTCtb50ZbYEpGDAjOcWFHUlQQMsgZM2zc8ZHPY4EkCAwEAAaNTMFEwHQYD +VR0OBBYEFExDmZyzdo8ccjX7iFIwU7JYMV+qMB8GA1UdIwQYMBaAFExDmZyzdo8c +cjX7iFIwU7JYMV+qMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB +ADEF/JIH+Ku9NqrO47Q/CEn9qpIgmqX10d1joDjchPY3OHIIyt8Xpo845mPBTM7L +dnMJSlkzJEk0ep9qAGGdKpBnLq8B/1mgCWQ81jwrwdYSsY+4xark+7+y0fij6qAt +L4T6aA37nbV5q5/DMOwZucFwRTf9ZI1IjC+MaQmnV01vGCogqqfLQ9v26bVBRE1K +UIixH0r3f/LWtuo0KaebZbb+oq6Zb8ljKJaUlt5OB8Zy5NrcP69r29QJUR57ukT6 +rt7fk5mOj2NBLMCErLHa7E6+GAUG94QEgdKzZ4yr2aduhMAfnOnK/HfuXO8TVa8/ ++oYENr47M8x139+yu92C8Be1MRk0VHteBaScUL+IaY3HgGbYR1lT0azvIyBN/DCN +bYczI7JQGYVitLuaUYFw/RtK7Qg1957/ZmGeGa+86aTLXbqsGjI951D81EIzdqod +1QW/Jn3yMNeVIzF9eYVEy2DIJjGgM2A8NWbqfWGUAUMRgyTxH1j42tnWG3eRnMsX +UnQfpY8i3v6gYoNNgEZktrqgpmukTWgl08TlDtBCjXTBkcBt4dxDApeoy7XWKq+/ +qBY/+uIsG30BRgJhAwApjdnCs7l5xpwtqluXFwOxyTWNV5IfChO7QFqWPlSVIHML +UidvpWWipVLZgK+oDks+bKTobcoXGW9oXobiIYqslXPy +-----END CERTIFICATE-----`.trim(); + const c1 = new X509Certificate(certPemUTCTime); + + assert.deepStrictEqual(c1.validFromDate, new Date('1949-12-25T23:59:58Z')); + assert.deepStrictEqual(c1.validToDate, new Date('1950-01-01T23:59:58Z')); + + // The GeneralizedTime format is used for dates in 2050 or later. + const certPemGeneralizedTime = `-----BEGIN CERTIFICATE----- +MIIE/TCCAuWgAwIBAgIUYHPUNd6S5xlNMjrWSaekgCBrbDQwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEBhMCS1IwIBcNNDkxMjI2MDAwMDAxWhgPMjA1MDAxMDIwMDAw +MDFaMA0xCzAJBgNVBAYTAktSMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAlBPjQXHTzQWflvq6Lc01E0gVSSUQd5XnfK9K8TEN8ic/6iJVBWK8OwTmwh6u +KdSO+DrTpoTA3Wo4T7oSL89xsyJN5JHiIT2VdZvgcXkv+ZL+rZ2INzYSSXbPQ8V+ +Md5A7tNWGJOvneD1Pb+AKrVXn6N1+xiKuv08U+d6ZCcv8P2cGUJCQr5BSg6eXPm2 +ZIoFhNLDaqleci0P/Bs7uMwKjVr2IP99bCMwTS2STxexEmYf4J3wgNXBOHxspLcS +p7Yt3JgezvzRn5kijQi7ceS24q/fsGCCwB706mOKdYLCfEL1DhhEr27+XICw7zOF +Q8tSe33IfSdxejEVV+lf/jGW5zFH5m+lDTJC0VAUCBG5E7q57yFaoQ44CQWtbMHZ ++dtodKx4B0lzWXJs8xkGo0rl9/1CuY2iPX3lB6xxlX50ruj8stccMwarRzUvfkjw +AhnbUs9X1ooFyVXmVYXWzR0gP1/q05Zob03khX1NipGbMf0RBI4WlItkiRsrEl9x +08YPbrUyd7JnFkgG0O5TcmTzHr9cTJHg5BzclQA9/V0HuslSVOkKMMlKHky2zcqY +dDBmWtfTrvowaB7hTGD6YK4R9JCDUy7oeeK4ZUxRNCnJY698HodE9lQu+F0cJpbY +uZExFapE/AWA8ftlw2/fXoK0L3DhYsOVQkHd2YbrvzZEHVMCAwEAAaNTMFEwHQYD +VR0OBBYEFNptaIzozylFlD0+JKULue+5gvfZMB8GA1UdIwQYMBaAFNptaIzozylF +lD0+JKULue+5gvfZMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB +AFXP4SCP6VFMINaKE/rLJOaFiVVuS4qDfpGRjuBryNxey4ErBFsXcPi/D0LIsNkb +c3qsex1cJadZdg3sqdkyliyYgzjJn1fSsiPT8GMiMnckw9awIwqtucGf+6SPdL6o +9jp0xg6KsRHWjN0GetCz89hy9HwSiSwiLpTxVYOMLjQ+ey8KXPk0LNaXve/++hrr +gN+cvcPKkspAE5SMTSKqHwVUD4MRJgdQqYDqB6demCq9Yl+kyQg9gVnuzkpKeNBT +qNVeeA6gczCpYV4rUMqT0UVVPbPOcygwZP2o7tUyNk6fmYzyLpi5R+FYD/PoowFp +LOrIaG426QaXhLr4U0i+HD/LhHZ4AWWt0OYAvbkk/xrhmagUcyeOxUrcYl6tA3NQ +sjPV2FNGitX+zOyxfMxcjf0RpaBbyMsO6DSfQidDchFvPR9VFX4THs/0mP02IK27 +MpsZj8AG2/jjPz6ytnWBJGuLeIt2sWnluZyldX+V9QEEhEmrEweUolacKF5ESODG +SHyZZVSUCK0bJfDfk5rXCQokWCIe+jHbW3CSWWmBRz6blZDeO/wI8nN4TWHDMCu6 +lawls1QdAwfP4CWIq4T7gsn/YqxMs74zDCXIF0tfuPmw5FMeCYVgnXQ7et8HBfeE +CWwQO8JZjJqFtqtuzy2n+gLCvqePgG/gmSqHOPm2ZbLW +-----END CERTIFICATE-----`.trim(); + const c2 = new X509Certificate(certPemGeneralizedTime); + + assert.deepStrictEqual(c2.validFromDate, new Date('2049-12-26T00:00:01Z')); + assert.deepStrictEqual(c2.validToDate, new Date('2050-01-02T00:00:01Z')); +} diff --git a/test/js/node/test/parallel/test-datetime-change-notify.js b/test/js/node/test/parallel/test-datetime-change-notify.js new file mode 100644 index 0000000000..0184351190 --- /dev/null +++ b/test/js/node/test/parallel/test-datetime-change-notify.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!common.hasIntl) + common.skip('Intl not present.'); + +if (!isMainThread) + common.skip('Test not support running within a worker'); + +const assert = require('assert'); + +const cases = [ + { + timeZone: 'Etc/UTC', + expected: /Coordinated Universal Time/, + }, + { + timeZone: 'America/New_York', + expected: /Eastern (?:Standard|Daylight) Time/, + }, + { + timeZone: 'America/Los_Angeles', + expected: /Pacific (?:Standard|Daylight) Time/, + }, + { + timeZone: 'Europe/Dublin', + expected: /Irish Standard Time|Greenwich Mean Time/, + }, +]; + +for (const { timeZone, expected } of cases) { + process.env.TZ = timeZone; + const date = new Date().toLocaleString('en-US', { timeZoneName: 'long' }); + assert.match(date, expected); +} diff --git a/test/js/node/test/parallel/test-debugger-backtrace.js b/test/js/node/test/parallel/test-debugger-backtrace.js new file mode 100644 index 0000000000..f66cc11d70 --- /dev/null +++ b/test/js/node/test/parallel/test-debugger-backtrace.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const fixtures = require('../common/fixtures'); +const startCLI = require('../common/debugger'); + +const assert = require('assert'); +const path = require('path'); + +// Display and navigate backtrace. +{ + const scriptFullPath = fixtures.path('debugger', 'backtrace.js'); + const script = path.relative(process.cwd(), scriptFullPath); + const cli = startCLI(['--port=0', script]); + + async function runTest() { + try { + await cli.waitForInitialBreak(); + await cli.waitForPrompt(); + await cli.stepCommand('c'); + await cli.command('bt'); + assert.ok(cli.output.includes(`#0 topFn ${script}:7:2`)); + await cli.command('backtrace'); + assert.ok(cli.output.includes(`#0 topFn ${script}:7:2`)); + } finally { + await cli.quit(); + } + } + + runTest(); +} diff --git a/test/js/node/test/parallel/test-debugger-exec.js b/test/js/node/test/parallel/test-debugger-exec.js new file mode 100644 index 0000000000..51bc749734 --- /dev/null +++ b/test/js/node/test/parallel/test-debugger-exec.js @@ -0,0 +1,68 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const fixtures = require('../common/fixtures'); +const startCLI = require('../common/debugger'); + +const assert = require('assert'); + +const cli = startCLI(['--port=0', fixtures.path('debugger/alive.js')]); + +async function waitInitialBreak() { + try { + await cli.waitForInitialBreak(); + await cli.waitForPrompt(); + await cli.command('exec [typeof heartbeat, typeof process.exit]'); + assert.match(cli.output, /\[ 'function', 'function' \]/, 'works w/o paren'); + + await cli.command('p [typeof heartbeat, typeof process.exit]'); + assert.match( + cli.output, + /\[ 'function', 'function' \]/, + 'works w/o paren, short' + ); + + await cli.command('repl'); + assert.match( + cli.output, + /Press Ctrl\+C to leave debug repl\n+> /, + 'shows hint for how to leave repl' + ); + assert.doesNotMatch(cli.output, /debug>/, 'changes the repl style'); + + await cli.command('[typeof heartbeat, typeof process.exit]'); + await cli.waitFor(/function/); + await cli.waitForPrompt(); + assert.match( + cli.output, + /\[ 'function', 'function' \]/, + 'can evaluate in the repl' + ); + assert.match(cli.output, /> $/); + + await cli.ctrlC(); + await cli.waitFor(/debug> $/); + await cli.command('exec("[typeof heartbeat, typeof process.exit]")'); + assert.match(cli.output, /\[ 'function', 'function' \]/, 'works w/ paren'); + await cli.command('p("[typeof heartbeat, typeof process.exit]")'); + assert.match( + cli.output, + /\[ 'function', 'function' \]/, + 'works w/ paren, short' + ); + + await cli.command('cont'); + await cli.command('exec [typeof heartbeat, typeof process.exit]'); + assert.match( + cli.output, + /\[ 'undefined', 'function' \]/, + 'non-paused exec can see global but not module-scope values' + ); + } finally { + await cli.quit(); + } +} + +waitInitialBreak(); diff --git a/test/js/node/test/parallel/test-debugger-invalid-json.mjs b/test/js/node/test/parallel/test-debugger-invalid-json.mjs new file mode 100644 index 0000000000..e4754a465f --- /dev/null +++ b/test/js/node/test/parallel/test-debugger-invalid-json.mjs @@ -0,0 +1,46 @@ +import { skipIfInspectorDisabled, mustCall } from '../common/index.mjs'; + +skipIfInspectorDisabled(); + +import startCLI from '../common/debugger.js'; + +import assert from 'assert'; +import http from 'http'; + +const host = '127.0.0.1'; + +{ + const server = http.createServer((req, res) => { + res.statusCode = 400; + res.end('Bad Request'); + }); + + server.listen(0, mustCall(async () => { + const port = server.address().port; + const cli = startCLI([`${host}:${port}`]); + try { + const code = await cli.quit(); + assert.strictEqual(code, 1); + } finally { + server.close(); + } + })); +} + +{ + const server = http.createServer((req, res) => { + res.statusCode = 200; + res.end('some data that is invalid json'); + }); + + server.listen(0, host, mustCall(async () => { + const port = server.address().port; + const cli = startCLI([`${host}:${port}`]); + try { + const code = await cli.quit(); + assert.strictEqual(code, 1); + } finally { + server.close(); + } + })); +} diff --git a/test/js/node/test/parallel/test-debugger-low-level.js b/test/js/node/test/parallel/test-debugger-low-level.js new file mode 100644 index 0000000000..31f67849f5 --- /dev/null +++ b/test/js/node/test/parallel/test-debugger-low-level.js @@ -0,0 +1,35 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const fixtures = require('../common/fixtures'); +const startCLI = require('../common/debugger'); +const assert = require('assert'); + +// Debugger agent direct access. +{ + const cli = startCLI(['--port=0', fixtures.path('debugger/three-lines.js')]); + const scriptPattern = /^\* (\d+): \S+debugger(?:\/|\\)three-lines\.js/m; + + async function testDebuggerLowLevel() { + try { + await cli.waitForInitialBreak(); + await cli.waitForPrompt(); + await cli.command('scripts'); + const [, scriptId] = cli.output.match(scriptPattern); + await cli.command( + `Debugger.getScriptSource({ scriptId: '${scriptId}' })` + ); + assert.match( + cli.output, + /scriptSource:[ \n]*'(?:\(function \(|let x = 1)/); + assert.match( + cli.output, + /let x = 1;/); + } finally { + await cli.quit(); + } + } + testDebuggerLowLevel(); +} diff --git a/test/js/node/test/parallel/test-debugger-preserve-breaks.js b/test/js/node/test/parallel/test-debugger-preserve-breaks.js new file mode 100644 index 0000000000..00168c570d --- /dev/null +++ b/test/js/node/test/parallel/test-debugger-preserve-breaks.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const fixtures = require('../common/fixtures'); +const startCLI = require('../common/debugger'); + +const assert = require('assert'); +const path = require('path'); + +const scriptFullPath = fixtures.path('debugger', 'three-lines.js'); +const script = path.relative(process.cwd(), scriptFullPath); + +// Run after quit. +const runTest = async () => { + const cli = startCLI(['--port=0', script]); + try { + await cli.waitForInitialBreak(); + await cli.waitForPrompt(); + await cli.command('breakpoints'); + assert.match(cli.output, /No breakpoints yet/); + await cli.command('sb(2)'); + await cli.command('sb(3)'); + await cli.command('breakpoints'); + assert.ok(cli.output.includes(`#0 ${script}:2`)); + assert.ok(cli.output.includes(`#1 ${script}:3`)); + await cli.stepCommand('c'); // hit line 2 + await cli.stepCommand('c'); // hit line 3 + assert.deepStrictEqual(cli.breakInfo, { filename: script, line: 3 }); + await cli.command('restart'); + await cli.waitForInitialBreak(); + assert.deepStrictEqual(cli.breakInfo, { filename: script, line: 1 }); + await cli.stepCommand('c'); + assert.deepStrictEqual(cli.breakInfo, { filename: script, line: 2 }); + await cli.stepCommand('c'); + assert.deepStrictEqual(cli.breakInfo, { filename: script, line: 3 }); + await cli.command('breakpoints'); + const msg = `SCRIPT: ${script}, OUTPUT: ${cli.output}`; + assert.ok(cli.output.includes(`#0 ${script}:2`), msg); + assert.ok(cli.output.includes(`#1 ${script}:3`), msg); + } finally { + await cli.quit(); + } +}; + +runTest(); diff --git a/test/js/node/test/parallel/test-debugger-repeat-last.js b/test/js/node/test/parallel/test-debugger-repeat-last.js new file mode 100644 index 0000000000..9a9b8eecdc --- /dev/null +++ b/test/js/node/test/parallel/test-debugger-repeat-last.js @@ -0,0 +1,45 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +const path = require('../common/fixtures').path; +const spawn = require('child_process').spawn; +const assert = require('assert'); +const fixture = path('debugger-repeat-last.js'); + +const args = [ + 'inspect', + '--port=0', + fixture, +]; + +const proc = spawn(process.execPath, args, { stdio: 'pipe' }); +proc.stdout.setEncoding('utf8'); + +let stdout = ''; + +let sentCommand = false; +let sentExit = false; + +proc.stdout.on('data', (data) => { + stdout += data; + + // Send 'n' as the first step. + if (!sentCommand && stdout.includes('> 1 ')) { + setImmediate(() => { proc.stdin.write('n\n'); }); + return sentCommand = true; + } + // Send empty (repeat last command) until we reach line 5. + if (sentCommand && !stdout.includes('> 5')) { + setImmediate(() => { proc.stdin.write('\n'); }); + return true; + } + if (!sentExit && stdout.includes('> 5')) { + setTimeout(() => { proc.stdin.write('\n\n\n.exit\n\n\n'); }, 1); + return sentExit = true; + } +}); + +process.on('exit', (exitCode) => { + assert.strictEqual(exitCode, 0); + console.log(stdout); +}); diff --git a/test/js/node/test/parallel/test-debugger-restart-message.js b/test/js/node/test/parallel/test-debugger-restart-message.js new file mode 100644 index 0000000000..e4001b47ee --- /dev/null +++ b/test/js/node/test/parallel/test-debugger-restart-message.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); + +const RESTARTS = 10; + +const fixtures = require('../common/fixtures'); +const startCLI = require('../common/debugger'); + +// Using `restart` should result in only one "Connect/For help" message. +{ + const script = fixtures.path('debugger', 'three-lines.js'); + const cli = startCLI(['--port=0', script]); + + const listeningRegExp = /Debugger listening on/g; + + async function onWaitForInitialBreak() { + try { + await cli.waitForInitialBreak(); + await cli.waitForPrompt(); + assert.strictEqual(cli.output.match(listeningRegExp).length, 1); + + for (let i = 0; i < RESTARTS; i++) { + await cli.stepCommand('restart'); + assert.strictEqual(cli.output.match(listeningRegExp).length, 1); + } + } finally { + await cli.quit(); + } + } + + onWaitForInitialBreak(); +} diff --git a/test/js/node/test/parallel/test-delayed-require.js b/test/js/node/test/parallel/test-delayed-require.js new file mode 100644 index 0000000000..355d503d26 --- /dev/null +++ b/test/js/node/test/parallel/test-delayed-require.js @@ -0,0 +1,32 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +setTimeout(common.mustCall(function() { + const a = require(fixtures.path('a')); + assert.strictEqual('A' in a, true); + assert.strictEqual(a.A(), 'A'); + assert.strictEqual(a.D(), 'D'); +}), 50); diff --git a/test/js/node/test/parallel/test-dgram-abort-closed.js b/test/js/node/test/parallel/test-dgram-abort-closed.js new file mode 100644 index 0000000000..6346b5feae --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-abort-closed.js @@ -0,0 +1,10 @@ +'use strict'; +require('../common'); +const dgram = require('dgram'); + +const controller = new AbortController(); +const socket = dgram.createSocket({ type: 'udp4', signal: controller.signal }); + +socket.close(); + +controller.abort(); diff --git a/test/js/node/test/parallel/test-dgram-bind-default-address.js b/test/js/node/test/parallel/test-dgram-bind-default-address.js new file mode 100644 index 0000000000..130d614c58 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-bind-default-address.js @@ -0,0 +1,53 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +// Skip test in FreeBSD jails since 0.0.0.0 will resolve to default interface +if (common.inFreeBSDJail) + common.skip('In a FreeBSD jail'); + +const assert = require('assert'); +const dgram = require('dgram'); + +dgram.createSocket('udp4').bind(0, common.mustCall(function() { + assert.strictEqual(typeof this.address().port, 'number'); + assert.ok(isFinite(this.address().port)); + assert.ok(this.address().port > 0); + assert.strictEqual(this.address().address, '0.0.0.0'); + this.close(); +})); + +if (!common.hasIPv6) { + common.printSkipMessage('udp6 part of test, because no IPv6 support'); + return; +} + +dgram.createSocket('udp6').bind(0, common.mustCall(function() { + assert.strictEqual(typeof this.address().port, 'number'); + assert.ok(isFinite(this.address().port)); + assert.ok(this.address().port > 0); + let address = this.address().address; + if (address === '::ffff:0.0.0.0') + address = '::'; + assert.strictEqual(address, '::'); + this.close(); +})); diff --git a/test/js/node/test/parallel/test-dgram-bind.js b/test/js/node/test/parallel/test-dgram-bind.js new file mode 100644 index 0000000000..8ae1213c7f --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-bind.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const socket = dgram.createSocket('udp4'); + +socket.on('listening', common.mustCall(() => { + assert.throws(() => { + socket.bind(); + }, { + code: 'ERR_SOCKET_ALREADY_BOUND', + name: 'Error', + message: /^Socket is already bound$/ + }); + + socket.close(); +})); + +const result = socket.bind(); // Should not throw. + +assert.strictEqual(result, socket); // Should have returned itself. diff --git a/test/js/node/test/parallel/test-dgram-bytes-length.js b/test/js/node/test/parallel/test-dgram-bytes-length.js new file mode 100644 index 0000000000..abf8209b11 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-bytes-length.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const message = Buffer.from('Some bytes'); +const client = dgram.createSocket('udp4'); +client.send( + message, + 0, + message.length, + 41234, + 'localhost', + function(err, bytes) { + assert.strictEqual(bytes, message.length); + client.close(); + } +); diff --git a/test/js/node/test/parallel/test-dgram-close-in-listening.js b/test/js/node/test/parallel/test-dgram-close-in-listening.js new file mode 100644 index 0000000000..ae3ab71d7e --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-close-in-listening.js @@ -0,0 +1,26 @@ +'use strict'; +// Ensure that if a dgram socket is closed before the sendQueue is drained +// will not crash + +const common = require('../common'); +const dgram = require('dgram'); + +const buf = Buffer.alloc(1024, 42); + +const socket = dgram.createSocket('udp4'); + +socket.on('listening', function() { + socket.close(); +}); + +// Get a random port for send +const portGetter = dgram.createSocket('udp4') + .bind(0, 'localhost', common.mustCall(() => { + // Adds a listener to 'listening' to send the data when + // the socket is available + socket.send(buf, 0, buf.length, + portGetter.address().port, + portGetter.address().address); + + portGetter.close(); + })); diff --git a/test/js/node/test/parallel/test-dgram-close-is-not-callback.js b/test/js/node/test/parallel/test-dgram-close-is-not-callback.js new file mode 100644 index 0000000000..bfb0bcd2bb --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-close-is-not-callback.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); + +const buf = Buffer.alloc(1024, 42); + +const socket = dgram.createSocket('udp4'); + +// Get a random port for send +const portGetter = dgram.createSocket('udp4') + .bind(0, 'localhost', common.mustCall(() => { + socket.send(buf, 0, buf.length, + portGetter.address().port, + portGetter.address().address); + + // If close callback is not function, ignore the argument. + socket.close('bad argument'); + portGetter.close(); + + socket.on('close', common.mustCall()); + })); diff --git a/test/js/node/test/parallel/test-dgram-close-signal.js b/test/js/node/test/parallel/test-dgram-close-signal.js new file mode 100644 index 0000000000..26de38b504 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-close-signal.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +{ + // Test bad signal. + assert.throws( + () => dgram.createSocket({ type: 'udp4', signal: {} }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +} + +{ + // Test close. + const controller = new AbortController(); + const { signal } = controller; + const server = dgram.createSocket({ type: 'udp4', signal }); + server.on('close', common.mustCall()); + controller.abort(); +} + +{ + // Test close with pre-aborted signal. + const signal = AbortSignal.abort(); + const server = dgram.createSocket({ type: 'udp4', signal }); + server.on('close', common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js b/test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js new file mode 100644 index 0000000000..065ff094f1 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js @@ -0,0 +1,38 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) + common.skip('dgram clustering is currently not supported on windows.'); + +const assert = require('assert'); +const cluster = require('cluster'); +const dgram = require('dgram'); + +if (cluster.isPrimary) { + cluster.fork(); +} else { + // When the socket attempts to bind, it requests a handle from the cluster. + // Close the socket before returning the handle from the cluster. + const socket = dgram.createSocket('udp4'); + const _getServer = cluster._getServer; + + cluster._getServer = common.mustCall(function(self, options, callback) { + socket.close(common.mustCall(() => { + _getServer.call(this, self, options, common.mustCall((err, handle) => { + assert.strictEqual(err, 0); + + // When the socket determines that it was already closed, it will + // close the handle. Use handle.close() to terminate the test. + const close = handle.close; + + handle.close = common.mustCall(function() { + setImmediate(() => cluster.worker.disconnect()); + return close.call(this); + }); + + callback(err, handle); + })); + })); + }); + + socket.bind(common.mustNotCall('Socket should not bind.')); +} diff --git a/test/js/node/test/parallel/test-dgram-cluster-close-in-listening.js b/test/js/node/test/parallel/test-dgram-cluster-close-in-listening.js new file mode 100644 index 0000000000..8cce540271 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-cluster-close-in-listening.js @@ -0,0 +1,29 @@ +'use strict'; +// Ensure that closing dgram sockets in 'listening' callbacks of cluster workers +// won't throw errors. + +const common = require('../common'); +const dgram = require('dgram'); +const cluster = require('cluster'); +if (common.isWindows) + common.skip('dgram clustering is currently not supported on windows.'); + +if (cluster.isPrimary) { + for (let i = 0; i < 3; i += 1) { + cluster.fork(); + } +} else { + const socket = dgram.createSocket('udp4'); + + socket.on('error', common.mustNotCall()); + + socket.on('listening', common.mustCall(() => { + socket.close(); + })); + + socket.on('close', common.mustCall(() => { + cluster.worker.disconnect(); + })); + + socket.bind(0); +} diff --git a/test/js/node/test/parallel/test-dgram-connect-send-callback-buffer-length.js b/test/js/node/test/parallel/test-dgram-connect-send-callback-buffer-length.js new file mode 100644 index 0000000000..723b2738e2 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-callback-buffer-length.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const dgram = require('dgram'); +const client = dgram.createSocket('udp4'); + +const buf = Buffer.allocUnsafe(256); +const offset = 20; +const len = buf.length - offset; + +const messageSent = common.mustSucceed(function messageSent(bytes) { + assert.notStrictEqual(bytes, buf.length); + assert.strictEqual(bytes, buf.length - offset); + client.close(); +}); + +client.bind(0, common.mustCall(() => { + client.connect(client.address().port, common.mustCall(() => { + client.send(buf, offset, len, messageSent); + })); +})); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-callback-buffer.js b/test/js/node/test/parallel/test-dgram-connect-send-callback-buffer.js new file mode 100644 index 0000000000..d6ef1f510c --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-callback-buffer.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const buf = Buffer.allocUnsafe(256); + +const onMessage = common.mustSucceed((bytes) => { + assert.strictEqual(bytes, buf.length); + client.close(); +}); + +client.bind(0, common.mustCall(() => { + client.connect(client.address().port, common.mustCall(() => { + client.send(buf, onMessage); + })); +})); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-callback-multi-buffer.js b/test/js/node/test/parallel/test-dgram-connect-send-callback-multi-buffer.js new file mode 100644 index 0000000000..defc73697c --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-callback-multi-buffer.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const messageSent = common.mustCall((err, bytes) => { + assert.strictEqual(bytes, buf1.length + buf2.length); +}); + +const buf1 = Buffer.alloc(256, 'x'); +const buf2 = Buffer.alloc(256, 'y'); + +client.on('listening', common.mustCall(() => { + const port = client.address().port; + client.connect(port, common.mustCall(() => { + client.send([buf1, buf2], messageSent); + })); +})); + +client.on('message', common.mustCall((buf, info) => { + const expected = Buffer.concat([buf1, buf2]); + assert.ok(buf.equals(expected), 'message was received correctly'); + client.close(); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-default-host.js b/test/js/node/test/parallel/test-dgram-connect-send-default-host.js new file mode 100644 index 0000000000..cd22cd2b64 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-default-host.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); +const server = dgram.createSocket('udp4'); + +const toSend = [Buffer.alloc(256, 'x'), + Buffer.alloc(256, 'y'), + Buffer.alloc(256, 'z'), + 'hello']; + +const received = []; + +server.on('listening', common.mustCall(() => { + const port = server.address().port; + client.connect(port, (err) => { + assert.ifError(err); + client.send(toSend[0], 0, toSend[0].length); + client.send(toSend[1]); + client.send([toSend[2]]); + client.send(toSend[3], 0, toSend[3].length); + + client.send(new Uint8Array(toSend[0]), 0, toSend[0].length); + client.send(new Uint8Array(toSend[1])); + client.send([new Uint8Array(toSend[2])]); + client.send(new Uint8Array(Buffer.from(toSend[3])), + 0, toSend[3].length); + }); +})); + +server.on('message', common.mustCall((buf, info) => { + received.push(buf.toString()); + + if (received.length === toSend.length * 2) { + // The replies may arrive out of order -> sort them before checking. + received.sort(); + + const expected = toSend.concat(toSend).map(String).sort(); + assert.deepStrictEqual(received, expected); + client.close(); + server.close(); + } +}, toSend.length * 2)); + +server.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-empty-array.js b/test/js/node/test/parallel/test-dgram-connect-send-empty-array.js new file mode 100644 index 0000000000..7727e43041 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-empty-array.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +client.on('message', common.mustCall((buf, info) => { + const expected = Buffer.alloc(0); + assert.ok(buf.equals(expected), `Expected empty message but got ${buf}`); + client.close(); +})); + +client.on('listening', common.mustCall(() => { + client.connect(client.address().port, + common.localhostIPv4, + common.mustCall(() => client.send([]))); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-empty-buffer.js b/test/js/node/test/parallel/test-dgram-connect-send-empty-buffer.js new file mode 100644 index 0000000000..bce4c82e5c --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-empty-buffer.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +client.bind(0, common.mustCall(function() { + const port = this.address().port; + client.connect(port, common.mustCall(() => { + const buf = Buffer.alloc(0); + client.send(buf, 0, 0, common.mustSucceed()); + })); + + client.on('message', common.mustCall((buffer) => { + assert.strictEqual(buffer.length, 0); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-empty-packet.js b/test/js/node/test/parallel/test-dgram-connect-send-empty-packet.js new file mode 100644 index 0000000000..577a9cbefd --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-empty-packet.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +client.bind(0, common.mustCall(function() { + client.connect(client.address().port, common.mustCall(() => { + client.on('message', common.mustCall(callback)); + const buf = Buffer.alloc(1); + + const interval = setInterval(function() { + client.send(buf, 0, 0, common.mustCall(callback)); + }, 10); + + function callback(firstArg) { + // If client.send() callback, firstArg should be null. + // If client.on('message') listener, firstArg should be a 0-length buffer. + if (firstArg instanceof Buffer) { + assert.strictEqual(firstArg.length, 0); + clearInterval(interval); + client.close(); + } + } + })); +})); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-multi-buffer-copy.js b/test/js/node/test/parallel/test-dgram-connect-send-multi-buffer-copy.js new file mode 100644 index 0000000000..0859a0cf86 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-multi-buffer-copy.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const onMessage = common.mustCall(common.mustSucceed((bytes) => { + assert.strictEqual(bytes, buf1.length + buf2.length); +})); + +const buf1 = Buffer.alloc(256, 'x'); +const buf2 = Buffer.alloc(256, 'y'); + +client.on('listening', common.mustCall(function() { + const toSend = [buf1, buf2]; + client.connect(client.address().port, common.mustCall(() => { + client.send(toSend, onMessage); + })); +})); + +client.on('message', common.mustCall(function onMessage(buf, info) { + const expected = Buffer.concat([buf1, buf2]); + assert.ok(buf.equals(expected), 'message was received correctly'); + client.close(); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-connect-send-multi-string-array.js b/test/js/node/test/parallel/test-dgram-connect-send-multi-string-array.js new file mode 100644 index 0000000000..e69aa82d47 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect-send-multi-string-array.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const socket = dgram.createSocket('udp4'); +const data = ['foo', 'bar', 'baz']; + +socket.on('message', common.mustCall((msg, rinfo) => { + socket.close(); + assert.deepStrictEqual(msg.toString(), data.join('')); +})); + +socket.bind(0, () => { + socket.connect(socket.address().port, common.mustCall(() => { + socket.send(data); + })); +}); diff --git a/test/js/node/test/parallel/test-dgram-implicit-bind.js b/test/js/node/test/parallel/test-dgram-implicit-bind.js new file mode 100644 index 0000000000..b5aa2781ce --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-implicit-bind.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); + +const source = dgram.createSocket('udp4'); +const target = dgram.createSocket('udp4'); +let messages = 0; + +target.on('message', common.mustCall(function(buf) { + if (buf.toString() === 'abc') ++messages; + if (buf.toString() === 'def') ++messages; + if (messages === 2) { + source.close(); + target.close(); + } +}, 2)); + +target.on('listening', common.mustCall(function() { + // Second .send() call should not throw a bind error. + const port = this.address().port; + source.send(Buffer.from('abc'), 0, 3, port, '127.0.0.1'); + source.send(Buffer.from('def'), 0, 3, port, '127.0.0.1'); +})); + +target.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-listen-after-bind.js b/test/js/node/test/parallel/test-dgram-listen-after-bind.js new file mode 100644 index 0000000000..a580a2386b --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-listen-after-bind.js @@ -0,0 +1,45 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const socket = dgram.createSocket('udp4'); + +socket.bind(); + +let fired = false; +const timer = setTimeout(() => { + socket.close(); +}, 100); + +socket.on('listening', common.mustCall(() => { + clearTimeout(timer); + fired = true; + socket.close(); +})); + +socket.on('close', common.mustCall(() => { + assert(fired, 'listening should fire after bind'); +})); diff --git a/test/js/node/test/parallel/test-dgram-oob-buffer.js b/test/js/node/test/parallel/test-dgram-oob-buffer.js new file mode 100644 index 0000000000..1e71815927 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-oob-buffer.js @@ -0,0 +1,45 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Some operating systems report errors when an UDP message is sent to an +// unreachable host. This error can be reported by sendto() and even by +// recvfrom(). Node should not propagate this error to the user. + +const common = require('../common'); +const dgram = require('dgram'); + +const socket = dgram.createSocket('udp4'); +const buf = Buffer.from([1, 2, 3, 4]); +const portGetter = dgram.createSocket('udp4') + .bind(0, 'localhost', common.mustCall(() => { + const { address, port } = portGetter.address(); + portGetter.close(common.mustCall(() => { + socket.send(buf, 0, 0, port, address, common.mustNotCall()); + socket.send(buf, 0, 4, port, address, common.mustNotCall()); + socket.send(buf, 1, 3, port, address, common.mustNotCall()); + socket.send(buf, 3, 1, port, address, common.mustNotCall()); + // Since length of zero means nothing, don't error despite OOB. + socket.send(buf, 4, 0, port, address, common.mustNotCall()); + + socket.close(); + })); + })); diff --git a/test/js/node/test/parallel/test-dgram-ref.js b/test/js/node/test/parallel/test-dgram-ref.js new file mode 100644 index 0000000000..0c5474b118 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-ref.js @@ -0,0 +1,35 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); + +// Should not hang, see https://github.com/nodejs/node-v0.x-archive/issues/1282 +dgram.createSocket('udp4'); +dgram.createSocket('udp6'); + +{ + // Test the case of ref()'ing a socket with no handle. + const s = dgram.createSocket('udp4'); + + s.close(common.mustCall(() => s.ref())); +} diff --git a/test/js/node/test/parallel/test-dgram-send-address-types.js b/test/js/node/test/parallel/test-dgram-send-address-types.js new file mode 100644 index 0000000000..a31e53f903 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-address-types.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const buf = Buffer.from('test'); + +const defaultCases = ['', null, undefined]; + +const onMessage = common.mustSucceed((bytes) => { + assert.strictEqual(bytes, buf.length); +}, defaultCases.length + 1); + +const client = dgram.createSocket('udp4').bind(0, () => { + const port = client.address().port; + + // Check valid addresses + defaultCases.forEach((address) => { + client.send(buf, port, address, onMessage); + }); + + // Valid address: not provided + client.send(buf, port, onMessage); + + // Check invalid addresses + [ + [], + 0, + 1, + true, + false, + 0n, + 1n, + {}, + Symbol(), + ].forEach((invalidInput) => { + const expectedError = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "address" argument must be of type string.' + + `${common.invalidArgTypeHelper(invalidInput)}` + }; + assert.throws(() => client.send(buf, port, invalidInput), expectedError); + }); +}); + +client.unref(); diff --git a/test/js/node/test/parallel/test-dgram-send-callback-buffer-empty-address.js b/test/js/node/test/parallel/test-dgram-send-callback-buffer-empty-address.js new file mode 100644 index 0000000000..f6254ec43e --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-callback-buffer-empty-address.js @@ -0,0 +1,16 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const buf = Buffer.alloc(256, 'x'); + +const onMessage = common.mustSucceed((bytes) => { + assert.strictEqual(bytes, buf.length); + client.close(); +}); + +client.bind(0, () => client.send(buf, client.address().port, onMessage)); diff --git a/test/js/node/test/parallel/test-dgram-send-callback-buffer-length-empty-address.js b/test/js/node/test/parallel/test-dgram-send-callback-buffer-length-empty-address.js new file mode 100644 index 0000000000..a95018d14b --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-callback-buffer-length-empty-address.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const dgram = require('dgram'); +const client = dgram.createSocket('udp4'); + +const buf = Buffer.alloc(256, 'x'); +const offset = 20; +const len = buf.length - offset; + +const onMessage = common.mustSucceed(function messageSent(bytes) { + assert.notStrictEqual(bytes, buf.length); + assert.strictEqual(bytes, buf.length - offset); + client.close(); +}); + +client.bind(0, () => client.send(buf, offset, len, + client.address().port, + onMessage)); diff --git a/test/js/node/test/parallel/test-dgram-send-callback-buffer-length.js b/test/js/node/test/parallel/test-dgram-send-callback-buffer-length.js new file mode 100644 index 0000000000..f570ecb20d --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-callback-buffer-length.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const dgram = require('dgram'); +const client = dgram.createSocket('udp4'); + +const buf = Buffer.allocUnsafe(256); +const offset = 20; +const len = buf.length - offset; + +const messageSent = common.mustSucceed(function messageSent(bytes) { + assert.notStrictEqual(bytes, buf.length); + assert.strictEqual(bytes, buf.length - offset); + client.close(); +}); + +client.bind(0, () => client.send(buf, offset, len, + client.address().port, + '127.0.0.1', + messageSent)); diff --git a/test/js/node/test/parallel/test-dgram-send-callback-buffer.js b/test/js/node/test/parallel/test-dgram-send-callback-buffer.js new file mode 100644 index 0000000000..11f8208de9 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-callback-buffer.js @@ -0,0 +1,19 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const buf = Buffer.allocUnsafe(256); + +const onMessage = common.mustSucceed((bytes) => { + assert.strictEqual(bytes, buf.length); + client.close(); +}); + +client.bind(0, () => client.send(buf, + client.address().port, + common.localhostIPv4, + onMessage)); diff --git a/test/js/node/test/parallel/test-dgram-send-callback-multi-buffer-empty-address.js b/test/js/node/test/parallel/test-dgram-send-callback-multi-buffer-empty-address.js new file mode 100644 index 0000000000..37e9e04c44 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-callback-multi-buffer-empty-address.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const messageSent = common.mustSucceed(function messageSent(bytes) { + assert.strictEqual(bytes, buf1.length + buf2.length); +}); + +const buf1 = Buffer.alloc(256, 'x'); +const buf2 = Buffer.alloc(256, 'y'); + +client.on('listening', function() { + const port = this.address().port; + client.send([buf1, buf2], port, messageSent); +}); + +client.on('message', common.mustCall(function onMessage(buf) { + const expected = Buffer.concat([buf1, buf2]); + assert.ok(buf.equals(expected), 'message was received correctly'); + client.close(); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-send-callback-multi-buffer.js b/test/js/node/test/parallel/test-dgram-send-callback-multi-buffer.js new file mode 100644 index 0000000000..09f01f6e8a --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-callback-multi-buffer.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const messageSent = common.mustCall((err, bytes) => { + assert.strictEqual(bytes, buf1.length + buf2.length); +}); + +const buf1 = Buffer.alloc(256, 'x'); +const buf2 = Buffer.alloc(256, 'y'); + +client.on('listening', () => { + const port = client.address().port; + client.send([buf1, buf2], port, common.localhostIPv4, messageSent); +}); + +client.on('message', common.mustCall((buf, info) => { + const expected = Buffer.concat([buf1, buf2]); + assert.ok(buf.equals(expected), 'message was received correctly'); + client.close(); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-send-callback-recursive.js b/test/js/node/test/parallel/test-dgram-send-callback-recursive.js new file mode 100644 index 0000000000..1a4c7c84fc --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-callback-recursive.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const dgram = require('dgram'); +const client = dgram.createSocket('udp4'); +const chunk = 'abc'; +let received = 0; +let sent = 0; +const limit = 10; +let async = false; +let port; + +function onsend() { + if (sent++ < limit) { + client.send(chunk, 0, chunk.length, port, common.localhostIPv4, onsend); + } else { + assert.strictEqual(async, true); + } +} + +client.on('listening', function() { + port = this.address().port; + + process.nextTick(() => { + async = true; + }); + + onsend(); +}); + +client.on('message', (buf, info) => { + received++; + if (received === limit) { + client.close(); + } +}); + +client.on('close', common.mustCall(function() { + assert.strictEqual(received, limit); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-send-cb-quelches-error.js b/test/js/node/test/parallel/test-dgram-send-cb-quelches-error.js new file mode 100644 index 0000000000..106d2870c2 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-cb-quelches-error.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const mustCall = common.mustCall; +const assert = require('assert'); +const dgram = require('dgram'); +const dns = require('dns'); + +const socket = dgram.createSocket('udp4'); +const buffer = Buffer.from('gary busey'); + +dns.setServers([]); + +socket.once('error', onEvent); + +// assert that: +// * callbacks act as "error" listeners if given. +// * error is never emitter for missing dns entries +// if a callback that handles error is present +// * error is emitted if a callback with no argument is passed +socket.send(buffer, 0, buffer.length, 100, + 'dne.example.com', mustCall(callbackOnly)); + +function callbackOnly(err) { + assert.ok(err); + socket.removeListener('error', onEvent); + socket.on('error', mustCall(onError)); + socket.send(buffer, 0, buffer.length, 100, 'dne.invalid'); +} + +function onEvent(err) { + assert.fail(`Error should not be emitted if there is callback: ${err}`); +} + +function onError(err) { + assert.ok(err); + socket.close(); +} diff --git a/test/js/node/test/parallel/test-dgram-send-empty-array.js b/test/js/node/test/parallel/test-dgram-send-empty-array.js new file mode 100644 index 0000000000..178b72bb12 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-empty-array.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +let interval; + +client.on('message', common.mustCall(function onMessage(buf, info) { + const expected = Buffer.alloc(0); + assert.ok(buf.equals(expected), `Expected empty message but got ${buf}`); + clearInterval(interval); + client.close(); +})); + +client.on('listening', common.mustCall(function() { + interval = setInterval(function() { + client.send([], client.address().port, common.localhostIPv4); + }, 10); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-send-empty-buffer.js b/test/js/node/test/parallel/test-dgram-send-empty-buffer.js new file mode 100644 index 0000000000..76b014b893 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-empty-buffer.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +client.bind(0, common.mustCall(function() { + const port = this.address().port; + + client.on('message', common.mustCall(function onMessage(buffer) { + assert.strictEqual(buffer.length, 0); + clearInterval(interval); + client.close(); + })); + + const buf = Buffer.alloc(0); + const interval = setInterval(function() { + client.send(buf, 0, 0, port, '127.0.0.1', common.mustCall()); + }, 10); +})); diff --git a/test/js/node/test/parallel/test-dgram-send-empty-packet.js b/test/js/node/test/parallel/test-dgram-send-empty-packet.js new file mode 100644 index 0000000000..eb4f79e8aa --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-empty-packet.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +client.bind(0, common.mustCall(function() { + + client.on('message', common.mustCall(callback)); + + const port = this.address().port; + const buf = Buffer.alloc(1); + + const interval = setInterval(function() { + client.send(buf, 0, 0, port, '127.0.0.1', common.mustCall(callback)); + }, 10); + + function callback(firstArg) { + // If client.send() callback, firstArg should be null. + // If client.on('message') listener, firstArg should be a 0-length buffer. + if (firstArg instanceof Buffer) { + assert.strictEqual(firstArg.length, 0); + clearInterval(interval); + client.close(); + } + } +})); diff --git a/test/js/node/test/parallel/test-dgram-send-multi-buffer-copy.js b/test/js/node/test/parallel/test-dgram-send-multi-buffer-copy.js new file mode 100644 index 0000000000..2ee87494b0 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-multi-buffer-copy.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const onMessage = common.mustCall(function(err, bytes) { + assert.strictEqual(bytes, buf1.length + buf2.length); +}); + +const buf1 = Buffer.alloc(256, 'x'); +const buf2 = Buffer.alloc(256, 'y'); + +client.on('listening', function() { + const toSend = [buf1, buf2]; + client.send(toSend, this.address().port, common.localhostIPv4, onMessage); + toSend.splice(0, 2); +}); + +client.on('message', common.mustCall(function onMessage(buf, info) { + const expected = Buffer.concat([buf1, buf2]); + assert.ok(buf.equals(expected), 'message was received correctly'); + client.close(); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-send-multi-string-array.js b/test/js/node/test/parallel/test-dgram-send-multi-string-array.js new file mode 100644 index 0000000000..8d73a6d183 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-multi-string-array.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const socket = dgram.createSocket('udp4'); +const data = ['foo', 'bar', 'baz']; + +socket.on('message', common.mustCall((msg, rinfo) => { + socket.close(); + assert.deepStrictEqual(msg.toString(), data.join('')); +})); + +socket.bind(() => socket.send(data, socket.address().port, 'localhost')); diff --git a/test/js/node/test/parallel/test-dgram-udp4.js b/test/js/node/test/parallel/test-dgram-udp4.js new file mode 100644 index 0000000000..c7ee34b2c1 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-udp4.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const message_to_send = 'A message to send'; + +const server = dgram.createSocket('udp4'); +server.on('message', common.mustCall((msg, rinfo) => { + assert.strictEqual(rinfo.address, common.localhostIPv4); + assert.strictEqual(msg.toString(), message_to_send.toString()); + server.send(msg, 0, msg.length, rinfo.port, rinfo.address); +})); +server.on('listening', common.mustCall(() => { + const client = dgram.createSocket('udp4'); + const port = server.address().port; + client.on('message', common.mustCall((msg, rinfo) => { + assert.strictEqual(rinfo.address, common.localhostIPv4); + assert.strictEqual(rinfo.port, port); + assert.strictEqual(msg.toString(), message_to_send.toString()); + client.close(); + server.close(); + })); + client.send(message_to_send, + 0, + message_to_send.length, + port, + 'localhost'); + client.on('close', common.mustCall()); +})); +server.on('close', common.mustCall()); +server.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-unref-in-cluster.js b/test/js/node/test/parallel/test-dgram-unref-in-cluster.js new file mode 100644 index 0000000000..40833a129d --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-unref-in-cluster.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); +const cluster = require('cluster'); +const assert = require('assert'); + +if (common.isWindows) + common.skip('dgram clustering is currently not supported on Windows.'); + +if (cluster.isPrimary) { + cluster.fork(); +} else { + const socket = dgram.createSocket('udp4'); + socket.unref(); + socket.bind(); + socket.on('listening', common.mustCall(() => { + const sockets = process.getActiveResourcesInfo().filter((item) => { + return item === 'UDPWrap'; + }); + assert.ok(sockets.length === 0); + process.disconnect(); + })); +} diff --git a/test/js/node/test/parallel/test-dgram-unref.js b/test/js/node/test/parallel/test-dgram-unref.js new file mode 100644 index 0000000000..930cbf095c --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-unref.js @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); + +{ + // Test the case of unref()'ing a socket with a handle. + const s = dgram.createSocket('udp4'); + s.bind(); + s.unref(); +} + +{ + // Test the case of unref()'ing a socket with no handle. + const s = dgram.createSocket('udp4'); + + s.close(common.mustCall(() => s.unref())); +} + +setTimeout(common.mustNotCall(), 1000).unref(); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-bind-store.js b/test/js/node/test/parallel/test-diagnostics-channel-bind-store.js new file mode 100644 index 0000000000..81fb299c2f --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-bind-store.js @@ -0,0 +1,108 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dc = require('diagnostics_channel'); +const { AsyncLocalStorage } = require('async_hooks'); + +let n = 0; +const thisArg = new Date(); +const inputs = [ + { foo: 'bar' }, + { baz: 'buz' }, +]; + +const channel = dc.channel('test'); + +// Bind a storage directly to published data +const store1 = new AsyncLocalStorage(); +channel.bindStore(store1); +let store1bound = true; + +// Bind a store with transformation of published data +const store2 = new AsyncLocalStorage(); +channel.bindStore(store2, common.mustCall((data) => { + assert.strictEqual(data, inputs[n]); + return { data }; +}, 4)); + +// Regular subscribers should see publishes from runStores calls +channel.subscribe(common.mustCall((data) => { + if (store1bound) { + assert.deepStrictEqual(data, store1.getStore()); + } + assert.deepStrictEqual({ data }, store2.getStore()); + assert.strictEqual(data, inputs[n]); +}, 4)); + +// Verify stores are empty before run +assert.strictEqual(store1.getStore(), undefined); +assert.strictEqual(store2.getStore(), undefined); + +channel.runStores(inputs[n], common.mustCall(function(a, b) { + // Verify this and argument forwarding + assert.strictEqual(this, thisArg); + assert.strictEqual(a, 1); + assert.strictEqual(b, 2); + + // Verify store 1 state matches input + assert.strictEqual(store1.getStore(), inputs[n]); + + // Verify store 2 state has expected transformation + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); + + // Should support nested contexts + n++; + channel.runStores(inputs[n], common.mustCall(function() { + // Verify this and argument forwarding + assert.strictEqual(this, undefined); + + // Verify store 1 state matches input + assert.strictEqual(store1.getStore(), inputs[n]); + + // Verify store 2 state has expected transformation + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); + })); + n--; + + // Verify store 1 state matches input + assert.strictEqual(store1.getStore(), inputs[n]); + + // Verify store 2 state has expected transformation + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); +}), thisArg, 1, 2); + +// Verify stores are empty after run +assert.strictEqual(store1.getStore(), undefined); +assert.strictEqual(store2.getStore(), undefined); + +// Verify unbinding works +assert.ok(channel.unbindStore(store1)); +store1bound = false; + +// Verify unbinding a store that is not bound returns false +assert.ok(!channel.unbindStore(store1)); + +n++; +channel.runStores(inputs[n], common.mustCall(() => { + // Verify after unbinding store 1 will remain undefined + assert.strictEqual(store1.getStore(), undefined); + + // Verify still bound store 2 receives expected data + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); +})); + +// Contain transformer errors and emit on next tick +const fail = new Error('fail'); +channel.bindStore(store1, () => { + throw fail; +}); + +let calledRunStores = false; +process.once('uncaughtException', common.mustCall((err) => { + assert.strictEqual(calledRunStores, true); + assert.strictEqual(err, fail); +})); + +channel.runStores(inputs[n], common.mustCall()); +calledRunStores = true; diff --git a/test/js/node/test/parallel/test-diagnostics-channel-has-subscribers.js b/test/js/node/test/parallel/test-diagnostics-channel-has-subscribers.js new file mode 100644 index 0000000000..de37267555 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-has-subscribers.js @@ -0,0 +1,10 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { channel, hasSubscribers } = require('diagnostics_channel'); + +const dc = channel('test'); +assert.ok(!hasSubscribers('test')); + +dc.subscribe(() => {}); +assert.ok(hasSubscribers('test')); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js b/test/js/node/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js new file mode 100644 index 0000000000..9498419b80 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); +const { Channel } = dc; + +const input = { + foo: 'bar' +}; + +// Should not have named channel +assert.ok(!dc.hasSubscribers('test')); + +// Individual channel objects can be created to avoid future lookups +const channel = dc.channel('test'); +assert.ok(channel instanceof Channel); + +// No subscribers yet, should not publish +assert.ok(!channel.hasSubscribers); + +const subscriber = common.mustCall((message, name) => { + assert.strictEqual(name, channel.name); + assert.deepStrictEqual(message, input); +}); + +// Now there's a subscriber, should publish +channel.subscribe(subscriber); +assert.ok(channel.hasSubscribers); + +// The ActiveChannel prototype swap should not fail instanceof +assert.ok(channel instanceof Channel); + +// Should trigger the subscriber once +channel.publish(input); + +// Should not publish after subscriber is unsubscribed +assert.ok(channel.unsubscribe(subscriber)); +assert.ok(!channel.hasSubscribers); + +// unsubscribe() should return false when subscriber is not found +assert.ok(!channel.unsubscribe(subscriber)); + +assert.throws(() => { + channel.subscribe(null); +}, { code: 'ERR_INVALID_ARG_TYPE' }); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-pub-sub.js b/test/js/node/test/parallel/test-diagnostics-channel-pub-sub.js new file mode 100644 index 0000000000..a7232ab58c --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-pub-sub.js @@ -0,0 +1,51 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); +const { Channel } = dc; + +const name = 'test'; +const input = { + foo: 'bar' +}; + +// Individual channel objects can be created to avoid future lookups +const channel = dc.channel(name); +assert.ok(channel instanceof Channel); + +// No subscribers yet, should not publish +assert.ok(!channel.hasSubscribers); + +const subscriber = common.mustCall((message, name) => { + assert.strictEqual(name, channel.name); + assert.deepStrictEqual(message, input); +}); + +// Now there's a subscriber, should publish +dc.subscribe(name, subscriber); +assert.ok(channel.hasSubscribers); + +// The ActiveChannel prototype swap should not fail instanceof +assert.ok(channel instanceof Channel); + +// Should trigger the subscriber once +channel.publish(input); + +// Should not publish after subscriber is unsubscribed +assert.ok(dc.unsubscribe(name, subscriber)); +assert.ok(!channel.hasSubscribers); + +// unsubscribe() should return false when subscriber is not found +assert.ok(!dc.unsubscribe(name, subscriber)); + +assert.throws(() => { + dc.subscribe(name, null); +}, { code: 'ERR_INVALID_ARG_TYPE' }); + +// Reaching zero subscribers should not delete from the channels map as there +// will be no more weakref to incRef if another subscribe happens while the +// channel object itself exists. +channel.subscribe(subscriber); +channel.unsubscribe(subscriber); +channel.subscribe(subscriber); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-safe-subscriber-errors.js b/test/js/node/test/parallel/test-diagnostics-channel-safe-subscriber-errors.js new file mode 100644 index 0000000000..b0c5ab2480 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-safe-subscriber-errors.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const input = { + foo: 'bar' +}; + +const channel = dc.channel('fail'); + +const error = new Error('nope'); + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err, error); +})); + +channel.subscribe(common.mustCall((message, name) => { + throw error; +})); + +// The failing subscriber should not stop subsequent subscribers from running +channel.subscribe(common.mustCall()); + +// Publish should continue without throwing +const fn = common.mustCall(); +channel.publish(input); +fn(); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-sync-unsubscribe.js b/test/js/node/test/parallel/test-diagnostics-channel-sync-unsubscribe.js new file mode 100644 index 0000000000..87bf44249f --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-sync-unsubscribe.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); +const dc = require('node:diagnostics_channel'); + +const channel_name = 'test:channel'; +const published_data = 'some message'; + +const onMessageHandler = common.mustCall(() => dc.unsubscribe(channel_name, onMessageHandler)); + +dc.subscribe(channel_name, onMessageHandler); + +// This must not throw. +dc.channel(channel_name).publish(published_data); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-error.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-error.js new file mode 100644 index 0000000000..672500e768 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-error.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedError = new Error('test'); +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustCall(check), + asyncEnd: common.mustCall(check), + error: common.mustCall((found) => { + check(found); + assert.deepStrictEqual(found.error, expectedError); + }) +}; + +channel.subscribe(handlers); + +channel.traceCallback(function(cb, err) { + assert.deepStrictEqual(this, thisArg); + setImmediate(cb, err); +}, 0, input, thisArg, common.mustCall((err, res) => { + assert.strictEqual(err, expectedError); + assert.strictEqual(res, undefined); +}), expectedError); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-run-stores.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-run-stores.js new file mode 100644 index 0000000000..874433efd2 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-run-stores.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const store = new AsyncLocalStorage(); + +const firstContext = { foo: 'bar' }; +const secondContext = { baz: 'buz' }; + +channel.start.bindStore(store, common.mustCall(() => { + return firstContext; +})); + +channel.asyncStart.bindStore(store, common.mustCall(() => { + return secondContext; +})); + +assert.strictEqual(store.getStore(), undefined); +channel.traceCallback(common.mustCall((cb) => { + assert.deepStrictEqual(store.getStore(), firstContext); + setImmediate(cb); +}), 0, {}, null, common.mustCall(() => { + assert.deepStrictEqual(store.getStore(), secondContext); +})); +assert.strictEqual(store.getStore(), undefined); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback.js new file mode 100644 index 0000000000..d306f0f51b --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +function checkAsync(found) { + check(found); + assert.strictEqual(found.error, undefined); + assert.deepStrictEqual(found.result, expectedResult); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustCall(checkAsync), + asyncEnd: common.mustCall(checkAsync), + error: common.mustNotCall() +}; + +channel.subscribe(handlers); + +channel.traceCallback(function(cb, err, res) { + assert.deepStrictEqual(this, thisArg); + setImmediate(cb, err, res); +}, 0, input, thisArg, common.mustCall((err, res) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(res, expectedResult); +}), null, expectedResult); + +assert.throws(() => { + channel.traceCallback(common.mustNotCall(), 0, input, thisArg, 1, 2, 3); +}, /"callback" argument must be of type function/); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise-run-stores.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise-run-stores.js new file mode 100644 index 0000000000..5292a6fe09 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise-run-stores.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const { setTimeout } = require('node:timers/promises'); +const { AsyncLocalStorage } = require('async_hooks'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const store = new AsyncLocalStorage(); + +const firstContext = { foo: 'bar' }; +const secondContext = { baz: 'buz' }; + +channel.start.bindStore(store, common.mustCall(() => { + return firstContext; +})); + +channel.asyncStart.bindStore(store, common.mustNotCall(() => { + return secondContext; +})); + +assert.strictEqual(store.getStore(), undefined); +channel.tracePromise(common.mustCall(async () => { + assert.deepStrictEqual(store.getStore(), firstContext); + await setTimeout(1); + // Should _not_ switch to second context as promises don't have an "after" + // point at which to do a runStores. + assert.deepStrictEqual(store.getStore(), firstContext); +})); +assert.strictEqual(store.getStore(), undefined); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise.js new file mode 100644 index 0000000000..20892ca40f --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +function checkAsync(found) { + check(found); + assert.strictEqual(found.error, undefined); + assert.deepStrictEqual(found.result, expectedResult); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustCall(checkAsync), + asyncEnd: common.mustCall(checkAsync), + error: common.mustNotCall() +}; + +channel.subscribe(handlers); + +channel.tracePromise(function(value) { + assert.deepStrictEqual(this, thisArg); + return Promise.resolve(value); +}, input, thisArg, expectedResult).then( + common.mustCall((value) => { + assert.deepStrictEqual(value, expectedResult); + }), + common.mustNotCall() +); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js new file mode 100644 index 0000000000..0965bf3fb4 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedError = new Error('test'); +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustCall((found) => { + check(found); + assert.deepStrictEqual(found.error, expectedError); + }) +}; + +channel.subscribe(handlers); +try { + channel.traceSync(function(err) { + assert.deepStrictEqual(this, thisArg); + assert.strictEqual(err, expectedError); + throw err; + }, input, thisArg, expectedError); + + throw new Error('It should not reach this error'); +} catch (error) { + assert.deepStrictEqual(error, expectedError); +} diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-run-stores.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-run-stores.js new file mode 100644 index 0000000000..3ffe5e6720 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-run-stores.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const store = new AsyncLocalStorage(); + +const context = { foo: 'bar' }; + +channel.start.bindStore(store, common.mustCall(() => { + return context; +})); + +assert.strictEqual(store.getStore(), undefined); +channel.traceSync(common.mustCall(() => { + assert.deepStrictEqual(store.getStore(), context); +})); +assert.strictEqual(store.getStore(), undefined); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync.js b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync.js new file mode 100644 index 0000000000..b28b47256b --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; +const arg = { baz: 'buz' }; + +function check(found) { + assert.strictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall((found) => { + check(found); + assert.strictEqual(found.result, expectedResult); + }), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall() +}; + +assert.strictEqual(channel.start.hasSubscribers, false); +channel.subscribe(handlers); +assert.strictEqual(channel.start.hasSubscribers, true); +const result1 = channel.traceSync(function(arg1) { + assert.strictEqual(arg1, arg); + assert.strictEqual(this, thisArg); + return expectedResult; +}, input, thisArg, arg); +assert.strictEqual(result1, expectedResult); + +channel.unsubscribe(handlers); +assert.strictEqual(channel.start.hasSubscribers, false); +const result2 = channel.traceSync(function(arg1) { + assert.strictEqual(arg1, arg); + assert.strictEqual(this, thisArg); + return expectedResult; +}, input, thisArg, arg); +assert.strictEqual(result2, expectedResult); diff --git a/test/js/node/test/parallel/test-diagnostics-channel-udp.js b/test/js/node/test/parallel/test-diagnostics-channel-udp.js new file mode 100644 index 0000000000..79869c6d80 --- /dev/null +++ b/test/js/node/test/parallel/test-diagnostics-channel-udp.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const dc = require('diagnostics_channel'); + +const udpSocketChannel = dc.channel('udp.socket'); + +const isUDPSocket = (socket) => socket instanceof dgram.Socket; + +udpSocketChannel.subscribe(common.mustCall(({ socket }) => { + assert.strictEqual(isUDPSocket(socket), true); +})); +const socket = dgram.createSocket('udp4'); +socket.close(); diff --git a/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js b/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js new file mode 100644 index 0000000000..e0cb4d1854 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new Resolver(); + +server.bind(0, common.mustCall(() => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.reverse('123.45.67.89', common.mustCall((err, res) => { + assert.strictEqual(err.code, 'ECANCELLED'); + assert.strictEqual(err.syscall, 'getHostByAddr'); + assert.strictEqual(err.hostname, '123.45.67.89'); + server.close(); + })); +})); + +server.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, '89.67.45.123.in-addr.arpa'); + + // Do not send a reply. + resolver.cancel(); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-cancel-promise.js b/test/js/node/test/parallel/test-dns-channel-cancel-promise.js new file mode 100644 index 0000000000..6dee3e6a77 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-cancel-promise.js @@ -0,0 +1,59 @@ +'use strict'; +const common = require('../common'); +const { promises: dnsPromises } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new dnsPromises.Resolver(); + +server.bind(0, common.mustCall(async () => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + + // Single promise + { + server.once('message', () => { + resolver.cancel(); + }); + + const hostname = 'example0.org'; + + await assert.rejects( + resolver.resolve4(hostname), + { + code: 'ECANCELLED', + syscall: 'queryA', + hostname + } + ); + } + + // Multiple promises + { + server.once('message', () => { + resolver.cancel(); + }); + + const assertions = []; + const assertionCount = 10; + + for (let i = 1; i <= assertionCount; i++) { + const hostname = `example${i}.org`; + + assertions.push( + assert.rejects( + resolver.resolve4(hostname), + { + code: 'ECANCELLED', + syscall: 'queryA', + hostname: hostname + } + ) + ); + } + + await Promise.all(assertions); + } + + server.close(); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-cancel.js b/test/js/node/test/parallel/test-dns-channel-cancel.js new file mode 100644 index 0000000000..405b31e4cc --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-cancel.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new Resolver(); + +const desiredQueries = 11; +let finishedQueries = 0; + +server.bind(0, common.mustCall(async () => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + + const callback = common.mustCall((err, res) => { + assert.strictEqual(err.code, 'ECANCELLED'); + assert.strictEqual(err.syscall, 'queryA'); + assert.strictEqual(err.hostname, `example${finishedQueries}.org`); + + finishedQueries++; + if (finishedQueries === desiredQueries) { + server.close(); + } + }, desiredQueries); + + const next = (...args) => { + callback(...args); + + server.once('message', () => { + resolver.cancel(); + }); + + // Multiple queries + for (let i = 1; i < desiredQueries; i++) { + resolver.resolve4(`example${i}.org`, callback); + } + }; + + server.once('message', () => { + resolver.cancel(); + }); + + // Single query + resolver.resolve4('example0.org', next); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-timeout.js b/test/js/node/test/parallel/test-dns-channel-timeout.js new file mode 100644 index 0000000000..153d7ad907 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-timeout.js @@ -0,0 +1,61 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const dns = require('dns'); + +if (typeof Bun !== 'undefined') { + if (process.platform === 'win32' && require('harness').isCI) { + // TODO(@heimskr): This test mysteriously takes forever in Windows in CI + // possibly due to UDP keeping the event loop alive longer than it should. + process.exit(0); + } +} + +for (const ctor of [dns.Resolver, dns.promises.Resolver]) { + for (const timeout of [null, true, false, '', '2']) { + assert.throws(() => new ctor({ timeout }), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } + + for (const timeout of [-2, 4.2, 2 ** 31]) { + assert.throws(() => new ctor({ timeout }), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); + } + + for (const timeout of [-1, 0, 1]) new ctor({ timeout }); // OK +} + +for (const timeout of [0, 1, 2]) { + const server = dgram.createSocket('udp4'); + server.bind(0, '127.0.0.1', common.mustCall(() => { + const resolver = new dns.Resolver({ timeout }); + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.resolve4('nodejs.org', common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ETIMEOUT', + name: /^(DNSException|Error)$/, + }); + server.close(); + })); + })); +} + +for (const timeout of [0, 1, 2]) { + const server = dgram.createSocket('udp4'); + server.bind(0, '127.0.0.1', common.mustCall(() => { + const resolver = new dns.promises.Resolver({ timeout }); + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.resolve4('nodejs.org').catch(common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ETIMEOUT', + name: /^(DNSException|Error)$/, + }); + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-dns-default-order-ipv4.js b/test/js/node/test/parallel/test-dns-default-order-ipv4.js new file mode 100644 index 0000000000..ea3deec4f7 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-ipv4.js @@ -0,0 +1,51 @@ +// Flags: --dns-result-order=ipv4first +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that --dns-result-order=ipv4first works as expected. + +if (!process.execArgv.includes("--dns-result-order=ipv4first")) { + process.exit(0); +} + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of ipv4first only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-default-order-ipv6.js b/test/js/node/test/parallel/test-dns-default-order-ipv6.js new file mode 100644 index 0000000000..aeb2dc2b2a --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-ipv6.js @@ -0,0 +1,51 @@ +// Flags: --dns-result-order=ipv6first +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that --dns-result-order=ipv6first works as expected. + +if (!process.execArgv.includes("--dns-result-order=ipv6first")) { + process.exit(0); +} + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of ipv6first only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-default-order-verbatim.js b/test/js/node/test/parallel/test-dns-default-order-verbatim.js new file mode 100644 index 0000000000..562250ca48 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-verbatim.js @@ -0,0 +1,55 @@ +// Flags: --dns-result-order=verbatim +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of verbatim only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter("verbatim"); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter("verbatim"); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter("verbatim"); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter("verbatim"); + + await allowFailed( + promisify(dns.lookup)('example.org', { order: 'ipv4first' }) + ); + checkParameter("ipv4first"); + + await allowFailed( + promisify(dns.lookup)('example.org', { order: 'ipv6first' }) + ); + checkParameter("ipv6first"); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-get-server.js b/test/js/node/test/parallel/test-dns-get-server.js new file mode 100644 index 0000000000..3ce6a45ac7 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-get-server.js @@ -0,0 +1,11 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Resolver } = require('dns'); + +const resolver = new Resolver(); +assert(resolver.getServers().length > 0); + +resolver._handle.getServers = common.mustCall(); +assert.strictEqual(resolver.getServers().length, 0); diff --git a/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js b/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js new file mode 100644 index 0000000000..934796198b --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js @@ -0,0 +1,44 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +Bun.dns.lookup = hostname => { + throw Object.assign(new Error('Out of memory'), { + name: 'DNSException', + code: 'ENOMEM', + syscall: 'getaddrinfo', + hostname, + }); +}; + +// This test ensures that dns.lookup issues a DeprecationWarning +// when invalid options type is given + +const dnsPromises = require('dns/promises'); + +common.expectWarning({ + // 'internal/test/binding': [ + // 'These APIs are for internal testing only. Do not use them.', + // ], +}); + +assert.throws(() => { + dnsPromises.lookup('127.0.0.1', { hints: '-1' }); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { hints: -1 }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { family: '6' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { all: 'true' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { verbatim: 'true' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { order: 'true' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', '6'), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('localhost'), + { code: 'ENOMEM' }); diff --git a/test/js/node/test/parallel/test-dns-lookup.js b/test/js/node/test/parallel/test-dns-lookup.js new file mode 100644 index 0000000000..bef563df60 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookup.js @@ -0,0 +1,223 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Stub `getaddrinfo` to *always* error. This has to be done before we load the +// `dns` module to guarantee that the `dns` module uses the stub. +if (typeof Bun === "undefined") { + const { internalBinding } = require('internal/test/binding'); + const cares = internalBinding('cares_wrap'); + cares.getaddrinfo = () => internalBinding('uv').UV_ENOMEM; +} else { + Bun.dns.lookup = (hostname) => Promise.reject(Object.assign(new Error('Out of memory'), { code: 'ENOMEM', hostname })); +} + +const dns = require('dns'); +const dnsPromises = dns.promises; + +{ + const err = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "hostname" argument must be of type string\. Received( type number|: "number")/ + }; + + assert.throws(() => dns.lookup(1, {}), err); + assert.throws(() => dnsPromises.lookup(1, {}), err); +} + +// This also verifies different expectWarning notations. +common.expectWarning({ + // For 'internal/test/binding' module. + ...(typeof Bun === "undefined"? { + 'internal/test/binding': [ + 'These APIs are for internal testing only. Do not use them.', + ] + } : {}), + // For calling `dns.lookup` with falsy `hostname`. + 'DeprecationWarning': { + DEP0118: 'The provided hostname "false" is not a valid ' + + 'hostname, and is supported in the dns module solely for compatibility.' + } +}); + +assert.throws(() => { + dns.lookup(false, 'cb'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => { + dns.lookup(false, 'options', 'cb'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +{ + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The argument 'hints' is invalid\. Received:? 100/ + }; + const options = { + hints: 100, + family: 0, + all: false + }; + + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +} + +{ + const family = 20; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /^The (property 'options.family' must be one of: 0, 4, 6|argument 'family' must be one of 0, 4 or 6)\. Received:? 20$/ + }; + const options = { + hints: 0, + family, + all: false + }; + + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +} + +[1, 0n, 1n, '', '0', Symbol(), true, false, {}, [], () => {}] + .forEach((family) => { + const err = { code: 'ERR_INVALID_ARG_VALUE' }; + const options = { family }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); + }); +[0n, 1n, '', '0', Symbol(), true, false].forEach((family) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => { dnsPromises.lookup(false, family); }, err); + assert.throws(() => { + dns.lookup(false, family, common.mustNotCall()); + }, err); +}); +assert.throws(() => dnsPromises.lookup(false, () => {}), + { code: 'ERR_INVALID_ARG_TYPE' }); + +[0n, 1n, '', '0', Symbol(), true, false, {}, [], () => {}].forEach((hints) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { hints }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((all) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { all }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((verbatim) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { verbatim }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((order) => { + const err = { code: 'ERR_INVALID_ARG_VALUE' }; + const options = { order }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +(async function() { + let res; + + res = await dnsPromises.lookup(false, { + hints: 0, + family: 0, + all: true + }); + assert.deepStrictEqual(res, []); + + res = await dnsPromises.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: true + }); + assert.deepStrictEqual(res, [{ address: '127.0.0.1', family: 4 }]); + + res = await dnsPromises.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: false + }); + assert.deepStrictEqual(res, { address: '127.0.0.1', family: 4 }); +})().then(common.mustCall()); + +dns.lookup(false, { + hints: 0, + family: 0, + all: true +}, common.mustSucceed((result, addressType) => { + assert.deepStrictEqual(result, []); + assert.strictEqual(addressType, undefined); +})); + +dns.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: true +}, common.mustSucceed((result, addressType) => { + assert.deepStrictEqual(result, [{ + address: '127.0.0.1', + family: 4 + }]); + assert.strictEqual(addressType, undefined); +})); + +dns.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: false +}, common.mustSucceed((result, addressType) => { + assert.strictEqual(result, '127.0.0.1'); + assert.strictEqual(addressType, 4); +})); + +let tickValue = 0; + +// Should fail due to stub. +dns.lookup('example.com', common.mustCall((error, result, addressType) => { + assert(error); + assert.strictEqual(tickValue, 1); + assert.strictEqual(error.code, 'ENOMEM'); + const descriptor = Object.getOwnPropertyDescriptor(error, 'message'); + // The error message should be non-enumerable. + assert.strictEqual(descriptor.enumerable, false); +})); + +// Make sure that the error callback is called on next tick. +tickValue = 1; + +// Should fail due to stub. +assert.rejects(dnsPromises.lookup('example.com'), + { code: 'ENOMEM', hostname: 'example.com' }).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-lookupService-promises.js b/test/js/node/test/parallel/test-dns-lookupService-promises.js new file mode 100644 index 0000000000..f4053d484d --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookupService-promises.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +const assert = require('assert'); +const dnsPromises = require('dns').promises; + +dnsPromises.lookupService('127.0.0.1', 22).then(common.mustCall((result) => { + assert(['ssh', '22'].includes(result.service)); + assert.strictEqual(typeof result.hostname, 'string'); + assert.notStrictEqual(result.hostname.length, 0); +})); + +// Use an IP from the RFC 5737 test range to cause an error. +// Refs: https://tools.ietf.org/html/rfc5737 +assert.rejects( + () => dnsPromises.lookupService('192.0.2.1', 22), + { code: /^(?:ENOTFOUND|EAI_AGAIN)$/ } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-lookupService.js b/test/js/node/test/parallel/test-dns-lookupService.js new file mode 100644 index 0000000000..e4e48de8bb --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookupService.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Stub `getnameinfo` to *always* error. +Bun.dns.lookupService = (addr, port) => { + throw Object.assign(new Error(`getnameinfo ENOENT ${addr}`), {code: 'ENOENT', syscall: 'getnameinfo'}); +}; + +const dns = require('dns'); + +assert.throws( + () => dns.lookupService('127.0.0.1', 80, common.mustNotCall()), + { + code: 'ENOENT', + message: 'getnameinfo ENOENT 127.0.0.1', + syscall: 'getnameinfo' + } +); + +assert.rejects( + dns.promises.lookupService('127.0.0.1', 80), + { + code: 'ENOENT', + message: 'getnameinfo ENOENT 127.0.0.1', + syscall: 'getnameinfo' + } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-multi-channel.js b/test/js/node/test/parallel/test-dns-multi-channel.js new file mode 100644 index 0000000000..026ef44e33 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-multi-channel.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const servers = [ + { + socket: dgram.createSocket('udp4'), + reply: { type: 'A', address: '1.2.3.4', ttl: 123, domain: 'example.org' } + }, + { + socket: dgram.createSocket('udp4'), + reply: { type: 'A', address: '5.6.7.8', ttl: 123, domain: 'example.org' } + }, +]; + +let waiting = servers.length; +for (const { socket, reply } of servers) { + socket.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + socket.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: [reply], + }), port, address); + })); + + socket.bind(0, common.mustCall(() => { + if (--waiting === 0) ready(); + })); +} + + +function ready() { + const resolvers = servers.map((server) => ({ + server, + resolver: new Resolver() + })); + + for (const { server: { socket, reply }, resolver } of resolvers) { + resolver.setServers([`127.0.0.1:${socket.address().port}`]); + resolver.resolve4('example.org', common.mustSucceed((res) => { + assert.deepStrictEqual(res, [reply.address]); + socket.close(); + })); + } +} diff --git a/test/js/node/test/parallel/test-dns-promises-exists.js b/test/js/node/test/parallel/test-dns-promises-exists.js new file mode 100644 index 0000000000..d88ecefaa9 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-promises-exists.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const dnsPromises = require('dns/promises'); +const dns = require('dns'); + +assert.strictEqual(dnsPromises, dns.promises); + +assert.strictEqual(dnsPromises.NODATA, dns.NODATA); +assert.strictEqual(dnsPromises.FORMERR, dns.FORMERR); +assert.strictEqual(dnsPromises.SERVFAIL, dns.SERVFAIL); +assert.strictEqual(dnsPromises.NOTFOUND, dns.NOTFOUND); +assert.strictEqual(dnsPromises.NOTIMP, dns.NOTIMP); +assert.strictEqual(dnsPromises.REFUSED, dns.REFUSED); +assert.strictEqual(dnsPromises.BADQUERY, dns.BADQUERY); +assert.strictEqual(dnsPromises.BADNAME, dns.BADNAME); +assert.strictEqual(dnsPromises.BADFAMILY, dns.BADFAMILY); +assert.strictEqual(dnsPromises.BADRESP, dns.BADRESP); +assert.strictEqual(dnsPromises.CONNREFUSED, dns.CONNREFUSED); +assert.strictEqual(dnsPromises.TIMEOUT, dns.TIMEOUT); +assert.strictEqual(dnsPromises.EOF, dns.EOF); +assert.strictEqual(dnsPromises.FILE, dns.FILE); +assert.strictEqual(dnsPromises.NOMEM, dns.NOMEM); +assert.strictEqual(dnsPromises.DESTRUCTION, dns.DESTRUCTION); +assert.strictEqual(dnsPromises.BADSTR, dns.BADSTR); +assert.strictEqual(dnsPromises.BADFLAGS, dns.BADFLAGS); +assert.strictEqual(dnsPromises.NONAME, dns.NONAME); +assert.strictEqual(dnsPromises.BADHINTS, dns.BADHINTS); +assert.strictEqual(dnsPromises.NOTINITIALIZED, dns.NOTINITIALIZED); +assert.strictEqual(dnsPromises.LOADIPHLPAPI, dns.LOADIPHLPAPI); +assert.strictEqual(dnsPromises.ADDRGETNETWORKPARAMS, dns.ADDRGETNETWORKPARAMS); +assert.strictEqual(dnsPromises.CANCELLED, dns.CANCELLED); diff --git a/test/js/node/test/parallel/test-dns-resolve-promises.js b/test/js/node/test/parallel/test-dns-resolve-promises.js new file mode 100644 index 0000000000..b9965614ac --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolve-promises.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dnsPromises = require('dns').promises; + +Bun.dns.resolve = (hostname, rrtype) => Promise.reject({code: 'EPERM', syscall: 'query' + rrtype[0].toUpperCase() + rrtype.substr(1), hostname}); + +assert.rejects( + dnsPromises.resolve('example.org'), + { + code: 'EPERM', + syscall: 'queryA', + hostname: 'example.org' + } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js b/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js new file mode 100644 index 0000000000..88369a87f8 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const dns = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); +const dnsPromises = dns.promises; + +const server = dgram.createSocket('udp4'); +const resolver = new dns.Resolver({ timeout: 100, tries: 1 }); +const resolverPromises = new dnsPromises.Resolver({ timeout: 100, tries: 1 }); + +server.on('message', common.mustCallAtLeast((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + + assert.strictEqual(domain, 'example.org'); + + const buf = dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: { type: 'A', address: '1.2.3.4', ttl: 123, domain }, + }); + // Overwrite the # of answers with 2, which is incorrect. The response is + // discarded in c-ares >= 1.21.0. This is the reason why a small timeout is + // used in the `Resolver` constructor. See + // https://github.com/nodejs/node/pull/50743#issue-1994909204 + buf.writeUInt16LE(2, 6); + server.send(buf, port, address); +}, 2)); + +server.bind(0, common.mustCall(async () => { + const address = server.address(); + resolver.setServers([`127.0.0.1:${address.port}`]); + resolverPromises.setServers([`127.0.0.1:${address.port}`]); + + resolverPromises.resolveAny('example.org') + .then(common.mustNotCall()) + .catch(common.expectsError({ + // May return EBADRESP or ETIMEOUT + code: /^(?:EBADRESP|ETIMEOUT)$/, + syscall: 'queryAny', + hostname: 'example.org' + })); + + resolver.resolveAny('example.org', common.mustCall((err) => { + assert.notStrictEqual(err.code, 'SUCCESS'); + assert.strictEqual(err.syscall, 'queryAny'); + assert.strictEqual(err.hostname, 'example.org'); + const descriptor = Object.getOwnPropertyDescriptor(err, 'message'); + // The error message should be non-enumerable. + assert.strictEqual(descriptor.enumerable, false); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-dns-resolveany.js b/test/js/node/test/parallel/test-dns-resolveany.js new file mode 100644 index 0000000000..f64dbfc93e --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolveany.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const dns = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); +const dnsPromises = dns.promises; + +const answers = [ + { type: 'A', address: '1.2.3.4', ttl: 123 }, + { type: 'AAAA', address: '::42', ttl: 123 }, + { type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 124 }, + { type: 'NS', value: 'foobar.org', ttl: 457 }, + { type: 'TXT', entries: [ 'v=spf1 ~all xyz\0foo' ] }, + { type: 'PTR', value: 'baz.org', ttl: 987 }, + { + type: 'SOA', + nsname: 'ns1.example.com', + hostmaster: 'admin.example.com', + serial: 156696742, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 60 + }, + { + type: 'CAA', + critical: 128, + issue: 'platynum.ch' + }, +]; + +const server = dgram.createSocket('udp4'); + +server.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + server.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: answers.map((answer) => Object.assign({ domain }, answer)), + }), port, address); +}, 2)); + +server.bind(0, common.mustCall(async () => { + const address = server.address(); + dns.setServers([`127.0.0.1:${address.port}`]); + + validateResults(await dnsPromises.resolveAny('example.org')); + + dns.resolveAny('example.org', common.mustSucceed((res) => { + validateResults(res); + server.close(); + })); +})); + +function validateResults(res) { + // TTL values are only provided for A and AAAA entries. + assert.deepStrictEqual(res.map(maybeRedactTTL), answers.map(maybeRedactTTL)); +} + +function maybeRedactTTL(r) { + const ret = { ...r }; + if (!['A', 'AAAA'].includes(r.type)) + delete ret.ttl; + return ret; +} diff --git a/test/js/node/test/parallel/test-dns-resolvens-typeerror.js b/test/js/node/test/parallel/test-dns-resolvens-typeerror.js new file mode 100644 index 0000000000..c1eea2ddeb --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolvens-typeerror.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test ensures `dns.resolveNs()` does not raise a C++-land assertion error +// and throw a JavaScript TypeError instead. +// Issue https://github.com/nodejs/node-v0.x-archive/issues/7070 + +const assert = require('assert'); +const dns = require('dns'); +const dnsPromises = dns.promises; + +assert.throws( + () => dnsPromises.resolveNs([]), // bad name + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^(The "(host)?name" argument must be of type string|Expected hostname to be a string)/ + } +); +assert.throws( + () => dns.resolveNs([]), // bad name + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^(The "(host)?name" argument must be of type string|Expected hostname to be a string)/ + } +); +assert.throws( + () => dns.resolveNs(''), // bad callback + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } +); diff --git a/test/js/node/test/parallel/test-dns-set-default-order.js b/test/js/node/test/parallel/test-dns-set-default-order.js new file mode 100644 index 0000000000..47bc08e6b1 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-set-default-order.js @@ -0,0 +1,108 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that `dns.setDefaultResultOrder()` and +// `dns.promises.setDefaultResultOrder()` work as expected. + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of order only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +assert.throws(() => dns.setDefaultResultOrder('my_order'), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.promises.setDefaultResultOrder('my_order'), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.setDefaultResultOrder(4), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.promises.setDefaultResultOrder(4), { + code: 'ERR_INVALID_ARG_VALUE', +}); + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + dns.setDefaultResultOrder('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('verbatim'); + + dns.setDefaultResultOrder('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); + + dns.setDefaultResultOrder('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); + + dns.promises.setDefaultResultOrder('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('verbatim'); + + dns.promises.setDefaultResultOrder('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); + + dns.promises.setDefaultResultOrder('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-setlocaladdress.js b/test/js/node/test/parallel/test-dns-setlocaladdress.js new file mode 100644 index 0000000000..25bece328f --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setlocaladdress.js @@ -0,0 +1,40 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const dns = require('dns'); +const resolver = new dns.Resolver(); +const promiseResolver = new dns.promises.Resolver(); + +// Verifies that setLocalAddress succeeds with IPv4 and IPv6 addresses +{ + resolver.setLocalAddress('127.0.0.1'); + resolver.setLocalAddress('::1'); + resolver.setLocalAddress('127.0.0.1', '::1'); + promiseResolver.setLocalAddress('127.0.0.1', '::1'); +} + +// Verify that setLocalAddress throws if called with an invalid address +{ + assert.throws(() => { + resolver.setLocalAddress('127.0.0.1', '127.0.0.1'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress('::1', '::1'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress('bad'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress(123); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => { + resolver.setLocalAddress('127.0.0.1', 42); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => { + resolver.setLocalAddress(); + }, Error); + assert.throws(() => { + promiseResolver.setLocalAddress(); + }, Error); +} diff --git a/test/js/node/test/parallel/test-dns-setserver-when-querying.js b/test/js/node/test/parallel/test-dns-setserver-when-querying.js new file mode 100644 index 0000000000..1a002df498 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setserver-when-querying.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const dns = require('dns'); + +const localhost = [ '127.0.0.1' ]; + +{ + // Fix https://github.com/nodejs/node/issues/14734 + + { + const resolver = new dns.Resolver(); + resolver.resolve('localhost', common.mustCall()); + + assert.throws(resolver.setServers.bind(resolver, localhost), { + code: 'ERR_DNS_SET_SERVERS_FAILED', + message: /[Tt]here are pending queries/ + }); + } + + { + dns.resolve('localhost', common.mustCall()); + + // should not throw + dns.setServers(localhost); + } +} diff --git a/test/js/node/test/parallel/test-dns-setservers-type-check.js b/test/js/node/test/parallel/test-dns-setservers-type-check.js new file mode 100644 index 0000000000..7a19dc5eb0 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setservers-type-check.js @@ -0,0 +1,117 @@ +'use strict'; +const common = require('../common'); +const { addresses } = require('../common/internet'); +const assert = require('assert'); +const dns = require('dns'); +const resolver = new dns.promises.Resolver(); +const dnsPromises = dns.promises; +const promiseResolver = new dns.promises.Resolver(); + +{ + [ + null, + undefined, + Number(addresses.DNS4_SERVER), + addresses.DNS4_SERVER, + { + address: addresses.DNS4_SERVER + }, + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers" argument must be an instance of Array\./ + }; + assert.throws( + () => { + dns.setServers(val); + }, errObj + ); + assert.throws( + () => { + resolver.setServers(val); + }, errObj + ); + assert.throws( + () => { + dnsPromises.setServers(val); + }, errObj + ); + assert.throws( + () => { + promiseResolver.setServers(val); + }, errObj + ); + }); +} + +{ + [ + [null], + [undefined], + [Number(addresses.DNS4_SERVER)], + [ + { + address: addresses.DNS4_SERVER + }, + ], + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers\[0\]" argument must be of type string\./ + }; + assert.throws( + () => { + dns.setServers(val); + }, errObj + ); + assert.throws( + () => { + resolver.setServers(val); + }, errObj + ); + assert.throws( + () => { + dnsPromises.setServers(val); + }, errObj + ); + assert.throws( + () => { + promiseResolver.setServers(val); + }, errObj + ); + }); +} + +// This test for 'dns/promises' +{ + const { + setServers + } = require('dns/promises'); + + // This should not throw any error. + (async () => { + setServers([ '127.0.0.1' ]); + })().then(common.mustCall()); + + [ + [null], + [undefined], + [Number(addresses.DNS4_SERVER)], + [ + { + address: addresses.DNS4_SERVER + }, + ], + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers\[0\]" argument must be of type string\./ + }; + assert.throws(() => { + setServers(val); + }, errObj); + }); +} diff --git a/test/js/node/test/parallel/test-dns.js b/test/js/node/test/parallel/test-dns.js new file mode 100644 index 0000000000..8c2b0f8e48 --- /dev/null +++ b/test/js/node/test/parallel/test-dns.js @@ -0,0 +1,461 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const assert = require('assert'); + +const dns = require('dns'); +const dnsPromises = dns.promises; +const dgram = require('dgram'); + +const existing = dns.getServers(); +assert(existing.length > 0); + +// Verify that setServers() handles arrays with holes and other oddities +{ + const servers = []; + + servers[0] = '127.0.0.1'; + servers[2] = '0.0.0.0'; + dns.setServers(servers); + + assert.deepStrictEqual(dns.getServers(), ['127.0.0.1', '0.0.0.0']); +} + +{ + const servers = ['127.0.0.1', '192.168.1.1']; + + servers[3] = '127.1.0.1'; + servers[4] = '127.1.0.1'; + servers[5] = '127.1.1.1'; + + Object.defineProperty(servers, 2, { + enumerable: true, + get: () => { + servers.length = 3; + return '0.0.0.0'; + } + }); + + dns.setServers(servers); + assert.deepStrictEqual(dns.getServers(), [ + '127.0.0.1', + '192.168.1.1', + '0.0.0.0', + ]); +} + +{ + // Various invalidities, all of which should throw a clean error. + const invalidServers = [ + ' ', + '\n', + '\0', + '1'.repeat(3 * 4), + // Check for REDOS issues. + ':'.repeat(100000), + '['.repeat(100000), + '['.repeat(100000) + ']'.repeat(100000) + 'a', + ]; + invalidServers.forEach((serv) => { + assert.throws( + () => { + dns.setServers([serv]); + }, + { + name: 'TypeError', + code: 'ERR_INVALID_IP_ADDRESS' + } + ); + }); +} + +const goog = [ + '8.8.8.8', + '8.8.4.4', +]; +dns.setServers(goog); +assert.deepStrictEqual(dns.getServers(), goog); +assert.throws(() => dns.setServers(['foobar']), { + code: 'ERR_INVALID_IP_ADDRESS', + name: 'TypeError', + message: 'Invalid IP address: foobar' +}); +assert.throws(() => dns.setServers(['127.0.0.1:va']), { + code: 'ERR_INVALID_IP_ADDRESS', + name: 'TypeError', + message: 'Invalid IP address: 127.0.0.1:va' +}); +assert.deepStrictEqual(dns.getServers(), goog); + +const goog6 = [ + '2001:4860:4860::8888', + '2001:4860:4860::8844', +]; +dns.setServers(goog6); +assert.deepStrictEqual(dns.getServers(), goog6); + +goog6.push('4.4.4.4'); +dns.setServers(goog6); +assert.deepStrictEqual(dns.getServers(), goog6); + +const ports = [ + '4.4.4.4:53', + '[2001:4860:4860::8888]:53', + '103.238.225.181:666', + '[fe80::483a:5aff:fee6:1f04]:666', + '[fe80::483a:5aff:fee6:1f04]', +]; +const portsExpected = [ + '4.4.4.4', + '2001:4860:4860::8888', + '103.238.225.181:666', + '[fe80::483a:5aff:fee6:1f04]:666', + 'fe80::483a:5aff:fee6:1f04', +]; +dns.setServers(ports); +assert.deepStrictEqual(dns.getServers(), portsExpected); + +dns.setServers([]); +assert.deepStrictEqual(dns.getServers(), []); + +{ + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "rrtype" argument must be of type string\. Received( an instance of Array|: \[\]|: "object")$/ + }; + assert.throws(() => { + dns.resolve('example.com', [], common.mustNotCall()); + }, errObj); + assert.throws(() => { + dnsPromises.resolve('example.com', []); + }, errObj); +} +{ + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "(host)?name" argument must be of type string\. Received:? undefined$/ + }; + assert.throws(() => { + dnsPromises.resolve(); + }, errObj); +} + +// dns.lookup should accept only falsey and string values +{ + const errorReg = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "hostname" argument must be of type string\. Received:? .*|^Expected hostname to be a string/ + }; + + assert.throws(() => dns.lookup({}, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup([], common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(true, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(1, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(common.mustNotCall(), common.mustNotCall()), + errorReg); + + assert.throws(() => dnsPromises.lookup({}), errorReg); + assert.throws(() => dnsPromises.lookup([]), errorReg); + assert.throws(() => dnsPromises.lookup(true), errorReg); + assert.throws(() => dnsPromises.lookup(1), errorReg); + assert.throws(() => dnsPromises.lookup(common.mustNotCall()), errorReg); +} + +// dns.lookup should accept falsey values +{ + const checkCallback = (err, address, family) => { + assert.ifError(err); + assert.strictEqual(address, null); + assert.strictEqual(family, 4); + }; + + ['', null, undefined, 0, NaN].forEach(async (value) => { + const res = await dnsPromises.lookup(value); + assert.deepStrictEqual(res, { address: null, family: 4 }); + dns.lookup(value, common.mustCall(checkCallback)); + }); +} + +{ + // Make sure that dns.lookup throws if hints does not represent a valid flag. + // (dns.V4MAPPED | dns.ADDRCONFIG | dns.ALL) + 1 is invalid because: + // - it's different from dns.V4MAPPED and dns.ADDRCONFIG and dns.ALL. + // - it's different from any subset of them bitwise ored. + // - it's different from 0. + // - it's an odd number different than 1, and thus is invalid, because + // flags are either === 1 or even. + const hints = (dns.V4MAPPED | dns.ADDRCONFIG | dns.ALL) + 1; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The (argument 'hints'|"hints" option) is invalid\. Received:? \d+/ + }; + + assert.throws(() => { + dnsPromises.lookup('nodejs.org', { hints }); + }, err); + assert.throws(() => { + dns.lookup('nodejs.org', { hints }, common.mustNotCall()); + }, err); +} + +assert.throws(() => dns.lookup('nodejs.org'), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => dns.lookup('nodejs.org', 4), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => dns.lookup('', { + family: 'nodejs.org', + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, +}), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +dns.lookup('', { family: 4, hints: 0 }, common.mustCall()); + +dns.lookup('', { + family: 6, + hints: dns.ADDRCONFIG +}, common.mustCall()); + +dns.lookup('', { hints: dns.V4MAPPED }, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.V4MAPPED | dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, + family: 'IPv4' +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, + family: 'IPv6' +}, common.mustCall()); + +(async function() { + await dnsPromises.lookup('', { family: 4, hints: 0 }); + await dnsPromises.lookup('', { family: 6, hints: dns.ADDRCONFIG }); + await dnsPromises.lookup('', { hints: dns.V4MAPPED }); + await dnsPromises.lookup('', { hints: dns.ADDRCONFIG | dns.V4MAPPED }); + await dnsPromises.lookup('', { hints: dns.ALL }); + await dnsPromises.lookup('', { hints: dns.V4MAPPED | dns.ALL }); + await dnsPromises.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL + }); + await dnsPromises.lookup('', { order: 'verbatim' }); +})().then(common.mustCall()); + +{ + const err = { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + message: 'The "address", "port", and "callback" arguments must be ' + + 'specified' + }; + + assert.throws(() => dns.lookupService('0.0.0.0'), err); + err.message = 'The "address" and "port" arguments must be specified'; + assert.throws(() => dnsPromises.lookupService('0.0.0.0'), err); +} + +{ + const invalidAddress = 'fasdfdsaf'; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The (argument 'address'|"address" argument) is invalid\. Received/ + }; + + assert.throws(() => { + dnsPromises.lookupService(invalidAddress, 0); + }, err); + + assert.throws(() => { + dns.lookupService(invalidAddress, 0, common.mustNotCall()); + }, err); +} + +const portErr = (port) => { + const err = { + code: 'ERR_SOCKET_BAD_PORT', + name: 'RangeError' + }; + + assert.throws(() => { + dnsPromises.lookupService('0.0.0.0', port); + }, err); + + assert.throws(() => { + dns.lookupService('0.0.0.0', port, common.mustNotCall()); + }, err); +}; +[null, undefined, 65538, 'test', NaN, Infinity, Symbol(), 0n, true, false, '', () => {}, {}].forEach(portErr); + +assert.throws(() => { + dns.lookupService('0.0.0.0', 80, null); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +{ + dns.resolveMx('foo.onion', function(err) { + assert.strictEqual(err.code, 'ENOTFOUND'); + assert.strictEqual(err.syscall, 'queryMx'); + assert.strictEqual(err.hostname, 'foo.onion'); + assert.strictEqual(err.message, 'queryMx ENOTFOUND foo.onion'); + }); +} + +{ + const cases = [ + // { method: 'resolveAny', + // answers: [ + // { type: 'A', address: '1.2.3.4', ttl: 0 }, + // { type: 'AAAA', address: '::42', ttl: 0 }, + // { type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 0 }, + // { type: 'NS', value: 'foobar.org', ttl: 0 }, + // { type: 'PTR', value: 'baz.org', ttl: 0 }, + // { + // type: 'SOA', + // nsname: 'ns1.example.com', + // hostmaster: 'admin.example.com', + // serial: 3210987654, + // refresh: 900, + // retry: 900, + // expire: 1800, + // minttl: 3333333333 + // }, + // ] }, + + { method: 'resolve4', + options: { ttl: true }, + answers: [ { type: 'A', address: '1.2.3.4', ttl: 0 } ] }, + + { method: 'resolve6', + options: { ttl: true }, + answers: [ { type: 'AAAA', address: '::42', ttl: 0 } ] }, + + { method: 'resolveSoa', + answers: [ + { + type: 'SOA', + nsname: 'ns1.example.com', + hostmaster: 'admin.example.com', + serial: 3210987654, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 3333333333 + }, + ] }, + ]; + + const server = dgram.createSocket('udp4'); + + server.on('message', common.mustCallAtLeast((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + server.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: cases[0].answers.map( + (answer) => Object.assign({ domain }, answer) + ), + }), port, address); + }, cases.length * 2 - 1)); + + server.bind(0, common.mustCall(() => { + const address = server.address(); + dns.setServers([`127.0.0.1:${address.port}`]); + + function validateResults(res) { + if (!Array.isArray(res)) + res = [res]; + + assert.deepStrictEqual(res.map(tweakEntry), + cases[0].answers.map(tweakEntry)); + } + + function tweakEntry(r) { + const ret = { ...r }; + + const { method } = cases[0]; + + // TTL values are only provided for A and AAAA entries. + if (!['A', 'AAAA'].includes(ret.type) && !/^resolve(4|6)?$/.test(method)) + delete ret.ttl; + + if (method !== 'resolveAny') + delete ret.type; + + return ret; + } + + (async function nextCase() { + if (cases.length === 0) + return server.close(); + + const { method, options } = cases[0]; + + validateResults(await dnsPromises[method]('example.org', options)); + + dns[method]('example.org', ...(options? [options] : []), common.mustSucceed((res) => { + validateResults(res); + cases.shift(); + nextCase(); + })); + })().then(common.mustCall()); + + })); +} diff --git a/test/js/node/test/parallel/test-domain-crypto.js b/test/js/node/test/parallel/test-domain-crypto.js new file mode 100644 index 0000000000..e0a470bd9d --- /dev/null +++ b/test/js/node/test/parallel/test-domain-crypto.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('node compiled without OpenSSL.'); + +const crypto = require('crypto'); + +// Pollution of global is intentional as part of test. +common.allowGlobals(require('domain')); +// See https://github.com/nodejs/node/commit/d1eff9ab +global.domain = require('domain'); + +// Should not throw a 'TypeError: undefined is not a function' exception +crypto.randomBytes(8); +crypto.randomBytes(8, common.mustSucceed()); +const buf = Buffer.alloc(8); +crypto.randomFillSync(buf); +crypto.pseudoRandomBytes(8); +crypto.pseudoRandomBytes(8, common.mustSucceed()); +crypto.pbkdf2('password', 'salt', 8, 8, 'sha1', common.mustSucceed()); diff --git a/test/js/node/test/parallel/test-domain-ee-error-listener.js b/test/js/node/test/parallel/test-domain-ee-error-listener.js new file mode 100644 index 0000000000..69041c7523 --- /dev/null +++ b/test/js/node/test/parallel/test-domain-ee-error-listener.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const domain = require('domain').create(); +const EventEmitter = require('events'); + +domain.on('error', common.mustNotCall()); + +const ee = new EventEmitter(); + +const plainObject = { justAn: 'object' }; +ee.once('error', common.mustCall((err) => { + assert.deepStrictEqual(err, plainObject); +})); +ee.emit('error', plainObject); + +const err = new Error('test error'); +ee.once('error', common.expectsError(err)); +ee.emit('error', err); diff --git a/test/js/node/test/parallel/test-domain-nested-throw.js b/test/js/node/test/parallel/test-domain-nested-throw.js new file mode 100644 index 0000000000..ec016ada72 --- /dev/null +++ b/test/js/node/test/parallel/test-domain-nested-throw.js @@ -0,0 +1,101 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const domain = require('domain'); + +if (process.argv[2] !== 'child') { + parent(); + return; +} + +function parent() { + const node = process.execPath; + const spawn = require('child_process').spawn; + const opt = { stdio: 'inherit' }; + const child = spawn(node, [__filename, 'child'], opt); + child.on('exit', function(c) { + assert(!c); + console.log('ok'); + }); +} + +let gotDomain1Error = false; +let gotDomain2Error = false; + +let threw1 = false; +let threw2 = false; + +function throw1() { + threw1 = true; + throw new Error('handled by domain1'); +} + +function throw2() { + threw2 = true; + throw new Error('handled by domain2'); +} + +function inner(throw1, throw2) { + const domain1 = domain.createDomain(); + + domain1.on('error', function(err) { + if (gotDomain1Error) { + console.error('got domain 1 twice'); + process.exit(1); + } + gotDomain1Error = true; + throw2(); + }); + + domain1.run(function() { + throw1(); + }); +} + +function outer() { + const domain2 = domain.createDomain(); + + domain2.on('error', function(err) { + if (gotDomain2Error) { + console.error('got domain 2 twice'); + process.exit(1); + } + gotDomain2Error = true; + }); + + domain2.run(function() { + inner(throw1, throw2); + }); +} + +process.on('exit', function() { + assert(gotDomain1Error); + assert(gotDomain2Error); + assert(threw1); + assert(threw2); + console.log('ok'); +}); + +outer(); diff --git a/test/js/node/test/parallel/test-domain-vm-promise-isolation.js b/test/js/node/test/parallel/test-domain-vm-promise-isolation.js new file mode 100644 index 0000000000..41aed1ee33 --- /dev/null +++ b/test/js/node/test/parallel/test-domain-vm-promise-isolation.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const domain = require('domain'); +const vm = require('vm'); + +// A promise created in a VM should not include a domain field but +// domains should still be able to propagate through them. +// +// See; https://github.com/nodejs/node/issues/40999 + +const context = vm.createContext({}); + +function run(code) { + const d = domain.createDomain(); + d.run(common.mustCall(() => { + const p = vm.runInContext(code, context)(); + assert.strictEqual(p.domain, undefined); + p.then(common.mustCall(() => { + assert.strictEqual(process.domain, d); + })); + })); +} + +for (let i = 0; i < 1000; i++) { + run('async () => null'); +} diff --git a/test/js/node/test/parallel/test-dsa-fips-invalid-key.js b/test/js/node/test/parallel/test-dsa-fips-invalid-key.js new file mode 100644 index 0000000000..05cc1d143a --- /dev/null +++ b/test/js/node/test/parallel/test-dsa-fips-invalid-key.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!common.hasFipsCrypto) + common.skip('node compiled without FIPS OpenSSL.'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const input = 'hello'; + +const dsapri = fixtures.readKey('dsa_private_1025.pem'); +const sign = crypto.createSign('SHA1'); +sign.update(input); + +assert.throws(function() { + sign.sign(dsapri); +}, /PEM_read_bio_PrivateKey failed/); diff --git a/test/js/node/test/parallel/test-error-prepare-stack-trace.js b/test/js/node/test/parallel/test-error-prepare-stack-trace.js new file mode 100644 index 0000000000..1ad29efb16 --- /dev/null +++ b/test/js/node/test/parallel/test-error-prepare-stack-trace.js @@ -0,0 +1,30 @@ +'use strict'; + +require('../common'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const assert = require('assert'); + +// Verify that the default Error.prepareStackTrace is present. +assert.strictEqual(typeof Error.prepareStackTrace, 'function'); + +// Error.prepareStackTrace() can be overridden. +{ + let prepareCalled = false; + Error.prepareStackTrace = (_error, trace) => { + prepareCalled = true; + }; + try { + throw new Error('foo'); + } catch (err) { + err.stack; // eslint-disable-line no-unused-expressions + } + assert(prepareCalled); +} + +if (process.argv[2] !== 'child') { + // Verify that the above test still passes when source-maps support is + // enabled. + spawnSyncAndExitWithoutError( + process.execPath, + ['--enable-source-maps', __filename, 'child']); +} diff --git a/test/js/node/test/parallel/test-eslint-alphabetize-errors.js b/test/js/node/test/parallel/test-eslint-alphabetize-errors.js new file mode 100644 index 0000000000..18c3aed3af --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-alphabetize-errors.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/alphabetize-errors'); + +new RuleTester().run('alphabetize-errors', rule, { + valid: [ + { code: ` + E('AAA', 'foo'); + E('BBB', 'bar'); + E('CCC', 'baz'); + `, options: [{ checkErrorDeclarations: true }] }, + ` + E('AAA', 'foo'); + E('CCC', 'baz'); + E('BBB', 'bar'); + `, + `const { + codes: { + ERR_A, + ERR_B, + }, + } = require("internal/errors")`, + ], + invalid: [ + { + code: ` + E('BBB', 'bar'); + E('AAA', 'foo'); + E('CCC', 'baz'); + `, + options: [{ checkErrorDeclarations: true }], + errors: [{ message: 'Out of ASCIIbetical order - BBB >= AAA', line: 3 }] + }, + { + code: `const { + codes: { + ERR_B, + ERR_A, + }, + } = require("internal/errors")`, + errors: [{ message: 'Out of ASCIIbetical order - ERR_B >= ERR_A', line: 4 }] + }, + { + code: 'const internalErrors = require("internal/errors")', + errors: [{ message: /Use destructuring/ }] + }, + { + code: 'const {codes} = require("internal/errors")', + errors: [{ message: /Use destructuring/ }] + }, + { + code: 'const {codes:{ERR_A}} = require("internal/errors")', + errors: [{ message: /Use multiline destructuring/ }] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-alphabetize-primordials.js b/test/js/node/test/parallel/test-eslint-alphabetize-primordials.js new file mode 100644 index 0000000000..3f63e1ecbd --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-alphabetize-primordials.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/alphabetize-primordials'); + +new RuleTester() + .run('alphabetize-primordials', rule, { + valid: [ + 'new Array()', + '"use strict";const {\nArray\n} = primordials;', + '"use strict";const {\n\tArray,\n\tDate,\n} = primordials', + '"use strict";const {\nDate,Array\n} = notPrimordials', + '"use strict";const {\nDate,globalThis:{Array}\n} = primordials', + '"use strict";const {\nBigInt,globalThis:{Array,Date,SharedArrayBuffer,parseInt}\n} = primordials', + '"use strict";const {\nFunctionPrototypeBind,Uint32Array,globalThis:{SharedArrayBuffer}\n} = primordials', + { + code: '"use strict";const fs = require("fs");const {\nArray\n} = primordials', + options: [{ enforceTopPosition: false }], + }, + ], + invalid: [ + { + code: '"use strict";const {Array} = primordials;', + errors: [{ message: /destructuring from primordials should be multiline/ }], + }, + { + code: '"use strict";const fs = require("fs");const {Date,Array} = primordials', + errors: [ + { message: /destructuring from primordials should be multiline/ }, + { message: /destructuring from primordials should be the first expression/ }, + { message: /Date >= Array/ }, + ], + }, + { + code: 'function fn() {"use strict";const {\nArray,\n} = primordials}', + errors: [{ message: /destructuring from primordials should be the first expression/ }], + }, + { + code: '"use strict";const {\nDate,Array} = primordials', + errors: [{ message: /Date >= Array/ }], + }, + { + code: '"use strict";const {\nglobalThis:{Date, Array}} = primordials', + errors: [{ message: /Date >= Array/ }], + }, + ] + }); diff --git a/test/js/node/test/parallel/test-eslint-async-iife-no-unused-result.js b/test/js/node/test/parallel/test-eslint-async-iife-no-unused-result.js new file mode 100644 index 0000000000..9f74b65868 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-async-iife-no-unused-result.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/async-iife-no-unused-result'); + +const message = 'The result of an immediately-invoked async function needs ' + + 'to be used (e.g. with `.then(common.mustCall())`)'; + +const tester = new RuleTester(); +tester.run('async-iife-no-unused-result', rule, { + valid: [ + '(() => {})()', + '(async () => {})', + '(async () => {})().then()', + '(async () => {})().catch()', + '(function () {})()', + '(async function () {})', + '(async function () {})().then()', + '(async function () {})().catch()', + ], + invalid: [ + { + code: '(async () => {})()', + errors: [{ message }], + }, + { + code: '(async function() {})()', + errors: [{ message }], + }, + { + code: "const common = require('../common');(async () => {})()", + errors: [{ message }], + output: "const common = require('../common');(async () => {})()" + + '.then(common.mustCall())', + }, + { + code: "const common = require('../common');(async function() {})()", + errors: [{ message }], + output: "const common = require('../common');(async function() {})()" + + '.then(common.mustCall())', + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-avoid-prototype-pollution.js b/test/js/node/test/parallel/test-eslint-avoid-prototype-pollution.js new file mode 100644 index 0000000000..c6b0fe6386 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-avoid-prototype-pollution.js @@ -0,0 +1,334 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/avoid-prototype-pollution'); + +new RuleTester() + .run('property-descriptor-no-prototype-pollution', rule, { + valid: [ + 'ObjectDefineProperties({}, {})', + 'ObjectCreate(null, {})', + 'ObjectDefineProperties({}, { key })', + 'ObjectCreate(null, { key })', + 'ObjectDefineProperties({}, { ...spread })', + 'ObjectCreate(null, { ...spread })', + 'ObjectDefineProperties({}, { key: valueDescriptor })', + 'ObjectCreate(null, { key: valueDescriptor })', + 'ObjectDefineProperties({}, { key: { ...{}, __proto__: null } })', + 'ObjectCreate(null, { key: { ...{}, __proto__: null } })', + 'ObjectDefineProperties({}, { key: { __proto__: null } })', + 'ObjectCreate(null, { key: { __proto__: null } })', + 'ObjectDefineProperties({}, { key: { __proto__: null, enumerable: true } })', + 'ObjectCreate(null, { key: { __proto__: null, enumerable: true } })', + 'ObjectDefineProperties({}, { key: { "__proto__": null } })', + 'ObjectCreate(null, { key: { "__proto__": null } })', + 'ObjectDefineProperties({}, { key: { \'__proto__\': null } })', + 'ObjectCreate(null, { key: { \'__proto__\': null } })', + 'ObjectDefineProperty({}, "key", ObjectCreate(null))', + 'ReflectDefineProperty({}, "key", ObjectCreate(null))', + 'ObjectDefineProperty({}, "key", valueDescriptor)', + 'ReflectDefineProperty({}, "key", valueDescriptor)', + 'ObjectDefineProperty({}, "key", { __proto__: null })', + 'ReflectDefineProperty({}, "key", { __proto__: null })', + 'ObjectDefineProperty({}, "key", { __proto__: null, enumerable: true })', + 'ReflectDefineProperty({}, "key", { __proto__: null, enumerable: true })', + 'ObjectDefineProperty({}, "key", { "__proto__": null })', + 'ReflectDefineProperty({}, "key", { "__proto__": null })', + 'ObjectDefineProperty({}, "key", { \'__proto__\': null })', + 'ReflectDefineProperty({}, "key", { \'__proto__\': null })', + 'async function myFn() { return { __proto__: null } }', + 'async function myFn() { function myFn() { return {} } return { __proto__: null } }', + 'const myFn = async function myFn() { return { __proto__: null } }', + 'const myFn = async function () { return { __proto__: null } }', + 'const myFn = async () => { return { __proto__: null } }', + 'const myFn = async () => ({ __proto__: null })', + 'function myFn() { return {} }', + 'const myFn = function myFn() { return {} }', + 'const myFn = function () { return {} }', + 'const myFn = () => { return {} }', + 'const myFn = () => ({})', + 'StringPrototypeReplace("some string", "some string", "some replacement")', + 'StringPrototypeReplaceAll("some string", "some string", "some replacement")', + 'StringPrototypeSplit("some string", "some string")', + 'new Proxy({}, otherObject)', + 'new Proxy({}, someFactory())', + 'new Proxy({}, { __proto__: null })', + 'new Proxy({}, { __proto__: null, ...{} })', + 'async function name(){return await SafePromiseAll([])}', + 'async function name(){const val = await SafePromiseAll([])}', + ], + invalid: [ + { + code: 'ObjectDefineProperties({}, ObjectGetOwnPropertyDescriptors({}))', + errors: [{ message: /prototype pollution/ }], + }, + { + code: 'ObjectCreate(null, ObjectGetOwnPropertyDescriptors({}))', + errors: [{ message: /prototype pollution/ }], + }, + { + code: 'ObjectDefineProperties({}, { key: {} })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectCreate(null, { key: {} })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperties({}, { key: { [void 0]: { ...{ __proto__: null } } } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectCreate(null, { key: { [void 0]: { ...{ __proto__: null } } } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperties({}, { key: { __proto__: Object.prototype } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectCreate(null, { key: { __proto__: Object.prototype } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperties({}, { key: { [`__proto__`]: null } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectCreate(null, { key: { [`__proto__`]: null } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperties({}, { key: { enumerable: true } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectCreate(null, { key: { enumerable: true } })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperty({}, "key", {})', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ReflectDefineProperty({}, "key", {})', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperty({}, "key", ObjectGetOwnPropertyDescriptor({}, "key"))', + errors: [{ + message: /prototype pollution/, + suggestions: [{ + desc: 'Wrap the property descriptor in a null-prototype object', + output: 'ObjectDefineProperty({}, "key", { __proto__: null,...ObjectGetOwnPropertyDescriptor({}, "key") })', + }], + }], + }, + { + code: 'ReflectDefineProperty({}, "key", ObjectGetOwnPropertyDescriptor({}, "key"))', + errors: [{ + message: /prototype pollution/, + suggestions: [{ + desc: 'Wrap the property descriptor in a null-prototype object', + output: + 'ReflectDefineProperty({}, "key", { __proto__: null,...ObjectGetOwnPropertyDescriptor({}, "key") })', + }], + }], + }, + { + code: 'ObjectDefineProperty({}, "key", ReflectGetOwnPropertyDescriptor({}, "key"))', + errors: [{ + message: /prototype pollution/, + suggestions: [{ + desc: 'Wrap the property descriptor in a null-prototype object', + output: + 'ObjectDefineProperty({}, "key", { __proto__: null,...ReflectGetOwnPropertyDescriptor({}, "key") })', + }], + }], + }, + { + code: 'ReflectDefineProperty({}, "key", ReflectGetOwnPropertyDescriptor({}, "key"))', + errors: [{ + message: /prototype pollution/, + suggestions: [{ + desc: 'Wrap the property descriptor in a null-prototype object', + output: + 'ReflectDefineProperty({}, "key", { __proto__: null,...ReflectGetOwnPropertyDescriptor({}, "key") })', + }], + }], + }, + { + code: 'ObjectDefineProperty({}, "key", { __proto__: Object.prototype })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ReflectDefineProperty({}, "key", { __proto__: Object.prototype })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperty({}, "key", { [`__proto__`]: null })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ReflectDefineProperty({}, "key", { [`__proto__`]: null })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ObjectDefineProperty({}, "key", { enumerable: true })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'ReflectDefineProperty({}, "key", { enumerable: true })', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'async function myFn(){ return {} }', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'async function myFn(){ async function someOtherFn() { return { __proto__: null } } return {} }', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'async function myFn(){ if (true) { return {} } return { __proto__: null } }', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'const myFn = async function myFn(){ return {} }', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'const myFn = async function (){ return {} }', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'const myFn = async () => { return {} }', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'const myFn = async () => ({})', + errors: [{ message: /null-prototype/ }], + }, + { + code: 'RegExpPrototypeTest(/some regex/, "some string")', + errors: [{ + message: /looks up the "exec" property/, + suggestions: [{ + desc: 'Use RegexpPrototypeExec instead', + output: 'RegExpPrototypeExec(/some regex/, "some string") !== null', + }], + }], + }, + { + code: 'RegExpPrototypeSymbolMatch(/some regex/, "some string")', + errors: [{ message: /looks up the "exec" property/ }], + }, + { + code: 'RegExpPrototypeSymbolMatchAll(/some regex/, "some string")', + errors: [{ message: /looks up the "exec" property/ }], + }, + { + code: 'RegExpPrototypeSymbolSearch(/some regex/, "some string")', + errors: [{ message: /SafeStringPrototypeSearch/ }], + }, + { + code: 'StringPrototypeMatch("some string", /some regex/)', + errors: [{ message: /looks up the Symbol\.match property/ }], + }, + { + code: 'let v = StringPrototypeMatch("some string", /some regex/)', + errors: [{ message: /looks up the Symbol\.match property/ }], + }, + { + code: 'let v = StringPrototypeMatch("some string", new RegExp("some regex"))', + errors: [{ message: /looks up the Symbol\.match property/ }], + }, + { + code: 'StringPrototypeMatchAll("some string", /some regex/)', + errors: [{ message: /looks up the Symbol\.matchAll property/ }], + }, + { + code: 'let v = StringPrototypeMatchAll("some string", new RegExp("some regex"))', + errors: [{ message: /looks up the Symbol\.matchAll property/ }], + }, + { + code: 'StringPrototypeReplace("some string", /some regex/, "some replacement")', + errors: [{ message: /looks up the Symbol\.replace property/ }], + }, + { + code: 'StringPrototypeReplace("some string", new RegExp("some regex"), "some replacement")', + errors: [{ message: /looks up the Symbol\.replace property/ }], + }, + { + code: 'StringPrototypeReplaceAll("some string", /some regex/, "some replacement")', + errors: [{ message: /looks up the Symbol\.replace property/ }], + }, + { + code: 'StringPrototypeReplaceAll("some string", new RegExp("some regex"), "some replacement")', + errors: [{ message: /looks up the Symbol\.replace property/ }], + }, + { + code: 'StringPrototypeSearch("some string", /some regex/)', + errors: [{ message: /SafeStringPrototypeSearch/ }], + }, + { + code: 'StringPrototypeSplit("some string", /some regex/)', + errors: [{ message: /looks up the Symbol\.split property/ }], + }, + { + code: 'new Proxy({}, {})', + errors: [{ message: /null-prototype/ }] + }, + { + code: 'new Proxy({}, { [`__proto__`]: null })', + errors: [{ message: /null-prototype/ }] + }, + { + code: 'new Proxy({}, { __proto__: Object.prototype })', + errors: [{ message: /null-prototype/ }] + }, + { + code: 'new Proxy({}, { ...{ __proto__: null } })', + errors: [{ message: /null-prototype/ }] + }, + { + code: 'PromisePrototypeCatch(promise, ()=>{})', + errors: [{ message: /\bPromisePrototypeThen\b/ }] + }, + { + code: 'PromiseAll([])', + errors: [{ message: /\bSafePromiseAll\b/ }] + }, + { + code: 'async function fn(){await SafePromiseAll([])}', + errors: [{ message: /\bSafePromiseAllReturnVoid\b/ }] + }, + { + code: 'async function fn(){await SafePromiseAllSettled([])}', + errors: [{ message: /\bSafePromiseAllSettledReturnVoid\b/ }] + }, + { + code: 'PromiseAllSettled([])', + errors: [{ message: /\bSafePromiseAllSettled\b/ }] + }, + { + code: 'PromiseAny([])', + errors: [{ message: /\bSafePromiseAny\b/ }] + }, + { + code: 'PromiseRace([])', + errors: [{ message: /\bSafePromiseRace\b/ }] + }, + { + code: 'ArrayPrototypeConcat([])', + errors: [{ message: /\bisConcatSpreadable\b/ }] + }, + ] + }); diff --git a/test/js/node/test/parallel/test-eslint-crypto-check.js b/test/js/node/test/parallel/test-eslint-crypto-check.js new file mode 100644 index 0000000000..2b2c0c2dd1 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-crypto-check.js @@ -0,0 +1,77 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/crypto-check'); + +const message = 'Please add a hasCrypto check to allow this test to be ' + + 'skipped when Node is built "--without-ssl".'; + +new RuleTester().run('crypto-check', rule, { + valid: [ + 'foo', + 'crypto', + ` + if (!common.hasCrypto) { + common.skip("missing crypto"); + } + require("crypto"); + `, + ` + if (!common.hasCrypto) { + common.skip("missing crypto"); + } + internalBinding("crypto"); + `, + ], + invalid: [ + { + code: 'require("common")\n' + + 'require("crypto")\n' + + 'if (!common.hasCrypto) {\n' + + ' common.skip("missing crypto");\n' + + '}', + errors: [{ message }] + }, + { + code: 'require("common")\n' + + 'require("crypto")', + errors: [{ message }], + output: 'require("common")\n' + + 'if (!common.hasCrypto) {' + + ' common.skip("missing crypto");' + + '}\n' + + 'require("crypto")' + }, + { + code: 'require("common")\n' + + 'if (common.foo) {}\n' + + 'require("crypto")', + errors: [{ message }], + output: 'require("common")\n' + + 'if (!common.hasCrypto) {' + + ' common.skip("missing crypto");' + + '}\n' + + 'if (common.foo) {}\n' + + 'require("crypto")' + }, + { + code: 'require("common")\n' + + 'if (common.foo) {}\n' + + 'internalBinding("crypto")', + errors: [{ message }], + output: 'require("common")\n' + + 'if (!common.hasCrypto) {' + + ' common.skip("missing crypto");' + + '}\n' + + 'if (common.foo) {}\n' + + 'internalBinding("crypto")' + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-documented-deprecation-codes.js b/test/js/node/test/parallel/test-eslint-documented-deprecation-codes.js new file mode 100644 index 0000000000..dc3dc46b7e --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-documented-deprecation-codes.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +if (!common.hasIntl) + common.skip('missing Intl'); +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/documented-deprecation-codes'); + +const mdFile = 'doc/api/deprecations.md'; + +const invalidCode = 'UNDOCUMENTED INVALID CODE'; + +new RuleTester().run('documented-deprecation-codes', rule, { + valid: [ + ` + deprecate(function() { + return this.getHeaders(); + }, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066') + `, + ], + invalid: [ + { + code: ` + deprecate(function foo(){}, 'bar', '${invalidCode}'); + `, + errors: [ + { + message: `"${invalidCode}" does not match the expected pattern`, + line: 2 + }, + { + message: `"${invalidCode}" is not documented in ${mdFile}`, + line: 2 + }, + ] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-documented-errors.js b/test/js/node/test/parallel/test-eslint-documented-errors.js new file mode 100644 index 0000000000..03131306d7 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-documented-errors.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/documented-errors'); + +const invalidCode = 'UNDOCUMENTED ERROR CODE'; + +new RuleTester().run('documented-errors', rule, { + valid: [ + ` + E('ERR_ASSERTION', 'foo'); + `, + ], + invalid: [ + { + code: ` + E('${invalidCode}', 'bar'); + `, + errors: [ + { + message: `"${invalidCode}" is not documented in doc/api/errors.md`, + line: 2 + }, + { + message: + `doc/api/errors.md does not have an anchor for "${invalidCode}"`, + line: 2 + }, + ] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-duplicate-requires.js b/test/js/node/test/parallel/test-eslint-duplicate-requires.js new file mode 100644 index 0000000000..f2a11b37ca --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-duplicate-requires.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const { RuleTester } = require('../../tools/eslint/node_modules/eslint'); +const rule = require('../../tools/eslint-rules/no-duplicate-requires'); + +new RuleTester({ + languageOptions: { + sourceType: 'script', + }, +}).run('no-duplicate-requires', rule, { + valid: [ + { + code: 'require("a"); require("b"); (function() { require("a"); });', + }, + { + code: 'require(a); require(a);', + }, + ], + invalid: [ + { + code: 'require("a"); require("a");', + errors: [{ message: '\'a\' require is duplicated.' }], + }, + ], +}); diff --git a/test/js/node/test/parallel/test-eslint-eslint-check.js b/test/js/node/test/parallel/test-eslint-eslint-check.js new file mode 100644 index 0000000000..ca34497c32 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-eslint-check.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/eslint-check'); + +const message = 'Please add a skipIfEslintMissing() call to allow this ' + + 'test to be skipped when Node.js is built ' + + 'from a source tarball.'; + +new RuleTester().run('eslint-check', rule, { + valid: [ + 'foo;', + 'require("common")\n' + + 'common.skipIfEslintMissing();\n' + + 'require("../../tools/eslint/node_modules/eslint")', + ], + invalid: [ + { + code: 'require("common")\n' + + 'require("../../tools/eslint/node_modules/eslint").RuleTester', + errors: [{ message }], + output: 'require("common")\n' + + 'common.skipIfEslintMissing();\n' + + 'require("../../tools/eslint/node_modules/eslint").RuleTester' + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-inspector-check.js b/test/js/node/test/parallel/test-eslint-inspector-check.js new file mode 100644 index 0000000000..c60dcf0874 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-inspector-check.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/inspector-check'); + +const message = 'Please add a skipIfInspectorDisabled() call to allow this ' + + 'test to be skipped when Node is built ' + + '\'--without-inspector\'.'; + +new RuleTester().run('inspector-check', rule, { + valid: [ + 'foo;', + 'require("common")\n' + + 'common.skipIfInspectorDisabled();\n' + + 'require("inspector")', + ], + invalid: [ + { + code: 'require("common")\n' + + 'require("inspector")', + errors: [{ message }], + output: 'require("common")\n' + + 'common.skipIfInspectorDisabled();\n' + + 'require("inspector")' + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-lowercase-name-for-primitive.js b/test/js/node/test/parallel/test-eslint-lowercase-name-for-primitive.js new file mode 100644 index 0000000000..f8029d7c8b --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-lowercase-name-for-primitive.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/lowercase-name-for-primitive'); + +new RuleTester().run('lowercase-name-for-primitive', rule, { + valid: [ + 'new errors.TypeError("ERR_INVALID_ARG_TYPE", "a", ["string", "number"])', + 'new errors.TypeError("ERR_INVALID_ARG_TYPE", "name", "string")', + 'new errors.TypeError("ERR_INVALID_ARG_TYPE", "name", "number")', + 'new errors.TypeError("ERR_INVALID_ARG_TYPE", "name", "boolean")', + 'new errors.TypeError("ERR_INVALID_ARG_TYPE", "name", "null")', + 'new errors.TypeError("ERR_INVALID_ARG_TYPE", "name", "undefined")', + ], + invalid: [ + { + code: "new errors.TypeError('ERR_INVALID_ARG_TYPE', 'a', 'Number')", + errors: [{ message: 'primitive should use lowercase: Number' }], + output: "new errors.TypeError('ERR_INVALID_ARG_TYPE', 'a', 'number')", + }, + { + code: "new errors.TypeError('ERR_INVALID_ARG_TYPE', 'a', 'STRING')", + errors: [{ message: 'primitive should use lowercase: STRING' }], + output: "new errors.TypeError('ERR_INVALID_ARG_TYPE', 'a', 'string')", + }, + { + code: "new e.TypeError('ERR_INVALID_ARG_TYPE', a, ['String','Number'])", + errors: [ + { message: 'primitive should use lowercase: String' }, + { message: 'primitive should use lowercase: Number' }, + ], + output: "new e.TypeError('ERR_INVALID_ARG_TYPE', a, ['string','number'])", + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-no-array-destructuring.js b/test/js/node/test/parallel/test-eslint-no-array-destructuring.js new file mode 100644 index 0000000000..3f9b6e0094 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-no-array-destructuring.js @@ -0,0 +1,139 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const { RuleTester } = require('../../tools/eslint/node_modules/eslint'); +const rule = require('../../tools/eslint-rules/no-array-destructuring'); + +const USE_OBJ_DESTRUCTURING = + 'Use object destructuring instead of array destructuring.'; +const USE_ARRAY_METHODS = + 'Use primordials.ArrayPrototypeSlice to avoid unsafe array iteration.'; + +new RuleTester() + .run('no-array-destructuring', rule, { + valid: [ + 'const first = [1, 2, 3][0];', + 'const {0:first} = [1, 2, 3];', + '({1:elem} = array);', + 'function name(param, { 0: key, 1: value },) {}', + ], + invalid: [ + { + code: 'const [Array] = args;', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'const {0:Array} = args;' + }, + { + code: 'const [ , res] = args;', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'const { 1:res} = args;', + }, + { + code: '[, elem] = options;', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: '({ 1:elem} = options);', + }, + { + code: 'const {values:[elem]} = options;', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'const {values:{0:elem}} = options;', + }, + { + code: '[[[elem]]] = options;', + errors: [ + { message: USE_OBJ_DESTRUCTURING }, + { message: USE_OBJ_DESTRUCTURING }, + { message: USE_OBJ_DESTRUCTURING }, + ], + output: '({0:[[elem]]} = options);', + }, + { + code: '[, ...rest] = options;', + errors: [{ message: USE_ARRAY_METHODS }], + }, + { + code: 'for(const [key, value] of new Map);', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'for(const {0:key, 1:value} of new Map);', + }, + { + code: 'let [first,,,fourth] = array;', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'let {0:first,3:fourth} = array;', + }, + { + code: 'let [,second,,fourth] = array;', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'let {1:second,3:fourth} = array;', + }, + { + code: 'let [ ,,,fourth ] = array;', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'let { 3:fourth } = array;', + }, + { + code: 'let [,,,fourth, fifth,, minorFall, majorLift,...music] = arr;', + errors: [{ message: USE_ARRAY_METHODS }], + }, + { + code: 'function map([key, value]) {}', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'function map({0:key, 1:value}) {}', + }, + { + code: 'function map([key, value],) {}', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'function map({0:key, 1:value},) {}', + }, + { + code: '(function([key, value]) {})', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: '(function({0:key, 1:value}) {})', + }, + { + code: '(function([key, value] = [null, 0]) {})', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: '(function({0:key, 1:value} = [null, 0]) {})', + }, + { + code: 'function map([key, ...values]) {}', + errors: [{ message: USE_ARRAY_METHODS }], + }, + { + code: 'function map([key, value], ...args) {}', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'function map({0:key, 1:value}, ...args) {}', + }, + { + code: 'async function map([key, value], ...args) {}', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'async function map({0:key, 1:value}, ...args) {}', + }, + { + code: 'async function* generator([key, value], ...args) {}', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'async function* generator({0:key, 1:value}, ...args) {}', + }, + { + code: 'function* generator([key, value], ...args) {}', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'function* generator({0:key, 1:value}, ...args) {}', + }, + { + code: 'const cb = ([key, value], ...args) => {}', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'const cb = ({0:key, 1:value}, ...args) => {}', + }, + { + code: 'class name{ method([key], ...args){} }', + errors: [{ message: USE_OBJ_DESTRUCTURING }], + output: 'class name{ method({0:key}, ...args){} }', + }, + ] + }); diff --git a/test/js/node/test/parallel/test-eslint-no-unescaped-regexp-dot.js b/test/js/node/test/parallel/test-eslint-no-unescaped-regexp-dot.js new file mode 100644 index 0000000000..457b76a2fc --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-no-unescaped-regexp-dot.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/no-unescaped-regexp-dot'); + +new RuleTester().run('no-unescaped-regexp-dot', rule, { + valid: [ + '/foo/', + String.raw`/foo\./`, + '/.+/', + '/.*/', + '/.?/', + '/.{5}/', + String.raw`/\\\./`, + ], + invalid: [ + { + code: '/./', + errors: [{ message: 'Unescaped dot character in regular expression' }] + }, + { + code: String.raw`/\\./`, + errors: [{ message: 'Unescaped dot character in regular expression' }] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-non-ascii-character.js b/test/js/node/test/parallel/test-eslint-non-ascii-character.js new file mode 100644 index 0000000000..2d71fda279 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-non-ascii-character.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/non-ascii-character'); + +new RuleTester().run('non-ascii-characters', rule, { + valid: [ + { + code: 'console.log("fhqwhgads")', + options: [] + }, + ], + invalid: [ + { + code: 'console.log("μ")', + options: [], + errors: [{ message: "Non-ASCII character 'μ' detected." }], + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-prefer-assert-iferror.js b/test/js/node/test/parallel/test-eslint-prefer-assert-iferror.js new file mode 100644 index 0000000000..cd8f46146d --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-assert-iferror.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-assert-iferror'); + +new RuleTester().run('prefer-assert-iferror', rule, { + valid: [ + 'assert.ifError(err);', + 'if (err) throw somethingElse;', + 'throw err;', + 'if (err) { throw somethingElse; }', + ], + invalid: [ + { + code: 'require("assert");\n' + + 'if (err) throw err;', + errors: [{ message: 'Use assert.ifError(err) instead.' }], + output: 'require("assert");\n' + + 'assert.ifError(err);' + }, + { + code: 'require("assert");\n' + + 'if (error) { throw error; }', + errors: [{ message: 'Use assert.ifError(error) instead.' }], + output: 'require("assert");\n' + + 'assert.ifError(error);' + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-prefer-assert-methods.js b/test/js/node/test/parallel/test-eslint-prefer-assert-methods.js new file mode 100644 index 0000000000..a77ffd01d4 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-assert-methods.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-assert-methods'); + +new RuleTester().run('prefer-assert-methods', rule, { + valid: [ + 'assert.strictEqual(foo, bar);', + 'assert(foo === bar && baz);', + 'assert.notStrictEqual(foo, bar);', + 'assert(foo !== bar && baz);', + 'assert.equal(foo, bar);', + 'assert(foo == bar && baz);', + 'assert.notEqual(foo, bar);', + 'assert(foo != bar && baz);', + 'assert.ok(foo);', + 'assert.ok(foo != bar);', + 'assert.ok(foo === bar && baz);', + ], + invalid: [ + { + code: 'assert(foo == bar);', + errors: [{ + message: "'assert.equal' should be used instead of '=='" + }], + output: 'assert.equal(foo, bar);' + }, + { + code: 'assert(foo === bar);', + errors: [{ + message: "'assert.strictEqual' should be used instead of '==='" + }], + output: 'assert.strictEqual(foo, bar);' + }, + { + code: 'assert(foo != bar);', + errors: [{ + message: "'assert.notEqual' should be used instead of '!='" + }], + output: 'assert.notEqual(foo, bar);' + }, + { + code: 'assert(foo !== bar);', + errors: [{ + message: "'assert.notStrictEqual' should be used instead of '!=='" + }], + output: 'assert.notStrictEqual(foo, bar);' + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-prefer-common-mustnotcall.js b/test/js/node/test/parallel/test-eslint-prefer-common-mustnotcall.js new file mode 100644 index 0000000000..a805b3c4ae --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-common-mustnotcall.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-common-mustnotcall'); + +const message = 'Please use common.mustNotCall(msg) instead of ' + + 'common.mustCall(fn, 0) or common.mustCall(0).'; + +new RuleTester().run('prefer-common-mustnotcall', rule, { + valid: [ + 'common.mustNotCall(fn)', + 'common.mustCall(fn)', + 'common.mustCall(fn, 1)', + ], + invalid: [ + { + code: 'common.mustCall(fn, 0)', + errors: [{ message }] + }, + { + code: 'common.mustCall(0)', + errors: [{ message }] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-prefer-common-mustsucceed.js b/test/js/node/test/parallel/test-eslint-prefer-common-mustsucceed.js new file mode 100644 index 0000000000..134d8bbc05 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-common-mustsucceed.js @@ -0,0 +1,51 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-common-mustsucceed'); + +const msg1 = 'Please use common.mustSucceed instead of ' + + 'common.mustCall(assert.ifError).'; +const msg2 = 'Please use common.mustSucceed instead of ' + + 'common.mustCall with assert.ifError.'; + +new RuleTester().run('prefer-common-mustsucceed', rule, { + valid: [ + 'foo((err) => assert.ifError(err))', + 'foo(function(err) { assert.ifError(err) })', + 'foo(assert.ifError)', + 'common.mustCall((err) => err)', + ], + invalid: [ + { + code: 'common.mustCall(assert.ifError)', + errors: [{ message: msg1 }] + }, + { + code: 'common.mustCall((err) => assert.ifError(err))', + errors: [{ message: msg2 }] + }, + { + code: 'common.mustCall((e) => assert.ifError(e))', + errors: [{ message: msg2 }] + }, + { + code: 'common.mustCall(function(e) { assert.ifError(e); })', + errors: [{ message: msg2 }] + }, + { + code: 'common.mustCall(function(e) { return assert.ifError(e); })', + errors: [{ message: msg2 }] + }, + { + code: 'common.mustCall(function(e) {{ assert.ifError(e); }})', + errors: [{ message: msg2 }] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-prefer-primordials.js b/test/js/node/test/parallel/test-eslint-prefer-primordials.js new file mode 100644 index 0000000000..61c84cbadd --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-primordials.js @@ -0,0 +1,265 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-primordials'); + +new RuleTester({ + languageOptions: { + sourceType: 'script', + }, +}) + .run('prefer-primordials', rule, { + valid: [ + 'new Array()', + 'JSON.stringify({})', + 'class A { *[Symbol.iterator] () { yield "a"; } }', + 'const a = { *[Symbol.iterator] () { yield "a"; } }', + 'Object.defineProperty(o, Symbol.toStringTag, { value: "o" })', + 'parseInt("10")', + ` + const { Reflect } = primordials; + module.exports = function() { + const { ownKeys } = Reflect; + } + `, + { + code: 'const { Array } = primordials; new Array()', + options: [{ name: 'Array' }] + }, + { + code: 'const { JSONStringify } = primordials; JSONStringify({})', + options: [{ name: 'JSON' }] + }, + { + code: 'const { SymbolFor } = primordials; SymbolFor("xxx")', + options: [{ name: 'Symbol' }] + }, + { + code: ` + const { SymbolIterator } = primordials; + class A { *[SymbolIterator] () { yield "a"; } } + `, + options: [{ name: 'Symbol' }] + }, + { + code: ` + const { Symbol } = primordials; + const a = { *[Symbol.iterator] () { yield "a"; } } + `, + options: [{ name: 'Symbol', ignore: ['iterator'] }] + }, + { + code: ` + const { ObjectDefineProperty, Symbol } = primordials; + ObjectDefineProperty(o, Symbol.toStringTag, { value: "o" }); + const val = Symbol.toStringTag; + const { toStringTag } = Symbol; + `, + options: [{ name: 'Symbol', ignore: ['toStringTag'] }] + }, + { + code: 'const { Symbol } = primordials; Symbol.for("xxx")', + options: [{ name: 'Symbol', ignore: ['for'] }] + }, + { + code: 'const { NumberParseInt } = primordials; NumberParseInt("xxx")', + options: [{ name: 'parseInt', into: 'Number' }] + }, + { + code: ` + const { ReflectOwnKeys } = primordials; + module.exports = function() { + ReflectOwnKeys({}) + } + `, + options: [{ name: 'Reflect' }], + }, + { + code: 'const { Map } = primordials; new Map()', + options: [{ name: 'Map', into: 'Safe' }], + }, + { + code: ` + const { Function } = primordials; + const rename = Function; + const obj = { rename }; + `, + options: [{ name: 'Function' }], + }, + { + code: ` + const { Function } = primordials; + let rename; + rename = Function; + const obj = { rename }; + `, + options: [{ name: 'Function' }], + }, + { + code: 'function identifier() {}', + options: [{ name: 'identifier' }] + }, + { + code: 'function* identifier() {}', + options: [{ name: 'identifier' }] + }, + { + code: 'class identifier {}', + options: [{ name: 'identifier' }] + }, + { + code: 'new class { identifier(){} }', + options: [{ name: 'identifier' }] + }, + { + code: 'const a = { identifier: \'4\' }', + options: [{ name: 'identifier' }] + }, + { + code: 'identifier:{const a = 4}', + options: [{ name: 'identifier' }] + }, + { + code: 'switch(0){case identifier:}', + options: [{ name: 'identifier' }] + }, + ], + invalid: [ + { + code: 'new Array()', + options: [{ name: 'Array' }], + errors: [{ message: /const { Array } = primordials/ }] + }, + { + code: 'JSON.parse("{}")', + options: [{ name: 'JSON' }], + errors: [ + { message: /const { JSONParse } = primordials/ }, + ] + }, + { + code: 'const { JSON } = primordials; JSON.parse("{}")', + options: [{ name: 'JSON' }], + errors: [{ message: /const { JSONParse } = primordials/ }] + }, + { + code: 'Symbol.for("xxx")', + options: [{ name: 'Symbol' }], + errors: [ + { message: /const { SymbolFor } = primordials/ }, + ] + }, + { + code: 'const { Symbol } = primordials; Symbol.for("xxx")', + options: [{ name: 'Symbol' }], + errors: [{ message: /const { SymbolFor } = primordials/ }] + }, + { + code: ` + const { Symbol } = primordials; + class A { *[Symbol.iterator] () { yield "a"; } } + `, + options: [{ name: 'Symbol' }], + errors: [{ message: /const { SymbolIterator } = primordials/ }] + }, + { + code: ` + const { Symbol } = primordials; + const a = { *[Symbol.iterator] () { yield "a"; } } + `, + options: [{ name: 'Symbol' }], + errors: [{ message: /const { SymbolIterator } = primordials/ }] + }, + { + code: ` + const { ObjectDefineProperty, Symbol } = primordials; + ObjectDefineProperty(o, Symbol.toStringTag, { value: "o" }) + `, + options: [{ name: 'Symbol' }], + errors: [{ message: /const { SymbolToStringTag } = primordials/ }] + }, + { + code: ` + const { Number } = primordials; + Number.parseInt('10') + `, + options: [{ name: 'Number' }], + errors: [{ message: /const { NumberParseInt } = primordials/ }] + }, + { + code: 'parseInt("10")', + options: [{ name: 'parseInt', into: 'Number' }], + errors: [{ message: /const { NumberParseInt } = primordials/ }] + }, + { + code: ` + module.exports = function() { + const { ownKeys } = Reflect; + } + `, + options: [{ name: 'Reflect' }], + errors: [{ message: /const { ReflectOwnKeys } = primordials/ }] + }, + { + code: ` + const { Reflect } = primordials; + module.exports = function() { + const { ownKeys } = Reflect; + } + `, + options: [{ name: 'Reflect' }], + errors: [{ message: /const { ReflectOwnKeys } = primordials/ }] + }, + { + code: 'new Map()', + options: [{ name: 'Map', into: 'Safe' }], + errors: [{ message: /const { SafeMap } = primordials/ }] + }, + { + code: ` + const { Function } = primordials; + const noop = Function.prototype; + `, + options: [{ name: 'Function' }], + errors: [{ message: /const { FunctionPrototype } = primordials/ }] + }, + { + code: ` + const obj = { Function }; + `, + options: [{ name: 'Function' }], + errors: [{ message: /const { Function } = primordials/ }] + }, + { + code: ` + const rename = Function; + `, + options: [{ name: 'Function' }], + errors: [{ message: /const { Function } = primordials/ }] + }, + { + code: ` + const rename = Function; + const obj = { rename }; + `, + options: [{ name: 'Function' }], + errors: [{ message: /const { Function } = primordials/ }] + }, + { + code: ` + let rename; + rename = Function; + const obj = { rename }; + `, + options: [{ name: 'Function' }], + errors: [{ message: /const { Function } = primordials/ }] + }, + ] + }); diff --git a/test/js/node/test/parallel/test-eslint-prefer-proto.js b/test/js/node/test/parallel/test-eslint-prefer-proto.js new file mode 100644 index 0000000000..7e927c967b --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-proto.js @@ -0,0 +1,69 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-proto'); + +new RuleTester().run('prefer-common-mustsucceed', rule, { + valid: [ + '({ __proto__: null })', + 'const x = { __proto__: null };', + ` + class X { + field = { __proto__: null }; + + constructor() { + this.x = { __proto__: X }; + } + } + `, + 'foo({ __proto__: Array.prototype })', + '({ "__proto__": null })', + "({ '__proto__': null })", + 'ObjectCreate(null, undefined)', + 'Object.create(null, undefined)', + 'Object.create(null, undefined, undefined)', + 'ObjectCreate(null, descriptors)', + 'Object.create(null, descriptors)', + 'ObjectCreate(Foo.prototype, descriptors)', + 'Object.create(Foo.prototype, descriptors)', + ], + invalid: [ + { + code: 'ObjectCreate(null)', + output: '{ __proto__: null }', + errors: [{ messageId: 'error', data: { value: 'null' } }], + }, + { + code: 'Object.create(null)', + output: '{ __proto__: null }', + errors: [{ messageId: 'error', data: { value: 'null' } }], + }, + { + code: 'ObjectCreate(null,)', + output: '{ __proto__: null }', + errors: [{ messageId: 'error', data: { value: 'null' } }], + }, + { + code: 'Object.create(null,)', + output: '{ __proto__: null }', + errors: [{ messageId: 'error', data: { value: 'null' } }], + }, + { + code: 'ObjectCreate(Foo)', + output: '{ __proto__: Foo }', + errors: [{ messageId: 'error', data: { value: 'Foo' } }], + }, + { + code: 'Object.create(Foo)', + output: '{ __proto__: Foo }', + errors: [{ messageId: 'error', data: { value: 'Foo' } }], + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-prefer-util-format-errors.js b/test/js/node/test/parallel/test-eslint-prefer-util-format-errors.js new file mode 100644 index 0000000000..3410265e9f --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-util-format-errors.js @@ -0,0 +1,32 @@ +'use strict'; + +/* eslint-disable no-template-curly-in-string */ + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-util-format-errors'); + +new RuleTester() + .run('prefer-util-format-errors', rule, { + valid: [ + 'E(\'ABC\', \'abc\');', + 'E(\'ABC\', (arg1, arg2) => `${arg2}${arg1}`);', + 'E(\'ABC\', (arg1, arg2) => `${arg1}{arg2.something}`);', + 'E(\'ABC\', (arg1, arg2) => fn(arg1, arg2));', + ], + invalid: [ + { + code: 'E(\'ABC\', (arg1, arg2) => `${arg1}${arg2}`);', + errors: [{ + message: 'Please use a printf-like formatted string that ' + + 'util.format can consume.' + }] + }, + ] + }); diff --git a/test/js/node/test/parallel/test-eslint-require-common-first.js b/test/js/node/test/parallel/test-eslint-require-common-first.js new file mode 100644 index 0000000000..ef19f95b97 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-require-common-first.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/require-common-first'); + +new RuleTester({ + languageOptions: { + sourceType: 'script', + }, +}).run('require-common-first', rule, { + valid: [ + { + code: 'require("common")\n' + + 'require("assert")' + }, + ], + invalid: [ + { + code: 'require("assert")\n' + + 'require("common")', + errors: [{ message: 'Mandatory module "common" must be loaded ' + + 'before any other modules.' }] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eslint-required-modules.js b/test/js/node/test/parallel/test-eslint-required-modules.js new file mode 100644 index 0000000000..4704163a38 --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-required-modules.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/required-modules'); + +new RuleTester({ + languageOptions: { + sourceType: 'script', + }, +}).run('required-modules', rule, { + valid: [ + { + code: 'require("common")', + options: [{ common: 'common' }] + }, + { + code: 'foo', + options: [] + }, + { + code: 'require("common")', + options: [{ common: 'common(/index\\.(m)?js)?$' }] + }, + { + code: 'require("common/index.js")', + options: [{ common: 'common(/index\\.(m)?js)?$' }] + }, + ], + invalid: [ + { + code: 'foo', + options: [{ common: 'common' }], + errors: [{ message: 'Mandatory module "common" must be loaded.' }] + }, + { + code: 'require("common/fixtures.js")', + options: [{ common: 'common(/index\\.(m)?js)?$' }], + errors: [{ + message: + 'Mandatory module "common" must be loaded.' + }] + }, + { + code: 'require("somethingElse")', + options: [{ common: 'common' }], + errors: [{ message: 'Mandatory module "common" must be loaded.' }] + }, + ] +}); diff --git a/test/js/node/test/parallel/test-eval-strict-referenceerror.js b/test/js/node/test/parallel/test-eval-strict-referenceerror.js new file mode 100644 index 0000000000..97f2b15ba0 --- /dev/null +++ b/test/js/node/test/parallel/test-eval-strict-referenceerror.js @@ -0,0 +1,27 @@ +/* eslint-disable strict */ +require('../common'); + +// In Node.js 0.10, a bug existed that caused strict functions to not capture +// their environment when evaluated. When run in 0.10 `test()` fails with a +// `ReferenceError`. See https://github.com/nodejs/node/issues/2245 for details. + +const assert = require('assert'); + +function test() { + + const code = [ + 'var foo = {m: 1};', + '', + 'function bar() {', + '\'use strict\';', + 'return foo; // foo isn\'t captured in 0.10', + '};', + ].join('\n'); + + eval(code); + + return bar(); // eslint-disable-line no-undef + +} + +assert.deepStrictEqual(test(), { m: 1 }); diff --git a/test/js/node/test/parallel/test-eval.js b/test/js/node/test/parallel/test-eval.js new file mode 100644 index 0000000000..46a4b7c546 --- /dev/null +++ b/test/js/node/test/parallel/test-eval.js @@ -0,0 +1,7 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// Verify that eval is allowed by default. +assert.strictEqual(eval('"eval"'), 'eval'); diff --git a/test/js/node/test/parallel/test-event-emitter-add-listeners.js b/test/js/node/test/parallel/test-event-emitter-add-listeners.js new file mode 100644 index 0000000000..f42d1f2487 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-add-listeners.js @@ -0,0 +1,86 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +{ + const ee = new EventEmitter(); + const events_new_listener_emitted = []; + const listeners_new_listener_emitted = []; + + // Sanity check + assert.strictEqual(ee.addListener, ee.on); + + ee.on('newListener', function(event, listener) { + // Don't track newListener listeners. + if (event === 'newListener') + return; + + events_new_listener_emitted.push(event); + listeners_new_listener_emitted.push(listener); + }); + + const hello = common.mustCall(function(a, b) { + assert.strictEqual(a, 'a'); + assert.strictEqual(b, 'b'); + }); + + ee.once('newListener', function(name, listener) { + assert.strictEqual(name, 'hello'); + assert.strictEqual(listener, hello); + assert.deepStrictEqual(this.listeners('hello'), []); + }); + + ee.on('hello', hello); + ee.once('foo', assert.fail); + assert.deepStrictEqual(['hello', 'foo'], events_new_listener_emitted); + assert.deepStrictEqual([hello, assert.fail], listeners_new_listener_emitted); + + ee.emit('hello', 'a', 'b'); +} + +// Just make sure that this doesn't throw: +{ + const f = new EventEmitter(); + + f.setMaxListeners(0); +} + +{ + const listen1 = () => {}; + const listen2 = () => {}; + const ee = new EventEmitter(); + + ee.once('newListener', function() { + assert.deepStrictEqual(ee.listeners('hello'), []); + ee.once('newListener', function() { + assert.deepStrictEqual(ee.listeners('hello'), []); + }); + ee.on('hello', listen2); + }); + ee.on('hello', listen1); + // The order of listeners on an event is not always the order in which the + // listeners were added. + assert.deepStrictEqual(ee.listeners('hello'), [listen2, listen1]); +} diff --git a/test/js/node/test/parallel/test-event-emitter-check-listener-leaks.js b/test/js/node/test/parallel/test-event-emitter-check-listener-leaks.js new file mode 100644 index 0000000000..04ef3ddab1 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-check-listener-leaks.js @@ -0,0 +1,103 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const events = require('events'); + +// default +{ + const e = new events.EventEmitter(); + + for (let i = 0; i < 10; i++) { + e.on('default', common.mustNotCall()); + } + assert.ok(!Object.hasOwn(e._events.default, 'warned')); + e.on('default', common.mustNotCall()); + assert.ok(e._events.default.warned); + + // symbol + const symbol = Symbol('symbol'); + e.setMaxListeners(1); + e.on(symbol, common.mustNotCall()); + assert.ok(!Object.hasOwn(e._events[symbol], 'warned')); + e.on(symbol, common.mustNotCall()); + assert.ok(Object.hasOwn(e._events[symbol], 'warned')); + + // specific + e.setMaxListeners(5); + for (let i = 0; i < 5; i++) { + e.on('specific', common.mustNotCall()); + } + assert.ok(!Object.hasOwn(e._events.specific, 'warned')); + e.on('specific', common.mustNotCall()); + assert.ok(e._events.specific.warned); + + // only one + e.setMaxListeners(1); + e.on('only one', common.mustNotCall()); + assert.ok(!Object.hasOwn(e._events['only one'], 'warned')); + e.on('only one', common.mustNotCall()); + assert.ok(Object.hasOwn(e._events['only one'], 'warned')); + + // unlimited + e.setMaxListeners(0); + for (let i = 0; i < 1000; i++) { + e.on('unlimited', common.mustNotCall()); + } + assert.ok(!Object.hasOwn(e._events.unlimited, 'warned')); +} + +// process-wide +{ + events.EventEmitter.defaultMaxListeners = 42; + const e = new events.EventEmitter(); + + for (let i = 0; i < 42; ++i) { + e.on('fortytwo', common.mustNotCall()); + } + assert.ok(!Object.hasOwn(e._events.fortytwo, 'warned')); + e.on('fortytwo', common.mustNotCall()); + assert.ok(Object.hasOwn(e._events.fortytwo, 'warned')); + delete e._events.fortytwo.warned; + + events.EventEmitter.defaultMaxListeners = 44; + e.on('fortytwo', common.mustNotCall()); + assert.ok(!Object.hasOwn(e._events.fortytwo, 'warned')); + e.on('fortytwo', common.mustNotCall()); + assert.ok(Object.hasOwn(e._events.fortytwo, 'warned')); +} + +// But _maxListeners still has precedence over defaultMaxListeners +{ + events.EventEmitter.defaultMaxListeners = 42; + const e = new events.EventEmitter(); + e.setMaxListeners(1); + e.on('uno', common.mustNotCall()); + assert.ok(!Object.hasOwn(e._events.uno, 'warned')); + e.on('uno', common.mustNotCall()); + assert.ok(Object.hasOwn(e._events.uno, 'warned')); + + // chainable + assert.strictEqual(e, e.setMaxListeners(1)); +} diff --git a/test/js/node/test/parallel/test-event-emitter-emit-context.js b/test/js/node/test/parallel/test-event-emitter-emit-context.js new file mode 100644 index 0000000000..e4c73cadc5 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-emit-context.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +// Test emit called by other context +const EE = new EventEmitter(); + +// Works as expected if the context has no `constructor.name` +{ + const ctx = { __proto__: null }; + assert.throws( + () => EE.emit.call(ctx, 'error', new Error('foo')), + common.expectsError({ name: 'Error', message: 'foo' }) + ); +} + +assert.strictEqual(EE.emit.call({}, 'foo'), false); diff --git a/test/js/node/test/parallel/test-event-emitter-error-monitor.js b/test/js/node/test/parallel/test-event-emitter-error-monitor.js new file mode 100644 index 0000000000..8254fc6254 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-error-monitor.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +const EE = new EventEmitter(); +const theErr = new Error('MyError'); + +EE.on( + EventEmitter.errorMonitor, + common.mustCall(function onErrorMonitor(e) { + assert.strictEqual(e, theErr); + }, 3) +); + +// Verify with no error listener +assert.throws( + () => EE.emit('error', theErr), theErr +); + +// Verify with error listener +EE.once('error', common.mustCall((e) => assert.strictEqual(e, theErr))); +EE.emit('error', theErr); + + +// Verify it works with once +process.nextTick(() => EE.emit('error', theErr)); +assert.rejects(EventEmitter.once(EE, 'notTriggered'), theErr).then(common.mustCall()); + +// Only error events trigger error monitor +EE.on('aEvent', common.mustCall()); +EE.emit('aEvent'); diff --git a/test/js/node/test/parallel/test-event-emitter-errors.js b/test/js/node/test/parallel/test-event-emitter-errors.js new file mode 100644 index 0000000000..f22fc3bd58 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-errors.js @@ -0,0 +1,37 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); +const util = require('util'); + +const EE = new EventEmitter(); + +assert.throws( + () => EE.emit('error', 'Accepts a string'), + { + code: 'ERR_UNHANDLED_ERROR', + name: 'Error', + message: "Unhandled error. ('Accepts a string')", + } +); + +assert.throws( + () => EE.emit('error', { message: 'Error!' }), + { + code: 'ERR_UNHANDLED_ERROR', + name: 'Error', + message: "Unhandled error. ({ message: 'Error!' })", + } +); + +assert.throws( + () => EE.emit('error', { + message: 'Error!', + [util.inspect.custom]() { throw new Error(); }, + }), + { + code: 'ERR_UNHANDLED_ERROR', + name: 'Error', + message: 'Unhandled error. ([object Object])', + } +); diff --git a/test/js/node/test/parallel/test-event-emitter-get-max-listeners.js b/test/js/node/test/parallel/test-event-emitter-get-max-listeners.js new file mode 100644 index 0000000000..43f9f0cebc --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-get-max-listeners.js @@ -0,0 +1,19 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +const emitter = new EventEmitter(); + +assert.strictEqual(emitter.getMaxListeners(), EventEmitter.defaultMaxListeners); + +emitter.setMaxListeners(0); +assert.strictEqual(emitter.getMaxListeners(), 0); + +emitter.setMaxListeners(3); +assert.strictEqual(emitter.getMaxListeners(), 3); + +// https://github.com/nodejs/node/issues/523 - second call should not throw. +const recv = {}; +EventEmitter.prototype.on.call(recv, 'event', () => {}); +EventEmitter.prototype.on.call(recv, 'event', () => {}); diff --git a/test/js/node/test/parallel/test-event-emitter-invalid-listener.js b/test/js/node/test/parallel/test-event-emitter-invalid-listener.js new file mode 100644 index 0000000000..f05766c72e --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-invalid-listener.js @@ -0,0 +1,20 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +const eventsMethods = ['on', 'once', 'removeListener', 'prependOnceListener']; + +// Verify that the listener must be a function for events methods +for (const method of eventsMethods) { + assert.throws(() => { + const ee = new EventEmitter(); + ee[method]('foo', null); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "listener" argument must be of type function. ' + + 'Received null', + }); +} diff --git a/test/js/node/test/parallel/test-event-emitter-listener-count.js b/test/js/node/test/parallel/test-event-emitter-listener-count.js new file mode 100644 index 0000000000..117d38f575 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-listener-count.js @@ -0,0 +1,18 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +const emitter = new EventEmitter(); +emitter.on('foo', () => {}); +emitter.on('foo', () => {}); +emitter.on('baz', () => {}); +// Allow any type +emitter.on(123, () => {}); + +assert.strictEqual(EventEmitter.listenerCount(emitter, 'foo'), 2); +assert.strictEqual(emitter.listenerCount('foo'), 2); +assert.strictEqual(emitter.listenerCount('bar'), 0); +assert.strictEqual(emitter.listenerCount('baz'), 1); +assert.strictEqual(emitter.listenerCount(123), 1); diff --git a/test/js/node/test/parallel/test-event-emitter-listeners-side-effects.js b/test/js/node/test/parallel/test-event-emitter-listeners-side-effects.js new file mode 100644 index 0000000000..f1a9e659e0 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-listeners-side-effects.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +require('../common'); +const assert = require('assert'); + +const EventEmitter = require('events').EventEmitter; + +const e = new EventEmitter(); +let fl; // foo listeners + +fl = e.listeners('foo'); +assert(Array.isArray(fl)); +assert.strictEqual(fl.length, 0); +assert(!(e._events instanceof Object)); +assert.deepStrictEqual(Object.keys(e._events), []); + +e.on('foo', assert.fail); +fl = e.listeners('foo'); +assert.deepEqual(e._events.foo, [assert.fail]); +assert(Array.isArray(fl)); +assert.strictEqual(fl.length, 1); +assert.strictEqual(fl[0], assert.fail); + +e.listeners('bar'); + +e.on('foo', assert.ok); +fl = e.listeners('foo'); + +assert(Array.isArray(e._events.foo)); +assert.strictEqual(e._events.foo.length, 2); +assert.strictEqual(e._events.foo[0], assert.fail); +assert.strictEqual(e._events.foo[1], assert.ok); + +assert(Array.isArray(fl)); +assert.strictEqual(fl.length, 2); +assert.strictEqual(fl[0], assert.fail); +assert.strictEqual(fl[1], assert.ok); + +console.log('ok'); diff --git a/test/js/node/test/parallel/test-event-emitter-listeners.js b/test/js/node/test/parallel/test-event-emitter-listeners.js new file mode 100644 index 0000000000..eb1da829c9 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-listeners.js @@ -0,0 +1,124 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +require('../common'); +const assert = require('assert'); +const events = require('events'); + +function listener() {} + +function listener2() {} + +function listener3() { + return 0; +} + +function listener4() { + return 1; +} + +{ + const ee = new events.EventEmitter(); + ee.on('foo', listener); + const fooListeners = ee.listeners('foo'); + assert.deepStrictEqual(ee.listeners('foo'), [listener]); + ee.removeAllListeners('foo'); + assert.deepStrictEqual(ee.listeners('foo'), []); + assert.deepStrictEqual(fooListeners, [listener]); +} + +{ + const ee = new events.EventEmitter(); + ee.on('foo', listener); + const eeListenersCopy = ee.listeners('foo'); + assert.deepStrictEqual(eeListenersCopy, [listener]); + assert.deepStrictEqual(ee.listeners('foo'), [listener]); + eeListenersCopy.push(listener2); + assert.deepStrictEqual(ee.listeners('foo'), [listener]); + assert.deepStrictEqual(eeListenersCopy, [listener, listener2]); +} + +{ + const ee = new events.EventEmitter(); + ee.on('foo', listener); + const eeListenersCopy = ee.listeners('foo'); + ee.on('foo', listener2); + assert.deepStrictEqual(ee.listeners('foo'), [listener, listener2]); + assert.deepStrictEqual(eeListenersCopy, [listener]); +} + +{ + const ee = new events.EventEmitter(); + ee.once('foo', listener); + assert.deepStrictEqual(ee.listeners('foo'), [listener]); +} + +{ + const ee = new events.EventEmitter(); + ee.on('foo', listener); + ee.once('foo', listener2); + assert.deepStrictEqual(ee.listeners('foo'), [listener, listener2]); +} + +{ + const ee = new events.EventEmitter(); + ee._events = undefined; + assert.deepStrictEqual(ee.listeners('foo'), []); +} + +{ + class TestStream extends events.EventEmitter {} + const s = new TestStream(); + assert.deepStrictEqual(s.listeners('foo'), []); +} + +{ + const ee = new events.EventEmitter(); + ee.on('foo', listener); + const wrappedListener = ee.rawListeners('foo'); + assert.strictEqual(wrappedListener.length, 1); + assert.strictEqual(wrappedListener[0], listener); + assert.notStrictEqual(wrappedListener, ee.rawListeners('foo')); + ee.once('foo', listener); + const wrappedListeners = ee.rawListeners('foo'); + assert.strictEqual(wrappedListeners.length, 2); + assert.strictEqual(wrappedListeners[0], listener); + assert.notStrictEqual(wrappedListeners[1], listener); + assert.strictEqual(wrappedListeners[1].listener, listener); + assert.notStrictEqual(wrappedListeners, ee.rawListeners('foo')); + ee.emit('foo'); + assert.strictEqual(wrappedListeners.length, 2); + assert.strictEqual(wrappedListeners[1].listener, listener); +} + +{ + const ee = new events.EventEmitter(); + ee.once('foo', listener3); + ee.on('foo', listener4); + const rawListeners = ee.rawListeners('foo'); + assert.strictEqual(rawListeners.length, 2); + assert.strictEqual(rawListeners[0](), 0); + const rawListener = ee.rawListeners('foo'); + assert.strictEqual(rawListener.length, 1); + assert.strictEqual(rawListener[0](), 1); +} diff --git a/test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-null.js b/test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-null.js new file mode 100644 index 0000000000..673b42336e --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-null.js @@ -0,0 +1,22 @@ +// Flags: --no-warnings +// The flag suppresses stderr output but the warning event will still emit +'use strict'; + +const common = require('../common'); +const events = require('events'); +const assert = require('assert'); + +const e = new events.EventEmitter(); +e.setMaxListeners(1); + +process.on('warning', common.mustCall((warning) => { + assert.ok(warning instanceof Error); + assert.strictEqual(warning.name, 'MaxListenersExceededWarning'); + assert.strictEqual(warning.emitter, e); + assert.strictEqual(warning.count, 2); + assert.strictEqual(warning.type, null); + assert.ok(warning.message.includes('2 null listeners added to [EventEmitter]. MaxListeners is 1.')); +})); + +e.on(null, () => {}); +e.on(null, () => {}); diff --git a/test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-symbol.js b/test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-symbol.js new file mode 100644 index 0000000000..e654b7697c --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-symbol.js @@ -0,0 +1,24 @@ +// Flags: --no-warnings +// The flag suppresses stderr output but the warning event will still emit +'use strict'; + +const common = require('../common'); +const events = require('events'); +const assert = require('assert'); + +const symbol = Symbol('symbol'); + +const e = new events.EventEmitter(); +e.setMaxListeners(1); + +process.on('warning', common.mustCall((warning) => { + assert.ok(warning instanceof Error); + assert.strictEqual(warning.name, 'MaxListenersExceededWarning'); + assert.strictEqual(warning.emitter, e); + assert.strictEqual(warning.count, 2); + assert.strictEqual(warning.type, symbol); + assert.ok(warning.message.includes('2 Symbol(symbol) listeners added to [EventEmitter]. MaxListeners is 1.')); +})); + +e.on(symbol, () => {}); +e.on(symbol, () => {}); diff --git a/test/js/node/test/parallel/test-event-emitter-max-listeners-warning.js b/test/js/node/test/parallel/test-event-emitter-max-listeners-warning.js new file mode 100644 index 0000000000..31bd8d0712 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-max-listeners-warning.js @@ -0,0 +1,30 @@ +// Flags: --no-warnings +// The flag suppresses stderr output but the warning event will still emit +'use strict'; + +const common = require('../common'); +const events = require('events'); +const assert = require('assert'); + +class FakeInput extends events.EventEmitter { + resume() {} + pause() {} + write() {} + end() {} +} + +const e = new FakeInput(); +e.setMaxListeners(1); + +process.on('warning', common.mustCall((warning) => { + assert.ok(warning instanceof Error); + assert.strictEqual(warning.name, 'MaxListenersExceededWarning'); + assert.strictEqual(warning.emitter, e); + assert.strictEqual(warning.count, 2); + assert.strictEqual(warning.type, 'event-type'); + assert.ok(warning.message.includes('2 event-type listeners added to [FakeInput]. MaxListeners is 1.')); +})); + +e.on('event-type', () => {}); +e.on('event-type', () => {}); // Trigger warning. +e.on('event-type', () => {}); // Verify that warning is emitted only once. diff --git a/test/js/node/test/parallel/test-event-emitter-max-listeners.js b/test/js/node/test/parallel/test-event-emitter-max-listeners.js new file mode 100644 index 0000000000..9b9c2ad0d5 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-max-listeners.js @@ -0,0 +1,88 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const events = require('events'); +const e = new events.EventEmitter(); + +e.on('maxListeners', common.mustCall()); + +// Should not corrupt the 'maxListeners' queue. +e.setMaxListeners(42); + +const rangeErrorObjs = [NaN, -1]; +const typeErrorObj = 'and even this'; + +for (const obj of rangeErrorObjs) { + assert.throws( + () => e.setMaxListeners(obj), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } + ); + + assert.throws( + () => events.defaultMaxListeners = obj, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } + ); +} + +assert.throws( + () => e.setMaxListeners(typeErrorObj), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } +); + +assert.throws( + () => events.defaultMaxListeners = typeErrorObj, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } +); + +e.emit('maxListeners'); + +{ + const { EventEmitter, defaultMaxListeners } = events; + for (const obj of rangeErrorObjs) { + assert.throws(() => EventEmitter.setMaxListeners(obj), { + code: 'ERR_OUT_OF_RANGE', + }); + } + + assert.throws(() => EventEmitter.setMaxListeners(typeErrorObj), { + code: 'ERR_INVALID_ARG_TYPE', + }); + + assert.throws( + () => EventEmitter.setMaxListeners(defaultMaxListeners, 'INVALID_EMITTER'), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +} diff --git a/test/js/node/test/parallel/test-event-emitter-method-names.js b/test/js/node/test/parallel/test-event-emitter-method-names.js new file mode 100644 index 0000000000..684024d027 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-method-names.js @@ -0,0 +1,35 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const events = require('events'); + +const E = events.EventEmitter.prototype; +assert.strictEqual(E.constructor.name, 'EventEmitter'); +assert.strictEqual(E.on, E.addListener); // Same method. +assert.strictEqual(E.off, E.removeListener); // Same method. +Object.getOwnPropertyNames(E).forEach(function(name) { + if (name === 'constructor' || name === 'on' || name === 'off') return; + if (typeof E[name] !== 'function') return; + assert.strictEqual(E[name].name, name); +}); diff --git a/test/js/node/test/parallel/test-event-emitter-modify-in-emit.js b/test/js/node/test/parallel/test-event-emitter-modify-in-emit.js new file mode 100644 index 0000000000..995fa01d11 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-modify-in-emit.js @@ -0,0 +1,80 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const events = require('events'); + +let callbacks_called = []; + +const e = new events.EventEmitter(); + +function callback1() { + callbacks_called.push('callback1'); + e.on('foo', callback2); + e.on('foo', callback3); + e.removeListener('foo', callback1); +} + +function callback2() { + callbacks_called.push('callback2'); + e.removeListener('foo', callback2); +} + +function callback3() { + callbacks_called.push('callback3'); + e.removeListener('foo', callback3); +} + +e.on('foo', callback1); +assert.strictEqual(e.listeners('foo').length, 1); + +e.emit('foo'); +assert.strictEqual(e.listeners('foo').length, 2); +assert.deepStrictEqual(['callback1'], callbacks_called); + +e.emit('foo'); +assert.strictEqual(e.listeners('foo').length, 0); +assert.deepStrictEqual(['callback1', 'callback2', 'callback3'], + callbacks_called); + +e.emit('foo'); +assert.strictEqual(e.listeners('foo').length, 0); +assert.deepStrictEqual(['callback1', 'callback2', 'callback3'], + callbacks_called); + +e.on('foo', callback1); +e.on('foo', callback2); +assert.strictEqual(e.listeners('foo').length, 2); +e.removeAllListeners('foo'); +assert.strictEqual(e.listeners('foo').length, 0); + +// Verify that removing callbacks while in emit allows emits to propagate to +// all listeners +callbacks_called = []; + +e.on('foo', callback2); +e.on('foo', callback3); +assert.strictEqual(e.listeners('foo').length, 2); +e.emit('foo'); +assert.deepStrictEqual(['callback2', 'callback3'], callbacks_called); +assert.strictEqual(e.listeners('foo').length, 0); diff --git a/test/js/node/test/parallel/test-event-emitter-no-error-provided-to-error-event.js b/test/js/node/test/parallel/test-event-emitter-no-error-provided-to-error-event.js new file mode 100644 index 0000000000..5c30b54533 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-no-error-provided-to-error-event.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const events = require('events'); +const domain = require('domain'); + +{ + const e = new events.EventEmitter(); + const d = domain.create(); + d.add(e); + d.on('error', common.mustCall((er) => { + assert(er instanceof Error, 'error created'); + })); + e.emit('error'); +} + +for (const arg of [false, null, undefined]) { + const e = new events.EventEmitter(); + const d = domain.create(); + d.add(e); + d.on('error', common.mustCall((er) => { + assert(er instanceof Error, 'error created'); + })); + e.emit('error', arg); +} + +for (const arg of [42, 'fortytwo', true]) { + const e = new events.EventEmitter(); + const d = domain.create(); + d.add(e); + d.on('error', common.mustCall((er) => { + assert.strictEqual(er, arg); + })); + e.emit('error', arg); +} diff --git a/test/js/node/test/parallel/test-event-emitter-num-args.js b/test/js/node/test/parallel/test-event-emitter-num-args.js new file mode 100644 index 0000000000..8bb2274303 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-num-args.js @@ -0,0 +1,54 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const events = require('events'); + +const e = new events.EventEmitter(); +const num_args_emitted = []; + +e.on('numArgs', function() { + const numArgs = arguments.length; + num_args_emitted.push(numArgs); +}); + +e.on('foo', function() { + num_args_emitted.push(arguments.length); +}); + +e.on('foo', function() { + num_args_emitted.push(arguments.length); +}); + +e.emit('numArgs'); +e.emit('numArgs', null); +e.emit('numArgs', null, null); +e.emit('numArgs', null, null, null); +e.emit('numArgs', null, null, null, null); +e.emit('numArgs', null, null, null, null, null); + +e.emit('foo', null, null, null, null); + +process.on('exit', function() { + assert.deepStrictEqual(num_args_emitted, [0, 1, 2, 3, 4, 5, 4, 4]); +}); diff --git a/test/js/node/test/parallel/test-event-emitter-once.js b/test/js/node/test/parallel/test-event-emitter-once.js new file mode 100644 index 0000000000..983f6141b9 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-once.js @@ -0,0 +1,70 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +const e = new EventEmitter(); + +e.once('hello', common.mustCall()); + +e.emit('hello', 'a', 'b'); +e.emit('hello', 'a', 'b'); +e.emit('hello', 'a', 'b'); +e.emit('hello', 'a', 'b'); + +function remove() { + assert.fail('once->foo should not be emitted'); +} + +e.once('foo', remove); +e.removeListener('foo', remove); +e.emit('foo'); + +e.once('e', common.mustCall(function() { + e.emit('e'); +})); + +e.once('e', common.mustCall()); + +e.emit('e'); + +{ + // once() has different code paths based on the number of arguments being + // emitted. Verify that all of the cases are covered. + const maxArgs = 4; + + for (let i = 0; i <= maxArgs; ++i) { + const ee = new EventEmitter(); + const args = ['foo']; + + for (let j = 0; j < i; ++j) + args.push(j); + + ee.once('foo', common.mustCall((...params) => { + assert.deepStrictEqual(params, args.slice(1)); + })); + + EventEmitter.prototype.emit.apply(ee, args); + } +} diff --git a/test/js/node/test/parallel/test-event-emitter-prepend.js b/test/js/node/test/parallel/test-event-emitter-prepend.js new file mode 100644 index 0000000000..ffe8544911 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-prepend.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const EventEmitter = require('events'); +const assert = require('assert'); + +const myEE = new EventEmitter(); +let m = 0; +// This one comes last. +myEE.on('foo', common.mustCall(() => assert.strictEqual(m, 2))); + +// This one comes second. +myEE.prependListener('foo', common.mustCall(() => assert.strictEqual(m++, 1))); + +// This one comes first. +myEE.prependOnceListener('foo', + common.mustCall(() => assert.strictEqual(m++, 0))); + +myEE.emit('foo'); + +// Test fallback if prependListener is undefined. +const stream = require('stream'); + +delete EventEmitter.prototype.prependListener; + +function Writable() { + this.writable = true; + stream.Stream.call(this); +} +Object.setPrototypeOf(Writable.prototype, stream.Stream.prototype); +Object.setPrototypeOf(Writable, stream.Stream); + +function Readable() { + this.readable = true; + stream.Stream.call(this); +} +Object.setPrototypeOf(Readable.prototype, stream.Stream.prototype); +Object.setPrototypeOf(Readable, stream.Stream); + +const w = new Writable(); +const r = new Readable(); +r.pipe(w); diff --git a/test/js/node/test/parallel/test-event-emitter-remove-all-listeners.js b/test/js/node/test/parallel/test-event-emitter-remove-all-listeners.js new file mode 100644 index 0000000000..c62183fd08 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-remove-all-listeners.js @@ -0,0 +1,123 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const events = require('events'); + + +function expect(expected) { + const actual = []; + process.on('exit', function() { + assert.deepStrictEqual(actual.sort(), expected.sort()); + }); + function listener(name) { + actual.push(name); + } + return common.mustCall(listener, expected.length); +} + +{ + const ee = new events.EventEmitter(); + const noop = common.mustNotCall(); + ee.on('foo', noop); + ee.on('bar', noop); + ee.on('baz', noop); + ee.on('baz', noop); + const fooListeners = ee.listeners('foo'); + const barListeners = ee.listeners('bar'); + const bazListeners = ee.listeners('baz'); + ee.on('removeListener', expect(['bar', 'baz', 'baz'])); + ee.removeAllListeners('bar'); + ee.removeAllListeners('baz'); + assert.deepStrictEqual(ee.listeners('foo'), [noop]); + assert.deepStrictEqual(ee.listeners('bar'), []); + assert.deepStrictEqual(ee.listeners('baz'), []); + // After calling removeAllListeners(), + // the old listeners array should stay unchanged. + assert.deepStrictEqual(fooListeners, [noop]); + assert.deepStrictEqual(barListeners, [noop]); + assert.deepStrictEqual(bazListeners, [noop, noop]); + // After calling removeAllListeners(), + // new listeners arrays is different from the old. + assert.notStrictEqual(ee.listeners('bar'), barListeners); + assert.notStrictEqual(ee.listeners('baz'), bazListeners); +} + +{ + const ee = new events.EventEmitter(); + ee.on('foo', common.mustNotCall()); + ee.on('bar', common.mustNotCall()); + // Expect LIFO order + ee.on('removeListener', expect(['foo', 'bar', 'removeListener'])); + ee.on('removeListener', expect(['foo', 'bar'])); + ee.removeAllListeners(); + assert.deepStrictEqual([], ee.listeners('foo')); + assert.deepStrictEqual([], ee.listeners('bar')); +} + +{ + const ee = new events.EventEmitter(); + ee.on('removeListener', common.mustNotCall()); + // Check for regression where removeAllListeners() throws when + // there exists a 'removeListener' listener, but there exists + // no listeners for the provided event type. + ee.removeAllListeners.bind(ee, 'foo'); +} + +{ + const ee = new events.EventEmitter(); + let expectLength = 2; + ee.on('removeListener', function(name, noop) { + assert.strictEqual(expectLength--, this.listeners('baz').length); + }); + ee.on('baz', common.mustNotCall()); + ee.on('baz', common.mustNotCall()); + ee.on('baz', common.mustNotCall()); + assert.strictEqual(ee.listeners('baz').length, expectLength + 1); + ee.removeAllListeners('baz'); + assert.strictEqual(ee.listeners('baz').length, 0); +} + +{ + const ee = new events.EventEmitter(); + assert.deepStrictEqual(ee, ee.removeAllListeners()); +} + +{ + const ee = new events.EventEmitter(); + ee._events = undefined; + assert.strictEqual(ee, ee.removeAllListeners()); +} + +{ + const ee = new events.EventEmitter(); + const symbol = Symbol('symbol'); + const noop = common.mustNotCall(); + ee.on(symbol, noop); + + ee.on('removeListener', common.mustCall((...args) => { + assert.deepStrictEqual(args, [symbol, noop]); + })); + + ee.removeAllListeners(); +} diff --git a/test/js/node/test/parallel/test-event-emitter-remove-listeners.js b/test/js/node/test/parallel/test-event-emitter-remove-listeners.js new file mode 100644 index 0000000000..5ab52b8320 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-remove-listeners.js @@ -0,0 +1,170 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); + +function listener1() {} + +function listener2() {} + +{ + const ee = new EventEmitter(); + ee.on('hello', listener1); + ee.on('removeListener', common.mustCall((name, cb) => { + assert.strictEqual(name, 'hello'); + assert.strictEqual(cb, listener1); + })); + ee.removeListener('hello', listener1); + assert.deepStrictEqual([], ee.listeners('hello')); +} + +{ + const ee = new EventEmitter(); + ee.on('hello', listener1); + ee.on('removeListener', common.mustNotCall()); + ee.removeListener('hello', listener2); + assert.deepStrictEqual([listener1], ee.listeners('hello')); +} + +{ + const ee = new EventEmitter(); + ee.on('hello', listener1); + ee.on('hello', listener2); + ee.once('removeListener', common.mustCall((name, cb) => { + assert.strictEqual(name, 'hello'); + assert.strictEqual(cb, listener1); + assert.deepStrictEqual([listener2], ee.listeners('hello')); + })); + ee.removeListener('hello', listener1); + assert.deepStrictEqual([listener2], ee.listeners('hello')); + ee.once('removeListener', common.mustCall((name, cb) => { + assert.strictEqual(name, 'hello'); + assert.strictEqual(cb, listener2); + assert.deepStrictEqual([], ee.listeners('hello')); + })); + ee.removeListener('hello', listener2); + assert.deepStrictEqual([], ee.listeners('hello')); +} + +{ + const ee = new EventEmitter(); + + function remove1() { + assert.fail('remove1 should not have been called'); + } + + function remove2() { + assert.fail('remove2 should not have been called'); + } + + ee.on('removeListener', common.mustCall(function(name, cb) { + if (cb !== remove1) return; + this.removeListener('quux', remove2); + this.emit('quux'); + }, 2)); + ee.on('quux', remove1); + ee.on('quux', remove2); + ee.removeListener('quux', remove1); +} + +{ + const ee = new EventEmitter(); + ee.on('hello', listener1); + ee.on('hello', listener2); + ee.once('removeListener', common.mustCall((name, cb) => { + assert.strictEqual(name, 'hello'); + assert.strictEqual(cb, listener1); + assert.deepStrictEqual([listener2], ee.listeners('hello')); + ee.once('removeListener', common.mustCall((name, cb) => { + assert.strictEqual(name, 'hello'); + assert.strictEqual(cb, listener2); + assert.deepStrictEqual([], ee.listeners('hello')); + })); + ee.removeListener('hello', listener2); + assert.deepStrictEqual([], ee.listeners('hello')); + })); + ee.removeListener('hello', listener1); + assert.deepStrictEqual([], ee.listeners('hello')); +} + +{ + const ee = new EventEmitter(); + const listener3 = common.mustCall(() => { + ee.removeListener('hello', listener4); + }, 2); + const listener4 = common.mustCall(); + + ee.on('hello', listener3); + ee.on('hello', listener4); + + // listener4 will still be called although it is removed by listener 3. + ee.emit('hello'); + // This is so because the internal listener array at time of emit + // was [listener3,listener4] + + // Internal listener array [listener3] + ee.emit('hello'); +} + +{ + const ee = new EventEmitter(); + + ee.once('hello', listener1); + ee.on('removeListener', common.mustCall((eventName, listener) => { + assert.strictEqual(eventName, 'hello'); + assert.strictEqual(listener, listener1); + })); + ee.emit('hello'); +} + +{ + const ee = new EventEmitter(); + + assert.deepStrictEqual(ee, ee.removeListener('foo', () => {})); +} + +{ + const ee = new EventEmitter(); + const listener = () => {}; + ee._events = undefined; + const e = ee.removeListener('foo', listener); + assert.strictEqual(e, ee); +} + +{ + const ee = new EventEmitter(); + + ee.on('foo', listener1); + ee.on('foo', listener2); + assert.deepStrictEqual(ee.listeners('foo'), [listener1, listener2]); + + ee.removeListener('foo', listener1); + assert.deepEqual(ee._events.foo, [listener2]); + + ee.on('foo', listener1); + assert.deepStrictEqual(ee.listeners('foo'), [listener2, listener1]); + + ee.removeListener('foo', listener1); + assert.deepEqual(ee._events.foo, [listener2]); +} diff --git a/test/js/node/test/parallel/test-event-emitter-set-max-listeners-side-effects.js b/test/js/node/test/parallel/test-event-emitter-set-max-listeners-side-effects.js new file mode 100644 index 0000000000..8e66e999a5 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-set-max-listeners-side-effects.js @@ -0,0 +1,32 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const events = require('events'); + +const e = new events.EventEmitter(); + +assert(!(e._events instanceof Object)); +assert.deepStrictEqual(Object.keys(e._events), []); +e.setMaxListeners(5); +assert.deepStrictEqual(Object.keys(e._events), []); diff --git a/test/js/node/test/parallel/test-event-emitter-special-event-names.js b/test/js/node/test/parallel/test-event-emitter-special-event-names.js new file mode 100644 index 0000000000..f34faba946 --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-special-event-names.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const EventEmitter = require('events'); +const assert = require('assert'); + +const ee = new EventEmitter(); +const handler = () => {}; + +assert.deepStrictEqual(ee.eventNames(), []); + +assert.strictEqual(ee._events.hasOwnProperty, undefined); +assert.strictEqual(ee._events.toString, undefined); + +ee.on('__proto__', handler); +ee.on('__defineGetter__', handler); +ee.on('toString', handler); + +assert.deepStrictEqual(ee.eventNames(), [ + '__proto__', + '__defineGetter__', + 'toString', +]); + +assert.deepStrictEqual(ee.listeners('__proto__'), [handler]); +assert.deepStrictEqual(ee.listeners('__defineGetter__'), [handler]); +assert.deepStrictEqual(ee.listeners('toString'), [handler]); + +ee.on('__proto__', common.mustCall(function(val) { + assert.strictEqual(val, 1); +})); +ee.emit('__proto__', 1); + +process.on('__proto__', common.mustCall(function(val) { + assert.strictEqual(val, 1); +})); +process.emit('__proto__', 1); diff --git a/test/js/node/test/parallel/test-event-emitter-subclass.js b/test/js/node/test/parallel/test-event-emitter-subclass.js new file mode 100644 index 0000000000..a6ef54e5fa --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-subclass.js @@ -0,0 +1,67 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const EventEmitter = require('events').EventEmitter; + +Object.setPrototypeOf(MyEE.prototype, EventEmitter.prototype); +Object.setPrototypeOf(MyEE, EventEmitter); + +function MyEE(cb) { + this.once(1, cb); + this.emit(1); + this.removeAllListeners(); + EventEmitter.call(this); +} + +const myee = new MyEE(common.mustCall()); + +Object.setPrototypeOf(ErrorEE.prototype, EventEmitter.prototype); +Object.setPrototypeOf(ErrorEE, EventEmitter); +function ErrorEE() { + this.emit('error', new Error('blerg')); +} + +assert.throws(function() { + new ErrorEE(); +}, /blerg/); + +process.on('exit', function() { + assert(!(myee._events instanceof Object)); + assert.deepStrictEqual(Object.keys(myee._events), []); + console.log('ok'); +}); + + +function MyEE2() { + EventEmitter.call(this); +} + +MyEE2.prototype = new EventEmitter(); + +const ee1 = new MyEE2(); +const ee2 = new MyEE2(); + +ee1.on('x', () => {}); + +assert.strictEqual(ee2.listenerCount('x'), 0); diff --git a/test/js/node/test/parallel/test-event-emitter-symbols.js b/test/js/node/test/parallel/test-event-emitter-symbols.js new file mode 100644 index 0000000000..98d44ff3fa --- /dev/null +++ b/test/js/node/test/parallel/test-event-emitter-symbols.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const EventEmitter = require('events'); +const assert = require('assert'); + +const ee = new EventEmitter(); +const foo = Symbol('foo'); +const listener = common.mustCall(); + +ee.on(foo, listener); +assert.deepStrictEqual(ee.listeners(foo), [listener]); + +ee.emit(foo); + +ee.removeAllListeners(); +assert.deepStrictEqual(ee.listeners(foo), []); + +ee.on(foo, listener); +assert.deepStrictEqual(ee.listeners(foo), [listener]); + +ee.removeListener(foo, listener); +assert.deepStrictEqual(ee.listeners(foo), []); diff --git a/test/js/node/test/parallel/test-event-target.js b/test/js/node/test/parallel/test-event-target.js new file mode 100644 index 0000000000..12246b15ae --- /dev/null +++ b/test/js/node/test/parallel/test-event-target.js @@ -0,0 +1,21 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const eventPhases = { + 'NONE': 0, + 'CAPTURING_PHASE': 1, + 'AT_TARGET': 2, + 'BUBBLING_PHASE': 3 +}; + +for (const [prop, value] of Object.entries(eventPhases)) { + // Check if the value of the property matches the expected value + assert.strictEqual(Event[prop], value, `Expected Event.${prop} to be ${value}, but got ${Event[prop]}`); + + const desc = Object.getOwnPropertyDescriptor(Event, prop); + assert.strictEqual(desc.writable, false, `${prop} should not be writable`); + assert.strictEqual(desc.configurable, false, `${prop} should not be configurable`); + assert.strictEqual(desc.enumerable, true, `${prop} should be enumerable`); +} diff --git a/test/js/node/test/parallel/test-events-customevent.js b/test/js/node/test/parallel/test-events-customevent.js new file mode 100644 index 0000000000..0cf36aa91c --- /dev/null +++ b/test/js/node/test/parallel/test-events-customevent.js @@ -0,0 +1,323 @@ +// Flags: --expose-internals + +'use strict'; + +const common = require('../common'); +const { ok, strictEqual, deepStrictEqual, throws } = require('node:assert'); +const { inspect } = require('node:util'); + +{ + ok(CustomEvent); + + // Default string + const tag = Object.prototype.toString.call(new CustomEvent('$')); + strictEqual(tag, '[object CustomEvent]'); +} + +{ + // No argument behavior - throw TypeError + throws(() => { + new CustomEvent(); + }, TypeError); + + throws(() => new CustomEvent(Symbol()), TypeError); + + // Too many arguments passed behavior - ignore additional arguments + const ev = new CustomEvent('foo', {}, {}); + strictEqual(ev.type, 'foo'); +} + +{ + const ev = new CustomEvent('$'); + strictEqual(ev.type, '$'); + strictEqual(ev.bubbles, false); + strictEqual(ev.cancelable, false); + strictEqual(ev.detail, null); +} + +{ + // Coercion to string works + strictEqual(new CustomEvent(1).type, '1'); + strictEqual(new CustomEvent(false).type, 'false'); + strictEqual(new CustomEvent({}).type, String({})); +} + +{ + const ev = new CustomEvent('$', { + detail: 56, + sweet: 'x', + cancelable: true, + }); + strictEqual(ev.type, '$'); + strictEqual(ev.bubbles, false); + strictEqual(ev.cancelable, true); + strictEqual(ev.sweet, undefined); + strictEqual(ev.detail, 56); +} + +{ + // Any types of value for `detail` are acceptable. + ['foo', 1, false, [], {}].forEach((i) => { + const ev = new CustomEvent('$', { detail: i }); + strictEqual(ev.detail, i); + }); +} + +{ + // Readonly `detail` behavior + const ev = new CustomEvent('$', { + detail: 56, + }); + strictEqual(ev.detail, 56); + try { + ev.detail = 96; + // eslint-disable-next-line no-unused-vars + } catch (error) { + common.mustCall()(); + } + strictEqual(ev.detail, 56); +} + +{ + const ev = new Event('$', { + detail: 96, + }); + strictEqual(ev.detail, undefined); +} + +// The following tests verify whether CustomEvent works the same as Event +// except carrying custom data. They're based on `parallel/test-eventtarget.js`. + +{ + const ev = new CustomEvent('$'); + strictEqual(ev.type, '$'); + strictEqual(ev.bubbles, false); + strictEqual(ev.cancelable, false); + strictEqual(ev.detail, null); + + strictEqual(ev.defaultPrevented, false); + strictEqual(typeof ev.timeStamp, 'number'); + + // Compatibility properties with the DOM + deepStrictEqual(ev.composedPath(), []); + strictEqual(ev.returnValue, true); + strictEqual(ev.composed, false); + strictEqual(ev.isTrusted, false); + strictEqual(ev.eventPhase, 0); + strictEqual(ev.cancelBubble, false); + + // Not cancelable + ev.preventDefault(); + strictEqual(ev.defaultPrevented, false); +} + +{ + // Invalid options + ['foo', 1, false].forEach((i) => + throws(() => new CustomEvent('foo', i), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + }), + ); +} + +{ + const ev = new CustomEvent('$'); + strictEqual(ev.constructor.name, 'CustomEvent'); + + // CustomEvent Statics + strictEqual(CustomEvent.NONE, 0); + strictEqual(CustomEvent.CAPTURING_PHASE, 1); + strictEqual(CustomEvent.AT_TARGET, 2); + strictEqual(CustomEvent.BUBBLING_PHASE, 3); + strictEqual(new CustomEvent('foo').eventPhase, CustomEvent.NONE); + + // CustomEvent is a function + strictEqual(CustomEvent.length, 1); +} + +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = true; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.stopPropagation(); + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = 'some-truthy-value'; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = true; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.stopPropagation(); + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = 'some-truthy-value'; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo', { cancelable: true }); + strictEqual(ev.type, 'foo'); + strictEqual(ev.cancelable, true); + strictEqual(ev.defaultPrevented, false); + + ev.preventDefault(); + strictEqual(ev.defaultPrevented, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.isTrusted, false); +} + +// Works with EventTarget + +{ + const obj = { sweet: 'x', memory: { x: 56, y: 96 } }; + const et = new EventTarget(); + const ev = new CustomEvent('$', { detail: obj }); + const fn = common.mustCall((event) => { + strictEqual(event, ev); + deepStrictEqual(event.detail, obj); + }); + et.addEventListener('$', fn); + et.dispatchEvent(ev); +} + +{ + const eventTarget = new EventTarget(); + const event = new CustomEvent('$'); + eventTarget.dispatchEvent(event); + strictEqual(event.target, eventTarget); +} + +{ + const obj = { sweet: 'x' }; + const eventTarget = new EventTarget(); + + const ev1 = common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(event.detail, obj); + strictEqual(this, eventTarget); + strictEqual(event.eventPhase, 2); + }, 2); + + const ev2 = { + handleEvent: common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(event.detail, obj); + strictEqual(this, ev2); + }), + }; + + eventTarget.addEventListener('foo', ev1); + eventTarget.addEventListener('foo', ev2, { once: true }); + ok(eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj }))); + eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj })); + + eventTarget.removeEventListener('foo', ev1); + eventTarget.dispatchEvent(new CustomEvent('foo')); +} + +{ + // Same event dispatched multiple times. + const obj = { sweet: 'x' }; + const event = new CustomEvent('foo', { detail: obj }); + const eventTarget1 = new EventTarget(); + const eventTarget2 = new EventTarget(); + + eventTarget1.addEventListener( + 'foo', + common.mustCall((event) => { + strictEqual(event.eventPhase, CustomEvent.AT_TARGET); + strictEqual(event.target, eventTarget1); + strictEqual(event.detail, obj); + deepStrictEqual(event.composedPath(), [eventTarget1]); + }), + ); + + eventTarget2.addEventListener( + 'foo', + common.mustCall((event) => { + strictEqual(event.eventPhase, CustomEvent.AT_TARGET); + strictEqual(event.target, eventTarget2); + strictEqual(event.detail, obj); + deepStrictEqual(event.composedPath(), [eventTarget2]); + }), + ); + + eventTarget1.dispatchEvent(event); + strictEqual(event.eventPhase, CustomEvent.NONE); + strictEqual(event.target, eventTarget1); + deepStrictEqual(event.composedPath(), []); + + eventTarget2.dispatchEvent(event); + strictEqual(event.eventPhase, CustomEvent.NONE); + strictEqual(event.target, eventTarget2); + deepStrictEqual(event.composedPath(), []); +} + +{ + const obj = { sweet: 'x' }; + const target = new EventTarget(); + const event = new CustomEvent('foo', { detail: obj }); + + strictEqual(event.target, null); + + target.addEventListener( + 'foo', + common.mustCall((event) => { + strictEqual(event.target, target); + strictEqual(event.currentTarget, target); + strictEqual(event.srcElement, target); + strictEqual(event.detail, obj); + }), + ); + target.dispatchEvent(event); +} + +{ + // Event subclassing + const SubEvent = class extends CustomEvent {}; + const ev = new SubEvent('foo', { detail: 56 }); + const eventTarget = new EventTarget(); + const fn = common.mustCall((event) => { + strictEqual(event, ev); + strictEqual(event.detail, 56); + }); + eventTarget.addEventListener('foo', fn, { once: true }); + eventTarget.dispatchEvent(ev); +} + +// Works with inspect + +{ + const ev = new CustomEvent('test'); + // TODO: unskip + // const evConstructorName = inspect(ev, { + // depth: -1, + // }); + // strictEqual(evConstructorName, 'CustomEvent'); + + const inspectResult = inspect(ev, { + depth: 1, + }); + ok(inspectResult.includes('CustomEvent')); +} diff --git a/test/js/node/test/parallel/test-events-getmaxlisteners.js b/test/js/node/test/parallel/test-events-getmaxlisteners.js new file mode 100644 index 0000000000..05b4e75b72 --- /dev/null +++ b/test/js/node/test/parallel/test-events-getmaxlisteners.js @@ -0,0 +1,19 @@ +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { getMaxListeners, EventEmitter, defaultMaxListeners, setMaxListeners } = require('node:events'); + +{ + const ee = new EventEmitter(); + assert.strictEqual(getMaxListeners(ee), defaultMaxListeners); + setMaxListeners(101, ee); + assert.strictEqual(getMaxListeners(ee), 101); +} + +{ + const et = new EventTarget(); + assert.strictEqual(getMaxListeners(et), defaultMaxListeners); + setMaxListeners(101, et); + assert.strictEqual(getMaxListeners(et), 101); +} diff --git a/test/js/node/test/parallel/test-events-list.js b/test/js/node/test/parallel/test-events-list.js new file mode 100644 index 0000000000..4e589b07f2 --- /dev/null +++ b/test/js/node/test/parallel/test-events-list.js @@ -0,0 +1,19 @@ +'use strict'; + +require('../common'); +const EventEmitter = require('events'); +const assert = require('assert'); + +const EE = new EventEmitter(); +const m = () => {}; +EE.on('foo', () => {}); +assert.deepStrictEqual(['foo'], EE.eventNames()); +EE.on('bar', m); +assert.deepStrictEqual(['foo', 'bar'], EE.eventNames()); +EE.removeListener('bar', m); +assert.deepStrictEqual(['foo'], EE.eventNames()); +const s = Symbol('s'); +EE.on(s, m); +assert.deepStrictEqual(['foo', s], EE.eventNames()); +EE.removeListener(s, m); +assert.deepStrictEqual(['foo'], EE.eventNames()); diff --git a/test/js/node/test/parallel/test-events-listener-count-with-listener.js b/test/js/node/test/parallel/test-events-listener-count-with-listener.js new file mode 100644 index 0000000000..1696cb1c90 --- /dev/null +++ b/test/js/node/test/parallel/test-events-listener-count-with-listener.js @@ -0,0 +1,65 @@ +'use strict'; + +const common = require('../common'); +const EventEmitter = require('events'); +const assert = require('assert'); + +const EE = new EventEmitter(); +const handler = common.mustCall(undefined, 3); +const anotherHandler = common.mustCall(); + +assert.strictEqual(EE.listenerCount('event'), 0); +assert.strictEqual(EE.listenerCount('event', handler), 0); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.once('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.removeAllListeners('event'); + +assert.strictEqual(EE.listenerCount('event'), 0); +assert.strictEqual(EE.listenerCount('event', handler), 0); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.on('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.once('event', anotherHandler); + +assert.strictEqual(EE.listenerCount('event'), 2); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 1); + +assert.strictEqual(EE.listenerCount('another-event'), 0); +assert.strictEqual(EE.listenerCount('another-event', handler), 0); +assert.strictEqual(EE.listenerCount('another-event', anotherHandler), 0); + +EE.once('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 3); +assert.strictEqual(EE.listenerCount('event', handler), 2); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 1); + +EE.emit('event'); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.emit('event'); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.off('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 0); +assert.strictEqual(EE.listenerCount('event', handler), 0); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); diff --git a/test/js/node/test/parallel/test-events-on-async-iterator.js b/test/js/node/test/parallel/test-events-on-async-iterator.js new file mode 100644 index 0000000000..298b1597db --- /dev/null +++ b/test/js/node/test/parallel/test-events-on-async-iterator.js @@ -0,0 +1,427 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { on, EventEmitter, listenerCount } = require('events'); + +async function basic() { + const ee = new EventEmitter(); + process.nextTick(() => { + ee.emit('foo', 'bar'); + // 'bar' is a spurious event, we are testing + // that it does not show up in the iterable + ee.emit('bar', 24); + ee.emit('foo', 42); + }); + + const iterable = on(ee, 'foo'); + + const expected = [['bar'], [42]]; + + for await (const event of iterable) { + const current = expected.shift(); + + assert.deepStrictEqual(current, event); + + if (expected.length === 0) { + break; + } + } + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function invalidArgType() { + assert.throws(() => on({}, 'foo'), common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + })); + + const ee = new EventEmitter(); + + [1, 'hi', null, false, () => {}, Symbol(), 1n].map((options) => { + return assert.throws(() => on(ee, 'foo', options), common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + })); + }); +} + +async function error() { + const ee = new EventEmitter(); + const _err = new Error('kaboom'); + process.nextTick(() => { + ee.emit('error', _err); + }); + + const iterable = on(ee, 'foo'); + let looped = false; + let thrown = false; + + try { + // eslint-disable-next-line no-unused-vars + for await (const event of iterable) { + looped = true; + } + } catch (err) { + thrown = true; + assert.strictEqual(err, _err); + } + assert.strictEqual(thrown, true); + assert.strictEqual(looped, false); +} + +async function errorDelayed() { + const ee = new EventEmitter(); + const _err = new Error('kaboom'); + process.nextTick(() => { + ee.emit('foo', 42); + ee.emit('error', _err); + }); + + const iterable = on(ee, 'foo'); + const expected = [[42]]; + let thrown = false; + + try { + for await (const event of iterable) { + const current = expected.shift(); + assert.deepStrictEqual(current, event); + } + } catch (err) { + thrown = true; + assert.strictEqual(err, _err); + } + assert.strictEqual(thrown, true); + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function throwInLoop() { + const ee = new EventEmitter(); + const _err = new Error('kaboom'); + + process.nextTick(() => { + ee.emit('foo', 42); + }); + + try { + for await (const event of on(ee, 'foo')) { + assert.deepStrictEqual(event, [42]); + throw _err; + } + } catch (err) { + assert.strictEqual(err, _err); + } + + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function next() { + const ee = new EventEmitter(); + const iterable = on(ee, 'foo'); + + process.nextTick(function() { + ee.emit('foo', 'bar'); + ee.emit('foo', 42); + iterable.return(); + }); + + const results = await Promise.all([ + iterable.next(), + iterable.next(), + iterable.next(), + ]); + + assert.deepStrictEqual(results, [{ + value: ['bar'], + done: false, + }, { + value: [42], + done: false, + }, { + value: undefined, + done: true, + }]); + + assert.deepStrictEqual(await iterable.next(), { + value: undefined, + done: true, + }); +} + +async function nextError() { + const ee = new EventEmitter(); + const iterable = on(ee, 'foo'); + const _err = new Error('kaboom'); + process.nextTick(function() { + ee.emit('error', _err); + }); + const results = await Promise.allSettled([ + iterable.next(), + iterable.next(), + iterable.next(), + ]); + assert.deepStrictEqual(results, [{ + status: 'rejected', + reason: _err, + }, { + status: 'fulfilled', + value: { + value: undefined, + done: true, + }, + }, { + status: 'fulfilled', + value: { + value: undefined, + done: true, + }, + }]); + assert.strictEqual(ee.listeners('error').length, 0); +} + +async function iterableThrow() { + const ee = new EventEmitter(); + const iterable = on(ee, 'foo'); + + process.nextTick(() => { + ee.emit('foo', 'bar'); + ee.emit('foo', 42); // lost in the queue + iterable.throw(_err); + }); + + const _err = new Error('kaboom'); + let thrown = false; + + assert.throws(() => { + // No argument + iterable.throw(); + }, { + name: 'TypeError', + }); + + const expected = [['bar'], [42]]; + + try { + for await (const event of iterable) { + assert.deepStrictEqual(event, expected.shift()); + } + } catch (err) { + thrown = true; + assert.strictEqual(err, _err); + } + assert.strictEqual(thrown, true); + assert.strictEqual(expected.length, 0); + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function eventTarget() { + const et = new EventTarget(); + const tick = () => et.dispatchEvent(new Event('tick')); + const interval = setInterval(tick, 0); + let count = 0; + for await (const [ event ] of on(et, 'tick')) { + count++; + assert.strictEqual(event.type, 'tick'); + if (count >= 5) { + break; + } + } + assert.strictEqual(count, 5); + clearInterval(interval); +} + +async function errorListenerCount() { + const et = new EventEmitter(); + on(et, 'foo'); + assert.strictEqual(et.listenerCount('error'), 1); +} + +// async function nodeEventTarget() { +// const et = new NodeEventTarget(); +// const tick = () => et.dispatchEvent(new Event('tick')); +// const interval = setInterval(tick, 0); +// let count = 0; +// for await (const [ event] of on(et, 'tick')) { +// count++; +// assert.strictEqual(event.type, 'tick'); +// if (count >= 5) { +// break; +// } +// } +// assert.strictEqual(count, 5); +// clearInterval(interval); +// } + +async function abortableOnBefore() { + const ee = new EventEmitter(); + const abortedSignal = AbortSignal.abort(); + [1, {}, null, false, 'hi'].forEach((signal) => { + assert.throws(() => on(ee, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + assert.throws(() => on(ee, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +async function eventTargetAbortableOnBefore() { + const et = new EventTarget(); + const abortedSignal = AbortSignal.abort(); + [1, {}, null, false, 'hi'].forEach((signal) => { + assert.throws(() => on(et, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + assert.throws(() => on(et, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +async function abortableOnAfter() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + const i = setInterval(() => ee.emit('foo', 'foo'), 10); + + async function foo() { + for await (const f of on(ee, 'foo', { signal: ac.signal })) { + assert.strictEqual(f, 'foo'); + } + } + + foo().catch(common.mustCall((error) => { + assert.strictEqual(error.name, 'AbortError'); + })).finally(() => { + clearInterval(i); + }); + + process.nextTick(() => ac.abort()); +} + +async function eventTargetAbortableOnAfter() { + const et = new EventTarget(); + const ac = new AbortController(); + + const i = setInterval(() => et.dispatchEvent(new Event('foo')), 10); + + async function foo() { + for await (const f of on(et, 'foo', { signal: ac.signal })) { + assert(f); + } + } + + foo().catch(common.mustCall((error) => { + assert.strictEqual(error.name, 'AbortError'); + })).finally(() => { + clearInterval(i); + }); + + process.nextTick(() => ac.abort()); +} + +async function eventTargetAbortableOnAfter2() { + const et = new EventTarget(); + const ac = new AbortController(); + + const i = setInterval(() => et.dispatchEvent(new Event('foo')), 10); + + async function foo() { + for await (const f of on(et, 'foo', { signal: ac.signal })) { + assert(f); + // Cancel after a single event has been triggered. + ac.abort(); + } + } + + foo().catch(common.mustCall((error) => { + assert.strictEqual(error.name, 'AbortError'); + })).finally(() => { + clearInterval(i); + }); +} + +async function abortableOnAfterDone() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + const i = setInterval(() => ee.emit('foo', 'foo'), 1); + let count = 0; + + async function foo() { + for await (const f of on(ee, 'foo', { signal: ac.signal })) { + assert.strictEqual(f[0], 'foo'); + if (++count === 5) + break; + } + ac.abort(); // No error will occur + } + + foo().finally(() => { + clearInterval(i); + }); +} + +async function abortListenerRemovedAfterComplete() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + const i = setInterval(() => ee.emit('foo', 'foo'), 1); + try { + // Below: either the kEvents map is empty or the 'abort' listener list is empty + + // Return case + const endedIterator = on(ee, 'foo', { signal: ac.signal }); + assert.ok(listenerCount(ac.signal, 'abort') > 0); + endedIterator.return(); + assert.strictEqual(listenerCount(ac.signal, 'abort') ?? listenerCount(ac.signal), 0); + + // Throw case + const throwIterator = on(ee, 'foo', { signal: ac.signal }); + assert.ok(listenerCount(ac.signal, 'abort') > 0); + throwIterator.throw(new Error()); + assert.strictEqual(listenerCount(ac.signal, 'abort') ?? listenerCount(ac.signal), 0); + + // Abort case + on(ee, 'foo', { signal: ac.signal }); + assert.ok(listenerCount(ac.signal, 'abort') > 0); + ac.abort(new Error()); + assert.strictEqual(listenerCount(ac.signal, 'abort') ?? listenerCount(ac.signal), 0); + } finally { + clearInterval(i); + } +} + +async function run() { + const funcs = [ + basic, + invalidArgType, + error, + errorDelayed, + throwInLoop, + next, + nextError, + iterableThrow, + eventTarget, + errorListenerCount, + // nodeEventTarget, + abortableOnBefore, + abortableOnAfter, + eventTargetAbortableOnBefore, + eventTargetAbortableOnAfter, + eventTargetAbortableOnAfter2, + abortableOnAfterDone, + abortListenerRemovedAfterComplete, + ]; + + for (const fn of funcs) { + await fn(); + } +} + +run().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-events-once.js b/test/js/node/test/parallel/test-events-once.js new file mode 100644 index 0000000000..25357fde67 --- /dev/null +++ b/test/js/node/test/parallel/test-events-once.js @@ -0,0 +1,287 @@ +'use strict'; +// Flags: --expose-internals --no-warnings + +const common = require('../common'); +const { once, EventEmitter, listenerCount } = require('events'); +const { + deepStrictEqual, + fail, + rejects, + strictEqual, + throws, +} = require('assert'); + +async function onceAnEvent() { + const ee = new EventEmitter(); + + process.nextTick(() => { + ee.emit('myevent', 42); + }); + + const [value] = await once(ee, 'myevent'); + strictEqual(value, 42); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceAnEventWithInvalidOptions() { + const ee = new EventEmitter(); + + await Promise.all([1, 'hi', null, false, () => {}, Symbol(), 1n].map((options) => { + return throws(() => once(ee, 'myevent', options), { + code: 'ERR_INVALID_ARG_TYPE', + }); + })); +} + +async function onceAnEventWithTwoArgs() { + const ee = new EventEmitter(); + + process.nextTick(() => { + ee.emit('myevent', 42, 24); + }); + + const value = await once(ee, 'myevent'); + deepStrictEqual(value, [42, 24]); +} + +async function catchesErrors() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + }); + + try { + await once(ee, 'myevent'); + } catch (_e) { + err = _e; + } + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function catchesErrorsWithAbortSignal() { + const ee = new EventEmitter(); + const ac = new AbortController(); + const signal = ac.signal; + + const expected = new Error('boom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + }); + + try { + const promise = once(ee, 'myevent', { signal }); + strictEqual(ee.listenerCount('error'), 1); + strictEqual(listenerCount(signal, "abort"), 1); + + await promise; + } catch (e) { + err = e; + } + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); + strictEqual(listenerCount(signal, "abort"), 0); +} + +async function stopListeningAfterCatchingError() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + ee.emit('myevent', 42, 24); + }); + + try { + await once(ee, 'myevent'); + } catch (_e) { + err = _e; + } + process.removeAllListeners('multipleResolves'); + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceError() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + process.nextTick(() => { + ee.emit('error', expected); + }); + + const promise = once(ee, 'error'); + strictEqual(ee.listenerCount('error'), 1); + const [ err ] = await promise; + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceWithEventTarget() { + const et = new EventTarget(); + const event = new Event('myevent'); + process.nextTick(() => { + et.dispatchEvent(event); + }); + const [ value ] = await once(et, 'myevent'); + strictEqual(value, event); +} + +async function onceWithEventTargetError() { + const et = new EventTarget(); + const error = new Event('error'); + process.nextTick(() => { + et.dispatchEvent(error); + }); + + const [ err ] = await once(et, 'error'); + strictEqual(err, error); +} + +async function onceWithInvalidEventEmmiter() { + const ac = new AbortController(); + return throws(() => once(ac, 'myevent'), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +async function prioritizesEventEmitter() { + const ee = new EventEmitter(); + ee.addEventListener = fail; + ee.removeAllListeners = fail; + process.nextTick(() => ee.emit('foo')); + await once(ee, 'foo'); +} + +async function abortSignalBefore() { + const ee = new EventEmitter(); + ee.on('error', common.mustNotCall()); + const abortedSignal = AbortSignal.abort(); + + await Promise.all([1, {}, 'hi', null, false].map((signal) => { + return throws(() => once(ee, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + })); + + return throws(() => once(ee, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +async function abortSignalAfter() { + const ee = new EventEmitter(); + const ac = new AbortController(); + ee.on('error', common.mustNotCall()); + const r = rejects(once(ee, 'foo', { signal: ac.signal }), { + name: 'AbortError', + }); + process.nextTick(() => ac.abort()); + return r; +} + +async function abortSignalAfterEvent() { + const ee = new EventEmitter(); + const ac = new AbortController(); + process.nextTick(() => { + ee.emit('foo'); + ac.abort(); + }); + const promise = once(ee, 'foo', { signal: ac.signal }); + strictEqual(listenerCount(ac.signal, "abort"), 1); + await promise; + strictEqual(listenerCount(ac.signal, "abort"), 0); +} + +async function abortSignalRemoveListener() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + try { + process.nextTick(() => ac.abort()); + await once(ee, 'test', { signal: ac.signal }); + } catch { + strictEqual(ee.listeners('test').length, 0); + strictEqual(ee.listeners('error').length, 0); + } +} + +async function eventTargetAbortSignalBefore() { + const et = new EventTarget(); + const abortedSignal = AbortSignal.abort(); + + await Promise.all([1, {}, 'hi', null, false].map((signal) => { + return throws(() => once(et, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + })); + + return throws(() => once(et, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +// TODO: unskip +// async function eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped() { +// const et = new EventTarget(); +// const ac = new AbortController(); +// const { signal } = ac; +// signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true }); + +// process.nextTick(() => ac.abort()); +// return rejects(once(et, 'foo', { signal }), { +// name: 'AbortError', +// }); +// } + +async function eventTargetAbortSignalAfter() { + const et = new EventTarget(); + const ac = new AbortController(); + const r = rejects(once(et, 'foo', { signal: ac.signal }), { + name: 'AbortError', + }); + process.nextTick(() => ac.abort()); + return r; +} + +async function eventTargetAbortSignalAfterEvent() { + const et = new EventTarget(); + const ac = new AbortController(); + process.nextTick(() => { + et.dispatchEvent(new Event('foo')); + ac.abort(); + }); + await once(et, 'foo', { signal: ac.signal }); +} + +Promise.all([ + onceAnEvent(), + onceAnEventWithInvalidOptions(), + onceAnEventWithTwoArgs(), + catchesErrors(), + catchesErrorsWithAbortSignal(), + stopListeningAfterCatchingError(), + onceError(), + onceWithEventTarget(), + onceWithEventTargetError(), + onceWithInvalidEventEmmiter(), + prioritizesEventEmitter(), + abortSignalBefore(), + abortSignalAfter(), + abortSignalAfterEvent(), + abortSignalRemoveListener(), + eventTargetAbortSignalBefore(), + // eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped(), + eventTargetAbortSignalAfter(), + eventTargetAbortSignalAfterEvent(), +]).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-events-static-geteventlisteners.js b/test/js/node/test/parallel/test-events-static-geteventlisteners.js new file mode 100644 index 0000000000..35b4e34325 --- /dev/null +++ b/test/js/node/test/parallel/test-events-static-geteventlisteners.js @@ -0,0 +1,51 @@ +'use strict'; +// Flags: --expose-internals --no-warnings +const common = require('../common'); + +const { + deepStrictEqual, + throws, +} = require('assert'); + +const { getEventListeners, EventEmitter } = require('events'); + +// Test getEventListeners on EventEmitter +{ + const fn1 = common.mustNotCall(); + const fn2 = common.mustNotCall(); + const emitter = new EventEmitter(); + emitter.on('foo', fn1); + emitter.on('foo', fn2); + emitter.on('baz', fn1); + emitter.on('baz', fn1); + deepStrictEqual(getEventListeners(emitter, 'foo'), [fn1, fn2]); + deepStrictEqual(getEventListeners(emitter, 'bar'), []); + deepStrictEqual(getEventListeners(emitter, 'baz'), [fn1, fn1]); +} +// Test getEventListeners on EventTarget +{ + const fn1 = common.mustNotCall(); + const fn2 = common.mustNotCall(); + const target = new EventTarget(); + target.addEventListener('foo', fn1); + target.addEventListener('foo', fn2); + target.addEventListener('baz', fn1); + target.addEventListener('baz', fn1); + deepStrictEqual(getEventListeners(target, 'foo'), [fn1, fn2]); + deepStrictEqual(getEventListeners(target, 'bar'), []); + deepStrictEqual(getEventListeners(target, 'baz'), [fn1]); +} + +{ + throws(() => { + getEventListeners('INVALID_EMITTER'); + }, /ERR_INVALID_ARG_TYPE/); +} +// { +// // Test weak listeners +// const target = new EventTarget(); +// const fn = common.mustNotCall(); +// target.addEventListener('foo', fn, { [kWeakHandler]: {} }); +// const listeners = getEventListeners(target, 'foo'); +// deepStrictEqual(listeners, [fn]); +// } diff --git a/test/js/node/test/parallel/test-events-uncaught-exception-stack.js b/test/js/node/test/parallel/test-events-uncaught-exception-stack.js new file mode 100644 index 0000000000..065bbd8a4b --- /dev/null +++ b/test/js/node/test/parallel/test-events-uncaught-exception-stack.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN https://github.com/oven-sh/bun/issues/12827 +const assert = require('assert'); +const EventEmitter = require('events'); + +// Tests that the error stack where the exception was thrown is *not* appended. + +process.on('uncaughtException', common.mustCall((err) => { + const lines = err.stack.split('\n'); + assert.strictEqual(lines[0], 'Error'); + lines.slice(1).forEach((line) => { + assert.match(line, /^ {4}at/); + }); +})); + +new EventEmitter().emit('error', new Error()); diff --git a/test/js/node/test/parallel/test-eventsource-disabled.js b/test/js/node/test/parallel/test-eventsource-disabled.js new file mode 100644 index 0000000000..ade4f51d99 --- /dev/null +++ b/test/js/node/test/parallel/test-eventsource-disabled.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(typeof EventSource, 'undefined'); diff --git a/test/js/node/test/parallel/test-eventtarget-once-twice.js b/test/js/node/test/parallel/test-eventtarget-once-twice.js new file mode 100644 index 0000000000..3358bab90e --- /dev/null +++ b/test/js/node/test/parallel/test-eventtarget-once-twice.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +const { once } = require('events'); + +const et = new EventTarget(); +(async function() { + await once(et, 'foo'); + await once(et, 'foo'); +})().then(common.mustCall()); + +et.dispatchEvent(new Event('foo')); +setImmediate(() => { + et.dispatchEvent(new Event('foo')); +}); diff --git a/test/js/node/test/parallel/test-exception-handler.js b/test/js/node/test/parallel/test-exception-handler.js new file mode 100644 index 0000000000..ca25ccd6ef --- /dev/null +++ b/test/js/node/test/parallel/test-exception-handler.js @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const MESSAGE = 'catch me if you can'; + +process.on('uncaughtException', common.mustCall((e) => { + console.log('uncaught exception! 1'); + assert.strictEqual(MESSAGE, e.message); +})); + +process.on('uncaughtException', common.mustCall((e) => { + console.log('uncaught exception! 2'); + assert.strictEqual(MESSAGE, e.message); +})); + +setTimeout(() => { + throw new Error(MESSAGE); +}, 10); diff --git a/test/js/node/test/parallel/test-exception-handler2.js b/test/js/node/test/parallel/test-exception-handler2.js new file mode 100644 index 0000000000..ae95d45245 --- /dev/null +++ b/test/js/node/test/parallel/test-exception-handler2.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +process.on('uncaughtException', function(err) { + console.log(`Caught exception: ${err}`); +}); + +setTimeout(common.mustCall(function() { + console.log('This will still run.'); +}), 50); + +// Intentionally cause an exception, but don't catch it. +nonexistentFunc(); // eslint-disable-line no-undef +assert.fail('This will not run.'); diff --git a/test/js/node/test/parallel/test-fetch.mjs b/test/js/node/test/parallel/test-fetch.mjs new file mode 100644 index 0000000000..bbdb7130ed --- /dev/null +++ b/test/js/node/test/parallel/test-fetch.mjs @@ -0,0 +1,36 @@ +import * as common from '../common/index.mjs'; + +import assert from 'assert'; +import events from 'events'; +import http from 'http'; + +assert.strictEqual(typeof globalThis.fetch, 'function'); +assert.strictEqual(typeof globalThis.FormData, 'function'); +assert.strictEqual(typeof globalThis.Headers, 'function'); +assert.strictEqual(typeof globalThis.Request, 'function'); +assert.strictEqual(typeof globalThis.Response, 'function'); + +{ + const asyncFunction = async function() {}.constructor; + + assert.ok(!(fetch instanceof asyncFunction)); + assert.notStrictEqual(Reflect.getPrototypeOf(fetch), Reflect.getPrototypeOf(async function() {})); + assert.strictEqual(Reflect.getPrototypeOf(fetch), Reflect.getPrototypeOf(function() {})); +} + +const server = http.createServer(common.mustCall((req, res) => { + res.end('Hello world'); +})); +server.listen(0); +await events.once(server, 'listening'); +const port = server.address().port; + +const response = await fetch(`http://localhost:${port}`); + +assert(response instanceof Response); +assert.strictEqual(response.status, 200); +assert.strictEqual(response.statusText, 'OK'); +const body = await response.text(); +assert.strictEqual(body, 'Hello world'); + +server.close(); diff --git a/test/js/node/test/parallel/test-file-read-noexist.js b/test/js/node/test/parallel/test-file-read-noexist.js new file mode 100644 index 0000000000..7293113f22 --- /dev/null +++ b/test/js/node/test/parallel/test-file-read-noexist.js @@ -0,0 +1,32 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); + +const filename = fixtures.path('does_not_exist.txt'); +fs.readFile(filename, 'latin1', common.mustCall(function(err, content) { + assert.ok(err); + assert.strictEqual(err.code, 'ENOENT'); +})); diff --git a/test/js/node/test/parallel/test-file-validate-mode-flag.js b/test/js/node/test/parallel/test-file-validate-mode-flag.js new file mode 100644 index 0000000000..62a9ef2ca2 --- /dev/null +++ b/test/js/node/test/parallel/test-file-validate-mode-flag.js @@ -0,0 +1,40 @@ +'use strict'; + +// Checks for crash regression: https://github.com/nodejs/node/issues/37430 + +const common = require('../common'); +const assert = require('assert'); +const { + open, + openSync, + promises: { + open: openPromise, + }, +} = require('fs'); + +// These should throw, not crash. +const invalid = 4_294_967_296; + +assert.throws(() => open(__filename, invalid, common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE' +}); + +assert.throws(() => open(__filename, 0, invalid, common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE' +}); + +assert.throws(() => openSync(__filename, invalid), { + code: 'ERR_OUT_OF_RANGE' +}); + +assert.throws(() => openSync(__filename, 0, invalid), { + code: 'ERR_OUT_OF_RANGE' +}); + +assert.rejects(openPromise(__filename, invalid), { + code: 'ERR_OUT_OF_RANGE' +}).then(common.mustCall()); + +assert.rejects(openPromise(__filename, 0, invalid), { + code: 'ERR_OUT_OF_RANGE' +}).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-filehandle-close.js b/test/js/node/test/parallel/test-filehandle-close.js new file mode 100644 index 0000000000..6e55f3f06d --- /dev/null +++ b/test/js/node/test/parallel/test-filehandle-close.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +// Test that using FileHandle.close to close an already-closed fd fails +// with EBADF. + +(async function() { + const fh = await fs.promises.open(__filename); + fs.closeSync(fh.fd); + + await assert.rejects(() => fh.close(), { + code: 'EBADF', + syscall: 'close' + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-access.js b/test/js/node/test/parallel/test-fs-access.js new file mode 100644 index 0000000000..e4ce90adc1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-access.js @@ -0,0 +1,237 @@ +'use strict'; + +// This tests that fs.access and fs.accessSync works as expected +// and the errors thrown from these APIs include the desired properties + +const common = require('../common'); +if (!common.isWindows && process.getuid() === 0) + common.skip('as this test should not be run as `root`'); + +if (common.isIBMi) + common.skip('IBMi has a different access permission mechanism'); + +const assert = require('assert'); +const fs = require('fs'); + +const { UV_ENOENT } = process.binding('uv'); + +const tmpdir = require('../common/tmpdir'); +const doesNotExist = tmpdir.resolve('__this_should_not_exist'); +const readOnlyFile = tmpdir.resolve('read_only_file'); +const readWriteFile = tmpdir.resolve('read_write_file'); + +function createFileWithPerms(file, mode) { + fs.writeFileSync(file, ''); + fs.chmodSync(file, mode); +} + +tmpdir.refresh(); +createFileWithPerms(readOnlyFile, 0o444); +createFileWithPerms(readWriteFile, 0o666); + +// On non-Windows supported platforms, fs.access(readOnlyFile, W_OK, ...) +// always succeeds if node runs as the super user, which is sometimes the +// case for tests running on our continuous testing platform agents. +// +// In this case, this test tries to change its process user id to a +// non-superuser user so that the test that checks for write access to a +// read-only file can be more meaningful. +// +// The change of user id is done after creating the fixtures files for the same +// reason: the test may be run as the superuser within a directory in which +// only the superuser can create files, and thus it may need superuser +// privileges to create them. +// +// There's not really any point in resetting the process' user id to 0 after +// changing it to 'nobody', since in the case that the test runs without +// superuser privilege, it is not possible to change its process user id to +// superuser. +// +// It can prevent the test from removing files created before the change of user +// id, but that's fine. In this case, it is the responsibility of the +// continuous integration platform to take care of that. +let hasWriteAccessForReadonlyFile = false; +if (!common.isWindows && process.getuid() === 0) { + hasWriteAccessForReadonlyFile = true; + try { + process.setuid('nobody'); + hasWriteAccessForReadonlyFile = false; + } catch { + // Continue regardless of error. + } +} + +assert.strictEqual(typeof fs.constants.F_OK, 'number'); +assert.strictEqual(typeof fs.constants.R_OK, 'number'); +assert.strictEqual(typeof fs.constants.W_OK, 'number'); +assert.strictEqual(typeof fs.constants.X_OK, 'number'); + +const throwNextTick = (e) => { process.nextTick(() => { throw e; }); }; + +fs.access(__filename, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(__filename) + .then(common.mustCall()) + .catch(throwNextTick); +fs.access(__filename, fs.constants.R_OK, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(__filename, fs.constants.R_OK) + .then(common.mustCall()) + .catch(throwNextTick); +fs.access(readOnlyFile, fs.constants.R_OK, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(readOnlyFile, fs.constants.R_OK) + .then(common.mustCall()) + .catch(throwNextTick); + +{ + const expectedError = (err) => { + assert.notStrictEqual(err, null); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + }; + const expectedErrorPromise = (err) => { + expectedError(err); + // TODO: https://github.com/oven-sh/bun/issues/2704 + // assert.match(err.stack, /at async Object\.access/); + }; + fs.access(doesNotExist, common.mustCall(expectedError)); + fs.promises.access(doesNotExist) + .then(common.mustNotCall(), common.mustCall(expectedErrorPromise)) + .catch(throwNextTick); +} + +{ + function expectedError(err) { + assert.strictEqual(this, undefined); + if (hasWriteAccessForReadonlyFile) { + assert.ifError(err); + } else { + assert.notStrictEqual(err, null); + assert.strictEqual(err.path, readOnlyFile); + } + } + fs.access(readOnlyFile, fs.constants.W_OK, common.mustCall(expectedError)); + fs.promises.access(readOnlyFile, fs.constants.W_OK) + .then(common.mustNotCall(), common.mustCall(expectedError)) + .catch(throwNextTick); +} + +{ + const expectedError = (err) => { + assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE'); + assert.ok(err instanceof TypeError); + return true; + }; + assert.throws( + () => { fs.access(100, fs.constants.F_OK, common.mustNotCall()); }, + expectedError + ); + + fs.promises.access(100, fs.constants.F_OK) + .then(common.mustNotCall(), common.mustCall(expectedError)) + .catch(throwNextTick); +} + +assert.throws( + () => { + fs.access(__filename, fs.constants.F_OK); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +assert.throws( + () => { + fs.access(__filename, fs.constants.F_OK, common.mustNotMutateObjectDeep({})); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +// Regular access should not throw. +fs.accessSync(__filename); +const mode = fs.constants.R_OK | fs.constants.W_OK; +fs.accessSync(readWriteFile, mode); + +// Invalid modes should throw. +[ + false, + 1n, + { [Symbol.toPrimitive]() { return fs.constants.R_OK; } }, + [1], + 'r', +].forEach((mode, i) => { + console.log(mode, i); + assert.throws( + () => fs.access(readWriteFile, mode, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + assert.throws( + () => fs.accessSync(readWriteFile, mode), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); +}); + +// Out of range modes should throw +[ + -1, + 8, + Infinity, + NaN, +].forEach((mode, i) => { + console.log(mode, i); + assert.throws( + () => fs.access(readWriteFile, mode, common.mustNotCall()), + { + code: 'ERR_OUT_OF_RANGE', + } + ); + assert.throws( + () => fs.accessSync(readWriteFile, mode), + { + code: 'ERR_OUT_OF_RANGE', + } + ); +}); + +assert.throws( + () => { fs.accessSync(doesNotExist); }, + (err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, access '${doesNotExist}'` + ); + assert.strictEqual(err.constructor, Error); + assert.strictEqual(err.syscall, 'access'); + assert.strictEqual(err.errno, UV_ENOENT); + return true; + } +); + +assert.throws( + () => { fs.accessSync(Buffer.from(doesNotExist)); }, + (err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, access '${doesNotExist}'` + ); + assert.strictEqual(err.constructor, Error); + assert.strictEqual(err.syscall, 'access'); + assert.strictEqual(err.errno, UV_ENOENT); + return true; + } +); diff --git a/test/js/node/test/parallel/test-fs-append-file-sync.js b/test/js/node/test/parallel/test-fs-append-file-sync.js new file mode 100644 index 0000000000..a3969b1a4a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-append-file-sync.js @@ -0,0 +1,103 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const currentFileData = 'ABCD'; +const m = 0o600; +const num = 220; +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const data = fixtures.utf8TestText; + +tmpdir.refresh(); + +// Test that empty file will be created and have content added. +const filename = tmpdir.resolve('append-sync.txt'); + +fs.appendFileSync(filename, data); + +const fileData = fs.readFileSync(filename); + +assert.strictEqual(Buffer.byteLength(data), fileData.length); + +// Test that appends data to a non empty file. +const filename2 = tmpdir.resolve('append-sync2.txt'); +fs.writeFileSync(filename2, currentFileData); + +fs.appendFileSync(filename2, data); + +const fileData2 = fs.readFileSync(filename2); + +assert.strictEqual(Buffer.byteLength(data) + currentFileData.length, + fileData2.length); + +// Test that appendFileSync accepts buffers. +const filename3 = tmpdir.resolve('append-sync3.txt'); +fs.writeFileSync(filename3, currentFileData); + +const buf = Buffer.from(data, 'utf8'); +fs.appendFileSync(filename3, buf); + +const fileData3 = fs.readFileSync(filename3); + +assert.strictEqual(buf.length + currentFileData.length, fileData3.length); + +const filename4 = tmpdir.resolve('append-sync4.txt'); +fs.writeFileSync(filename4, currentFileData, common.mustNotMutateObjectDeep({ mode: m })); + +[ + true, false, 0, 1, Infinity, () => {}, {}, [], undefined, null, +].forEach((value) => { + console.log(value); + assert.throws( + () => fs.appendFileSync(filename4, value, common.mustNotMutateObjectDeep({ mode: m })), + { message: /data/, code: 'ERR_INVALID_ARG_TYPE' } + ); +}); +fs.appendFileSync(filename4, `${num}`, common.mustNotMutateObjectDeep({ mode: m })); + +// Windows permissions aren't Unix. +if (!common.isWindows) { + const st = fs.statSync(filename4); + assert.strictEqual(st.mode & 0o700, m); +} + +const fileData4 = fs.readFileSync(filename4); + +assert.strictEqual(Buffer.byteLength(String(num)) + currentFileData.length, + fileData4.length); + +// Test that appendFile accepts file descriptors. +const filename5 = tmpdir.resolve('append-sync5.txt'); +fs.writeFileSync(filename5, currentFileData); + +const filename5fd = fs.openSync(filename5, 'a+', 0o600); +fs.appendFileSync(filename5fd, data); +fs.closeSync(filename5fd); + +const fileData5 = fs.readFileSync(filename5); + +assert.strictEqual(Buffer.byteLength(data) + currentFileData.length, + fileData5.length); diff --git a/test/js/node/test/parallel/test-fs-append-file.js b/test/js/node/test/parallel/test-fs-append-file.js new file mode 100644 index 0000000000..1e20625e5b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-append-file.js @@ -0,0 +1,187 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const currentFileData = 'ABCD'; +const fixtures = require('../common/fixtures'); +const s = fixtures.utf8TestText; + +tmpdir.refresh(); + +const throwNextTick = (e) => { process.nextTick(() => { throw e; }); }; + +// Test that empty file will be created and have content added (callback API). +{ + const filename = tmpdir.resolve('append.txt'); + + fs.appendFile(filename, s, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + })); + })); +} + +// Test that empty file will be created and have content added (promise API). +{ + const filename = tmpdir.resolve('append-promise.txt'); + + fs.promises.appendFile(filename, s) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appends data to a non-empty file (callback API). +{ + const filename = tmpdir.resolve('append-non-empty.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.appendFile(filename, s, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })); + })); +} + +// Test that appends data to a non-empty file (promise API). +{ + const filename = tmpdir.resolve('append-non-empty-promise.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.promises.appendFile(filename, s) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appendFile accepts buffers (callback API). +{ + const filename = tmpdir.resolve('append-buffer.txt'); + fs.writeFileSync(filename, currentFileData); + + const buf = Buffer.from(s, 'utf8'); + + fs.appendFile(filename, buf, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(buf.length + currentFileData.length, buffer.length); + })); + })); +} + +// Test that appendFile accepts buffers (promises API). +{ + const filename = tmpdir.resolve('append-buffer-promises.txt'); + fs.writeFileSync(filename, currentFileData); + + const buf = Buffer.from(s, 'utf8'); + + fs.promises.appendFile(filename, buf) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(buf.length + currentFileData.length, buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appendFile does not accept invalid data type (callback API). +[false, 5, {}, null, undefined].forEach(async (data) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + message: /"data"|"buffer"/ + }; + const filename = tmpdir.resolve('append-invalid-data.txt'); + + assert.throws( + () => fs.appendFile(filename, data, common.mustNotCall()), + errObj + ); + + assert.throws( + () => fs.appendFileSync(filename, data), + errObj + ); + + await assert.rejects( + fs.promises.appendFile(filename, data), + errObj + ); + // The filename shouldn't exist if throwing error. + assert.throws( + () => fs.statSync(filename), + { + code: 'ENOENT', + message: /no such file or directory/ + } + ); +}); + +// Test that appendFile accepts file descriptors (callback API). +{ + const filename = tmpdir.resolve('append-descriptors.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.open(filename, 'a+', common.mustSucceed((fd) => { + fs.appendFile(fd, s, common.mustSucceed(() => { + fs.close(fd, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })); + })); + })); + })); +} + +// Test that appendFile accepts file descriptors (promises API). +{ + const filename = tmpdir.resolve('append-descriptors-promises.txt'); + fs.writeFileSync(filename, currentFileData); + + let fd; + fs.promises.open(filename, 'a+') + .then(common.mustCall((fileDescriptor) => { + fd = fileDescriptor; + return fs.promises.appendFile(fd, s); + })) + .then(common.mustCall(() => fd.close())) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then(common.mustCall((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })) + .catch(throwNextTick); +} + +assert.throws( + () => fs.appendFile(tmpdir.resolve('append6.txt'), console.log), + { code: 'ERR_INVALID_ARG_TYPE' }); diff --git a/test/js/node/test/parallel/test-fs-assert-encoding-error.js b/test/js/node/test/parallel/test-fs-assert-encoding-error.js new file mode 100644 index 0000000000..9b22e042c5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-assert-encoding-error.js @@ -0,0 +1,80 @@ +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const tmpdir = require('../common/tmpdir'); + +const testPath = tmpdir.resolve('assert-encoding-error'); +const options = 'test'; +const expectedError = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}; + +assert.throws(() => { + fs.readFile(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readFileSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.readdir(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readdirSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.readlink(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readlinkSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.writeFile(testPath, 'data', options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.writeFileSync(testPath, 'data', options); +}, expectedError); + +assert.throws(() => { + fs.appendFile(testPath, 'data', options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.appendFileSync(testPath, 'data', options); +}, expectedError); + +assert.throws(() => { + fs.watch(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.realpath(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.realpathSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.mkdtemp(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.mkdtempSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.ReadStream(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.WriteStream(testPath, options); +}, expectedError); diff --git a/test/js/node/test/parallel/test-fs-buffertype-writesync.js b/test/js/node/test/parallel/test-fs-buffertype-writesync.js new file mode 100644 index 0000000000..5649a00569 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-buffertype-writesync.js @@ -0,0 +1,16 @@ +'use strict'; +require('../common'); + +// This test ensures that writeSync throws for invalid data input. + +const assert = require('assert'); +const fs = require('fs'); + +[ + true, false, 0, 1, Infinity, () => {}, {}, [], undefined, null, +].forEach((value) => { + assert.throws( + () => fs.writeSync(1, value), + { message: /"buffer"/, code: 'ERR_INVALID_ARG_TYPE' } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-chmod-mask.js b/test/js/node/test/parallel/test-fs-chmod-mask.js new file mode 100644 index 0000000000..53f1931be4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-chmod-mask.js @@ -0,0 +1,89 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in fs APIs. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +let mode; +// On Windows chmod is only able to manipulate write permission +if (common.isWindows) { + mode = 0o444; // read-only +} else { + mode = 0o777; +} + +const maskToIgnore = 0o10000; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function test(mode, asString) { + const suffix = asString ? 'str' : 'num'; + const input = asString ? + (mode | maskToIgnore).toString(8) : (mode | maskToIgnore); + + { + const file = tmpdir.resolve(`chmod-async-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + + fs.chmod(file, input, common.mustSucceed(() => { + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + })); + } + + { + const file = tmpdir.resolve(`chmodSync-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + + fs.chmodSync(file, input); + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + } + + { + const file = tmpdir.resolve(`fchmod-async-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.open(file, 'w', common.mustSucceed((fd) => { + fs.fchmod(fd, input, common.mustSucceed(() => { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + fs.close(fd, assert.ifError); + })); + })); + } + + { + const file = tmpdir.resolve(`fchmodSync-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + const fd = fs.openSync(file, 'w'); + + fs.fchmodSync(fd, input); + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + + fs.close(fd, assert.ifError); + } + + if (fs.lchmod) { + const link = tmpdir.resolve(`lchmod-src-${suffix}`); + const file = tmpdir.resolve(`lchmod-dest-${suffix}`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.symlinkSync(file, link); + + fs.lchmod(link, input, common.mustSucceed(() => { + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode); + })); + } + + if (fs.lchmodSync) { + const link = tmpdir.resolve(`lchmodSync-src-${suffix}`); + const file = tmpdir.resolve(`lchmodSync-dest-${suffix}`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.symlinkSync(file, link); + + fs.lchmodSync(link, input); + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode); + } +} + +test(mode, true); +test(mode, false); diff --git a/test/js/node/test/parallel/test-fs-chmod.js b/test/js/node/test/parallel/test-fs-chmod.js new file mode 100644 index 0000000000..5c5821df46 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-chmod.js @@ -0,0 +1,152 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +let mode_async; +let mode_sync; + +// Need to hijack fs.open/close to make sure that things +// get closed once they're opened. +fs._open = fs.open; +fs._openSync = fs.openSync; +fs.open = open; +fs.openSync = openSync; +fs._close = fs.close; +fs._closeSync = fs.closeSync; +fs.close = close; +fs.closeSync = closeSync; + +let openCount = 0; + +function open() { + openCount++; + return fs._open.apply(fs, arguments); +} + +function openSync() { + openCount++; + return fs._openSync.apply(fs, arguments); +} + +function close() { + openCount--; + return fs._close.apply(fs, arguments); +} + +function closeSync() { + openCount--; + return fs._closeSync.apply(fs, arguments); +} + + +// On Windows chmod is only able to manipulate write permission +if (common.isWindows) { + mode_async = 0o400; // read-only + mode_sync = 0o600; // read-write +} else { + mode_async = 0o777; + mode_sync = 0o644; +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const file1 = tmpdir.resolve('a.js'); +const file2 = tmpdir.resolve('a1.js'); + +// Create file1. +fs.closeSync(fs.openSync(file1, 'w')); + +fs.chmod(file1, mode_async.toString(8), common.mustSucceed(() => { + if (common.isWindows) { + assert.ok((fs.statSync(file1).mode & 0o777) & mode_async); + } else { + assert.strictEqual(fs.statSync(file1).mode & 0o777, mode_async); + } + + fs.chmodSync(file1, mode_sync); + if (common.isWindows) { + assert.ok((fs.statSync(file1).mode & 0o777) & mode_sync); + } else { + assert.strictEqual(fs.statSync(file1).mode & 0o777, mode_sync); + } +})); + +fs.open(file2, 'w', common.mustSucceed((fd) => { + fs.fchmod(fd, mode_async.toString(8), common.mustSucceed(() => { + if (common.isWindows) { + assert.ok((fs.fstatSync(fd).mode & 0o777) & mode_async); + } else { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode_async); + } + + assert.throws( + () => fs.fchmod(fd, {}), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + + fs.fchmodSync(fd, mode_sync); + if (common.isWindows) { + assert.ok((fs.fstatSync(fd).mode & 0o777) & mode_sync); + } else { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode_sync); + } + + fs.close(fd, assert.ifError); + })); +})); + +// lchmod +if (fs.lchmod) { + const link = tmpdir.resolve('symbolic-link'); + + fs.symlinkSync(file2, link); + + fs.lchmod(link, mode_async, common.mustSucceed(() => { + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode_async); + + fs.lchmodSync(link, mode_sync); + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode_sync); + + })); +} + +[false, 1, {}, [], null, undefined].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "path" argument must be of type string or an instance ' + + // 'of Buffer or URL.' + + // common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.chmod(input, 1, common.mustNotCall()), errObj); + assert.throws(() => fs.chmodSync(input, 1), errObj); +}); + +process.on('exit', function() { + assert.strictEqual(openCount, 0); +}); diff --git a/test/js/node/test/parallel/test-fs-chown-type-check.js b/test/js/node/test/parallel/test-fs-chown-type-check.js new file mode 100644 index 0000000000..0ca78aa86e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-chown-type-check.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.chown(i, 1, 1, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.chownSync(i, 1, 1), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +[false, 'test', {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.chown('not_a_file_that_exists', i, 1, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.chown('not_a_file_that_exists', 1, i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.chownSync('not_a_file_that_exists', i, 1), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.chownSync('not_a_file_that_exists', 1, i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-close-errors.js b/test/js/node/test/parallel/test-fs-close-errors.js new file mode 100644 index 0000000000..0c48d39cd9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-close-errors.js @@ -0,0 +1,35 @@ +'use strict'; + +// This tests that the errors thrown from fs.close and fs.closeSync +// include the desired properties + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +['', false, null, undefined, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "fd" argument must be of type number.' + + // common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.close(input), errObj); + assert.throws(() => fs.closeSync(input), errObj); +}); + +{ + // Test error when cb is not a function + const fd = fs.openSync(__filename, 'r'); + + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }; + + ['', false, null, {}, []].forEach((input) => { + assert.throws(() => fs.close(fd, input), errObj); + }); + + fs.closeSync(fd); +} diff --git a/test/js/node/test/parallel/test-fs-close.js b/test/js/node/test/parallel/test-fs-close.js new file mode 100644 index 0000000000..da0d0dfdc8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-close.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +const fd = fs.openSync(__filename, 'r'); + +fs.close(fd, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); diff --git a/test/js/node/test/parallel/test-fs-constants.js b/test/js/node/test/parallel/test-fs-constants.js new file mode 100644 index 0000000000..49bcabd808 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-constants.js @@ -0,0 +1,8 @@ +'use strict'; +require('../common'); +const fs = require('fs'); +const assert = require('assert'); + +// Check if the two constants accepted by chmod() on Windows are defined. +assert.notStrictEqual(fs.constants.S_IRUSR, undefined); +assert.notStrictEqual(fs.constants.S_IWUSR, undefined); diff --git a/test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js b/test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js new file mode 100644 index 0000000000..d668ec63ec --- /dev/null +++ b/test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js @@ -0,0 +1,58 @@ +'use strict'; + +// Test that fs.copyFile() respects file permissions. +// Ref: https://github.com/nodejs/node/issues/26936 + +const common = require('../common'); + +if (!common.isWindows && process.getuid() === 0) + common.skip('as this test should not be run as `root`'); + +if (common.isIBMi) + common.skip('IBMi has a different access permission mechanism'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const assert = require('assert'); +const fs = require('fs'); + +let n = 0; + +function beforeEach() { + n++; + const source = tmpdir.resolve(`source${n}`); + const dest = tmpdir.resolve(`dest${n}`); + fs.writeFileSync(source, 'source'); + fs.writeFileSync(dest, 'dest'); + fs.chmodSync(dest, '444'); + + const check = (err) => { + const expected = ['EACCES', 'EPERM']; + assert(expected.includes(err.code), `${err.code} not in ${expected}`); + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'dest'); + return true; + }; + + return { source, dest, check }; +} + +// Test synchronous API. +{ + const { source, dest, check } = beforeEach(); + assert.throws(() => { fs.copyFileSync(source, dest); }, check); +} + +// Test promises API. +{ + const { source, dest, check } = beforeEach(); + (async () => { + await assert.rejects(fs.promises.copyFile(source, dest), check); + })().then(common.mustCall()); +} + +// Test callback API. +{ + const { source, dest, check } = beforeEach(); + fs.copyFile(source, dest, common.mustCall(check)); +} diff --git a/test/js/node/test/parallel/test-fs-copyfile.js b/test/js/node/test/parallel/test-fs-copyfile.js new file mode 100644 index 0000000000..4541ef76b0 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-copyfile.js @@ -0,0 +1,164 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const { + UV_ENOENT, + UV_EEXIST +} = process.binding('uv'); +const src = fixtures.path('a.js'); +const dest = tmpdir.resolve('copyfile.out'); +const { + COPYFILE_EXCL, + COPYFILE_FICLONE, + COPYFILE_FICLONE_FORCE, + UV_FS_COPYFILE_EXCL, + UV_FS_COPYFILE_FICLONE, + UV_FS_COPYFILE_FICLONE_FORCE +} = fs.constants; + +function verify(src, dest) { + const srcData = fs.readFileSync(src, 'utf8'); + const srcStat = fs.statSync(src); + const destData = fs.readFileSync(dest, 'utf8'); + const destStat = fs.statSync(dest); + + assert.strictEqual(srcData, destData); + assert.strictEqual(srcStat.mode, destStat.mode); + assert.strictEqual(srcStat.size, destStat.size); +} + +tmpdir.refresh(); + +// Verify that flags are defined. +assert.strictEqual(typeof COPYFILE_EXCL, 'number'); +assert.strictEqual(typeof COPYFILE_FICLONE, 'number'); +assert.strictEqual(typeof COPYFILE_FICLONE_FORCE, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_EXCL, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_FICLONE, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_FICLONE_FORCE, 'number'); +assert.strictEqual(COPYFILE_EXCL, UV_FS_COPYFILE_EXCL); +assert.strictEqual(COPYFILE_FICLONE, UV_FS_COPYFILE_FICLONE); +assert.strictEqual(COPYFILE_FICLONE_FORCE, UV_FS_COPYFILE_FICLONE_FORCE); + +// Verify that files are overwritten when no flags are provided. +fs.writeFileSync(dest, '', 'utf8'); +const result = fs.copyFileSync(src, dest); +assert.strictEqual(result, undefined); +verify(src, dest); + +// Verify that files are overwritten with default flags. +fs.copyFileSync(src, dest, 0); +verify(src, dest); + +// Verify that UV_FS_COPYFILE_FICLONE can be used. +fs.unlinkSync(dest); +fs.copyFileSync(src, dest, UV_FS_COPYFILE_FICLONE); +verify(src, dest); + +// Verify that COPYFILE_FICLONE_FORCE can be used. +try { + fs.unlinkSync(dest); + fs.copyFileSync(src, dest, COPYFILE_FICLONE_FORCE); + verify(src, dest); +} catch (err) { + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); + assert.strictEqual(err.path, src); + assert.strictEqual(err.dest, dest); +} + +// Copies asynchronously. +tmpdir.refresh(); // Don't use unlinkSync() since the last test may fail. +fs.copyFile(src, dest, common.mustSucceed(() => { + verify(src, dest); + + // Copy asynchronously with flags. + fs.copyFile(src, dest, COPYFILE_EXCL, common.mustCall((err) => { + if (err.code === 'ENOENT') { // Could be ENOENT or EEXIST + assert.strictEqual(err.message, + 'ENOENT: no such file or directory, copyfile ' + + `'${src}' -> '${dest}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'copyfile'); + } else { + assert.strictEqual(err.message, + 'EEXIST: file already exists, copyfile ' + + `'${src}' -> '${dest}'`); + assert.strictEqual(err.errno, UV_EEXIST); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'copyfile'); + } + })); +})); + +// Throws if callback is not a function. +assert.throws(() => { + fs.copyFile(src, dest, 0, 0); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +// Throws if the source path is not a string. +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.copyFile(i, dest, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /src/ + } + ); + assert.throws( + () => fs.copyFile(src, i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /dest/ + } + ); + assert.throws( + () => fs.copyFileSync(i, dest), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /src/ + } + ); + assert.throws( + () => fs.copyFileSync(src, i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /dest/ + } + ); +}); + +assert.throws(() => { + fs.copyFileSync(src, dest, 'r'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /mode/ +}); + +assert.throws(() => { + fs.copyFileSync(src, dest, 8); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}); + +assert.throws(() => { + fs.copyFile(src, dest, 'r', common.mustNotCall()); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /mode/ +}); diff --git a/test/js/node/test/parallel/test-fs-empty-readStream.js b/test/js/node/test/parallel/test-fs-empty-readStream.js new file mode 100644 index 0000000000..7cbe4d5005 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-empty-readStream.js @@ -0,0 +1,50 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const emptyFile = fixtures.path('empty.txt'); + +fs.open(emptyFile, 'r', common.mustSucceed((fd) => { + const read = fs.createReadStream(emptyFile, { fd }); + + read.once('data', common.mustNotCall('data event should not emit')); + + read.once('end', common.mustCall()); +})); + +fs.open(emptyFile, 'r', common.mustSucceed((fd) => { + const read = fs.createReadStream(emptyFile, { fd }); + + read.pause(); + + read.once('data', common.mustNotCall('data event should not emit')); + + read.once('end', common.mustNotCall('end event should not emit')); + + setTimeout(common.mustCall(() => { + assert.strictEqual(read.isPaused(), true); + }), common.platformTimeout(50)); +})); diff --git a/test/js/node/test/parallel/test-fs-exists.js b/test/js/node/test/parallel/test-fs-exists.js new file mode 100644 index 0000000000..857f3f2617 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-exists.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const f = __filename; + +assert.throws(() => fs.exists(f), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.exists(), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.exists(f, {}), { code: 'ERR_INVALID_ARG_TYPE' }); + +fs.exists(f, common.mustCall(function(y) { + assert.strictEqual(y, true); +})); + +fs.exists(`${f}-NO`, common.mustCall(function(y) { + assert.strictEqual(y, false); +})); + +// If the path is invalid, fs.exists will still invoke the callback with false +// instead of throwing errors +fs.exists(new URL('https://foo'), common.mustCall(function(y) { + assert.strictEqual(y, false); +})); + +fs.exists({}, common.mustCall(function(y) { + assert.strictEqual(y, false); +})); + +assert(fs.existsSync(f)); +assert(!fs.existsSync(`${f}-NO`)); + +// fs.existsSync() never throws +assert(!fs.existsSync()); +assert(!fs.existsSync({})); +assert(!fs.existsSync(new URL('https://foo'))); diff --git a/test/js/node/test/parallel/test-fs-fchmod.js b/test/js/node/test/parallel/test-fs-fchmod.js new file mode 100644 index 0000000000..6896a38ffb --- /dev/null +++ b/test/js/node/test/parallel/test-fs-fchmod.js @@ -0,0 +1,84 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +// This test ensures that input for fchmod is valid, testing for valid +// inputs for fd and mode + +// Check input type +[false, null, undefined, {}, [], ''].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "fd" argument must be of type number.' + + // common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); +}); + + +[false, null, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + }; + assert.throws(() => fs.fchmod(1, input), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +// EDIT: Bun checks the callback first, then the mode. Original check did not have callback set. +assert.throws(() => fs.fchmod(1, '123x', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE' +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be >= 0 and <= ' + + `2147483647. Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "mode" is out of range. It must be >= 0 and <= ' + + `4294967295. Received ${input}` + }; + + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +[NaN, Infinity].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); + errObj.message = errObj.message.replace('fd', 'mode'); + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +[1.5].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); + errObj.message = errObj.message.replace('fd', 'mode'); + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-fchown.js b/test/js/node/test/parallel/test-fs-fchown.js new file mode 100644 index 0000000000..cbdd038951 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-fchown.js @@ -0,0 +1,61 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const common = require('../common'); + +function testFd(input, errObj) { + assert.throws(() => fs.fchown(input, 0, 0, () => {}), errObj); + assert.throws(() => fs.fchownSync(input, 0, 0), errObj); +} + +function testUid(input, errObj) { + assert.throws(() => fs.fchown(1, input, 0, common.mustNotCall()), errObj); + assert.throws(() => fs.fchownSync(1, input), errObj); +} + +function testGid(input, errObj) { + assert.throws(() => fs.fchown(1, 1, input, common.mustNotCall()), errObj); + assert.throws(() => fs.fchownSync(1, 1, input), errObj); +} + +['', false, null, undefined, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /fd|uid|gid/ + }; + testFd(input, errObj); + testUid(input, errObj); + testGid(input, errObj); +}); + +[Infinity, NaN].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + testFd(input, errObj); + errObj.message = errObj.message.replace('fd', 'uid'); + testUid(input, errObj); + errObj.message = errObj.message.replace('uid', 'gid'); + testGid(input, errObj); +}); + +[-2, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be ' + + `>= 0 and <= 2147483647. Received ${input}` + }; + testFd(input, errObj); + errObj.message = 'The value of "uid" is out of range. It must be >= -1 and ' + + `<= 4294967295. Received ${input}`; + testUid(input, errObj); + errObj.message = errObj.message.replace('uid', 'gid'); + testGid(input, errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js b/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js new file mode 100644 index 0000000000..18216b4f41 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs').promises; + +(async () => { + const filehandle = await fs.open(__filename); + + assert.notStrictEqual(filehandle.fd, -1); + await filehandle.close(); + assert.strictEqual(filehandle.fd, -1); + + // Open another file handle first. This would typically receive the fd + // that `filehandle` previously used. In earlier versions of Node.js, the + // .stat() call would then succeed because it still used the original fd; + // See https://github.com/nodejs/node/issues/31361 for more details. + const otherFilehandle = await fs.open(process.execPath); + + await assert.rejects(() => filehandle.stat(), { + code: 'EBADF', + syscall: 'fstat' + }); + + await otherFilehandle.close(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-fsync.js b/test/js/node/test/parallel/test-fs-fsync.js new file mode 100644 index 0000000000..6168c08d5b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-fsync.js @@ -0,0 +1,58 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +const fs = require('fs'); + +const fileFixture = fixtures.path('a.js'); +const fileTemp = tmpdir.resolve('a.js'); + +// Copy fixtures to temp. +tmpdir.refresh(); +fs.copyFileSync(fileFixture, fileTemp); + +fs.open(fileTemp, 'a', 0o777, common.mustSucceed((fd) => { + fs.fdatasyncSync(fd); + + fs.fsyncSync(fd); + + fs.fdatasync(fd, common.mustSucceed(() => { + fs.fsync(fd, common.mustSucceed(() => { + fs.closeSync(fd); + })); + })); +})); + +['', false, null, undefined, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }; + assert.throws(() => fs.fdatasync(input), errObj); + assert.throws(() => fs.fdatasyncSync(input), errObj); + assert.throws(() => fs.fsync(input), errObj); + assert.throws(() => fs.fsyncSync(input), errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-lchmod.js b/test/js/node/test/parallel/test-fs-lchmod.js new file mode 100644 index 0000000000..c5f1f30bb9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-lchmod.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { promises } = fs; +const f = __filename; + +// This test ensures that input for lchmod is valid, testing for valid +// inputs for path, mode and callback + +if (!common.isMacOS) { + common.skip('lchmod is only available on macOS'); +} + +// Check callback +assert.throws(() => fs.lchmod(f), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.lchmod(), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.lchmod(f, {}), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Check path +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.lchmod(i, 0o777, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.lchmodSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// Check mode +[false, null, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + }; + + assert.rejects(promises.lchmod(f, input, () => {}), errObj).then(common.mustCall()); + assert.throws(() => fs.lchmodSync(f, input), errObj); +}); + +assert.throws(() => fs.lchmod(f, '123x', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE' +}); +assert.throws(() => fs.lchmodSync(f, '123x'), { + code: 'ERR_INVALID_ARG_VALUE' +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "mode" is out of range. It must be >= 0 and <= ' + + `4294967295. Received ${input}` + }; + + assert.rejects(promises.lchmod(f, input, () => {}), errObj).then(common.mustCall()); + assert.throws(() => fs.lchmodSync(f, input), errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-lchown.js b/test/js/node/test/parallel/test-fs-lchown.js new file mode 100644 index 0000000000..d2a9718685 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-lchown.js @@ -0,0 +1,64 @@ +'use strict'; + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { promises } = fs; + +// Validate the path argument. +[false, 1, {}, [], null, undefined].forEach((i) => { + const err = { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }; + + assert.throws(() => fs.lchown(i, 1, 1, common.mustNotCall()), err); + assert.throws(() => fs.lchownSync(i, 1, 1), err); + promises.lchown(false, 1, 1) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); +}); + +// Validate the uid and gid arguments. +[false, 'test', {}, [], null, undefined].forEach((i) => { + const err = { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }; + + assert.throws( + () => fs.lchown('not_a_file_that_exists', i, 1, common.mustNotCall()), + err + ); + assert.throws( + () => fs.lchown('not_a_file_that_exists', 1, i, common.mustNotCall()), + err + ); + assert.throws(() => fs.lchownSync('not_a_file_that_exists', i, 1), err); + assert.throws(() => fs.lchownSync('not_a_file_that_exists', 1, i), err); + + promises.lchown('not_a_file_that_exists', i, 1) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); + + promises.lchown('not_a_file_that_exists', 1, i) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); +}); + +// Validate the callback argument. +[false, 1, 'test', {}, [], null, undefined].forEach((i) => { + assert.throws(() => fs.lchown('not_a_file_that_exists', 1, 1, i), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +if (!common.isWindows) { + const testFile = tmpdir.resolve(path.basename(__filename)); + const uid = process.geteuid(); + const gid = process.getegid(); + + tmpdir.refresh(); + fs.copyFileSync(__filename, testFile); + fs.lchownSync(testFile, uid, gid); + fs.lchown(testFile, uid, gid, common.mustSucceed(async (err) => { + await promises.lchown(testFile, uid, gid); + })); +} diff --git a/test/js/node/test/parallel/test-fs-link.js b/test/js/node/test/parallel/test-fs-link.js new file mode 100644 index 0000000000..df5f606d0c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-link.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Test creating and reading hard link +const srcPath = tmpdir.resolve('hardlink-target.txt'); +const dstPath = tmpdir.resolve('link1.js'); +fs.writeFileSync(srcPath, 'hello world'); + +function callback(err) { + assert.ifError(err); + const dstContent = fs.readFileSync(dstPath, 'utf8'); + assert.strictEqual(dstContent, 'hello world'); +} + +fs.link(srcPath, dstPath, common.mustCall(callback)); + +// test error outputs + +[false, 1, [], {}, null, undefined].forEach((i) => { + assert.throws( + () => fs.link(i, '', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.link('', i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.linkSync(i, ''), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.linkSync('', i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-make-callback.js b/test/js/node/test/parallel/test-fs-make-callback.js new file mode 100644 index 0000000000..d9341ab06f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-make-callback.js @@ -0,0 +1,28 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const callbackThrowValues = [null, true, false, 0, 1, 'foo', /foo/, [], {}]; + +const { sep } = require('path'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function testMakeCallback(cb) { + return function() { + // fs.mkdtemp() calls makeCallback() on its third argument + fs.mkdtemp(`${tmpdir.path}${sep}`, {}, cb); + }; +} + +function invalidCallbackThrowsTests() { + callbackThrowValues.forEach((value) => { + assert.throws(testMakeCallback(value), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + }); +} + +invalidCallbackThrowsTests(); diff --git a/test/js/node/test/parallel/test-fs-makeStatsCallback.js b/test/js/node/test/parallel/test-fs-makeStatsCallback.js new file mode 100644 index 0000000000..953fc065e8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-makeStatsCallback.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const callbackThrowValues = [null, true, false, 0, 1, 'foo', /foo/, [], {}]; + +function testMakeStatsCallback(cb) { + return function() { + // fs.stat() calls makeStatsCallback() on its second argument + fs.stat(__filename, cb); + }; +} + +// Verify the case where a callback function is provided +testMakeStatsCallback(common.mustCall())(); + +function invalidCallbackThrowsTests() { + callbackThrowValues.forEach((value) => { + assert.throws(testMakeStatsCallback(value), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + }); +} + +invalidCallbackThrowsTests(); diff --git a/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js b/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js new file mode 100644 index 0000000000..cca28ca5ff --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js @@ -0,0 +1,40 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in fs.mkdir(). + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +if (common.isWindows) { + common.skip('mode is not supported in mkdir on Windows'); + return; +} + +const mode = 0o644; +const maskToIgnore = 0o10000; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function test(mode, asString) { + const suffix = asString ? 'str' : 'num'; + const input = asString ? + (mode | maskToIgnore).toString(8) : (mode | maskToIgnore); + + { + const dir = tmpdir.resolve(`mkdirSync-${suffix}`); + fs.mkdirSync(dir, input); + assert.strictEqual(fs.statSync(dir).mode & 0o777, mode); + } + + { + const dir = tmpdir.resolve(`mkdir-${suffix}`); + fs.mkdir(dir, input, common.mustSucceed(() => { + assert.strictEqual(fs.statSync(dir).mode & 0o777, mode); + })); + } +} + +test(mode, true); +test(mode, false); diff --git a/test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js b/test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js new file mode 100644 index 0000000000..034a230948 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js @@ -0,0 +1,70 @@ +'use strict'; + +// Test that mkdir with recursive option returns appropriate error +// when executed on folder it does not have permission to access. +// Ref: https://github.com/nodejs/node/issues/31481 + +const common = require('../common'); + +if (!common.isWindows && process.getuid() === 0) + common.skip('as this test should not be run as `root`'); + +if (common.isIBMi) + common.skip('IBMi has a different access permission mechanism'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const assert = require('assert'); +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +let n = 0; + +function makeDirectoryReadOnly(dir) { + let accessErrorCode = 'EACCES'; + if (common.isWindows) { + accessErrorCode = 'EPERM'; + execSync(`icacls ${dir} /deny "everyone:(OI)(CI)(DE,DC,AD,WD)"`); + } else { + fs.chmodSync(dir, '444'); + } + return accessErrorCode; +} + +function makeDirectoryWritable(dir) { + if (common.isWindows) { + execSync(`icacls ${dir} /remove:d "everyone"`); + } +} + +// Synchronous API should return an EACCESS error with path populated. +{ + const dir = tmpdir.resolve(`mkdirp_${n++}`); + fs.mkdirSync(dir); + const codeExpected = makeDirectoryReadOnly(dir); + let err = null; + try { + fs.mkdirSync(path.join(dir, '/foo'), { recursive: true }); + } catch (_err) { + err = _err; + } + makeDirectoryWritable(dir); + assert(err); + assert.strictEqual(err.code, codeExpected); + assert(err.path); +} + +// Asynchronous API should return an EACCESS error with path populated. +{ + const dir = tmpdir.resolve(`mkdirp_${n++}`); + fs.mkdirSync(dir); + const codeExpected = makeDirectoryReadOnly(dir); + fs.mkdir(path.join(dir, '/bar'), { recursive: true }, (err) => { + makeDirectoryWritable(dir); + assert(err); + assert.strictEqual(err.code, codeExpected); + assert(err.path); + }); +} diff --git a/test/js/node/test/parallel/test-fs-mkdir-rmdir.js b/test/js/node/test/parallel/test-fs-mkdir-rmdir.js new file mode 100644 index 0000000000..7fa3473f1c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir-rmdir.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const d = tmpdir.resolve('dir'); + +tmpdir.refresh(); + +// Make sure the directory does not exist +assert(!fs.existsSync(d)); +// Create the directory now +fs.mkdirSync(d); +// Make sure the directory exists +assert(fs.existsSync(d)); +// Try creating again, it should fail with EEXIST +assert.throws(function() { + fs.mkdirSync(d); +}, /EEXIST: file already exists, mkdir/); +// Remove the directory now +fs.rmdirSync(d); +// Make sure the directory does not exist +assert(!fs.existsSync(d)); + +// Similarly test the Async version +fs.mkdir(d, 0o666, common.mustSucceed(() => { + fs.mkdir(d, 0o666, common.mustCall(function(err) { + assert.strictEqual(this, undefined); + assert.ok(err, 'got no error'); + assert.match(err.message, /^EEXIST/); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.path, d); + + fs.rmdir(d, assert.ifError); + })); +})); diff --git a/test/js/node/test/parallel/test-fs-mkdtemp-prefix-check.js b/test/js/node/test/parallel/test-fs-mkdtemp-prefix-check.js new file mode 100644 index 0000000000..af5cb6ab7d --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdtemp-prefix-check.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const prefixValues = [undefined, null, 0, true, false, 1]; + +function fail(value) { + assert.throws( + () => { + fs.mkdtempSync(value, {}); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +} + +function failAsync(value) { + assert.throws( + () => { + fs.mkdtemp(value, common.mustNotCall()); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +} + +for (const prefixValue of prefixValues) { + fail(prefixValue); + failAsync(prefixValue); +} diff --git a/test/js/node/test/parallel/test-fs-non-number-arguments-throw.js b/test/js/node/test/parallel/test-fs-non-number-arguments-throw.js new file mode 100644 index 0000000000..dbcec683cd --- /dev/null +++ b/test/js/node/test/parallel/test-fs-non-number-arguments-throw.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const tempFile = tmpdir.resolve('fs-non-number-arguments-throw'); + +tmpdir.refresh(); +fs.writeFileSync(tempFile, 'abc\ndef'); + +// A sanity check when using numbers instead of strings +const sanity = 'def'; +const saneEmitter = fs.createReadStream(tempFile, { start: 4, end: 6 }); + +assert.throws( + () => { + fs.createReadStream(tempFile, { start: '4', end: 6 }); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +assert.throws( + () => { + fs.createReadStream(tempFile, { start: 4, end: '6' }); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +assert.throws( + () => { + fs.createWriteStream(tempFile, { start: '4' }); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +saneEmitter.on('data', common.mustCall(function(data) { + assert.strictEqual( + sanity, data.toString('utf8'), + `read ${data.toString('utf8')} instead of ${sanity}`); +})); diff --git a/test/js/node/test/parallel/test-fs-null-bytes.js b/test/js/node/test/parallel/test-fs-null-bytes.js new file mode 100644 index 0000000000..302d37196f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-null-bytes.js @@ -0,0 +1,158 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +function check(async, sync) { + const argsSync = Array.prototype.slice.call(arguments, 2); + const argsAsync = argsSync.concat(common.mustNotCall()); + + if (sync) { + assert.throws( + () => { + sync.apply(null, argsSync); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + }); + } + + if (async) { + assert.throws( + () => { + async.apply(null, argsAsync); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError' + }); + } +} + +check(fs.access, fs.accessSync, 'foo\u0000bar'); +check(fs.access, fs.accessSync, 'foo\u0000bar', fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, 'foo\u0000bar', 'abc'); +check(fs.chmod, fs.chmodSync, 'foo\u0000bar', '0644'); +check(fs.chown, fs.chownSync, 'foo\u0000bar', 12, 34); +check(fs.copyFile, fs.copyFileSync, 'foo\u0000bar', 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', 'foo\u0000bar'); +check(fs.lchown, fs.lchownSync, 'foo\u0000bar', 12, 34); +check(fs.link, fs.linkSync, 'foo\u0000bar', 'foobar'); +check(fs.link, fs.linkSync, 'foobar', 'foo\u0000bar'); +check(fs.lstat, fs.lstatSync, 'foo\u0000bar'); +check(fs.mkdir, fs.mkdirSync, 'foo\u0000bar', '0755'); +check(fs.open, fs.openSync, 'foo\u0000bar', 'r'); +check(fs.readFile, fs.readFileSync, 'foo\u0000bar'); +check(fs.readdir, fs.readdirSync, 'foo\u0000bar'); +check(fs.readdir, fs.readdirSync, 'foo\u0000bar', { recursive: true }); +check(fs.readlink, fs.readlinkSync, 'foo\u0000bar'); +check(fs.realpath, fs.realpathSync, 'foo\u0000bar'); +check(fs.rename, fs.renameSync, 'foo\u0000bar', 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', 'foo\u0000bar'); +check(fs.rmdir, fs.rmdirSync, 'foo\u0000bar'); +check(fs.stat, fs.statSync, 'foo\u0000bar'); +check(fs.symlink, fs.symlinkSync, 'foo\u0000bar', 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', 'foo\u0000bar'); +check(fs.truncate, fs.truncateSync, 'foo\u0000bar'); +check(fs.unlink, fs.unlinkSync, 'foo\u0000bar'); +check(null, fs.unwatchFile, 'foo\u0000bar', common.mustNotCall()); +check(fs.utimes, fs.utimesSync, 'foo\u0000bar', 0, 0); +check(null, fs.watch, 'foo\u0000bar', common.mustNotCall()); +check(null, fs.watchFile, 'foo\u0000bar', common.mustNotCall()); +check(fs.writeFile, fs.writeFileSync, 'foo\u0000bar', 'abc'); + +const fileUrl = new URL('file:///C:/foo\u0000bar'); +const fileUrl2 = new URL('file:///C:/foo%00bar'); + +check(fs.access, fs.accessSync, fileUrl); +check(fs.access, fs.accessSync, fileUrl, fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, fileUrl, 'abc'); +check(fs.chmod, fs.chmodSync, fileUrl, '0644'); +check(fs.chown, fs.chownSync, fileUrl, 12, 34); +check(fs.copyFile, fs.copyFileSync, fileUrl, 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', fileUrl); +check(fs.lchown, fs.lchownSync, fileUrl, 12, 34); +check(fs.link, fs.linkSync, fileUrl, 'foobar'); +check(fs.link, fs.linkSync, 'foobar', fileUrl); +check(fs.lstat, fs.lstatSync, fileUrl); +check(fs.mkdir, fs.mkdirSync, fileUrl, '0755'); +check(fs.open, fs.openSync, fileUrl, 'r'); +check(fs.readFile, fs.readFileSync, fileUrl); +check(fs.readdir, fs.readdirSync, fileUrl); +check(fs.readdir, fs.readdirSync, fileUrl, { recursive: true }); +check(fs.readlink, fs.readlinkSync, fileUrl); +check(fs.realpath, fs.realpathSync, fileUrl); +check(fs.rename, fs.renameSync, fileUrl, 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', fileUrl); +check(fs.rmdir, fs.rmdirSync, fileUrl); +check(fs.stat, fs.statSync, fileUrl); +check(fs.symlink, fs.symlinkSync, fileUrl, 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', fileUrl); +check(fs.truncate, fs.truncateSync, fileUrl); +check(fs.unlink, fs.unlinkSync, fileUrl); +check(null, fs.unwatchFile, fileUrl, assert.fail); +check(fs.utimes, fs.utimesSync, fileUrl, 0, 0); +check(null, fs.watch, fileUrl, assert.fail); +check(null, fs.watchFile, fileUrl, assert.fail); +check(fs.writeFile, fs.writeFileSync, fileUrl, 'abc'); + +check(fs.access, fs.accessSync, fileUrl2); +check(fs.access, fs.accessSync, fileUrl2, fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, fileUrl2, 'abc'); +check(fs.chmod, fs.chmodSync, fileUrl2, '0644'); +check(fs.chown, fs.chownSync, fileUrl2, 12, 34); +check(fs.copyFile, fs.copyFileSync, fileUrl2, 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', fileUrl2); +check(fs.lchown, fs.lchownSync, fileUrl2, 12, 34); +check(fs.link, fs.linkSync, fileUrl2, 'foobar'); +check(fs.link, fs.linkSync, 'foobar', fileUrl2); +check(fs.lstat, fs.lstatSync, fileUrl2); +check(fs.mkdir, fs.mkdirSync, fileUrl2, '0755'); +check(fs.open, fs.openSync, fileUrl2, 'r'); +check(fs.readFile, fs.readFileSync, fileUrl2); +check(fs.readdir, fs.readdirSync, fileUrl2); +check(fs.readdir, fs.readdirSync, fileUrl2, { recursive: true }); +check(fs.readlink, fs.readlinkSync, fileUrl2); +check(fs.realpath, fs.realpathSync, fileUrl2); +check(fs.rename, fs.renameSync, fileUrl2, 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', fileUrl2); +check(fs.rmdir, fs.rmdirSync, fileUrl2); +check(fs.stat, fs.statSync, fileUrl2); +check(fs.symlink, fs.symlinkSync, fileUrl2, 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', fileUrl2); +check(fs.truncate, fs.truncateSync, fileUrl2); +check(fs.unlink, fs.unlinkSync, fileUrl2); +check(null, fs.unwatchFile, fileUrl2, assert.fail); +check(fs.utimes, fs.utimesSync, fileUrl2, 0, 0); +check(null, fs.watch, fileUrl2, assert.fail); +check(null, fs.watchFile, fileUrl2, assert.fail); +check(fs.writeFile, fs.writeFileSync, fileUrl2, 'abc'); + +// An 'error' for exists means that it doesn't exist. +// One of many reasons why this file is the absolute worst. +fs.exists('foo\u0000bar', common.mustCall((exists) => { + assert(!exists); +})); +assert(!fs.existsSync('foo\u0000bar')); diff --git a/test/js/node/test/parallel/test-fs-open-mode-mask.js b/test/js/node/test/parallel/test-fs-open-mode-mask.js new file mode 100644 index 0000000000..a79591c0ae --- /dev/null +++ b/test/js/node/test/parallel/test-fs-open-mode-mask.js @@ -0,0 +1,40 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in fs.open(). + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const mode = common.isWindows ? 0o444 : 0o644; + +const maskToIgnore = 0o10000; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function test(mode, asString) { + const suffix = asString ? 'str' : 'num'; + const input = asString ? + (mode | maskToIgnore).toString(8) : (mode | maskToIgnore); + + { + const file = tmpdir.resolve(`openSync-${suffix}.txt`); + const fd = fs.openSync(file, 'w+', input); + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + fs.closeSync(fd); + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + } + + { + const file = tmpdir.resolve(`open-${suffix}.txt`); + fs.open(file, 'w+', input, common.mustSucceed((fd) => { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + fs.closeSync(fd); + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + })); + } +} + +test(mode, true); +test(mode, false); diff --git a/test/js/node/test/parallel/test-fs-open-no-close.js b/test/js/node/test/parallel/test-fs-open-no-close.js new file mode 100644 index 0000000000..41b58f5ef7 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-open-no-close.js @@ -0,0 +1,31 @@ +'use strict'; + +// Refs: https://github.com/nodejs/node/issues/34266 +// Failing to close a file should not keep the event loop open. + +const common = require('../common'); +const assert = require('assert'); + +const fs = require('fs'); + +const debuglog = (arg) => { + console.log(new Date().toLocaleString(), arg); +}; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +let openFd; + +fs.open(`${tmpdir.path}/dummy`, 'wx+', common.mustCall((err, fd) => { + debuglog('fs open() callback'); + assert.ifError(err); + openFd = fd; +})); +debuglog('waiting for callback'); + +process.on('beforeExit', common.mustCall(() => { + if (openFd) { + fs.closeSync(openFd); + } +})); diff --git a/test/js/node/test/parallel/test-fs-open-numeric-flags.js b/test/js/node/test/parallel/test-fs-open-numeric-flags.js new file mode 100644 index 0000000000..3237b2764e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-open-numeric-flags.js @@ -0,0 +1,15 @@ +'use strict'; +require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// O_WRONLY without O_CREAT shall fail with ENOENT +const pathNE = tmpdir.resolve('file-should-not-exist'); +assert.throws( + () => fs.openSync(pathNE, fs.constants.O_WRONLY), + (e) => e.code === 'ENOENT' +); diff --git a/test/js/node/test/parallel/test-fs-open.js b/test/js/node/test/parallel/test-fs-open.js new file mode 100644 index 0000000000..0855e521f7 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-open.js @@ -0,0 +1,120 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +let caughtException = false; + +try { + // Should throw ENOENT, not EBADF + // see https://github.com/joyent/node/pull/1228 + fs.openSync('/8hvftyuncxrt/path/to/file/that/does/not/exist', 'r'); +} catch (e) { + assert.strictEqual(e.code, 'ENOENT'); + caughtException = true; +} +assert.strictEqual(caughtException, true); + +fs.openSync(__filename); + +fs.open(__filename, common.mustSucceed()); + +fs.open(__filename, 'r', common.mustSucceed()); + +fs.open(__filename, 'rs', common.mustSucceed()); + +fs.open(__filename, 'r', 0, common.mustSucceed()); + +fs.open(__filename, 'r', null, common.mustSucceed()); + +async function promise() { + await fs.promises.open(__filename); + await fs.promises.open(__filename, 'r'); +} + +promise().then(common.mustCall()).catch(common.mustNotCall()); + +assert.throws( + () => fs.open(__filename, 'r', 'boom', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError' + } +); + +for (const extra of [[], ['r'], ['r', 0], ['r', 0, 'bad callback']]) { + assert.throws( + () => fs.open(__filename, ...extra), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +} + +[false, 1, [], {}, null, undefined].forEach((i) => { + assert.throws( + () => fs.open(i, 'r', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.openSync(i, 'r', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.rejects( + fs.promises.open(i, 'r'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ).then(common.mustCall()); +}); + +// Check invalid modes. +[false, [], {}].forEach((mode) => { + assert.throws( + () => fs.open(__filename, 'r', mode, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE' + } + ); + assert.throws( + () => fs.openSync(__filename, 'r', mode, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE' + } + ); + assert.rejects( + fs.promises.open(__filename, 'r', mode), + { + code: 'ERR_INVALID_ARG_TYPE' + } + ).then(common.mustCall()); +}); diff --git a/test/js/node/test/parallel/test-fs-options-immutable.js b/test/js/node/test/parallel/test-fs-options-immutable.js new file mode 100644 index 0000000000..fffdecdc87 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-options-immutable.js @@ -0,0 +1,70 @@ +'use strict'; +const common = require('../common'); + +// These tests make sure that the `options` object passed to these functions are +// never altered. +// +// Refer: https://github.com/nodejs/node/issues/7655 + +const fs = require('fs'); + +const options = common.mustNotMutateObjectDeep({}); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +fs.readFile(__filename, options, common.mustSucceed()); +fs.readFileSync(__filename, options); + +fs.readdir(__dirname, options, common.mustSucceed()); +fs.readdirSync(__dirname, options); + +if (common.canCreateSymLink()) { + const sourceFile = tmpdir.resolve('test-readlink'); + const linkFile = tmpdir.resolve('test-readlink-link'); + + fs.writeFileSync(sourceFile, ''); + fs.symlinkSync(sourceFile, linkFile); + + fs.readlink(linkFile, options, common.mustSucceed()); + fs.readlinkSync(linkFile, options); +} + +{ + const fileName = tmpdir.resolve('writeFile'); + fs.writeFileSync(fileName, 'ABCD', options); + fs.writeFile(fileName, 'ABCD', options, common.mustSucceed()); +} + +{ + const fileName = tmpdir.resolve('appendFile'); + fs.appendFileSync(fileName, 'ABCD', options); + fs.appendFile(fileName, 'ABCD', options, common.mustSucceed()); +} + +if (!common.isIBMi) { // IBMi does not support fs.watch() + const watch = fs.watch(__filename, options, common.mustNotCall()); + watch.close(); +} + +{ + fs.watchFile(__filename, options, common.mustNotCall()); + fs.unwatchFile(__filename); +} + +{ + fs.realpathSync(__filename, options); + fs.realpath(__filename, options, common.mustSucceed()); +} + +{ + const tempFileName = tmpdir.resolve('mkdtemp-'); + fs.mkdtempSync(tempFileName, options); + fs.mkdtemp(tempFileName, options, common.mustSucceed()); +} + +{ + const fileName = tmpdir.resolve('streams'); + fs.WriteStream(fileName, options).once('open', common.mustCall(() => { + fs.ReadStream(fileName, options).destroy(); + })).end(); +} diff --git a/test/js/node/test/parallel/test-fs-promises-exists.js b/test/js/node/test/parallel/test-fs-promises-exists.js new file mode 100644 index 0000000000..b9bbeee1de --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-exists.js @@ -0,0 +1,9 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fsPromises = require('fs/promises'); + +assert.strictEqual(fsPromises, fs.promises); +assert.strictEqual(fsPromises.constants, fs.constants); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-append-file.js b/test/js/node/test/parallel/test-fs-promises-file-handle-append-file.js new file mode 100644 index 0000000000..90bb6e3925 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-append-file.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.appendFile method. + +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateAppendBuffer() { + const filePath = path.resolve(tmpDir, 'tmp-append-file-buffer.txt'); + const fileHandle = await open(filePath, 'a'); + const buffer = Buffer.from('a&Dp'.repeat(100), 'utf8'); + + await fileHandle.appendFile(buffer); + const appendedFileData = fs.readFileSync(filePath); + assert.deepStrictEqual(appendedFileData, buffer); + + await fileHandle.close(); +} + +async function validateAppendString() { + const filePath = path.resolve(tmpDir, 'tmp-append-file-string.txt'); + const fileHandle = await open(filePath, 'a'); + const string = 'x~yz'.repeat(100); + + await fileHandle.appendFile(string); + const stringAsBuffer = Buffer.from(string, 'utf8'); + const appendedFileData = fs.readFileSync(filePath); + assert.deepStrictEqual(appendedFileData, stringAsBuffer); + + await fileHandle.close(); +} + +validateAppendBuffer() + .then(validateAppendString) + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-chmod.js b/test/js/node/test/parallel/test-fs-promises-file-handle-chmod.js new file mode 100644 index 0000000000..5c7414a9b1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-chmod.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.chmod method. + +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateFilePermission() { + const filePath = path.resolve(tmpDir, 'tmp-chmod.txt'); + const fileHandle = await open(filePath, 'w+', 0o444); + // File created with r--r--r-- 444 + const statsBeforeMod = fs.statSync(filePath); + assert.strictEqual(statsBeforeMod.mode & 0o444, 0o444); + + let expectedAccess; + const newPermissions = 0o765; + + if (common.isWindows) { + // Chmod in Windows will only toggle read only/write access. The + // fs.Stats.mode in Windows is computed using read/write + // bits (not exec). Read-only at best returns 444; r/w 666. + // Refer: /deps/uv/src/win/fs.cfs; + expectedAccess = 0o664; + } else { + expectedAccess = newPermissions; + } + + // Change the permissions to rwxr--r-x + await fileHandle.chmod(newPermissions); + const statsAfterMod = fs.statSync(filePath); + assert.deepStrictEqual(statsAfterMod.mode & expectedAccess, expectedAccess); + + await fileHandle.close(); +} + +validateFilePermission().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-dispose.js b/test/js/node/test/parallel/test-fs-promises-file-handle-dispose.js new file mode 100644 index 0000000000..406430eef4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-dispose.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); +const { promises: fs } = require('fs'); + +async function doOpen() { + const fh = await fs.open(__filename); + fh.on('close', common.mustCall()); + await fh[Symbol.asyncDispose](); +} + +doOpen().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js b/test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js new file mode 100644 index 0000000000..7ae881801a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js @@ -0,0 +1,54 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const file = tmpdir.resolve('read_stream_filehandle_worker.txt'); +const input = 'hello world'; +const { Worker, isMainThread, workerData } = require('worker_threads'); + +if (isMainThread || !workerData) { + tmpdir.refresh(); + fs.writeFileSync(file, input); + + fs.promises.open(file, 'r').then((handle) => { + handle.on('close', common.mustNotCall()); + new Worker(__filename, { + workerData: { handle }, + transferList: [handle] + }); + }); + fs.promises.open(file, 'r').then(async (handle) => { + try { + fs.createReadStream(null, { fd: handle }); + assert.throws(() => { + new Worker(__filename, { + workerData: { handle }, + transferList: [handle] + }); + }, { + code: 25, + name: 'DataCloneError', + }); + } finally { + await handle.close(); + } + }); +} else { + let output = ''; + + const handle = workerData.handle; + handle.on('close', common.mustCall()); + const stream = fs.createReadStream(null, { fd: handle }); + + stream.on('data', common.mustCallAtLeast((data) => { + output += data; + })); + + stream.on('end', common.mustCall(() => { + handle.close(); + assert.strictEqual(output, input); + })); + + stream.on('close', common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-stat.js b/test/js/node/test/parallel/test-fs-promises-file-handle-stat.js new file mode 100644 index 0000000000..c989c405c5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-stat.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.stat method. + +const { open } = require('fs').promises; +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); + +tmpdir.refresh(); + +async function validateStat() { + const filePath = tmpdir.resolve('tmp-read-file.txt'); + const fileHandle = await open(filePath, 'w+'); + const stats = await fileHandle.stat(); + assert.ok(stats.mtime instanceof Date); + await fileHandle.close(); +} + +validateStat() + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js b/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js new file mode 100644 index 0000000000..71f312b6f9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.write method. + +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { finished } = require('stream/promises'); +const { buffer } = require('stream/consumers'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateWrite() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-write.txt'); + const fileHandle = await open(filePathForHandle, 'w'); + const buffer = Buffer.from('Hello world'.repeat(100), 'utf8'); + + const stream = fileHandle.createWriteStream(); + stream.end(buffer); + await finished(stream); + + const readFileData = fs.readFileSync(filePathForHandle); + assert.deepStrictEqual(buffer, readFileData); +} + +async function validateRead() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-read.txt'); + const buf = Buffer.from('Hello world'.repeat(100), 'utf8'); + + fs.writeFileSync(filePathForHandle, buf); + + const fileHandle = await open(filePathForHandle); + assert.deepStrictEqual( + await buffer(fileHandle.createReadStream()), + buf + ); +} + +Promise.all([ + validateWrite(), + validateRead(), +]).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js b/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js new file mode 100644 index 0000000000..ac2f18e9bb --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js @@ -0,0 +1,35 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +const { access, copyFile, open } = require('fs').promises; + +async function validate() { + tmpdir.refresh(); + const dest = tmpdir.resolve('baz.js'); + await assert.rejects( + copyFile(fixtures.path('baz.js'), dest, 'r'), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + await copyFile(fixtures.path('baz.js'), dest); + await assert.rejects( + access(dest, 'r'), + { code: 'ERR_INVALID_ARG_TYPE', message: /mode/ } + ); + await access(dest); + const handle = await open(dest, 'r+'); + await handle.datasync(); + await handle.sync(); + const buf = Buffer.from('hello world'); + await handle.write(buf); + const ret = await handle.read(Buffer.alloc(11), 0, 11, 0); + assert.strictEqual(ret.bytesRead, 11); + assert.deepStrictEqual(ret.buffer, buf); + await handle.close(); +} + +validate(); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-truncate.js b/test/js/node/test/parallel/test-fs-promises-file-handle-truncate.js new file mode 100644 index 0000000000..687f8c2725 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-truncate.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { open, readFile } = require('fs').promises; +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +async function validateTruncate() { + const text = 'Hello world'; + const filename = tmpdir.resolve('truncate-file.txt'); + const fileHandle = await open(filename, 'w+'); + + const buffer = Buffer.from(text, 'utf8'); + await fileHandle.write(buffer, 0, buffer.length); + + assert.strictEqual((await readFile(filename)).toString(), text); + + await fileHandle.truncate(5); + assert.strictEqual((await readFile(filename)).toString(), 'Hello'); + + await fileHandle.close(); +} + +validateTruncate().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-write.js b/test/js/node/test/parallel/test-fs-promises-file-handle-write.js new file mode 100644 index 0000000000..7f3d12d481 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-write.js @@ -0,0 +1,77 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.write method. + +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateWrite() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-write.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + const buffer = Buffer.from('Hello world'.repeat(100), 'utf8'); + + await fileHandle.write(buffer, 0, buffer.length); + const readFileData = fs.readFileSync(filePathForHandle); + assert.deepStrictEqual(buffer, readFileData); + + await fileHandle.close(); +} + +async function validateEmptyWrite() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-empty-write.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + const buffer = Buffer.from(''); // empty buffer + + await fileHandle.write(buffer, 0, buffer.length); + const readFileData = fs.readFileSync(filePathForHandle); + assert.deepStrictEqual(buffer, readFileData); + + await fileHandle.close(); +} + +async function validateNonUint8ArrayWrite() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-data-write.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + const buffer = Buffer.from('Hello world', 'utf8').toString('base64'); + + await fileHandle.write(buffer, 0, buffer.length); + const readFileData = fs.readFileSync(filePathForHandle); + assert.deepStrictEqual(Buffer.from(buffer, 'utf8'), readFileData); + + await fileHandle.close(); +} + +async function validateNonStringValuesWrite() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-non-string-write.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + const nonStringValues = [ + 123, {}, new Map(), null, undefined, 0n, () => {}, Symbol(), true, + new String('notPrimitive'), + { toString() { return 'amObject'; } }, + { [Symbol.toPrimitive]: (hint) => 'amObject' }, + ]; + for (const nonStringValue of nonStringValues) { + await assert.rejects( + fileHandle.write(nonStringValue), + { message: /"buffer"/, code: 'ERR_INVALID_ARG_TYPE' } + ); + } + + await fileHandle.close(); +} + +Promise.all([ + validateWrite(), + validateEmptyWrite(), + validateNonUint8ArrayWrite(), + validateNonStringValuesWrite(), +]).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-readfile-empty.js b/test/js/node/test/parallel/test-fs-promises-readfile-empty.js new file mode 100644 index 0000000000..ef15a26811 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-readfile-empty.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); + +const assert = require('assert'); +const { promises: fs } = require('fs'); +const fixtures = require('../common/fixtures'); + +const fn = fixtures.path('empty.txt'); + +fs.readFile(fn) + .then(assert.ok); + +fs.readFile(fn, 'utf8') + .then(assert.strictEqual.bind(this, '')); + +fs.readFile(fn, { encoding: 'utf8' }) + .then(assert.strictEqual.bind(this, '')); diff --git a/test/js/node/test/parallel/test-fs-promises-readfile-with-fd.js b/test/js/node/test/parallel/test-fs-promises-readfile-with-fd.js new file mode 100644 index 0000000000..b5d1c26168 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-readfile-with-fd.js @@ -0,0 +1,34 @@ +'use strict'; + +// This test makes sure that `readFile()` always reads from the current +// position of the file, instead of reading from the beginning of the file. + +const common = require('../common'); +const assert = require('assert'); +const { writeFileSync } = require('fs'); +const { open } = require('fs').promises; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const fn = tmpdir.resolve('test.txt'); +writeFileSync(fn, 'Hello World'); + +async function readFileTest() { + const handle = await open(fn, 'r'); + + /* Read only five bytes, so that the position moves to five. */ + const buf = Buffer.alloc(5); + const { bytesRead } = await handle.read(buf, 0, 5, null); + assert.strictEqual(bytesRead, 5); + assert.strictEqual(buf.toString(), 'Hello'); + + /* readFile() should read from position five, instead of zero. */ + assert.strictEqual((await handle.readFile()).toString(), ' World'); + + await handle.close(); +} + + +readFileTest() + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-watch.js b/test/js/node/test/parallel/test-fs-promises-watch.js new file mode 100644 index 0000000000..692ed33dbc --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-watch.js @@ -0,0 +1,136 @@ +'use strict'; +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const { watch } = require('fs/promises'); +const fs = require('fs'); +const assert = require('assert'); +const { join } = require('path'); +const { setTimeout } = require('timers/promises'); +const tmpdir = require('../common/tmpdir'); + +class WatchTestCase { + constructor(shouldInclude, dirName, fileName, field) { + this.dirName = dirName; + this.fileName = fileName; + this.field = field; + this.shouldSkip = !shouldInclude; + } + get dirPath() { return tmpdir.resolve(this.dirName); } + get filePath() { return join(this.dirPath, this.fileName); } +} + +const kCases = [ + // Watch on a directory should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows || common.isAIX, + 'watch1', + 'foo', + 'filePath' + ), + // Watch on a file should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows, + 'watch2', + 'bar', + 'dirPath' + ), +]; + +tmpdir.refresh(); + +for (const testCase of kCases) { + if (testCase.shouldSkip) continue; + fs.mkdirSync(testCase.dirPath); + // Long content so it's actually flushed. + const content1 = Date.now() + testCase.fileName.toLowerCase().repeat(1e4); + fs.writeFileSync(testCase.filePath, content1); + + let interval; + async function test() { + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + await setTimeout(common.platformTimeout(100)); + } + + const watcher = watch(testCase[testCase.field]); + for await (const { eventType, filename } of watcher) { + clearInterval(interval); + assert.strictEqual(['rename', 'change'].includes(eventType), true); + assert.strictEqual(filename, testCase.fileName); + break; + } + + // Waiting on it again is a non-op + // eslint-disable-next-line no-unused-vars + for await (const p of watcher) { + assert.fail('should not run'); + } + } + + // Long content so it's actually flushed. toUpperCase so there's real change. + const content2 = Date.now() + testCase.fileName.toUpperCase().repeat(1e4); + interval = setInterval(() => { + fs.writeFileSync(testCase.filePath, ''); + fs.writeFileSync(testCase.filePath, content2); + }, 100); + + test().then(common.mustCall()); +} + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(1)) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(__filename, 1)) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { persistent: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { recursive: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { encoding: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { signal: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +(async () => { + const ac = new AbortController(); + const { signal } = ac; + setImmediate(() => ac.abort()); + try { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(__filename, { signal })) { } + } catch (err) { + assert.strictEqual(err.name, 'AbortError'); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-writefile-typedarray.js b/test/js/node/test/parallel/test-fs-promises-writefile-typedarray.js new file mode 100644 index 0000000000..32d9cffa23 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-writefile-typedarray.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const fsPromises = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +const dest = path.resolve(tmpDir, 'tmp.txt'); +// Use a file size larger than `kReadFileMaxChunkSize`. +const buffer = Buffer.from('012'.repeat(2 ** 14)); + +(async () => { + for (const Constructor of [Uint8Array, Uint16Array, Uint32Array]) { + const array = new Constructor(buffer.buffer); + await fsPromises.writeFile(dest, array); + const data = await fsPromises.readFile(dest); + assert.deepStrictEqual(data, buffer); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-writefile-with-fd.js b/test/js/node/test/parallel/test-fs-promises-writefile-with-fd.js new file mode 100644 index 0000000000..7cb9eeeca9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-writefile-with-fd.js @@ -0,0 +1,35 @@ +'use strict'; + +// This test makes sure that `writeFile()` always writes from the current +// position of the file, instead of truncating the file. + +const common = require('../common'); +const assert = require('assert'); +const { readFileSync } = require('fs'); +const { open } = require('fs').promises; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const fn = tmpdir.resolve('test.txt'); + +async function writeFileTest() { + const handle = await open(fn, 'w'); + + /* Write only five bytes, so that the position moves to five. */ + const buf = Buffer.from('Hello'); + const { bytesWritten } = await handle.write(buf, 0, 5, null); + assert.strictEqual(bytesWritten, 5); + + /* Write some more with writeFile(). */ + await handle.writeFile('World'); + + /* New content should be written at position five, instead of zero. */ + assert.strictEqual(readFileSync(fn).toString(), 'HelloWorld'); + + await handle.close(); +} + + +writeFileTest() + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promisified.js b/test/js/node/test/parallel/test-fs-promisified.js new file mode 100644 index 0000000000..63430f64a8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promisified.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { promisify } = require('util'); + +const read = promisify(fs.read); +const write = promisify(fs.write); +const exists = promisify(fs.exists); + +{ + const fd = fs.openSync(__filename, 'r'); + read(fd, Buffer.alloc(1024), 0, 1024, null).then(common.mustCall((obj) => { + assert.strictEqual(typeof obj.bytesRead, 'number'); + assert(obj.buffer instanceof Buffer); + fs.closeSync(fd); + })); +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +{ + const filename = tmpdir.resolve('write-promise.txt'); + const fd = fs.openSync(filename, 'w'); + write(fd, Buffer.from('foobar')).then(common.mustCall((obj) => { + assert.strictEqual(typeof obj.bytesWritten, 'number'); + assert.strictEqual(obj.buffer.toString(), 'foobar'); + fs.closeSync(fd); + })); +} + +{ + exists(__filename).then(common.mustCall((x) => { + assert.strictEqual(x, true); + })); +} diff --git a/test/js/node/test/parallel/test-fs-read-empty-buffer.js b/test/js/node/test/parallel/test-fs-read-empty-buffer.js new file mode 100644 index 0000000000..6abfcb5aae --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-empty-buffer.js @@ -0,0 +1,41 @@ +'use strict'; +require('../common'); +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); +const fsPromises = fs.promises; + +const buffer = new Uint8Array(); + +assert.throws( + () => fs.readSync(fd, buffer, 0, 10, 0), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } +); + +assert.throws( + () => fs.read(fd, buffer, 0, 1, 0, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } +); + +(async () => { + const filehandle = await fsPromises.open(filepath, 'r'); + assert.rejects( + () => filehandle.read(buffer, 0, 1, 0), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } + ).then(common.mustCall()); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-read-file-assert-encoding.js b/test/js/node/test/parallel/test-fs-read-file-assert-encoding.js new file mode 100644 index 0000000000..77cd0e3ab1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-file-assert-encoding.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const encoding = 'foo-8'; +const filename = 'bar.txt'; +assert.throws( + () => fs.readFile(filename, { encoding }, common.mustNotCall()), + { code: 'ERR_INVALID_ARG_VALUE', name: 'TypeError' } +); diff --git a/test/js/node/test/parallel/test-fs-read-file-sync-hostname.js b/test/js/node/test/parallel/test-fs-read-file-sync-hostname.js new file mode 100644 index 0000000000..599f48b6cc --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-file-sync-hostname.js @@ -0,0 +1,33 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.isLinux) + common.skip('Test is linux specific.'); + +const assert = require('assert'); +const fs = require('fs'); + +// Test to make sure reading a file under the /proc directory works. See: +// https://groups.google.com/forum/#!topic/nodejs-dev/rxZ_RoH1Gn0 +const hostname = fs.readFileSync('/proc/sys/kernel/hostname'); +assert.ok(hostname.length > 0); diff --git a/test/js/node/test/parallel/test-fs-read-file-sync.js b/test/js/node/test/parallel/test-fs-read-file-sync.js new file mode 100644 index 0000000000..e95c96d1c4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-file-sync.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +const fn = fixtures.path('elipses.txt'); +tmpdir.refresh(); + +const s = fs.readFileSync(fn, 'utf8'); +for (let i = 0; i < s.length; i++) { + assert.strictEqual(s[i], '\u2026'); +} +assert.strictEqual(s.length, 10000); + +// Test file permissions set for readFileSync() in append mode. +{ + const expectedMode = 0o666 & ~process.umask(); + + for (const test of [ + { }, + { encoding: 'ascii' }, + { encoding: 'base64' }, + { encoding: 'hex' }, + { encoding: 'latin1' }, + { encoding: 'uTf8' }, // case variation + { encoding: 'utf16le' }, + { encoding: 'utf8' }, + ]) { + const opts = { ...test, flag: 'a+' }; + const file = tmpdir.resolve(`testReadFileSyncAppend${opts.encoding ?? ''}.txt`); + const variant = `for '${file}'`; + + const content = fs.readFileSync(file, opts); + assert.strictEqual(opts.encoding ? content : content.toString(), '', `file contents ${variant}`); + assert.strictEqual(fs.statSync(file).mode & 0o777, expectedMode, `file permissions ${variant}`); + } +} diff --git a/test/js/node/test/parallel/test-fs-read-offset-null.js b/test/js/node/test/parallel/test-fs-read-offset-null.js new file mode 100644 index 0000000000..012c94e41e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-offset-null.js @@ -0,0 +1,64 @@ +'use strict'; + + +// Test to assert the desired functioning of fs.read +// when {offset:null} is passed as options parameter + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fsPromises = fs.promises; +const fixtures = require('../common/fixtures'); +const filepath = fixtures.path('x.txt'); + +const buf = Buffer.alloc(1); +// Reading only one character, hence buffer of one byte is enough. + +// Tests are done by making sure the first letter in buffer is +// same as first letter in file. +// 120 is the ascii code of letter x. + +// Tests for callback API. +fs.open(filepath, 'r', common.mustSucceed((fd) => { + fs.read(fd, { offset: null, buffer: buf }, + common.mustSucceed((bytesRead, buffer) => { + assert.strictEqual(buffer[0], 120); + fs.close(fd, common.mustSucceed(() => {})); + })); +})); + +fs.open(filepath, 'r', common.mustSucceed((fd) => { + fs.read(fd, buf, { offset: null }, + common.mustSucceed((bytesRead, buffer) => { + assert.strictEqual(buffer[0], 120); + fs.close(fd, common.mustSucceed(() => {})); + })); +})); + +let filehandle = null; + +// Tests for promises api +(async () => { + filehandle = await fsPromises.open(filepath, 'r'); + const readObject = await filehandle.read(buf, { offset: null }); + assert.strictEqual(readObject.buffer[0], 120); +})() +.finally(() => filehandle?.close()) +.then(common.mustCall()); + +// Undocumented: omitted position works the same as position === null +(async () => { + filehandle = await fsPromises.open(filepath, 'r'); + const readObject = await filehandle.read(buf, null, buf.length); + assert.strictEqual(readObject.buffer[0], 120); +})() +.finally(() => filehandle?.close()) +.then(common.mustCall()); + +(async () => { + filehandle = await fsPromises.open(filepath, 'r'); + const readObject = await filehandle.read(buf, null, buf.length, 0); + assert.strictEqual(readObject.buffer[0], 120); +})() +.finally(() => filehandle?.close()) +.then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-read-optional-params.js b/test/js/node/test/parallel/test-fs-read-optional-params.js new file mode 100644 index 0000000000..e89f86ee5f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-optional-params.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const assert = require('assert'); +const filepath = fixtures.path('x.txt'); + +const expected = Buffer.from('xyz\n'); +const defaultBufferAsync = Buffer.alloc(16384); +const bufferAsOption = Buffer.allocUnsafe(expected.byteLength); + +function testValid(message, ...options) { + const paramsMsg = `${message} (as params)`; + const paramsFilehandle = fs.openSync(filepath, 'r'); + fs.read(paramsFilehandle, ...options, common.mustSucceed((bytesRead, buffer) => { + assert.strictEqual(bytesRead, expected.byteLength, paramsMsg); + assert.deepStrictEqual(defaultBufferAsync.byteLength, buffer.byteLength, paramsMsg); + fs.closeSync(paramsFilehandle); + })); + + const optionsMsg = `${message} (as options)`; + const optionsFilehandle = fs.openSync(filepath, 'r'); + fs.read(optionsFilehandle, bufferAsOption, ...options, common.mustSucceed((bytesRead, buffer) => { + assert.strictEqual(bytesRead, expected.byteLength, optionsMsg); + assert.deepStrictEqual(bufferAsOption.byteLength, buffer.byteLength, optionsMsg); + fs.closeSync(optionsFilehandle); + })); +} + +testValid('Not passing in any object'); +testValid('Passing in a null', null); +testValid('Passing in an empty object', common.mustNotMutateObjectDeep({})); +testValid('Passing in an object', common.mustNotMutateObjectDeep({ + offset: 0, + length: bufferAsOption.byteLength, + position: 0, +})); diff --git a/test/js/node/test/parallel/test-fs-read-promises-optional-params.js b/test/js/node/test/parallel/test-fs-read-promises-optional-params.js new file mode 100644 index 0000000000..f9007a69ba --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-promises-optional-params.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const read = require('util').promisify(fs.read); +const assert = require('assert'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); + +const expected = Buffer.from('xyz\n'); +const defaultBufferAsync = Buffer.alloc(16384); +const bufferAsOption = Buffer.allocUnsafe(expected.byteLength); + +read(fd, common.mustNotMutateObjectDeep({})) + .then(function({ bytesRead, buffer }) { + assert.strictEqual(bytesRead, expected.byteLength); + assert.deepStrictEqual(defaultBufferAsync.byteLength, buffer.byteLength); + }) + .then(common.mustCall()); + +read(fd, bufferAsOption, common.mustNotMutateObjectDeep({ position: 0 })) + .then(function({ bytesRead, buffer }) { + assert.strictEqual(bytesRead, expected.byteLength); + assert.deepStrictEqual(bufferAsOption.byteLength, buffer.byteLength); + }) + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-read-stream-autoClose.js b/test/js/node/test/parallel/test-fs-read-stream-autoClose.js new file mode 100644 index 0000000000..728d4f5eee --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-autoClose.js @@ -0,0 +1,15 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const writeFile = tmpdir.resolve('write-autoClose.txt'); +tmpdir.refresh(); + +const file = fs.createWriteStream(writeFile, { autoClose: true }); + +file.on('finish', common.mustCall(() => { + assert.strictEqual(file.destroyed, false); +})); +file.end('asd'); diff --git a/test/js/node/test/parallel/test-fs-read-stream-double-close.js b/test/js/node/test/parallel/test-fs-read-stream-double-close.js new file mode 100644 index 0000000000..32117a0a1e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-double-close.js @@ -0,0 +1,19 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); + +{ + const s = fs.createReadStream(__filename); + + s.close(common.mustCall()); + s.close(common.mustCall()); +} + +{ + const s = fs.createReadStream(__filename); + + // This is a private API, but it is worth testing. close calls this + s.destroy(null, common.mustCall()); + s.destroy(null, common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-read-stream-encoding.js b/test/js/node/test/parallel/test-fs-read-stream-encoding.js new file mode 100644 index 0000000000..8eeaee6572 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-encoding.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const stream = require('stream'); +const fixtures = require('../common/fixtures'); +const encoding = 'base64'; + +const example = fixtures.path('x.txt'); +const assertStream = new stream.Writable({ + write: function(chunk, enc, next) { + const expected = Buffer.from('xyz'); + assert(chunk.equals(expected)); + } +}); +assertStream.setDefaultEncoding(encoding); +fs.createReadStream(example, encoding).pipe(assertStream); diff --git a/test/js/node/test/parallel/test-fs-read-stream-fd-leak.js b/test/js/node/test/parallel/test-fs-read-stream-fd-leak.js new file mode 100644 index 0000000000..81712def76 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-fd-leak.js @@ -0,0 +1,62 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +let openCount = 0; +const _fsopen = fs.open; +const _fsclose = fs.close; + +const loopCount = 50; +const totalCheck = 50; +const emptyTxt = fixtures.path('empty.txt'); + +fs.open = function() { + openCount++; + return _fsopen.apply(null, arguments); +}; + +fs.close = function() { + openCount--; + return _fsclose.apply(null, arguments); +}; + +function testLeak(endFn, callback) { + console.log(`testing for leaks from fs.createReadStream().${endFn}()...`); + + let i = 0; + let check = 0; + + function checkFunction() { + if (openCount !== 0 && check < totalCheck) { + check++; + setTimeout(checkFunction, 100); + return; + } + + assert.strictEqual( + openCount, + 0, + `no leaked file descriptors using ${endFn}() (got ${openCount})` + ); + + openCount = 0; + callback && setTimeout(callback, 100); + } + + setInterval(function() { + const s = fs.createReadStream(emptyTxt); + s[endFn](); + + if (++i === loopCount) { + clearTimeout(this); + setTimeout(checkFunction, 100); + } + }, 2); +} + +testLeak('close', function() { + testLeak('destroy'); +}); diff --git a/test/js/node/test/parallel/test-fs-read-stream-fd.js b/test/js/node/test/parallel/test-fs-read-stream-fd.js new file mode 100644 index 0000000000..f40d35b967 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-fd.js @@ -0,0 +1,45 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const file = tmpdir.resolve('read_stream_fd_test.txt'); +const input = 'hello world'; + +let output = ''; +tmpdir.refresh(); +fs.writeFileSync(file, input); + +const fd = fs.openSync(file, 'r'); +const stream = fs.createReadStream(null, { fd: fd, encoding: 'utf8' }); + +assert.strictEqual(stream.path, undefined); + +stream.on('data', common.mustCallAtLeast((data) => { + output += data; +})); + +process.on('exit', () => { + assert.strictEqual(output, input); +}); diff --git a/test/js/node/test/parallel/test-fs-read-stream-resume.js b/test/js/node/test/parallel/test-fs-read-stream-resume.js new file mode 100644 index 0000000000..36e3d809cd --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-resume.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); + +const fs = require('fs'); + +const file = fixtures.path('x.txt'); +let data = ''; +let first = true; + +const stream = fs.createReadStream(file); +stream.setEncoding('utf8'); +stream.on('data', common.mustCallAtLeast(function(chunk) { + data += chunk; + if (first) { + first = false; + stream.resume(); + } +})); + +process.nextTick(function() { + stream.pause(); + setTimeout(function() { + stream.resume(); + }, 100); +}); + +process.on('exit', function() { + assert.strictEqual(data, 'xyz\n'); +}); diff --git a/test/js/node/test/parallel/test-fs-read-zero-length.js b/test/js/node/test/parallel/test-fs-read-zero-length.js new file mode 100644 index 0000000000..ac2efc73f5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-zero-length.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); +const bufferAsync = Buffer.alloc(0); +const bufferSync = Buffer.alloc(0); + +fs.read(fd, bufferAsync, 0, 0, 0, common.mustCall((err, bytesRead) => { + assert.strictEqual(bytesRead, 0); + assert.deepStrictEqual(bufferAsync, Buffer.alloc(0)); +})); + +const r = fs.readSync(fd, bufferSync, 0, 0, 0); +assert.deepStrictEqual(bufferSync, Buffer.alloc(0)); +assert.strictEqual(r, 0); diff --git a/test/js/node/test/parallel/test-fs-readdir-buffer.js b/test/js/node/test/parallel/test-fs-readdir-buffer.js new file mode 100644 index 0000000000..54b7353cb1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-buffer.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); + +if (!common.isMacOS) { + common.skip('this tests works only on MacOS'); +} + +const assert = require('assert'); + +fs.readdir( + Buffer.from('/dev'), + { withFileTypes: true, encoding: 'buffer' }, + common.mustCall((e, d) => { + assert.strictEqual(e, null); + }) +); diff --git a/test/js/node/test/parallel/test-fs-readdir-pipe.js b/test/js/node/test/parallel/test-fs-readdir-pipe.js new file mode 100644 index 0000000000..592e7a3d54 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-pipe.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { readdir, readdirSync } = require('fs'); + +if (!common.isWindows) { + common.skip('This test is specific to Windows to test enumerate pipes'); +} + +// Ref: https://github.com/nodejs/node/issues/56002 +// This test is specific to Windows. + +const pipe = '\\\\.\\pipe\\'; + +const { length } = readdirSync(pipe); +assert.ok(length >= 0, `${length} is not greater or equal to 0`); + +readdir(pipe, common.mustSucceed((files) => { + assert.ok(files.length >= 0, `${files.length} is not greater or equal to 0`); +})); diff --git a/test/js/node/test/parallel/test-fs-readdir-recursive.js b/test/js/node/test/parallel/test-fs-readdir-recursive.js new file mode 100644 index 0000000000..ffe4d03d0a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-recursive.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const fs = require('fs'); +const net = require('net'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const server = net.createServer().listen(common.PIPE, common.mustCall(() => { + // The process should not crash + // See https://github.com/nodejs/node/issues/52159 + fs.readdirSync(tmpdir.path, { recursive: true }); + server.close(); +})); diff --git a/test/js/node/test/parallel/test-fs-readdir-types-symlinks.js b/test/js/node/test/parallel/test-fs-readdir-types-symlinks.js new file mode 100644 index 0000000000..afdbdb1636 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-types-symlinks.js @@ -0,0 +1,36 @@ +'use strict'; + +// Refs: https://github.com/nodejs/node/issues/52663 +const common = require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +if (!common.canCreateSymLink()) + common.skip('insufficient privileges'); + +const tmpdir = require('../common/tmpdir'); +const readdirDir = tmpdir.path; +// clean up the tmpdir +tmpdir.refresh(); + +// a/1, a/2 +const a = path.join(readdirDir, 'a'); +fs.mkdirSync(a); +fs.writeFileSync(path.join(a, '1'), 'irrelevant'); +fs.writeFileSync(path.join(a, '2'), 'irrelevant'); + +// b/1 +const b = path.join(readdirDir, 'b'); +fs.mkdirSync(b); +fs.writeFileSync(path.join(b, '1'), 'irrelevant'); + +// b/c -> a +const c = path.join(readdirDir, 'b', 'c'); +fs.symlinkSync(a, c, 'dir'); + +// Just check that the number of entries are the same +assert.strictEqual( + fs.readdirSync(b, { recursive: true, withFileTypes: true }).length, + fs.readdirSync(b, { recursive: true, withFileTypes: false }).length +); diff --git a/test/js/node/test/parallel/test-fs-readfile-empty.js b/test/js/node/test/parallel/test-fs-readfile-empty.js new file mode 100644 index 0000000000..f6303777b2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-empty.js @@ -0,0 +1,45 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +// Trivial test of fs.readFile on an empty file. +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const fn = fixtures.path('empty.txt'); + +fs.readFile(fn, common.mustCall((err, data) => { + assert.ok(data); +})); + +fs.readFile(fn, 'utf8', common.mustCall((err, data) => { + assert.strictEqual(data, ''); +})); + +fs.readFile(fn, { encoding: 'utf8' }, common.mustCall((err, data) => { + assert.strictEqual(data, ''); +})); + +assert.ok(fs.readFileSync(fn)); +assert.strictEqual(fs.readFileSync(fn, 'utf8'), ''); diff --git a/test/js/node/test/parallel/test-fs-readfile-eof.js b/test/js/node/test/parallel/test-fs-readfile-eof.js new file mode 100644 index 0000000000..d7f9e21c5b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-eof.js @@ -0,0 +1,44 @@ +'use strict'; +const common = require('../common'); + +if (common.isWindows || common.isAIX || common.isIBMi) + common.skip(`No /dev/stdin on ${process.platform}.`); + +const assert = require('assert'); +const fs = require('fs/promises'); +const childType = ['child-encoding', 'child-non-encoding']; + +if (process.argv[2] === childType[0]) { + fs.readFile('/dev/stdin', 'utf8').then((data) => { + process.stdout.write(data); + }); + return; +} else if (process.argv[2] === childType[1]) { + fs.readFile('/dev/stdin').then((data) => { + process.stdout.write(data); + }); + return; +} + +const data1 = 'Hello'; +const data2 = 'World'; +const expected = `${data1}\n${data2}\n`; + +const exec = require('child_process').exec; + +function test(child) { + exec(...common.escapePOSIXShell`(echo "${data1}"; sleep 0.5; echo "${data2}") | "${process.execPath}" "${__filename}" "${child}"`, + common.mustSucceed((stdout, stderr) => { + assert.strictEqual( + stdout, + expected, + `expected to read(${child === childType[0] ? 'with' : 'without'} encoding): '${expected}' but got: '${stdout}'`); + assert.strictEqual( + stderr, + '', + `expected not to read anything from stderr but got: '${stderr}'`); + })); +} + +test(childType[0]); +test(childType[1]); diff --git a/test/js/node/test/parallel/test-fs-readfile-fd.js b/test/js/node/test/parallel/test-fs-readfile-fd.js new file mode 100644 index 0000000000..1779d9f97d --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-fd.js @@ -0,0 +1,94 @@ +'use strict'; +const common = require('../common'); + +// Test fs.readFile using a file descriptor. + +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const fn = fixtures.path('empty.txt'); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +tempFd(function(fd, close) { + fs.readFile(fd, function(err, data) { + assert.ok(data); + close(); + }); +}); + +tempFd(function(fd, close) { + fs.readFile(fd, 'utf8', function(err, data) { + assert.strictEqual(data, ''); + close(); + }); +}); + +tempFdSync(function(fd) { + assert.ok(fs.readFileSync(fd)); +}); + +tempFdSync(function(fd) { + assert.strictEqual(fs.readFileSync(fd, 'utf8'), ''); +}); + +function tempFd(callback) { + fs.open(fn, 'r', function(err, fd) { + assert.ifError(err); + callback(fd, function() { + fs.close(fd, function(err) { + assert.ifError(err); + }); + }); + }); +} + +function tempFdSync(callback) { + const fd = fs.openSync(fn, 'r'); + callback(fd); + fs.closeSync(fd); +} + +{ + // This test makes sure that `readFile()` always reads from the current + // position of the file, instead of reading from the beginning of the file, + // when used with file descriptors. + + const filename = tmpdir.resolve('test.txt'); + fs.writeFileSync(filename, 'Hello World'); + + { + // Tests the fs.readFileSync(). + const fd = fs.openSync(filename, 'r'); + + // Read only five bytes, so that the position moves to five. + const buf = Buffer.alloc(5); + assert.strictEqual(fs.readSync(fd, buf, 0, 5), 5); + assert.strictEqual(buf.toString(), 'Hello'); + + // readFileSync() should read from position five, instead of zero. + assert.strictEqual(fs.readFileSync(fd).toString(), ' World'); + + fs.closeSync(fd); + } + + { + // Tests the fs.readFile(). + fs.open(filename, 'r', common.mustSucceed((fd) => { + const buf = Buffer.alloc(5); + + // Read only five bytes, so that the position moves to five. + fs.read(fd, buf, 0, 5, null, common.mustSucceed((bytes) => { + assert.strictEqual(bytes, 5); + assert.strictEqual(buf.toString(), 'Hello'); + + fs.readFile(fd, common.mustSucceed((data) => { + // readFile() should read from position five, instead of zero. + assert.strictEqual(data.toString(), ' World'); + + fs.closeSync(fd); + })); + })); + })); + } +} diff --git a/test/js/node/test/parallel/test-fs-readfile-pipe-large.js b/test/js/node/test/parallel/test-fs-readfile-pipe-large.js new file mode 100644 index 0000000000..fa5fea3ca3 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-pipe-large.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); + +// Simulate `cat readfile.js | node readfile.js` + +if (common.isWindows || common.isAIX || common.isIBMi) + common.skip(`No /dev/stdin on ${process.platform}.`); + +const assert = require('assert'); +const fs = require('fs'); + +if (process.argv[2] === 'child') { + fs.readFile('/dev/stdin', function(er, data) { + assert.ifError(er); + process.stdout.write(data); + }); + return; +} + +const tmpdir = require('../common/tmpdir'); + +const filename = tmpdir.resolve('readfile_pipe_large_test.txt'); +const dataExpected = 'a'.repeat(999999); +tmpdir.refresh(); +fs.writeFileSync(filename, dataExpected); + +const exec = require('child_process').exec; +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`; +exec(cmd, { ...opts, maxBuffer: 1000000 }, common.mustSucceed((stdout, stderr) => { + assert.strictEqual( + stdout, + dataExpected, + `expect it reads the file and outputs 999999 'a' but got : ${stdout}` + ); + assert.strictEqual( + stderr, + '', + `expect that it does not write to stderr, but got : ${stderr}` + ); + console.log('ok'); +})); diff --git a/test/js/node/test/parallel/test-fs-readfile-pipe.js b/test/js/node/test/parallel/test-fs-readfile-pipe.js new file mode 100644 index 0000000000..782265e8ce --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-pipe.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Simulate `cat readfile.js | node readfile.js` + +if (common.isWindows || common.isAIX || common.isIBMi) + common.skip(`No /dev/stdin on ${process.platform}.`); + +const assert = require('assert'); +const fs = require('fs'); + +if (process.argv[2] === 'child') { + fs.readFile('/dev/stdin', common.mustSucceed((data) => { + process.stdout.write(data); + })); + return; +} + +const fixtures = require('../common/fixtures'); + +const filename = fixtures.path('readfile_pipe_test.txt'); +const dataExpected = fs.readFileSync(filename).toString(); + +const exec = require('child_process').exec; +exec(...common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`, common.mustSucceed((stdout, stderr) => { + assert.strictEqual( + stdout, + dataExpected, + `expected to read: '${dataExpected}' but got: '${stdout}'`); + assert.strictEqual( + stderr, + '', + `expected not to read anything from stderr but got: '${stderr}'`); + console.log('ok'); +})); diff --git a/test/js/node/test/parallel/test-fs-readfile-unlink.js b/test/js/node/test/parallel/test-fs-readfile-unlink.js new file mode 100644 index 0000000000..1688567fd6 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-unlink.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Test that unlink succeeds immediately after readFile completes. + +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const fileName = tmpdir.resolve('test.bin'); +const buf = Buffer.alloc(512 * 1024, 42); + +tmpdir.refresh(); + +fs.writeFileSync(fileName, buf); + +fs.readFile(fileName, common.mustSucceed((data) => { + assert.strictEqual(data.length, buf.length); + assert.strictEqual(buf[0], 42); + + // Unlink should not throw. This is part of the test. It used to throw on + // Windows due to a bug. + fs.unlinkSync(fileName); +})); diff --git a/test/js/node/test/parallel/test-fs-readfile-zero-byte-liar.js b/test/js/node/test/parallel/test-fs-readfile-zero-byte-liar.js new file mode 100644 index 0000000000..a3ba3985ab --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-zero-byte-liar.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Test that readFile works even when stat returns size 0. + +const assert = require('assert'); +const fs = require('fs'); + +const dataExpected = fs.readFileSync(__filename, 'utf8'); + +// Sometimes stat returns size=0, but it's a lie. +fs._fstat = fs.fstat; +fs._fstatSync = fs.fstatSync; + +fs.fstat = (fd, cb) => { + fs._fstat(fd, (er, st) => { + if (er) return cb(er); + st.size = 0; + return cb(er, st); + }); +}; + +fs.fstatSync = (fd) => { + const st = fs._fstatSync(fd); + st.size = 0; + return st; +}; + +const d = fs.readFileSync(__filename, 'utf8'); +assert.strictEqual(d, dataExpected); + +fs.readFile(__filename, 'utf8', common.mustCall((er, d) => { + assert.strictEqual(d, dataExpected); +})); diff --git a/test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js b/test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js new file mode 100644 index 0000000000..60c7dccd1a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js @@ -0,0 +1,34 @@ +'use strict'; +const common = require('../common'); + +// Simulate `cat readfile.js | node readfile.js` + +if (common.isWindows || common.isAIX || common.isIBMi) + common.skip(`No /dev/stdin on ${process.platform}.`); + +const assert = require('assert'); +const fs = require('fs'); + +if (process.argv[2] === 'child') { + process.stdout.write(fs.readFileSync('/dev/stdin', 'utf8')); + return; +} + +const tmpdir = require('../common/tmpdir'); + +const filename = tmpdir.resolve('readfilesync_pipe_large_test.txt'); +const dataExpected = 'a'.repeat(999999); +tmpdir.refresh(); +fs.writeFileSync(filename, dataExpected); + +const exec = require('child_process').exec; +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`; +exec( + cmd, + { ...opts, maxBuffer: 1_000_000 }, + common.mustSucceed((stdout, stderr) => { + assert.strictEqual(stdout, dataExpected); + assert.strictEqual(stderr, ''); + console.log('ok'); + }) +); diff --git a/test/js/node/test/parallel/test-fs-readlink-type-check.js b/test/js/node/test/parallel/test-fs-readlink-type-check.js new file mode 100644 index 0000000000..58d431308c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readlink-type-check.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.readlink(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.readlinkSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-readv-promises.js b/test/js/node/test/parallel/test-fs-readv-promises.js new file mode 100644 index 0000000000..cdfc3d3b21 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readv-promises.js @@ -0,0 +1,63 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs').promises; +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const expected = 'ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف'; +const exptectedBuff = Buffer.from(expected); + +let cnt = 0; +function getFileName() { + return tmpdir.resolve(`readv_promises_${++cnt}.txt`); +} + +const allocateEmptyBuffers = (combinedLength) => { + const bufferArr = []; + // Allocate two buffers, each half the size of exptectedBuff + bufferArr[0] = Buffer.alloc(Math.floor(combinedLength / 2)); + bufferArr[1] = Buffer.alloc(combinedLength - bufferArr[0].length); + + return bufferArr; +}; + +(async () => { + { + const filename = getFileName(); + await fs.writeFile(filename, exptectedBuff); + const handle = await fs.open(filename, 'r'); + const bufferArr = allocateEmptyBuffers(exptectedBuff.length); + const expectedLength = exptectedBuff.length; + + let { bytesRead, buffers } = await handle.readv([Buffer.from('')], + null); + assert.strictEqual(bytesRead, 0); + assert.deepStrictEqual(buffers, [Buffer.from('')]); + + ({ bytesRead, buffers } = await handle.readv(bufferArr, null)); + assert.strictEqual(bytesRead, expectedLength); + assert.deepStrictEqual(buffers, bufferArr); + assert(Buffer.concat(bufferArr).equals(await fs.readFile(filename))); + handle.close(); + } + + { + const filename = getFileName(); + await fs.writeFile(filename, exptectedBuff); + const handle = await fs.open(filename, 'r'); + const bufferArr = allocateEmptyBuffers(exptectedBuff.length); + const expectedLength = exptectedBuff.length; + + let { bytesRead, buffers } = await handle.readv([Buffer.from('')]); + assert.strictEqual(bytesRead, 0); + assert.deepStrictEqual(buffers, [Buffer.from('')]); + + ({ bytesRead, buffers } = await handle.readv(bufferArr)); + assert.strictEqual(bytesRead, expectedLength); + assert.deepStrictEqual(buffers, bufferArr); + assert(Buffer.concat(bufferArr).equals(await fs.readFile(filename))); + handle.close(); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-readv-promisify.js b/test/js/node/test/parallel/test-fs-readv-promisify.js new file mode 100644 index 0000000000..2af418bcc2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readv-promisify.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const readv = require('util').promisify(fs.readv); +const assert = require('assert'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); + +const expected = [Buffer.from('xyz\n')]; + +readv(fd, expected) + .then(function({ bytesRead, buffers }) { + assert.deepStrictEqual(bytesRead, expected[0].length); + assert.deepStrictEqual(buffers, expected); + }) + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-readv-sync.js b/test/js/node/test/parallel/test-fs-readv-sync.js new file mode 100644 index 0000000000..548f54cbb9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readv-sync.js @@ -0,0 +1,92 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const expected = 'ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف'; + +const exptectedBuff = Buffer.from(expected); +const expectedLength = exptectedBuff.length; + +const filename = tmpdir.resolve('readv_sync.txt'); +fs.writeFileSync(filename, exptectedBuff); + +const allocateEmptyBuffers = (combinedLength) => { + const bufferArr = []; + // Allocate two buffers, each half the size of exptectedBuff + bufferArr[0] = Buffer.alloc(Math.floor(combinedLength / 2)); + bufferArr[1] = Buffer.alloc(combinedLength - bufferArr[0].length); + + return bufferArr; +}; + +// fs.readvSync with array of buffers with all parameters +{ + const fd = fs.openSync(filename, 'r'); + + const bufferArr = allocateEmptyBuffers(exptectedBuff.length); + + let read = fs.readvSync(fd, [Buffer.from('')], 0); + assert.strictEqual(read, 0); + + read = fs.readvSync(fd, bufferArr, 0); + assert.strictEqual(read, expectedLength); + + fs.closeSync(fd); + + assert(Buffer.concat(bufferArr).equals(fs.readFileSync(filename))); +} + +// fs.readvSync with array of buffers without position +{ + const fd = fs.openSync(filename, 'r'); + + const bufferArr = allocateEmptyBuffers(exptectedBuff.length); + + let read = fs.readvSync(fd, [Buffer.from('')]); + assert.strictEqual(read, 0); + + read = fs.readvSync(fd, bufferArr); + assert.strictEqual(read, expectedLength); + + fs.closeSync(fd); + + assert(Buffer.concat(bufferArr).equals(fs.readFileSync(filename))); +} + +/** + * Testing with incorrect arguments + */ +const wrongInputs = [false, 'test', {}, [{}], ['sdf'], null, undefined]; + +{ + const fd = fs.openSync(filename, 'r'); + + for (const wrongInput of wrongInputs) { + assert.throws( + () => fs.readvSync(fd, wrongInput, null), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + } + + fs.closeSync(fd); +} + +{ + // fs.readv with wrong fd argument + for (const wrongInput of wrongInputs) { + assert.throws( + () => fs.readvSync(wrongInput), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + } +} diff --git a/test/js/node/test/parallel/test-fs-readv.js b/test/js/node/test/parallel/test-fs-readv.js new file mode 100644 index 0000000000..111719a7ef --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readv.js @@ -0,0 +1,93 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const expected = 'ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف'; + +let cnt = 0; +const getFileName = () => tmpdir.resolve(`readv_${++cnt}.txt`); +const exptectedBuff = Buffer.from(expected); + +const allocateEmptyBuffers = (combinedLength) => { + const bufferArr = []; + // Allocate two buffers, each half the size of exptectedBuff + bufferArr[0] = Buffer.alloc(Math.floor(combinedLength / 2)); + bufferArr[1] = Buffer.alloc(combinedLength - bufferArr[0].length); + + return bufferArr; +}; + +const getCallback = (fd, bufferArr) => { + return common.mustSucceed((bytesRead, buffers) => { + assert.deepStrictEqual(bufferArr, buffers); + const expectedLength = exptectedBuff.length; + assert.deepStrictEqual(bytesRead, expectedLength); + fs.closeSync(fd); + + assert(Buffer.concat(bufferArr).equals(exptectedBuff)); + }); +}; + +// fs.readv with array of buffers with all parameters +{ + const filename = getFileName(); + const fd = fs.openSync(filename, 'w+'); + fs.writeSync(fd, exptectedBuff); + + const bufferArr = allocateEmptyBuffers(exptectedBuff.length); + const callback = getCallback(fd, bufferArr); + + fs.readv(fd, bufferArr, 0, callback); +} + +// fs.readv with array of buffers without position +{ + const filename = getFileName(); + fs.writeFileSync(filename, exptectedBuff); + const fd = fs.openSync(filename, 'r'); + + const bufferArr = allocateEmptyBuffers(exptectedBuff.length); + const callback = getCallback(fd, bufferArr); + + fs.readv(fd, bufferArr, callback); +} + +/** + * Testing with incorrect arguments + */ +const wrongInputs = [false, 'test', {}, [{}], ['sdf'], null, undefined]; + +{ + const filename = getFileName(2); + fs.writeFileSync(filename, exptectedBuff); + const fd = fs.openSync(filename, 'r'); + + for (const wrongInput of wrongInputs) { + assert.throws( + () => fs.readv(fd, wrongInput, null, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + } + + fs.closeSync(fd); +} + +{ + // fs.readv with wrong fd argument + for (const wrongInput of wrongInputs) { + assert.throws( + () => fs.readv(wrongInput, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + } +} diff --git a/test/js/node/test/parallel/test-fs-realpath-native.js b/test/js/node/test/parallel/test-fs-realpath-native.js new file mode 100644 index 0000000000..0b51e6cc89 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath-native.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const filename = __filename.toLowerCase(); + +// Bun: fix current working directory +process.chdir(require('path').join(__dirname, '..', '..')); + +assert.strictEqual( + fs.realpathSync.native('./test/parallel/test-fs-realpath-native.js') + .toLowerCase(), + filename); + +fs.realpath.native( + './test/parallel/test-fs-realpath-native.js', + common.mustSucceed(function(res) { + assert.strictEqual(res.toLowerCase(), filename); + assert.strictEqual(this, undefined); + })); diff --git a/test/js/node/test/parallel/test-fs-realpath-pipe.js b/test/js/node/test/parallel/test-fs-realpath-pipe.js new file mode 100644 index 0000000000..f637642ca2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath-pipe.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); + +if (common.isWindows || common.isAIX || common.isIBMi) + common.skip(`No /dev/stdin on ${process.platform}.`); + +const assert = require('assert'); + +const { spawnSync } = require('child_process'); + +for (const code of [ + `require('fs').realpath('/dev/stdin', (err, resolvedPath) => { + if (err) { + console.error(err); + process.exit(1); + } + if (resolvedPath) { + process.exit(2); + } + });`, + `try { + if (require('fs').realpathSync('/dev/stdin')) { + process.exit(2); + } + } catch (e) { + console.error(e); + process.exit(1); + }`, +]) { + const child = spawnSync(process.execPath, ['-e', code], { + stdio: 'pipe' + }); + if (child.status !== 2) { + console.log(code); + console.log(child.stderr.toString()); + } + assert.strictEqual(child.status, 2); +} diff --git a/test/js/node/test/parallel/test-fs-realpath.js b/test/js/node/test/parallel/test-fs-realpath.js new file mode 100644 index 0000000000..d944195de3 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath.js @@ -0,0 +1,618 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +let async_completed = 0; +let async_expected = 0; +const unlink = []; +const skipSymlinks = !common.canCreateSymLink(); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +let root = '/'; +let assertEqualPath = assert.strictEqual; +if (common.isWindows) { + // Something like "C:\\" + root = process.cwd().slice(0, 3); + assertEqualPath = function(path_left, path_right, message) { + assert + .strictEqual(path_left.toLowerCase(), path_right.toLowerCase(), message); + }; +} + +process.nextTick(runTest); + +function tmp(p) { + return path.join(tmpDir, p); +} + +const targetsAbsDir = path.join(tmpDir, 'targets'); +const tmpAbsDir = tmpDir; + +// Set up targetsAbsDir and expected subdirectories +fs.mkdirSync(targetsAbsDir); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index')); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'one')); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'two')); + +function asynctest(testBlock, args, callback, assertBlock) { + async_expected++; + testBlock.apply(testBlock, args.concat(function(err) { + let ignoreError = false; + if (assertBlock) { + try { + ignoreError = assertBlock.apply(assertBlock, arguments); + } catch (e) { + err = e; + } + } + async_completed++; + callback(ignoreError ? null : err); + })); +} + +// sub-tests: +function test_simple_error_callback(realpath, realpathSync, cb) { + realpath('/this/path/does/not/exist', common.mustCall(function(err, s) { + assert(err); + assert(!s); + cb(); + })); +} + +function test_simple_error_cb_with_null_options(realpath, realpathSync, cb) { + realpath('/this/path/does/not/exist', null, common.mustCall(function(err, s) { + assert(err); + assert(!s); + cb(); + })); +} + +function test_simple_relative_symlink(realpath, realpathSync, callback) { + console.log('test_simple_relative_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const entry = `${tmpDir}/symlink`; + const expected = `${tmpDir}/cycles/root.js`; + [ + [entry, `../${path.basename(tmpDir)}/cycles/root.js`], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + console.log('fs.symlinkSync(%j, %j, %j)', t[1], t[0], 'file'); + fs.symlinkSync(t[1], t[0], 'file'); + unlink.push(t[0]); + }); + const result = realpathSync(entry); + assertEqualPath(result, path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_simple_absolute_symlink(realpath, realpathSync, callback) { + console.log('test_simple_absolute_symlink'); + + // This one should still run, even if skipSymlinks is set, + // because it uses a junction. + const type = skipSymlinks ? 'junction' : 'dir'; + + console.log('using type=%s', type); + + const entry = `${tmpAbsDir}/symlink`; + const expected = fixtures.path('nested-index', 'one'); + [ + [entry, expected], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + console.error('fs.symlinkSync(%j, %j, %j)', t[1], t[0], type); + fs.symlinkSync(t[1], t[0], type); + unlink.push(t[0]); + }); + const result = realpathSync(entry); + assertEqualPath(result, path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_deep_relative_file_symlink(realpath, realpathSync, callback) { + console.log('test_deep_relative_file_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + const expected = fixtures.path('cycles', 'root.js'); + const linkData1 = path + .relative(path.join(targetsAbsDir, 'nested-index', 'one'), + expected); + const linkPath1 = path.join(targetsAbsDir, + 'nested-index', 'one', 'symlink1.js'); + try { fs.unlinkSync(linkPath1); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData1, linkPath1, 'file'); + + const linkData2 = '../one/symlink1.js'; + const entry = path.join(targetsAbsDir, + 'nested-index', 'two', 'symlink1-b.js'); + try { fs.unlinkSync(entry); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData2, entry, 'file'); + unlink.push(linkPath1); + unlink.push(entry); + + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_deep_relative_dir_symlink(realpath, realpathSync, callback) { + console.log('test_deep_relative_dir_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const expected = fixtures.path('cycles', 'folder'); + const path1b = path.join(targetsAbsDir, 'nested-index', 'one'); + const linkPath1b = path.join(path1b, 'symlink1-dir'); + const linkData1b = path.relative(path1b, expected); + try { fs.unlinkSync(linkPath1b); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData1b, linkPath1b, 'dir'); + + const linkData2b = '../one/symlink1-dir'; + const entry = path.join(targetsAbsDir, + 'nested-index', 'two', 'symlink12-dir'); + try { fs.unlinkSync(entry); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData2b, entry, 'dir'); + unlink.push(linkPath1b); + unlink.push(entry); + + assertEqualPath(realpathSync(entry), path.resolve(expected)); + + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_cyclic_link_protection(realpath, realpathSync, callback) { + console.log('test_cyclic_link_protection'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const entry = path.join(tmpDir, '/cycles/realpath-3a'); + [ + [entry, '../cycles/realpath-3b'], + [path.join(tmpDir, '/cycles/realpath-3b'), '../cycles/realpath-3c'], + [path.join(tmpDir, '/cycles/realpath-3c'), '../cycles/realpath-3a'], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + fs.symlinkSync(t[1], t[0], 'dir'); + unlink.push(t[0]); + }); + assert.throws(() => { + realpathSync(entry); + }, { code: 'ELOOP', name: 'Error' }); + asynctest( + realpath, [entry], callback, common.mustCall(function(err, result) { + assert.strictEqual(err.path, entry); + assert.strictEqual(result, undefined); + return true; + })); +} + +function test_cyclic_link_overprotection(realpath, realpathSync, callback) { + console.log('test_cyclic_link_overprotection'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const cycles = `${tmpDir}/cycles`; + const expected = realpathSync(cycles); + const folder = `${cycles}/folder`; + const link = `${folder}/cycles`; + let testPath = cycles; + testPath += '/folder/cycles'.repeat(10); + try { fs.unlinkSync(link); } catch { + // Continue regardless of error. + } + fs.symlinkSync(cycles, link, 'dir'); + unlink.push(link); + assertEqualPath(realpathSync(testPath), path.resolve(expected)); + asynctest(realpath, [testPath], callback, function(er, res) { + assertEqualPath(res, path.resolve(expected)); + }); +} + +function test_relative_input_cwd(realpath, realpathSync, callback) { + console.log('test_relative_input_cwd'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + // We need to calculate the relative path to the tmp dir from cwd + const entrydir = process.cwd(); + const entry = path.relative(entrydir, + path.join(`${tmpDir}/cycles/realpath-3a`)); + const expected = `${tmpDir}/cycles/root.js`; + [ + [entry, '../cycles/realpath-3b'], + [`${tmpDir}/cycles/realpath-3b`, '../cycles/realpath-3c'], + [`${tmpDir}/cycles/realpath-3c`, 'root.js'], + ].forEach(function(t) { + const fn = t[0]; + console.error('fn=%j', fn); + try { fs.unlinkSync(fn); } catch { + // Continue regardless of error. + } + const b = path.basename(t[1]); + const type = (b === 'root.js' ? 'file' : 'dir'); + console.log('fs.symlinkSync(%j, %j, %j)', t[1], fn, type); + fs.symlinkSync(t[1], fn, 'file'); + unlink.push(fn); + }); + + const origcwd = process.cwd(); + process.chdir(entrydir); + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + process.chdir(origcwd); + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +function test_deep_symlink_mix(realpath, realpathSync, callback) { + console.log('test_deep_symlink_mix'); + if (common.isWindows) { + // This one is a mix of files and directories, and it's quite tricky + // to get the file/dir links sorted out correctly. + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + // /tmp/node-test-realpath-f1 -> $tmpDir/node-test-realpath-d1/foo + // /tmp/node-test-realpath-d1 -> $tmpDir/node-test-realpath-d2 + // /tmp/node-test-realpath-d2/foo -> $tmpDir/node-test-realpath-f2 + // /tmp/node-test-realpath-f2 + // -> $tmpDir/targets/nested-index/one/realpath-c + // $tmpDir/targets/nested-index/one/realpath-c + // -> $tmpDir/targets/nested-index/two/realpath-c + // $tmpDir/targets/nested-index/two/realpath-c -> $tmpDir/cycles/root.js + // $tmpDir/targets/cycles/root.js (hard) + + const entry = tmp('node-test-realpath-f1'); + try { fs.unlinkSync(tmp('node-test-realpath-d2/foo')); } catch { + // Continue regardless of error. + } + try { fs.rmdirSync(tmp('node-test-realpath-d2')); } catch { + // Continue regardless of error. + } + fs.mkdirSync(tmp('node-test-realpath-d2'), 0o700); + try { + [ + [entry, `${tmpDir}/node-test-realpath-d1/foo`], + [tmp('node-test-realpath-d1'), + `${tmpDir}/node-test-realpath-d2`], + [tmp('node-test-realpath-d2/foo'), '../node-test-realpath-f2'], + [tmp('node-test-realpath-f2'), + `${targetsAbsDir}/nested-index/one/realpath-c`], + [`${targetsAbsDir}/nested-index/one/realpath-c`, + `${targetsAbsDir}/nested-index/two/realpath-c`], + [`${targetsAbsDir}/nested-index/two/realpath-c`, + `${tmpDir}/cycles/root.js`], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + fs.symlinkSync(t[1], t[0]); + unlink.push(t[0]); + }); + } finally { + unlink.push(tmp('node-test-realpath-d2')); + } + const expected = `${tmpAbsDir}/cycles/root.js`; + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +function test_non_symlinks(realpath, realpathSync, callback) { + console.log('test_non_symlinks'); + const entrydir = path.dirname(tmpAbsDir); + const entry = `${tmpAbsDir.slice(entrydir.length + 1)}/cycles/root.js`; + const expected = `${tmpAbsDir}/cycles/root.js`; + const origcwd = process.cwd(); + process.chdir(entrydir); + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + process.chdir(origcwd); + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +const upone = path.join(process.cwd(), '..'); +function test_escape_cwd(realpath, realpathSync, cb) { + console.log('test_escape_cwd'); + asynctest(realpath, ['..'], cb, function(er, uponeActual) { + assertEqualPath( + upone, uponeActual, + `realpath("..") expected: ${path.resolve(upone)} actual:${uponeActual}`); + }); +} + +function test_upone_actual(realpath, realpathSync, cb) { + console.log('test_upone_actual'); + const uponeActual = realpathSync('..'); + assertEqualPath(upone, uponeActual); + cb(); +} + +// Going up with .. multiple times +// . +// `-- a/ +// |-- b/ +// | `-- e -> .. +// `-- d -> .. +// realpath(a/b/e/d/a/b/e/d/a) ==> a +function test_up_multiple(realpath, realpathSync, cb) { + console.error('test_up_multiple'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return cb(); + } + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + fs.mkdirSync(tmp('a'), 0o755); + fs.mkdirSync(tmp('a/b'), 0o755); + fs.symlinkSync('..', tmp('a/d'), 'dir'); + unlink.push(tmp('a/d')); + fs.symlinkSync('..', tmp('a/b/e'), 'dir'); + unlink.push(tmp('a/b/e')); + + const abedabed = tmp('abedabed'.split('').join('/')); + const abedabed_real = tmp(''); + + const abedabeda = tmp('abedabeda'.split('').join('/')); + const abedabeda_real = tmp('a'); + + assertEqualPath(realpathSync(abedabeda), abedabeda_real); + assertEqualPath(realpathSync(abedabed), abedabed_real); + + realpath(abedabeda, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabeda_real, real); + realpath(abedabed, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabed_real, real); + cb(); + }); + }); +} + + +// Going up with .. multiple times with options = null +// . +// `-- a/ +// |-- b/ +// | `-- e -> .. +// `-- d -> .. +// realpath(a/b/e/d/a/b/e/d/a) ==> a +function test_up_multiple_with_null_options(realpath, realpathSync, cb) { + console.error('test_up_multiple'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return cb(); + } + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + fs.mkdirSync(tmp('a'), 0o755); + fs.mkdirSync(tmp('a/b'), 0o755); + fs.symlinkSync('..', tmp('a/d'), 'dir'); + unlink.push(tmp('a/d')); + fs.symlinkSync('..', tmp('a/b/e'), 'dir'); + unlink.push(tmp('a/b/e')); + + const abedabed = tmp('abedabed'.split('').join('/')); + const abedabed_real = tmp(''); + + const abedabeda = tmp('abedabeda'.split('').join('/')); + const abedabeda_real = tmp('a'); + + assertEqualPath(realpathSync(abedabeda), abedabeda_real); + assertEqualPath(realpathSync(abedabed), abedabed_real); + + realpath(abedabeda, null, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabeda_real, real); + realpath(abedabed, null, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabed_real, real); + cb(); + }); + }); +} + +// Absolute symlinks with children. +// . +// `-- a/ +// |-- b/ +// | `-- c/ +// | `-- x.txt +// `-- link -> /tmp/node-test-realpath-abs-kids/a/b/ +// realpath(root+'/a/link/c/x.txt') ==> root+'/a/b/c/x.txt' +function test_abs_with_kids(realpath, realpathSync, cb) { + console.log('test_abs_with_kids'); + + // This one should still run, even if skipSymlinks is set, + // because it uses a junction. + const type = skipSymlinks ? 'junction' : 'dir'; + + console.log('using type=%s', type); + + const root = `${tmpAbsDir}/node-test-realpath-abs-kids`; + function cleanup() { + ['/a/b/c/x.txt', + '/a/link', + ].forEach(function(file) { + try { fs.unlinkSync(root + file); } catch { + // Continue regardless of error. + } + }); + ['/a/b/c', + '/a/b', + '/a', + '', + ].forEach(function(folder) { + try { fs.rmdirSync(root + folder); } catch { + // Continue regardless of error. + } + }); + } + + function setup() { + cleanup(); + ['', + '/a', + '/a/b', + '/a/b/c', + ].forEach(function(folder) { + console.log(`mkdir ${root}${folder}`); + fs.mkdirSync(root + folder, 0o700); + }); + fs.writeFileSync(`${root}/a/b/c/x.txt`, 'foo'); + fs.symlinkSync(`${root}/a/b`, `${root}/a/link`, type); + } + setup(); + const linkPath = `${root}/a/link/c/x.txt`; + const expectPath = `${root}/a/b/c/x.txt`; + const actual = realpathSync(linkPath); + // console.log({link:linkPath,expect:expectPath,actual:actual},'sync'); + assertEqualPath(actual, path.resolve(expectPath)); + asynctest(realpath, [linkPath], cb, function(er, actual) { + // console.log({link:linkPath,expect:expectPath,actual:actual},'async'); + assertEqualPath(actual, path.resolve(expectPath)); + cleanup(); + }); +} + +function test_root(realpath, realpathSync, cb) { + assertEqualPath(root, realpathSync('/')); + realpath('/', function(err, result) { + assert.ifError(err); + assertEqualPath(root, result); + cb(); + }); +} + +function test_root_with_null_options(realpath, realpathSync, cb) { + realpath('/', null, function(err, result) { + assert.ifError(err); + assertEqualPath(root, result); + cb(); + }); +} + +// ---------------------------------------------------------------------------- + +const tests = [ + test_simple_error_callback, + test_simple_error_cb_with_null_options, + test_simple_relative_symlink, + test_simple_absolute_symlink, + test_deep_relative_file_symlink, + test_deep_relative_dir_symlink, + test_cyclic_link_protection, + test_cyclic_link_overprotection, + test_relative_input_cwd, + test_deep_symlink_mix, + test_non_symlinks, + test_escape_cwd, + test_upone_actual, + test_abs_with_kids, + test_up_multiple, + test_up_multiple_with_null_options, + test_root, + test_root_with_null_options, +]; +const numtests = tests.length; +let testsRun = 0; +function runNextTest(err) { + assert.ifError(err); + const test = tests.shift(); + if (!test) { + return console.log(`${numtests} subtests completed OK for fs.realpath`); + } + testsRun++; + test(fs.realpath, fs.realpathSync, common.mustSucceed(() => { + testsRun++; + test(fs.realpath.native, + fs.realpathSync.native, + common.mustCall(runNextTest)); + })); +} + +function runTest() { + const tmpDirs = ['cycles', 'cycles/folder']; + tmpDirs.forEach(function(t) { + t = tmp(t); + fs.mkdirSync(t, 0o700); + }); + fs.writeFileSync(tmp('cycles/root.js'), "console.error('roooot!');"); + console.error('start tests'); + runNextTest(); +} + + +process.on('exit', function() { + assert.strictEqual(2 * numtests, testsRun); + assert.strictEqual(async_completed, async_expected); +}); diff --git a/test/js/node/test/parallel/test-fs-rmdir-type-check.js b/test/js/node/test/parallel/test-fs-rmdir-type-check.js new file mode 100644 index 0000000000..7014ce27f8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-type-check.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +[false, 1, [], {}, null, undefined].forEach((i) => { + assert.throws( + () => fs.rmdir(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.rmdirSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-sir-writes-alot.js b/test/js/node/test/parallel/test-fs-sir-writes-alot.js new file mode 100644 index 0000000000..7d213d6c6f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-sir-writes-alot.js @@ -0,0 +1,70 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const fs = require('fs'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); + +const filename = tmpdir.resolve('out.txt'); + +tmpdir.refresh(); + +const fd = fs.openSync(filename, 'w'); + +const line = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'; + +const N = 10240; +let complete = 0; + +for (let i = 0; i < N; i++) { + // Create a new buffer for each write. Before the write is actually + // executed by the thread pool, the buffer will be collected. + const buffer = Buffer.from(line); + fs.write(fd, buffer, 0, buffer.length, null, function(er, written) { + complete++; + if (complete === N) { + fs.closeSync(fd); + const s = fs.createReadStream(filename); + s.on('data', testBuffer); + } + }); +} + +let bytesChecked = 0; + +function testBuffer(b) { + for (let i = 0; i < b.length; i++) { + bytesChecked++; + if (b[i] !== 'a'.charCodeAt(0) && b[i] !== '\n'.charCodeAt(0)) { + throw new Error(`invalid char ${i},${b[i]}`); + } + } +} + +process.on('exit', function() { + // Probably some of the writes are going to overlap, so we can't assume + // that we get (N * line.length). Let's just make sure we've checked a + // few... + assert.ok(bytesChecked > 1000); +}); diff --git a/test/js/node/test/parallel/test-fs-stat-bigint.js b/test/js/node/test/parallel/test-fs-stat-bigint.js new file mode 100644 index 0000000000..0a2bea92e5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stat-bigint.js @@ -0,0 +1,247 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const promiseFs = require('fs').promises; +const tmpdir = require('../common/tmpdir'); +const { isDate } = require('util').types; +const { inspect } = require('util'); + +tmpdir.refresh(); + +let testIndex = 0; + +function getFilename() { + const filename = tmpdir.resolve(`test-file-${++testIndex}`); + fs.writeFileSync(filename, 'test'); + return filename; +} + +function verifyStats(bigintStats, numStats, allowableDelta) { + // allowableDelta: It's possible that the file stats are updated between the + // two stat() calls so allow for a small difference. + for (const key of Object.keys(numStats)) { + const val = numStats[key]; + if (isDate(val)) { + const time = val.getTime(); + const time2 = bigintStats[key].getTime(); + assert( + time - time2 <= allowableDelta, + `difference of ${key}.getTime() should <= ${allowableDelta}.\n` + + `Number version ${time}, BigInt version ${time2}n`); + } else if (key === 'mode') { + assert.strictEqual(bigintStats[key], BigInt(val)); + assert.strictEqual( + bigintStats.isBlockDevice(), + numStats.isBlockDevice() + ); + assert.strictEqual( + bigintStats.isCharacterDevice(), + numStats.isCharacterDevice() + ); + assert.strictEqual( + bigintStats.isDirectory(), + numStats.isDirectory() + ); + assert.strictEqual( + bigintStats.isFIFO(), + numStats.isFIFO() + ); + assert.strictEqual( + bigintStats.isFile(), + numStats.isFile() + ); + assert.strictEqual( + bigintStats.isSocket(), + numStats.isSocket() + ); + assert.strictEqual( + bigintStats.isSymbolicLink(), + numStats.isSymbolicLink() + ); + } else if (key.endsWith('Ms')) { + const nsKey = key.replace('Ms', 'Ns'); + const msFromBigInt = bigintStats[key]; + const nsFromBigInt = bigintStats[nsKey]; + const msFromBigIntNs = Number(nsFromBigInt / (10n ** 6n)); + const msFromNum = numStats[key]; + + assert( + msFromNum - Number(msFromBigInt) <= allowableDelta, + `Number version ${key} = ${msFromNum}, ` + + `BigInt version ${key} = ${msFromBigInt}n, ` + + `Allowable delta = ${allowableDelta}`); + + assert( + msFromNum - Number(msFromBigIntNs) <= allowableDelta, + `Number version ${key} = ${msFromNum}, ` + + `BigInt version ${nsKey} = ${nsFromBigInt}n` + + ` = ${msFromBigIntNs}ms, Allowable delta = ${allowableDelta}`); + } else if (Number.isSafeInteger(val)) { + assert.strictEqual( + bigintStats[key], BigInt(val), + `${inspect(bigintStats[key])} !== ${inspect(BigInt(val))}\n` + + `key=${key}, val=${val}` + ); + } else { + assert( + Number(bigintStats[key]) - val < 1, + `${key} is not a safe integer, difference should < 1.\n` + + `Number version ${val}, BigInt version ${bigintStats[key]}n`); + } + } +} + +const runSyncTest = (func, arg) => { + const startTime = process.hrtime.bigint(); + const bigintStats = func(arg, common.mustNotMutateObjectDeep({ bigint: true })); + const numStats = func(arg); + const endTime = process.hrtime.bigint(); + const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); + verifyStats(bigintStats, numStats, allowableDelta); +}; + +{ + const filename = getFilename(); + runSyncTest(fs.statSync, filename); +} + +if (!common.isWindows) { + const filename = getFilename(); + const link = `${filename}-link`; + fs.symlinkSync(filename, link); + runSyncTest(fs.lstatSync, link); +} + +{ + const filename = getFilename(); + const fd = fs.openSync(filename, 'r'); + runSyncTest(fs.fstatSync, fd); + fs.closeSync(fd); +} + +{ + assert.throws( + () => fs.statSync('does_not_exist'), + { code: 'ENOENT' }); + assert.strictEqual( + fs.statSync('does_not_exist', common.mustNotMutateObjectDeep({ throwIfNoEntry: false })), + undefined); +} + +{ + assert.throws( + () => fs.lstatSync('does_not_exist'), + { code: 'ENOENT' }); + assert.strictEqual( + fs.lstatSync('does_not_exist', common.mustNotMutateObjectDeep({ throwIfNoEntry: false })), + undefined); +} + +{ + assert.throws( + () => fs.fstatSync(9999), + { code: 'EBADF' }); + assert.throws( + () => fs.fstatSync(9999, common.mustNotMutateObjectDeep({ throwIfNoEntry: false })), + { code: 'EBADF' }); +} + +const runCallbackTest = (func, arg, done) => { + const startTime = process.hrtime.bigint(); + func(arg, common.mustNotMutateObjectDeep({ bigint: true }), common.mustCall((err, bigintStats) => { + func(arg, common.mustCall((err, numStats) => { + const endTime = process.hrtime.bigint(); + const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); + verifyStats(bigintStats, numStats, allowableDelta); + if (done) { + done(); + } + })); + })); +}; + +{ + const filename = getFilename(); + runCallbackTest(fs.stat, filename); +} + +if (!common.isWindows) { + const filename = getFilename(); + const link = `${filename}-link`; + fs.symlinkSync(filename, link); + runCallbackTest(fs.lstat, link); +} + +{ + const filename = getFilename(); + const fd = fs.openSync(filename, 'r'); + runCallbackTest(fs.fstat, fd, () => { fs.closeSync(fd); }); +} + +const runPromiseTest = async (func, arg) => { + const startTime = process.hrtime.bigint(); + const bigintStats = await func(arg, common.mustNotMutateObjectDeep({ bigint: true })); + const numStats = await func(arg); + const endTime = process.hrtime.bigint(); + const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); + verifyStats(bigintStats, numStats, allowableDelta); +}; + +{ + const filename = getFilename(); + runPromiseTest(promiseFs.stat, filename); +} + +if (!common.isWindows) { + const filename = getFilename(); + const link = `${filename}-link`; + fs.symlinkSync(filename, link); + runPromiseTest(promiseFs.lstat, link); +} + +(async function() { + const filename = getFilename(); + const handle = await promiseFs.open(filename, 'r'); + const startTime = process.hrtime.bigint(); + const bigintStats = await handle.stat(common.mustNotMutateObjectDeep({ bigint: true })); + const numStats = await handle.stat(); + const endTime = process.hrtime.bigint(); + const allowableDelta = Math.ceil(Number(endTime - startTime) / 1e6); + verifyStats(bigintStats, numStats, allowableDelta); + await handle.close(); +})().then(common.mustCall()); + +{ + // These two tests have an equivalent in ./test-fs-stat.js + + // BigIntStats Date properties can be set before reading them + fs.stat(__filename, { bigint: true }, common.mustSucceed((s) => { + s.atime = 2; + s.mtime = 3; + s.ctime = 4; + s.birthtime = 5; + + assert.strictEqual(s.atime, 2); + assert.strictEqual(s.mtime, 3); + assert.strictEqual(s.ctime, 4); + assert.strictEqual(s.birthtime, 5); + })); + + // BigIntStats Date properties can be set after reading them + fs.stat(__filename, { bigint: true }, common.mustSucceed((s) => { + // eslint-disable-next-line no-unused-expressions + s.atime, s.mtime, s.ctime, s.birthtime; + + s.atime = 2; + s.mtime = 3; + s.ctime = 4; + s.birthtime = 5; + + assert.strictEqual(s.atime, 2); + assert.strictEqual(s.mtime, 3); + assert.strictEqual(s.ctime, 4); + assert.strictEqual(s.birthtime, 5); + })); +} diff --git a/test/js/node/test/parallel/test-fs-stream-double-close.js b/test/js/node/test/parallel/test-fs-stream-double-close.js new file mode 100644 index 0000000000..8c0037b243 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-double-close.js @@ -0,0 +1,54 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +test1(fs.createReadStream(__filename)); +test2(fs.createReadStream(__filename)); +test3(fs.createReadStream(__filename)); + +test1(fs.createWriteStream(`${tmpdir.path}/dummy1`)); +test2(fs.createWriteStream(`${tmpdir.path}/dummy2`)); +test3(fs.createWriteStream(`${tmpdir.path}/dummy3`)); + +function test1(stream) { + stream.destroy(); + stream.destroy(); +} + +function test2(stream) { + stream.destroy(); + stream.on('open', common.mustCall(function(fd) { + stream.destroy(); + })); +} + +function test3(stream) { + stream.on('open', common.mustCall(function(fd) { + stream.destroy(); + stream.destroy(); + })); +} diff --git a/test/js/node/test/parallel/test-fs-symlink-buffer-path.js b/test/js/node/test/parallel/test-fs-symlink-buffer-path.js new file mode 100644 index 0000000000..ecad001d5e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-symlink-buffer-path.js @@ -0,0 +1,59 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.canCreateSymLink()) + common.skip('insufficient privileges'); + +const fixtures = require('../common/fixtures'); + +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Test creating and reading symbolic link +const linkData = fixtures.path('/cycles/root.js'); +const linkPath = tmpdir.resolve('symlink1.js'); + +let linkTime; +let fileTime; + +// Refs: https://github.com/nodejs/node/issues/34514 +fs.symlinkSync(Buffer.from(linkData), linkPath); + +fs.lstat(linkPath, common.mustSucceed((stats) => { + linkTime = stats.mtime.getTime(); +})); + +fs.stat(linkPath, common.mustSucceed((stats) => { + fileTime = stats.mtime.getTime(); +})); + +fs.readlink(linkPath, common.mustSucceed((destination) => { + assert.strictEqual(destination, linkData); +})); + +process.on('exit', () => { + assert.notStrictEqual(linkTime, fileTime); +}); diff --git a/test/js/node/test/parallel/test-fs-symlink-dir-junction-relative.js b/test/js/node/test/parallel/test-fs-symlink-dir-junction-relative.js new file mode 100644 index 0000000000..01ef8c8bdc --- /dev/null +++ b/test/js/node/test/parallel/test-fs-symlink-dir-junction-relative.js @@ -0,0 +1,58 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Test creating and resolving relative junction or symbolic link + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const linkPath1 = tmpdir.resolve('junction1'); +const linkPath2 = tmpdir.resolve('junction2'); +const linkTarget = fixtures.fixturesDir; +const linkData = fixtures.fixturesDir; + +tmpdir.refresh(); + +// Test fs.symlink() +fs.symlink(linkData, linkPath1, 'junction', common.mustSucceed(() => { + verifyLink(linkPath1); +})); + +// Test fs.symlinkSync() +fs.symlinkSync(linkData, linkPath2, 'junction'); +verifyLink(linkPath2); + +function verifyLink(linkPath) { + const stats = fs.lstatSync(linkPath); + assert.ok(stats.isSymbolicLink()); + + const data1 = fs.readFileSync(`${linkPath}/x.txt`, 'ascii'); + const data2 = fs.readFileSync(`${linkTarget}/x.txt`, 'ascii'); + assert.strictEqual(data1, data2); + + // Clean up. + fs.unlinkSync(linkPath); +} diff --git a/test/js/node/test/parallel/test-fs-truncate-clear-file-zero.js b/test/js/node/test/parallel/test-fs-truncate-clear-file-zero.js new file mode 100644 index 0000000000..234e65e580 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-truncate-clear-file-zero.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +// This test ensures that `fs.truncate` opens the file with `r+` and not `w`, +// which had earlier resulted in the target file's content getting zeroed out. +// https://github.com/nodejs/node-v0.x-archive/issues/6233 + +const assert = require('assert'); +const fs = require('fs'); + +const filename = `${tmpdir.path}/truncate-file.txt`; + +tmpdir.refresh(); + +// Synchronous test. +{ + fs.writeFileSync(filename, '0123456789'); + assert.strictEqual(fs.readFileSync(filename).toString(), '0123456789'); + fs.truncateSync(filename, 5); + assert.strictEqual(fs.readFileSync(filename).toString(), '01234'); +} + +// Asynchronous test. +{ + fs.writeFileSync(filename, '0123456789'); + assert.strictEqual(fs.readFileSync(filename).toString(), '0123456789'); + fs.truncate( + filename, + 5, + common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(filename).toString(), '01234'); + }) + ); +} diff --git a/test/js/node/test/parallel/test-fs-truncate-sync.js b/test/js/node/test/parallel/test-fs-truncate-sync.js new file mode 100644 index 0000000000..66250cf438 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-truncate-sync.js @@ -0,0 +1,21 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const tmp = tmpdir.path; + +tmpdir.refresh(); + +const filename = path.resolve(tmp, 'truncate-sync-file.txt'); + +fs.writeFileSync(filename, 'hello world', 'utf8'); + +const fd = fs.openSync(filename, 'r+'); + +fs.truncateSync(fd, 5); +assert(fs.readFileSync(fd).equals(Buffer.from('hello'))); + +fs.closeSync(fd); +fs.unlinkSync(filename); diff --git a/test/js/node/test/parallel/test-fs-unlink-type-check.js b/test/js/node/test/parallel/test-fs-unlink-type-check.js new file mode 100644 index 0000000000..006e9ad734 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-unlink-type-check.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.unlink(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.unlinkSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-utimes-y2K38.js b/test/js/node/test/parallel/test-fs-utimes-y2K38.js new file mode 100644 index 0000000000..9e42e90feb --- /dev/null +++ b/test/js/node/test/parallel/test-fs-utimes-y2K38.js @@ -0,0 +1,66 @@ +'use strict'; +const common = require('../common'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const assert = require('assert'); +const fs = require('fs'); + +// Check for Y2K38 support. For Windows, assume it's there. Windows +// doesn't have `touch` and `date -r` which are used in the check for support. +if (!common.isWindows) { + const testFilePath = `${tmpdir.path}/y2k38-test`; + const testFileDate = '204001020304'; + const { spawnSync } = require('child_process'); + const touchResult = spawnSync('touch', + ['-t', testFileDate, testFilePath], + { encoding: 'utf8' }); + if (touchResult.status !== 0) { + common.skip('File system appears to lack Y2K38 support (touch failed)'); + } + + // On some file systems that lack Y2K38 support, `touch` will succeed but + // the time will be incorrect. + const dateResult = spawnSync('date', + ['-r', testFilePath, '+%Y%m%d%H%M'], + { encoding: 'utf8' }); + if (dateResult.status === 0) { + if (dateResult.stdout.trim() !== testFileDate) { + common.skip('File system appears to lack Y2k38 support (date failed)'); + } + } else { + // On some platforms `date` may not support the `-r` option. Usually + // this will result in a non-zero status and usage information printed. + // In this case optimistically proceed -- the earlier `touch` succeeded + // but validation that the file has the correct time is not easily possible. + assert.match(dateResult.stderr, /[Uu]sage:/); + } +} + +// Ref: https://github.com/nodejs/node/issues/13255 +const path = `${tmpdir.path}/test-utimes-precision`; +fs.writeFileSync(path, ''); + +const Y2K38_mtime = 2 ** 31; +fs.utimesSync(path, Y2K38_mtime, Y2K38_mtime); +const Y2K38_stats = fs.statSync(path); +assert.strictEqual(Y2K38_stats.mtime.getTime() / 1000, Y2K38_mtime); + +if (common.isWindows) { + // This value would get converted to (double)1713037251359.9998 + const truncate_mtime = 1713037251360; + fs.utimesSync(path, truncate_mtime / 1000, truncate_mtime / 1000); + const truncate_stats = fs.statSync(path); + assert.strictEqual(truncate_stats.mtime.getTime(), truncate_mtime); + + // test Y2K38 for windows + // This value if treaded as a `signed long` gets converted to -2135622133469. + // POSIX systems stores timestamps in {long t_sec, long t_usec}. + // NTFS stores times in nanoseconds in a single `uint64_t`, so when libuv + // calculates (long)`uv_timespec_t.tv_sec` we get 2's complement. + const overflow_mtime = 2159345162531; + fs.utimesSync(path, overflow_mtime / 1000, overflow_mtime / 1000); + const overflow_stats = fs.statSync(path); + assert.strictEqual(overflow_stats.mtime.getTime(), overflow_mtime); +} diff --git a/test/js/node/test/parallel/test-fs-watch-abort-signal.js b/test/js/node/test/parallel/test-fs-watch-abort-signal.js new file mode 100644 index 0000000000..33936d2d49 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-abort-signal.js @@ -0,0 +1,30 @@ +// Flags: --expose-internals +'use strict'; + +// Verify that AbortSignal integration works for fs.watch + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + + +{ + // Signal aborted after creating the watcher + const file = fixtures.path('empty.js'); + const ac = new AbortController(); + const { signal } = ac; + const watcher = fs.watch(file, { signal }); + watcher.once('close', common.mustCall()); + setImmediate(() => ac.abort()); +} +{ + // Signal aborted before creating the watcher + const file = fixtures.path('empty.js'); + const signal = AbortSignal.abort(); + const watcher = fs.watch(file, { signal }); + watcher.once('close', common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-watch-close-when-destroyed.js b/test/js/node/test/parallel/test-fs-watch-close-when-destroyed.js new file mode 100644 index 0000000000..afa5307e4d --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-close-when-destroyed.js @@ -0,0 +1,48 @@ +'use strict'; + +// This tests that closing a watcher when the underlying handle is +// already destroyed will result in a noop instead of a crash. + +const common = require('../common'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const tmpdir = require('../common/tmpdir'); +const fs = require('fs'); + +tmpdir.refresh(); +const root = tmpdir.resolve('watched-directory'); +fs.mkdirSync(root); + +const watcher = fs.watch(root, { persistent: false, recursive: false }); + +// The following listeners may or may not be invoked. + +watcher.addListener('error', () => { + setTimeout( + () => { watcher.close(); }, // Should not crash if it's invoked + common.platformTimeout(10) + ); +}); + +watcher.addListener('change', () => { + setTimeout( + () => { watcher.close(); }, + common.platformTimeout(10) + ); +}); + +fs.rmdirSync(root); +// Wait for the listener to hit +setTimeout( + common.mustCall(), + common.platformTimeout(100) +); diff --git a/test/js/node/test/parallel/test-fs-watch-encoding.js b/test/js/node/test/parallel/test-fs-watch-encoding.js new file mode 100644 index 0000000000..758c6c26be --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-encoding.js @@ -0,0 +1,95 @@ +'use strict'; + +// This test is a bit more complicated than it ideally needs to be to work +// around issues on OS X and SmartOS. +// +// On OS X, watch events are subject to peculiar timing oddities such that an +// event might fire out of order. The synchronous refreshing of the tmp +// directory might trigger an event on the watchers that are instantiated after +// it! +// +// On SmartOS, the watch events fire but the filename is null. + +const common = require('../common'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const fn = '新建文夹件.txt'; +const a = tmpdir.resolve(fn); + +const watchers = new Set(); + +function registerWatcher(watcher) { + watchers.add(watcher); +} + +function unregisterWatcher(watcher) { + watcher.close(); + watchers.delete(watcher); + if (watchers.size === 0) { + clearInterval(interval); + } +} + +{ + // Test that using the `encoding` option has the expected result. + const watcher = fs.watch( + tmpdir.path, + { encoding: 'hex' }, + (event, filename) => { + if (['e696b0e5bbbae69687e5a4b9e4bbb62e747874', null].includes(filename)) + done(watcher); + } + ); + registerWatcher(watcher); +} + +{ + // Test that in absence of `encoding` option has the expected result. + const watcher = fs.watch( + tmpdir.path, + (event, filename) => { + if ([fn, null].includes(filename)) + done(watcher); + } + ); + registerWatcher(watcher); +} + +{ + // Test that using the `encoding` option has the expected result. + const watcher = fs.watch( + tmpdir.path, + { encoding: 'buffer' }, + (event, filename) => { + if (filename instanceof Buffer && filename.toString('utf8') === fn) + done(watcher); + else if (filename === null) + done(watcher); + } + ); + registerWatcher(watcher); +} + +const done = common.mustCall(unregisterWatcher, watchers.size); + +// OS X and perhaps other systems can have surprising race conditions with +// file events. So repeat the operation in case it is missed the first time. +const interval = setInterval(() => { + const fd = fs.openSync(a, 'w+'); + fs.closeSync(fd); + fs.unlinkSync(a); +}, common.platformTimeout(100)); diff --git a/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js b/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js new file mode 100644 index 0000000000..e4baf90fd1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js @@ -0,0 +1,47 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Make sure the deletion event gets reported in the following scenario: +// 1. Watch a file. +// 2. The initial stat() goes okay. +// 3. Something deletes the watched file. +// 4. The second stat() fails with ENOENT. + +// The second stat() translates into the first 'change' event but a logic error +// stopped it from getting emitted. +// https://github.com/nodejs/node-v0.x-archive/issues/4027 + +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('watched'); +fs.writeFileSync(filename, 'quis custodiet ipsos custodes'); + +fs.watchFile(filename, { interval: 50 }, common.mustCall(function(curr, prev) { + fs.unwatchFile(filename); +})); + +fs.unlinkSync(filename); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js new file mode 100644 index 0000000000..511829fa38 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +// Add a file to subfolder of a watching folder + +const rootDirectory = fs.mkdtempSync(testDir + path.sep); +const testDirectory = path.join(rootDirectory, 'test-4'); +fs.mkdirSync(testDirectory); + +const file = 'folder-5'; +const filePath = path.join(testDirectory, file); +fs.mkdirSync(filePath); + +const subfolderPath = path.join(filePath, 'subfolder-6'); +fs.mkdirSync(subfolderPath); + +const childrenFile = 'file-7.txt'; +const childrenAbsolutePath = path.join(subfolderPath, childrenFile); +const relativePath = path.join(file, path.basename(subfolderPath), childrenFile); + +const watcher = fs.watch(testDirectory, { recursive: true }); +let watcherClosed = false; +watcher.on('change', function(event, filename) { + if (filename === relativePath) { + assert.strictEqual(event, 'rename'); + watcher.close(); + watcherClosed = true; + } +}); + +// Do the write with a delay to ensure that the OS is ready to notify us. +setTimeout(() => { + fs.writeFileSync(childrenAbsolutePath, 'world'); +}, common.platformTimeout(200)); + +process.once('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); +}); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js new file mode 100644 index 0000000000..852c7088d5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js @@ -0,0 +1,51 @@ +'use strict'; + +const common = require('../common'); +const { setTimeout } = require('timers/promises'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const { pathToFileURL } = require('url'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +(async () => { + // Add a file to already watching folder, and use URL as the path + + const rootDirectory = fs.mkdtempSync(testDir + path.sep); + const testDirectory = path.join(rootDirectory, 'test-5'); + fs.mkdirSync(testDirectory); + + const filePath = path.join(testDirectory, 'file-8.txt'); + const url = pathToFileURL(testDirectory); + + const watcher = fs.watch(url, { recursive: true }); + let watcherClosed = false; + watcher.on('change', function(event, filename) { + if (filename === path.basename(filePath)) { + assert.strictEqual(event, 'rename'); + watcher.close(); + watcherClosed = true; + } + }); + + await setTimeout(common.platformTimeout(100)); + fs.writeFileSync(filePath, 'world'); + + process.on('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js new file mode 100644 index 0000000000..e8724102c8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +// Add a file to already watching folder + +const rootDirectory = fs.mkdtempSync(testDir + path.sep); +const testDirectory = path.join(rootDirectory, 'test-1'); +fs.mkdirSync(testDirectory); + +const testFile = path.join(testDirectory, 'file-1.txt'); + +const watcher = fs.watch(testDirectory, { recursive: true }); +let watcherClosed = false; +watcher.on('change', function(event, filename) { + if (filename === path.basename(testFile)) { + assert.strictEqual(event, 'rename'); + watcher.close(); + watcherClosed = true; + } +}); + +// Do the write with a delay to ensure that the OS is ready to notify us. +setTimeout(() => { + fs.writeFileSync(testFile, 'world'); +}, common.platformTimeout(200)); + +process.once('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); +}); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js new file mode 100644 index 0000000000..1a6671de2f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +const { setTimeout } = require('timers/promises'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +(async () => { + // Add a folder to already watching folder + + const rootDirectory = fs.mkdtempSync(testDir + path.sep); + const testDirectory = path.join(rootDirectory, 'test-2'); + fs.mkdirSync(testDirectory); + + const testFile = path.join(testDirectory, 'folder-2'); + + const watcher = fs.watch(testDirectory, { recursive: true }); + let watcherClosed = false; + watcher.on('change', function(event, filename) { + if (filename === path.basename(testFile)) { + assert.strictEqual(event, 'rename'); + watcher.close(); + watcherClosed = true; + } + }); + + await setTimeout(common.platformTimeout(100)); + fs.mkdirSync(testFile); + + process.once('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-assert-leaks.js b/test/js/node/test/parallel/test-fs-watch-recursive-assert-leaks.js new file mode 100644 index 0000000000..f5950e38ce --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-assert-leaks.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +const { setTimeout } = require('timers/promises'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +// Assert recursive watch does not leak handles +const rootDirectory = fs.mkdtempSync(testDir + path.sep); +const testDirectory = path.join(rootDirectory, 'test-7'); +const filePath = path.join(testDirectory, 'only-file.txt'); +fs.mkdirSync(testDirectory); + +let watcherClosed = false; +const watcher = fs.watch(testDirectory, { recursive: true }); +watcher.on('change', common.mustCallAtLeast(async (event, filename) => { + await setTimeout(common.platformTimeout(100)); + if (filename === path.basename(filePath)) { + watcher.close(); + watcherClosed = true; + } + await setTimeout(common.platformTimeout(100)); + assert(!process._getActiveHandles().some((handle) => handle.constructor.name === 'StatWatcher')); +})); + +process.on('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); +}); + +// Do the write with a delay to ensure that the OS is ready to notify us. +(async () => { + await setTimeout(200); + fs.writeFileSync(filePath, 'content'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-delete.js b/test/js/node/test/parallel/test-fs-watch-recursive-delete.js new file mode 100644 index 0000000000..8e78ad54d6 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-delete.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const fs = require('fs'); + +if (common.isSunOS) + common.skip('SunOS behaves differently'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +tmpdir.refresh(); + +fs.mkdirSync(tmpdir.resolve('./parent/child'), { recursive: true }); + +fs.writeFileSync(tmpdir.resolve('./parent/child/test.tmp'), 'test'); + +const toWatch = tmpdir.resolve('./parent'); + +const onFileUpdate = common.mustCallAtLeast((eventType, filename) => { + // We are only checking for the filename to avoid having Windows, Linux and Mac specific assertions + if (fs.readdirSync(tmpdir.resolve('./parent')).length === 0) { + watcher.close(); + } +}, 1); + +const watcher = fs.watch(toWatch, { recursive: true }, onFileUpdate); + +// We must wait a bit `fs.rm()` to let the watcher be set up properly +setTimeout(() => { + fs.rm(tmpdir.resolve('./parent/child'), { recursive: true }, common.mustCall()); +}, common.platformTimeout(500)); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js b/test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js new file mode 100644 index 0000000000..dd7a64e1f0 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); +const { watch, writeFileSync } = require('node:fs'); +const { join } = require('node:path'); +const tmpdir = require('../common/tmpdir.js'); +const assert = require('assert'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +tmpdir.refresh(); + +const tmpDir = tmpdir.path; +const filename = join(tmpDir, 'test.file'); + +const keepalive = setTimeout(() => { + throw new Error('timed out'); +}, common.platformTimeout(30_000)); + +function doWatch() { + const watcher = watch(tmpDir, { recursive: true }, common.mustCall((eventType, _filename) => { + clearTimeout(keepalive); + watcher.close(); + assert.strictEqual(eventType, 'rename'); + assert.strictEqual(join(tmpDir, _filename), filename); + })); + + // Do the write with a delay to ensure that the OS is ready to notify us. + setTimeout(() => { + writeFileSync(filename, 'foobar2'); + }, common.platformTimeout(200)); +} + +if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + setTimeout(doWatch, common.platformTimeout(100)); +} else { + doWatch(); +} diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-update-file.js b/test/js/node/test/parallel/test-fs-watch-recursive-update-file.js new file mode 100644 index 0000000000..e27a4c37e4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-update-file.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +// Watch a folder and update an already existing file in it. + +const rootDirectory = fs.mkdtempSync(testDir + path.sep); +const testDirectory = path.join(rootDirectory, 'test-0'); +fs.mkdirSync(testDirectory); + +const testFile = path.join(testDirectory, 'file-1.txt'); +fs.writeFileSync(testFile, 'hello'); + +const watcher = fs.watch(testDirectory, { recursive: true }); +watcher.on('change', common.mustCallAtLeast(function(event, filename) { + // Libuv inconsistently emits a rename event for the file we are watching + assert.ok(event === 'change' || event === 'rename'); + + if (filename === path.basename(testFile)) { + watcher.close(); + } +})); + +// Do the write with a delay to ensure that the OS is ready to notify us. +setTimeout(() => { + fs.writeFileSync(testFile, 'hello'); +}, common.platformTimeout(200)); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-validate.js b/test/js/node/test/parallel/test-fs-watch-recursive-validate.js new file mode 100644 index 0000000000..09eccc2d7e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-validate.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +(async () => { + // Handle non-boolean values for options.recursive + + if (!common.isWindows && !common.isMacOS) { + assert.throws(() => { + const testsubdir = fs.mkdtempSync(testDir + path.sep); + fs.watch(testsubdir, { recursive: '1' }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + }); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js b/test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js new file mode 100644 index 0000000000..3449db8e59 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +(async () => { + // Watch a file (not a folder) using fs.watch + + const rootDirectory = fs.mkdtempSync(testDir + path.sep); + const testDirectory = path.join(rootDirectory, 'test-6'); + fs.mkdirSync(testDirectory); + + const filePath = path.join(testDirectory, 'only-file.txt'); + fs.writeFileSync(filePath, 'hello'); + + const watcher = fs.watch(filePath, { recursive: true }); + let watcherClosed = false; + let interval; + watcher.on('change', function(event, filename) { + assert.strictEqual(event, 'change'); + + if (filename === path.basename(filePath)) { + clearInterval(interval); + interval = null; + watcher.close(); + watcherClosed = true; + } + }); + + interval = setInterval(() => { + fs.writeFileSync(filePath, 'world'); + }, common.platformTimeout(10)); + + process.on('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); + assert.strictEqual(interval, null); + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watch-ref-unref.js b/test/js/node/test/parallel/test-fs-watch-ref-unref.js new file mode 100644 index 0000000000..e51ecaf5b2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-ref-unref.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const fs = require('fs'); + +const watcher = fs.watch(__filename, common.mustNotCall()); + +watcher.unref(); + +setTimeout( + common.mustCall(() => { + watcher.ref(); + watcher.unref(); + }), + common.platformTimeout(100) +); diff --git a/test/js/node/test/parallel/test-fs-watch-stop-sync.js b/test/js/node/test/parallel/test-fs-watch-stop-sync.js new file mode 100644 index 0000000000..7f0882e489 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-stop-sync.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); + +// This test checks that the `stop` event is emitted asynchronously. +// +// If it isn't asynchronous, then the listener will be called during the +// execution of `watch.stop()`. That would be a bug. +// +// If it is asynchronous, then the listener will be removed before the event is +// emitted. + +const fs = require('fs'); + +const listener = common.mustNotCall( + 'listener should have been removed before the event was emitted' +); + +const watch = fs.watchFile(__filename, common.mustNotCall()); +watch.once('stop', listener); +watch.stop(); +watch.removeListener('stop', listener); diff --git a/test/js/node/test/parallel/test-fs-watch.js b/test/js/node/test/parallel/test-fs-watch.js new file mode 100644 index 0000000000..5194e04fce --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch.js @@ -0,0 +1,109 @@ +'use strict'; +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// Tests if `filename` is provided to watcher on supported platforms + +const fs = require('fs'); +const assert = require('assert'); +const { join } = require('path'); + +class WatchTestCase { + constructor(shouldInclude, dirName, fileName, field) { + this.dirName = dirName; + this.fileName = fileName; + this.field = field; + this.shouldSkip = !shouldInclude; + } + get dirPath() { return tmpdir.resolve(this.dirName); } + get filePath() { return join(this.dirPath, this.fileName); } +} + +const cases = [ + // Watch on a file should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows || common.isAIX, + 'watch1', + 'foo', + 'filePath' + ), + // Watch on a directory should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows, + 'watch2', + 'bar', + 'dirPath' + ), +]; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function doWatchTest(testCase) { + let interval; + const pathToWatch = testCase[testCase.field]; + const watcher = fs.watch(pathToWatch); + watcher.on('error', (err) => { + if (interval) { + clearInterval(interval); + interval = null; + } + assert.fail(err); + }); + watcher.on('close', common.mustCall(() => { + watcher.close(); // Closing a closed watcher should be a noop + })); + watcher.on('change', common.mustCall(function(eventType, argFilename) { + if (interval) { + clearInterval(interval); + interval = null; + } + if (common.isMacOS) + assert.strictEqual(['rename', 'change'].includes(eventType), true); + else + assert.strictEqual(eventType, 'change'); + assert.strictEqual(argFilename, testCase.fileName); + + watcher.close(); + + // We document that watchers cannot be used anymore when it's closed, + // here we turn the methods into noops instead of throwing + watcher.close(); // Closing a closed watcher should be a noop + })); + + // Long content so it's actually flushed. toUpperCase so there's real change. + const content2 = Date.now() + testCase.fileName.toUpperCase().repeat(1e4); + interval = setInterval(() => { + fs.writeFileSync(testCase.filePath, ''); + fs.writeFileSync(testCase.filePath, content2); + }, 100); +} + +for (const testCase of cases) { + if (testCase.shouldSkip) continue; + fs.mkdirSync(testCase.dirPath); + // Long content so it's actually flushed. + const content1 = Date.now() + testCase.fileName.toLowerCase().repeat(1e4); + fs.writeFileSync(testCase.filePath, content1); + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + setTimeout(() => { + doWatchTest(testCase); + }, common.platformTimeout(100)); + } else { + doWatchTest(testCase); + } +} + +[false, 1, {}, [], null, undefined].forEach((input) => { + assert.throws( + () => fs.watch(input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-watchfile-ref-unref.js b/test/js/node/test/parallel/test-fs-watchfile-ref-unref.js new file mode 100644 index 0000000000..4ac2691ea9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watchfile-ref-unref.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); + +const fs = require('fs'); +const assert = require('assert'); + +const uncalledListener = common.mustNotCall(); +const uncalledListener2 = common.mustNotCall(); +const watcher = fs.watchFile(__filename, uncalledListener); + +watcher.unref(); +watcher.unref(); +watcher.ref(); +watcher.unref(); +watcher.ref(); +watcher.ref(); +watcher.unref(); + +fs.unwatchFile(__filename, uncalledListener); + +// Watch the file with two different listeners. +fs.watchFile(__filename, uncalledListener); +const watcher2 = fs.watchFile(__filename, uncalledListener2); + +setTimeout( + common.mustCall(() => { + fs.unwatchFile(__filename, common.mustNotCall()); + assert.strictEqual(watcher2.listenerCount('change'), 2); + fs.unwatchFile(__filename, uncalledListener); + assert.strictEqual(watcher2.listenerCount('change'), 1); + watcher2.unref(); + }), + common.platformTimeout(100) +); diff --git a/test/js/node/test/parallel/test-fs-write-file-buffer.js b/test/js/node/test/parallel/test-fs-write-file-buffer.js new file mode 100644 index 0000000000..23dc48081a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-file-buffer.js @@ -0,0 +1,51 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const fs = require('fs'); + +let data = [ + '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcH', + 'Bw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/', + '2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e', + 'Hh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAQABADASIAAhEBAxEB/8QA', + 'HwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF', + 'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK', + 'FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1', + 'dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG', + 'x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEB', + 'AQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAEC', + 'AxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRom', + 'JygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE', + 'hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU', + '1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDhfBUFl/wk', + 'OmPqKJJZw3aiZFBw4z93jnkkc9u9dj8XLfSI/EBt7DTo7ea2Ox5YXVo5FC7g', + 'Tjq24nJPXNVtO0KATRvNHCIg3zoWJWQHqp+o4pun+EtJ0zxBq8mnLJa2d1L5', + '0NvnKRjJBUE5PAx3NYxxUY0pRtvYHSc5Ka2X9d7H/9k=']; + +data = data.join('\n'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const buf = Buffer.from(data, 'base64'); +fs.writeFileSync(tmpdir.resolve('test.jpg'), buf); diff --git a/test/js/node/test/parallel/test-fs-write-file-invalid-path.js b/test/js/node/test/parallel/test-fs-write-file-invalid-path.js new file mode 100644 index 0000000000..aaa7eacde5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-file-invalid-path.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +if (!common.isWindows) + common.skip('This test is for Windows only.'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const DATA_VALUE = 'hello'; + +// Refs: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx +// Ignore '/', '\\' and ':' +const RESERVED_CHARACTERS = '<>"|?*'; + +[...RESERVED_CHARACTERS].forEach((ch) => { + const pathname = tmpdir.resolve(`somefile_${ch}`); + assert.throws( + () => { + fs.writeFileSync(pathname, DATA_VALUE); + }, + /^Error: ENOENT: no such file or directory, open '.*'$/, + `failed with '${ch}'`); +}); + +// Test for ':' (NTFS data streams). +// Refs: https://msdn.microsoft.com/en-us/library/windows/desktop/bb540537.aspx +const pathname = tmpdir.resolve('foo:bar'); +fs.writeFileSync(pathname, DATA_VALUE); + +let content = ''; +const fileDataStream = fs.createReadStream(pathname, { + encoding: 'utf8' +}); + +fileDataStream.on('data', (data) => { + content += data; +}); + +fileDataStream.on('end', common.mustCall(() => { + assert.strictEqual(content, DATA_VALUE); +})); diff --git a/test/js/node/test/parallel/test-fs-write-no-fd.js b/test/js/node/test/parallel/test-fs-write-no-fd.js new file mode 100644 index 0000000000..576457203e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-no-fd.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); + +assert.throws(function() { + fs.write(null, Buffer.allocUnsafe(1), 0, 1, common.mustNotCall()); +}, /TypeError/); + +assert.throws(function() { + fs.write(null, '1', 0, 1, common.mustNotCall()); +}, /TypeError/); diff --git a/test/js/node/test/parallel/test-fs-write-stream-close-without-callback.js b/test/js/node/test/parallel/test-fs-write-stream-close-without-callback.js new file mode 100644 index 0000000000..7bf83cd809 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-close-without-callback.js @@ -0,0 +1,12 @@ +'use strict'; + +require('../common'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const s = fs.createWriteStream(tmpdir.resolve('nocallback')); + +s.end('hello world'); +s.close(); diff --git a/test/js/node/test/parallel/test-fs-write-stream-encoding.js b/test/js/node/test/parallel/test-fs-write-stream-encoding.js new file mode 100644 index 0000000000..f06fae923c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-encoding.js @@ -0,0 +1,35 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const stream = require('stream'); +const tmpdir = require('../common/tmpdir'); +const firstEncoding = 'base64'; +const secondEncoding = 'latin1'; + +const examplePath = fixtures.path('x.txt'); +const dummyPath = tmpdir.resolve('x.txt'); + +tmpdir.refresh(); + +const exampleReadStream = fs.createReadStream(examplePath, { + encoding: firstEncoding +}); + +const dummyWriteStream = fs.createWriteStream(dummyPath, { + encoding: firstEncoding +}); + +exampleReadStream.pipe(dummyWriteStream).on('finish', function() { + const assertWriteStream = new stream.Writable({ + write: function(chunk, enc, next) { + const expected = Buffer.from('xyz\n'); + assert(chunk.equals(expected)); + } + }); + assertWriteStream.setDefaultEncoding(secondEncoding); + fs.createReadStream(dummyPath, { + encoding: secondEncoding + }).pipe(assertWriteStream); +}); diff --git a/test/js/node/test/parallel/test-fs-write-stream-end.js b/test/js/node/test/parallel/test-fs-write-stream-end.js new file mode 100644 index 0000000000..7f0cc65540 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-end.js @@ -0,0 +1,59 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + const file = tmpdir.resolve('write-end-test0.txt'); + const stream = fs.createWriteStream(file); + stream.end(); + stream.on('close', common.mustCall()); +} + +{ + const file = tmpdir.resolve('write-end-test1.txt'); + const stream = fs.createWriteStream(file); + stream.end('a\n', 'utf8'); + stream.on('close', common.mustCall(function() { + const content = fs.readFileSync(file, 'utf8'); + assert.strictEqual(content, 'a\n'); + })); +} + +{ + const file = tmpdir.resolve('write-end-test2.txt'); + const stream = fs.createWriteStream(file); + stream.end(); + + let calledOpen = false; + stream.on('open', () => { + calledOpen = true; + }); + stream.on('finish', common.mustCall(() => { + assert.strictEqual(calledOpen, true); + })); +} diff --git a/test/js/node/test/parallel/test-fs-write-stream.js b/test/js/node/test/parallel/test-fs-write-stream.js new file mode 100644 index 0000000000..a3dccf7cdc --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream.js @@ -0,0 +1,66 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const file = tmpdir.resolve('write.txt'); + +tmpdir.refresh(); + +{ + const stream = fs.WriteStream(file); + const _fs_close = fs.close; + + fs.close = function(fd) { + assert.ok(fd, 'fs.close must not be called without an undefined fd.'); + fs.close = _fs_close; + fs.closeSync(fd); + }; + stream.destroy(); +} + +{ + const stream = fs.createWriteStream(file); + + stream.on('drain', function() { + assert.fail('\'drain\' event must not be emitted before ' + + 'stream.write() has been called at least once.'); + }); + stream.destroy(); +} + +// Throws if data is not of type Buffer. +{ + const stream = fs.createWriteStream(file); + stream.on('error', common.mustNotCall()); + assert.throws(() => { + stream.write(42); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + stream.destroy(); +} diff --git a/test/js/node/test/parallel/test-fs-write-sync.js b/test/js/node/test/parallel/test-fs-write-sync.js new file mode 100644 index 0000000000..733892c35e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-sync.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const filename = tmpdir.resolve('write.txt'); + +tmpdir.refresh(); + +{ + const parameters = [Buffer.from('bár'), 0, Buffer.byteLength('bár')]; + + // The first time fs.writeSync is called with all parameters provided. + // After that, each pop in the cycle removes the final parameter. So: + // - The 2nd time fs.writeSync with a buffer, without the length parameter. + // - The 3rd time fs.writeSync with a buffer, without the offset and length + // parameters. + while (parameters.length > 0) { + const fd = fs.openSync(filename, 'w'); + + let written = fs.writeSync(fd, ''); + assert.strictEqual(written, 0); + + fs.writeSync(fd, 'foo'); + + written = fs.writeSync(fd, ...parameters); + assert.ok(written > 3); + fs.closeSync(fd); + + assert.strictEqual(fs.readFileSync(filename, 'utf-8'), 'foobár'); + + parameters.pop(); + } +} diff --git a/test/js/node/test/parallel/test-fs-writestream-open-write.js b/test/js/node/test/parallel/test-fs-writestream-open-write.js new file mode 100644 index 0000000000..af02d90ae6 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-writestream-open-write.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const { strictEqual } = require('assert'); +const fs = require('fs'); + +// Regression test for https://github.com/nodejs/node/issues/51993 + +tmpdir.refresh(); + +const file = tmpdir.resolve('test-fs-writestream-open-write.txt'); + +const w = fs.createWriteStream(file); + +w.on('open', common.mustCall(() => { + w.write('hello'); + + process.nextTick(() => { + w.write('world'); + w.end(); + }); +})); + +w.on('close', common.mustCall(() => { + strictEqual(fs.readFileSync(file, 'utf8'), 'helloworld'); + fs.unlinkSync(file); +})); diff --git a/test/js/node/test/parallel/test-global-domexception.js b/test/js/node/test/parallel/test-global-domexception.js new file mode 100644 index 0000000000..d19b5a5e4f --- /dev/null +++ b/test/js/node/test/parallel/test-global-domexception.js @@ -0,0 +1,11 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); + +assert.strictEqual(typeof DOMException, 'function'); + +assert.throws(() => { + atob('我要抛错!'); +}, DOMException); diff --git a/test/js/node/test/parallel/test-global-encoder.js b/test/js/node/test/parallel/test-global-encoder.js new file mode 100644 index 0000000000..0e98bc806c --- /dev/null +++ b/test/js/node/test/parallel/test-global-encoder.js @@ -0,0 +1,8 @@ +'use strict'; + +require('../common'); +const { strictEqual } = require('assert'); +const util = require('util'); + +strictEqual(TextDecoder, util.TextDecoder); +strictEqual(TextEncoder, util.TextEncoder); diff --git a/test/js/node/test/parallel/test-global-webcrypto.js b/test/js/node/test/parallel/test-global-webcrypto.js new file mode 100644 index 0000000000..9eb18ca9d1 --- /dev/null +++ b/test/js/node/test/parallel/test-global-webcrypto.js @@ -0,0 +1,13 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +/* eslint-disable no-restricted-properties */ +assert.strictEqual(globalThis.crypto, crypto.webcrypto); +assert.strictEqual(Crypto, crypto.webcrypto.constructor); +assert.strictEqual(SubtleCrypto, crypto.webcrypto.subtle.constructor); diff --git a/test/js/node/test/parallel/test-handle-wrap-close-abort.js b/test/js/node/test/parallel/test-handle-wrap-close-abort.js new file mode 100644 index 0000000000..b91f9dd349 --- /dev/null +++ b/test/js/node/test/parallel/test-handle-wrap-close-abort.js @@ -0,0 +1,37 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +process.on('uncaughtException', common.mustCall(2)); + +setTimeout(function() { + process.nextTick(function() { + const c = setInterval(function() { + clearInterval(c); + throw new Error('setInterval'); + }, 1); + }); + setTimeout(function() { + throw new Error('setTimeout'); + }, 1); +}, 1); diff --git a/test/js/node/test/parallel/test-http-abort-before-end.js b/test/js/node/test/parallel/test-http-abort-before-end.js new file mode 100644 index 0000000000..5577f256ca --- /dev/null +++ b/test/js/node/test/parallel/test-http-abort-before-end.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); + +const server = http.createServer(common.mustNotCall()); + +server.listen(0, common.mustCall(() => { + const req = http.request({ + method: 'GET', + host: '127.0.0.1', + port: server.address().port + }); + + req.on('abort', common.mustCall(() => { + server.close(); + })); + + req.on('error', common.mustNotCall()); + + req.abort(); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-agent-false.js b/test/js/node/test/parallel/test-http-agent-false.js new file mode 100644 index 0000000000..2f4505ef66 --- /dev/null +++ b/test/js/node/test/parallel/test-http-agent-false.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); + +// Sending `agent: false` when `port: null` is also passed in (i.e. the result +// of a `url.parse()` call with the default port used, 80 or 443), should not +// result in an assertion error... +const opts = { + host: '127.0.0.1', + port: null, + path: '/', + method: 'GET', + agent: false +}; + +// We just want an "error" (no local HTTP server on port 80) or "response" +// to happen (user happens ot have HTTP server running on port 80). +// As long as the process doesn't crash from a C++ assertion then we're good. +const req = http.request(opts); + +// Will be called by either the response event or error event, not both +const oneResponse = common.mustCall(); +req.on('response', oneResponse); +req.on('error', oneResponse); +req.end(); diff --git a/test/js/node/test/parallel/test-http-agent-keepalive-delay.js b/test/js/node/test/parallel/test-http-agent-keepalive-delay.js new file mode 100644 index 0000000000..b5edd78b66 --- /dev/null +++ b/test/js/node/test/parallel/test-http-agent-keepalive-delay.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const { Agent } = require('_http_agent'); + +const agent = new Agent({ + keepAlive: true, + keepAliveMsecs: 1000, +}); + +const server = http.createServer(common.mustCall((req, res) => { + res.end('ok'); +})); + +server.listen(0, common.mustCall(() => { + const createConnection = agent.createConnection; + agent.createConnection = (options, ...args) => { + assert.strictEqual(options.keepAlive, true); + assert.strictEqual(options.keepAliveInitialDelay, agent.keepAliveMsecs); + return createConnection.call(agent, options, ...args); + }; + http.get({ + host: 'localhost', + port: server.address().port, + agent: agent, + path: '/' + }, common.mustCall((res) => { + // for emit end event + res.on('data', () => {}); + res.on('end', () => { + server.close(); + }); + })); +})); diff --git a/test/js/node/test/parallel/test-http-agent-no-protocol.js b/test/js/node/test/parallel/test-http-agent-no-protocol.js new file mode 100644 index 0000000000..d1eaf242a5 --- /dev/null +++ b/test/js/node/test/parallel/test-http-agent-no-protocol.js @@ -0,0 +1,41 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); +const url = require('url'); + +const server = http.createServer(common.mustCall((req, res) => { + res.end(); +})).listen(0, '127.0.0.1', common.mustCall(() => { + const opts = url.parse(`http://127.0.0.1:${server.address().port}/`); + + // Remove the `protocol` field… the `http` module should fall back + // to "http:", as defined by the global, default `http.Agent` instance. + opts.agent = new http.Agent(); + opts.agent.protocol = null; + + http.get(opts, common.mustCall((res) => { + res.resume(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http-agent-null.js b/test/js/node/test/parallel/test-http-agent-null.js new file mode 100644 index 0000000000..0f87d09813 --- /dev/null +++ b/test/js/node/test/parallel/test-http-agent-null.js @@ -0,0 +1,37 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); + +const server = http.createServer(common.mustCall((req, res) => { + res.end(); +})).listen(0, common.mustCall(() => { + const options = { + agent: null, + port: server.address().port + }; + http.get(options, common.mustCall((res) => { + res.resume(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http-agent-uninitialized-with-handle.js b/test/js/node/test/parallel/test-http-agent-uninitialized-with-handle.js new file mode 100644 index 0000000000..77f0177173 --- /dev/null +++ b/test/js/node/test/parallel/test-http-agent-uninitialized-with-handle.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const net = require('net'); + +const agent = new http.Agent({ + keepAlive: true, +}); +const socket = new net.Socket(); +// If _handle exists then internals assume a couple methods exist. +socket._handle = { + ref() { }, + readStart() { }, +}; + +const server = http.createServer(common.mustCall((req, res) => { + res.end(); +})).listen(0, common.mustCall(() => { + const req = new http.ClientRequest(`http://localhost:${server.address().port}/`); + + // Manually add the socket without a _handle. + agent.freeSockets[agent.getName(req)] = [socket]; + // Now force the agent to use the socket and check that _handle exists before + // calling asyncReset(). + agent.addRequest(req, {}); + req.on('response', common.mustCall(() => { + server.close(); + })); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-agent-uninitialized.js b/test/js/node/test/parallel/test-http-agent-uninitialized.js new file mode 100644 index 0000000000..dbb38e3b0f --- /dev/null +++ b/test/js/node/test/parallel/test-http-agent-uninitialized.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const net = require('net'); + +const agent = new http.Agent({ + keepAlive: true, +}); +const socket = new net.Socket(); + +const server = http.createServer(common.mustCall((req, res) => { + res.end(); +})).listen(0, common.mustCall(() => { + const req = new http.ClientRequest(`http://localhost:${server.address().port}/`); + + // Manually add the socket without a _handle. + agent.freeSockets[agent.getName(req)] = [socket]; + // Now force the agent to use the socket and check that _handle exists before + // calling asyncReset(). + agent.addRequest(req, {}); + req.on('response', common.mustCall(() => { + server.close(); + })); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-allow-req-after-204-res.js b/test/js/node/test/parallel/test-http-allow-req-after-204-res.js new file mode 100644 index 0000000000..84dd876985 --- /dev/null +++ b/test/js/node/test/parallel/test-http-allow-req-after-204-res.js @@ -0,0 +1,61 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); +const Countdown = require('../common/countdown'); + +// first 204 or 304 works, subsequent anything fails +const codes = [204, 200]; + +const countdown = new Countdown(codes.length, () => server.close()); + +const server = http.createServer(common.mustCall((req, res) => { + const code = codes.shift(); + assert.strictEqual(typeof code, 'number'); + assert.ok(code > 0); + res.writeHead(code, {}); + res.end(); +}, codes.length)); + +function nextRequest() { + + const request = http.get({ + port: server.address().port, + path: '/' + }, common.mustCall((response) => { + response.on('end', common.mustCall(() => { + if (countdown.dec()) { + // throws error: + nextRequest(); + // TODO: investigate why this does not work fine even though it should. + // works just fine: + // process.nextTick(nextRequest); + } + })); + response.resume(); + })); + request.end(); +} + +server.listen(0, nextRequest); diff --git a/test/js/node/test/parallel/test-http-bind-twice.js b/test/js/node/test/parallel/test-http-bind-twice.js new file mode 100644 index 0000000000..50834cc5cb --- /dev/null +++ b/test/js/node/test/parallel/test-http-bind-twice.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server1 = http.createServer(common.mustNotCall()); +server1.listen(0, '127.0.0.1', common.mustCall(function() { + const server2 = http.createServer(common.mustNotCall()); + server2.listen(this.address().port, '127.0.0.1', common.mustNotCall()); + + server2.on('error', common.mustCall(function(e) { + assert.strictEqual(e.code, 'EADDRINUSE'); + server1.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http-buffer-sanity.js b/test/js/node/test/parallel/test-http-buffer-sanity.js new file mode 100644 index 0000000000..05c027fd48 --- /dev/null +++ b/test/js/node/test/parallel/test-http-buffer-sanity.js @@ -0,0 +1,71 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const bufferSize = 5 * 1024 * 1024; +let measuredSize = 0; + +const buffer = Buffer.allocUnsafe(bufferSize); +for (let i = 0; i < buffer.length; i++) { + buffer[i] = i % 256; +} + +const server = http.Server(function(req, res) { + server.close(); + + let i = 0; + + req.on('data', (d) => { + measuredSize += d.length; + for (let j = 0; j < d.length; j++) { + assert.strictEqual(d[j], buffer[i]); + i++; + } + }); + + req.on('end', common.mustCall(() => { + assert.strictEqual(measuredSize, bufferSize); + res.writeHead(200); + res.write('thanks'); + res.end(); + })); +}); + +server.listen(0, common.mustCall(() => { + const req = http.request({ + port: server.address().port, + method: 'POST', + path: '/', + headers: { 'content-length': buffer.length } + }, common.mustCall((res) => { + res.setEncoding('utf8'); + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', common.mustCall(() => { + assert.strictEqual(data, 'thanks'); + })); + })); + req.end(buffer); +})); diff --git a/test/js/node/test/parallel/test-http-chunk-problem.js b/test/js/node/test/parallel/test-http-chunk-problem.js new file mode 100644 index 0000000000..3629b75766 --- /dev/null +++ b/test/js/node/test/parallel/test-http-chunk-problem.js @@ -0,0 +1,103 @@ +'use strict'; +// http://groups.google.com/group/nodejs/browse_thread/thread/f66cd3c960406919 +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +if (process.argv[2] === 'request') { + const http = require('http'); + const options = { + port: +process.argv[3], + path: '/' + }; + + http.get(options, (res) => { + res.pipe(process.stdout); + }); + + return; +} + +if (process.argv[2] === 'shasum') { + const crypto = require('crypto'); + const shasum = crypto.createHash('sha1'); + process.stdin.on('data', (d) => { + shasum.update(d); + }); + + process.stdin.on('close', () => { + process.stdout.write(shasum.digest('hex')); + }); + + return; +} + +const http = require('http'); +const cp = require('child_process'); + +const tmpdir = require('../common/tmpdir'); + +const filename = tmpdir.resolve('big'); +let server; + +function executeRequest(cb) { + // The execPath might contain chars that should be escaped in a shell context. + // On non-Windows, we can pass the path via the env; `"` is not a valid char on + // Windows, so we can simply pass the path. + const node = `"${common.isWindows ? process.execPath : '$NODE'}"`; + const file = `"${common.isWindows ? __filename : '$FILE'}"`; + const env = common.isWindows ? process.env : { ...process.env, NODE: process.execPath, FILE: __filename }; + cp.exec([node, + file, + 'request', + server.address().port, + '|', + node, + file, + 'shasum' ].join(' '), + { env }, + (err, stdout, stderr) => { + if (stderr.trim() !== '') { + console.log(stderr); + } + assert.ifError(err); + assert.strictEqual(stdout.slice(0, 40), + '8c206a1a87599f532ce68675536f0b1546900d7a'); + cb(); + } + ); +} + + +tmpdir.refresh(); + +common.createZeroFilledFile(filename); + +server = http.createServer(function(req, res) { + res.writeHead(200); + + // Create the subprocess + const cat = cp.spawn('cat', [filename]); + + // Stream the data through to the response as binary chunks + cat.stdout.on('data', (data) => { + res.write(data); + }); + + cat.stdout.on('end', () => res.end()); + + // End the response on exit (and log errors) + cat.on('exit', (code) => { + if (code !== 0) { + console.error(`subprocess exited with code ${code}`); + process.exit(1); + } + }); + +}); + +server.listen(0, () => { + executeRequest(() => server.close()); +}); diff --git a/test/js/node/test/parallel/test-http-chunked-smuggling.js b/test/js/node/test/parallel/test-http-chunked-smuggling.js new file mode 100644 index 0000000000..dbad45e1be --- /dev/null +++ b/test/js/node/test/parallel/test-http-chunked-smuggling.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const net = require('net'); +const assert = require('assert'); + +// Verify that invalid chunk extensions cannot be used to perform HTTP request +// smuggling attacks. + +const server = http.createServer(common.mustCall((request, response) => { + assert.notStrictEqual(request.url, '/admin'); + response.end('hello world'); +}), 1); + +server.listen(0, common.mustCall(start)); + +function start() { + const sock = net.connect(server.address().port); + + sock.write('' + + 'GET / HTTP/1.1\r\n' + + 'Host: localhost:8080\r\n' + + 'Transfer-Encoding: chunked\r\n' + + '\r\n' + + '2;\n' + + 'xx\r\n' + + '4c\r\n' + + '0\r\n' + + '\r\n' + + 'GET /admin HTTP/1.1\r\n' + + 'Host: localhost:8080\r\n' + + 'Transfer-Encoding: chunked\r\n' + + '\r\n' + + '0\r\n' + + '\r\n' + ); + + sock.resume(); + sock.on('end', common.mustCall(function() { + server.close(); + })); +} diff --git a/test/js/node/test/parallel/test-http-chunked.js b/test/js/node/test/parallel/test-http-chunked.js new file mode 100644 index 0000000000..cfa34e3afe --- /dev/null +++ b/test/js/node/test/parallel/test-http-chunked.js @@ -0,0 +1,48 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const fixtures = require('../common/fixtures'); +const UTF8_STRING = fixtures.utf8TestText; + +const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf8' }); + res.end(UTF8_STRING, 'utf8'); +})); +server.listen(0, common.mustCall(() => { + let data = ''; + http.get({ + path: '/', + host: 'localhost', + port: server.address().port + }, common.mustCall((x) => { + x.setEncoding('utf8'); + x.on('data', (c) => data += c); + x.on('end', common.mustCall(() => { + assert.strictEqual(typeof data, 'string'); + assert.strictEqual(UTF8_STRING, data); + server.close(); + })); + })).end(); +})); diff --git a/test/js/node/test/parallel/test-http-client-abort2.js b/test/js/node/test/parallel/test-http-client-abort2.js new file mode 100644 index 0000000000..bc4b0e4083 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-abort2.js @@ -0,0 +1,38 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); + +const server = http.createServer(common.mustCall((req, res) => { + res.end('Hello'); +})); + +server.listen(0, common.mustCall(() => { + const options = { port: server.address().port }; + const req = http.get(options, common.mustCall((res) => { + res.on('data', (data) => { + req.abort(); + server.close(); + }); + })); +})); diff --git a/test/js/node/test/parallel/test-http-client-close-with-default-agent.js b/test/js/node/test/parallel/test-http-client-close-with-default-agent.js new file mode 100644 index 0000000000..ea1e1481ba --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-close-with-default-agent.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer(function(req, res) { + res.writeHead(200); + res.end(); +}); + +server.listen(0, common.mustCall(() => { + const req = http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + res.resume(); + server.close(); + }); + + req.end(); +})); + +// This timer should never go off as the server will close the socket +setTimeout(common.mustNotCall(), common.platformTimeout(1000)).unref(); diff --git a/test/js/node/test/parallel/test-http-client-defaults.js b/test/js/node/test/parallel/test-http-client-defaults.js new file mode 100644 index 0000000000..43419d1dfd --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-defaults.js @@ -0,0 +1,23 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const ClientRequest = require('http').ClientRequest; + +{ + const req = new ClientRequest({ createConnection: () => {} }); + assert.strictEqual(req.path, '/'); + assert.strictEqual(req.method, 'GET'); +} + +{ + const req = new ClientRequest({ method: '', createConnection: () => {} }); + assert.strictEqual(req.path, '/'); + assert.strictEqual(req.method, 'GET'); +} + +{ + const req = new ClientRequest({ path: '', createConnection: () => {} }); + assert.strictEqual(req.path, '/'); + assert.strictEqual(req.method, 'GET'); +} diff --git a/test/js/node/test/parallel/test-http-client-encoding.js b/test/js/node/test/parallel/test-http-client-encoding.js new file mode 100644 index 0000000000..a4701cdbd0 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-encoding.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer((req, res) => { + res.end('ok'); + server.close(); +}).listen(0, common.mustCall(() => { + http.request({ + port: server.address().port, + encoding: 'utf8' + }, common.mustCall((res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', common.mustCall(() => assert.strictEqual(data, 'ok'))); + })).end(); +})); diff --git a/test/js/node/test/parallel/test-http-client-get-url.js b/test/js/node/test/parallel/test-http-client-get-url.js new file mode 100644 index 0000000000..3b091a72ed --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-get-url.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); +const testPath = '/foo?bar'; + +const server = http.createServer(common.mustCall((req, res) => { + assert.strictEqual(req.method, 'GET'); + assert.strictEqual(req.url, testPath); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('hello\n'); + res.end(); +}, 3)); + +server.listen(0, common.localhostIPv4, common.mustCall(() => { + const u = `http://${common.localhostIPv4}:${server.address().port}${testPath}`; + http.get(u, common.mustCall(() => { + http.get(url.parse(u), common.mustCall(() => { + http.get(new URL(u), common.mustCall(() => { + server.close(); + })); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-http-client-input-function.js b/test/js/node/test/parallel/test-http-client-input-function.js new file mode 100644 index 0000000000..3a2f93aa0e --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-input-function.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +{ + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200); + res.end('hello world'); + })).listen(0, '127.0.0.1'); + + server.on('listening', common.mustCall(() => { + const req = new http.ClientRequest(server.address(), common.mustCall((response) => { + let body = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => { + body += chunk; + }); + + response.on('end', common.mustCall(() => { + assert.strictEqual(body, 'hello world'); + server.close(); + })); + })); + + req.end(); + })); +} diff --git a/test/js/node/test/parallel/test-http-client-keep-alive-hint.js b/test/js/node/test/parallel/test-http-client-keep-alive-hint.js new file mode 100644 index 0000000000..2618dfd552 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-keep-alive-hint.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer( + { keepAliveTimeout: common.platformTimeout(60000) }, + function(req, res) { + req.resume(); + res.writeHead(200, { 'Connection': 'keep-alive', 'Keep-Alive': 'timeout=1' }); + res.end('FOO'); + } +); + +server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + + res.resume(); + server.close(); + }); +})); + + +// This timer should never go off as the agent will parse the hint and terminate earlier +setTimeout(common.mustNotCall(), common.platformTimeout(3000)).unref(); diff --git a/test/js/node/test/parallel/test-http-client-keep-alive-release-before-finish.js b/test/js/node/test/parallel/test-http-client-keep-alive-release-before-finish.js new file mode 100644 index 0000000000..e6e0bac1bb --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-keep-alive-release-before-finish.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); + +const server = http.createServer((req, res) => { + res.end(); +}).listen(0, common.mustCall(() => { + const agent = new http.Agent({ + maxSockets: 1, + keepAlive: true + }); + + const port = server.address().port; + + const post = http.request({ + agent, + method: 'POST', + port, + }, common.mustCall((res) => { + res.resume(); + })); + + // What happens here is that the server `end`s the response before we send + // `something`, and the client thought that this is a green light for sending + // next GET request + post.write(Buffer.alloc(16 * 1024, 'X')); + setTimeout(() => { + post.end('something'); + }, 100); + + http.request({ + agent, + method: 'GET', + port, + }, common.mustCall((res) => { + server.close(); + res.connection.end(); + })).end(); +})); diff --git a/test/js/node/test/parallel/test-http-client-pipe-end.js b/test/js/node/test/parallel/test-http-client-pipe-end.js new file mode 100644 index 0000000000..32d8efb2f6 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-pipe-end.js @@ -0,0 +1,63 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// See https://github.com/joyent/node/issues/3257 + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const http = require('http'); + +const server = http.createServer(function(req, res) { + req.resume(); + req.once('end', function() { + res.writeHead(200); + res.end(); + server.close(); + }); +}); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +server.listen(common.PIPE, function() { + const req = http.request({ + socketPath: common.PIPE, + headers: { 'Content-Length': '1' }, + method: 'POST', + path: '/' + }); + + req.write('.'); + + sched(function() { req.end(); }, 5); +}); + +// Schedule a callback after `ticks` event loop ticks +function sched(cb, ticks) { + function fn() { + if (--ticks) + setImmediate(fn); + else + cb(); + } + setImmediate(fn); +} diff --git a/test/js/node/test/parallel/test-http-client-race-2.js b/test/js/node/test/parallel/test-http-client-race-2.js new file mode 100644 index 0000000000..951b8e0d74 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-race-2.js @@ -0,0 +1,112 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +// +// Slight variation on test-http-client-race to test for another race +// condition involving the parsers FreeList used internally by http.Client. +// + +const body1_s = '1111111111111111'; +const body2_s = '22222'; +const body3_s = '3333333333333333333'; + +const server = http.createServer(function(req, res) { + const pathname = url.parse(req.url).pathname; + + let body; + switch (pathname) { + case '/1': body = body1_s; break; + case '/2': body = body2_s; break; + default: body = body3_s; + } + + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Length': body.length + }); + res.end(body); +}); +server.listen(0); + +let body1 = ''; +let body2 = ''; +let body3 = ''; + +server.on('listening', function() { + // + // Client #1 is assigned Parser #1 + // + const req1 = http.get({ port: this.address().port, path: '/1' }); + req1.on('response', function(res1) { + res1.setEncoding('utf8'); + + res1.on('data', function(chunk) { + body1 += chunk; + }); + + res1.on('end', function() { + // + // Delay execution a little to allow the 'close' event to be processed + // (required to trigger this bug!) + // + setTimeout(function() { + // + // The bug would introduce itself here: Client #2 would be allocated the + // parser that previously belonged to Client #1. But we're not finished + // with Client #1 yet! + // + // At this point, the bug would manifest itself and crash because the + // internal state of the parser was no longer valid for use by Client #1 + // + const req2 = http.get({ port: server.address().port, path: '/2' }); + req2.on('response', function(res2) { + res2.setEncoding('utf8'); + res2.on('data', function(chunk) { body2 += chunk; }); + res2.on('end', function() { + + // + // Just to be really sure we've covered all our bases, execute a + // request using client2. + // + const req3 = http.get({ port: server.address().port, path: '/3' }); + req3.on('response', function(res3) { + res3.setEncoding('utf8'); + res3.on('data', function(chunk) { body3 += chunk; }); + res3.on('end', function() { server.close(); }); + }); + }); + }); + }, 500); + }); + }); +}); + +process.on('exit', function() { + assert.strictEqual(body1_s, body1); + assert.strictEqual(body2_s, body2); + assert.strictEqual(body3_s, body3); +}); diff --git a/test/js/node/test/parallel/test-http-client-race.js b/test/js/node/test/parallel/test-http-client-race.js new file mode 100644 index 0000000000..60b6b49737 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-race.js @@ -0,0 +1,69 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +const body1_s = '1111111111111111'; +const body2_s = '22222'; + +const server = http.createServer(function(req, res) { + const body = url.parse(req.url).pathname === '/1' ? body1_s : body2_s; + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Length': body.length + }); + res.end(body); +}); +server.listen(0); + +let body1 = ''; +let body2 = ''; + +server.on('listening', function() { + const req1 = http.request({ port: this.address().port, path: '/1' }); + req1.end(); + req1.on('response', function(res1) { + res1.setEncoding('utf8'); + + res1.on('data', function(chunk) { + body1 += chunk; + }); + + res1.on('end', function() { + const req2 = http.request({ port: server.address().port, path: '/2' }); + req2.end(); + req2.on('response', function(res2) { + res2.setEncoding('utf8'); + res2.on('data', function(chunk) { body2 += chunk; }); + res2.on('end', function() { server.close(); }); + }); + }); + }); +}); + +process.on('exit', function() { + assert.strictEqual(body1_s, body1); + assert.strictEqual(body2_s, body2); +}); diff --git a/test/js/node/test/parallel/test-http-client-read-in-error.js b/test/js/node/test/parallel/test-http-client-read-in-error.js new file mode 100644 index 0000000000..5e38e49c1f --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-read-in-error.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); +const http = require('http'); + +class Agent extends http.Agent { + createConnection() { + const socket = new net.Socket(); + + socket.on('error', function() { + socket.push('HTTP/1.1 200\r\n\r\n'); + }); + + let onNewListener; + socket.on('newListener', onNewListener = (name) => { + if (name !== 'error') + return; + socket.removeListener('newListener', onNewListener); + + // Let other listeners to be set up too + process.nextTick(() => { + this.breakSocket(socket); + }); + }); + + return socket; + } + + breakSocket(socket) { + socket.emit('error', new Error('Intentional error')); + } +} + +const agent = new Agent(); + +http.request({ + agent +}).once('error', function() { + console.log('ignore'); + this.on('data', common.mustNotCall()); +}); diff --git a/test/js/node/test/parallel/test-http-client-res-destroyed.js b/test/js/node/test/parallel/test-http-client-res-destroyed.js new file mode 100644 index 0000000000..188ab06c15 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-res-destroyed.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +{ + const server = http.createServer(common.mustCall((req, res) => { + res.end('asd'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ + port: server.address().port + }, common.mustCall((res) => { + assert.strictEqual(res.destroyed, false); + res.destroy(); + assert.strictEqual(res.destroyed, true); + res.on('close', common.mustCall(() => { + server.close(); + })); + })); + })); +} + +{ + const server = http.createServer(common.mustCall((req, res) => { + res.end('asd'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ + port: server.address().port + }, common.mustCall((res) => { + assert.strictEqual(res.destroyed, false); + res.on('close', common.mustCall(() => { + assert.strictEqual(res.destroyed, true); + server.close(); + })).resume(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-http-client-timeout-agent.js b/test/js/node/test/parallel/test-http-client-timeout-agent.js new file mode 100644 index 0000000000..66c0c0f6b9 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-timeout-agent.js @@ -0,0 +1,94 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +let requests_sent = 0; +let requests_done = 0; +const options = { + method: 'GET', + port: undefined, + host: '127.0.0.1', +}; + +const server = http.createServer((req, res) => { + const m = /\/(.*)/.exec(req.url); + const reqid = parseInt(m[1], 10); + if (reqid % 2) { + // Do not reply the request + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write(reqid.toString()); + res.end(); + } +}); + +server.listen(0, options.host, function() { + options.port = this.address().port; + + for (requests_sent = 0; requests_sent < 30; requests_sent += 1) { + options.path = `/${requests_sent}`; + const req = http.request(options); + req.id = requests_sent; + req.on('response', (res) => { + res.on('data', function(data) { + console.log(`res#${this.req.id} data:${data}`); + }); + res.on('end', function(data) { + console.log(`res#${this.req.id} end`); + requests_done += 1; + req.destroy(); + }); + }); + req.on('close', function() { + console.log(`req#${this.id} close`); + }); + req.on('error', function() { + console.log(`req#${this.id} error`); + this.destroy(); + }); + req.setTimeout(50, function() { + console.log(`req#${this.id} timeout`); + this.abort(); + requests_done += 1; + }); + req.end(); + } + + setTimeout(function maybeDone() { + if (requests_done >= requests_sent) { + setTimeout(() => { + server.close(); + }, 100); + } else { + setTimeout(maybeDone, 100); + } + }, 100); +}); + +process.on('exit', () => { + console.error(`done=${requests_done} sent=${requests_sent}`); + // Check that timeout on http request was not called too much + assert.strictEqual(requests_done, requests_sent); +}); diff --git a/test/js/node/test/parallel/test-http-client-timeout-connect-listener.js b/test/js/node/test/parallel/test-http-client-timeout-connect-listener.js new file mode 100644 index 0000000000..ea09aff718 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-timeout-connect-listener.js @@ -0,0 +1,42 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that `ClientRequest.prototype.setTimeout()` does +// not add a listener for the `'connect'` event to the socket if the +// socket is already connected. + +const assert = require('assert'); +const http = require('http'); + +// Maximum allowed value for timeouts. +const timeout = 2 ** 31 - 1; + +const server = http.createServer((req, res) => { + res.end(); +}); + +server.listen(0, common.mustCall(() => { + const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); + const options = { port: server.address().port, agent: agent }; + + doRequest(options, common.mustCall(() => { + const req = doRequest(options, common.mustCall(() => { + agent.destroy(); + server.close(); + })); + + req.on('socket', common.mustCall((socket) => { + assert.strictEqual(socket.listenerCount('connect'), 0); + })); + })); +})); + +function doRequest(options, callback) { + const req = http.get(options, (res) => { + res.on('end', callback); + res.resume(); + }); + + req.setTimeout(timeout); + return req; +} diff --git a/test/js/node/test/parallel/test-http-client-timeout.js b/test/js/node/test/parallel/test-http-client-timeout.js new file mode 100644 index 0000000000..b83bd1ddf0 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-timeout.js @@ -0,0 +1,54 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +const options = { + method: 'GET', + port: undefined, + host: '127.0.0.1', + path: '/' +}; + +const server = http.createServer(function(req, res) { + // This space intentionally left blank +}); + +server.listen(0, options.host, function() { + options.port = this.address().port; + const req = http.request(options, function(res) { + // This space intentionally left blank + }); + req.on('close', function() { + assert.strictEqual(req.destroyed, true); + server.close(); + }); + function destroy() { + req.destroy(); + } + const s = req.setTimeout(1, destroy); + assert.ok(s instanceof http.ClientRequest); + req.on('error', destroy); + req.end(); +}); diff --git a/test/js/node/test/parallel/test-http-client-upload-buf.js b/test/js/node/test/parallel/test-http-client-upload-buf.js new file mode 100644 index 0000000000..1c75612c80 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-upload-buf.js @@ -0,0 +1,64 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const N = 1024; + +const server = http.createServer(common.mustCall(function(req, res) { + assert.strictEqual(req.method, 'POST'); + let bytesReceived = 0; + + req.on('data', function(chunk) { + bytesReceived += chunk.length; + }); + + req.on('end', common.mustCall(function() { + assert.strictEqual(N, bytesReceived); + console.log('request complete from server'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('hello\n'); + res.end(); + })); +})); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const req = http.request({ + port: this.address().port, + method: 'POST', + path: '/' + }, common.mustCall(function(res) { + res.setEncoding('utf8'); + res.on('data', function(chunk) { + console.log(chunk); + }); + res.on('end', common.mustCall(function() { + server.close(); + })); + })); + + req.write(Buffer.allocUnsafe(N)); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-client-upload.js b/test/js/node/test/parallel/test-http-client-upload.js new file mode 100644 index 0000000000..830c37da8e --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-upload.js @@ -0,0 +1,67 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer(common.mustCall(function(req, res) { + assert.strictEqual(req.method, 'POST'); + req.setEncoding('utf8'); + + let sent_body = ''; + + req.on('data', function(chunk) { + console.log(`server got: ${JSON.stringify(chunk)}`); + sent_body += chunk; + }); + + req.on('end', common.mustCall(function() { + assert.strictEqual(sent_body, '1\n2\n3\n'); + console.log('request complete from server'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('hello\n'); + res.end(); + })); +})); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const req = http.request({ + port: this.address().port, + method: 'POST', + path: '/' + }, common.mustCall(function(res) { + res.setEncoding('utf8'); + res.on('data', function(chunk) { + console.log(chunk); + }); + res.on('end', common.mustCall(function() { + server.close(); + })); + })); + + req.write('1\n'); + req.write('2\n'); + req.write('3\n'); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-client-with-create-connection.js b/test/js/node/test/parallel/test-http-client-with-create-connection.js new file mode 100644 index 0000000000..2000243fc2 --- /dev/null +++ b/test/js/node/test/parallel/test-http-client-with-create-connection.js @@ -0,0 +1,56 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const http = require('http'); +const net = require('net'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +let count = 0; +let server1; +let server2; + +function request(options) { + count++; + http.get({ + ...options, + createConnection: (...args) => { + return net.connect(...args); + } + }, (res) => { + res.resume(); + res.on('end', () => { + if (--count === 0) { + server1.close(); + server2.close(); + } + }); + }); +} + +server1 = http.createServer((req, res) => { + res.end('ok'); +}).listen(common.PIPE, () => { + server2 = http.createServer((req, res) => { + res.end('ok'); + }).listen(() => { + request({ + path: '/', + socketPath: common.PIPE, + }); + + request({ + socketPath: common.PIPE, + }); + + request({ + path: '/', + port: server2.address().port, + }); + + request({ + port: server2.address().port, + }); + }); +}); diff --git a/test/js/node/test/parallel/test-http-contentLength0.js b/test/js/node/test/parallel/test-http-contentLength0.js new file mode 100644 index 0000000000..975e2abe88 --- /dev/null +++ b/test/js/node/test/parallel/test-http-contentLength0.js @@ -0,0 +1,44 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const http = require('http'); + +// Simple test of Node's HTTP Client choking on a response +// with a 'Content-Length: 0 ' response header. +// I.E. a space character after the 'Content-Length' throws an `error` event. + + +const s = http.createServer(function(req, res) { + res.writeHead(200, { 'Content-Length': '0 ' }); + res.end(); +}); +s.listen(0, function() { + + const request = http.request({ port: this.address().port }, (response) => { + console.log(`STATUS: ${response.statusCode}`); + s.close(); + response.resume(); + }); + + request.end(); +}); diff --git a/test/js/node/test/parallel/test-http-date-header.js b/test/js/node/test/parallel/test-http-date-header.js new file mode 100644 index 0000000000..169af2bf2a --- /dev/null +++ b/test/js/node/test/parallel/test-http-date-header.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +const testResBody = 'other stuff!\n'; + +const server = http.createServer((req, res) => { + assert.ok(!('date' in req.headers), + 'Request headers contained a Date.'); + res.writeHead(200, { + 'Content-Type': 'text/plain' + }); + res.end(testResBody); +}); +server.listen(0); + + +server.addListener('listening', () => { + const options = { + port: server.address().port, + path: '/', + method: 'GET' + }; + const req = http.request(options, (res) => { + assert.ok('date' in res.headers, + 'Response headers didn\'t contain a Date.'); + res.addListener('end', () => { + server.close(); + }); + res.resume(); + }); + req.end(); +}); diff --git a/test/js/node/test/parallel/test-http-decoded-auth.js b/test/js/node/test/parallel/test-http-decoded-auth.js new file mode 100644 index 0000000000..7b09f47cc7 --- /dev/null +++ b/test/js/node/test/parallel/test-http-decoded-auth.js @@ -0,0 +1,48 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +const testCases = [ + { + username: 'test@test"', + password: '123456^', + expected: 'dGVzdEB0ZXN0IjoxMjM0NTZe' + }, + { + username: 'test%40test', + password: '123456', + expected: 'dGVzdEB0ZXN0OjEyMzQ1Ng==' + }, + { + username: 'not%3Agood', + password: 'god', + expected: 'bm90Omdvb2Q6Z29k' + }, + { + username: 'not%22good', + password: 'g%5Eod', + expected: 'bm90Imdvb2Q6Z15vZA==' + }, + { + username: 'test1234::::', + password: 'mypass', + expected: 'dGVzdDEyMzQ6Ojo6Om15cGFzcw==' + }, +]; + +for (const testCase of testCases) { + const server = http.createServer(function(request, response) { + // The correct authorization header is be passed + assert.strictEqual(request.headers.authorization, `Basic ${testCase.expected}`); + response.writeHead(200, {}); + response.end('ok'); + server.close(); + }); + + server.listen(0, function() { + // make the request + const url = new URL(`http://${testCase.username}:${testCase.password}@localhost:${this.address().port}`); + http.request(url).end(); + }); +} diff --git a/test/js/node/test/parallel/test-http-default-encoding.js b/test/js/node/test/parallel/test-http-default-encoding.js new file mode 100644 index 0000000000..0c0de0808a --- /dev/null +++ b/test/js/node/test/parallel/test-http-default-encoding.js @@ -0,0 +1,58 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +const expected = 'This is a unicode text: سلام'; +let result = ''; + +const server = http.Server((req, res) => { + req.setEncoding('utf8'); + req.on('data', (chunk) => { + result += chunk; + }).on('end', () => { + res.writeHead(200); + res.end('hello world\n'); + server.close(); + }); + +}); + +server.listen(0, function() { + http.request({ + port: this.address().port, + path: '/', + method: 'POST' + }, (res) => { + console.log(res.statusCode); + res.resume(); + }).on('error', (e) => { + console.log(e.message); + process.exit(1); + }).end(expected); +}); + +process.on('exit', () => { + assert.strictEqual(result, expected); +}); diff --git a/test/js/node/test/parallel/test-http-end-throw-socket-handling.js b/test/js/node/test/parallel/test-http-end-throw-socket-handling.js new file mode 100644 index 0000000000..956096270e --- /dev/null +++ b/test/js/node/test/parallel/test-http-end-throw-socket-handling.js @@ -0,0 +1,53 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const Countdown = require('../common/countdown'); + +// Make sure that throwing in 'end' handler doesn't lock +// up the socket forever. +// +// This is NOT a good way to handle errors in general, but all +// the same, we should not be so brittle and easily broken. + +const http = require('http'); +const countdown = new Countdown(10, () => server.close()); + +const server = http.createServer((req, res) => { + countdown.dec(); + res.end('ok'); +}); + +server.listen(0, common.mustCall(() => { + for (let i = 0; i < 10; i++) { + const options = { port: server.address().port }; + const req = http.request(options, (res) => { + res.resume(); + res.on('end', common.mustCall(() => { + throw new Error('gleep glorp'); + })); + }); + req.end(); + } +})); + +process.on('uncaughtException', common.mustCall(10)); diff --git a/test/js/node/test/parallel/test-http-eof-on-connect.js b/test/js/node/test/parallel/test-http-eof-on-connect.js new file mode 100644 index 0000000000..5e885bb91a --- /dev/null +++ b/test/js/node/test/parallel/test-http-eof-on-connect.js @@ -0,0 +1,41 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +const net = require('net'); +const http = require('http'); + +// This is a regression test for https://github.com/joyent/node/issues/44 +// It is separate from test-http-malformed-request.js because it is only +// reproducible on the first packet on the first connection to a server. + +const server = http.createServer(common.mustNotCall()); +server.listen(0); + +server.on('listening', function() { + net.createConnection(this.address().port).on('connect', function() { + this.destroy(); + }).on('close', function() { + server.close(); + }); +}); diff --git a/test/js/node/test/parallel/test-http-extra-response.js b/test/js/node/test/parallel/test-http-extra-response.js new file mode 100644 index 0000000000..6d1a770487 --- /dev/null +++ b/test/js/node/test/parallel/test-http-extra-response.js @@ -0,0 +1,80 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const net = require('net'); + +// If an HTTP server is broken and sends data after the end of the response, +// node should ignore it and drop the connection. +// Demos this bug: https://github.com/joyent/node/issues/680 + +const body = 'hello world\r\n'; +const fullResponse = + 'HTTP/1.1 500 Internal Server Error\r\n' + + `Content-Length: ${body.length}\r\n` + + 'Content-Type: text/plain\r\n' + + 'Date: Fri + 18 Feb 2011 06:22:45 GMT\r\n' + + 'Host: 10.20.149.2\r\n' + + 'Access-Control-Allow-Credentials: true\r\n' + + 'Server: badly broken/0.1 (OS NAME)\r\n' + + '\r\n' + + body; + +const server = net.createServer(function(socket) { + let postBody = ''; + + socket.setEncoding('utf8'); + + socket.on('data', function(chunk) { + postBody += chunk; + + if (postBody.includes('\r\n')) { + socket.write(fullResponse); + socket.end(fullResponse); + } + }); + + socket.on('error', function(err) { + assert.strictEqual(err.code, 'ECONNRESET'); + }); +}); + + +server.listen(0, common.mustCall(function() { + http.get({ port: this.address().port }, common.mustCall(function(res) { + let buffer = ''; + console.log(`Got res code: ${res.statusCode}`); + + res.setEncoding('utf8'); + res.on('data', function(chunk) { + buffer += chunk; + }); + + res.on('end', common.mustCall(function() { + console.log(`Response ended, read ${buffer.length} bytes`); + assert.strictEqual(body, buffer); + server.close(); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-http-get-pipeline-problem.js b/test/js/node/test/parallel/test-http-get-pipeline-problem.js new file mode 100644 index 0000000000..750b11bffe --- /dev/null +++ b/test/js/node/test/parallel/test-http-get-pipeline-problem.js @@ -0,0 +1,101 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// In previous versions of Node.js (e.g., 0.6.0), this sort of thing would halt +// after http.globalAgent.maxSockets number of files. +// See https://groups.google.com/forum/#!topic/nodejs-dev/V5fB69hFa9o +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const http = require('http'); +const fs = require('fs'); +const Countdown = require('../common/countdown'); + +http.globalAgent.maxSockets = 1; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const image = fixtures.readSync('/person.jpg'); + +console.log(`image.length = ${image.length}`); + +const total = 10; +const responseCountdown = new Countdown(total, common.mustCall(() => { + checkFiles(); + server.close(); +})); + +const server = http.Server(function(req, res) { + setTimeout(function() { + res.writeHead(200, { + 'content-type': 'image/jpeg', + 'connection': 'close', + 'content-length': image.length + }); + res.end(image); + }, 1); +}); + + +server.listen(0, function() { + for (let i = 0; i < total; i++) { + (function() { + const x = i; + + const opts = { + port: server.address().port, + headers: { connection: 'close' } + }; + + http.get(opts, function(res) { + console.error(`recv ${x}`); + const s = fs.createWriteStream(`${tmpdir.path}/${x}.jpg`); + res.pipe(s); + + s.on('finish', function() { + console.error(`done ${x}`); + responseCountdown.dec(); + }); + }).on('error', function(e) { + console.error('error! ', e.message); + throw e; + }); + })(); + } +}); + +function checkFiles() { + // Should see 1.jpg, 2.jpg, ..., 100.jpg in tmpDir + const files = fs.readdirSync(tmpdir.path); + assert(total <= files.length); + + for (let i = 0; i < total; i++) { + const fn = `${i}.jpg`; + assert.ok(files.includes(fn), `couldn't find '${fn}'`); + const stat = fs.statSync(`${tmpdir.path}/${fn}`); + assert.strictEqual( + image.length, stat.size, + `size doesn't match on '${fn}'. Got ${stat.size} bytes`); + } +} diff --git a/test/js/node/test/parallel/test-http-head-request.js b/test/js/node/test/parallel/test-http-head-request.js new file mode 100644 index 0000000000..26d490d357 --- /dev/null +++ b/test/js/node/test/parallel/test-http-head-request.js @@ -0,0 +1,57 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); + +const body = 'hello world\n'; + +function test(headers) { + const server = http.createServer(function(req, res) { + console.error('req: %s headers: %j', req.method, headers); + res.writeHead(200, headers); + res.end(); + server.close(); + }); + + server.listen(0, common.mustCall(function() { + const request = http.request({ + port: this.address().port, + method: 'HEAD', + path: '/' + }, common.mustCall(function(response) { + console.error('response start'); + response.on('end', common.mustCall(function() { + console.error('response end'); + })); + response.resume(); + })); + request.end(); + })); +} + +test({ + 'Transfer-Encoding': 'chunked' +}); +test({ + 'Content-Length': body.length +}); diff --git a/test/js/node/test/parallel/test-http-head-response-has-no-body-end-implicit-headers.js b/test/js/node/test/parallel/test-http-head-response-has-no-body-end-implicit-headers.js new file mode 100644 index 0000000000..5ebd9f8a90 --- /dev/null +++ b/test/js/node/test/parallel/test-http-head-response-has-no-body-end-implicit-headers.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); + +// This test is to make sure that when the HTTP server +// responds to a HEAD request with data to res.end, +// it does not send any body but the response is sent +// anyway. + +const server = http.createServer(function(req, res) { + res.end('FAIL'); // broken: sends FAIL from hot path. +}); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const req = http.request({ + port: this.address().port, + method: 'HEAD', + path: '/' + }, common.mustCall(function(res) { + res.on('end', common.mustCall(function() { + server.close(); + })); + res.resume(); + })); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-head-response-has-no-body-end.js b/test/js/node/test/parallel/test-http-head-response-has-no-body-end.js new file mode 100644 index 0000000000..824a1bafe3 --- /dev/null +++ b/test/js/node/test/parallel/test-http-head-response-has-no-body-end.js @@ -0,0 +1,48 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); + +// This test is to make sure that when the HTTP server +// responds to a HEAD request with data to res.end, +// it does not send any body. + +const server = http.createServer(function(req, res) { + res.writeHead(200); + res.end('FAIL'); // broken: sends FAIL from hot path. +}); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const req = http.request({ + port: this.address().port, + method: 'HEAD', + path: '/' + }, common.mustCall(function(res) { + res.on('end', common.mustCall(function() { + server.close(); + })); + res.resume(); + })); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-head-response-has-no-body.js b/test/js/node/test/parallel/test-http-head-response-has-no-body.js new file mode 100644 index 0000000000..bd96d7161b --- /dev/null +++ b/test/js/node/test/parallel/test-http-head-response-has-no-body.js @@ -0,0 +1,48 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); + +// This test is to make sure that when the HTTP server +// responds to a HEAD request, it does not send any body. +// In this case it was sending '0\r\n\r\n' + +const server = http.createServer(function(req, res) { + res.writeHead(200); // broken: defaults to TE chunked + res.end(); +}); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const req = http.request({ + port: this.address().port, + method: 'HEAD', + path: '/' + }, common.mustCall(function(res) { + res.on('end', common.mustCall(function() { + server.close(); + })); + res.resume(); + })); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-header-owstext.js b/test/js/node/test/parallel/test-http-header-owstext.js new file mode 100644 index 0000000000..bc094137a2 --- /dev/null +++ b/test/js/node/test/parallel/test-http-header-owstext.js @@ -0,0 +1,49 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that the http-parser strips leading and trailing OWS from +// header values. It sends the header values in chunks to force the parser to +// build the string up through multiple calls to on_header_value(). + +const assert = require('assert'); +const http = require('http'); +const net = require('net'); + +function check(hdr, snd, rcv) { + const server = http.createServer(common.mustCall((req, res) => { + assert.strictEqual(req.headers[hdr], rcv); + req.pipe(res); + })); + + server.listen(0, common.mustCall(function() { + const client = net.connect(this.address().port, start); + function start() { + client.write('GET / HTTP/1.1\r\n' + hdr + ':', drain); + } + + function drain() { + if (snd.length === 0) { + return client.write('\r\nConnection: close\r\n\r\n'); + } + client.write(snd.shift(), drain); + } + + const bufs = []; + client.on('data', function(chunk) { + bufs.push(chunk); + }); + client.on('end', common.mustCall(function() { + const head = Buffer.concat(bufs) + .toString('latin1') + .split('\r\n')[0]; + assert.strictEqual(head, 'HTTP/1.1 200 OK'); + server.close(); + })); + })); +} + +check('host', [' \t foo.com\t'], 'foo.com'); +check('host', [' \t foo\tcom\t'], 'foo\tcom'); +check('host', [' \t', ' ', ' foo.com\t', '\t '], 'foo.com'); +check('host', [' \t', ' \t'.repeat(100), '\t '], ''); +check('host', [' \t', ' - - - - ', '\t '], '- - - -'); diff --git a/test/js/node/test/parallel/test-http-highwatermark.js b/test/js/node/test/parallel/test-http-highwatermark.js new file mode 100644 index 0000000000..79d9c46a55 --- /dev/null +++ b/test/js/node/test/parallel/test-http-highwatermark.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); +const http = require('http'); + +// These test cases to check socketOnDrain where needPause becomes false. +// When send large response enough to exceed highWaterMark, it expect the socket +// to be paused and res.write would be failed. +// And it should be resumed when outgoingData falls below highWaterMark. + +let requestReceived = 0; + +const server = http.createServer(function(req, res) { + const id = ++requestReceived; + const enoughToDrain = req.connection.writableHighWaterMark; + const body = 'x'.repeat(enoughToDrain * 100); + + if (id === 1) { + // Case of needParse = false + req.connection.once('pause', common.mustCall(() => { + assert(req.connection._paused, '_paused must be true because it exceeds' + + 'highWaterMark by second request'); + })); + } else { + // Case of needParse = true + const resume = req.connection.parser.resume.bind(req.connection.parser); + req.connection.parser.resume = common.mustCall((...args) => { + const paused = req.connection._paused; + assert(!paused, '_paused must be false because it become false by ' + + 'socketOnDrain when outgoingData falls below ' + + 'highWaterMark'); + return resume(...args); + }); + } + assert(!res.write(body), 'res.write must return false because it will ' + + 'exceed highWaterMark on this call'); + res.end(); +}).on('listening', () => { + const c = net.createConnection(server.address().port, () => { + c.write('GET / HTTP/1.1\r\n\r\n'); + c.write('GET / HTTP/1.1\r\n\r\n', + () => setImmediate(() => c.resume())); + c.end(); + }); + + c.on('end', () => { + server.close(); + }); +}); + +server.listen(0); diff --git a/test/js/node/test/parallel/test-http-host-headers.js b/test/js/node/test/parallel/test-http-host-headers.js new file mode 100644 index 0000000000..97d200ade1 --- /dev/null +++ b/test/js/node/test/parallel/test-http-host-headers.js @@ -0,0 +1,96 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); +const httpServer = http.createServer(reqHandler); + +function reqHandler(req, res) { + if (req.url === '/setHostFalse5') { + assert.strictEqual(req.headers.host, undefined); + } else { + assert.strictEqual( + req.headers.host, `localhost:${this.address().port}`, + `Wrong host header for req[${req.url}]: ${req.headers.host}`); + } + res.writeHead(200, {}); + res.end('ok'); +} + +testHttp(); + +function testHttp() { + + let counter = 0; + + function cb(res) { + counter--; + if (counter === 0) { + httpServer.close(); + } + res.resume(); + } + + httpServer.listen(0, (er) => { + assert.ifError(er); + http.get({ + method: 'GET', + path: `/${counter++}`, + host: 'localhost', + port: httpServer.address().port, + rejectUnauthorized: false + }, cb).on('error', common.mustNotCall()); + + http.request({ + method: 'GET', + path: `/${counter++}`, + host: 'localhost', + port: httpServer.address().port, + rejectUnauthorized: false + }, cb).on('error', common.mustNotCall()).end(); + + http.request({ + method: 'POST', + path: `/${counter++}`, + host: 'localhost', + port: httpServer.address().port, + rejectUnauthorized: false + }, cb).on('error', common.mustNotCall()).end(); + + http.request({ + method: 'PUT', + path: `/${counter++}`, + host: 'localhost', + port: httpServer.address().port, + rejectUnauthorized: false + }, cb).on('error', common.mustNotCall()).end(); + + http.request({ + method: 'DELETE', + path: `/${counter++}`, + host: 'localhost', + port: httpServer.address().port, + rejectUnauthorized: false + }, cb).on('error', common.mustNotCall()).end(); + }); +} diff --git a/test/js/node/test/parallel/test-http-keep-alive-timeout-custom.js b/test/js/node/test/parallel/test-http-keep-alive-timeout-custom.js new file mode 100644 index 0000000000..a74aa5a212 --- /dev/null +++ b/test/js/node/test/parallel/test-http-keep-alive-timeout-custom.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +const server = http.createServer(common.mustCall((req, res) => { + const body = 'hello world\n'; + + res.writeHead(200, { + 'Content-Length': body.length, + 'Keep-Alive': 'timeout=50' + }); + res.write(body); + res.end(); +})); +server.keepAliveTimeout = 12010; + +const agent = new http.Agent({ maxSockets: 1, keepAlive: true }); + +server.listen(0, common.mustCall(function() { + http.get({ + path: '/', port: this.address().port, agent: agent + }, common.mustCall((response) => { + response.resume(); + assert.strictEqual( + response.headers['keep-alive'], 'timeout=50'); + server.close(); + agent.destroy(); + })); +})); diff --git a/test/js/node/test/parallel/test-http-localaddress-bind-error.js b/test/js/node/test/parallel/test-http-localaddress-bind-error.js new file mode 100644 index 0000000000..d4bd72bae1 --- /dev/null +++ b/test/js/node/test/parallel/test-http-localaddress-bind-error.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const invalidLocalAddress = '1.2.3.4'; + +const server = http.createServer(function(req, res) { + console.log(`Connect from: ${req.connection.remoteAddress}`); + + req.on('end', function() { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(`You are from: ${req.connection.remoteAddress}`); + }); + req.resume(); +}); + +server.listen(0, '127.0.0.1', common.mustCall(function() { + http.request({ + host: 'localhost', + port: this.address().port, + path: '/', + method: 'GET', + localAddress: invalidLocalAddress + }, function(res) { + assert.fail('unexpectedly got response from server'); + }).on('error', common.mustCall(function(e) { + console.log(`client got error: ${e.message}`); + server.close(); + })).end(); +})); diff --git a/test/js/node/test/parallel/test-http-malformed-request.js b/test/js/node/test/parallel/test-http-malformed-request.js new file mode 100644 index 0000000000..1f2fecc117 --- /dev/null +++ b/test/js/node/test/parallel/test-http-malformed-request.js @@ -0,0 +1,47 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); +const http = require('http'); +const url = require('url'); + +// Make sure no exceptions are thrown when receiving malformed HTTP +// requests. +const server = http.createServer(common.mustCall((req, res) => { + console.log(`req: ${JSON.stringify(url.parse(req.url))}`); + + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('Hello World'); + res.end(); + + server.close(); +})); +server.listen(0); + +server.on('listening', function() { + const c = net.createConnection(this.address().port); + c.on('connect', function() { + c.write('GET /hello?foo=%99bar HTTP/1.1\r\nHost: example.com\r\n\r\n'); + c.end(); + }); +}); diff --git a/test/js/node/test/parallel/test-http-missing-header-separator-cr.js b/test/js/node/test/parallel/test-http-missing-header-separator-cr.js new file mode 100644 index 0000000000..1129ec4ed9 --- /dev/null +++ b/test/js/node/test/parallel/test-http-missing-header-separator-cr.js @@ -0,0 +1,83 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const http = require('http'); +const net = require('net'); + +function serverHandler(server, msg) { + const client = net.connect(server.address().port, 'localhost'); + + let response = ''; + + client.on('data', common.mustCall((chunk) => { + response += chunk; + })); + + client.setEncoding('utf8'); + client.on('error', common.mustNotCall()); + client.on('end', common.mustCall(() => { + assert.strictEqual( + response, + 'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n' + ); + server.close(); + })); + client.write(msg); + client.resume(); +} + +{ + const msg = [ + 'GET / HTTP/1.1', + 'Host: localhost', + 'Dummy: x\nContent-Length: 23', + '', + 'GET / HTTP/1.1', + 'Dummy: GET /admin HTTP/1.1', + 'Host: localhost', + '', + '', + ].join('\r\n'); + + const server = http.createServer(common.mustNotCall()); + + server.listen(0, common.mustSucceed(serverHandler.bind(null, server, msg))); +} + +{ + const msg = [ + 'POST / HTTP/1.1', + 'Host: localhost', + 'x:x\nTransfer-Encoding: chunked', + '', + '1', + 'A', + '0', + '', + '', + ].join('\r\n'); + + const server = http.createServer(common.mustNotCall()); + + server.listen(0, common.mustSucceed(serverHandler.bind(null, server, msg))); +} + +{ + const msg = [ + 'POST / HTTP/1.1', + 'Host: localhost', + 'x:\nTransfer-Encoding: chunked', + '', + '1', + 'A', + '0', + '', + '', + ].join('\r\n'); + + const server = http.createServer(common.mustNotCall()); + + server.listen(0, common.mustSucceed(serverHandler.bind(null, server, msg))); +} diff --git a/test/js/node/test/parallel/test-http-outgoing-destroy.js b/test/js/node/test/parallel/test-http-outgoing-destroy.js new file mode 100644 index 0000000000..47d5e948ab --- /dev/null +++ b/test/js/node/test/parallel/test-http-outgoing-destroy.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const http = require('http'); +const OutgoingMessage = http.OutgoingMessage; + +{ + const msg = new OutgoingMessage(); + assert.strictEqual(msg.destroyed, false); + msg.destroy(); + assert.strictEqual(msg.destroyed, true); + msg.write('asd', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED'); + })); + msg.on('error', common.mustNotCall()); +} diff --git a/test/js/node/test/parallel/test-http-outgoing-end-types.js b/test/js/node/test/parallel/test-http-outgoing-end-types.js new file mode 100644 index 0000000000..20b443bff2 --- /dev/null +++ b/test/js/node/test/parallel/test-http-outgoing-end-types.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const httpServer = http.createServer(common.mustCall(function(req, res) { + httpServer.close(); + assert.throws(() => { + res.end(['Throws.']); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + res.end(); +})); + +httpServer.listen(0, common.mustCall(function() { + http.get({ port: this.address().port }); +})); diff --git a/test/js/node/test/parallel/test-http-outgoing-finish.js b/test/js/node/test/parallel/test-http-outgoing-finish.js new file mode 100644 index 0000000000..0f71cccdf8 --- /dev/null +++ b/test/js/node/test/parallel/test-http-outgoing-finish.js @@ -0,0 +1,76 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const http = require('http'); + +http.createServer(function(req, res) { + req.resume(); + req.on('end', function() { + write(res); + }); + this.close(); +}).listen(0, function() { + const req = http.request({ + port: this.address().port, + method: 'PUT' + }); + write(req); + req.on('response', function(res) { + res.resume(); + }); +}); + +const buf = Buffer.alloc(1024 * 16, 'x'); +function write(out) { + const name = out.constructor.name; + let finishEvent = false; + let endCb = false; + + // First, write until it gets some backpressure + while (out.write(buf, common.mustSucceed())); + + // Now end, and make sure that we don't get the 'finish' event + // before the tick where the cb gets called. We give it until + // nextTick because this is added as a listener before the endcb + // is registered. The order is not what we're testing here, just + // that 'finish' isn't emitted until the stream is fully flushed. + out.on('finish', function() { + finishEvent = true; + console.error(`${name} finish event`); + process.nextTick(function() { + assert(endCb, `${name} got finish event before endcb!`); + console.log(`ok - ${name} finishEvent`); + }); + }); + + out.end(buf, common.mustCall(function() { + endCb = true; + console.error(`${name} endCb`); + process.nextTick(function() { + assert(finishEvent, `${name} got endCb event before finishEvent!`); + console.log(`ok - ${name} endCb`); + }); + })); +} diff --git a/test/js/node/test/parallel/test-http-outgoing-finished.js b/test/js/node/test/parallel/test-http-outgoing-finished.js new file mode 100644 index 0000000000..7da1b7429a --- /dev/null +++ b/test/js/node/test/parallel/test-http-outgoing-finished.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); +const { finished } = require('stream'); + +const http = require('http'); +const assert = require('assert'); + +const server = http.createServer(function(req, res) { + let closed = false; + res + .on('close', common.mustCall(() => { + closed = true; + finished(res, common.mustCall(() => { + server.close(); + })); + })) + .end(); + finished(res, common.mustCall(() => { + assert.strictEqual(closed, true); + })); + +}).listen(0, function() { + http + .request({ + port: this.address().port, + method: 'GET' + }) + .on('response', function(res) { + res.resume(); + }) + .end(); +}); diff --git a/test/js/node/test/parallel/test-http-outgoing-writableFinished.js b/test/js/node/test/parallel/test-http-outgoing-writableFinished.js new file mode 100644 index 0000000000..6f84d91e71 --- /dev/null +++ b/test/js/node/test/parallel/test-http-outgoing-writableFinished.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer(common.mustCall(function(req, res) { + assert.strictEqual(res.writableFinished, false); + res + .on('finish', common.mustCall(() => { + assert.strictEqual(res.writableFinished, true); + server.close(); + })) + .end(); +})); + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const clientRequest = http.request({ + port: server.address().port, + method: 'GET', + path: '/' + }); + + assert.strictEqual(clientRequest.writableFinished, false); + clientRequest + .on('finish', common.mustCall(() => { + assert.strictEqual(clientRequest.writableFinished, true); + })) + .end(); + assert.strictEqual(clientRequest.writableFinished, false); +})); diff --git a/test/js/node/test/parallel/test-http-outgoing-write-types.js b/test/js/node/test/parallel/test-http-outgoing-write-types.js new file mode 100644 index 0000000000..6257b87eea --- /dev/null +++ b/test/js/node/test/parallel/test-http-outgoing-write-types.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const httpServer = http.createServer(common.mustCall(function(req, res) { + httpServer.close(); + assert.throws(() => { + res.write(['Throws.']); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + // should not throw + res.write('1a2b3c'); + // should not throw + res.write(new Uint8Array(1024)); + // should not throw + res.write(Buffer.from('1'.repeat(1024))); + res.end(); +})); + +httpServer.listen(0, common.mustCall(function() { + http.get({ port: this.address().port }); +})); diff --git a/test/js/node/test/parallel/test-http-pause-no-dump.js b/test/js/node/test/parallel/test-http-pause-no-dump.js new file mode 100644 index 0000000000..b794634c48 --- /dev/null +++ b/test/js/node/test/parallel/test-http-pause-no-dump.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer(common.mustCall(function(req, res) { + req.once('data', common.mustCall(() => { + req.pause(); + res.writeHead(200); + res.end(); + res.on('finish', common.mustCall(() => { + assert(!req._dumped); + })); + })); +})); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const req = http.request({ + port: this.address().port, + method: 'POST', + path: '/' + }, common.mustCall(function(res) { + assert.strictEqual(res.statusCode, 200); + res.resume(); + res.on('end', common.mustCall(() => { + server.close(); + })); + })); + + req.end(Buffer.allocUnsafe(1024)); +})); diff --git a/test/js/node/test/parallel/test-http-pause-resume-one-end.js b/test/js/node/test/parallel/test-http-pause-resume-one-end.js new file mode 100644 index 0000000000..34bf3be478 --- /dev/null +++ b/test/js/node/test/parallel/test-http-pause-resume-one-end.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +const server = http.Server(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello World\n'); + server.close(); +}); + +server.listen(0, common.mustCall(function() { + const opts = { + port: this.address().port, + headers: { connection: 'close' } + }; + + http.get(opts, common.mustCall(function(res) { + res.on('data', common.mustCall(function() { + res.pause(); + setImmediate(function() { + res.resume(); + }); + })); + + res.on('end', common.mustCall(() => { + assert.strictEqual(res.destroyed, false); + })); + assert.strictEqual(res.destroyed, false); + res.on('close', common.mustCall(() => { + assert.strictEqual(res.destroyed, true); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-http-pause.js b/test/js/node/test/parallel/test-http-pause.js new file mode 100644 index 0000000000..555eece2e3 --- /dev/null +++ b/test/js/node/test/parallel/test-http-pause.js @@ -0,0 +1,78 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +const expectedServer = 'Request Body from Client'; +let resultServer = ''; +const expectedClient = 'Response Body from Server'; +let resultClient = ''; + +const server = http.createServer((req, res) => { + console.error('pause server request'); + req.pause(); + setTimeout(() => { + console.error('resume server request'); + req.resume(); + req.setEncoding('utf8'); + req.on('data', (chunk) => { + resultServer += chunk; + }); + req.on('end', () => { + console.error(resultServer); + res.writeHead(200); + res.end(expectedClient); + }); + }, 100); +}); + +server.listen(0, function() { + // Anonymous function rather than arrow function to test `this` value. + assert.strictEqual(this, server); + const req = http.request({ + port: this.address().port, + path: '/', + method: 'POST' + }, (res) => { + console.error('pause client response'); + res.pause(); + setTimeout(() => { + console.error('resume client response'); + res.resume(); + res.on('data', (chunk) => { + resultClient += chunk; + }); + res.on('end', () => { + console.error(resultClient); + server.close(); + }); + }, 100); + }); + req.end(expectedServer); +}); + +process.on('exit', () => { + assert.strictEqual(resultServer, expectedServer); + assert.strictEqual(resultClient, expectedClient); +}); diff --git a/test/js/node/test/parallel/test-http-pipe-fs.js b/test/js/node/test/parallel/test-http-pipe-fs.js new file mode 100644 index 0000000000..b7c1a02918 --- /dev/null +++ b/test/js/node/test/parallel/test-http-pipe-fs.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const http = require('http'); +const fs = require('fs'); +const Countdown = require('../common/countdown'); +const NUMBER_OF_STREAMS = 2; + +const countdown = new Countdown(NUMBER_OF_STREAMS, () => server.close()); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const file = tmpdir.resolve('http-pipe-fs-test.txt'); + +const server = http.createServer(common.mustCall(function(req, res) { + const stream = fs.createWriteStream(file); + req.pipe(stream); + stream.on('close', function() { + res.writeHead(200); + res.end(); + }); +}, 2)).listen(0, function() { + http.globalAgent.maxSockets = 1; + + for (let i = 0; i < NUMBER_OF_STREAMS; ++i) { + const req = http.request({ + port: server.address().port, + method: 'POST', + headers: { + 'Content-Length': 5 + } + }, function(res) { + res.on('end', function() { + console.error(`res${i + 1} end`); + countdown.dec(); + }); + res.resume(); + }); + req.on('socket', function(s) { + console.error(`req${i + 1} start`); + }); + req.end('12345'); + } +}); diff --git a/test/js/node/test/parallel/test-http-pipeline-socket-parser-typeerror.js b/test/js/node/test/parallel/test-http-pipeline-socket-parser-typeerror.js new file mode 100644 index 0000000000..e092154b97 --- /dev/null +++ b/test/js/node/test/parallel/test-http-pipeline-socket-parser-typeerror.js @@ -0,0 +1,64 @@ +'use strict'; +require('../common'); + +// This test ensures that Node.js doesn't crash because of a TypeError by +// checking in `connectionListener` that the socket still has the parser. +// https://github.com/nodejs/node/issues/3508 + +const http = require('http'); +const net = require('net'); + +let once = false; +let first = null; +let second = null; + +const chunk = Buffer.alloc(1024, 'X'); + +let size = 0; + +let more; +let done; + +const server = http + .createServer((req, res) => { + if (!once) server.close(); + once = true; + + if (first === null) { + first = res; + return; + } + if (second === null) { + second = res; + res.write(chunk); + } else { + res.end(chunk); + } + size += res.outputSize; + if (size <= req.socket.writableHighWaterMark) { + more(); + return; + } + done(); + }) + .on('upgrade', (req, socket) => { + second.end(chunk, () => { + socket.end(); + }); + first.end('hello'); + }) + .listen(0, () => { + const s = net.connect(server.address().port); + more = () => { + s.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); + }; + done = () => { + s.write( + 'GET / HTTP/1.1\r\n\r\n' + + 'GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: ws\r\n\r\naaa' + ); + }; + more(); + more(); + s.resume(); + }); diff --git a/test/js/node/test/parallel/test-http-proxy.js b/test/js/node/test/parallel/test-http-proxy.js new file mode 100644 index 0000000000..af3497630b --- /dev/null +++ b/test/js/node/test/parallel/test-http-proxy.js @@ -0,0 +1,107 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +const cookies = [ + 'session_token=; path=/; expires=Sun, 15-Sep-2030 13:48:52 GMT', + 'prefers_open_id=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT', +]; + +const headers = { 'content-type': 'text/plain', + 'set-cookie': cookies, + 'hello': 'world' }; + +const backend = http.createServer(function(req, res) { + console.error('backend request'); + res.writeHead(200, headers); + res.write('hello world\n'); + res.end(); +}); + +const proxy = http.createServer(function(req, res) { + console.error(`proxy req headers: ${JSON.stringify(req.headers)}`); + http.get({ + port: backend.address().port, + path: url.parse(req.url).pathname + }, function(proxy_res) { + + console.error(`proxy res headers: ${JSON.stringify(proxy_res.headers)}`); + + assert.strictEqual(proxy_res.headers.hello, 'world'); + assert.strictEqual(proxy_res.headers['content-type'], 'text/plain'); + assert.deepStrictEqual(proxy_res.headers['set-cookie'], cookies); + + res.writeHead(proxy_res.statusCode, proxy_res.headers); + + proxy_res.on('data', function(chunk) { + res.write(chunk); + }); + + proxy_res.on('end', function() { + res.end(); + console.error('proxy res'); + }); + }); +}); + +let body = ''; + +let nlistening = 0; +function startReq() { + nlistening++; + if (nlistening < 2) return; + + http.get({ + port: proxy.address().port, + path: '/test' + }, function(res) { + console.error('got res'); + assert.strictEqual(res.statusCode, 200); + + assert.strictEqual(res.headers.hello, 'world'); + assert.strictEqual(res.headers['content-type'], 'text/plain'); + assert.deepStrictEqual(res.headers['set-cookie'], cookies); + + res.setEncoding('utf8'); + res.on('data', function(chunk) { body += chunk; }); + res.on('end', function() { + proxy.close(); + backend.close(); + console.error('closed both'); + }); + }); + console.error('client req'); +} + +console.error('listen proxy'); +proxy.listen(0, startReq); + +console.error('listen backend'); +backend.listen(0, startReq); + +process.on('exit', function() { + assert.strictEqual(body, 'hello world\n'); +}); diff --git a/test/js/node/test/parallel/test-http-request-agent.js b/test/js/node/test/parallel/test-http-request-agent.js new file mode 100644 index 0000000000..453ac380d0 --- /dev/null +++ b/test/js/node/test/parallel/test-http-request-agent.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const fixtures = require('../common/fixtures'); + +// This test ensures that a http request callback is called when the agent +// option is set. +// See https://github.com/nodejs/node-v0.x-archive/issues/1531 + +const https = require('https'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = https.createServer(options, function(req, res) { + res.writeHead(200); + res.end('hello world\n'); +}); + +server.listen(0, common.mustCall(function() { + console.error('listening'); + https.get({ + agent: false, + path: '/', + port: this.address().port, + rejectUnauthorized: false + }, common.mustCall(function(res) { + console.error(res.statusCode, res.headers); + res.resume(); + server.close(); + })).on('error', function(e) { + console.error(e); + process.exit(1); + }); +})); diff --git a/test/js/node/test/parallel/test-http-request-arguments.js b/test/js/node/test/parallel/test-http-request-arguments.js new file mode 100644 index 0000000000..5cdd514fd5 --- /dev/null +++ b/test/js/node/test/parallel/test-http-request-arguments.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +// Test providing both a url and options, with the options partially +// replacing address and port portions of the URL provided. +{ + const server = http.createServer( + common.mustCall((req, res) => { + assert.strictEqual(req.url, '/testpath'); + res.end(); + server.close(); + }) + ); + server.listen( + 0, + common.mustCall(() => { + http.get( + 'http://example.com/testpath', + { hostname: 'localhost', port: server.address().port }, + common.mustCall((res) => { + res.resume(); + }) + ); + }) + ); +} diff --git a/test/js/node/test/parallel/test-http-request-end-twice.js b/test/js/node/test/parallel/test-http-request-end-twice.js new file mode 100644 index 0000000000..47f08fd6e4 --- /dev/null +++ b/test/js/node/test/parallel/test-http-request-end-twice.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.Server(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('hello world\n'); +}); +server.listen(0, function() { + const req = http.get({ port: this.address().port }, function(res) { + res.on('end', function() { + assert.strictEqual(req.end(), req); + server.close(); + }); + res.resume(); + }); +}); diff --git a/test/js/node/test/parallel/test-http-request-end.js b/test/js/node/test/parallel/test-http-request-end.js new file mode 100644 index 0000000000..6f141fda1e --- /dev/null +++ b/test/js/node/test/parallel/test-http-request-end.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const expected = 'Post Body For Test'; +const expectedStatusCode = 200; + +const server = http.Server(function(req, res) { + let result = ''; + + req.setEncoding('utf8'); + req.on('data', function(chunk) { + result += chunk; + }); + + req.on('end', common.mustCall(() => { + assert.strictEqual(result, expected); + res.writeHead(expectedStatusCode); + res.end('hello world\n'); + server.close(); + })); + +}); + +server.listen(0, function() { + const req = http.request({ + port: this.address().port, + path: '/', + method: 'POST' + }, function(res) { + assert.strictEqual(res.statusCode, expectedStatusCode); + res.resume(); + }).on('error', common.mustNotCall()); + + const result = req.end(expected); + + assert.strictEqual(req, result); +}); diff --git a/test/js/node/test/parallel/test-http-request-large-payload.js b/test/js/node/test/parallel/test-http-request-large-payload.js new file mode 100644 index 0000000000..3be100b740 --- /dev/null +++ b/test/js/node/test/parallel/test-http-request-large-payload.js @@ -0,0 +1,26 @@ +'use strict'; +require('../common'); + +// This test ensures Node.js doesn't throw an error when making requests with +// the payload 16kb or more in size. +// https://github.com/nodejs/node/issues/2821 + +const http = require('http'); + +const server = http.createServer(function(req, res) { + res.writeHead(200); + res.end(); + + server.close(); +}); + +server.listen(0, function() { + const req = http.request({ + method: 'POST', + port: this.address().port + }); + + const payload = Buffer.alloc(16390, 'Й'); + req.write(payload); + req.end(); +}); diff --git a/test/js/node/test/parallel/test-http-request-method-delete-payload.js b/test/js/node/test/parallel/test-http-request-method-delete-payload.js new file mode 100644 index 0000000000..03728846df --- /dev/null +++ b/test/js/node/test/parallel/test-http-request-method-delete-payload.js @@ -0,0 +1,30 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const http = require('http'); + +const data = 'PUT / HTTP/1.1\r\n\r\n'; + +const server = http.createServer(common.mustCall(function(req, res) { + req.on('data', function(chunk) { + assert.strictEqual(chunk, Buffer.from(data)); + }); + res.setHeader('Content-Type', 'text/plain'); + for (let i = 0; i < req.rawHeaders.length; i += 2) { + if (req.rawHeaders[i].toLowerCase() === 'host') continue; + if (req.rawHeaders[i].toLowerCase() === 'connection') continue; + res.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`); + } + res.end(); +})).unref(); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const req = http.request({ method: 'DELETE', port }, function(res) { + res.resume(); + }); + + req.write(data); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http-request-smuggling-content-length.js b/test/js/node/test/parallel/test-http-request-smuggling-content-length.js new file mode 100644 index 0000000000..4ae39b93f4 --- /dev/null +++ b/test/js/node/test/parallel/test-http-request-smuggling-content-length.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const net = require('net'); +const assert = require('assert'); + +// Verify that a request with a space before the content length will result +// in a 400 Bad Request. + +const server = http.createServer(common.mustNotCall()); + +server.listen(0, common.mustCall(start)); + +function start() { + const sock = net.connect(server.address().port); + + sock.write('GET / HTTP/1.1\r\nHost: localhost:5000\r\n' + + 'Content-Length : 5\r\n\r\nhello'); + + let body = ''; + sock.setEncoding('utf8'); + sock.on('data', (chunk) => { + body += chunk; + }); + sock.on('end', common.mustCall(function() { + assert.strictEqual(body, 'HTTP/1.1 400 Bad Request\r\n' + + 'Connection: close\r\n\r\n'); + server.close(); + })); +} diff --git a/test/js/node/test/parallel/test-http-res-write-after-end.js b/test/js/node/test/parallel/test-http-res-write-after-end.js new file mode 100644 index 0000000000..285938c8fe --- /dev/null +++ b/test/js/node/test/parallel/test-http-res-write-after-end.js @@ -0,0 +1,45 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.Server(common.mustCall(function(req, res) { + res.on('error', common.expectsError({ + code: 'ERR_STREAM_WRITE_AFTER_END', + name: 'Error' + })); + + res.write('This should write.'); + res.end(); + + const r = res.write('This should raise an error.'); + // Write after end should return false + assert.strictEqual(r, false); +})); + +server.listen(0, function() { + http.get({ port: this.address().port }, function(res) { + server.close(); + }); +}); diff --git a/test/js/node/test/parallel/test-http-res-write-end-dont-take-array.js b/test/js/node/test/parallel/test-http-res-write-end-dont-take-array.js new file mode 100644 index 0000000000..8bebfc14e4 --- /dev/null +++ b/test/js/node/test/parallel/test-http-res-write-end-dont-take-array.js @@ -0,0 +1,73 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer(); + +server.once('request', common.mustCall((req, res) => { + server.on('request', common.mustCall((req, res) => { + res.end(Buffer.from('asdf')); + })); + // `res.write()` should accept `string`. + res.write('string'); + // `res.write()` should accept `buffer`. + res.write(Buffer.from('asdf')); + + const expectedError = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }; + + // `res.write()` should not accept an Array. + assert.throws( + () => { + res.write(['array']); + }, + expectedError + ); + + // `res.end()` should not accept an Array. + assert.throws( + () => { + res.end(['moo']); + }, + expectedError + ); + + // `res.end()` should accept `string`. + res.end('string'); +})); + +server.listen(0, function() { + // Just make a request, other tests handle responses. + http.get({ port: this.address().port }, (res) => { + res.resume(); + // Do it again to test .end(Buffer); + http.get({ port: server.address().port }, (res) => { + res.resume(); + server.close(); + }); + }); +}); diff --git a/test/js/node/test/parallel/test-http-response-readable.js b/test/js/node/test/parallel/test-http-response-readable.js new file mode 100644 index 0000000000..9ecfbc4ca9 --- /dev/null +++ b/test/js/node/test/parallel/test-http-response-readable.js @@ -0,0 +1,41 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); + +const testServer = new http.Server(function(req, res) { + res.writeHead(200); + res.end('Hello world'); +}); + +testServer.listen(0, function() { + http.get({ port: this.address().port }, function(res) { + assert.strictEqual(res.readable, true); + res.on('end', function() { + assert.strictEqual(res.readable, false); + testServer.close(); + }); + res.resume(); + }); +}); diff --git a/test/js/node/test/parallel/test-http-response-writehead-returns-this.js b/test/js/node/test/parallel/test-http-response-writehead-returns-this.js new file mode 100644 index 0000000000..a62c2eca03 --- /dev/null +++ b/test/js/node/test/parallel/test-http-response-writehead-returns-this.js @@ -0,0 +1,22 @@ +'use strict'; +require('../common'); +const http = require('http'); +const assert = require('assert'); + +const server = http.createServer((req, res) => { + res.writeHead(200, { 'a-header': 'a-header-value' }).end('abc'); +}); + +server.listen(0, () => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.headers['a-header'], 'a-header-value'); + + const chunks = []; + + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'abc'); + server.close(); + }); + }); +}); diff --git a/test/js/node/test/parallel/test-http-server-delete-parser.js b/test/js/node/test/parallel/test-http-server-delete-parser.js new file mode 100644 index 0000000000..4215ee2f9d --- /dev/null +++ b/test/js/node/test/parallel/test-http-server-delete-parser.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); + +const http = require('http'); + +const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('okay', common.mustCall(() => { + delete res.socket.parser; + })); + res.end(); +})); + +server.listen(0, '127.0.0.1', common.mustCall(() => { + const req = http.request({ + port: server.address().port, + host: '127.0.0.1', + method: 'GET', + }); + req.end(); +})); + +server.unref(); diff --git a/test/js/node/test/parallel/test-http-server-non-utf8-header.js b/test/js/node/test/parallel/test-http-server-non-utf8-header.js new file mode 100644 index 0000000000..8ce82ac4ca --- /dev/null +++ b/test/js/node/test/parallel/test-http-server-non-utf8-header.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +const nonUtf8Header = 'bår'; +const nonUtf8ToLatin1 = Buffer.from(nonUtf8Header).toString('latin1'); + +{ + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, [ + 'content-disposition', + Buffer.from(nonUtf8Header).toString('binary'), + ]); + res.end('hello'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.headers['content-disposition'], nonUtf8ToLatin1); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + // Test multi-value header + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, [ + 'content-disposition', + [Buffer.from(nonUtf8Header).toString('binary')], + ]); + res.end('hello'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.headers['content-disposition'], nonUtf8ToLatin1); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, [ + 'Content-Length', '5', + 'content-disposition', + Buffer.from(nonUtf8Header).toString('binary'), + ]); + res.end('hello'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.headers['content-disposition'], nonUtf8ToLatin1); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} diff --git a/test/js/node/test/parallel/test-http-server-options-incoming-message.js b/test/js/node/test/parallel/test-http-server-options-incoming-message.js new file mode 100644 index 0000000000..d0f4a769d7 --- /dev/null +++ b/test/js/node/test/parallel/test-http-server-options-incoming-message.js @@ -0,0 +1,41 @@ +'use strict'; + +/** + * This test covers http.Server({ IncomingMessage }) option: + * With IncomingMessage option the server should use + * the new class for creating req Object instead of the default + * http.IncomingMessage. + */ +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +class MyIncomingMessage extends http.IncomingMessage { + getUserAgent() { + return this.headers['user-agent'] || 'unknown'; + } +} + +const server = http.createServer({ + IncomingMessage: MyIncomingMessage +}, common.mustCall(function(req, res) { + assert.strictEqual(req.getUserAgent(), 'node-test'); + res.statusCode = 200; + res.end(); +})); +server.listen(); + +server.on('listening', function makeRequest() { + http.get({ + port: this.address().port, + headers: { + 'User-Agent': 'node-test' + } + }, (res) => { + assert.strictEqual(res.statusCode, 200); + res.on('end', () => { + server.close(); + }); + res.resume(); + }); +}); diff --git a/test/js/node/test/parallel/test-http-server-options-server-response.js b/test/js/node/test/parallel/test-http-server-options-server-response.js new file mode 100644 index 0000000000..f5adf39bed --- /dev/null +++ b/test/js/node/test/parallel/test-http-server-options-server-response.js @@ -0,0 +1,35 @@ +'use strict'; + +/** + * This test covers http.Server({ ServerResponse }) option: + * With ServerResponse option the server should use + * the new class for creating res Object instead of the default + * http.ServerResponse. + */ +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +class MyServerResponse extends http.ServerResponse { + status(code) { + return this.writeHead(code, { 'Content-Type': 'text/plain' }); + } +} + +const server = http.Server({ + ServerResponse: MyServerResponse +}, common.mustCall(function(req, res) { + res.status(200); + res.end(); +})); +server.listen(); + +server.on('listening', function makeRequest() { + http.get({ port: this.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + res.on('end', () => { + server.close(); + }); + res.resume(); + }); +}); diff --git a/test/js/node/test/parallel/test-http-server-stale-close.js b/test/js/node/test/parallel/test-http-server-stale-close.js new file mode 100644 index 0000000000..b9322ed9fc --- /dev/null +++ b/test/js/node/test/parallel/test-http-server-stale-close.js @@ -0,0 +1,53 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const http = require('http'); +const fork = require('child_process').fork; +const assert = require('assert'); + +if (process.env.NODE_TEST_FORK_PORT) { + const req = http.request({ + headers: { 'Content-Length': '42' }, + method: 'POST', + host: '127.0.0.1', + port: +process.env.NODE_TEST_FORK_PORT, + }, process.exit); + req.write('BAM'); + req.end(); +} else { + const server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Length': '42' }); + req.pipe(res); + assert.strictEqual(req.destroyed, false); + req.on('close', () => { + assert.strictEqual(req.destroyed, true); + server.close(); + res.end(); + }); + }); + server.listen(0, function() { + fork(__filename, { + env: { ...process.env, NODE_TEST_FORK_PORT: this.address().port } + }); + }); +} diff --git a/test/js/node/test/parallel/test-http-server-write-after-end.js b/test/js/node/test/parallel/test-http-server-write-after-end.js new file mode 100644 index 0000000000..ba28771312 --- /dev/null +++ b/test/js/node/test/parallel/test-http-server-write-after-end.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); + +// Fix for https://github.com/nodejs/node/issues/14368 + +const server = http.createServer(handle); + +function handle(req, res) { + res.on('error', common.mustNotCall()); + + res.write('hello'); + res.end(); + + setImmediate(common.mustCall(() => { + res.write('world', common.mustCall((err) => { + common.expectsError({ + code: 'ERR_STREAM_WRITE_AFTER_END', + name: 'Error' + })(err); + server.close(); + })); + })); +} + +server.listen(0, common.mustCall(() => { + http.get(`http://localhost:${server.address().port}`); +})); diff --git a/test/js/node/test/parallel/test-http-set-header-chain.js b/test/js/node/test/parallel/test-http-set-header-chain.js new file mode 100644 index 0000000000..aa9519129a --- /dev/null +++ b/test/js/node/test/parallel/test-http-set-header-chain.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); +const expected = { + '__proto__': null, + 'testheader1': 'foo', + 'testheader2': 'bar', + 'testheader3': 'xyz' +}; +const server = http.createServer(common.mustCall((req, res) => { + let retval = res.setHeader('testheader1', 'foo'); + + // Test that the setHeader returns the same response object. + assert.strictEqual(retval, res); + + retval = res.setHeader('testheader2', 'bar').setHeader('testheader3', 'xyz'); + // Test that chaining works for setHeader. + assert.deepStrictEqual(res.getHeaders(), expected); + res.end('ok'); +})); +server.listen(0, () => { + http.get({ port: server.address().port }, common.mustCall((res) => { + res.on('data', () => {}); + res.on('end', common.mustCall(() => { + server.close(); + })); + })); +}); diff --git a/test/js/node/test/parallel/test-http-status-code.js b/test/js/node/test/parallel/test-http-status-code.js new file mode 100644 index 0000000000..246d22c131 --- /dev/null +++ b/test/js/node/test/parallel/test-http-status-code.js @@ -0,0 +1,58 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const Countdown = require('../common/countdown'); + +// Simple test of Node's HTTP ServerResponse.statusCode +// ServerResponse.prototype.statusCode + +const tests = [200, 202, 300, 404, 451, 500]; +let test; +const countdown = new Countdown(tests.length, () => s.close()); + +const s = http.createServer(function(req, res) { + res.writeHead(test, { 'Content-Type': 'text/plain' }); + console.log(`--\nserver: statusCode after writeHead: ${res.statusCode}`); + assert.strictEqual(res.statusCode, test); + res.end('hello world\n'); +}); + +s.listen(0, nextTest); + + +function nextTest() { + test = tests.shift(); + + http.get({ port: s.address().port }, function(response) { + console.log(`client: expected status: ${test}`); + console.log(`client: statusCode: ${response.statusCode}`); + assert.strictEqual(response.statusCode, test); + response.on('end', function() { + if (countdown.dec()) + nextTest(); + }); + response.resume(); + }); +} diff --git a/test/js/node/test/parallel/test-http-upgrade-reconsume-stream.js b/test/js/node/test/parallel/test-http-upgrade-reconsume-stream.js new file mode 100644 index 0000000000..e712ea647b --- /dev/null +++ b/test/js/node/test/parallel/test-http-upgrade-reconsume-stream.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); +const http = require('http'); + +// Tests that, after the HTTP parser stopped owning a socket that emits an +// 'upgrade' event, another C++ stream can start owning it (e.g. a TLSSocket). + +const server = http.createServer(common.mustNotCall()); + +server.on('upgrade', common.mustCall((request, socket, head) => { + // This should not crash. + new tls.TLSSocket(socket); + server.close(); + socket.destroy(); +})); + +server.listen(0, common.mustCall(() => { + http.get({ + port: server.address().port, + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket' + } + }).on('error', () => {}); +})); diff --git a/test/js/node/test/parallel/test-http-url.parse-auth-with-header-in-request.js b/test/js/node/test/parallel/test-http-url.parse-auth-with-header-in-request.js new file mode 100644 index 0000000000..ea5793ee18 --- /dev/null +++ b/test/js/node/test/parallel/test-http-url.parse-auth-with-header-in-request.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +function check(request) { + // The correct authorization header is be passed + assert.strictEqual(request.headers.authorization, 'NoAuthForYOU'); +} + +const server = http.createServer(function(request, response) { + // Run the check function + check(request); + response.writeHead(200, {}); + response.end('ok'); + server.close(); +}); + +server.listen(0, function() { + const testURL = + url.parse(`http://asdf:qwer@localhost:${this.address().port}`); + // The test here is if you set a specific authorization header in the + // request we should not override that with basic auth + testURL.headers = { + Authorization: 'NoAuthForYOU' + }; + + // make the request + http.request(testURL).end(); +}); diff --git a/test/js/node/test/parallel/test-http-url.parse-auth.js b/test/js/node/test/parallel/test-http-url.parse-auth.js new file mode 100644 index 0000000000..2bb5311586 --- /dev/null +++ b/test/js/node/test/parallel/test-http-url.parse-auth.js @@ -0,0 +1,48 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +function check(request) { + // The correct authorization header is be passed + assert.strictEqual(request.headers.authorization, 'Basic dXNlcjpwYXNzOg=='); +} + +const server = http.createServer(function(request, response) { + // Run the check function + check(request); + response.writeHead(200, {}); + response.end('ok'); + server.close(); +}); + +server.listen(0, function() { + const port = this.address().port; + // username = "user", password = "pass:" + const testURL = url.parse(`http://user:pass%3A@localhost:${port}`); + + // make the request + http.request(testURL).end(); +}); diff --git a/test/js/node/test/parallel/test-http-url.parse-basic.js b/test/js/node/test/parallel/test-http-url.parse-basic.js new file mode 100644 index 0000000000..71885b4bd0 --- /dev/null +++ b/test/js/node/test/parallel/test-http-url.parse-basic.js @@ -0,0 +1,58 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +let testURL; + +// Make sure the basics work +function check(request) { + // Default method should still be 'GET' + assert.strictEqual(request.method, 'GET'); + // There are no URL params, so you should not see any + assert.strictEqual(request.url, '/'); + // The host header should use the url.parse.hostname + assert.strictEqual(request.headers.host, + `${testURL.hostname}:${testURL.port}`); +} + +const server = http.createServer(function(request, response) { + // Run the check function + check(request); + response.writeHead(200, {}); + response.end('ok'); + server.close(); +}); + +server.listen(0, function() { + testURL = url.parse(`http://localhost:${this.address().port}`); + + // make the request + const clientRequest = http.request(testURL); + // Since there is a little magic with the agent + // make sure that an http request uses the http.Agent + assert.ok(clientRequest.agent instanceof http.Agent); + clientRequest.end(); +}); diff --git a/test/js/node/test/parallel/test-http-url.parse-only-support-http-https-protocol.js b/test/js/node/test/parallel/test-http-url.parse-only-support-http-https-protocol.js new file mode 100644 index 0000000000..3f6633075c --- /dev/null +++ b/test/js/node/test/parallel/test-http-url.parse-only-support-http-https-protocol.js @@ -0,0 +1,45 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +const invalidUrls = [ + 'file:///whatever', + 'mailto:asdf@asdf.com', + 'ftp://www.example.com', + 'javascript:alert(\'hello\');', + 'xmpp:foo@bar.com', + 'f://some.host/path', +]; + +for (const invalid of invalidUrls) { + assert.throws( + () => { http.request(url.parse(invalid)); }, + { + code: 'ERR_INVALID_PROTOCOL', + name: 'TypeError' + } + ); +} diff --git a/test/js/node/test/parallel/test-http-url.parse-path.js b/test/js/node/test/parallel/test-http-url.parse-path.js new file mode 100644 index 0000000000..25e4838c4a --- /dev/null +++ b/test/js/node/test/parallel/test-http-url.parse-path.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +function check(request) { + // A path should come over + assert.strictEqual(request.url, '/asdf'); +} + +const server = http.createServer(function(request, response) { + // Run the check function + check(request); + response.writeHead(200, {}); + response.end('ok'); + server.close(); +}); + +server.listen(0, function() { + const testURL = url.parse(`http://localhost:${this.address().port}/asdf`); + + // make the request + http.request(testURL).end(); +}); diff --git a/test/js/node/test/parallel/test-http-url.parse-post.js b/test/js/node/test/parallel/test-http-url.parse-post.js new file mode 100644 index 0000000000..db5ee78fe6 --- /dev/null +++ b/test/js/node/test/parallel/test-http-url.parse-post.js @@ -0,0 +1,54 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +let testURL; + +function check(request) { + // url.parse should not mess with the method + assert.strictEqual(request.method, 'POST'); + // Everything else should be right + assert.strictEqual(request.url, '/asdf?qwer=zxcv'); + // The host header should use the url.parse.hostname + assert.strictEqual(request.headers.host, + `${testURL.hostname}:${testURL.port}`); +} + +const server = http.createServer(function(request, response) { + // Run the check function + check(request); + response.writeHead(200, {}); + response.end('ok'); + server.close(); +}); + +server.listen(0, function() { + testURL = url.parse(`http://localhost:${this.address().port}/asdf?qwer=zxcv`); + testURL.method = 'POST'; + + // make the request + http.request(testURL).end(); +}); diff --git a/test/js/node/test/parallel/test-http-url.parse-search.js b/test/js/node/test/parallel/test-http-url.parse-search.js new file mode 100644 index 0000000000..0759c779d3 --- /dev/null +++ b/test/js/node/test/parallel/test-http-url.parse-search.js @@ -0,0 +1,47 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +function check(request) { + // A path should come over with params + assert.strictEqual(request.url, '/asdf?qwer=zxcv'); +} + +const server = http.createServer(function(request, response) { + // Run the check function + check(request); + response.writeHead(200, {}); + response.end('ok'); + server.close(); +}); + +server.listen(0, function() { + const port = this.address().port; + const testURL = url.parse(`http://localhost:${port}/asdf?qwer=zxcv`); + + // make the request + http.request(testURL).end(); +}); diff --git a/test/js/node/test/parallel/test-http-write-empty-string.js b/test/js/node/test/parallel/test-http-write-empty-string.js new file mode 100644 index 0000000000..88eff08f76 --- /dev/null +++ b/test/js/node/test/parallel/test-http-write-empty-string.js @@ -0,0 +1,54 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const http = require('http'); + +const server = http.createServer(function(request, response) { + console.log(`responding to ${request.url}`); + + response.writeHead(200, { 'Content-Type': 'text/plain' }); + response.write('1\n'); + response.write(''); + response.write('2\n'); + response.write(''); + response.end('3\n'); + + this.close(); +}); + +server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, common.mustCall((res) => { + let response = ''; + + assert.strictEqual(res.statusCode, 200); + res.setEncoding('ascii'); + res.on('data', (chunk) => { + response += chunk; + }); + res.on('end', common.mustCall(() => { + assert.strictEqual(response, '1\n2\n3\n'); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-http-zero-length-write.js b/test/js/node/test/parallel/test-http-zero-length-write.js new file mode 100644 index 0000000000..f765ad0545 --- /dev/null +++ b/test/js/node/test/parallel/test-http-zero-length-write.js @@ -0,0 +1,94 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const http = require('http'); + +const Stream = require('stream'); + +function getSrc() { + // An old-style readable stream. + // The Readable class prevents this behavior. + const src = new Stream(); + + // Start out paused, just so we don't miss anything yet. + let paused = false; + src.pause = function() { + paused = true; + }; + src.resume = function() { + paused = false; + }; + + const chunks = [ '', 'asdf', '', 'foo', '', 'bar', '' ]; + const interval = setInterval(function() { + if (paused) + return; + + const chunk = chunks.shift(); + if (chunk !== undefined) { + src.emit('data', chunk); + } else { + src.emit('end'); + clearInterval(interval); + } + }, 1); + + return src; +} + + +const expect = 'asdffoobar'; + +const server = http.createServer(function(req, res) { + let actual = ''; + req.setEncoding('utf8'); + req.on('data', function(c) { + actual += c; + }); + req.on('end', function() { + assert.strictEqual(actual, expect); + getSrc().pipe(res); + }); + server.close(); +}); + +server.listen(0, function() { + const req = http.request({ port: this.address().port, method: 'POST' }); + let actual = ''; + req.on('response', function(res) { + res.setEncoding('utf8'); + res.on('data', function(c) { + actual += c; + }); + res.on('end', function() { + assert.strictEqual(actual, expect); + }); + }); + getSrc().pipe(req); +}); + +process.on('exit', function(c) { + if (!c) console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-http-zerolengthbuffer.js b/test/js/node/test/parallel/test-http-zerolengthbuffer.js new file mode 100644 index 0000000000..c59fc18108 --- /dev/null +++ b/test/js/node/test/parallel/test-http-zerolengthbuffer.js @@ -0,0 +1,23 @@ +'use strict'; +// Serving up a zero-length buffer should work. + +const common = require('../common'); +const http = require('http'); + +const server = http.createServer((req, res) => { + const buffer = Buffer.alloc(0); + res.writeHead(200, { 'Content-Type': 'text/html', + 'Content-Length': buffer.length }); + res.end(buffer); +}); + +server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, common.mustCall((res) => { + + res.on('data', common.mustNotCall()); + + res.on('end', (d) => { + server.close(); + }); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-clean-output.js b/test/js/node/test/parallel/test-http2-clean-output.js new file mode 100644 index 0000000000..27b7c338c9 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-clean-output.js @@ -0,0 +1,40 @@ +'use strict'; + +const { + hasCrypto, + mustCall, + skip +} = require('../common'); +if (!hasCrypto) + skip('missing crypto'); + +const { + strictEqual +} = require('assert'); +const { + createServer, + connect +} = require('http2'); +const { + spawnSync +} = require('child_process'); + +// Validate that there is no unexpected output when +// using http2 +if (process.argv[2] !== 'child') { + const { + stdout, stderr, status + } = spawnSync(process.execPath, [__filename, 'child'], { encoding: 'utf8' }); + strictEqual(stderr, ''); + strictEqual(stdout, ''); + strictEqual(status, 0); +} else { + const server = createServer(); + server.listen(0, mustCall(() => { + const client = connect(`http://localhost:${server.address().port}`); + client.on('connect', mustCall(() => { + client.close(); + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-http2-client-priority-before-connect.js b/test/js/node/test/parallel/test-http2-client-priority-before-connect.js new file mode 100644 index 0000000000..7aa13a5e45 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-client-priority-before-connect.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// We use the lower-level API here +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end('ok'); +})); + +server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.priority({}); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-client-request-listeners-warning.js b/test/js/node/test/parallel/test-http2-client-request-listeners-warning.js new file mode 100644 index 0000000000..854e9535fd --- /dev/null +++ b/test/js/node/test/parallel/test-http2-client-request-listeners-warning.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const EventEmitter = require('events'); + +// This test ensures that a MaxListenersExceededWarning isn't emitted if +// more than EventEmitter.defaultMaxListeners requests are started on a +// ClientHttp2Session before it has finished connecting. + +process.on('warning', common.mustNotCall('A warning was emitted')); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond(); + stream.end(); +}); + +server.listen(common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + function request() { + return new Promise((resolve, reject) => { + const stream = client.request(); + stream.on('error', reject); + stream.on('response', resolve); + stream.end(); + }); + } + + const requests = []; + for (let i = 0; i < EventEmitter.defaultMaxListeners + 1; i++) { + requests.push(request()); + } + + Promise.all(requests).then(common.mustCall()).finally(common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-client-upload-reject.js b/test/js/node/test/parallel/test-http2-client-upload-reject.js new file mode 100644 index 0000000000..7234493031 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-client-upload-reject.js @@ -0,0 +1,49 @@ +'use strict'; + +// Verifies that uploading data from a client works + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const loc = fixtures.path('person-large.jpg'); + +assert(fs.existsSync(loc)); + +fs.readFile(loc, common.mustSucceed((data) => { + const server = http2.createServer(); + + server.on('stream', common.mustCall((stream) => { + // Wait for some data to come through. + setImmediate(() => { + stream.on('close', common.mustCall(() => { + assert.strictEqual(stream.rstCode, 0); + })); + + stream.respond({ ':status': 400 }); + stream.end(); + }); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 400); + })); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); + + const str = fs.createReadStream(loc); + str.pipe(req); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-client-upload.js b/test/js/node/test/parallel/test-http2-client-upload.js new file mode 100644 index 0000000000..d073cd94e6 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-client-upload.js @@ -0,0 +1,58 @@ +'use strict'; + +// Verifies that uploading data from a client works + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); +const Countdown = require('../common/countdown'); + +const loc = fixtures.path('person-large.jpg'); +let fileData; + +assert(fs.existsSync(loc)); + +fs.readFile(loc, common.mustSucceed((data) => { + fileData = data; + + const server = http2.createServer(); + let client; + + const countdown = new Countdown(2, () => { + server.close(); + client.close(); + }); + + server.on('stream', common.mustCall((stream) => { + let data = Buffer.alloc(0); + stream.on('data', (chunk) => data = Buffer.concat([data, chunk])); + stream.on('end', common.mustCall(() => { + assert.deepStrictEqual(data, fileData); + })); + // Waiting on close avoids spurious ECONNRESET seen in windows CI. + // Not sure if this is actually a bug; more details at + // https://github.com/nodejs/node/issues/20750#issuecomment-511015247 + stream.on('close', () => countdown.dec()); + stream.respond(); + stream.end(); + })); + + server.listen(0, common.mustCall(() => { + client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + + req.resume(); + req.on('end', common.mustCall()); + + const str = fs.createReadStream(loc); + str.on('end', common.mustCall()); + str.on('close', () => countdown.dec()); + str.pipe(req); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-client-write-before-connect.js b/test/js/node/test/parallel/test-http2-client-write-before-connect.js new file mode 100644 index 0000000000..6efefc5870 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-client-write-before-connect.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// We use the lower-level API here +server.on('stream', common.mustCall((stream, headers, flags) => { + let data = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'some data more data'); + })); + stream.respond(); + stream.end('ok'); +})); + +server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':method': 'POST' }); + req.write('some data '); + req.end('more data'); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-client-write-empty-string.js b/test/js/node/test/parallel/test-http2-client-write-empty-string.js new file mode 100644 index 0000000000..f0f0b8f012 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-client-write-empty-string.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); + +for (const chunkSequence of [ + [ '' ], + [ '', '' ], +]) { + const server = http2.createServer(); + server.on('stream', common.mustCall((stream, headers, flags) => { + stream.respond({ 'content-type': 'text/html' }); + + let data = ''; + stream.on('data', common.mustNotCall((chunk) => { + data += chunk.toString(); + })); + stream.on('end', common.mustCall(() => { + stream.end(`"${data}"`); + })); + })); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const req = client.request({ + ':method': 'POST', + ':path': '/' + }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + })); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', common.mustCallAtLeast((d) => data += d)); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, '""'); + server.close(); + client.close(); + })); + + for (const chunk of chunkSequence) + req.write(chunk); + req.end(); + })); +} diff --git a/test/js/node/test/parallel/test-http2-compat-errors.js b/test/js/node/test/parallel/test-http2-compat-errors.js new file mode 100644 index 0000000000..18dc385422 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-errors.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); + +// Errors should not be reported both in Http2ServerRequest +// and Http2ServerResponse + +let expected = null; + +const server = h2.createServer(common.mustCall(function(req, res) { + res.stream.on('error', common.mustCall()); + req.on('error', common.mustNotCall()); + res.on('error', common.mustNotCall()); + req.on('aborted', common.mustCall()); + res.on('aborted', common.mustNotCall()); + + res.write('hello'); + + expected = new Error('kaboom'); + res.stream.destroy(expected); + server.close(); +})); + +server.listen(0, common.mustCall(function() { + const url = `http://localhost:${server.address().port}`; + const client = h2.connect(url, common.mustCall(() => { + const request = client.request(); + request.on('data', common.mustCall((chunk) => { + client.destroy(); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-expect-continue.js b/test/js/node/test/parallel/test-http2-compat-expect-continue.js new file mode 100644 index 0000000000..d0decb1472 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-expect-continue.js @@ -0,0 +1,95 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +{ + const testResBody = 'other stuff!\n'; + + // Checks the full 100-continue flow from client sending 'expect: 100-continue' + // through server receiving it, sending back :status 100, writing the rest of + // the request to finally the client receiving to. + + const server = http2.createServer(); + + let sentResponse = false; + + server.on('request', common.mustCall((req, res) => { + res.end(testResBody); + sentResponse = true; + })); + + server.listen(0); + + server.on('listening', common.mustCall(() => { + let body = ''; + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ + ':method': 'POST', + 'expect': '100-continue' + }); + + let gotContinue = false; + req.on('continue', common.mustCall(() => { + gotContinue = true; + })); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(gotContinue, true); + assert.strictEqual(sentResponse, true); + assert.strictEqual(headers[':status'], 200); + req.end(); + })); + + req.setEncoding('utf8'); + req.on('data', common.mustCall((chunk) => { body += chunk; })); + req.on('end', common.mustCall(() => { + assert.strictEqual(body, testResBody); + client.close(); + server.close(); + })); + })); +} + +{ + // Checks the full 100-continue flow from client sending 'expect: 100-continue' + // through server receiving it and ending the request. + + const server = http2.createServer(); + + server.on('request', common.mustCall((req, res) => { + res.end(); + })); + + server.listen(0); + + server.on('listening', common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ + ':path': '/', + 'expect': '100-continue' + }); + + let gotContinue = false; + req.on('continue', common.mustCall(() => { + gotContinue = true; + })); + + let gotResponse = false; + req.on('response', common.mustCall(() => { + gotResponse = true; + })); + + req.setEncoding('utf8'); + req.on('end', common.mustCall(() => { + assert.strictEqual(gotContinue, true); + assert.strictEqual(gotResponse, true); + client.close(); + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-http2-compat-expect-handling.js b/test/js/node/test/parallel/test-http2-compat-expect-handling.js new file mode 100644 index 0000000000..77f2275834 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-expect-handling.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const expectValue = 'meoww'; + +const server = http2.createServer(common.mustNotCall()); + +server.once('checkExpectation', common.mustCall((req, res) => { + assert.strictEqual(req.headers.expect, expectValue); + res.statusCode = 417; + res.end(); +})); + +server.listen(0, common.mustCall(() => nextTest(2))); + +function nextTest(testsToRun) { + if (!testsToRun) { + return server.close(); + } + + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + const req = client.request({ + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + 'expect': expectValue + }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 417); + req.resume(); + })); + + req.on('end', common.mustCall(() => { + client.close(); + nextTest(testsToRun - 1); + })); +} diff --git a/test/js/node/test/parallel/test-http2-compat-method-connect.js b/test/js/node/test/parallel/test-http2-compat-method-connect.js new file mode 100644 index 0000000000..21ad23e92b --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-method-connect.js @@ -0,0 +1,40 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(common.mustNotCall()); + +server.listen(0, common.mustCall(() => testMethodConnect(2))); + +server.once('connect', common.mustCall((req, res) => { + assert.strictEqual(req.headers[':method'], 'CONNECT'); + res.statusCode = 405; + res.end(); +})); + +function testMethodConnect(testsToRun) { + if (!testsToRun) { + return server.close(); + } + + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + const req = client.request({ + ':method': 'CONNECT', + ':authority': `localhost:${port}` + }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 405); + })); + req.resume(); + req.on('end', common.mustCall(() => { + client.close(); + testMethodConnect(testsToRun - 1); + })); + req.end(); +} diff --git a/test/js/node/test/parallel/test-http2-compat-serverrequest-pause.js b/test/js/node/test/parallel/test-http2-compat-serverrequest-pause.js new file mode 100644 index 0000000000..2abc9e3da4 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverrequest-pause.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Check that pause & resume work as expected with Http2ServerRequest + +const testStr = 'Request Body from Client'; + +const server = h2.createServer(); + +server.on('request', common.mustCall((req, res) => { + let data = ''; + req.pause(); + req.setEncoding('utf8'); + req.on('data', common.mustCall((chunk) => (data += chunk))); + setTimeout(common.mustCall(() => { + assert.strictEqual(data, ''); + req.resume(); + }), common.platformTimeout(100)); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, testStr); + res.end(); + })); + + // Shouldn't throw if underlying Http2Stream no longer exists + res.on('finish', common.mustCall(() => process.nextTick(() => { + req.pause(); + req.resume(); + }))); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + const client = h2.connect(`http://localhost:${port}`); + const request = client.request({ + ':path': '/foobar', + ':method': 'POST', + ':scheme': 'http', + ':authority': `localhost:${port}` + }); + request.resume(); + request.end(testStr); + request.on('end', common.mustCall(function() { + client.close(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverrequest-pipe.js b/test/js/node/test/parallel/test-http2-compat-serverrequest-pipe.js new file mode 100644 index 0000000000..35c183e18e --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverrequest-pipe.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +if (common.isWindows) return; // TODO: BUN +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); + +// Piping should work as expected with createWriteStream + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const loc = fixtures.path('person-large.jpg'); +const fn = tmpdir.resolve('http2-url-tests.js'); + +const server = http2.createServer(); + +server.on('request', common.mustCall((req, res) => { + const dest = req.pipe(fs.createWriteStream(fn)); + dest.on('finish', common.mustCall(() => { + assert.strictEqual(req.complete, true); + assert.strictEqual(fs.readFileSync(loc).length, fs.readFileSync(fn).length); + fs.unlinkSync(fn); + res.end(); + })); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.close(); + } + } + + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(maybeClose)); + const str = fs.createReadStream(loc); + str.on('end', common.mustCall(maybeClose)); + str.pipe(req); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse-end-after-statuses-without-body.js b/test/js/node/test/parallel/test-http2-compat-serverresponse-end-after-statuses-without-body.js new file mode 100644 index 0000000000..ce8cbe600c --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse-end-after-statuses-without-body.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); + +// This test case ensures that calling of res.end after sending +// 204, 205 and 304 HTTP statuses will not cause an error +// See issue: https://github.com/nodejs/node/issues/21740 + +const { + HTTP_STATUS_NO_CONTENT, + HTTP_STATUS_RESET_CONTENT, + HTTP_STATUS_NOT_MODIFIED +} = h2.constants; + +const statusWithoutBody = [ + HTTP_STATUS_NO_CONTENT, + HTTP_STATUS_RESET_CONTENT, + HTTP_STATUS_NOT_MODIFIED, +]; +const STATUS_CODES_COUNT = statusWithoutBody.length; + +const server = h2.createServer(common.mustCall(function(req, res) { + res.writeHead(statusWithoutBody.pop()); + res.end(); +}, STATUS_CODES_COUNT)); + +server.listen(0, common.mustCall(function() { + const url = `http://localhost:${server.address().port}`; + const client = h2.connect(url, common.mustCall(() => { + let responseCount = 0; + const closeAfterResponse = () => { + if (STATUS_CODES_COUNT === ++responseCount) { + client.destroy(); + server.close(); + } + }; + + for (let i = 0; i < STATUS_CODES_COUNT; i++) { + const request = client.request(); + request.on('response', common.mustCall(closeAfterResponse)); + } + + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse-finished.js b/test/js/node/test/parallel/test-http2-compat-serverresponse-finished.js new file mode 100644 index 0000000000..a42d40227c --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse-finished.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); +const net = require('net'); + +// Http2ServerResponse.finished +const server = h2.createServer(); +server.listen(0, common.mustCall(() => { + const port = server.address().port; + server.once('request', common.mustCall((request, response) => { + assert.ok(response.socket instanceof net.Socket); + assert.ok(response.connection instanceof net.Socket); + assert.strictEqual(response.socket, response.connection); + + response.on('finish', common.mustCall(() => { + assert.strictEqual(response.socket, undefined); + assert.strictEqual(response.connection, undefined); + process.nextTick(common.mustCall(() => { + assert.ok(response.stream); + server.close(); + })); + })); + assert.strictEqual(response.finished, false); + assert.strictEqual(response.writableEnded, false); + response.end(); + assert.strictEqual(response.finished, true); + assert.strictEqual(response.writableEnded, true); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(() => { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(() => { + client.close(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse-flushheaders.js b/test/js/node/test/parallel/test-http2-compat-serverresponse-flushheaders.js new file mode 100644 index 0000000000..7760bf8c7d --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse-flushheaders.js @@ -0,0 +1,59 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.flushHeaders + +let serverResponse; + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + assert.strictEqual(response.headersSent, false); + assert.strictEqual(response._header, false); // Alias for headersSent + response.flushHeaders(); + assert.strictEqual(response.headersSent, true); + assert.strictEqual(response._header, true); + response.flushHeaders(); // Idempotent + + assert.throws(() => { + response.writeHead(400, { 'foo-bar': 'abc123' }); + }, { + code: 'ERR_HTTP2_HEADERS_SENT' + }); + + response.on('finish', common.mustCall(function() { + server.close(); + process.nextTick(() => { + response.flushHeaders(); // Idempotent + }); + })); + serverResponse = response; + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers, flags) { + assert.strictEqual(headers['foo-bar'], undefined); + assert.strictEqual(headers[':status'], 200); + serverResponse.end(); + }, 1)); + request.on('end', common.mustCall(function() { + client.close(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse-headers-send-date.js b/test/js/node/test/parallel/test-http2-compat-serverresponse-headers-send-date.js new file mode 100644 index 0000000000..b22b1f7304 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse-headers-send-date.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) { common.skip('missing crypto'); } +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(common.mustCall((request, response) => { + response.sendDate = false; + response.writeHead(200); + response.end(); +})); + +server.listen(0, common.mustCall(() => { + const session = http2.connect(`http://localhost:${server.address().port}`); + const req = session.request(); + + req.on('response', common.mustCall((headers, flags) => { + assert.strictEqual('Date' in headers, false); + assert.strictEqual('date' in headers, false); + })); + + req.on('end', common.mustCall(() => { + session.close(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse-statuscode.js b/test/js/node/test/parallel/test-http2-compat-serverresponse-statuscode.js new file mode 100644 index 0000000000..6064a5936e --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse-statuscode.js @@ -0,0 +1,76 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse should have a statusCode property + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const expectedDefaultStatusCode = 200; + const realStatusCodes = { + continue: 100, + ok: 200, + multipleChoices: 300, + badRequest: 400, + internalServerError: 500 + }; + const fakeStatusCodes = { + tooLow: 99, + tooHigh: 600 + }; + + assert.strictEqual(response.statusCode, expectedDefaultStatusCode); + + // Setting the response.statusCode should not throw. + response.statusCode = realStatusCodes.ok; + response.statusCode = realStatusCodes.multipleChoices; + response.statusCode = realStatusCodes.badRequest; + response.statusCode = realStatusCodes.internalServerError; + + assert.throws(() => { + response.statusCode = realStatusCodes.continue; + }, { + code: 'ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + name: 'RangeError' + }); + assert.throws(() => { + response.statusCode = fakeStatusCodes.tooLow; + }, { + code: 'ERR_HTTP2_STATUS_INVALID', + name: 'RangeError' + }); + assert.throws(() => { + response.statusCode = fakeStatusCodes.tooHigh; + }, { + code: 'ERR_HTTP2_STATUS_INVALID', + name: 'RangeError' + }); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.close(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse-writehead-array.js b/test/js/node/test/parallel/test-http2-compat-serverresponse-writehead-array.js new file mode 100644 index 0000000000..a0cb65d4bf --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse-writehead-array.js @@ -0,0 +1,96 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +// Http2ServerResponse.writeHead should support arrays and nested arrays + +{ + const server = http2.createServer(); + server.listen(0, common.mustCall(() => { + const port = server.address().port; + + server.once('request', common.mustCall((request, response) => { + const returnVal = response.writeHead(200, [ + ['foo', 'bar'], + ['foo', 'baz'], + ['ABC', 123], + ]); + assert.strictEqual(returnVal, response); + response.end(common.mustCall(() => { server.close(); })); + })); + + const client = http2.connect(`http://localhost:${port}`, common.mustCall(() => { + const request = client.request(); + + request.on('response', common.mustCall((headers) => { + assert.strictEqual(headers.foo, 'bar, baz'); + assert.strictEqual(headers.abc, '123'); + assert.strictEqual(headers[':status'], 200); + }, 1)); + request.on('end', common.mustCall(() => { + client.close(); + })); + request.end(); + request.resume(); + })); + })); +} + +{ + const server = http2.createServer(); + server.listen(0, common.mustCall(() => { + const port = server.address().port; + + server.once('request', common.mustCall((request, response) => { + const returnVal = response.writeHead(200, ['foo', 'bar', 'foo', 'baz', 'ABC', 123]); + assert.strictEqual(returnVal, response); + response.end(common.mustCall(() => { server.close(); })); + })); + + const client = http2.connect(`http://localhost:${port}`, common.mustCall(() => { + const request = client.request(); + + request.on('response', common.mustCall((headers) => { + assert.strictEqual(headers.foo, 'bar, baz'); + assert.strictEqual(headers.abc, '123'); + assert.strictEqual(headers[':status'], 200); + }, 1)); + request.on('end', common.mustCall(() => { + client.close(); + })); + request.end(); + request.resume(); + })); + })); +} + +{ + const server = http2.createServer(); + server.listen(0, common.mustCall(() => { + const port = server.address().port; + + server.once('request', common.mustCall((request, response) => { + try { + response.writeHead(200, ['foo', 'bar', 'ABC', 123, 'extra']); + } catch (err) { + assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE'); + } + + response.end(common.mustCall(() => { server.close(); })); + })); + + const client = http2.connect(`http://localhost:${port}`, common.mustCall(() => { + const request = client.request(); + + request.on('end', common.mustCall(() => { + client.close(); + })); + request.end(); + request.resume(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse-writehead.js b/test/js/node/test/parallel/test-http2-compat-serverresponse-writehead.js new file mode 100644 index 0000000000..8157dcbedd --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse-writehead.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.writeHead should override previous headers + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.setHeader('foo-bar', 'def456'); + + // Override + const returnVal = response.writeHead(418, { 'foo-bar': 'abc123' }); + + assert.strictEqual(returnVal, response); + + assert.throws(() => { response.writeHead(300); }, { + code: 'ERR_HTTP2_HEADERS_SENT' + }); + + response.on('finish', common.mustCall(function() { + server.close(); + process.nextTick(common.mustCall(() => { + // The stream is invalid at this point, + // and this line verifies this does not throw. + response.writeHead(300); + })); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers['foo-bar'], 'abc123'); + assert.strictEqual(headers[':status'], 418); + }, 1)); + request.on('end', common.mustCall(function() { + client.close(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-serverresponse.js b/test/js/node/test/parallel/test-http2-compat-serverresponse.js new file mode 100644 index 0000000000..fbde58693b --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-serverresponse.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse should expose convenience properties + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + assert.strictEqual(response.req, request); + + // Verify that writing to response.req is allowed. + response.req = null; + + response.on('finish', common.mustCall(function() { + process.nextTick(() => { + server.close(); + }); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.close(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-type.js b/test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-type.js new file mode 100644 index 0000000000..caf5824e07 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-type.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const assert = require('node:assert'); +const http2 = require('node:http2'); +const debug = require('node:util').debuglog('test'); + +const testResBody = 'response content'; + +{ + // Invalid object value + + const server = http2.createServer(); + + server.on('request', common.mustCall((req, res) => { + debug('Server sending early hints...'); + res.writeEarlyHints('this should not be here'); + + debug('Server sending full response...'); + res.end(testResBody); + })); + + server.listen(0); + + server.on('listening', common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + debug('Client sending request...'); + + req.on('headers', common.mustNotCall()); + + process.on('uncaughtException', (err) => { + debug(`Caught an exception: ${JSON.stringify(err)}`); + if (err.name === 'AssertionError') throw err; + assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE'); + process.exit(0); + }); + })); +} diff --git a/test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-value.js b/test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-value.js new file mode 100644 index 0000000000..d640f13fae --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-value.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const assert = require('node:assert'); +const http2 = require('node:http2'); +const debug = require('node:util').debuglog('test'); + +const testResBody = 'response content'; + +{ + // Invalid link header value + + const server = http2.createServer(); + + server.on('request', common.mustCall((req, res) => { + debug('Server sending early hints...'); + res.writeEarlyHints({ link: BigInt(100) }); + + debug('Server sending full response...'); + res.end(testResBody); + })); + + server.listen(0); + + server.on('listening', common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + debug('Client sending request...'); + + req.on('headers', common.mustNotCall()); + + process.on('uncaughtException', (err) => { + debug(`Caught an exception: ${JSON.stringify(err)}`); + if (err.name === 'AssertionError') throw err; + assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE'); + process.exit(0); + }); + })); +} diff --git a/test/js/node/test/parallel/test-http2-compat-write-head-destroyed.js b/test/js/node/test/parallel/test-http2-compat-write-head-destroyed.js new file mode 100644 index 0000000000..842bf0e9ab --- /dev/null +++ b/test/js/node/test/parallel/test-http2-compat-write-head-destroyed.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); + +// Check that writeHead, write and end do not crash in compatibility mode + +const server = http2.createServer(common.mustCall((req, res) => { + // Destroy the stream first + req.stream.destroy(); + + res.writeHead(200); + res.write('hello '); + res.end('world'); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request(); + req.on('response', common.mustNotCall()); + req.on('close', common.mustCall((arg) => { + client.close(); + server.close(); + })); + req.resume(); +})); diff --git a/test/js/node/test/parallel/test-http2-connect-tls-with-delay.js b/test/js/node/test/parallel/test-http2-connect-tls-with-delay.js new file mode 100644 index 0000000000..0b3753ae38 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-connect-tls-with-delay.js @@ -0,0 +1,50 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const serverOptions = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = http2.createSecureServer(serverOptions, (req, res) => { + res.end(); +}); + +server.listen(0, '127.0.0.1', common.mustCall(() => { + const options = { + ALPNProtocols: ['h2'], + host: '127.0.0.1', + servername: 'localhost', + port: server.address().port, + rejectUnauthorized: false + }; + + const socket = tls.connect(options, async () => { + socket.once('readable', () => { + const client = http2.connect( + 'https://localhost:' + server.address().port, + { ...options, createConnection: () => socket } + ); + + client.once('remoteSettings', common.mustCall(() => { + const req = client.request({ + ':path': '/' + }); + req.on('data', () => req.resume()); + req.on('end', common.mustCall(() => { + client.close(); + req.close(); + server.close(); + })); + req.end(); + })); + }); + }); +})); diff --git a/test/js/node/test/parallel/test-http2-cookies.js b/test/js/node/test/parallel/test-http2-cookies.js new file mode 100644 index 0000000000..a270c1d73b --- /dev/null +++ b/test/js/node/test/parallel/test-http2-cookies.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const setCookie = [ + 'a=b', + 'c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly', + 'e=f', +]; + +// We use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + + assert.strictEqual(typeof headers.abc, 'string'); + assert.strictEqual(headers.abc, '1, 2, 3'); + assert.strictEqual(typeof headers.cookie, 'string'); + assert.strictEqual(headers.cookie, 'a=b; c=d; e=f'); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200, + 'set-cookie': setCookie + }); + + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + ':path': '/', + 'abc': [1, 2, 3], + 'cookie': ['a=b', 'c=d', 'e=f'], + }); + req.resume(); + + req.on('response', common.mustCall((headers) => { + assert(Array.isArray(headers['set-cookie'])); + assert.deepStrictEqual(headers['set-cookie'], setCookie); + })); + + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); + req.end(); + +})); diff --git a/test/js/node/test/parallel/test-http2-date-header.js b/test/js/node/test/parallel/test-http2-date-header.js new file mode 100644 index 0000000000..2b63e1b789 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-date-header.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + // Date header is defaulted + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall((headers) => { + // The date header must be set to a non-invalid value + assert.notStrictEqual((new Date()).toString(), 'Invalid Date'); + })); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-dont-override.js b/test/js/node/test/parallel/test-http2-dont-override.js new file mode 100644 index 0000000000..3f87e14be1 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-dont-override.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const options = {}; + +const server = http2.createServer(options); + +// Options are defaulted but the options are not modified +assert.deepStrictEqual(Object.keys(options), []); + +server.on('stream', common.mustCall((stream) => { + const headers = {}; + const options = {}; + stream.respond(headers, options); + + // The headers are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(headers), []); + + // Options are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(options), []); + + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const headers = {}; + const options = {}; + + const req = client.request(headers, options); + + // The headers are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(headers), []); + + // Options are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(options), []); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-large-write-close.js b/test/js/node/test/parallel/test-http2-large-write-close.js new file mode 100644 index 0000000000..3761ebe305 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-large-write-close.js @@ -0,0 +1,46 @@ +'use strict'; +const isCI = process.env.CI !== undefined; +const common = require('../common'); +if (common.isWindows && isCI) return; // TODO: BUN +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const http2 = require('http2'); + +const content = Buffer.alloc(1e5, 0x44); + +const server = http2.createSecureServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}); +server.on('stream', common.mustCall((stream) => { + stream.respond({ + 'Content-Type': 'application/octet-stream', + 'Content-Length': (content.length.toString() * 2), + 'Vary': 'Accept-Encoding' + }); + + stream.write(content); + stream.write(content); + stream.end(); + stream.close(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`https://localhost:${server.address().port}`, + { rejectUnauthorized: false }); + + const req = client.request({ ':path': '/' }); + req.end(); + + let receivedBufferLength = 0; + req.on('data', common.mustCallAtLeast((buf) => { + receivedBufferLength += buf.length; + }, 1)); + req.on('close', common.mustCall(() => { + assert.strictEqual(receivedBufferLength, content.length * 2); + client.close(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-large-write-destroy.js b/test/js/node/test/parallel/test-http2-large-write-destroy.js new file mode 100644 index 0000000000..b59c66bb04 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-large-write-destroy.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const http2 = require('http2'); + +// This test will result in a crash due to a missed CHECK in C++ or +// a straight-up segfault if the C++ doesn't send RST_STREAM through +// properly when calling destroy. + +const content = Buffer.alloc(60000, 0x44); + +const server = http2.createSecureServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}); +server.on('stream', common.mustCall((stream) => { + stream.respond({ + 'Content-Type': 'application/octet-stream', + 'Content-Length': (content.length.toString() * 2), + 'Vary': 'Accept-Encoding' + }, { waitForTrailers: true }); + + stream.write(content); + stream.destroy(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`https://localhost:${server.address().port}`, + { rejectUnauthorized: false }); + + const req = client.request({ ':path': '/' }); + req.end(); + req.resume(); // Otherwise close won't be emitted if there's pending data. + + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-large-writes-session-memory-leak.js b/test/js/node/test/parallel/test-http2-large-writes-session-memory-leak.js new file mode 100644 index 0000000000..f59065607e --- /dev/null +++ b/test/js/node/test/parallel/test-http2-large-writes-session-memory-leak.js @@ -0,0 +1,57 @@ +'use strict'; +const isCI = process.env.CI !== undefined; +const common = require('../common'); +if (common.isWindows && isCI) return; // TODO: BUN +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const http2 = require('http2'); + +// Regression test for https://github.com/nodejs/node/issues/29223. +// There was a "leak" in the accounting of session memory leading +// to streams eventually failing with NGHTTP2_ENHANCE_YOUR_CALM. + +const server = http2.createSecureServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), +}); + +// Simple server that sends 200k and closes the stream. +const data200k = 'a'.repeat(200 * 1024); +server.on('stream', (stream) => { + stream.write(data200k); + stream.end(); +}); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`https://localhost:${server.address().port}`, { + ca: fixtures.readKey('agent2-cert.pem'), + servername: 'agent2', + + // Set maxSessionMemory to 1MB so the leak causes errors faster. + maxSessionMemory: 1 + }); + + // Repeatedly create a new stream and read the incoming data. Even though we + // only have one stream active at a time, prior to the fix for #29223, + // session memory would steadily increase and we'd eventually hit the 1MB + // maxSessionMemory limit and get NGHTTP2_ENHANCE_YOUR_CALM errors trying to + // create new streams. + let streamsLeft = 50; + function newStream() { + const stream = client.request({ ':path': '/' }); + + stream.on('data', () => { }); + + stream.on('close', () => { + if (streamsLeft-- > 0) { + newStream(); + } else { + client.destroy(); + server.close(); + } + }); + } + + newStream(); +})); diff --git a/test/js/node/test/parallel/test-http2-malformed-altsvc.js b/test/js/node/test/parallel/test-http2-malformed-altsvc.js new file mode 100644 index 0000000000..28c0fb46b4 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-malformed-altsvc.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const net = require('net'); +const h2test = require('../common/http2'); + +const server = http2.createServer(); +server.on('stream', common.mustNotCall()); + +const settings = new h2test.SettingsFrame(); +const settingsAck = new h2test.SettingsFrame(true); +const altsvc = new h2test.AltSvcFrame((1 << 14) + 1); + +server.listen(0, () => { + const client = net.connect(server.address().port, () => { + client.write(h2test.kClientMagic, () => { + client.write(settings.data, () => { + client.write(settingsAck.data); + // Prior to nghttp2 1.31.1, sending this malformed altsvc frame + // would cause a segfault. This test is successful if a segfault + // does not occur. + client.write(altsvc.data, common.mustCall(() => { + client.destroy(); + })); + }); + }); + }); + + // An error may or may not be emitted on the client side, we don't care + // either way if it is, but we don't want to die if it is. + client.on('error', () => {}); + client.on('close', common.mustCall(() => server.close())); + client.resume(); +}); diff --git a/test/js/node/test/parallel/test-http2-many-writes-and-destroy.js b/test/js/node/test/parallel/test-http2-many-writes-and-destroy.js new file mode 100644 index 0000000000..78db76e001 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-many-writes-and-destroy.js @@ -0,0 +1,30 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); + +{ + const server = http2.createServer((req, res) => { + req.pipe(res); + }); + + server.listen(0, () => { + const url = `http://localhost:${server.address().port}`; + const client = http2.connect(url); + const req = client.request({ ':method': 'POST' }); + + for (let i = 0; i < 4000; i++) { + req.write(Buffer.alloc(6)); + } + + req.on('close', common.mustCall(() => { + console.log('(req onclose)'); + server.close(); + client.close(); + })); + + req.once('data', common.mustCall(() => req.destroy())); + }); +} diff --git a/test/js/node/test/parallel/test-http2-max-session-memory-leak.js b/test/js/node/test/parallel/test-http2-max-session-memory-leak.js new file mode 100644 index 0000000000..476c605783 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-max-session-memory-leak.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); + +// Regression test for https://github.com/nodejs/node/issues/27416. +// Check that received data is accounted for correctly in the maxSessionMemory +// mechanism. + +const bodyLength = 8192; +const maxSessionMemory = 1; // 1 MiB +const requestCount = 1000; + +const server = http2.createServer({ maxSessionMemory }); +server.on('stream', (stream) => { + stream.respond(); + stream.end(); +}); + +server.listen(common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`, { + maxSessionMemory + }); + + function request() { + return new Promise((resolve, reject) => { + const stream = client.request({ + ':method': 'POST', + 'content-length': bodyLength + }); + stream.on('error', reject); + stream.on('response', resolve); + stream.end('a'.repeat(bodyLength)); + }); + } + + (async () => { + for (let i = 0; i < requestCount; i++) { + await request(); + } + + client.close(); + server.close(); + })().then(common.mustCall()); +})); diff --git a/test/js/node/test/parallel/test-http2-methods.js b/test/js/node/test/parallel/test-http2-methods.js new file mode 100644 index 0000000000..936a264e99 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-methods.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const methods = ['GET', 'POST', 'PATCH', 'FOO', 'A_B_C']; +let expected = methods.length; + +// We use the lower-level API here +server.on('stream', common.mustCall(onStream, expected)); + +function onStream(stream, headers, flags) { + const method = headers[':method']; + assert.notStrictEqual(method, undefined); + assert(methods.includes(method), `method ${method} not included`); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const headers = { ':path': '/' }; + + methods.forEach((method) => { + headers[':method'] = method; + const req = client.request(headers); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + if (--expected === 0) { + server.close(); + client.close(); + } + })); + req.end(); + }); +})); diff --git a/test/js/node/test/parallel/test-http2-multiplex.js b/test/js/node/test/parallel/test-http2-multiplex.js new file mode 100644 index 0000000000..4c157d0ede --- /dev/null +++ b/test/js/node/test/parallel/test-http2-multiplex.js @@ -0,0 +1,58 @@ +'use strict'; + +// Tests opening 100 concurrent simultaneous uploading streams over a single +// connection and makes sure that the data for each is appropriately echoed. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const Countdown = require('../common/countdown'); + +const server = http2.createServer(); + +const count = 100; + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.pipe(stream); +}, count)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + client.setMaxListeners(100); + + const countdown = new Countdown(count, () => { + server.close(); + client.close(); + }); + + function doRequest() { + const req = client.request({ ':method': 'POST' }); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => data += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'abcdefghij'); + })); + req.on('close', common.mustCall(() => countdown.dec())); + + let n = 0; + function writeChunk() { + if (n < 10) { + req.write(String.fromCharCode(97 + n)); + setTimeout(writeChunk, 10); + } else { + req.end(); + } + n++; + } + + writeChunk(); + } + + for (let n = 0; n < count; n++) + doRequest(); +})); diff --git a/test/js/node/test/parallel/test-http2-multistream-destroy-on-read-tls.js b/test/js/node/test/parallel/test-http2-multistream-destroy-on-read-tls.js new file mode 100644 index 0000000000..91cbec6b2d --- /dev/null +++ b/test/js/node/test/parallel/test-http2-multistream-destroy-on-read-tls.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const http2 = require('http2'); + +// Regression test for https://github.com/nodejs/node/issues/29353. +// Test that it’s okay for an HTTP2 + TLS server to destroy a stream instance +// while reading it. + +const server = http2.createSecureServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem') +}); + +const filenames = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; + +server.on('stream', common.mustCall((stream) => { + function write() { + stream.write('a'.repeat(10240)); + stream.once('drain', write); + } + write(); +}, filenames.length)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`https://localhost:${server.address().port}`, { + ca: fixtures.readKey('agent2-cert.pem'), + servername: 'agent2' + }); + + let destroyed = 0; + for (const entry of filenames) { + const stream = client.request({ + ':path': `/${entry}` + }); + stream.once('data', common.mustCall(() => { + stream.destroy(); + + if (++destroyed === filenames.length) { + client.destroy(); + server.close(); + } + })); + } +})); diff --git a/test/js/node/test/parallel/test-http2-no-wanttrailers-listener.js b/test/js/node/test/parallel/test-http2-no-wanttrailers-listener.js new file mode 100644 index 0000000000..09293f2584 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-no-wanttrailers-listener.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// We use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond(undefined, { waitForTrailers: true }); + // There is no wantTrailers handler so this should close naturally + // without hanging. If the test completes without timing out, then + // it passes. + stream.end('ok'); +} + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const client = h2.connect(`http://localhost:${this.address().port}`); + const req = client.request(); + req.resume(); + req.on('trailers', common.mustNotCall()); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-options-server-response.js b/test/js/node/test/parallel/test-http2-options-server-response.js new file mode 100644 index 0000000000..6f1ae1881d --- /dev/null +++ b/test/js/node/test/parallel/test-http2-options-server-response.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); + +class MyServerResponse extends h2.Http2ServerResponse { + status(code) { + return this.writeHead(code, { 'Content-Type': 'text/plain' }); + } +} + +const server = h2.createServer({ + Http2ServerResponse: MyServerResponse +}, (req, res) => { + res.status(200); + res.end(); +}); +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall()); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-pipe-named-pipe.js b/test/js/node/test/parallel/test-http2-pipe-named-pipe.js new file mode 100644 index 0000000000..86e8dbd2f7 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-pipe-named-pipe.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +if (common.isWindows) return; // TODO: BUN +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const net = require('net'); + +// HTTP/2 servers can listen on a named pipe. + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const loc = fixtures.path('person-large.jpg'); +const fn = tmpdir.resolve('person-large.jpg'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + const dest = stream.pipe(fs.createWriteStream(fn)); + + stream.on('end', common.mustCall(() => { + stream.respond(); + stream.end(); + })); + + dest.on('finish', common.mustCall(() => { + assert.strictEqual(fs.readFileSync(fn).length, + fs.readFileSync(loc).length); + })); +})); + +server.listen(common.PIPE, common.mustCall(() => { + const client = http2.connect('http://localhost', { + createConnection(url) { + return net.connect(server.address()); + } + }); + + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + req.resume(); + + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); + + const str = fs.createReadStream(loc); + str.on('end', common.mustCall()); + str.pipe(req); +})); diff --git a/test/js/node/test/parallel/test-http2-pipe.js b/test/js/node/test/parallel/test-http2-pipe.js new file mode 100644 index 0000000000..ebd89e23d8 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-pipe.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); + +// Piping should work as expected with createWriteStream + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const loc = fixtures.path('person-large.jpg'); +const fn = tmpdir.resolve('http2-url-tests.js'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + const dest = stream.pipe(fs.createWriteStream(fn)); + + dest.on('finish', () => { + assert.strictEqual(fs.readFileSync(loc).length, + fs.readFileSync(fn).length); + }); + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':method': 'POST' }); + + req.on('response', common.mustCall()); + req.resume(); + + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); + + const str = fs.createReadStream(loc); + str.on('end', common.mustCall()); + str.pipe(req); +})); diff --git a/test/js/node/test/parallel/test-http2-removed-header-stays-removed.js b/test/js/node/test/parallel/test-http2-removed-header-stays-removed.js new file mode 100644 index 0000000000..663249749a --- /dev/null +++ b/test/js/node/test/parallel/test-http2-removed-header-stays-removed.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) { common.skip('missing crypto'); } +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(common.mustCall((request, response) => { + response.setHeader('date', 'snacks o clock'); + response.end(); +})); + +server.listen(0, common.mustCall(() => { + const session = http2.connect(`http://localhost:${server.address().port}`); + const req = session.request(); + req.on('response', (headers, flags) => { + assert.strictEqual(headers.date, 'snacks o clock'); + }); + req.on('end', () => { + session.close(); + server.close(); + }); +})); diff --git a/test/js/node/test/parallel/test-http2-request-remove-connect-listener.js b/test/js/node/test/parallel/test-http2-request-remove-connect-listener.js new file mode 100644 index 0000000000..61de140c22 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-request-remove-connect-listener.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + client.once('connect', common.mustCall()); + + req.on('response', common.mustCall(() => { + assert.strictEqual(client.listenerCount('connect'), 0); + })); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-request-response-proto.js b/test/js/node/test/parallel/test-http2-request-response-proto.js new file mode 100644 index 0000000000..49b15dfc70 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-request-response-proto.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const { + Http2ServerRequest, + Http2ServerResponse, +} = http2; + +const protoRequest = { __proto__: Http2ServerRequest.prototype }; +const protoResponse = { __proto__: Http2ServerResponse.prototype }; + +assert.strictEqual(protoRequest instanceof Http2ServerRequest, true); +assert.strictEqual(protoResponse instanceof Http2ServerResponse, true); diff --git a/test/js/node/test/parallel/test-http2-res-corked.js b/test/js/node/test/parallel/test-http2-res-corked.js new file mode 100644 index 0000000000..5a6c623edf --- /dev/null +++ b/test/js/node/test/parallel/test-http2-res-corked.js @@ -0,0 +1,54 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) { common.skip('missing crypto'); } + +// Test for Http2ServerResponse#[writableCorked,cork,uncork] + +const { strictEqual } = require('assert'); +const http2 = require('http2'); + +{ + let corksLeft = 0; + const server = http2.createServer(common.mustCall((req, res) => { + strictEqual(res.writableCorked, corksLeft); + res.write(Buffer.from('1'.repeat(1024))); + res.cork(); + corksLeft++; + strictEqual(res.writableCorked, corksLeft); + res.write(Buffer.from('1'.repeat(1024))); + res.cork(); + corksLeft++; + strictEqual(res.writableCorked, corksLeft); + res.write(Buffer.from('1'.repeat(1024))); + res.cork(); + corksLeft++; + strictEqual(res.writableCorked, corksLeft); + res.write(Buffer.from('1'.repeat(1024))); + res.cork(); + corksLeft++; + strictEqual(res.writableCorked, corksLeft); + res.uncork(); + corksLeft--; + strictEqual(res.writableCorked, corksLeft); + res.uncork(); + corksLeft--; + strictEqual(res.writableCorked, corksLeft); + res.uncork(); + corksLeft--; + strictEqual(res.writableCorked, corksLeft); + res.uncork(); + corksLeft--; + strictEqual(res.writableCorked, corksLeft); + res.end(); + })); + server.listen(0, common.mustCall(() => { + const URL = `http://localhost:${server.address().port}`; + const client = http2.connect(URL); + const req = client.request(); + req.on('data', common.mustCall(2)); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-http2-respond-file-204.js b/test/js/node/test/parallel/test-http2-respond-file-204.js new file mode 100644 index 0000000000..0c59b0e729 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-respond-file-204.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const http2 = require('http2'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_STATUS +} = http2.constants; + +const fname = fixtures.path('elipses.txt'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + assert.throws(() => { + stream.respondWithFile(fname, { + [HTTP2_HEADER_STATUS]: 204, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + }, { + code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN', + name: 'Error', + message: 'Responses with 204 status must not have a payload' + }); + stream.respond({}); + stream.end(); +}); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall()); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + client.close(); + server.close(); + })); + req.end(); +}); diff --git a/test/js/node/test/parallel/test-http2-respond-file-compat.js b/test/js/node/test/parallel/test-http2-respond-file-compat.js new file mode 100644 index 0000000000..0205f2d0d8 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-respond-file-compat.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const fixtures = require('../common/fixtures'); + +const fname = fixtures.path('elipses.txt'); + +const server = http2.createServer(common.mustCall((request, response) => { + response.stream.respondWithFile(fname); +})); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall()); + req.on('end', common.mustCall(() => { + client.close(); + server.close(); + })); + req.end(); + req.resume(); +}); diff --git a/test/js/node/test/parallel/test-http2-respond-file-error-dir.js b/test/js/node/test/parallel/test-http2-respond-file-error-dir.js new file mode 100644 index 0000000000..155e005432 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-respond-file-error-dir.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const assert = require('assert'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFile(process.cwd(), { + 'content-type': 'text/plain' + }, { + onError(err) { + common.expectsError({ + code: 'ERR_HTTP2_SEND_FILE', + name: 'Error', + message: 'Directories cannot be sent' + })(err); + + stream.respond({ ':status': 404 }); + stream.end(); + }, + statCheck: common.mustNotCall() + }); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 404); + })); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + client.close(); + server.close(); + })); + req.end(); +}); diff --git a/test/js/node/test/parallel/test-http2-respond-file-error-pipe-offset.js b/test/js/node/test/parallel/test-http2-respond-file-error-pipe-offset.js new file mode 100644 index 0000000000..bd043e42f4 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-respond-file-error-pipe-offset.js @@ -0,0 +1,65 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +if (common.isWindows) + common.skip('no mkfifo on Windows'); +const child_process = require('child_process'); +const fs = require('fs'); +const http2 = require('http2'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const pipeName = tmpdir.resolve('pipe'); + +const mkfifo = child_process.spawnSync('mkfifo', [ pipeName ]); +if (mkfifo.error && mkfifo.error.code === 'ENOENT') { + common.skip('missing mkfifo'); +} + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFile(pipeName, { + 'content-type': 'text/plain' + }, { + offset: 10, + onError(err) { + common.expectsError({ + code: 'ERR_HTTP2_SEND_FILE_NOSEEK', + name: 'Error', + message: 'Offset or length can only be specified for regular files' + })(err); + + stream.respond({ ':status': 404 }); + stream.end(); + }, + statCheck: common.mustNotCall() + }); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 404); + })); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + client.close(); + server.close(); + })); + req.end(); +}); + +fs.writeFile(pipeName, 'Hello, world!\n', common.mustCall((err) => { + // It's possible for the reading end of the pipe to get the expected error + // and break everything down before we're finished, so allow `EPIPE` but + // no other errors. + if (err?.code !== 'EPIPE') { + assert.ifError(err); + } +})); diff --git a/test/js/node/test/parallel/test-http2-respond-with-file-connection-abort.js b/test/js/node/test/parallel/test-http2-respond-with-file-connection-abort.js new file mode 100644 index 0000000000..d5ed364570 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-respond-with-file-connection-abort.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const net = require('net'); + +const { + HTTP2_HEADER_CONTENT_TYPE +} = http2.constants; + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.on('error', (err) => assert.strictEqual(err.code, 'ECONNRESET')); + stream.respondWithFile(process.execPath, { + [HTTP2_HEADER_CONTENT_TYPE]: 'application/octet-stream' + }); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall()); + req.once('data', common.mustCall(() => { + net.Socket.prototype.destroy.call(client.socket); + server.close(); + })); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http2-serve-file.js b/test/js/node/test/parallel/test-http2-serve-file.js new file mode 100644 index 0000000000..7b73fe639e --- /dev/null +++ b/test/js/node/test/parallel/test-http2-serve-file.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const tls = require('tls'); + +const ajs_data = fixtures.readSync('a.js', 'utf8'); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS +} = http2.constants; + +const key = fixtures.readKey('agent8-key.pem', 'binary'); +const cert = fixtures.readKey('agent8-cert.pem', 'binary'); +const ca = fixtures.readKey('fake-startcom-root-cert.pem', 'binary'); + +const server = http2.createSecureServer({ key, cert }); + +server.on('stream', (stream, headers) => { + const name = headers[HTTP2_HEADER_PATH].slice(1); + const file = fixtures.path(name); + fs.stat(file, (err, stat) => { + if (err != null || stat.isDirectory()) { + stream.respond({ [HTTP2_HEADER_STATUS]: 404 }); + stream.end(); + } else { + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + const str = fs.createReadStream(file); + str.pipe(stream); + } + }); +}); + +server.listen(0, () => { + + const secureContext = tls.createSecureContext({ ca }); + const client = http2.connect(`https://localhost:${server.address().port}`, + { secureContext }); + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + client.close(); + server.close(); + } + } + + // Request for a file that does exist, response is 200 + const req1 = client.request({ [HTTP2_HEADER_PATH]: '/a.js' }, + { endStream: true }); + req1.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200); + })); + let req1_data = ''; + req1.setEncoding('utf8'); + req1.on('data', (chunk) => req1_data += chunk); + req1.on('end', common.mustCall(() => { + assert.strictEqual(req1_data, ajs_data); + maybeClose(); + })); + + // Request for a file that does not exist, response is 404 + const req2 = client.request({ [HTTP2_HEADER_PATH]: '/does_not_exist' }, + { endStream: true }); + req2.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 404); + })); + req2.on('data', common.mustNotCall()); + req2.on('end', common.mustCall(() => maybeClose())); + +}); diff --git a/test/js/node/test/parallel/test-http2-server-async-dispose.js b/test/js/node/test/parallel/test-http2-server-async-dispose.js new file mode 100644 index 0000000000..4782e12e41 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-server-async-dispose.js @@ -0,0 +1,15 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); + +const server = http2.createServer(); + +server.listen(0, common.mustCall(() => { + server.on('close', common.mustCall()); + server[Symbol.asyncDispose]().then(common.mustCall()); +})); diff --git a/test/js/node/test/parallel/test-http2-server-errors.js b/test/js/node/test/parallel/test-http2-server-errors.js new file mode 100644 index 0000000000..959ddccdc7 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-server-errors.js @@ -0,0 +1,88 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Errors should not be reported both in Http2ServerRequest +// and Http2ServerResponse + +{ + let expected = null; + + const server = h2.createServer(); + + server.on('stream', common.mustCall(function(stream) { + stream.on('error', common.mustCall(function(err) { + assert.strictEqual(err, expected); + })); + + stream.resume(); + stream.write('hello'); + + expected = new Error('kaboom'); + stream.destroy(expected); + server.close(); + })); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + }; + const request = client.request(headers); + request.on('data', common.mustCall(function(chunk) { + // Cause an error on the server side + client.destroy(); + })); + request.end(); + })); + })); +} + +{ + let expected = null; + + const server = h2.createServer(); + + process.on('uncaughtException', common.mustCall(function(err) { + assert.strictEqual(err.message, 'kaboom no handler'); + })); + + server.on('stream', common.mustCall(function(stream) { + // There is no 'error' handler, and this will crash + stream.write('hello'); + stream.resume(); + + expected = new Error('kaboom no handler'); + stream.destroy(expected); + server.close(); + })); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + }; + const request = client.request(headers); + request.on('data', common.mustCall(function(chunk) { + // Cause an error on the server side + client.destroy(); + })); + request.end(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-http2-server-rst-before-respond.js b/test/js/node/test/parallel/test-http2-server-rst-before-respond.js new file mode 100644 index 0000000000..d551c7121f --- /dev/null +++ b/test/js/node/test/parallel/test-http2-server-rst-before-respond.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// We use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.close(); + + assert.throws(() => { + stream.additionalHeaders({ + ':status': 123, + 'abc': 123 + }); + }, { code: 'ERR_HTTP2_INVALID_STREAM' }); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('headers', common.mustNotCall()); + req.on('close', common.mustCall(() => { + assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, req.rstCode); + server.close(); + client.close(); + })); + req.on('response', common.mustNotCall()); +})); diff --git a/test/js/node/test/parallel/test-http2-session-timeout.js b/test/js/node/test/parallel/test-http2-session-timeout.js new file mode 100644 index 0000000000..c1dacdcb45 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-session-timeout.js @@ -0,0 +1,64 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const hrtime = process.hrtime.bigint; +const NS_PER_MS = 1_000_000n; + +let requests = 0; +const mustNotCall = () => { + assert.fail(`Timeout after ${requests} request(s)`); +}; + +const server = http2.createServer(); +// Disable server timeout until first request. We will set the timeout based on +// how long the first request takes. +server.timeout = 0n; + +server.on('request', (req, res) => res.end()); +server.on('timeout', mustNotCall); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + const url = `http://localhost:${port}`; + const client = http2.connect(url); + let startTime = hrtime(); + makeReq(); + + function makeReq() { + const request = client.request({ + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + }); + request.resume(); + request.end(); + + requests += 1; + + request.on('end', () => { + const diff = hrtime() - startTime; + const milliseconds = diff / NS_PER_MS; + if (server.timeout === 0n) { + // Set the timeout now. First connection will take significantly longer + // than subsequent connections, so using the duration of the first + // connection as the timeout should be robust. Double it anyway for good + // measure. + server.timeout = milliseconds * 2n; + startTime = hrtime(); + makeReq(); + } else if (milliseconds < server.timeout * 2n) { + makeReq(); + } else { + server.removeListener('timeout', mustNotCall); + server.close(); + client.close(); + } + }); + } +})); diff --git a/test/js/node/test/parallel/test-http2-socket-proxy-handler-for-has.js b/test/js/node/test/parallel/test-http2-socket-proxy-handler-for-has.js new file mode 100644 index 0000000000..a8dfbfe07a --- /dev/null +++ b/test/js/node/test/parallel/test-http2-socket-proxy-handler-for-has.js @@ -0,0 +1,38 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const http2 = require('http2'); + +const serverOptions = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; +const server = http2.createSecureServer(serverOptions, common.mustCall( + (req, res) => { + const request = req; + assert.strictEqual(request.socket.encrypted, true); + assert.ok('encrypted' in request.socket); + res.end(); + } +)); +server.listen(common.mustCall(() => { + const port = server.address().port; + const client = http2.connect('https://localhost:' + port, { + ca: fixtures.readKey('agent1-cert.pem'), + rejectUnauthorized: false + }); + const req = client.request({}); + req.on('response', common.mustCall((headers, flags) => { + console.log(headers); + server.close(common.mustCall()); + })); + req.on('end', common.mustCall(() => { + client.close(); + })); + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http2-status-code.js b/test/js/node/test/parallel/test-http2-status-code.js new file mode 100644 index 0000000000..d3642b4ff0 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-status-code.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const codes = [ 200, 202, 300, 400, 404, 451, 500 ]; +let test = 0; + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + const status = codes[test++]; + stream.respond({ ':status': status }, { endStream: true }); +}, 7)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = codes.length; + function maybeClose() { + if (--remaining === 0) { + client.close(); + server.close(); + } + } + + function doTest(expected) { + const req = client.request(); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], expected); + })); + req.resume(); + req.on('end', common.mustCall(maybeClose)); + } + + for (let n = 0; n < codes.length; n++) + doTest(codes[n]); +})); diff --git a/test/js/node/test/parallel/test-http2-trailers-after-session-close.js b/test/js/node/test/parallel/test-http2-trailers-after-session-close.js new file mode 100644 index 0000000000..20589115b1 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-trailers-after-session-close.js @@ -0,0 +1,52 @@ +'use strict'; + +// Fixes: https://github.com/nodejs/node/issues/42713 +const common = require('../common'); +if (common.isWindows) return; // TODO BUN +if (!common.hasCrypto) { + common.skip('missing crypto'); +} +const assert = require('assert'); +const http2 = require('http2'); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_METHOD, +} = http2.constants; + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + server.close(); + stream.session.close(); + stream.on('wantTrailers', common.mustCall(() => { + stream.sendTrailers({ xyz: 'abc' }); + })); + + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }, { waitForTrailers: true }); + stream.write('some data'); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + client.socket.on('close', common.mustCall()); + const req = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: 'POST', + }); + req.end(); + req.on('response', common.mustCall()); + let data = ''; + req.on('data', (chunk) => { + data += chunk; + }); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'some data'); + })); + req.on('trailers', common.mustCall((headers) => { + assert.strictEqual(headers.xyz, 'abc'); + })); + req.on('close', common.mustCall()); +})); diff --git a/test/js/node/test/parallel/test-http2-trailers.js b/test/js/node/test/parallel/test-http2-trailers.js new file mode 100644 index 0000000000..dba9aac12d --- /dev/null +++ b/test/js/node/test/parallel/test-http2-trailers.js @@ -0,0 +1,73 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); +const body = + '

this is some data

'; +const trailerKey = 'test-trailer'; +const trailerValue = 'testing'; + +const server = h2.createServer(); + +// We use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.on('trailers', common.mustCall((headers) => { + assert.strictEqual(headers[trailerKey], trailerValue); + stream.end(body); + })); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }, { waitForTrailers: true }); + stream.on('wantTrailers', () => { + stream.sendTrailers({ [trailerKey]: trailerValue }); + assert.throws( + () => stream.sendTrailers({}), + { + code: 'ERR_HTTP2_TRAILERS_ALREADY_SENT', + name: 'Error' + } + ); + }); + + assert.throws( + () => stream.sendTrailers({}), + { + code: 'ERR_HTTP2_TRAILERS_NOT_READY', + name: 'Error' + } + ); +} + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const client = h2.connect(`http://localhost:${this.address().port}`); + const req = client.request({ ':path': '/', ':method': 'POST' }, + { waitForTrailers: true }); + req.on('wantTrailers', () => { + req.sendTrailers({ [trailerKey]: trailerValue }); + }); + req.on('data', common.mustCall()); + req.on('trailers', common.mustCall((headers) => { + assert.strictEqual(headers[trailerKey], trailerValue); + })); + req.on('close', common.mustCall(() => { + assert.throws( + () => req.sendTrailers({}), + { + code: 'ERR_HTTP2_INVALID_STREAM', + name: 'Error' + } + ); + server.close(); + client.close(); + })); + req.end('data'); + +})); diff --git a/test/js/node/test/parallel/test-http2-unbound-socket-proxy.js b/test/js/node/test/parallel/test-http2-unbound-socket-proxy.js new file mode 100644 index 0000000000..74ca016944 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-unbound-socket-proxy.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const net = require('net'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end('ok'); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const socket = client.socket; + const req = client.request(); + req.resume(); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + + // Tests to make sure accessing the socket proxy fails with an + // informative error. + setImmediate(common.mustCall(() => { + assert.throws(() => { + socket.example; // eslint-disable-line no-unused-expressions + }, { + code: 'ERR_HTTP2_SOCKET_UNBOUND' + }); + assert.throws(() => { + socket.example = 1; + }, { + code: 'ERR_HTTP2_SOCKET_UNBOUND' + }); + assert.throws(() => { + // eslint-disable-next-line no-unused-expressions + socket instanceof net.Socket; + }, { + code: 'ERR_HTTP2_SOCKET_UNBOUND' + }); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-write-callbacks.js b/test/js/node/test/parallel/test-http2-write-callbacks.js new file mode 100644 index 0000000000..eca7f00ea7 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-write-callbacks.js @@ -0,0 +1,37 @@ +'use strict'; + +// Verifies that write callbacks are called + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + stream.write('abc', common.mustCall(() => { + stream.end('xyz'); + })); + let actual = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => actual += chunk); + stream.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':method': 'POST' }); + req.write('abc', common.mustCall(() => { + req.end('xyz'); + })); + let actual = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-write-empty-string.js b/test/js/node/test/parallel/test-http2-write-empty-string.js new file mode 100644 index 0000000000..ea591176a4 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-write-empty-string.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(function(request, response) { + response.writeHead(200, { 'Content-Type': 'text/plain' }); + response.write('1\n'); + response.write(''); + response.write('2\n'); + response.write(''); + response.end('3\n'); + + this.close(); +}); + +server.listen(0, common.mustCall(function() { + const client = http2.connect(`http://localhost:${this.address().port}`); + const headers = { ':path': '/' }; + const req = client.request(headers).setEncoding('ascii'); + + let res = ''; + + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200); + })); + + req.on('data', (chunk) => { + res += chunk; + }); + + req.on('end', common.mustCall(function() { + assert.strictEqual(res, '1\n2\n3\n'); + client.close(); + })); + + req.end(); +})); diff --git a/test/js/node/test/parallel/test-http2-zero-length-header.js b/test/js/node/test/parallel/test-http2-zero-length-header.js new file mode 100644 index 0000000000..2e7876858a --- /dev/null +++ b/test/js/node/test/parallel/test-http2-zero-length-header.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +server.on('stream', (stream, headers) => { + assert.deepStrictEqual(headers, { + ':scheme': 'http', + ':authority': `localhost:${server.address().port}`, + ':method': 'GET', + ':path': '/', + 'bar': '', + '__proto__': null, + [http2.sensitiveHeaders]: [] + }); + stream.session.destroy(); + server.close(); +}); +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}/`); + client.request({ ':path': '/', '': 'foo', 'bar': '' }).end(); +})); diff --git a/test/js/node/test/parallel/test-http2-zero-length-write.js b/test/js/node/test/parallel/test-http2-zero-length-write.js new file mode 100644 index 0000000000..0b50715330 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-zero-length-write.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const { Readable } = require('stream'); + +function getSrc() { + const chunks = [ '', 'asdf', '', 'foo', '', 'bar', '' ]; + return new Readable({ + read() { + const chunk = chunks.shift(); + if (chunk !== undefined) + this.push(chunk); + else + this.push(null); + } + }); +} + +const expect = 'asdffoobar'; + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + let actual = ''; + stream.respond(); + stream.resume(); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => actual += chunk); + stream.on('end', common.mustCall(() => { + getSrc().pipe(stream); + assert.strictEqual(actual, expect); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + let actual = ''; + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + req.setEncoding('utf8'); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(actual, expect); + server.close(); + client.close(); + })); + getSrc().pipe(req); +})); diff --git a/test/js/node/test/parallel/test-https-agent-constructor.js b/test/js/node/test/parallel/test-https-agent-constructor.js new file mode 100644 index 0000000000..69156ba0f6 --- /dev/null +++ b/test/js/node/test/parallel/test-https-agent-constructor.js @@ -0,0 +1,9 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const https = require('https'); + +assert.ok(https.Agent() instanceof https.Agent); diff --git a/test/js/node/test/parallel/test-https-agent-session-eviction.js b/test/js/node/test/parallel/test-https-agent-session-eviction.js new file mode 100644 index 0000000000..da56007105 --- /dev/null +++ b/test/js/node/test/parallel/test-https-agent-session-eviction.js @@ -0,0 +1,73 @@ +// Flags: --tls-min-v1.0 +'use strict'; + +const common = require('../common'); +const { readKey } = require('../common/fixtures'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const https = require('https'); +const { SSL_OP_NO_TICKET } = require('crypto').constants; + +const options = { + key: readKey('agent1-key.pem'), + cert: readKey('agent1-cert.pem'), + secureOptions: SSL_OP_NO_TICKET, + ciphers: 'RSA@SECLEVEL=0' +}; + +// Create TLS1.2 server +https.createServer(options, function(req, res) { + res.writeHead(200, { 'Connection': 'close' }); + res.end('ohai'); +}).listen(0, function() { + first(this); +}); + +// Do request and let agent cache the session +function first(server) { + const port = server.address().port; + const req = https.request({ + port: port, + rejectUnauthorized: false + }, function(res) { + res.resume(); + + server.close(function() { + faultyServer(port); + }); + }); + req.end(); +} + +// Create TLS1 server +function faultyServer(port) { + options.secureProtocol = 'TLSv1_method'; + https.createServer(options, function(req, res) { + res.writeHead(200, { 'Connection': 'close' }); + res.end('hello faulty'); + }).listen(port, function() { + second(this); + }); +} + +// Attempt to request using cached session +function second(server, session) { + const req = https.request({ + port: server.address().port, + ciphers: (common.hasOpenSSL31 ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + rejectUnauthorized: false + }, function(res) { + res.resume(); + }); + + // Although we have a TLS 1.2 session to offer to the TLS 1.0 server, + // connection to the TLS 1.0 server should work. + req.on('response', common.mustCall(function(res) { + // The test is now complete for OpenSSL 1.1.0. + server.close(); + })); + + req.end(); +} diff --git a/test/js/node/test/parallel/test-https-agent.js b/test/js/node/test/parallel/test-https-agent.js new file mode 100644 index 0000000000..ce4bc6e5bd --- /dev/null +++ b/test/js/node/test/parallel/test-https-agent.js @@ -0,0 +1,72 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const https = require('https'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + + +const server = https.Server(options, (req, res) => { + res.writeHead(200); + res.end('hello world\n'); +}); + + +let responses = 0; +const N = 4; +const M = 4; + + +server.listen(0, () => { + for (let i = 0; i < N; i++) { + setTimeout(() => { + for (let j = 0; j < M; j++) { + https.get({ + path: '/', + port: server.address().port, + rejectUnauthorized: false + }, function(res) { + res.resume(); + assert.strictEqual(res.statusCode, 200); + if (++responses === N * M) server.close(); + }).on('error', (e) => { + throw e; + }); + } + }, i); + } +}); + + +process.on('exit', () => { + assert.strictEqual(responses, N * M); +}); diff --git a/test/js/node/test/parallel/test-https-client-get-url.js b/test/js/node/test/parallel/test-https-client-get-url.js new file mode 100644 index 0000000000..fb91a4f1e7 --- /dev/null +++ b/test/js/node/test/parallel/test-https-client-get-url.js @@ -0,0 +1,57 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// Disable strict server certificate validation by the client +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +const assert = require('assert'); +const https = require('https'); +const url = require('url'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = https.createServer(options, common.mustCall((req, res) => { + assert.strictEqual(req.method, 'GET'); + assert.strictEqual(req.url, '/foo?bar'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('hello\n'); + res.end(); +}, 3)); + +server.listen(0, common.mustCall(() => { + const u = `https://${common.localhostIPv4}:${server.address().port}/foo?bar`; + https.get(u, common.mustCall(() => { + https.get(url.parse(u), common.mustCall(() => { + https.get(new URL(u), common.mustCall(() => { + server.close(); + })); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-https-client-renegotiation-limit.js b/test/js/node/test/parallel/test-https-client-renegotiation-limit.js new file mode 100644 index 0000000000..35fcc6bfcc --- /dev/null +++ b/test/js/node/test/parallel/test-https-client-renegotiation-limit.js @@ -0,0 +1,111 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + +const assert = require('assert'); +const tls = require('tls'); +const https = require('https'); +const fixtures = require('../common/fixtures'); + +// Renegotiation as a protocol feature was dropped after TLS1.2. +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +// Renegotiation limits to test +const LIMITS = [0, 1, 2, 3, 5, 10, 16]; + +{ + let n = 0; + function next() { + if (n >= LIMITS.length) return; + tls.CLIENT_RENEG_LIMIT = LIMITS[n++]; + test(next); + } + next(); +} + +function test(next) { + const options = { + cert: fixtures.readKey('rsa_cert.crt'), + key: fixtures.readKey('rsa_private.pem'), + }; + + const server = https.createServer(options, (req, res) => { + const conn = req.connection; + conn.on('error', (err) => { + console.error(`Caught exception: ${err}`); + assert.match(err.message, /TLS session renegotiation attack/); + conn.destroy(); + }); + res.end('ok'); + }); + + server.listen(0, () => { + const agent = https.Agent({ + keepAlive: true, + }); + + let client; + let renegs = 0; + + const options = { + rejectUnauthorized: false, + agent, + }; + + const { port } = server.address(); + + https.get(`https://localhost:${port}/`, options, (res) => { + client = res.socket; + + client.on('close', (hadErr) => { + assert.strictEqual(hadErr, false); + assert.strictEqual(renegs, tls.CLIENT_RENEG_LIMIT + 1); + server.close(); + process.nextTick(next); + }); + + client.on('error', (err) => { + console.log('CLIENT ERR', err); + throw err; + }); + + spam(); + + // Simulate renegotiation attack + function spam() { + client.renegotiate({}, (err) => { + assert.ifError(err); + assert.ok(renegs <= tls.CLIENT_RENEG_LIMIT); + setImmediate(spam); + }); + renegs++; + } + }); + + }); +} diff --git a/test/js/node/test/parallel/test-https-connecting-to-http.js b/test/js/node/test/parallel/test-https-connecting-to-http.js new file mode 100644 index 0000000000..195ad38ed4 --- /dev/null +++ b/test/js/node/test/parallel/test-https-connecting-to-http.js @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// This tests the situation where you try to connect a https client +// to an http server. You should get an error and exit. +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http = require('http'); +const https = require('https'); +const server = http.createServer(common.mustNotCall()); + +server.listen(0, common.mustCall(function() { + const req = https.get({ port: this.address().port }, common.mustNotCall()); + + req.on('error', common.mustCall(function(e) { + console.log('Got expected error: ', e.message); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-https-foafssl.js b/test/js/node/test/parallel/test-https-foafssl.js new file mode 100644 index 0000000000..d6dde97a41 --- /dev/null +++ b/test/js/node/test/parallel/test-https-foafssl.js @@ -0,0 +1,89 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const https = require('https'); +const spawn = require('child_process').spawn; + +const options = { + key: fixtures.readKey('rsa_private.pem'), + cert: fixtures.readKey('rsa_cert.crt'), + requestCert: true, + rejectUnauthorized: false +}; + +const webIdUrl = 'URI:http://example.com/#me'; +const modulus = fixtures.readKey('rsa_cert_foafssl_b.modulus', 'ascii').replace(/\n/g, ''); +const exponent = fixtures.readKey('rsa_cert_foafssl_b.exponent', 'ascii').replace(/\n/g, ''); + +const CRLF = '\r\n'; +const body = 'hello world\n'; +let cert; + +const server = https.createServer(options, common.mustCall(function(req, res) { + console.log('got request'); + + cert = req.connection.getPeerCertificate(); + + assert.strictEqual(cert.subjectaltname, webIdUrl); + assert.strictEqual(cert.exponent, exponent); + assert.strictEqual(cert.modulus, modulus); + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end(body, () => { console.log('stream finished'); }); + console.log('sent response'); +})); + +server.listen(0, function() { + const args = ['s_client', + '-quiet', + '-connect', `127.0.0.1:${this.address().port}`, + '-cert', fixtures.path('keys/rsa_cert_foafssl_b.crt'), + '-key', fixtures.path('keys/rsa_private_b.pem')]; + + const client = spawn(common.opensslCli, args); + + client.stdout.on('data', function(data) { + console.log('response received'); + const message = data.toString(); + const contents = message.split(CRLF + CRLF).pop(); + assert.strictEqual(body, contents); + server.close((e) => { + assert.ifError(e); + console.log('server closed'); + }); + console.log('server.close() called'); + }); + + client.stdin.write('GET /\r\n\r\n'); + + client.on('error', function(error) { + throw error; + }); +}); diff --git a/test/js/node/test/parallel/test-https-localaddress-bind-error.js b/test/js/node/test/parallel/test-https-localaddress-bind-error.js new file mode 100644 index 0000000000..57e4dd054d --- /dev/null +++ b/test/js/node/test/parallel/test-https-localaddress-bind-error.js @@ -0,0 +1,62 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const https = require('https'); + +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const invalidLocalAddress = '1.2.3.4'; + +const server = https.createServer(options, function(req, res) { + console.log(`Connect from: ${req.connection.remoteAddress}`); + + req.on('end', function() { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(`You are from: ${req.connection.remoteAddress}`); + }); + req.resume(); +}); + +server.listen(0, '127.0.0.1', common.mustCall(function() { + https.request({ + host: 'localhost', + port: this.address().port, + path: '/', + method: 'GET', + localAddress: invalidLocalAddress + }, function(res) { + assert.fail('unexpectedly got response from server'); + }).on('error', common.mustCall(function(e) { + console.log(`client got error: ${e.message}`); + server.close(); + })).end(); +})); diff --git a/test/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js b/test/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js new file mode 100644 index 0000000000..2dd46ac878 --- /dev/null +++ b/test/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js @@ -0,0 +1,63 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +// This test starts an https server and tries +// to connect to it using a self-signed certificate. +// This certificate´s keyUsage does not include the keyCertSign +// bit, which used to crash node. The test ensures node +// will not crash. Key and certificate are from #37889. +// Note: This test assumes that the connection will succeed. + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const crypto = require('crypto'); + +// See #37990 for details on why this is problematic with FIPS. +if (process.config.variables.openssl_is_fips) + common.skip('Skipping as test uses non-fips compliant EC curve'); + +// This test will fail for OpenSSL < 1.1.1h +const minOpenSSL = 269488271; + +if (crypto.constants.OPENSSL_VERSION_NUMBER < minOpenSSL) + common.skip('OpenSSL < 1.1.1h'); + +const https = require('https'); +const path = require('path'); + +const key = + fixtures.readKey(path.join('selfsigned-no-keycertsign', 'key.pem')); + +const cert = + fixtures.readKey(path.join('selfsigned-no-keycertsign', 'cert.pem')); + +const serverOptions = { + key: key, + cert: cert +}; + +// Start the server +const httpsServer = https.createServer(serverOptions, (req, res) => { + res.writeHead(200); + res.end('hello world\n'); +}); +httpsServer.listen(0); + +httpsServer.on('listening', () => { + // Once the server started listening, built the client config + // with the server´s used port + const clientOptions = { + hostname: '127.0.0.1', + port: httpsServer.address().port, + ca: cert + }; + // Try to connect + const req = https.request(clientOptions, common.mustCall((res) => { + httpsServer.close(); + })); + + req.on('error', common.mustNotCall()); + req.end(); +}); diff --git a/test/js/node/test/parallel/test-https-socket-options.js b/test/js/node/test/parallel/test-https-socket-options.js new file mode 100644 index 0000000000..b41054d5aa --- /dev/null +++ b/test/js/node/test/parallel/test-https-socket-options.js @@ -0,0 +1,85 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const fixtures = require('../common/fixtures'); +const https = require('https'); +const http = require('http'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const body = 'hello world\n'; + +// Try first with http server + +const server_http = http.createServer(function(req, res) { + console.log('got HTTP request'); + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end(body); +}); + + +server_http.listen(0, function() { + const req = http.request({ + port: this.address().port, + rejectUnauthorized: false + }, function(res) { + server_http.close(); + res.resume(); + }); + // These methods should exist on the request and get passed down to the socket + req.setNoDelay(true); + req.setTimeout(1000, () => {}); + req.setSocketKeepAlive(true, 1000); + req.end(); +}); + +// Then try https server (requires functions to be +// mirrored in tls.js's CryptoStream) + +const server_https = https.createServer(options, function(req, res) { + console.log('got HTTPS request'); + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end(body); +}); + +server_https.listen(0, function() { + const req = https.request({ + port: this.address().port, + rejectUnauthorized: false + }, function(res) { + server_https.close(); + res.resume(); + }); + // These methods should exist on the request and get passed down to the socket + req.setNoDelay(true); + req.setTimeout(1000, () => {}); + req.setSocketKeepAlive(true, 1000); + req.end(); +}); diff --git a/test/js/node/test/parallel/test-https-truncate.js b/test/js/node/test/parallel/test-https-truncate.js new file mode 100644 index 0000000000..beed36cd7c --- /dev/null +++ b/test/js/node/test/parallel/test-https-truncate.js @@ -0,0 +1,72 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const https = require('https'); + +const key = fixtures.readKey('agent1-key.pem'); +const cert = fixtures.readKey('agent1-cert.pem'); + +// Number of bytes discovered empirically to trigger the bug +const data = Buffer.alloc(1024 * 32 + 1); + +httpsTest(); + +function httpsTest() { + const sopt = { key, cert }; + + const server = https.createServer(sopt, function(req, res) { + res.setHeader('content-length', data.length); + res.end(data); + server.close(); + }); + + server.listen(0, function() { + const opts = { port: this.address().port, rejectUnauthorized: false }; + https.get(opts).on('response', function(res) { + test(res); + }); + }); +} + + +const test = common.mustCall(function(res) { + res.on('end', common.mustCall(function() { + assert.strictEqual(res.readableLength, 0); + assert.strictEqual(bytes, data.length); + })); + + // Pause and then resume on each chunk, to ensure that there will be + // a lone byte hanging out at the very end. + let bytes = 0; + res.on('data', function(chunk) { + bytes += chunk.length; + this.pause(); + setTimeout(() => { this.resume(); }, 1); + }); +}); diff --git a/test/js/node/test/parallel/test-https-unix-socket-self-signed.js b/test/js/node/test/parallel/test-https-unix-socket-self-signed.js new file mode 100644 index 0000000000..9de249524a --- /dev/null +++ b/test/js/node/test/parallel/test-https-unix-socket-self-signed.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const fixtures = require('../common/fixtures'); +const https = require('https'); +const options = { + cert: fixtures.readKey('rsa_cert.crt'), + key: fixtures.readKey('rsa_private.pem') +}; + +const server = https.createServer(options, common.mustCall((req, res) => { + res.end('bye\n'); + server.close(); +})); + +server.listen(common.PIPE, common.mustCall(() => { + https.get({ + socketPath: common.PIPE, + rejectUnauthorized: false + }); +})); diff --git a/test/js/node/test/parallel/test-icu-env.js b/test/js/node/test/parallel/test-icu-env.js new file mode 100644 index 0000000000..45b9fea8db --- /dev/null +++ b/test/js/node/test/parallel/test-icu-env.js @@ -0,0 +1,288 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { execFileSync } = require('child_process'); + +// system-icu should not be tested +const hasBuiltinICU = process.config.variables.icu_gyp_path === 'tools/icu/icu-generic.gyp'; +if (!hasBuiltinICU) + common.skip('system ICU'); + +// small-icu doesn't support non-English locales +const hasFullICU = (() => { + try { + const january = new Date(9e8); + const spanish = new Intl.DateTimeFormat('es', { month: 'long' }); + return spanish.format(january) === 'enero'; + } catch { + return false; + } +})(); +if (!hasFullICU) + common.skip('small ICU'); + +const icuVersionMajor = Number(process.config.variables.icu_ver_major ?? 0); +if (icuVersionMajor < 71) + common.skip('ICU too old'); + + +function runEnvOutside(addEnv, code, ...args) { + return execFileSync( + process.execPath, + ['-e', `process.stdout.write(String(${code}));`], + { env: { ...process.env, ...addEnv }, encoding: 'utf8' } + ); +} + +function runEnvInside(addEnv, func, ...args) { + Object.assign(process.env, addEnv); // side effects! + return func(...args); +} + +function isPack(array) { + const firstItem = array[0]; + return array.every((item) => item === firstItem); +} + +function isSet(array) { + const deduped = new Set(array); + return array.length === deduped.size; +} + + +const localesISO639 = [ + 'eng', 'cmn', 'hin', 'spa', + 'fra', 'arb', 'ben', 'rus', + 'por', 'urd', 'ind', 'deu', + 'jpn', 'pcm', 'mar', 'tel', +]; + +const locales = [ + 'en', 'zh', 'hi', 'es', + 'fr', 'ar', 'bn', 'ru', + 'pt', 'ur', 'id', 'de', + 'ja', 'pcm', 'mr', 'te', +]; + +// These must not overlap +const zones = [ + 'America/New_York', + 'UTC', + 'Asia/Irkutsk', + 'Australia/North', + 'Antarctica/South_Pole', +]; + + +assert.deepStrictEqual(Intl.getCanonicalLocales(localesISO639), locales); + + +// On some platforms these keep original locale (for example, 'January') +const enero = runEnvOutside( + { LANG: 'es' }, + 'new Intl.DateTimeFormat(undefined, { month: "long" } ).format(new Date(9e8))' +); +const janvier = runEnvOutside( + { LANG: 'fr' }, + 'new Intl.DateTimeFormat(undefined, { month: "long" } ).format(new Date(9e8))' +); +const isMockable = enero !== janvier; + +// Tests with mocked env +if (isMockable) { + assert.strictEqual( + isSet(zones.map((TZ) => runEnvOutside({ TZ }, 'new Date(333333333333).toString()'))), + true + ); + assert.strictEqual( + isSet(zones.map((TZ) => runEnvOutside({ TZ }, 'new Date(333333333333).toLocaleString()'))), + true + ); + assert.deepStrictEqual( + locales.map((LANG) => runEnvOutside({ LANG, TZ: 'Europe/Zurich' }, 'new Date(333333333333).toString()')), + [ + 'Fri Jul 25 1980 01:35:33 GMT+0100 (Central European Standard Time)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (中欧标准时间)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (मध्य यूरोपीय मानक समय)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (hora estándar de Europa central)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (heure normale d’Europe centrale)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (توقيت وسط أوروبا الرسمي)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (মধ্য ইউরোপীয় মানক সময়)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (Центральная Европа, стандартное время)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (Horário Padrão da Europa Central)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (وسطی یورپ کا معیاری وقت)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (Waktu Standar Eropa Tengah)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (Mitteleuropäische Normalzeit)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (中央ヨーロッパ標準時)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (Mídúl Yúrop Fíksd Taim)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (मध्‍य युरोपियन प्रमाण वेळ)', + 'Fri Jul 25 1980 01:35:33 GMT+0100 (సెంట్రల్ యూరోపియన్ ప్రామాణిక సమయం)', + ] + ); + assert.deepStrictEqual( + locales.map((LANG) => runEnvOutside({ LANG, TZ: 'Europe/Zurich' }, 'new Date(333333333333).toLocaleString()')), + [ + '7/25/1980, 1:35:33 AM', + '1980/7/25 01:35:33', + '25/7/1980, 1:35:33 am', + '25/7/1980, 1:35:33', + '25/07/1980 01:35:33', + '٢٥‏/٧‏/١٩٨٠، ١:٣٥:٣٣ ص', + '২৫/৭/১৯৮০, ১:৩৫:৩৩ AM', + '25.07.1980, 01:35:33', + '25/07/1980, 01:35:33', + '25/7/1980، 1:35:33 AM', + '25/7/1980, 01.35.33', + '25.7.1980, 01:35:33', + '1980/7/25 1:35:33', + '25/7/1980 01:35:33', + '२५/७/१९८०, १:३५:३३ AM', + '25/7/1980 1:35:33 AM', + ] + ); + assert.strictEqual( + runEnvOutside({ LANG: 'en' }, '["z", "ä"].sort(new Intl.Collator().compare)'), + 'ä,z' + ); + assert.strictEqual( + runEnvOutside({ LANG: 'sv' }, '["z", "ä"].sort(new Intl.Collator().compare)'), + 'z,ä' + ); + assert.deepStrictEqual( + locales.map( + (LANG) => runEnvOutside({ LANG, TZ: 'Europe/Zurich' }, 'new Intl.DateTimeFormat().format(333333333333)') + ), + [ + '7/25/1980', '1980/7/25', + '25/7/1980', '25/7/1980', + '25/07/1980', '٢٥‏/٧‏/١٩٨٠', + '২৫/৭/১৯৮০', '25.07.1980', + '25/07/1980', '25/7/1980', + '25/7/1980', '25.7.1980', + '1980/7/25', '25/7/1980', + '२५/७/१९८०', '25/7/1980', + ] + ); + assert.deepStrictEqual( + locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.DisplayNames(undefined, { type: "region" }).of("CH")')), + [ + 'Switzerland', '瑞士', + 'स्विट्ज़रलैंड', 'Suiza', + 'Suisse', 'سويسرا', + 'সুইজারল্যান্ড', 'Швейцария', + 'Suíça', 'سوئٹزر لینڈ', + 'Swiss', 'Schweiz', + 'スイス', 'Swítsaland', + 'स्वित्झर्लंड', 'స్విట్జర్లాండ్', + ] + ); + assert.deepStrictEqual( + locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.NumberFormat().format(275760.913)')), + [ + '275,760.913', '275,760.913', + '2,75,760.913', '275.760,913', + '275 760,913', '٢٧٥٬٧٦٠٫٩١٣', + '২,৭৫,৭৬০.৯১৩', '275 760,913', + '275.760,913', '275,760.913', + '275.760,913', '275.760,913', + '275,760.913', '275,760.913', + '२,७५,७६०.९१३', '2,75,760.913', + ] + ); + assert.deepStrictEqual( + locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.PluralRules().select(0)')), + [ + 'other', 'other', 'one', 'other', + 'one', 'zero', 'one', 'many', + 'one', 'other', 'other', 'other', + 'other', 'one', 'other', 'other', + ] + ); + assert.deepStrictEqual( + locales.map((LANG) => runEnvOutside({ LANG }, 'new Intl.RelativeTimeFormat().format(-586920.617, "hour")')), + [ + '586,920.617 hours ago', + '586,920.617小时前', + '5,86,920.617 घंटे पहले', + 'hace 586.920,617 horas', + 'il y a 586 920,617 heures', + 'قبل ٥٨٦٬٩٢٠٫٦١٧ ساعة', + '৫,৮৬,৯২০.৬১৭ ঘন্টা আগে', + '586 920,617 часа назад', + 'há 586.920,617 horas', + '586,920.617 گھنٹے پہلے', + '586.920,617 jam yang lalu', + 'vor 586.920,617 Stunden', + '586,920.617 時間前', + '586,920.617 áwa wé dọ́n pas', + '५,८६,९२०.६१७ तासांपूर्वी', + '5,86,920.617 గంటల క్రితం', + ] + ); +} + + +// Tests with process.env mutated inside +{ + // process.env.TZ is not intercepted in Workers + if (common.isMainThread) { + assert.strictEqual( + isSet(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toString()))), + true + ); + assert.strictEqual( + isSet(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toLocaleString()))), + true + ); + } else { + assert.strictEqual( + isPack(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toString()))), + true + ); + assert.strictEqual( + isPack(zones.map((TZ) => runEnvInside({ TZ }, () => new Date(333333333333).toLocaleString()))), + true + ); + } + + assert.strictEqual( + isPack(locales.map((LANG) => runEnvInside({ LANG, TZ: 'Europe/Zurich' }, () => new Date(333333333333).toString()))), + true + ); + assert.strictEqual( + isPack(locales.map( + (LANG) => runEnvInside({ LANG, TZ: 'Europe/Zurich' }, () => new Date(333333333333).toLocaleString()) + )), + true + ); + assert.deepStrictEqual( + runEnvInside({ LANG: 'en' }, () => ['z', 'ä'].sort(new Intl.Collator().compare)), + runEnvInside({ LANG: 'sv' }, () => ['z', 'ä'].sort(new Intl.Collator().compare)) + ); + assert.strictEqual( + isPack(locales.map( + (LANG) => runEnvInside({ LANG, TZ: 'Europe/Zurich' }, () => new Intl.DateTimeFormat().format(333333333333)) + )), + true + ); + assert.strictEqual( + isPack(locales.map( + (LANG) => runEnvInside({ LANG }, () => new Intl.DisplayNames(undefined, { type: 'region' }).of('CH')) + )), + true + ); + assert.strictEqual( + isPack(locales.map((LANG) => runEnvInside({ LANG }, () => new Intl.NumberFormat().format(275760.913)))), + true + ); + assert.strictEqual( + isPack(locales.map((LANG) => runEnvInside({ LANG }, () => new Intl.PluralRules().select(0)))), + true + ); + assert.strictEqual( + isPack(locales.map( + (LANG) => runEnvInside({ LANG }, () => new Intl.RelativeTimeFormat().format(-586920.617, 'hour')) + )), + true + ); +} diff --git a/test/js/node/test/parallel/test-icu-punycode.js b/test/js/node/test/parallel/test-icu-punycode.js new file mode 100644 index 0000000000..29e88f9b9a --- /dev/null +++ b/test/js/node/test/parallel/test-icu-punycode.js @@ -0,0 +1,57 @@ +'use strict'; +// Flags: --expose-internals +const common = require('../common'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +const { internalBinding } = require('internal/test/binding'); +const icu = internalBinding('icu'); +const assert = require('assert'); + +// Test hasConverter method +assert(icu.hasConverter('utf-8'), + 'hasConverter should report converter exists for utf-8'); +assert(!icu.hasConverter('x'), + 'hasConverter should report converter does not exist for x'); + +const tests = require('../fixtures/url-idna.js'); +const fixtures = require('../fixtures/icu-punycode-toascii.json'); + +{ + for (const [i, { ascii, unicode }] of tests.entries()) { + assert.strictEqual(ascii, icu.toASCII(unicode), `toASCII(${i + 1})`); + assert.strictEqual(unicode, icu.toUnicode(ascii), `toUnicode(${i + 1})`); + assert.strictEqual(ascii, icu.toASCII(icu.toUnicode(ascii)), + `toASCII(toUnicode(${i + 1}))`); + assert.strictEqual(unicode, icu.toUnicode(icu.toASCII(unicode)), + `toUnicode(toASCII(${i + 1}))`); + } +} + +{ + for (const [i, test] of fixtures.entries()) { + if (typeof test === 'string') + continue; // skip comments + const { comment, input, output } = test; + let caseComment = `case ${i + 1}`; + if (comment) + caseComment += ` (${comment})`; + if (output === null) { + assert.throws( + () => icu.toASCII(input), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: 'Cannot convert name to ASCII' + } + ); + icu.toASCII(input, true); // Should not throw. + } else { + assert.strictEqual(icu.toASCII(input), output, `ToASCII ${caseComment}`); + assert.strictEqual(icu.toASCII(input, true), output, + `ToASCII ${caseComment} in lenient mode`); + } + icu.toUnicode(input); // Should not throw. + } +} diff --git a/test/js/node/test/parallel/test-icu-transcode.js b/test/js/node/test/parallel/test-icu-transcode.js new file mode 100644 index 0000000000..e9aced128e --- /dev/null +++ b/test/js/node/test/parallel/test-icu-transcode.js @@ -0,0 +1,90 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +const buffer = require('buffer'); +const assert = require('assert'); +const orig = Buffer.from('těst ☕', 'utf8'); + +// Test Transcoding +const tests = { + 'latin1': [0x74, 0x3f, 0x73, 0x74, 0x20, 0x3f], + 'ascii': [0x74, 0x3f, 0x73, 0x74, 0x20, 0x3f], + 'ucs2': [0x74, 0x00, 0x1b, 0x01, 0x73, + 0x00, 0x74, 0x00, 0x20, 0x00, + 0x15, 0x26] +}; + +for (const test in tests) { + const dest = buffer.transcode(orig, 'utf8', test); + assert.strictEqual(dest.length, tests[test].length, `utf8->${test} length`); + for (let n = 0; n < tests[test].length; n++) + assert.strictEqual(dest[n], tests[test][n], `utf8->${test} char ${n}`); +} + +{ + const dest = buffer.transcode(Buffer.from(tests.ucs2), 'ucs2', 'utf8'); + assert.strictEqual(dest.toString(), orig.toString()); +} + +{ + const utf8 = Buffer.from('€'.repeat(4000), 'utf8'); + const ucs2 = Buffer.from('€'.repeat(4000), 'ucs2'); + const utf8_to_ucs2 = buffer.transcode(utf8, 'utf8', 'ucs2'); + const ucs2_to_utf8 = buffer.transcode(ucs2, 'ucs2', 'utf8'); + assert.deepStrictEqual(utf8, ucs2_to_utf8); + assert.deepStrictEqual(ucs2, utf8_to_ucs2); + assert.strictEqual(ucs2_to_utf8.toString('utf8'), + utf8_to_ucs2.toString('ucs2')); +} + +assert.throws( + () => buffer.transcode(null, 'utf8', 'ascii'), + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "source" argument must be an instance of Buffer ' + + 'or Uint8Array. Received null' + } +); + +assert.throws( + () => buffer.transcode(Buffer.from('a'), 'b', 'utf8'), + /^Error: Unable to transcode Buffer \[U_ILLEGAL_ARGUMENT_ERROR\]/ +); + +assert.throws( + () => buffer.transcode(Buffer.from('a'), 'uf8', 'b'), + /^Error: Unable to transcode Buffer \[U_ILLEGAL_ARGUMENT_ERROR\]$/ +); + +assert.deepStrictEqual( + buffer.transcode(Buffer.from('hi', 'ascii'), 'ascii', 'utf16le'), + Buffer.from('hi', 'utf16le')); +assert.deepStrictEqual( + buffer.transcode(Buffer.from('hi', 'latin1'), 'latin1', 'utf16le'), + Buffer.from('hi', 'utf16le')); +assert.deepStrictEqual( + buffer.transcode(Buffer.from('hä', 'latin1'), 'latin1', 'utf16le'), + Buffer.from('hä', 'utf16le')); + +// Test that Uint8Array arguments are okay. +{ + const uint8array = new Uint8Array([...Buffer.from('hä', 'latin1')]); + assert.deepStrictEqual( + buffer.transcode(uint8array, 'latin1', 'utf16le'), + Buffer.from('hä', 'utf16le')); +} + +{ + const dest = buffer.transcode(new Uint8Array(), 'utf8', 'latin1'); + assert.strictEqual(dest.length, 0); +} + +// Test that it doesn't crash +{ + buffer.transcode(new buffer.SlowBuffer(1), 'utf16le', 'ucs2'); +} diff --git a/test/js/node/test/parallel/test-inspect-support-for-node_options.js b/test/js/node/test/parallel/test-inspect-support-for-node_options.js new file mode 100644 index 0000000000..05bb3b2c42 --- /dev/null +++ b/test/js/node/test/parallel/test-inspect-support-for-node_options.js @@ -0,0 +1,30 @@ +'use strict'; +const common = require('../common'); +const cluster = require('cluster'); +const assert = require('assert'); + +common.skipIfInspectorDisabled(); + +checkForInspectSupport('--inspect'); + +function checkForInspectSupport(flag) { + + const nodeOptions = JSON.stringify(flag); + const numWorkers = 2; + process.env.NODE_OPTIONS = flag; + + if (cluster.isPrimary) { + for (let i = 0; i < numWorkers; i++) { + cluster.fork(); + } + + cluster.on('online', (worker) => { + worker.disconnect(); + }); + + cluster.on('exit', common.mustCall((worker, code, signal) => { + const errMsg = `For NODE_OPTIONS ${nodeOptions}, failed to start cluster`; + assert.strictEqual(worker.exitedAfterDisconnect, true, errMsg); + }, numWorkers)); + } +} diff --git a/test/js/node/test/parallel/test-inspector-has-inspector-false.js b/test/js/node/test/parallel/test-inspector-has-inspector-false.js new file mode 100644 index 0000000000..56a50408bb --- /dev/null +++ b/test/js/node/test/parallel/test-inspector-has-inspector-false.js @@ -0,0 +1,15 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (process.features.inspector) { + common.skip('V8 inspector is enabled'); +} + +const inspector = require('internal/util/inspector'); + +inspector.sendInspectorCommand( + common.mustNotCall('Inspector callback should not be called'), + common.mustCall(1), +); diff --git a/test/js/node/test/parallel/test-inspector-stops-no-file.js b/test/js/node/test/parallel/test-inspector-stops-no-file.js new file mode 100644 index 0000000000..9ec09fb15d --- /dev/null +++ b/test/js/node/test/parallel/test-inspector-stops-no-file.js @@ -0,0 +1,16 @@ +'use strict'; +require('../common'); + +const spawn = require('child_process').spawn; + +const child = spawn(process.execPath, + [ '--inspect', 'no-such-script.js' ], + { 'stdio': 'inherit' }); + +function signalHandler() { + child.kill(); + process.exit(1); +} + +process.on('SIGINT', signalHandler); +process.on('SIGTERM', signalHandler); diff --git a/test/js/node/test/parallel/test-instanceof.js b/test/js/node/test/parallel/test-instanceof.js new file mode 100644 index 0000000000..5a8b588e7d --- /dev/null +++ b/test/js/node/test/parallel/test-instanceof.js @@ -0,0 +1,11 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + + +// Regression test for instanceof, see +// https://github.com/nodejs/node/issues/7592 +const F = () => {}; +F.prototype = {}; +assert({ __proto__: F.prototype } instanceof F); diff --git a/test/js/node/test/parallel/test-internal-module-require.js b/test/js/node/test/parallel/test-internal-module-require.js new file mode 100644 index 0000000000..3a02e25cd0 --- /dev/null +++ b/test/js/node/test/parallel/test-internal-module-require.js @@ -0,0 +1,114 @@ +'use strict'; + +// // Flags: --expose-internals +// // This verifies that +// // 1. We do not leak internal modules unless the --require-internals option +// // is on. +// // 2. We do not accidentally leak any modules to the public global scope. +// // 3. Deprecated modules are properly deprecated. + +const common = require('../common'); + +common.skip("This test is not going to be implemented in Bun. We do not support --expose-internals.") + +// if (!common.isMainThread) { +// common.skip('Cannot test the existence of --expose-internals from worker'); +// } + +// const assert = require('assert'); +// const fork = require('child_process').fork; + +// const expectedPublicModules = new Set([ +// '_http_agent', +// '_http_client', +// '_http_common', +// '_http_incoming', +// '_http_outgoing', +// '_http_server', +// '_stream_duplex', +// '_stream_passthrough', +// '_stream_readable', +// '_stream_transform', +// '_stream_wrap', +// '_stream_writable', +// '_tls_common', +// '_tls_wrap', +// 'assert', +// 'async_hooks', +// 'buffer', +// 'child_process', +// 'cluster', +// 'console', +// 'constants', +// 'crypto', +// 'dgram', +// 'dns', +// 'domain', +// 'events', +// 'fs', +// 'http', +// 'http2', +// 'https', +// 'inspector', +// 'module', +// 'net', +// 'os', +// 'path', +// 'perf_hooks', +// 'process', +// 'punycode', +// 'querystring', +// 'readline', +// 'repl', +// 'stream', +// 'string_decoder', +// 'sys', +// 'timers', +// 'tls', +// 'trace_events', +// 'tty', +// 'url', +// 'util', +// 'v8', +// 'vm', +// 'worker_threads', +// 'zlib', +// ]); + +// if (process.argv[2] === 'child') { +// assert(!process.execArgv.includes('--expose-internals')); +// process.once('message', ({ allBuiltins }) => { +// const publicModules = new Set(); +// for (const id of allBuiltins) { +// if (id.startsWith('internal/')) { +// assert.throws(() => { +// require(id); +// }, { +// code: 'MODULE_NOT_FOUND', +// message: `Cannot find module '${id}'` +// }); +// } else { +// require(id); +// publicModules.add(id); +// } +// } +// assert(allBuiltins.length > publicModules.size); +// // Make sure all the public modules are available through +// // require('module').builtinModules +// assert.deepStrictEqual( +// publicModules, +// new Set(require('module').builtinModules) +// ); +// assert.deepStrictEqual(publicModules, expectedPublicModules); +// }); +// } else { +// assert(process.execArgv.includes('--expose-internals')); +// const child = fork(__filename, ['child'], { +// execArgv: [] +// }); +// const { builtinModules } = require('module'); +// // When --expose-internals is on, require('module').builtinModules +// // contains internal modules. +// const message = { allBuiltins: builtinModules }; +// child.send(message); +// } diff --git a/test/js/node/test/parallel/test-internal-process-binding.js b/test/js/node/test/parallel/test-internal-process-binding.js new file mode 100644 index 0000000000..09e3f31096 --- /dev/null +++ b/test/js/node/test/parallel/test-internal-process-binding.js @@ -0,0 +1,10 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(undefined, process._internalBinding); +assert.strictEqual(undefined, process.internalBinding); +assert.throws(() => { + process.binding('module_wrap'); +}, /No such module/); diff --git a/test/js/node/test/parallel/test-intl-v8BreakIterator.js b/test/js/node/test/parallel/test-intl-v8BreakIterator.js new file mode 100644 index 0000000000..257d6b2a76 --- /dev/null +++ b/test/js/node/test/parallel/test-intl-v8BreakIterator.js @@ -0,0 +1,10 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +assert(!('v8BreakIterator' in Intl)); +assert(!vm.runInNewContext('"v8BreakIterator" in Intl')); diff --git a/test/js/node/test/parallel/test-intl.js b/test/js/node/test/parallel/test-intl.js new file mode 100644 index 0000000000..7d1742f2c7 --- /dev/null +++ b/test/js/node/test/parallel/test-intl.js @@ -0,0 +1,163 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { execFile } = require('child_process'); + +// Does node think that i18n was enabled? +let enablei18n = process.config.variables.v8_enable_i18n_support; +if (enablei18n === undefined) { + enablei18n = 0; +} + +// Returns true if no specific locale ids were configured (i.e. "all") +// Else, returns true if loc is in the configured list +// Else, returns false +function haveLocale(loc) { + const locs = process.config.variables.icu_locales.split(','); + return locs.includes(loc); +} + +// Always run these. They should always pass, even if the locale +// param is ignored. +assert.strictEqual('Ç'.toLocaleLowerCase('el'), 'ç'); +assert.strictEqual('Ç'.toLocaleLowerCase('tr'), 'ç'); +assert.strictEqual('Ç'.toLowerCase(), 'ç'); + +assert.strictEqual('ç'.toLocaleUpperCase('el'), 'Ç'); +assert.strictEqual('ç'.toLocaleUpperCase('tr'), 'Ç'); +assert.strictEqual('ç'.toUpperCase(), 'Ç'); + +if (!common.hasIntl) { + const erMsg = + `"Intl" object is NOT present but v8_enable_i18n_support is ${enablei18n}`; + assert.strictEqual(enablei18n, 0, erMsg); + common.skip('Intl tests because Intl object not present.'); +} else { + const erMsg = + `"Intl" object is present but v8_enable_i18n_support is ${ + enablei18n}. Is this test out of date?`; + assert.strictEqual(enablei18n, 1, erMsg); + + // Construct a new date at the beginning of Unix time + const date0 = new Date(0); + + // Use the GMT time zone + const GMT = 'Etc/GMT'; + + // Construct an English formatter. Should format to "Jan 70" + const dtf = new Intl.DateTimeFormat(['en'], { + timeZone: GMT, + month: 'short', + year: '2-digit' + }); + + // If list is specified and doesn't contain 'en' then return. + if (process.config.variables.icu_locales && !haveLocale('en')) { + common.printSkipMessage( + 'detailed Intl tests because English is not listed as supported.'); + // Smoke test. Does it format anything, or fail? + console.log(`Date(0) formatted to: ${dtf.format(date0)}`); + return; + } + + // Check casing + { + assert.strictEqual('I'.toLocaleLowerCase('tr'), 'ı'); + } + + // Check with toLocaleString + { + const localeString = dtf.format(date0); + assert.strictEqual(localeString, 'Jan 70'); + } + // Options to request GMT + const optsGMT = { timeZone: GMT }; + + // Test format + { + const localeString = date0.toLocaleString(['en'], optsGMT); + assert.strictEqual(localeString, '1/1/1970, 12:00:00 AM'); + } + // number format + { + const numberFormat = new Intl.NumberFormat(['en']).format(12345.67890); + assert.strictEqual(numberFormat, '12,345.679'); + } + // If list is specified and doesn't contain 'en-US' then return. + if (process.config.variables.icu_locales && !haveLocale('en-US')) { + common.printSkipMessage('detailed Intl tests because American English is ' + + 'not listed as supported.'); + return; + } + // Number format resolved options + { + const numberFormat = new Intl.NumberFormat('en-US', { style: 'percent' }); + const resolvedOptions = numberFormat.resolvedOptions(); + assert.strictEqual(resolvedOptions.locale, 'en-US'); + assert.strictEqual(resolvedOptions.style, 'percent'); + } + // Significant Digits + { + const loc = ['en-US']; + const opts = { maximumSignificantDigits: 4 }; + const num = 10.001; + const numberFormat = new Intl.NumberFormat(loc, opts).format(num); + assert.strictEqual(numberFormat, '10'); + } + + const collOpts = { sensitivity: 'base', ignorePunctuation: true }; + const coll = new Intl.Collator(['en'], collOpts); + + // Ignore punctuation + assert.strictEqual(coll.compare('blackbird', 'black-bird'), 0); + // Compare less + assert.strictEqual(coll.compare('blackbird', 'red-bird'), -1); + // Compare greater + assert.strictEqual(coll.compare('bluebird', 'blackbird'), 1); + // Ignore case + assert.strictEqual(coll.compare('Bluebird', 'bluebird'), 0); + // `ffi` ligature (contraction) + assert.strictEqual(coll.compare('\ufb03', 'ffi'), 0); + + { + // Regression test for https://github.com/nodejs/node/issues/27379 + const env = { ...process.env, LC_ALL: 'ja' }; + execFile( + process.execPath, ['-p', 'new Date().toLocaleString()'], + { env }, + common.mustSucceed() + ); + } + + { + // Regression test for https://github.com/nodejs/node/issues/27418 + const env = { ...process.env, LC_ALL: 'fr@EURO' }; + execFile( + process.execPath, + ['-p', 'new Intl.NumberFormat().resolvedOptions().locale'], + { env }, + common.mustSucceed() + ); + } +} diff --git a/test/js/node/test/parallel/test-kill-segfault-freebsd.js b/test/js/node/test/parallel/test-kill-segfault-freebsd.js new file mode 100644 index 0000000000..e17b00741b --- /dev/null +++ b/test/js/node/test/parallel/test-kill-segfault-freebsd.js @@ -0,0 +1,19 @@ +'use strict'; +require('../common'); + +// This test ensures Node.js doesn't crash on hitting Ctrl+C in order to +// terminate the currently running process (especially on FreeBSD). +// https://github.com/nodejs/node-v0.x-archive/issues/9326 + +const assert = require('assert'); +const child_process = require('child_process'); + +// NOTE: Was crashing on FreeBSD +const cp = child_process.spawn(process.execPath, [ + '-e', + 'process.kill(process.pid, "SIGINT")', +]); + +cp.on('exit', function(code) { + assert.notStrictEqual(code, 0); +}); diff --git a/test/js/node/test/parallel/test-listen-fd-detached-inherit.js b/test/js/node/test/parallel/test-listen-fd-detached-inherit.js new file mode 100644 index 0000000000..2a8e70f0f9 --- /dev/null +++ b/test/js/node/test/parallel/test-listen-fd-detached-inherit.js @@ -0,0 +1,118 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isWindows) + common.skip('This test is disabled on windows.'); + +const assert = require('assert'); +const http = require('http'); +const net = require('net'); +const spawn = require('child_process').spawn; + +switch (process.argv[2]) { + case 'child': return child(); + case 'parent': return parent(); + default: return test(); +} + +// Spawn the parent, and listen for it to tell us the pid of the child. +// WARNING: This is an example of listening on some arbitrary FD number +// that has already been bound elsewhere in advance. However, binding +// server handles to stdio fd's is NOT a good or reliable way to do +// concurrency in HTTP servers! Use the cluster module, or if you want +// a more low-level approach, use child process IPC manually. +function test() { + const parent = spawn(process.execPath, [__filename, 'parent'], { + stdio: [ 0, 'pipe', 2 ] + }); + let json = ''; + parent.stdout.on('data', function(c) { + json += c.toString(); + if (json.includes('\n')) next(); + }); + function next() { + console.error('output from parent = %s', json); + const child = JSON.parse(json); + // Now make sure that we can request to the subprocess, then kill it. + http.get({ + server: 'localhost', + port: child.port, + path: '/', + }).on('response', function(res) { + let s = ''; + res.on('data', function(c) { + s += c.toString(); + }); + res.on('end', function() { + // Kill the subprocess before we start doing asserts. + // It's really annoying when tests leave orphans! + process.kill(child.pid, 'SIGKILL'); + try { + parent.kill(); + } catch { + // Continue regardless of error. + } + + assert.strictEqual(s, 'hello from child\n'); + assert.strictEqual(res.statusCode, 200); + }); + }); + } +} + +// Listen on port, and then pass the handle to the detached child. +// Then output the child's pid, and immediately exit. +function parent() { + const server = net.createServer(function(conn) { + conn.end('HTTP/1.1 403 Forbidden\r\n\r\nI got problems.\r\n'); + throw new Error('Should not see connections on parent'); + }).listen(0, function() { + console.error('server listening on %d', this.address().port); + + const child = spawn(process.execPath, [__filename, 'child'], { + stdio: [ 0, 1, 2, server._handle ], + detached: true + }); + + console.log('%j\n', { pid: child.pid, port: this.address().port }); + + // Now close the parent, so that the child is the only thing + // referencing that handle. Note that connections will still + // be accepted, because the child has the fd open, but the parent + // will exit gracefully. + server.close(); + child.unref(); + }); +} + +// Run as a child of the parent() mode. +function child() { + // Start a server on fd=3 + http.createServer(function(req, res) { + console.error('request on child'); + console.error('%s %s', req.method, req.url, req.headers); + res.end('hello from child\n'); + }).listen({ fd: 3 }, function() { + console.error('child listening on fd=3'); + }); +} diff --git a/test/js/node/test/parallel/test-listen-fd-detached.js b/test/js/node/test/parallel/test-listen-fd-detached.js new file mode 100644 index 0000000000..fba96a112f --- /dev/null +++ b/test/js/node/test/parallel/test-listen-fd-detached.js @@ -0,0 +1,115 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isWindows) + common.skip('This test is disabled on windows.'); + +const assert = require('assert'); +const http = require('http'); +const net = require('net'); +const spawn = require('child_process').spawn; + +switch (process.argv[2]) { + case 'child': return child(); + case 'parent': return parent(); + default: return test(); +} + +// Spawn the parent, and listen for it to tell us the pid of the child. +// WARNING: This is an example of listening on some arbitrary FD number +// that has already been bound elsewhere in advance. However, binding +// server handles to stdio fd's is NOT a good or reliable way to do +// concurrency in HTTP servers! Use the cluster module, or if you want +// a more low-level approach, use child process IPC manually. +function test() { + const parent = spawn(process.execPath, [__filename, 'parent'], { + stdio: [ 0, 'pipe', 2 ] + }); + let json = ''; + parent.stdout.on('data', function(c) { + json += c.toString(); + if (json.includes('\n')) next(); + }); + function next() { + console.error('output from parent = %s', json); + const child = JSON.parse(json); + // Now make sure that we can request to the subprocess, then kill it. + http.get({ + server: 'localhost', + port: child.port, + path: '/', + }).on('response', function(res) { + let s = ''; + res.on('data', function(c) { + s += c.toString(); + }); + res.on('end', function() { + // Kill the subprocess before we start doing asserts. + // it's really annoying when tests leave orphans! + process.kill(child.pid, 'SIGKILL'); + try { + parent.kill(); + } catch { + // Continue regardless of error. + } + + assert.strictEqual(s, 'hello from child\n'); + assert.strictEqual(res.statusCode, 200); + }); + }); + } +} + +function parent() { + const server = net.createServer(function(conn) { + console.error('connection on parent'); + conn.end('hello from parent\n'); + }).listen(0, function() { + console.error('server listening on %d', this.address().port); + + const child = spawn(process.execPath, [__filename, 'child'], { + stdio: [ 'ignore', 'ignore', 'ignore', server._handle ], + detached: true + }); + + console.log('%j\n', { pid: child.pid, port: this.address().port }); + + // Now close the parent, so that the child is the only thing + // referencing that handle. Note that connections will still + // be accepted, because the child has the fd open, but the parent + // will exit gracefully. + server.close(); + child.unref(); + }); +} + +function child() { + // Start a server on fd=3 + http.createServer(function(req, res) { + console.error('request on child'); + console.error('%s %s', req.method, req.url, req.headers); + res.end('hello from child\n'); + }).listen({ fd: 3 }, function() { + console.error('child listening on fd=3'); + }); +} diff --git a/test/js/node/test/parallel/test-memory-usage-emfile.js b/test/js/node/test/parallel/test-memory-usage-emfile.js new file mode 100644 index 0000000000..05b112e918 --- /dev/null +++ b/test/js/node/test/parallel/test-memory-usage-emfile.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); + +// On IBMi, the rss memory always returns zero +if (common.isIBMi) + common.skip('On IBMi, the rss memory always returns zero'); + +const assert = require('assert'); + +const fs = require('fs'); + +const files = []; + +while (files.length < 256) + files.push(fs.openSync(__filename, 'r')); + +const r = process.memoryUsage.rss(); +assert.strictEqual(r > 0, true); diff --git a/test/js/node/test/parallel/test-memory-usage.js b/test/js/node/test/parallel/test-memory-usage.js new file mode 100644 index 0000000000..8e5ea4de5b --- /dev/null +++ b/test/js/node/test/parallel/test-memory-usage.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Flags: --predictable-gc-schedule +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const r = process.memoryUsage(); +// On IBMi, the rss memory always returns zero +if (!common.isIBMi) { + assert.ok(r.rss > 0); + assert.ok(process.memoryUsage.rss() > 0); +} + +assert.ok(r.heapTotal > 0); +assert.ok(r.heapUsed > 0); +assert.ok(r.external > 0); + +assert.strictEqual(typeof r.arrayBuffers, 'number'); +if (r.arrayBuffers > 0) { + const size = 10 * 1024 * 1024; + // eslint-disable-next-line no-unused-vars + const ab = new ArrayBuffer(size); + + const after = process.memoryUsage(); + assert.ok(after.external - r.external >= size, + `${after.external} - ${r.external} >= ${size}`); + assert.strictEqual(after.arrayBuffers - r.arrayBuffers, size, + `${after.arrayBuffers} - ${r.arrayBuffers} === ${size}`); +} diff --git a/test/js/node/test/parallel/test-messagechannel.js b/test/js/node/test/parallel/test-messagechannel.js new file mode 100644 index 0000000000..4f92924daa --- /dev/null +++ b/test/js/node/test/parallel/test-messagechannel.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); + +// See: https://github.com/nodejs/node/issues/49940 +(async () => { + new MessageChannel().port1.postMessage({}, { + transfer: { + *[Symbol.iterator]() {} + } + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-messageevent-brandcheck.js b/test/js/node/test/parallel/test-messageevent-brandcheck.js new file mode 100644 index 0000000000..17f2b708cc --- /dev/null +++ b/test/js/node/test/parallel/test-messageevent-brandcheck.js @@ -0,0 +1,14 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +[ + 'data', + 'origin', + 'lastEventId', + 'source', + 'ports', +].forEach((i) => { + assert.throws(() => Reflect.get(MessageEvent.prototype, i, {}), TypeError); +}); diff --git a/test/js/node/test/parallel/test-microtask-queue-integration.js b/test/js/node/test/parallel/test-microtask-queue-integration.js new file mode 100644 index 0000000000..69d55253a2 --- /dev/null +++ b/test/js/node/test/parallel/test-microtask-queue-integration.js @@ -0,0 +1,63 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const implementations = [ + function(fn) { + Promise.resolve().then(fn); + }, +]; + +let expected = 0; +let done = 0; + +process.on('exit', function() { + assert.strictEqual(done, expected); +}); + +function test(scheduleMicrotask) { + let nextTickCalled = false; + expected++; + + scheduleMicrotask(function() { + process.nextTick(function() { + nextTickCalled = true; + }); + + setTimeout(function() { + assert(nextTickCalled); + done++; + }, 0); + }); +} + +// first tick case +implementations.forEach(test); + +// tick callback case +setTimeout(function() { + implementations.forEach(function(impl) { + process.nextTick(test.bind(null, impl)); + }); +}, 0); diff --git a/test/js/node/test/parallel/test-microtask-queue-run-immediate.js b/test/js/node/test/parallel/test-microtask-queue-run-immediate.js new file mode 100644 index 0000000000..577391993b --- /dev/null +++ b/test/js/node/test/parallel/test-microtask-queue-run-immediate.js @@ -0,0 +1,59 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +function enqueueMicrotask(fn) { + Promise.resolve().then(fn); +} + +let done = 0; + +process.on('exit', function() { + assert.strictEqual(done, 2); +}); + +// No nextTick, microtask +setImmediate(function() { + enqueueMicrotask(function() { + done++; + }); +}); + + +// No nextTick, microtask with nextTick +setImmediate(function() { + let called = false; + + enqueueMicrotask(function() { + process.nextTick(function() { + called = true; + }); + }); + + setImmediate(function() { + if (called) + done++; + }); + +}); diff --git a/test/js/node/test/parallel/test-microtask-queue-run.js b/test/js/node/test/parallel/test-microtask-queue-run.js new file mode 100644 index 0000000000..5281cb4f3c --- /dev/null +++ b/test/js/node/test/parallel/test-microtask-queue-run.js @@ -0,0 +1,59 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +function enqueueMicrotask(fn) { + Promise.resolve().then(fn); +} + +let done = 0; + +process.on('exit', function() { + assert.strictEqual(done, 2); +}); + +// No nextTick, microtask +setTimeout(function() { + enqueueMicrotask(function() { + done++; + }); +}, 0); + + +// No nextTick, microtask with nextTick +setTimeout(function() { + let called = false; + + enqueueMicrotask(function() { + process.nextTick(function() { + called = true; + }); + }); + + setTimeout(function() { + if (called) + done++; + }, 0); + +}, 0); diff --git a/test/js/node/test/parallel/test-module-builtin.js b/test/js/node/test/parallel/test-module-builtin.js new file mode 100644 index 0000000000..3897d71ecf --- /dev/null +++ b/test/js/node/test/parallel/test-module-builtin.js @@ -0,0 +1,14 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { builtinModules } = require('module'); + +// Includes modules in lib/ (even deprecated ones) +assert(builtinModules.includes('http')); +assert(builtinModules.includes('sys')); + +// Does not include internal modules +assert.deepStrictEqual( + builtinModules.filter((mod) => mod.startsWith('internal/')), + [] +); diff --git a/test/js/node/test/parallel/test-module-cache.js b/test/js/node/test/parallel/test-module-cache.js new file mode 100644 index 0000000000..87913c72cc --- /dev/null +++ b/test/js/node/test/parallel/test-module-cache.js @@ -0,0 +1,18 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filePath = tmpdir.resolve('test-module-cache.json'); +assert.throws( + () => require(filePath), + { code: 'MODULE_NOT_FOUND' } +); + +fs.writeFileSync(filePath, '[]'); + +const content = require(filePath); +assert.strictEqual(Array.isArray(content), true); +assert.strictEqual(content.length, 0); diff --git a/test/js/node/test/parallel/test-module-circular-symlinks.js b/test/js/node/test/parallel/test-module-circular-symlinks.js new file mode 100644 index 0000000000..e8d80640df --- /dev/null +++ b/test/js/node/test/parallel/test-module-circular-symlinks.js @@ -0,0 +1,68 @@ +'use strict'; + +// This tests to make sure that modules with symlinked circular dependencies +// do not blow out the module cache and recurse forever. See issue +// https://github.com/nodejs/node/pull/5950 for context. PR #5950 attempted +// to solve a problem with symlinked peer dependencies by caching using the +// symlink path. Unfortunately, that breaks the case tested in this module +// because each symlinked module, despite pointing to the same code on disk, +// is loaded and cached as a separate module instance, which blows up the +// cache and leads to a recursion bug. + +// This test should pass in Node.js v4 and v5. It should pass in Node.js v6 +// after https://github.com/nodejs/node/pull/5950 has been reverted. + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +// {tmpDir} +// ├── index.js +// └── node_modules +// ├── moduleA +// │ ├── index.js +// │ └── node_modules +// │ └── moduleB -> {tmpDir}/node_modules/moduleB +// └── moduleB +// ├── index.js +// └── node_modules +// └── moduleA -> {tmpDir}/node_modules/moduleA + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const tmpDir = tmpdir.path; + +const node_modules = path.join(tmpDir, 'node_modules'); +const moduleA = path.join(node_modules, 'moduleA'); +const moduleB = path.join(node_modules, 'moduleB'); +const moduleA_link = path.join(moduleB, 'node_modules', 'moduleA'); +const moduleB_link = path.join(moduleA, 'node_modules', 'moduleB'); + +fs.mkdirSync(node_modules); +fs.mkdirSync(moduleA); +fs.mkdirSync(moduleB); +fs.mkdirSync(path.join(moduleA, 'node_modules')); +fs.mkdirSync(path.join(moduleB, 'node_modules')); + +try { + fs.symlinkSync(moduleA, moduleA_link); + fs.symlinkSync(moduleB, moduleB_link); +} catch (err) { + if (err.code !== 'EPERM') throw err; + common.skip('insufficient privileges for symlinks'); +} + +fs.writeFileSync(path.join(tmpDir, 'index.js'), + 'module.exports = require(\'moduleA\');', 'utf8'); +fs.writeFileSync(path.join(moduleA, 'index.js'), + 'module.exports = {b: require(\'moduleB\')};', 'utf8'); +fs.writeFileSync(path.join(moduleB, 'index.js'), + 'module.exports = {a: require(\'moduleA\')};', 'utf8'); + +// Ensure that the symlinks are not followed forever... +const obj = require(path.join(tmpDir, 'index')); +assert.ok(obj); +assert.ok(obj.b); +assert.ok(obj.b.a); +assert.ok(!obj.b.a.b); diff --git a/test/js/node/test/parallel/test-module-main-extension-lookup.js b/test/js/node/test/parallel/test-module-main-extension-lookup.js new file mode 100644 index 0000000000..58d78e09b1 --- /dev/null +++ b/test/js/node/test/parallel/test-module-main-extension-lookup.js @@ -0,0 +1,9 @@ +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const { execFileSync } = require('child_process'); + +const node = process.argv[0]; + +execFileSync(node, [fixtures.path('es-modules', 'test-esm-ok.mjs')]); +execFileSync(node, [fixtures.path('es-modules', 'noext')]); diff --git a/test/js/node/test/parallel/test-module-readonly.js b/test/js/node/test/parallel/test-module-readonly.js new file mode 100644 index 0000000000..973f8ad521 --- /dev/null +++ b/test/js/node/test/parallel/test-module-readonly.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); + +if (!common.isWindows) { + // TODO: Similar checks on *nix-like systems (e.g using chmod or the like) + common.skip('test only runs on Windows'); +} +if (common.isWindows) return; // TODO: BUN + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const cp = require('child_process'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Create readOnlyMod.js and set to read only +const readOnlyMod = tmpdir.resolve('readOnlyMod'); +const readOnlyModRelative = path.relative(__dirname, readOnlyMod); +const readOnlyModFullPath = `${readOnlyMod}.js`; + +fs.writeFileSync(readOnlyModFullPath, 'module.exports = 42;'); + +// Removed any inherited ACEs, and any explicitly granted ACEs for the +// current user +cp.execSync( + `icacls.exe "${readOnlyModFullPath}" /inheritance:r /remove "%USERNAME%"`); + +// Grant the current user read & execute only +cp.execSync(`icacls.exe "${readOnlyModFullPath}" /grant "%USERNAME%":RX`); + +let except = null; +try { + // Attempt to load the module. Will fail if write access is required + require(readOnlyModRelative); +} catch (err) { + except = err; +} + +// Remove the explicitly granted rights, and re-enable inheritance +cp.execSync( + `icacls.exe "${readOnlyModFullPath}" /remove "%USERNAME%" /inheritance:e`); + +// Delete the test module (note: tmpdir should get cleaned anyway) +fs.unlinkSync(readOnlyModFullPath); + +assert.ifError(except); diff --git a/test/js/node/test/parallel/test-module-relative-lookup.js b/test/js/node/test/parallel/test-module-relative-lookup.js new file mode 100644 index 0000000000..1bd505392c --- /dev/null +++ b/test/js/node/test/parallel/test-module-relative-lookup.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const _module = require('module'); // Avoid collision with global.module + +// Current directory gets highest priority for local modules +function testFirstInPath(moduleName, isLocalModule) { + const assertFunction = isLocalModule ? + assert.strictEqual : + assert.notStrictEqual; + + let paths = _module._resolveLookupPaths(moduleName); + + assertFunction(paths[0], '.'); + + paths = _module._resolveLookupPaths(moduleName, null); + assertFunction(paths && paths[0], '.'); +} + +testFirstInPath('./lodash', true); + +// Relative path on Windows, but a regular file name elsewhere +testFirstInPath('.\\lodash', common.isWindows); diff --git a/test/js/node/test/parallel/test-net-autoselectfamily-attempt-timeout-default-value.js b/test/js/node/test/parallel/test-net-autoselectfamily-attempt-timeout-default-value.js new file mode 100644 index 0000000000..7822769527 --- /dev/null +++ b/test/js/node/test/parallel/test-net-autoselectfamily-attempt-timeout-default-value.js @@ -0,0 +1,8 @@ +'use strict'; + +const { platformTimeout } = require('../common'); + +const assert = require('assert'); +const { getDefaultAutoSelectFamilyAttemptTimeout } = require('net'); + +assert.strictEqual(getDefaultAutoSelectFamilyAttemptTimeout(), platformTimeout(2500)); diff --git a/test/js/node/test/parallel/test-net-bind-twice.js b/test/js/node/test/parallel/test-net-bind-twice.js new file mode 100644 index 0000000000..f59818a1e8 --- /dev/null +++ b/test/js/node/test/parallel/test-net-bind-twice.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server1 = net.createServer(common.mustNotCall()); +server1.listen(0, '127.0.0.1', common.mustCall(function() { + const server2 = net.createServer(common.mustNotCall()); + server2.listen(this.address().port, '127.0.0.1', common.mustNotCall()); + + server2.on('error', common.mustCall(function(e) { + assert.strictEqual(e.code, 'EADDRINUSE'); + server1.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-net-buffersize.js b/test/js/node/test/parallel/test-net-buffersize.js new file mode 100644 index 0000000000..7225d70af3 --- /dev/null +++ b/test/js/node/test/parallel/test-net-buffersize.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const iter = 10; + +const server = net.createServer(function(socket) { + socket.on('readable', function() { + socket.read(); + }); + + socket.on('end', function() { + server.close(); + }); +}); + +server.listen(0, common.mustCall(function() { + const client = net.connect(this.address().port); + + client.on('finish', common.mustCall(() => { + assert.strictEqual(client.bufferSize, 0); + })); + + for (let i = 1; i < iter; i++) { + client.write('a'); + assert.strictEqual(client.bufferSize, i); + } + + client.end(); +})); diff --git a/test/js/node/test/parallel/test-net-connect-abort-controller.js b/test/js/node/test/parallel/test-net-connect-abort-controller.js new file mode 100644 index 0000000000..9c259cc3fc --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-abort-controller.js @@ -0,0 +1,96 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); +const server = net.createServer(); +const { getEventListeners, once } = require('events'); + +const liveConnections = new Set(); + +server.listen(0, common.mustCall(async () => { + const port = server.address().port; + const host = 'localhost'; + const socketOptions = (signal) => ({ port, host, signal }); + server.on('connection', (connection) => { + liveConnections.add(connection); + connection.on('close', () => { + liveConnections.delete(connection); + }); + }); + + const assertAbort = async (socket, testName) => { + try { + await once(socket, 'close'); + assert.fail(`close ${testName} should have thrown`); + } catch (err) { + assert.strictEqual(err.name, 'AbortError'); + } + }; + + async function postAbort() { + const ac = new AbortController(); + const { signal } = ac; + const socket = net.connect(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + ac.abort(); + await assertAbort(socket, 'postAbort'); + } + + async function preAbort() { + const ac = new AbortController(); + const { signal } = ac; + ac.abort(); + const socket = net.connect(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + await assertAbort(socket, 'preAbort'); + } + + async function tickAbort() { + const ac = new AbortController(); + const { signal } = ac; + setImmediate(() => ac.abort()); + const socket = net.connect(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + await assertAbort(socket, 'tickAbort'); + } + + async function testConstructor() { + const ac = new AbortController(); + const { signal } = ac; + ac.abort(); + const socket = new net.Socket(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + await assertAbort(socket, 'testConstructor'); + } + + async function testConstructorPost() { + const ac = new AbortController(); + const { signal } = ac; + const socket = new net.Socket(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + ac.abort(); + await assertAbort(socket, 'testConstructorPost'); + } + + async function testConstructorPostTick() { + const ac = new AbortController(); + const { signal } = ac; + const socket = new net.Socket(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + setImmediate(() => ac.abort()); + await assertAbort(socket, 'testConstructorPostTick'); + } + + await postAbort(); + await preAbort(); + await tickAbort(); + await testConstructor(); + await testConstructorPost(); + await testConstructorPostTick(); + + // Killing the net.socket without connecting hangs the server. + for (const connection of liveConnections) { + connection.destroy(); + } + server.close(common.mustCall()); +})); diff --git a/test/js/node/test/parallel/test-net-connect-call-socket-connect.js b/test/js/node/test/parallel/test-net-connect-call-socket-connect.js new file mode 100644 index 0000000000..88551889fe --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-call-socket-connect.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common'); + +// This test checks that calling `net.connect` internally calls +// `Socket.prototype.connect`. +// +// This is important for people who monkey-patch `Socket.prototype.connect` +// since it's not possible to monkey-patch `net.connect` directly (as the core +// `connect` function is called internally in Node instead of calling the +// `exports.connect` function). +// +// Monkey-patching of `Socket.prototype.connect` is done by - among others - +// most APM vendors, the async-listener module and the +// continuation-local-storage module. +// +// Related: +// - https://github.com/nodejs/node/pull/12342 +// - https://github.com/nodejs/node/pull/12852 + +const net = require('net'); +const Socket = net.Socket; + +// Monkey patch Socket.prototype.connect to check that it's called. +const orig = Socket.prototype.connect; +Socket.prototype.connect = common.mustCall(function() { + return orig.apply(this, arguments); +}); + +const server = net.createServer(); + +server.listen(common.mustCall(function() { + const port = server.address().port; + const client = net.connect({ port }, common.mustCall(function() { + client.end(); + })); + client.on('end', common.mustCall(function() { + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-net-connect-destroy.js b/test/js/node/test/parallel/test-net-connect-destroy.js new file mode 100644 index 0000000000..73fdb988f9 --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-destroy.js @@ -0,0 +1,7 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const socket = new net.Socket(); +socket.on('close', common.mustCall()); +socket.destroy(); diff --git a/test/js/node/test/parallel/test-net-connect-immediate-destroy.js b/test/js/node/test/parallel/test-net-connect-immediate-destroy.js new file mode 100644 index 0000000000..3ca58c356b --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-immediate-destroy.js @@ -0,0 +1,11 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0); +const port = server.address().port; +const socket = net.connect(port, common.localhostIPv4, common.mustNotCall()); +socket.on('error', common.mustNotCall()); +server.close(); +socket.destroy(); diff --git a/test/js/node/test/parallel/test-net-connect-options-path.js b/test/js/node/test/parallel/test-net-connect-options-path.js new file mode 100644 index 0000000000..eb0686d46d --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-options-path.js @@ -0,0 +1,60 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const net = require('net'); + +// This file tests the option handling of net.connect, +// net.createConnect, and new Socket().connect + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const CLIENT_VARIANTS = 12; + +// Test connect(path) +{ + const prefix = `${common.PIPE}-net-connect-options-path`; + const serverPath = `${prefix}-server`; + let counter = 0; + const server = net.createServer() + .on('connection', common.mustCall(function(socket) { + socket.end('ok'); + }, CLIENT_VARIANTS)) + .listen(serverPath, common.mustCall(function() { + const getConnectCb = () => common.mustCall(function() { + this.end(); + this.on('close', common.mustCall(function() { + counter++; + if (counter === CLIENT_VARIANTS) { + server.close(); + } + })); + }); + + // CLIENT_VARIANTS depends on the following code + net.connect(serverPath, getConnectCb()).resume(); + net.connect(serverPath) + .on('connect', getConnectCb()) + .resume(); + net.createConnection(serverPath, getConnectCb()).resume(); + net.createConnection(serverPath) + .on('connect', getConnectCb()) + .resume(); + new net.Socket().connect(serverPath, getConnectCb()).resume(); + new net.Socket().connect(serverPath) + .on('connect', getConnectCb()) + .resume(); + net.connect({ path: serverPath }, getConnectCb()).resume(); + net.connect({ path: serverPath }) + .on('connect', getConnectCb()) + .resume(); + net.createConnection({ path: serverPath }, getConnectCb()).resume(); + net.createConnection({ path: serverPath }) + .on('connect', getConnectCb()) + .resume(); + new net.Socket().connect({ path: serverPath }, getConnectCb()).resume(); + new net.Socket().connect({ path: serverPath }) + .on('connect', getConnectCb()) + .resume(); + })); +} diff --git a/test/js/node/test/parallel/test-net-dns-lookup-skip.js b/test/js/node/test/parallel/test-net-dns-lookup-skip.js new file mode 100644 index 0000000000..06dbd5932b --- /dev/null +++ b/test/js/node/test/parallel/test-net-dns-lookup-skip.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +function check(addressType) { + const server = net.createServer(function(client) { + client.end(); + server.close(); + }); + + const address = addressType === 4 ? '127.0.0.1' : '::1'; + server.listen(0, address, function() { + net.connect(this.address().port, address) + .on('lookup', common.mustNotCall()); + }); +} + +check(4); +common.hasIPv6 && check(6); diff --git a/test/js/node/test/parallel/test-net-during-close.js b/test/js/node/test/parallel/test-net-during-close.js new file mode 100644 index 0000000000..3670ed9c27 --- /dev/null +++ b/test/js/node/test/parallel/test-net-during-close.js @@ -0,0 +1,42 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(function(socket) { + socket.end(); +}); + +server.listen(0, common.mustCall(function() { + /* eslint-disable no-unused-expressions */ + const client = net.createConnection(this.address().port); + server.close(); + // Server connection event has not yet fired client is still attempting to + // connect. Accessing properties should not throw in this case. + client.remoteAddress; + client.remoteFamily; + client.remotePort; + // Exit now, do not wait for the client error event. + process.exit(0); + /* eslint-enable no-unused-expressions */ +})); diff --git a/test/js/node/test/parallel/test-net-end-without-connect.js b/test/js/node/test/parallel/test-net-end-without-connect.js new file mode 100644 index 0000000000..45d0b5477e --- /dev/null +++ b/test/js/node/test/parallel/test-net-end-without-connect.js @@ -0,0 +1,30 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const sock = new net.Socket(); +sock.end(common.mustCall(() => { + assert.strictEqual(sock.writable, false); +})); diff --git a/test/js/node/test/parallel/test-net-isip.js b/test/js/node/test/parallel/test-net-isip.js new file mode 100644 index 0000000000..840ffe76af --- /dev/null +++ b/test/js/node/test/parallel/test-net-isip.js @@ -0,0 +1,96 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); + +assert.strictEqual(net.isIP('127.0.0.1'), 4); +assert.strictEqual(net.isIP('x127.0.0.1'), 0); +assert.strictEqual(net.isIP('example.com'), 0); +assert.strictEqual(net.isIP('0000:0000:0000:0000:0000:0000:0000:0000'), 6); +assert.strictEqual(net.isIP('0000:0000:0000:0000:0000:0000:0000:0000::0000'), + 0); +assert.strictEqual(net.isIP('1050:0:0:0:5:600:300c:326b'), 6); +assert.strictEqual(net.isIP('2001:252:0:1::2008:6'), 6); +assert.strictEqual(net.isIP('2001:dead:beef:1::2008:6'), 6); +assert.strictEqual(net.isIP('2001::'), 6); +assert.strictEqual(net.isIP('2001:dead::'), 6); +assert.strictEqual(net.isIP('2001:dead:beef::'), 6); +assert.strictEqual(net.isIP('2001:dead:beef:1::'), 6); +assert.strictEqual(net.isIP('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), 6); +assert.strictEqual(net.isIP(':2001:252:0:1::2008:6:'), 0); +assert.strictEqual(net.isIP(':2001:252:0:1::2008:6'), 0); +assert.strictEqual(net.isIP('2001:252:0:1::2008:6:'), 0); +assert.strictEqual(net.isIP('2001:252::1::2008:6'), 0); +assert.strictEqual(net.isIP('::2001:252:1:2008:6'), 6); +assert.strictEqual(net.isIP('::2001:252:1:1.1.1.1'), 6); +assert.strictEqual(net.isIP('::2001:252:1:255.255.255.255'), 6); +assert.strictEqual(net.isIP('::2001:252:1:255.255.255.255.76'), 0); +assert.strictEqual(net.isIP('fe80::2008%eth0'), 6); +assert.strictEqual(net.isIP('fe80::2008%eth0.0'), 6); +assert.strictEqual(net.isIP('fe80::2008%eth0@1'), 0); +assert.strictEqual(net.isIP('::anything'), 0); +assert.strictEqual(net.isIP('::1'), 6); +assert.strictEqual(net.isIP('::'), 6); +assert.strictEqual(net.isIP('0000:0000:0000:0000:0000:0000:12345:0000'), 0); +assert.strictEqual(net.isIP('0'), 0); +assert.strictEqual(net.isIP(), 0); +assert.strictEqual(net.isIP(''), 0); +assert.strictEqual(net.isIP(null), 0); +assert.strictEqual(net.isIP(123), 0); +assert.strictEqual(net.isIP(true), 0); +assert.strictEqual(net.isIP({}), 0); +assert.strictEqual(net.isIP({ toString: () => '::2001:252:1:255.255.255.255' }), + 6); +assert.strictEqual(net.isIP({ toString: () => '127.0.0.1' }), 4); +assert.strictEqual(net.isIP({ toString: () => 'bla' }), 0); + +assert.strictEqual(net.isIPv4('127.0.0.1'), true); +assert.strictEqual(net.isIPv4('example.com'), false); +assert.strictEqual(net.isIPv4('2001:252:0:1::2008:6'), false); +assert.strictEqual(net.isIPv4(), false); +assert.strictEqual(net.isIPv4(''), false); +assert.strictEqual(net.isIPv4(null), false); +assert.strictEqual(net.isIPv4(123), false); +assert.strictEqual(net.isIPv4(true), false); +assert.strictEqual(net.isIPv4({}), false); +assert.strictEqual(net.isIPv4({ + toString: () => '::2001:252:1:255.255.255.255' +}), false); +assert.strictEqual(net.isIPv4({ toString: () => '127.0.0.1' }), true); +assert.strictEqual(net.isIPv4({ toString: () => 'bla' }), false); + +assert.strictEqual(net.isIPv6('127.0.0.1'), false); +assert.strictEqual(net.isIPv6('example.com'), false); +assert.strictEqual(net.isIPv6('2001:252:0:1::2008:6'), true); +assert.strictEqual(net.isIPv6(), false); +assert.strictEqual(net.isIPv6(''), false); +assert.strictEqual(net.isIPv6(null), false); +assert.strictEqual(net.isIPv6(123), false); +assert.strictEqual(net.isIPv6(true), false); +assert.strictEqual(net.isIPv6({}), false); +assert.strictEqual(net.isIPv6({ + toString: () => '::2001:252:1:255.255.255.255' +}), true); +assert.strictEqual(net.isIPv6({ toString: () => '127.0.0.1' }), false); +assert.strictEqual(net.isIPv6({ toString: () => 'bla' }), false); diff --git a/test/js/node/test/parallel/test-net-isipv4.js b/test/js/node/test/parallel/test-net-isipv4.js new file mode 100644 index 0000000000..2c478e6ac6 --- /dev/null +++ b/test/js/node/test/parallel/test-net-isipv4.js @@ -0,0 +1,46 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); + +const v4 = [ + '0.0.0.0', + '8.8.8.8', + '127.0.0.1', + '100.100.100.100', + '192.168.0.1', + '18.101.25.153', + '123.23.34.2', + '172.26.168.134', + '212.58.241.131', + '128.0.0.0', + '23.71.254.72', + '223.255.255.255', + '192.0.2.235', + '99.198.122.146', + '46.51.197.88', + '173.194.34.134', +]; + +const v4not = [ + '.100.100.100.100', + '100..100.100.100.', + '100.100.100.100.', + '999.999.999.999', + '256.256.256.256', + '256.100.100.100.100', + '123.123.123', + 'http://123.123.123', + '1000.2.3.4', + '999.2.3.4', + '0000000192.168.0.200', + '192.168.0.2000000000', +]; + +for (const ip of v4) { + assert.strictEqual(net.isIPv4(ip), true); +} + +for (const ip of v4not) { + assert.strictEqual(net.isIPv4(ip), false); +} diff --git a/test/js/node/test/parallel/test-net-isipv6.js b/test/js/node/test/parallel/test-net-isipv6.js new file mode 100644 index 0000000000..dbb8d80b7b --- /dev/null +++ b/test/js/node/test/parallel/test-net-isipv6.js @@ -0,0 +1,244 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); + +const v6 = [ + '::', + '1::', + '::1', + '1::8', + '1::7:8', + '1:2:3:4:5:6:7:8', + '1:2:3:4:5:6::8', + '1:2:3:4:5:6:7::', + '1:2:3:4:5::7:8', + '1:2:3:4:5::8', + '1:2:3::8', + '1::4:5:6:7:8', + '1::6:7:8', + '1::3:4:5:6:7:8', + '1:2:3:4::6:7:8', + '1:2::4:5:6:7:8', + '::2:3:4:5:6:7:8', + '1:2::8', + '2001:0000:1234:0000:0000:C1C0:ABCD:0876', + '3ffe:0b00:0000:0000:0001:0000:0000:000a', + 'FF02:0000:0000:0000:0000:0000:0000:0001', + '0000:0000:0000:0000:0000:0000:0000:0001', + '0000:0000:0000:0000:0000:0000:0000:0000', + '::ffff:192.168.1.26', + '2::10', + 'ff02::1', + 'fe80::', + '2002::', + '2001:db8::', + '2001:0db8:1234::', + '::ffff:0:0', + '::ffff:192.168.1.1', + '1:2:3:4::8', + '1::2:3:4:5:6:7', + '1::2:3:4:5:6', + '1::2:3:4:5', + '1::2:3:4', + '1::2:3', + '::2:3:4:5:6:7', + '::2:3:4:5:6', + '::2:3:4:5', + '::2:3:4', + '::2:3', + '::8', + '1:2:3:4:5:6::', + '1:2:3:4:5::', + '1:2:3:4::', + '1:2:3::', + '1:2::', + '1:2:3:4::7:8', + '1:2:3::7:8', + '1:2::7:8', + '1:2:3:4:5:6:1.2.3.4', + '1:2:3:4:5::1.2.3.4', + '1:2:3:4::1.2.3.4', + '1:2:3::1.2.3.4', + '1:2::1.2.3.4', + '1::1.2.3.4', + '1:2:3:4::5:1.2.3.4', + '1:2:3::5:1.2.3.4', + '1:2::5:1.2.3.4', + '1::5:1.2.3.4', + '1::5:11.22.33.44', + 'fe80::217:f2ff:254.7.237.98', + 'fe80::217:f2ff:fe07:ed62', + '2001:DB8:0:0:8:800:200C:417A', + 'FF01:0:0:0:0:0:0:101', + '0:0:0:0:0:0:0:1', + '0:0:0:0:0:0:0:0', + '2001:DB8::8:800:200C:417A', + 'FF01::101', + '0:0:0:0:0:0:13.1.68.3', + '0:0:0:0:0:FFFF:129.144.52.38', + '::13.1.68.3', + '::FFFF:129.144.52.38', + 'fe80:0000:0000:0000:0204:61ff:fe9d:f156', + 'fe80:0:0:0:204:61ff:fe9d:f156', + 'fe80::204:61ff:fe9d:f156', + 'fe80:0:0:0:204:61ff:254.157.241.86', + 'fe80::204:61ff:254.157.241.86', + 'fe80::1', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + '2001:db8:85a3:0:0:8a2e:370:7334', + '2001:db8:85a3::8a2e:370:7334', + '2001:0db8:0000:0000:0000:0000:1428:57ab', + '2001:0db8:0000:0000:0000::1428:57ab', + '2001:0db8:0:0:0:0:1428:57ab', + '2001:0db8:0:0::1428:57ab', + '2001:0db8::1428:57ab', + '2001:db8::1428:57ab', + '::ffff:12.34.56.78', + '::ffff:0c22:384e', + '2001:0db8:1234:0000:0000:0000:0000:0000', + '2001:0db8:1234:ffff:ffff:ffff:ffff:ffff', + '2001:db8:a::123', + '::ffff:192.0.2.128', + '::ffff:c000:280', + 'a:b:c:d:e:f:f1:f2', + 'a:b:c::d:e:f:f1', + 'a:b:c::d:e:f', + 'a:b:c::d:e', + 'a:b:c::d', + '::a', + '::a:b:c', + '::a:b:c:d:e:f:f1', + 'a::', + 'a:b:c::', + 'a:b:c:d:e:f:f1::', + 'a:bb:ccc:dddd:000e:00f:0f::', + '0:a:0:a:0:0:0:a', + '0:a:0:0:a:0:0:a', + '2001:db8:1:1:1:1:0:0', + '2001:db8:1:1:1:0:0:0', + '2001:db8:1:1:0:0:0:0', + '2001:db8:1:0:0:0:0:0', + '2001:db8:0:0:0:0:0:0', + '2001:0:0:0:0:0:0:0', + 'A:BB:CCC:DDDD:000E:00F:0F::', + '0:0:0:0:0:0:0:a', + '0:0:0:0:a:0:0:0', + '0:0:0:a:0:0:0:0', + 'a:0:0:a:0:0:a:a', + 'a:0:0:a:0:0:0:a', + 'a:0:0:0:a:0:0:a', + 'a:0:0:0:a:0:0:0', + 'a:0:0:0:0:0:0:0', + 'fe80::7:8%eth0', + 'fe80::7:8%1', +]; + +const v6not = [ + '', + '1:', + ':1', + '11:36:12', + '02001:0000:1234:0000:0000:C1C0:ABCD:0876', + '2001:0000:1234:0000:00001:C1C0:ABCD:0876', + '2001:0000:1234: 0000:0000:C1C0:ABCD:0876', + '2001:1:1:1:1:1:255Z255X255Y255', + '3ffe:0b00:0000:0001:0000:0000:000a', + 'FF02:0000:0000:0000:0000:0000:0000:0000:0001', + '3ffe:b00::1::a', + '::1111:2222:3333:4444:5555:6666::', + '1:2:3::4:5::7:8', + '12345::6:7:8', + '1::5:400.2.3.4', + '1::5:260.2.3.4', + '1::5:256.2.3.4', + '1::5:1.256.3.4', + '1::5:1.2.256.4', + '1::5:1.2.3.256', + '1::5:300.2.3.4', + '1::5:1.300.3.4', + '1::5:1.2.300.4', + '1::5:1.2.3.300', + '1::5:900.2.3.4', + '1::5:1.900.3.4', + '1::5:1.2.900.4', + '1::5:1.2.3.900', + '1::5:300.300.300.300', + '1::5:3000.30.30.30', + '1::400.2.3.4', + '1::260.2.3.4', + '1::256.2.3.4', + '1::1.256.3.4', + '1::1.2.256.4', + '1::1.2.3.256', + '1::300.2.3.4', + '1::1.300.3.4', + '1::1.2.300.4', + '1::1.2.3.300', + '1::900.2.3.4', + '1::1.900.3.4', + '1::1.2.900.4', + '1::1.2.3.900', + '1::300.300.300.300', + '1::3000.30.30.30', + '::400.2.3.4', + '::260.2.3.4', + '::256.2.3.4', + '::1.256.3.4', + '::1.2.256.4', + '::1.2.3.256', + '::300.2.3.4', + '::1.300.3.4', + '::1.2.300.4', + '::1.2.3.300', + '::900.2.3.4', + '::1.900.3.4', + '::1.2.900.4', + '::1.2.3.900', + '::300.300.300.300', + '::3000.30.30.30', + '2001:DB8:0:0:8:800:200C:417A:221', + 'FF01::101::2', + '1111:2222:3333:4444::5555:', + '1111:2222:3333::5555:', + '1111:2222::5555:', + '1111::5555:', + '::5555:', + ':::', + '1111:', + ':', + ':1111:2222:3333:4444::5555', + ':1111:2222:3333::5555', + ':1111:2222::5555', + ':1111::5555', + ':::5555', + '1.2.3.4:1111:2222:3333:4444::5555', + '1.2.3.4:1111:2222:3333::5555', + '1.2.3.4:1111:2222::5555', + '1.2.3.4:1111::5555', + '1.2.3.4::5555', + '1.2.3.4::', + 'fe80:0000:0000:0000:0204:61ff:254.157.241.086', + '123', + 'ldkfj', + '2001::FFD3::57ab', + '2001:db8:85a3::8a2e:37023:7334', + '2001:db8:85a3::8a2e:370k:7334', + '1:2:3:4:5:6:7:8:9', + '1::2::3', + '1:::3:4:5', + '1:2:3::4:5:6:7:8:9', + '::ffff:2.3.4', + '::ffff:257.1.2.3', + '::ffff:12345678901234567890.1.26', + '2001:0000:1234:0000:0000:C1C0:ABCD:0876 0', + '02001:0000:1234:0000:0000:C1C0:ABCD:0876', +]; + +for (const ip of v6) { + assert.strictEqual(net.isIPv6(ip), true); +} + +for (const ip of v6not) { + assert.strictEqual(net.isIPv6(ip), false); +} diff --git a/test/js/node/test/parallel/test-net-listen-after-destroying-stdin.js b/test/js/node/test/parallel/test-net-listen-after-destroying-stdin.js new file mode 100644 index 0000000000..4ffec304be --- /dev/null +++ b/test/js/node/test/parallel/test-net-listen-after-destroying-stdin.js @@ -0,0 +1,22 @@ +'use strict'; +// Just test that destroying stdin doesn't mess up listening on a server. +// This is a regression test for +// https://github.com/nodejs/node-v0.x-archive/issues/746. + +const common = require('../common'); +const net = require('net'); + +process.stdin.destroy(); + +const server = net.createServer(common.mustCall((socket) => { + console.log('accepted...'); + socket.end(common.mustCall(() => { console.log('finished...'); })); + server.close(common.mustCall(() => { console.log('closed'); })); +})); + + +server.listen(0, common.mustCall(() => { + console.log('listening...'); + + net.createConnection(server.address().port); +})); diff --git a/test/js/node/test/parallel/test-net-listen-error.js b/test/js/node/test/parallel/test-net-listen-error.js new file mode 100644 index 0000000000..05ca799d3e --- /dev/null +++ b/test/js/node/test/parallel/test-net-listen-error.js @@ -0,0 +1,29 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(function(socket) { +}); +server.listen(1, '1.1.1.1', common.mustNotCall()); // EACCES or EADDRNOTAVAIL +server.on('error', common.mustCall()); diff --git a/test/js/node/test/parallel/test-net-listen-exclusive-random-ports.js b/test/js/node/test/parallel/test-net-listen-exclusive-random-ports.js new file mode 100644 index 0000000000..66dfb59820 --- /dev/null +++ b/test/js/node/test/parallel/test-net-listen-exclusive-random-ports.js @@ -0,0 +1,37 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); + +if (cluster.isPrimary) { + const worker1 = cluster.fork(); + + worker1.on('message', function(port1) { + assert.strictEqual(port1, port1 | 0, + `first worker could not listen on port ${port1}`); + const worker2 = cluster.fork(); + + worker2.on('message', function(port2) { + assert.strictEqual(port2, port2 | 0, + `second worker could not listen on port ${port2}`); + assert.notStrictEqual(port1, port2, 'ports should not be equal'); + worker1.kill(); + worker2.kill(); + }); + }); +} else { + const server = net.createServer(() => {}); + + server.on('error', function(err) { + process.send(err.code); + }); + + server.listen({ + port: 0, + exclusive: true + }, function() { + process.send(server.address().port); + }); +} diff --git a/test/js/node/test/parallel/test-net-listen-handle-in-cluster-1.js b/test/js/node/test/parallel/test-net-listen-handle-in-cluster-1.js new file mode 100644 index 0000000000..07e002bf2a --- /dev/null +++ b/test/js/node/test/parallel/test-net-listen-handle-in-cluster-1.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); +const cluster = require('cluster'); + +// Test if the worker can listen with handle successfully +if (cluster.isPrimary) { + const worker = cluster.fork(); + const server = net.createServer(); + worker.on('online', common.mustCall(() => { + server.listen(common.mustCall(() => { + // Send the server to worker + worker.send(null, server); + })); + })); + worker.on('exit', common.mustCall(() => { + server.close(); + })); +} else { + // The `got` function of net.Server will create a TCP server by listen(handle) + // See lib/internal/child_process.js + process.on('message', common.mustCall((_, server) => { + assert.strictEqual(server instanceof net.Server, true); + process.exit(0); + })); +} diff --git a/test/js/node/test/parallel/test-net-listening.js b/test/js/node/test/parallel/test-net-listening.js new file mode 100644 index 0000000000..8f2880b0bf --- /dev/null +++ b/test/js/node/test/parallel/test-net-listening.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(); + +assert.strictEqual(server.listening, false); + +server.listen(0, common.mustCall(() => { + assert.strictEqual(server.listening, true); + + server.close(common.mustCall(() => { + assert.strictEqual(server.listening, false); + })); +})); diff --git a/test/js/node/test/parallel/test-net-local-address-port.js b/test/js/node/test/parallel/test-net-local-address-port.js new file mode 100644 index 0000000000..cfc6f61ef3 --- /dev/null +++ b/test/js/node/test/parallel/test-net-local-address-port.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(common.mustCall(function(socket) { + assert.strictEqual(socket.localAddress, common.localhostIPv4); + assert.strictEqual(socket.localPort, this.address().port); + assert.strictEqual(socket.localFamily, this.address().family); + socket.on('end', function() { + server.close(); + }); + socket.resume(); +})); + +server.listen(0, common.localhostIPv4, function() { + const client = net.createConnection(this.address() + .port, common.localhostIPv4); + client.on('connect', function() { + client.end(); + }); +}); diff --git a/test/js/node/test/parallel/test-net-remote-address.js b/test/js/node/test/parallel/test-net-remote-address.js new file mode 100644 index 0000000000..a116cb99d3 --- /dev/null +++ b/test/js/node/test/parallel/test-net-remote-address.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); +const { strictEqual } = require('assert'); + +const server = net.createServer(); + +server.listen(common.mustCall(function() { + const socket = net.connect({ port: server.address().port }); + + strictEqual(socket.connecting, true); + strictEqual(socket.remoteAddress, undefined); + + socket.on('connect', common.mustCall(function() { + strictEqual(socket.remoteAddress !== undefined, true); + socket.end(); + })); + + socket.on('end', common.mustCall(function() { + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-net-server-close-before-ipc-response.js b/test/js/node/test/parallel/test-net-server-close-before-ipc-response.js new file mode 100644 index 0000000000..e85bc96ee6 --- /dev/null +++ b/test/js/node/test/parallel/test-net-server-close-before-ipc-response.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); +const cluster = require('cluster'); + +// Process should exit +if (cluster.isPrimary) { + cluster.fork(); +} else { + const send = process.send; + process.send = function(message) { + // listenOnPrimaryHandle in net.js should call handle.close() + if (message.act === 'close') { + setImmediate(() => { + process.disconnect(); + }); + } + return send.apply(this, arguments); + }; + net.createServer().listen(0, common.mustNotCall()).close(); +} diff --git a/test/js/node/test/parallel/test-net-server-listen-remove-callback.js b/test/js/node/test/parallel/test-net-server-listen-remove-callback.js new file mode 100644 index 0000000000..a874099fb8 --- /dev/null +++ b/test/js/node/test/parallel/test-net-server-listen-remove-callback.js @@ -0,0 +1,44 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); + +// Server should only fire listen callback once +const server = net.createServer(); + +server.on('close', function() { + const listeners = server.listeners('listening'); + console.log('Closed, listeners:', listeners.length); + assert.strictEqual(listeners.length, 0); +}); + +server.listen(0, function() { + server.close(); +}); + +server.once('close', function() { + server.listen(0, function() { + server.close(); + }); +}); diff --git a/test/js/node/test/parallel/test-net-server-unref.js b/test/js/node/test/parallel/test-net-server-unref.js new file mode 100644 index 0000000000..935ba5d639 --- /dev/null +++ b/test/js/node/test/parallel/test-net-server-unref.js @@ -0,0 +1,30 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); + +const s = net.createServer(); +s.listen(0); +s.unref(); + +setTimeout(common.mustNotCall(), 1000).unref(); diff --git a/test/js/node/test/parallel/test-net-socket-byteswritten.js b/test/js/node/test/parallel/test-net-socket-byteswritten.js new file mode 100644 index 0000000000..b7b7af89e2 --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-byteswritten.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(function(socket) { + socket.end(); +}); + +server.listen(0, common.mustCall(function() { + const socket = net.connect(server.address().port); + + // Cork the socket, then write twice; this should cause a writev, which + // previously caused an err in the bytesWritten count. + socket.cork(); + + socket.write('one'); + socket.write(Buffer.from('twø', 'utf8')); + + socket.uncork(); + + // one = 3 bytes, twø = 4 bytes + assert.strictEqual(socket.bytesWritten, 3 + 4); + + socket.on('connect', common.mustCall(function() { + assert.strictEqual(socket.bytesWritten, 3 + 4); + })); + + socket.on('end', common.mustCall(function() { + assert.strictEqual(socket.bytesWritten, 3 + 4); + + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-net-socket-close-after-end.js b/test/js/node/test/parallel/test-net-socket-close-after-end.js new file mode 100644 index 0000000000..06bf55f89d --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-close-after-end.js @@ -0,0 +1,31 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(); + +server.on('connection', (socket) => { + let endEmitted = false; + + socket.once('readable', () => { + setTimeout(() => { + socket.read(); + }, common.platformTimeout(100)); + }); + socket.on('end', () => { + endEmitted = true; + }); + socket.on('close', () => { + assert(endEmitted); + server.close(); + }); + socket.end('foo'); +}); + +server.listen(common.mustCall(() => { + const socket = net.createConnection(server.address().port, () => { + socket.end('foo'); + }); +})); diff --git a/test/js/node/test/parallel/test-net-socket-connect-without-cb.js b/test/js/node/test/parallel/test-net-socket-connect-without-cb.js new file mode 100644 index 0000000000..274083eb29 --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-connect-without-cb.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that socket.connect can be called without callback +// which is optional. + +const net = require('net'); + +const server = net.createServer(common.mustCall(function(conn) { + conn.end(); + server.close(); +})).listen(0, common.mustCall(function() { + const client = new net.Socket(); + + client.on('connect', common.mustCall(function() { + client.end(); + })); + + const address = server.address(); + if (!common.hasIPv6 && address.family === 'IPv6') { + // Necessary to pass CI running inside containers. + client.connect(address.port); + } else { + client.connect(address); + } +})); diff --git a/test/js/node/test/parallel/test-net-socket-connecting.js b/test/js/node/test/parallel/test-net-socket-connecting.js new file mode 100644 index 0000000000..21aa261192 --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-connecting.js @@ -0,0 +1,21 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer((conn) => { + conn.end(); + server.close(); +}).listen(0, () => { + const client = net.connect(server.address().port, () => { + assert.strictEqual(client.connecting, false); + + // Legacy getter + assert.strictEqual(client._connecting, false); + client.end(); + }); + assert.strictEqual(client.connecting, true); + + // Legacy getter + assert.strictEqual(client._connecting, true); +}); diff --git a/test/js/node/test/parallel/test-net-socket-end-before-connect.js b/test/js/node/test/parallel/test-net-socket-end-before-connect.js new file mode 100644 index 0000000000..d40c90620e --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-end-before-connect.js @@ -0,0 +1,13 @@ +'use strict'; + +const common = require('../common'); + +const net = require('net'); + +const server = net.createServer(); + +server.listen(common.mustCall(() => { + const socket = net.createConnection(server.address().port); + socket.on('close', common.mustCall(() => server.close())); + socket.end(); +})); diff --git a/test/js/node/test/parallel/test-net-socket-ready-without-cb.js b/test/js/node/test/parallel/test-net-socket-ready-without-cb.js new file mode 100644 index 0000000000..29da68e173 --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-ready-without-cb.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that socket.connect can be called without callback +// which is optional. + +const net = require('net'); + +const server = net.createServer(common.mustCall(function(conn) { + conn.end(); + server.close(); +})).listen(0, 'localhost', common.mustCall(function() { + const client = new net.Socket(); + + client.on('ready', common.mustCall(function() { + client.end(); + })); + + client.connect(server.address()); +})); diff --git a/test/js/node/test/parallel/test-net-socket-timeout-unref.js b/test/js/node/test/parallel/test-net-socket-timeout-unref.js new file mode 100644 index 0000000000..ae6bde49ab --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-timeout-unref.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +// Test that unref'ed sockets with timeouts do not prevent exit. + +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(function(c) { + c.write('hello'); + c.unref(); +}); +server.listen(0); +server.unref(); + +let connections = 0; +const sockets = []; +const delays = [8, 5, 3, 6, 2, 4]; + +delays.forEach(function(T) { + const socket = net.createConnection(server.address().port, 'localhost'); + socket.on('connect', common.mustCall(function() { + if (++connections === delays.length) { + sockets.forEach(function(s) { + s.socket.setTimeout(s.timeout, function() { + s.socket.destroy(); + throw new Error('socket timed out unexpectedly'); + }); + + s.socket.unref(); + }); + } + })); + + sockets.push({ socket: socket, timeout: T * 1000 }); +}); diff --git a/test/js/node/test/parallel/test-net-socket-write-error.js b/test/js/node/test/parallel/test-net-socket-write-error.js new file mode 100644 index 0000000000..e68db68c0d --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-write-error.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const server = net.createServer().listen(0, connectToServer); + +function connectToServer() { + const client = net.createConnection(this.address().port, () => { + client.on('error', common.mustNotCall()); + assert.throws(() => { + client.write(1337); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + + client.destroy(); + }) + .on('close', () => server.close()); +} diff --git a/test/js/node/test/parallel/test-net-writable.js b/test/js/node/test/parallel/test-net-writable.js new file mode 100644 index 0000000000..3659869efb --- /dev/null +++ b/test/js/node/test/parallel/test-net-writable.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(common.mustCall(function(s) { + server.close(); + s.end(); +})).listen(0, '127.0.0.1', common.mustCall(function() { + const socket = net.connect(this.address().port, '127.0.0.1'); + socket.on('end', common.mustCall(() => { + assert.strictEqual(socket.writable, true); + socket.write('hello world'); + })); +})); diff --git a/test/js/node/test/parallel/test-net-write-cb-on-destroy-before-connect.js b/test/js/node/test/parallel/test-net-write-cb-on-destroy-before-connect.js new file mode 100644 index 0000000000..99efb66034 --- /dev/null +++ b/test/js/node/test/parallel/test-net-write-cb-on-destroy-before-connect.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0, common.mustCall(() => { + const socket = new net.Socket(); + + socket.on('connect', common.mustNotCall()); + + socket.connect({ + port: server.address().port, + }); + + assert(socket.connecting); + + socket.write('foo', common.expectsError({ + code: 'ERR_SOCKET_CLOSED_BEFORE_CONNECTION', + name: 'Error' + })); + + socket.destroy(); + server.close(); +})); diff --git a/test/js/node/test/parallel/test-net-write-connect-write.js b/test/js/node/test/parallel/test-net-write-connect-write.js new file mode 100644 index 0000000000..1f09b04f17 --- /dev/null +++ b/test/js/node/test/parallel/test-net-write-connect-write.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(function(socket) { + socket.pipe(socket); +}).listen(0, common.mustCall(function() { + const conn = net.connect(this.address().port); + let received = ''; + + conn.setEncoding('utf8'); + conn.write('before'); + conn.on('connect', function() { + conn.write(' after'); + }); + conn.on('data', function(buf) { + received += buf; + conn.end(); + }); + conn.on('end', common.mustCall(function() { + server.close(); + assert.strictEqual(received, 'before after'); + })); +})); diff --git a/test/js/node/test/parallel/test-net-write-slow.js b/test/js/node/test/parallel/test-net-write-slow.js new file mode 100644 index 0000000000..cf2d5790d9 --- /dev/null +++ b/test/js/node/test/parallel/test-net-write-slow.js @@ -0,0 +1,63 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const SIZE = 2E5; +const N = 10; +let flushed = 0; +let received = 0; +const buf = Buffer.alloc(SIZE, 'a'); + +const server = net.createServer(function(socket) { + socket.setNoDelay(); + socket.setTimeout(9999); + socket.on('timeout', function() { + assert.fail(`flushed: ${flushed}, received: ${received}/${SIZE * N}`); + }); + + for (let i = 0; i < N; ++i) { + socket.write(buf, function() { + ++flushed; + if (flushed === N) { + socket.setTimeout(0); + } + }); + } + socket.end(); + +}).listen(0, common.mustCall(function() { + const conn = net.connect(this.address().port); + conn.on('data', function(buf) { + received += buf.length; + conn.pause(); + setTimeout(function() { + conn.resume(); + }, 20); + }); + conn.on('end', common.mustCall(function() { + server.close(); + assert.strictEqual(received, SIZE * N); + })); +})); diff --git a/test/js/node/test/parallel/test-next-tick-doesnt-hang.js b/test/js/node/test/parallel/test-next-tick-doesnt-hang.js new file mode 100644 index 0000000000..36c1740bbf --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-doesnt-hang.js @@ -0,0 +1,30 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +// This test verifies that having a single nextTick statement and nothing else +// does not hang the event loop. If this test times out it has failed. + +require('../common'); +process.nextTick(function() { + // Nothing +}); diff --git a/test/js/node/test/parallel/test-next-tick-domain.js b/test/js/node/test/parallel/test-next-tick-domain.js new file mode 100644 index 0000000000..3e55ef3225 --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-domain.js @@ -0,0 +1,31 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const origNextTick = process.nextTick; + +require('domain'); + +// Requiring domain should not change nextTick. +assert.strictEqual(origNextTick, process.nextTick); diff --git a/test/js/node/test/parallel/test-next-tick-errors.js b/test/js/node/test/parallel/test-next-tick-errors.js new file mode 100644 index 0000000000..6fd079625a --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-errors.js @@ -0,0 +1,74 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const order = []; +let exceptionHandled = false; + +// This nextTick function will throw an error. It should only be called once. +// When it throws an error, it should still get removed from the queue. +process.nextTick(function() { + order.push('A'); + // cause an error + what(); // eslint-disable-line no-undef +}); + +// This nextTick function should remain in the queue when the first one +// is removed. It should be called if the error in the first one is +// caught (which we do in this test). +process.nextTick(function() { + order.push('C'); +}); + +function testNextTickWith(val) { + assert.throws(() => { + process.nextTick(val); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +} + +testNextTickWith(false); +testNextTickWith(true); +testNextTickWith(1); +testNextTickWith('str'); +testNextTickWith({}); +testNextTickWith([]); + +process.on('uncaughtException', function(err, errorOrigin) { + assert.strictEqual(errorOrigin, 'uncaughtException'); + + if (!exceptionHandled) { + exceptionHandled = true; + order.push('B'); + } else { + // If we get here then the first process.nextTick got called twice + order.push('OOPS!'); + } +}); + +process.on('exit', function() { + assert.deepStrictEqual(order, ['A', 'B', 'C']); +}); diff --git a/test/js/node/test/parallel/test-next-tick-fixed-queue-regression.js b/test/js/node/test/parallel/test-next-tick-fixed-queue-regression.js new file mode 100644 index 0000000000..1fe82d02b1 --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-fixed-queue-regression.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); + +// This tests a highly specific regression tied to the FixedQueue size, which +// was introduced in Node.js 9.7.0: https://github.com/nodejs/node/pull/18617 +// More specifically, a nextTick list could potentially end up not fully +// clearing in one run through if exactly 2048 ticks were added after +// microtasks were executed within the nextTick loop. + +process.nextTick(() => { + Promise.resolve(1).then(() => { + for (let i = 0; i < 2047; i++) + process.nextTick(common.mustCall()); + const immediate = setImmediate(common.mustNotCall()); + process.nextTick(common.mustCall(() => clearImmediate(immediate))); + }); +}); diff --git a/test/js/node/test/parallel/test-next-tick-intentional-starvation.js b/test/js/node/test/parallel/test-next-tick-intentional-starvation.js new file mode 100644 index 0000000000..ed357cb233 --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-intentional-starvation.js @@ -0,0 +1,61 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// This is the inverse of test-next-tick-starvation. it verifies +// that process.nextTick will *always* come before other events + +let ran = false; +let starved = false; +const start = +new Date(); +let timerRan = false; + +function spin() { + ran = true; + const now = +new Date(); + if (now - start > 100) { + console.log('The timer is starving, just as we planned.'); + starved = true; + + // now let it out. + return; + } + + process.nextTick(spin); +} + +function onTimeout() { + if (!starved) throw new Error('The timer escaped!'); + console.log('The timer ran once the ban was lifted'); + timerRan = true; +} + +spin(); +setTimeout(onTimeout, 50); + +process.on('exit', function() { + assert.ok(ran); + assert.ok(starved); + assert.ok(timerRan); +}); diff --git a/test/js/node/test/parallel/test-next-tick-ordering.js b/test/js/node/test/parallel/test-next-tick-ordering.js new file mode 100644 index 0000000000..8d3ee6488c --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-ordering.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +let i; + +const N = 30; +const done = []; + +function get_printer(timeout) { + return function() { + console.log(`Running from setTimeout ${timeout}`); + done.push(timeout); + }; +} + +process.nextTick(function() { + console.log('Running from nextTick'); + done.push('nextTick'); +}); + +for (i = 0; i < N; i += 1) { + setTimeout(get_printer(i), i); +} + +console.log('Running from main.'); + + +process.on('exit', function() { + assert.strictEqual(done[0], 'nextTick'); + // Disabling this test. I don't think we can ensure the order + // for (i = 0; i < N; i += 1) { + // assert.strictEqual(i, done[i + 1]); + // } +}); diff --git a/test/js/node/test/parallel/test-next-tick-ordering2.js b/test/js/node/test/parallel/test-next-tick-ordering2.js new file mode 100644 index 0000000000..6c42bd8e57 --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-ordering2.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const order = []; +process.nextTick(function() { + setTimeout(function() { + order.push('setTimeout'); + }, 0); + + process.nextTick(function() { + order.push('nextTick'); + }); +}); + +process.on('exit', function() { + assert.deepStrictEqual(order, ['nextTick', 'setTimeout']); +}); diff --git a/test/js/node/test/parallel/test-next-tick-when-exiting.js b/test/js/node/test/parallel/test-next-tick-when-exiting.js new file mode 100644 index 0000000000..36dc296646 --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick-when-exiting.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +process.on('exit', () => { + assert.strictEqual(process._exiting, true); + + process.nextTick( + common.mustNotCall('process is exiting, should not be called') + ); +}); + +process.exit(); diff --git a/test/js/node/test/parallel/test-next-tick.js b/test/js/node/test/parallel/test-next-tick.js new file mode 100644 index 0000000000..47823f45bc --- /dev/null +++ b/test/js/node/test/parallel/test-next-tick.js @@ -0,0 +1,63 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); + +process.nextTick(common.mustCall(function() { + process.nextTick(common.mustCall(function() { + process.nextTick(common.mustCall()); + })); +})); + +setTimeout(common.mustCall(function() { + process.nextTick(common.mustCall()); +}), 50); + +process.nextTick(common.mustCall()); + +const obj = {}; + +process.nextTick(function(a, b) { + assert.strictEqual(a, 42); + assert.strictEqual(b, obj); + assert.strictEqual(this, undefined); +}, 42, obj); + +process.nextTick((a, b) => { + assert.strictEqual(a, 42); + assert.strictEqual(b, obj); + assert.deepStrictEqual(this, {}); +}, 42, obj); + +process.nextTick(function() { + assert.strictEqual(this, undefined); +}, 1, 2, 3, 4); + +process.nextTick(() => { + assert.deepStrictEqual(this, {}); +}, 1, 2, 3, 4); + +process.on('exit', function() { + process.nextTick(common.mustNotCall()); +}); diff --git a/test/js/node/test/parallel/test-no-node-snapshot.js b/test/js/node/test/parallel/test-no-node-snapshot.js new file mode 100644 index 0000000000..a636040a4c --- /dev/null +++ b/test/js/node/test/parallel/test-no-node-snapshot.js @@ -0,0 +1,5 @@ +'use strict'; + +// Flags: --no-node-snapshot + +require('../common'); diff --git a/test/js/node/test/parallel/test-os-eol.js b/test/js/node/test/parallel/test-os-eol.js new file mode 100644 index 0000000000..412751a151 --- /dev/null +++ b/test/js/node/test/parallel/test-os-eol.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const os = require('os'); + +const eol = common.isWindows ? '\r\n' : '\n'; + +assert.strictEqual(os.EOL, eol); + +// Test that the `Error` is a `TypeError` but do not check the message as it +// varies between different JavaScript engines. +assert.throws(function() { os.EOL = 123; }, TypeError); + +const foo = 'foo'; +Object.defineProperties(os, { + EOL: { + configurable: true, + enumerable: true, + writable: false, + value: foo + } +}); +assert.strictEqual(os.EOL, foo); diff --git a/test/js/node/test/parallel/test-os-homedir-no-envvar.js b/test/js/node/test/parallel/test-os-homedir-no-envvar.js new file mode 100644 index 0000000000..75d439b2ed --- /dev/null +++ b/test/js/node/test/parallel/test-os-homedir-no-envvar.js @@ -0,0 +1,31 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +const os = require('os'); +const path = require('path'); + + +if (process.argv[2] === 'child') { + if (common.isWindows) + assert.strictEqual(process.env.USERPROFILE, undefined); + else + assert.strictEqual(process.env.HOME, undefined); + + const home = os.homedir(); + + assert.strictEqual(typeof home, 'string'); + assert(home.includes(path.sep)); +} else { + if (common.isWindows) + delete process.env.USERPROFILE; + else + delete process.env.HOME; + + const child = cp.spawnSync(process.execPath, [__filename, 'child'], { + env: process.env, + stdio: 'inherit', + }); + + assert.strictEqual(child.status, 0); +} diff --git a/test/js/node/test/parallel/test-os-process-priority.js b/test/js/node/test/parallel/test-os-process-priority.js new file mode 100644 index 0000000000..2edabf53df --- /dev/null +++ b/test/js/node/test/parallel/test-os-process-priority.js @@ -0,0 +1,145 @@ +'use strict'; +const common = require('../common'); +// IBMi process priority is different. +if (common.isIBMi) + common.skip('IBMi has a different process priority'); + +const assert = require('assert'); +const os = require('os'); +const { + PRIORITY_LOW, + PRIORITY_BELOW_NORMAL, + PRIORITY_NORMAL, + PRIORITY_ABOVE_NORMAL, + PRIORITY_HIGH, + PRIORITY_HIGHEST +} = os.constants.priority; + +// Validate priority constants. +assert.strictEqual(typeof PRIORITY_LOW, 'number'); +assert.strictEqual(typeof PRIORITY_BELOW_NORMAL, 'number'); +assert.strictEqual(typeof PRIORITY_NORMAL, 'number'); +assert.strictEqual(typeof PRIORITY_ABOVE_NORMAL, 'number'); +assert.strictEqual(typeof PRIORITY_HIGH, 'number'); +assert.strictEqual(typeof PRIORITY_HIGHEST, 'number'); + +// Test pid type validation. +[null, true, false, 'foo', {}, [], /x/].forEach((pid) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "pid" argument must be of type number\./ + }; + + assert.throws(() => { + os.setPriority(pid, PRIORITY_NORMAL); + }, errObj); + + assert.throws(() => { + os.getPriority(pid); + }, errObj); +}); + +// Test pid range validation. +[NaN, Infinity, -Infinity, 3.14, 2 ** 32].forEach((pid) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + message: /The value of "pid" is out of range\./ + }; + + assert.throws(() => { + os.setPriority(pid, PRIORITY_NORMAL); + }, errObj); + + assert.throws(() => { + os.getPriority(pid); + }, errObj); +}); + +// Test priority type validation. +[null, true, false, 'foo', {}, [], /x/].forEach((priority) => { + assert.throws(() => { + os.setPriority(0, priority); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "priority" argument must be of type number\./ + }); +}); + +// Test priority range validation. +[ + NaN, + Infinity, + -Infinity, + 3.14, + 2 ** 32, + PRIORITY_HIGHEST - 1, + PRIORITY_LOW + 1, +].forEach((priority) => { + assert.throws(() => { + os.setPriority(0, priority); + }, { + code: 'ERR_OUT_OF_RANGE', + message: /The value of "priority" is out of range\./ + }); +}); + +// Verify that valid values work. +for (let i = PRIORITY_HIGHEST; i <= PRIORITY_LOW; i++) { + // A pid of 0 corresponds to the current process. + try { + os.setPriority(0, i); + } catch (err) { + // The current user might not have sufficient permissions to set this + // specific priority level. Skip this priority, but keep trying lower + // priorities. + if (err.info.code === 'EACCES') + continue; + + assert(err); + } + + checkPriority(0, i); + + // An undefined pid corresponds to the current process. + os.setPriority(i); + checkPriority(undefined, i); + + // Specifying the actual pid works. + os.setPriority(process.pid, i); + checkPriority(process.pid, i); +} + +{ + assert.throws(() => { os.getPriority(-1); }, { + code: 'ERR_SYSTEM_ERROR', + message: /A system error occurred: uv_os_getpriority returned /, + name: 'SystemError' + }); +} + + +function checkPriority(pid, expected) { + const priority = os.getPriority(pid); + + // Verify that the priority values match on Unix, and are range mapped on + // Windows. + if (!common.isWindows) { + assert.strictEqual(priority, expected); + return; + } + + // On Windows setting PRIORITY_HIGHEST will only work for elevated user, + // for others it will be silently reduced to PRIORITY_HIGH + if (expected < PRIORITY_HIGH) + assert.ok(priority === PRIORITY_HIGHEST || priority === PRIORITY_HIGH); + else if (expected < PRIORITY_ABOVE_NORMAL) + assert.strictEqual(priority, PRIORITY_HIGH); + else if (expected < PRIORITY_NORMAL) + assert.strictEqual(priority, PRIORITY_ABOVE_NORMAL); + else if (expected < PRIORITY_BELOW_NORMAL) + assert.strictEqual(priority, PRIORITY_NORMAL); + else if (expected < PRIORITY_LOW) + assert.strictEqual(priority, PRIORITY_BELOW_NORMAL); + else + assert.strictEqual(priority, PRIORITY_LOW); +} diff --git a/test/js/node/test/parallel/test-os-userinfo-handles-getter-errors.js b/test/js/node/test/parallel/test-os-userinfo-handles-getter-errors.js new file mode 100644 index 0000000000..ca7b560012 --- /dev/null +++ b/test/js/node/test/parallel/test-os-userinfo-handles-getter-errors.js @@ -0,0 +1,19 @@ +'use strict'; +// Tests that os.userInfo correctly handles errors thrown by option property +// getters. See https://github.com/nodejs/node/issues/12370. + +const common = require('../common'); +const assert = require('assert'); +const execFile = require('child_process').execFile; + +const script = `os.userInfo({ + get encoding() { + throw new Error('xyz'); + } +})`; + +const node = process.execPath; +execFile(node, [ '-e', script ], common.mustCall((err, stdout, stderr) => { + // Edited for Bun to lowercase `error` + assert(stderr.includes('xyz'), 'userInfo crashes'); +})); diff --git a/test/js/node/test/parallel/test-os.js b/test/js/node/test/parallel/test-os.js new file mode 100644 index 0000000000..3d9fe5c1a6 --- /dev/null +++ b/test/js/node/test/parallel/test-os.js @@ -0,0 +1,281 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const os = require('os'); +const path = require('path'); +const { inspect } = require('util'); + +const is = { + number: (value, key) => { + assert(!Number.isNaN(value), `${key} should not be NaN`); + assert.strictEqual(typeof value, 'number'); + }, + string: (value) => { assert.strictEqual(typeof value, 'string'); }, + array: (value) => { assert.ok(Array.isArray(value)); }, + object: (value) => { + assert.strictEqual(typeof value, 'object'); + assert.notStrictEqual(value, null); + } +}; + +process.env.TMPDIR = '/tmpdir'; +process.env.TMP = '/tmp'; +process.env.TEMP = '/temp'; +if (common.isWindows) { + assert.strictEqual(os.tmpdir(), '/temp'); + process.env.TEMP = ''; + assert.strictEqual(os.tmpdir(), '/tmp'); + process.env.TMP = ''; + const expected = `${process.env.SystemRoot || process.env.windir}\\temp`; + assert.strictEqual(os.tmpdir(), expected); + process.env.TEMP = '\\temp\\'; + assert.strictEqual(os.tmpdir(), '\\temp'); + process.env.TEMP = '\\tmpdir/'; + assert.strictEqual(os.tmpdir(), '\\tmpdir/'); + process.env.TEMP = '\\'; + assert.strictEqual(os.tmpdir(), '\\'); + process.env.TEMP = 'C:\\'; + assert.strictEqual(os.tmpdir(), 'C:\\'); +} else { + assert.strictEqual(os.tmpdir(), '/tmpdir'); + process.env.TMPDIR = ''; + assert.strictEqual(os.tmpdir(), '/tmp'); + process.env.TMP = ''; + assert.strictEqual(os.tmpdir(), '/temp'); + process.env.TEMP = ''; + assert.strictEqual(os.tmpdir(), '/tmp'); + process.env.TMPDIR = '/tmpdir/'; + assert.strictEqual(os.tmpdir(), '/tmpdir'); + process.env.TMPDIR = '/tmpdir\\'; + assert.strictEqual(os.tmpdir(), '/tmpdir\\'); + process.env.TMPDIR = '/'; + assert.strictEqual(os.tmpdir(), '/'); +} + +const endianness = os.endianness(); +is.string(endianness); +assert.match(endianness, /[BL]E/); + +const hostname = os.hostname(); +is.string(hostname); +assert.ok(hostname.length > 0); + +// IBMi process priority is different. +if (!common.isIBMi) { + const { PRIORITY_BELOW_NORMAL, PRIORITY_LOW } = os.constants.priority; + // Priority means niceness: higher numeric value <=> lower priority + const LOWER_PRIORITY = os.getPriority() < PRIORITY_BELOW_NORMAL ? PRIORITY_BELOW_NORMAL : PRIORITY_LOW; + os.setPriority(LOWER_PRIORITY); + const priority = os.getPriority(); + is.number(priority); + assert.strictEqual(priority, LOWER_PRIORITY); +} + +// On IBMi, os.uptime() returns 'undefined' +if (!common.isIBMi) { + const uptime = os.uptime(); + is.number(uptime); + assert.ok(uptime > 0); +} + +const cpus = os.cpus(); +is.array(cpus); +assert.ok(cpus.length > 0); +for (const cpu of cpus) { + assert.strictEqual(typeof cpu.model, 'string'); + assert.strictEqual(typeof cpu.speed, 'number'); + assert.strictEqual(typeof cpu.times.user, 'number'); + assert.strictEqual(typeof cpu.times.nice, 'number'); + assert.strictEqual(typeof cpu.times.sys, 'number'); + assert.strictEqual(typeof cpu.times.idle, 'number'); + assert.strictEqual(typeof cpu.times.irq, 'number'); +} + +const type = os.type(); +is.string(type); +assert.ok(type.length > 0); + +const release = os.release(); +is.string(release); +assert.ok(release.length > 0); +// TODO: Check format on more than just AIX +if (common.isAIX) + assert.match(release, /^\d+\.\d+$/); + +const platform = os.platform(); +is.string(platform); +assert.ok(platform.length > 0); + +const arch = os.arch(); +is.string(arch); +assert.ok(arch.length > 0); + +if (!common.isSunOS) { + // not implemented yet + assert.ok(os.loadavg().length > 0); + assert.ok(os.freemem() > 0); + assert.ok(os.totalmem() > 0); +} + +const interfaces = os.networkInterfaces(); +switch (platform) { + case 'linux': { + const filter = (e) => + e.address === '127.0.0.1' && + e.netmask === '255.0.0.0'; + + const actual = interfaces.lo.filter(filter); + const expected = [{ + address: '127.0.0.1', + netmask: '255.0.0.0', + family: 'IPv4', + mac: '00:00:00:00:00:00', + internal: true, + cidr: '127.0.0.1/8' + }]; + assert.deepStrictEqual(actual, expected); + break; + } + case 'win32': { + const filter = (e) => + e.address === '127.0.0.1'; + + const actual = interfaces['Loopback Pseudo-Interface 1'].filter(filter); + const expected = [{ + address: '127.0.0.1', + netmask: '255.0.0.0', + family: 'IPv4', + mac: '00:00:00:00:00:00', + internal: true, + cidr: '127.0.0.1/8' + }]; + assert.deepStrictEqual(actual, expected); + break; + } +} +const netmaskToCIDRSuffixMap = new Map(Object.entries({ + '255.0.0.0': 8, + '255.255.255.0': 24, + 'ffff:ffff:ffff:ffff::': 64, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff': 128 +})); + +Object.values(interfaces) + .flat(Infinity) + .map((v) => ({ v, mask: netmaskToCIDRSuffixMap.get(v.netmask) })) + .forEach(({ v, mask }) => { + assert.ok('cidr' in v, `"cidr" prop not found in ${inspect(v)}`); + if (mask) { + assert.strictEqual(v.cidr, `${v.address}/${mask}`); + } + }); + +const EOL = os.EOL; +if (common.isWindows) { + assert.strictEqual(EOL, '\r\n'); +} else { + assert.strictEqual(EOL, '\n'); +} + +const home = os.homedir(); +is.string(home); +assert.ok(home.includes(path.sep)); + +const version = os.version(); +assert.strictEqual(typeof version, 'string'); +assert(version); + +if (common.isWindows && process.env.USERPROFILE) { + assert.strictEqual(home, process.env.USERPROFILE); + delete process.env.USERPROFILE; + assert.ok(os.homedir().includes(path.sep)); + process.env.USERPROFILE = home; +} else if (!common.isWindows && process.env.HOME) { + assert.strictEqual(home, process.env.HOME); + delete process.env.HOME; + assert.ok(os.homedir().includes(path.sep)); + process.env.HOME = home; +} + +const pwd = os.userInfo(); +is.object(pwd); +const pwdBuf = os.userInfo({ encoding: 'buffer' }); + +if (common.isWindows) { + assert.strictEqual(pwd.uid, -1); + assert.strictEqual(pwd.gid, -1); + assert.strictEqual(pwd.shell, null); + assert.strictEqual(pwdBuf.uid, -1); + assert.strictEqual(pwdBuf.gid, -1); + assert.strictEqual(pwdBuf.shell, null); +} else { + is.number(pwd.uid); + is.number(pwd.gid); + assert.strictEqual(typeof pwd.shell, 'string'); + // It's possible for /etc/passwd to leave the user's shell blank. + if (pwd.shell.length > 0) { + assert(pwd.shell.includes(path.sep)); + } + assert.strictEqual(pwd.uid, pwdBuf.uid); + assert.strictEqual(pwd.gid, pwdBuf.gid); + assert.strictEqual(pwd.shell, pwdBuf.shell.toString('utf8')); +} + +is.string(pwd.username); +assert.ok(pwd.homedir.includes(path.sep)); +assert.strictEqual(pwd.username, pwdBuf.username.toString('utf8')); +assert.strictEqual(pwd.homedir, pwdBuf.homedir.toString('utf8')); + +assert.strictEqual(`${os.hostname}`, os.hostname()); +assert.strictEqual(`${os.homedir}`, os.homedir()); +assert.strictEqual(`${os.release}`, os.release()); +assert.strictEqual(`${os.type}`, os.type()); +assert.strictEqual(`${os.endianness}`, os.endianness()); +assert.strictEqual(`${os.tmpdir}`, os.tmpdir()); +assert.strictEqual(`${os.arch}`, os.arch()); +assert.strictEqual(`${os.platform}`, os.platform()); +assert.strictEqual(`${os.version}`, os.version()); +assert.strictEqual(`${os.machine}`, os.machine()); +assert.strictEqual(+os.totalmem, os.totalmem()); + +// Assert that the following values are coercible to numbers. +// On IBMi, os.uptime() returns 'undefined' +if (!common.isIBMi) { + is.number(+os.uptime, 'uptime'); + is.number(os.uptime(), 'uptime'); +} + +is.number(+os.availableParallelism, 'availableParallelism'); +is.number(os.availableParallelism(), 'availableParallelism'); +is.number(+os.freemem, 'freemem'); +is.number(os.freemem(), 'freemem'); + +const devNull = os.devNull; +if (common.isWindows) { + assert.strictEqual(devNull, '\\\\.\\nul'); +} else { + assert.strictEqual(devNull, '/dev/null'); +} + +assert.ok(os.availableParallelism() > 0); diff --git a/test/js/node/test/parallel/test-outgoing-message-destroy.js b/test/js/node/test/parallel/test-outgoing-message-destroy.js new file mode 100644 index 0000000000..0ee7b5f40e --- /dev/null +++ b/test/js/node/test/parallel/test-outgoing-message-destroy.js @@ -0,0 +1,13 @@ +'use strict'; + +// Test that http.OutgoingMessage,prototype.destroy() returns `this`. +require('../common'); + +const assert = require('assert'); +const http = require('http'); +const outgoingMessage = new http.OutgoingMessage(); + +assert.strictEqual(outgoingMessage.destroyed, false); +assert.strictEqual(outgoingMessage.destroy(), outgoingMessage); +assert.strictEqual(outgoingMessage.destroyed, true); +assert.strictEqual(outgoingMessage.destroy(), outgoingMessage); diff --git a/test/js/node/test/parallel/test-path-basename.js b/test/js/node/test/parallel/test-path-basename.js new file mode 100644 index 0000000000..b16f9e5d63 --- /dev/null +++ b/test/js/node/test/parallel/test-path-basename.js @@ -0,0 +1,76 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const path = require('path'); + +assert.strictEqual(path.basename(__filename), 'test-path-basename.js'); +assert.strictEqual(path.basename(__filename, '.js'), 'test-path-basename'); +assert.strictEqual(path.basename('.js', '.js'), ''); +assert.strictEqual(path.basename('js', '.js'), 'js'); +assert.strictEqual(path.basename('file.js', '.ts'), 'file.js'); +assert.strictEqual(path.basename('file', '.js'), 'file'); +assert.strictEqual(path.basename('file.js.old', '.js.old'), 'file'); +assert.strictEqual(path.basename(''), ''); +assert.strictEqual(path.basename('/dir/basename.ext'), 'basename.ext'); +assert.strictEqual(path.basename('/basename.ext'), 'basename.ext'); +assert.strictEqual(path.basename('basename.ext'), 'basename.ext'); +assert.strictEqual(path.basename('basename.ext/'), 'basename.ext'); +assert.strictEqual(path.basename('basename.ext//'), 'basename.ext'); +assert.strictEqual(path.basename('aaa/bbb', '/bbb'), 'bbb'); +assert.strictEqual(path.basename('aaa/bbb', 'a/bbb'), 'bbb'); +assert.strictEqual(path.basename('aaa/bbb', 'bbb'), 'bbb'); +assert.strictEqual(path.basename('aaa/bbb//', 'bbb'), 'bbb'); +assert.strictEqual(path.basename('aaa/bbb', 'bb'), 'b'); +assert.strictEqual(path.basename('aaa/bbb', 'b'), 'bb'); +assert.strictEqual(path.basename('/aaa/bbb', '/bbb'), 'bbb'); +assert.strictEqual(path.basename('/aaa/bbb', 'a/bbb'), 'bbb'); +assert.strictEqual(path.basename('/aaa/bbb', 'bbb'), 'bbb'); +assert.strictEqual(path.basename('/aaa/bbb//', 'bbb'), 'bbb'); +assert.strictEqual(path.basename('/aaa/bbb', 'bb'), 'b'); +assert.strictEqual(path.basename('/aaa/bbb', 'b'), 'bb'); +assert.strictEqual(path.basename('/aaa/bbb'), 'bbb'); +assert.strictEqual(path.basename('/aaa/'), 'aaa'); +assert.strictEqual(path.basename('/aaa/b'), 'b'); +assert.strictEqual(path.basename('/a/b'), 'b'); +assert.strictEqual(path.basename('//a'), 'a'); +assert.strictEqual(path.basename('a', 'a'), ''); + +// On Windows a backslash acts as a path separator. +assert.strictEqual(path.win32.basename('\\dir\\basename.ext'), 'basename.ext'); +assert.strictEqual(path.win32.basename('\\basename.ext'), 'basename.ext'); +assert.strictEqual(path.win32.basename('basename.ext'), 'basename.ext'); +assert.strictEqual(path.win32.basename('basename.ext\\'), 'basename.ext'); +assert.strictEqual(path.win32.basename('basename.ext\\\\'), 'basename.ext'); +assert.strictEqual(path.win32.basename('foo'), 'foo'); +assert.strictEqual(path.win32.basename('aaa\\bbb', '\\bbb'), 'bbb'); +assert.strictEqual(path.win32.basename('aaa\\bbb', 'a\\bbb'), 'bbb'); +assert.strictEqual(path.win32.basename('aaa\\bbb', 'bbb'), 'bbb'); +assert.strictEqual(path.win32.basename('aaa\\bbb\\\\\\\\', 'bbb'), 'bbb'); +assert.strictEqual(path.win32.basename('aaa\\bbb', 'bb'), 'b'); +assert.strictEqual(path.win32.basename('aaa\\bbb', 'b'), 'bb'); +assert.strictEqual(path.win32.basename('C:'), ''); +assert.strictEqual(path.win32.basename('C:.'), '.'); +assert.strictEqual(path.win32.basename('C:\\'), ''); +assert.strictEqual(path.win32.basename('C:\\dir\\base.ext'), 'base.ext'); +assert.strictEqual(path.win32.basename('C:\\basename.ext'), 'basename.ext'); +assert.strictEqual(path.win32.basename('C:basename.ext'), 'basename.ext'); +assert.strictEqual(path.win32.basename('C:basename.ext\\'), 'basename.ext'); +assert.strictEqual(path.win32.basename('C:basename.ext\\\\'), 'basename.ext'); +assert.strictEqual(path.win32.basename('C:foo'), 'foo'); +assert.strictEqual(path.win32.basename('file:stream'), 'file:stream'); +assert.strictEqual(path.win32.basename('a', 'a'), ''); + +// On unix a backslash is just treated as any other character. +assert.strictEqual(path.posix.basename('\\dir\\basename.ext'), + '\\dir\\basename.ext'); +assert.strictEqual(path.posix.basename('\\basename.ext'), '\\basename.ext'); +assert.strictEqual(path.posix.basename('basename.ext'), 'basename.ext'); +assert.strictEqual(path.posix.basename('basename.ext\\'), 'basename.ext\\'); +assert.strictEqual(path.posix.basename('basename.ext\\\\'), 'basename.ext\\\\'); +assert.strictEqual(path.posix.basename('foo'), 'foo'); + +// POSIX filenames may include control characters +// c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html +const controlCharFilename = `Icon${String.fromCharCode(13)}`; +assert.strictEqual(path.posix.basename(`/a/b/${controlCharFilename}`), + controlCharFilename); diff --git a/test/js/node/test/parallel/test-path-dirname.js b/test/js/node/test/parallel/test-path-dirname.js new file mode 100644 index 0000000000..0d4a182884 --- /dev/null +++ b/test/js/node/test/parallel/test-path-dirname.js @@ -0,0 +1,59 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); + +assert.strictEqual(path.dirname(__filename).slice(-13), + common.isWindows ? 'test\\parallel' : 'test/parallel'); + +assert.strictEqual(path.posix.dirname('/a/b/'), '/a'); +assert.strictEqual(path.posix.dirname('/a/b'), '/a'); +assert.strictEqual(path.posix.dirname('/a'), '/'); +assert.strictEqual(path.posix.dirname(''), '.'); +assert.strictEqual(path.posix.dirname('/'), '/'); +assert.strictEqual(path.posix.dirname('////'), '/'); +assert.strictEqual(path.posix.dirname('//a'), '//'); +assert.strictEqual(path.posix.dirname('foo'), '.'); + +assert.strictEqual(path.win32.dirname('c:\\'), 'c:\\'); +assert.strictEqual(path.win32.dirname('c:\\foo'), 'c:\\'); +assert.strictEqual(path.win32.dirname('c:\\foo\\'), 'c:\\'); +assert.strictEqual(path.win32.dirname('c:\\foo\\bar'), 'c:\\foo'); +assert.strictEqual(path.win32.dirname('c:\\foo\\bar\\'), 'c:\\foo'); +assert.strictEqual(path.win32.dirname('c:\\foo\\bar\\baz'), 'c:\\foo\\bar'); +assert.strictEqual(path.win32.dirname('c:\\foo bar\\baz'), 'c:\\foo bar'); +assert.strictEqual(path.win32.dirname('\\'), '\\'); +assert.strictEqual(path.win32.dirname('\\foo'), '\\'); +assert.strictEqual(path.win32.dirname('\\foo\\'), '\\'); +assert.strictEqual(path.win32.dirname('\\foo\\bar'), '\\foo'); +assert.strictEqual(path.win32.dirname('\\foo\\bar\\'), '\\foo'); +assert.strictEqual(path.win32.dirname('\\foo\\bar\\baz'), '\\foo\\bar'); +assert.strictEqual(path.win32.dirname('\\foo bar\\baz'), '\\foo bar'); +assert.strictEqual(path.win32.dirname('c:'), 'c:'); +assert.strictEqual(path.win32.dirname('c:foo'), 'c:'); +assert.strictEqual(path.win32.dirname('c:foo\\'), 'c:'); +assert.strictEqual(path.win32.dirname('c:foo\\bar'), 'c:foo'); +assert.strictEqual(path.win32.dirname('c:foo\\bar\\'), 'c:foo'); +assert.strictEqual(path.win32.dirname('c:foo\\bar\\baz'), 'c:foo\\bar'); +assert.strictEqual(path.win32.dirname('c:foo bar\\baz'), 'c:foo bar'); +assert.strictEqual(path.win32.dirname('file:stream'), '.'); +assert.strictEqual(path.win32.dirname('dir\\file:stream'), 'dir'); +assert.strictEqual(path.win32.dirname('\\\\unc\\share'), + '\\\\unc\\share'); +assert.strictEqual(path.win32.dirname('\\\\unc\\share\\foo'), + '\\\\unc\\share\\'); +assert.strictEqual(path.win32.dirname('\\\\unc\\share\\foo\\'), + '\\\\unc\\share\\'); +assert.strictEqual(path.win32.dirname('\\\\unc\\share\\foo\\bar'), + '\\\\unc\\share\\foo'); +assert.strictEqual(path.win32.dirname('\\\\unc\\share\\foo\\bar\\'), + '\\\\unc\\share\\foo'); +assert.strictEqual(path.win32.dirname('\\\\unc\\share\\foo\\bar\\baz'), + '\\\\unc\\share\\foo\\bar'); +assert.strictEqual(path.win32.dirname('/a/b/'), '/a'); +assert.strictEqual(path.win32.dirname('/a/b'), '/a'); +assert.strictEqual(path.win32.dirname('/a'), '/'); +assert.strictEqual(path.win32.dirname(''), '.'); +assert.strictEqual(path.win32.dirname('/'), '/'); +assert.strictEqual(path.win32.dirname('////'), '/'); +assert.strictEqual(path.win32.dirname('foo'), '.'); diff --git a/test/js/node/test/parallel/test-path-extname.js b/test/js/node/test/parallel/test-path-extname.js new file mode 100644 index 0000000000..be5a6316b0 --- /dev/null +++ b/test/js/node/test/parallel/test-path-extname.js @@ -0,0 +1,100 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const path = require('path'); + +const failures = []; +const slashRE = /\//g; + +const testPaths = [ + [__filename, '.js'], + ['', ''], + ['/path/to/file', ''], + ['/path/to/file.ext', '.ext'], + ['/path.to/file.ext', '.ext'], + ['/path.to/file', ''], + ['/path.to/.file', ''], + ['/path.to/.file.ext', '.ext'], + ['/path/to/f.ext', '.ext'], + ['/path/to/..ext', '.ext'], + ['/path/to/..', ''], + ['file', ''], + ['file.ext', '.ext'], + ['.file', ''], + ['.file.ext', '.ext'], + ['/file', ''], + ['/file.ext', '.ext'], + ['/.file', ''], + ['/.file.ext', '.ext'], + ['.path/file.ext', '.ext'], + ['file.ext.ext', '.ext'], + ['file.', '.'], + ['.', ''], + ['./', ''], + ['.file.ext', '.ext'], + ['.file', ''], + ['.file.', '.'], + ['.file..', '.'], + ['..', ''], + ['../', ''], + ['..file.ext', '.ext'], + ['..file', '.file'], + ['..file.', '.'], + ['..file..', '.'], + ['...', '.'], + ['...ext', '.ext'], + ['....', '.'], + ['file.ext/', '.ext'], + ['file.ext//', '.ext'], + ['file/', ''], + ['file//', ''], + ['file./', '.'], + ['file.//', '.'], +]; + +for (const testPath of testPaths) { + const expected = testPath[1]; + const extNames = [path.posix.extname, path.win32.extname]; + for (const extname of extNames) { + let input = testPath[0]; + let os; + if (extname === path.win32.extname) { + input = input.replace(slashRE, '\\'); + os = 'win32'; + } else { + os = 'posix'; + } + const actual = extname(input); + const message = `path.${os}.extname(${JSON.stringify(input)})\n expect=${ + JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected) + failures.push(`\n${message}`); + } + const input = `C:${testPath[0].replace(slashRE, '\\')}`; + const actual = path.win32.extname(input); + const message = `path.win32.extname(${JSON.stringify(input)})\n expect=${ + JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected) + failures.push(`\n${message}`); +} +assert.strictEqual(failures.length, 0, failures.join('')); + +// On Windows, backslash is a path separator. +assert.strictEqual(path.win32.extname('.\\'), ''); +assert.strictEqual(path.win32.extname('..\\'), ''); +assert.strictEqual(path.win32.extname('file.ext\\'), '.ext'); +assert.strictEqual(path.win32.extname('file.ext\\\\'), '.ext'); +assert.strictEqual(path.win32.extname('file\\'), ''); +assert.strictEqual(path.win32.extname('file\\\\'), ''); +assert.strictEqual(path.win32.extname('file.\\'), '.'); +assert.strictEqual(path.win32.extname('file.\\\\'), '.'); + +// On *nix, backslash is a valid name component like any other character. +assert.strictEqual(path.posix.extname('.\\'), ''); +assert.strictEqual(path.posix.extname('..\\'), '.\\'); +assert.strictEqual(path.posix.extname('file.ext\\'), '.ext\\'); +assert.strictEqual(path.posix.extname('file.ext\\\\'), '.ext\\\\'); +assert.strictEqual(path.posix.extname('file\\'), ''); +assert.strictEqual(path.posix.extname('file\\\\'), ''); +assert.strictEqual(path.posix.extname('file.\\'), '.\\'); +assert.strictEqual(path.posix.extname('file.\\\\'), '.\\\\'); diff --git a/test/js/node/test/parallel/test-path-glob.js b/test/js/node/test/parallel/test-path-glob.js new file mode 100644 index 0000000000..47647e1278 --- /dev/null +++ b/test/js/node/test/parallel/test-path-glob.js @@ -0,0 +1,44 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const path = require('path'); + +const globs = { + win32: [ + ['foo\\bar\\baz', 'foo\\[bcr]ar\\baz', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[!bcr]ar\\baz', false], // Matches anything except 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[bc-r]ar\\baz', true], // Matches 'bar' or 'car' using range in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\*\\!bar\\*\\baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo\\bar1\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar1' + ['foo\\bar5\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar5' + ['foo\\barx\\baz', 'foo\\bar[a-z]\\baz', true], // Matches 'bar' followed by any lowercase letter in 'foo\\barx' + ['foo\\bar\\baz\\boo', 'foo\\[bc-r]ar\\baz\\*', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo/**', true], // Matches anything in 'foo' + ['foo\\bar\\baz', '*', false], // No match + ], + posix: [ + ['foo/bar/baz', 'foo/[bcr]ar/baz', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[!bcr]ar/baz', false], // Matches anything except 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[bc-r]ar/baz', true], // Matches 'bar' or 'car' using range in 'foo/bar' + ['foo/bar/baz', 'foo/*/!bar/*/baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo/bar1/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar1' + ['foo/bar5/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar5' + ['foo/barx/baz', 'foo/bar[a-z]/baz', true], // Matches 'bar' followed by any lowercase letter in 'foo/barx' + ['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/**', true], // Matches anything in 'foo' + ['foo/bar/baz', '*', false], // No match + ], +}; + + +for (const [platform, platformGlobs] of Object.entries(globs)) { + for (const [pathStr, glob, expected] of platformGlobs) { + const actual = path[platform].matchesGlob(pathStr, glob); + assert.strictEqual(actual, expected, `Expected ${pathStr} to ` + (expected ? '' : 'not ') + `match ${glob} on ${platform}`); + } +} + +// Test for non-string input +assert.throws(() => path.matchesGlob(123, 'foo/bar/baz'), /.*must be of type string.*/); +assert.throws(() => path.matchesGlob('foo/bar/baz', 123), /.*must be of type string.*/); diff --git a/test/js/node/test/parallel/test-path-isabsolute.js b/test/js/node/test/parallel/test-path-isabsolute.js new file mode 100644 index 0000000000..66b4f1ee51 --- /dev/null +++ b/test/js/node/test/parallel/test-path-isabsolute.js @@ -0,0 +1,28 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const path = require('path'); + +assert.strictEqual(path.win32.isAbsolute('/'), true); +assert.strictEqual(path.win32.isAbsolute('//'), true); +assert.strictEqual(path.win32.isAbsolute('//server'), true); +assert.strictEqual(path.win32.isAbsolute('//server/file'), true); +assert.strictEqual(path.win32.isAbsolute('\\\\server\\file'), true); +assert.strictEqual(path.win32.isAbsolute('\\\\server'), true); +assert.strictEqual(path.win32.isAbsolute('\\\\'), true); +assert.strictEqual(path.win32.isAbsolute('c'), false); +assert.strictEqual(path.win32.isAbsolute('c:'), false); +assert.strictEqual(path.win32.isAbsolute('c:\\'), true); +assert.strictEqual(path.win32.isAbsolute('c:/'), true); +assert.strictEqual(path.win32.isAbsolute('c://'), true); +assert.strictEqual(path.win32.isAbsolute('C:/Users/'), true); +assert.strictEqual(path.win32.isAbsolute('C:\\Users\\'), true); +assert.strictEqual(path.win32.isAbsolute('C:cwd/another'), false); +assert.strictEqual(path.win32.isAbsolute('C:cwd\\another'), false); +assert.strictEqual(path.win32.isAbsolute('directory/directory'), false); +assert.strictEqual(path.win32.isAbsolute('directory\\directory'), false); + +assert.strictEqual(path.posix.isAbsolute('/home/foo'), true); +assert.strictEqual(path.posix.isAbsolute('/home/foo/..'), true); +assert.strictEqual(path.posix.isAbsolute('bar/'), false); +assert.strictEqual(path.posix.isAbsolute('./baz'), false); diff --git a/test/js/node/test/parallel/test-path-join.js b/test/js/node/test/parallel/test-path-join.js new file mode 100644 index 0000000000..d6d1839996 --- /dev/null +++ b/test/js/node/test/parallel/test-path-join.js @@ -0,0 +1,143 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const path = require('path'); + +const failures = []; +const backslashRE = /\\/g; + +const joinTests = [ + [ [path.posix.join, path.win32.join], + // Arguments result + [[['.', 'x/b', '..', '/b/c.js'], 'x/b/c.js'], + [[], '.'], + [['/.', 'x/b', '..', '/b/c.js'], '/x/b/c.js'], + [['/foo', '../../../bar'], '/bar'], + [['foo', '../../../bar'], '../../bar'], + [['foo/', '../../../bar'], '../../bar'], + [['foo/x', '../../../bar'], '../bar'], + [['foo/x', './bar'], 'foo/x/bar'], + [['foo/x/', './bar'], 'foo/x/bar'], + [['foo/x/', '.', 'bar'], 'foo/x/bar'], + [['./'], './'], + [['.', './'], './'], + [['.', '.', '.'], '.'], + [['.', './', '.'], '.'], + [['.', '/./', '.'], '.'], + [['.', '/////./', '.'], '.'], + [['.'], '.'], + [['', '.'], '.'], + [['', 'foo'], 'foo'], + [['foo', '/bar'], 'foo/bar'], + [['', '/foo'], '/foo'], + [['', '', '/foo'], '/foo'], + [['', '', 'foo'], 'foo'], + [['foo', ''], 'foo'], + [['foo/', ''], 'foo/'], + [['foo', '', '/bar'], 'foo/bar'], + [['./', '..', '/foo'], '../foo'], + [['./', '..', '..', '/foo'], '../../foo'], + [['.', '..', '..', '/foo'], '../../foo'], + [['', '..', '..', '/foo'], '../../foo'], + [['/'], '/'], + [['/', '.'], '/'], + [['/', '..'], '/'], + [['/', '..', '..'], '/'], + [[''], '.'], + [['', ''], '.'], + [[' /foo'], ' /foo'], + [[' ', 'foo'], ' /foo'], + [[' ', '.'], ' '], + [[' ', '/'], ' /'], + [[' ', ''], ' '], + [['/', 'foo'], '/foo'], + [['/', '/foo'], '/foo'], + [['/', '//foo'], '/foo'], + [['/', '', '/foo'], '/foo'], + [['', '/', 'foo'], '/foo'], + [['', '/', '/foo'], '/foo'], + ], + ], +]; + +// Windows-specific join tests +joinTests.push([ + path.win32.join, + joinTests[0][1].slice(0).concat( + [// Arguments result + // UNC path expected + [['//foo/bar'], '\\\\foo\\bar\\'], + [['\\/foo/bar'], '\\\\foo\\bar\\'], + [['\\\\foo/bar'], '\\\\foo\\bar\\'], + // UNC path expected - server and share separate + [['//foo', 'bar'], '\\\\foo\\bar\\'], + [['//foo/', 'bar'], '\\\\foo\\bar\\'], + [['//foo', '/bar'], '\\\\foo\\bar\\'], + // UNC path expected - questionable + [['//foo', '', 'bar'], '\\\\foo\\bar\\'], + [['//foo/', '', 'bar'], '\\\\foo\\bar\\'], + [['//foo/', '', '/bar'], '\\\\foo\\bar\\'], + // UNC path expected - even more questionable + [['', '//foo', 'bar'], '\\\\foo\\bar\\'], + [['', '//foo/', 'bar'], '\\\\foo\\bar\\'], + [['', '//foo/', '/bar'], '\\\\foo\\bar\\'], + // No UNC path expected (no double slash in first component) + [['\\', 'foo/bar'], '\\foo\\bar'], + [['\\', '/foo/bar'], '\\foo\\bar'], + [['', '/', '/foo/bar'], '\\foo\\bar'], + // No UNC path expected (no non-slashes in first component - + // questionable) + [['//', 'foo/bar'], '\\foo\\bar'], + [['//', '/foo/bar'], '\\foo\\bar'], + [['\\\\', '/', '/foo/bar'], '\\foo\\bar'], + [['//'], '\\'], + // No UNC path expected (share name missing - questionable). + [['//foo'], '\\foo'], + [['//foo/'], '\\foo\\'], + [['//foo', '/'], '\\foo\\'], + [['//foo', '', '/'], '\\foo\\'], + // No UNC path expected (too many leading slashes - questionable) + [['///foo/bar'], '\\foo\\bar'], + [['////foo', 'bar'], '\\foo\\bar'], + [['\\\\\\/foo/bar'], '\\foo\\bar'], + // Drive-relative vs drive-absolute paths. This merely describes the + // status quo, rather than being obviously right + [['c:'], 'c:.'], + [['c:.'], 'c:.'], + [['c:', ''], 'c:.'], + [['', 'c:'], 'c:.'], + [['c:.', '/'], 'c:.\\'], + [['c:.', 'file'], 'c:file'], + [['c:', '/'], 'c:\\'], + [['c:', 'file'], 'c:\\file'], + ] + ), +]); +joinTests.forEach((test) => { + if (!Array.isArray(test[0])) + test[0] = [test[0]]; + test[0].forEach((join) => { + test[1].forEach((test) => { + const actual = join.apply(null, test[0]); + const expected = test[1]; + // For non-Windows specific tests with the Windows join(), we need to try + // replacing the slashes since the non-Windows specific tests' `expected` + // use forward slashes + let actualAlt; + let os; + if (join === path.win32.join) { + actualAlt = actual.replace(backslashRE, '/'); + os = 'win32'; + } else { + os = 'posix'; + } + if (actual !== expected && actualAlt !== expected) { + const delimiter = test[0].map(JSON.stringify).join(','); + const message = `path.${os}.join(${delimiter})\n expect=${ + JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); + }); +}); +assert.strictEqual(failures.length, 0, failures.join('')); diff --git a/test/js/node/test/parallel/test-path-makelong.js b/test/js/node/test/parallel/test-path-makelong.js new file mode 100644 index 0000000000..7a4783953c --- /dev/null +++ b/test/js/node/test/parallel/test-path-makelong.js @@ -0,0 +1,88 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const path = require('path'); + +if (common.isWindows) { + const file = fixtures.path('a.js'); + const resolvedFile = path.resolve(file); + + assert.strictEqual(path.toNamespacedPath(file), + `\\\\?\\${resolvedFile}`); + assert.strictEqual(path.toNamespacedPath(`\\\\?\\${file}`), + `\\\\?\\${resolvedFile}`); + assert.strictEqual(path.toNamespacedPath( + '\\\\someserver\\someshare\\somefile'), + '\\\\?\\UNC\\someserver\\someshare\\somefile'); + assert.strictEqual(path.toNamespacedPath( + '\\\\?\\UNC\\someserver\\someshare\\somefile'), + '\\\\?\\UNC\\someserver\\someshare\\somefile'); + assert.strictEqual(path.toNamespacedPath('\\\\.\\pipe\\somepipe'), + '\\\\.\\pipe\\somepipe'); +} + +assert.strictEqual(path.toNamespacedPath(''), ''); +assert.strictEqual(path.toNamespacedPath(null), null); +assert.strictEqual(path.toNamespacedPath(100), 100); +assert.strictEqual(path.toNamespacedPath(path), path); +assert.strictEqual(path.toNamespacedPath(false), false); +assert.strictEqual(path.toNamespacedPath(true), true); + +const emptyObj = {}; +assert.strictEqual(path.posix.toNamespacedPath('/foo/bar'), '/foo/bar'); +assert.strictEqual(path.posix.toNamespacedPath('foo/bar'), 'foo/bar'); +assert.strictEqual(path.posix.toNamespacedPath(null), null); +assert.strictEqual(path.posix.toNamespacedPath(true), true); +assert.strictEqual(path.posix.toNamespacedPath(1), 1); +assert.strictEqual(path.posix.toNamespacedPath(), undefined); +assert.strictEqual(path.posix.toNamespacedPath(emptyObj), emptyObj); +if (common.isWindows) { + // These tests cause resolve() to insert the cwd, so we cannot test them from + // non-Windows platforms (easily) + assert.strictEqual(path.toNamespacedPath(''), ''); + assert.strictEqual(path.win32.toNamespacedPath('foo\\bar').toLowerCase(), + `\\\\?\\${process.cwd().toLowerCase()}\\foo\\bar`); + assert.strictEqual(path.win32.toNamespacedPath('foo/bar').toLowerCase(), + `\\\\?\\${process.cwd().toLowerCase()}\\foo\\bar`); + const currentDeviceLetter = path.parse(process.cwd()).root.substring(0, 2); + assert.strictEqual( + path.win32.toNamespacedPath(currentDeviceLetter).toLowerCase(), + `\\\\?\\${process.cwd().toLowerCase()}`); + assert.strictEqual(path.win32.toNamespacedPath('C').toLowerCase(), + `\\\\?\\${process.cwd().toLowerCase()}\\c`); +} +assert.strictEqual(path.win32.toNamespacedPath('C:\\foo'), '\\\\?\\C:\\foo'); +assert.strictEqual(path.win32.toNamespacedPath('C:/foo'), '\\\\?\\C:\\foo'); +assert.strictEqual(path.win32.toNamespacedPath('\\\\foo\\bar'), + '\\\\?\\UNC\\foo\\bar\\'); +assert.strictEqual(path.win32.toNamespacedPath('//foo//bar'), + '\\\\?\\UNC\\foo\\bar\\'); +assert.strictEqual(path.win32.toNamespacedPath('\\\\?\\foo'), '\\\\?\\foo\\'); +assert.strictEqual(path.win32.toNamespacedPath('\\\\?\\c:\\Windows/System'), '\\\\?\\c:\\Windows\\System'); +assert.strictEqual(path.win32.toNamespacedPath(null), null); +assert.strictEqual(path.win32.toNamespacedPath(true), true); +assert.strictEqual(path.win32.toNamespacedPath(1), 1); +assert.strictEqual(path.win32.toNamespacedPath(), undefined); +assert.strictEqual(path.win32.toNamespacedPath(emptyObj), emptyObj); diff --git a/test/js/node/test/parallel/test-path-normalize.js b/test/js/node/test/parallel/test-path-normalize.js new file mode 100644 index 0000000000..6164faa377 --- /dev/null +++ b/test/js/node/test/parallel/test-path-normalize.js @@ -0,0 +1,49 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const path = require('path'); + +assert.strictEqual(path.win32.normalize('./fixtures///b/../b/c.js'), 'fixtures\\b\\c.js'); +assert.strictEqual(path.win32.normalize('/foo/../../../bar'), '\\bar'); +assert.strictEqual(path.win32.normalize('a//b//../b'), 'a\\b'); +assert.strictEqual(path.win32.normalize('a//b//./c'), 'a\\b\\c'); +assert.strictEqual(path.win32.normalize('a//b//.'), 'a\\b'); +assert.strictEqual(path.win32.normalize('//server/share/dir/file.ext'), '\\\\server\\share\\dir\\file.ext'); +assert.strictEqual(path.win32.normalize('/a/b/c/../../../x/y/z'), '\\x\\y\\z'); +assert.strictEqual(path.win32.normalize('C:'), 'C:.'); +assert.strictEqual(path.win32.normalize('C:..\\abc'), 'C:..\\abc'); +assert.strictEqual(path.win32.normalize('C:..\\..\\abc\\..\\def'), 'C:..\\..\\def'); +assert.strictEqual(path.win32.normalize('C:\\.'), 'C:\\'); +assert.strictEqual(path.win32.normalize('file:stream'), 'file:stream'); +assert.strictEqual(path.win32.normalize('bar\\foo..\\..\\'), 'bar\\'); +assert.strictEqual(path.win32.normalize('bar\\foo..\\..'), 'bar'); +assert.strictEqual(path.win32.normalize('bar\\foo..\\..\\baz'), 'bar\\baz'); +assert.strictEqual(path.win32.normalize('bar\\foo..\\'), 'bar\\foo..\\'); +assert.strictEqual(path.win32.normalize('bar\\foo..'), 'bar\\foo..'); +assert.strictEqual(path.win32.normalize('..\\foo..\\..\\..\\bar'), '..\\..\\bar'); +assert.strictEqual(path.win32.normalize('..\\...\\..\\.\\...\\..\\..\\bar'), '..\\..\\bar'); +assert.strictEqual(path.win32.normalize('../../../foo/../../../bar'), '..\\..\\..\\..\\..\\bar'); +assert.strictEqual(path.win32.normalize('../../../foo/../../../bar/../../'), '..\\..\\..\\..\\..\\..\\'); +assert.strictEqual(path.win32.normalize('../foobar/barfoo/foo/../../../bar/../../'), '..\\..\\'); +assert.strictEqual(path.win32.normalize('../.../../foobar/../../../bar/../../baz'), '..\\..\\..\\..\\baz'); +assert.strictEqual(path.win32.normalize('foo/bar\\baz'), 'foo\\bar\\baz'); + +assert.strictEqual(path.posix.normalize('./fixtures///b/../b/c.js'), 'fixtures/b/c.js'); +assert.strictEqual(path.posix.normalize('/foo/../../../bar'), '/bar'); +assert.strictEqual(path.posix.normalize('a//b//../b'), 'a/b'); +assert.strictEqual(path.posix.normalize('a//b//./c'), 'a/b/c'); +assert.strictEqual(path.posix.normalize('a//b//.'), 'a/b'); +assert.strictEqual(path.posix.normalize('/a/b/c/../../../x/y/z'), '/x/y/z'); +assert.strictEqual(path.posix.normalize('///..//./foo/.//bar'), '/foo/bar'); +assert.strictEqual(path.posix.normalize('bar/foo../../'), 'bar/'); +assert.strictEqual(path.posix.normalize('bar/foo../..'), 'bar'); +assert.strictEqual(path.posix.normalize('bar/foo../../baz'), 'bar/baz'); +assert.strictEqual(path.posix.normalize('bar/foo../'), 'bar/foo../'); +assert.strictEqual(path.posix.normalize('bar/foo..'), 'bar/foo..'); +assert.strictEqual(path.posix.normalize('../foo../../../bar'), '../../bar'); +assert.strictEqual(path.posix.normalize('../.../.././.../../../bar'), '../../bar'); +assert.strictEqual(path.posix.normalize('../../../foo/../../../bar'), '../../../../../bar'); +assert.strictEqual(path.posix.normalize('../../../foo/../../../bar/../../'), '../../../../../../'); +assert.strictEqual(path.posix.normalize('../foobar/barfoo/foo/../../../bar/../../'), '../../'); +assert.strictEqual(path.posix.normalize('../.../../foobar/../../../bar/../../baz'), '../../../../baz'); +assert.strictEqual(path.posix.normalize('foo/bar\\baz'), 'foo/bar\\baz'); diff --git a/test/js/node/test/parallel/test-path-parse-format.js b/test/js/node/test/parallel/test-path-parse-format.js new file mode 100644 index 0000000000..ca14120422 --- /dev/null +++ b/test/js/node/test/parallel/test-path-parse-format.js @@ -0,0 +1,226 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); + +const winPaths = [ + // [path, root] + ['C:\\path\\dir\\index.html', 'C:\\'], + ['C:\\another_path\\DIR\\1\\2\\33\\\\index', 'C:\\'], + ['another_path\\DIR with spaces\\1\\2\\33\\index', ''], + ['\\', '\\'], + ['\\foo\\C:', '\\'], + ['file', ''], + ['file:stream', ''], + ['.\\file', ''], + ['C:', 'C:'], + ['C:.', 'C:'], + ['C:..', 'C:'], + ['C:abc', 'C:'], + ['C:\\', 'C:\\'], + ['C:\\abc', 'C:\\' ], + ['', ''], + + // unc + ['\\\\server\\share\\file_path', '\\\\server\\share\\'], + ['\\\\server two\\shared folder\\file path.zip', + '\\\\server two\\shared folder\\'], + ['\\\\teela\\admin$\\system32', '\\\\teela\\admin$\\'], + ['\\\\?\\UNC\\server\\share', '\\\\?\\UNC\\'], +]; + +const winSpecialCaseParseTests = [ + ['t', { base: 't', name: 't', root: '', dir: '', ext: '' }], + ['/foo/bar', { root: '/', dir: '/foo', base: 'bar', ext: '', name: 'bar' }], +]; + +const winSpecialCaseFormatTests = [ + [{ dir: 'some\\dir' }, 'some\\dir\\'], + [{ base: 'index.html' }, 'index.html'], + [{ root: 'C:\\' }, 'C:\\'], + [{ name: 'index', ext: '.html' }, 'index.html'], + [{ dir: 'some\\dir', name: 'index', ext: '.html' }, 'some\\dir\\index.html'], + [{ root: 'C:\\', name: 'index', ext: '.html' }, 'C:\\index.html'], + [{}, ''], +]; + +const unixPaths = [ + // [path, root] + ['/home/user/dir/file.txt', '/'], + ['/home/user/a dir/another File.zip', '/'], + ['/home/user/a dir//another&File.', '/'], + ['/home/user/a$$$dir//another File.zip', '/'], + ['user/dir/another File.zip', ''], + ['file', ''], + ['.\\file', ''], + ['./file', ''], + ['C:\\foo', ''], + ['/', '/'], + ['', ''], + ['.', ''], + ['..', ''], + ['/foo', '/'], + ['/foo.', '/'], + ['/foo.bar', '/'], + ['/.', '/'], + ['/.foo', '/'], + ['/.foo.bar', '/'], + ['/foo/bar.baz', '/'], +]; + +const unixSpecialCaseFormatTests = [ + [{ dir: 'some/dir' }, 'some/dir/'], + [{ base: 'index.html' }, 'index.html'], + [{ root: '/' }, '/'], + [{ name: 'index', ext: '.html' }, 'index.html'], + [{ dir: 'some/dir', name: 'index', ext: '.html' }, 'some/dir/index.html'], + [{ root: '/', name: 'index', ext: '.html' }, '/index.html'], + [{}, ''], +]; + +const errors = [ + { method: 'parse', input: [null] }, + { method: 'parse', input: [{}] }, + { method: 'parse', input: [true] }, + { method: 'parse', input: [1] }, + { method: 'parse', input: [] }, + { method: 'format', input: [null] }, + { method: 'format', input: [''] }, + { method: 'format', input: [true] }, + { method: 'format', input: [1] }, +]; + +checkParseFormat(path.win32, winPaths); +checkParseFormat(path.posix, unixPaths); +checkSpecialCaseParseFormat(path.win32, winSpecialCaseParseTests); +checkErrors(path.win32); +checkErrors(path.posix); +checkFormat(path.win32, winSpecialCaseFormatTests); +checkFormat(path.posix, unixSpecialCaseFormatTests); + +// Test removal of trailing path separators +const trailingTests = [ + [ path.win32.parse, + [['.\\', { root: '', dir: '', base: '.', ext: '', name: '.' }], + ['\\\\', { root: '\\', dir: '\\', base: '', ext: '', name: '' }], + ['\\\\', { root: '\\', dir: '\\', base: '', ext: '', name: '' }], + ['c:\\foo\\\\\\', + { root: 'c:\\', dir: 'c:\\', base: 'foo', ext: '', name: 'foo' }], + ['D:\\foo\\\\\\bar.baz', + { root: 'D:\\', + dir: 'D:\\foo\\\\', + base: 'bar.baz', + ext: '.baz', + name: 'bar' }, + ], + ], + ], + [ path.posix.parse, + [['./', { root: '', dir: '', base: '.', ext: '', name: '.' }], + ['//', { root: '/', dir: '/', base: '', ext: '', name: '' }], + ['///', { root: '/', dir: '/', base: '', ext: '', name: '' }], + ['/foo///', { root: '/', dir: '/', base: 'foo', ext: '', name: 'foo' }], + ['/foo///bar.baz', + { root: '/', dir: '/foo//', base: 'bar.baz', ext: '.baz', name: 'bar' }, + ], + ], + ], +]; +const failures = []; +for (const [parse, testList] of trailingTests) { + const os = parse === path.win32.parse ? 'win32' : 'posix'; + for (const [input, expected] of testList) { + const actual = parse(input); + const message = `path.${os}.parse(${JSON.stringify(input)})\n expect=${ + JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + const actualKeys = Object.keys(actual); + const expectedKeys = Object.keys(expected); + let failed = (actualKeys.length !== expectedKeys.length); + if (!failed) { + for (let i = 0; i < actualKeys.length; ++i) { + const key = actualKeys[i]; + if (!expectedKeys.includes(key) || actual[key] !== expected[key]) { + failed = true; + break; + } + } + } + if (failed) + failures.push(`\n${message}`); + } +} +assert.strictEqual(failures.length, 0, failures.join('')); + +function checkErrors(path) { + errors.forEach(({ method, input }) => { + assert.throws(() => { + path[method].apply(path, input); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + }); +} + +function checkParseFormat(path, paths) { + paths.forEach(([element, root]) => { + const output = path.parse(element); + assert.strictEqual(typeof output.root, 'string'); + assert.strictEqual(typeof output.dir, 'string'); + assert.strictEqual(typeof output.base, 'string'); + assert.strictEqual(typeof output.ext, 'string'); + assert.strictEqual(typeof output.name, 'string'); + assert.strictEqual(path.format(output), element); + assert.strictEqual(output.root, root); + assert(output.dir.startsWith(output.root)); + assert.strictEqual(output.dir, output.dir ? path.dirname(element) : ''); + assert.strictEqual(output.base, path.basename(element)); + assert.strictEqual(output.ext, path.extname(element)); + }); +} + +function checkSpecialCaseParseFormat(path, testCases) { + testCases.forEach(([element, expect]) => { + assert.deepStrictEqual(path.parse(element), expect); + }); +} + +function checkFormat(path, testCases) { + testCases.forEach(([element, expect]) => { + assert.strictEqual(path.format(element), expect); + }); + + [null, undefined, 1, true, false, 'string'].forEach((pathObject) => { + assert.throws(() => { + path.format(pathObject); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + }); +} + +// See https://github.com/nodejs/node/issues/44343 +assert.strictEqual(path.format({ name: 'x', ext: 'png' }), 'x.png'); +assert.strictEqual(path.format({ name: 'x', ext: '.png' }), 'x.png'); diff --git a/test/js/node/test/parallel/test-path-posix-exists.js b/test/js/node/test/parallel/test-path-posix-exists.js new file mode 100644 index 0000000000..dc12ed6daf --- /dev/null +++ b/test/js/node/test/parallel/test-path-posix-exists.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(require('path/posix'), require('path').posix); diff --git a/test/js/node/test/parallel/test-path-posix-relative-on-windows.js b/test/js/node/test/parallel/test-path-posix-relative-on-windows.js new file mode 100644 index 0000000000..bcaaca8b18 --- /dev/null +++ b/test/js/node/test/parallel/test-path-posix-relative-on-windows.js @@ -0,0 +1,10 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const path = require('path'); + +// Refs: https://github.com/nodejs/node/issues/13683 + +const relativePath = path.posix.relative('a/b/c', '../../x'); +assert.match(relativePath, /^(\.\.\/){3,5}x$/); diff --git a/test/js/node/test/parallel/test-path-relative.js b/test/js/node/test/parallel/test-path-relative.js new file mode 100644 index 0000000000..f6a9f5662a --- /dev/null +++ b/test/js/node/test/parallel/test-path-relative.js @@ -0,0 +1,69 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const path = require('path'); + +const failures = []; + +const relativeTests = [ + [ path.win32.relative, + // Arguments result + [['c:/blah\\blah', 'd:/games', 'd:\\games'], + ['c:/aaaa/bbbb', 'c:/aaaa', '..'], + ['c:/aaaa/bbbb', 'c:/cccc', '..\\..\\cccc'], + ['c:/aaaa/bbbb', 'c:/aaaa/bbbb', ''], + ['c:/aaaa/bbbb', 'c:/aaaa/cccc', '..\\cccc'], + ['c:/aaaa/', 'c:/aaaa/cccc', 'cccc'], + ['c:/', 'c:\\aaaa\\bbbb', 'aaaa\\bbbb'], + ['c:/aaaa/bbbb', 'd:\\', 'd:\\'], + ['c:/AaAa/bbbb', 'c:/aaaa/bbbb', ''], + ['c:/aaaaa/', 'c:/aaaa/cccc', '..\\aaaa\\cccc'], + ['C:\\foo\\bar\\baz\\quux', 'C:\\', '..\\..\\..\\..'], + ['C:\\foo\\test', 'C:\\foo\\test\\bar\\package.json', 'bar\\package.json'], + ['C:\\foo\\bar\\baz-quux', 'C:\\foo\\bar\\baz', '..\\baz'], + ['C:\\foo\\bar\\baz', 'C:\\foo\\bar\\baz-quux', '..\\baz-quux'], + ['\\\\foo\\bar', '\\\\foo\\bar\\baz', 'baz'], + ['\\\\foo\\bar\\baz', '\\\\foo\\bar', '..'], + ['\\\\foo\\bar\\baz-quux', '\\\\foo\\bar\\baz', '..\\baz'], + ['\\\\foo\\bar\\baz', '\\\\foo\\bar\\baz-quux', '..\\baz-quux'], + ['C:\\baz-quux', 'C:\\baz', '..\\baz'], + ['C:\\baz', 'C:\\baz-quux', '..\\baz-quux'], + ['\\\\foo\\baz-quux', '\\\\foo\\baz', '..\\baz'], + ['\\\\foo\\baz', '\\\\foo\\baz-quux', '..\\baz-quux'], + ['C:\\baz', '\\\\foo\\bar\\baz', '\\\\foo\\bar\\baz'], + ['\\\\foo\\bar\\baz', 'C:\\baz', 'C:\\baz'], + ], + ], + [ path.posix.relative, + // Arguments result + [['/var/lib', '/var', '..'], + ['/var/lib', '/bin', '../../bin'], + ['/var/lib', '/var/lib', ''], + ['/var/lib', '/var/apache', '../apache'], + ['/var/', '/var/lib', 'lib'], + ['/', '/var/lib', 'var/lib'], + ['/foo/test', '/foo/test/bar/package.json', 'bar/package.json'], + ['/Users/a/web/b/test/mails', '/Users/a/web/b', '../..'], + ['/foo/bar/baz-quux', '/foo/bar/baz', '../baz'], + ['/foo/bar/baz', '/foo/bar/baz-quux', '../baz-quux'], + ['/baz-quux', '/baz', '../baz'], + ['/baz', '/baz-quux', '../baz-quux'], + ['/page1/page2/foo', '/', '../../..'], + ], + ], +]; +relativeTests.forEach((test) => { + const relative = test[0]; + test[1].forEach((test) => { + const actual = relative(test[0], test[1]); + const expected = test[2]; + if (actual !== expected) { + const os = relative === path.win32.relative ? 'win32' : 'posix'; + const message = `path.${os}.relative(${ + test.slice(0, 2).map(JSON.stringify).join(',')})\n expect=${ + JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); +}); +assert.strictEqual(failures.length, 0, failures.join('')); diff --git a/test/js/node/test/parallel/test-path-resolve.js b/test/js/node/test/parallel/test-path-resolve.js new file mode 100644 index 0000000000..f34eadcc0c --- /dev/null +++ b/test/js/node/test/parallel/test-path-resolve.js @@ -0,0 +1,111 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const child = require('child_process'); +const path = require('path'); + +const failures = []; +const slashRE = /\//g; +const backslashRE = /\\/g; + +const posixyCwd = common.isWindows ? + (() => { + const _ = process.cwd() + .replaceAll(path.sep, path.posix.sep); + return _.slice(_.indexOf(path.posix.sep)); + })() : + process.cwd(); + +const resolveTests = [ + [ path.win32.resolve, + // Arguments result + [[['c:/blah\\blah', 'd:/games', 'c:../a'], 'c:\\blah\\a'], + [['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], 'd:\\e.exe'], + [['c:/ignore', 'c:/some/file'], 'c:\\some\\file'], + [['d:/ignore', 'd:some/dir//'], 'd:\\ignore\\some\\dir'], + [['.'], process.cwd()], + [['//server/share', '..', 'relative\\'], '\\\\server\\share\\relative'], + [['c:/', '//'], 'c:\\'], + [['c:/', '//dir'], 'c:\\dir'], + [['c:/', '//server/share'], '\\\\server\\share\\'], + [['c:/', '//server//share'], '\\\\server\\share\\'], + [['c:/', '///some//dir'], 'c:\\some\\dir'], + [['C:\\foo\\tmp.3\\', '..\\tmp.3\\cycles\\root.js'], + 'C:\\foo\\tmp.3\\cycles\\root.js'], + // IMPORTANT NOTE: + // - PR originally landed in #54224 and #55623 to fix issue #54025 + // - It caused a regression (issue #56002) and was reverted in #56088 + // - This behavior did _not_ land in even-numbered versions + // If node decides to adopt this, we need to revisit these tests + // + // [['\\\\.\\PHYSICALDRIVE0'], '\\\\.\\PHYSICALDRIVE0'], + // [['\\\\?\\PHYSICALDRIVE0'], '\\\\?\\PHYSICALDRIVE0'], + [['\\\\.\\PHYSICALDRIVE0'], '\\\\.\\PHYSICALDRIVE0\\'], + [['\\\\?\\PHYSICALDRIVE0'], '\\\\?\\PHYSICALDRIVE0\\'], + ], + ], + [ path.posix.resolve, + // Arguments result + [[['/var/lib', '../', 'file/'], '/var/file'], + [['/var/lib', '/../', 'file/'], '/file'], + [['a/b/c/', '../../..'], posixyCwd], + [['.'], posixyCwd], + [['/some/dir', '.', '/absolute/'], '/absolute'], + [['/foo/tmp.3/', '../tmp.3/cycles/root.js'], '/foo/tmp.3/cycles/root.js'], + ], + ], +]; +resolveTests.forEach(([resolve, tests]) => { + tests.forEach(([test, expected]) => { + const actual = resolve.apply(null, test); + let actualAlt; + const os = resolve === path.win32.resolve ? 'win32' : 'posix'; + if (resolve === path.win32.resolve && !common.isWindows) + actualAlt = actual.replace(backslashRE, '/'); + else if (resolve !== path.win32.resolve && common.isWindows) + actualAlt = actual.replace(slashRE, '\\'); + + const message = + `path.${os}.resolve(${test.map(JSON.stringify).join(',')})\n expect=${ + JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected && actualAlt !== expected) + failures.push(message); + }); +}); +assert.strictEqual(failures.length, 0, failures.join('\n')); + +if (common.isWindows) { + // Test resolving the current Windows drive letter from a spawned process. + // See https://github.com/nodejs/node/issues/7215 + const currentDriveLetter = path.parse(process.cwd()).root.substring(0, 2); + const resolveFixture = fixtures.path('path-resolve.js'); + const spawnResult = child.spawnSync( + process.argv[0], [resolveFixture, currentDriveLetter]); + const resolvedPath = spawnResult.stdout.toString().trim(); + assert.strictEqual(resolvedPath.toLowerCase(), process.cwd().toLowerCase()); +} + + +// Test originally was this: +// +// if (!common.isWindows) { +// // Test handling relative paths to be safe when process.cwd() fails. +// process.cwd = () => ''; +// assert.strictEqual(process.cwd(), ''); +// const resolved = path.resolve(); +// const expected = '.'; +// assert.strictEqual(resolved, expected); +// } +// +// In Bun, process.cwd() doesn't affect the behavior of `path.resolve()` (it uses +// getcwd(2)). This has the following implications: +// 1. overriding process.cwd() has no affect on path.resolve(); +// 2. getcwd isn't affected by $CWD, so it cannot be removed that way; +// 3. The Bun CLI caches cwd on BunProcess.m_cachedCwd at startup, so deleting +// it after the process starts keeps `process.cwd()` at the original value; +// 4. If the current directory is deleted before starting bun, the CLI catches +// it and exits with an error code. +// +// Because of all this, I cannot reproduce a scenario where cwd is empty, so +// this test is commented out. diff --git a/test/js/node/test/parallel/test-path-win32-exists.js b/test/js/node/test/parallel/test-path-win32-exists.js new file mode 100644 index 0000000000..c9efa74dbd --- /dev/null +++ b/test/js/node/test/parallel/test-path-win32-exists.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(require('path/win32'), require('path').win32); diff --git a/test/js/node/test/parallel/test-path-zero-length-strings.js b/test/js/node/test/parallel/test-path-zero-length-strings.js new file mode 100644 index 0000000000..f6516ffff8 --- /dev/null +++ b/test/js/node/test/parallel/test-path-zero-length-strings.js @@ -0,0 +1,39 @@ +'use strict'; + +// These testcases are specific to one uncommon behavior in path module. Few +// of the functions in path module, treat '' strings as current working +// directory. This test makes sure that the behavior is intact between commits. +// See: https://github.com/nodejs/node/pull/2106 + +require('../common'); +const assert = require('assert'); +const path = require('path'); +const pwd = process.cwd(); + +// Join will internally ignore all the zero-length strings and it will return +// '.' if the joined string is a zero-length string. +assert.strictEqual(path.posix.join(''), '.'); +assert.strictEqual(path.posix.join('', ''), '.'); +assert.strictEqual(path.win32.join(''), '.'); +assert.strictEqual(path.win32.join('', ''), '.'); +assert.strictEqual(path.join(pwd), pwd); +assert.strictEqual(path.join(pwd, ''), pwd); + +// Normalize will return '.' if the input is a zero-length string +assert.strictEqual(path.posix.normalize(''), '.'); +assert.strictEqual(path.win32.normalize(''), '.'); +assert.strictEqual(path.normalize(pwd), pwd); + +// Since '' is not a valid path in any of the common environments, return false +assert.strictEqual(path.posix.isAbsolute(''), false); +assert.strictEqual(path.win32.isAbsolute(''), false); + +// Resolve, internally ignores all the zero-length strings and returns the +// current working directory +assert.strictEqual(path.resolve(''), pwd); +assert.strictEqual(path.resolve('', ''), pwd); + +// Relative, internally calls resolve. So, '' is actually the current directory +assert.strictEqual(path.relative('', pwd), ''); +assert.strictEqual(path.relative(pwd, ''), ''); +assert.strictEqual(path.relative(pwd, pwd), ''); diff --git a/test/js/node/test/parallel/test-path.js b/test/js/node/test/parallel/test-path.js new file mode 100644 index 0000000000..0cb55d42aa --- /dev/null +++ b/test/js/node/test/parallel/test-path.js @@ -0,0 +1,73 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); + +// Test thrown TypeErrors +const typeErrorTests = [true, false, 7, null, {}, undefined, [], NaN]; + +function fail(fn) { + const args = Array.from(arguments).slice(1); + + assert.throws(() => { + fn.apply(null, args); + }, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }); +} + +for (const test of typeErrorTests) { + for (const namespace of [path.posix, path.win32]) { + fail(namespace.join, test); + fail(namespace.resolve, test); + fail(namespace.normalize, test); + fail(namespace.isAbsolute, test); + fail(namespace.relative, test, 'foo'); + fail(namespace.relative, 'foo', test); + fail(namespace.parse, test); + fail(namespace.dirname, test); + fail(namespace.basename, test); + fail(namespace.extname, test); + + // Undefined is a valid value as the second argument to basename + if (test !== undefined) { + fail(namespace.basename, 'foo', test); + } + } +} + +// path.sep tests +// windows +assert.strictEqual(path.win32.sep, '\\'); +// posix +assert.strictEqual(path.posix.sep, '/'); + +// path.delimiter tests +// windows +assert.strictEqual(path.win32.delimiter, ';'); +// posix +assert.strictEqual(path.posix.delimiter, ':'); + +if (common.isWindows) + assert.strictEqual(path, path.win32); +else + assert.strictEqual(path, path.posix); diff --git a/test/js/node/test/parallel/test-perf-gc-crash.js b/test/js/node/test/parallel/test-perf-gc-crash.js new file mode 100644 index 0000000000..d980e91a2f --- /dev/null +++ b/test/js/node/test/parallel/test-perf-gc-crash.js @@ -0,0 +1,25 @@ +'use strict'; + +require('../common'); + +// Refers to https://github.com/nodejs/node/issues/39548 + +// The test fails if this crashes. If it closes normally, +// then all is good. + +const { + PerformanceObserver, +} = require('perf_hooks'); + +// We don't actually care if the observer callback is called here. +const gcObserver = new PerformanceObserver(() => {}); + +gcObserver.observe({ entryTypes: ['gc'] }); + +gcObserver.disconnect(); + +const gcObserver2 = new PerformanceObserver(() => {}); + +gcObserver2.observe({ entryTypes: ['gc'] }); + +gcObserver2.disconnect(); diff --git a/test/js/node/test/parallel/test-performance-measure.js b/test/js/node/test/parallel/test-performance-measure.js new file mode 100644 index 0000000000..949258f96e --- /dev/null +++ b/test/js/node/test/parallel/test-performance-measure.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const { PerformanceObserver, performance } = require('perf_hooks'); +const DELAY = 1000; +const ALLOWED_MARGIN = 10; + +const expected = ['Start to Now', 'A to Now', 'A to B']; +const obs = new PerformanceObserver(common.mustCall((items) => { + items.getEntries().forEach(({ name, duration }) => { + assert.ok(duration > (DELAY - ALLOWED_MARGIN)); + assert.strictEqual(expected.shift(), name); + }); +})); +obs.observe({ entryTypes: ['measure'] }); + +performance.mark('A'); +setTimeout(common.mustCall(() => { + performance.measure('Start to Now'); + performance.measure('A to Now', 'A'); + + performance.mark('B'); + performance.measure('A to B', 'A', 'B'); +}), DELAY); diff --git a/test/js/node/test/parallel/test-performanceobserver-gc.js b/test/js/node/test/parallel/test-performanceobserver-gc.js new file mode 100644 index 0000000000..fe9397631c --- /dev/null +++ b/test/js/node/test/parallel/test-performanceobserver-gc.js @@ -0,0 +1,17 @@ +'use strict'; + +require('../common'); + +// Verifies that setting up two observers to listen +// to gc performance does not crash. + +const { + PerformanceObserver, +} = require('perf_hooks'); + +// We don't actually care if the callback is ever invoked in this test +const obs = new PerformanceObserver(() => {}); +const obs2 = new PerformanceObserver(() => {}); + +obs.observe({ type: 'gc' }); +obs2.observe({ type: 'gc' }); diff --git a/test/js/node/test/parallel/test-pipe-abstract-socket-http.js b/test/js/node/test/parallel/test-pipe-abstract-socket-http.js new file mode 100644 index 0000000000..6d3beb44d1 --- /dev/null +++ b/test/js/node/test/parallel/test-pipe-abstract-socket-http.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +if (!common.isLinux) common.skip(); + +const server = http.createServer( + common.mustCall((req, res) => { + res.end('ok'); + }) +); + +server.listen( + '\0abstract', + common.mustCall(() => { + http.get( + { + socketPath: server.address(), + }, + common.mustCall((res) => { + assert.strictEqual(res.statusCode, 200); + server.close(); + }) + ); + }) +); diff --git a/test/js/node/test/parallel/test-pipe-address.js b/test/js/node/test/parallel/test-pipe-address.js new file mode 100644 index 0000000000..77ada78670 --- /dev/null +++ b/test/js/node/test/parallel/test-pipe-address.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const assert = require('assert'); +const net = require('net'); +const server = net.createServer(common.mustNotCall()); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +server.listen(common.PIPE, common.mustCall(function() { + assert.strictEqual(server.address(), common.PIPE); + server.close(); +})); diff --git a/test/js/node/test/parallel/test-pipe-file-to-http.js b/test/js/node/test/parallel/test-pipe-file-to-http.js new file mode 100644 index 0000000000..6c1244427d --- /dev/null +++ b/test/js/node/test/parallel/test-pipe-file-to-http.js @@ -0,0 +1,83 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const http = require('http'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('big'); +let count = 0; + +const server = http.createServer((req, res) => { + let timeoutId; + assert.strictEqual(req.method, 'POST'); + req.pause(); + + setTimeout(() => { + req.resume(); + }, 1000); + + req.on('data', (chunk) => { + count += chunk.length; + }); + + req.on('end', () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(); + }); +}); +server.listen(0); + +server.on('listening', () => { + common.createZeroFilledFile(filename); + makeRequest(); +}); + +function makeRequest() { + const req = http.request({ + port: server.address().port, + path: '/', + method: 'POST' + }); + + const s = fs.ReadStream(filename); + s.pipe(req); + s.on('close', common.mustSucceed()); + + req.on('response', (res) => { + res.resume(); + res.on('end', () => { + server.close(); + }); + }); +} + +process.on('exit', () => { + assert.strictEqual(count, 1024 * 10240); +}); diff --git a/test/js/node/test/parallel/test-pipe-head.js b/test/js/node/test/parallel/test-pipe-head.js new file mode 100644 index 0000000000..1e79249c29 --- /dev/null +++ b/test/js/node/test/parallel/test-pipe-head.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); + +const exec = require('child_process').exec; + +const nodePath = process.argv[0]; +const script = fixtures.path('print-10-lines.js'); + +const cmd = `"${nodePath}" "${script}" | head -2`; + +exec(cmd, common.mustSucceed((stdout, stderr) => { + const lines = stdout.split('\n'); + assert.strictEqual(lines.length, 3); +})); diff --git a/test/js/node/test/parallel/test-pipe-return-val.js b/test/js/node/test/parallel/test-pipe-return-val.js new file mode 100644 index 0000000000..f2a7f573ec --- /dev/null +++ b/test/js/node/test/parallel/test-pipe-return-val.js @@ -0,0 +1,33 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// This test ensures SourceStream.pipe(DestStream) returns DestStream + +require('../common'); +const Stream = require('stream').Stream; +const assert = require('assert'); + +const sourceStream = new Stream(); +const destStream = new Stream(); +const result = sourceStream.pipe(destStream); + +assert.strictEqual(result, destStream); diff --git a/test/js/node/test/parallel/test-pipe-writev.js b/test/js/node/test/parallel/test-pipe-writev.js new file mode 100644 index 0000000000..5e5b42e6a7 --- /dev/null +++ b/test/js/node/test/parallel/test-pipe-writev.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); +if (common.isWindows) + common.skip('Unix-specific test'); + +const assert = require('assert'); +const net = require('net'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const server = net.createServer((connection) => { + connection.on('error', (err) => { + throw err; + }); + + const writev = connection._writev.bind(connection); + connection._writev = common.mustCall(writev); + + connection.cork(); + connection.write('pi'); + connection.write('ng'); + connection.end(); +}); + +server.on('error', (err) => { + throw err; +}); + +server.listen(common.PIPE, () => { + const client = net.connect(common.PIPE); + + client.on('error', (err) => { + throw err; + }); + + client.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'ping'); + })); + + client.on('end', () => { + server.close(); + }); +}); diff --git a/test/js/node/test/parallel/test-preload-print-process-argv.js b/test/js/node/test/parallel/test-preload-print-process-argv.js new file mode 100644 index 0000000000..9d2774f8a4 --- /dev/null +++ b/test/js/node/test/parallel/test-preload-print-process-argv.js @@ -0,0 +1,34 @@ +'use strict'; + +// This tests that process.argv is the same in the preloaded module +// and the user module. + +require('../common'); + +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fs = require('fs'); + +tmpdir.refresh(); + +fs.writeFileSync( + tmpdir.resolve('preload.js'), + 'console.log(JSON.stringify(process.argv));', + 'utf-8'); + +fs.writeFileSync( + tmpdir.resolve('main.js'), + 'console.log(JSON.stringify(process.argv));', + 'utf-8'); + +const child = spawnSync(process.execPath, ['-r', './preload.js', 'main.js'], + { cwd: tmpdir.path }); + +if (child.status !== 0) { + console.log(child.stderr.toString()); + assert.strictEqual(child.status, 0); +} + +const lines = child.stdout.toString().trim().split('\n'); +assert.deepStrictEqual(JSON.parse(lines[0]), JSON.parse(lines[1])); diff --git a/test/js/node/test/parallel/test-preload-self-referential.js b/test/js/node/test/parallel/test-preload-self-referential.js new file mode 100644 index 0000000000..2624527deb --- /dev/null +++ b/test/js/node/test/parallel/test-preload-self-referential.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { exec } = require('child_process'); + +const nodeBinary = process.argv[0]; + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const selfRefModule = fixtures.path('self_ref_module'); +const fixtureA = fixtures.path('printA.js'); + +exec(`"${nodeBinary}" -r self_ref "${fixtureA}"`, { cwd: selfRefModule }, + (err, stdout, stderr) => { + assert.ifError(err); + assert.strictEqual(stdout, 'A\n'); + }); diff --git a/test/js/node/test/parallel/test-process-abort.js b/test/js/node/test/parallel/test-process-abort.js new file mode 100644 index 0000000000..665e1399a3 --- /dev/null +++ b/test/js/node/test/parallel/test-process-abort.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (!common.isMainThread) + common.skip('process.abort() is not available in Workers'); + +// Check that our built-in methods do not have a prototype/constructor behaviour +// if they don't need to. This could be tested for any of our C++ methods. +assert.strictEqual(process.abort.prototype, undefined); +assert.throws(() => new process.abort(), TypeError); diff --git a/test/js/node/test/parallel/test-process-argv-0.js b/test/js/node/test/parallel/test-process-argv-0.js new file mode 100644 index 0000000000..21b406873f --- /dev/null +++ b/test/js/node/test/parallel/test-process-argv-0.js @@ -0,0 +1,42 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const path = require('path'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +if (process.argv[2] !== 'child') { + const child = spawn(process.execPath, [__filename, 'child'], { + cwd: path.dirname(process.execPath) + }); + + let childArgv0 = ''; + child.stdout.on('data', function(chunk) { + childArgv0 += chunk; + }); + process.on('exit', function() { + assert.strictEqual(childArgv0, process.execPath); + }); +} else { + process.stdout.write(process.argv[0]); +} diff --git a/test/js/node/test/parallel/test-process-assert.js b/test/js/node/test/parallel/test-process-assert.js new file mode 100644 index 0000000000..f740d3d70c --- /dev/null +++ b/test/js/node/test/parallel/test-process-assert.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +assert.strictEqual(process.assert(1, 'error'), undefined); +assert.throws(() => { + process.assert(undefined, 'errorMessage'); +}, { + code: 'ERR_ASSERTION', + name: 'Error', + message: 'errorMessage' +}); +assert.throws(() => { + process.assert(false); +}, { + code: 'ERR_ASSERTION', + name: 'Error', + message: 'assertion error' +}); diff --git a/test/js/node/test/parallel/test-process-available-memory.js b/test/js/node/test/parallel/test-process-available-memory.js new file mode 100644 index 0000000000..67de5b5e0b --- /dev/null +++ b/test/js/node/test/parallel/test-process-available-memory.js @@ -0,0 +1,5 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const availableMemory = process.availableMemory(); +assert(typeof availableMemory, 'number'); diff --git a/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js b/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js new file mode 100644 index 0000000000..6e9d764be9 --- /dev/null +++ b/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +common.skipIfWorker(); + +// Test that 'exit' is emitted if 'beforeExit' throws. + +process.on('exit', common.mustCall(() => { + process.exitCode = 0; +})); +process.on('beforeExit', common.mustCall(() => { + throw new Error(); +})); diff --git a/test/js/node/test/parallel/test-process-beforeexit.js b/test/js/node/test/parallel/test-process-beforeexit.js new file mode 100644 index 0000000000..e04b756cad --- /dev/null +++ b/test/js/node/test/parallel/test-process-beforeexit.js @@ -0,0 +1,81 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); + +process.once('beforeExit', common.mustCall(tryImmediate)); + +function tryImmediate() { + setImmediate(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryTimer)); + })); +} + +function tryTimer() { + setTimeout(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryListen)); + }), 1); +} + +function tryListen() { + net.createServer() + .listen(0) + .on('listening', common.mustCall(function() { + this.close(); + process.once('beforeExit', common.mustCall(tryRepeatedTimer)); + })); +} + +// Test that a function invoked from the beforeExit handler can use a timer +// to keep the event loop open, which can use another timer to keep the event +// loop open, etc. +// +// After N times, call function `tryNextTick` to test behaviors of the +// `process.nextTick`. +function tryRepeatedTimer() { + const N = 5; + let n = 0; + const repeatedTimer = common.mustCall(function() { + if (++n < N) + setTimeout(repeatedTimer, 1); + else // n == N + process.once('beforeExit', common.mustCall(tryNextTickSetImmediate)); + }, N); + setTimeout(repeatedTimer, 1); +} + +// Test if the callback of `process.nextTick` can be invoked. +function tryNextTickSetImmediate() { + process.nextTick(common.mustCall(function() { + setImmediate(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryNextTick)); + })); + })); +} + +// Test that `process.nextTick` won't keep the event loop running by itself. +function tryNextTick() { + process.nextTick(common.mustCall(function() { + process.once('beforeExit', common.mustNotCall()); + })); +} diff --git a/test/js/node/test/parallel/test-process-binding-util.js b/test/js/node/test/parallel/test-process-binding-util.js new file mode 100644 index 0000000000..a834676e05 --- /dev/null +++ b/test/js/node/test/parallel/test-process-binding-util.js @@ -0,0 +1,58 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const util = require('util'); + +const utilBinding = process.binding('util'); +assert.deepStrictEqual( + Object.keys(utilBinding).sort(), + [ + 'isAnyArrayBuffer', + 'isArgumentsObject', + 'isArrayBuffer', + 'isArrayBufferView', + 'isAsyncFunction', + 'isBigInt64Array', + 'isBigIntObject', + 'isBigUint64Array', + 'isBooleanObject', + 'isBoxedPrimitive', + 'isCryptoKey', + 'isDataView', + 'isDate', + 'isEventTarget', + 'isExternal', + 'isFloat16Array', + 'isFloat32Array', + 'isFloat64Array', + 'isGeneratorFunction', + 'isGeneratorObject', + 'isInt16Array', + 'isInt32Array', + 'isInt8Array', + 'isKeyObject', + 'isMap', + 'isMapIterator', + 'isModuleNamespaceObject', + 'isNativeError', + 'isNumberObject', + 'isPromise', + 'isProxy', + 'isRegExp', + 'isSet', + 'isSetIterator', + 'isSharedArrayBuffer', + 'isStringObject', + 'isSymbolObject', + 'isTypedArray', + 'isUint16Array', + 'isUint32Array', + 'isUint8Array', + 'isUint8ClampedArray', + 'isWeakMap', + 'isWeakSet', + ]); + +for (const k of Object.keys(utilBinding)) { + assert.strictEqual(utilBinding[k], util.types[k]); +} diff --git a/test/js/node/test/parallel/test-process-chdir-errormessage.js b/test/js/node/test/parallel/test-process-chdir-errormessage.js new file mode 100644 index 0000000000..0ed368287b --- /dev/null +++ b/test/js/node/test/parallel/test-process-chdir-errormessage.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); +const assert = require('assert'); + +assert.throws( + () => { + process.chdir('does-not-exist'); + }, + { + name: 'Error', + code: 'ENOENT', + message: /ENOENT: no such file or directory, chdir .+ -> 'does-not-exist'/, + path: process.cwd(), + syscall: 'chdir', + dest: 'does-not-exist' + } +); diff --git a/test/js/node/test/parallel/test-process-chdir.js b/test/js/node/test/parallel/test-process-chdir.js new file mode 100644 index 0000000000..ee59df853b --- /dev/null +++ b/test/js/node/test/parallel/test-process-chdir.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const tmpdir = require('../common/tmpdir'); + +process.chdir('..'); +assert.notStrictEqual(process.cwd(), __dirname); +process.chdir(__dirname); +assert.strictEqual(process.cwd(), __dirname); + +let dirName; +if (process.versions.icu) { + // ICU is available, use characters that could possibly be decomposed + dirName = 'weird \uc3a4\uc3ab\uc3af characters \u00e1\u00e2\u00e3'; +} else { + // ICU is unavailable, use characters that can't be decomposed + dirName = 'weird \ud83d\udc04 characters \ud83d\udc05'; +} +const dir = tmpdir.resolve(dirName); + +// Make sure that the tmp directory is clean +tmpdir.refresh(); + +fs.mkdirSync(dir); +process.chdir(dir); +assert.strictEqual(process.cwd().normalize(), dir.normalize()); + +process.chdir('..'); +assert.strictEqual(process.cwd().normalize(), + path.resolve(tmpdir.path).normalize()); + +const err = { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "directory" argument must be of type string/ +}; +assert.throws(function() { process.chdir({}); }, err); +assert.throws(function() { process.chdir(); }, err); diff --git a/test/js/node/test/parallel/test-process-config.js b/test/js/node/test/parallel/test-process-config.js new file mode 100644 index 0000000000..20ebc36a99 --- /dev/null +++ b/test/js/node/test/parallel/test-process-config.js @@ -0,0 +1,69 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); + +// Checks that the internal process.config is equivalent to the config.gypi file +// created when we run configure. + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +// Check for existence of `process.config`. +assert(Object.hasOwn(process, 'config')); + +// Ensure that `process.config` is an Object. +assert.strictEqual(Object(process.config), process.config); + +// Ensure that you can't change config values +assert.throws(() => { process.config.variables = 42; }, TypeError); + +const configPath = path.resolve(__dirname, '..', '..', 'config.gypi'); + +if (!fs.existsSync(configPath)) { + common.skip('config.gypi does not exist.'); +} + +let config = fs.readFileSync(configPath, 'utf8'); + +// Clean up comment at the first line. +config = config.split('\n').slice(1).join('\n'); +config = config.replace(/"/g, '\\"'); +config = config.replace(/'/g, '"'); +config = JSON.parse(config, (key, value) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; +}); + +try { + assert.deepStrictEqual(config, process.config); +} catch (e) { + // If the assert fails, it only shows 3 lines. We need all the output to + // compare. + console.log('config:', config); + console.log('process.config:', process.config); + + throw e; +} diff --git a/test/js/node/test/parallel/test-process-constants-noatime.js b/test/js/node/test/parallel/test-process-constants-noatime.js new file mode 100644 index 0000000000..bd1a848ed7 --- /dev/null +++ b/test/js/node/test/parallel/test-process-constants-noatime.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const constants = require('fs').constants; + +if (common.isLinux) { + assert('O_NOATIME' in constants); + assert.strictEqual(constants.O_NOATIME, 0x40000); +} else { + assert(!('O_NOATIME' in constants)); +} diff --git a/test/js/node/test/parallel/test-process-constrained-memory.js b/test/js/node/test/parallel/test-process-constrained-memory.js new file mode 100644 index 0000000000..03f99b166f --- /dev/null +++ b/test/js/node/test/parallel/test-process-constrained-memory.js @@ -0,0 +1,6 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const constrainedMemory = process.constrainedMemory(); +assert.strictEqual(typeof constrainedMemory, 'number'); diff --git a/test/js/node/test/parallel/test-process-cpuUsage.js b/test/js/node/test/parallel/test-process-cpuUsage.js new file mode 100644 index 0000000000..f1580d5f09 --- /dev/null +++ b/test/js/node/test/parallel/test-process-cpuUsage.js @@ -0,0 +1,118 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const result = process.cpuUsage(); + +// Validate the result of calling with no previous value argument. +validateResult(result); + +// Validate the result of calling with a previous value argument. +validateResult(process.cpuUsage(result)); + +// Ensure the results are >= the previous. +let thisUsage; +let lastUsage = process.cpuUsage(); +for (let i = 0; i < 10; i++) { + thisUsage = process.cpuUsage(); + validateResult(thisUsage); + assert(thisUsage.user >= lastUsage.user); + assert(thisUsage.system >= lastUsage.system); + lastUsage = thisUsage; +} + +// Ensure that the diffs are >= 0. +let startUsage; +let diffUsage; +for (let i = 0; i < 10; i++) { + startUsage = process.cpuUsage(); + diffUsage = process.cpuUsage(startUsage); + validateResult(startUsage); + validateResult(diffUsage); + assert(diffUsage.user >= 0); + assert(diffUsage.system >= 0); +} + +// Ensure that an invalid shape for the previous value argument throws an error. +assert.throws( + () => process.cpuUsage(1), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue" argument must be of type object. ' + + 'Received type number (1)' + } +); + +// Check invalid types. +[ + {}, + { user: 'a' }, + { user: null, system: 'c' }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue.user" property must be of type number.' + + common.invalidArgTypeHelper(value.user) + } + ); +}); + +[ + { user: 3, system: 'b' }, + { user: 3, system: null }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue.system" property must be of type number.' + + common.invalidArgTypeHelper(value.system) + } + ); +}); + +// Check invalid values. +[ + { user: -1, system: 2 }, + { user: Number.POSITIVE_INFINITY, system: 4 }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'RangeError', + message: "The property 'prevValue.user' is invalid. " + + `Received ${value.user}`, + } + ); +}); + +[ + { user: 3, system: -2 }, + { user: 5, system: Number.NEGATIVE_INFINITY }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'RangeError', + message: "The property 'prevValue.system' is invalid. " + + `Received ${value.system}`, + } + ); +}); + +// Ensure that the return value is the expected shape. +function validateResult(result) { + assert.notStrictEqual(result, null); + + assert(Number.isFinite(result.user)); + assert(Number.isFinite(result.system)); + + assert(result.user >= 0); + assert(result.system >= 0); +} diff --git a/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js b/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js new file mode 100644 index 0000000000..cc93e01abd --- /dev/null +++ b/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js @@ -0,0 +1,47 @@ +'use strict'; + +// This is a regression test for some scenarios in which node would pass +// unsanitized user input to a printf-like formatting function when dlopen +// fails, potentially crashing the process. + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const assert = require('assert'); +const fs = require('fs'); + +// This error message should not be passed to a printf-like function. +assert.throws(() => { + process.dlopen({ exports: {} }, 'foo-%s.node'); +}, ({ name, code, message }) => { + assert.strictEqual(name, 'Error'); + assert.strictEqual(code, 'ERR_DLOPEN_FAILED'); + if (!common.isAIX && !common.isIBMi) { + assert.match(message, /foo-%s\.node/); + } + return true; +}); + +const notBindingDir = 'test/addons/not-a-binding'; +const notBindingPath = `${notBindingDir}/build/Release/binding.node`; +const strangeBindingPath = `${tmpdir.path}/binding-%s.node`; +// Ensure that the addon directory exists, but skip the remainder of the test if +// the addon has not been compiled. +// fs.accessSync(notBindingDir); +// try { +// fs.copyFileSync(notBindingPath, strangeBindingPath); +// } catch (err) { +// if (err.code !== 'ENOENT') throw err; +// common.skip(`addon not found: ${notBindingPath}`); +// } + +// This error message should also not be passed to a printf-like function. +assert.throws(() => { + process.dlopen({ exports: {} }, strangeBindingPath); +}, { + name: 'Error', + code: 'ERR_DLOPEN_FAILED', + message: /binding-%s\.node/ +}); diff --git a/test/js/node/test/parallel/test-process-dlopen-undefined-exports.js b/test/js/node/test/parallel/test-process-dlopen-undefined-exports.js new file mode 100644 index 0000000000..3766a73a45 --- /dev/null +++ b/test/js/node/test/parallel/test-process-dlopen-undefined-exports.js @@ -0,0 +1,10 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const someBindingPath = './test/addons/hello-world/build/Release/binding.node'; + +assert.throws(() => { + process.dlopen({ exports: undefined }, someBindingPath); +}, Error); diff --git a/test/js/node/test/parallel/test-process-domain-segfault.js b/test/js/node/test/parallel/test-process-domain-segfault.js new file mode 100644 index 0000000000..78009f4687 --- /dev/null +++ b/test/js/node/test/parallel/test-process-domain-segfault.js @@ -0,0 +1,32 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test ensures that setting `process.domain` to `null` does not result in +// node crashing with a segfault. +// https://github.com/nodejs/node-v0.x-archive/issues/4256 + +process.domain = null; +setTimeout(function() { + console.log('this console.log statement should not make node crash'); +}, 1); diff --git a/test/js/node/test/parallel/test-process-emit.js b/test/js/node/test/parallel/test-process-emit.js new file mode 100644 index 0000000000..8c2ad675cf --- /dev/null +++ b/test/js/node/test/parallel/test-process-emit.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const sym = Symbol(); + +process.on('normal', common.mustCall((data) => { + assert.strictEqual(data, 'normalData'); +})); + +process.on(sym, common.mustCall((data) => { + assert.strictEqual(data, 'symbolData'); +})); + +process.on('SIGPIPE', common.mustCall((data) => { + assert.strictEqual(data, 'signalData'); +})); + +process.emit('normal', 'normalData'); +process.emit(sym, 'symbolData'); +process.emit('SIGPIPE', 'signalData'); + +assert.strictEqual(Number.isNaN(process._eventsCount), false); diff --git a/test/js/node/test/parallel/test-process-emitwarning.js b/test/js/node/test/parallel/test-process-emitwarning.js new file mode 100644 index 0000000000..e1c7473f8a --- /dev/null +++ b/test/js/node/test/parallel/test-process-emitwarning.js @@ -0,0 +1,81 @@ +// Flags: --no-warnings +// The flag suppresses stderr output but the warning event will still emit +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const testMsg = 'A Warning'; +const testCode = 'CODE001'; +const testDetail = 'Some detail'; +const testType = 'CustomWarning'; + +process.on('warning', common.mustCall((warning) => { + assert(warning); + assert.match(warning.name, /^(?:Warning|CustomWarning)/); + assert.strictEqual(warning.message, testMsg); + if (warning.code) assert.strictEqual(warning.code, testCode); + if (warning.detail) assert.strictEqual(warning.detail, testDetail); +}, 15)); + +class CustomWarning extends Error { + constructor() { + super(); + this.name = testType; + this.message = testMsg; + this.code = testCode; + Error.captureStackTrace(this, CustomWarning); + } +} + +[ + [testMsg], + [testMsg, testType], + [testMsg, CustomWarning], + [testMsg, testType, CustomWarning], + [testMsg, testType, testCode], + [testMsg, { type: testType }], + [testMsg, { type: testType, code: testCode }], + [testMsg, { type: testType, code: testCode, detail: testDetail }], + [new CustomWarning()], + // Detail will be ignored for the following. No errors thrown + [testMsg, { type: testType, code: testCode, detail: true }], + [testMsg, { type: testType, code: testCode, detail: [] }], + [testMsg, { type: testType, code: testCode, detail: null }], + [testMsg, { type: testType, code: testCode, detail: 1 }], +].forEach((args) => { + process.emitWarning(...args); +}); + +const warningNoToString = new CustomWarning(); +warningNoToString.toString = null; +process.emitWarning(warningNoToString); + +const warningThrowToString = new CustomWarning(); +warningThrowToString.toString = function() { + throw new Error('invalid toString'); +}; +process.emitWarning(warningThrowToString); + +// TypeError is thrown on invalid input +[ + [1], + [{}], + [true], + [[]], + ['', '', {}], + ['', 1], + ['', '', 1], + ['', true], + ['', '', true], + ['', []], + ['', '', []], + [], + [undefined, 'foo', 'bar'], + [undefined], +].forEach((args) => { + assert.throws( + () => process.emitWarning(...args), + { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' } + ); +}); diff --git a/test/js/node/test/parallel/test-process-env-windows-error-reset.js b/test/js/node/test/parallel/test-process-env-windows-error-reset.js new file mode 100644 index 0000000000..881da06d29 --- /dev/null +++ b/test/js/node/test/parallel/test-process-env-windows-error-reset.js @@ -0,0 +1,22 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +// This checks that after accessing a missing env var, a subsequent +// env read will succeed even for empty variables. + +{ + process.env.FOO = ''; + process.env.NONEXISTENT_ENV_VAR; // eslint-disable-line no-unused-expressions + const foo = process.env.FOO; + + assert.strictEqual(foo, ''); +} + +{ + process.env.FOO = ''; + process.env.NONEXISTENT_ENV_VAR; // eslint-disable-line no-unused-expressions + const hasFoo = 'FOO' in process.env; + + assert.strictEqual(hasFoo, true); +} diff --git a/test/js/node/test/parallel/test-process-euid-egid.js b/test/js/node/test/parallel/test-process-euid-egid.js new file mode 100644 index 0000000000..06854ba3f5 --- /dev/null +++ b/test/js/node/test/parallel/test-process-euid-egid.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (common.isWindows) { + assert.strictEqual(process.geteuid, undefined); + assert.strictEqual(process.getegid, undefined); + assert.strictEqual(process.seteuid, undefined); + assert.strictEqual(process.setegid, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws(() => { + process.seteuid({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "id" argument must be of type number or string. ' + + 'Received an instance of Object' +}); + +assert.throws(() => { + process.seteuid('fhqwhgadshgnsdhjsdbkhsdabkfabkveyb'); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'User identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); + +// IBMi does not support below operations. +if (common.isIBMi) + return; + +// If we're not running as super user... +if (process.getuid() !== 0) { + // Should not throw. + process.getegid(); + process.geteuid(); + + assert.throws(() => { + process.setegid('nobody'); + }, /(?:EPERM: .+|Group identifier does not exist: nobody)$/); + + assert.throws(() => { + process.seteuid('nobody'); + }, /(?:EPERM: .+|User identifier does not exist: nobody)$/); + + return; +} + +// If we are running as super user... +const oldgid = process.getegid(); +try { + process.setegid('nobody'); +} catch (err) { + if (err.message !== 'Group identifier does not exist: nobody') { + throw err; + } else { + process.setegid('nogroup'); + } +} +const newgid = process.getegid(); +assert.notStrictEqual(newgid, oldgid); + +const olduid = process.geteuid(); +process.seteuid('nobody'); +const newuid = process.geteuid(); +assert.notStrictEqual(newuid, olduid); diff --git a/test/js/node/test/parallel/test-process-exception-capture-errors.js b/test/js/node/test/parallel/test-process-exception-capture-errors.js new file mode 100644 index 0000000000..8eb825267c --- /dev/null +++ b/test/js/node/test/parallel/test-process-exception-capture-errors.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +assert.throws( + () => process.setUncaughtExceptionCaptureCallback(42), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "fn" argument must be of type function or null. ' + + 'Received type number (42)' + } +); + +process.setUncaughtExceptionCaptureCallback(common.mustNotCall()); + +assert.throws( + () => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()), + { + code: 'ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET', + name: 'Error', + message: /setupUncaughtExceptionCapture.*called while a capture callback/ + } +); diff --git a/test/js/node/test/parallel/test-process-exception-capture-should-abort-on-uncaught.js b/test/js/node/test/parallel/test-process-exception-capture-should-abort-on-uncaught.js new file mode 100644 index 0000000000..fd7443153f --- /dev/null +++ b/test/js/node/test/parallel/test-process-exception-capture-should-abort-on-uncaught.js @@ -0,0 +1,13 @@ +// Flags: --abort-on-uncaught-exception +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN https://github.com/oven-sh/bun/issues/12827 +const assert = require('assert'); + +assert.strictEqual(process.hasUncaughtExceptionCaptureCallback(), false); + +// This should make the process not crash even though the flag was passed. +process.setUncaughtExceptionCaptureCallback(common.mustCall((err) => { + assert.strictEqual(err.message, 'foo'); +})); +throw new Error('foo'); diff --git a/test/js/node/test/parallel/test-process-exception-capture.js b/test/js/node/test/parallel/test-process-exception-capture.js new file mode 100644 index 0000000000..3b38247359 --- /dev/null +++ b/test/js/node/test/parallel/test-process-exception-capture.js @@ -0,0 +1,14 @@ +// Flags: --abort-on-uncaught-exception +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN https://github.com/oven-sh/bun/issues/12827 +const assert = require('assert'); + +assert.strictEqual(process.hasUncaughtExceptionCaptureCallback(), false); + +// This should make the process not crash even though the flag was passed. +process.setUncaughtExceptionCaptureCallback(common.mustCall((err) => { + assert.strictEqual(err.message, 'foo'); +})); +process.on('uncaughtException', common.mustNotCall()); +throw new Error('foo'); diff --git a/test/js/node/test/parallel/test-process-execpath.js b/test/js/node/test/parallel/test-process-execpath.js new file mode 100644 index 0000000000..0fce35e264 --- /dev/null +++ b/test/js/node/test/parallel/test-process-execpath.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) + common.skip('symlinks are weird on windows'); + +const assert = require('assert'); +const child_process = require('child_process'); +const fs = require('fs'); + +assert.strictEqual(process.execPath, fs.realpathSync(process.execPath)); + +if (process.argv[2] === 'child') { + // The console.log() output is part of the test here. + console.log(process.execPath); +} else { + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + + const symlinkedNode = tmpdir.resolve('symlinked-node'); + fs.symlinkSync(process.execPath, symlinkedNode); + + const proc = child_process.spawnSync(symlinkedNode, [__filename, 'child']); + assert.strictEqual(proc.stderr.toString(), ''); + assert.strictEqual(proc.stdout.toString(), `${process.execPath}\n`); + assert.strictEqual(proc.status, 0); +} diff --git a/test/js/node/test/parallel/test-process-exit-code-validation.js b/test/js/node/test/parallel/test-process-exit-code-validation.js new file mode 100644 index 0000000000..59934fa31d --- /dev/null +++ b/test/js/node/test/parallel/test-process-exit-code-validation.js @@ -0,0 +1,145 @@ +'use strict'; + +require('../common'); + +const invalids = [ + { + code: '', + expected: 1, + pattern: 'Received type string \\(""\\)$', + }, + { + code: '1 one', + expected: 1, + pattern: 'Received type string \\("1 one"\\)$', + }, + { + code: 'two', + expected: 1, + pattern: 'Received type string \\("two"\\)$', + }, + { + code: {}, + expected: 1, + pattern: 'Received an instance of Object$', + }, + { + code: [], + expected: 1, + pattern: 'Received an instance of Array$', + }, + { + code: true, + expected: 1, + pattern: 'Received type boolean \\(true\\)$', + }, + { + code: false, + expected: 1, + pattern: 'Received type boolean \\(false\\)$', + }, + { + code: 2n, + expected: 1, + pattern: 'Received type bigint \\(2n\\)$', + }, + { + code: 2.1, + expected: 1, + pattern: 'Received 2.1$', + }, + { + code: Infinity, + expected: 1, + pattern: 'Received Infinity$', + }, + { + code: NaN, + expected: 1, + pattern: 'Received NaN$', + }, +]; +const valids = [ + { + code: 1, + expected: 1, + }, + { + code: '2', + expected: 2, + }, + { + code: undefined, + expected: 0, + }, + { + code: null, + expected: 0, + }, + { + code: 0, + expected: 0, + }, + { + code: '0', + expected: 0, + }, +]; +const args = [...invalids, ...valids]; + +if (process.argv[2] === undefined) { + const { spawnSync } = require('node:child_process'); + const { inspect, debuglog } = require('node:util'); + const { throws, strictEqual } = require('node:assert'); + + const debug = debuglog('test'); + const node = process.execPath; + const test = (index, useProcessExitCode) => { + const { status: code } = spawnSync(node, [ + __filename, + index, + useProcessExitCode, + ]); + console.log(`actual: ${code}, ${args[index].expected} ${index} ${!!useProcessExitCode} ${args[index].code}`); + debug(`actual: ${code}, ${inspect(args[index])} ${!!useProcessExitCode}`); + strictEqual( + code, + args[index].expected, + `actual: ${code}, ${inspect(args[index])}` + ); + }; + + // Check process.exitCode + for (const arg of invalids) { + debug(`invaild code: ${inspect(arg.code)}`); + throws(() => (process.exitCode = arg.code), new RegExp(arg.pattern)); + } + for (const arg of valids) { + debug(`vaild code: ${inspect(arg.code)}`); + process.exitCode = arg.code; + } + + throws(() => { + delete process.exitCode; + // }, /Cannot delete property 'exitCode' of #/); + }, /Unable to delete property./); + process.exitCode = 0; + + // Check process.exit([code]) + for (const index of args.keys()) { + test(index); + test(index, true); + } +} else { + const index = parseInt(process.argv[2]); + const useProcessExitCode = process.argv[3] !== 'undefined'; + if (Number.isNaN(index)) { + return process.exit(100); + } + + if (useProcessExitCode) { + process.exitCode = args[index].code; + } else { + process.exit(args[index].code); + } +} diff --git a/test/js/node/test/parallel/test-process-exit-from-before-exit.js b/test/js/node/test/parallel/test-process-exit-from-before-exit.js new file mode 100644 index 0000000000..7f20c22f0b --- /dev/null +++ b/test/js/node/test/parallel/test-process-exit-from-before-exit.js @@ -0,0 +1,30 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +process.on('beforeExit', common.mustCall(function() { + setTimeout(common.mustNotCall(), 5); + process.exit(0); // Should execute immediately even if we schedule new work. + assert.fail(); +})); diff --git a/test/js/node/test/parallel/test-process-exit-handler.js b/test/js/node/test/parallel/test-process-exit-handler.js new file mode 100644 index 0000000000..d74e320fe6 --- /dev/null +++ b/test/js/node/test/parallel/test-process-exit-handler.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); + +if (!common.isMainThread) + common.skip('execArgv does not affect Workers'); + +// This test ensures that no asynchronous operations are performed in the 'exit' +// handler. +// https://github.com/nodejs/node/issues/12322 + +process.on('exit', () => { + setTimeout(() => process.abort(), 0); // Should not run. + for (const start = Date.now(); Date.now() - start < 10;); +}); diff --git a/test/js/node/test/parallel/test-process-exit-recursive.js b/test/js/node/test/parallel/test-process-exit-recursive.js new file mode 100644 index 0000000000..727aa4abe7 --- /dev/null +++ b/test/js/node/test/parallel/test-process-exit-recursive.js @@ -0,0 +1,37 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// Recursively calling .exit() should not overflow the call stack +let nexits = 0; + +process.on('exit', function(code) { + assert.strictEqual(nexits++, 0); + assert.strictEqual(code, 1); + + // Now override the exit code of 1 with 0 so that the test passes + process.exit(0); +}); + +process.exit(1); diff --git a/test/js/node/test/parallel/test-process-exit.js b/test/js/node/test/parallel/test-process-exit.js new file mode 100644 index 0000000000..cd605949af --- /dev/null +++ b/test/js/node/test/parallel/test-process-exit.js @@ -0,0 +1,35 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// Calling .exit() from within "exit" should not overflow the call stack +let nexits = 0; + +process.on('exit', function(code) { + assert.strictEqual(nexits++, 0); + assert.strictEqual(code, 0); + process.exit(); +}); + +// "exit" should be emitted unprovoked diff --git a/test/js/node/test/parallel/test-process-features.js b/test/js/node/test/parallel/test-process-features.js new file mode 100644 index 0000000000..3b4677c561 --- /dev/null +++ b/test/js/node/test/parallel/test-process-features.js @@ -0,0 +1,22 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const keys = new Set(Object.keys(process.features)); + +assert.deepStrictEqual(keys, new Set([ + 'inspector', + 'debug', + 'uv', + 'ipv6', + 'tls_alpn', + 'tls_sni', + 'tls_ocsp', + 'tls', + 'cached_builtins', +])); + +for (const key of keys) { + assert.strictEqual(typeof process.features[key], 'boolean'); +} diff --git a/test/js/node/test/parallel/test-process-getgroups.js b/test/js/node/test/parallel/test-process-getgroups.js new file mode 100644 index 0000000000..28df13205f --- /dev/null +++ b/test/js/node/test/parallel/test-process-getgroups.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Check `id -G` and `process.getgroups()` return same groups. + +if (common.isMacOS) + common.skip('Output of `id -G` is unreliable on Darwin.'); + +const assert = require('assert'); +const exec = require('child_process').exec; + +if (typeof process.getgroups === 'function') { + const groups = unique(process.getgroups()); + assert(Array.isArray(groups)); + assert(groups.length > 0); + exec('id -G', function(err, stdout) { + assert.ifError(err); + const real_groups = unique(stdout.match(/\d+/g).map(Number)); + assert.deepStrictEqual(groups, real_groups); + check(groups, real_groups); + check(real_groups, groups); + }); +} + +function check(a, b) { + for (let i = 0; i < a.length; ++i) assert(b.includes(a[i])); +} + +function unique(groups) { + return [...new Set(groups)].sort(); +} diff --git a/test/js/node/test/parallel/test-process-hrtime-bigint.js b/test/js/node/test/parallel/test-process-hrtime-bigint.js new file mode 100644 index 0000000000..e5ce40a994 --- /dev/null +++ b/test/js/node/test/parallel/test-process-hrtime-bigint.js @@ -0,0 +1,14 @@ +'use strict'; + +// Tests that process.hrtime.bigint() works. + +require('../common'); +const assert = require('assert'); + +const start = process.hrtime.bigint(); +assert.strictEqual(typeof start, 'bigint'); + +const end = process.hrtime.bigint(); +assert.strictEqual(typeof end, 'bigint'); + +assert(end - start >= 0n); diff --git a/test/js/node/test/parallel/test-process-hrtime.js b/test/js/node/test/parallel/test-process-hrtime.js new file mode 100644 index 0000000000..34ef514aac --- /dev/null +++ b/test/js/node/test/parallel/test-process-hrtime.js @@ -0,0 +1,74 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// The default behavior, return an Array "tuple" of numbers +const tuple = process.hrtime(); + +// Validate the default behavior +validateTuple(tuple); + +// Validate that passing an existing tuple returns another valid tuple +validateTuple(process.hrtime(tuple)); + +// Test that only an Array may be passed to process.hrtime() +assert.throws(() => { + process.hrtime(1); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "time" argument must be an instance of Array. Received type ' + + 'number (1)' +}); +assert.throws(() => { + process.hrtime([]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 0' +}); +assert.throws(() => { + process.hrtime([1]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 1' +}); +assert.throws(() => { + process.hrtime([1, 2, 3]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 3' +}); + +function validateTuple(tuple) { + assert(Array.isArray(tuple)); + assert.strictEqual(tuple.length, 2); + assert(Number.isInteger(tuple[0])); + assert(Number.isInteger(tuple[1])); +} + +const diff = process.hrtime([0, 1e9 - 1]); +assert(diff[1] >= 0); // https://github.com/nodejs/node/issues/4751 diff --git a/test/js/node/test/parallel/test-process-kill-null.js b/test/js/node/test/parallel/test-process-kill-null.js new file mode 100644 index 0000000000..88fc677454 --- /dev/null +++ b/test/js/node/test/parallel/test-process-kill-null.js @@ -0,0 +1,42 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { mustCall } = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +const cat = spawn('cat'); + +assert.ok(process.kill(cat.pid, 0)); + +cat.on('exit', mustCall(function() { + assert.throws(function() { + process.kill(cat.pid, 0); + }, Error); +})); + +cat.stdout.on('data', mustCall(function() { + process.kill(cat.pid, 'SIGKILL'); +})); + +// EPIPE when null sig fails +cat.stdin.write('test'); diff --git a/test/js/node/test/parallel/test-process-kill-pid.js b/test/js/node/test/parallel/test-process-kill-pid.js new file mode 100644 index 0000000000..1fa1d6c2ab --- /dev/null +++ b/test/js/node/test/parallel/test-process-kill-pid.js @@ -0,0 +1,116 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const assert = require('assert'); + +// Test variants of pid +// +// null: TypeError +// undefined: TypeError +// +// 'SIGTERM': TypeError +// +// String(process.pid): TypeError +// +// Nan, Infinity, -Infinity: TypeError +// +// 0, String(0): our group process +// +// process.pid, String(process.pid): ourself + +assert.throws(() => process.kill('SIGTERM'), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "pid" argument must be of type number. Received type string ("SIGTERM")' +}); + +[null, undefined, NaN, Infinity, -Infinity].forEach((val) => { + assert.throws(() => process.kill(val), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "pid" argument must be of type number.' + + common.invalidArgTypeHelper(val) + }); +}); + +// Test that kill throws an error for unknown signal names +assert.throws(() => process.kill(0, 'test'), { + code: 'ERR_UNKNOWN_SIGNAL', + name: 'TypeError', + message: 'Unknown signal: test' +}); + +// Test that kill throws an error for invalid signal numbers +assert.throws(() => process.kill(0, 987), { + code: 'EINVAL', + name: 'SystemError', + message: 'kill() failed: EINVAL: Invalid argument' +}); + +// Test kill argument processing in valid cases. +// +// Monkey patch _kill so that we don't actually send any signals, particularly +// that we don't kill our process group, or try to actually send ANY signals on +// windows, which doesn't support them. +function kill(tryPid, trySig, expectPid, expectSig) { + let getPid; + let getSig; + const origKill = process._kill; + process._kill = function(pid, sig) { + getPid = pid; + getSig = sig; + + // un-monkey patch process._kill + process._kill = origKill; + }; + + process.kill(tryPid, trySig); + + assert.strictEqual(getPid.toString(), expectPid.toString()); + assert.strictEqual(getSig, expectSig); +} + +// Note that SIGHUP and SIGTERM map to 1 and 15 respectively, even on Windows +// (for Windows, libuv maps 1 and 15 to the correct behavior). + +kill(0, 'SIGHUP', 0, 1); +kill(0, undefined, 0, 15); +kill('0', 'SIGHUP', 0, 1); +kill('0', undefined, 0, 15); + +// Confirm that numeric signal arguments are supported + +kill(0, 1, 0, 1); +kill(0, 15, 0, 15); + +// Negative numbers are meaningful on unix +kill(-1, 'SIGHUP', -1, 1); +kill(-1, undefined, -1, 15); +kill('-1', 'SIGHUP', -1, 1); +kill('-1', undefined, -1, 15); + +kill(process.pid, 'SIGHUP', process.pid, 1); +kill(process.pid, undefined, process.pid, 15); +kill(String(process.pid), 'SIGHUP', process.pid, 1); +kill(String(process.pid), undefined, process.pid, 15); diff --git a/test/js/node/test/parallel/test-process-next-tick.js b/test/js/node/test/parallel/test-process-next-tick.js new file mode 100644 index 0000000000..66913beebf --- /dev/null +++ b/test/js/node/test/parallel/test-process-next-tick.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const N = 2; + +function cb() { + throw new Error(); +} + +for (let i = 0; i < N; ++i) { + process.nextTick(common.mustCall(cb)); +} + +process.on('uncaughtException', common.mustCall(N)); + +process.on('exit', function() { + process.removeAllListeners('uncaughtException'); +}); + +[null, 1, 'test', {}, [], Infinity, true].forEach((i) => { + assert.throws( + () => process.nextTick(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); +}); diff --git a/test/js/node/test/parallel/test-process-no-deprecation.js b/test/js/node/test/parallel/test-process-no-deprecation.js new file mode 100644 index 0000000000..bcda99de25 --- /dev/null +++ b/test/js/node/test/parallel/test-process-no-deprecation.js @@ -0,0 +1,32 @@ +'use strict'; +// Flags: --no-warnings + +// The --no-warnings flag only suppresses writing the warning to stderr, not the +// emission of the corresponding event. This test file can be run without it. + +const common = require('../common'); +process.noDeprecation = true; + +const assert = require('assert'); + +function listener() { + assert.fail('received unexpected warning'); +} + +process.addListener('warning', listener); + +process.emitWarning('Something is deprecated.', 'DeprecationWarning'); + +// The warning would be emitted in the next tick, so continue after that. +process.nextTick(common.mustCall(() => { + // Check that deprecations can be re-enabled. + process.noDeprecation = false; + process.removeListener('warning', listener); + + process.addListener('warning', common.mustCall((warning) => { + assert.strictEqual(warning.name, 'DeprecationWarning'); + assert.strictEqual(warning.message, 'Something else is deprecated.'); + })); + + process.emitWarning('Something else is deprecated.', 'DeprecationWarning'); +})); diff --git a/test/js/node/test/parallel/test-process-ppid.js b/test/js/node/test/parallel/test-process-ppid.js new file mode 100644 index 0000000000..d78ef3a2dd --- /dev/null +++ b/test/js/node/test/parallel/test-process-ppid.js @@ -0,0 +1,16 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +if (process.argv[2] === 'child') { + // The following console.log() call is part of the test's functionality. + console.log(process.ppid); +} else { + const child = cp.spawnSync(process.execPath, [__filename, 'child']); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + assert.strictEqual(+child.stdout.toString().trim(), process.pid); + assert.strictEqual(child.stderr.toString().trim(), ''); +} diff --git a/test/js/node/test/parallel/test-process-really-exit.js b/test/js/node/test/parallel/test-process-really-exit.js new file mode 100644 index 0000000000..8445d220ca --- /dev/null +++ b/test/js/node/test/parallel/test-process-really-exit.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +// Ensure that the reallyExit hook is executed. +// see: https://github.com/nodejs/node/issues/25650 +if (process.argv[2] === 'subprocess') { + process.reallyExit = function() { + console.info('really exited'); + }; + process.exit(); +} else { + const { spawnSync } = require('child_process'); + const out = spawnSync(process.execPath, [__filename, 'subprocess']); + const observed = out.output[1].toString('utf8').trim(); + assert.strictEqual(observed, 'really exited'); +} diff --git a/test/js/node/test/parallel/test-process-release.js b/test/js/node/test/parallel/test-process-release.js new file mode 100644 index 0000000000..98a089a8f9 --- /dev/null +++ b/test/js/node/test/parallel/test-process-release.js @@ -0,0 +1,32 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const versionParts = process.versions.node.split('.'); + +assert.strictEqual(process.release.name, 'node'); + +// It's expected that future LTS release lines will have additional +// branches in here +if (versionParts[0] === '4' && versionParts[1] >= 2) { + assert.strictEqual(process.release.lts, 'Argon'); +} else if (versionParts[0] === '6' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Boron'); +} else if (versionParts[0] === '8' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Carbon'); +} else if (versionParts[0] === '10' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Dubnium'); +} else if (versionParts[0] === '12' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Erbium'); +} else if (versionParts[0] === '14' && versionParts[1] >= 15) { + assert.strictEqual(process.release.lts, 'Fermium'); +} else if (versionParts[0] === '16' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Gallium'); +} else if (versionParts[0] === '18' && versionParts[1] >= 12) { + assert.strictEqual(process.release.lts, 'Hydrogen'); +} else if (versionParts[0] === '20' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Iron'); +} else { + assert.strictEqual(process.release.lts, undefined); +} diff --git a/test/js/node/test/parallel/test-process-remove-all-signal-listeners.js b/test/js/node/test/parallel/test-process-remove-all-signal-listeners.js new file mode 100644 index 0000000000..afd574daaa --- /dev/null +++ b/test/js/node/test/parallel/test-process-remove-all-signal-listeners.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +if (common.isWindows) + common.skip('Win32 does not support signals.'); + +const assert = require('assert'); +const spawn = require('child_process').spawn; + +if (process.argv[2] !== '--do-test') { + // We are the primary, fork a child so we can verify it exits with correct + // status. + process.env.DOTEST = 'y'; + const child = spawn(process.execPath, [__filename, '--do-test']); + + child.once('exit', common.mustCall(function(code, signal) { + assert.strictEqual(signal, 'SIGINT'); + })); + + return; +} + +process.on('SIGINT', function() { + // Remove all handlers and kill ourselves. We should terminate by SIGINT + // now that we have no handlers. + process.removeAllListeners('SIGINT'); + process.kill(process.pid, 'SIGINT'); +}); + +// Signal handlers aren't sufficient to keep node alive, so resume stdin +process.stdin.resume(); + +// Demonstrate that signals are being handled +process.kill(process.pid, 'SIGINT'); diff --git a/test/js/node/test/parallel/test-process-setgroups.js b/test/js/node/test/parallel/test-process-setgroups.js new file mode 100644 index 0000000000..c26b5dbaf1 --- /dev/null +++ b/test/js/node/test/parallel/test-process-setgroups.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (common.isWindows) { + assert.strictEqual(process.setgroups, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws( + () => { + process.setgroups(); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "groups" argument must be an instance of Array. ' + + 'Received undefined' + } +); + +assert.throws( + () => { + process.setgroups([1, -1]); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } +); + +[undefined, null, true, {}, [], () => {}].forEach((val) => { + assert.throws( + () => { + process.setgroups([val]); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "groups[0]" argument must be ' + + 'of type number or string.' + + common.invalidArgTypeHelper(val) + } + ); +}); + +assert.throws(() => { + process.setgroups([1, 'fhqwhgadshgnsdhjsdbkhsdabkfabkveyb']); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'Group identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); diff --git a/test/js/node/test/parallel/test-process-title-cli.js b/test/js/node/test/parallel/test-process-title-cli.js new file mode 100644 index 0000000000..98b3da003f --- /dev/null +++ b/test/js/node/test/parallel/test-process-title-cli.js @@ -0,0 +1,17 @@ +// Flags: --title=foo +'use strict'; + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +if (common.isSunOS) + common.skip(`Unsupported platform [${process.platform}]`); + +if (common.isIBMi) + common.skip('Unsupported platform IBMi'); + +const assert = require('assert'); + +// Verifies that the --title=foo command line flag set the process +// title on startup. +assert.strictEqual(process.title, 'foo'); diff --git a/test/js/node/test/parallel/test-process-uid-gid.js b/test/js/node/test/parallel/test-process-uid-gid.js new file mode 100644 index 0000000000..0e8e0e89a0 --- /dev/null +++ b/test/js/node/test/parallel/test-process-uid-gid.js @@ -0,0 +1,100 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); + +if (common.isWindows) { + // uid/gid functions are POSIX only. + assert.strictEqual(process.getuid, undefined); + assert.strictEqual(process.getgid, undefined); + assert.strictEqual(process.setuid, undefined); + assert.strictEqual(process.setgid, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws(() => { + process.setuid({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "id" argument must be of type ' + + 'number or string. Received an instance of Object' +}); + +assert.throws(() => { + process.setuid('fhqwhgadshgnsdhjsdbkhsdabkfabkveyb'); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'User identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); + +// Passing -0 shouldn't crash the process +// Refs: https://github.com/nodejs/node/issues/32750 +// And neither should values exceeding 2 ** 31 - 1. +for (const id of [-0, 2 ** 31, 2 ** 32 - 1]) { + for (const fn of [process.setuid, process.setuid, process.setgid, process.setegid]) { + try { fn(id); } catch { + // Continue regardless of error. + } + } +} + +// If we're not running as super user... +if (process.getuid() !== 0) { + // Should not throw. + process.getgid(); + process.getuid(); + + assert.throws( + () => { process.setgid('nobody'); }, + /(?:EPERM: .+|Group identifier does not exist: nobody)$/ + ); + + assert.throws( + () => { process.setuid('nobody'); }, + /(?:EPERM: .+|User identifier does not exist: nobody)$/ + ); + return; +} + +// If we are running as super user... +const oldgid = process.getgid(); +try { + process.setgid('nobody'); +} catch (err) { + if (err.code !== 'ERR_UNKNOWN_CREDENTIAL') { + throw err; + } + process.setgid('nogroup'); +} + +const newgid = process.getgid(); +assert.notStrictEqual(newgid, oldgid); + +const olduid = process.getuid(); +process.setuid('nobody'); +const newuid = process.getuid(); +assert.notStrictEqual(newuid, olduid); diff --git a/test/js/node/test/parallel/test-process-umask-mask.js b/test/js/node/test/parallel/test-process-umask-mask.js new file mode 100644 index 0000000000..d599379761 --- /dev/null +++ b/test/js/node/test/parallel/test-process-umask-mask.js @@ -0,0 +1,32 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in +// process.umask() + +const common = require('../common'); +const assert = require('assert'); + +if (!common.isMainThread) + common.skip('Setting process.umask is not supported in Workers'); + +let mask; + +if (common.isWindows) { + mask = 0o600; +} else { + mask = 0o664; +} + +const maskToIgnore = 0o10000; + +const old = process.umask(); + +function test(input, output) { + process.umask(input); + assert.strictEqual(process.umask(), output); + + process.umask(old); +} + +test(mask | maskToIgnore, mask); +test((mask | maskToIgnore).toString(8), mask); diff --git a/test/js/node/test/parallel/test-process-umask.js b/test/js/node/test/parallel/test-process-umask.js new file mode 100644 index 0000000000..e90955f394 --- /dev/null +++ b/test/js/node/test/parallel/test-process-umask.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (!common.isMainThread) { + assert.strictEqual(typeof process.umask(), 'number'); + assert.throws(() => { + process.umask('0664'); + }, { code: 'ERR_WORKER_UNSUPPORTED_OPERATION' }); + + common.skip('Setting process.umask is not supported in Workers'); +} + +// Note in Windows one can only set the "user" bits. +let mask; +if (common.isWindows) { + mask = '0600'; +} else { + mask = '0664'; +} + +const old = process.umask(mask); + +assert.strictEqual(process.umask(old), parseInt(mask, 8)); + +// Confirm reading the umask does not modify it. +// 1. If the test fails, this call will succeed, but the mask will be set to 0 +assert.strictEqual(process.umask(), old); +// 2. If the test fails, process.umask() will return 0 +assert.strictEqual(process.umask(), old); + +assert.throws(() => { + process.umask({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', +}); + +['123x', 'abc', '999'].forEach((value) => { + assert.throws(() => { + process.umask(value); + }, { + code: 'ERR_INVALID_ARG_VALUE', + }); +}); diff --git a/test/js/node/test/parallel/test-process-uptime.js b/test/js/node/test/parallel/test-process-uptime.js new file mode 100644 index 0000000000..eabb6cf266 --- /dev/null +++ b/test/js/node/test/parallel/test-process-uptime.js @@ -0,0 +1,37 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +console.error(process.uptime()); +// Add some wiggle room for different platforms. +// Verify that the returned value is in seconds - +// 15 seconds should be a good estimate. +assert.ok(process.uptime() <= 15); + +const original = process.uptime(); + +setTimeout(function() { + const uptime = process.uptime(); + assert.ok(original < uptime); +}, 10); diff --git a/test/js/node/test/parallel/test-process-warning.js b/test/js/node/test/parallel/test-process-warning.js new file mode 100644 index 0000000000..c1fbbf775f --- /dev/null +++ b/test/js/node/test/parallel/test-process-warning.js @@ -0,0 +1,68 @@ +'use strict'; + +const common = require('../common'); +const { + hijackStderr, + restoreStderr +} = require('../common/hijackstdio'); +const assert = require('assert'); + +function test1() { + // Output is skipped if the argument to the 'warning' event is + // not an Error object. + hijackStderr(common.mustNotCall('stderr.write must not be called')); + process.emit('warning', 'test'); + setImmediate(test2); +} + +function test2() { + // Output is skipped if it's a deprecation warning and + // process.noDeprecation = true + process.noDeprecation = true; + process.emitWarning('test', 'DeprecationWarning'); + process.noDeprecation = false; + setImmediate(test3); +} + +function test3() { + restoreStderr(); + // Type defaults to warning when the second argument is an object + process.emitWarning('test', {}); + process.once('warning', common.mustCall((warning) => { + assert.strictEqual(warning.name, 'Warning'); + })); + setImmediate(test4); +} + +function test4() { + // process.emitWarning will throw when process.throwDeprecation is true + // and type is `DeprecationWarning`. + process.throwDeprecation = true; + process.once('uncaughtException', (err) => { + assert.match(err.toString(), /^DeprecationWarning: test$/); + }); + try { + process.emitWarning('test', 'DeprecationWarning'); + } catch { + assert.fail('Unreachable'); + } + process.throwDeprecation = false; + setImmediate(test5); +} + +function test5() { + // Setting toString to a non-function should not cause an error + const err = new Error('test'); + err.toString = 1; + process.emitWarning(err); + setImmediate(test6); +} + +function test6() { + process.emitWarning('test', { detail: 'foo' }); + process.on('warning', (warning) => { + assert.strictEqual(warning.detail, 'foo'); + }); +} + +test1(); diff --git a/test/js/node/test/parallel/test-promise-handled-rejection-no-warning.js b/test/js/node/test/parallel/test-promise-handled-rejection-no-warning.js new file mode 100644 index 0000000000..8878d67fab --- /dev/null +++ b/test/js/node/test/parallel/test-promise-handled-rejection-no-warning.js @@ -0,0 +1,7 @@ +'use strict'; +const common = require('../common'); + +// This test verifies that DEP0018 does not occur when rejections are handled. +process.on('warning', common.mustNotCall()); +process.on('unhandledRejection', common.mustCall()); +Promise.reject(new Error()); diff --git a/test/js/node/test/parallel/test-promise-unhandled-issue-43655.js b/test/js/node/test/parallel/test-promise-unhandled-issue-43655.js new file mode 100644 index 0000000000..4fd2c1a711 --- /dev/null +++ b/test/js/node/test/parallel/test-promise-unhandled-issue-43655.js @@ -0,0 +1,27 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +function delay(time) { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); +} + +async function test() { + for (let i = 0; i < 100000; i++) { + await new Promise((resolve, reject) => { + reject('value'); + }) + .then(() => { }, () => { }); + } + + const time0 = Date.now(); + await delay(0); + + const diff = Date.now() - time0; + assert.ok(Date.now() - time0 < 500, `Expected less than 500ms, got ${diff}ms`); +} + +test(); diff --git a/test/js/node/test/parallel/test-promise-unhandled-silent.js b/test/js/node/test/parallel/test-promise-unhandled-silent.js new file mode 100644 index 0000000000..edf5111eae --- /dev/null +++ b/test/js/node/test/parallel/test-promise-unhandled-silent.js @@ -0,0 +1,21 @@ +// Flags: --unhandled-rejections=none +'use strict'; + +const common = require('../common'); + +// Verify that ignoring unhandled rejection works fine and that no warning is +// logged. + +new Promise(() => { + throw new Error('One'); +}); + +Promise.reject('test'); + +process.on('warning', common.mustNotCall('warning')); +process.on('uncaughtException', common.mustNotCall('uncaughtException')); +process.on('rejectionHandled', common.mustNotCall('rejectionHandled')); + +process.on('unhandledRejection', common.mustCall(2)); + +setTimeout(common.mustCall(), 2); diff --git a/test/js/node/test/parallel/test-promise-unhandled-throw-handler.js b/test/js/node/test/parallel/test-promise-unhandled-throw-handler.js new file mode 100644 index 0000000000..26a1d2f85c --- /dev/null +++ b/test/js/node/test/parallel/test-promise-unhandled-throw-handler.js @@ -0,0 +1,36 @@ +// Flags: --unhandled-rejections=throw +'use strict'; + +const common = require('../common'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); + +// Verify that the unhandledRejection handler prevents triggering +// uncaught exceptions + +const err1 = new Error('One'); + +const errors = [err1, null]; + +const ref = new Promise(() => { + throw err1; +}); +// Explicitly reject `null`. +Promise.reject(null); + +process.on('warning', common.mustNotCall('warning')); +process.on('rejectionHandled', common.mustNotCall('rejectionHandled')); +process.on('exit', assert.strictEqual.bind(null, 0)); +process.on('uncaughtException', common.mustNotCall('uncaughtException')); + +const timer = setTimeout(() => console.log(ref), 1000); + +const counter = new Countdown(2, () => { + clearTimeout(timer); +}); + +process.on('unhandledRejection', common.mustCall((err) => { + counter.dec(); + const knownError = errors.shift(); + assert.deepStrictEqual(err, knownError); +}, 2)); diff --git a/test/js/node/test/parallel/test-punycode.js b/test/js/node/test/parallel/test-punycode.js new file mode 100644 index 0000000000..567244e773 --- /dev/null +++ b/test/js/node/test/parallel/test-punycode.js @@ -0,0 +1,270 @@ +// Flags: --pending-deprecation + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// In Node, there's a deprecation warning check here, but we don't print one. + +const punycode = require('punycode'); +const assert = require('assert'); + +assert.strictEqual(punycode.encode('ü'), 'tda'); +assert.strictEqual(punycode.encode('Goethe'), 'Goethe-'); +assert.strictEqual(punycode.encode('Bücher'), 'Bcher-kva'); +assert.strictEqual( + punycode.encode( + 'Willst du die Blüthe des frühen, die Früchte des späteren Jahres' + ), + 'Willst du die Blthe des frhen, die Frchte des spteren Jahres-x9e96lkal' +); +assert.strictEqual(punycode.encode('日本語'), 'wgv71a119e'); +assert.strictEqual(punycode.encode('𩸽'), 'x73l'); + +assert.strictEqual(punycode.decode('tda'), 'ü'); +assert.strictEqual(punycode.decode('Goethe-'), 'Goethe'); +assert.strictEqual(punycode.decode('Bcher-kva'), 'Bücher'); +assert.strictEqual( + punycode.decode( + 'Willst du die Blthe des frhen, die Frchte des spteren Jahres-x9e96lkal' + ), + 'Willst du die Blüthe des frühen, die Früchte des späteren Jahres' +); +assert.strictEqual(punycode.decode('wgv71a119e'), '日本語'); +assert.strictEqual(punycode.decode('x73l'), '𩸽'); +assert.throws(() => { + punycode.decode(' '); +}, /^RangeError: Invalid input$/); +assert.throws(() => { + punycode.decode('α-'); +}, /^RangeError: Illegal input >= 0x80 \(not a basic code point\)$/); +assert.throws(() => { + punycode.decode('あ'); +}, /^RangeError: Invalid input$/); + +// http://tools.ietf.org/html/rfc3492#section-7.1 +const tests = [ + // (A) Arabic (Egyptian) + { + encoded: 'egbpdaj6bu4bxfgehfvwxn', + decoded: '\u0644\u064A\u0647\u0645\u0627\u0628\u062A\u0643\u0644\u0645' + + '\u0648\u0634\u0639\u0631\u0628\u064A\u061F' + }, + + // (B) Chinese (simplified) + { + encoded: 'ihqwcrb4cv8a8dqg056pqjye', + decoded: '\u4ED6\u4EEC\u4E3A\u4EC0\u4E48\u4E0D\u8BF4\u4E2D\u6587' + }, + + // (C) Chinese (traditional) + { + encoded: 'ihqwctvzc91f659drss3x8bo0yb', + decoded: '\u4ED6\u5011\u7232\u4EC0\u9EBD\u4E0D\u8AAA\u4E2D\u6587' + }, + + // (D) Czech: Proprostnemluvesky + { + encoded: 'Proprostnemluvesky-uyb24dma41a', + decoded: '\u0050\u0072\u006F\u010D\u0070\u0072\u006F\u0073\u0074\u011B' + + '\u006E\u0065\u006D\u006C\u0075\u0076\u00ED\u010D\u0065\u0073\u006B\u0079' + }, + + // (E) Hebrew + { + encoded: '4dbcagdahymbxekheh6e0a7fei0b', + decoded: '\u05DC\u05DE\u05D4\u05D4\u05DD\u05E4\u05E9\u05D5\u05D8\u05DC' + + '\u05D0\u05DE\u05D3\u05D1\u05E8\u05D9\u05DD\u05E2\u05D1\u05E8\u05D9\u05EA' + }, + + // (F) Hindi (Devanagari) + { + encoded: 'i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd', + decoded: '\u092F\u0939\u0932\u094B\u0917\u0939\u093F\u0928\u094D\u0926' + + '\u0940\u0915\u094D\u092F\u094B\u0902\u0928\u0939\u0940\u0902\u092C' + + '\u094B\u0932\u0938\u0915\u0924\u0947\u0939\u0948\u0902' + }, + + // (G) Japanese (kanji and hiragana) + { + encoded: 'n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa', + decoded: '\u306A\u305C\u307F\u3093\u306A\u65E5\u672C\u8A9E\u3092\u8A71' + + '\u3057\u3066\u304F\u308C\u306A\u3044\u306E\u304B' + }, + + // (H) Korean (Hangul syllables) + { + encoded: '989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5jpsd879' + + 'ccm6fea98c', + decoded: '\uC138\uACC4\uC758\uBAA8\uB4E0\uC0AC\uB78C\uB4E4\uC774\uD55C' + + '\uAD6D\uC5B4\uB97C\uC774\uD574\uD55C\uB2E4\uBA74\uC5BC\uB9C8\uB098' + + '\uC88B\uC744\uAE4C' + }, + + // (I) Russian (Cyrillic) + { + encoded: 'b1abfaaepdrnnbgefbadotcwatmq2g4l', + decoded: '\u043F\u043E\u0447\u0435\u043C\u0443\u0436\u0435\u043E\u043D' + + '\u0438\u043D\u0435\u0433\u043E\u0432\u043E\u0440\u044F\u0442\u043F' + + '\u043E\u0440\u0443\u0441\u0441\u043A\u0438' + }, + + // (J) Spanish: PorqunopuedensimplementehablarenEspaol + { + encoded: 'PorqunopuedensimplementehablarenEspaol-fmd56a', + decoded: '\u0050\u006F\u0072\u0071\u0075\u00E9\u006E\u006F\u0070\u0075' + + '\u0065\u0064\u0065\u006E\u0073\u0069\u006D\u0070\u006C\u0065\u006D' + + '\u0065\u006E\u0074\u0065\u0068\u0061\u0062\u006C\u0061\u0072\u0065' + + '\u006E\u0045\u0073\u0070\u0061\u00F1\u006F\u006C' + }, + + // (K) Vietnamese: Tisaohkhngth + // chnitingVit + { + encoded: 'TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g', + decoded: '\u0054\u1EA1\u0069\u0073\u0061\u006F\u0068\u1ECD\u006B\u0068' + + '\u00F4\u006E\u0067\u0074\u0068\u1EC3\u0063\u0068\u1EC9\u006E\u00F3' + + '\u0069\u0074\u0069\u1EBF\u006E\u0067\u0056\u0069\u1EC7\u0074' + }, + + // (L) 3B + { + encoded: '3B-ww4c5e180e575a65lsy2b', + decoded: '\u0033\u5E74\u0042\u7D44\u91D1\u516B\u5148\u751F' + }, + + // (M) -with-SUPER-MONKEYS + { + encoded: '-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n', + decoded: '\u5B89\u5BA4\u5948\u7F8E\u6075\u002D\u0077\u0069\u0074\u0068' + + '\u002D\u0053\u0055\u0050\u0045\u0052\u002D\u004D\u004F\u004E\u004B' + + '\u0045\u0059\u0053' + }, + + // (N) Hello-Another-Way- + { + encoded: 'Hello-Another-Way--fc4qua05auwb3674vfr0b', + decoded: '\u0048\u0065\u006C\u006C\u006F\u002D\u0041\u006E\u006F\u0074' + + '\u0068\u0065\u0072\u002D\u0057\u0061\u0079\u002D\u305D\u308C\u305E' + + '\u308C\u306E\u5834\u6240' + }, + + // (O) 2 + { + encoded: '2-u9tlzr9756bt3uc0v', + decoded: '\u3072\u3068\u3064\u5C4B\u6839\u306E\u4E0B\u0032' + }, + + // (P) MajiKoi5 + { + encoded: 'MajiKoi5-783gue6qz075azm5e', + decoded: '\u004D\u0061\u006A\u0069\u3067\u004B\u006F\u0069\u3059\u308B' + + '\u0035\u79D2\u524D' + }, + + // (Q) de + { + encoded: 'de-jg4avhby1noc0d', + decoded: '\u30D1\u30D5\u30A3\u30FC\u0064\u0065\u30EB\u30F3\u30D0' + }, + + // (R) + { + encoded: 'd9juau41awczczp', + decoded: '\u305D\u306E\u30B9\u30D4\u30FC\u30C9\u3067' + }, + + // (S) -> $1.00 <- + { + encoded: '-> $1.00 <--', + decoded: '\u002D\u003E\u0020\u0024\u0031\u002E\u0030\u0030\u0020\u003C' + + '\u002D' + }, +]; + +let errors = 0; +const handleError = (error, name) => { + console.error( + `FAIL: ${name} expected ${error.expected}, got ${error.actual}` + ); + errors++; +}; + +const regexNonASCII = /[^\x20-\x7E]/; +const testBattery = { + encode: (test) => assert.strictEqual( + punycode.encode(test.decoded), + test.encoded + ), + decode: (test) => assert.strictEqual( + punycode.decode(test.encoded), + test.decoded + ), + toASCII: (test) => assert.strictEqual( + punycode.toASCII(test.decoded), + regexNonASCII.test(test.decoded) ? + `xn--${test.encoded}` : + test.decoded + ), + toUnicode: (test) => assert.strictEqual( + punycode.toUnicode( + regexNonASCII.test(test.decoded) ? + `xn--${test.encoded}` : + test.decoded + ), + regexNonASCII.test(test.decoded) ? + test.decoded.toLowerCase() : + test.decoded + ) +}; + +tests.forEach((testCase) => { + Object.keys(testBattery).forEach((key) => { + try { + testBattery[key](testCase); + } catch (error) { + handleError(error, key); + } + }); +}); + +// BMP code point +assert.strictEqual(punycode.ucs2.encode([0x61]), 'a'); +// Supplementary code point (surrogate pair) +assert.strictEqual(punycode.ucs2.encode([0x1D306]), '\uD834\uDF06'); +// high surrogate +assert.strictEqual(punycode.ucs2.encode([0xD800]), '\uD800'); +// High surrogate followed by non-surrogates +assert.strictEqual(punycode.ucs2.encode([0xD800, 0x61, 0x62]), '\uD800ab'); +// low surrogate +assert.strictEqual(punycode.ucs2.encode([0xDC00]), '\uDC00'); +// Low surrogate followed by non-surrogates +assert.strictEqual(punycode.ucs2.encode([0xDC00, 0x61, 0x62]), '\uDC00ab'); + +assert.strictEqual(errors, 0); + +// test map domain +assert.strictEqual(punycode.toASCII('Bücher@日本語.com'), + 'Bücher@xn--wgv71a119e.com'); +assert.strictEqual(punycode.toUnicode('Bücher@xn--wgv71a119e.com'), + 'Bücher@日本語.com'); diff --git a/test/js/node/test/parallel/test-querystring-escape.js b/test/js/node/test/parallel/test-querystring-escape.js new file mode 100644 index 0000000000..5f3ea3aedc --- /dev/null +++ b/test/js/node/test/parallel/test-querystring-escape.js @@ -0,0 +1,41 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const qs = require('querystring'); + +assert.strictEqual(qs.escape(5), '5'); +assert.strictEqual(qs.escape('test'), 'test'); +assert.strictEqual(qs.escape({}), '%5Bobject%20Object%5D'); +assert.strictEqual(qs.escape([5, 10]), '5%2C10'); +assert.strictEqual(qs.escape('Ŋōđĕ'), '%C5%8A%C5%8D%C4%91%C4%95'); +assert.strictEqual(qs.escape('testŊōđĕ'), 'test%C5%8A%C5%8D%C4%91%C4%95'); +assert.strictEqual(qs.escape(`${String.fromCharCode(0xD800 + 1)}test`), + '%F0%90%91%B4est'); + +assert.throws( + () => qs.escape(String.fromCharCode(0xD800 + 1)), + { + code: 'ERR_INVALID_URI', + name: 'URIError', + message: 'URI malformed' + } +); + +// Using toString for objects +assert.strictEqual( + qs.escape({ test: 5, toString: () => 'test', valueOf: () => 10 }), + 'test' +); + +// `toString` is not callable, must throw an error. +// Error message will vary between different JavaScript engines, so only check +// that it is a `TypeError`. +assert.throws(() => qs.escape({ toString: 5 }), TypeError); + +// Should use valueOf instead of non-callable toString. +assert.strictEqual(qs.escape({ toString: 5, valueOf: () => 'test' }), 'test'); + +// Error message will vary between different JavaScript engines, so only check +// that it is a `TypeError`. +assert.throws(() => qs.escape(Symbol('test')), TypeError); diff --git a/test/js/node/test/parallel/test-querystring-maxKeys-non-finite.js b/test/js/node/test/parallel/test-querystring-maxKeys-non-finite.js new file mode 100644 index 0000000000..610c30c7a3 --- /dev/null +++ b/test/js/node/test/parallel/test-querystring-maxKeys-non-finite.js @@ -0,0 +1,58 @@ +'use strict'; +// This test was originally written to test a regression +// that was introduced by +// https://github.com/nodejs/node/pull/2288#issuecomment-179543894 +require('../common'); + +const assert = require('assert'); +const parse = require('querystring').parse; + +// Taken from express-js/body-parser +// https://github.com/expressjs/body-parser/blob/ed25264fb494cf0c8bc992b8257092cd4f694d5e/test/urlencoded.js#L636-L651 +function createManyParams(count) { + let str = ''; + + if (count === 0) { + return str; + } + + str += '0=0'; + + for (let i = 1; i < count; i++) { + const n = i.toString(36); + str += `&${n}=${n}`; + } + + return str; +} + +const count = 10000; +const originalMaxLength = 1000; +const params = createManyParams(count); + +// thealphanerd +// 27def4f introduced a change to parse that would cause Infinity +// to be passed to String.prototype.split as an argument for limit +// In this instance split will always return an empty array +// this test confirms that the output of parse is the expected length +// when passed Infinity as the argument for maxKeys +const resultInfinity = parse(params, undefined, undefined, { + maxKeys: Infinity +}); +const resultNaN = parse(params, undefined, undefined, { + maxKeys: NaN +}); +const resultInfinityString = parse(params, undefined, undefined, { + maxKeys: 'Infinity' +}); +const resultNaNString = parse(params, undefined, undefined, { + maxKeys: 'NaN' +}); + +// Non Finite maxKeys should return the length of input +assert.strictEqual(Object.keys(resultInfinity).length, count); +assert.strictEqual(Object.keys(resultNaN).length, count); +// Strings maxKeys should return the maxLength +// defined by parses internals +assert.strictEqual(Object.keys(resultInfinityString).length, originalMaxLength); +assert.strictEqual(Object.keys(resultNaNString).length, originalMaxLength); diff --git a/test/js/node/test/parallel/test-querystring-multichar-separator.js b/test/js/node/test/parallel/test-querystring-multichar-separator.js new file mode 100644 index 0000000000..720733b1e2 --- /dev/null +++ b/test/js/node/test/parallel/test-querystring-multichar-separator.js @@ -0,0 +1,25 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const qs = require('querystring'); + +function check(actual, expected) { + assert(!(actual instanceof Object)); + assert.deepStrictEqual(Object.keys(actual).sort(), + Object.keys(expected).sort()); + Object.keys(expected).forEach(function(key) { + assert.deepStrictEqual(actual[key], expected[key]); + }); +} + +check(qs.parse('foo=>bar&&bar=>baz', '&&', '=>'), + { foo: 'bar', bar: 'baz' }); + +check(qs.stringify({ foo: 'bar', bar: 'baz' }, '&&', '=>'), + 'foo=>bar&&bar=>baz'); + +check(qs.parse('foo==>bar, bar==>baz', ', ', '==>'), + { foo: 'bar', bar: 'baz' }); + +check(qs.stringify({ foo: 'bar', bar: 'baz' }, ', ', '==>'), + 'foo==>bar, bar==>baz'); diff --git a/test/js/node/test/parallel/test-querystring.js b/test/js/node/test/parallel/test-querystring.js new file mode 100644 index 0000000000..b24ec5b569 --- /dev/null +++ b/test/js/node/test/parallel/test-querystring.js @@ -0,0 +1,480 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const inspect = require('util').inspect; + +// test using assert +const qs = require('querystring'); + +function createWithNoPrototype(properties) { + const noProto = { __proto__: null }; + properties.forEach((property) => { + noProto[property.key] = property.value; + }); + return noProto; +} +// Folding block, commented to pass gjslint +// {{{ +// [ wonkyQS, canonicalQS, obj ] +const qsTestCases = [ + ['__proto__=1', + '__proto__=1', + createWithNoPrototype([{ key: '__proto__', value: '1' }])], + ['__defineGetter__=asdf', + '__defineGetter__=asdf', + JSON.parse('{"__defineGetter__":"asdf"}')], + ['foo=918854443121279438895193', + 'foo=918854443121279438895193', + { 'foo': '918854443121279438895193' }], + ['foo=bar', 'foo=bar', { 'foo': 'bar' }], + ['foo=bar&foo=quux', 'foo=bar&foo=quux', { 'foo': ['bar', 'quux'] }], + ['foo=1&bar=2', 'foo=1&bar=2', { 'foo': '1', 'bar': '2' }], + ['my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F', + 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F', + { 'my weird field': 'q1!2"\'w$5&7/z8)?' }], + ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', { 'foo=baz': 'bar' }], + ['foo=baz=bar', 'foo=baz%3Dbar', { 'foo': 'baz=bar' }], + ['str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + 'str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + { 'str': 'foo', + 'arr': ['1', '2', '3'], + 'somenull': '', + 'undef': '' }], + [' foo = bar ', '%20foo%20=%20bar%20', { ' foo ': ' bar ' }], + ['foo=%zx', 'foo=%25zx', { 'foo': '%zx' }], + ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', { 'foo': '\ufffd' }], + // See: https://github.com/joyent/node/issues/1707 + ['hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', + 'hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', + { hasOwnProperty: 'x', + toString: 'foo', + valueOf: 'bar', + __defineGetter__: 'baz' }], + // See: https://github.com/joyent/node/issues/3058 + ['foo&bar=baz', 'foo=&bar=baz', { foo: '', bar: 'baz' }], + ['a=b&c&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }], + ['a=b&c=&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }], + ['a=b&=c&d=e', 'a=b&=c&d=e', { 'a': 'b', '': 'c', 'd': 'e' }], + ['a=b&=&c=d', 'a=b&=&c=d', { 'a': 'b', '': '', 'c': 'd' }], + ['&&foo=bar&&', 'foo=bar', { foo: 'bar' }], + ['&', '', {}], + ['&&&&', '', {}], + ['&=&', '=', { '': '' }], + ['&=&=', '=&=', { '': [ '', '' ] }], + ['=', '=', { '': '' }], + ['+', '%20=', { ' ': '' }], + ['+=', '%20=', { ' ': '' }], + ['+&', '%20=', { ' ': '' }], + ['=+', '=%20', { '': ' ' }], + ['+=&', '%20=', { ' ': '' }], + ['a&&b', 'a=&b=', { 'a': '', 'b': '' }], + ['a=a&&b=b', 'a=a&b=b', { 'a': 'a', 'b': 'b' }], + ['&a', 'a=', { 'a': '' }], + ['&=', '=', { '': '' }], + ['a&a&', 'a=&a=', { a: [ '', '' ] }], + ['a&a&a&', 'a=&a=&a=', { a: [ '', '', '' ] }], + ['a&a&a&a&', 'a=&a=&a=&a=', { a: [ '', '', '', '' ] }], + ['a=&a=value&a=', 'a=&a=value&a=', { a: [ '', 'value', '' ] }], + ['foo+bar=baz+quux', 'foo%20bar=baz%20quux', { 'foo bar': 'baz quux' }], + ['+foo=+bar', '%20foo=%20bar', { ' foo': ' bar' }], + ['a+', 'a%20=', { 'a ': '' }], + ['=a+', '=a%20', { '': 'a ' }], + ['a+&', 'a%20=', { 'a ': '' }], + ['=a+&', '=a%20', { '': 'a ' }], + ['%20+', '%20%20=', { ' ': '' }], + ['=%20+', '=%20%20', { '': ' ' }], + ['%20+&', '%20%20=', { ' ': '' }], + ['=%20+&', '=%20%20', { '': ' ' }], + [null, '', {}], + [undefined, '', {}], +]; + +// [ wonkyQS, canonicalQS, obj ] +const qsColonTestCases = [ + ['foo:bar', 'foo:bar', { 'foo': 'bar' }], + ['foo:bar;foo:quux', 'foo:bar;foo:quux', { 'foo': ['bar', 'quux'] }], + ['foo:1&bar:2;baz:quux', + 'foo:1%26bar%3A2;baz:quux', + { 'foo': '1&bar:2', 'baz': 'quux' }], + ['foo%3Abaz:bar', 'foo%3Abaz:bar', { 'foo:baz': 'bar' }], + ['foo:baz:bar', 'foo:baz%3Abar', { 'foo': 'baz:bar' }], +]; + +// [wonkyObj, qs, canonicalObj] +function extendedFunction() {} +extendedFunction.prototype = { a: 'b' }; +const qsWeirdObjects = [ + // eslint-disable-next-line node-core/no-unescaped-regexp-dot + [{ regexp: /./g }, 'regexp=', { 'regexp': '' }], + // eslint-disable-next-line node-core/no-unescaped-regexp-dot + [{ regexp: new RegExp('.', 'g') }, 'regexp=', { 'regexp': '' }], + [{ fn: () => {} }, 'fn=', { 'fn': '' }], + [{ fn: new Function('') }, 'fn=', { 'fn': '' }], + [{ math: Math }, 'math=', { 'math': '' }], + [{ e: extendedFunction }, 'e=', { 'e': '' }], + [{ d: new Date() }, 'd=', { 'd': '' }], + [{ d: Date }, 'd=', { 'd': '' }], + [ + { f: new Boolean(false), t: new Boolean(true) }, + 'f=&t=', + { 'f': '', 't': '' }, + ], + [{ f: false, t: true }, 'f=false&t=true', { 'f': 'false', 't': 'true' }], + [{ n: null }, 'n=', { 'n': '' }], + [{ nan: NaN }, 'nan=', { 'nan': '' }], + [{ inf: Infinity }, 'inf=', { 'inf': '' }], + [{ a: [], b: [] }, '', {}], + [{ a: 1, b: [] }, 'a=1', { 'a': '1' }], +]; +// }}} + +const vm = require('vm'); +const foreignObject = vm.runInNewContext('({"foo": ["bar", "baz"]})'); + +const qsNoMungeTestCases = [ + ['', {}], + ['foo=bar&foo=baz', { 'foo': ['bar', 'baz'] }], + ['foo=bar&foo=baz', foreignObject], + ['blah=burp', { 'blah': 'burp' }], + ['a=!-._~\'()*', { 'a': '!-._~\'()*' }], + ['a=abcdefghijklmnopqrstuvwxyz', { 'a': 'abcdefghijklmnopqrstuvwxyz' }], + ['a=ABCDEFGHIJKLMNOPQRSTUVWXYZ', { 'a': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }], + ['a=0123456789', { 'a': '0123456789' }], + ['gragh=1&gragh=3&goo=2', { 'gragh': ['1', '3'], 'goo': '2' }], + ['frappucino=muffin&goat%5B%5D=scone&pond=moose', + { 'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose' }], + ['trololol=yes&lololo=no', { 'trololol': 'yes', 'lololo': 'no' }], +]; + +const qsUnescapeTestCases = [ + ['there is nothing to unescape here', + 'there is nothing to unescape here'], + ['there%20are%20several%20spaces%20that%20need%20to%20be%20unescaped', + 'there are several spaces that need to be unescaped'], + ['there%2Qare%0-fake%escaped values in%%%%this%9Hstring', + 'there%2Qare%0-fake%escaped values in%%%%this%9Hstring'], + ['%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37', + ' !"#$%&\'()*+,-./01234567'], + ['%%2a', '%*'], + ['%2sf%2a', '%2sf*'], + ['%2%2af%2a', '%2*f*'], +]; + +assert.strictEqual(qs.parse('id=918854443121279438895193').id, + '918854443121279438895193'); + +function check(actual, expected, input) { + assert(!(actual instanceof Object)); + const actualKeys = Object.keys(actual).sort(); + const expectedKeys = Object.keys(expected).sort(); + let msg; + if (typeof input === 'string') { + msg = `Input: ${inspect(input)}\n` + + `Actual keys: ${inspect(actualKeys)}\n` + + `Expected keys: ${inspect(expectedKeys)}`; + } + assert.deepStrictEqual(actualKeys, expectedKeys, msg); + expectedKeys.forEach((key) => { + if (typeof input === 'string') { + msg = `Input: ${inspect(input)}\n` + + `Key: ${inspect(key)}\n` + + `Actual value: ${inspect(actual[key])}\n` + + `Expected value: ${inspect(expected[key])}`; + } else { + msg = undefined; + } + assert.deepStrictEqual(actual[key], expected[key], msg); + }); +} + +// Test that the canonical qs is parsed properly. +qsTestCases.forEach((testCase) => { + check(qs.parse(testCase[0]), testCase[2], testCase[0]); +}); + +// Test that the colon test cases can do the same +qsColonTestCases.forEach((testCase) => { + check(qs.parse(testCase[0], ';', ':'), testCase[2], testCase[0]); +}); + +// Test the weird objects, that they get parsed properly +qsWeirdObjects.forEach((testCase) => { + check(qs.parse(testCase[1]), testCase[2], testCase[1]); +}); + +qsNoMungeTestCases.forEach((testCase) => { + assert.deepStrictEqual(qs.stringify(testCase[1], '&', '='), testCase[0]); +}); + +// Test the nested qs-in-qs case +{ + const f = qs.parse('a=b&q=x%3Dy%26y%3Dz'); + check(f, createWithNoPrototype([ + { key: 'a', value: 'b' }, + { key: 'q', value: 'x=y&y=z' }, + ])); + + f.q = qs.parse(f.q); + const expectedInternal = createWithNoPrototype([ + { key: 'x', value: 'y' }, + { key: 'y', value: 'z' }, + ]); + check(f.q, expectedInternal); +} + +// nested in colon +{ + const f = qs.parse('a:b;q:x%3Ay%3By%3Az', ';', ':'); + check(f, createWithNoPrototype([ + { key: 'a', value: 'b' }, + { key: 'q', value: 'x:y;y:z' }, + ])); + f.q = qs.parse(f.q, ';', ':'); + const expectedInternal = createWithNoPrototype([ + { key: 'x', value: 'y' }, + { key: 'y', value: 'z' }, + ]); + check(f.q, expectedInternal); +} + +// Now test stringifying + +// basic +qsTestCases.forEach((testCase) => { + assert.strictEqual(qs.stringify(testCase[2]), testCase[1]); +}); + +qsColonTestCases.forEach((testCase) => { + assert.strictEqual(qs.stringify(testCase[2], ';', ':'), testCase[1]); +}); + +qsWeirdObjects.forEach((testCase) => { + assert.strictEqual(qs.stringify(testCase[0]), testCase[1]); +}); + +// BigInt values + +assert.strictEqual(qs.stringify({ foo: 2n ** 1023n }), + 'foo=' + 2n ** 1023n); +assert.strictEqual(qs.stringify([0n, 1n, 2n]), + '0=0&1=1&2=2'); + +assert.strictEqual(qs.stringify({ foo: 2n ** 1023n }, + null, + null, + { encodeURIComponent: (c) => c }), + 'foo=' + 2n ** 1023n); +assert.strictEqual(qs.stringify([0n, 1n, 2n], + null, + null, + { encodeURIComponent: (c) => c }), + '0=0&1=1&2=2'); + +// Invalid surrogate pair throws URIError +assert.throws( + () => qs.stringify({ foo: '\udc00' }), + { + code: 'ERR_INVALID_URI', + name: 'URIError', + message: 'URI malformed' + } +); + +// Coerce numbers to string +assert.strictEqual(qs.stringify({ foo: 0 }), 'foo=0'); +assert.strictEqual(qs.stringify({ foo: -0 }), 'foo=0'); +assert.strictEqual(qs.stringify({ foo: 3 }), 'foo=3'); +assert.strictEqual(qs.stringify({ foo: -72.42 }), 'foo=-72.42'); +assert.strictEqual(qs.stringify({ foo: NaN }), 'foo='); +assert.strictEqual(qs.stringify({ foo: 1e21 }), 'foo=1e%2B21'); +assert.strictEqual(qs.stringify({ foo: Infinity }), 'foo='); + +// nested +{ + const f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }) + }); + assert.strictEqual(f, 'a=b&q=x%3Dy%26y%3Dz'); +} + +qs.parse(undefined); // Should not throw. + +// nested in colon +{ + const f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }, ';', ':') + }, ';', ':'); + assert.strictEqual(f, 'a:b;q:x%3Ay%3By%3Az'); +} + +// empty string +assert.strictEqual(qs.stringify(), ''); +assert.strictEqual(qs.stringify(0), ''); +assert.strictEqual(qs.stringify([]), ''); +assert.strictEqual(qs.stringify(null), ''); +assert.strictEqual(qs.stringify(true), ''); + +check(qs.parse(), {}); + +// empty sep +check(qs.parse('a', []), { a: '' }); + +// empty eq +check(qs.parse('a', null, []), { '': 'a' }); + +// Test limiting +assert.strictEqual( + Object.keys(qs.parse('a=1&b=1&c=1', null, null, { maxKeys: 1 })).length, + 1); + +// Test limiting with a case that starts from `&` +assert.strictEqual( + Object.keys(qs.parse('&a', null, null, { maxKeys: 1 })).length, + 0); + +// Test removing limit +{ + function testUnlimitedKeys() { + const query = {}; + + for (let i = 0; i < 2000; i++) query[i] = i; + + const url = qs.stringify(query); + + assert.strictEqual( + Object.keys(qs.parse(url, null, null, { maxKeys: 0 })).length, + 2000); + } + + testUnlimitedKeys(); +} + +{ + const b = qs.unescapeBuffer('%d3%f2Ug%1f6v%24%5e%98%cb' + + '%0d%ac%a2%2f%9d%eb%d8%a2%e6'); + // + assert.strictEqual(b[0], 0xd3); + assert.strictEqual(b[1], 0xf2); + assert.strictEqual(b[2], 0x55); + assert.strictEqual(b[3], 0x67); + assert.strictEqual(b[4], 0x1f); + assert.strictEqual(b[5], 0x36); + assert.strictEqual(b[6], 0x76); + assert.strictEqual(b[7], 0x24); + assert.strictEqual(b[8], 0x5e); + assert.strictEqual(b[9], 0x98); + assert.strictEqual(b[10], 0xcb); + assert.strictEqual(b[11], 0x0d); + assert.strictEqual(b[12], 0xac); + assert.strictEqual(b[13], 0xa2); + assert.strictEqual(b[14], 0x2f); + assert.strictEqual(b[15], 0x9d); + assert.strictEqual(b[16], 0xeb); + assert.strictEqual(b[17], 0xd8); + assert.strictEqual(b[18], 0xa2); + assert.strictEqual(b[19], 0xe6); +} + +assert.strictEqual(qs.unescapeBuffer('a+b', true).toString(), 'a b'); +assert.strictEqual(qs.unescapeBuffer('a+b').toString(), 'a+b'); +assert.strictEqual(qs.unescapeBuffer('a%').toString(), 'a%'); +assert.strictEqual(qs.unescapeBuffer('a%2').toString(), 'a%2'); +assert.strictEqual(qs.unescapeBuffer('a%20').toString(), 'a '); +assert.strictEqual(qs.unescapeBuffer('a%2g').toString(), 'a%2g'); +assert.strictEqual(qs.unescapeBuffer('a%%').toString(), 'a%%'); + +// Test invalid encoded string +check(qs.parse('%\u0100=%\u0101'), { '%Ā': '%ā' }); + +// Test custom decode +{ + function demoDecode(str) { + return str + str; + } + + check( + qs.parse('a=a&b=b&c=c', null, null, { decodeURIComponent: demoDecode }), + { aa: 'aa', bb: 'bb', cc: 'cc' }); + check( + qs.parse('a=a&b=b&c=c', null, '==', { decodeURIComponent: (str) => str }), + { 'a=a': '', 'b=b': '', 'c=c': '' }); +} + +// Test QueryString.unescape +{ + function errDecode(str) { + throw new Error('To jump to the catch scope'); + } + + check(qs.parse('a=a', null, null, { decodeURIComponent: errDecode }), + { a: 'a' }); +} + +// Test custom encode +{ + function demoEncode(str) { + return str[0]; + } + + const obj = { aa: 'aa', bb: 'bb', cc: 'cc' }; + assert.strictEqual( + qs.stringify(obj, null, null, { encodeURIComponent: demoEncode }), + 'a=a&b=b&c=c'); +} + +// Test custom encode for different types +{ + const obj = { number: 1, bigint: 2n, true: true, false: false, object: {} }; + assert.strictEqual( + qs.stringify(obj, null, null, { encodeURIComponent: (v) => v }), + 'number=1&bigint=2&true=true&false=false&object='); +} + +// Test QueryString.unescapeBuffer +qsUnescapeTestCases.forEach((testCase) => { + assert.strictEqual(qs.unescape(testCase[0]), testCase[1]); + assert.strictEqual(qs.unescapeBuffer(testCase[0]).toString(), testCase[1]); +}); + +// Test overriding .unescape +{ + const prevUnescape = qs.unescape; + qs.unescape = (str) => { + return str.replace(/o/g, '_'); + }; + check( + qs.parse('foo=bor'), + createWithNoPrototype([{ key: 'f__', value: 'b_r' }])); + qs.unescape = prevUnescape; +} +// Test separator and "equals" parsing order +check(qs.parse('foo&bar', '&', '&'), { foo: '', bar: '' }); diff --git a/test/js/node/test/parallel/test-quic-internal-endpoint-listen-defaults.js b/test/js/node/test/parallel/test-quic-internal-endpoint-listen-defaults.js new file mode 100644 index 0000000000..9fb9f9461c --- /dev/null +++ b/test/js/node/test/parallel/test-quic-internal-endpoint-listen-defaults.js @@ -0,0 +1,76 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { internalBinding } = require('internal/test/binding'); +const { + ok, + strictEqual, + deepStrictEqual, +} = require('node:assert'); + +const { + SocketAddress: _SocketAddress, + AF_INET, +} = internalBinding('block_list'); +const quic = internalBinding('quic'); + +quic.setCallbacks({ + onEndpointClose: common.mustCall((...args) => { + deepStrictEqual(args, [0, 0]); + }), + + // The following are unused in this test + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, +}); + +const endpoint = new quic.Endpoint({}); + +const state = new DataView(endpoint.state); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); +strictEqual(endpoint.address(), undefined); + +endpoint.listen({}); + +ok(state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); +ok(state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); +ok(state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); +const address = endpoint.address(); +ok(address instanceof _SocketAddress); + +const detail = address.detail({ + address: undefined, + port: undefined, + family: undefined, + flowlabel: undefined, +}); + +strictEqual(detail.address, '127.0.0.1'); +strictEqual(detail.family, AF_INET); +strictEqual(detail.flowlabel, 0); +ok(detail.port !== 0); + +endpoint.closeGracefully(); + +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); +strictEqual(endpoint.address(), undefined); diff --git a/test/js/node/test/parallel/test-quic-internal-endpoint-options.js b/test/js/node/test/parallel/test-quic-internal-endpoint-options.js new file mode 100644 index 0000000000..672fac18b4 --- /dev/null +++ b/test/js/node/test/parallel/test-quic-internal-endpoint-options.js @@ -0,0 +1,215 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); +const { + throws, +} = require('node:assert'); + +const { internalBinding } = require('internal/test/binding'); +const quic = internalBinding('quic'); + +quic.setCallbacks({ + onEndpointClose() {}, + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, +}); + +throws(() => new quic.Endpoint(), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => new quic.Endpoint('a'), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => new quic.Endpoint(null), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => new quic.Endpoint(false), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +{ + // Just Works... using all defaults + new quic.Endpoint({}); +} + +const cases = [ + { + key: 'retryTokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'tokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsTotal', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxStatelessResetsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'addressLRUSize', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxRetries', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxPayloadSize', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'unacknowledgedPacketThreshold', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'validateAddress', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'disableStatelessReset', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'ipv6Only', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'cc', + valid: [ + quic.CC_ALGO_RENO, + quic.CC_ALGO_CUBIC, + quic.CC_ALGO_BBR, + quic.CC_ALGO_BBR2, + quic.CC_ALGO_RENO_STR, + quic.CC_ALGO_CUBIC_STR, + quic.CC_ALGO_BBR_STR, + quic.CC_ALGO_BBR2_STR, + ], + invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpReceiveBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpSendBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpTTL', + valid: [0, 1, 2, 3, 4, 255], + invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'resetTokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + key: 'tokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + // Unknown options are ignored entirely for any value type + key: 'ignored', + valid: ['a', null, false, true, {}, [], () => {}], + invalid: [], + }, +]; + +for (const { key, valid, invalid } of cases) { + for (const value of valid) { + const options = {}; + options[key] = value; + new quic.Endpoint(options); + } + + for (const value of invalid) { + const options = {}; + options[key] = value; + throws(() => new quic.Endpoint(options), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } +} diff --git a/test/js/node/test/parallel/test-quic-internal-endpoint-stats-state.js b/test/js/node/test/parallel/test-quic-internal-endpoint-stats-state.js new file mode 100644 index 0000000000..566dd675d7 --- /dev/null +++ b/test/js/node/test/parallel/test-quic-internal-endpoint-stats-state.js @@ -0,0 +1,79 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); +const { + strictEqual, +} = require('node:assert'); + +const { internalBinding } = require('internal/test/binding'); +const quic = internalBinding('quic'); + +const { + IDX_STATS_ENDPOINT_CREATED_AT, + IDX_STATS_ENDPOINT_DESTROYED_AT, + IDX_STATS_ENDPOINT_BYTES_RECEIVED, + IDX_STATS_ENDPOINT_BYTES_SENT, + IDX_STATS_ENDPOINT_PACKETS_RECEIVED, + IDX_STATS_ENDPOINT_PACKETS_SENT, + IDX_STATS_ENDPOINT_SERVER_SESSIONS, + IDX_STATS_ENDPOINT_CLIENT_SESSIONS, + IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT, + IDX_STATS_ENDPOINT_RETRY_COUNT, + IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT, + IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT, + IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, + IDX_STATS_ENDPOINT_COUNT, + IDX_STATE_ENDPOINT_BOUND, + IDX_STATE_ENDPOINT_BOUND_SIZE, + IDX_STATE_ENDPOINT_RECEIVING, + IDX_STATE_ENDPOINT_RECEIVING_SIZE, + IDX_STATE_ENDPOINT_LISTENING, + IDX_STATE_ENDPOINT_LISTENING_SIZE, + IDX_STATE_ENDPOINT_CLOSING, + IDX_STATE_ENDPOINT_CLOSING_SIZE, + IDX_STATE_ENDPOINT_BUSY, + IDX_STATE_ENDPOINT_BUSY_SIZE, + IDX_STATE_ENDPOINT_PENDING_CALLBACKS, + IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE, +} = quic; + +const endpoint = new quic.Endpoint({}); + +const state = new DataView(endpoint.state); +strictEqual(IDX_STATE_ENDPOINT_BOUND_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_RECEIVING_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_LISTENING_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_CLOSING_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_BUSY_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE, 8); + +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BOUND), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_RECEIVING), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_LISTENING), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_CLOSING), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); +strictEqual(state.getBigUint64(IDX_STATE_ENDPOINT_PENDING_CALLBACKS), 0n); + +endpoint.markBusy(true); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 1); +endpoint.markBusy(false); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); + +const stats = new BigUint64Array(endpoint.stats); +strictEqual(stats[IDX_STATS_ENDPOINT_CREATED_AT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_SENT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 0n); +strictEqual(IDX_STATS_ENDPOINT_COUNT, 13); diff --git a/test/js/node/test/parallel/test-quic-internal-setcallbacks.js b/test/js/node/test/parallel/test-quic-internal-setcallbacks.js new file mode 100644 index 0000000000..881e9161ca --- /dev/null +++ b/test/js/node/test/parallel/test-quic-internal-setcallbacks.js @@ -0,0 +1,38 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); +const { internalBinding } = require('internal/test/binding'); +const quic = internalBinding('quic'); + +const { throws } = require('assert'); + +const callbacks = { + onEndpointClose() {}, + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, +}; +// Fail if any callback is missing +for (const fn of Object.keys(callbacks)) { + // eslint-disable-next-line no-unused-vars + const { [fn]: _, ...rest } = callbacks; + throws(() => quic.setCallbacks(rest), { + code: 'ERR_MISSING_ARGS', + }); +} +// If all callbacks are present it should work +quic.setCallbacks(callbacks); diff --git a/test/js/node/test/parallel/test-readable-from-iterator-closing.js b/test/js/node/test/parallel/test-readable-from-iterator-closing.js new file mode 100644 index 0000000000..02252ffe56 --- /dev/null +++ b/test/js/node/test/parallel/test-readable-from-iterator-closing.js @@ -0,0 +1,197 @@ +'use strict'; + +const { mustCall, mustNotCall } = require('../common'); +const { Readable } = require('stream'); +const { strictEqual } = require('assert'); + +async function asyncSupport() { + const finallyMustCall = mustCall(); + const bodyMustCall = mustCall(); + + async function* infiniteGenerate() { + try { + while (true) yield 'a'; + } finally { + finallyMustCall(); + } + } + + const stream = Readable.from(infiniteGenerate()); + + for await (const chunk of stream) { + bodyMustCall(); + strictEqual(chunk, 'a'); + break; + } +} + +async function syncSupport() { + const finallyMustCall = mustCall(); + const bodyMustCall = mustCall(); + + function* infiniteGenerate() { + try { + while (true) yield 'a'; + } finally { + finallyMustCall(); + } + } + + const stream = Readable.from(infiniteGenerate()); + + for await (const chunk of stream) { + bodyMustCall(); + strictEqual(chunk, 'a'); + break; + } +} + +async function syncPromiseSupport() { + const returnMustBeAwaited = mustCall(); + const bodyMustCall = mustCall(); + + function* infiniteGenerate() { + try { + while (true) yield Promise.resolve('a'); + } finally { + // eslint-disable-next-line no-unsafe-finally + return { then(cb) { + returnMustBeAwaited(); + cb(); + } }; + } + } + + const stream = Readable.from(infiniteGenerate()); + + for await (const chunk of stream) { + bodyMustCall(); + strictEqual(chunk, 'a'); + break; + } +} + +async function syncRejectedSupport() { + const returnMustBeAwaited = mustCall(); + const bodyMustNotCall = mustNotCall(); + const catchMustCall = mustCall(); + const secondNextMustNotCall = mustNotCall(); + + function* generate() { + try { + yield Promise.reject('a'); + secondNextMustNotCall(); + } finally { + // eslint-disable-next-line no-unsafe-finally + return { then(cb) { + returnMustBeAwaited(); + cb(); + } }; + } + } + + const stream = Readable.from(generate()); + + try { + for await (const chunk of stream) { + bodyMustNotCall(chunk); + } + } catch { + catchMustCall(); + } +} + +async function noReturnAfterThrow() { + const returnMustNotCall = mustNotCall(); + const bodyMustNotCall = mustNotCall(); + const catchMustCall = mustCall(); + const nextMustCall = mustCall(); + + const stream = Readable.from({ + [Symbol.asyncIterator]() { return this; }, + async next() { + nextMustCall(); + throw new Error('a'); + }, + async return() { + returnMustNotCall(); + return { done: true }; + }, + }); + + try { + for await (const chunk of stream) { + bodyMustNotCall(chunk); + } + } catch { + catchMustCall(); + } +} + +async function closeStreamWhileNextIsPending() { + const finallyMustCall = mustCall(); + const dataMustCall = mustCall(); + + let resolveDestroy; + const destroyed = + new Promise((resolve) => { resolveDestroy = mustCall(resolve); }); + let resolveYielded; + const yielded = + new Promise((resolve) => { resolveYielded = mustCall(resolve); }); + + async function* infiniteGenerate() { + try { + while (true) { + yield 'a'; + resolveYielded(); + await destroyed; + } + } finally { + finallyMustCall(); + } + } + + const stream = Readable.from(infiniteGenerate()); + + stream.on('data', (data) => { + dataMustCall(); + strictEqual(data, 'a'); + }); + + yielded.then(() => { + stream.destroy(); + resolveDestroy(); + }); +} + +async function closeAfterNullYielded() { + const finallyMustCall = mustCall(); + const dataMustCall = mustCall(3); + + function* generate() { + try { + yield 'a'; + yield 'a'; + yield 'a'; + } finally { + finallyMustCall(); + } + } + + const stream = Readable.from(generate()); + + stream.on('data', (chunk) => { + dataMustCall(); + strictEqual(chunk, 'a'); + }); +} + +Promise.all([ + asyncSupport(), + syncSupport(), + syncPromiseSupport(), + syncRejectedSupport(), + noReturnAfterThrow(), + closeStreamWhileNextIsPending(), + closeAfterNullYielded(), +]).then(mustCall()); diff --git a/test/js/node/test/parallel/test-readable-from-web-enqueue-then-close.js b/test/js/node/test/parallel/test-readable-from-web-enqueue-then-close.js new file mode 100644 index 0000000000..e96df70c9e --- /dev/null +++ b/test/js/node/test/parallel/test-readable-from-web-enqueue-then-close.js @@ -0,0 +1,26 @@ +'use strict'; +const { mustCall } = require('../common'); +const { Readable, Duplex } = require('stream'); +const { strictEqual } = require('assert'); + +function start(controller) { + controller.enqueue(new Uint8Array(1)); + controller.close(); +} + +Readable.fromWeb(new ReadableStream({ start })) +.on('data', mustCall((d) => { + strictEqual(d.length, 1); +})) +.on('end', mustCall()) +.resume(); + +Duplex.fromWeb({ + readable: new ReadableStream({ start }), + writable: new WritableStream({ write(chunk) {} }) +}) +.on('data', mustCall((d) => { + strictEqual(d.length, 1); +})) +.on('end', mustCall()) +.resume(); diff --git a/test/js/node/test/parallel/test-readable-from.js b/test/js/node/test/parallel/test-readable-from.js new file mode 100644 index 0000000000..b844574dc9 --- /dev/null +++ b/test/js/node/test/parallel/test-readable-from.js @@ -0,0 +1,223 @@ +'use strict'; + +const { mustCall } = require('../common'); +const { once } = require('events'); +const { Readable } = require('stream'); +const { strictEqual, throws } = require('assert'); +const common = require('../common'); + +{ + throws(() => { + Readable.from(null); + }, /ERR_INVALID_ARG_TYPE/); +} + +async function toReadableBasicSupport() { + async function* generate() { + yield 'a'; + yield 'b'; + yield 'c'; + } + + const stream = Readable.from(generate()); + + const expected = ['a', 'b', 'c']; + + for await (const chunk of stream) { + strictEqual(chunk, expected.shift()); + } +} + +async function toReadableSyncIterator() { + function* generate() { + yield 'a'; + yield 'b'; + yield 'c'; + } + + const stream = Readable.from(generate()); + + const expected = ['a', 'b', 'c']; + + for await (const chunk of stream) { + strictEqual(chunk, expected.shift()); + } +} + +async function toReadablePromises() { + const promises = [ + Promise.resolve('a'), + Promise.resolve('b'), + Promise.resolve('c'), + ]; + + const stream = Readable.from(promises); + + const expected = ['a', 'b', 'c']; + + for await (const chunk of stream) { + strictEqual(chunk, expected.shift()); + } +} + +async function toReadableString() { + const stream = Readable.from('abc'); + + const expected = ['abc']; + + for await (const chunk of stream) { + strictEqual(chunk, expected.shift()); + } +} + +async function toReadableBuffer() { + const stream = Readable.from(Buffer.from('abc')); + + const expected = ['abc']; + + for await (const chunk of stream) { + strictEqual(chunk.toString(), expected.shift()); + } +} + +async function toReadableOnData() { + async function* generate() { + yield 'a'; + yield 'b'; + yield 'c'; + } + + const stream = Readable.from(generate()); + + let iterations = 0; + const expected = ['a', 'b', 'c']; + + stream.on('data', (chunk) => { + iterations++; + strictEqual(chunk, expected.shift()); + }); + + await once(stream, 'end'); + + strictEqual(iterations, 3); +} + +async function toReadableOnDataNonObject() { + async function* generate() { + yield 'a'; + yield 'b'; + yield 'c'; + } + + const stream = Readable.from(generate(), { objectMode: false }); + + let iterations = 0; + const expected = ['a', 'b', 'c']; + + stream.on('data', (chunk) => { + iterations++; + strictEqual(chunk instanceof Buffer, true); + strictEqual(chunk.toString(), expected.shift()); + }); + + await once(stream, 'end'); + + strictEqual(iterations, 3); +} + +async function destroysTheStreamWhenThrowing() { + async function* generate() { // eslint-disable-line require-yield + throw new Error('kaboom'); + } + + const stream = Readable.from(generate()); + + stream.read(); + + const [err] = await once(stream, 'error'); + strictEqual(err.message, 'kaboom'); + strictEqual(stream.destroyed, true); + +} + +async function asTransformStream() { + async function* generate(stream) { + for await (const chunk of stream) { + yield chunk.toUpperCase(); + } + } + + const source = new Readable({ + objectMode: true, + read() { + this.push('a'); + this.push('b'); + this.push('c'); + this.push(null); + } + }); + + const stream = Readable.from(generate(source)); + + const expected = ['A', 'B', 'C']; + + for await (const chunk of stream) { + strictEqual(chunk, expected.shift()); + } +} + +async function endWithError() { + async function* generate() { + yield 1; + yield 2; + yield Promise.reject('Boum'); + } + + const stream = Readable.from(generate()); + + const expected = [1, 2]; + + try { + for await (const chunk of stream) { + strictEqual(chunk, expected.shift()); + } + throw new Error(); + } catch (err) { + strictEqual(expected.length, 0); + strictEqual(err, 'Boum'); + } +} + +async function destroyingStreamWithErrorThrowsInGenerator() { + const validateError = common.mustCall((e) => { + strictEqual(e, 'Boum'); + }); + async function* generate() { + try { + yield 1; + yield 2; + yield 3; + throw new Error(); + } catch (e) { + validateError(e); + } + } + const stream = Readable.from(generate()); + stream.read(); + stream.once('error', common.mustCall()); + stream.destroy('Boum'); +} + +Promise.all([ + toReadableBasicSupport(), + toReadableSyncIterator(), + toReadablePromises(), + toReadableString(), + toReadableBuffer(), + toReadableOnData(), + toReadableOnDataNonObject(), + destroysTheStreamWhenThrowing(), + asTransformStream(), + endWithError(), + destroyingStreamWithErrorThrowsInGenerator(), +]).then(mustCall()); diff --git a/test/js/node/test/parallel/test-readable-large-hwm.js b/test/js/node/test/parallel/test-readable-large-hwm.js new file mode 100644 index 0000000000..d5bf25bc0e --- /dev/null +++ b/test/js/node/test/parallel/test-readable-large-hwm.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const { Readable } = require('stream'); + +// Make sure that readable completes +// even when reading larger buffer. +const bufferSize = 10 * 1024 * 1024; +let n = 0; +const r = new Readable({ + read() { + // Try to fill readable buffer piece by piece. + r.push(Buffer.alloc(bufferSize / 10)); + + if (n++ > 10) { + r.push(null); + } + } +}); + +r.on('readable', () => { + while (true) { + const ret = r.read(bufferSize); + if (ret === null) + break; + } +}); +r.on('end', common.mustCall()); diff --git a/test/js/node/test/parallel/test-readable-single-end.js b/test/js/node/test/parallel/test-readable-single-end.js new file mode 100644 index 0000000000..0969d49aa4 --- /dev/null +++ b/test/js/node/test/parallel/test-readable-single-end.js @@ -0,0 +1,16 @@ +'use strict'; + +const common = require('../common'); +const { Readable } = require('stream'); + +// This test ensures that there will not be an additional empty 'readable' +// event when stream has ended (only 1 event signalling about end) + +const r = new Readable({ + read: () => {}, +}); + +r.push(null); + +r.on('readable', common.mustCall()); +r.on('end', common.mustCall()); diff --git a/test/js/node/test/parallel/test-readline-async-iterators-destroy.js b/test/js/node/test/parallel/test-readline-async-iterators-destroy.js new file mode 100644 index 0000000000..0a3fb01890 --- /dev/null +++ b/test/js/node/test/parallel/test-readline-async-iterators-destroy.js @@ -0,0 +1,89 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const { once } = require('events'); +const readline = require('readline'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('test.txt'); + +const testContents = [ + '', + '\n', + 'line 1', + 'line 1\nline 2 南越国是前203年至前111年存在于岭南地区的一个国家\nline 3\ntrailing', + 'line 1\nline 2\nline 3 ends with newline\n', +]; + +async function testSimpleDestroy() { + for (const fileContent of testContents) { + fs.writeFileSync(filename, fileContent); + + const readable = fs.createReadStream(filename); + const rli = readline.createInterface({ + input: readable, + crlfDelay: Infinity + }); + + const iteratedLines = []; + for await (const k of rli) { + iteratedLines.push(k); + break; + } + + const expectedLines = fileContent.split('\n'); + if (expectedLines[expectedLines.length - 1] === '') { + expectedLines.pop(); + } + expectedLines.splice(1); + + assert.deepStrictEqual(iteratedLines, expectedLines); + + rli.close(); + readable.destroy(); + + await once(readable, 'close'); + } +} + +async function testMutualDestroy() { + for (const fileContent of testContents) { + fs.writeFileSync(filename, fileContent); + + const readable = fs.createReadStream(filename); + const rli = readline.createInterface({ + input: readable, + crlfDelay: Infinity + }); + + const expectedLines = fileContent.split('\n'); + if (expectedLines[expectedLines.length - 1] === '') { + expectedLines.pop(); + } + expectedLines.splice(2); + + const iteratedLines = []; + for await (const k of rli) { + iteratedLines.push(k); + for await (const l of rli) { + iteratedLines.push(l); + break; + } + assert.deepStrictEqual(iteratedLines, expectedLines); + break; + } + + assert.deepStrictEqual(iteratedLines, expectedLines); + + rli.close(); + readable.destroy(); + + await once(readable, 'close'); + } +} + +testSimpleDestroy().then(testMutualDestroy).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-readline-async-iterators.js b/test/js/node/test/parallel/test-readline-async-iterators.js new file mode 100644 index 0000000000..32fa32a128 --- /dev/null +++ b/test/js/node/test/parallel/test-readline-async-iterators.js @@ -0,0 +1,120 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const readline = require('readline'); +const { Readable } = require('stream'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('test.txt'); + +const testContents = [ + '', + '\n', + 'line 1', + 'line 1\nline 2 南越国是前203年至前111年存在于岭南地区的一个国家\nline 3\ntrailing', + 'line 1\nline 2\nline 3 ends with newline\n', +]; + +async function testSimple() { + for (const fileContent of testContents) { + fs.writeFileSync(filename, fileContent); + + const readable = fs.createReadStream(filename); + const rli = readline.createInterface({ + input: readable, + crlfDelay: Infinity + }); + + const iteratedLines = []; + for await (const k of rli) { + iteratedLines.push(k); + } + + const expectedLines = fileContent.split('\n'); + if (expectedLines[expectedLines.length - 1] === '') { + expectedLines.pop(); + } + assert.deepStrictEqual(iteratedLines, expectedLines); + assert.strictEqual(iteratedLines.join(''), fileContent.replace(/\n/g, '')); + } +} + +async function testMutual() { + for (const fileContent of testContents) { + fs.writeFileSync(filename, fileContent); + + const readable = fs.createReadStream(filename); + const rli = readline.createInterface({ + input: readable, + crlfDelay: Infinity + }); + + const expectedLines = fileContent.split('\n'); + if (expectedLines[expectedLines.length - 1] === '') { + expectedLines.pop(); + } + const iteratedLines = []; + let iterated = false; + for await (const k of rli) { + // This outer loop should only iterate once. + assert.strictEqual(iterated, false); + iterated = true; + iteratedLines.push(k); + for await (const l of rli) { + iteratedLines.push(l); + } + assert.deepStrictEqual(iteratedLines, expectedLines); + } + assert.deepStrictEqual(iteratedLines, expectedLines); + } +} + +async function testSlowStreamForLeaks() { + const message = 'a\nb\nc\n'; + const DELAY = 1; + const REPETITIONS = 100; + const warningCallback = common.mustNotCall(); + process.on('warning', warningCallback); + + function getStream() { + const readable = Readable({ + objectMode: true, + }); + readable._read = () => {}; + let i = REPETITIONS; + function schedule() { + setTimeout(() => { + i--; + if (i < 0) { + readable.push(null); + } else { + readable.push(message); + schedule(); + } + }, DELAY); + } + schedule(); + return readable; + } + const iterable = readline.createInterface({ + input: getStream(), + }); + + let lines = 0; + // eslint-disable-next-line no-unused-vars + for await (const _ of iterable) { + lines++; + } + + assert.strictEqual(lines, 3 * REPETITIONS); + process.off('warning', warningCallback); +} + +testSimple() + .then(testMutual) + .then(testSlowStreamForLeaks) + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-readline-emit-keypress-events.js b/test/js/node/test/parallel/test-readline-emit-keypress-events.js new file mode 100644 index 0000000000..a9ffd3276c --- /dev/null +++ b/test/js/node/test/parallel/test-readline-emit-keypress-events.js @@ -0,0 +1,72 @@ +'use strict'; +// emitKeypressEvents is thoroughly tested in test-readline-keys.js. +// However, that test calls it implicitly. This is just a quick sanity check +// to verify that it works when called explicitly. + +require('../common'); +const assert = require('assert'); +const readline = require('readline'); +const PassThrough = require('stream').PassThrough; + +const expectedSequence = ['f', 'o', 'o']; +const expectedKeys = [ + { sequence: 'f', name: 'f', ctrl: false, meta: false, shift: false }, + { sequence: 'o', name: 'o', ctrl: false, meta: false, shift: false }, + { sequence: 'o', name: 'o', ctrl: false, meta: false, shift: false }, +]; + +{ + const stream = new PassThrough(); + const sequence = []; + const keys = []; + + readline.emitKeypressEvents(stream); + stream.on('keypress', (s, k) => { + sequence.push(s); + keys.push(k); + }); + stream.write('foo'); + + assert.deepStrictEqual(sequence, expectedSequence); + assert.deepStrictEqual(keys, expectedKeys); +} + +{ + const stream = new PassThrough(); + const sequence = []; + const keys = []; + + stream.on('keypress', (s, k) => { + sequence.push(s); + keys.push(k); + }); + readline.emitKeypressEvents(stream); + stream.write('foo'); + + assert.deepStrictEqual(sequence, expectedSequence); + assert.deepStrictEqual(keys, expectedKeys); +} + +{ + const stream = new PassThrough(); + const sequence = []; + const keys = []; + const keypressListener = (s, k) => { + sequence.push(s); + keys.push(k); + }; + + stream.on('keypress', keypressListener); + readline.emitKeypressEvents(stream); + stream.removeListener('keypress', keypressListener); + stream.write('foo'); + + assert.deepStrictEqual(sequence, []); + assert.deepStrictEqual(keys, []); + + stream.on('keypress', keypressListener); + stream.write('foo'); + + assert.deepStrictEqual(sequence, expectedSequence); + assert.deepStrictEqual(keys, expectedKeys); +} diff --git a/test/js/node/test/parallel/test-readline-interface-escapecodetimeout.js b/test/js/node/test/parallel/test-readline-interface-escapecodetimeout.js new file mode 100644 index 0000000000..a0c0e77cb8 --- /dev/null +++ b/test/js/node/test/parallel/test-readline-interface-escapecodetimeout.js @@ -0,0 +1,46 @@ +'use strict'; +require('../common'); + +// This test ensures that the escapeCodeTimeout option set correctly + +const assert = require('assert'); +const readline = require('readline'); +const EventEmitter = require('events').EventEmitter; + +class FakeInput extends EventEmitter { + resume() {} + pause() {} + write() {} + end() {} +} + +{ + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + escapeCodeTimeout: 50 + }); + assert.strictEqual(rli.escapeCodeTimeout, 50); + rli.close(); +} + +[ + null, + {}, + NaN, + '50', +].forEach((invalidInput) => { + assert.throws(() => { + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + escapeCodeTimeout: invalidInput + }); + rli.close(); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE' + }); +}); diff --git a/test/js/node/test/parallel/test-readline-position.js b/test/js/node/test/parallel/test-readline-position.js new file mode 100644 index 0000000000..3603a42ece --- /dev/null +++ b/test/js/node/test/parallel/test-readline-position.js @@ -0,0 +1,36 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const { PassThrough } = require('stream'); +const readline = require('readline'); +const assert = require('assert'); + +const ctrlU = { ctrl: true, name: 'u' }; + +common.skipIfDumbTerminal(); + +{ + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input, + prompt: '' + }); + + const tests = [ + [1, 'a'], + [2, 'ab'], + [2, '丁'], + [0, '\u0301'], // COMBINING ACUTE ACCENT + [1, 'a\u0301'], // á + [0, '\u20DD'], // COMBINING ENCLOSING CIRCLE + [2, 'a\u20DDb'], // a⃝b + [0, '\u200E'], // LEFT-TO-RIGHT MARK + ]; + + for (const [cursor, string] of tests) { + rl.write(string); + assert.strictEqual(rl.getCursorPos().cols, cursor); + rl.write(null, ctrlU); + } +} diff --git a/test/js/node/test/parallel/test-readline-reopen.js b/test/js/node/test/parallel/test-readline-reopen.js new file mode 100644 index 0000000000..fd305fee3e --- /dev/null +++ b/test/js/node/test/parallel/test-readline-reopen.js @@ -0,0 +1,44 @@ +'use strict'; + +// Regression test for https://github.com/nodejs/node/issues/13557 +// Tests that multiple subsequent readline instances can re-use an input stream. + +const common = require('../common'); +const assert = require('assert'); +const readline = require('readline'); +const { PassThrough } = require('stream'); + +const input = new PassThrough(); +const output = new PassThrough(); + +const rl1 = readline.createInterface({ + input, + output, + terminal: true +}); + +rl1.on('line', common.mustCall(rl1OnLine)); + +// Write a line plus the first byte of a UTF-8 multibyte character to make sure +// that it doesn’t get lost when closing the readline instance. +input.write(Buffer.concat([ + Buffer.from('foo\n'), + Buffer.from([ 0xe2 ]), // Exactly one third of a ☃ snowman. +])); + +function rl1OnLine(line) { + assert.strictEqual(line, 'foo'); + rl1.close(); + const rl2 = readline.createInterface({ + input, + output, + terminal: true + }); + + rl2.on('line', common.mustCall((line) => { + assert.strictEqual(line, '☃bar'); + rl2.close(); + })); + input.write(Buffer.from([0x98, 0x83])); // The rest of the ☃ snowman. + input.write('bar\n'); +} diff --git a/test/js/node/test/parallel/test-readline-undefined-columns.js b/test/js/node/test/parallel/test-readline-undefined-columns.js new file mode 100644 index 0000000000..25bafe957f --- /dev/null +++ b/test/js/node/test/parallel/test-readline-undefined-columns.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const PassThrough = require('stream').PassThrough; +const readline = require('readline'); + +common.skipIfDumbTerminal(); + +// Checks that tab completion still works +// when output column size is undefined + +const iStream = new PassThrough(); +const oStream = new PassThrough(); + +readline.createInterface({ + terminal: true, + input: iStream, + output: oStream, + completer: function(line, cb) { + cb(null, [['process.stdout', 'process.stdin', 'process.stderr'], line]); + } +}); + +let output = ''; + +oStream.on('data', function(data) { + output += data; +}); + +oStream.on('end', common.mustCall(() => { + const expect = 'process.stdout\r\n' + + 'process.stdin\r\n' + + 'process.stderr'; + assert.match(output, new RegExp(expect)); +})); + +iStream.write('process.s\t'); + +// Completion works. +assert.match(output, /process\.std\b/); +// Completion doesn’t show all results yet. +assert.doesNotMatch(output, /stdout/); + +iStream.write('\t'); +oStream.end(); diff --git a/test/js/node/test/parallel/test-readline.js b/test/js/node/test/parallel/test-readline.js new file mode 100644 index 0000000000..77799fc14c --- /dev/null +++ b/test/js/node/test/parallel/test-readline.js @@ -0,0 +1,151 @@ +'use strict'; +const common = require('../common'); +const { PassThrough } = require('stream'); +const readline = require('readline'); +const assert = require('assert'); + +common.skipIfDumbTerminal(); + +{ + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input + }); + + rl.on('line', common.mustCall((data) => { + assert.strictEqual(data, 'abc'); + })); + + input.end('abc'); +} + +{ + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input + }); + + rl.on('line', common.mustNotCall('must not be called before newline')); + + input.write('abc'); +} + +{ + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input + }); + + rl.on('line', common.mustCall((data) => { + assert.strictEqual(data, 'abc'); + })); + + input.write('abc\n'); +} + +{ + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input + }); + + rl.write('foo'); + assert.strictEqual(rl.cursor, 3); + + const key = { + xterm: { + home: ['\x1b[H', { ctrl: true, name: 'a' }], + end: ['\x1b[F', { ctrl: true, name: 'e' }], + }, + gnome: { + home: ['\x1bOH', { ctrl: true, name: 'a' }], + end: ['\x1bOF', { ctrl: true, name: 'e' }] + }, + rxvt: { + home: ['\x1b[7', { ctrl: true, name: 'a' }], + end: ['\x1b[8', { ctrl: true, name: 'e' }] + }, + putty: { + home: ['\x1b[1~', { ctrl: true, name: 'a' }], + end: ['\x1b[>~', { ctrl: true, name: 'e' }] + } + }; + + [key.xterm, key.gnome, key.rxvt, key.putty].forEach(function(key) { + rl.write.apply(rl, key.home); + assert.strictEqual(rl.cursor, 0); + rl.write.apply(rl, key.end); + assert.strictEqual(rl.cursor, 3); + }); + +} + +{ + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input + }); + + const key = { + xterm: { + home: ['\x1b[H', { ctrl: true, name: 'a' }], + metab: ['\x1bb', { meta: true, name: 'b' }], + metaf: ['\x1bf', { meta: true, name: 'f' }], + } + }; + + rl.write('foo bar.hop/zoo'); + rl.write.apply(rl, key.xterm.home); + [ + { cursor: 4, key: key.xterm.metaf }, + { cursor: 7, key: key.xterm.metaf }, + { cursor: 8, key: key.xterm.metaf }, + { cursor: 11, key: key.xterm.metaf }, + { cursor: 12, key: key.xterm.metaf }, + { cursor: 15, key: key.xterm.metaf }, + { cursor: 12, key: key.xterm.metab }, + { cursor: 11, key: key.xterm.metab }, + { cursor: 8, key: key.xterm.metab }, + { cursor: 7, key: key.xterm.metab }, + { cursor: 4, key: key.xterm.metab }, + { cursor: 0, key: key.xterm.metab }, + ].forEach(function(action) { + rl.write.apply(rl, action.key); + assert.strictEqual(rl.cursor, action.cursor); + }); +} + +{ + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input + }); + + const key = { + xterm: { + home: ['\x1b[H', { ctrl: true, name: 'a' }], + metad: ['\x1bd', { meta: true, name: 'd' }] + } + }; + + rl.write('foo bar.hop/zoo'); + rl.write.apply(rl, key.xterm.home); + [ + 'bar.hop/zoo', + '.hop/zoo', + 'hop/zoo', + '/zoo', + 'zoo', + '', + ].forEach(function(expectedLine) { + rl.write.apply(rl, key.xterm.metad); + assert.strictEqual(rl.cursor, 0); + assert.strictEqual(rl.line, expectedLine); + }); +} diff --git a/test/js/node/test/parallel/test-ref-unref-return.js b/test/js/node/test/parallel/test-ref-unref-return.js new file mode 100644 index 0000000000..aec2fff5ce --- /dev/null +++ b/test/js/node/test/parallel/test-ref-unref-return.js @@ -0,0 +1,12 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); +const dgram = require('dgram'); + +assert.ok((new net.Server()).ref() instanceof net.Server); +assert.ok((new net.Server()).unref() instanceof net.Server); +assert.ok((new net.Socket()).ref() instanceof net.Socket); +assert.ok((new net.Socket()).unref() instanceof net.Socket); +assert.ok((new dgram.Socket('udp4')).ref() instanceof dgram.Socket); +assert.ok((new dgram.Socket('udp6')).unref() instanceof dgram.Socket); diff --git a/test/js/node/test/parallel/test-regression-object-prototype.js b/test/js/node/test/parallel/test-regression-object-prototype.js new file mode 100644 index 0000000000..2ea1ba858a --- /dev/null +++ b/test/js/node/test/parallel/test-regression-object-prototype.js @@ -0,0 +1,28 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* eslint-disable node-core/require-common-first, node-core/required-modules */ +'use strict'; + +Object.prototype.xadsadsdasasdxx = function() { +}; + +console.log('puts after'); diff --git a/test/js/node/test/parallel/test-repl-clear-immediate-crash.js b/test/js/node/test/parallel/test-repl-clear-immediate-crash.js new file mode 100644 index 0000000000..ce8aaf48e7 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-clear-immediate-crash.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +const child_process = require('child_process'); +const assert = require('assert'); + +// Regression test for https://github.com/nodejs/node/issues/37806: +const proc = child_process.spawn(process.execPath, ['-i']); +proc.on('error', common.mustNotCall()); +proc.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); +})); +proc.stdin.write('clearImmediate({});\n.exit\n'); diff --git a/test/js/node/test/parallel/test-repl-dynamic-import.js b/test/js/node/test/parallel/test-repl-dynamic-import.js new file mode 100644 index 0000000000..a043e31bf5 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-dynamic-import.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const child_process = require('child_process'); +const child = child_process.spawn(process.execPath, [ + '--interactive', + '--expose-gc', +], { + stdio: 'pipe' +}); +child.stdin.write('\nimport("fs");\n_.then(gc);\n'); +// Wait for concurrent GC to finish +setTimeout(() => { + child.stdin.write('\nimport("fs");\n'); + child.stdin.write('\nprocess.exit(0);\n'); +}, common.platformTimeout(50)); +child.on('exit', (code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +}); diff --git a/test/js/node/test/parallel/test-repl-preview-without-inspector.js b/test/js/node/test/parallel/test-repl-preview-without-inspector.js new file mode 100644 index 0000000000..8905d21483 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-preview-without-inspector.js @@ -0,0 +1,161 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { REPLServer } = require('repl'); +const { Stream } = require('stream'); + +if (process.features.inspector) + common.skip('test is for node compiled with --without-inspector only'); + +// Ignore terminal settings. This is so the test can be run intact if TERM=dumb. +process.env.TERM = ''; +const PROMPT = 'repl > '; + +class REPLStream extends Stream { + readable = true; + writable = true; + + constructor() { + super(); + this.lines = ['']; + } + run(data) { + for (const entry of data) { + this.emit('data', entry); + } + this.emit('data', '\n'); + } + write(chunk) { + const chunkLines = chunk.toString('utf8').split('\n'); + this.lines[this.lines.length - 1] += chunkLines[0]; + if (chunkLines.length > 1) { + this.lines.push(...chunkLines.slice(1)); + } + this.emit('line'); + return true; + } + wait() { + this.lines = ['']; + return new Promise((resolve, reject) => { + const onError = (err) => { + this.removeListener('line', onLine); + reject(err); + }; + const onLine = () => { + if (this.lines[this.lines.length - 1].includes(PROMPT)) { + this.removeListener('error', onError); + this.removeListener('line', onLine); + resolve(this.lines); + } + }; + this.once('error', onError); + this.on('line', onLine); + }); + } + pause() { } + resume() { } +} + +function runAndWait(cmds, repl) { + const promise = repl.inputStream.wait(); + for (const cmd of cmds) { + repl.inputStream.run(cmd); + } + return promise; +} + +const repl = REPLServer({ + prompt: PROMPT, + stream: new REPLStream(), + ignoreUndefined: true, + useColors: true, + terminal: true, +}); + +repl.inputStream.run([ + 'function foo(x) { return x; }', + 'function koo() { console.log("abc"); }', + 'a = undefined;', + 'const r = 5;', +]); + +const testCases = [{ + input: 'foo', + preview: [ + 'foo\r', + '\x1B[36m[Function: foo]\x1B[39m', + ] +}, { + input: 'r', + preview: [ + 'r\r', + '\x1B[33m5\x1B[39m', + ] +}, { + input: 'koo', + preview: [ + 'koo\r', + '\x1B[36m[Function: koo]\x1B[39m', + ] +}, { + input: 'a', + preview: ['a\r'] // No "undefined" preview. +}, { + input: " { b: 1 }['b'] === 1", + preview: [ + " { b: 1 }['b'] === 1\r", + '\x1B[33mtrue\x1B[39m', + ] +}, { + input: "{ b: 1 }['b'] === 1;", + preview: [ + "{ b: 1 }['b'] === 1;\r", + '\x1B[33mfalse\x1B[39m', + ] +}, { + input: '{ a: true }', + preview: [ + '{ a: true }\r', + '{ a: \x1B[33mtrue\x1B[39m }', + ] +}, { + input: '{ a: true };', + preview: [ + '{ a: true };\r', + '\x1B[33mtrue\x1B[39m', + ] +}, { + input: ' \t { a: true};', + preview: [ + ' { a: true};\r', + '\x1B[33mtrue\x1B[39m', + ] +}, { + input: '1n + 2n', + preview: [ + '1n + 2n\r', + '\x1B[33m3n\x1B[39m', + ] +}, { + input: '{};1', + preview: [ + '{};1\r', + '\x1B[33m1\x1B[39m', + ], +}]; + +async function runTest() { + for (const { input, preview } of testCases) { + const toBeRun = input.split('\n'); + let lines = await runAndWait(toBeRun, repl); + // Remove error messages. That allows the code to run in different + // engines. + // eslint-disable-next-line no-control-regex + lines = lines.map((line) => line.replace(/Error: .+?\x1B/, '')); + assert.strictEqual(lines.pop(), '\x1B[1G\x1B[0Jrepl > \x1B[8G'); + assert.deepStrictEqual(lines, preview); + } +} + +runTest().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-repl-syntax-error-handling.js b/test/js/node/test/parallel/test-repl-syntax-error-handling.js new file mode 100644 index 0000000000..91a8614d1d --- /dev/null +++ b/test/js/node/test/parallel/test-repl-syntax-error-handling.js @@ -0,0 +1,71 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +switch (process.argv[2]) { + case 'child': + return child(); + case undefined: + return parent(); + default: + throw new Error('invalid'); +} + +function parent() { + const spawn = require('child_process').spawn; + const child = spawn(process.execPath, [__filename, 'child']); + + child.stderr.setEncoding('utf8'); + child.stderr.on('data', function(c) { + console.error(`${c}`); + throw new Error('should not get stderr data'); + }); + + child.stdout.setEncoding('utf8'); + let out = ''; + child.stdout.on('data', function(c) { + out += c; + }); + child.stdout.on('end', function() { + assert.strictEqual(out, '10\n'); + console.log('ok - got expected output'); + }); + + child.on('exit', function(c) { + assert(!c); + console.log('ok - exit success'); + }); +} + +function child() { + const vm = require('vm'); + let caught; + try { + vm.runInThisContext('haf!@##&$!@$*!@', { displayErrors: false }); + } catch { + caught = true; + } + assert(caught); + vm.runInThisContext('console.log(10)', { displayErrors: false }); +} diff --git a/test/js/node/test/parallel/test-require-cache.js b/test/js/node/test/parallel/test-require-cache.js new file mode 100644 index 0000000000..7b62ab5764 --- /dev/null +++ b/test/js/node/test/parallel/test-require-cache.js @@ -0,0 +1,44 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +{ + const relativePath = '../fixtures/semicolon'; + const absolutePath = require.resolve(relativePath); + const fakeModule = {}; + + require.cache[absolutePath] = { exports: fakeModule }; + + assert.strictEqual(require(relativePath), fakeModule); +} + + +{ + const relativePath = 'fs'; + const fakeModule = {}; + + require.cache[relativePath] = { exports: fakeModule }; + + assert.strictEqual(require(relativePath), fakeModule); +} diff --git a/test/js/node/test/parallel/test-require-delete-array-iterator.js b/test/js/node/test/parallel/test-require-delete-array-iterator.js new file mode 100644 index 0000000000..5424ef8b75 --- /dev/null +++ b/test/js/node/test/parallel/test-require-delete-array-iterator.js @@ -0,0 +1,13 @@ +'use strict'; + +const common = require('../common'); + + +const ArrayIteratorPrototype = + Object.getPrototypeOf(Array.prototype[Symbol.iterator]()); + +delete Array.prototype[Symbol.iterator]; +delete ArrayIteratorPrototype.next; + +require('../common/fixtures'); +import('../fixtures/es-modules/test-esm-ok.mjs').then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-require-dot.js b/test/js/node/test/parallel/test-require-dot.js new file mode 100644 index 0000000000..7145e688d4 --- /dev/null +++ b/test/js/node/test/parallel/test-require-dot.js @@ -0,0 +1,23 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const m = require('module'); +const fixtures = require('../common/fixtures'); + +const a = require(fixtures.path('module-require', 'relative', 'dot.js')); +const b = require(fixtures.path('module-require', 'relative', 'dot-slash.js')); + +assert.strictEqual(a.value, 42); +// require(".") should resolve like require("./") +assert.strictEqual(a, b); + +process.env.NODE_PATH = fixtures.path('module-require', 'relative'); +m._initPaths(); + +assert.throws( + () => require('.'), + { + message: /Cannot find module '\.'/, + code: 'MODULE_NOT_FOUND' + } +); diff --git a/test/js/node/test/parallel/test-require-empty-main.js b/test/js/node/test/parallel/test-require-empty-main.js new file mode 100644 index 0000000000..73f141d1f9 --- /dev/null +++ b/test/js/node/test/parallel/test-require-empty-main.js @@ -0,0 +1,25 @@ +'use strict'; +require('../common'); + +// A package.json with an empty "main" property should use index.js if present. +// require.resolve() should resolve to index.js for the same reason. +// +// In fact, any "main" property that doesn't resolve to a file should result +// in index.js being used, but that's already checked for by other tests. +// This test only concerns itself with the empty string. + +const assert = require('assert'); +const path = require('path'); +const fixtures = require('../common/fixtures'); + +const where = fixtures.path('require-empty-main'); +const expected = path.join(where, 'index.js'); + +test(); +setImmediate(test); + +function test() { + assert.strictEqual(require.resolve(where), expected); + assert.strictEqual(require(where), 42); + assert.strictEqual(require.resolve(where), expected); +} diff --git a/test/js/node/test/parallel/test-require-extensions-main.js b/test/js/node/test/parallel/test-require-extensions-main.js new file mode 100644 index 0000000000..16fbad6cf7 --- /dev/null +++ b/test/js/node/test/parallel/test-require-extensions-main.js @@ -0,0 +1,13 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const fixturesRequire = require(fixtures.path('require-bin', 'bin', 'req.js')); + +assert.strictEqual( + fixturesRequire, + '', + 'test-require-extensions-main failed to import fixture requirements: ' + + fixturesRequire +); diff --git a/test/js/node/test/parallel/test-require-extensions-same-filename-as-dir-trailing-slash.js b/test/js/node/test/parallel/test-require-extensions-same-filename-as-dir-trailing-slash.js new file mode 100644 index 0000000000..1a37fd1b54 --- /dev/null +++ b/test/js/node/test/parallel/test-require-extensions-same-filename-as-dir-trailing-slash.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const content = + require(fixtures.path('json-with-directory-name-module', + 'module-stub', + 'one-trailing-slash', + 'two', + 'three.js')); + +assert.notStrictEqual(content.rocko, 'artischocko'); +assert.strictEqual(content, 'hello from module-stub!'); diff --git a/test/js/node/test/parallel/test-require-long-path.js b/test/js/node/test/parallel/test-require-long-path.js new file mode 100644 index 0000000000..469bf8f978 --- /dev/null +++ b/test/js/node/test/parallel/test-require-long-path.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +if (!common.isWindows) + common.skip('this test is Windows-specific.'); + +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); + +// Make a path that is more than 260 chars long. +const dirNameLen = Math.max(260 - tmpdir.path.length, 1); +const dirName = tmpdir.resolve('x'.repeat(dirNameLen)); +const fullDirPath = path.resolve(dirName); + +const indexFile = path.join(fullDirPath, 'index.js'); +const otherFile = path.join(fullDirPath, 'other.js'); + +tmpdir.refresh(); + +fs.mkdirSync(fullDirPath); +fs.writeFileSync(indexFile, 'require("./other");'); +fs.writeFileSync(otherFile, ''); + +require(indexFile); +require(otherFile); + +tmpdir.refresh(); diff --git a/test/js/node/test/parallel/test-require-process.js b/test/js/node/test/parallel/test-require-process.js new file mode 100644 index 0000000000..57af1508f0 --- /dev/null +++ b/test/js/node/test/parallel/test-require-process.js @@ -0,0 +1,7 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const nativeProcess = require('process'); +// require('process') should return global process reference +assert.strictEqual(nativeProcess, process); diff --git a/test/js/node/test/parallel/test-require-unicode.js b/test/js/node/test/parallel/test-require-unicode.js new file mode 100644 index 0000000000..362ec6487a --- /dev/null +++ b/test/js/node/test/parallel/test-require-unicode.js @@ -0,0 +1,17 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const dirname = tmpdir.resolve('\u4e2d\u6587\u76ee\u5f55'); +fs.mkdirSync(dirname); +fs.writeFileSync(path.join(dirname, 'file.js'), 'module.exports = 42;'); +fs.writeFileSync(path.join(dirname, 'package.json'), + JSON.stringify({ name: 'test', main: 'file.js' })); +assert.strictEqual(require(dirname), 42); +assert.strictEqual(require(path.join(dirname, 'file.js')), 42); diff --git a/test/js/node/test/parallel/test-sigint-infinite-loop.js b/test/js/node/test/parallel/test-sigint-infinite-loop.js new file mode 100644 index 0000000000..30eb98ecb8 --- /dev/null +++ b/test/js/node/test/parallel/test-sigint-infinite-loop.js @@ -0,0 +1,34 @@ +'use strict'; +// This test is to assert that we can SIGINT a script which loops forever. +// Ref(http): +// groups.google.com/group/nodejs-dev/browse_thread/thread/e20f2f8df0296d3f +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +console.log('start'); + +const c = spawn(process.execPath, ['-e', 'while(true) { console.log("hi"); }']); + +let sentKill = false; + +c.stdout.on('data', function(s) { + // Prevent race condition: + // Wait for the first bit of output from the child process + // so that we're sure that it's in the V8 event loop and not + // just in the startup phase of execution. + if (!sentKill) { + c.kill('SIGINT'); + console.log('SIGINT infinite-loop.js'); + sentKill = true; + } +}); + +c.on('exit', common.mustCall(function(code) { + assert.ok(code !== 0); + console.log('killed infinite-loop.js'); +})); + +process.on('exit', function() { + assert.ok(sentKill); +}); diff --git a/test/js/node/test/parallel/test-signal-args.js b/test/js/node/test/parallel/test-signal-args.js new file mode 100644 index 0000000000..7b72ed6dcb --- /dev/null +++ b/test/js/node/test/parallel/test-signal-args.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (common.isWindows) + common.skip('Sending signals with process.kill is not supported on Windows'); +if (!common.isMainThread) + common.skip('No signal handling available in Workers'); + +process.once('SIGINT', common.mustCall((signal) => { + assert.strictEqual(signal, 'SIGINT'); +})); + +process.kill(process.pid, 'SIGINT'); + +process.once('SIGTERM', common.mustCall((signal) => { + assert.strictEqual(signal, 'SIGTERM'); +})); + +process.kill(process.pid, 'SIGTERM'); + +// Prevent Node.js from exiting due to empty event loop before signal handlers +// are fired +setImmediate(() => {}); diff --git a/test/js/node/test/parallel/test-signal-handler-remove-on-exit.js b/test/js/node/test/parallel/test-signal-handler-remove-on-exit.js new file mode 100644 index 0000000000..1c87497172 --- /dev/null +++ b/test/js/node/test/parallel/test-signal-handler-remove-on-exit.js @@ -0,0 +1,9 @@ +'use strict'; +require('../common'); + +// Regression test for https://github.com/nodejs/node/issues/30581 +// This script should not crash. + +function dummy() {} +process.on('SIGINT', dummy); +process.on('exit', () => process.removeListener('SIGINT', dummy)); diff --git a/test/js/node/test/parallel/test-signal-handler.js b/test/js/node/test/parallel/test-signal-handler.js new file mode 100644 index 0000000000..05ec4e7f73 --- /dev/null +++ b/test/js/node/test/parallel/test-signal-handler.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); + +if (common.isWindows) + common.skip('SIGUSR1 and SIGHUP signals are not supported'); +if (!common.isMainThread) + common.skip('Signal handling in Workers is not supported'); + +console.log(`process.pid: ${process.pid}`); + +process.on('SIGUSR1', common.mustCall()); + +process.on('SIGUSR1', common.mustCall(function() { + setTimeout(function() { + console.log('End.'); + process.exit(0); + }, 5); +})); + +let i = 0; +setInterval(function() { + console.log(`running process...${++i}`); + + if (i === 5) { + process.kill(process.pid, 'SIGUSR1'); + } +}, 1); + +// Test on condition where a watcher for SIGNAL +// has been previously registered, and `process.listeners(SIGNAL).length === 1` +process.on('SIGHUP', common.mustNotCall()); +process.removeAllListeners('SIGHUP'); +process.on('SIGHUP', common.mustCall()); +process.kill(process.pid, 'SIGHUP'); diff --git a/test/js/node/test/parallel/test-signal-unregister.js b/test/js/node/test/parallel/test-signal-unregister.js new file mode 100644 index 0000000000..2c4d312942 --- /dev/null +++ b/test/js/node/test/parallel/test-signal-unregister.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const fixtures = require('../common/fixtures'); + +const child = spawn(process.argv[0], [fixtures.path('should_exit.js')]); +child.stdout.once('data', function() { + child.kill('SIGINT'); +}); +child.on('exit', common.mustCall(function(exitCode, signalCode) { + assert.strictEqual(exitCode, null); + assert.strictEqual(signalCode, 'SIGINT'); +})); diff --git a/test/js/node/test/parallel/test-spawn-cmd-named-pipe.js b/test/js/node/test/parallel/test-spawn-cmd-named-pipe.js new file mode 100644 index 0000000000..bfc3ce4a39 --- /dev/null +++ b/test/js/node/test/parallel/test-spawn-cmd-named-pipe.js @@ -0,0 +1,51 @@ +'use strict'; +const common = require('../common'); +// This test is intended for Windows only +if (!common.isWindows) + common.skip('this test is Windows-specific.'); +if (common.isWindows) return; // TODO: BUN + +const assert = require('assert'); + +if (!process.argv[2]) { + // parent + const net = require('net'); + const spawn = require('child_process').spawn; + const path = require('path'); + + const pipeNamePrefix = `${path.basename(__filename)}.${process.pid}`; + const stdinPipeName = `\\\\.\\pipe\\${pipeNamePrefix}.stdin`; + const stdoutPipeName = `\\\\.\\pipe\\${pipeNamePrefix}.stdout`; + + const stdinPipeServer = net.createServer(function(c) { + c.on('end', common.mustCall()); + c.end('hello'); + }); + stdinPipeServer.listen(stdinPipeName); + + const output = []; + + const stdoutPipeServer = net.createServer(function(c) { + c.on('data', function(x) { + output.push(x); + }); + c.on('end', common.mustCall(function() { + assert.strictEqual(output.join(''), 'hello'); + })); + }); + stdoutPipeServer.listen(stdoutPipeName); + + const args = + [`"${__filename}"`, 'child', '<', stdinPipeName, '>', stdoutPipeName]; + + const child = spawn(`"${process.execPath}"`, args, { shell: true }); + + child.on('exit', common.mustCall(function(exitCode) { + stdinPipeServer.close(); + stdoutPipeServer.close(); + assert.strictEqual(exitCode, 0); + })); +} else { + // child + process.stdin.pipe(process.stdout); +} diff --git a/test/js/node/test/parallel/test-stdin-child-proc.js b/test/js/node/test/parallel/test-stdin-child-proc.js new file mode 100644 index 0000000000..bbb6a29cd7 --- /dev/null +++ b/test/js/node/test/parallel/test-stdin-child-proc.js @@ -0,0 +1,15 @@ +'use strict'; +// This tests that pausing and resuming stdin does not hang and timeout +// when done in a child process. See test/parallel/test-stdin-pause-resume.js +const common = require('../common'); +const assert = require('assert'); +const child_process = require('child_process'); +const path = require('path'); +const cp = child_process.spawn( + process.execPath, + [path.resolve(__dirname, 'test-stdin-pause-resume.js')] +); + +cp.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); +})); diff --git a/test/js/node/test/parallel/test-stdin-from-file-spawn.js b/test/js/node/test/parallel/test-stdin-from-file-spawn.js new file mode 100644 index 0000000000..3830ac124a --- /dev/null +++ b/test/js/node/test/parallel/test-stdin-from-file-spawn.js @@ -0,0 +1,42 @@ +'use strict'; +const common = require('../common'); +const process = require('process'); + +let defaultShell; +if (process.platform === 'linux' || process.platform === 'darwin') { + defaultShell = '/bin/sh'; +} else if (process.platform === 'win32') { + defaultShell = 'cmd.exe'; +} else { + common.skip('This is test exists only on Linux/Win32/macOS'); +} + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const tmpdir = require('../common/tmpdir'); + +const tmpDir = tmpdir.path; +tmpdir.refresh(); +const tmpCmdFile = path.join(tmpDir, 'test-stdin-from-file-spawn-cmd'); +const tmpJsFile = path.join(tmpDir, 'test-stdin-from-file-spawn.js'); +fs.writeFileSync(tmpCmdFile, 'echo hello'); +fs.writeFileSync(tmpJsFile, ` +'use strict'; +const { spawn } = require('child_process'); +// Reference the object to invoke the getter +process.stdin; +setTimeout(() => { + let ok = false; + const child = spawn(process.env.SHELL || '${defaultShell}', + [], { stdio: ['inherit', 'pipe'] }); + child.stdout.on('data', () => { + ok = true; + }); + child.on('close', () => { + process.exit(ok ? 0 : -1); + }); +}, 100); +`); + +execSync(`${process.argv[0]} ${tmpJsFile} < ${tmpCmdFile}`); diff --git a/test/js/node/test/parallel/test-stdin-hang.js b/test/js/node/test/parallel/test-stdin-hang.js new file mode 100644 index 0000000000..887f31fdb7 --- /dev/null +++ b/test/js/node/test/parallel/test-stdin-hang.js @@ -0,0 +1,32 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test *only* verifies that invoking the stdin getter does not +// cause node to hang indefinitely. +// If it does, then the test-runner will nuke it. + +// invoke the getter. +process.stdin; // eslint-disable-line no-unused-expressions + +console.error('Should exit normally now.'); diff --git a/test/js/node/test/parallel/test-stdin-pause-resume.js b/test/js/node/test/parallel/test-stdin-pause-resume.js new file mode 100644 index 0000000000..459256099e --- /dev/null +++ b/test/js/node/test/parallel/test-stdin-pause-resume.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +console.error('before opening stdin'); +process.stdin.resume(); +console.error('stdin opened'); +setTimeout(function() { + console.error('pausing stdin'); + process.stdin.pause(); + setTimeout(function() { + console.error('opening again'); + process.stdin.resume(); + setTimeout(function() { + console.error('pausing again'); + process.stdin.pause(); + console.error('should exit now'); + }, 1); + }, 1); +}, 1); diff --git a/test/js/node/test/parallel/test-stdin-pipe-large.js b/test/js/node/test/parallel/test-stdin-pipe-large.js new file mode 100644 index 0000000000..5f4a2f10c8 --- /dev/null +++ b/test/js/node/test/parallel/test-stdin-pipe-large.js @@ -0,0 +1,23 @@ +'use strict'; +// See https://github.com/nodejs/node/issues/5927 + +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +if (process.argv[2] === 'child') { + process.stdin.pipe(process.stdout); + return; +} + +const child = spawn(process.execPath, [__filename, 'child'], { stdio: 'pipe' }); + +const expectedBytes = 1024 * 1024; +let readBytes = 0; + +child.stdin.end(Buffer.alloc(expectedBytes)); + +child.stdout.on('data', (chunk) => readBytes += chunk.length); +child.stdout.on('end', common.mustCall(() => { + assert.strictEqual(readBytes, expectedBytes); +})); diff --git a/test/js/node/test/parallel/test-stdin-pipe-resume.js b/test/js/node/test/parallel/test-stdin-pipe-resume.js new file mode 100644 index 0000000000..e9000933a3 --- /dev/null +++ b/test/js/node/test/parallel/test-stdin-pipe-resume.js @@ -0,0 +1,27 @@ +'use strict'; +// This tests that piping stdin will cause it to resume() as well. +require('../common'); +const assert = require('assert'); + +if (process.argv[2] === 'child') { + process.stdin.pipe(process.stdout); +} else { + const spawn = require('child_process').spawn; + const buffers = []; + const child = spawn(process.execPath, [__filename, 'child']); + child.stdout.on('data', function(c) { + buffers.push(c); + }); + child.stdout.on('close', function() { + const b = Buffer.concat(buffers).toString(); + assert.strictEqual(b, 'Hello, world\n'); + console.log('ok'); + }); + child.stdin.write('Hel'); + child.stdin.write('lo,'); + child.stdin.write(' wo'); + setTimeout(function() { + child.stdin.write('rld\n'); + child.stdin.end(); + }, 10); +} diff --git a/test/js/node/test/parallel/test-stdin-script-child-option.js b/test/js/node/test/parallel/test-stdin-script-child-option.js new file mode 100644 index 0000000000..5526d66f3f --- /dev/null +++ b/test/js/node/test/parallel/test-stdin-script-child-option.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const expected = '--option-to-be-seen-on-child'; + +const { spawn } = require('child_process'); +const child = spawn(process.execPath, ['-', expected], { stdio: 'pipe' }); + +child.stdin.end('console.log(process.argv[2])'); + +let actual = ''; +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (chunk) => actual += chunk); +child.stdout.on('end', common.mustCall(() => { + assert.strictEqual(actual.trim(), expected); +})); diff --git a/test/js/node/test/parallel/test-stdio-closed.js b/test/js/node/test/parallel/test-stdio-closed.js new file mode 100644 index 0000000000..cc9f1e86cc --- /dev/null +++ b/test/js/node/test/parallel/test-stdio-closed.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +if (common.isWindows) { + if (process.argv[2] === 'child') { + /* eslint-disable no-unused-expressions */ + process.stdin; + process.stdout; + process.stderr; + return; + /* eslint-enable no-unused-expressions */ + } + const python = process.env.PYTHON || 'python'; + const script = fixtures.path('spawn_closed_stdio.py'); + const proc = spawn(python, [script, process.execPath, __filename, 'child']); + proc.on('exit', common.mustCall(function(exitCode) { + assert.strictEqual(exitCode, 0); + })); + return; +} + +if (process.argv[2] === 'child') { + [0, 1, 2].forEach((i) => fs.fstatSync(i)); + return; +} + +// Run the script in a shell but close stdout and stderr. +const cmd = `"${process.execPath}" "${__filename}" child 1>&- 2>&-`; +const proc = spawn('/bin/sh', ['-c', cmd], { stdio: 'inherit' }); + +proc.on('exit', common.mustCall(function(exitCode) { + assert.strictEqual(exitCode, 0); +})); diff --git a/test/js/node/test/parallel/test-stdio-pipe-stderr.js b/test/js/node/test/parallel/test-stdio-pipe-stderr.js new file mode 100644 index 0000000000..c914877062 --- /dev/null +++ b/test/js/node/test/parallel/test-stdio-pipe-stderr.js @@ -0,0 +1,36 @@ +'use strict'; +require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const { spawnSync } = require('child_process'); + +// Test that invoking node with require, and piping stderr to file, +// does not result in exception, +// see: https://github.com/nodejs/node/issues/11257 + +tmpdir.refresh(); +const fakeModulePath = tmpdir.resolve('batman.js'); +const stderrOutputPath = tmpdir.resolve('stderr-output.txt'); +// We need to redirect stderr to a file to produce #11257 +const stream = fs.createWriteStream(stderrOutputPath); + +// The error described in #11257 only happens when we require a +// non-built-in module. +fs.writeFileSync(fakeModulePath, '', 'utf8'); + +stream.on('open', () => { + spawnSync(process.execPath, { + input: `require(${JSON.stringify(fakeModulePath)})`, + stdio: ['pipe', 'pipe', stream] + }); + const stderr = fs.readFileSync(stderrOutputPath, 'utf8').trim(); + assert.strictEqual( + stderr, + '', + `piping stderr to file should not result in exception: ${stderr}` + ); + stream.end(); + fs.unlinkSync(stderrOutputPath); + fs.unlinkSync(fakeModulePath); +}); diff --git a/test/js/node/test/parallel/test-stdio-undestroy.js b/test/js/node/test/parallel/test-stdio-undestroy.js new file mode 100644 index 0000000000..b525672db2 --- /dev/null +++ b/test/js/node/test/parallel/test-stdio-undestroy.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +if (process.argv[2] === 'child') { + process.stdout.destroy(); + process.stderr.destroy(); + console.log('stdout'); + process.stdout.write('rocks\n'); + console.error('stderr'); + setTimeout(function() { + process.stderr.write('rocks too\n'); + }, 10); + return; +} + +const proc = spawn(process.execPath, [__filename, 'child'], { stdio: 'pipe' }); + +let stdout = ''; +proc.stdout.setEncoding('utf8'); +proc.stdout.on('data', common.mustCallAtLeast(function(chunk) { + stdout += chunk; +}, 1)); + +let stderr = ''; +proc.stderr.setEncoding('utf8'); +proc.stderr.on('data', common.mustCallAtLeast(function(chunk) { + stderr += chunk; +}, 1)); + +proc.on('exit', common.mustCall(function(exitCode) { + assert.strictEqual(exitCode, 0); + assert.strictEqual(stdout, 'stdout\nrocks\n'); + assert.strictEqual(stderr, 'stderr\nrocks too\n'); +})); diff --git a/test/js/node/test/parallel/test-stdout-cannot-be-closed-child-process-pipe.js b/test/js/node/test/parallel/test-stdout-cannot-be-closed-child-process-pipe.js new file mode 100644 index 0000000000..7cd4b90c00 --- /dev/null +++ b/test/js/node/test/parallel/test-stdout-cannot-be-closed-child-process-pipe.js @@ -0,0 +1,32 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +if (process.argv[2] === 'child') + process.stdout.end('foo'); +else + parent(); + +function parent() { + const spawn = require('child_process').spawn; + const child = spawn(process.execPath, [__filename, 'child']); + let out = ''; + let err = ''; + + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + + child.stdout.on('data', function(c) { + out += c; + }); + child.stderr.on('data', function(c) { + err += c; + }); + + child.on('close', function(code, signal) { + assert.strictEqual(code, 0); + assert.strictEqual(err, ''); + assert.strictEqual(out, 'foo'); + console.log('ok'); + }); +} diff --git a/test/js/node/test/parallel/test-stdout-pipeline-destroy.js b/test/js/node/test/parallel/test-stdout-pipeline-destroy.js new file mode 100644 index 0000000000..291579cf69 --- /dev/null +++ b/test/js/node/test/parallel/test-stdout-pipeline-destroy.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const { Transform, Readable, pipeline } = require('stream'); +const assert = require('assert'); + +const reader = new Readable({ + read(size) { this.push('foo'); } +}); + +let count = 0; + +const err = new Error('this-error-gets-hidden'); + +const transform = new Transform({ + transform(chunk, enc, cb) { + if (count++ >= 5) + this.emit('error', err); + else + cb(null, count.toString() + '\n'); + } +}); + +pipeline( + reader, + transform, + process.stdout, + common.mustCall((e) => { + assert.strictEqual(e, err); + }) +); diff --git a/test/js/node/test/parallel/test-stdout-stderr-reading.js b/test/js/node/test/parallel/test-stdout-stderr-reading.js new file mode 100644 index 0000000000..57bfffa272 --- /dev/null +++ b/test/js/node/test/parallel/test-stdout-stderr-reading.js @@ -0,0 +1,67 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Verify that stdout is never read from. +const net = require('net'); +const read = net.Socket.prototype.read; + +net.Socket.prototype.read = function() { + if (this.fd === 1) + throw new Error('reading from stdout!'); + if (this.fd === 2) + throw new Error('reading from stderr!'); + return read.apply(this, arguments); +}; + +if (process.argv[2] === 'child') + child(); +else + parent(); + +function parent() { + const spawn = require('child_process').spawn; + const node = process.execPath; + + const c1 = spawn(node, [__filename, 'child']); + let c1out = ''; + c1.stdout.setEncoding('utf8'); + c1.stdout.on('data', function(chunk) { + c1out += chunk; + }); + c1.stderr.setEncoding('utf8'); + c1.stderr.on('data', function(chunk) { + console.error(`c1err: ${chunk.split('\n').join('\nc1err: ')}`); + }); + c1.on('close', common.mustCall(function(code, signal) { + assert(!code); + assert(!signal); + assert.strictEqual(c1out, 'ok\n'); + console.log('ok'); + })); + + const c2 = spawn(node, ['-e', 'console.log("ok")']); + let c2out = ''; + c2.stdout.setEncoding('utf8'); + c2.stdout.on('data', function(chunk) { + c2out += chunk; + }); + c1.stderr.setEncoding('utf8'); + c1.stderr.on('data', function(chunk) { + console.error(`c1err: ${chunk.split('\n').join('\nc1err: ')}`); + }); + c2.on('close', common.mustCall(function(code, signal) { + assert(!code); + assert(!signal); + assert.strictEqual(c2out, 'ok\n'); + console.log('ok'); + })); +} + +function child() { + // Should not be reading *ever* in here. + net.Socket.prototype.read = function() { + throw new Error('no reading allowed in child'); + }; + console.log('ok'); +} diff --git a/test/js/node/test/parallel/test-stdout-stderr-write.js b/test/js/node/test/parallel/test-stdout-stderr-write.js new file mode 100644 index 0000000000..803fc70536 --- /dev/null +++ b/test/js/node/test/parallel/test-stdout-stderr-write.js @@ -0,0 +1,8 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// https://github.com/nodejs/node/pull/39246 +assert.strictEqual(process.stderr.write('asd'), true); +assert.strictEqual(process.stdout.write('asd'), true); diff --git a/test/js/node/test/parallel/test-stdout-to-file.js b/test/js/node/test/parallel/test-stdout-to-file.js new file mode 100644 index 0000000000..9114f22443 --- /dev/null +++ b/test/js/node/test/parallel/test-stdout-to-file.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const childProcess = require('child_process'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +const scriptString = fixtures.path('print-chars.js'); +const scriptBuffer = fixtures.path('print-chars-from-buffer.js'); +const tmpFile = tmpdir.resolve('stdout.txt'); + +tmpdir.refresh(); + +function test(size, useBuffer, cb) { + const cmd = `"${process.argv[0]}" "${ + useBuffer ? scriptBuffer : scriptString}" ${size} > "${tmpFile}"`; + + try { + fs.unlinkSync(tmpFile); + } catch { + // Continue regardless of error. + } + + console.log(`${size} chars to ${tmpFile}...`); + + childProcess.exec(cmd, common.mustSucceed(() => { + console.log('done!'); + + const stat = fs.statSync(tmpFile); + + console.log(`${tmpFile} has ${stat.size} bytes`); + + assert.strictEqual(size, stat.size); + fs.unlinkSync(tmpFile); + + cb(); + })); +} + +test(1024 * 1024, false, common.mustCall(function() { + console.log('Done printing with string'); + test(1024 * 1024, true, common.mustCall(function() { + console.log('Done printing with buffer'); + })); +})); diff --git a/test/js/node/test/parallel/test-strace-openat-openssl.js b/test/js/node/test/parallel/test-strace-openat-openssl.js new file mode 100644 index 0000000000..13882e67ae --- /dev/null +++ b/test/js/node/test/parallel/test-strace-openat-openssl.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../common'); +const { spawn, spawnSync } = require('node:child_process'); +const { createInterface } = require('node:readline'); +const assert = require('node:assert'); + +if (!common.hasCrypto) + common.skip('missing crypto'); +if (!common.isLinux) + common.skip('linux only'); +if (common.isASan) + common.skip('strace does not work well with address sanitizer builds'); +if (spawnSync('strace').error !== undefined) { + common.skip('missing strace'); +} + +{ + const allowedOpenCalls = new Set([ + '/etc/ssl/openssl.cnf', + ]); + const strace = spawn('strace', [ + '-f', '-ff', + '-e', 'trace=open,openat', + '-s', '512', + '-D', process.execPath, '-e', 'require("crypto")', + ]); + + // stderr is the default for strace + const rl = createInterface({ input: strace.stderr }); + rl.on('line', (line) => { + if (!line.startsWith('open')) { + return; + } + + const file = line.match(/"(.*?)"/)[1]; + // skip .so reading attempt + if (file.match(/.+\.so(\.?)/) !== null) { + return; + } + // skip /proc/* + if (file.match(/\/proc\/.+/) !== null) { + return; + } + + assert(allowedOpenCalls.delete(file), `${file} is not in the list of allowed openat calls`); + }); + const debugOutput = []; + strace.stderr.setEncoding('utf8'); + strace.stderr.on('data', (chunk) => { + debugOutput.push(chunk.toString()); + }); + strace.on('error', common.mustNotCall()); + strace.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0, debugOutput); + const missingKeys = Array.from(allowedOpenCalls.keys()); + if (missingKeys.length) { + assert.fail(`The following openat call are missing: ${missingKeys.join(',')}`); + } + })); +} diff --git a/test/js/node/test/parallel/test-stream-aliases-legacy.js b/test/js/node/test/parallel/test-stream-aliases-legacy.js new file mode 100644 index 0000000000..2c87f0ad0f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-aliases-legacy.js @@ -0,0 +1,14 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const stream = require('stream'); + +// Verify that all individual aliases are left in place. + +assert.strictEqual(stream.Readable, require('_stream_readable')); +assert.strictEqual(stream.Writable, require('_stream_writable')); +assert.strictEqual(stream.Duplex, require('_stream_duplex')); +assert.strictEqual(stream.Transform, require('_stream_transform')); +assert.strictEqual(stream.PassThrough, require('_stream_passthrough')); diff --git a/test/js/node/test/parallel/test-stream-auto-destroy.js b/test/js/node/test/parallel/test-stream-auto-destroy.js new file mode 100644 index 0000000000..2a1a5190de --- /dev/null +++ b/test/js/node/test/parallel/test-stream-auto-destroy.js @@ -0,0 +1,112 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +{ + const r = new stream.Readable({ + autoDestroy: true, + read() { + this.push('hello'); + this.push('world'); + this.push(null); + }, + destroy: common.mustCall((err, cb) => cb()) + }); + + let ended = false; + + r.resume(); + + r.on('end', common.mustCall(() => { + ended = true; + })); + + r.on('close', common.mustCall(() => { + assert(ended); + })); +} + +{ + const w = new stream.Writable({ + autoDestroy: true, + write(data, enc, cb) { + cb(null); + }, + destroy: common.mustCall((err, cb) => cb()) + }); + + let finished = false; + + w.write('hello'); + w.write('world'); + w.end(); + + w.on('finish', common.mustCall(() => { + finished = true; + })); + + w.on('close', common.mustCall(() => { + assert(finished); + })); +} + +{ + const t = new stream.Transform({ + autoDestroy: true, + transform(data, enc, cb) { + cb(null, data); + }, + destroy: common.mustCall((err, cb) => cb()) + }); + + let ended = false; + let finished = false; + + t.write('hello'); + t.write('world'); + t.end(); + + t.resume(); + + t.on('end', common.mustCall(() => { + ended = true; + })); + + t.on('finish', common.mustCall(() => { + finished = true; + })); + + t.on('close', common.mustCall(() => { + assert(ended); + assert(finished); + })); +} + +{ + const r = new stream.Readable({ + read() { + r2.emit('error', new Error('fail')); + } + }); + const r2 = new stream.Readable({ + autoDestroy: true, + destroy: common.mustCall((err, cb) => cb()) + }); + + r.pipe(r2); +} + +{ + const r = new stream.Readable({ + read() { + w.emit('error', new Error('fail')); + } + }); + const w = new stream.Writable({ + autoDestroy: true, + destroy: common.mustCall((err, cb) => cb()) + }); + + r.pipe(w); +} diff --git a/test/js/node/test/parallel/test-stream-await-drain-writers-in-synchronously-recursion-write.js b/test/js/node/test/parallel/test-stream-await-drain-writers-in-synchronously-recursion-write.js new file mode 100644 index 0000000000..110d46bb9f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-await-drain-writers-in-synchronously-recursion-write.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const { PassThrough } = require('stream'); + +const encode = new PassThrough({ + highWaterMark: 1 +}); + +const decode = new PassThrough({ + highWaterMark: 1 +}); + +const send = common.mustCall((buf) => { + encode.write(buf); +}, 4); + +let i = 0; +const onData = common.mustCall(() => { + if (++i === 2) { + send(Buffer.from([0x3])); + send(Buffer.from([0x4])); + } +}, 4); + +encode.pipe(decode).on('data', onData); + +send(Buffer.from([0x1])); +send(Buffer.from([0x2])); diff --git a/test/js/node/test/parallel/test-stream-backpressure.js b/test/js/node/test/parallel/test-stream-backpressure.js new file mode 100644 index 0000000000..03bcc233c8 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-backpressure.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +let pushes = 0; +const total = 65500 + 40 * 1024; +const rs = new stream.Readable({ + read: common.mustCall(function() { + if (pushes++ === 10) { + this.push(null); + return; + } + + const length = this._readableState.length; + + // We are at most doing two full runs of _reads + // before stopping, because Readable is greedy + // to keep its buffer full + assert(length <= total); + + this.push(Buffer.alloc(65500)); + for (let i = 0; i < 40; i++) { + this.push(Buffer.alloc(1024)); + } + + // We will be over highWaterMark at this point + // but a new call to _read is scheduled anyway. + }, 11) +}); + +const ws = stream.Writable({ + write: common.mustCall(function(data, enc, cb) { + setImmediate(cb); + }, 41 * 10) +}); + +rs.pipe(ws); diff --git a/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js b/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js new file mode 100644 index 0000000000..7436428a30 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js @@ -0,0 +1,19 @@ +'use strict'; + +require('../common'); + +// This tests that the prototype accessors added by StreamBase::AddMethods +// are not enumerable. They could be enumerated when inspecting the prototype +// with util.inspect or the inspector protocol. + +const assert = require('assert'); + +// Or anything that calls StreamBase::AddMethods when setting up its prototype +const TTY = process.binding('tty_wrap').TTY; + +{ + const ttyIsEnumerable = Object.prototype.propertyIsEnumerable.bind(TTY); + assert.strictEqual(ttyIsEnumerable('bytesRead'), false); + assert.strictEqual(ttyIsEnumerable('fd'), false); + assert.strictEqual(ttyIsEnumerable('_externalStream'), false); +} diff --git a/test/js/node/test/parallel/test-stream-big-packet.js b/test/js/node/test/parallel/test-stream-big-packet.js new file mode 100644 index 0000000000..fdbe3cd211 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-big-packet.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +let passed = false; + +class TestStream extends stream.Transform { + _transform(chunk, encoding, done) { + if (!passed) { + // Char 'a' only exists in the last write + passed = chunk.toString().includes('a'); + } + done(); + } +} + +const s1 = new stream.Transform({ + transform(chunk, encoding, cb) { + process.nextTick(cb, null, chunk); + } +}); +const s2 = new stream.PassThrough(); +const s3 = new TestStream(); +s1.pipe(s3); +// Don't let s2 auto close which may close s3 +s2.pipe(s3, { end: false }); + +// We must write a buffer larger than highWaterMark +const big = Buffer.alloc(s1.writableHighWaterMark + 1, 'x'); + +// Since big is larger than highWaterMark, it will be buffered internally. +assert(!s1.write(big)); +// 'tiny' is small enough to pass through internal buffer. +assert(s2.write('tiny')); + +// Write some small data in next IO loop, which will never be written to s3 +// Because 'drain' event is not emitted from s1 and s1 is still paused +setImmediate(s1.write.bind(s1), 'later'); + +// Assert after two IO loops when all operations have been done. +process.on('exit', function() { + assert(passed, 'Large buffer is not handled properly by Writable Stream'); +}); diff --git a/test/js/node/test/parallel/test-stream-big-push.js b/test/js/node/test/parallel/test-stream-big-push.js new file mode 100644 index 0000000000..f9e75edd3f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-big-push.js @@ -0,0 +1,74 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); +const str = 'asdfasdfasdfasdfasdf'; + +const r = new stream.Readable({ + highWaterMark: 5, + encoding: 'utf8' +}); + +let reads = 0; + +function _read() { + if (reads === 0) { + setTimeout(() => { + r.push(str); + }, 1); + reads++; + } else if (reads === 1) { + const ret = r.push(str); + assert.strictEqual(ret, false); + reads++; + } else { + r.push(null); + } +} + +r._read = common.mustCall(_read, 3); + +r.on('end', common.mustCall()); + +// Push some data in to start. +// We've never gotten any read event at this point. +const ret = r.push(str); +// Should be false. > hwm +assert(!ret); +let chunk = r.read(); +assert.strictEqual(chunk, str); +chunk = r.read(); +assert.strictEqual(chunk, null); + +r.once('readable', () => { + // This time, we'll get *all* the remaining data, because + // it's been added synchronously, as the read WOULD take + // us below the hwm, and so it triggered a _read() again, + // which synchronously added more, which we then return. + chunk = r.read(); + assert.strictEqual(chunk, str + str); + + chunk = r.read(); + assert.strictEqual(chunk, null); +}); diff --git a/test/js/node/test/parallel/test-stream-catch-rejections.js b/test/js/node/test/parallel/test-stream-catch-rejections.js new file mode 100644 index 0000000000..81427c3575 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-catch-rejections.js @@ -0,0 +1,51 @@ +'use strict'; + +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +{ + const r = new stream.Readable({ + captureRejections: true, + read() { + } + }); + r.push('hello'); + r.push('world'); + + const err = new Error('kaboom'); + + r.on('error', common.mustCall((_err) => { + assert.strictEqual(err, _err); + assert.strictEqual(r.destroyed, true); + })); + + r.on('data', async () => { + throw err; + }); +} + +{ + const w = new stream.Writable({ + captureRejections: true, + highWaterMark: 1, + write(chunk, enc, cb) { + process.nextTick(cb); + } + }); + + const err = new Error('kaboom'); + + w.write('hello', () => { + w.write('world'); + }); + + w.on('error', common.mustCall((_err) => { + assert.strictEqual(err, _err); + assert.strictEqual(w.destroyed, true); + })); + + w.on('drain', common.mustCall(async () => { + throw err; + }, 2)); +} diff --git a/test/js/node/test/parallel/test-stream-compose-operator.js b/test/js/node/test/parallel/test-stream-compose-operator.js new file mode 100644 index 0000000000..4fefb004f5 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-compose-operator.js @@ -0,0 +1,127 @@ +'use strict'; + +const common = require('../common'); +const { + Readable, Transform, +} = require('stream'); +const assert = require('assert'); + +{ + // with async generator + const stream = Readable.from(['a', 'b', 'c', 'd']).compose(async function *(stream) { + let str = ''; + for await (const chunk of stream) { + str += chunk; + + if (str.length === 2) { + yield str; + str = ''; + } + } + }); + const result = ['ab', 'cd']; + (async () => { + for await (const item of stream) { + assert.strictEqual(item, result.shift()); + } + })().then(common.mustCall()); +} + +{ + // With Transformer + const stream = Readable.from(['a', 'b', 'c', 'd']).compose(new Transform({ + objectMode: true, + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk); + }, 4) + })); + const result = ['a', 'b', 'c', 'd']; + (async () => { + for await (const item of stream) { + assert.strictEqual(item, result.shift()); + } + })().then(common.mustCall()); +} + +{ + // Throwing an error during `compose` (before waiting for data) + const stream = Readable.from([1, 2, 3, 4, 5]).compose(async function *(stream) { // eslint-disable-line require-yield + + throw new Error('boom'); + }); + + assert.rejects(async () => { + for await (const item of stream) { + assert.fail('should not reach here, got ' + item); + } + }, /boom/).then(common.mustCall()); +} + +{ + // Throwing an error during `compose` (when waiting for data) + const stream = Readable.from([1, 2, 3, 4, 5]).compose(async function *(stream) { + for await (const chunk of stream) { + if (chunk === 3) { + throw new Error('boom'); + } + yield chunk; + } + }); + + assert.rejects( + stream.toArray(), + /boom/, + ).then(common.mustCall()); +} + +{ + // Throwing an error during `compose` (after finishing all readable data) + const stream = Readable.from([1, 2, 3, 4, 5]).compose(async function *(stream) { // eslint-disable-line require-yield + + // eslint-disable-next-line no-unused-vars,no-empty + for await (const chunk of stream) { + } + + throw new Error('boom'); + }); + assert.rejects( + stream.toArray(), + /boom/, + ).then(common.mustCall()); +} + +{ + // AbortSignal + const ac = new AbortController(); + const stream = Readable.from([1, 2, 3, 4, 5]) + .compose(async function *(source) { + // Should not reach here + for await (const chunk of source) { + yield chunk; + } + }, { signal: ac.signal }); + + ac.abort(); + + assert.rejects(async () => { + for await (const item of stream) { + assert.fail('should not reach here, got ' + item); + } + }, { + name: 'AbortError', + }).then(common.mustCall()); +} + +{ + assert.throws( + () => Readable.from(['a']).compose(Readable.from(['b'])), + { code: 'ERR_INVALID_ARG_VALUE' } + ); +} + +{ + assert.throws( + () => Readable.from(['a']).compose(), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +} diff --git a/test/js/node/test/parallel/test-stream-compose.js b/test/js/node/test/parallel/test-stream-compose.js new file mode 100644 index 0000000000..d7a54e1776 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-compose.js @@ -0,0 +1,539 @@ +'use strict'; + +const common = require('../common'); +const { + Duplex, + Readable, + Transform, + Writable, + finished, + compose, + PassThrough +} = require('stream'); +const assert = require('assert'); + +{ + let res = ''; + compose( + new Transform({ + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk + chunk); + }) + }), + new Transform({ + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk.toString().toUpperCase()); + }) + }) + ) + .end('asd') + .on('data', common.mustCall((buf) => { + res += buf; + })) + .on('end', common.mustCall(() => { + assert.strictEqual(res, 'ASDASD'); + })); +} + +{ + let res = ''; + compose( + async function*(source) { + for await (const chunk of source) { + yield chunk + chunk; + } + }, + async function*(source) { + for await (const chunk of source) { + yield chunk.toString().toUpperCase(); + } + } + ) + .end('asd') + .on('data', common.mustCall((buf) => { + res += buf; + })) + .on('end', common.mustCall(() => { + assert.strictEqual(res, 'ASDASD'); + })); +} + +{ + let res = ''; + compose( + async function*(source) { + for await (const chunk of source) { + yield chunk + chunk; + } + } + ) + .end('asd') + .on('data', common.mustCall((buf) => { + res += buf; + })) + .on('end', common.mustCall(() => { + assert.strictEqual(res, 'asdasd'); + })); +} + +{ + let res = ''; + compose( + Readable.from(['asd']), + new Transform({ + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk.toString().toUpperCase()); + }) + }) + ) + .on('data', common.mustCall((buf) => { + res += buf; + })) + .on('end', common.mustCall(() => { + assert.strictEqual(res, 'ASD'); + })); +} + +{ + let res = ''; + compose( + async function* () { + yield 'asd'; + }(), + new Transform({ + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk.toString().toUpperCase()); + }) + }) + ) + .on('data', common.mustCall((buf) => { + res += buf; + })) + .on('end', common.mustCall(() => { + assert.strictEqual(res, 'ASD'); + })); +} + +{ + let res = ''; + compose( + new Transform({ + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk.toString().toUpperCase()); + }) + }), + async function*(source) { + for await (const chunk of source) { + yield chunk; + } + }, + new Writable({ + write: common.mustCall((chunk, encoding, callback) => { + res += chunk; + callback(null); + }) + }) + ) + .end('asd') + .on('finish', common.mustCall(() => { + assert.strictEqual(res, 'ASD'); + })); +} + +{ + let res = ''; + compose( + new Transform({ + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk.toString().toUpperCase()); + }) + }), + async function*(source) { + for await (const chunk of source) { + yield chunk; + } + }, + async function(source) { + for await (const chunk of source) { + res += chunk; + } + } + ) + .end('asd') + .on('finish', common.mustCall(() => { + assert.strictEqual(res, 'ASD'); + })); +} + +{ + let res; + compose( + new Transform({ + objectMode: true, + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, { chunk }); + }) + }), + async function*(source) { + for await (const chunk of source) { + yield chunk; + } + }, + new Transform({ + objectMode: true, + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, { chunk }); + }) + }) + ) + .end(true) + .on('data', common.mustCall((buf) => { + res = buf; + })) + .on('end', common.mustCall(() => { + assert.strictEqual(res.chunk.chunk, true); + })); +} + +{ + const _err = new Error('asd'); + compose( + new Transform({ + objectMode: true, + transform: common.mustCall((chunk, encoding, callback) => { + callback(_err); + }) + }), + async function*(source) { + for await (const chunk of source) { + yield chunk; + } + }, + new Transform({ + objectMode: true, + transform: common.mustNotCall((chunk, encoding, callback) => { + callback(null, { chunk }); + }) + }) + ) + .end(true) + .on('data', common.mustNotCall()) + .on('end', common.mustNotCall()) + .on('error', (err) => { + assert.strictEqual(err, _err); + }); +} + +{ + const _err = new Error('asd'); + compose( + new Transform({ + objectMode: true, + transform: common.mustCall((chunk, encoding, callback) => { + callback(null, chunk); + }) + }), + async function*(source) { // eslint-disable-line require-yield + let tmp = ''; + for await (const chunk of source) { + tmp += chunk; + throw _err; + } + return tmp; + }, + new Transform({ + objectMode: true, + transform: common.mustNotCall((chunk, encoding, callback) => { + callback(null, { chunk }); + }) + }) + ) + .end(true) + .on('data', common.mustNotCall()) + .on('end', common.mustNotCall()) + .on('error', (err) => { + assert.strictEqual(err, _err); + }); +} + +{ + let buf = ''; + + // Convert into readable Duplex. + const s1 = compose(async function* () { + yield 'Hello'; + yield 'World'; + }(), async function* (source) { + for await (const chunk of source) { + yield String(chunk).toUpperCase(); + } + }, async function(source) { + for await (const chunk of source) { + buf += chunk; + } + }); + + assert.strictEqual(s1.writable, false); + assert.strictEqual(s1.readable, false); + + finished(s1.resume(), common.mustCall((err) => { + assert(!err); + assert.strictEqual(buf, 'HELLOWORLD'); + })); +} + +{ + let buf = ''; + // Convert into transform duplex. + const s2 = compose(async function* (source) { + for await (const chunk of source) { + yield String(chunk).toUpperCase(); + } + }); + s2.end('helloworld'); + s2.resume(); + s2.on('data', (chunk) => { + buf += chunk; + }); + + finished(s2.resume(), common.mustCall((err) => { + assert(!err); + assert.strictEqual(buf, 'HELLOWORLD'); + })); +} + +{ + let buf = ''; + + // Convert into readable Duplex. + const s1 = compose(async function* () { + yield 'Hello'; + yield 'World'; + }()); + + // Convert into transform duplex. + const s2 = compose(async function* (source) { + for await (const chunk of source) { + yield String(chunk).toUpperCase(); + } + }); + + // Convert into writable duplex. + const s3 = compose(async function(source) { + for await (const chunk of source) { + buf += chunk; + } + }); + + const s4 = compose(s1, s2, s3); + + finished(s4, common.mustCall((err) => { + assert(!err); + assert.strictEqual(buf, 'HELLOWORLD'); + })); +} + +{ + let buf = ''; + + // Convert into readable Duplex. + const s1 = compose(async function* () { + yield 'Hello'; + yield 'World'; + }(), async function* (source) { + for await (const chunk of source) { + yield String(chunk).toUpperCase(); + } + }, async function(source) { + for await (const chunk of source) { + buf += chunk; + } + }); + + finished(s1, common.mustCall((err) => { + assert(!err); + assert.strictEqual(buf, 'HELLOWORLD'); + })); +} + +{ + assert.throws( + () => compose(), + { code: 'ERR_MISSING_ARGS' } + ); +} + +{ + assert.throws( + () => compose(new Writable(), new PassThrough()), + { code: 'ERR_INVALID_ARG_VALUE' } + ); +} + +{ + assert.throws( + () => compose(new PassThrough(), new Readable({ read() {} }), new PassThrough()), + { code: 'ERR_INVALID_ARG_VALUE' } + ); +} + +{ + let buf = ''; + + // Convert into readable Duplex. + const s1 = compose(async function* () { + yield 'Hello'; + yield 'World'; + }(), async function* (source) { + for await (const chunk of source) { + yield String(chunk).toUpperCase(); + } + }, async function(source) { + for await (const chunk of source) { + buf += chunk; + } + return buf; + }); + + finished(s1, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_INVALID_RETURN_VALUE'); + })); +} + +{ + let buf = ''; + + // Convert into readable Duplex. + const s1 = compose('HelloWorld', async function* (source) { + for await (const chunk of source) { + yield String(chunk).toUpperCase(); + } + }, async function(source) { + for await (const chunk of source) { + buf += chunk; + } + }); + + finished(s1, common.mustCall((err) => { + assert(!err); + assert.strictEqual(buf, 'HELLOWORLD'); + })); +} + +{ + // In the new stream than should use the writeable of the first stream and readable of the last stream + // #46829 + (async () => { + const newStream = compose( + new PassThrough({ + // reading FROM you in object mode or not + readableObjectMode: false, + + // writing TO you in object mode or not + writableObjectMode: false, + }), + new Transform({ + // reading FROM you in object mode or not + readableObjectMode: true, + + // writing TO you in object mode or not + writableObjectMode: false, + transform: (chunk, encoding, callback) => { + callback(null, { + value: chunk.toString() + }); + } + }) + ); + + assert.strictEqual(newStream.writableObjectMode, false); + assert.strictEqual(newStream.readableObjectMode, true); + + newStream.write('Steve Rogers'); + newStream.write('On your left'); + + newStream.end(); + + assert.deepStrictEqual(await newStream.toArray(), [{ value: 'Steve Rogers' }, { value: 'On your left' }]); + })().then(common.mustCall()); +} + +{ + // In the new stream than should use the writeable of the first stream and readable of the last stream + // #46829 + (async () => { + const newStream = compose( + new PassThrough({ + // reading FROM you in object mode or not + readableObjectMode: true, + + // writing TO you in object mode or not + writableObjectMode: true, + }), + new Transform({ + // reading FROM you in object mode or not + readableObjectMode: false, + + // writing TO you in object mode or not + writableObjectMode: true, + transform: (chunk, encoding, callback) => { + callback(null, chunk.value); + } + }) + ); + + assert.strictEqual(newStream.writableObjectMode, true); + assert.strictEqual(newStream.readableObjectMode, false); + + newStream.write({ value: 'Steve Rogers' }); + newStream.write({ value: 'On your left' }); + + newStream.end(); + + assert.deepStrictEqual(await newStream.toArray(), [Buffer.from('Steve RogersOn your left')]); + })().then(common.mustCall()); +} + +{ + class DuplexProcess extends Duplex { + constructor(options) { + super({ ...options, objectMode: true }); + this.stuff = []; + } + + _write(message, _, callback) { + this.stuff.push(message); + callback(); + } + + _destroy(err, cb) { + cb(err); + } + + _read() { + if (this.stuff.length) { + this.push(this.stuff.shift()); + } else if (this.writableEnded) { + this.push(null); + } else { + this._read(); + } + } + } + + const pass = new PassThrough({ objectMode: true }); + const duplex = new DuplexProcess(); + + const composed = compose( + pass, + duplex + ).on('error', () => {}); + + composed.write('hello'); + composed.write('world'); + composed.end(); + + composed.destroy(new Error('an unexpected error')); + assert.strictEqual(duplex.destroyed, true); + +} diff --git a/test/js/node/test/parallel/test-stream-construct.js b/test/js/node/test/parallel/test-stream-construct.js new file mode 100644 index 0000000000..907b9aa0e3 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-construct.js @@ -0,0 +1,280 @@ +'use strict'; + +const common = require('../common'); +const { Writable, Readable, Duplex } = require('stream'); +const assert = require('assert'); + +{ + // Multiple callback. + new Writable({ + construct: common.mustCall((callback) => { + callback(); + callback(); + }) + }).on('error', common.expectsError({ + name: 'Error', + code: 'ERR_MULTIPLE_CALLBACK' + })); +} + +{ + // Multiple callback. + new Readable({ + construct: common.mustCall((callback) => { + callback(); + callback(); + }) + }).on('error', common.expectsError({ + name: 'Error', + code: 'ERR_MULTIPLE_CALLBACK' + })); +} + +{ + // Synchronous error. + + new Writable({ + construct: common.mustCall((callback) => { + callback(new Error('test')); + }) + }).on('error', common.expectsError({ + name: 'Error', + message: 'test' + })); +} + +{ + // Synchronous error. + + new Readable({ + construct: common.mustCall((callback) => { + callback(new Error('test')); + }) + }).on('error', common.expectsError({ + name: 'Error', + message: 'test' + })); +} + +{ + // Asynchronous error. + + new Writable({ + construct: common.mustCall((callback) => { + process.nextTick(callback, new Error('test')); + }) + }).on('error', common.expectsError({ + name: 'Error', + message: 'test' + })); +} + +{ + // Asynchronous error. + + new Readable({ + construct: common.mustCall((callback) => { + process.nextTick(callback, new Error('test')); + }) + }).on('error', common.expectsError({ + name: 'Error', + message: 'test' + })); +} + +function testDestroy(factory) { + { + let constructed = false; + const s = factory({ + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }) + }); + s.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + s.destroy(); + } + + { + let constructed = false; + const s = factory({ + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }) + }); + s.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + s.destroy(null, () => { + assert.strictEqual(constructed, true); + }); + } + + { + let constructed = false; + const s = factory({ + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }) + }); + s.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + s.destroy(); + } + + + { + let constructed = false; + const s = factory({ + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }) + }); + s.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + s.on('error', common.mustCall((err) => { + assert.strictEqual(err.message, 'kaboom'); + })); + s.destroy(new Error('kaboom'), (err) => { + assert.strictEqual(err.message, 'kaboom'); + assert.strictEqual(constructed, true); + }); + } + + { + let constructed = false; + const s = factory({ + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }) + }); + s.on('error', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + s.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + s.destroy(new Error()); + } +} +testDestroy((opts) => new Readable({ + read: common.mustNotCall(), + ...opts +})); +testDestroy((opts) => new Writable({ + write: common.mustNotCall(), + final: common.mustNotCall(), + ...opts +})); + +{ + let constructed = false; + const r = new Readable({ + autoDestroy: true, + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }), + read: common.mustCall(() => { + assert.strictEqual(constructed, true); + r.push(null); + }) + }); + r.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + r.on('data', common.mustNotCall()); +} + +{ + let constructed = false; + const w = new Writable({ + autoDestroy: true, + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }), + write: common.mustCall((chunk, encoding, cb) => { + assert.strictEqual(constructed, true); + process.nextTick(cb); + }), + final: common.mustCall((cb) => { + assert.strictEqual(constructed, true); + process.nextTick(cb); + }) + }); + w.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + w.end('data'); +} + +{ + let constructed = false; + const w = new Writable({ + autoDestroy: true, + construct: common.mustCall((cb) => { + constructed = true; + process.nextTick(cb); + }), + write: common.mustNotCall(), + final: common.mustCall((cb) => { + assert.strictEqual(constructed, true); + process.nextTick(cb); + }) + }); + w.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); + w.end(); +} + +{ + new Duplex({ + construct: common.mustCall() + }); +} + +{ + // https://github.com/nodejs/node/issues/34448 + + let constructed = false; + const d = new Duplex({ + readable: false, + construct: common.mustCall((callback) => { + setImmediate(common.mustCall(() => { + constructed = true; + callback(); + })); + }), + write(chunk, encoding, callback) { + callback(); + }, + read() { + this.push(null); + } + }); + d.resume(); + d.end('foo'); + d.on('close', common.mustCall(() => { + assert.strictEqual(constructed, true); + })); +} + +{ + // Construct should not cause stream to read. + new Readable({ + construct: common.mustCall((callback) => { + callback(); + }), + read: common.mustNotCall() + }); +} diff --git a/test/js/node/test/parallel/test-stream-consumers.js b/test/js/node/test/parallel/test-stream-consumers.js new file mode 100644 index 0000000000..883d55dc6f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-consumers.js @@ -0,0 +1,262 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const { + arrayBuffer, + blob, + buffer, + text, + json, +} = require('stream/consumers'); + +const { + Readable, + PassThrough +} = require('stream'); + +const { + TransformStream, +} = require('stream/web'); + +const buf = Buffer.from('hellothere'); +const kArrayBuffer = + buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + +{ + const passthrough = new PassThrough(); + + blob(passthrough).then(common.mustCall(async (blob) => { + assert.strictEqual(blob.size, 10); + assert.deepStrictEqual(await blob.arrayBuffer(), kArrayBuffer); + })); + + passthrough.write('hello'); + setTimeout(() => passthrough.end('there'), 10); +} + +{ + const passthrough = new PassThrough(); + + arrayBuffer(passthrough).then(common.mustCall(async (ab) => { + assert.strictEqual(ab.byteLength, 10); + assert.deepStrictEqual(ab, kArrayBuffer); + })); + + passthrough.write('hello'); + setTimeout(() => passthrough.end('there'), 10); +} + +{ + const passthrough = new PassThrough(); + + buffer(passthrough).then(common.mustCall(async (buf) => { + assert.strictEqual(buf.byteLength, 10); + assert.deepStrictEqual(buf.buffer, kArrayBuffer); + })); + + passthrough.write('hello'); + setTimeout(() => passthrough.end('there'), 10); +} + + +{ + const passthrough = new PassThrough(); + + text(passthrough).then(common.mustCall(async (str) => { + assert.strictEqual(str.length, 10); + assert.strictEqual(str, 'hellothere'); + })); + + passthrough.write('hello'); + setTimeout(() => passthrough.end('there'), 10); +} + +{ + const readable = new Readable({ + read() {} + }); + + text(readable).then((data) => { + assert.strictEqual(data, 'foo\ufffd\ufffd\ufffd'); + }); + + readable.push(new Uint8Array([0x66, 0x6f, 0x6f, 0xed, 0xa0, 0x80])); + readable.push(null); +} + +{ + const passthrough = new PassThrough(); + + json(passthrough).then(common.mustCall(async (str) => { + assert.strictEqual(str.length, 10); + assert.strictEqual(str, 'hellothere'); + })); + + passthrough.write('"hello'); + setTimeout(() => passthrough.end('there"'), 10); +} + +{ + const { writable, readable } = new TransformStream(); + + blob(readable).then(common.mustCall(async (blob) => { + assert.strictEqual(blob.size, 10); + assert.deepStrictEqual(await blob.arrayBuffer(), kArrayBuffer); + })); + + const writer = writable.getWriter(); + writer.write('hello'); + setTimeout(() => { + writer.write('there'); + writer.close(); + }, 10); + + assert.rejects(blob(readable), { code: 'ERR_INVALID_STATE' }).then(common.mustCall()); +} + +{ + const { writable, readable } = new TransformStream(); + + arrayBuffer(readable).then(common.mustCall(async (ab) => { + assert.strictEqual(ab.byteLength, 10); + assert.deepStrictEqual(ab, kArrayBuffer); + })); + + const writer = writable.getWriter(); + writer.write('hello'); + setTimeout(() => { + writer.write('there'); + writer.close(); + }, 10); + + assert.rejects(arrayBuffer(readable), { code: 'ERR_INVALID_STATE' }).then(common.mustCall()); +} + +{ + const { writable, readable } = new TransformStream(); + + text(readable).then(common.mustCall(async (str) => { + assert.strictEqual(str.length, 10); + assert.strictEqual(str, 'hellothere'); + })); + + const writer = writable.getWriter(); + writer.write('hello'); + setTimeout(() => { + writer.write('there'); + writer.close(); + }, 10); + + assert.rejects(text(readable), { code: 'ERR_INVALID_STATE' }).then(common.mustCall()); +} + +{ + const { writable, readable } = new TransformStream(); + + json(readable).then(common.mustCall(async (str) => { + assert.strictEqual(str.length, 10); + assert.strictEqual(str, 'hellothere'); + })); + + const writer = writable.getWriter(); + writer.write('"hello'); + setTimeout(() => { + writer.write('there"'); + writer.close(); + }, 10); + + assert.rejects(json(readable), { code: 'ERR_INVALID_STATE' }).then(common.mustCall()); +} + +{ + const stream = new PassThrough({ + readableObjectMode: true, + writableObjectMode: true, + }); + + blob(stream).then(common.mustCall((blob) => { + assert.strictEqual(blob.size, 30); + })); + + stream.write({}); + stream.end({}); +} + +{ + const stream = new PassThrough({ + readableObjectMode: true, + writableObjectMode: true, + }); + + arrayBuffer(stream).then(common.mustCall((ab) => { + assert.strictEqual(ab.byteLength, 30); + assert.strictEqual( + Buffer.from(ab).toString(), + '[object Object][object Object]'); + })); + + stream.write({}); + stream.end({}); +} + +{ + const stream = new PassThrough({ + readableObjectMode: true, + writableObjectMode: true, + }); + + buffer(stream).then(common.mustCall((buf) => { + assert.strictEqual(buf.byteLength, 30); + assert.strictEqual( + buf.toString(), + '[object Object][object Object]'); + })); + + stream.write({}); + stream.end({}); +} + +{ + const stream = new PassThrough({ + readableObjectMode: true, + writableObjectMode: true, + }); + + assert.rejects(text(stream), { + code: 'ERR_INVALID_ARG_TYPE', + }).then(common.mustCall()); + + stream.write({}); + stream.end({}); +} + +{ + const stream = new PassThrough({ + readableObjectMode: true, + writableObjectMode: true, + }); + + assert.rejects(json(stream), { + code: 'ERR_INVALID_ARG_TYPE', + }).then(common.mustCall()); + + stream.write({}); + stream.end({}); +} + +{ + const stream = new TransformStream(); + text(stream.readable).then(common.mustCall((str) => { + // Incomplete utf8 character is flushed as a replacement char + assert.strictEqual(str.charCodeAt(0), 0xfffd); + })); + const writer = stream.writable.getWriter(); + Promise.all([ + writer.write(new Uint8Array([0xe2])), + writer.write(new Uint8Array([0x82])), + writer.close(), + ]).then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-decoder-objectmode.js b/test/js/node/test/parallel/test-stream-decoder-objectmode.js new file mode 100644 index 0000000000..4c572fed6b --- /dev/null +++ b/test/js/node/test/parallel/test-stream-decoder-objectmode.js @@ -0,0 +1,20 @@ +'use strict'; + +require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +const readable = new stream.Readable({ + read: () => {}, + encoding: 'utf16le', + objectMode: true +}); + +readable.push(Buffer.from('abc', 'utf16le')); +readable.push(Buffer.from('def', 'utf16le')); +readable.push(null); + +// Without object mode, these would be concatenated into a single chunk. +assert.strictEqual(readable.read(), 'abc'); +assert.strictEqual(readable.read(), 'def'); +assert.strictEqual(readable.read(), null); diff --git a/test/js/node/test/parallel/test-stream-destroy-event-order.js b/test/js/node/test/parallel/test-stream-destroy-event-order.js new file mode 100644 index 0000000000..a88fff820d --- /dev/null +++ b/test/js/node/test/parallel/test-stream-destroy-event-order.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +const rs = new Readable({ + read() {} +}); + +let closed = false; +let errored = false; + +rs.on('close', common.mustCall(() => { + closed = true; + assert(errored); +})); + +rs.on('error', common.mustCall((err) => { + errored = true; + assert(!closed); +})); + +rs.destroy(new Error('kaboom')); diff --git a/test/js/node/test/parallel/test-stream-drop-take.js b/test/js/node/test/parallel/test-stream-drop-take.js new file mode 100644 index 0000000000..97e6c74dfa --- /dev/null +++ b/test/js/node/test/parallel/test-stream-drop-take.js @@ -0,0 +1,124 @@ +'use strict'; + +const common = require('../common'); +const { + Readable, +} = require('stream'); +const { deepStrictEqual, rejects, throws, strictEqual } = require('assert'); + +const { from } = Readable; + +const fromAsync = (...args) => from(...args).map(async (x) => x); + +const naturals = () => from(async function*() { + let i = 1; + while (true) { + yield i++; + } +}()); + +{ + // Synchronous streams + (async () => { + deepStrictEqual(await from([1, 2, 3]).drop(2).toArray(), [3]); + deepStrictEqual(await from([1, 2, 3]).take(1).toArray(), [1]); + deepStrictEqual(await from([]).drop(2).toArray(), []); + deepStrictEqual(await from([]).take(1).toArray(), []); + deepStrictEqual(await from([1, 2, 3]).drop(1).take(1).toArray(), [2]); + deepStrictEqual(await from([1, 2]).drop(0).toArray(), [1, 2]); + deepStrictEqual(await from([1, 2]).take(0).toArray(), []); + })().then(common.mustCall()); + // Asynchronous streams + (async () => { + deepStrictEqual(await fromAsync([1, 2, 3]).drop(2).toArray(), [3]); + deepStrictEqual(await fromAsync([1, 2, 3]).take(1).toArray(), [1]); + deepStrictEqual(await fromAsync([]).drop(2).toArray(), []); + deepStrictEqual(await fromAsync([]).take(1).toArray(), []); + deepStrictEqual(await fromAsync([1, 2, 3]).drop(1).take(1).toArray(), [2]); + deepStrictEqual(await fromAsync([1, 2]).drop(0).toArray(), [1, 2]); + deepStrictEqual(await fromAsync([1, 2]).take(0).toArray(), []); + })().then(common.mustCall()); + // Infinite streams + // Asynchronous streams + (async () => { + deepStrictEqual(await naturals().take(1).toArray(), [1]); + deepStrictEqual(await naturals().drop(1).take(1).toArray(), [2]); + const next10 = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + deepStrictEqual(await naturals().drop(10).take(10).toArray(), next10); + deepStrictEqual(await naturals().take(5).take(1).toArray(), [1]); + })().then(common.mustCall()); +} + + +// Don't wait for next item in the original stream when already consumed the requested take amount +{ + let reached = false; + let resolve; + const promise = new Promise((res) => resolve = res); + + const stream = from((async function *() { + yield 1; + await promise; + reached = true; + yield 2; + })()); + + stream.take(1) + .toArray() + .then(common.mustCall(() => { + strictEqual(reached, false); + })) + .finally(() => resolve()); +} + +{ + // Coercion + (async () => { + // The spec made me do this ^^ + deepStrictEqual(await naturals().take('cat').toArray(), []); + deepStrictEqual(await naturals().take('2').toArray(), [1, 2]); + deepStrictEqual(await naturals().take(true).toArray(), [1]); + })().then(common.mustCall()); +} + +{ + // Support for AbortSignal + const ac = new AbortController(); + rejects( + Readable.from([1, 2, 3]).take(1, { signal: ac.signal }).toArray(), { + name: 'AbortError', + }).then(common.mustCall()); + rejects( + Readable.from([1, 2, 3]).drop(1, { signal: ac.signal }).toArray(), { + name: 'AbortError', + }).then(common.mustCall()); + ac.abort(); +} + +{ + // Support for AbortSignal, already aborted + const signal = AbortSignal.abort(); + rejects( + Readable.from([1, 2, 3]).take(1, { signal }).toArray(), { + name: 'AbortError', + }).then(common.mustCall()); +} + +{ + // Error cases + const invalidArgs = [ + -1, + -Infinity, + -40, + ]; + + for (const example of invalidArgs) { + throws(() => from([]).take(example).toArray(), /ERR_OUT_OF_RANGE/); + } + + throws(() => Readable.from([1]).drop(1, 1), /ERR_INVALID_ARG_TYPE/); + throws(() => Readable.from([1]).drop(1, { signal: true }), /ERR_INVALID_ARG_TYPE/); + + throws(() => Readable.from([1]).take(1, 1), /ERR_INVALID_ARG_TYPE/); + throws(() => Readable.from([1]).take(1, { signal: true }), /ERR_INVALID_ARG_TYPE/); +} diff --git a/test/js/node/test/parallel/test-stream-duplex-destroy.js b/test/js/node/test/parallel/test-stream-duplex-destroy.js new file mode 100644 index 0000000000..5286738405 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex-destroy.js @@ -0,0 +1,286 @@ +'use strict'; + +const common = require('../common'); +const { Duplex } = require('stream'); +const assert = require('assert'); + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {} + }); + + duplex.resume(); + + duplex.on('end', common.mustNotCall()); + duplex.on('finish', common.mustNotCall()); + duplex.on('close', common.mustCall()); + + duplex.destroy(); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {} + }); + duplex.resume(); + + const expected = new Error('kaboom'); + + duplex.on('end', common.mustNotCall()); + duplex.on('finish', common.mustNotCall()); + duplex.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + duplex.destroy(expected); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {} + }); + + duplex._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, expected); + cb(err); + }); + + const expected = new Error('kaboom'); + + duplex.on('finish', common.mustNotCall('no finish event')); + duplex.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + duplex.destroy(expected); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const expected = new Error('kaboom'); + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {}, + destroy: common.mustCall(function(err, cb) { + assert.strictEqual(err, expected); + cb(); + }) + }); + duplex.resume(); + + duplex.on('end', common.mustNotCall('no end event')); + duplex.on('finish', common.mustNotCall('no finish event')); + + // Error is swallowed by the custom _destroy + duplex.on('error', common.mustNotCall('no error event')); + duplex.on('close', common.mustCall()); + + duplex.destroy(expected); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {} + }); + + duplex._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(); + }); + + duplex.destroy(); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {} + }); + duplex.resume(); + + duplex._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + process.nextTick(() => { + this.push(null); + this.end(); + cb(); + }); + }); + + const fail = common.mustNotCall('no finish or end event'); + + duplex.on('finish', fail); + duplex.on('end', fail); + + duplex.destroy(); + + duplex.removeListener('end', fail); + duplex.removeListener('finish', fail); + duplex.on('end', common.mustNotCall()); + duplex.on('finish', common.mustNotCall()); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {} + }); + + const expected = new Error('kaboom'); + + duplex._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(expected); + }); + + duplex.on('finish', common.mustNotCall('no finish event')); + duplex.on('end', common.mustNotCall('no end event')); + duplex.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + duplex.destroy(); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {}, + allowHalfOpen: true + }); + duplex.resume(); + + duplex.on('finish', common.mustNotCall()); + duplex.on('end', common.mustNotCall()); + + duplex.destroy(); + assert.strictEqual(duplex.destroyed, true); +} + +{ + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {}, + }); + + duplex.destroyed = true; + assert.strictEqual(duplex.destroyed, true); + + // The internal destroy() mechanism should not be triggered + duplex.on('finish', common.mustNotCall()); + duplex.on('end', common.mustNotCall()); + duplex.destroy(); +} + +{ + function MyDuplex() { + assert.strictEqual(this.destroyed, false); + this.destroyed = false; + Duplex.call(this); + } + + Object.setPrototypeOf(MyDuplex.prototype, Duplex.prototype); + Object.setPrototypeOf(MyDuplex, Duplex); + + new MyDuplex(); +} + +{ + const duplex = new Duplex({ + writable: false, + autoDestroy: true, + write(chunk, enc, cb) { cb(); }, + read() {}, + }); + duplex.push(null); + duplex.resume(); + duplex.on('close', common.mustCall()); +} + +{ + const duplex = new Duplex({ + readable: false, + autoDestroy: true, + write(chunk, enc, cb) { cb(); }, + read() {}, + }); + duplex.end(); + duplex.on('close', common.mustCall()); +} + +{ + const duplex = new Duplex({ + allowHalfOpen: false, + autoDestroy: true, + write(chunk, enc, cb) { cb(); }, + read() {}, + }); + duplex.push(null); + duplex.resume(); + const orgEnd = duplex.end; + duplex.end = common.mustNotCall(); + duplex.on('end', () => { + // Ensure end() is called in next tick to allow + // any pending writes to be invoked first. + process.nextTick(() => { + duplex.end = common.mustCall(orgEnd); + }); + }); + duplex.on('close', common.mustCall()); +} + +{ + // Check abort signal + const controller = new AbortController(); + const { signal } = controller; + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {}, + signal, + }); + let count = 0; + duplex.on('error', common.mustCall((e) => { + assert.strictEqual(count++, 0); // Ensure not called twice + assert.strictEqual(e.name, 'AbortError'); + })); + duplex.on('close', common.mustCall()); + controller.abort(); +} + +{ + const duplex = new Duplex({ + read() {}, + write(chunk, enc, cb) { cb(); } + }); + + duplex.cork(); + duplex.write('foo', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED'); + })); + duplex.destroy(); +} + +{ + // Check Symbol.asyncDispose + const duplex = new Duplex({ + write(chunk, enc, cb) { cb(); }, + read() {}, + }); + let count = 0; + duplex.on('error', common.mustCall((e) => { + assert.strictEqual(count++, 0); // Ensure not called twice + assert.strictEqual(e.name, 'AbortError'); + })); + duplex.on('close', common.mustCall()); + duplex[Symbol.asyncDispose]().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-duplex-end.js b/test/js/node/test/parallel/test-stream-duplex-end.js new file mode 100644 index 0000000000..2c7706146e --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex-end.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const Duplex = require('stream').Duplex; + +{ + const stream = new Duplex({ + read() {} + }); + assert.strictEqual(stream.allowHalfOpen, true); + stream.on('finish', common.mustNotCall()); + assert.strictEqual(stream.listenerCount('end'), 0); + stream.resume(); + stream.push(null); +} + +{ + const stream = new Duplex({ + read() {}, + allowHalfOpen: false + }); + assert.strictEqual(stream.allowHalfOpen, false); + stream.on('finish', common.mustCall()); + assert.strictEqual(stream.listenerCount('end'), 0); + stream.resume(); + stream.push(null); +} + +{ + const stream = new Duplex({ + read() {}, + allowHalfOpen: false + }); + assert.strictEqual(stream.allowHalfOpen, false); + stream._writableState.ended = true; + stream.on('finish', common.mustNotCall()); + assert.strictEqual(stream.listenerCount('end'), 0); + stream.resume(); + stream.push(null); +} diff --git a/test/js/node/test/parallel/test-stream-duplex-from.js b/test/js/node/test/parallel/test-stream-duplex-from.js new file mode 100644 index 0000000000..e3c117ff8d --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex-from.js @@ -0,0 +1,403 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Duplex, Readable, Writable, pipeline, PassThrough } = require('stream'); +const { ReadableStream, WritableStream } = require('stream/web'); +const { Blob } = require('buffer'); + +{ + const d = Duplex.from({ + readable: new Readable({ + read() { + this.push('asd'); + this.push(null); + } + }) + }); + assert.strictEqual(d.readable, true); + assert.strictEqual(d.writable, false); + d.once('readable', common.mustCall(function() { + assert.strictEqual(d.read().toString(), 'asd'); + })); + d.once('end', common.mustCall(function() { + assert.strictEqual(d.readable, false); + })); +} + +{ + const d = Duplex.from(new Readable({ + read() { + this.push('asd'); + this.push(null); + } + })); + assert.strictEqual(d.readable, true); + assert.strictEqual(d.writable, false); + d.once('readable', common.mustCall(function() { + assert.strictEqual(d.read().toString(), 'asd'); + })); + d.once('end', common.mustCall(function() { + assert.strictEqual(d.readable, false); + })); +} + +{ + let ret = ''; + const d = Duplex.from(new Writable({ + write(chunk, encoding, callback) { + ret += chunk; + callback(); + } + })); + assert.strictEqual(d.readable, false); + assert.strictEqual(d.writable, true); + d.end('asd'); + d.on('finish', common.mustCall(function() { + assert.strictEqual(d.writable, false); + assert.strictEqual(ret, 'asd'); + })); +} + +{ + let ret = ''; + const d = Duplex.from({ + writable: new Writable({ + write(chunk, encoding, callback) { + ret += chunk; + callback(); + } + }) + }); + assert.strictEqual(d.readable, false); + assert.strictEqual(d.writable, true); + d.end('asd'); + d.on('finish', common.mustCall(function() { + assert.strictEqual(d.writable, false); + assert.strictEqual(ret, 'asd'); + })); +} + +{ + let ret = ''; + const d = Duplex.from({ + readable: new Readable({ + read() { + this.push('asd'); + this.push(null); + } + }), + writable: new Writable({ + write(chunk, encoding, callback) { + ret += chunk; + callback(); + } + }) + }); + assert.strictEqual(d.readable, true); + assert.strictEqual(d.writable, true); + d.once('readable', common.mustCall(function() { + assert.strictEqual(d.read().toString(), 'asd'); + })); + d.once('end', common.mustCall(function() { + assert.strictEqual(d.readable, false); + })); + d.end('asd'); + d.once('finish', common.mustCall(function() { + assert.strictEqual(d.writable, false); + assert.strictEqual(ret, 'asd'); + })); +} + +{ + const d = Duplex.from(Promise.resolve('asd')); + assert.strictEqual(d.readable, true); + assert.strictEqual(d.writable, false); + d.once('readable', common.mustCall(function() { + assert.strictEqual(d.read().toString(), 'asd'); + })); + d.once('end', common.mustCall(function() { + assert.strictEqual(d.readable, false); + })); +} + +{ + // https://github.com/nodejs/node/issues/40497 + pipeline( + ['abc\ndef\nghi'], + Duplex.from(async function * (source) { + let rest = ''; + for await (const chunk of source) { + const lines = (rest + chunk.toString()).split('\n'); + rest = lines.pop(); + for (const line of lines) { + yield line; + } + } + yield rest; + }), + async function * (source) { // eslint-disable-line require-yield + let ret = ''; + for await (const x of source) { + ret += x; + } + assert.strictEqual(ret, 'abcdefghi'); + }, + common.mustSucceed(), + ); +} + +// Ensure that isDuplexNodeStream was called +{ + const duplex = new Duplex(); + assert.strictEqual(Duplex.from(duplex), duplex); +} + +// Ensure that Duplex.from works for blobs +{ + const blob = new Blob(['blob']); + const expectedByteLength = blob.size; + const duplex = Duplex.from(blob); + duplex.on('data', common.mustCall((arrayBuffer) => { + assert.strictEqual(arrayBuffer.byteLength, expectedByteLength); + })); +} + +// Ensure that given a promise rejection it emits an error +{ + const myErrorMessage = 'myCustomError'; + Duplex.from(Promise.reject(myErrorMessage)) + .on('error', common.mustCall((error) => { + assert.strictEqual(error, myErrorMessage); + })); +} + +// Ensure that given a promise rejection on an async function it emits an error +{ + const myErrorMessage = 'myCustomError'; + async function asyncFn() { + return Promise.reject(myErrorMessage); + } + + Duplex.from(asyncFn) + .on('error', common.mustCall((error) => { + assert.strictEqual(error, myErrorMessage); + })); +} + +// Ensure that Duplex.from throws an Invalid return value when function is void +{ + assert.throws(() => Duplex.from(() => {}), { + code: 'ERR_INVALID_RETURN_VALUE', + }); +} + +// Ensure data if a sub object has a readable stream it's duplexified +{ + const msg = Buffer.from('hello'); + const duplex = Duplex.from({ + readable: Readable({ + read() { + this.push(msg); + this.push(null); + } + }) + }).on('data', common.mustCall((data) => { + assert.strictEqual(data, msg); + })); + + assert.strictEqual(duplex.writable, false); +} + +// Ensure data if a sub object has a writable stream it's duplexified +{ + const msg = Buffer.from('hello'); + const duplex = Duplex.from({ + writable: Writable({ + write: common.mustCall((data) => { + assert.strictEqual(data, msg); + }) + }) + }); + + duplex.write(msg); + assert.strictEqual(duplex.readable, false); +} + +// Ensure data if a sub object has a writable and readable stream it's duplexified +{ + const msg = Buffer.from('hello'); + + const duplex = Duplex.from({ + readable: Readable({ + read() { + this.push(msg); + this.push(null); + } + }), + writable: Writable({ + write: common.mustCall((data) => { + assert.strictEqual(data, msg); + }) + }) + }); + + duplex.pipe(duplex) + .on('data', common.mustCall((data) => { + assert.strictEqual(data, msg); + assert.strictEqual(duplex.readable, true); + assert.strictEqual(duplex.writable, true); + })) + .on('end', common.mustCall()); +} + +// Ensure that given readable stream that throws an error it calls destroy +{ + const myErrorMessage = 'error!'; + const duplex = Duplex.from(Readable({ + read() { + throw new Error(myErrorMessage); + } + })); + duplex.on('error', common.mustCall((msg) => { + assert.strictEqual(msg.message, myErrorMessage); + })); +} + +// Ensure that given writable stream that throws an error it calls destroy +{ + const myErrorMessage = 'error!'; + const duplex = Duplex.from(Writable({ + write(chunk, enc, cb) { + cb(myErrorMessage); + } + })); + + duplex.on('error', common.mustCall((msg) => { + assert.strictEqual(msg, myErrorMessage); + })); + + duplex.write('test'); +} + +{ + const through = new PassThrough({ objectMode: true }); + + let res = ''; + const d = Readable.from(['foo', 'bar'], { objectMode: true }) + .pipe(Duplex.from({ + writable: through, + readable: through + })); + + d.on('data', (data) => { + d.pause(); + setImmediate(() => { + d.resume(); + }); + res += data; + }).on('end', common.mustCall(() => { + assert.strictEqual(res, 'foobar'); + })).on('close', common.mustCall()); +} + +function makeATestReadableStream(value) { + return new ReadableStream({ + start(controller) { + controller.enqueue(value); + controller.close(); + } + }); +} + +function makeATestWritableStream(writeFunc) { + return new WritableStream({ + write(chunk) { + writeFunc(chunk); + } + }); +} + +{ + const d = Duplex.from({ + readable: makeATestReadableStream('foo'), + }); + assert.strictEqual(d.readable, true); + assert.strictEqual(d.writable, false); + + d.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'foo'); + })); + + d.on('end', common.mustCall(() => { + assert.strictEqual(d.readable, false); + })); +} + +{ + const d = Duplex.from(makeATestReadableStream('foo')); + + assert.strictEqual(d.readable, true); + assert.strictEqual(d.writable, false); + + d.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'foo'); + })); + + d.on('end', common.mustCall(() => { + assert.strictEqual(d.readable, false); + })); +} + +{ + let ret = ''; + const d = Duplex.from({ + writable: makeATestWritableStream((chunk) => ret += chunk), + }); + + assert.strictEqual(d.readable, false); + assert.strictEqual(d.writable, true); + + d.end('foo'); + d.on('finish', common.mustCall(() => { + assert.strictEqual(ret, 'foo'); + assert.strictEqual(d.writable, false); + })); +} + +{ + let ret = ''; + const d = Duplex.from(makeATestWritableStream((chunk) => ret += chunk)); + + assert.strictEqual(d.readable, false); + assert.strictEqual(d.writable, true); + + d.end('foo'); + d.on('finish', common.mustCall(() => { + assert.strictEqual(ret, 'foo'); + assert.strictEqual(d.writable, false); + })); +} + +{ + let ret = ''; + const d = Duplex.from({ + readable: makeATestReadableStream('foo'), + writable: makeATestWritableStream((chunk) => ret += chunk), + }); + + d.end('bar'); + + d.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'foo'); + })); + + d.on('end', common.mustCall(() => { + assert.strictEqual(d.readable, false); + })); + + d.on('finish', common.mustCall(() => { + assert.strictEqual(ret, 'bar'); + assert.strictEqual(d.writable, false); + })); +} diff --git a/test/js/node/test/parallel/test-stream-duplex-props.js b/test/js/node/test/parallel/test-stream-duplex-props.js new file mode 100644 index 0000000000..aa6b23125a --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex-props.js @@ -0,0 +1,31 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { Duplex } = require('stream'); + +{ + const d = new Duplex({ + objectMode: true, + highWaterMark: 100 + }); + + assert.strictEqual(d.writableObjectMode, true); + assert.strictEqual(d.writableHighWaterMark, 100); + assert.strictEqual(d.readableObjectMode, true); + assert.strictEqual(d.readableHighWaterMark, 100); +} + +{ + const d = new Duplex({ + readableObjectMode: false, + readableHighWaterMark: 10, + writableObjectMode: true, + writableHighWaterMark: 100 + }); + + assert.strictEqual(d.writableObjectMode, true); + assert.strictEqual(d.writableHighWaterMark, 100); + assert.strictEqual(d.readableObjectMode, false); + assert.strictEqual(d.readableHighWaterMark, 10); +} diff --git a/test/js/node/test/parallel/test-stream-duplex-readable-end.js b/test/js/node/test/parallel/test-stream-duplex-readable-end.js new file mode 100644 index 0000000000..3b1d4d21ce --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex-readable-end.js @@ -0,0 +1,31 @@ +'use strict'; +// https://github.com/nodejs/node/issues/35926 +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +let loops = 5; + +const src = new stream.Readable({ + highWaterMark: 16 * 1024, + read() { + if (loops--) + this.push(Buffer.alloc(20000)); + } +}); + +const dst = new stream.Transform({ + highWaterMark: 16 * 1024, + transform(chunk, output, fn) { + this.push(null); + fn(); + } +}); + +src.pipe(dst); + +dst.on('data', () => { }); +dst.on('end', common.mustCall(() => { + assert.strictEqual(loops, 3); + assert.ok(src.isPaused()); +})); diff --git a/test/js/node/test/parallel/test-stream-duplex-readable-writable.js b/test/js/node/test/parallel/test-stream-duplex-readable-writable.js new file mode 100644 index 0000000000..aec88fc120 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex-readable-writable.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const { Duplex } = require('stream'); +const assert = require('assert'); + +{ + const duplex = new Duplex({ + readable: false + }); + assert.strictEqual(duplex.readable, false); + duplex.push('asd'); + duplex.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_PUSH_AFTER_EOF'); + })); + duplex.on('data', common.mustNotCall()); + duplex.on('end', common.mustNotCall()); +} + +{ + const duplex = new Duplex({ + writable: false, + write: common.mustNotCall() + }); + assert.strictEqual(duplex.writable, false); + duplex.write('asd'); + duplex.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END'); + })); + duplex.on('finish', common.mustNotCall()); +} + +{ + const duplex = new Duplex({ + readable: false + }); + assert.strictEqual(duplex.readable, false); + duplex.on('data', common.mustNotCall()); + duplex.on('end', common.mustNotCall()); + async function run() { + for await (const chunk of duplex) { + assert(false, chunk); + } + } + run().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-duplex-writable-finished.js b/test/js/node/test/parallel/test-stream-duplex-writable-finished.js new file mode 100644 index 0000000000..20c0781a22 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex-writable-finished.js @@ -0,0 +1,30 @@ +'use strict'; + +const common = require('../common'); +const { Duplex } = require('stream'); +const assert = require('assert'); + +// basic +{ + // Find it on Duplex.prototype + assert(Object.hasOwn(Duplex.prototype, 'writableFinished')); +} + +// event +{ + const duplex = new Duplex(); + + duplex._write = (chunk, encoding, cb) => { + // The state finished should start in false. + assert.strictEqual(duplex.writableFinished, false); + cb(); + }; + + duplex.on('finish', common.mustCall(() => { + assert.strictEqual(duplex.writableFinished, true); + })); + + duplex.end('testing finished state', common.mustCall(() => { + assert.strictEqual(duplex.writableFinished, true); + })); +} diff --git a/test/js/node/test/parallel/test-stream-duplex.js b/test/js/node/test/parallel/test-stream-duplex.js new file mode 100644 index 0000000000..490744910c --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplex.js @@ -0,0 +1,133 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const Duplex = require('stream').Duplex; +const { ReadableStream, WritableStream } = require('stream/web'); + +const stream = new Duplex({ objectMode: true }); + +assert(Duplex() instanceof Duplex); +assert(stream._readableState.objectMode); +assert(stream._writableState.objectMode); +assert(stream.allowHalfOpen); +assert.strictEqual(stream.listenerCount('end'), 0); + +let written; +let read; + +stream._write = (obj, _, cb) => { + written = obj; + cb(); +}; + +stream._read = () => {}; + +stream.on('data', (obj) => { + read = obj; +}); + +stream.push({ val: 1 }); +stream.end({ val: 2 }); + +process.on('exit', () => { + assert.strictEqual(read.val, 1); + assert.strictEqual(written.val, 2); +}); + +// Duplex.fromWeb +{ + const dataToRead = Buffer.from('hello'); + const dataToWrite = Buffer.from('world'); + + const readable = new ReadableStream({ + start(controller) { + controller.enqueue(dataToRead); + }, + }); + + const writable = new WritableStream({ + write: common.mustCall((chunk) => { + assert.strictEqual(chunk, dataToWrite); + }) + }); + + const pair = { readable, writable }; + const duplex = Duplex.fromWeb(pair); + + duplex.write(dataToWrite); + duplex.once('data', common.mustCall((chunk) => { + assert.strictEqual(chunk, dataToRead); + })); +} + +// Duplex.fromWeb - using utf8 and objectMode +{ + const dataToRead = 'hello'; + const dataToWrite = 'world'; + + const readable = new ReadableStream({ + start(controller) { + controller.enqueue(dataToRead); + }, + }); + + const writable = new WritableStream({ + write: common.mustCall((chunk) => { + assert.strictEqual(chunk, dataToWrite); + }) + }); + + const pair = { + readable, + writable + }; + const duplex = Duplex.fromWeb(pair, { encoding: 'utf8', objectMode: true }); + + duplex.write(dataToWrite); + duplex.once('data', common.mustCall((chunk) => { + assert.strictEqual(chunk, dataToRead); + })); +} +// Duplex.toWeb +{ + const dataToRead = Buffer.from('hello'); + const dataToWrite = Buffer.from('world'); + + const duplex = Duplex({ + read() { + this.push(dataToRead); + this.push(null); + }, + write: common.mustCall((chunk) => { + assert.strictEqual(chunk, dataToWrite); + }) + }); + + const { writable, readable } = Duplex.toWeb(duplex); + writable.getWriter().write(dataToWrite); + + readable.getReader().read().then(common.mustCall((result) => { + assert.deepStrictEqual(Buffer.from(result.value), dataToRead); + })); +} diff --git a/test/js/node/test/parallel/test-stream-duplexpair.js b/test/js/node/test/parallel/test-stream-duplexpair.js new file mode 100644 index 0000000000..3e1b3044dd --- /dev/null +++ b/test/js/node/test/parallel/test-stream-duplexpair.js @@ -0,0 +1,74 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Duplex, duplexPair } = require('stream'); + +{ + const pair = duplexPair(); + + assert(pair[0] instanceof Duplex); + assert(pair[1] instanceof Duplex); + assert.notStrictEqual(pair[0], pair[1]); +} + +{ + // Verify that the iterable for array assignment works + const [ clientSide, serverSide ] = duplexPair(); + assert(clientSide instanceof Duplex); + assert(serverSide instanceof Duplex); + clientSide.on( + 'data', + common.mustCall((d) => assert.strictEqual(`${d}`, 'foo')) + ); + clientSide.on('end', common.mustNotCall()); + serverSide.write('foo'); +} + +{ + const [ clientSide, serverSide ] = duplexPair(); + assert(clientSide instanceof Duplex); + assert(serverSide instanceof Duplex); + serverSide.on( + 'data', + common.mustCall((d) => assert.strictEqual(`${d}`, 'foo')) + ); + serverSide.on('end', common.mustCall()); + clientSide.end('foo'); +} + +{ + const [ serverSide, clientSide ] = duplexPair(); + serverSide.cork(); + serverSide.write('abc'); + serverSide.write('12'); + serverSide.end('\n'); + serverSide.uncork(); + let characters = ''; + clientSide.on('readable', function() { + for (let segment; (segment = this.read()) !== null;) + characters += segment; + }); + clientSide.on('end', common.mustCall(function() { + assert.strictEqual(characters, 'abc12\n'); + })); +} + +// Test the case where the the _write never calls [kCallback] +// because a zero-size push doesn't trigger a _read +{ + const [ serverSide, clientSide ] = duplexPair(); + serverSide.write(''); + serverSide.write('12'); + serverSide.write(''); + serverSide.write(''); + serverSide.end('\n'); + let characters = ''; + clientSide.on('readable', function() { + for (let segment; (segment = this.read()) !== null;) + characters += segment; + }); + clientSide.on('end', common.mustCall(function() { + assert.strictEqual(characters, '12\n'); + })); +} diff --git a/test/js/node/test/parallel/test-stream-end-of-streams.js b/test/js/node/test/parallel/test-stream-end-of-streams.js new file mode 100644 index 0000000000..80a39d052b --- /dev/null +++ b/test/js/node/test/parallel/test-stream-end-of-streams.js @@ -0,0 +1,20 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const { Duplex, finished } = require('stream'); + +assert.throws( + () => { + // Passing empty object to mock invalid stream + // should throw error + finished({}, () => {}); + }, + { code: 'ERR_INVALID_ARG_TYPE' } +); + +const streamObj = new Duplex(); +streamObj.end(); +// Below code should not throw any errors as the +// streamObj is `Stream` +finished(streamObj, () => {}); diff --git a/test/js/node/test/parallel/test-stream-end-paused.js b/test/js/node/test/parallel/test-stream-end-paused.js new file mode 100644 index 0000000000..f29c82f532 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-end-paused.js @@ -0,0 +1,50 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Make sure we don't miss the end event for paused 0-length streams + +const Readable = require('stream').Readable; +const stream = new Readable(); +let calledRead = false; +stream._read = function() { + assert(!calledRead); + calledRead = true; + this.push(null); +}; + +stream.on('data', function() { + throw new Error('should not ever get data'); +}); +stream.pause(); + +setTimeout(common.mustCall(function() { + stream.on('end', common.mustCall()); + stream.resume(); +}), 1); + +process.on('exit', function() { + assert(calledRead); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream-err-multiple-callback-construction.js b/test/js/node/test/parallel/test-stream-err-multiple-callback-construction.js new file mode 100644 index 0000000000..829af3ffe7 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-err-multiple-callback-construction.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +class TestWritable extends stream.Writable { + _write(_chunk, _encoding, callback) { + callback(); + } + + _final(callback) { + process.nextTick(callback); + process.nextTick(callback); + } +} + +const writable = new TestWritable(); + +writable.on('finish', common.mustCall()); +writable.on('error', common.mustCall((error) => { + assert.strictEqual(error.message, 'Callback called multiple times'); +})); + +writable.write('some data'); +writable.end(); diff --git a/test/js/node/test/parallel/test-stream-error-once.js b/test/js/node/test/parallel/test-stream-error-once.js new file mode 100644 index 0000000000..71f268cfa4 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-error-once.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const { Writable, Readable } = require('stream'); + +{ + const writable = new Writable(); + writable.on('error', common.mustCall()); + writable.end(); + writable.write('h'); + writable.write('h'); +} + +{ + const readable = new Readable(); + readable.on('error', common.mustCall()); + readable.push(null); + readable.push('h'); + readable.push('h'); +} diff --git a/test/js/node/test/parallel/test-stream-event-names.js b/test/js/node/test/parallel/test-stream-event-names.js new file mode 100644 index 0000000000..e9eab40088 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-event-names.js @@ -0,0 +1,42 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { Readable, Writable, Duplex } = require('stream'); + +{ + const stream = new Readable(); + assert.strictEqual(stream.eventNames().length, 0); +} + +{ + const stream = new Readable(); + stream.on('foo', () => {}); + stream.on('data', () => {}); + stream.on('error', () => {}); + assert.deepStrictEqual(stream.eventNames(), ['error', 'data', 'foo']); +} + +{ + const stream = new Writable(); + assert.strictEqual(stream.eventNames().length, 0); +} + +{ + const stream = new Writable(); + stream.on('foo', () => {}); + stream.on('drain', () => {}); + stream.on('prefinish', () => {}); + assert.deepStrictEqual(stream.eventNames(), ['prefinish', 'drain', 'foo']); +} +{ + const stream = new Duplex(); + assert.strictEqual(stream.eventNames().length, 0); +} + +{ + const stream = new Duplex(); + stream.on('foo', () => {}); + stream.on('finish', () => {}); + assert.deepStrictEqual(stream.eventNames(), ['finish', 'foo']); +} diff --git a/test/js/node/test/parallel/test-stream-events-prepend.js b/test/js/node/test/parallel/test-stream-events-prepend.js new file mode 100644 index 0000000000..80fedf8fae --- /dev/null +++ b/test/js/node/test/parallel/test-stream-events-prepend.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); + +class Writable extends stream.Writable { + constructor() { + super(); + this.prependListener = undefined; + } + + _write(chunk, end, cb) { + cb(); + } +} + +class Readable extends stream.Readable { + _read() { + this.push(null); + } +} + +const w = new Writable(); +w.on('pipe', common.mustCall()); + +const r = new Readable(); +r.pipe(w); diff --git a/test/js/node/test/parallel/test-stream-filter.js b/test/js/node/test/parallel/test-stream-filter.js new file mode 100644 index 0000000000..e7711012bb --- /dev/null +++ b/test/js/node/test/parallel/test-stream-filter.js @@ -0,0 +1,174 @@ +'use strict'; + +const common = require('../common'); +const { + Readable, +} = require('stream'); +const assert = require('assert'); +const { once } = require('events'); +const { setTimeout } = require('timers/promises'); + +{ + // Filter works on synchronous streams with a synchronous predicate + const stream = Readable.from([1, 2, 3, 4, 5]).filter((x) => x < 3); + const result = [1, 2]; + (async () => { + for await (const item of stream) { + assert.strictEqual(item, result.shift()); + } + })().then(common.mustCall()); +} + +{ + // Filter works on synchronous streams with an asynchronous predicate + const stream = Readable.from([1, 2, 3, 4, 5]).filter(async (x) => { + await Promise.resolve(); + return x > 3; + }); + const result = [4, 5]; + (async () => { + for await (const item of stream) { + assert.strictEqual(item, result.shift()); + } + })().then(common.mustCall()); +} + +{ + // Map works on asynchronous streams with a asynchronous mapper + const stream = Readable.from([1, 2, 3, 4, 5]).map(async (x) => { + await Promise.resolve(); + return x + x; + }).filter((x) => x > 5); + const result = [6, 8, 10]; + (async () => { + for await (const item of stream) { + assert.strictEqual(item, result.shift()); + } + })().then(common.mustCall()); +} + +{ + // Filter works on an infinite stream + const stream = Readable.from(async function* () { + while (true) yield 1; + }()).filter(common.mustCall(async (x) => { + return x < 3; + }, 5)); + (async () => { + let i = 1; + for await (const item of stream) { + assert.strictEqual(item, 1); + if (++i === 5) break; + } + })().then(common.mustCall()); +} + +{ + // Filter works on constructor created streams + let i = 0; + const stream = new Readable({ + read() { + if (i === 10) { + this.push(null); + return; + } + this.push(Uint8Array.from([i])); + i++; + }, + highWaterMark: 0, + }).filter(common.mustCall(async ([x]) => { + return x !== 5; + }, 10)); + (async () => { + const result = (await stream.toArray()).map((x) => x[0]); + const expected = [...Array(10).keys()].filter((x) => x !== 5); + assert.deepStrictEqual(result, expected); + })().then(common.mustCall()); +} + +{ + // Throwing an error during `filter` (sync) + const stream = Readable.from([1, 2, 3, 4, 5]).filter((x) => { + if (x === 3) { + throw new Error('boom'); + } + return true; + }); + assert.rejects( + stream.map((x) => x + x).toArray(), + /boom/, + ).then(common.mustCall()); +} + +{ + // Throwing an error during `filter` (async) + const stream = Readable.from([1, 2, 3, 4, 5]).filter(async (x) => { + if (x === 3) { + throw new Error('boom'); + } + return true; + }); + assert.rejects( + stream.filter(() => true).toArray(), + /boom/, + ).then(common.mustCall()); +} + +{ + // Concurrency + AbortSignal + const ac = new AbortController(); + let calls = 0; + const stream = Readable.from([1, 2, 3, 4]).filter(async (_, { signal }) => { + calls++; + await once(signal, 'abort'); + }, { signal: ac.signal, concurrency: 2 }); + // pump + assert.rejects(async () => { + for await (const item of stream) { + // nope + console.log(item); + } + }, { + name: 'AbortError', + }).then(common.mustCall()); + + setImmediate(() => { + ac.abort(); + assert.strictEqual(calls, 2); + }); +} + +{ + // Concurrency result order + const stream = Readable.from([1, 2]).filter(async (item, { signal }) => { + await setTimeout(10 - item, { signal }); + return true; + }, { concurrency: 2 }); + + (async () => { + const expected = [1, 2]; + for await (const item of stream) { + assert.strictEqual(item, expected.shift()); + } + })().then(common.mustCall()); +} + +{ + // Error cases + assert.throws(() => Readable.from([1]).filter(1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => Readable.from([1]).filter((x) => x, { concurrency: 'Foo' }), /ERR_OUT_OF_RANGE/); + assert.throws(() => Readable.from([1]).filter((x) => x, 1), /ERR_INVALID_ARG_TYPE/); +} +{ + // Test result is a Readable + const stream = Readable.from([1, 2, 3, 4, 5]).filter((x) => true); + assert.strictEqual(stream.readable, true); +} +{ + const stream = Readable.from([1, 2, 3, 4, 5]); + Object.defineProperty(stream, 'map', { + value: common.mustNotCall(), + }); + // Check that map isn't getting called. + stream.filter(() => true); +} diff --git a/test/js/node/test/parallel/test-stream-flatMap.js b/test/js/node/test/parallel/test-stream-flatMap.js new file mode 100644 index 0000000000..9295b8a0f8 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-flatMap.js @@ -0,0 +1,129 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const { + Readable, +} = require('stream'); +const assert = require('assert'); +const { setTimeout } = require('timers/promises'); +const { createReadStream } = require('fs'); + +function oneTo5() { + return Readable.from([1, 2, 3, 4, 5]); +} + +{ + // flatMap works on synchronous streams with a synchronous mapper + (async () => { + assert.deepStrictEqual( + await oneTo5().flatMap((x) => [x + x]).toArray(), + [2, 4, 6, 8, 10] + ); + assert.deepStrictEqual( + await oneTo5().flatMap(() => []).toArray(), + [] + ); + assert.deepStrictEqual( + await oneTo5().flatMap((x) => [x, x]).toArray(), + [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] + ); + })().then(common.mustCall()); +} + + +{ + // flatMap works on sync/async streams with an asynchronous mapper + (async () => { + assert.deepStrictEqual( + await oneTo5().flatMap(async (x) => [x, x]).toArray(), + [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] + ); + const asyncOneTo5 = oneTo5().map(async (x) => x); + assert.deepStrictEqual( + await asyncOneTo5.flatMap(async (x) => [x, x]).toArray(), + [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] + ); + })().then(common.mustCall()); +} +{ + // flatMap works on a stream where mapping returns a stream + (async () => { + const result = await oneTo5().flatMap(async (x) => { + return Readable.from([x, x]); + }).toArray(); + assert.deepStrictEqual(result, [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]); + })().then(common.mustCall()); + // flatMap works on an objectMode stream where mappign returns a stream + (async () => { + const result = await oneTo5().flatMap(() => { + return createReadStream(fixtures.path('x.txt')); + }).toArray(); + // The resultant stream is in object mode so toArray shouldn't flatten + assert.strictEqual(result.length, 5); + assert.deepStrictEqual( + Buffer.concat(result).toString(), + 'xyz\n'.repeat(5) + ); + + })().then(common.mustCall()); + +} + +{ + // Concurrency + AbortSignal + const ac = new AbortController(); + const stream = oneTo5().flatMap(common.mustNotCall(async (_, { signal }) => { + await setTimeout(100, { signal }); + }), { signal: ac.signal, concurrency: 2 }); + // pump + assert.rejects(async () => { + for await (const item of stream) { + // nope + console.log(item); + } + }, { + name: 'AbortError', + }).then(common.mustCall()); + + queueMicrotask(() => { + ac.abort(); + }); +} + +{ + // Already aborted AbortSignal + const stream = oneTo5().flatMap(common.mustNotCall(async (_, { signal }) => { + await setTimeout(100, { signal }); + }), { signal: AbortSignal.abort() }); + // pump + assert.rejects(async () => { + for await (const item of stream) { + // nope + console.log(item); + } + }, { + name: 'AbortError', + }).then(common.mustCall()); +} + +{ + // Error cases + assert.throws(() => Readable.from([1]).flatMap(1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => Readable.from([1]).flatMap((x) => x, { concurrency: 'Foo' }), /ERR_OUT_OF_RANGE/); + assert.throws(() => Readable.from([1]).flatMap((x) => x, 1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => Readable.from([1]).flatMap((x) => x, { signal: true }), /ERR_INVALID_ARG_TYPE/); +} +{ + // Test result is a Readable + const stream = oneTo5().flatMap((x) => x); + assert.strictEqual(stream.readable, true); +} +{ + const stream = oneTo5(); + Object.defineProperty(stream, 'map', { + value: common.mustNotCall(), + }); + // Check that map isn't getting called. + stream.flatMap(() => true); +} diff --git a/test/js/node/test/parallel/test-stream-forEach.js b/test/js/node/test/parallel/test-stream-forEach.js new file mode 100644 index 0000000000..627ea0ccf1 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-forEach.js @@ -0,0 +1,139 @@ +'use strict'; + +const common = require('../common'); +const { + Readable, +} = require('stream'); +const assert = require('assert'); +const { once } = require('events'); + +{ + // forEach works on synchronous streams with a synchronous predicate + const stream = Readable.from([1, 2, 3]); + const result = [1, 2, 3]; + (async () => { + await stream.forEach((value) => assert.strictEqual(value, result.shift())); + })().then(common.mustCall()); +} + +{ + // forEach works an asynchronous streams + const stream = Readable.from([1, 2, 3]).filter(async (x) => { + await Promise.resolve(); + return true; + }); + const result = [1, 2, 3]; + (async () => { + await stream.forEach((value) => assert.strictEqual(value, result.shift())); + })().then(common.mustCall()); +} + +{ + // forEach works on asynchronous streams with a asynchronous forEach fn + const stream = Readable.from([1, 2, 3]).filter(async (x) => { + await Promise.resolve(); + return true; + }); + const result = [1, 2, 3]; + (async () => { + await stream.forEach(async (value) => { + await Promise.resolve(); + assert.strictEqual(value, result.shift()); + }); + })().then(common.mustCall()); +} + +{ + // forEach works on an infinite stream + const ac = new AbortController(); + const { signal } = ac; + const stream = Readable.from(async function* () { + while (true) yield 1; + }(), { signal }); + let i = 0; + assert.rejects(stream.forEach(common.mustCall((x) => { + i++; + if (i === 10) ac.abort(); + assert.strictEqual(x, 1); + }, 10)), { name: 'AbortError' }).then(common.mustCall()); +} + +{ + // Emitting an error during `forEach` + const stream = Readable.from([1, 2, 3, 4, 5]); + assert.rejects(stream.forEach(async (x) => { + if (x === 3) { + stream.emit('error', new Error('boom')); + } + }), /boom/).then(common.mustCall()); +} + +{ + // Throwing an error during `forEach` (sync) + const stream = Readable.from([1, 2, 3, 4, 5]); + assert.rejects(stream.forEach((x) => { + if (x === 3) { + throw new Error('boom'); + } + }), /boom/).then(common.mustCall()); +} + +{ + // Throwing an error during `forEach` (async) + const stream = Readable.from([1, 2, 3, 4, 5]); + assert.rejects(stream.forEach(async (x) => { + if (x === 3) { + return Promise.reject(new Error('boom')); + } + }), /boom/).then(common.mustCall()); +} + +{ + // Concurrency + AbortSignal + const ac = new AbortController(); + let calls = 0; + const forEachPromise = + Readable.from([1, 2, 3, 4]).forEach(async (_, { signal }) => { + calls++; + await once(signal, 'abort'); + }, { signal: ac.signal, concurrency: 2, highWaterMark: 0 }); + // pump + assert.rejects(async () => { + await forEachPromise; + }, { + name: 'AbortError', + }).then(common.mustCall()); + + setImmediate(() => { + ac.abort(); + assert.strictEqual(calls, 2); + }); +} + +{ + // Error cases + assert.rejects(async () => { + await Readable.from([1]).forEach(1); + }, /ERR_INVALID_ARG_TYPE/).then(common.mustCall()); + assert.rejects(async () => { + await Readable.from([1]).forEach((x) => x, { + concurrency: 'Foo' + }); + }, /ERR_OUT_OF_RANGE/).then(common.mustCall()); + assert.rejects(async () => { + await Readable.from([1]).forEach((x) => x, 1); + }, /ERR_INVALID_ARG_TYPE/).then(common.mustCall()); +} +{ + // Test result is a Promise + const stream = Readable.from([1, 2, 3, 4, 5]).forEach((_) => true); + assert.strictEqual(typeof stream.then, 'function'); +} +{ + const stream = Readable.from([1, 2, 3, 4, 5]); + Object.defineProperty(stream, 'map', { + value: common.mustNotCall(), + }); + // Check that map isn't getting called. + stream.forEach(() => true); +} diff --git a/test/js/node/test/parallel/test-stream-inheritance.js b/test/js/node/test/parallel/test-stream-inheritance.js new file mode 100644 index 0000000000..658bd2be33 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-inheritance.js @@ -0,0 +1,63 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { Readable, Writable, Duplex, Transform } = require('stream'); + +const readable = new Readable({ read() {} }); +const writable = new Writable({ write() {} }); +const duplex = new Duplex({ read() {}, write() {} }); +const transform = new Transform({ transform() {} }); + +assert.ok(readable instanceof Readable); +assert.ok(!(writable instanceof Readable)); +assert.ok(duplex instanceof Readable); +assert.ok(transform instanceof Readable); + +assert.ok(!(readable instanceof Writable)); +assert.ok(writable instanceof Writable); +assert.ok(duplex instanceof Writable); +assert.ok(transform instanceof Writable); + +assert.ok(!(readable instanceof Duplex)); +assert.ok(!(writable instanceof Duplex)); +assert.ok(duplex instanceof Duplex); +assert.ok(transform instanceof Duplex); + +assert.ok(!(readable instanceof Transform)); +assert.ok(!(writable instanceof Transform)); +assert.ok(!(duplex instanceof Transform)); +assert.ok(transform instanceof Transform); + +assert.ok(!(null instanceof Writable)); +assert.ok(!(undefined instanceof Writable)); + +// Simple inheritance check for `Writable` works fine in a subclass constructor. +function CustomWritable() { + assert.ok( + this instanceof CustomWritable, + `${this} does not inherit from CustomWritable` + ); + assert.ok( + this instanceof Writable, + `${this} does not inherit from Writable` + ); +} + +Object.setPrototypeOf(CustomWritable, Writable); +Object.setPrototypeOf(CustomWritable.prototype, Writable.prototype); + +new CustomWritable(); + +assert.throws( + CustomWritable, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'undefined does not inherit from CustomWritable' + } +); + +class OtherCustomWritable extends Writable {} + +assert(!(new OtherCustomWritable() instanceof CustomWritable)); +assert(!(new CustomWritable() instanceof OtherCustomWritable)); diff --git a/test/js/node/test/parallel/test-stream-ispaused.js b/test/js/node/test/parallel/test-stream-ispaused.js new file mode 100644 index 0000000000..a57928f934 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-ispaused.js @@ -0,0 +1,44 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +const readable = new stream.Readable(); + +// _read is a noop, here. +readable._read = Function(); + +// Default state of a stream is not "paused" +assert.ok(!readable.isPaused()); + +// Make the stream start flowing... +readable.on('data', Function()); + +// still not paused. +assert.ok(!readable.isPaused()); + +readable.pause(); +assert.ok(readable.isPaused()); +readable.resume(); +assert.ok(!readable.isPaused()); diff --git a/test/js/node/test/parallel/test-stream-iterator-helpers-test262-tests.mjs b/test/js/node/test/parallel/test-stream-iterator-helpers-test262-tests.mjs new file mode 100644 index 0000000000..59bcfdc565 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-iterator-helpers-test262-tests.mjs @@ -0,0 +1,146 @@ +import { mustCall } from '../common/index.mjs'; +import { Readable } from 'stream'; +import assert from 'assert'; + +// These tests are manually ported from the draft PR for the test262 test suite +// Authored by Rick Waldron in https://github.com/tc39/test262/pull/2818/files + +// test262 license: +// The << Software identified by reference to the Ecma Standard* ("Software)">> +// is protected by copyright and is being made available under the +// "BSD License", included below. This Software may be subject to third party +// rights (rights from parties other than Ecma International), including patent +// rights, and no licenses under such third party rights are granted under this +// license even if the third party concerned is a member of Ecma International. +// SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT +// http://www.ecma-international.org/memento/codeofconduct.htm FOR INFORMATION +// REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA +// INTERNATIONAL STANDARDS* + +// Copyright (C) 2012-2013 Ecma International +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the authors nor Ecma International may be used to +// endorse or promote products derived from this software without specific +// prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS +// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +// NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// * Ecma International Standards hereafter means Ecma International Standards +// as well as Ecma Technical Reports + + +// Note all the tests that check AsyncIterator's prototype itself and things +// that happen before stream conversion were not ported. +{ + // drop/length + assert.strictEqual(Readable.prototype.drop.length, 1); + const descriptor = Object.getOwnPropertyDescriptor( + Readable.prototype, + 'drop' + ); + assert.strictEqual(descriptor.enumerable, false); + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.writable, true); + // drop/limit-equals-total + const iterator = Readable.from([1, 2]).drop(2); + const result = await iterator[Symbol.asyncIterator]().next(); + assert.deepStrictEqual(result, { done: true, value: undefined }); + // drop/limit-greater-than-total.js + const iterator2 = Readable.from([1, 2]).drop(3); + const result2 = await iterator2[Symbol.asyncIterator]().next(); + assert.deepStrictEqual(result2, { done: true, value: undefined }); + // drop/limit-less-than-total.js + const iterator3 = Readable.from([1, 2]).drop(1); + const result3 = await iterator3[Symbol.asyncIterator]().next(); + assert.deepStrictEqual(result3, { done: false, value: 2 }); + // drop/limit-rangeerror + assert.throws(() => Readable.from([1]).drop(-1), RangeError); + assert.throws(() => { + Readable.from([1]).drop({ + valueOf() { + throw new Error('boom'); + } + }); + }, /boom/); + // drop/limit-tointeger + const two = await Readable.from([1, 2]).drop({ valueOf: () => 1 }).toArray(); + assert.deepStrictEqual(two, [2]); + // drop/name + assert.strictEqual(Readable.prototype.drop.name, 'drop'); + // drop/non-constructible + assert.throws(() => new Readable.prototype.drop(1), TypeError); + // drop/proto + const proto = Object.getPrototypeOf(Readable.prototype.drop); + assert.strictEqual(proto, Function.prototype); +} +{ + // every/abrupt-iterator-close + const stream = Readable.from([1, 2, 3]); + const e = new Error(); + await assert.rejects(stream.every(mustCall(() => { + throw e; + }, 1)), e); +} +{ + // every/callable-fn + await assert.rejects(Readable.from([1, 2]).every({}), TypeError); +} +{ + // every/callable + Readable.prototype.every.call(Readable.from([]), () => {}); + // eslint-disable-next-line array-callback-return + Readable.from([]).every(() => {}); + assert.throws(() => { + const r = Readable.from([]); + new r.every(() => {}); + }, TypeError); +} + +{ + // every/false + const iterator = Readable.from([1, 2, 3]); + const result = await iterator.every((v) => v === 1); + assert.strictEqual(result, false); +} +{ + // every/every + const iterator = Readable.from([1, 2, 3]); + const result = await iterator.every((v) => true); + assert.strictEqual(result, true); +} + +{ + // every/is-function + assert.strictEqual(typeof Readable.prototype.every, 'function'); +} +{ + // every/length + assert.strictEqual(Readable.prototype.every.length, 1); + // every/name + assert.strictEqual(Readable.prototype.every.name, 'every'); + // every/propdesc + const descriptor = Object.getOwnPropertyDescriptor( + Readable.prototype, + 'every' + ); + assert.strictEqual(descriptor.enumerable, false); + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.writable, true); +} diff --git a/test/js/node/test/parallel/test-stream-map.js b/test/js/node/test/parallel/test-stream-map.js new file mode 100644 index 0000000000..4a7a53c559 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-map.js @@ -0,0 +1,360 @@ +'use strict'; + +const common = require('../common'); +const { + Readable, +} = require('stream'); +const assert = require('assert'); +const { once } = require('events'); +const { setTimeout } = require('timers/promises'); + +function createDependentPromises(n) { + const promiseAndResolveArray = []; + + for (let i = 0; i < n; i++) { + let res; + const promise = new Promise((resolve) => { + if (i === 0) { + res = resolve; + return; + } + res = () => promiseAndResolveArray[i - 1][0].then(resolve); + }); + + promiseAndResolveArray.push([promise, res]); + } + + return promiseAndResolveArray; +} + +{ + // Map works on synchronous streams with a synchronous mapper + const stream = Readable.from([1, 2, 3, 4, 5]).map((x) => x + x); + (async () => { + assert.deepStrictEqual(await stream.toArray(), [2, 4, 6, 8, 10]); + })().then(common.mustCall()); +} + +{ + // Map works on synchronous streams with an asynchronous mapper + const stream = Readable.from([1, 2, 3, 4, 5]).map(async (x) => { + await Promise.resolve(); + return x + x; + }); + (async () => { + assert.deepStrictEqual(await stream.toArray(), [2, 4, 6, 8, 10]); + })().then(common.mustCall()); +} + +{ + // Map works on asynchronous streams with a asynchronous mapper + const stream = Readable.from([1, 2, 3, 4, 5]).map(async (x) => { + return x + x; + }).map((x) => x + x); + (async () => { + assert.deepStrictEqual(await stream.toArray(), [4, 8, 12, 16, 20]); + })().then(common.mustCall()); +} + +{ + // Map works on an infinite stream + const stream = Readable.from(async function* () { + while (true) yield 1; + }()).map(common.mustCall(async (x) => { + return x + x; + }, 5)); + (async () => { + let i = 1; + for await (const item of stream) { + assert.strictEqual(item, 2); + if (++i === 5) break; + } + })().then(common.mustCall()); +} + +{ + // Map works on non-objectMode streams + const stream = new Readable({ + read() { + this.push(Uint8Array.from([1])); + this.push(Uint8Array.from([2])); + this.push(null); + } + }).map(async ([x]) => { + return x + x; + }).map((x) => x + x); + const result = [4, 8]; + (async () => { + for await (const item of stream) { + assert.strictEqual(item, result.shift()); + } + })().then(common.mustCall()); +} + +{ + // Does not care about data events + const source = new Readable({ + read() { + this.push(Uint8Array.from([1])); + this.push(Uint8Array.from([2])); + this.push(null); + } + }); + setImmediate(() => stream.emit('data', Uint8Array.from([1]))); + const stream = source.map(async ([x]) => { + return x + x; + }).map((x) => x + x); + const result = [4, 8]; + (async () => { + for await (const item of stream) { + assert.strictEqual(item, result.shift()); + } + })().then(common.mustCall()); +} + +{ + // Emitting an error during `map` + const stream = Readable.from([1, 2, 3, 4, 5]).map(async (x) => { + if (x === 3) { + stream.emit('error', new Error('boom')); + } + return x + x; + }); + assert.rejects( + stream.map((x) => x + x).toArray(), + /boom/, + ).then(common.mustCall()); +} + +{ + // Throwing an error during `map` (sync) + const stream = Readable.from([1, 2, 3, 4, 5]).map((x) => { + if (x === 3) { + throw new Error('boom'); + } + return x + x; + }); + assert.rejects( + stream.map((x) => x + x).toArray(), + /boom/, + ).then(common.mustCall()); +} + + +{ + // Throwing an error during `map` (async) + const stream = Readable.from([1, 2, 3, 4, 5]).map(async (x) => { + if (x === 3) { + throw new Error('boom'); + } + return x + x; + }); + assert.rejects( + stream.map((x) => x + x).toArray(), + /boom/, + ).then(common.mustCall()); +} + +{ + // Concurrency + AbortSignal + const ac = new AbortController(); + const range = Readable.from([1, 2, 3, 4, 5]); + const stream = range.map(common.mustCall(async (_, { signal }) => { + await once(signal, 'abort'); + throw signal.reason; + }, 2), { signal: ac.signal, concurrency: 2, highWaterMark: 0 }); + // pump + assert.rejects(async () => { + for await (const item of stream) { + assert.fail('should not reach here, got ' + item); + } + }, { + name: 'AbortError', + }).then(common.mustCall()); + + setImmediate(() => { + ac.abort(); + }); +} + +{ + // Concurrency result order + const stream = Readable.from([1, 2]).map(async (item, { signal }) => { + await setTimeout(10 - item, { signal }); + return item; + }, { concurrency: 2 }); + + (async () => { + const expected = [1, 2]; + for await (const item of stream) { + assert.strictEqual(item, expected.shift()); + } + })().then(common.mustCall()); +} + + +{ + // highWaterMark with small concurrency + const finishOrder = []; + + const promises = createDependentPromises(4); + + const raw = Readable.from([2, 0, 1, 3]); + const stream = raw.map(async (item) => { + const [promise, resolve] = promises[item]; + resolve(); + + await promise; + finishOrder.push(item); + return item; + }, { concurrency: 2 }); + + (async () => { + await stream.toArray(); + + assert.deepStrictEqual(finishOrder, [0, 1, 2, 3]); + })().then(common.mustCall(), common.mustNotCall()); +} + +{ + // highWaterMark with a lot of items and large concurrency + const finishOrder = []; + + const promises = createDependentPromises(20); + + const input = [10, 1, 0, 3, 4, 2, 5, 7, 8, 9, 6, 11, 12, 13, 18, 15, 16, 17, 14, 19]; + const raw = Readable.from(input); + // Should be + // 10, 1, 0, 3, 4, 2 | next: 0 + // 10, 1, 3, 4, 2, 5 | next: 1 + // 10, 3, 4, 2, 5, 7 | next: 2 + // 10, 3, 4, 5, 7, 8 | next: 3 + // 10, 4, 5, 7, 8, 9 | next: 4 + // 10, 5, 7, 8, 9, 6 | next: 5 + // 10, 7, 8, 9, 6, 11 | next: 6 + // 10, 7, 8, 9, 11, 12 | next: 7 + // 10, 8, 9, 11, 12, 13 | next: 8 + // 10, 9, 11, 12, 13, 18 | next: 9 + // 10, 11, 12, 13, 18, 15 | next: 10 + // 11, 12, 13, 18, 15, 16 | next: 11 + // 12, 13, 18, 15, 16, 17 | next: 12 + // 13, 18, 15, 16, 17, 14 | next: 13 + // 18, 15, 16, 17, 14, 19 | next: 14 + // 18, 15, 16, 17, 19 | next: 15 + // 18, 16, 17, 19 | next: 16 + // 18, 17, 19 | next: 17 + // 18, 19 | next: 18 + // 19 | next: 19 + // + + const stream = raw.map(async (item) => { + const [promise, resolve] = promises[item]; + resolve(); + + await promise; + finishOrder.push(item); + return item; + }, { concurrency: 6 }); + + (async () => { + const outputOrder = await stream.toArray(); + + assert.deepStrictEqual(outputOrder, input); + assert.deepStrictEqual(finishOrder, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); + })().then(common.mustCall(), common.mustNotCall()); +} + +{ + // Custom highWaterMark with a lot of items and large concurrency + const finishOrder = []; + + const promises = createDependentPromises(20); + + const input = [11, 1, 0, 3, 4, 2, 5, 7, 8, 9, 6, 10, 12, 13, 18, 15, 16, 17, 14, 19]; + const raw = Readable.from(input); + // Should be + // 11, 1, 0, 3, 4 | next: 0, buffer: [] + // 11, 1, 3, 4, 2 | next: 1, buffer: [0] + // 11, 3, 4, 2, 5 | next: 2, buffer: [0, 1] + // 11, 3, 4, 5, 7 | next: 3, buffer: [0, 1, 2] + // 11, 4, 5, 7, 8 | next: 4, buffer: [0, 1, 2, 3] + // 11, 5, 7, 8, 9 | next: 5, buffer: [0, 1, 2, 3, 4] + // 11, 7, 8, 9, 6 | next: 6, buffer: [0, 1, 2, 3, 4, 5] + // 11, 7, 8, 9, 10 | next: 7, buffer: [0, 1, 2, 3, 4, 5, 6] -- buffer full + // 11, 8, 9, 10, 12 | next: 8, buffer: [0, 1, 2, 3, 4, 5, 6] + // 11, 9, 10, 12, 13 | next: 9, buffer: [0, 1, 2, 3, 4, 5, 6] + // 11, 10, 12, 13, 18 | next: 10, buffer: [0, 1, 2, 3, 4, 5, 6] + // 11, 12, 13, 18, 15 | next: 11, buffer: [0, 1, 2, 3, 4, 5, 6] + // 12, 13, 18, 15, 16 | next: 12, buffer: [] -- all items flushed as 11 is consumed and all the items wait for it + // 13, 18, 15, 16, 17 | next: 13, buffer: [] + // 18, 15, 16, 17, 14 | next: 14, buffer: [] + // 18, 15, 16, 17, 19 | next: 15, buffer: [14] + // 18, 16, 17, 19 | next: 16, buffer: [14, 15] + // 18, 17, 19 | next: 17, buffer: [14, 15, 16] + // 18, 19 | next: 18, buffer: [14, 15, 16, 17] + // 19 | next: 19, buffer: [] -- all items flushed + // + + const stream = raw.map(async (item) => { + const [promise, resolve] = promises[item]; + resolve(); + + await promise; + finishOrder.push(item); + return item; + }, { concurrency: 5, highWaterMark: 7 }); + + (async () => { + const outputOrder = await stream.toArray(); + + assert.deepStrictEqual(outputOrder, input); + assert.deepStrictEqual(finishOrder, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); + })().then(common.mustCall(), common.mustNotCall()); +} + +{ + // Where there is a delay between the first and the next item it should not wait for filled queue + // before yielding to the user + const promises = createDependentPromises(3); + + const raw = Readable.from([0, 1, 2]); + + const stream = raw + .map(async (item) => { + if (item !== 0) { + await promises[item][0]; + } + + return item; + }, { concurrency: 2 }) + .map((item) => { + // eslint-disable-next-line no-unused-vars + for (const [_, resolve] of promises) { + resolve(); + } + + return item; + }); + + (async () => { + await stream.toArray(); + })().then(common.mustCall(), common.mustNotCall()); +} + +{ + // Error cases + assert.throws(() => Readable.from([1]).map(1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => Readable.from([1]).map((x) => x, { + concurrency: 'Foo' + }), /ERR_OUT_OF_RANGE/); + assert.throws(() => Readable.from([1]).map((x) => x, { + concurrency: -1 + }), /ERR_OUT_OF_RANGE/); + assert.throws(() => Readable.from([1]).map((x) => x, 1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => Readable.from([1]).map((x) => x, { signal: true }), /ERR_INVALID_ARG_TYPE/); +} +{ + // Test result is a Readable + const stream = Readable.from([1, 2, 3, 4, 5]).map((x) => x); + assert.strictEqual(stream.readable, true); +} diff --git a/test/js/node/test/parallel/test-stream-objectmode-undefined.js b/test/js/node/test/parallel/test-stream-objectmode-undefined.js new file mode 100644 index 0000000000..64b960f92b --- /dev/null +++ b/test/js/node/test/parallel/test-stream-objectmode-undefined.js @@ -0,0 +1,44 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable, Writable, Transform } = require('stream'); + +{ + const stream = new Readable({ + objectMode: true, + read: common.mustCall(() => { + stream.push(undefined); + stream.push(null); + }) + }); + + stream.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk, undefined); + })); +} + +{ + const stream = new Writable({ + objectMode: true, + write: common.mustCall((chunk) => { + assert.strictEqual(chunk, undefined); + }) + }); + + stream.write(undefined); +} + +{ + const stream = new Transform({ + objectMode: true, + transform: common.mustCall((chunk) => { + stream.push(chunk); + }) + }); + + stream.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk, undefined); + })); + + stream.write(undefined); +} diff --git a/test/js/node/test/parallel/test-stream-once-readable-pipe.js b/test/js/node/test/parallel/test-stream-once-readable-pipe.js new file mode 100644 index 0000000000..e8f4e9422d --- /dev/null +++ b/test/js/node/test/parallel/test-stream-once-readable-pipe.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable, Writable } = require('stream'); + +// This test ensures that if have 'readable' listener +// on Readable instance it will not disrupt the pipe. + +{ + let receivedData = ''; + const w = new Writable({ + write: (chunk, env, callback) => { + receivedData += chunk; + callback(); + }, + }); + + const data = ['foo', 'bar', 'baz']; + const r = new Readable({ + read: () => {}, + }); + + r.once('readable', common.mustCall()); + + r.pipe(w); + r.push(data[0]); + r.push(data[1]); + r.push(data[2]); + r.push(null); + + w.on('finish', common.mustCall(() => { + assert.strictEqual(receivedData, data.join('')); + })); +} + +{ + let receivedData = ''; + const w = new Writable({ + write: (chunk, env, callback) => { + receivedData += chunk; + callback(); + }, + }); + + const data = ['foo', 'bar', 'baz']; + const r = new Readable({ + read: () => {}, + }); + + r.pipe(w); + r.push(data[0]); + r.push(data[1]); + r.push(data[2]); + r.push(null); + r.once('readable', common.mustCall()); + + w.on('finish', common.mustCall(() => { + assert.strictEqual(receivedData, data.join('')); + })); +} diff --git a/test/js/node/test/parallel/test-stream-passthrough-drain.js b/test/js/node/test/parallel/test-stream-passthrough-drain.js new file mode 100644 index 0000000000..244bf87407 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-passthrough-drain.js @@ -0,0 +1,10 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { PassThrough } = require('stream'); + +const pt = new PassThrough({ highWaterMark: 0 }); +pt.on('drain', common.mustCall()); +assert(!pt.write('hello1')); +pt.read(); +pt.read(); diff --git a/test/js/node/test/parallel/test-stream-pipe-after-end.js b/test/js/node/test/parallel/test-stream-pipe-after-end.js new file mode 100644 index 0000000000..045d27e085 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-after-end.js @@ -0,0 +1,69 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable, Writable } = require('stream'); + +class TestReadable extends Readable { + constructor(opt) { + super(opt); + this._ended = false; + } + + _read() { + if (this._ended) + this.emit('error', new Error('_read called twice')); + this._ended = true; + this.push(null); + } +} + +class TestWritable extends Writable { + constructor(opt) { + super(opt); + this._written = []; + } + + _write(chunk, encoding, cb) { + this._written.push(chunk); + cb(); + } +} + +// This one should not emit 'end' until we read() from it later. +const ender = new TestReadable(); + +// What happens when you pipe() a Readable that's already ended? +const piper = new TestReadable(); +// pushes EOF null, and length=0, so this will trigger 'end' +piper.read(); + +setTimeout(common.mustCall(function() { + ender.on('end', common.mustCall()); + const c = ender.read(); + assert.strictEqual(c, null); + + const w = new TestWritable(); + w.on('finish', common.mustCall()); + piper.pipe(w); +}), 1); diff --git a/test/js/node/test/parallel/test-stream-pipe-await-drain-manual-resume.js b/test/js/node/test/parallel/test-stream-pipe-await-drain-manual-resume.js new file mode 100644 index 0000000000..a95a5e05ae --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-await-drain-manual-resume.js @@ -0,0 +1,75 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +// A consumer stream with a very low highWaterMark, which starts in a state +// where it buffers the chunk it receives rather than indicating that they +// have been consumed. +const writable = new stream.Writable({ + highWaterMark: 5 +}); + +let isCurrentlyBufferingWrites = true; +const queue = []; + +writable._write = (chunk, encoding, cb) => { + if (isCurrentlyBufferingWrites) + queue.push({ chunk, cb }); + else + cb(); +}; + +const readable = new stream.Readable({ + read() {} +}); + +readable.pipe(writable); + +readable.once('pause', common.mustCall(() => { + assert.strictEqual( + readable._readableState.awaitDrainWriters, + writable, + 'Expected awaitDrainWriters to be a Writable but instead got ' + + `${readable._readableState.awaitDrainWriters}` + ); + // First pause, resume manually. The next write() to writable will still + // return false, because chunks are still being buffered, so it will increase + // the awaitDrain counter again. + + process.nextTick(common.mustCall(() => { + readable.resume(); + })); + + readable.once('pause', common.mustCall(() => { + assert.strictEqual( + readable._readableState.awaitDrainWriters, + writable, + '.resume() should not reset the awaitDrainWriters, but instead got ' + + `${readable._readableState.awaitDrainWriters}` + ); + // Second pause, handle all chunks from now on. Once all callbacks that + // are currently queued up are handled, the awaitDrain drain counter should + // fall back to 0 and all chunks that are pending on the readable side + // should be flushed. + isCurrentlyBufferingWrites = false; + for (const queued of queue) + queued.cb(); + })); +})); + +readable.push(Buffer.alloc(100)); // Fill the writable HWM, first 'pause'. +readable.push(Buffer.alloc(100)); // Second 'pause'. +readable.push(Buffer.alloc(100)); // Should get through to the writable. +readable.push(null); + +writable.on('finish', common.mustCall(() => { + assert.strictEqual( + readable._readableState.awaitDrainWriters, + null, + `awaitDrainWriters should be reset to null + after all chunks are written but instead got + ${readable._readableState.awaitDrainWriters}` + ); + // Everything okay, all chunks were written. +})); diff --git a/test/js/node/test/parallel/test-stream-pipe-await-drain-push-while-write.js b/test/js/node/test/parallel/test-stream-pipe-await-drain-push-while-write.js new file mode 100644 index 0000000000..089767166c --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-await-drain-push-while-write.js @@ -0,0 +1,38 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +const writable = new stream.Writable({ + highWaterMark: 16 * 1024, + write: common.mustCall(function(chunk, encoding, cb) { + assert.strictEqual( + readable._readableState.awaitDrainWriters, + null, + ); + + if (chunk.length === 32 * 1024) { // first chunk + readable.push(Buffer.alloc(34 * 1024)); // above hwm + // We should check if awaitDrain counter is increased in the next + // tick, because awaitDrain is incremented after this method finished + process.nextTick(() => { + assert.strictEqual(readable._readableState.awaitDrainWriters, writable); + }); + } + + process.nextTick(cb); + }, 3) +}); + +// A readable stream which produces two buffers. +const bufs = [Buffer.alloc(32 * 1024), Buffer.alloc(33 * 1024)]; // above hwm +const readable = new stream.Readable({ + highWaterMark: 16 * 1024, + read: function() { + while (bufs.length > 0) { + this.push(bufs.shift()); + } + } +}); + +readable.pipe(writable); diff --git a/test/js/node/test/parallel/test-stream-pipe-await-drain.js b/test/js/node/test/parallel/test-stream-pipe-await-drain.js new file mode 100644 index 0000000000..35b86f67f9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-await-drain.js @@ -0,0 +1,67 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +// This is very similar to test-stream-pipe-cleanup-pause.js. + +const reader = new stream.Readable(); +const writer1 = new stream.Writable(); +const writer2 = new stream.Writable(); +const writer3 = new stream.Writable(); + +// 560000 is chosen here because it is larger than the (default) highWaterMark +// and will cause `.write()` to return false +// See: https://github.com/nodejs/node/issues/5820 +const buffer = Buffer.allocUnsafe(560000); + +reader._read = () => {}; + +writer1._write = common.mustCall(function(chunk, encoding, cb) { + this.emit('chunk-received'); + process.nextTick(cb); +}, 1); + +writer1.once('chunk-received', () => { + assert.strictEqual( + reader._readableState.awaitDrainWriters.size, + 0, + 'awaitDrain initial value should be 0, actual is ' + + reader._readableState.awaitDrainWriters.size + ); + setImmediate(() => { + // This one should *not* get through to writer1 because writer2 is not + // "done" processing. + reader.push(buffer); + }); +}); + +// A "slow" consumer: +writer2._write = common.mustCall((chunk, encoding, cb) => { + assert.strictEqual( + reader._readableState.awaitDrainWriters.size, + 1, + 'awaitDrain should be 1 after first push, actual is ' + + reader._readableState.awaitDrainWriters.size + ); + // Not calling cb here to "simulate" slow stream. + // This should be called exactly once, since the first .write() call + // will return false. +}, 1); + +writer3._write = common.mustCall((chunk, encoding, cb) => { + assert.strictEqual( + reader._readableState.awaitDrainWriters.size, + 2, + 'awaitDrain should be 2 after second push, actual is ' + + reader._readableState.awaitDrainWriters.size + ); + // Not calling cb here to "simulate" slow stream. + // This should be called exactly once, since the first .write() call + // will return false. +}, 1); + +reader.pipe(writer1); +reader.pipe(writer2); +reader.pipe(writer3); +reader.push(buffer); diff --git a/test/js/node/test/parallel/test-stream-pipe-cleanup-pause.js b/test/js/node/test/parallel/test-stream-pipe-cleanup-pause.js new file mode 100644 index 0000000000..3cdab94648 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-cleanup-pause.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); + +const reader = new stream.Readable(); +const writer1 = new stream.Writable(); +const writer2 = new stream.Writable(); + +// 560000 is chosen here because it is larger than the (default) highWaterMark +// and will cause `.write()` to return false +// See: https://github.com/nodejs/node/issues/2323 +const buffer = Buffer.allocUnsafe(560000); + +reader._read = () => {}; + +writer1._write = common.mustCall(function(chunk, encoding, cb) { + this.emit('chunk-received'); + cb(); +}, 1); +writer1.once('chunk-received', function() { + reader.unpipe(writer1); + reader.pipe(writer2); + reader.push(buffer); + setImmediate(function() { + reader.push(buffer); + setImmediate(function() { + reader.push(buffer); + }); + }); +}); + +writer2._write = common.mustCall(function(chunk, encoding, cb) { + cb(); +}, 3); + +reader.pipe(writer1); +reader.push(buffer); diff --git a/test/js/node/test/parallel/test-stream-pipe-cleanup.js b/test/js/node/test/parallel/test-stream-pipe-cleanup.js new file mode 100644 index 0000000000..cdb4d503d6 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-cleanup.js @@ -0,0 +1,125 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// This test asserts that Stream.prototype.pipe does not leave listeners +// hanging on the source or dest. +require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +function Writable() { + this.writable = true; + this.endCalls = 0; + stream.Stream.call(this); +} +Object.setPrototypeOf(Writable.prototype, stream.Stream.prototype); +Object.setPrototypeOf(Writable, stream.Stream); +Writable.prototype.end = function() { + this.endCalls++; +}; + +Writable.prototype.destroy = function() { + this.endCalls++; +}; + +function Readable() { + this.readable = true; + stream.Stream.call(this); +} +Object.setPrototypeOf(Readable.prototype, stream.Stream.prototype); +Object.setPrototypeOf(Readable, stream.Stream); + +function Duplex() { + this.readable = true; + Writable.call(this); +} +Object.setPrototypeOf(Duplex.prototype, Writable.prototype); +Object.setPrototypeOf(Duplex, Writable); + +let i = 0; +const limit = 100; + +let w = new Writable(); + +let r; + +for (i = 0; i < limit; i++) { + r = new Readable(); + r.pipe(w); + r.emit('end'); +} +assert.strictEqual(r.listeners('end').length, 0); +assert.strictEqual(w.endCalls, limit); + +w.endCalls = 0; + +for (i = 0; i < limit; i++) { + r = new Readable(); + r.pipe(w); + r.emit('close'); +} +assert.strictEqual(r.listeners('close').length, 0); +assert.strictEqual(w.endCalls, limit); + +w.endCalls = 0; + +r = new Readable(); + +for (i = 0; i < limit; i++) { + w = new Writable(); + r.pipe(w); + w.emit('close'); +} +assert.strictEqual(w.listeners('close').length, 0); + +r = new Readable(); +w = new Writable(); +const d = new Duplex(); +r.pipe(d); // pipeline A +d.pipe(w); // pipeline B +assert.strictEqual(r.listeners('end').length, 2); // A.onend, A.cleanup +assert.strictEqual(r.listeners('close').length, 2); // A.onclose, A.cleanup +assert.strictEqual(d.listeners('end').length, 2); // B.onend, B.cleanup +// A.cleanup, B.onclose, B.cleanup +assert.strictEqual(d.listeners('close').length, 3); +assert.strictEqual(w.listeners('end').length, 0); +assert.strictEqual(w.listeners('close').length, 1); // B.cleanup + +r.emit('end'); +assert.strictEqual(d.endCalls, 1); +assert.strictEqual(w.endCalls, 0); +assert.strictEqual(r.listeners('end').length, 0); +assert.strictEqual(r.listeners('close').length, 0); +assert.strictEqual(d.listeners('end').length, 2); // B.onend, B.cleanup +assert.strictEqual(d.listeners('close').length, 2); // B.onclose, B.cleanup +assert.strictEqual(w.listeners('end').length, 0); +assert.strictEqual(w.listeners('close').length, 1); // B.cleanup + +d.emit('end'); +assert.strictEqual(d.endCalls, 1); +assert.strictEqual(w.endCalls, 1); +assert.strictEqual(r.listeners('end').length, 0); +assert.strictEqual(r.listeners('close').length, 0); +assert.strictEqual(d.listeners('end').length, 0); +assert.strictEqual(d.listeners('close').length, 0); +assert.strictEqual(w.listeners('end').length, 0); +assert.strictEqual(w.listeners('close').length, 0); diff --git a/test/js/node/test/parallel/test-stream-pipe-deadlock.js b/test/js/node/test/parallel/test-stream-pipe-deadlock.js new file mode 100644 index 0000000000..bf75445877 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-deadlock.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +const { Readable, Writable } = require('stream'); + +// https://github.com/nodejs/node/issues/48666 +(async () => { + // Prepare src that is internally ended, with buffered data pending + const src = new Readable({ read() {} }); + src.push(Buffer.alloc(100)); + src.push(null); + src.pause(); + + // Give it time to settle + await new Promise((resolve) => setImmediate(resolve)); + + const dst = new Writable({ + highWaterMark: 1000, + write(buf, enc, cb) { + process.nextTick(cb); + } + }); + + dst.write(Buffer.alloc(1000)); // Fill write buffer + dst.on('finish', common.mustCall()); + src.pipe(dst); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-stream-pipe-error-handling.js b/test/js/node/test/parallel/test-stream-pipe-error-handling.js new file mode 100644 index 0000000000..cf3a3699d0 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-error-handling.js @@ -0,0 +1,124 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Stream, PassThrough } = require('stream'); + +{ + const source = new Stream(); + const dest = new Stream(); + + source.pipe(dest); + + let gotErr = null; + source.on('error', function(err) { + gotErr = err; + }); + + const err = new Error('This stream turned into bacon.'); + source.emit('error', err); + assert.strictEqual(gotErr, err); +} + +{ + const source = new Stream(); + const dest = new Stream(); + + source.pipe(dest); + + const err = new Error('This stream turned into bacon.'); + + let gotErr = null; + try { + source.emit('error', err); + } catch (e) { + gotErr = e; + } + + assert.strictEqual(gotErr, err); +} + +{ + const R = Stream.Readable; + const W = Stream.Writable; + + const r = new R({ autoDestroy: false }); + const w = new W({ autoDestroy: false }); + let removed = false; + + r._read = common.mustCall(function() { + setTimeout(common.mustCall(function() { + assert(removed); + assert.throws(function() { + w.emit('error', new Error('fail')); + }, /^Error: fail$/); + }), 1); + }); + + w.on('error', myOnError); + r.pipe(w); + w.removeListener('error', myOnError); + removed = true; + + function myOnError() { + throw new Error('this should not happen'); + } +} + +{ + const R = Stream.Readable; + const W = Stream.Writable; + + const r = new R(); + const w = new W(); + let removed = false; + + r._read = common.mustCall(function() { + setTimeout(common.mustCall(function() { + assert(removed); + w.emit('error', new Error('fail')); + }), 1); + }); + + w.on('error', common.mustCall()); + w._write = () => {}; + + r.pipe(w); + // Removing some OTHER random listener should not do anything + w.removeListener('error', () => {}); + removed = true; +} + +{ + const _err = new Error('this should be handled'); + const destination = new PassThrough(); + destination.once('error', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + + const stream = new Stream(); + stream + .pipe(destination); + + destination.destroy(_err); +} diff --git a/test/js/node/test/parallel/test-stream-pipe-error-unhandled.js b/test/js/node/test/parallel/test-stream-pipe-error-unhandled.js new file mode 100644 index 0000000000..42c1ce77fe --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-error-unhandled.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable, Writable } = require('stream'); + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err.message, 'asd'); +})); + +const r = new Readable({ + read() { + this.push('asd'); + } +}); +const w = new Writable({ + autoDestroy: true, + write() {} +}); + +r.pipe(w); +w.destroy(new Error('asd')); diff --git a/test/js/node/test/parallel/test-stream-pipe-event.js b/test/js/node/test/parallel/test-stream-pipe-event.js new file mode 100644 index 0000000000..d7772df6a1 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-event.js @@ -0,0 +1,51 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +function Writable() { + this.writable = true; + stream.Stream.call(this); +} +Object.setPrototypeOf(Writable.prototype, stream.Stream.prototype); +Object.setPrototypeOf(Writable, stream.Stream); + +function Readable() { + this.readable = true; + stream.Stream.call(this); +} +Object.setPrototypeOf(Readable.prototype, stream.Stream.prototype); +Object.setPrototypeOf(Readable, stream.Stream); + +let passed = false; + +const w = new Writable(); +w.on('pipe', function(src) { + passed = true; +}); + +const r = new Readable(); +r.pipe(w); + +assert.ok(passed); diff --git a/test/js/node/test/parallel/test-stream-pipe-flow-after-unpipe.js b/test/js/node/test/parallel/test-stream-pipe-flow-after-unpipe.js new file mode 100644 index 0000000000..048b7ea5e5 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-flow-after-unpipe.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const { Readable, Writable } = require('stream'); + +// Tests that calling .unpipe() un-blocks a stream that is paused because +// it is waiting on the writable side to finish a write(). + +const rs = new Readable({ + highWaterMark: 1, + // That this gets called at least 20 times is the real test here. + read: common.mustCallAtLeast(() => rs.push('foo'), 20) +}); + +const ws = new Writable({ + highWaterMark: 1, + write: common.mustCall(() => { + // Ignore the callback, this write() simply never finishes. + setImmediate(() => rs.unpipe(ws)); + }) +}); + +let chunks = 0; +rs.on('data', common.mustCallAtLeast(() => { + chunks++; + if (chunks >= 20) + rs.pause(); // Finish this test. +})); + +rs.pipe(ws); diff --git a/test/js/node/test/parallel/test-stream-pipe-flow.js b/test/js/node/test/parallel/test-stream-pipe-flow.js new file mode 100644 index 0000000000..1f2e8f54ce --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-flow.js @@ -0,0 +1,90 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable, Writable, PassThrough } = require('stream'); + +{ + let ticks = 17; + + const rs = new Readable({ + objectMode: true, + read: () => { + if (ticks-- > 0) + return process.nextTick(() => rs.push({})); + rs.push({}); + rs.push(null); + } + }); + + const ws = new Writable({ + highWaterMark: 0, + objectMode: true, + write: (data, end, cb) => setImmediate(cb) + }); + + rs.on('end', common.mustCall()); + ws.on('finish', common.mustCall()); + rs.pipe(ws); +} + +{ + let missing = 8; + + const rs = new Readable({ + objectMode: true, + read: () => { + if (missing--) rs.push({}); + else rs.push(null); + } + }); + + const pt = rs + .pipe(new PassThrough({ objectMode: true, highWaterMark: 2 })) + .pipe(new PassThrough({ objectMode: true, highWaterMark: 2 })); + + pt.on('end', () => { + wrapper.push(null); + }); + + const wrapper = new Readable({ + objectMode: true, + read: () => { + process.nextTick(() => { + let data = pt.read(); + if (data === null) { + pt.once('readable', () => { + data = pt.read(); + if (data !== null) wrapper.push(data); + }); + } else { + wrapper.push(data); + } + }); + } + }); + + wrapper.resume(); + wrapper.on('end', common.mustCall()); +} + +{ + // Only register drain if there is backpressure. + const rs = new Readable({ read() {} }); + + const pt = rs + .pipe(new PassThrough({ objectMode: true, highWaterMark: 2 })); + assert.strictEqual(pt.listenerCount('drain'), 0); + pt.on('finish', () => { + assert.strictEqual(pt.listenerCount('drain'), 0); + }); + + rs.push('asd'); + assert.strictEqual(pt.listenerCount('drain'), 0); + + process.nextTick(() => { + rs.push('asd'); + assert.strictEqual(pt.listenerCount('drain'), 0); + rs.push(null); + assert.strictEqual(pt.listenerCount('drain'), 0); + }); +} diff --git a/test/js/node/test/parallel/test-stream-pipe-manual-resume.js b/test/js/node/test/parallel/test-stream-pipe-manual-resume.js new file mode 100644 index 0000000000..08269acfd3 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-manual-resume.js @@ -0,0 +1,35 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); + +function test(throwCodeInbetween) { + // Check that a pipe does not stall if .read() is called unexpectedly + // (i.e. the stream is not resumed by the pipe). + + const n = 1000; + let counter = n; + const rs = stream.Readable({ + objectMode: true, + read: common.mustCallAtLeast(() => { + if (--counter >= 0) + rs.push({ counter }); + else + rs.push(null); + }, n) + }); + + const ws = stream.Writable({ + objectMode: true, + write: common.mustCall((data, enc, cb) => { + setImmediate(cb); + }, n) + }); + + setImmediate(() => throwCodeInbetween(rs, ws)); + + rs.pipe(ws); +} + +test((rs) => rs.read()); +test((rs) => rs.resume()); +test(() => 0); diff --git a/test/js/node/test/parallel/test-stream-pipe-multiple-pipes.js b/test/js/node/test/parallel/test-stream-pipe-multiple-pipes.js new file mode 100644 index 0000000000..890c274b9d --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-multiple-pipes.js @@ -0,0 +1,51 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +const readable = new stream.Readable({ + read: () => {} +}); + +const writables = []; + +for (let i = 0; i < 5; i++) { + const target = new stream.Writable({ + write: common.mustCall((chunk, encoding, callback) => { + target.output.push(chunk); + callback(); + }, 1) + }); + target.output = []; + + target.on('pipe', common.mustCall()); + readable.pipe(target); + + + writables.push(target); +} + +const input = Buffer.from([1, 2, 3, 4, 5]); + +readable.push(input); + +// The pipe() calls will postpone emission of the 'resume' event using nextTick, +// so no data will be available to the writable streams until then. +process.nextTick(common.mustCall(() => { + for (const target of writables) { + assert.deepStrictEqual(target.output, [input]); + + target.on('unpipe', common.mustCall()); + readable.unpipe(target); + } + + readable.push('something else'); // This does not get through. + readable.push(null); + readable.resume(); // Make sure the 'end' event gets emitted. +})); + +readable.on('end', common.mustCall(() => { + for (const target of writables) { + assert.deepStrictEqual(target.output, [input]); + } +})); diff --git a/test/js/node/test/parallel/test-stream-pipe-needDrain.js b/test/js/node/test/parallel/test-stream-pipe-needDrain.js new file mode 100644 index 0000000000..7faf45417a --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-needDrain.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable, Writable } = require('stream'); + +// Pipe should pause temporarily if writable needs drain. +{ + const w = new Writable({ + write(buf, encoding, callback) { + process.nextTick(callback); + }, + highWaterMark: 1 + }); + + while (w.write('asd')); + + assert.strictEqual(w.writableNeedDrain, true); + + const r = new Readable({ + read() { + this.push('asd'); + this.push(null); + } + }); + + r.on('pause', common.mustCall(2)); + r.on('end', common.mustCall()); + + r.pipe(w); +} diff --git a/test/js/node/test/parallel/test-stream-pipe-same-destination-twice.js b/test/js/node/test/parallel/test-stream-pipe-same-destination-twice.js new file mode 100644 index 0000000000..ff71639588 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-same-destination-twice.js @@ -0,0 +1,78 @@ +'use strict'; +const common = require('../common'); + +// Regression test for https://github.com/nodejs/node/issues/12718. +// Tests that piping a source stream twice to the same destination stream +// works, and that a subsequent unpipe() call only removes the pipe *once*. +const assert = require('assert'); +const { PassThrough, Writable } = require('stream'); + +{ + const passThrough = new PassThrough(); + const dest = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + assert.strictEqual(`${chunk}`, 'foobar'); + cb(); + }) + }); + + passThrough.pipe(dest); + passThrough.pipe(dest); + + assert.strictEqual(passThrough._events.data.length, 2); + assert.strictEqual(passThrough._readableState.pipes.length, 2); + assert.strictEqual(passThrough._readableState.pipes[0], dest); + assert.strictEqual(passThrough._readableState.pipes[1], dest); + + passThrough.unpipe(dest); + + assert.strictEqual(passThrough._events.data.length, 1); + assert.strictEqual(passThrough._readableState.pipes.length, 1); + assert.deepStrictEqual(passThrough._readableState.pipes, [dest]); + + passThrough.write('foobar'); + passThrough.pipe(dest); +} + +{ + const passThrough = new PassThrough(); + const dest = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + assert.strictEqual(`${chunk}`, 'foobar'); + cb(); + }, 2) + }); + + passThrough.pipe(dest); + passThrough.pipe(dest); + + assert.strictEqual(passThrough._events.data.length, 2); + assert.strictEqual(passThrough._readableState.pipes.length, 2); + assert.strictEqual(passThrough._readableState.pipes[0], dest); + assert.strictEqual(passThrough._readableState.pipes[1], dest); + + passThrough.write('foobar'); +} + +{ + const passThrough = new PassThrough(); + const dest = new Writable({ + write: common.mustNotCall() + }); + + passThrough.pipe(dest); + passThrough.pipe(dest); + + assert.strictEqual(passThrough._events.data.length, 2); + assert.strictEqual(passThrough._readableState.pipes.length, 2); + assert.strictEqual(passThrough._readableState.pipes[0], dest); + assert.strictEqual(passThrough._readableState.pipes[1], dest); + + passThrough.unpipe(dest); + passThrough.unpipe(dest); + + assert.strictEqual(passThrough._events.data, undefined); + assert.strictEqual(passThrough._readableState.pipes.length, 0); + + passThrough.write('foobar'); +} diff --git a/test/js/node/test/parallel/test-stream-pipe-unpipe-streams.js b/test/js/node/test/parallel/test-stream-pipe-unpipe-streams.js new file mode 100644 index 0000000000..74c4353993 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-unpipe-streams.js @@ -0,0 +1,95 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Readable, Writable } = require('stream'); + +const source = Readable({ read: () => {} }); +const dest1 = Writable({ write: () => {} }); +const dest2 = Writable({ write: () => {} }); + +source.pipe(dest1); +source.pipe(dest2); + +dest1.on('unpipe', common.mustCall()); +dest2.on('unpipe', common.mustCall()); + +assert.strictEqual(source._readableState.pipes[0], dest1); +assert.strictEqual(source._readableState.pipes[1], dest2); +assert.strictEqual(source._readableState.pipes.length, 2); + +// Should be able to unpipe them in the reverse order that they were piped. + +source.unpipe(dest2); + +assert.deepStrictEqual(source._readableState.pipes, [dest1]); +assert.notStrictEqual(source._readableState.pipes, dest2); + +dest2.on('unpipe', common.mustNotCall()); +source.unpipe(dest2); + +source.unpipe(dest1); + +assert.strictEqual(source._readableState.pipes.length, 0); + +{ + // Test `cleanup()` if we unpipe all streams. + const source = Readable({ read: () => {} }); + const dest1 = Writable({ write: () => {} }); + const dest2 = Writable({ write: () => {} }); + + let destCount = 0; + const srcCheckEventNames = ['end', 'data']; + const destCheckEventNames = ['close', 'finish', 'drain', 'error', 'unpipe']; + + const checkSrcCleanup = common.mustCall(() => { + assert.strictEqual(source._readableState.pipes.length, 0); + assert.strictEqual(source._readableState.flowing, false); + for (const eventName of srcCheckEventNames) { + assert.strictEqual( + source.listenerCount(eventName), 0, + `source's '${eventName}' event listeners not removed` + ); + } + }); + + function checkDestCleanup(dest) { + const currentDestId = ++destCount; + source.pipe(dest); + + const unpipeChecker = common.mustCall(() => { + assert.deepStrictEqual( + dest.listeners('unpipe'), [unpipeChecker], + `destination{${currentDestId}} should have a 'unpipe' event ` + + 'listener which is `unpipeChecker`' + ); + dest.removeListener('unpipe', unpipeChecker); + for (const eventName of destCheckEventNames) { + assert.strictEqual( + dest.listenerCount(eventName), 0, + `destination{${currentDestId}}'s '${eventName}' event ` + + 'listeners not removed' + ); + } + + if (--destCount === 0) + checkSrcCleanup(); + }); + + dest.on('unpipe', unpipeChecker); + } + + checkDestCleanup(dest1); + checkDestCleanup(dest2); + source.unpipe(); +} + +{ + const src = Readable({ read: () => {} }); + const dst = Writable({ write: () => {} }); + src.pipe(dst); + src.on('resume', common.mustCall(() => { + src.on('pause', common.mustCall()); + src.unpipe(dst); + })); +} diff --git a/test/js/node/test/parallel/test-stream-pipe-without-listenerCount.js b/test/js/node/test/parallel/test-stream-pipe-without-listenerCount.js new file mode 100644 index 0000000000..c2b73c74b1 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipe-without-listenerCount.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); + +const r = new stream.Stream(); +r.listenerCount = undefined; + +const w = new stream.Stream(); +w.listenerCount = undefined; + +w.on('pipe', function() { + r.emit('error', new Error('Readable Error')); + w.emit('error', new Error('Writable Error')); +}); +r.on('error', common.mustCall()); +w.on('error', common.mustCall()); +r.pipe(w); diff --git a/test/js/node/test/parallel/test-stream-pipeline-async-iterator.js b/test/js/node/test/parallel/test-stream-pipeline-async-iterator.js new file mode 100644 index 0000000000..06a2ed6ca8 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipeline-async-iterator.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const { Readable, PassThrough, pipeline } = require('stream'); +const assert = require('assert'); + +const _err = new Error('kaboom'); + +async function run() { + const source = new Readable({ + read() { + } + }); + source.push('hello'); + source.push('world'); + + setImmediate(() => { source.destroy(_err); }); + + const iterator = pipeline( + source, + new PassThrough(), + () => {}); + + iterator.setEncoding('utf8'); + + for await (const k of iterator) { + assert.strictEqual(k, 'helloworld'); + } +} + +run().catch(common.mustCall((err) => assert.strictEqual(err, _err))); diff --git a/test/js/node/test/parallel/test-stream-pipeline-duplex.js b/test/js/node/test/parallel/test-stream-pipeline-duplex.js new file mode 100644 index 0000000000..0dbd27a717 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipeline-duplex.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const { pipeline, Duplex, PassThrough } = require('stream'); +const assert = require('assert'); + +const remote = new PassThrough(); +const local = new Duplex({ + read() {}, + write(chunk, enc, callback) { + callback(); + } +}); + +pipeline(remote, local, remote, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); +})); + +setImmediate(() => { + remote.end(); +}); diff --git a/test/js/node/test/parallel/test-stream-pipeline-listeners.js b/test/js/node/test/parallel/test-stream-pipeline-listeners.js new file mode 100644 index 0000000000..81e287b77c --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipeline-listeners.js @@ -0,0 +1,76 @@ +'use strict'; + +const common = require('../common'); +const { pipeline, Duplex, PassThrough, Writable } = require('stream'); +const assert = require('assert'); + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err.message, 'no way'); +}, 2)); + +// Ensure that listeners is removed if last stream is readable +// And other stream's listeners unchanged +const a = new PassThrough(); +a.end('foobar'); +const b = new Duplex({ + write(chunk, encoding, callback) { + callback(); + } +}); +pipeline(a, b, common.mustCall((error) => { + if (error) { + assert.ifError(error); + } + + assert(a.listenerCount('error') > 0); + assert.strictEqual(b.listenerCount('error'), 0); + setTimeout(() => { + assert.strictEqual(b.listenerCount('error'), 0); + b.destroy(new Error('no way')); + }, 100); +})); + +// Async generators +const c = new PassThrough(); +c.end('foobar'); +const d = pipeline( + c, + async function* (source) { + for await (const chunk of source) { + yield String(chunk).toUpperCase(); + } + }, + common.mustCall((error) => { + if (error) { + assert.ifError(error); + } + + assert(c.listenerCount('error') > 0); + assert.strictEqual(d.listenerCount('error'), 0); + setTimeout(() => { + assert.strictEqual(b.listenerCount('error'), 0); + d.destroy(new Error('no way')); + }, 100); + }) +); + +// If last stream is not readable, will not throw and remove listeners +const e = new PassThrough(); +e.end('foobar'); +const f = new Writable({ + write(chunk, encoding, callback) { + callback(); + } +}); +pipeline(e, f, common.mustCall((error) => { + if (error) { + assert.ifError(error); + } + + assert(e.listenerCount('error') > 0); + assert(f.listenerCount('error') > 0); + setTimeout(() => { + assert(f.listenerCount('error') > 0); + f.destroy(new Error('no way')); + }, 100); +})); diff --git a/test/js/node/test/parallel/test-stream-pipeline-process.js b/test/js/node/test/parallel/test-stream-pipeline-process.js new file mode 100644 index 0000000000..2212c702ff --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipeline-process.js @@ -0,0 +1,19 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const os = require('os'); + +if (process.argv[2] === 'child') { + const { pipeline } = require('stream'); + pipeline( + process.stdin, + process.stdout, + common.mustSucceed() + ); +} else { + const cp = require('child_process'); + cp.exec(...common.escapePOSIXShell`echo hello | "${process.execPath}" "${__filename}" child`, common.mustSucceed((stdout) => { + assert.strictEqual(stdout.split(os.EOL).shift().trim(), 'hello'); + })); +} diff --git a/test/js/node/test/parallel/test-stream-pipeline-queued-end-in-destroy.js b/test/js/node/test/parallel/test-stream-pipeline-queued-end-in-destroy.js new file mode 100644 index 0000000000..480e5b7f72 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipeline-queued-end-in-destroy.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable, Duplex, pipeline } = require('stream'); + +// Test that the callback for pipeline() is called even when the ._destroy() +// method of the stream places an .end() request to itself that does not +// get processed before the destruction of the stream (i.e. the 'close' event). +// Refs: https://github.com/nodejs/node/issues/24456 + +const readable = new Readable({ + read: common.mustCall() +}); + +const duplex = new Duplex({ + write(chunk, enc, cb) { + // Simulate messages queueing up. + }, + read() {}, + destroy(err, cb) { + // Call end() from inside the destroy() method, like HTTP/2 streams + // do at the time of writing. + this.end(); + cb(err); + } +}); + +duplex.on('finished', common.mustNotCall()); + +pipeline(readable, duplex, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); +})); + +// Write one chunk of data, and destroy the stream later. +// That should trigger the pipeline destruction. +readable.push('foo'); +setImmediate(() => { + readable.destroy(); +}); diff --git a/test/js/node/test/parallel/test-stream-pipeline-uncaught.js b/test/js/node/test/parallel/test-stream-pipeline-uncaught.js new file mode 100644 index 0000000000..8aa1c47b7f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipeline-uncaught.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); +const { + pipeline, + PassThrough +} = require('stream'); +const assert = require('assert'); + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err.message, 'error'); +})); + +// Ensure that pipeline that ends with Promise +// still propagates error to uncaughtException. +const s = new PassThrough(); +s.end('data'); +pipeline(s, async function(source) { + for await (const chunk of source) { } // eslint-disable-line no-unused-vars, no-empty +}, common.mustSucceed(() => { + throw new Error('error'); +})); diff --git a/test/js/node/test/parallel/test-stream-pipeline-with-empty-string.js b/test/js/node/test/parallel/test-stream-pipeline-with-empty-string.js new file mode 100644 index 0000000000..5df1ff9edf --- /dev/null +++ b/test/js/node/test/parallel/test-stream-pipeline-with-empty-string.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); +const { + pipeline, + PassThrough +} = require('stream'); + + +async function runTest() { + await pipeline( + '', + new PassThrough({ objectMode: true }), + common.mustCall(), + ); +} + +runTest().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-stream-preprocess.js b/test/js/node/test/parallel/test-stream-preprocess.js new file mode 100644 index 0000000000..d42c2fd63e --- /dev/null +++ b/test/js/node/test/parallel/test-stream-preprocess.js @@ -0,0 +1,60 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const fs = require('fs'); +const rl = require('readline'); +const fixtures = require('../common/fixtures'); + +const BOM = '\uFEFF'; + +// Get the data using a non-stream way to compare with the streamed data. +const modelData = fixtures.readSync('file-to-read-without-bom.txt', 'utf8'); +const modelDataFirstCharacter = modelData[0]; + +// Detect the number of forthcoming 'line' events for mustCall() 'expected' arg. +const lineCount = modelData.match(/\n/g).length; + +// Ensure both without-bom and with-bom test files are textwise equal. +assert.strictEqual(fixtures.readSync('file-to-read-with-bom.txt', 'utf8'), + `${BOM}${modelData}` +); + +// An unjustified BOM stripping with a non-BOM character unshifted to a stream. +const inputWithoutBOM = + fs.createReadStream(fixtures.path('file-to-read-without-bom.txt'), 'utf8'); + +inputWithoutBOM.once('readable', common.mustCall(() => { + const maybeBOM = inputWithoutBOM.read(1); + assert.strictEqual(maybeBOM, modelDataFirstCharacter); + assert.notStrictEqual(maybeBOM, BOM); + + inputWithoutBOM.unshift(maybeBOM); + + let streamedData = ''; + rl.createInterface({ + input: inputWithoutBOM, + }).on('line', common.mustCall((line) => { + streamedData += `${line}\n`; + }, lineCount)).on('close', common.mustCall(() => { + assert.strictEqual(streamedData, modelData); + })); +})); + +// A justified BOM stripping. +const inputWithBOM = + fs.createReadStream(fixtures.path('file-to-read-with-bom.txt'), 'utf8'); + +inputWithBOM.once('readable', common.mustCall(() => { + const maybeBOM = inputWithBOM.read(1); + assert.strictEqual(maybeBOM, BOM); + + let streamedData = ''; + rl.createInterface({ + input: inputWithBOM, + }).on('line', common.mustCall((line) => { + streamedData += `${line}\n`; + }, lineCount)).on('close', common.mustCall(() => { + assert.strictEqual(streamedData, modelData); + })); +})); diff --git a/test/js/node/test/parallel/test-stream-push-order.js b/test/js/node/test/parallel/test-stream-push-order.js new file mode 100644 index 0000000000..f026cb5b8a --- /dev/null +++ b/test/js/node/test/parallel/test-stream-push-order.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const Readable = require('stream').Readable; +const assert = require('assert'); + +const s = new Readable({ + highWaterMark: 20, + encoding: 'ascii' +}); + +const list = ['1', '2', '3', '4', '5', '6']; + +s._read = function(n) { + const one = list.shift(); + if (!one) { + s.push(null); + } else { + const two = list.shift(); + s.push(one); + s.push(two); + } +}; + +s.read(0); + +// ACTUALLY [1, 3, 5, 6, 4, 2] + +process.on('exit', function() { + assert.strictEqual(s.readableBuffer.join(','), '1,2,3,4,5,6'); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream-push-strings.js b/test/js/node/test/parallel/test-stream-push-strings.js new file mode 100644 index 0000000000..d582c8add0 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-push-strings.js @@ -0,0 +1,67 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const Readable = require('stream').Readable; + +class MyStream extends Readable { + constructor(options) { + super(options); + this._chunks = 3; + } + + _read(n) { + switch (this._chunks--) { + case 0: + return this.push(null); + case 1: + return setTimeout(() => { + this.push('last chunk'); + }, 100); + case 2: + return this.push('second to last chunk'); + case 3: + return process.nextTick(() => { + this.push('first chunk'); + }); + default: + throw new Error('?'); + } + } +} + +const ms = new MyStream(); +const results = []; +ms.on('readable', function() { + let chunk; + while (null !== (chunk = ms.read())) + results.push(String(chunk)); +}); + +const expect = [ 'first chunksecond to last chunk', 'last chunk' ]; +process.on('exit', function() { + assert.strictEqual(ms._chunks, -1); + assert.deepStrictEqual(results, expect); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream-readable-aborted.js b/test/js/node/test/parallel/test-stream-readable-aborted.js new file mode 100644 index 0000000000..9badffc51f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-aborted.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable, Duplex } = require('stream'); + +{ + const readable = new Readable({ + read() { + } + }); + assert.strictEqual(readable.readableAborted, false); + readable.destroy(); + assert.strictEqual(readable.readableAborted, true); +} + +{ + const readable = new Readable({ + read() { + } + }); + assert.strictEqual(readable.readableAborted, false); + readable.push(null); + readable.destroy(); + assert.strictEqual(readable.readableAborted, true); +} + +{ + const readable = new Readable({ + read() { + } + }); + assert.strictEqual(readable.readableAborted, false); + readable.push('asd'); + readable.destroy(); + assert.strictEqual(readable.readableAborted, true); +} + +{ + const readable = new Readable({ + read() { + } + }); + assert.strictEqual(readable.readableAborted, false); + readable.push('asd'); + readable.push(null); + assert.strictEqual(readable.readableAborted, false); + readable.on('end', common.mustCall(() => { + assert.strictEqual(readable.readableAborted, false); + readable.destroy(); + assert.strictEqual(readable.readableAborted, false); + queueMicrotask(() => { + assert.strictEqual(readable.readableAborted, false); + }); + })); + readable.resume(); +} + +{ + const duplex = new Duplex({ + readable: false, + write() {} + }); + duplex.destroy(); + assert.strictEqual(duplex.readableAborted, false); +} diff --git a/test/js/node/test/parallel/test-stream-readable-add-chunk-during-data.js b/test/js/node/test/parallel/test-stream-readable-add-chunk-during-data.js new file mode 100644 index 0000000000..6c36359790 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-add-chunk-during-data.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +// Verify that .push() and .unshift() can be called from 'data' listeners. + +for (const method of ['push', 'unshift']) { + const r = new Readable({ read() {} }); + r.once('data', common.mustCall((chunk) => { + assert.strictEqual(r.readableLength, 0); + r[method](chunk); + assert.strictEqual(r.readableLength, chunk.length); + + r.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk.toString(), 'Hello, world'); + })); + })); + + r.push('Hello, world'); +} diff --git a/test/js/node/test/parallel/test-stream-readable-constructor-set-methods.js b/test/js/node/test/parallel/test-stream-readable-constructor-set-methods.js new file mode 100644 index 0000000000..1b9f0496b9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-constructor-set-methods.js @@ -0,0 +1,11 @@ +'use strict'; +const common = require('../common'); + +const Readable = require('stream').Readable; + +const _read = common.mustCall(function _read(n) { + this.push(null); +}); + +const r = new Readable({ read: _read }); +r.resume(); diff --git a/test/js/node/test/parallel/test-stream-readable-data.js b/test/js/node/test/parallel/test-stream-readable-data.js new file mode 100644 index 0000000000..277adddde6 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-data.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); + +const { Readable } = require('stream'); + +const readable = new Readable({ + read() {} +}); + +function read() {} + +readable.setEncoding('utf8'); +readable.on('readable', read); +readable.removeListener('readable', read); + +process.nextTick(function() { + readable.on('data', common.mustCall()); + readable.push('hello'); +}); diff --git a/test/js/node/test/parallel/test-stream-readable-default-encoding.js b/test/js/node/test/parallel/test-stream-readable-default-encoding.js new file mode 100644 index 0000000000..954f1643ba --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-default-encoding.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +{ + assert.throws(() => { + new Readable({ + read: () => {}, + defaultEncoding: 'my invalid encoding', + }); + }, { + code: 'ERR_UNKNOWN_ENCODING', + }); +} + +{ + const r = new Readable({ + read() {}, + defaultEncoding: 'hex' + }); + + r.push('ab'); + + r.on('data', common.mustCall((chunk) => assert.strictEqual(chunk.toString('hex'), 'ab')), 1); +} + +{ + const r = new Readable({ + read() {}, + defaultEncoding: 'hex', + }); + + r.push('xy', 'utf-8'); + + r.on('data', common.mustCall((chunk) => assert.strictEqual(chunk.toString('utf-8'), 'xy')), 1); +} diff --git a/test/js/node/test/parallel/test-stream-readable-destroy.js b/test/js/node/test/parallel/test-stream-readable-destroy.js new file mode 100644 index 0000000000..fb7da632f7 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-destroy.js @@ -0,0 +1,405 @@ +'use strict'; + +const common = require('../common'); +const { Readable, addAbortSignal } = require('stream'); +const assert = require('assert'); + +{ + const read = new Readable({ + read() {} + }); + read.resume(); + + read.on('close', common.mustCall()); + + read.destroy(); + assert.strictEqual(read.errored, null); + assert.strictEqual(read.destroyed, true); +} + +{ + const read = new Readable({ + read() {} + }); + read.resume(); + + const expected = new Error('kaboom'); + + read.on('end', common.mustNotCall('no end event')); + read.on('close', common.mustCall()); + read.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + read.destroy(expected); + assert.strictEqual(read.errored, expected); + assert.strictEqual(read.destroyed, true); +} + +{ + const read = new Readable({ + read() {} + }); + + read._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, expected); + cb(err); + }); + + const expected = new Error('kaboom'); + + read.on('end', common.mustNotCall('no end event')); + read.on('close', common.mustCall()); + read.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + read.destroy(expected); + assert.strictEqual(read.destroyed, true); +} + +{ + const read = new Readable({ + read() {}, + destroy: common.mustCall(function(err, cb) { + assert.strictEqual(err, expected); + cb(); + }) + }); + + const expected = new Error('kaboom'); + + read.on('end', common.mustNotCall('no end event')); + + // Error is swallowed by the custom _destroy + read.on('error', common.mustNotCall('no error event')); + read.on('close', common.mustCall()); + + read.destroy(expected); + assert.strictEqual(read.destroyed, true); +} + +{ + const read = new Readable({ + read() {} + }); + + read._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(); + }); + + read.destroy(); + assert.strictEqual(read.destroyed, true); +} + +{ + const read = new Readable({ + read() {} + }); + read.resume(); + + read._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + process.nextTick(() => { + this.push(null); + cb(); + }); + }); + + const fail = common.mustNotCall('no end event'); + + read.on('end', fail); + read.on('close', common.mustCall()); + + read.destroy(); + + read.removeListener('end', fail); + read.on('end', common.mustNotCall()); + assert.strictEqual(read.destroyed, true); +} + +{ + const read = new Readable({ + read() {} + }); + + const expected = new Error('kaboom'); + + read._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(expected); + }); + + let ticked = false; + read.on('end', common.mustNotCall('no end event')); + read.on('error', common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(read._readableState.errorEmitted, true); + assert.strictEqual(read._readableState.errored, expected); + assert.strictEqual(err, expected); + })); + + read.destroy(); + assert.strictEqual(read._readableState.errorEmitted, false); + assert.strictEqual(read._readableState.errored, expected); + assert.strictEqual(read.destroyed, true); + ticked = true; +} + +{ + const read = new Readable({ + read() {} + }); + read.resume(); + + read.destroyed = true; + assert.strictEqual(read.destroyed, true); + + // The internal destroy() mechanism should not be triggered + read.on('end', common.mustNotCall()); + read.destroy(); +} + +{ + function MyReadable() { + assert.strictEqual(this.destroyed, false); + this.destroyed = false; + Readable.call(this); + } + + Object.setPrototypeOf(MyReadable.prototype, Readable.prototype); + Object.setPrototypeOf(MyReadable, Readable); + + new MyReadable(); +} + +{ + // Destroy and destroy callback + const read = new Readable({ + read() {} + }); + read.resume(); + + const expected = new Error('kaboom'); + + let ticked = false; + read.on('close', common.mustCall(() => { + assert.strictEqual(read._readableState.errorEmitted, true); + assert.strictEqual(ticked, true); + })); + read.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + assert.strictEqual(read._readableState.errored, null); + assert.strictEqual(read._readableState.errorEmitted, false); + + read.destroy(expected, common.mustCall(function(err) { + assert.strictEqual(read._readableState.errored, expected); + assert.strictEqual(err, expected); + })); + assert.strictEqual(read._readableState.errorEmitted, false); + assert.strictEqual(read._readableState.errored, expected); + ticked = true; +} + +{ + const readable = new Readable({ + destroy: common.mustCall(function(err, cb) { + process.nextTick(cb, new Error('kaboom 1')); + }), + read() {} + }); + + let ticked = false; + readable.on('close', common.mustCall(() => { + assert.strictEqual(ticked, true); + assert.strictEqual(readable._readableState.errorEmitted, true); + })); + readable.on('error', common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(err.message, 'kaboom 1'); + assert.strictEqual(readable._readableState.errorEmitted, true); + })); + + readable.destroy(); + assert.strictEqual(readable.destroyed, true); + assert.strictEqual(readable._readableState.errored, null); + assert.strictEqual(readable._readableState.errorEmitted, false); + + // Test case where `readable.destroy()` is called again with an error before + // the `_destroy()` callback is called. + readable.destroy(new Error('kaboom 2')); + assert.strictEqual(readable._readableState.errorEmitted, false); + assert.strictEqual(readable._readableState.errored, null); + + ticked = true; +} + +{ + const read = new Readable({ + read() {} + }); + + read.destroy(); + read.push('hi'); + read.on('data', common.mustNotCall()); +} + +{ + const read = new Readable({ + read: common.mustNotCall() + }); + read.destroy(); + assert.strictEqual(read.destroyed, true); + read.read(); +} + +{ + const read = new Readable({ + autoDestroy: false, + read() { + this.push(null); + this.push('asd'); + } + }); + + read.on('error', common.mustCall(() => { + assert(read._readableState.errored); + })); + read.resume(); +} + +{ + const controller = new AbortController(); + const read = addAbortSignal(controller.signal, new Readable({ + read() { + this.push('asd'); + }, + })); + + read.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + })); + controller.abort(); + read.on('data', common.mustNotCall()); +} + +{ + const controller = new AbortController(); + const read = new Readable({ + signal: controller.signal, + read() { + this.push('asd'); + }, + }); + + read.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + })); + controller.abort(); + read.on('data', common.mustNotCall()); +} + +{ + const controller = new AbortController(); + const read = addAbortSignal(controller.signal, new Readable({ + objectMode: true, + read() { + return false; + } + })); + read.push('asd'); + + read.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + })); + assert.rejects((async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const chunk of read) { } + })(), /AbortError/).then(common.mustCall()); + setTimeout(() => controller.abort(), 0); +} + +{ + const read = new Readable({ + read() { + }, + }); + + read.on('data', common.mustNotCall()); + read.on('error', common.mustCall((e) => { + read.push('asd'); + read.read(); + })); + read.on('close', common.mustCall((e) => { + read.push('asd'); + read.read(); + })); + read.destroy(new Error('asd')); +} + +{ + const read = new Readable({ + read() { + }, + }); + + read.on('data', common.mustNotCall()); + read.on('close', common.mustCall((e) => { + read.push('asd'); + read.read(); + })); + read.destroy(); +} + +{ + const read = new Readable({ + read() { + }, + }); + + read.on('data', common.mustNotCall()); + read.on('close', common.mustCall((e) => { + read.push('asd'); + read.unshift('asd'); + })); + read.destroy(); +} + +{ + const read = new Readable({ + read() { + }, + }); + + read.on('data', common.mustNotCall()); + read.destroy(); + read.unshift('asd'); +} + +{ + const read = new Readable({ + read() { + }, + }); + + read.resume(); + read.on('data', common.mustNotCall()); + read.on('close', common.mustCall((e) => { + read.push('asd'); + })); + read.destroy(); +} + +{ + const read = new Readable({ + read() { + }, + }); + + read.on('data', common.mustNotCall()); + read.destroy(); + read.push('asd'); +} diff --git a/test/js/node/test/parallel/test-stream-readable-didRead.js b/test/js/node/test/parallel/test-stream-readable-didRead.js new file mode 100644 index 0000000000..878340ba19 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-didRead.js @@ -0,0 +1,111 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { isDisturbed, isErrored, Readable } = require('stream'); + +function noop() {} + +function check(readable, data, fn) { + assert.strictEqual(readable.readableDidRead, false); + assert.strictEqual(isDisturbed(readable), false); + assert.strictEqual(isErrored(readable), false); + if (data === -1) { + readable.on('error', common.mustCall(() => { + assert.strictEqual(isErrored(readable), true); + })); + readable.on('data', common.mustNotCall()); + readable.on('end', common.mustNotCall()); + } else { + readable.on('error', common.mustNotCall()); + if (data === -2) { + readable.on('end', common.mustNotCall()); + } else { + readable.on('end', common.mustCall()); + } + if (data > 0) { + readable.on('data', common.mustCallAtLeast(data)); + } else { + readable.on('data', common.mustNotCall()); + } + } + readable.on('close', common.mustCall()); + fn(); + setImmediate(() => { + assert.strictEqual(readable.readableDidRead, data > 0); + if (data > 0) { + assert.strictEqual(isDisturbed(readable), true); + } + }); +} + +{ + const readable = new Readable({ + read() { + this.push(null); + } + }); + check(readable, 0, () => { + readable.read(); + }); +} + +{ + const readable = new Readable({ + read() { + this.push(null); + } + }); + check(readable, 0, () => { + readable.resume(); + }); +} + +{ + const readable = new Readable({ + read() { + this.push(null); + } + }); + check(readable, -2, () => { + readable.destroy(); + }); +} + +{ + const readable = new Readable({ + read() { + this.push(null); + } + }); + + check(readable, -1, () => { + readable.destroy(new Error()); + }); +} + +{ + const readable = new Readable({ + read() { + this.push('data'); + this.push(null); + } + }); + + check(readable, 1, () => { + readable.on('data', noop); + }); +} + +{ + const readable = new Readable({ + read() { + this.push('data'); + this.push(null); + } + }); + + check(readable, 1, () => { + readable.on('data', noop); + readable.off('data', noop); + }); +} diff --git a/test/js/node/test/parallel/test-stream-readable-dispose.js b/test/js/node/test/parallel/test-stream-readable-dispose.js new file mode 100644 index 0000000000..e940bf1688 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-dispose.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const { Readable } = require('stream'); +const assert = require('assert'); + +{ + const read = new Readable({ + read() {} + }); + read.resume(); + + read.on('end', common.mustNotCall('no end event')); + read.on('close', common.mustCall()); + read.on('error', common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + })); + + read[Symbol.asyncDispose]().then(common.mustCall(() => { + assert.strictEqual(read.errored.name, 'AbortError'); + assert.strictEqual(read.destroyed, true); + })); +} diff --git a/test/js/node/test/parallel/test-stream-readable-emit-readable-short-stream.js b/test/js/node/test/parallel/test-stream-readable-emit-readable-short-stream.js new file mode 100644 index 0000000000..d8b84bfbe7 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-emit-readable-short-stream.js @@ -0,0 +1,146 @@ +'use strict'; + +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +{ + const r = new stream.Readable({ + read: common.mustCall(function() { + this.push('content'); + this.push(null); + }) + }); + + const t = new stream.Transform({ + transform: common.mustCall(function(chunk, encoding, callback) { + this.push(chunk); + return callback(); + }), + flush: common.mustCall(function(callback) { + return callback(); + }) + }); + + r.pipe(t); + t.on('readable', common.mustCall(function() { + while (true) { + const chunk = t.read(); + if (!chunk) + break; + + assert.strictEqual(chunk.toString(), 'content'); + } + }, 2)); +} + +{ + const t = new stream.Transform({ + transform: common.mustCall(function(chunk, encoding, callback) { + this.push(chunk); + return callback(); + }), + flush: common.mustCall(function(callback) { + return callback(); + }) + }); + + t.end('content'); + + t.on('readable', common.mustCall(function() { + while (true) { + const chunk = t.read(); + if (!chunk) + break; + assert.strictEqual(chunk.toString(), 'content'); + } + })); +} + +{ + const t = new stream.Transform({ + transform: common.mustCall(function(chunk, encoding, callback) { + this.push(chunk); + return callback(); + }), + flush: common.mustCall(function(callback) { + return callback(); + }) + }); + + t.write('content'); + t.end(); + + t.on('readable', common.mustCall(function() { + while (true) { + const chunk = t.read(); + if (!chunk) + break; + assert.strictEqual(chunk.toString(), 'content'); + } + })); +} + +{ + const t = new stream.Readable({ + read() { + } + }); + + t.on('readable', common.mustCall(function() { + while (true) { + const chunk = t.read(); + if (!chunk) + break; + assert.strictEqual(chunk.toString(), 'content'); + } + })); + + t.push('content'); + t.push(null); +} + +{ + const t = new stream.Readable({ + read() { + } + }); + + t.on('readable', common.mustCall(function() { + while (true) { + const chunk = t.read(); + if (!chunk) + break; + assert.strictEqual(chunk.toString(), 'content'); + } + }, 2)); + + process.nextTick(() => { + t.push('content'); + t.push(null); + }); +} + +{ + const t = new stream.Transform({ + transform: common.mustCall(function(chunk, encoding, callback) { + this.push(chunk); + return callback(); + }), + flush: common.mustCall(function(callback) { + return callback(); + }) + }); + + t.on('readable', common.mustCall(function() { + while (true) { + const chunk = t.read(); + if (!chunk) + break; + assert.strictEqual(chunk.toString(), 'content'); + } + }, 2)); + + t.write('content'); + t.end(); +} diff --git a/test/js/node/test/parallel/test-stream-readable-emittedReadable.js b/test/js/node/test/parallel/test-stream-readable-emittedReadable.js new file mode 100644 index 0000000000..ba613f9e9f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-emittedReadable.js @@ -0,0 +1,73 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const Readable = require('stream').Readable; + +const readable = new Readable({ + read: () => {} +}); + +// Initialized to false. +assert.strictEqual(readable._readableState.emittedReadable, false); + +const expected = [Buffer.from('foobar'), Buffer.from('quo'), null]; +readable.on('readable', common.mustCall(() => { + // emittedReadable should be true when the readable event is emitted + assert.strictEqual(readable._readableState.emittedReadable, true); + assert.deepStrictEqual(readable.read(), expected.shift()); + // emittedReadable is reset to false during read() + assert.strictEqual(readable._readableState.emittedReadable, false); +}, 3)); + +// When the first readable listener is just attached, +// emittedReadable should be false +assert.strictEqual(readable._readableState.emittedReadable, false); + +// These trigger a single 'readable', as things are batched up +process.nextTick(common.mustCall(() => { + readable.push('foo'); +})); +process.nextTick(common.mustCall(() => { + readable.push('bar'); +})); + +// These triggers two readable events +setImmediate(common.mustCall(() => { + readable.push('quo'); + process.nextTick(common.mustCall(() => { + readable.push(null); + })); +})); + +const noRead = new Readable({ + read: () => {} +}); + +noRead.on('readable', common.mustCall(() => { + // emittedReadable should be true when the readable event is emitted + assert.strictEqual(noRead._readableState.emittedReadable, true); + noRead.read(0); + // emittedReadable is not reset during read(0) + assert.strictEqual(noRead._readableState.emittedReadable, true); +})); + +noRead.push('foo'); +noRead.push(null); + +const flowing = new Readable({ + read: () => {} +}); + +flowing.on('data', common.mustCall(() => { + // When in flowing mode, emittedReadable is always false. + assert.strictEqual(flowing._readableState.emittedReadable, false); + flowing.read(); + assert.strictEqual(flowing._readableState.emittedReadable, false); +}, 3)); + +flowing.push('foooo'); +flowing.push('bar'); +flowing.push('quo'); +process.nextTick(common.mustCall(() => { + flowing.push(null); +})); diff --git a/test/js/node/test/parallel/test-stream-readable-end-destroyed.js b/test/js/node/test/parallel/test-stream-readable-end-destroyed.js new file mode 100644 index 0000000000..4b60bf4614 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-end-destroyed.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const { Readable } = require('stream'); + +{ + // Don't emit 'end' after 'close'. + + const r = new Readable(); + + r.on('end', common.mustNotCall()); + r.resume(); + r.destroy(); + r.on('close', common.mustCall(() => { + r.push(null); + })); +} diff --git a/test/js/node/test/parallel/test-stream-readable-ended.js b/test/js/node/test/parallel/test-stream-readable-ended.js new file mode 100644 index 0000000000..bdd714c955 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-ended.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const { Readable } = require('stream'); +const assert = require('assert'); + +// basic +{ + // Find it on Readable.prototype + assert(Object.hasOwn(Readable.prototype, 'readableEnded')); +} + +// event +{ + const readable = new Readable(); + + readable._read = () => { + // The state ended should start in false. + assert.strictEqual(readable.readableEnded, false); + readable.push('asd'); + assert.strictEqual(readable.readableEnded, false); + readable.push(null); + assert.strictEqual(readable.readableEnded, false); + }; + + readable.on('end', common.mustCall(() => { + assert.strictEqual(readable.readableEnded, true); + })); + + readable.on('data', common.mustCall(() => { + assert.strictEqual(readable.readableEnded, false); + })); +} + +// Verifies no `error` triggered on multiple .push(null) invocations +{ + const readable = new Readable(); + + readable.on('readable', () => { readable.read(); }); + readable.on('error', common.mustNotCall()); + readable.on('end', common.mustCall()); + + readable.push('a'); + readable.push(null); + readable.push(null); +} diff --git a/test/js/node/test/parallel/test-stream-readable-error-end.js b/test/js/node/test/parallel/test-stream-readable-error-end.js new file mode 100644 index 0000000000..b46fd7f32c --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-error-end.js @@ -0,0 +1,15 @@ +'use strict'; + +const common = require('../common'); +const { Readable } = require('stream'); + +{ + const r = new Readable({ read() {} }); + + r.on('end', common.mustNotCall()); + r.on('data', common.mustCall()); + r.on('error', common.mustCall()); + r.push('asd'); + r.push(null); + r.destroy(new Error('kaboom')); +} diff --git a/test/js/node/test/parallel/test-stream-readable-event.js b/test/js/node/test/parallel/test-stream-readable-event.js new file mode 100644 index 0000000000..4f2383508a --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-event.js @@ -0,0 +1,128 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const Readable = require('stream').Readable; + +{ + // First test, not reading when the readable is added. + // make sure that on('readable', ...) triggers a readable event. + const r = new Readable({ + highWaterMark: 3 + }); + + r._read = common.mustNotCall(); + + // This triggers a 'readable' event, which is lost. + r.push(Buffer.from('blerg')); + + setTimeout(function() { + // We're testing what we think we are + assert(!r._readableState.reading); + r.on('readable', common.mustCall()); + }, 1); +} + +{ + // Second test, make sure that readable is re-emitted if there's + // already a length, while it IS reading. + + const r = new Readable({ + highWaterMark: 3 + }); + + r._read = common.mustCall(); + + // This triggers a 'readable' event, which is lost. + r.push(Buffer.from('bl')); + + setTimeout(function() { + // Assert we're testing what we think we are + assert(r._readableState.reading); + r.on('readable', common.mustCall()); + }, 1); +} + +{ + // Third test, not reading when the stream has not passed + // the highWaterMark but *has* reached EOF. + const r = new Readable({ + highWaterMark: 30 + }); + + r._read = common.mustNotCall(); + + // This triggers a 'readable' event, which is lost. + r.push(Buffer.from('blerg')); + r.push(null); + + setTimeout(function() { + // Assert we're testing what we think we are + assert(!r._readableState.reading); + r.on('readable', common.mustCall()); + }, 1); +} + +{ + // Pushing an empty string in non-objectMode should + // trigger next `read()`. + const underlyingData = ['', 'x', 'y', '', 'z']; + const expected = underlyingData.filter((data) => data); + const result = []; + + const r = new Readable({ + encoding: 'utf8', + }); + r._read = function() { + process.nextTick(() => { + if (!underlyingData.length) { + this.push(null); + } else { + this.push(underlyingData.shift()); + } + }); + }; + + r.on('readable', () => { + const data = r.read(); + if (data !== null) result.push(data); + }); + + r.on('end', common.mustCall(() => { + assert.deepStrictEqual(result, expected); + })); +} + +{ + // #20923 + const r = new Readable(); + r._read = function() { + // Actually doing thing here + }; + r.on('data', function() {}); + + r.removeAllListeners(); + + assert.strictEqual(r.eventNames().length, 0); +} diff --git a/test/js/node/test/parallel/test-stream-readable-flow-recursion.js b/test/js/node/test/parallel/test-stream-readable-flow-recursion.js new file mode 100644 index 0000000000..bac6427fb0 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-flow-recursion.js @@ -0,0 +1,77 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// This test verifies that passing a huge number to read(size) +// will push up the highWaterMark, and cause the stream to read +// more data continuously, but without triggering a nextTick +// warning or RangeError. + +const Readable = require('stream').Readable; + +// Throw an error if we trigger a nextTick warning. +process.throwDeprecation = true; + +const stream = new Readable({ highWaterMark: 2 }); +let reads = 0; +let total = 5000; +stream._read = function(size) { + reads++; + size = Math.min(size, total); + total -= size; + if (size === 0) + stream.push(null); + else + stream.push(Buffer.allocUnsafe(size)); +}; + +let depth = 0; + +function flow(stream, size, callback) { + depth += 1; + const chunk = stream.read(size); + + if (!chunk) + stream.once('readable', flow.bind(null, stream, size, callback)); + else + callback(chunk); + + depth -= 1; + console.log(`flow(${depth}): exit`); +} + +flow(stream, 5000, function() { + console.log(`complete (${depth})`); +}); + +process.on('exit', function(code) { + assert.strictEqual(reads, 2); + // We pushed up the high water mark + assert.strictEqual(stream.readableHighWaterMark, 8192); + // Length is 0 right now, because we pulled it all out. + assert.strictEqual(stream.readableLength, 0); + assert(!code); + assert.strictEqual(depth, 0); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream-readable-from-web-termination.js b/test/js/node/test/parallel/test-stream-readable-from-web-termination.js new file mode 100644 index 0000000000..68ed7d6969 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-from-web-termination.js @@ -0,0 +1,15 @@ +'use strict'; +require('../common'); +const { Readable } = require('stream'); + +{ + const r = Readable.from(['data']); + + const wrapper = Readable.fromWeb(Readable.toWeb(r)); + + wrapper.on('data', () => { + // Destroying wrapper while emitting data should not cause uncaught + // exceptions + wrapper.destroy(); + }); +} diff --git a/test/js/node/test/parallel/test-stream-readable-hwm-0-async.js b/test/js/node/test/parallel/test-stream-readable-hwm-0-async.js new file mode 100644 index 0000000000..866b524893 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-hwm-0-async.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); + +// This test ensures that Readable stream will continue to call _read +// for streams with highWaterMark === 0 once the stream returns data +// by calling push() asynchronously. + +const { Readable } = require('stream'); + +let count = 5; + +const r = new Readable({ + // Called 6 times: First 5 return data, last one signals end of stream. + read: common.mustCall(() => { + process.nextTick(common.mustCall(() => { + if (count--) + r.push('a'); + else + r.push(null); + })); + }, 6), + highWaterMark: 0, +}); + +r.on('end', common.mustCall()); +r.on('data', common.mustCall(5)); diff --git a/test/js/node/test/parallel/test-stream-readable-hwm-0-no-flow-data.js b/test/js/node/test/parallel/test-stream-readable-hwm-0-no-flow-data.js new file mode 100644 index 0000000000..5f0186d720 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-hwm-0-no-flow-data.js @@ -0,0 +1,104 @@ +'use strict'; + +const common = require('../common'); + +// Ensure that subscribing the 'data' event will not make the stream flow. +// The 'data' event will require calling read() by hand. +// +// The test is written for the (somewhat rare) highWaterMark: 0 streams to +// specifically catch any regressions that might occur with these streams. + +const assert = require('assert'); +const { Readable } = require('stream'); + +const streamData = [ 'a', null ]; + +// Track the calls so we can assert their order later. +const calls = []; +const r = new Readable({ + read: common.mustCall(() => { + calls.push('_read:' + streamData[0]); + process.nextTick(() => { + calls.push('push:' + streamData[0]); + r.push(streamData.shift()); + }); + }, streamData.length), + highWaterMark: 0, + + // Object mode is used here just for testing convenience. It really + // shouldn't affect the order of events. Just the data and its format. + objectMode: true, +}); + +assert.strictEqual(r.readableFlowing, null); +r.on('readable', common.mustCall(() => { + calls.push('readable'); +}, 2)); +assert.strictEqual(r.readableFlowing, false); +r.on('data', common.mustCall((data) => { + calls.push('data:' + data); +}, 1)); +r.on('end', common.mustCall(() => { + calls.push('end'); +})); +assert.strictEqual(r.readableFlowing, false); + +// The stream emits the events asynchronously but that's not guaranteed to +// happen on the next tick (especially since the _read implementation above +// uses process.nextTick). +// +// We use setImmediate here to give the stream enough time to emit all the +// events it's about to emit. +setImmediate(() => { + + // Only the _read, push, readable calls have happened. No data must be + // emitted yet. + assert.deepStrictEqual(calls, ['_read:a', 'push:a', 'readable']); + + // Calling 'r.read()' should trigger the data event. + assert.strictEqual(r.read(), 'a'); + assert.deepStrictEqual( + calls, + ['_read:a', 'push:a', 'readable', 'data:a']); + + // The next 'read()' will return null because hwm: 0 does not buffer any + // data and the _read implementation above does the push() asynchronously. + // + // Note: This 'null' signals "no data available". It isn't the end-of-stream + // null value as the stream doesn't know yet that it is about to reach the + // end. + // + // Using setImmediate again to give the stream enough time to emit all the + // events it wants to emit. + assert.strictEqual(r.read(), null); + setImmediate(() => { + + // There's a new 'readable' event after the data has been pushed. + // The 'end' event will be emitted only after a 'read()'. + // + // This is somewhat special for the case where the '_read' implementation + // calls 'push' asynchronously. If 'push' was synchronous, the 'end' event + // would be emitted here _before_ we call read(). + assert.deepStrictEqual( + calls, + ['_read:a', 'push:a', 'readable', 'data:a', + '_read:null', 'push:null', 'readable']); + + assert.strictEqual(r.read(), null); + + // While it isn't really specified whether the 'end' event should happen + // synchronously with read() or not, we'll assert the current behavior + // ('end' event happening on the next tick after read()) so any changes + // to it are noted and acknowledged in the future. + assert.deepStrictEqual( + calls, + ['_read:a', 'push:a', 'readable', 'data:a', + '_read:null', 'push:null', 'readable']); + process.nextTick(() => { + assert.deepStrictEqual( + calls, + ['_read:a', 'push:a', 'readable', 'data:a', + '_read:null', 'push:null', 'readable', 'end']); + }); + }); +}); diff --git a/test/js/node/test/parallel/test-stream-readable-hwm-0.js b/test/js/node/test/parallel/test-stream-readable-hwm-0.js new file mode 100644 index 0000000000..5bb17882db --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-hwm-0.js @@ -0,0 +1,30 @@ +'use strict'; + +const common = require('../common'); + +// This test ensures that Readable stream will call _read() for streams +// with highWaterMark === 0 upon .read(0) instead of just trying to +// emit 'readable' event. + +const assert = require('assert'); +const { Readable } = require('stream'); + +const r = new Readable({ + // Must be called only once upon setting 'readable' listener + read: common.mustCall(), + highWaterMark: 0, +}); + +let pushedNull = false; +// This will trigger read(0) but must only be called after push(null) +// because the we haven't pushed any data +r.on('readable', common.mustCall(() => { + assert.strictEqual(r.read(), null); + assert.strictEqual(pushedNull, true); +})); +r.on('end', common.mustCall()); +process.nextTick(() => { + assert.strictEqual(r.read(), null); + pushedNull = true; + r.push(null); +}); diff --git a/test/js/node/test/parallel/test-stream-readable-infinite-read.js b/test/js/node/test/parallel/test-stream-readable-infinite-read.js new file mode 100644 index 0000000000..df88d78b74 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-infinite-read.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +const buf = Buffer.alloc(8192); + +const readable = new Readable({ + highWaterMark: 16 * 1024, + read: common.mustCall(function() { + this.push(buf); + }, 31) +}); + +let i = 0; + +readable.on('readable', common.mustCall(function() { + if (i++ === 10) { + // We will just terminate now. + process.removeAllListeners('readable'); + return; + } + + const data = readable.read(); + // TODO(mcollina): there is something odd in the highWaterMark logic + // investigate. + if (i === 1) { + assert.strictEqual(data.length, 8192 * 2); + } else { + assert.strictEqual(data.length, 8192 * 3); + } +}, 11)); diff --git a/test/js/node/test/parallel/test-stream-readable-invalid-chunk.js b/test/js/node/test/parallel/test-stream-readable-invalid-chunk.js new file mode 100644 index 0000000000..0fcc76ae73 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-invalid-chunk.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +const stream = require('stream'); + +function testPushArg(val) { + const readable = new stream.Readable({ + read: () => {} + }); + readable.on('error', common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + })); + readable.push(val); +} + +testPushArg([]); +testPushArg({}); +testPushArg(0); + +function testUnshiftArg(val) { + const readable = new stream.Readable({ + read: () => {} + }); + readable.on('error', common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + })); + readable.unshift(val); +} + +testUnshiftArg([]); +testUnshiftArg({}); +testUnshiftArg(0); diff --git a/test/js/node/test/parallel/test-stream-readable-needReadable.js b/test/js/node/test/parallel/test-stream-readable-needReadable.js new file mode 100644 index 0000000000..c4bc90bb19 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-needReadable.js @@ -0,0 +1,99 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const Readable = require('stream').Readable; + +const readable = new Readable({ + read: () => {} +}); + +// Initialized to false. +assert.strictEqual(readable._readableState.needReadable, false); + +readable.on('readable', common.mustCall(() => { + // When the readable event fires, needReadable is reset. + assert.strictEqual(readable._readableState.needReadable, false); + readable.read(); +})); + +// If a readable listener is attached, then a readable event is needed. +assert.strictEqual(readable._readableState.needReadable, true); + +readable.push('foo'); +readable.push(null); + +readable.on('end', common.mustCall(() => { + // No need to emit readable anymore when the stream ends. + assert.strictEqual(readable._readableState.needReadable, false); +})); + +const asyncReadable = new Readable({ + read: () => {} +}); + +asyncReadable.on('readable', common.mustCall(() => { + if (asyncReadable.read() !== null) { + // After each read(), the buffer is empty. + // If the stream doesn't end now, + // then we need to notify the reader on future changes. + assert.strictEqual(asyncReadable._readableState.needReadable, true); + } +}, 2)); + +process.nextTick(common.mustCall(() => { + asyncReadable.push('foooo'); +})); +process.nextTick(common.mustCall(() => { + asyncReadable.push('bar'); +})); +setImmediate(common.mustCall(() => { + asyncReadable.push(null); + assert.strictEqual(asyncReadable._readableState.needReadable, false); +})); + +const flowing = new Readable({ + read: () => {} +}); + +// Notice this must be above the on('data') call. +flowing.push('foooo'); +flowing.push('bar'); +flowing.push('quo'); +process.nextTick(common.mustCall(() => { + flowing.push(null); +})); + +// When the buffer already has enough data, and the stream is +// in flowing mode, there is no need for the readable event. +flowing.on('data', common.mustCall(function(data) { + assert.strictEqual(flowing._readableState.needReadable, false); +}, 3)); + +const slowProducer = new Readable({ + read: () => {} +}); + +slowProducer.on('readable', common.mustCall(() => { + const chunk = slowProducer.read(8); + const state = slowProducer._readableState; + if (chunk === null) { + // The buffer doesn't have enough data, and the stream is not need, + // we need to notify the reader when data arrives. + assert.strictEqual(state.needReadable, true); + } else { + assert.strictEqual(state.needReadable, false); + } +}, 4)); + +process.nextTick(common.mustCall(() => { + slowProducer.push('foo'); + process.nextTick(common.mustCall(() => { + slowProducer.push('foo'); + process.nextTick(common.mustCall(() => { + slowProducer.push('foo'); + process.nextTick(common.mustCall(() => { + slowProducer.push(null); + })); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-stream-readable-next-no-null.js b/test/js/node/test/parallel/test-stream-readable-next-no-null.js new file mode 100644 index 0000000000..7599e386ca --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-next-no-null.js @@ -0,0 +1,19 @@ +'use strict'; +const { mustNotCall, expectsError } = require('../common'); +const { Readable } = require('stream'); + +async function* generate() { + yield null; +} + +const stream = Readable.from(generate()); + +stream.on('error', expectsError({ + code: 'ERR_STREAM_NULL_VALUES', + name: 'TypeError', + message: 'May not write null values to stream' +})); + +stream.on('data', mustNotCall()); + +stream.on('end', mustNotCall()); diff --git a/test/js/node/test/parallel/test-stream-readable-no-unneeded-readable.js b/test/js/node/test/parallel/test-stream-readable-no-unneeded-readable.js new file mode 100644 index 0000000000..20092b57d9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-no-unneeded-readable.js @@ -0,0 +1,62 @@ +'use strict'; +const common = require('../common'); +const { Readable, PassThrough } = require('stream'); + +function test(r) { + const wrapper = new Readable({ + read: () => { + let data = r.read(); + + if (data) { + wrapper.push(data); + return; + } + + r.once('readable', function() { + data = r.read(); + if (data) { + wrapper.push(data); + } + // else: the end event should fire + }); + }, + }); + + r.once('end', function() { + wrapper.push(null); + }); + + wrapper.resume(); + wrapper.once('end', common.mustCall()); +} + +{ + const source = new Readable({ + read: () => {} + }); + source.push('foo'); + source.push('bar'); + source.push(null); + + const pt = source.pipe(new PassThrough()); + test(pt); +} + +{ + // This is the underlying cause of the above test case. + const pushChunks = ['foo', 'bar']; + const r = new Readable({ + read: () => { + const chunk = pushChunks.shift(); + if (chunk) { + // synchronous call + r.push(chunk); + } else { + // asynchronous call + process.nextTick(() => r.push(null)); + } + }, + }); + + test(r); +} diff --git a/test/js/node/test/parallel/test-stream-readable-object-multi-push-async.js b/test/js/node/test/parallel/test-stream-readable-object-multi-push-async.js new file mode 100644 index 0000000000..3bdbe4d351 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-object-multi-push-async.js @@ -0,0 +1,183 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +const MAX = 42; +const BATCH = 10; + +{ + const readable = new Readable({ + objectMode: true, + read: common.mustCall(function() { + console.log('>> READ'); + fetchData((err, data) => { + if (err) { + this.destroy(err); + return; + } + + if (data.length === 0) { + console.log('pushing null'); + this.push(null); + return; + } + + console.log('pushing'); + data.forEach((d) => this.push(d)); + }); + }, Math.floor(MAX / BATCH) + 2) + }); + + let i = 0; + function fetchData(cb) { + if (i > MAX) { + setTimeout(cb, 10, null, []); + } else { + const array = []; + const max = i + BATCH; + for (; i < max; i++) { + array.push(i); + } + setTimeout(cb, 10, null, array); + } + } + + readable.on('readable', () => { + let data; + console.log('readable emitted'); + while ((data = readable.read()) !== null) { + console.log(data); + } + }); + + readable.on('end', common.mustCall(() => { + assert.strictEqual(i, (Math.floor(MAX / BATCH) + 1) * BATCH); + })); +} + +{ + const readable = new Readable({ + objectMode: true, + read: common.mustCall(function() { + console.log('>> READ'); + fetchData((err, data) => { + if (err) { + this.destroy(err); + return; + } + + if (data.length === 0) { + console.log('pushing null'); + this.push(null); + return; + } + + console.log('pushing'); + data.forEach((d) => this.push(d)); + }); + }, Math.floor(MAX / BATCH) + 2) + }); + + let i = 0; + function fetchData(cb) { + if (i > MAX) { + setTimeout(cb, 10, null, []); + } else { + const array = []; + const max = i + BATCH; + for (; i < max; i++) { + array.push(i); + } + setTimeout(cb, 10, null, array); + } + } + + readable.on('data', (data) => { + console.log('data emitted', data); + }); + + readable.on('end', common.mustCall(() => { + assert.strictEqual(i, (Math.floor(MAX / BATCH) + 1) * BATCH); + })); +} + +{ + const readable = new Readable({ + objectMode: true, + read: common.mustCall(function() { + console.log('>> READ'); + fetchData((err, data) => { + if (err) { + this.destroy(err); + return; + } + + console.log('pushing'); + data.forEach((d) => this.push(d)); + + if (data[BATCH - 1] >= MAX) { + console.log('pushing null'); + this.push(null); + } + }); + }, Math.floor(MAX / BATCH) + 1) + }); + + let i = 0; + function fetchData(cb) { + const array = []; + const max = i + BATCH; + for (; i < max; i++) { + array.push(i); + } + setTimeout(cb, 10, null, array); + } + + readable.on('data', (data) => { + console.log('data emitted', data); + }); + + readable.on('end', common.mustCall(() => { + assert.strictEqual(i, (Math.floor(MAX / BATCH) + 1) * BATCH); + })); +} + +{ + const readable = new Readable({ + objectMode: true, + read: common.mustNotCall() + }); + + readable.on('data', common.mustNotCall()); + + readable.push(null); + + let nextTickPassed = false; + process.nextTick(() => { + nextTickPassed = true; + }); + + readable.on('end', common.mustCall(() => { + assert.strictEqual(nextTickPassed, true); + })); +} + +{ + const readable = new Readable({ + objectMode: true, + read: common.mustCall() + }); + + readable.on('data', (data) => { + console.log('data emitted', data); + }); + + readable.on('end', common.mustCall()); + + setImmediate(() => { + readable.push('aaa'); + readable.push(null); + }); +} diff --git a/test/js/node/test/parallel/test-stream-readable-pause-and-resume.js b/test/js/node/test/parallel/test-stream-readable-pause-and-resume.js new file mode 100644 index 0000000000..53229ec333 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-pause-and-resume.js @@ -0,0 +1,74 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +let ticks = 18; +let expectedData = 19; + +const rs = new Readable({ + objectMode: true, + read: () => { + if (ticks-- > 0) + return process.nextTick(() => rs.push({})); + rs.push({}); + rs.push(null); + } +}); + +rs.on('end', common.mustCall()); +readAndPause(); + +function readAndPause() { + // Does a on(data) -> pause -> wait -> resume -> on(data) ... loop. + // Expects on(data) to never fire if the stream is paused. + const ondata = common.mustCall((data) => { + rs.pause(); + + expectedData--; + if (expectedData <= 0) + return; + + setImmediate(function() { + rs.removeListener('data', ondata); + readAndPause(); + rs.resume(); + }); + }, 1); // Only call ondata once + + rs.on('data', ondata); +} + +{ + const readable = new Readable({ + read() {} + }); + + function read() {} + + readable.setEncoding('utf8'); + readable.on('readable', read); + readable.removeListener('readable', read); + readable.pause(); + + process.nextTick(function() { + assert(readable.isPaused()); + }); +} + +{ + const { PassThrough } = require('stream'); + + const source3 = new PassThrough(); + const target3 = new PassThrough(); + + const chunk = Buffer.allocUnsafe(1000); + while (target3.write(chunk)); + + source3.pipe(target3); + target3.on('drain', common.mustCall(() => { + assert(!source3.isPaused()); + })); + target3.on('data', () => {}); +} diff --git a/test/js/node/test/parallel/test-stream-readable-readable-then-resume.js b/test/js/node/test/parallel/test-stream-readable-readable-then-resume.js new file mode 100644 index 0000000000..63dbc306e7 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-readable-then-resume.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const { Readable } = require('stream'); +const assert = require('assert'); + +// This test verifies that a stream could be resumed after +// removing the readable event in the same tick + +check(new Readable({ + objectMode: true, + highWaterMark: 1, + read() { + if (!this.first) { + this.push('hello'); + this.first = true; + return; + } + + this.push(null); + } +})); + +function check(s) { + const readableListener = common.mustNotCall(); + s.on('readable', readableListener); + s.on('end', common.mustCall()); + assert.strictEqual(s.removeListener, s.off); + s.removeListener('readable', readableListener); + s.resume(); +} diff --git a/test/js/node/test/parallel/test-stream-readable-readable.js b/test/js/node/test/parallel/test-stream-readable-readable.js new file mode 100644 index 0000000000..6e1a7fb32e --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-readable.js @@ -0,0 +1,45 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Readable } = require('stream'); + +{ + const r = new Readable({ + read() {} + }); + assert.strictEqual(r.readable, true); + r.destroy(); + assert.strictEqual(r.readable, false); +} + +{ + const mustNotCall = common.mustNotCall(); + const r = new Readable({ + read() {} + }); + assert.strictEqual(r.readable, true); + r.on('end', mustNotCall); + r.resume(); + r.push(null); + assert.strictEqual(r.readable, true); + r.off('end', mustNotCall); + r.on('end', common.mustCall(() => { + assert.strictEqual(r.readable, false); + })); +} + +{ + const r = new Readable({ + read: common.mustCall(() => { + process.nextTick(() => { + r.destroy(new Error()); + assert.strictEqual(r.readable, false); + }); + }) + }); + r.resume(); + r.on('error', common.mustCall(() => { + assert.strictEqual(r.readable, false); + })); +} diff --git a/test/js/node/test/parallel/test-stream-readable-reading-readingMore.js b/test/js/node/test/parallel/test-stream-readable-reading-readingMore.js new file mode 100644 index 0000000000..5e39c86dcb --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-reading-readingMore.js @@ -0,0 +1,171 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const Readable = require('stream').Readable; + +{ + const readable = new Readable({ + read(size) {} + }); + + const state = readable._readableState; + + // Starting off with false initially. + assert.strictEqual(state.reading, false); + assert.strictEqual(state.readingMore, false); + + readable.on('data', common.mustCall((data) => { + // While in a flowing state with a 'readable' listener + // we should not be reading more + if (readable.readableFlowing) + assert.strictEqual(state.readingMore, true); + + // Reading as long as we've not ended + assert.strictEqual(state.reading, !state.ended); + }, 2)); + + function onStreamEnd() { + // End of stream; state.reading is false + // And so should be readingMore. + assert.strictEqual(state.readingMore, false); + assert.strictEqual(state.reading, false); + } + + const expectedReadingMore = [true, true, false]; + readable.on('readable', common.mustCall(() => { + // There is only one readingMore scheduled from on('data'), + // after which everything is governed by the .read() call + assert.strictEqual(state.readingMore, expectedReadingMore.shift()); + + // If the stream has ended, we shouldn't be reading + assert.strictEqual(state.ended, !state.reading); + + // Consume all the data + while (readable.read() !== null); + + if (expectedReadingMore.length === 0) // Reached end of stream + process.nextTick(common.mustCall(onStreamEnd, 1)); + }, 3)); + + readable.on('end', common.mustCall(onStreamEnd)); + readable.push('pushed'); + + readable.read(6); + + // reading + assert.strictEqual(state.reading, true); + assert.strictEqual(state.readingMore, true); + + // add chunk to front + readable.unshift('unshifted'); + + // end + readable.push(null); +} + +{ + const readable = new Readable({ + read(size) {} + }); + + const state = readable._readableState; + + // Starting off with false initially. + assert.strictEqual(state.reading, false); + assert.strictEqual(state.readingMore, false); + + readable.on('data', common.mustCall((data) => { + // While in a flowing state without a 'readable' listener + // we should be reading more + if (readable.readableFlowing) + assert.strictEqual(state.readingMore, true); + + // Reading as long as we've not ended + assert.strictEqual(state.reading, !state.ended); + }, 2)); + + function onStreamEnd() { + // End of stream; state.reading is false + // And so should be readingMore. + assert.strictEqual(state.readingMore, false); + assert.strictEqual(state.reading, false); + } + + readable.on('end', common.mustCall(onStreamEnd)); + readable.push('pushed'); + + // Stop emitting 'data' events + assert.strictEqual(state.flowing, true); + readable.pause(); + + // paused + assert.strictEqual(state.reading, false); + assert.strictEqual(state.flowing, false); + + readable.resume(); + assert.strictEqual(state.reading, false); + assert.strictEqual(state.flowing, true); + + // add chunk to front + readable.unshift('unshifted'); + + // end + readable.push(null); +} + +{ + const readable = new Readable({ + read(size) {} + }); + + const state = readable._readableState; + + // Starting off with false initially. + assert.strictEqual(state.reading, false); + assert.strictEqual(state.readingMore, false); + + const onReadable = common.mustNotCall(); + + readable.on('readable', onReadable); + + readable.on('data', common.mustCall((data) => { + // Reading as long as we've not ended + assert.strictEqual(state.reading, !state.ended); + }, 2)); + + readable.removeListener('readable', onReadable); + + function onStreamEnd() { + // End of stream; state.reading is false + // And so should be readingMore. + assert.strictEqual(state.readingMore, false); + assert.strictEqual(state.reading, false); + } + + readable.on('end', common.mustCall(onStreamEnd)); + readable.push('pushed'); + + // We are still not flowing, we will be resuming in the next tick + assert.strictEqual(state.flowing, false); + + // Wait for nextTick, so the readableListener flag resets + process.nextTick(function() { + readable.resume(); + + // Stop emitting 'data' events + assert.strictEqual(state.flowing, true); + readable.pause(); + + // paused + assert.strictEqual(state.flowing, false); + + readable.resume(); + assert.strictEqual(state.flowing, true); + + // add chunk to front + readable.unshift('unshifted'); + + // end + readable.push(null); + }); +} diff --git a/test/js/node/test/parallel/test-stream-readable-resume-hwm.js b/test/js/node/test/parallel/test-stream-readable-resume-hwm.js new file mode 100644 index 0000000000..3f0bbad243 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-resume-hwm.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const { Readable } = require('stream'); + +// readable.resume() should not lead to a ._read() call being scheduled +// when we exceed the high water mark already. + +const readable = new Readable({ + read: common.mustNotCall(), + highWaterMark: 100 +}); + +// Fill up the internal buffer so that we definitely exceed the HWM: +for (let i = 0; i < 10; i++) + readable.push('a'.repeat(200)); + +// Call resume, and pause after one chunk. +// The .pause() is just so that we don’t empty the buffer fully, which would +// be a valid reason to call ._read(). +readable.resume(); +readable.once('data', common.mustCall(() => readable.pause())); diff --git a/test/js/node/test/parallel/test-stream-readable-resumeScheduled.js b/test/js/node/test/parallel/test-stream-readable-resumeScheduled.js new file mode 100644 index 0000000000..aa521629b6 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-resumeScheduled.js @@ -0,0 +1,65 @@ +'use strict'; +const common = require('../common'); + +// Testing Readable Stream resumeScheduled state + +const assert = require('assert'); +const { Readable, Writable } = require('stream'); + +{ + // pipe() test case + const r = new Readable({ read() {} }); + const w = new Writable(); + + // resumeScheduled should start = `false`. + assert.strictEqual(r._readableState.resumeScheduled, false); + + // Calling pipe() should change the state value = true. + r.pipe(w); + assert.strictEqual(r._readableState.resumeScheduled, true); + + process.nextTick(common.mustCall(() => { + assert.strictEqual(r._readableState.resumeScheduled, false); + })); +} + +{ + // 'data' listener test case + const r = new Readable({ read() {} }); + + // resumeScheduled should start = `false`. + assert.strictEqual(r._readableState.resumeScheduled, false); + + r.push(Buffer.from([1, 2, 3])); + + // Adding 'data' listener should change the state value + r.on('data', common.mustCall(() => { + assert.strictEqual(r._readableState.resumeScheduled, false); + })); + assert.strictEqual(r._readableState.resumeScheduled, true); + + process.nextTick(common.mustCall(() => { + assert.strictEqual(r._readableState.resumeScheduled, false); + })); +} + +{ + // resume() test case + const r = new Readable({ read() {} }); + + // resumeScheduled should start = `false`. + assert.strictEqual(r._readableState.resumeScheduled, false); + + // Calling resume() should change the state value. + r.resume(); + assert.strictEqual(r._readableState.resumeScheduled, true); + + r.on('resume', common.mustCall(() => { + // The state value should be `false` again + assert.strictEqual(r._readableState.resumeScheduled, false); + })); + + process.nextTick(common.mustCall(() => { + assert.strictEqual(r._readableState.resumeScheduled, false); + })); +} diff --git a/test/js/node/test/parallel/test-stream-readable-setEncoding-existing-buffers.js b/test/js/node/test/parallel/test-stream-readable-setEncoding-existing-buffers.js new file mode 100644 index 0000000000..eb75260bac --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-setEncoding-existing-buffers.js @@ -0,0 +1,60 @@ +'use strict'; +require('../common'); +const { Readable } = require('stream'); +const assert = require('assert'); + +{ + // Call .setEncoding() while there are bytes already in the buffer. + const r = new Readable({ read() {} }); + + r.push(Buffer.from('a')); + r.push(Buffer.from('b')); + + r.setEncoding('utf8'); + const chunks = []; + r.on('data', (chunk) => chunks.push(chunk)); + + process.nextTick(() => { + assert.deepStrictEqual(chunks, ['ab']); + }); +} + +{ + // Call .setEncoding() while the buffer contains a complete, + // but chunked character. + const r = new Readable({ read() {} }); + + r.push(Buffer.from([0xf0])); + r.push(Buffer.from([0x9f])); + r.push(Buffer.from([0x8e])); + r.push(Buffer.from([0x89])); + + r.setEncoding('utf8'); + const chunks = []; + r.on('data', (chunk) => chunks.push(chunk)); + + process.nextTick(() => { + assert.deepStrictEqual(chunks, ['🎉']); + }); +} + +{ + // Call .setEncoding() while the buffer contains an incomplete character, + // and finish the character later. + const r = new Readable({ read() {} }); + + r.push(Buffer.from([0xf0])); + r.push(Buffer.from([0x9f])); + + r.setEncoding('utf8'); + + r.push(Buffer.from([0x8e])); + r.push(Buffer.from([0x89])); + + const chunks = []; + r.on('data', (chunk) => chunks.push(chunk)); + + process.nextTick(() => { + assert.deepStrictEqual(chunks, ['🎉']); + }); +} diff --git a/test/js/node/test/parallel/test-stream-readable-setEncoding-null.js b/test/js/node/test/parallel/test-stream-readable-setEncoding-null.js new file mode 100644 index 0000000000..b95b26bb79 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-setEncoding-null.js @@ -0,0 +1,15 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + + +{ + const readable = new Readable({ encoding: 'hex' }); + assert.strictEqual(readable._readableState.encoding, 'hex'); + + readable.setEncoding(null); + + assert.strictEqual(readable._readableState.encoding, 'utf8'); +} diff --git a/test/js/node/test/parallel/test-stream-readable-strategy-option.js b/test/js/node/test/parallel/test-stream-readable-strategy-option.js new file mode 100644 index 0000000000..a32e70ef21 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-strategy-option.js @@ -0,0 +1,75 @@ +'use strict'; +const common = require('../common'); +const { Readable } = require('stream'); +const assert = require('assert'); +const { strictEqual } = require('assert'); + +{ + // Strategy 2 + const streamData = ['a', 'b', 'c', null]; + + // Fulfill a Readable object + const readable = new Readable({ + read: common.mustCall(() => { + process.nextTick(() => { + readable.push(streamData.shift()); + }); + }, streamData.length), + }); + + // Use helper to convert it to a Web ReadableStream using ByteLength strategy + const readableStream = Readable.toWeb(readable, { + strategy: new ByteLengthQueuingStrategy({ highWaterMark: 1 }), + }); + + assert(!readableStream.locked); + readableStream.getReader().read().then(common.mustCall()); +} + +{ + // Strategy 2 + const streamData = ['a', 'b', 'c', null]; + + // Fulfill a Readable object + const readable = new Readable({ + read: common.mustCall(() => { + process.nextTick(() => { + readable.push(streamData.shift()); + }); + }, streamData.length), + }); + + // Use helper to convert it to a Web ReadableStream using Count strategy + const readableStream = Readable.toWeb(readable, { + strategy: new CountQueuingStrategy({ highWaterMark: 1 }), + }); + + assert(!readableStream.locked); + readableStream.getReader().read().then(common.mustCall()); +} + +{ + const desireSizeExpected = 2; + + const stringStream = new ReadableStream( + { + start(controller) { + // Check if the strategy is being assigned on the init of the ReadableStream + strictEqual(controller.desiredSize, desireSizeExpected); + controller.enqueue('a'); + controller.enqueue('b'); + controller.close(); + }, + }, + new CountQueuingStrategy({ highWaterMark: desireSizeExpected }) + ); + + const reader = stringStream.getReader(); + + reader.read().then(common.mustCall()); + reader.read().then(common.mustCall()); + reader.read().then(({ value, done }) => { + strictEqual(value, undefined); + strictEqual(done, true); + }); +} diff --git a/test/js/node/test/parallel/test-stream-readable-to-web-termination.js b/test/js/node/test/parallel/test-stream-readable-to-web-termination.js new file mode 100644 index 0000000000..13fce9bc71 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-to-web-termination.js @@ -0,0 +1,12 @@ +'use strict'; +require('../common'); +const { Readable } = require('stream'); + +{ + const r = Readable.from([]); + // Cancelling reader while closing should not cause uncaught exceptions + r.on('close', () => reader.cancel()); + + const reader = Readable.toWeb(r).getReader(); + reader.read(); +} diff --git a/test/js/node/test/parallel/test-stream-readable-unpipe-resume.js b/test/js/node/test/parallel/test-stream-readable-unpipe-resume.js new file mode 100644 index 0000000000..b40f724bcc --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-unpipe-resume.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const stream = require('stream'); +const fs = require('fs'); + +const readStream = fs.createReadStream(process.execPath); + +const transformStream = new stream.Transform({ + transform: common.mustCall(() => { + readStream.unpipe(); + readStream.resume(); + }) +}); + +readStream.on('end', common.mustCall()); + +readStream + .pipe(transformStream) + .resume(); diff --git a/test/js/node/test/parallel/test-stream-readable-unshift.js b/test/js/node/test/parallel/test-stream-readable-unshift.js new file mode 100644 index 0000000000..e39a9abf36 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-unshift.js @@ -0,0 +1,170 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +{ + // Check that strings are saved as Buffer + const readable = new Readable({ read() {} }); + + const string = 'abc'; + + readable.on('data', common.mustCall((chunk) => { + assert(Buffer.isBuffer(chunk)); + assert.strictEqual(chunk.toString('utf8'), string); + }, 1)); + + readable.unshift(string); + +} + +{ + // Check that data goes at the beginning + const readable = new Readable({ read() {} }); + const unshift = 'front'; + const push = 'back'; + + const expected = [unshift, push]; + readable.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk.toString('utf8'), expected.shift()); + }, 2)); + + + readable.push(push); + readable.unshift(unshift); +} + +{ + // Check that buffer is saved with correct encoding + const readable = new Readable({ read() {} }); + + const encoding = 'base64'; + const string = Buffer.from('abc').toString(encoding); + + readable.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk.toString(encoding), string); + }, 1)); + + readable.unshift(string, encoding); + +} + +{ + + const streamEncoding = 'base64'; + + function checkEncoding(readable) { + + // chunk encodings + const encodings = ['utf8', 'binary', 'hex', 'base64']; + const expected = []; + + readable.on('data', common.mustCall((chunk) => { + const { encoding, string } = expected.pop(); + assert.strictEqual(chunk.toString(encoding), string); + }, encodings.length)); + + for (const encoding of encodings) { + const string = 'abc'; + + // If encoding is the same as the state.encoding the string is + // saved as is + const expect = encoding !== streamEncoding ? + Buffer.from(string, encoding).toString(streamEncoding) : string; + + expected.push({ encoding, string: expect }); + + readable.unshift(string, encoding); + } + } + + const r1 = new Readable({ read() {} }); + r1.setEncoding(streamEncoding); + checkEncoding(r1); + + const r2 = new Readable({ read() {}, encoding: streamEncoding }); + checkEncoding(r2); + +} + +{ + // Both .push & .unshift should have the same behaviour + // When setting an encoding, each chunk should be emitted with that encoding + const encoding = 'base64'; + + function checkEncoding(readable) { + const string = 'abc'; + readable.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk, Buffer.from(string).toString(encoding)); + }, 2)); + + readable.push(string); + readable.unshift(string); + } + + const r1 = new Readable({ read() {} }); + r1.setEncoding(encoding); + checkEncoding(r1); + + const r2 = new Readable({ read() {}, encoding }); + checkEncoding(r2); + +} + +{ + // Check that ObjectMode works + const readable = new Readable({ objectMode: true, read() {} }); + + const chunks = ['a', 1, {}, []]; + + readable.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk, chunks.pop()); + }, chunks.length)); + + for (const chunk of chunks) { + readable.unshift(chunk); + } +} + +{ + + // Should not throw: https://github.com/nodejs/node/issues/27192 + const highWaterMark = 50; + class ArrayReader extends Readable { + constructor(opt) { + super({ highWaterMark }); + // The error happened only when pushing above hwm + this.buffer = new Array(highWaterMark * 2).fill(0).map(String); + } + _read(size) { + while (this.buffer.length) { + const chunk = this.buffer.shift(); + if (!this.buffer.length) { + this.push(chunk); + this.push(null); + return true; + } + if (!this.push(chunk)) + return; + } + } + } + + function onRead() { + while (null !== (stream.read())) { + // Remove the 'readable' listener before unshifting + stream.removeListener('readable', onRead); + stream.unshift('a'); + stream.on('data', common.mustCall((chunk) => { + // console.log(chunk.length); + }, 50)); + break; + } + } + + const stream = new ArrayReader(); + stream.once('readable', common.mustCall(onRead)); + stream.on('end', common.mustCall()); + +} diff --git a/test/js/node/test/parallel/test-stream-readable-with-unimplemented-_read.js b/test/js/node/test/parallel/test-stream-readable-with-unimplemented-_read.js new file mode 100644 index 0000000000..85e83aa3b6 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readable-with-unimplemented-_read.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const { Readable } = require('stream'); + +const readable = new Readable(); + +readable.read(); +readable.on('error', common.expectsError({ + code: 'ERR_METHOD_NOT_IMPLEMENTED', + name: 'Error', + message: 'The _read() method is not implemented' +})); +readable.on('close', common.mustCall()); diff --git a/test/js/node/test/parallel/test-stream-readableListening-state.js b/test/js/node/test/parallel/test-stream-readableListening-state.js new file mode 100644 index 0000000000..5e3071faf3 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-readableListening-state.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +const r = new stream.Readable({ + read: () => {} +}); + +// readableListening state should start in `false`. +assert.strictEqual(r._readableState.readableListening, false); + +r.on('readable', common.mustCall(() => { + // Inside the readable event this state should be true. + assert.strictEqual(r._readableState.readableListening, true); +})); + +r.push(Buffer.from('Testing readableListening state')); + +const r2 = new stream.Readable({ + read: () => {} +}); + +// readableListening state should start in `false`. +assert.strictEqual(r2._readableState.readableListening, false); + +r2.on('data', common.mustCall((chunk) => { + // readableListening should be false because we don't have + // a `readable` listener + assert.strictEqual(r2._readableState.readableListening, false); +})); + +r2.push(Buffer.from('Testing readableListening state')); diff --git a/test/js/node/test/parallel/test-stream-reduce.js b/test/js/node/test/parallel/test-stream-reduce.js new file mode 100644 index 0000000000..4cee2b5d71 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-reduce.js @@ -0,0 +1,132 @@ +'use strict'; + +const common = require('../common'); +const { + Readable, +} = require('stream'); +const assert = require('assert'); + +function sum(p, c) { + return p + c; +} + +{ + // Does the same thing as `(await stream.toArray()).reduce(...)` + (async () => { + const tests = [ + [[], sum, 0], + [[1], sum, 0], + [[1, 2, 3, 4, 5], sum, 0], + [[...Array(100).keys()], sum, 0], + [['a', 'b', 'c'], sum, ''], + [[1, 2], sum], + [[1, 2, 3], (x, y) => y], + ]; + for (const [values, fn, initial] of tests) { + const streamReduce = await Readable.from(values) + .reduce(fn, initial); + const arrayReduce = values.reduce(fn, initial); + assert.deepStrictEqual(streamReduce, arrayReduce); + } + // Does the same thing as `(await stream.toArray()).reduce(...)` with an + // asynchronous reducer + for (const [values, fn, initial] of tests) { + const streamReduce = await Readable.from(values) + .map(async (x) => x) + .reduce(fn, initial); + const arrayReduce = values.reduce(fn, initial); + assert.deepStrictEqual(streamReduce, arrayReduce); + } + })().then(common.mustCall()); +} +{ + // Works with an async reducer, with or without initial value + (async () => { + const six = await Readable.from([1, 2, 3]).reduce(async (p, c) => p + c, 0); + assert.strictEqual(six, 6); + })().then(common.mustCall()); + (async () => { + const six = await Readable.from([1, 2, 3]).reduce(async (p, c) => p + c); + assert.strictEqual(six, 6); + })().then(common.mustCall()); +} +{ + // Works lazily + assert.rejects(Readable.from([1, 2, 3, 4, 5, 6]) + .map(common.mustCall((x) => { + return x; + }, 3)) // Two consumed and one buffered by `map` due to default concurrency + .reduce(async (p, c) => { + if (p === 1) { + throw new Error('boom'); + } + return c; + }, 0) + , /boom/).then(common.mustCall()); +} + +{ + // Support for AbortSignal + const ac = new AbortController(); + assert.rejects(async () => { + await Readable.from([1, 2, 3]).reduce(async (p, c) => { + if (c === 3) { + await new Promise(() => {}); // Explicitly do not pass signal here + } + return Promise.resolve(); + }, 0, { signal: ac.signal }); + }, { + name: 'AbortError', + }).then(common.mustCall()); + ac.abort(); +} + + +{ + // Support for AbortSignal - pre aborted + const stream = Readable.from([1, 2, 3]); + assert.rejects(async () => { + await stream.reduce(async (p, c) => { + if (c === 3) { + await new Promise(() => {}); // Explicitly do not pass signal here + } + return Promise.resolve(); + }, 0, { signal: AbortSignal.abort() }); + }, { + name: 'AbortError', + }).then(common.mustCall(() => { + assert.strictEqual(stream.destroyed, true); + })); +} + +{ + // Support for AbortSignal - deep + const stream = Readable.from([1, 2, 3]); + assert.rejects(async () => { + await stream.reduce(async (p, c, { signal }) => { + signal.addEventListener('abort', common.mustCall(), { once: true }); + if (c === 3) { + await new Promise(() => {}); // Explicitly do not pass signal here + } + return Promise.resolve(); + }, 0, { signal: AbortSignal.abort() }); + }, { + name: 'AbortError', + }).then(common.mustCall(() => { + assert.strictEqual(stream.destroyed, true); + })); +} + +{ + // Error cases + assert.rejects(() => Readable.from([]).reduce(1), /TypeError/).then(common.mustCall()); + assert.rejects(() => Readable.from([]).reduce('5'), /TypeError/).then(common.mustCall()); + assert.rejects(() => Readable.from([]).reduce((x, y) => x + y, 0, 1), /ERR_INVALID_ARG_TYPE/).then(common.mustCall()); + assert.rejects(() => Readable.from([]).reduce((x, y) => x + y, 0, { signal: true }), /ERR_INVALID_ARG_TYPE/).then(common.mustCall()); +} + +{ + // Test result is a Promise + const result = Readable.from([1, 2, 3, 4, 5]).reduce(sum, 0); + assert.ok(result instanceof Promise); +} diff --git a/test/js/node/test/parallel/test-stream-set-default-hwm.js b/test/js/node/test/parallel/test-stream-set-default-hwm.js new file mode 100644 index 0000000000..3d78907b74 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-set-default-hwm.js @@ -0,0 +1,36 @@ +'use strict'; + +require('../common'); + +const assert = require('node:assert'); +const { + setDefaultHighWaterMark, + getDefaultHighWaterMark, + Writable, + Readable, + Transform +} = require('stream'); + +assert.notStrictEqual(getDefaultHighWaterMark(false), 32 * 1000); +setDefaultHighWaterMark(false, 32 * 1000); +assert.strictEqual(getDefaultHighWaterMark(false), 32 * 1000); + +assert.notStrictEqual(getDefaultHighWaterMark(true), 32); +setDefaultHighWaterMark(true, 32); +assert.strictEqual(getDefaultHighWaterMark(true), 32); + +const w = new Writable({ + write() {} +}); +assert.strictEqual(w.writableHighWaterMark, 32 * 1000); + +const r = new Readable({ + read() {} +}); +assert.strictEqual(r.readableHighWaterMark, 32 * 1000); + +const t = new Transform({ + transform() {} +}); +assert.strictEqual(t.writableHighWaterMark, 32 * 1000); +assert.strictEqual(t.readableHighWaterMark, 32 * 1000); diff --git a/test/js/node/test/parallel/test-stream-some-find-every.mjs b/test/js/node/test/parallel/test-stream-some-find-every.mjs new file mode 100644 index 0000000000..0617102bc4 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-some-find-every.mjs @@ -0,0 +1,172 @@ +import * as common from '../common/index.mjs'; +import { setTimeout } from 'timers/promises'; +import { Readable } from 'stream'; +import assert from 'assert'; + + +function oneTo5() { + return Readable.from([1, 2, 3, 4, 5]); +} + +function oneTo5Async() { + return oneTo5().map(async (x) => { + await Promise.resolve(); + return x; + }); +} +{ + // Some, find, and every work with a synchronous stream and predicate + assert.strictEqual(await oneTo5().some((x) => x > 3), true); + assert.strictEqual(await oneTo5().every((x) => x > 3), false); + assert.strictEqual(await oneTo5().find((x) => x > 3), 4); + assert.strictEqual(await oneTo5().some((x) => x > 6), false); + assert.strictEqual(await oneTo5().every((x) => x < 6), true); + assert.strictEqual(await oneTo5().find((x) => x > 6), undefined); + assert.strictEqual(await Readable.from([]).some(() => true), false); + assert.strictEqual(await Readable.from([]).every(() => true), true); + assert.strictEqual(await Readable.from([]).find(() => true), undefined); +} + +{ + // Some, find, and every work with an asynchronous stream and synchronous predicate + assert.strictEqual(await oneTo5Async().some((x) => x > 3), true); + assert.strictEqual(await oneTo5Async().every((x) => x > 3), false); + assert.strictEqual(await oneTo5Async().find((x) => x > 3), 4); + assert.strictEqual(await oneTo5Async().some((x) => x > 6), false); + assert.strictEqual(await oneTo5Async().every((x) => x < 6), true); + assert.strictEqual(await oneTo5Async().find((x) => x > 6), undefined); +} + +{ + // Some, find, and every work on synchronous streams with an asynchronous predicate + assert.strictEqual(await oneTo5().some(async (x) => x > 3), true); + assert.strictEqual(await oneTo5().every(async (x) => x > 3), false); + assert.strictEqual(await oneTo5().find(async (x) => x > 3), 4); + assert.strictEqual(await oneTo5().some(async (x) => x > 6), false); + assert.strictEqual(await oneTo5().every(async (x) => x < 6), true); + assert.strictEqual(await oneTo5().find(async (x) => x > 6), undefined); +} + +{ + // Some, find, and every work on asynchronous streams with an asynchronous predicate + assert.strictEqual(await oneTo5Async().some(async (x) => x > 3), true); + assert.strictEqual(await oneTo5Async().every(async (x) => x > 3), false); + assert.strictEqual(await oneTo5Async().find(async (x) => x > 3), 4); + assert.strictEqual(await oneTo5Async().some(async (x) => x > 6), false); + assert.strictEqual(await oneTo5Async().every(async (x) => x < 6), true); + assert.strictEqual(await oneTo5Async().find(async (x) => x > 6), undefined); +} + +{ + async function checkDestroyed(stream) { + await setTimeout(); + assert.strictEqual(stream.destroyed, true); + } + + { + // Some, find, and every short circuit + const someStream = oneTo5(); + await someStream.some(common.mustCall((x) => x > 2, 3)); + await checkDestroyed(someStream); + + const everyStream = oneTo5(); + await everyStream.every(common.mustCall((x) => x < 3, 3)); + await checkDestroyed(everyStream); + + const findStream = oneTo5(); + await findStream.find(common.mustCall((x) => x > 1, 2)); + await checkDestroyed(findStream); + + // When short circuit isn't possible the whole stream is iterated + await oneTo5().some(common.mustCall(() => false, 5)); + await oneTo5().every(common.mustCall(() => true, 5)); + await oneTo5().find(common.mustCall(() => false, 5)); + } + + { + // Some, find, and every short circuit async stream/predicate + const someStream = oneTo5Async(); + await someStream.some(common.mustCall(async (x) => x > 2, 3)); + await checkDestroyed(someStream); + + const everyStream = oneTo5Async(); + await everyStream.every(common.mustCall(async (x) => x < 3, 3)); + await checkDestroyed(everyStream); + + const findStream = oneTo5Async(); + await findStream.find(common.mustCall(async (x) => x > 1, 2)); + await checkDestroyed(findStream); + + // When short circuit isn't possible the whole stream is iterated + await oneTo5Async().some(common.mustCall(async () => false, 5)); + await oneTo5Async().every(common.mustCall(async () => true, 5)); + await oneTo5Async().find(common.mustCall(async () => false, 5)); + } +} + +{ + // Concurrency doesn't affect which value is found. + const found = await Readable.from([1, 2]).find(async (val) => { + if (val === 1) { + await setTimeout(100); + } + return true; + }, { concurrency: 2 }); + assert.strictEqual(found, 1); +} + +{ + // Support for AbortSignal + for (const op of ['some', 'every', 'find']) { + { + const ac = new AbortController(); + assert.rejects(Readable.from([1, 2, 3])[op]( + () => new Promise(() => { }), + { signal: ac.signal } + ), { + name: 'AbortError', + }, `${op} should abort correctly with sync abort`).then(common.mustCall()); + ac.abort(); + } + { + // Support for pre-aborted AbortSignal + assert.rejects(Readable.from([1, 2, 3])[op]( + () => new Promise(() => { }), + { signal: AbortSignal.abort() } + ), { + name: 'AbortError', + }, `${op} should abort with pre-aborted abort controller`).then(common.mustCall()); + } + } +} +{ + // Error cases + for (const op of ['some', 'every', 'find']) { + assert.rejects(async () => { + await Readable.from([1])[op](1); + }, /ERR_INVALID_ARG_TYPE/, `${op} should throw for invalid function`).then(common.mustCall()); + assert.rejects(async () => { + await Readable.from([1])[op]((x) => x, { + concurrency: 'Foo' + }); + }, /ERR_OUT_OF_RANGE/, `${op} should throw for invalid concurrency`).then(common.mustCall()); + assert.rejects(async () => { + await Readable.from([1])[op]((x) => x, 1); + }, /ERR_INVALID_ARG_TYPE/, `${op} should throw for invalid concurrency`).then(common.mustCall()); + assert.rejects(async () => { + await Readable.from([1])[op]((x) => x, { + signal: true + }); + }, /ERR_INVALID_ARG_TYPE/, `${op} should throw for invalid signal`).then(common.mustCall()); + } +} +{ + for (const op of ['some', 'every', 'find']) { + const stream = oneTo5(); + Object.defineProperty(stream, 'map', { + value: common.mustNotCall(), + }); + // Check that map isn't getting called. + stream[op](() => {}); + } +} diff --git a/test/js/node/test/parallel/test-stream-toArray.js b/test/js/node/test/parallel/test-stream-toArray.js new file mode 100644 index 0000000000..690b3c4b08 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-toArray.js @@ -0,0 +1,91 @@ +'use strict'; + +const common = require('../common'); +const { Readable } = require('stream'); +const assert = require('assert'); + +{ + // Works on a synchronous stream + (async () => { + const tests = [ + [], + [1], + [1, 2, 3], + Array(100).fill().map((_, i) => i), + ]; + for (const test of tests) { + const stream = Readable.from(test); + const result = await stream.toArray(); + assert.deepStrictEqual(result, test); + } + })().then(common.mustCall()); +} + +{ + // Works on a non-object-mode stream + (async () => { + const firstBuffer = Buffer.from([1, 2, 3]); + const secondBuffer = Buffer.from([4, 5, 6]); + const stream = Readable.from( + [firstBuffer, secondBuffer], + { objectMode: false }); + const result = await stream.toArray(); + assert.strictEqual(Array.isArray(result), true); + assert.deepStrictEqual(result, [firstBuffer, secondBuffer]); + })().then(common.mustCall()); +} + +{ + // Works on an asynchronous stream + (async () => { + const tests = [ + [], + [1], + [1, 2, 3], + Array(100).fill().map((_, i) => i), + ]; + for (const test of tests) { + const stream = Readable.from(test).map((x) => Promise.resolve(x)); + const result = await stream.toArray(); + assert.deepStrictEqual(result, test); + } + })().then(common.mustCall()); +} + +{ + // Support for AbortSignal + const ac = new AbortController(); + let stream; + assert.rejects(async () => { + stream = Readable.from([1, 2, 3]).map(async (x) => { + if (x === 3) { + await new Promise(() => {}); // Explicitly do not pass signal here + } + return Promise.resolve(x); + }); + await stream.toArray({ signal: ac.signal }); + }, { + name: 'AbortError', + }).then(common.mustCall(() => { + // Only stops toArray, does not destroy the stream + assert(stream.destroyed, false); + })); + ac.abort(); +} +{ + // Test result is a Promise + const result = Readable.from([1, 2, 3, 4, 5]).toArray(); + assert.strictEqual(result instanceof Promise, true); +} +{ + // Error cases + assert.rejects(async () => { + await Readable.from([1]).toArray(1); + }, /ERR_INVALID_ARG_TYPE/).then(common.mustCall()); + + assert.rejects(async () => { + await Readable.from([1]).toArray({ + signal: true + }); + }, /ERR_INVALID_ARG_TYPE/).then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-toWeb-allows-server-response.js b/test/js/node/test/parallel/test-stream-toWeb-allows-server-response.js new file mode 100644 index 0000000000..fd7a14d596 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-toWeb-allows-server-response.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const { Writable } = require('stream'); + +const assert = require('assert'); +const http = require('http'); + +// Check if Writable.toWeb works on the response object after creating a server. +const server = http.createServer( + common.mustCall((req, res) => { + const webStreamResponse = Writable.toWeb(res); + assert.strictEqual(webStreamResponse instanceof WritableStream, true); + res.end(); + }) +); + +server.listen( + 0, + common.mustCall(() => { + http.get( + { + port: server.address().port, + }, + common.mustCall(() => { + server.close(); + }) + ); + }) +); diff --git a/test/js/node/test/parallel/test-stream-transform-callback-twice.js b/test/js/node/test/parallel/test-stream-transform-callback-twice.js new file mode 100644 index 0000000000..bf2ccdcde4 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-callback-twice.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +const { Transform } = require('stream'); +const stream = new Transform({ + transform(chunk, enc, cb) { cb(); cb(); } +}); + +stream.on('error', common.expectsError({ + name: 'Error', + message: 'Callback called multiple times', + code: 'ERR_MULTIPLE_CALLBACK' +})); + +stream.write('foo'); diff --git a/test/js/node/test/parallel/test-stream-transform-constructor-set-methods.js b/test/js/node/test/parallel/test-stream-transform-constructor-set-methods.js new file mode 100644 index 0000000000..a20a1a07cf --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-constructor-set-methods.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const { Transform } = require('stream'); + +const t = new Transform(); + +assert.throws( + () => { + t.end(Buffer.from('blerg')); + }, + { + name: 'Error', + code: 'ERR_METHOD_NOT_IMPLEMENTED', + message: 'The _transform() method is not implemented' + } +); + +const _transform = common.mustCall((chunk, _, next) => { + next(); +}); + +const _final = common.mustCall((next) => { + next(); +}); + +const _flush = common.mustCall((next) => { + next(); +}); + +const t2 = new Transform({ + transform: _transform, + flush: _flush, + final: _final +}); + +assert.strictEqual(t2._transform, _transform); +assert.strictEqual(t2._flush, _flush); +assert.strictEqual(t2._final, _final); + +t2.end(Buffer.from('blerg')); +t2.resume(); diff --git a/test/js/node/test/parallel/test-stream-transform-destroy.js b/test/js/node/test/parallel/test-stream-transform-destroy.js new file mode 100644 index 0000000000..428bab9ce3 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-destroy.js @@ -0,0 +1,154 @@ +'use strict'; + +const common = require('../common'); +const { Transform } = require('stream'); +const assert = require('assert'); + +{ + const transform = new Transform({ + transform(chunk, enc, cb) {} + }); + + transform.resume(); + + transform.on('end', common.mustNotCall()); + transform.on('close', common.mustCall()); + transform.on('finish', common.mustNotCall()); + + transform.destroy(); +} + +{ + const transform = new Transform({ + transform(chunk, enc, cb) {} + }); + transform.resume(); + + const expected = new Error('kaboom'); + + transform.on('end', common.mustNotCall()); + transform.on('finish', common.mustNotCall()); + transform.on('close', common.mustCall()); + transform.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + transform.destroy(expected); +} + +{ + const transform = new Transform({ + transform(chunk, enc, cb) {} + }); + + transform._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, expected); + cb(err); + }, 1); + + const expected = new Error('kaboom'); + + transform.on('finish', common.mustNotCall('no finish event')); + transform.on('close', common.mustCall()); + transform.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + transform.destroy(expected); +} + +{ + const expected = new Error('kaboom'); + const transform = new Transform({ + transform(chunk, enc, cb) {}, + destroy: common.mustCall(function(err, cb) { + assert.strictEqual(err, expected); + cb(); + }, 1) + }); + transform.resume(); + + transform.on('end', common.mustNotCall('no end event')); + transform.on('close', common.mustCall()); + transform.on('finish', common.mustNotCall('no finish event')); + + // Error is swallowed by the custom _destroy + transform.on('error', common.mustNotCall('no error event')); + + transform.destroy(expected); +} + +{ + const transform = new Transform({ + transform(chunk, enc, cb) {} + }); + + transform._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(); + }, 1); + + transform.destroy(); +} + +{ + const transform = new Transform({ + transform(chunk, enc, cb) {} + }); + transform.resume(); + + transform._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + process.nextTick(() => { + this.push(null); + this.end(); + cb(); + }); + }, 1); + + const fail = common.mustNotCall('no event'); + + transform.on('finish', fail); + transform.on('end', fail); + transform.on('close', common.mustCall()); + + transform.destroy(); + + transform.removeListener('end', fail); + transform.removeListener('finish', fail); + transform.on('end', common.mustCall()); + transform.on('finish', common.mustNotCall()); +} + +{ + const transform = new Transform({ + transform(chunk, enc, cb) {} + }); + + const expected = new Error('kaboom'); + + transform._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(expected); + }, 1); + + transform.on('close', common.mustCall()); + transform.on('finish', common.mustNotCall('no finish event')); + transform.on('end', common.mustNotCall('no end event')); + transform.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + transform.destroy(); +} + +{ + const transform = new Transform({ + transform(chunk, enc, cb) {} + }); + transform.on('error', common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + })); + transform.on('close', common.mustCall()); + transform[Symbol.asyncDispose]().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-transform-final-sync.js b/test/js/node/test/parallel/test-stream-transform-final-sync.js new file mode 100644 index 0000000000..5cc80703ee --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-final-sync.js @@ -0,0 +1,110 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const stream = require('stream'); +let state = 0; + + +// What you do +// +// const stream = new stream.Transform({ +// transform: function transformCallback(chunk, _, next) { +// // part 1 +// this.push(chunk); +// //part 2 +// next(); +// }, +// final: function endCallback(done) { +// // part 1 +// process.nextTick(function () { +// // part 2 +// done(); +// }); +// }, +// flush: function flushCallback(done) { +// // part 1 +// process.nextTick(function () { +// // part 2 +// done(); +// }); +// } +// }); +// t.on('data', dataListener); +// t.on('end', endListener); +// t.on('finish', finishListener); +// t.write(1); +// t.write(4); +// t.end(7, endMethodCallback); +// +// The order things are called +// +// 1. transformCallback part 1 +// 2. dataListener +// 3. transformCallback part 2 +// 4. transformCallback part 1 +// 5. dataListener +// 6. transformCallback part 2 +// 7. transformCallback part 1 +// 8. dataListener +// 9. transformCallback part 2 +// 10. finalCallback part 1 +// 11. finalCallback part 2 +// 12. flushCallback part 1 +// 13. finishListener +// 14. endMethodCallback +// 15. flushCallback part 2 +// 16. endListener + +const t = new stream.Transform({ + objectMode: true, + transform: common.mustCall(function(chunk, _, next) { + // transformCallback part 1 + assert.strictEqual(++state, chunk); + this.push(state); + // transformCallback part 2 + assert.strictEqual(++state, chunk + 2); + process.nextTick(next); + }, 3), + final: common.mustCall(function(done) { + state++; + // finalCallback part 1 + assert.strictEqual(state, 10); + state++; + // finalCallback part 2 + assert.strictEqual(state, 11); + done(); + }, 1), + flush: common.mustCall(function(done) { + state++; + // fluchCallback part 1 + assert.strictEqual(state, 12); + process.nextTick(function() { + state++; + // fluchCallback part 2 + assert.strictEqual(state, 13); + done(); + }); + }, 1) +}); +t.on('finish', common.mustCall(function() { + state++; + // finishListener + assert.strictEqual(state, 15); +}, 1)); +t.on('end', common.mustCall(function() { + state++; + // endEvent + assert.strictEqual(state, 16); +}, 1)); +t.on('data', common.mustCall(function(d) { + // dataListener + assert.strictEqual(++state, d + 1); +}, 3)); +t.write(1); +t.write(4); +t.end(7, common.mustCall(function() { + state++; + // endMethodCallback + assert.strictEqual(state, 14); +}, 1)); diff --git a/test/js/node/test/parallel/test-stream-transform-final.js b/test/js/node/test/parallel/test-stream-transform-final.js new file mode 100644 index 0000000000..e0b2b7e40f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-final.js @@ -0,0 +1,112 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const stream = require('stream'); +let state = 0; + + +// What you do: +// +// const stream = new stream.Transform({ +// transform: function transformCallback(chunk, _, next) { +// // part 1 +// this.push(chunk); +// //part 2 +// next(); +// }, +// final: function endCallback(done) { +// // part 1 +// process.nextTick(function () { +// // part 2 +// done(); +// }); +// }, +// flush: function flushCallback(done) { +// // part 1 +// process.nextTick(function () { +// // part 2 +// done(); +// }); +// } +// }); +// t.on('data', dataListener); +// t.on('end', endListener); +// t.on('finish', finishListener); +// t.write(1); +// t.write(4); +// t.end(7, endMethodCallback); +// +// The order things are called + +// 1. transformCallback part 1 +// 2. dataListener +// 3. transformCallback part 2 +// 4. transformCallback part 1 +// 5. dataListener +// 6. transformCallback part 2 +// 7. transformCallback part 1 +// 8. dataListener +// 9. transformCallback part 2 +// 10. finalCallback part 1 +// 11. finalCallback part 2 +// 12. flushCallback part 1 +// 13. finishListener +// 14. endMethodCallback +// 15. flushCallback part 2 +// 16. endListener + +const t = new stream.Transform({ + objectMode: true, + transform: common.mustCall(function(chunk, _, next) { + // transformCallback part 1 + assert.strictEqual(++state, chunk); + this.push(state); + // transformCallback part 2 + assert.strictEqual(++state, chunk + 2); + process.nextTick(next); + }, 3), + final: common.mustCall(function(done) { + state++; + // finalCallback part 1 + assert.strictEqual(state, 10); + setTimeout(function() { + state++; + // finalCallback part 2 + assert.strictEqual(state, 11); + done(); + }, 100); + }, 1), + flush: common.mustCall(function(done) { + state++; + // flushCallback part 1 + assert.strictEqual(state, 12); + process.nextTick(function() { + state++; + // flushCallback part 2 + assert.strictEqual(state, 13); + done(); + }); + }, 1) +}); +t.on('finish', common.mustCall(function() { + state++; + // finishListener + assert.strictEqual(state, 15); +}, 1)); +t.on('end', common.mustCall(function() { + state++; + // end event + assert.strictEqual(state, 16); +}, 1)); +t.on('data', common.mustCall(function(d) { + // dataListener + assert.strictEqual(++state, d + 1); +}, 3)); +t.write(1); +t.write(4); +t.end(7, common.mustCall(function() { + state++; + // endMethodCallback + assert.strictEqual(state, 14); +}, 1)); diff --git a/test/js/node/test/parallel/test-stream-transform-flush-data.js b/test/js/node/test/parallel/test-stream-transform-flush-data.js new file mode 100644 index 0000000000..51e2c8bc52 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-flush-data.js @@ -0,0 +1,28 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const Transform = require('stream').Transform; + + +const expected = 'asdf'; + + +function _transform(d, e, n) { + n(); +} + +function _flush(n) { + n(null, expected); +} + +const t = new Transform({ + transform: _transform, + flush: _flush +}); + +t.end(Buffer.from('blerg')); +t.on('data', (data) => { + assert.strictEqual(data.toString(), expected); +}); diff --git a/test/js/node/test/parallel/test-stream-transform-hwm0.js b/test/js/node/test/parallel/test-stream-transform-hwm0.js new file mode 100644 index 0000000000..8e8971f21f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-hwm0.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Transform } = require('stream'); + +const t = new Transform({ + objectMode: true, highWaterMark: 0, + transform(chunk, enc, callback) { + process.nextTick(() => callback(null, chunk, enc)); + } +}); + +assert.strictEqual(t.write(1), false); +t.on('drain', common.mustCall(() => { + assert.strictEqual(t.write(2), false); + t.end(); +})); + +t.once('readable', common.mustCall(() => { + assert.strictEqual(t.read(), 1); + setImmediate(common.mustCall(() => { + assert.strictEqual(t.read(), null); + t.once('readable', common.mustCall(() => { + assert.strictEqual(t.read(), 2); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-stream-transform-objectmode-falsey-value.js b/test/js/node/test/parallel/test-stream-transform-objectmode-falsey-value.js new file mode 100644 index 0000000000..78ede5d100 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-objectmode-falsey-value.js @@ -0,0 +1,51 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const stream = require('stream'); +const PassThrough = stream.PassThrough; + +const src = new PassThrough({ objectMode: true }); +const tx = new PassThrough({ objectMode: true }); +const dest = new PassThrough({ objectMode: true }); + +const expect = [ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]; +const results = []; + +dest.on('data', common.mustCall(function(x) { + results.push(x); +}, expect.length)); + +src.pipe(tx).pipe(dest); + +let i = -1; +const int = setInterval(common.mustCall(function() { + if (results.length === expect.length) { + src.end(); + clearInterval(int); + assert.deepStrictEqual(results, expect); + } else { + src.write(i++); + } +}, expect.length + 1), 1); diff --git a/test/js/node/test/parallel/test-stream-transform-split-highwatermark.js b/test/js/node/test/parallel/test-stream-transform-split-highwatermark.js new file mode 100644 index 0000000000..290c7d957c --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-split-highwatermark.js @@ -0,0 +1,73 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const { Transform, Readable, Writable, getDefaultHighWaterMark } = require('stream'); + +const DEFAULT = getDefaultHighWaterMark(); + +function testTransform(expectedReadableHwm, expectedWritableHwm, options) { + const t = new Transform(options); + assert.strictEqual(t._readableState.highWaterMark, expectedReadableHwm); + assert.strictEqual(t._writableState.highWaterMark, expectedWritableHwm); +} + +// Test overriding defaultHwm +testTransform(666, DEFAULT, { readableHighWaterMark: 666 }); +testTransform(DEFAULT, 777, { writableHighWaterMark: 777 }); +testTransform(666, 777, { + readableHighWaterMark: 666, + writableHighWaterMark: 777, +}); + +// Test highWaterMark overriding +testTransform(555, 555, { + highWaterMark: 555, + readableHighWaterMark: 666, +}); +testTransform(555, 555, { + highWaterMark: 555, + writableHighWaterMark: 777, +}); +testTransform(555, 555, { + highWaterMark: 555, + readableHighWaterMark: 666, + writableHighWaterMark: 777, +}); + +// Test undefined, null +[undefined, null].forEach((v) => { + testTransform(DEFAULT, DEFAULT, { readableHighWaterMark: v }); + testTransform(DEFAULT, DEFAULT, { writableHighWaterMark: v }); + testTransform(666, DEFAULT, { highWaterMark: v, readableHighWaterMark: 666 }); + testTransform(DEFAULT, 777, { highWaterMark: v, writableHighWaterMark: 777 }); +}); + +// test NaN +{ + assert.throws(() => { + new Transform({ readableHighWaterMark: NaN }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.readableHighWaterMark' is invalid. " + + 'Received NaN' + }); + + assert.throws(() => { + new Transform({ writableHighWaterMark: NaN }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.writableHighWaterMark' is invalid. " + + 'Received NaN' + }); +} + +// Test non Duplex streams ignore the options +{ + const r = new Readable({ readableHighWaterMark: 666 }); + assert.strictEqual(r._readableState.highWaterMark, DEFAULT); + const w = new Writable({ writableHighWaterMark: 777 }); + assert.strictEqual(w._writableState.highWaterMark, DEFAULT); +} diff --git a/test/js/node/test/parallel/test-stream-transform-split-objectmode.js b/test/js/node/test/parallel/test-stream-transform-split-objectmode.js new file mode 100644 index 0000000000..f1341290d2 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-transform-split-objectmode.js @@ -0,0 +1,83 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const Transform = require('stream').Transform; + +const parser = new Transform({ + readableObjectMode: true +}); + +assert(parser._readableState.objectMode); +assert(!parser._writableState.objectMode); +assert.strictEqual(parser.readableHighWaterMark, 16); +assert.strictEqual(parser.writableHighWaterMark, process.platform === 'win32' ? 16 * 1024 : 64 * 1024); +assert.strictEqual(parser.readableHighWaterMark, + parser._readableState.highWaterMark); +assert.strictEqual(parser.writableHighWaterMark, + parser._writableState.highWaterMark); + +parser._transform = function(chunk, enc, callback) { + callback(null, { val: chunk[0] }); +}; + +let parsed; + +parser.on('data', function(obj) { + parsed = obj; +}); + +parser.end(Buffer.from([42])); + +process.on('exit', function() { + assert.strictEqual(parsed.val, 42); +}); + + +const serializer = new Transform({ writableObjectMode: true }); + +assert(!serializer._readableState.objectMode); +assert(serializer._writableState.objectMode); +assert.strictEqual(serializer.readableHighWaterMark, process.platform === 'win32' ? 16 * 1024 : 64 * 1024); +assert.strictEqual(serializer.writableHighWaterMark, 16); +assert.strictEqual(parser.readableHighWaterMark, + parser._readableState.highWaterMark); +assert.strictEqual(parser.writableHighWaterMark, + parser._writableState.highWaterMark); + +serializer._transform = function(obj, _, callback) { + callback(null, Buffer.from([obj.val])); +}; + +let serialized; + +serializer.on('data', function(chunk) { + serialized = chunk; +}); + +serializer.write({ val: 42 }); + +process.on('exit', function() { + assert.strictEqual(serialized[0], 42); +}); diff --git a/test/js/node/test/parallel/test-stream-typedarray.js b/test/js/node/test/parallel/test-stream-typedarray.js new file mode 100644 index 0000000000..a374989276 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-typedarray.js @@ -0,0 +1,105 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Readable, Writable } = require('stream'); + +const buffer = Buffer.from('ABCD'); +const views = common.getArrayBufferViews(buffer); + +{ + // Simple Writable test. + let n = 0; + const writable = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + assert(chunk instanceof Buffer); + assert(ArrayBuffer.isView(chunk)); + assert.deepStrictEqual(common.getBufferSources(chunk)[n], views[n]); + n++; + cb(); + }, views.length), + }); + + views.forEach((msg) => writable.write(msg)); + writable.end(); +} + +{ + // Writable test with object mode True. + let n = 0; + const writable = new Writable({ + objectMode: true, + write: common.mustCall((chunk, encoding, cb) => { + assert(!(chunk instanceof Buffer)); + assert(ArrayBuffer.isView(chunk)); + assert.deepStrictEqual(common.getBufferSources(chunk)[n], views[n]); + n++; + cb(); + }, views.length), + }); + + views.forEach((msg) => writable.write(msg)); + writable.end(); +} + + +{ + // Writable test, multiple writes carried out via writev. + let n = 0; + let callback; + const writable = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + assert(chunk instanceof Buffer); + assert(ArrayBuffer.isView(chunk)); + assert.deepStrictEqual(common.getBufferSources(chunk)[n], views[n]); + n++; + callback = cb; + }), + + writev: common.mustCall((chunks, cb) => { + assert.strictEqual(chunks.length, views.length); + let res = ''; + for (const chunk of chunks) { + assert.strictEqual(chunk.encoding, 'buffer'); + res += chunk.chunk; + } + assert.strictEqual(res, 'ABCD'.repeat(9)); + }), + + }); + views.forEach((msg) => writable.write(msg)); + writable.end(views[0]); + callback(); +} + + +{ + // Simple Readable test. + const readable = new Readable({ + read() {} + }); + + readable.push(views[1]); + readable.push(views[2]); + readable.unshift(views[0]); + + const buf = readable.read(); + assert(buf instanceof Buffer); + assert.deepStrictEqual([...buf], [...views[0], ...views[1], ...views[2]]); +} + +{ + // Readable test, setEncoding. + const readable = new Readable({ + read() {} + }); + + readable.setEncoding('utf8'); + + readable.push(views[1]); + readable.push(views[2]); + readable.unshift(views[0]); + + const out = readable.read(); + assert.strictEqual(out, 'ABCD'.repeat(3)); +} diff --git a/test/js/node/test/parallel/test-stream-uint8array.js b/test/js/node/test/parallel/test-stream-uint8array.js new file mode 100644 index 0000000000..f1de4c873f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-uint8array.js @@ -0,0 +1,101 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Readable, Writable } = require('stream'); + +const ABC = new Uint8Array([0x41, 0x42, 0x43]); +const DEF = new Uint8Array([0x44, 0x45, 0x46]); +const GHI = new Uint8Array([0x47, 0x48, 0x49]); + +{ + // Simple Writable test. + + let n = 0; + const writable = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + assert(chunk instanceof Buffer); + if (n++ === 0) { + assert.strictEqual(String(chunk), 'ABC'); + } else { + assert.strictEqual(String(chunk), 'DEF'); + } + + cb(); + }, 2) + }); + + writable.write(ABC); + writable.end(DEF); +} + +{ + // Writable test, pass in Uint8Array in object mode. + + const writable = new Writable({ + objectMode: true, + write: common.mustCall((chunk, encoding, cb) => { + assert(!(chunk instanceof Buffer)); + assert(chunk instanceof Uint8Array); + assert.strictEqual(chunk, ABC); + assert.strictEqual(encoding, undefined); + cb(); + }) + }); + + writable.end(ABC); +} + +{ + // Writable test, multiple writes carried out via writev. + let callback; + + const writable = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + assert(chunk instanceof Buffer); + assert.strictEqual(encoding, 'buffer'); + assert.strictEqual(String(chunk), 'ABC'); + callback = cb; + }), + writev: common.mustCall((chunks, cb) => { + assert.strictEqual(chunks.length, 2); + assert.strictEqual(chunks[0].encoding, 'buffer'); + assert.strictEqual(chunks[1].encoding, 'buffer'); + assert.strictEqual(chunks[0].chunk + chunks[1].chunk, 'DEFGHI'); + }) + }); + + writable.write(ABC); + writable.write(DEF); + writable.end(GHI); + callback(); +} + +{ + // Simple Readable test. + const readable = new Readable({ + read() {} + }); + + readable.push(DEF); + readable.unshift(ABC); + + const buf = readable.read(); + assert(buf instanceof Buffer); + assert.deepStrictEqual([...buf], [...ABC, ...DEF]); +} + +{ + // Readable test, setEncoding. + const readable = new Readable({ + read() {} + }); + + readable.setEncoding('utf8'); + + readable.push(DEF); + readable.unshift(ABC); + + const out = readable.read(); + assert.strictEqual(out, 'ABCDEF'); +} diff --git a/test/js/node/test/parallel/test-stream-unpipe-event.js b/test/js/node/test/parallel/test-stream-unpipe-event.js new file mode 100644 index 0000000000..46cc8e8cb0 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-unpipe-event.js @@ -0,0 +1,85 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Writable, Readable } = require('stream'); +class NullWriteable extends Writable { + _write(chunk, encoding, callback) { + return callback(); + } +} +class QuickEndReadable extends Readable { + _read() { + this.push(null); + } +} +class NeverEndReadable extends Readable { + _read() {} +} + +{ + const dest = new NullWriteable(); + const src = new QuickEndReadable(); + dest.on('pipe', common.mustCall()); + dest.on('unpipe', common.mustCall()); + src.pipe(dest); + setImmediate(() => { + assert.strictEqual(src._readableState.pipes.length, 0); + }); +} + +{ + const dest = new NullWriteable(); + const src = new NeverEndReadable(); + dest.on('pipe', common.mustCall()); + dest.on('unpipe', common.mustNotCall('unpipe should not have been emitted')); + src.pipe(dest); + setImmediate(() => { + assert.strictEqual(src._readableState.pipes.length, 1); + }); +} + +{ + const dest = new NullWriteable(); + const src = new NeverEndReadable(); + dest.on('pipe', common.mustCall()); + dest.on('unpipe', common.mustCall()); + src.pipe(dest); + src.unpipe(dest); + setImmediate(() => { + assert.strictEqual(src._readableState.pipes.length, 0); + }); +} + +{ + const dest = new NullWriteable(); + const src = new QuickEndReadable(); + dest.on('pipe', common.mustCall()); + dest.on('unpipe', common.mustCall()); + src.pipe(dest, { end: false }); + setImmediate(() => { + assert.strictEqual(src._readableState.pipes.length, 0); + }); +} + +{ + const dest = new NullWriteable(); + const src = new NeverEndReadable(); + dest.on('pipe', common.mustCall()); + dest.on('unpipe', common.mustNotCall('unpipe should not have been emitted')); + src.pipe(dest, { end: false }); + setImmediate(() => { + assert.strictEqual(src._readableState.pipes.length, 1); + }); +} + +{ + const dest = new NullWriteable(); + const src = new NeverEndReadable(); + dest.on('pipe', common.mustCall()); + dest.on('unpipe', common.mustCall()); + src.pipe(dest, { end: false }); + src.unpipe(dest); + setImmediate(() => { + assert.strictEqual(src._readableState.pipes.length, 0); + }); +} diff --git a/test/js/node/test/parallel/test-stream-unshift-empty-chunk.js b/test/js/node/test/parallel/test-stream-unshift-empty-chunk.js new file mode 100644 index 0000000000..e8136a68e9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-unshift-empty-chunk.js @@ -0,0 +1,80 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// This test verifies that stream.unshift(Buffer.alloc(0)) or +// stream.unshift('') does not set state.reading=false. +const Readable = require('stream').Readable; + +const r = new Readable(); +let nChunks = 10; +const chunk = Buffer.alloc(10, 'x'); + +r._read = function(n) { + setImmediate(() => { + r.push(--nChunks === 0 ? null : chunk); + }); +}; + +let readAll = false; +const seen = []; +r.on('readable', () => { + let chunk; + while ((chunk = r.read()) !== null) { + seen.push(chunk.toString()); + // Simulate only reading a certain amount of the data, + // and then putting the rest of the chunk back into the + // stream, like a parser might do. We just fill it with + // 'y' so that it's easy to see which bits were touched, + // and which were not. + const putBack = Buffer.alloc(readAll ? 0 : 5, 'y'); + readAll = !readAll; + r.unshift(putBack); + } +}); + +const expect = + [ 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy', + 'xxxxxxxxxx', + 'yyyyy' ]; + +r.on('end', () => { + assert.deepStrictEqual(seen, expect); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream-unshift-read-race.js b/test/js/node/test/parallel/test-stream-unshift-read-race.js new file mode 100644 index 0000000000..fe110ea285 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-unshift-read-race.js @@ -0,0 +1,128 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// This test verifies that: +// 1. unshift() does not cause colliding _read() calls. +// 2. unshift() after the 'end' event is an error, but after the EOF +// signalling null, it is ok, and just creates a new readable chunk. +// 3. push() after the EOF signaling null is an error. +// 4. _read() is not called after pushing the EOF null chunk. + +const stream = require('stream'); +const hwm = 10; +const r = stream.Readable({ highWaterMark: hwm, autoDestroy: false }); +const chunks = 10; + +const data = Buffer.allocUnsafe(chunks * hwm + Math.ceil(hwm / 2)); +for (let i = 0; i < data.length; i++) { + const c = 'asdf'.charCodeAt(i % 4); + data[i] = c; +} + +let pos = 0; +let pushedNull = false; +r._read = function(n) { + assert(!pushedNull, '_read after null push'); + + // Every third chunk is fast + push(!(chunks % 3)); + + function push(fast) { + assert(!pushedNull, 'push() after null push'); + const c = pos >= data.length ? null : data.slice(pos, pos + n); + pushedNull = c === null; + if (fast) { + pos += n; + r.push(c); + if (c === null) pushError(); + } else { + setTimeout(function() { + pos += n; + r.push(c); + if (c === null) pushError(); + }, 1); + } + } +}; + +function pushError() { + r.unshift(Buffer.allocUnsafe(1)); + w.end(); + + assert.throws(() => { + r.push(Buffer.allocUnsafe(1)); + }, { + code: 'ERR_STREAM_PUSH_AFTER_EOF', + name: 'Error', + message: 'stream.push() after EOF' + }); +} + + +const w = stream.Writable(); +const written = []; +w._write = function(chunk, encoding, cb) { + written.push(chunk.toString()); + cb(); +}; + +r.on('end', common.mustNotCall()); + +r.on('readable', function() { + let chunk; + while (null !== (chunk = r.read(10))) { + w.write(chunk); + if (chunk.length > 4) + r.unshift(Buffer.from('1234')); + } +}); + +w.on('finish', common.mustCall(function() { + // Each chunk should start with 1234, and then be asfdasdfasdf... + // The first got pulled out before the first unshift('1234'), so it's + // lacking that piece. + assert.strictEqual(written[0], 'asdfasdfas'); + let asdf = 'd'; + console.error(`0: ${written[0]}`); + for (let i = 1; i < written.length; i++) { + console.error(`${i.toString(32)}: ${written[i]}`); + assert.strictEqual(written[i].slice(0, 4), '1234'); + for (let j = 4; j < written[i].length; j++) { + const c = written[i].charAt(j); + assert.strictEqual(c, asdf); + switch (asdf) { + case 'a': asdf = 's'; break; + case 's': asdf = 'd'; break; + case 'd': asdf = 'f'; break; + case 'f': asdf = 'a'; break; + } + } + } +})); + +process.on('exit', function() { + assert.strictEqual(written.length, 18); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream-writable-aborted.js b/test/js/node/test/parallel/test-stream-writable-aborted.js new file mode 100644 index 0000000000..01d638115b --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-aborted.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { Writable } = require('stream'); + +{ + const writable = new Writable({ + write() { + } + }); + assert.strictEqual(writable.writableAborted, false); + writable.destroy(); + assert.strictEqual(writable.writableAborted, true); +} + +{ + const writable = new Writable({ + write() { + } + }); + assert.strictEqual(writable.writableAborted, false); + writable.end(); + writable.destroy(); + assert.strictEqual(writable.writableAborted, true); +} diff --git a/test/js/node/test/parallel/test-stream-writable-change-default-encoding.js b/test/js/node/test/parallel/test-stream-writable-change-default-encoding.js new file mode 100644 index 0000000000..9a9482cb62 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-change-default-encoding.js @@ -0,0 +1,78 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const stream = require('stream'); + +class MyWritable extends stream.Writable { + constructor(fn, options) { + super(options); + this.fn = fn; + } + + _write(chunk, encoding, callback) { + this.fn(Buffer.isBuffer(chunk), typeof chunk, encoding); + callback(); + } +} + +(function defaultCondingIsUtf8() { + const m = new MyWritable(function(isBuffer, type, enc) { + assert.strictEqual(enc, 'utf8'); + }, { decodeStrings: false }); + m.write('foo'); + m.end(); +}()); + +(function changeDefaultEncodingToAscii() { + const m = new MyWritable(function(isBuffer, type, enc) { + assert.strictEqual(enc, 'ascii'); + }, { decodeStrings: false }); + m.setDefaultEncoding('ascii'); + m.write('bar'); + m.end(); +}()); + +// Change default encoding to invalid value. +assert.throws(() => { + const m = new MyWritable( + (isBuffer, type, enc) => {}, + { decodeStrings: false }); + m.setDefaultEncoding({}); + m.write('bar'); + m.end(); +}, { + name: 'TypeError', + code: 'ERR_UNKNOWN_ENCODING', + message: 'Unknown encoding: [object Object]' +}); + +(function checkVariableCaseEncoding() { + const m = new MyWritable(function(isBuffer, type, enc) { + assert.strictEqual(enc, 'ascii'); + }, { decodeStrings: false }); + m.setDefaultEncoding('AsCii'); + m.write('bar'); + m.end(); +}()); diff --git a/test/js/node/test/parallel/test-stream-writable-clear-buffer.js b/test/js/node/test/parallel/test-stream-writable-clear-buffer.js new file mode 100644 index 0000000000..c4d7ae151a --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-clear-buffer.js @@ -0,0 +1,35 @@ +'use strict'; + +// This test ensures that the _writeableState.bufferedRequestCount and +// the actual buffered request count are the same. + +const common = require('../common'); +const Stream = require('stream'); +const assert = require('assert'); + +class StreamWritable extends Stream.Writable { + constructor() { + super({ objectMode: true }); + } + + // Refs: https://github.com/nodejs/node/issues/6758 + // We need a timer like on the original issue thread. + // Otherwise the code will never reach our test case. + _write(chunk, encoding, cb) { + setImmediate(cb); + } +} + +const testStream = new StreamWritable(); +testStream.cork(); + +for (let i = 1; i <= 5; i++) { + testStream.write(i, common.mustCall(() => { + assert.strictEqual( + testStream._writableState.bufferedRequestCount, + testStream._writableState.getBuffer().length + ); + })); +} + +testStream.end(); diff --git a/test/js/node/test/parallel/test-stream-writable-constructor-set-methods.js b/test/js/node/test/parallel/test-stream-writable-constructor-set-methods.js new file mode 100644 index 0000000000..34fda8edda --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-constructor-set-methods.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const { Writable } = require('stream'); + +const bufferBlerg = Buffer.from('blerg'); +const w = new Writable(); + +assert.throws( + () => { + w.end(bufferBlerg); + }, + { + name: 'Error', + code: 'ERR_METHOD_NOT_IMPLEMENTED', + message: 'The _write() method is not implemented' + } +); + +const _write = common.mustCall((chunk, _, next) => { + next(); +}); + +const _writev = common.mustCall((chunks, next) => { + assert.strictEqual(chunks.length, 2); + next(); +}); + +const w2 = new Writable({ write: _write, writev: _writev }); + +assert.strictEqual(w2._write, _write); +assert.strictEqual(w2._writev, _writev); + +w2.write(bufferBlerg); + +w2.cork(); +w2.write(bufferBlerg); +w2.write(bufferBlerg); + +w2.end(); diff --git a/test/js/node/test/parallel/test-stream-writable-decoded-encoding.js b/test/js/node/test/parallel/test-stream-writable-decoded-encoding.js new file mode 100644 index 0000000000..e3caa9928f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-decoded-encoding.js @@ -0,0 +1,105 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const stream = require('stream'); + +class MyWritable extends stream.Writable { + constructor(fn, options) { + super(options); + this.fn = fn; + } + + _write(chunk, encoding, callback) { + this.fn(Buffer.isBuffer(chunk), typeof chunk, encoding); + callback(); + } +} + +{ + const m = new MyWritable(function(isBuffer, type, enc) { + assert(isBuffer); + assert.strictEqual(type, 'object'); + assert.strictEqual(enc, 'buffer'); + }, { decodeStrings: true }); + m.write('some-text', 'utf8'); + m.end(); +} + +{ + const m = new MyWritable(function(isBuffer, type, enc) { + assert(!isBuffer); + assert.strictEqual(type, 'string'); + assert.strictEqual(enc, 'utf8'); + }, { decodeStrings: false }); + m.write('some-text', 'utf8'); + m.end(); +} + +{ + assert.throws(() => { + const m = new MyWritable(null, { + defaultEncoding: 'my invalid encoding', + }); + m.end(); + }, { + code: 'ERR_UNKNOWN_ENCODING', + }); +} + +{ + const w = new MyWritable(function(isBuffer, type, enc) { + assert(!isBuffer); + assert.strictEqual(type, 'string'); + assert.strictEqual(enc, 'hex'); + }, { + defaultEncoding: 'hex', + decodeStrings: false + }); + w.write('asd'); + w.end(); +} + +{ + const w = new MyWritable(function(isBuffer, type, enc) { + assert(!isBuffer); + assert.strictEqual(type, 'string'); + assert.strictEqual(enc, 'utf8'); + }, { + defaultEncoding: null, + decodeStrings: false + }); + w.write('asd'); + w.end(); +} + +{ + const m = new MyWritable(function(isBuffer, type, enc) { + assert.strictEqual(type, 'object'); + assert.strictEqual(enc, 'utf8'); + }, { defaultEncoding: 'hex', + objectMode: true }); + m.write({ foo: 'bar' }, 'utf8'); + m.end(); +} diff --git a/test/js/node/test/parallel/test-stream-writable-destroy.js b/test/js/node/test/parallel/test-stream-writable-destroy.js new file mode 100644 index 0000000000..05d7932b88 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-destroy.js @@ -0,0 +1,501 @@ +'use strict'; + +const common = require('../common'); +const { Writable, addAbortSignal } = require('stream'); +const assert = require('assert'); + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write.on('finish', common.mustNotCall()); + write.on('close', common.mustCall()); + + write.destroy(); + assert.strictEqual(write.destroyed, true); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { + this.destroy(new Error('asd')); + cb(); + } + }); + + write.on('error', common.mustCall()); + write.on('finish', common.mustNotCall()); + write.end('asd'); + assert.strictEqual(write.destroyed, true); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + const expected = new Error('kaboom'); + + write.on('finish', common.mustNotCall()); + write.on('close', common.mustCall()); + write.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + write.destroy(expected); + assert.strictEqual(write.destroyed, true); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write._destroy = function(err, cb) { + assert.strictEqual(err, expected); + cb(err); + }; + + const expected = new Error('kaboom'); + + write.on('finish', common.mustNotCall('no finish event')); + write.on('close', common.mustCall()); + write.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + write.destroy(expected); + assert.strictEqual(write.destroyed, true); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); }, + destroy: common.mustCall(function(err, cb) { + assert.strictEqual(err, expected); + cb(); + }) + }); + + const expected = new Error('kaboom'); + + write.on('finish', common.mustNotCall('no finish event')); + write.on('close', common.mustCall()); + + // Error is swallowed by the custom _destroy + write.on('error', common.mustNotCall('no error event')); + + write.destroy(expected); + assert.strictEqual(write.destroyed, true); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(); + }); + + write.destroy(); + assert.strictEqual(write.destroyed, true); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + process.nextTick(() => { + this.end(); + cb(); + }); + }); + + const fail = common.mustNotCall('no finish event'); + + write.on('finish', fail); + write.on('close', common.mustCall()); + + write.destroy(); + + assert.strictEqual(write.destroyed, true); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + const expected = new Error('kaboom'); + + write._destroy = common.mustCall(function(err, cb) { + assert.strictEqual(err, null); + cb(expected); + }); + + write.on('close', common.mustCall()); + write.on('finish', common.mustNotCall('no finish event')); + write.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + + write.destroy(); + assert.strictEqual(write.destroyed, true); +} + +{ + // double error case + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + let ticked = false; + write.on('close', common.mustCall(() => { + assert.strictEqual(ticked, true); + })); + write.on('error', common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(err.message, 'kaboom 1'); + assert.strictEqual(write._writableState.errorEmitted, true); + })); + + const expected = new Error('kaboom 1'); + write.destroy(expected); + write.destroy(new Error('kaboom 2')); + assert.strictEqual(write._writableState.errored, expected); + assert.strictEqual(write._writableState.errorEmitted, false); + assert.strictEqual(write.destroyed, true); + ticked = true; +} + +{ + const writable = new Writable({ + destroy: common.mustCall(function(err, cb) { + process.nextTick(cb, new Error('kaboom 1')); + }), + write(chunk, enc, cb) { + cb(); + } + }); + + let ticked = false; + writable.on('close', common.mustCall(() => { + writable.on('error', common.mustNotCall()); + writable.destroy(new Error('hello')); + assert.strictEqual(ticked, true); + assert.strictEqual(writable._writableState.errorEmitted, true); + })); + writable.on('error', common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(err.message, 'kaboom 1'); + assert.strictEqual(writable._writableState.errorEmitted, true); + })); + + writable.destroy(); + assert.strictEqual(writable.destroyed, true); + assert.strictEqual(writable._writableState.errored, null); + assert.strictEqual(writable._writableState.errorEmitted, false); + + // Test case where `writable.destroy()` is called again with an error before + // the `_destroy()` callback is called. + writable.destroy(new Error('kaboom 2')); + assert.strictEqual(writable._writableState.errorEmitted, false); + assert.strictEqual(writable._writableState.errored, null); + + ticked = true; +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write.destroyed = true; + assert.strictEqual(write.destroyed, true); + + // The internal destroy() mechanism should not be triggered + write.on('close', common.mustNotCall()); + write.destroy(); +} + +{ + function MyWritable() { + assert.strictEqual(this.destroyed, false); + this.destroyed = false; + Writable.call(this); + } + + Object.setPrototypeOf(MyWritable.prototype, Writable.prototype); + Object.setPrototypeOf(MyWritable, Writable); + + new MyWritable(); +} + +{ + // Destroy and destroy callback + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write.destroy(); + + const expected = new Error('kaboom'); + + write.destroy(expected, common.mustCall((err) => { + assert.strictEqual(err, undefined); + })); +} + +{ + // Checks that `._undestroy()` restores the state so that `final` will be + // called again. + const write = new Writable({ + write: common.mustNotCall(), + final: common.mustCall((cb) => cb(), 2), + autoDestroy: true + }); + + write.end(); + write.once('close', common.mustCall(() => { + write._undestroy(); + write.end(); + })); +} + +{ + const write = new Writable(); + + write.destroy(); + write.on('error', common.mustNotCall()); + write.write('asd', common.expectsError({ + name: 'Error', + code: 'ERR_STREAM_DESTROYED', + message: 'Cannot call write after a stream was destroyed' + })); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write.on('error', common.mustNotCall()); + + write.cork(); + write.write('asd', common.mustCall()); + write.uncork(); + + write.cork(); + write.write('asd', common.expectsError({ + name: 'Error', + code: 'ERR_STREAM_DESTROYED', + message: 'Cannot call write after a stream was destroyed' + })); + write.destroy(); + write.write('asd', common.expectsError({ + name: 'Error', + code: 'ERR_STREAM_DESTROYED', + message: 'Cannot call write after a stream was destroyed' + })); + write.uncork(); +} + +{ + // Call end(cb) after error & destroy + + const write = new Writable({ + write(chunk, enc, cb) { cb(new Error('asd')); } + }); + write.on('error', common.mustCall(() => { + write.destroy(); + let ticked = false; + write.end(common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED'); + })); + ticked = true; + })); + write.write('asd'); +} + +{ + // Call end(cb) after finish & destroy + + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + write.on('finish', common.mustCall(() => { + write.destroy(); + let ticked = false; + write.end(common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED'); + })); + ticked = true; + })); + write.end(); +} + +{ + // Call end(cb) after error & destroy and don't trigger + // unhandled exception. + + const write = new Writable({ + write(chunk, enc, cb) { process.nextTick(cb); } + }); + const _err = new Error('asd'); + write.once('error', common.mustCall((err) => { + assert.strictEqual(err.message, 'asd'); + })); + write.end('asd', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + write.destroy(_err); +} + +{ + // Call buffered write callback with error + + const _err = new Error('asd'); + const write = new Writable({ + write(chunk, enc, cb) { + process.nextTick(cb, _err); + }, + autoDestroy: false + }); + write.cork(); + write.write('asd', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + write.write('asd', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + write.on('error', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + write.uncork(); +} + +{ + // Ensure callback order. + + let state = 0; + const write = new Writable({ + write(chunk, enc, cb) { + // `setImmediate()` is used on purpose to ensure the callback is called + // after `process.nextTick()` callbacks. + setImmediate(cb); + } + }); + write.write('asd', common.mustCall(() => { + assert.strictEqual(state++, 0); + })); + write.write('asd', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED'); + assert.strictEqual(state++, 1); + })); + write.destroy(); +} + +{ + const write = new Writable({ + autoDestroy: false, + write(chunk, enc, cb) { + cb(); + cb(); + } + }); + + write.on('error', common.mustCall(() => { + assert(write._writableState.errored); + })); + write.write('asd'); +} + +{ + const ac = new AbortController(); + const write = addAbortSignal(ac.signal, new Writable({ + write(chunk, enc, cb) { cb(); } + })); + + write.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(write.destroyed, true); + })); + write.write('asd'); + ac.abort(); +} + +{ + const ac = new AbortController(); + const write = new Writable({ + signal: ac.signal, + write(chunk, enc, cb) { cb(); } + }); + + write.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(write.destroyed, true); + })); + write.write('asd'); + ac.abort(); +} + +{ + const signal = AbortSignal.abort(); + + const write = new Writable({ + signal, + write(chunk, enc, cb) { cb(); } + }); + + write.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(write.destroyed, true); + })); +} + +{ + // Destroy twice + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write.end(common.mustCall()); + write.destroy(); + write.destroy(); +} + +{ + // https://github.com/nodejs/node/issues/39356 + const s = new Writable({ + final() {} + }); + const _err = new Error('oh no'); + // Remove `callback` and it works + s.end(common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + s.on('error', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + s.destroy(_err); +} + +{ + const write = new Writable({ + write(chunk, enc, cb) { cb(); } + }); + + write.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); + assert.strictEqual(write.destroyed, true); + })); + write[Symbol.asyncDispose]().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-writable-end-cb-error.js b/test/js/node/test/parallel/test-stream-writable-end-cb-error.js new file mode 100644 index 0000000000..f140d939bc --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-end-cb-error.js @@ -0,0 +1,78 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +{ + // Invoke end callback on failure. + const writable = new stream.Writable(); + + const _err = new Error('kaboom'); + writable._write = (chunk, encoding, cb) => { + process.nextTick(cb, _err); + }; + + writable.on('error', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + writable.write('asd'); + writable.end(common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + writable.end(common.mustCall((err) => { + assert.strictEqual(err, _err); + })); +} + +{ + // Don't invoke end callback twice + const writable = new stream.Writable(); + + writable._write = (chunk, encoding, cb) => { + process.nextTick(cb); + }; + + let called = false; + writable.end('asd', common.mustCall((err) => { + called = true; + assert.strictEqual(err, null); + })); + + writable.on('error', common.mustCall((err) => { + assert.strictEqual(err.message, 'kaboom'); + })); + writable.on('finish', common.mustCall(() => { + assert.strictEqual(called, true); + writable.emit('error', new Error('kaboom')); + })); +} + +{ + const w = new stream.Writable({ + write(chunk, encoding, callback) { + setImmediate(callback); + }, + finish(callback) { + setImmediate(callback); + } + }); + w.end('testing ended state', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END'); + })); + assert.strictEqual(w.destroyed, false); + assert.strictEqual(w.writableEnded, true); + w.end(common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END'); + })); + assert.strictEqual(w.destroyed, false); + assert.strictEqual(w.writableEnded, true); + w.end('end', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END'); + })); + assert.strictEqual(w.destroyed, true); + w.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END'); + })); + w.on('finish', common.mustNotCall()); +} diff --git a/test/js/node/test/parallel/test-stream-writable-end-cb-uncaught.js b/test/js/node/test/parallel/test-stream-writable-end-cb-uncaught.js new file mode 100644 index 0000000000..02586b45d9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-end-cb-uncaught.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err.message, 'kaboom'); +})); + +const writable = new stream.Writable(); +const _err = new Error('kaboom'); + +writable._write = (chunk, encoding, cb) => { + cb(); +}; +writable._final = (cb) => { + cb(_err); +}; + +writable.write('asd'); +writable.end(common.mustCall((err) => { + assert.strictEqual(err, _err); +})); diff --git a/test/js/node/test/parallel/test-stream-writable-end-multiple.js b/test/js/node/test/parallel/test-stream-writable-end-multiple.js new file mode 100644 index 0000000000..000f5b07f5 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-end-multiple.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const stream = require('stream'); + +const writable = new stream.Writable(); +writable._write = (chunk, encoding, cb) => { + setTimeout(() => cb(), 10); +}; + +writable.end('testing ended state', common.mustCall()); +writable.end(common.mustCall()); +writable.on('finish', common.mustCall(() => { + let ticked = false; + writable.end(common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED'); + })); + ticked = true; +})); diff --git a/test/js/node/test/parallel/test-stream-writable-ended-state.js b/test/js/node/test/parallel/test-stream-writable-ended-state.js new file mode 100644 index 0000000000..2c40c62a9e --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-ended-state.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const stream = require('stream'); + +const writable = new stream.Writable(); + +writable._write = (chunk, encoding, cb) => { + assert.strictEqual(writable._writableState.ended, false); + assert.strictEqual(writable._writableState.writable, undefined); + assert.strictEqual(writable.writableEnded, false); + cb(); +}; + +assert.strictEqual(writable._writableState.ended, false); +assert.strictEqual(writable._writableState.writable, undefined); +assert.strictEqual(writable.writable, true); +assert.strictEqual(writable.writableEnded, false); + +writable.end('testing ended state', common.mustCall(() => { + assert.strictEqual(writable._writableState.ended, true); + assert.strictEqual(writable._writableState.writable, undefined); + assert.strictEqual(writable.writable, false); + assert.strictEqual(writable.writableEnded, true); +})); + +assert.strictEqual(writable._writableState.ended, true); +assert.strictEqual(writable._writableState.writable, undefined); +assert.strictEqual(writable.writable, false); +assert.strictEqual(writable.writableEnded, true); diff --git a/test/js/node/test/parallel/test-stream-writable-final-async.js b/test/js/node/test/parallel/test-stream-writable-final-async.js new file mode 100644 index 0000000000..c17b843322 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-final-async.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +const { + Duplex, +} = require('stream'); +const { setTimeout } = require('timers/promises'); + +{ + class Foo extends Duplex { + async _final(callback) { + await setTimeout(common.platformTimeout(1)); + callback(); + } + + _read() {} + } + + const foo = new Foo(); + foo._write = common.mustCall((chunk, encoding, cb) => { + cb(); + }); + foo.end('test', common.mustCall()); + foo.on('error', common.mustNotCall()); +} diff --git a/test/js/node/test/parallel/test-stream-writable-final-destroy.js b/test/js/node/test/parallel/test-stream-writable-final-destroy.js new file mode 100644 index 0000000000..8d3bf72c89 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-final-destroy.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); + +const { Writable } = require('stream'); + +{ + const w = new Writable({ + write(chunk, encoding, callback) { + callback(null); + }, + final(callback) { + queueMicrotask(callback); + } + }); + w.end(); + w.destroy(); + + w.on('prefinish', common.mustNotCall()); + w.on('finish', common.mustNotCall()); + w.on('close', common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-writable-final-throw.js b/test/js/node/test/parallel/test-stream-writable-final-throw.js new file mode 100644 index 0000000000..e7dd21abc3 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-final-throw.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const { + Duplex, +} = require('stream'); + +{ + class Foo extends Duplex { + _final(callback) { + throw new Error('fhqwhgads'); + } + + _read() {} + } + + const foo = new Foo(); + foo._write = common.mustCall((chunk, encoding, cb) => { + cb(); + }); + foo.end('test', common.expectsError({ message: 'fhqwhgads' })); + foo.on('error', common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream-writable-finish-destroyed.js b/test/js/node/test/parallel/test-stream-writable-finish-destroyed.js new file mode 100644 index 0000000000..22657a170f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-finish-destroyed.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +const { Writable } = require('stream'); + +{ + const w = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + w.on('close', common.mustCall(() => { + cb(); + })); + }) + }); + + w.on('finish', common.mustNotCall()); + w.end('asd'); + w.destroy(); +} + +{ + const w = new Writable({ + write: common.mustCall((chunk, encoding, cb) => { + w.on('close', common.mustCall(() => { + cb(); + w.end(); + })); + }) + }); + + w.on('finish', common.mustNotCall()); + w.write('asd'); + w.destroy(); +} + +{ + const w = new Writable({ + write() { + } + }); + w.on('finish', common.mustNotCall()); + w.end(); + w.destroy(); +} diff --git a/test/js/node/test/parallel/test-stream-writable-finished-state.js b/test/js/node/test/parallel/test-stream-writable-finished-state.js new file mode 100644 index 0000000000..b42137ed0b --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-finished-state.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const stream = require('stream'); + +const writable = new stream.Writable(); + +writable._write = (chunk, encoding, cb) => { + // The state finished should start in false. + assert.strictEqual(writable._writableState.finished, false); + cb(); +}; + +writable.on('finish', common.mustCall(() => { + assert.strictEqual(writable._writableState.finished, true); +})); + +writable.end('testing finished state', common.mustCall(() => { + assert.strictEqual(writable._writableState.finished, true); +})); diff --git a/test/js/node/test/parallel/test-stream-writable-finished.js b/test/js/node/test/parallel/test-stream-writable-finished.js new file mode 100644 index 0000000000..933a80a2f9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-finished.js @@ -0,0 +1,99 @@ +'use strict'; + +const common = require('../common'); +const { Writable } = require('stream'); +const assert = require('assert'); + +// basic +{ + // Find it on Writable.prototype + assert(Object.hasOwn(Writable.prototype, 'writableFinished')); +} + +// event +{ + const writable = new Writable(); + + writable._write = (chunk, encoding, cb) => { + // The state finished should start in false. + assert.strictEqual(writable.writableFinished, false); + cb(); + }; + + writable.on('finish', common.mustCall(() => { + assert.strictEqual(writable.writableFinished, true); + })); + + writable.end('testing finished state', common.mustCall(() => { + assert.strictEqual(writable.writableFinished, true); + })); +} + +{ + // Emit finish asynchronously. + + const w = new Writable({ + write(chunk, encoding, cb) { + cb(); + } + }); + + w.end(); + w.on('finish', common.mustCall()); +} + +{ + // Emit prefinish synchronously. + + const w = new Writable({ + write(chunk, encoding, cb) { + cb(); + } + }); + + let sync = true; + w.on('prefinish', common.mustCall(() => { + assert.strictEqual(sync, true); + })); + w.end(); + sync = false; +} + +{ + // Emit prefinish synchronously w/ final. + + const w = new Writable({ + write(chunk, encoding, cb) { + cb(); + }, + final(cb) { + cb(); + } + }); + + let sync = true; + w.on('prefinish', common.mustCall(() => { + assert.strictEqual(sync, true); + })); + w.end(); + sync = false; +} + + +{ + // Call _final synchronously. + + let sync = true; + const w = new Writable({ + write(chunk, encoding, cb) { + cb(); + }, + final: common.mustCall((cb) => { + assert.strictEqual(sync, true); + cb(); + }) + }); + + w.end(); + sync = false; +} diff --git a/test/js/node/test/parallel/test-stream-writable-invalid-chunk.js b/test/js/node/test/parallel/test-stream-writable-invalid-chunk.js new file mode 100644 index 0000000000..09032c07c5 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-invalid-chunk.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +function testWriteType(val, objectMode, code) { + const writable = new stream.Writable({ + objectMode, + write: () => {} + }); + writable.on('error', common.mustNotCall()); + if (code) { + assert.throws(() => { + writable.write(val); + }, { code }); + } else { + writable.write(val); + } +} + +testWriteType([], false, 'ERR_INVALID_ARG_TYPE'); +testWriteType({}, false, 'ERR_INVALID_ARG_TYPE'); +testWriteType(0, false, 'ERR_INVALID_ARG_TYPE'); +testWriteType(true, false, 'ERR_INVALID_ARG_TYPE'); +testWriteType(0.0, false, 'ERR_INVALID_ARG_TYPE'); +testWriteType(undefined, false, 'ERR_INVALID_ARG_TYPE'); +testWriteType(null, false, 'ERR_STREAM_NULL_VALUES'); + +testWriteType([], true); +testWriteType({}, true); +testWriteType(0, true); +testWriteType(true, true); +testWriteType(0.0, true); +testWriteType(undefined, true); +testWriteType(null, true, 'ERR_STREAM_NULL_VALUES'); diff --git a/test/js/node/test/parallel/test-stream-writable-needdrain-state.js b/test/js/node/test/parallel/test-stream-writable-needdrain-state.js new file mode 100644 index 0000000000..0e72d832bc --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-needdrain-state.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +const stream = require('stream'); +const assert = require('assert'); + +const transform = new stream.Transform({ + transform: _transform, + highWaterMark: 1 +}); + +function _transform(chunk, encoding, cb) { + process.nextTick(() => { + assert.strictEqual(transform._writableState.needDrain, true); + cb(); + }); +} + +assert.strictEqual(transform._writableState.needDrain, false); + +transform.write('asdasd', common.mustCall(() => { + assert.strictEqual(transform._writableState.needDrain, false); +})); + +assert.strictEqual(transform._writableState.needDrain, true); diff --git a/test/js/node/test/parallel/test-stream-writable-null.js b/test/js/node/test/parallel/test-stream-writable-null.js new file mode 100644 index 0000000000..99419f1cf9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-null.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const stream = require('stream'); + +class MyWritable extends stream.Writable { + constructor(options) { + super({ autoDestroy: false, ...options }); + } + _write(chunk, encoding, callback) { + assert.notStrictEqual(chunk, null); + callback(); + } +} + +{ + const m = new MyWritable({ objectMode: true }); + m.on('error', common.mustNotCall()); + assert.throws(() => { + m.write(null); + }, { + code: 'ERR_STREAM_NULL_VALUES' + }); +} + +{ + const m = new MyWritable(); + m.on('error', common.mustNotCall()); + assert.throws(() => { + m.write(false); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +{ // Should not throw. + const m = new MyWritable({ objectMode: true }); + m.write(false, assert.ifError); +} + +{ // Should not throw. + const m = new MyWritable({ objectMode: true }).on('error', (e) => { + assert.ifError(e || new Error('should not get here')); + }); + m.write(false, assert.ifError); +} diff --git a/test/js/node/test/parallel/test-stream-writable-properties.js b/test/js/node/test/parallel/test-stream-writable-properties.js new file mode 100644 index 0000000000..424bb58710 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-properties.js @@ -0,0 +1,22 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const { Writable } = require('stream'); + +{ + const w = new Writable(); + assert.strictEqual(w.writableCorked, 0); + w.uncork(); + assert.strictEqual(w.writableCorked, 0); + w.cork(); + assert.strictEqual(w.writableCorked, 1); + w.cork(); + assert.strictEqual(w.writableCorked, 2); + w.uncork(); + assert.strictEqual(w.writableCorked, 1); + w.uncork(); + assert.strictEqual(w.writableCorked, 0); + w.uncork(); + assert.strictEqual(w.writableCorked, 0); +} diff --git a/test/js/node/test/parallel/test-stream-writable-writable.js b/test/js/node/test/parallel/test-stream-writable-writable.js new file mode 100644 index 0000000000..ef5454dc52 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-writable.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Writable } = require('stream'); + +{ + const w = new Writable({ + write() {} + }); + assert.strictEqual(w.writable, true); + w.destroy(); + assert.strictEqual(w.writable, false); +} + +{ + const w = new Writable({ + write: common.mustCall((chunk, encoding, callback) => { + callback(new Error()); + }) + }); + assert.strictEqual(w.writable, true); + w.write('asd'); + assert.strictEqual(w.writable, false); + w.on('error', common.mustCall()); +} + +{ + const w = new Writable({ + write: common.mustCall((chunk, encoding, callback) => { + process.nextTick(() => { + callback(new Error()); + assert.strictEqual(w.writable, false); + }); + }) + }); + w.write('asd'); + w.on('error', common.mustCall()); +} + +{ + const w = new Writable({ + write: common.mustNotCall() + }); + assert.strictEqual(w.writable, true); + w.end(); + assert.strictEqual(w.writable, false); +} diff --git a/test/js/node/test/parallel/test-stream-writable-write-cb-error.js b/test/js/node/test/parallel/test-stream-writable-write-cb-error.js new file mode 100644 index 0000000000..72db1b7e3f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-write-cb-error.js @@ -0,0 +1,58 @@ +'use strict'; +const common = require('../common'); +const { Writable } = require('stream'); +const assert = require('assert'); + +// Ensure callback is always invoked before +// error is emitted. Regardless if error was +// sync or async. + +{ + let callbackCalled = false; + // Sync Error + const writable = new Writable({ + write: common.mustCall((buf, enc, cb) => { + cb(new Error()); + }) + }); + writable.on('error', common.mustCall(() => { + assert.strictEqual(callbackCalled, true); + })); + writable.write('hi', common.mustCall(() => { + callbackCalled = true; + })); +} + +{ + let callbackCalled = false; + // Async Error + const writable = new Writable({ + write: common.mustCall((buf, enc, cb) => { + process.nextTick(cb, new Error()); + }) + }); + writable.on('error', common.mustCall(() => { + assert.strictEqual(callbackCalled, true); + })); + writable.write('hi', common.mustCall(() => { + callbackCalled = true; + })); +} + +{ + // Sync Error + const writable = new Writable({ + write: common.mustCall((buf, enc, cb) => { + cb(new Error()); + }) + }); + + writable.on('error', common.mustCall()); + + let cnt = 0; + // Ensure we don't live lock on sync error + while (writable.write('a')) + cnt++; + + assert.strictEqual(cnt, 0); +} diff --git a/test/js/node/test/parallel/test-stream-writable-write-cb-twice.js b/test/js/node/test/parallel/test-stream-writable-write-cb-twice.js new file mode 100644 index 0000000000..244698c522 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-write-cb-twice.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +const { Writable } = require('stream'); + +{ + // Sync + Sync + const writable = new Writable({ + write: common.mustCall((buf, enc, cb) => { + cb(); + cb(); + }) + }); + writable.write('hi'); + writable.on('error', common.expectsError({ + code: 'ERR_MULTIPLE_CALLBACK', + name: 'Error' + })); +} + +{ + // Sync + Async + const writable = new Writable({ + write: common.mustCall((buf, enc, cb) => { + cb(); + process.nextTick(() => { + cb(); + }); + }) + }); + writable.write('hi'); + writable.on('error', common.expectsError({ + code: 'ERR_MULTIPLE_CALLBACK', + name: 'Error' + })); +} + +{ + // Async + Async + const writable = new Writable({ + write: common.mustCall((buf, enc, cb) => { + process.nextTick(cb); + process.nextTick(() => { + cb(); + }); + }) + }); + writable.write('hi'); + writable.on('error', common.expectsError({ + code: 'ERR_MULTIPLE_CALLBACK', + name: 'Error' + })); +} diff --git a/test/js/node/test/parallel/test-stream-writable-write-error.js b/test/js/node/test/parallel/test-stream-writable-write-error.js new file mode 100644 index 0000000000..069e32e1be --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-write-error.js @@ -0,0 +1,75 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Writable } = require('stream'); + +function expectError(w, args, code, sync) { + if (sync) { + if (code) { + assert.throws(() => w.write(...args), { code }); + } else { + w.write(...args); + } + } else { + let errorCalled = false; + let ticked = false; + w.write(...args, common.mustCall((err) => { + assert.strictEqual(ticked, true); + assert.strictEqual(errorCalled, false); + assert.strictEqual(err.code, code); + })); + ticked = true; + w.on('error', common.mustCall((err) => { + errorCalled = true; + assert.strictEqual(err.code, code); + })); + } +} + +function test(autoDestroy) { + { + const w = new Writable({ + autoDestroy, + _write() {} + }); + w.end(); + expectError(w, ['asd'], 'ERR_STREAM_WRITE_AFTER_END'); + } + + { + const w = new Writable({ + autoDestroy, + _write() {} + }); + w.destroy(); + } + + { + const w = new Writable({ + autoDestroy, + _write() {} + }); + expectError(w, [null], 'ERR_STREAM_NULL_VALUES', true); + } + + { + const w = new Writable({ + autoDestroy, + _write() {} + }); + expectError(w, [{}], 'ERR_INVALID_ARG_TYPE', true); + } + + { + const w = new Writable({ + decodeStrings: false, + autoDestroy, + _write() {} + }); + expectError(w, ['asd', 'noencoding'], 'ERR_UNKNOWN_ENCODING', true); + } +} + +test(false); +test(true); diff --git a/test/js/node/test/parallel/test-stream-writable-write-writev-finish.js b/test/js/node/test/parallel/test-stream-writable-write-writev-finish.js new file mode 100644 index 0000000000..9fce315f8b --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writable-write-writev-finish.js @@ -0,0 +1,152 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +// Ensure consistency between the finish event when using cork() +// and writev and when not using them + +{ + const writable = new stream.Writable(); + + writable._write = (chunks, encoding, cb) => { + cb(new Error('write test error')); + }; + + writable.on('finish', common.mustNotCall()); + writable.on('prefinish', common.mustNotCall()); + writable.on('error', common.mustCall((er) => { + assert.strictEqual(er.message, 'write test error'); + })); + + writable.end('test'); +} + +{ + const writable = new stream.Writable(); + + writable._write = (chunks, encoding, cb) => { + setImmediate(cb, new Error('write test error')); + }; + + writable.on('finish', common.mustNotCall()); + writable.on('prefinish', common.mustNotCall()); + writable.on('error', common.mustCall((er) => { + assert.strictEqual(er.message, 'write test error'); + })); + + writable.end('test'); +} + +{ + const writable = new stream.Writable(); + + writable._write = (chunks, encoding, cb) => { + cb(new Error('write test error')); + }; + + writable._writev = (chunks, cb) => { + cb(new Error('writev test error')); + }; + + writable.on('finish', common.mustNotCall()); + writable.on('prefinish', common.mustNotCall()); + writable.on('error', common.mustCall((er) => { + assert.strictEqual(er.message, 'writev test error'); + })); + + writable.cork(); + writable.write('test'); + + setImmediate(function() { + writable.end('test'); + }); +} + +{ + const writable = new stream.Writable(); + + writable._write = (chunks, encoding, cb) => { + setImmediate(cb, new Error('write test error')); + }; + + writable._writev = (chunks, cb) => { + setImmediate(cb, new Error('writev test error')); + }; + + writable.on('finish', common.mustNotCall()); + writable.on('prefinish', common.mustNotCall()); + writable.on('error', common.mustCall((er) => { + assert.strictEqual(er.message, 'writev test error'); + })); + + writable.cork(); + writable.write('test'); + + setImmediate(function() { + writable.end('test'); + }); +} + +// Regression test for +// https://github.com/nodejs/node/issues/13812 + +{ + const rs = new stream.Readable(); + rs.push('ok'); + rs.push(null); + rs._read = () => {}; + + const ws = new stream.Writable(); + + ws.on('finish', common.mustNotCall()); + ws.on('error', common.mustCall()); + + ws._write = (chunk, encoding, done) => { + setImmediate(done, new Error()); + }; + rs.pipe(ws); +} + +{ + const rs = new stream.Readable(); + rs.push('ok'); + rs.push(null); + rs._read = () => {}; + + const ws = new stream.Writable(); + + ws.on('finish', common.mustNotCall()); + ws.on('error', common.mustCall()); + + ws._write = (chunk, encoding, done) => { + done(new Error()); + }; + rs.pipe(ws); +} + +{ + const w = new stream.Writable(); + w._write = (chunk, encoding, cb) => { + process.nextTick(cb); + }; + w.on('error', common.mustCall()); + w.on('finish', common.mustNotCall()); + w.on('prefinish', () => { + w.write("shouldn't write in prefinish listener"); + }); + w.end(); +} + +{ + const w = new stream.Writable(); + w._write = (chunk, encoding, cb) => { + process.nextTick(cb); + }; + w.on('error', common.mustCall()); + w.on('finish', () => { + w.write("shouldn't write in finish listener"); + }); + w.end(); +} diff --git a/test/js/node/test/parallel/test-stream-writableState-ending.js b/test/js/node/test/parallel/test-stream-writableState-ending.js new file mode 100644 index 0000000000..d301d355cc --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writableState-ending.js @@ -0,0 +1,37 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const stream = require('stream'); + +const writable = new stream.Writable(); + +function testStates(ending, finished, ended) { + assert.strictEqual(writable._writableState.ending, ending); + assert.strictEqual(writable._writableState.finished, finished); + assert.strictEqual(writable._writableState.ended, ended); +} + +writable._write = (chunk, encoding, cb) => { + // Ending, finished, ended start in false. + testStates(false, false, false); + cb(); +}; + +writable.on('finish', () => { + // Ending, finished, ended = true. + testStates(true, true, true); +}); + +const result = writable.end('testing function end()', () => { + // Ending, finished, ended = true. + testStates(true, true, true); +}); + +// End returns the writable instance +assert.strictEqual(result, writable); + +// Ending, ended = true. +// finished = false. +testStates(true, false, true); diff --git a/test/js/node/test/parallel/test-stream-writableState-uncorked-bufferedRequestCount.js b/test/js/node/test/parallel/test-stream-writableState-uncorked-bufferedRequestCount.js new file mode 100644 index 0000000000..b7375b9fa2 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writableState-uncorked-bufferedRequestCount.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +const writable = new stream.Writable(); + +writable._writev = common.mustCall((chunks, cb) => { + assert.strictEqual(chunks.length, 2); + cb(); +}, 1); + +writable._write = common.mustCall((chunk, encoding, cb) => { + cb(); +}, 1); + +// first cork +writable.cork(); +assert.strictEqual(writable._writableState.corked, 1); +assert.strictEqual(writable._writableState.bufferedRequestCount, 0); + +// cork again +writable.cork(); +assert.strictEqual(writable._writableState.corked, 2); + +// The first chunk is buffered +writable.write('first chunk'); +assert.strictEqual(writable._writableState.bufferedRequestCount, 1); + +// First uncork does nothing +writable.uncork(); +assert.strictEqual(writable._writableState.corked, 1); +assert.strictEqual(writable._writableState.bufferedRequestCount, 1); + +process.nextTick(uncork); + +// The second chunk is buffered, because we uncork at the end of tick +writable.write('second chunk'); +assert.strictEqual(writable._writableState.corked, 1); +assert.strictEqual(writable._writableState.bufferedRequestCount, 2); + +function uncork() { + // Second uncork flushes the buffer + writable.uncork(); + assert.strictEqual(writable._writableState.corked, 0); + assert.strictEqual(writable._writableState.bufferedRequestCount, 0); + + // Verify that end() uncorks correctly + writable.cork(); + writable.write('third chunk'); + writable.end(); + + // End causes an uncork() as well + assert.strictEqual(writable._writableState.corked, 0); + assert.strictEqual(writable._writableState.bufferedRequestCount, 0); +} diff --git a/test/js/node/test/parallel/test-stream-write-destroy.js b/test/js/node/test/parallel/test-stream-write-destroy.js new file mode 100644 index 0000000000..d436b98f84 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-write-destroy.js @@ -0,0 +1,62 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { Writable } = require('stream'); + +// Test interaction between calling .destroy() on a writable and pending +// writes. + +for (const withPendingData of [ false, true ]) { + for (const useEnd of [ false, true ]) { + const callbacks = []; + + const w = new Writable({ + write(data, enc, cb) { + callbacks.push(cb); + }, + // Effectively disable the HWM to observe 'drain' events more easily. + highWaterMark: 1 + }); + + let chunksWritten = 0; + let drains = 0; + w.on('drain', () => drains++); + + function onWrite(err) { + if (err) { + assert.strictEqual(w.destroyed, true); + assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED'); + } else { + chunksWritten++; + } + } + + w.write('abc', onWrite); + assert.strictEqual(chunksWritten, 0); + assert.strictEqual(drains, 0); + callbacks.shift()(); + assert.strictEqual(chunksWritten, 1); + assert.strictEqual(drains, 1); + + if (withPendingData) { + // Test 2 cases: There either is or is not data still in the write queue. + // (The second write will never actually get executed either way.) + w.write('def', onWrite); + } + if (useEnd) { + // Again, test 2 cases: Either we indicate that we want to end the + // writable or not. + w.end('ghi', onWrite); + } else { + w.write('ghi', onWrite); + } + + assert.strictEqual(chunksWritten, 1); + w.destroy(); + assert.strictEqual(chunksWritten, 1); + callbacks.shift()(); + assert.strictEqual(chunksWritten, useEnd && !withPendingData ? 1 : 2); + assert.strictEqual(callbacks.length, 0); + assert.strictEqual(drains, 1); + } +} diff --git a/test/js/node/test/parallel/test-stream-write-drain.js b/test/js/node/test/parallel/test-stream-write-drain.js new file mode 100644 index 0000000000..bd65c1fdbb --- /dev/null +++ b/test/js/node/test/parallel/test-stream-write-drain.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const { Writable } = require('stream'); + +// Don't emit 'drain' if ended + +const w = new Writable({ + write(data, enc, cb) { + process.nextTick(cb); + }, + highWaterMark: 1 +}); + +w.on('drain', common.mustNotCall()); +w.write('asd'); +w.end(); diff --git a/test/js/node/test/parallel/test-stream-write-final.js b/test/js/node/test/parallel/test-stream-write-final.js new file mode 100644 index 0000000000..56537bd7fa --- /dev/null +++ b/test/js/node/test/parallel/test-stream-write-final.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const stream = require('stream'); +let shutdown = false; + +const w = new stream.Writable({ + final: common.mustCall(function(cb) { + assert.strictEqual(this, w); + setTimeout(function() { + shutdown = true; + cb(); + }, 100); + }), + write: function(chunk, e, cb) { + process.nextTick(cb); + } +}); +w.on('finish', common.mustCall(function() { + assert(shutdown); +})); +w.write(Buffer.allocUnsafe(1)); +w.end(Buffer.allocUnsafe(0)); diff --git a/test/js/node/test/parallel/test-stream-writev.js b/test/js/node/test/parallel/test-stream-writev.js new file mode 100644 index 0000000000..5a42411c6f --- /dev/null +++ b/test/js/node/test/parallel/test-stream-writev.js @@ -0,0 +1,130 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const stream = require('stream'); + +const queue = []; +for (let decode = 0; decode < 2; decode++) { + for (let uncork = 0; uncork < 2; uncork++) { + for (let multi = 0; multi < 2; multi++) { + queue.push([!!decode, !!uncork, !!multi]); + } + } +} + +run(); + +function run() { + const t = queue.pop(); + if (t) + test(t[0], t[1], t[2], run); + else + console.log('ok'); +} + +function test(decode, uncork, multi, next) { + console.log(`# decode=${decode} uncork=${uncork} multi=${multi}`); + let counter = 0; + let expectCount = 0; + function cnt(msg) { + expectCount++; + const expect = expectCount; + return function(er) { + assert.ifError(er); + counter++; + assert.strictEqual(counter, expect); + }; + } + + const w = new stream.Writable({ decodeStrings: decode }); + w._write = common.mustNotCall('Should not call _write'); + + const expectChunks = decode ? [ + { encoding: 'buffer', + chunk: [104, 101, 108, 108, 111, 44, 32] }, + { encoding: 'buffer', + chunk: [119, 111, 114, 108, 100] }, + { encoding: 'buffer', + chunk: [33] }, + { encoding: 'buffer', + chunk: [10, 97, 110, 100, 32, 116, 104, 101, 110, 46, 46, 46] }, + { encoding: 'buffer', + chunk: [250, 206, 190, 167, 222, 173, 190, 239, 222, 202, 251, 173] }, + ] : [ + { encoding: 'ascii', chunk: 'hello, ' }, + { encoding: 'utf8', chunk: 'world' }, + { encoding: 'buffer', chunk: [33] }, + { encoding: 'latin1', chunk: '\nand then...' }, + { encoding: 'hex', chunk: 'facebea7deadbeefdecafbad' }, + ]; + + let actualChunks; + w._writev = function(chunks, cb) { + actualChunks = chunks.map(function(chunk) { + return { + encoding: chunk.encoding, + chunk: Buffer.isBuffer(chunk.chunk) ? + Array.prototype.slice.call(chunk.chunk) : chunk.chunk + }; + }); + cb(); + }; + + w.cork(); + w.write('hello, ', 'ascii', cnt('hello')); + w.write('world', 'utf8', cnt('world')); + + if (multi) + w.cork(); + + w.write(Buffer.from('!'), 'buffer', cnt('!')); + w.write('\nand then...', 'latin1', cnt('and then')); + + if (multi) + w.uncork(); + + w.write('facebea7deadbeefdecafbad', 'hex', cnt('hex')); + + if (uncork) + w.uncork(); + + w.end(cnt('end')); + + w.on('finish', function() { + // Make sure finish comes after all the write cb + cnt('finish')(); + assert.deepStrictEqual(actualChunks, expectChunks); + next(); + }); +} + +{ + const w = new stream.Writable({ + writev: common.mustCall(function(chunks, cb) { + cb(); + }) + }); + w.write('asd', common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-stream2-base64-single-char-read-end.js b/test/js/node/test/parallel/test-stream2-base64-single-char-read-end.js new file mode 100644 index 0000000000..2e1eb15f9f --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-base64-single-char-read-end.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const { Readable: R, Writable: W } = require('stream'); +const assert = require('assert'); + +const src = new R({ encoding: 'base64' }); +const dst = new W(); +let hasRead = false; +const accum = []; + +src._read = function(n) { + if (!hasRead) { + hasRead = true; + process.nextTick(function() { + src.push(Buffer.from('1')); + src.push(null); + }); + } +}; + +dst._write = function(chunk, enc, cb) { + accum.push(chunk); + cb(); +}; + +src.on('end', function() { + assert.strictEqual(String(Buffer.concat(accum)), 'MQ=='); + clearTimeout(timeout); +}); + +src.pipe(dst); + +const timeout = setTimeout(function() { + assert.fail('timed out waiting for _write'); +}, 100); diff --git a/test/js/node/test/parallel/test-stream2-basic.js b/test/js/node/test/parallel/test-stream2-basic.js new file mode 100644 index 0000000000..2670deda53 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-basic.js @@ -0,0 +1,445 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +const { Readable: R, Writable: W } = require('stream'); +const assert = require('assert'); + +const EE = require('events').EventEmitter; + +class TestReader extends R { + constructor(n) { + super(); + this._buffer = Buffer.alloc(n || 100, 'x'); + this._pos = 0; + this._bufs = 10; + } + + _read(n) { + const max = this._buffer.length - this._pos; + n = Math.max(n, 0); + const toRead = Math.min(n, max); + if (toRead === 0) { + // Simulate the read buffer filling up with some more bytes some time + // in the future. + setTimeout(() => { + this._pos = 0; + this._bufs -= 1; + if (this._bufs <= 0) { + // read them all! + if (!this.ended) + this.push(null); + } else { + // now we have more. + // kinda cheating by calling _read, but whatever, + // it's just fake anyway. + this._read(n); + } + }, 10); + return; + } + + const ret = this._buffer.slice(this._pos, this._pos + toRead); + this._pos += toRead; + this.push(ret); + } +} + +class TestWriter extends EE { + constructor() { + super(); + this.received = []; + this.flush = false; + } + + write(c) { + this.received.push(c.toString()); + this.emit('write', c); + return true; + } + + end(c) { + if (c) this.write(c); + this.emit('end', this.received); + } +} + +{ + // Test basic functionality + const r = new TestReader(20); + + const reads = []; + const expect = [ 'x', + 'xx', + 'xxx', + 'xxxx', + 'xxxxx', + 'xxxxxxxxx', + 'xxxxxxxxxx', + 'xxxxxxxxxxxx', + 'xxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxxxxxxxx' ]; + + r.on('end', common.mustCall(function() { + assert.deepStrictEqual(reads, expect); + })); + + let readSize = 1; + function flow() { + let res; + while (null !== (res = r.read(readSize++))) { + reads.push(res.toString()); + } + r.once('readable', flow); + } + + flow(); +} + +{ + // Verify pipe + const r = new TestReader(5); + + const expect = [ 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx' ]; + + const w = new TestWriter(); + + w.on('end', common.mustCall(function(received) { + assert.deepStrictEqual(received, expect); + })); + + r.pipe(w); +} + + +[1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(function(SPLIT) { + // Verify unpipe + const r = new TestReader(5); + + // Unpipe after 3 writes, then write to another stream instead. + let expect = [ 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx' ]; + expect = [ expect.slice(0, SPLIT), expect.slice(SPLIT) ]; + + const w = [ new TestWriter(), new TestWriter() ]; + + let writes = SPLIT; + w[0].on('write', function() { + if (--writes === 0) { + r.unpipe(); + assert.deepStrictEqual(r._readableState.pipes, []); + w[0].end(); + r.pipe(w[1]); + assert.deepStrictEqual(r._readableState.pipes, [w[1]]); + } + }); + + let ended = 0; + + w[0].on('end', common.mustCall(function(results) { + ended++; + assert.strictEqual(ended, 1); + assert.deepStrictEqual(results, expect[0]); + })); + + w[1].on('end', common.mustCall(function(results) { + ended++; + assert.strictEqual(ended, 2); + assert.deepStrictEqual(results, expect[1]); + })); + + r.pipe(w[0]); +}); + + +{ + // Verify both writers get the same data when piping to destinations + const r = new TestReader(5); + const w = [ new TestWriter(), new TestWriter() ]; + + const expect = [ 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx' ]; + + w[0].on('end', common.mustCall(function(received) { + assert.deepStrictEqual(received, expect); + })); + w[1].on('end', common.mustCall(function(received) { + assert.deepStrictEqual(received, expect); + })); + + r.pipe(w[0]); + r.pipe(w[1]); +} + + +[1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(function(SPLIT) { + // Verify multi-unpipe + const r = new TestReader(5); + + // Unpipe after 3 writes, then write to another stream instead. + let expect = [ 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx', + 'xxxxx' ]; + expect = [ expect.slice(0, SPLIT), expect.slice(SPLIT) ]; + + const w = [ new TestWriter(), new TestWriter(), new TestWriter() ]; + + let writes = SPLIT; + w[0].on('write', function() { + if (--writes === 0) { + r.unpipe(); + w[0].end(); + r.pipe(w[1]); + } + }); + + let ended = 0; + + w[0].on('end', common.mustCall(function(results) { + ended++; + assert.strictEqual(ended, 1); + assert.deepStrictEqual(results, expect[0]); + })); + + w[1].on('end', common.mustCall(function(results) { + ended++; + assert.strictEqual(ended, 2); + assert.deepStrictEqual(results, expect[1]); + })); + + r.pipe(w[0]); + r.pipe(w[2]); +}); + +{ + // Verify that back pressure is respected + const r = new R({ objectMode: true }); + r._read = common.mustNotCall(); + let counter = 0; + r.push(['one']); + r.push(['two']); + r.push(['three']); + r.push(['four']); + r.push(null); + + const w1 = new R(); + w1.write = function(chunk) { + assert.strictEqual(chunk[0], 'one'); + w1.emit('close'); + process.nextTick(function() { + r.pipe(w2); + r.pipe(w3); + }); + }; + w1.end = common.mustNotCall(); + + r.pipe(w1); + + const expected = ['two', 'two', 'three', 'three', 'four', 'four']; + + const w2 = new R(); + w2.write = function(chunk) { + assert.strictEqual(chunk[0], expected.shift()); + assert.strictEqual(counter, 0); + + counter++; + + if (chunk[0] === 'four') { + return true; + } + + setTimeout(function() { + counter--; + w2.emit('drain'); + }, 10); + + return false; + }; + w2.end = common.mustCall(); + + const w3 = new R(); + w3.write = function(chunk) { + assert.strictEqual(chunk[0], expected.shift()); + assert.strictEqual(counter, 1); + + counter++; + + if (chunk[0] === 'four') { + return true; + } + + setTimeout(function() { + counter--; + w3.emit('drain'); + }, 50); + + return false; + }; + w3.end = common.mustCall(function() { + assert.strictEqual(counter, 2); + assert.strictEqual(expected.length, 0); + }); +} + +{ + // Verify read(0) behavior for ended streams + const r = new R(); + let written = false; + let ended = false; + r._read = common.mustNotCall(); + + r.push(Buffer.from('foo')); + r.push(null); + + const v = r.read(0); + + assert.strictEqual(v, null); + + const w = new R(); + w.write = function(buffer) { + written = true; + assert.strictEqual(ended, false); + assert.strictEqual(buffer.toString(), 'foo'); + }; + + w.end = common.mustCall(function() { + ended = true; + assert.strictEqual(written, true); + }); + + r.pipe(w); +} + +{ + // Verify synchronous _read ending + const r = new R(); + let called = false; + r._read = function(n) { + r.push(null); + }; + + r.once('end', function() { + // Verify that this is called before the next tick + called = true; + }); + + r.read(); + + process.nextTick(function() { + assert.strictEqual(called, true); + }); +} + +{ + // Verify that adding readable listeners trigger data flow + const r = new R({ highWaterMark: 5 }); + let onReadable = false; + let readCalled = 0; + + r._read = function(n) { + if (readCalled++ === 2) + r.push(null); + else + r.push(Buffer.from('asdf')); + }; + + r.on('readable', function() { + onReadable = true; + r.read(); + }); + + r.on('end', common.mustCall(function() { + assert.strictEqual(readCalled, 3); + assert.ok(onReadable); + })); +} + +{ + // Verify that streams are chainable + const r = new R(); + r._read = common.mustCall(); + const r2 = r.setEncoding('utf8').pause().resume().pause(); + assert.strictEqual(r, r2); +} + +{ + // Verify readableEncoding property + assert(Object.hasOwn(R.prototype, 'readableEncoding')); + + const r = new R({ encoding: 'utf8' }); + assert.strictEqual(r.readableEncoding, 'utf8'); +} + +{ + // Verify readableObjectMode property + assert(Object.hasOwn(R.prototype, 'readableObjectMode')); + + const r = new R({ objectMode: true }); + assert.strictEqual(r.readableObjectMode, true); +} + +{ + // Verify writableObjectMode property + assert(Object.hasOwn(W.prototype, 'writableObjectMode')); + + const w = new W({ objectMode: true }); + assert.strictEqual(w.writableObjectMode, true); +} diff --git a/test/js/node/test/parallel/test-stream2-compatibility.js b/test/js/node/test/parallel/test-stream2-compatibility.js new file mode 100644 index 0000000000..d760db8b32 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-compatibility.js @@ -0,0 +1,70 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const { Readable: R, Writable: W } = require('stream'); +const assert = require('assert'); + +let ondataCalled = 0; + +class TestReader extends R { + constructor() { + super(); + this._buffer = Buffer.alloc(100, 'x'); + + this.on('data', () => { + ondataCalled++; + }); + } + + _read(n) { + this.push(this._buffer); + this._buffer = Buffer.alloc(0); + } +} + +const reader = new TestReader(); +setImmediate(function() { + assert.strictEqual(ondataCalled, 1); + console.log('ok'); + reader.push(null); +}); + +class TestWriter extends W { + constructor() { + super(); + this.write('foo'); + this.end(); + } + + _write(chunk, enc, cb) { + cb(); + } +} + +const writer = new TestWriter(); + +process.on('exit', function() { + assert.strictEqual(reader.readable, false); + assert.strictEqual(writer.writable, false); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream2-decode-partial.js b/test/js/node/test/parallel/test-stream2-decode-partial.js new file mode 100644 index 0000000000..9d9ae21bfe --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-decode-partial.js @@ -0,0 +1,23 @@ +'use strict'; +require('../common'); +const { Readable } = require('stream'); +const assert = require('assert'); + +let buf = ''; +const euro = Buffer.from([0xE2, 0x82, 0xAC]); +const cent = Buffer.from([0xC2, 0xA2]); +const source = Buffer.concat([euro, cent]); + +const readable = Readable({ encoding: 'utf8' }); +readable.push(source.slice(0, 2)); +readable.push(source.slice(2, 4)); +readable.push(source.slice(4, 6)); +readable.push(null); + +readable.on('data', function(data) { + buf += data; +}); + +process.on('exit', function() { + assert.strictEqual(buf, '€¢'); +}); diff --git a/test/js/node/test/parallel/test-stream2-finish-pipe-error.js b/test/js/node/test/parallel/test-stream2-finish-pipe-error.js new file mode 100644 index 0000000000..a603e154b9 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-finish-pipe-error.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const stream = require('stream'); + +process.on('uncaughtException', common.mustCall()); + +const r = new stream.Readable(); +r._read = function(size) { + r.push(Buffer.allocUnsafe(size)); +}; + +const w = new stream.Writable(); +w._write = function(data, encoding, cb) { + cb(null); +}; + +r.pipe(w); + +// end() after pipe should cause unhandled exception +w.end(); diff --git a/test/js/node/test/parallel/test-stream2-finish-pipe.js b/test/js/node/test/parallel/test-stream2-finish-pipe.js new file mode 100644 index 0000000000..5e2969aad4 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-finish-pipe.js @@ -0,0 +1,44 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const stream = require('stream'); + +const r = new stream.Readable(); +r._read = function(size) { + r.push(Buffer.allocUnsafe(size)); +}; + +const w = new stream.Writable(); +w._write = function(data, encoding, cb) { + process.nextTick(cb, null); +}; + +r.pipe(w); + +// end() must be called in nextTick or a WRITE_AFTER_END error occurs. +process.nextTick(() => { + // This might sound unrealistic, but it happens in net.js. When + // socket.allowHalfOpen === false, EOF will cause .destroySoon() call which + // ends the writable side of net.Socket. + w.end(); +}); diff --git a/test/js/node/test/parallel/test-stream2-large-read-stall.js b/test/js/node/test/parallel/test-stream2-large-read-stall.js new file mode 100644 index 0000000000..5f5618ce73 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-large-read-stall.js @@ -0,0 +1,74 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// If everything aligns so that you do a read(n) of exactly the +// remaining buffer, then make sure that 'end' still emits. + +const READSIZE = 100; +const PUSHSIZE = 20; +const PUSHCOUNT = 1000; +const HWM = 50; + +const Readable = require('stream').Readable; +const r = new Readable({ + highWaterMark: HWM +}); +const rs = r._readableState; + +r._read = push; + +r.on('readable', function() { + console.error('>> readable'); + let ret; + do { + console.error(` > read(${READSIZE})`); + ret = r.read(READSIZE); + console.error(` < ${ret?.length} (${rs.length} remain)`); + } while (ret && ret.length === READSIZE); + + console.error('<< after read()', + ret?.length, + rs.needReadable, + rs.length); +}); + +r.on('end', common.mustCall(function() { + assert.strictEqual(pushes, PUSHCOUNT + 1); +})); + +let pushes = 0; +function push() { + if (pushes > PUSHCOUNT) + return; + + if (pushes++ === PUSHCOUNT) { + console.error(' push(EOF)'); + return r.push(null); + } + + console.error(` push #${pushes}`); + if (r.push(Buffer.allocUnsafe(PUSHSIZE))) + setTimeout(push, 1); +} diff --git a/test/js/node/test/parallel/test-stream2-objects.js b/test/js/node/test/parallel/test-stream2-objects.js new file mode 100644 index 0000000000..b7ad074628 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-objects.js @@ -0,0 +1,297 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +const { Readable, Writable } = require('stream'); +const assert = require('assert'); + +function toArray(callback) { + const stream = new Writable({ objectMode: true }); + const list = []; + stream.write = function(chunk) { + list.push(chunk); + }; + + stream.end = common.mustCall(function() { + callback(list); + }); + + return stream; +} + +function fromArray(list) { + const r = new Readable({ objectMode: true }); + r._read = common.mustNotCall(); + list.forEach(function(chunk) { + r.push(chunk); + }); + r.push(null); + + return r; +} + +{ + // Verify that objects can be read from the stream + const r = fromArray([{ one: '1' }, { two: '2' }]); + + const v1 = r.read(); + const v2 = r.read(); + const v3 = r.read(); + + assert.deepStrictEqual(v1, { one: '1' }); + assert.deepStrictEqual(v2, { two: '2' }); + assert.strictEqual(v3, null); +} + +{ + // Verify that objects can be piped into the stream + const r = fromArray([{ one: '1' }, { two: '2' }]); + + r.pipe(toArray(common.mustCall(function(list) { + assert.deepStrictEqual(list, [ + { one: '1' }, + { two: '2' }, + ]); + }))); +} + +{ + // Verify that read(n) is ignored + const r = fromArray([{ one: '1' }, { two: '2' }]); + const value = r.read(2); + + assert.deepStrictEqual(value, { one: '1' }); +} + +{ + // Verify that objects can be synchronously read + const r = new Readable({ objectMode: true }); + const list = [{ one: '1' }, { two: '2' }]; + r._read = function(n) { + const item = list.shift(); + r.push(item || null); + }; + + r.pipe(toArray(common.mustCall(function(list) { + assert.deepStrictEqual(list, [ + { one: '1' }, + { two: '2' }, + ]); + }))); +} + +{ + // Verify that objects can be asynchronously read + const r = new Readable({ objectMode: true }); + const list = [{ one: '1' }, { two: '2' }]; + r._read = function(n) { + const item = list.shift(); + process.nextTick(function() { + r.push(item || null); + }); + }; + + r.pipe(toArray(common.mustCall(function(list) { + assert.deepStrictEqual(list, [ + { one: '1' }, + { two: '2' }, + ]); + }))); +} + +{ + // Verify that strings can be read as objects + const r = new Readable({ + objectMode: true + }); + r._read = common.mustNotCall(); + const list = ['one', 'two', 'three']; + list.forEach(function(str) { + r.push(str); + }); + r.push(null); + + r.pipe(toArray(common.mustCall(function(array) { + assert.deepStrictEqual(array, list); + }))); +} + +{ + // Verify read(0) behavior for object streams + const r = new Readable({ + objectMode: true + }); + r._read = common.mustNotCall(); + + r.push('foobar'); + r.push(null); + + r.pipe(toArray(common.mustCall(function(array) { + assert.deepStrictEqual(array, ['foobar']); + }))); +} + +{ + // Verify the behavior of pushing falsey values + const r = new Readable({ + objectMode: true + }); + r._read = common.mustNotCall(); + + r.push(false); + r.push(0); + r.push(''); + r.push(null); + + r.pipe(toArray(common.mustCall(function(array) { + assert.deepStrictEqual(array, [false, 0, '']); + }))); +} + +{ + // Verify high watermark _read() behavior + const r = new Readable({ + highWaterMark: 6, + objectMode: true + }); + let calls = 0; + const list = ['1', '2', '3', '4', '5', '6', '7', '8']; + + r._read = function(n) { + calls++; + }; + + list.forEach(function(c) { + r.push(c); + }); + + const v = r.read(); + + assert.strictEqual(calls, 0); + assert.strictEqual(v, '1'); + + const v2 = r.read(); + assert.strictEqual(v2, '2'); + + const v3 = r.read(); + assert.strictEqual(v3, '3'); + + assert.strictEqual(calls, 1); +} + +{ + // Verify high watermark push behavior + const r = new Readable({ + highWaterMark: 6, + objectMode: true + }); + r._read = common.mustNotCall(); + for (let i = 0; i < 6; i++) { + const bool = r.push(i); + assert.strictEqual(bool, i !== 5); + } +} + +{ + // Verify that objects can be written to stream + const w = new Writable({ objectMode: true }); + + w._write = function(chunk, encoding, cb) { + assert.deepStrictEqual(chunk, { foo: 'bar' }); + cb(); + }; + + w.on('finish', common.mustCall()); + w.write({ foo: 'bar' }); + w.end(); +} + +{ + // Verify that multiple objects can be written to stream + const w = new Writable({ objectMode: true }); + const list = []; + + w._write = function(chunk, encoding, cb) { + list.push(chunk); + cb(); + }; + + w.on('finish', common.mustCall(function() { + assert.deepStrictEqual(list, [0, 1, 2, 3, 4]); + })); + + w.write(0); + w.write(1); + w.write(2); + w.write(3); + w.write(4); + w.end(); +} + +{ + // Verify that strings can be written as objects + const w = new Writable({ + objectMode: true + }); + const list = []; + + w._write = function(chunk, encoding, cb) { + list.push(chunk); + process.nextTick(cb); + }; + + w.on('finish', common.mustCall(function() { + assert.deepStrictEqual(list, ['0', '1', '2', '3', '4']); + })); + + w.write('0'); + w.write('1'); + w.write('2'); + w.write('3'); + w.write('4'); + w.end(); +} + +{ + // Verify that stream buffers finish until callback is called + const w = new Writable({ + objectMode: true + }); + let called = false; + + w._write = function(chunk, encoding, cb) { + assert.strictEqual(chunk, 'foo'); + + process.nextTick(function() { + called = true; + cb(); + }); + }; + + w.on('finish', common.mustCall(function() { + assert.strictEqual(called, true); + })); + + w.write('foo'); + w.end(); +} diff --git a/test/js/node/test/parallel/test-stream2-pipe-error-handling.js b/test/js/node/test/parallel/test-stream2-pipe-error-handling.js new file mode 100644 index 0000000000..d3f4838105 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-pipe-error-handling.js @@ -0,0 +1,106 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +{ + let count = 1000; + + const source = new stream.Readable(); + source._read = function(n) { + n = Math.min(count, n); + count -= n; + source.push(Buffer.allocUnsafe(n)); + }; + + let unpipedDest; + source.unpipe = function(dest) { + unpipedDest = dest; + stream.Readable.prototype.unpipe.call(this, dest); + }; + + const dest = new stream.Writable(); + dest._write = function(chunk, encoding, cb) { + cb(); + }; + + source.pipe(dest); + + let gotErr = null; + dest.on('error', function(err) { + gotErr = err; + }); + + let unpipedSource; + dest.on('unpipe', function(src) { + unpipedSource = src; + }); + + const err = new Error('This stream turned into bacon.'); + dest.emit('error', err); + assert.strictEqual(gotErr, err); + assert.strictEqual(unpipedSource, source); + assert.strictEqual(unpipedDest, dest); +} + +{ + let count = 1000; + + const source = new stream.Readable(); + source._read = function(n) { + n = Math.min(count, n); + count -= n; + source.push(Buffer.allocUnsafe(n)); + }; + + let unpipedDest; + source.unpipe = function(dest) { + unpipedDest = dest; + stream.Readable.prototype.unpipe.call(this, dest); + }; + + const dest = new stream.Writable({ autoDestroy: false }); + dest._write = function(chunk, encoding, cb) { + cb(); + }; + + source.pipe(dest); + + let unpipedSource; + dest.on('unpipe', function(src) { + unpipedSource = src; + }); + + const err = new Error('This stream turned into bacon.'); + + let gotErr = null; + try { + dest.emit('error', err); + } catch (e) { + gotErr = e; + } + assert.strictEqual(gotErr, err); + assert.strictEqual(unpipedSource, source); + assert.strictEqual(unpipedDest, dest); +} diff --git a/test/js/node/test/parallel/test-stream2-pipe-error-once-listener.js b/test/js/node/test/parallel/test-stream2-pipe-error-once-listener.js new file mode 100644 index 0000000000..003e78e64f --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-pipe-error-once-listener.js @@ -0,0 +1,53 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +require('../common'); +const stream = require('stream'); + +class Read extends stream.Readable { + _read(size) { + this.push('x'); + this.push(null); + } +} + +class Write extends stream.Writable { + _write(buffer, encoding, cb) { + this.emit('error', new Error('boom')); + this.emit('alldone'); + } +} + +const read = new Read(); +const write = new Write(); + +write.once('error', () => {}); +write.once('alldone', function(err) { + console.log('ok'); +}); + +process.on('exit', function(c) { + console.error('error thrown even with listener'); +}); + +read.pipe(write); diff --git a/test/js/node/test/parallel/test-stream2-push.js b/test/js/node/test/parallel/test-stream2-push.js new file mode 100644 index 0000000000..748a77b9c4 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-push.js @@ -0,0 +1,136 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const { Readable, Writable } = require('stream'); + +const EE = require('events').EventEmitter; + + +// A mock thing a bit like the net.Socket/tcp_wrap.handle interaction + +const stream = new Readable({ + highWaterMark: 16, + encoding: 'utf8' +}); + +const source = new EE(); + +stream._read = function() { + console.error('stream._read'); + readStart(); +}; + +let ended = false; +stream.on('end', function() { + ended = true; +}); + +source.on('data', function(chunk) { + const ret = stream.push(chunk); + console.error('data', stream.readableLength); + if (!ret) + readStop(); +}); + +source.on('end', function() { + stream.push(null); +}); + +let reading = false; + +function readStart() { + console.error('readStart'); + reading = true; +} + +function readStop() { + console.error('readStop'); + reading = false; + process.nextTick(function() { + const r = stream.read(); + if (r !== null) + writer.write(r); + }); +} + +const writer = new Writable({ + decodeStrings: false +}); + +const written = []; + +const expectWritten = + [ 'asdfgasdfgasdfgasdfg', + 'asdfgasdfgasdfgasdfg', + 'asdfgasdfgasdfgasdfg', + 'asdfgasdfgasdfgasdfg', + 'asdfgasdfgasdfgasdfg', + 'asdfgasdfgasdfgasdfg' ]; + +writer._write = function(chunk, encoding, cb) { + console.error(`WRITE ${chunk}`); + written.push(chunk); + process.nextTick(cb); +}; + +writer.on('finish', finish); + + +// Now emit some chunks. + +const chunk = 'asdfg'; + +let set = 0; +readStart(); +data(); +function data() { + assert(reading); + source.emit('data', chunk); + assert(reading); + source.emit('data', chunk); + assert(reading); + source.emit('data', chunk); + assert(reading); + source.emit('data', chunk); + assert(!reading); + if (set++ < 5) + setTimeout(data, 10); + else + end(); +} + +function finish() { + console.error('finish'); + assert.deepStrictEqual(written, expectWritten); + console.log('ok'); +} + +function end() { + source.emit('end'); + assert(!reading); + writer.end(stream.read()); + setImmediate(function() { + assert(ended); + }); +} diff --git a/test/js/node/test/parallel/test-stream2-read-sync-stack.js b/test/js/node/test/parallel/test-stream2-read-sync-stack.js new file mode 100644 index 0000000000..e6a5ea7e52 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-read-sync-stack.js @@ -0,0 +1,46 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const Readable = require('stream').Readable; + +// This tests synchronous read callbacks and verifies that even if they nest +// heavily the process handles it without an error + +const r = new Readable(); +const N = 256 * 1024; + +let reads = 0; +r._read = function(n) { + const chunk = reads++ === N ? null : Buffer.allocUnsafe(1); + r.push(chunk); +}; + +r.on('readable', function onReadable() { + if (!(r.readableLength % 256)) + console.error('readable', r.readableLength); + r.read(N * 2); +}); + +r.on('end', common.mustCall()); + +r.read(0); diff --git a/test/js/node/test/parallel/test-stream2-readable-empty-buffer-no-eof.js b/test/js/node/test/parallel/test-stream2-readable-empty-buffer-no-eof.js new file mode 100644 index 0000000000..7be2c358ee --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-readable-empty-buffer-no-eof.js @@ -0,0 +1,117 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const Readable = require('stream').Readable; + +test1(); +test2(); + +function test1() { + const r = new Readable(); + + // Should not end when we get a Buffer.alloc(0) or '' as the _read + // result that just means that there is *temporarily* no data, but to + // go ahead and try again later. + // + // note that this is very unusual. it only works for crypto streams + // because the other side of the stream will call read(0) to cycle + // data through openssl. that's why setImmediate() is used to call + // r.read(0) again later, otherwise there is no more work being done + // and the process just exits. + + const buf = Buffer.alloc(5, 'x'); + let reads = 5; + r._read = function(n) { + switch (reads--) { + case 5: + return setImmediate(() => { + return r.push(buf); + }); + case 4: + setImmediate(() => { + return r.push(Buffer.alloc(0)); + }); + return setImmediate(r.read.bind(r, 0)); + case 3: + setImmediate(r.read.bind(r, 0)); + return process.nextTick(() => { + return r.push(Buffer.alloc(0)); + }); + case 2: + setImmediate(r.read.bind(r, 0)); + return r.push(Buffer.alloc(0)); // Not-EOF! + case 1: + return r.push(buf); + case 0: + return r.push(null); // EOF + default: + throw new Error('unreachable'); + } + }; + + const results = []; + function flow() { + let chunk; + while (null !== (chunk = r.read())) + results.push(String(chunk)); + } + r.on('readable', flow); + r.on('end', () => { + results.push('EOF'); + }); + flow(); + + process.on('exit', () => { + assert.deepStrictEqual(results, [ 'xxxxx', 'xxxxx', 'EOF' ]); + console.log('ok'); + }); +} + +function test2() { + const r = new Readable({ encoding: 'base64' }); + let reads = 5; + r._read = function(n) { + if (!reads--) + return r.push(null); // EOF + return r.push(Buffer.from('x')); + }; + + const results = []; + function flow() { + let chunk; + while (null !== (chunk = r.read())) + results.push(String(chunk)); + } + r.on('readable', flow); + r.on('end', () => { + results.push('EOF'); + }); + flow(); + + process.on('exit', () => { + assert.deepStrictEqual(results, [ 'eHh4', 'eHg=', 'EOF' ]); + console.log('ok'); + }); +} diff --git a/test/js/node/test/parallel/test-stream2-readable-legacy-drain.js b/test/js/node/test/parallel/test-stream2-readable-legacy-drain.js new file mode 100644 index 0000000000..beb3657776 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-readable-legacy-drain.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const Stream = require('stream'); +const Readable = Stream.Readable; + +const r = new Readable(); +const N = 256; +let reads = 0; +r._read = function(n) { + return r.push(++reads === N ? null : Buffer.allocUnsafe(1)); +}; + +r.on('end', common.mustCall()); + +const w = new Stream(); +w.writable = true; +let buffered = 0; +w.write = function(c) { + buffered += c.length; + process.nextTick(drain); + return false; +}; + +function drain() { + assert(buffered <= 3); + buffered = 0; + w.emit('drain'); +} + +w.end = common.mustCall(); + +r.pipe(w); diff --git a/test/js/node/test/parallel/test-stream2-readable-non-empty-end.js b/test/js/node/test/parallel/test-stream2-readable-non-empty-end.js new file mode 100644 index 0000000000..417f2c3b0e --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-readable-non-empty-end.js @@ -0,0 +1,72 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +let len = 0; +const chunks = new Array(10); +for (let i = 1; i <= 10; i++) { + chunks[i - 1] = Buffer.allocUnsafe(i); + len += i; +} + +const test = new Readable(); +let n = 0; +test._read = function(size) { + const chunk = chunks[n++]; + setTimeout(function() { + test.push(chunk === undefined ? null : chunk); + }, 1); +}; + +test.on('end', thrower); +function thrower() { + throw new Error('this should not happen!'); +} + +let bytesread = 0; +test.on('readable', function() { + const b = len - bytesread - 1; + const res = test.read(b); + if (res) { + bytesread += res.length; + console.error(`br=${bytesread} len=${len}`); + setTimeout(next, 1); + } + test.read(0); +}); +test.read(0); + +function next() { + // Now let's make 'end' happen + test.removeListener('end', thrower); + test.on('end', common.mustCall()); + + // One to get the last byte + let r = test.read(); + assert(r); + assert.strictEqual(r.length, 1); + r = test.read(); + assert.strictEqual(r, null); +} diff --git a/test/js/node/test/parallel/test-stream2-readable-wrap-destroy.js b/test/js/node/test/parallel/test-stream2-readable-wrap-destroy.js new file mode 100644 index 0000000000..e310ae09e6 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-readable-wrap-destroy.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); + +const { Readable } = require('stream'); +const EE = require('events').EventEmitter; + +const oldStream = new EE(); +oldStream.pause = () => {}; +oldStream.resume = () => {}; + +{ + new Readable({ + autoDestroy: false, + destroy: common.mustCall() + }) + .wrap(oldStream); + oldStream.emit('destroy'); +} + +{ + new Readable({ + autoDestroy: false, + destroy: common.mustCall() + }) + .wrap(oldStream); + oldStream.emit('close'); +} diff --git a/test/js/node/test/parallel/test-stream2-readable-wrap-empty.js b/test/js/node/test/parallel/test-stream2-readable-wrap-empty.js new file mode 100644 index 0000000000..3dbbdaa9b5 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-readable-wrap-empty.js @@ -0,0 +1,38 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const { Readable } = require('stream'); +const EE = require('events').EventEmitter; + +const oldStream = new EE(); +oldStream.pause = () => {}; +oldStream.resume = () => {}; + +const newStream = new Readable().wrap(oldStream); + +newStream + .on('readable', () => {}) + .on('end', common.mustCall()); + +oldStream.emit('end'); diff --git a/test/js/node/test/parallel/test-stream2-readable-wrap-error.js b/test/js/node/test/parallel/test-stream2-readable-wrap-error.js new file mode 100644 index 0000000000..2d2c26e2ca --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-readable-wrap-error.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Readable } = require('stream'); +const EE = require('events').EventEmitter; + +class LegacyStream extends EE { + pause() {} + resume() {} +} + +{ + const err = new Error(); + const oldStream = new LegacyStream(); + const r = new Readable({ autoDestroy: true }) + .wrap(oldStream) + .on('error', common.mustCall(() => { + assert.strictEqual(r._readableState.errorEmitted, true); + assert.strictEqual(r._readableState.errored, err); + assert.strictEqual(r.destroyed, true); + })); + oldStream.emit('error', err); +} + +{ + const err = new Error(); + const oldStream = new LegacyStream(); + const r = new Readable({ autoDestroy: false }) + .wrap(oldStream) + .on('error', common.mustCall(() => { + assert.strictEqual(r._readableState.errorEmitted, true); + assert.strictEqual(r._readableState.errored, err); + assert.strictEqual(r.destroyed, false); + })); + oldStream.emit('error', err); +} diff --git a/test/js/node/test/parallel/test-stream2-readable-wrap.js b/test/js/node/test/parallel/test-stream2-readable-wrap.js new file mode 100644 index 0000000000..eebe72bc0d --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-readable-wrap.js @@ -0,0 +1,100 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable, Writable } = require('stream'); +const EE = require('events').EventEmitter; + +function runTest(highWaterMark, objectMode, produce) { + + const old = new EE(); + const r = new Readable({ highWaterMark, objectMode }); + assert.strictEqual(r, r.wrap(old)); + + r.on('end', common.mustCall()); + + old.pause = function() { + old.emit('pause'); + flowing = false; + }; + + old.resume = function() { + old.emit('resume'); + flow(); + }; + + // Make sure pause is only emitted once. + let pausing = false; + r.on('pause', () => { + assert.strictEqual(pausing, false); + pausing = true; + process.nextTick(() => { + pausing = false; + }); + }); + + let flowing; + let chunks = 10; + let oldEnded = false; + const expected = []; + function flow() { + flowing = true; + while (flowing && chunks-- > 0) { + const item = produce(); + expected.push(item); + old.emit('data', item); + } + if (chunks <= 0) { + oldEnded = true; + old.emit('end'); + } + } + + const w = new Writable({ highWaterMark: highWaterMark * 2, + objectMode }); + const written = []; + w._write = function(chunk, encoding, cb) { + written.push(chunk); + setTimeout(cb, 1); + }; + + w.on('finish', common.mustCall(function() { + performAsserts(); + })); + + r.pipe(w); + + flow(); + + function performAsserts() { + assert(oldEnded); + assert.deepStrictEqual(written, expected); + } +} + +runTest(100, false, function() { return Buffer.allocUnsafe(100); }); +runTest(10, false, function() { return Buffer.from('xxxxxxxxxx'); }); +runTest(1, true, function() { return { foo: 'bar' }; }); + +const objectChunks = [ 5, 'a', false, 0, '', 'xyz', { x: 4 }, 7, [], 555 ]; +runTest(1, true, function() { return objectChunks.shift(); }); diff --git a/test/js/node/test/parallel/test-stream2-set-encoding.js b/test/js/node/test/parallel/test-stream2-set-encoding.js new file mode 100644 index 0000000000..2d35b161bf --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-set-encoding.js @@ -0,0 +1,323 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable: R } = require('stream'); + +class TestReader extends R { + constructor(n, opts) { + super(opts); + this.pos = 0; + this.len = n || 100; + } + + _read(n) { + setTimeout(() => { + if (this.pos >= this.len) { + // Double push(null) to test eos handling + this.push(null); + return this.push(null); + } + + n = Math.min(n, this.len - this.pos); + if (n <= 0) { + // Double push(null) to test eos handling + this.push(null); + return this.push(null); + } + + this.pos += n; + const ret = Buffer.alloc(n, 'a'); + + return this.push(ret); + }, 1); + } +} + +{ + // Verify utf8 encoding + const tr = new TestReader(100); + tr.setEncoding('utf8'); + const out = []; + const expect = + [ 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(10))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + + +{ + // Verify hex encoding + const tr = new TestReader(100); + tr.setEncoding('hex'); + const out = []; + const expect = + [ '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(10))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + +{ + // Verify hex encoding with read(13) + const tr = new TestReader(100); + tr.setEncoding('hex'); + const out = []; + const expect = + [ '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '16161' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(13))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + +{ + // Verify base64 encoding + const tr = new TestReader(100); + tr.setEncoding('base64'); + const out = []; + const expect = + [ 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYQ==' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(10))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + +{ + // Verify utf8 encoding + const tr = new TestReader(100, { encoding: 'utf8' }); + const out = []; + const expect = + [ 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(10))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + + +{ + // Verify hex encoding + const tr = new TestReader(100, { encoding: 'hex' }); + const out = []; + const expect = + [ '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161', + '6161616161' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(10))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + +{ + // Verify hex encoding with read(13) + const tr = new TestReader(100, { encoding: 'hex' }); + const out = []; + const expect = + [ '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '1616161616161', + '6161616161616', + '16161' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(13))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + +{ + // Verify base64 encoding + const tr = new TestReader(100, { encoding: 'base64' }); + const out = []; + const expect = + [ 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYWFhYWFh', + 'YWFhYWFhYW', + 'FhYQ==' ]; + + tr.on('readable', function flow() { + let chunk; + while (null !== (chunk = tr.read(10))) + out.push(chunk); + }); + + tr.on('end', common.mustCall(function() { + assert.deepStrictEqual(out, expect); + })); +} + +{ + // Verify chaining behavior + const tr = new TestReader(100); + assert.deepStrictEqual(tr.setEncoding('utf8'), tr); +} diff --git a/test/js/node/test/parallel/test-stream2-transform.js b/test/js/node/test/parallel/test-stream2-transform.js new file mode 100644 index 0000000000..f222f1c03b --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-transform.js @@ -0,0 +1,492 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { PassThrough, Transform } = require('stream'); + +{ + // Verify writable side consumption + const tx = new Transform({ + highWaterMark: 10 + }); + + let transformed = 0; + tx._transform = function(chunk, encoding, cb) { + transformed += chunk.length; + tx.push(chunk); + cb(); + }; + + for (let i = 1; i <= 10; i++) { + tx.write(Buffer.allocUnsafe(i)); + } + tx.end(); + + assert.strictEqual(tx.readableLength, 10); + assert.strictEqual(transformed, 10); + assert.deepStrictEqual(tx.writableBuffer.map(function(c) { + return c.chunk.length; + }), [5, 6, 7, 8, 9, 10]); +} + +{ + // Verify passthrough behavior + const pt = new PassThrough(); + + pt.write(Buffer.from('foog')); + pt.write(Buffer.from('bark')); + pt.write(Buffer.from('bazy')); + pt.write(Buffer.from('kuel')); + pt.end(); + + assert.strictEqual(pt.read(5).toString(), 'foogb'); + assert.strictEqual(pt.read(5).toString(), 'arkba'); + assert.strictEqual(pt.read(5).toString(), 'zykue'); + assert.strictEqual(pt.read(5).toString(), 'l'); +} + +{ + // Verify object passthrough behavior + const pt = new PassThrough({ objectMode: true }); + + pt.write(1); + pt.write(true); + pt.write(false); + pt.write(0); + pt.write('foo'); + pt.write(''); + pt.write({ a: 'b' }); + pt.end(); + + assert.strictEqual(pt.read(), 1); + assert.strictEqual(pt.read(), true); + assert.strictEqual(pt.read(), false); + assert.strictEqual(pt.read(), 0); + assert.strictEqual(pt.read(), 'foo'); + assert.strictEqual(pt.read(), ''); + assert.deepStrictEqual(pt.read(), { a: 'b' }); +} + +{ + // Verify passthrough constructor behavior + const pt = PassThrough(); + + assert(pt instanceof PassThrough); +} + +{ + // Verify transform constructor behavior + const pt = Transform(); + + assert(pt instanceof Transform); +} + +{ + // Perform a simple transform + const pt = new Transform(); + pt._transform = function(c, e, cb) { + const ret = Buffer.alloc(c.length, 'x'); + pt.push(ret); + cb(); + }; + + pt.write(Buffer.from('foog')); + pt.write(Buffer.from('bark')); + pt.write(Buffer.from('bazy')); + pt.write(Buffer.from('kuel')); + pt.end(); + + assert.strictEqual(pt.read(5).toString(), 'xxxxx'); + assert.strictEqual(pt.read(5).toString(), 'xxxxx'); + assert.strictEqual(pt.read(5).toString(), 'xxxxx'); + assert.strictEqual(pt.read(5).toString(), 'x'); +} + +{ + // Verify simple object transform + const pt = new Transform({ objectMode: true }); + pt._transform = function(c, e, cb) { + pt.push(JSON.stringify(c)); + cb(); + }; + + pt.write(1); + pt.write(true); + pt.write(false); + pt.write(0); + pt.write('foo'); + pt.write(''); + pt.write({ a: 'b' }); + pt.end(); + + assert.strictEqual(pt.read(), '1'); + assert.strictEqual(pt.read(), 'true'); + assert.strictEqual(pt.read(), 'false'); + assert.strictEqual(pt.read(), '0'); + assert.strictEqual(pt.read(), '"foo"'); + assert.strictEqual(pt.read(), '""'); + assert.strictEqual(pt.read(), '{"a":"b"}'); +} + +{ + // Verify async passthrough + const pt = new Transform(); + pt._transform = function(chunk, encoding, cb) { + setTimeout(function() { + pt.push(chunk); + cb(); + }, 10); + }; + + pt.write(Buffer.from('foog')); + pt.write(Buffer.from('bark')); + pt.write(Buffer.from('bazy')); + pt.write(Buffer.from('kuel')); + pt.end(); + + pt.on('finish', common.mustCall(function() { + assert.strictEqual(pt.read(5).toString(), 'foogb'); + assert.strictEqual(pt.read(5).toString(), 'arkba'); + assert.strictEqual(pt.read(5).toString(), 'zykue'); + assert.strictEqual(pt.read(5).toString(), 'l'); + })); +} + +{ + // Verify asymmetric transform (expand) + const pt = new Transform(); + + // Emit each chunk 2 times. + pt._transform = function(chunk, encoding, cb) { + setTimeout(function() { + pt.push(chunk); + setTimeout(function() { + pt.push(chunk); + cb(); + }, 10); + }, 10); + }; + + pt.write(Buffer.from('foog')); + pt.write(Buffer.from('bark')); + pt.write(Buffer.from('bazy')); + pt.write(Buffer.from('kuel')); + pt.end(); + + pt.on('finish', common.mustCall(function() { + assert.strictEqual(pt.read(5).toString(), 'foogf'); + assert.strictEqual(pt.read(5).toString(), 'oogba'); + assert.strictEqual(pt.read(5).toString(), 'rkbar'); + assert.strictEqual(pt.read(5).toString(), 'kbazy'); + assert.strictEqual(pt.read(5).toString(), 'bazyk'); + assert.strictEqual(pt.read(5).toString(), 'uelku'); + assert.strictEqual(pt.read(5).toString(), 'el'); + })); +} + +{ + // Verify asymmetric transform (compress) + const pt = new Transform(); + + // Each output is the first char of 3 consecutive chunks, + // or whatever's left. + pt.state = ''; + + pt._transform = function(chunk, encoding, cb) { + const s = (chunk ||= '').toString(); + setTimeout(() => { + this.state += s.charAt(0); + if (this.state.length === 3) { + pt.push(Buffer.from(this.state)); + this.state = ''; + } + cb(); + }, 10); + }; + + pt._flush = function(cb) { + // Just output whatever we have. + pt.push(Buffer.from(this.state)); + this.state = ''; + cb(); + }; + + pt.write(Buffer.from('aaaa')); + pt.write(Buffer.from('bbbb')); + pt.write(Buffer.from('cccc')); + pt.write(Buffer.from('dddd')); + pt.write(Buffer.from('eeee')); + pt.write(Buffer.from('aaaa')); + pt.write(Buffer.from('bbbb')); + pt.write(Buffer.from('cccc')); + pt.write(Buffer.from('dddd')); + pt.write(Buffer.from('eeee')); + pt.write(Buffer.from('aaaa')); + pt.write(Buffer.from('bbbb')); + pt.write(Buffer.from('cccc')); + pt.write(Buffer.from('dddd')); + pt.end(); + + // 'abcdeabcdeabcd' + pt.on('finish', common.mustCall(function() { + assert.strictEqual(pt.read(5).toString(), 'abcde'); + assert.strictEqual(pt.read(5).toString(), 'abcde'); + assert.strictEqual(pt.read(5).toString(), 'abcd'); + })); +} + +// This tests for a stall when data is written to a full stream +// that has empty transforms. +{ + // Verify complex transform behavior + let count = 0; + let saved = null; + const pt = new Transform({ highWaterMark: 3 }); + pt._transform = function(c, e, cb) { + if (count++ === 1) + saved = c; + else { + if (saved) { + pt.push(saved); + saved = null; + } + pt.push(c); + } + + cb(); + }; + + pt.once('readable', function() { + process.nextTick(function() { + pt.write(Buffer.from('d')); + pt.write(Buffer.from('ef'), common.mustCall(function() { + pt.end(); + })); + assert.strictEqual(pt.read().toString(), 'abcdef'); + assert.strictEqual(pt.read(), null); + }); + }); + + pt.write(Buffer.from('abc')); +} + + +{ + // Verify passthrough event emission + const pt = new PassThrough(); + let emits = 0; + pt.on('readable', function() { + emits++; + }); + + pt.write(Buffer.from('foog')); + pt.write(Buffer.from('bark')); + + assert.strictEqual(emits, 0); + assert.strictEqual(pt.read(5).toString(), 'foogb'); + assert.strictEqual(String(pt.read(5)), 'null'); + assert.strictEqual(emits, 0); + + pt.write(Buffer.from('bazy')); + pt.write(Buffer.from('kuel')); + + assert.strictEqual(emits, 0); + assert.strictEqual(pt.read(5).toString(), 'arkba'); + assert.strictEqual(pt.read(5).toString(), 'zykue'); + assert.strictEqual(pt.read(5), null); + + pt.end(); + + assert.strictEqual(emits, 1); + assert.strictEqual(pt.read(5).toString(), 'l'); + assert.strictEqual(pt.read(5), null); + assert.strictEqual(emits, 1); +} + +{ + // Verify passthrough event emission reordering + const pt = new PassThrough(); + let emits = 0; + pt.on('readable', function() { + emits++; + }); + + pt.write(Buffer.from('foog')); + pt.write(Buffer.from('bark')); + + assert.strictEqual(emits, 0); + assert.strictEqual(pt.read(5).toString(), 'foogb'); + assert.strictEqual(pt.read(5), null); + + pt.once('readable', common.mustCall(function() { + assert.strictEqual(pt.read(5).toString(), 'arkba'); + assert.strictEqual(pt.read(5), null); + + pt.once('readable', common.mustCall(function() { + assert.strictEqual(pt.read(5).toString(), 'zykue'); + assert.strictEqual(pt.read(5), null); + pt.once('readable', common.mustCall(function() { + assert.strictEqual(pt.read(5).toString(), 'l'); + assert.strictEqual(pt.read(5), null); + assert.strictEqual(emits, 3); + })); + pt.end(); + })); + pt.write(Buffer.from('kuel')); + })); + + pt.write(Buffer.from('bazy')); +} + +{ + // Verify passthrough facade + const pt = new PassThrough(); + const datas = []; + pt.on('data', function(chunk) { + datas.push(chunk.toString()); + }); + + pt.on('end', common.mustCall(function() { + assert.deepStrictEqual(datas, ['foog', 'bark', 'bazy', 'kuel']); + })); + + pt.write(Buffer.from('foog')); + setTimeout(function() { + pt.write(Buffer.from('bark')); + setTimeout(function() { + pt.write(Buffer.from('bazy')); + setTimeout(function() { + pt.write(Buffer.from('kuel')); + setTimeout(function() { + pt.end(); + }, 10); + }, 10); + }, 10); + }, 10); +} + +{ + // Verify object transform (JSON parse) + const jp = new Transform({ objectMode: true }); + jp._transform = function(data, encoding, cb) { + try { + jp.push(JSON.parse(data)); + cb(); + } catch (er) { + cb(er); + } + }; + + // Anything except null/undefined is fine. + // those are "magic" in the stream API, because they signal EOF. + const objects = [ + { foo: 'bar' }, + 100, + 'string', + { nested: { things: [ { foo: 'bar' }, 100, 'string' ] } }, + ]; + + let ended = false; + jp.on('end', function() { + ended = true; + }); + + for (const obj of objects) { + jp.write(JSON.stringify(obj)); + const res = jp.read(); + assert.deepStrictEqual(res, obj); + } + + jp.end(); + // Read one more time to get the 'end' event + jp.read(); + + process.nextTick(common.mustCall(function() { + assert.strictEqual(ended, true); + })); +} + +{ + // Verify object transform (JSON stringify) + const js = new Transform({ objectMode: true }); + js._transform = function(data, encoding, cb) { + try { + js.push(JSON.stringify(data)); + cb(); + } catch (er) { + cb(er); + } + }; + + // Anything except null/undefined is fine. + // those are "magic" in the stream API, because they signal EOF. + const objects = [ + { foo: 'bar' }, + 100, + 'string', + { nested: { things: [ { foo: 'bar' }, 100, 'string' ] } }, + ]; + + let ended = false; + js.on('end', function() { + ended = true; + }); + + for (const obj of objects) { + js.write(obj); + const res = js.read(); + assert.strictEqual(res, JSON.stringify(obj)); + } + + js.end(); + // Read one more time to get the 'end' event + js.read(); + + process.nextTick(common.mustCall(function() { + assert.strictEqual(ended, true); + })); +} + +{ + const s = new Transform({ + objectMode: true, + construct(callback) { + this.push('header from constructor'); + callback(); + }, + transform: (row, encoding, callback) => { + callback(null, row); + }, + }); + + const expected = [ + 'header from constructor', + 'firstLine', + 'secondLine', + ]; + s.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), expected.shift()); + }, 3)); + s.write('firstLine'); + process.nextTick(() => s.write('secondLine')); +} diff --git a/test/js/node/test/parallel/test-stream2-unpipe-drain.js b/test/js/node/test/parallel/test-stream2-unpipe-drain.js new file mode 100644 index 0000000000..4c283df680 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-unpipe-drain.js @@ -0,0 +1,72 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const stream = require('stream'); + +class TestWriter extends stream.Writable { + _write(buffer, encoding, callback) { + console.log('write called'); + // Super slow write stream (callback never called) + } +} + +const dest = new TestWriter(); + +class TestReader extends stream.Readable { + constructor() { + super(); + this.reads = 0; + } + + _read(size) { + this.reads += 1; + this.push(Buffer.alloc(size)); + } +} + +const src1 = new TestReader(); +const src2 = new TestReader(); + +src1.pipe(dest); + +src1.once('readable', () => { + process.nextTick(() => { + + src2.pipe(dest); + + src2.once('readable', () => { + process.nextTick(() => { + + src1.unpipe(dest); + }); + }); + }); +}); + + +process.on('exit', () => { + assert.strictEqual(src1.reads, 2); + assert.strictEqual(src2.reads, 2); +}); diff --git a/test/js/node/test/parallel/test-stream2-unpipe-leak.js b/test/js/node/test/parallel/test-stream2-unpipe-leak.js new file mode 100644 index 0000000000..52c16368f5 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-unpipe-leak.js @@ -0,0 +1,73 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const stream = require('stream'); + +const chunk = Buffer.from('hallo'); + +class TestWriter extends stream.Writable { + _write(buffer, encoding, callback) { + callback(null); + } +} + +const dest = new TestWriter(); + +// Set this high so that we'd trigger a nextTick warning +// and/or RangeError if we do maybeReadMore wrong. +class TestReader extends stream.Readable { + constructor() { + super({ + highWaterMark: 0x10000 + }); + } + + _read(size) { + this.push(chunk); + } +} + +const src = new TestReader(); + +for (let i = 0; i < 10; i++) { + src.pipe(dest); + src.unpipe(dest); +} + +assert.strictEqual(src.listeners('end').length, 0); +assert.strictEqual(src.listeners('readable').length, 0); + +assert.strictEqual(dest.listeners('unpipe').length, 0); +assert.strictEqual(dest.listeners('drain').length, 0); +assert.strictEqual(dest.listeners('error').length, 0); +assert.strictEqual(dest.listeners('close').length, 0); +assert.strictEqual(dest.listeners('finish').length, 0); + +console.error(src._readableState); +process.on('exit', function() { + src.readableBuffer.length = 0; + console.error(src._readableState); + assert(src.readableLength >= src.readableHighWaterMark); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-stream2-writable.js b/test/js/node/test/parallel/test-stream2-writable.js new file mode 100644 index 0000000000..6d233ae6b6 --- /dev/null +++ b/test/js/node/test/parallel/test-stream2-writable.js @@ -0,0 +1,464 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +const { Writable: W, Duplex: D } = require('stream'); +const assert = require('assert'); + +class TestWriter extends W { + constructor(opts) { + super(opts); + this.buffer = []; + this.written = 0; + } + + _write(chunk, encoding, cb) { + // Simulate a small unpredictable latency + setTimeout(() => { + this.buffer.push(chunk.toString()); + this.written += chunk.length; + cb(); + }, Math.floor(Math.random() * 10)); + } +} + +const chunks = new Array(50); +for (let i = 0; i < chunks.length; i++) { + chunks[i] = 'x'.repeat(i); +} + +{ + // Verify fast writing + const tw = new TestWriter({ + highWaterMark: 100 + }); + + tw.on('finish', common.mustCall(function() { + // Got chunks in the right order + assert.deepStrictEqual(tw.buffer, chunks); + })); + + chunks.forEach(function(chunk) { + // Ignore backpressure. Just buffer it all up. + tw.write(chunk); + }); + tw.end(); +} + +{ + // Verify slow writing + const tw = new TestWriter({ + highWaterMark: 100 + }); + + tw.on('finish', common.mustCall(function() { + // Got chunks in the right order + assert.deepStrictEqual(tw.buffer, chunks); + })); + + let i = 0; + (function W() { + tw.write(chunks[i++]); + if (i < chunks.length) + setTimeout(W, 10); + else + tw.end(); + })(); +} + +{ + // Verify write backpressure + const tw = new TestWriter({ + highWaterMark: 50 + }); + + let drains = 0; + + tw.on('finish', common.mustCall(function() { + // Got chunks in the right order + assert.deepStrictEqual(tw.buffer, chunks); + assert.strictEqual(drains, 17); + })); + + tw.on('drain', function() { + drains++; + }); + + let i = 0; + (function W() { + let ret; + do { + ret = tw.write(chunks[i++]); + } while (ret !== false && i < chunks.length); + + if (i < chunks.length) { + assert(tw.writableLength >= 50); + tw.once('drain', W); + } else { + tw.end(); + } + })(); +} + +{ + // Verify write buffersize + const tw = new TestWriter({ + highWaterMark: 100 + }); + + const encodings = + [ 'hex', + 'utf8', + 'utf-8', + 'ascii', + 'latin1', + 'binary', + 'base64', + 'ucs2', + 'ucs-2', + 'utf16le', + 'utf-16le', + undefined ]; + + tw.on('finish', function() { + // Got the expected chunks + assert.deepStrictEqual(tw.buffer, chunks); + }); + + chunks.forEach(function(chunk, i) { + const enc = encodings[i % encodings.length]; + chunk = Buffer.from(chunk); + tw.write(chunk.toString(enc), enc); + }); +} + +{ + // Verify write with no buffersize + const tw = new TestWriter({ + highWaterMark: 100, + decodeStrings: false + }); + + tw._write = function(chunk, encoding, cb) { + assert.strictEqual(typeof chunk, 'string'); + chunk = Buffer.from(chunk, encoding); + return TestWriter.prototype._write.call(this, chunk, encoding, cb); + }; + + const encodings = + [ 'hex', + 'utf8', + 'utf-8', + 'ascii', + 'latin1', + 'binary', + 'base64', + 'ucs2', + 'ucs-2', + 'utf16le', + 'utf-16le', + undefined ]; + + tw.on('finish', function() { + // Got the expected chunks + assert.deepStrictEqual(tw.buffer, chunks); + }); + + chunks.forEach(function(chunk, i) { + const enc = encodings[i % encodings.length]; + chunk = Buffer.from(chunk); + tw.write(chunk.toString(enc), enc); + }); +} + +{ + // Verify write callbacks + const callbacks = chunks.map(function(chunk, i) { + return [i, function(err) { + assert.strictEqual(err, null); + callbacks._called[i] = chunk; + }]; + }).reduce(function(set, x) { + set[`callback-${x[0]}`] = x[1]; + return set; + }, {}); + callbacks._called = []; + + const tw = new TestWriter({ + highWaterMark: 100 + }); + + tw.on('finish', common.mustCall(function() { + process.nextTick(common.mustCall(function() { + // Got chunks in the right order + assert.deepStrictEqual(tw.buffer, chunks); + // Called all callbacks + assert.deepStrictEqual(callbacks._called, chunks); + })); + })); + + chunks.forEach(function(chunk, i) { + tw.write(chunk, callbacks[`callback-${i}`]); + }); + tw.end(); +} + +{ + // Verify end() callback + const tw = new TestWriter(); + tw.end(common.mustCall(function(err) { + assert.strictEqual(err, null); + })); +} + +const helloWorldBuffer = Buffer.from('hello world'); + +{ + // Verify end() callback with chunk + const tw = new TestWriter(); + tw.end(helloWorldBuffer, common.mustCall(function(err) { + assert.strictEqual(err, null); + })); +} + +{ + // Verify end() callback with chunk and encoding + const tw = new TestWriter(); + tw.end('hello world', 'ascii', common.mustCall()); +} + +{ + // Verify end() callback after write() call + const tw = new TestWriter(); + tw.write(helloWorldBuffer); + tw.end(common.mustCall()); +} + +{ + // Verify end() callback after write() callback + const tw = new TestWriter(); + let writeCalledback = false; + tw.write(helloWorldBuffer, function() { + writeCalledback = true; + }); + tw.end(common.mustCall(function() { + assert.strictEqual(writeCalledback, true); + })); +} + +{ + // Verify encoding is ignored for buffers + const tw = new W(); + const hex = '018b5e9a8f6236ffe30e31baf80d2cf6eb'; + tw._write = common.mustCall(function(chunk) { + assert.strictEqual(chunk.toString('hex'), hex); + }); + const buf = Buffer.from(hex, 'hex'); + tw.write(buf, 'latin1'); +} + +{ + // Verify writables cannot be piped + const w = new W({ autoDestroy: false }); + w._write = common.mustNotCall(); + let gotError = false; + w.on('error', function() { + gotError = true; + }); + w.pipe(process.stdout); + assert.strictEqual(gotError, true); +} + +{ + // Verify that duplex streams cannot be piped + const d = new D(); + d._read = common.mustCall(); + d._write = common.mustNotCall(); + let gotError = false; + d.on('error', function() { + gotError = true; + }); + d.pipe(process.stdout); + assert.strictEqual(gotError, false); +} + +{ + // Verify that end(chunk) twice is an error + const w = new W(); + w._write = common.mustCall((msg) => { + assert.strictEqual(msg.toString(), 'this is the end'); + }); + let gotError = false; + w.on('error', function(er) { + gotError = true; + assert.strictEqual(er.message, 'write after end'); + }); + w.end('this is the end'); + w.end('and so is this'); + process.nextTick(common.mustCall(function() { + assert.strictEqual(gotError, true); + })); +} + +{ + // Verify stream doesn't end while writing + const w = new W(); + let wrote = false; + w._write = function(chunk, e, cb) { + assert.strictEqual(this.writing, undefined); + wrote = true; + this.writing = true; + setTimeout(() => { + this.writing = false; + cb(); + }, 1); + }; + w.on('finish', common.mustCall(function() { + assert.strictEqual(wrote, true); + assert.strictEqual(this.writing, false); + })); + w.write(Buffer.alloc(0)); + w.end(); +} + +{ + // Verify finish does not come before write() callback + const w = new W(); + let writeCb = false; + w._write = function(chunk, e, cb) { + setTimeout(function() { + writeCb = true; + cb(); + }, 10); + }; + w.on('finish', common.mustCall(function() { + assert.strictEqual(writeCb, true); + })); + w.write(Buffer.alloc(0)); + w.end(); +} + +{ + // Verify finish does not come before synchronous _write() callback + const w = new W(); + let writeCb = false; + w._write = function(chunk, e, cb) { + cb(); + }; + w.on('finish', common.mustCall(function() { + assert.strictEqual(writeCb, true); + })); + w.write(Buffer.alloc(0), function() { + writeCb = true; + }); + w.end(); +} + +{ + // Verify finish is emitted if the last chunk is empty + const w = new W(); + w._write = function(chunk, e, cb) { + process.nextTick(cb); + }; + w.on('finish', common.mustCall()); + w.write(Buffer.allocUnsafe(1)); + w.end(Buffer.alloc(0)); +} + +{ + // Verify that finish is emitted after shutdown + const w = new W(); + let shutdown = false; + + w._final = common.mustCall(function(cb) { + assert.strictEqual(this, w); + setTimeout(function() { + shutdown = true; + cb(); + }, 100); + }); + w._write = function(chunk, e, cb) { + process.nextTick(cb); + }; + w.on('finish', common.mustCall(function() { + assert.strictEqual(shutdown, true); + })); + w.write(Buffer.allocUnsafe(1)); + w.end(Buffer.allocUnsafe(0)); +} + +{ + // Verify that error is only emitted once when failing in _finish. + const w = new W(); + + w._final = common.mustCall(function(cb) { + cb(new Error('test')); + }); + w.on('error', common.mustCall((err) => { + assert.strictEqual(w._writableState.errorEmitted, true); + assert.strictEqual(err.message, 'test'); + w.on('error', common.mustNotCall()); + w.destroy(new Error()); + })); + w.end(); +} + +{ + // Verify that error is only emitted once when failing in write. + const w = new W(); + w.on('error', common.mustNotCall()); + assert.throws(() => { + w.write(null); + }, { + code: 'ERR_STREAM_NULL_VALUES' + }); +} + +{ + // Verify that error is only emitted once when failing in write after end. + const w = new W(); + w.on('error', common.mustCall((err) => { + assert.strictEqual(w._writableState.errorEmitted, true); + assert.strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END'); + })); + w.end(); + w.write('hello'); + w.destroy(new Error()); +} + +{ + // Verify that finish is not emitted after error + const w = new W(); + + w._final = common.mustCall(function(cb) { + cb(new Error()); + }); + w._write = function(chunk, e, cb) { + process.nextTick(cb); + }; + w.on('error', common.mustCall()); + w.on('prefinish', common.mustNotCall()); + w.on('finish', common.mustNotCall()); + w.write(Buffer.allocUnsafe(1)); + w.end(Buffer.allocUnsafe(0)); +} diff --git a/test/js/node/test/parallel/test-stream3-cork-end.js b/test/js/node/test/parallel/test-stream3-cork-end.js new file mode 100644 index 0000000000..0cbc033a2e --- /dev/null +++ b/test/js/node/test/parallel/test-stream3-cork-end.js @@ -0,0 +1,91 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const stream = require('stream'); +const Writable = stream.Writable; + +// Test the buffering behavior of Writable streams. +// +// The call to cork() triggers storing chunks which are flushed +// on calling end() and the stream subsequently ended. +// +// node version target: 0.12 + +const expectedChunks = ['please', 'buffer', 'me', 'kindly']; +const inputChunks = expectedChunks.slice(0); +let seenChunks = []; +let seenEnd = false; + +const w = new Writable(); +// Let's arrange to store the chunks. +w._write = function(chunk, encoding, cb) { + // Stream end event is not seen before the last write. + assert.ok(!seenEnd); + // Default encoding given none was specified. + assert.strictEqual(encoding, 'buffer'); + + seenChunks.push(chunk); + cb(); +}; +// Let's record the stream end event. +w.on('finish', () => { + seenEnd = true; +}); + +function writeChunks(remainingChunks, callback) { + const writeChunk = remainingChunks.shift(); + let writeState; + + if (writeChunk) { + setImmediate(() => { + writeState = w.write(writeChunk); + // We were not told to stop writing. + assert.ok(writeState); + + writeChunks(remainingChunks, callback); + }); + } else { + callback(); + } +} + +// Do an initial write. +w.write('stuff'); +// The write was immediate. +assert.strictEqual(seenChunks.length, 1); +// Reset the seen chunks. +seenChunks = []; + +// Trigger stream buffering. +w.cork(); + +// Write the bufferedChunks. +writeChunks(inputChunks, () => { + // Should not have seen anything yet. + assert.strictEqual(seenChunks.length, 0); + + // Trigger flush and ending the stream. + w.end(); + + // Stream should not ended in current tick. + assert.ok(!seenEnd); + + // Buffered bytes should be seen in current tick. + assert.strictEqual(seenChunks.length, 4); + + // Did the chunks match. + for (let i = 0, l = expectedChunks.length; i < l; i++) { + const seen = seenChunks[i]; + // There was a chunk. + assert.ok(seen); + + const expected = Buffer.from(expectedChunks[i]); + // It was what we expected. + assert.ok(seen.equals(expected)); + } + + setImmediate(() => { + // Stream should have ended in next tick. + assert.ok(seenEnd); + }); +}); diff --git a/test/js/node/test/parallel/test-stream3-cork-uncork.js b/test/js/node/test/parallel/test-stream3-cork-uncork.js new file mode 100644 index 0000000000..dfb901af41 --- /dev/null +++ b/test/js/node/test/parallel/test-stream3-cork-uncork.js @@ -0,0 +1,86 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const stream = require('stream'); +const Writable = stream.Writable; + +// Test the buffering behavior of Writable streams. +// +// The call to cork() triggers storing chunks which are flushed +// on calling uncork() in the same tick. +// +// node version target: 0.12 + +const expectedChunks = ['please', 'buffer', 'me', 'kindly']; +const inputChunks = expectedChunks.slice(0); +let seenChunks = []; +let seenEnd = false; + +const w = new Writable(); +// Let's arrange to store the chunks. +w._write = function(chunk, encoding, cb) { + // Default encoding given none was specified. + assert.strictEqual(encoding, 'buffer'); + + seenChunks.push(chunk); + cb(); +}; +// Let's record the stream end event. +w.on('finish', () => { + seenEnd = true; +}); + +function writeChunks(remainingChunks, callback) { + const writeChunk = remainingChunks.shift(); + let writeState; + + if (writeChunk) { + setImmediate(() => { + writeState = w.write(writeChunk); + // We were not told to stop writing. + assert.ok(writeState); + + writeChunks(remainingChunks, callback); + }); + } else { + callback(); + } +} + +// Do an initial write. +w.write('stuff'); +// The write was immediate. +assert.strictEqual(seenChunks.length, 1); +// Reset the chunks seen so far. +seenChunks = []; + +// Trigger stream buffering. +w.cork(); + +// Write the bufferedChunks. +writeChunks(inputChunks, () => { + // Should not have seen anything yet. + assert.strictEqual(seenChunks.length, 0); + + // Trigger writing out the buffer. + w.uncork(); + + // Buffered bytes should be seen in current tick. + assert.strictEqual(seenChunks.length, 4); + + // Did the chunks match. + for (let i = 0, l = expectedChunks.length; i < l; i++) { + const seen = seenChunks[i]; + // There was a chunk. + assert.ok(seen); + + const expected = Buffer.from(expectedChunks[i]); + // It was what we expected. + assert.ok(seen.equals(expected)); + } + + setImmediate(() => { + // The stream should not have been ended. + assert.ok(!seenEnd); + }); +}); diff --git a/test/js/node/test/parallel/test-stream3-pause-then-read.js b/test/js/node/test/parallel/test-stream3-pause-then-read.js new file mode 100644 index 0000000000..1a38547220 --- /dev/null +++ b/test/js/node/test/parallel/test-stream3-pause-then-read.js @@ -0,0 +1,170 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const stream = require('stream'); +const Readable = stream.Readable; +const Writable = stream.Writable; + +const totalChunks = 100; +const chunkSize = 99; +const expectTotalData = totalChunks * chunkSize; +let expectEndingData = expectTotalData; + +const r = new Readable({ highWaterMark: 1000 }); +let chunks = totalChunks; +r._read = function(n) { + console.log('_read called', chunks); + if (!(chunks % 2)) + setImmediate(push); + else if (!(chunks % 3)) + process.nextTick(push); + else + push(); +}; + +let totalPushed = 0; +function push() { + const chunk = chunks-- > 0 ? Buffer.alloc(chunkSize, 'x') : null; + if (chunk) { + totalPushed += chunk.length; + } + console.log('chunks', chunks); + r.push(chunk); +} + +read100(); + +// First we read 100 bytes. +function read100() { + readn(100, onData); +} + +function readn(n, then) { + console.error(`read ${n}`); + expectEndingData -= n; + (function read() { + const c = r.read(n); + console.error('c', c); + if (!c) + r.once('readable', read); + else { + assert.strictEqual(c.length, n); + assert(!r.readableFlowing); + then(); + } + })(); +} + +// Then we listen to some data events. +function onData() { + expectEndingData -= 100; + console.error('onData'); + let seen = 0; + r.on('data', function od(c) { + seen += c.length; + if (seen >= 100) { + // Seen enough + r.removeListener('data', od); + r.pause(); + if (seen > 100) { + // Oh no, seen too much! + // Put the extra back. + const diff = seen - 100; + r.unshift(c.slice(c.length - diff)); + console.error('seen too much', seen, diff); + } + + // Nothing should be lost in-between. + setImmediate(pipeLittle); + } + }); +} + +// Just pipe 200 bytes, then unshift the extra and unpipe. +function pipeLittle() { + expectEndingData -= 200; + console.error('pipe a little'); + const w = new Writable(); + let written = 0; + w.on('finish', () => { + assert.strictEqual(written, 200); + setImmediate(read1234); + }); + w._write = function(chunk, encoding, cb) { + written += chunk.length; + if (written >= 200) { + r.unpipe(w); + w.end(); + cb(); + if (written > 200) { + const diff = written - 200; + written -= diff; + r.unshift(chunk.slice(chunk.length - diff)); + } + } else { + setImmediate(cb); + } + }; + r.pipe(w); +} + +// Now read 1234 more bytes. +function read1234() { + readn(1234, resumePause); +} + +function resumePause() { + console.error('resumePause'); + // Don't read anything, just resume and re-pause a whole bunch. + r.resume(); + r.pause(); + r.resume(); + r.pause(); + r.resume(); + r.pause(); + r.resume(); + r.pause(); + r.resume(); + r.pause(); + setImmediate(pipe); +} + + +function pipe() { + console.error('pipe the rest'); + const w = new Writable(); + let written = 0; + w._write = function(chunk, encoding, cb) { + written += chunk.length; + cb(); + }; + w.on('finish', () => { + console.error('written', written, totalPushed); + assert.strictEqual(written, expectEndingData); + assert.strictEqual(totalPushed, expectTotalData); + console.log('ok'); + }); + r.pipe(w); +} diff --git a/test/js/node/test/parallel/test-stream3-pipeline-async-iterator.js b/test/js/node/test/parallel/test-stream3-pipeline-async-iterator.js new file mode 100644 index 0000000000..ad1e464777 --- /dev/null +++ b/test/js/node/test/parallel/test-stream3-pipeline-async-iterator.js @@ -0,0 +1,27 @@ +/* eslint-disable node-core/require-common-first, require-yield */ +'use strict'; +const { pipeline } = require('node:stream/promises'); +{ + // Ensure that async iterators can act as readable and writable streams + async function* myCustomReadable() { + yield 'Hello'; + yield 'World'; + } + + const messages = []; + async function* myCustomWritable(stream) { + for await (const chunk of stream) { + messages.push(chunk); + } + } + + (async () => { + await pipeline( + myCustomReadable, + myCustomWritable, + ); + // Importing here to avoid initializing streams + require('assert').deepStrictEqual(messages, ['Hello', 'World']); + })() + .then(require('../common').mustCall()); +} diff --git a/test/js/node/test/parallel/test-streams-highwatermark.js b/test/js/node/test/parallel/test-streams-highwatermark.js new file mode 100644 index 0000000000..e5c2f0b597 --- /dev/null +++ b/test/js/node/test/parallel/test-streams-highwatermark.js @@ -0,0 +1,111 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const stream = require('stream'); +const { inspect } = Bun; + +{ + // This test ensures that the stream implementation correctly handles values + // for highWaterMark which exceed the range of signed 32 bit integers and + // rejects invalid values. + + // This number exceeds the range of 32 bit integer arithmetic but should still + // be handled correctly. + const ovfl = Number.MAX_SAFE_INTEGER; + + const readable = stream.Readable({ highWaterMark: ovfl }); + assert.strictEqual(readable._readableState.highWaterMark, ovfl); + + const writable = stream.Writable({ highWaterMark: ovfl }); + assert.strictEqual(writable._writableState.highWaterMark, ovfl); + + for (const invalidHwm of [true, false, '5', {}, -5, NaN]) { + for (const type of [stream.Readable, stream.Writable]) { + assert.throws(() => { + type({ highWaterMark: invalidHwm }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.highWaterMark' is invalid. " + + `Received ${inspect(invalidHwm)}` + }); + } + } +} + +{ + // This test ensures that the push method's implementation + // correctly handles the edge case where the highWaterMark and + // the state.length are both zero + + const readable = stream.Readable({ highWaterMark: 0 }); + + for (let i = 0; i < 3; i++) { + const needMoreData = readable.push(); + assert.strictEqual(needMoreData, true); + } +} + +{ + // This test ensures that the read(n) method's implementation + // correctly handles the edge case where the highWaterMark, state.length + // and n are all zero + + const readable = stream.Readable({ highWaterMark: 0 }); + + readable._read = common.mustCall(); + readable.read(0); +} + +{ + // Parse size as decimal integer + ['1', '1.0', 1].forEach((size) => { + const readable = new stream.Readable({ + read: common.mustCall(), + highWaterMark: 0, + }); + readable.read(size); + + assert.strictEqual(readable._readableState.highWaterMark, Number(size)); + }); +} + +{ + // Test highwatermark limit + const hwm = 0x40000000 + 1; + const readable = stream.Readable({ + read() {}, + }); + + assert.throws(() => readable.read(hwm), common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "size" is out of range.' + + ' It must be <= 1GiB. Received ' + + hwm, + })); +} + +{ + const res = []; + const r = new stream.Readable({ + read() {}, + }); + const w = new stream.Writable({ + highWaterMark: 0, + write(chunk, encoding, callback) { + res.push(chunk.toString()); + callback(); + }, + }); + + r.pipe(w); + r.push('a'); + r.push('b'); + r.push('c'); + r.push(null); + + r.on('end', common.mustCall(() => { + assert.deepStrictEqual(res, ['a', 'b', 'c']); + })); +} diff --git a/test/js/node/test/parallel/test-string-decoder-end.js b/test/js/node/test/parallel/test-string-decoder-end.js new file mode 100644 index 0000000000..5a3c5cc720 --- /dev/null +++ b/test/js/node/test/parallel/test-string-decoder-end.js @@ -0,0 +1,128 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Verify that the string decoder works getting 1 byte at a time, +// the whole buffer at once, and that both match the .toString(enc) +// result of the entire buffer. + +require('../common'); +const assert = require('assert'); +const SD = require('string_decoder').StringDecoder; +const encodings = ['base64', 'base64url', 'hex', 'utf8', 'utf16le', 'ucs2']; + +const bufs = [ '☃💩', 'asdf' ].map((b) => Buffer.from(b)); + +// Also test just arbitrary bytes from 0-15. +for (let i = 1; i <= 16; i++) { + const bytes = '.'.repeat(i - 1).split('.').map((_, j) => j + 0x78); + bufs.push(Buffer.from(bytes)); +} + +encodings.forEach(testEncoding); + +testEnd('utf8', Buffer.of(0xE2), Buffer.of(0x61), '\uFFFDa'); +testEnd('utf8', Buffer.of(0xE2), Buffer.of(0x82), '\uFFFD\uFFFD'); +testEnd('utf8', Buffer.of(0xE2), Buffer.of(0xE2), '\uFFFD\uFFFD'); +testEnd('utf8', Buffer.of(0xE2, 0x82), Buffer.of(0x61), '\uFFFDa'); +testEnd('utf8', Buffer.of(0xE2, 0x82), Buffer.of(0xAC), '\uFFFD\uFFFD'); +testEnd('utf8', Buffer.of(0xE2, 0x82), Buffer.of(0xE2), '\uFFFD\uFFFD'); +testEnd('utf8', Buffer.of(0xE2, 0x82, 0xAC), Buffer.of(0x61), '€a'); + +testEnd('utf16le', Buffer.of(0x3D), Buffer.of(0x61, 0x00), 'a'); +testEnd('utf16le', Buffer.of(0x3D), Buffer.of(0xD8, 0x4D, 0xDC), '\u4DD8'); +testEnd('utf16le', Buffer.of(0x3D, 0xD8), Buffer.of(), '\uD83D'); +testEnd('utf16le', Buffer.of(0x3D, 0xD8), Buffer.of(0x61, 0x00), '\uD83Da'); +testEnd( + 'utf16le', + Buffer.of(0x3D, 0xD8), + Buffer.of(0x4D, 0xDC), + '\uD83D\uDC4D' +); +testEnd('utf16le', Buffer.of(0x3D, 0xD8, 0x4D), Buffer.of(), '\uD83D'); +testEnd( + 'utf16le', + Buffer.of(0x3D, 0xD8, 0x4D), + Buffer.of(0x61, 0x00), + '\uD83Da' +); +testEnd('utf16le', Buffer.of(0x3D, 0xD8, 0x4D), Buffer.of(0xDC), '\uD83D'); +testEnd( + 'utf16le', + Buffer.of(0x3D, 0xD8, 0x4D, 0xDC), + Buffer.of(0x61, 0x00), + '👍a' +); + +testEnd('base64', Buffer.of(0x61), Buffer.of(), 'YQ=='); +testEnd('base64', Buffer.of(0x61), Buffer.of(0x61), 'YQ==YQ=='); +testEnd('base64', Buffer.of(0x61, 0x61), Buffer.of(), 'YWE='); +testEnd('base64', Buffer.of(0x61, 0x61), Buffer.of(0x61), 'YWE=YQ=='); +testEnd('base64', Buffer.of(0x61, 0x61, 0x61), Buffer.of(), 'YWFh'); +testEnd('base64', Buffer.of(0x61, 0x61, 0x61), Buffer.of(0x61), 'YWFhYQ=='); + +testEnd('base64url', Buffer.of(0x61), Buffer.of(), 'YQ'); +testEnd('base64url', Buffer.of(0x61), Buffer.of(0x61), 'YQYQ'); +testEnd('base64url', Buffer.of(0x61, 0x61), Buffer.of(), 'YWE'); +testEnd('base64url', Buffer.of(0x61, 0x61), Buffer.of(0x61), 'YWEYQ'); +testEnd('base64url', Buffer.of(0x61, 0x61, 0x61), Buffer.of(), 'YWFh'); +testEnd('base64url', Buffer.of(0x61, 0x61, 0x61), Buffer.of(0x61), 'YWFhYQ'); + +function testEncoding(encoding) { + bufs.forEach((buf) => { + testBuf(encoding, buf); + }); +} + +function testBuf(encoding, buf) { + // Write one byte at a time. + let s = new SD(encoding); + let res1 = ''; + for (let i = 0; i < buf.length; i++) { + res1 += s.write(buf.slice(i, i + 1)); + } + res1 += s.end(); + + // Write the whole buffer at once. + let res2 = ''; + s = new SD(encoding); + res2 += s.write(buf); + res2 += s.end(); + + // .toString() on the buffer + const res3 = buf.toString(encoding); + + // One byte at a time should match toString + assert.strictEqual(res1, res3); + // All bytes at once should match toString + assert.strictEqual(res2, res3); +} + +function testEnd(encoding, incomplete, next, expected) { + let res = ''; + const s = new SD(encoding); + res += s.write(incomplete); + res += s.end(); + res += s.write(next); + res += s.end(); + + assert.strictEqual(res, expected); +} diff --git a/test/js/node/test/parallel/test-string-decoder-fuzz.js b/test/js/node/test/parallel/test-string-decoder-fuzz.js new file mode 100644 index 0000000000..542876e96e --- /dev/null +++ b/test/js/node/test/parallel/test-string-decoder-fuzz.js @@ -0,0 +1,49 @@ +'use strict'; +require('../common'); +const { StringDecoder } = require('string_decoder'); +const util = require('util'); +const assert = require('assert'); + +// Tests that, for random sequences of bytes, our StringDecoder gives the +// same result as a direction conversion using Buffer.toString(). +// In particular, it checks that StringDecoder aligns with V8’s own output. + +function rand(max) { + return Math.floor(Math.random() * max); +} + +function randBuf(maxLen) { + const buf = Buffer.allocUnsafe(rand(maxLen)); + for (let i = 0; i < buf.length; i++) + buf[i] = rand(256); + return buf; +} + +const encodings = [ + 'utf16le', 'utf8', 'ascii', 'hex', 'base64', 'latin1', 'base64url', +]; + +function runSingleFuzzTest() { + const enc = encodings[rand(encodings.length)]; + const sd = new StringDecoder(enc); + const bufs = []; + const strings = []; + + const N = rand(10); + for (let i = 0; i < N; ++i) { + const buf = randBuf(50); + bufs.push(buf); + strings.push(sd.write(buf)); + } + strings.push(sd.end()); + + assert.strictEqual(strings.join(''), Buffer.concat(bufs).toString(enc), + `Mismatch:\n${util.inspect(strings)}\n` + + util.inspect(bufs.map((buf) => buf.toString('hex'))) + + `\nfor encoding ${enc}`); +} + +const start = Date.now(); +// Run this for 1 second +while (Date.now() - start < 1000) + runSingleFuzzTest(); diff --git a/test/js/node/test/parallel/test-string-decoder.js b/test/js/node/test/parallel/test-string-decoder.js new file mode 100644 index 0000000000..d82a149bf2 --- /dev/null +++ b/test/js/node/test/parallel/test-string-decoder.js @@ -0,0 +1,287 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const inspect = require('util').inspect; +const StringDecoder = require('string_decoder').StringDecoder; + +// Test default encoding +let decoder = new StringDecoder(); +assert.strictEqual(decoder.encoding, 'utf8'); + +// Should work without 'new' keyword +const decoder2 = {}; +StringDecoder.call(decoder2); +assert.strictEqual(decoder2.encoding, 'utf8'); + +// UTF-8 +test('utf-8', Buffer.from('$', 'utf-8'), '$'); +test('utf-8', Buffer.from('¢', 'utf-8'), '¢'); +test('utf-8', Buffer.from('€', 'utf-8'), '€'); +test('utf-8', Buffer.from('𤭢', 'utf-8'), '𤭢'); +// A mixed ascii and non-ascii string +// Test stolen from deps/v8/test/cctest/test-strings.cc +// U+02E4 -> CB A4 +// U+0064 -> 64 +// U+12E4 -> E1 8B A4 +// U+0030 -> 30 +// U+3045 -> E3 81 85 +test( + 'utf-8', + Buffer.from([0xCB, 0xA4, 0x64, 0xE1, 0x8B, 0xA4, 0x30, 0xE3, 0x81, 0x85]), + '\u02e4\u0064\u12e4\u0030\u3045' +); + +// Some invalid input, known to have caused trouble with chunking +// in https://github.com/nodejs/node/pull/7310#issuecomment-226445923 +// 00: |00000000 ASCII +// 41: |01000001 ASCII +// B8: 10|111000 continuation +// CC: 110|01100 two-byte head +// E2: 1110|0010 three-byte head +// F0: 11110|000 four-byte head +// F1: 11110|001'another four-byte head +// FB: 111110|11 "five-byte head", not UTF-8 +test('utf-8', Buffer.from('C9B5A941', 'hex'), '\u0275\ufffdA'); +test('utf-8', Buffer.from('E2', 'hex'), '\ufffd'); +test('utf-8', Buffer.from('E241', 'hex'), '\ufffdA'); +test('utf-8', Buffer.from('CCCCB8', 'hex'), '\ufffd\u0338'); +test('utf-8', Buffer.from('F0B841', 'hex'), '\ufffdA'); +test('utf-8', Buffer.from('F1CCB8', 'hex'), '\ufffd\u0338'); +test('utf-8', Buffer.from('F0FB00', 'hex'), '\ufffd\ufffd\0'); +test('utf-8', Buffer.from('CCE2B8B8', 'hex'), '\ufffd\u2e38'); +test('utf-8', Buffer.from('E2B8CCB8', 'hex'), '\ufffd\u0338'); +test('utf-8', Buffer.from('E2FBCC01', 'hex'), '\ufffd\ufffd\ufffd\u0001'); +test('utf-8', Buffer.from('CCB8CDB9', 'hex'), '\u0338\u0379'); +// CESU-8 of U+1D40D + +// V8 has changed their invalid UTF-8 handling, see +// https://chromium-review.googlesource.com/c/v8/v8/+/671020 for more info. +test('utf-8', Buffer.from('EDA0B5EDB08D', 'hex'), + '\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd'); + +// UCS-2 +test('ucs2', Buffer.from('ababc', 'ucs2'), 'ababc'); + +// UTF-16LE +test('utf16le', Buffer.from('3DD84DDC', 'hex'), '\ud83d\udc4d'); // thumbs up + +// Additional UTF-8 tests +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('E1', 'hex')), ''); + +// A quick test for lastChar, lastNeed & lastTotal which are undocumented. +assert(decoder.lastChar.equals(new Uint8Array([0xe1, 0, 0, 0]))); +assert.strictEqual(decoder.lastNeed, 2); +assert.strictEqual(decoder.lastTotal, 3); + +assert.strictEqual(decoder.end(), '\ufffd'); + +// ArrayBufferView tests +const arrayBufferViewStr = 'String for ArrayBufferView tests\n'; +const inputBuffer = Buffer.from(arrayBufferViewStr.repeat(8), 'utf8'); +for (const expectView of common.getArrayBufferViews(inputBuffer)) { + assert.strictEqual( + decoder.write(expectView), + inputBuffer.toString('utf8') + ); + assert.strictEqual(decoder.end(), ''); +} + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('E18B', 'hex')), ''); +assert.strictEqual(decoder.end(), '\ufffd'); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('\ufffd')), '\ufffd'); +assert.strictEqual(decoder.end(), ''); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('\ufffd\ufffd\ufffd')), + '\ufffd\ufffd\ufffd'); +assert.strictEqual(decoder.end(), ''); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('EFBFBDE2', 'hex')), '\ufffd'); +assert.strictEqual(decoder.end(), '\ufffd'); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('F1', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('41F2', 'hex')), '\ufffdA'); +assert.strictEqual(decoder.end(), '\ufffd'); + +// Additional utf8Text test +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.text(Buffer.from([0x41]), 2), ''); + +// Additional UTF-16LE surrogate pair tests +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD8', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('4D', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('DC', 'hex')), '\ud83d\udc4d'); +assert.strictEqual(decoder.end(), ''); + +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD8', 'hex')), ''); +assert.strictEqual(decoder.end(), '\ud83d'); + +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD8', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('4D', 'hex')), ''); +assert.strictEqual(decoder.end(), '\ud83d'); + +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD84D', 'hex')), '\ud83d'); +assert.strictEqual(decoder.end(), ''); + +// Regression test for https://github.com/nodejs/node/issues/22358 +// (unaligned UTF-16 access). +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.alloc(1)), ''); +assert.strictEqual(decoder.write(Buffer.alloc(20)), '\0'.repeat(10)); +assert.strictEqual(decoder.write(Buffer.alloc(48)), '\0'.repeat(24)); +assert.strictEqual(decoder.end(), ''); + +// Regression tests for https://github.com/nodejs/node/issues/22626 +// (not enough replacement chars when having seen more than one byte of an +// incomplete multibyte characters). +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('f69b', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('d1', 'hex')), '\ufffd\ufffd'); +assert.strictEqual(decoder.end(), '\ufffd'); +assert.strictEqual(decoder.write(Buffer.from('f4', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('bde5', 'hex')), '\ufffd\ufffd'); +assert.strictEqual(decoder.end(), '\ufffd'); + +assert.throws( + () => new StringDecoder(1), + { + code: 'ERR_UNKNOWN_ENCODING', + name: 'TypeError', + message: 'Unknown encoding: 1' + } +); + +assert.throws( + () => new StringDecoder('test'), + { + code: 'ERR_UNKNOWN_ENCODING', + name: 'TypeError', + message: 'Unknown encoding: test' + } +); + +assert.throws( + () => new StringDecoder('utf8').write(null), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "buf" argument must be an instance of Buffer, TypedArray,' + + ' or DataView. Received null' + } +); + +// Skipped in Bun: JSC supports much larger strings, so it is extremely hard to +// actually produce this exception. +// if (common.enoughTestMem) { +// assert.throws( +// () => new StringDecoder().write(Buffer.alloc((process.arch === 'ia32' ? 0x18ffffe8 : 0x1fffffe8) + 1).fill('a')), +// { +// code: 'ERR_STRING_TOO_LONG', +// } +// ); +// } + +assert.throws( + () => new StringDecoder('utf8').__proto__.write(Buffer.from('abc')), // eslint-disable-line no-proto + { + code: 'ERR_INVALID_THIS', + } +); + +// Test verifies that StringDecoder will correctly decode the given input +// buffer with the given encoding to the expected output. It will attempt all +// possible ways to write() the input buffer, see writeSequences(). The +// singleSequence allows for easy debugging of a specific sequence which is +// useful in case of test failures. +function test(encoding, input, expected, singleSequence) { + let sequences; + if (!singleSequence) { + sequences = writeSequences(input.length); + } else { + sequences = [singleSequence]; + } + const hexNumberRE = /.{2}/g; + sequences.forEach((sequence) => { + const decoder = new StringDecoder(encoding); + let output = ''; + sequence.forEach((write) => { + output += decoder.write(input.slice(write[0], write[1])); + }); + output += decoder.end(); + if (output !== expected) { + const message = + `Expected "${unicodeEscape(expected)}", ` + + `but got "${unicodeEscape(output)}"\n` + + `input: ${input.toString('hex').match(hexNumberRE)}\n` + + `Write sequence: ${JSON.stringify(sequence)}\n` + + `Full Decoder State: ${inspect(decoder)}`; + assert.fail(message); + } + }); +} + +// unicodeEscape prints the str contents as unicode escape codes. +function unicodeEscape(str) { + let r = ''; + for (let i = 0; i < str.length; i++) { + r += `\\u${str.charCodeAt(i).toString(16)}`; + } + return r; +} + +// writeSequences returns an array of arrays that describes all possible ways a +// buffer of the given length could be split up and passed to sequential write +// calls. +// +// e.G. writeSequences(3) will return: [ +// [ [ 0, 3 ] ], +// [ [ 0, 2 ], [ 2, 3 ] ], +// [ [ 0, 1 ], [ 1, 3 ] ], +// [ [ 0, 1 ], [ 1, 2 ], [ 2, 3 ] ] +// ] +function writeSequences(length, start, sequence) { + if (start === undefined) { + start = 0; + sequence = []; + } else if (start === length) { + return [sequence]; + } + let sequences = []; + for (let end = length; end > start; end--) { + const subSequence = sequence.concat([[start, end]]); + const subSequences = writeSequences(length, end, subSequence, sequences); + sequences = sequences.concat(subSequences); + } + return sequences; +} diff --git a/test/js/node/test/parallel/test-stringbytes-external.js b/test/js/node/test/parallel/test-stringbytes-external.js new file mode 100644 index 0000000000..d64312f525 --- /dev/null +++ b/test/js/node/test/parallel/test-stringbytes-external.js @@ -0,0 +1,143 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +// Minimum string size to overflow into external string space +const EXTERN_APEX = 0xFBEE9; + +// Manually controlled string for checking binary output +let ucs2_control = 'a\u0000'; +let write_str = 'a'; + + +// First do basic checks +let b = Buffer.from(write_str, 'ucs2'); +// first check latin1 +let c = b.toString('latin1'); +assert.strictEqual(b[0], 0x61); +assert.strictEqual(b[1], 0); +assert.strictEqual(ucs2_control, c); +// now check binary +c = b.toString('binary'); +assert.strictEqual(b[0], 0x61); +assert.strictEqual(b[1], 0); +assert.strictEqual(ucs2_control, c); + +// Now create big strings +const size = 1 << 20; +write_str = write_str.repeat(size); +ucs2_control = ucs2_control.repeat(size); + +// Check resultant buffer and output string +b = Buffer.from(write_str, 'ucs2'); +// Check fist Buffer created from write string +for (let i = 0; i < b.length; i += 2) { + assert.strictEqual(b[i], 0x61); + assert.strictEqual(b[i + 1], 0); +} + +// Create another string to create an external string +const b_ucs = b.toString('ucs2'); + +// Check control against external binary string +const l_bin = b.toString('latin1'); +assert.strictEqual(ucs2_control, l_bin); + +// Check control against external binary string +const b_bin = b.toString('binary'); +assert.strictEqual(ucs2_control, b_bin); + +// Create buffer copy from external +const c_bin = Buffer.from(l_bin, 'latin1'); +const c_ucs = Buffer.from(b_ucs, 'ucs2'); +// Make sure they're the same length +assert.strictEqual(c_bin.length, c_ucs.length); +// Make sure Buffers from externals are the same +for (let i = 0; i < c_bin.length; i++) { + assert.strictEqual(c_bin[i], c_ucs[i]); +} +// Check resultant strings +assert.strictEqual(c_bin.toString('ucs2'), c_ucs.toString('ucs2')); +assert.strictEqual(c_bin.toString('latin1'), ucs2_control); +assert.strictEqual(c_ucs.toString('latin1'), ucs2_control); + + +// Now let's test BASE64 and HEX encoding/decoding +const RADIOS = 2; +const PRE_HALF_APEX = Math.ceil(EXTERN_APEX / 2) - RADIOS; +const PRE_3OF4_APEX = Math.ceil((EXTERN_APEX / 4) * 3) - RADIOS; + +{ + for (let j = 0; j < RADIOS * 2; j += 1) { + const datum = b; + const slice = datum.slice(0, PRE_HALF_APEX + j); + const slice2 = datum.slice(0, PRE_HALF_APEX + j + 2); + const pumped_string = slice.toString('hex'); + const pumped_string2 = slice2.toString('hex'); + const decoded = Buffer.from(pumped_string, 'hex'); + + // The string are the same? + for (let k = 0; k < pumped_string.length; ++k) { + assert.strictEqual(pumped_string[k], pumped_string2[k]); + } + + // The recoded buffer is the same? + for (let i = 0; i < decoded.length; ++i) { + assert.strictEqual(datum[i], decoded[i]); + } + } +} + +{ + for (let j = 0; j < RADIOS * 2; j += 1) { + const datum = b; + const slice = datum.slice(0, PRE_3OF4_APEX + j); + const slice2 = datum.slice(0, PRE_3OF4_APEX + j + 2); + const pumped_string = slice.toString('base64'); + const pumped_string2 = slice2.toString('base64'); + const decoded = Buffer.from(pumped_string, 'base64'); + + // The string are the same? + for (let k = 0; k < pumped_string.length - 3; ++k) { + assert.strictEqual(pumped_string[k], pumped_string2[k]); + } + + // The recoded buffer is the same? + for (let i = 0; i < decoded.length; ++i) { + assert.strictEqual(datum[i], decoded[i]); + } + } +} + +// https://github.com/nodejs/node/issues/1024 +{ + const a = 'x'.repeat(1 << 20 - 1); + const b = Buffer.from(a, 'ucs2').toString('ucs2'); + const c = Buffer.from(b, 'utf8').toString('utf8'); + + assert.strictEqual(a.length, b.length); + assert.strictEqual(b.length, c.length); + + assert.strictEqual(a, b); + assert.strictEqual(b, c); +} diff --git a/test/js/node/test/parallel/test-sync-fileread.js b/test/js/node/test/parallel/test-sync-fileread.js new file mode 100644 index 0000000000..826f62d220 --- /dev/null +++ b/test/js/node/test/parallel/test-sync-fileread.js @@ -0,0 +1,7 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +assert.strictEqual(fs.readFileSync(fixtures.path('x.txt')).toString(), 'xyz\n'); diff --git a/test/js/node/test/parallel/test-sys.js b/test/js/node/test/parallel/test-sys.js new file mode 100644 index 0000000000..a7a77b8e1c --- /dev/null +++ b/test/js/node/test/parallel/test-sys.js @@ -0,0 +1,28 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const sys = require('sys'); // eslint-disable-line no-restricted-modules +const util = require('util'); + +assert.strictEqual(sys, util); diff --git a/test/js/node/test/parallel/test-timers-api-refs.js b/test/js/node/test/parallel/test-timers-api-refs.js new file mode 100644 index 0000000000..3c55a05ac4 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-api-refs.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const timers = require('timers'); + +// Delete global APIs to make sure they're not relied on by the internal timers +// code +delete global.setTimeout; +delete global.clearTimeout; +delete global.setInterval; +delete global.clearInterval; +delete global.setImmediate; +delete global.clearImmediate; + +const timeoutCallback = () => { timers.clearTimeout(timeout); }; +const timeout = timers.setTimeout(common.mustCall(timeoutCallback), 1); + +const intervalCallback = () => { timers.clearInterval(interval); }; +const interval = timers.setInterval(common.mustCall(intervalCallback), 1); + +const immediateCallback = () => { timers.clearImmediate(immediate); }; +const immediate = timers.setImmediate(immediateCallback); diff --git a/test/js/node/test/parallel/test-timers-args.js b/test/js/node/test/parallel/test-timers-args.js new file mode 100644 index 0000000000..1ba44d8bcf --- /dev/null +++ b/test/js/node/test/parallel/test-timers-args.js @@ -0,0 +1,31 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +function range(n) { + return 'x'.repeat(n + 1).split('').map(function(_, i) { return i; }); +} + +function timeout(nargs) { + const args = range(nargs); + setTimeout.apply(null, [callback, 1].concat(args)); + + function callback() { + assert.deepStrictEqual([].slice.call(arguments), args); + if (nargs < 128) timeout(nargs + 1); + } +} + +function interval(nargs) { + const args = range(nargs); + const timer = setTimeout.apply(null, [callback, 1].concat(args)); + + function callback() { + clearInterval(timer); + assert.deepStrictEqual([].slice.call(arguments), args); + if (nargs < 128) interval(nargs + 1); + } +} + +timeout(0); +interval(0); diff --git a/test/js/node/test/parallel/test-timers-clear-object-does-not-throw-error.js b/test/js/node/test/parallel/test-timers-clear-object-does-not-throw-error.js new file mode 100644 index 0000000000..9752f53abd --- /dev/null +++ b/test/js/node/test/parallel/test-timers-clear-object-does-not-throw-error.js @@ -0,0 +1,8 @@ +'use strict'; +require('../common'); + +// This test makes sure clearing timers with +// objects doesn't throw +clearImmediate({}); +clearTimeout({}); +clearInterval({}); diff --git a/test/js/node/test/parallel/test-timers-clear-timeout-interval-equivalent.js b/test/js/node/test/parallel/test-timers-clear-timeout-interval-equivalent.js new file mode 100644 index 0000000000..94611b7070 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-clear-timeout-interval-equivalent.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); + +// This test makes sure that timers created with setTimeout can be disarmed by +// clearInterval and that timers created with setInterval can be disarmed by +// clearTimeout. +// +// This behavior is documented in the HTML Living Standard: +// +// * Refs: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-setinterval + +// Disarm interval with clearTimeout. +const interval = setInterval(common.mustNotCall(), 1); +clearTimeout(interval); + +// Disarm timeout with clearInterval. +const timeout = setTimeout(common.mustNotCall(), 1); +clearInterval(timeout); diff --git a/test/js/node/test/parallel/test-timers-clearImmediate.js b/test/js/node/test/parallel/test-timers-clearImmediate.js new file mode 100644 index 0000000000..ccd9826bb0 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-clearImmediate.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); + +const N = 3; + +function next() { + const fn = common.mustCall(() => clearImmediate(immediate)); + const immediate = setImmediate(fn); +} + +for (let i = 0; i < N; i++) { + next(); +} diff --git a/test/js/node/test/parallel/test-timers-immediate.js b/test/js/node/test/parallel/test-timers-immediate.js new file mode 100644 index 0000000000..0227e38efa --- /dev/null +++ b/test/js/node/test/parallel/test-timers-immediate.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +let mainFinished = false; + +setImmediate(common.mustCall(function() { + assert.strictEqual(mainFinished, true); + clearImmediate(immediateB); +})); + +const immediateB = setImmediate(common.mustNotCall()); + +setImmediate(common.mustCall((...args) => { + assert.deepStrictEqual(args, [1, 2, 3]); +}), 1, 2, 3); + +setImmediate(common.mustCall((...args) => { + assert.deepStrictEqual(args, [1, 2, 3, 4, 5]); +}), 1, 2, 3, 4, 5); + +mainFinished = true; diff --git a/test/js/node/test/parallel/test-timers-interval-throw.js b/test/js/node/test/parallel/test-timers-interval-throw.js new file mode 100644 index 0000000000..876f0de55a --- /dev/null +++ b/test/js/node/test/parallel/test-timers-interval-throw.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +// To match browser behaviour, interval should continue +// being rescheduled even if it throws. + +let count = 2; +const interval = setInterval(() => { throw new Error('IntervalError'); }, 1); + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err.message, 'IntervalError'); + if (--count === 0) { + clearInterval(interval); + } +}, 2)); diff --git a/test/js/node/test/parallel/test-timers-non-integer-delay.js b/test/js/node/test/parallel/test-timers-non-integer-delay.js new file mode 100644 index 0000000000..089c1fee8b --- /dev/null +++ b/test/js/node/test/parallel/test-timers-non-integer-delay.js @@ -0,0 +1,81 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// This test makes sure that non-integer timer delays do not make the process +// hang. See https://github.com/joyent/node/issues/8065 and +// https://github.com/joyent/node/issues/8068 which have been fixed by +// https://github.com/joyent/node/pull/8073. +// +// If the process hangs, this test will make the tests suite timeout, +// otherwise it will exit very quickly (after 50 timers with a short delay +// fire). +// +// We have to set at least several timers with a non-integer delay to +// reproduce the issue. Sometimes, a timer with a non-integer delay will +// expire correctly. 50 timers has always been more than enough to reproduce +// it 100%. + +const TIMEOUT_DELAY = 1.1; +let N = 50; + +const interval = setInterval(common.mustCall(() => { + if (--N === 0) { + clearInterval(interval); + } +}, N), TIMEOUT_DELAY); + +// Test non-integer delay ordering +{ + const ordering = []; + + setTimeout(common.mustCall(() => { + ordering.push(1); + }), 1); + + setTimeout(common.mustCall(() => { + ordering.push(2); + }), 1.8); + + setTimeout(common.mustCall(() => { + ordering.push(3); + }), 1.1); + + setTimeout(common.mustCall(() => { + ordering.push(4); + }), 1); + + setTimeout(common.mustCall(() => { + const expected = [1, 2, 3, 4]; + + assert.deepStrictEqual( + ordering, + expected, + `Non-integer delay ordering should be ${expected}, but got ${ordering}` + ); + + // 2 should always be last of these delays due to ordering guarantees by + // the implementation. + }), 2); +} diff --git a/test/js/node/test/parallel/test-timers-not-emit-duration-zero.js b/test/js/node/test/parallel/test-timers-not-emit-duration-zero.js new file mode 100644 index 0000000000..c6a51c25b3 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-not-emit-duration-zero.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +function timerNotCanceled() { + assert.fail('Timer should be canceled'); +} + +process.on( + 'warning', + common.mustNotCall(() => { + assert.fail('Timer should be canceled'); + }) +); + +{ + const timeout = setTimeout(timerNotCanceled, 0); + clearTimeout(timeout); +} + +{ + const interval = setInterval(timerNotCanceled, 0); + clearInterval(interval); +} + +{ + const timeout = setTimeout(timerNotCanceled, 0); + timeout.refresh(); + clearTimeout(timeout); +} diff --git a/test/js/node/test/parallel/test-timers-process-tampering.js b/test/js/node/test/parallel/test-timers-process-tampering.js new file mode 100644 index 0000000000..766cc9f356 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-process-tampering.js @@ -0,0 +1,8 @@ +// Check that setImmediate works even if process is tampered with. +// This is a regression test for https://github.com/nodejs/node/issues/17681. + +'use strict'; +const common = require('../common'); +global.process = {}; // Boom! +common.allowGlobals(global.process); +setImmediate(common.mustCall()); diff --git a/test/js/node/test/parallel/test-timers-promises-scheduler.js b/test/js/node/test/parallel/test-timers-promises-scheduler.js new file mode 100644 index 0000000000..7caf92fdf6 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-promises-scheduler.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); + +const { scheduler } = require('timers/promises'); +const { setTimeout } = require('timers'); +const { + strictEqual, + rejects, +} = require('assert'); + +async function testYield() { + await scheduler.yield(); + process.emit('foo'); +} +testYield().then(common.mustCall()); +queueMicrotask(common.mustCall(() => { + process.addListener('foo', common.mustCall()); +})); + +async function testWait() { + let value = 0; + setTimeout(() => value++, 10); + await scheduler.wait(15); + strictEqual(value, 1); +} + +testWait().then(common.mustCall()); + +async function testCancelableWait1() { + const ac = new AbortController(); + const wait = scheduler.wait(1e6, { signal: ac.signal }); + ac.abort(); + await rejects(wait, { + code: 'ABORT_ERR', + message: 'The operation was aborted', + }); +} + +testCancelableWait1().then(common.mustCall()); + +async function testCancelableWait2() { + const wait = scheduler.wait(10000, { signal: AbortSignal.abort() }); + await rejects(wait, { + code: 'ABORT_ERR', + message: 'The operation was aborted', + }); +} + +testCancelableWait2().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-timers-refresh-in-callback.js b/test/js/node/test/parallel/test-timers-refresh-in-callback.js new file mode 100644 index 0000000000..df62512acd --- /dev/null +++ b/test/js/node/test/parallel/test-timers-refresh-in-callback.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); + +// This test checks whether a refresh called inside the callback will keep +// the event loop alive to run the timer again. + +let didCall = false; +const timer = setTimeout(common.mustCall(() => { + if (!didCall) { + didCall = true; + timer.refresh(); + } +}, 2), 1); diff --git a/test/js/node/test/parallel/test-timers-same-timeout-wrong-list-deleted.js b/test/js/node/test/parallel/test-timers-same-timeout-wrong-list-deleted.js new file mode 100644 index 0000000000..f02603ad88 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-same-timeout-wrong-list-deleted.js @@ -0,0 +1,34 @@ +'use strict'; + +// This is a regression test for https://github.com/nodejs/node/issues/7722. +// +// When nested timers have the same timeout, calling clearTimeout on the +// older timer after it has fired causes the list the newer timer is in +// to be deleted. Since the newer timer was not cleared, it still blocks +// the event loop completing for the duration of its timeout, however, since +// no reference exists to it in its list, it cannot be canceled and its +// callback is not called when the timeout elapses. + +const common = require('../common'); + +const TIMEOUT = common.platformTimeout(100); + +const handle1 = setTimeout(common.mustCall(function() { + // Cause the old TIMEOUT list to be deleted + clearTimeout(handle1); + + // Cause a new list with the same key (TIMEOUT) to be created for this timer + const handle2 = setTimeout(common.mustNotCall(), TIMEOUT); + + setTimeout(common.mustCall(function() { + // Attempt to cancel the second timer. Fix for this bug will keep the + // newer timer from being dereferenced by keeping its list from being + // erroneously deleted. If we are able to cancel the timer successfully, + // the bug is fixed. + clearTimeout(handle2); + }), 1); + + // When this callback completes, `listOnTimeout` should now look at the + // correct list and refrain from removing the new TIMEOUT list which + // contains the reference to the newer timer. +}), TIMEOUT); diff --git a/test/js/node/test/parallel/test-timers-setimmediate-infinite-loop.js b/test/js/node/test/parallel/test-timers-setimmediate-infinite-loop.js new file mode 100644 index 0000000000..49adc390fa --- /dev/null +++ b/test/js/node/test/parallel/test-timers-setimmediate-infinite-loop.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that if an Immediate callback clears subsequent +// immediates we don't get stuck in an infinite loop. +// +// If the process does get stuck, it will be timed out by the test +// runner. +// +// Ref: https://github.com/nodejs/node/issues/9756 + +setImmediate(common.mustCall(function() { + clearImmediate(i2); + clearImmediate(i3); +})); + +const i2 = setImmediate(common.mustNotCall()); + +const i3 = setImmediate(common.mustNotCall()); diff --git a/test/js/node/test/parallel/test-timers-this.js b/test/js/node/test/parallel/test-timers-this.js new file mode 100644 index 0000000000..a2a028fb06 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-this.js @@ -0,0 +1,50 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const immediateHandler = setImmediate(common.mustCall(function() { + assert.strictEqual(this, immediateHandler); +})); + +const immediateArgsHandler = setImmediate(common.mustCall(function() { + assert.strictEqual(this, immediateArgsHandler); +}), 'args ...'); + +const intervalHandler = setInterval(common.mustCall(function() { + clearInterval(intervalHandler); + assert.strictEqual(this, intervalHandler); +}), 1); + +const intervalArgsHandler = setInterval(common.mustCall(function() { + clearInterval(intervalArgsHandler); + assert.strictEqual(this, intervalArgsHandler); +}), 1, 'args ...'); + +const timeoutHandler = setTimeout(common.mustCall(function() { + assert.strictEqual(this, timeoutHandler); +}), 1); + +const timeoutArgsHandler = setTimeout(common.mustCall(function() { + assert.strictEqual(this, timeoutArgsHandler); +}), 1, 'args ...'); diff --git a/test/js/node/test/parallel/test-timers-timeout-with-non-integer.js b/test/js/node/test/parallel/test-timers-timeout-with-non-integer.js new file mode 100644 index 0000000000..96efc69e50 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-timeout-with-non-integer.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); + +/** + * This test is for https://github.com/nodejs/node/issues/24203 + */ +let count = 50; +const time = 1.00000000000001; +const exec = common.mustCall(() => { + if (--count === 0) { + return; + } + setTimeout(exec, time); +}, count); +exec(); diff --git a/test/js/node/test/parallel/test-timers-uncaught-exception.js b/test/js/node/test/parallel/test-timers-uncaught-exception.js new file mode 100644 index 0000000000..8bcf72e36c --- /dev/null +++ b/test/js/node/test/parallel/test-timers-uncaught-exception.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const errorMsg = 'BAM!'; + +// The first timer throws... +setTimeout(common.mustCall(function() { + throw new Error(errorMsg); +}), 1); + +// ...but the second one should still run +setTimeout(common.mustCall(), 1); + +function uncaughtException(err) { + assert.strictEqual(err.message, errorMsg); +} + +process.on('uncaughtException', common.mustCall(uncaughtException)); diff --git a/test/js/node/test/parallel/test-timers-unref-throw-then-ref.js b/test/js/node/test/parallel/test-timers-unref-throw-then-ref.js new file mode 100644 index 0000000000..1dd5fdd0ad --- /dev/null +++ b/test/js/node/test/parallel/test-timers-unref-throw-then-ref.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +process.once('uncaughtException', common.mustCall((err) => { + common.expectsError({ + message: 'Timeout Error' + })(err); +})); + +let called = false; +const t = setTimeout(() => { + assert(!called); + called = true; + t.ref(); + throw new Error('Timeout Error'); +}, 1).unref(); + +setTimeout(common.mustCall(), 1); diff --git a/test/js/node/test/parallel/test-timers-unrefd-interval-still-fires.js b/test/js/node/test/parallel/test-timers-unrefd-interval-still-fires.js new file mode 100644 index 0000000000..98172d18bc --- /dev/null +++ b/test/js/node/test/parallel/test-timers-unrefd-interval-still-fires.js @@ -0,0 +1,22 @@ +'use strict'; +// Regression test for https://github.com/nodejs/node-v0.x-archive/issues/8900. +const common = require('../common'); + +const TEST_DURATION = common.platformTimeout(1000); +let N = 3; + +const keepOpen = + setTimeout( + common.mustNotCall('Test timed out. keepOpen was not canceled.'), + TEST_DURATION); + +const timer = setInterval(common.mustCall(() => { + if (--N === 0) { + clearInterval(timer); + timer._onTimeout = + common.mustNotCall('Unrefd interval fired after being cleared'); + clearTimeout(keepOpen); + } +}, N), 1); + +timer.unref(); diff --git a/test/js/node/test/parallel/test-timers-unrefed-in-beforeexit.js b/test/js/node/test/parallel/test-timers-unrefed-in-beforeexit.js new file mode 100644 index 0000000000..a38b55bf45 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-unrefed-in-beforeexit.js @@ -0,0 +1,7 @@ +'use strict'; + +const common = require('../common'); + +process.on('beforeExit', common.mustCall(() => { + setTimeout(common.mustNotCall(), 1).unref(); +})); diff --git a/test/js/node/test/parallel/test-timers-user-call.js b/test/js/node/test/parallel/test-timers-user-call.js new file mode 100644 index 0000000000..4ff24e688b --- /dev/null +++ b/test/js/node/test/parallel/test-timers-user-call.js @@ -0,0 +1,40 @@ +// Make sure `setTimeout()` and friends don't throw if the user-supplied +// function has .call() and .apply() monkey-patched to undesirable values. + +// Refs: https://github.com/nodejs/node/issues/12956 + +'use strict'; + +const common = require('../common'); + +{ + const fn = common.mustCall(10); + fn.call = 'not a function'; + fn.apply = 'also not a function'; + setTimeout(fn, 1); + setTimeout(fn, 1, 'oneArg'); + setTimeout(fn, 1, 'two', 'args'); + setTimeout(fn, 1, 'three', '(3)', 'args'); + setTimeout(fn, 1, 'more', 'than', 'three', 'args'); + + setImmediate(fn, 1); + setImmediate(fn, 1, 'oneArg'); + setImmediate(fn, 1, 'two', 'args'); + setImmediate(fn, 1, 'three', '(3)', 'args'); + setImmediate(fn, 1, 'more', 'than', 'three', 'args'); +} + +{ + const testInterval = (...args) => { + const fn = common.mustCall(() => { clearInterval(interval); }); + fn.call = 'not a function'; + fn.apply = 'also not a function'; + const interval = setInterval(fn, 1, ...args); + }; + + testInterval(); + testInterval('oneArg'); + testInterval('two', 'args'); + testInterval('three', '(3)', 'args'); + testInterval('more', 'than', 'three', 'args'); +} diff --git a/test/js/node/test/parallel/test-timers-zero-timeout.js b/test/js/node/test/parallel/test-timers-zero-timeout.js new file mode 100644 index 0000000000..61a5b2131b --- /dev/null +++ b/test/js/node/test/parallel/test-timers-zero-timeout.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// https://github.com/joyent/node/issues/2079 - zero timeout drops extra args +{ + setTimeout(common.mustCall(f), 0, 'foo', 'bar', 'baz'); + setTimeout(() => {}, 0); + + function f(a, b, c) { + assert.strictEqual(a, 'foo'); + assert.strictEqual(b, 'bar'); + assert.strictEqual(c, 'baz'); + } +} + +{ + let ncalled = 3; + + const f = common.mustCall((a, b, c) => { + assert.strictEqual(a, 'foo'); + assert.strictEqual(b, 'bar'); + assert.strictEqual(c, 'baz'); + if (--ncalled === 0) clearTimeout(iv); + }, ncalled); + + const iv = setInterval(f, 0, 'foo', 'bar', 'baz'); +} diff --git a/test/js/node/test/parallel/test-tls-add-context.js b/test/js/node/test/parallel/test-tls-add-context.js new file mode 100644 index 0000000000..8d02866ce5 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-add-context.js @@ -0,0 +1,75 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const tls = require('tls'); + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + +const serverOptions = { + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert'), + ca: [ loadPEM('ca2-cert') ], + requestCert: true, + rejectUnauthorized: false, +}; + +let connections = 0; + +const server = tls.createServer(serverOptions, (c) => { + if (++connections === 3) { + server.close(); + } + if (c.servername === 'unknowncontext') { + assert.strictEqual(c.authorized, false); + return; + } + assert.strictEqual(c.authorized, true); +}); + +const secureContext = { + key: loadPEM('agent1-key'), + cert: loadPEM('agent1-cert'), + ca: [ loadPEM('ca1-cert') ], +}; +server.addContext('context1', secureContext); +server.addContext('context2', tls.createSecureContext(secureContext)); + +const clientOptionsBase = { + key: loadPEM('agent1-key'), + cert: loadPEM('agent1-cert'), + ca: [ loadPEM('ca1-cert') ], + rejectUnauthorized: false, +}; + +server.listen(0, common.mustCall(() => { + const client1 = tls.connect({ + ...clientOptionsBase, + port: server.address().port, + servername: 'context1', + }, common.mustCall(() => { + client1.end(); + })); + + const client2 = tls.connect({ + ...clientOptionsBase, + port: server.address().port, + servername: 'context2', + }, common.mustCall(() => { + client2.end(); + })); + + const client3 = tls.connect({ + ...clientOptionsBase, + port: server.address().port, + servername: 'unknowncontext', + }, common.mustCall(() => { + client3.end(); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-alert-handling.js b/test/js/node/test/parallel/test-tls-alert-handling.js new file mode 100644 index 0000000000..bd86149bc5 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-alert-handling.js @@ -0,0 +1,96 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI'); + +const assert = require('assert'); +const net = require('net'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +let clientClosed = false; +let errorReceived = false; +function canCloseServer() { + return clientClosed && errorReceived; +} + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`, 'utf-8'); +} + +const opts = { + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert') +}; + +const max_iter = 20; +let iter = 0; + +const errorHandler = common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_WRONG_VERSION_NUMBER'); + assert.strictEqual(err.library, 'SSL routines'); + if (!common.hasOpenSSL3) assert.strictEqual(err.function, 'ssl3_get_record'); + assert.strictEqual(err.reason, 'wrong version number'); + errorReceived = true; + if (canCloseServer()) + server.close(); +}); +const server = tls.createServer(opts, common.mustCall(function(s) { + s.pipe(s); + s.on('error', errorHandler); +}, 2)); + +server.listen(0, common.mustCall(function() { + sendClient(); +})); + +server.on('tlsClientError', common.mustNotCall()); + +server.on('error', common.mustNotCall()); + +function sendClient() { + const client = tls.connect(server.address().port, { + rejectUnauthorized: false + }); + client.on('data', common.mustCall(function() { + if (iter++ === 2) sendBADTLSRecord(); + if (iter < max_iter) { + client.write('a'); + return; + } + client.end(); + }, max_iter)); + client.write('a', common.mustCall()); + client.on('error', common.mustNotCall()); + client.on('close', common.mustCall(function() { + clientClosed = true; + if (canCloseServer()) + server.close(); + })); +} + + +function sendBADTLSRecord() { + const BAD_RECORD = Buffer.from([0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + const socket = net.connect(server.address().port); + const client = tls.connect({ + socket: socket, + rejectUnauthorized: false + }, common.mustCall(function() { + client.write('x'); + client.on('data', (data) => { + socket.end(BAD_RECORD); + }); + })); + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + assert.strictEqual(err.library, 'SSL routines'); + if (!common.hasOpenSSL3) + assert.strictEqual(err.function, 'ssl3_read_bytes'); + assert.strictEqual(err.reason, 'tlsv1 alert protocol version'); + })); +} diff --git a/test/js/node/test/parallel/test-tls-alert.js b/test/js/node/test/parallel/test-tls-alert.js new file mode 100644 index 0000000000..04000771aa --- /dev/null +++ b/test/js/node/test/parallel/test-tls-alert.js @@ -0,0 +1,53 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + +const assert = require('assert'); +const { execFile } = require('child_process'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + +const server = tls.Server({ + secureProtocol: 'TLSv1_2_server_method', + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert') +}, null).listen(0, common.mustCall(() => { + const args = ['s_client', '-quiet', '-tls1_1', + '-cipher', (common.hasOpenSSL31 ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + '-connect', `127.0.0.1:${server.address().port}`]; + + execFile(common.opensslCli, args, common.mustCall((err, _, stderr) => { + assert.strictEqual(err.code, 1); + assert.match(stderr, /SSL alert number 70/); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-ca-concat.js b/test/js/node/test/parallel/test-tls-ca-concat.js new file mode 100644 index 0000000000..38a6a4dfec --- /dev/null +++ b/test/js/node/test/parallel/test-tls-ca-concat.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +// Check ca option can contain concatenated certs by prepending an unrelated +// non-CA cert and showing that agent6's CA root is still found. + +const { + connect, keys +} = require(fixtures.path('tls-connect')); + +connect({ + client: { + checkServerIdentity: (servername, cert) => { }, + ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, + }, + server: { + cert: keys.agent6.cert, + key: keys.agent6.key, + }, +}, common.mustSucceed((pair, cleanup) => { + return cleanup(); +})); diff --git a/test/js/node/test/parallel/test-tls-cert-ext-encoding.js b/test/js/node/test/parallel/test-tls-cert-ext-encoding.js new file mode 100644 index 0000000000..4556b57918 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cert-ext-encoding.js @@ -0,0 +1,89 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (common.hasOpenSSL3) + // TODO(danbev) This test fails with the following error: + // error:0D00008F:asn1 encoding routines::no matching choice type + // + // I've not been able to figure out the reason for this but there + // is a note in https://wiki.openssl.org/index.php/OpenSSL_3.0 which + // indicates that this might not work at the moment: + // "OCSP, PEM, ASN.1 have some very limited library context support" + common.skip('when using OpenSSL 3.x'); + +// NOTE: This certificate is hand-generated, hence it is not located in +// `test/fixtures/keys` to avoid confusion. +// +// The key property of this cert is that subjectAltName contains a string with +// a type `23` which cannot be encoded into string by `X509V3_EXT_print`. +const pem = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAzrmfPz5M3wTq2/CwMeSQr/N+R1FCJ+O5n+SMleKvBqaK63eJ +kL4BnySMc+ZLKCt4UQSsPFIBK63QFq8n6/vjuTDMJiBTsvzytw8zJt1Zr2HA71N3 +VIPt6NdJ/w5lgddTYxR7XudJZJ5lk3PkG8ZgrhuenPYP80UJYVzAC2YZ9KYe3r2B +rVbut1j+8h0TwVcx2Zg5PorsC/EVxHwo4dCmIHceodikr3UVqHneRcrDBytdG6Mo +IqHhZJwBeii/EES9tpWwWbzYYh+38aGGLIF2h5UlVpr0bdBVVUg+uVX3y/Qmu2Qv +4CrAO2IPV6JER9Niwl3ktzNjOMAUQG6BCRSqRQIDAQABAoIBAAmB0+cOsG5ZRYvT +5+aDgnv1EMuq2wYGnRTTZ/vErxP5OM5XcwYrFtwAzEzQPIieZywisOEdTFx74+QH +LijWLsTnj5v5RKAorejpVArnhyZfsoXPKt/CKYDZ1ddbDCQKiRU3be0RafisqDM9 +0zHLz8pyDrtdPaKMfD/0Cgj8KxlrLTmfD4otPXds8fZpQe1hR1y12XKVp47l1siW +qFGTaUPDJpQ67xybR08x5DOqmyo4cNMOuReRWrc/qRbWint9U1882eOH09gVfpJZ +Gp6FZVPSgz10MZdLSPLhXqZkY4IxIvNltjBDqkmivd12CD+GVr0qUmTJHzTpk+kG +/CWuRQkCgYEA4EFf8SJHEl0fLDJnOQFyUPY3MalMuopUkQ5CBUe3QXjQhHXsRDfj +Ci/lyzShJkHPbMDHb/rx3lYZB0xNhwnMWKS1gCFVgOCOTZLfD0K1Anxc1hOSgVxI +y5FdO9VW7oQNlsMH/WuDHps0HhJW/00lcrmdyoUM1+fE/3yPQndhUmMCgYEA6/z6 +8Gq4PHHNql+gwunAH2cZKNdmcP4Co8MvXCZwIJsLenUuLIZQ/YBKZoM/y5X/cFAG +WFJJuUe6KFetPaDm6NgZgpOmawyUwd5czDjJ6wWgsRywiTISInfJlgWLBVMOuba7 +iBL9Xuy0hmcbj0ByoRW9l3gCiBX3yJw3I6wqXTcCgYBnjei22eRF15iIeTHvQfq+ +5iNwnEQhM7V/Uj0sYQR/iEGJmUaj7ca6somDf2cW2nblOlQeIpxD1jAyjYqTW/Pv +zwc9BqeMHqW3rqWwT1Z0smbQODOD5tB6qEKMWaSN+Y6o2qC65kWjAXpclI110PME ++i+iEDRxEsaGT8d7otLfDwKBgQCs+xBaQG/x5p2SAGzP0xYALstzc4jk1FzM+5rw +mkBgtiXQyqpg+sfNOkfPIvAVZEsMYax0+0SNKrWbMsGLRjFchmMUovQ+zccQ4NT2 +4b2op8Rlbxk8R9ahK1s5u7Bu47YMjZSjJwBQn4OobVX3SI994njJ2a9JX4j0pQWK +AX5AOwKBgAfOsr8HSHTcxSW4F9gegj+hXsRYbdA+eUkFhEGrYyRJgIlQrk/HbuZC +mKd/bQ5R/vwd1cxgV6A0APzpZtbwdhvP0RWji+WnPPovgGcfK0AHFstHnga67/uu +h2LHnKQZ1qWHn+BXWo5d7hBRwWVaK66g3GDN0blZpSz1kKcpy1Pl +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICwjCCAaqgAwIBAgIDAQABMA0GCSqGSIb3DQEBDQUAMBUxEzARBgNVBAMWCmxv +Y2FsLmhvc3QwHhcNMTkxMjA1MDQyODMzWhcNNDQxMTI5MDQyODMzWjAVMRMwEQYD +VQQDFgpsb2NhbC5ob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +zrmfPz5M3wTq2/CwMeSQr/N+R1FCJ+O5n+SMleKvBqaK63eJkL4BnySMc+ZLKCt4 +UQSsPFIBK63QFq8n6/vjuTDMJiBTsvzytw8zJt1Zr2HA71N3VIPt6NdJ/w5lgddT +YxR7XudJZJ5lk3PkG8ZgrhuenPYP80UJYVzAC2YZ9KYe3r2BrVbut1j+8h0TwVcx +2Zg5PorsC/EVxHwo4dCmIHceodikr3UVqHneRcrDBytdG6MoIqHhZJwBeii/EES9 +tpWwWbzYYh+38aGGLIF2h5UlVpr0bdBVVUg+uVX3y/Qmu2Qv4CrAO2IPV6JER9Ni +wl3ktzNjOMAUQG6BCRSqRQIDAQABoxswGTAXBgNVHREEEDAOlwwqLmxvY2FsLmhv +c3QwDQYJKoZIhvcNAQENBQADggEBAH5ThRLDLwOGuhKsifyiq7k8gbx1FqRegO7H +SIiIYYB35v5Pk0ZPN8QBJwNQzJEjUMjCpHXNdBxknBXRaA8vkbnryMfJm37gPTwA +m6r0uEG78WgcEAe8bgf9iKtQGP/iydKXpSSpDgKoHbswIxD5qtzT+o6VNnkRTSfK +/OGwakluFSoJ/Q9rLpR8lKjA01BhetXMmHbETiY8LSkxOymMldXSzUTD1WdrVn8U +L3dobxT//R/0GraKXG02mf3gZNlb0MMTvW0pVwVy39YmcPEGh8L0hWh1rpAA/VXC +f79uOowv3lLTzQ9na5EThA0tp8d837hdYrrIHh5cfTqBDxG0Tu8= +-----END CERTIFICATE----- +`; + +const tls = require('tls'); + +const options = { + key: pem, + cert: pem, +}; + +const server = tls.createServer(options, (socket) => { + socket.end(); +}); +server.listen(0, common.mustCall(function() { + const client = tls.connect({ + port: this.address().port, + rejectUnauthorized: false + }, common.mustCall(() => { + // This should not crash process: + client.getPeerCertificate(); + + server.close(); + client.end(); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-cert-regression.js b/test/js/node/test/parallel/test-tls-cert-regression.js new file mode 100644 index 0000000000..478402772e --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cert-regression.js @@ -0,0 +1,80 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); + +const cert = +`-----BEGIN CERTIFICATE----- +MIIDNDCCAp2gAwIBAgIJAJvXLQpGPpm7MA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV +BAYTAkdCMRAwDgYDVQQIEwdHd3luZWRkMREwDwYDVQQHEwhXYXVuZmF3cjEUMBIG +A1UEChMLQWNrbmFjayBMdGQxEjAQBgNVBAsTCVRlc3QgQ2VydDESMBAGA1UEAxMJ +bG9jYWxob3N0MB4XDTA5MTEwMjE5MzMwNVoXDTEwMTEwMjE5MzMwNVowcDELMAkG +A1UEBhMCR0IxEDAOBgNVBAgTB0d3eW5lZGQxETAPBgNVBAcTCFdhdW5mYXdyMRQw +EgYDVQQKEwtBY2tuYWNrIEx0ZDESMBAGA1UECxMJVGVzdCBDZXJ0MRIwEAYDVQQD +Ewlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANdym7nGe2yw +6LlJfJrQtC5TmKOGrSXiyolYCbGOy4xZI4KD31d3097jhlQFJyF+10gwkE62DuJe +fLvBZDUsvLe1R8bzlVhZnBVn+3QJyUIWQAL+DsRj8P3KoD7k363QN5dIaA1GOAg2 +vZcPy1HCUsvOgvDXGRUCZqNLAyt+h/cpAgMBAAGjgdUwgdIwHQYDVR0OBBYEFK4s +VBV4shKUj3UX/fvSJnFaaPBjMIGiBgNVHSMEgZowgZeAFK4sVBV4shKUj3UX/fvS +JnFaaPBjoXSkcjBwMQswCQYDVQQGEwJHQjEQMA4GA1UECBMHR3d5bmVkZDERMA8G +A1UEBxMIV2F1bmZhd3IxFDASBgNVBAoTC0Fja25hY2sgTHRkMRIwEAYDVQQLEwlU +ZXN0IENlcnQxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAJvXLQpGPpm7MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAFxR7BA1mUlsYqPiogtxSIfLzHWh+s0bJ +SBuhNrHes4U8QxS8+x/KWjd/81gzsf9J1C2VzTlFaydAgigz3SkQYgs+TMnFkT2o +9jqoJrcdf4WpZ2DQXUALaZgwNzPumMUSx8Ac5gO+BY/RHyP6fCodYvdNwyKslnI3 +US7eCSHZsVo= +-----END CERTIFICATE-----`; + +const key = +`-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDXcpu5xntssOi5SXya0LQuU5ijhq0l4sqJWAmxjsuMWSOCg99X +d9Pe44ZUBSchftdIMJBOtg7iXny7wWQ1LLy3tUfG85VYWZwVZ/t0CclCFkAC/g7E +Y/D9yqA+5N+t0DeXSGgNRjgINr2XD8tRwlLLzoLw1xkVAmajSwMrfof3KQIDAQAB +AoGBAIBHR/tT93ce2mJAJAXV0AJpWc+7x2pwX2FpXtQujnlxNZhnRlrBCRCD7h4m +t0bVS/86kyGaesBDvAbavfx/N5keYzzmmSp5Ht8IPqKPydGWdigk4x90yWvktai7 +dWuRKF94FXr0GUuBONb/dfHdp4KBtzN7oIF9WydYGGXA9ZmBAkEA8/k01bfwQZIu +AgcdNEM94Zcug1gSspXtUu8exNQX4+PNVbadghZb1+OnUO4d3gvWfqvAnaXD3KV6 +N4OtUhQQ0QJBAOIRbKMfaymQ9yE3CQQxYfKmEhHXWARXVwuYqIFqjmhSjSXx0l/P +7mSHz1I9uDvxkJev8sQgu1TKIyTOdqPH1tkCQQDPa6H1yYoj1Un0Q2Qa2Mg1kTjk +Re6vkjPQ/KcmJEOjZjtekgFbZfLzmwLXFXqjG2FjFFaQMSxR3QYJSJQEYjbhAkEA +sy7OZcjcXnjZeEkv61Pc57/7qIp/6Aj2JGnefZ1gvI1Z9Q5kCa88rA/9Iplq8pA4 +ZBKAoDW1ZbJGAsFmxc/6mQJAdPilhci0qFN86IGmf+ZBnwsDflIwHKDaVofti4wQ +sPWhSOb9VQjMXekI4Y2l8fqAVTS2Fn6+8jkVKxXBywSVCw== +-----END RSA PRIVATE KEY-----`; + +function test(cert, key, cb) { + const server = tls.createServer({ + cert, + key + }).listen(0, function() { + server.close(cb); + }); +} + +test(cert, key, common.mustCall(function() { + test(Buffer.from(cert), Buffer.from(key), common.mustCall()); +})); diff --git a/test/js/node/test/parallel/test-tls-client-abort.js b/test/js/node/test/parallel/test-tls-client-abort.js new file mode 100644 index 0000000000..50c9a4b324 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-client-abort.js @@ -0,0 +1,35 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const cert = fixtures.readKey('rsa_cert.crt'); +const key = fixtures.readKey('rsa_private.pem'); + +const conn = tls.connect({ cert, key, port: 0 }, common.mustNotCall()); +conn.on('error', function() {}); +conn.destroy(); diff --git a/test/js/node/test/parallel/test-tls-client-destroy-soon.js b/test/js/node/test/parallel/test-tls-client-destroy-soon.js new file mode 100644 index 0000000000..7cd5db8ade --- /dev/null +++ b/test/js/node/test/parallel/test-tls-client-destroy-soon.js @@ -0,0 +1,69 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Create an ssl server. First connection, validate that not resume. +// Cache session and close connection. Use session on second connection. +// ASSERT resumption. + +const isCI = process.env.CI !== undefined; +const common = require('../common'); +if (common.isWindows && isCI) return; // TODO: BUN +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem') +}; + +const big = Buffer.alloc(2 * 1024 * 1024, 'Y'); + +// create server +const server = tls.createServer(options, common.mustCall(function(socket) { + socket.end(big); + socket.destroySoon(); +})); + +// start listening +server.listen(0, common.mustCall(function() { + const client = tls.connect({ + port: this.address().port, + rejectUnauthorized: false + }, common.mustCall(function() { + let bytesRead = 0; + + client.on('readable', function() { + const d = client.read(); + if (d) + bytesRead += d.length; + }); + + client.on('end', common.mustCall(function() { + server.close(); + assert.strictEqual(big.length, bytesRead); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-client-renegotiation-limit.js b/test/js/node/test/parallel/test-tls-client-renegotiation-limit.js new file mode 100644 index 0000000000..71d7a85bae --- /dev/null +++ b/test/js/node/test/parallel/test-tls-client-renegotiation-limit.js @@ -0,0 +1,101 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +// Renegotiation as a protocol feature was dropped after TLS1.2. +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +// Renegotiation limits to test +const LIMITS = [0, 1, 2, 3, 5, 10, 16]; + +{ + let n = 0; + function next() { + if (n >= LIMITS.length) return; + tls.CLIENT_RENEG_LIMIT = LIMITS[n++]; + test(next); + } + next(); +} + +function test(next) { + const options = { + cert: fixtures.readKey('rsa_cert.crt'), + key: fixtures.readKey('rsa_private.pem'), + }; + + const server = tls.createServer(options, (conn) => { + conn.on('error', (err) => { + console.error(`Caught exception: ${err}`); + assert.match(err.message, /TLS session renegotiation attack/); + conn.destroy(); + }); + conn.pipe(conn); + }); + + server.listen(0, () => { + const options = { + host: server.address().host, + port: server.address().port, + rejectUnauthorized: false, + }; + const client = tls.connect(options, spam); + + let renegs = 0; + + client.on('close', () => { + assert.strictEqual(renegs, tls.CLIENT_RENEG_LIMIT + 1); + server.close(); + process.nextTick(next); + }); + + client.on('error', (err) => { + console.log('CLIENT ERR', err); + throw err; + }); + + client.on('close', (hadErr) => { + assert.strictEqual(hadErr, false); + }); + + // Simulate renegotiation attack + function spam() { + client.write(''); + client.renegotiate({}, (err) => { + assert.ifError(err); + assert.ok(renegs <= tls.CLIENT_RENEG_LIMIT); + spam(); + }); + renegs++; + } + }); +} diff --git a/test/js/node/test/parallel/test-tls-client-verify.js b/test/js/node/test/parallel/test-tls-client-verify.js new file mode 100644 index 0000000000..a8de1078bf --- /dev/null +++ b/test/js/node/test/parallel/test-tls-client-verify.js @@ -0,0 +1,144 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const testCases = [ + { ca: ['ca1-cert'], + key: 'agent2-key', + cert: 'agent2-cert', + servers: [ + { ok: true, key: 'agent1-key', cert: 'agent1-cert' }, + { ok: false, key: 'agent2-key', cert: 'agent2-cert' }, + { ok: false, key: 'agent3-key', cert: 'agent3-cert' }, + ] }, + + { ca: [], + key: 'agent2-key', + cert: 'agent2-cert', + servers: [ + { ok: false, key: 'agent1-key', cert: 'agent1-cert' }, + { ok: false, key: 'agent2-key', cert: 'agent2-cert' }, + { ok: false, key: 'agent3-key', cert: 'agent3-cert' }, + ] }, + + { ca: ['ca1-cert', 'ca2-cert'], + key: 'agent2-key', + cert: 'agent2-cert', + servers: [ + { ok: true, key: 'agent1-key', cert: 'agent1-cert' }, + { ok: false, key: 'agent2-key', cert: 'agent2-cert' }, + { ok: true, key: 'agent3-key', cert: 'agent3-cert' }, + ] }, +]; + + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + +let successfulTests = 0; + +function testServers(index, servers, clientOptions, cb) { + const serverOptions = servers[index]; + if (!serverOptions) { + cb(); + return; + } + + const ok = serverOptions.ok; + + if (serverOptions.key) { + serverOptions.key = loadPEM(serverOptions.key); + } + + if (serverOptions.cert) { + serverOptions.cert = loadPEM(serverOptions.cert); + } + + const server = tls.createServer(serverOptions, common.mustCall(function(s) { + s.end('hello world\n'); + })); + + server.listen(0, common.mustCall(function() { + let b = ''; + + console.error('connecting...'); + clientOptions.port = this.address().port; + const client = tls.connect(clientOptions, common.mustCall(function() { + const authorized = client.authorized || + (client.authorizationError === 'ERR_TLS_CERT_ALTNAME_INVALID'); + + console.error(`expected: ${ok} authed: ${authorized}`); + + assert.strictEqual(authorized, ok); + server.close(); + })); + + client.on('data', function(d) { + b += d.toString(); + }); + + client.on('end', common.mustCall(function() { + assert.strictEqual(b, 'hello world\n'); + })); + + client.on('close', common.mustCall(function() { + testServers(index + 1, servers, clientOptions, cb); + })); + })); +} + + +function runTest(testIndex) { + const tcase = testCases[testIndex]; + if (!tcase) return; + + const clientOptions = { + port: undefined, + ca: tcase.ca.map(loadPEM), + key: loadPEM(tcase.key), + cert: loadPEM(tcase.cert), + rejectUnauthorized: false + }; + + + testServers(0, tcase.servers, clientOptions, common.mustCall(function() { + successfulTests++; + runTest(testIndex + 1); + })); +} + + +runTest(0); + + +process.on('exit', function() { + console.log(`successful tests: ${successfulTests}`); + assert.strictEqual(successfulTests, testCases.length); +}); diff --git a/test/js/node/test/parallel/test-tls-connect-no-host.js b/test/js/node/test/parallel/test-tls-connect-no-host.js new file mode 100644 index 0000000000..97b95332c4 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-connect-no-host.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); +const assert = require('assert'); + +const cert = fixtures.readKey('rsa_cert.crt'); +const key = fixtures.readKey('rsa_private.pem'); + +// https://github.com/nodejs/node/issues/1489 +// tls.connect(options) with no options.host should accept a cert with +// CN:'localhost' +const server = tls.createServer({ + key, + cert +}).listen(0, common.mustCall(function() { + const socket = tls.connect({ + port: this.address().port, + ca: cert, + // No host set here. 'localhost' is the default, + // but tls.checkServerIdentity() breaks before the fix with: + // Error: Hostname/IP doesn't match certificate's altnames: + // "Host: undefined. is not cert's CN: localhost" + }, common.mustCall(function() { + assert(socket.authorized); + socket.destroy(); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-connect-secure-context.js b/test/js/node/test/parallel/test-tls-connect-secure-context.js new file mode 100644 index 0000000000..31941656c0 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-connect-secure-context.js @@ -0,0 +1,53 @@ +'use strict'; +require('../common'); + +// Verify connection with explicitly created client SecureContext. + +const fixtures = require('../common/fixtures'); +const { + assert, connect, keys, tls +} = require(fixtures.path('tls-connect')); + +connect({ + client: { + servername: 'agent1', + secureContext: tls.createSecureContext({ + ca: keys.agent1.ca, + }), + }, + server: { + cert: keys.agent1.cert, + key: keys.agent1.key, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +connect({ + client: { + servername: 'agent1', + secureContext: tls.createSecureContext({ + ca: keys.agent1.ca, + ciphers: null, + clientCertEngine: null, + crl: null, + dhparam: null, + passphrase: null, + pfx: null, + privateKeyIdentifier: null, + privateKeyEngine: null, + sessionIdContext: null, + sessionTimeout: null, + sigalgs: null, + ticketKeys: null, + }), + }, + server: { + cert: keys.agent1.cert, + key: keys.agent1.key, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); diff --git a/test/js/node/test/parallel/test-tls-dhe.js b/test/js/node/test/parallel/test-tls-dhe.js new file mode 100644 index 0000000000..46779b09ff --- /dev/null +++ b/test/js/node/test/parallel/test-tls-dhe.js @@ -0,0 +1,112 @@ +// Flags: --no-warnings +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('missing openssl-cli'); + +const assert = require('assert'); +const { X509Certificate } = require('crypto'); +const { once } = require('events'); +const tls = require('tls'); +const { execFile } = require('child_process'); +const fixtures = require('../common/fixtures'); + +const key = fixtures.readKey('agent2-key.pem'); +const cert = fixtures.readKey('agent2-cert.pem'); + +// Prefer DHE over ECDHE when possible. +const dheCipher = 'DHE-RSA-AES128-SHA256'; +const ecdheCipher = 'ECDHE-RSA-AES128-SHA256'; +const ciphers = `${dheCipher}:${ecdheCipher}`; + +// Test will emit a warning because the DH parameter size is < 2048 bits +common.expectWarning('SecurityWarning', + 'DH parameter is less than 2048 bits'); + +function loadDHParam(n) { + const keyname = `dh${n}.pem`; + return fixtures.readKey(keyname); +} + +function test(dhparam, keylen, expectedCipher) { + const options = { + key, + cert, + ciphers, + dhparam, + maxVersion: 'TLSv1.2', + }; + + const server = tls.createServer(options, (conn) => conn.end()); + + server.listen(0, '127.0.0.1', common.mustCall(() => { + const args = ['s_client', '-connect', `127.0.0.1:${server.address().port}`, + '-cipher', `${ciphers}:@SECLEVEL=1`]; + + execFile(common.opensslCli, args, common.mustSucceed((stdout) => { + assert(keylen === null || + stdout.includes(`Server Temp Key: DH, ${keylen} bits`)); + assert(stdout.includes(`Cipher : ${expectedCipher}`)); + server.close(); + })); + })); + + return once(server, 'close'); +} + +function testCustomParam(keylen, expectedCipher) { + const dhparam = loadDHParam(keylen); + if (keylen === 'error') keylen = null; + return test(dhparam, keylen, expectedCipher); +} + +(async () => { + // By default, DHE is disabled while ECDHE is enabled. + for (const dhparam of [undefined, null]) { + await test(dhparam, null, ecdheCipher); + } + + // The DHE parameters selected by OpenSSL depend on the strength of the + // certificate's key. For this test, we can assume that the modulus length + // of the certificate's key is equal to the size of the DHE parameter, but + // that is really only true for a few modulus lengths. + const { + publicKey: { asymmetricKeyDetails: { modulusLength } } + } = new X509Certificate(cert); + await test('auto', modulusLength, dheCipher); + + assert.throws(() => { + testCustomParam(512); + }, /DH parameter is less than 1024 bits/); + + // Custom DHE parameters are supported (but discouraged). + await testCustomParam(1024, dheCipher); + await testCustomParam(2048, dheCipher); + + // Invalid DHE parameters are discarded. ECDHE remains enabled. + await testCustomParam('error', ecdheCipher); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-tls-ecdh-auto.js b/test/js/node/test/parallel/test-tls-ecdh-auto.js new file mode 100644 index 0000000000..11c588d8ac --- /dev/null +++ b/test/js/node/test/parallel/test-tls-ecdh-auto.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that the value "auto" on ecdhCurve option is +// supported to enable automatic curve selection in TLS server. + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('missing openssl-cli'); + +const assert = require('assert'); +const tls = require('tls'); +const { execFile } = require('child_process'); +const fixtures = require('../common/fixtures'); + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + +const options = { + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert'), + ciphers: '-ALL:ECDHE-RSA-AES128-SHA256', + ecdhCurve: 'auto', + maxVersion: 'TLSv1.2', +}; + +const reply = 'I AM THE WALRUS'; // Something recognizable + +const server = tls.createServer(options, (conn) => { + conn.end(reply); +}).listen(0, common.mustCall(() => { + const args = ['s_client', + '-cipher', `${options.ciphers}`, + '-connect', `127.0.0.1:${server.address().port}`]; + + execFile(common.opensslCli, args, common.mustSucceed((stdout) => { + assert(stdout.includes(reply)); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-ecdh-multiple.js b/test/js/node/test/parallel/test-tls-ecdh-multiple.js new file mode 100644 index 0000000000..5bf119f48b --- /dev/null +++ b/test/js/node/test/parallel/test-tls-ecdh-multiple.js @@ -0,0 +1,61 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that ecdhCurve option of TLS server supports colon +// separated ECDH curve names as value. + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('missing openssl-cli'); + +const assert = require('assert'); +const tls = require('tls'); +const { execFile } = require('child_process'); +const fixtures = require('../common/fixtures'); + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + +const options = { + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert'), + ciphers: '-ALL:ECDHE-RSA-AES128-SHA256', + ecdhCurve: 'secp256k1:prime256v1:secp521r1', + maxVersion: 'TLSv1.2', +}; + +const reply = 'I AM THE WALRUS'; // Something recognizable + +const server = tls.createServer(options, (conn) => { + conn.end(reply); +}).listen(0, common.mustCall(() => { + const args = ['s_client', + '-cipher', `${options.ciphers}`, + '-connect', `127.0.0.1:${server.address().port}`]; + + execFile(common.opensslCli, args, common.mustSucceed((stdout) => { + assert(stdout.includes(reply)); + server.close(); + })); +})); + +{ + // Some unsupported curves. + const unsupportedCurves = [ + 'wap-wsg-idm-ecid-wtls1', + 'c2pnb163v1', + 'prime192v3', + ]; + + // Brainpool is not supported in FIPS mode. + if (common.hasFipsCrypto) + unsupportedCurves.push('brainpoolP256r1'); + + unsupportedCurves.forEach((ecdhCurve) => { + assert.throws(() => tls.createServer({ ecdhCurve }), + /Error: Failed to set ECDH curve/); + }); +} diff --git a/test/js/node/test/parallel/test-tls-ecdh.js b/test/js/node/test/parallel/test-tls-ecdh.js new file mode 100644 index 0000000000..8c879f850c --- /dev/null +++ b/test/js/node/test/parallel/test-tls-ecdh.js @@ -0,0 +1,59 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('missing openssl-cli'); + +const assert = require('assert'); +const tls = require('tls'); + +const exec = require('child_process').exec; + +const options = { + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + ciphers: '-ALL:ECDHE-RSA-AES128-SHA256', + ecdhCurve: 'prime256v1', + maxVersion: 'TLSv1.2' +}; + +const reply = 'I AM THE WALRUS'; // Something recognizable + +const server = tls.createServer(options, common.mustCall(function(conn) { + conn.end(reply); +})); + +server.listen(0, '127.0.0.1', common.mustCall(function() { + const cmd = `"${common.opensslCli}" s_client -cipher ${ + options.ciphers} -connect 127.0.0.1:${this.address().port}`; + + exec(cmd, common.mustSucceed((stdout, stderr) => { + assert(stdout.includes(reply)); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-env-extra-ca-no-crypto.js b/test/js/node/test/parallel/test-tls-env-extra-ca-no-crypto.js new file mode 100644 index 0000000000..6f2aca505e --- /dev/null +++ b/test/js/node/test/parallel/test-tls-env-extra-ca-no-crypto.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { fork } = require('child_process'); + +// This test ensures that trying to load extra certs won't throw even when +// there is no crypto support, i.e., built with "./configure --without-ssl". +if (process.argv[2] === 'child') { + // exit +} else { + const NODE_EXTRA_CA_CERTS = fixtures.path('keys', 'ca1-cert.pem'); + + fork( + __filename, + ['child'], + { env: { ...process.env, NODE_EXTRA_CA_CERTS } }, + ).on('exit', common.mustCall(function(status) { + // Client did not succeed in connecting + assert.strictEqual(status, 0); + })); +} diff --git a/test/js/node/test/parallel/test-tls-fast-writing.js b/test/js/node/test/parallel/test-tls-fast-writing.js new file mode 100644 index 0000000000..4718acf285 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-fast-writing.js @@ -0,0 +1,76 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const tls = require('tls'); + +const options = { key: fixtures.readKey('rsa_private.pem'), + cert: fixtures.readKey('rsa_cert.crt'), + ca: [ fixtures.readKey('rsa_ca.crt') ] }; + +const server = tls.createServer(options, onconnection); +let gotChunk = false; +let gotDrain = false; + +function onconnection(conn) { + conn.on('data', function(c) { + if (!gotChunk) { + gotChunk = true; + console.log('ok - got chunk'); + } + + // Just some basic sanity checks. + assert(c.length); + assert(Buffer.isBuffer(c)); + + if (gotDrain) + process.exit(0); + }); +} + +server.listen(0, function() { + const chunk = Buffer.alloc(1024, 'x'); + const opt = { port: this.address().port, rejectUnauthorized: false }; + const conn = tls.connect(opt, function() { + conn.on('drain', ondrain); + write(); + }); + function ondrain() { + if (!gotDrain) { + gotDrain = true; + console.log('ok - got drain'); + } + if (gotChunk) + process.exit(0); + write(); + } + + function write() { + // This needs to return false eventually + while (false !== conn.write(chunk)); + } +}); diff --git a/test/js/node/test/parallel/test-tls-inception.js b/test/js/node/test/parallel/test-tls-inception.js new file mode 100644 index 0000000000..7310308e6f --- /dev/null +++ b/test/js/node/test/parallel/test-tls-inception.js @@ -0,0 +1,86 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); + +const net = require('net'); + +const options = { + key: fixtures.readKey('rsa_private.pem'), + cert: fixtures.readKey('rsa_cert.crt') +}; + +const body = 'A'.repeat(40000); + +// the "proxy" server +const a = tls.createServer(options, function(socket) { + const myOptions = { + host: '127.0.0.1', + port: b.address().port, + rejectUnauthorized: false + }; + const dest = net.connect(myOptions); + dest.pipe(socket); + socket.pipe(dest); + + dest.on('end', function() { + socket.destroy(); + }); +}); + +// the "target" server +const b = tls.createServer(options, function(socket) { + socket.end(body); +}); + +a.listen(0, function() { + b.listen(0, function() { + const myOptions = { + host: '127.0.0.1', + port: a.address().port, + rejectUnauthorized: false + }; + const socket = tls.connect(myOptions); + const ssl = tls.connect({ + socket: socket, + rejectUnauthorized: false + }); + ssl.setEncoding('utf8'); + let buf = ''; + ssl.on('data', function(data) { + buf += data; + }); + ssl.on('end', common.mustCall(function() { + assert.strictEqual(buf, body); + ssl.end(); + a.close(); + b.close(); + })); + }); +}); diff --git a/test/js/node/test/parallel/test-tls-multiple-cas-as-string.js b/test/js/node/test/parallel/test-tls-multiple-cas-as-string.js new file mode 100644 index 0000000000..679d6b6c4c --- /dev/null +++ b/test/js/node/test/parallel/test-tls-multiple-cas-as-string.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +// Verify that multiple CA certificates can be provided, and that for +// convenience that can also be in newline-separated strings. + +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const ca1 = fixtures.readKey('ca1-cert.pem', 'utf8'); +const ca2 = fixtures.readKey('ca2-cert.pem', 'utf8'); +const cert = fixtures.readKey('agent3-cert.pem', 'utf8'); +const key = fixtures.readKey('agent3-key.pem', 'utf8'); + +function test(ca) { + const server = tls.createServer({ ca, cert, key }); + + server.addContext('agent3', { ca, cert, key }); + + const host = common.localhostIPv4; + server.listen(0, host, common.mustCall(() => { + const socket = tls.connect({ + servername: 'agent3', + host, + port: server.address().port, + ca + }, common.mustCall(() => { + socket.end(); + })); + + socket.on('close', () => { + server.close(); + }); + })); +} + +// `ca1` is not actually necessary for the certificate validation -- maybe +// the fixtures should be written in a way that requires it? +test([ca1, ca2]); +test(`${ca1}\n${ca2}`); diff --git a/test/js/node/test/parallel/test-tls-net-connect-prefer-path.js b/test/js/node/test/parallel/test-tls-net-connect-prefer-path.js new file mode 100644 index 0000000000..d7b7dcc351 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-net-connect-prefer-path.js @@ -0,0 +1,66 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const fixtures = require('../common/fixtures'); + +// This tests that both tls and net will ignore host and port if path is +// provided. + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const tls = require('tls'); +const net = require('net'); +const assert = require('assert'); + +function libName(lib) { + return lib === net ? 'net' : 'tls'; +} + +function mkServer(lib, tcp, cb) { + const handler = (socket) => { + socket.write(`${libName(lib)}:${ + server.address().port || server.address() + }`); + socket.end(); + }; + const args = [handler]; + if (lib === tls) { + args.unshift({ + cert: fixtures.readKey('rsa_cert.crt'), + key: fixtures.readKey('rsa_private.pem') + }); + } + const server = lib.createServer(...args); + server.listen(tcp ? 0 : common.PIPE, common.mustCall(() => cb(server))); +} + +function testLib(lib, cb) { + mkServer(lib, true, (tcpServer) => { + mkServer(lib, false, (unixServer) => { + const client = lib.connect({ + path: unixServer.address(), + port: tcpServer.address().port, + host: 'localhost', + rejectUnauthorized: false + }, () => { + const bufs = []; + client.on('data', common.mustCall((d) => { + bufs.push(d); + })); + client.on('end', common.mustCall(() => { + const resp = Buffer.concat(bufs).toString(); + assert.strictEqual(resp, `${libName(lib)}:${unixServer.address()}`); + tcpServer.close(); + unixServer.close(); + cb(); + })); + }); + }); + }); +} + +testLib(net, common.mustCall(() => testLib(tls, common.mustCall()))); diff --git a/test/js/node/test/parallel/test-tls-no-sslv3.js b/test/js/node/test/parallel/test-tls-no-sslv3.js new file mode 100644 index 0000000000..9282beb4bd --- /dev/null +++ b/test/js/node/test/parallel/test-tls-no-sslv3.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (common.opensslCli === false) + common.skip('node compiled without OpenSSL CLI.'); + +const assert = require('assert'); +const tls = require('tls'); +const spawn = require('child_process').spawn; +const fixtures = require('../common/fixtures'); + +const cert = fixtures.readKey('rsa_cert.crt'); +const key = fixtures.readKey('rsa_private.pem'); +const server = tls.createServer({ cert, key }, common.mustNotCall()); +const errors = []; +let stderr = ''; + +server.listen(0, '127.0.0.1', function() { + const address = `${this.address().address}:${this.address().port}`; + const args = ['s_client', + '-ssl3', + '-connect', address]; + + const client = spawn(common.opensslCli, args, { stdio: 'pipe' }); + client.stdout.pipe(process.stdout); + client.stderr.pipe(process.stderr); + client.stderr.setEncoding('utf8'); + client.stderr.on('data', (data) => stderr += data); + + client.once('exit', common.mustCall(function(exitCode) { + assert.strictEqual(exitCode, 1); + server.close(); + })); +}); + +server.on('tlsClientError', (err) => errors.push(err)); + +process.on('exit', function() { + if (/[Uu]nknown option:? -ssl3/.test(stderr)) { + common.printSkipMessage('`openssl s_client -ssl3` not supported.'); + } else { + assert.strictEqual(errors.length, 1); + assert(/:version too low/.test(errors[0].message)); + } +}); diff --git a/test/js/node/test/parallel/test-tls-ocsp-callback.js b/test/js/node/test/parallel/test-tls-ocsp-callback.js new file mode 100644 index 0000000000..04a60a0890 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-ocsp-callback.js @@ -0,0 +1,113 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const assert = require('assert'); + +const SSL_OP_NO_TICKET = require('crypto').constants.SSL_OP_NO_TICKET; + +const pfx = fixtures.readKey('agent1.pfx'); +const key = fixtures.readKey('agent1-key.pem'); +const cert = fixtures.readKey('agent1-cert.pem'); +const ca = fixtures.readKey('ca1-cert.pem'); + +function test(testOptions, cb) { + const options = { + key, + cert, + ca: [ca] + }; + const requestCount = testOptions.response ? 0 : 1; + + if (!testOptions.ocsp) + assert.strictEqual(testOptions.response, undefined); + + if (testOptions.pfx) { + delete options.key; + delete options.cert; + options.pfx = testOptions.pfx; + options.passphrase = testOptions.passphrase; + } + + const server = tls.createServer(options, common.mustCall((cleartext) => { + cleartext.on('error', function(er) { + // We're ok with getting ECONNRESET in this test, but it's + // timing-dependent, and thus unreliable. Any other errors + // are just failures, though. + if (er.code !== 'ECONNRESET') + throw er; + }); + cleartext.end(); + }, requestCount)); + + if (!testOptions.ocsp) + server.on('OCSPRequest', common.mustNotCall()); + else + server.on('OCSPRequest', common.mustCall((cert, issuer, callback) => { + assert.ok(Buffer.isBuffer(cert)); + assert.ok(Buffer.isBuffer(issuer)); + + // Callback a little later to ensure that async really works. + return setTimeout(callback, 100, null, testOptions.response ? + Buffer.from(testOptions.response) : null); + })); + + server.listen(0, function() { + const client = tls.connect({ + port: this.address().port, + requestOCSP: testOptions.ocsp, + secureOptions: testOptions.ocsp ? 0 : SSL_OP_NO_TICKET, + rejectUnauthorized: false + }, common.mustCall(requestCount)); + + client.on('OCSPResponse', common.mustCall((resp) => { + if (testOptions.response) { + assert.strictEqual(resp.toString(), testOptions.response); + client.destroy(); + } else { + assert.strictEqual(resp, null); + } + }, testOptions.ocsp === false ? 0 : 1)); + + client.on('close', common.mustCall(() => { + server.close(cb); + })); + }); +} + +test({ ocsp: true, response: false }); +test({ ocsp: true, response: 'hello world' }); +test({ ocsp: false }); + +if (!common.hasFipsCrypto) { + test({ ocsp: true, response: 'hello pfx', pfx: pfx, passphrase: 'sample' }); +} diff --git a/test/js/node/test/parallel/test-tls-on-empty-socket.js b/test/js/node/test/parallel/test-tls-on-empty-socket.js new file mode 100644 index 0000000000..87d51a81bb --- /dev/null +++ b/test/js/node/test/parallel/test-tls-on-empty-socket.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const net = require('net'); +const fixtures = require('../common/fixtures'); + +let out = ''; + +const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}, function(c) { + c.end('hello'); +}).listen(0, function() { + const socket = new net.Socket(); + + const s = tls.connect({ + socket: socket, + rejectUnauthorized: false + }, function() { + s.on('data', function(chunk) { + out += chunk; + }); + s.on('end', function() { + s.destroy(); + server.close(); + }); + }); + + socket.connect(this.address().port); +}); + +process.on('exit', function() { + assert.strictEqual(out, 'hello'); +}); diff --git a/test/js/node/test/parallel/test-tls-peer-certificate-encoding.js b/test/js/node/test/parallel/test-tls-peer-certificate-encoding.js new file mode 100644 index 0000000000..154c31c0a1 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-peer-certificate-encoding.js @@ -0,0 +1,53 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const util = require('util'); +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent5-key.pem'), + cert: fixtures.readKey('agent5-cert.pem'), + ca: [ fixtures.readKey('ca2-cert.pem') ] +}; + +const server = tls.createServer(options, (cleartext) => { + cleartext.end('World'); +}); +server.listen(0, common.mustCall(function() { + const socket = tls.connect({ + port: this.address().port, + rejectUnauthorized: false + }, common.mustCall(() => { + const peerCert = socket.getPeerCertificate(); + + console.error(util.inspect(peerCert)); + assert.strictEqual(peerCert.subject.CN, 'Ádám Lippai'); + server.close(); + })); + socket.end('Hello'); +})); diff --git a/test/js/node/test/parallel/test-tls-peer-certificate-multi-keys.js b/test/js/node/test/parallel/test-tls-peer-certificate-multi-keys.js new file mode 100644 index 0000000000..ce4a0d406f --- /dev/null +++ b/test/js/node/test/parallel/test-tls-peer-certificate-multi-keys.js @@ -0,0 +1,62 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('rsa_private.pem'), + cert: fixtures.readKey('rsa_cert.crt') +}; + +const server = tls.createServer(options, function(cleartext) { + cleartext.end('World'); +}); + +server.once('secureConnection', common.mustCall(function(socket) { + const cert = socket.getCertificate(); + // The server's local cert is the client's peer cert. + assert.deepStrictEqual( + cert.subject.OU, + ['Test TLS Certificate', 'Engineering'] + ); +})); + +server.listen(0, common.mustCall(function() { + const socket = tls.connect({ + port: this.address().port, + rejectUnauthorized: false + }, common.mustCall(function() { + const peerCert = socket.getPeerCertificate(); + assert.deepStrictEqual( + peerCert.subject.OU, + ['Test TLS Certificate', 'Engineering'] + ); + server.close(); + })); + socket.end('Hello'); +})); diff --git a/test/js/node/test/parallel/test-tls-psk-server.js b/test/js/node/test/parallel/test-tls-psk-server.js new file mode 100644 index 0000000000..b926095840 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-psk-server.js @@ -0,0 +1,77 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); +if (!common.opensslCli) + common.skip('missing openssl cli'); + +const assert = require('assert'); + +const tls = require('tls'); +const spawn = require('child_process').spawn; + +const CIPHERS = 'PSK+HIGH'; +const KEY = 'd731ef57be09e5204f0b205b60627028'; +const IDENTITY = 'TestUser'; + +const server = tls.createServer({ + ciphers: CIPHERS, + pskIdentityHint: IDENTITY, + pskCallback(socket, identity) { + assert.ok(socket instanceof tls.TLSSocket); + assert.ok(typeof identity === 'string'); + if (identity === IDENTITY) + return Buffer.from(KEY, 'hex'); + } +}); + +server.on('connection', common.mustCall()); + +server.on('secureConnection', (socket) => { + socket.write('hello\r\n'); + + socket.on('data', (data) => { + socket.write(data); + }); +}); + +let gotHello = false; +let sentWorld = false; +let gotWorld = false; + +server.listen(0, () => { + const client = spawn(common.opensslCli, [ + 's_client', + '-connect', `127.0.0.1:${server.address().port}`, + '-cipher', CIPHERS, + '-psk', KEY, + '-psk_identity', IDENTITY, + ]); + + let out = ''; + + client.stdout.setEncoding('utf8'); + client.stdout.on('data', (d) => { + out += d; + + if (!gotHello && /hello/.test(out)) { + gotHello = true; + client.stdin.write('world\r\n'); + sentWorld = true; + } + + if (!gotWorld && /world/.test(out)) { + gotWorld = true; + client.stdin.end(); + } + }); + + client.on('exit', common.mustCall((code) => { + assert.ok(gotHello); + assert.ok(sentWorld); + assert.ok(gotWorld); + assert.strictEqual(code, 0); + server.close(); + })); +}); diff --git a/test/js/node/test/parallel/test-tls-reuse-host-from-socket.js b/test/js/node/test/parallel/test-tls-reuse-host-from-socket.js new file mode 100644 index 0000000000..1a7705d911 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-reuse-host-from-socket.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const net = require('net'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}).listen(0, common.mustCall(() => { + const socket = net.connect(server.address().port, common.mustCall(() => { + const opts = { socket, rejectUnauthorized: false }; + const secureSocket = tls.connect(opts, common.mustCall(() => { + secureSocket.destroy(); + server.close(); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-secure-context-usage-order.js b/test/js/node/test/parallel/test-tls-secure-context-usage-order.js new file mode 100644 index 0000000000..c79a3eac77 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-secure-context-usage-order.js @@ -0,0 +1,99 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +// This test ensures that when a TLS connection is established, the server +// selects the most recently added SecureContext that matches the servername. + +const assert = require('assert'); +const tls = require('tls'); + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + +const serverOptions = { + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert'), + requestCert: true, + rejectUnauthorized: false, +}; + +const badSecureContext = { + key: loadPEM('agent1-key'), + cert: loadPEM('agent1-cert'), + ca: [ loadPEM('ca2-cert') ] +}; + +const goodSecureContext = { + key: loadPEM('agent1-key'), + cert: loadPEM('agent1-cert'), + ca: [ loadPEM('ca1-cert') ] +}; + +const server = tls.createServer(serverOptions, (c) => { + // The 'a' and 'b' subdomains are used to distinguish between client + // connections. + // Connection to subdomain 'a' is made when the 'bad' secure context is + // the only one in use. + if ('a.example.com' === c.servername) { + assert.strictEqual(c.authorized, false); + } + // Connection to subdomain 'b' is made after the 'good' context has been + // added. + if ('b.example.com' === c.servername) { + assert.strictEqual(c.authorized, true); + } +}); + +// 1. Add the 'bad' secure context. A connection using this context will not be +// authorized. +server.addContext('*.example.com', badSecureContext); + +server.listen(0, () => { + const options = { + port: server.address().port, + key: loadPEM('agent1-key'), + cert: loadPEM('agent1-cert'), + ca: [loadPEM('ca1-cert')], + servername: 'a.example.com', + rejectUnauthorized: false, + }; + + // 2. Make a connection using servername 'a.example.com'. Since a 'bad' + // secure context is used, this connection should not be authorized. + const client = tls.connect(options, () => { + client.end(); + }); + + client.on('close', common.mustCall(() => { + // 3. Add a 'good' secure context. + server.addContext('*.example.com', goodSecureContext); + + options.servername = 'b.example.com'; + // 4. Make a connection using servername 'b.example.com'. This connection + // should be authorized because the 'good' secure context is the most + // recently added matching context. + + const other = tls.connect(options, () => { + other.end(); + }); + + other.on('close', common.mustCall(() => { + // 5. Make another connection using servername 'b.example.com' to ensure + // that the array of secure contexts is not reversed in place with each + // SNICallback call, as someone might be tempted to refactor this piece of + // code by using Array.prototype.reverse() method. + const onemore = tls.connect(options, () => { + onemore.end(); + }); + + onemore.on('close', common.mustCall(() => { + server.close(); + })); + })); + })); +}); diff --git a/test/js/node/test/parallel/test-tls-securepair-server.js b/test/js/node/test/parallel/test-tls-securepair-server.js new file mode 100644 index 0000000000..78cd9f7254 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-securepair-server.js @@ -0,0 +1,145 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('missing openssl-cli'); + +const assert = require('assert'); +const tls = require('tls'); +const net = require('net'); +const spawn = require('child_process').spawn; +const fixtures = require('../common/fixtures'); + +const key = fixtures.readKey('rsa_private.pem'); +const cert = fixtures.readKey('rsa_cert.crt'); + +function log(a) { + console.error('***server***', a); +} + +const server = net.createServer(common.mustCall(function(socket) { + log(`connection fd=${socket.fd}`); + const sslcontext = tls.createSecureContext({ key, cert }); + sslcontext.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); + + const pair = tls.createSecurePair(sslcontext, true); + + assert.ok(pair.encrypted.writable); + assert.ok(pair.cleartext.writable); + + pair.encrypted.pipe(socket); + socket.pipe(pair.encrypted); + + log('i set it secure'); + + pair.on('secure', function() { + log('connected+secure!'); + pair.cleartext.write('hello\r\n'); + log(pair.cleartext.getPeerCertificate()); + log(pair.cleartext.getCipher()); + }); + + pair.cleartext.on('data', function(data) { + log(`read bytes ${data.length}`); + pair.cleartext.write(data); + }); + + socket.on('end', function() { + log('socket end'); + }); + + pair.cleartext.on('error', function(err) { + log('got error: '); + log(err); + socket.destroy(); + }); + + pair.encrypted.on('error', function(err) { + log('encrypted error: '); + log(err); + socket.destroy(); + }); + + socket.on('error', function(err) { + log('socket error: '); + log(err); + socket.destroy(); + }); + + socket.on('close', function(err) { + log('socket closed'); + }); + + pair.on('error', function(err) { + log('secure error: '); + log(err); + socket.destroy(); + }); +})); + +let gotHello = false; +let sentWorld = false; +let gotWorld = false; + +server.listen(0, common.mustCall(function() { + // To test use: openssl s_client -connect localhost:8000 + + const args = ['s_client', '-connect', `127.0.0.1:${this.address().port}`]; + + const client = spawn(common.opensslCli, args); + + + let out = ''; + + client.stdout.setEncoding('utf8'); + client.stdout.on('data', function(d) { + out += d; + + if (!gotHello && /hello/.test(out)) { + gotHello = true; + client.stdin.write('world\r\n'); + sentWorld = true; + } + + if (!gotWorld && /world/.test(out)) { + gotWorld = true; + client.stdin.end(); + } + }); + + client.stdout.pipe(process.stdout, { end: false }); + + client.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); + server.close(); + })); +})); + +process.on('exit', function() { + assert.ok(gotHello); + assert.ok(sentWorld); + assert.ok(gotWorld); +}); diff --git a/test/js/node/test/parallel/test-tls-server-connection-server.js b/test/js/node/test/parallel/test-tls-server-connection-server.js new file mode 100644 index 0000000000..7fb2c74996 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-server-connection-server.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = tls.createServer(options, function(s) { + s.end('hello'); +}).listen(0, function() { + const opts = { + port: this.address().port, + rejectUnauthorized: false + }; + + server.on('connection', common.mustCall(function(socket) { + assert.strictEqual(socket.server, server); + server.close(); + })); + + const client = tls.connect(opts, function() { + client.end(); + }); +}); diff --git a/test/js/node/test/parallel/test-tls-server-verify.js b/test/js/node/test/parallel/test-tls-server-verify.js new file mode 100644 index 0000000000..51ccd0d747 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-server-verify.js @@ -0,0 +1,348 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + +// This is a rather complex test which sets up various TLS servers with node +// and connects to them using the 'openssl s_client' command line utility +// with various keys. Depending on the certificate authority and other +// parameters given to the server, the various clients are +// - rejected, +// - accepted and "unauthorized", or +// - accepted and "authorized". + +const assert = require('assert'); +const { spawn } = require('child_process'); +const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = + require('crypto').constants; +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const testCases = + [{ title: 'Do not request certs. Everyone is unauthorized.', + requestCert: false, + rejectUnauthorized: false, + renegotiate: false, + CAs: ['ca1-cert'], + clients: + [{ name: 'agent1', shouldReject: false, shouldAuth: false }, + { name: 'agent2', shouldReject: false, shouldAuth: false }, + { name: 'agent3', shouldReject: false, shouldAuth: false }, + { name: 'nocert', shouldReject: false, shouldAuth: false }, + ] }, + + { title: 'Allow both authed and unauthed connections with CA1', + requestCert: true, + rejectUnauthorized: false, + renegotiate: false, + CAs: ['ca1-cert'], + clients: + [{ name: 'agent1', shouldReject: false, shouldAuth: true }, + { name: 'agent2', shouldReject: false, shouldAuth: false }, + { name: 'agent3', shouldReject: false, shouldAuth: false }, + { name: 'nocert', shouldReject: false, shouldAuth: false }, + ] }, + + { title: 'Do not request certs at connection. Do that later', + requestCert: false, + rejectUnauthorized: false, + renegotiate: true, + CAs: ['ca1-cert'], + clients: + [{ name: 'agent1', shouldReject: false, shouldAuth: true }, + { name: 'agent2', shouldReject: false, shouldAuth: false }, + { name: 'agent3', shouldReject: false, shouldAuth: false }, + { name: 'nocert', shouldReject: false, shouldAuth: false }, + ] }, + + { title: 'Allow only authed connections with CA1', + requestCert: true, + rejectUnauthorized: true, + renegotiate: false, + CAs: ['ca1-cert'], + clients: + [{ name: 'agent1', shouldReject: false, shouldAuth: true }, + { name: 'agent2', shouldReject: true }, + { name: 'agent3', shouldReject: true }, + { name: 'nocert', shouldReject: true }, + ] }, + + { title: 'Allow only authed connections with CA1 and CA2', + requestCert: true, + rejectUnauthorized: true, + renegotiate: false, + CAs: ['ca1-cert', 'ca2-cert'], + clients: + [{ name: 'agent1', shouldReject: false, shouldAuth: true }, + { name: 'agent2', shouldReject: true }, + { name: 'agent3', shouldReject: false, shouldAuth: true }, + { name: 'nocert', shouldReject: true }, + ] }, + + + { title: 'Allow only certs signed by CA2 but not in the CRL', + requestCert: true, + rejectUnauthorized: true, + renegotiate: false, + CAs: ['ca2-cert'], + crl: 'ca2-crl', + clients: [ + { name: 'agent1', shouldReject: true, shouldAuth: false }, + { name: 'agent2', shouldReject: true, shouldAuth: false }, + { name: 'agent3', shouldReject: false, shouldAuth: true }, + // Agent4 has a cert in the CRL. + { name: 'agent4', shouldReject: true, shouldAuth: false }, + { name: 'nocert', shouldReject: true }, + ] }, + ]; + +function filenamePEM(n) { + return fixtures.path('keys', `${n}.pem`); +} + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + + +const serverKey = loadPEM('agent2-key'); +const serverCert = loadPEM('agent2-cert'); + + +function runClient(prefix, port, options, cb) { + + // Client can connect in three ways: + // - Self-signed cert + // - Certificate, but not signed by CA. + // - Certificate signed by CA. + + const args = ['s_client', '-connect', `127.0.0.1:${port}`]; + + console.log(`${prefix} connecting with`, options.name); + + switch (options.name) { + case 'agent1': + // Signed by CA1 + args.push('-key'); + args.push(filenamePEM('agent1-key')); + args.push('-cert'); + args.push(filenamePEM('agent1-cert')); + break; + + case 'agent2': + // Self-signed + // This is also the key-cert pair that the server will use. + args.push('-key'); + args.push(filenamePEM('agent2-key')); + args.push('-cert'); + args.push(filenamePEM('agent2-cert')); + break; + + case 'agent3': + // Signed by CA2 + args.push('-key'); + args.push(filenamePEM('agent3-key')); + args.push('-cert'); + args.push(filenamePEM('agent3-cert')); + break; + + case 'agent4': + // Signed by CA2 (rejected by ca2-crl) + args.push('-key'); + args.push(filenamePEM('agent4-key')); + args.push('-cert'); + args.push(filenamePEM('agent4-cert')); + break; + + case 'nocert': + // Do not send certificate + break; + + default: + throw new Error(`${prefix}Unknown agent name`); + } + + // To test use: openssl s_client -connect localhost:8000 + const client = spawn(common.opensslCli, args); + + let out = ''; + + let rejected = true; + let authed = false; + let goodbye = false; + + client.stdout.setEncoding('utf8'); + client.stdout.on('data', function(d) { + out += d; + + if (!goodbye && /_unauthed/.test(out)) { + console.error(`${prefix} * unauthed`); + goodbye = true; + client.kill(); + authed = false; + rejected = false; + } + + if (!goodbye && /_authed/.test(out)) { + console.error(`${prefix} * authed`); + goodbye = true; + client.kill(); + authed = true; + rejected = false; + } + }); + + client.on('exit', function(code) { + if (options.shouldReject) { + assert.strictEqual( + rejected, true, + `${prefix}${options.name} NOT rejected, but should have been`); + } else { + assert.strictEqual( + rejected, false, + `${prefix}${options.name} rejected, but should NOT have been`); + assert.strictEqual( + authed, options.shouldAuth, + `${prefix}${options.name} authed is ${authed} but should have been ${ + options.shouldAuth}`); + } + + cb(); + }); +} + + +// Run the tests +let successfulTests = 0; +function runTest(port, testIndex) { + const prefix = `${testIndex} `; + const tcase = testCases[testIndex]; + if (!tcase) return; + + console.error(`${prefix}Running '${tcase.title}'`); + + const cas = tcase.CAs.map(loadPEM); + + const crl = tcase.crl ? loadPEM(tcase.crl) : null; + + const serverOptions = { + key: serverKey, + cert: serverCert, + ca: cas, + crl: crl, + requestCert: tcase.requestCert, + rejectUnauthorized: tcase.rejectUnauthorized + }; + + // If renegotiating - session might be resumed and openssl won't request + // client's certificate (probably because of bug in the openssl) + if (tcase.renegotiate) { + serverOptions.secureOptions = + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; + // Renegotiation as a protocol feature was dropped after TLS1.2. + serverOptions.maxVersion = 'TLSv1.2'; + } + + let renegotiated = false; + const server = tls.Server(serverOptions, function handleConnection(c) { + c.on('error', function(e) { + // child.kill() leads ECONNRESET error in the TLS connection of + // openssl s_client via spawn(). A test result is already + // checked by the data of client.stdout before child.kill() so + // these tls errors can be ignored. + }); + if (tcase.renegotiate && !renegotiated) { + renegotiated = true; + setTimeout(function() { + console.error(`${prefix}- connected, renegotiating`); + c.write('\n_renegotiating\n'); + return c.renegotiate({ + requestCert: true, + rejectUnauthorized: false + }, function(err) { + assert.ifError(err); + c.write('\n_renegotiated\n'); + handleConnection(c); + }); + }, 200); + return; + } + + if (c.authorized) { + console.error(`${prefix}- authed connection: ${ + c.getPeerCertificate().subject.CN}`); + c.write('\n_authed\n'); + } else { + console.error(`${prefix}- unauthed connection: %s`, c.authorizationError); + c.write('\n_unauthed\n'); + } + }); + + function runNextClient(clientIndex) { + const options = tcase.clients[clientIndex]; + if (options) { + runClient(`${prefix}${clientIndex} `, port, options, function() { + runNextClient(clientIndex + 1); + }); + } else { + server.close(); + successfulTests++; + runTest(0, nextTest++); + } + } + + server.listen(port, function() { + port = server.address().port; + if (tcase.debug) { + console.error(`${prefix}TLS server running on port ${port}`); + } else if (tcase.renegotiate) { + runNextClient(0); + } else { + let clientsCompleted = 0; + for (let i = 0; i < tcase.clients.length; i++) { + runClient(`${prefix}${i} `, port, tcase.clients[i], function() { + clientsCompleted++; + if (clientsCompleted === tcase.clients.length) { + server.close(); + successfulTests++; + runTest(0, nextTest++); + } + }); + } + } + }); +} + + +let nextTest = 0; +runTest(0, nextTest++); + + +process.on('exit', function() { + assert.strictEqual(successfulTests, testCases.length); +}); diff --git a/test/js/node/test/parallel/test-tls-session-cache.js b/test/js/node/test/parallel/test-tls-session-cache.js new file mode 100644 index 0000000000..e4ecb53282 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-session-cache.js @@ -0,0 +1,164 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const tls = require('tls'); +const { spawn } = require('child_process'); + +if (!common.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + + +doTest({ tickets: false }, function() { + doTest({ tickets: true }, function() { + doTest({ tickets: false, invalidSession: true }, function() { + console.error('all done'); + }); + }); +}); + +function doTest(testOptions, callback) { + const key = fixtures.readKey('rsa_private.pem'); + const cert = fixtures.readKey('rsa_cert.crt'); + const options = { + key, + cert, + ca: [cert], + requestCert: true, + rejectUnauthorized: false, + secureProtocol: 'TLS_method', + ciphers: 'RSA@SECLEVEL=0' + }; + let requestCount = 0; + let resumeCount = 0; + let newSessionCount = 0; + let session; + + const server = tls.createServer(options, function(cleartext) { + cleartext.on('error', function(er) { + // We're ok with getting ECONNRESET in this test, but it's + // timing-dependent, and thus unreliable. Any other errors + // are just failures, though. + if (er.code !== 'ECONNRESET') + throw er; + }); + ++requestCount; + cleartext.end(''); + }); + server.on('newSession', function(id, data, cb) { + ++newSessionCount; + // Emulate asynchronous store + setImmediate(() => { + assert.ok(!session); + session = { id, data }; + cb(); + }); + }); + server.on('resumeSession', function(id, callback) { + ++resumeCount; + assert.ok(session); + assert.strictEqual(session.id.toString('hex'), id.toString('hex')); + + let data = session.data; + + // Return an invalid session to test Node does not crash. + if (testOptions.invalidSession) { + data = Buffer.from('INVALID SESSION'); + session = null; + } + + // Just to check that async really works there + setImmediate(() => { + callback(null, data); + }); + }); + + server.listen(0, function() { + const args = [ + 's_client', + '-tls1', + '-cipher', (common.hasOpenSSL31 ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + '-connect', `localhost:${this.address().port}`, + '-servername', 'ohgod', + '-key', fixtures.path('keys/rsa_private.pem'), + '-cert', fixtures.path('keys/rsa_cert.crt'), + '-reconnect', + ].concat(testOptions.tickets ? [] : '-no_ticket'); + + function spawnClient() { + const client = spawn(common.opensslCli, args, { + stdio: [ 0, 1, 'pipe' ] + }); + let err = ''; + client.stderr.setEncoding('utf8'); + client.stderr.on('data', function(chunk) { + err += chunk; + }); + + client.on('exit', common.mustCall(function(code, signal) { + if (code !== 0) { + // If SmartOS and connection refused, then retry. See + // https://github.com/nodejs/node/issues/2663. + if (common.isSunOS && err.includes('Connection refused')) { + requestCount = 0; + spawnClient(); + return; + } + assert.fail(`code: ${code}, signal: ${signal}, output: ${err}`); + } + assert.strictEqual(code, 0); + server.close(common.mustCall(function() { + setImmediate(callback); + })); + })); + } + + spawnClient(); + }); + + process.on('exit', function() { + // Each test run connects 6 times: an initial request and 5 reconnect + // requests. + assert.strictEqual(requestCount, 6); + + if (testOptions.tickets) { + // No session cache callbacks are called. + assert.strictEqual(resumeCount, 0); + assert.strictEqual(newSessionCount, 0); + } else if (testOptions.invalidSession) { + // The resume callback was called, but each connection established a + // fresh session. + assert.strictEqual(resumeCount, 5); + assert.strictEqual(newSessionCount, 6); + } else { + // The resume callback was called, and only the initial connection + // establishes a fresh session. + assert.ok(session); + assert.strictEqual(resumeCount, 5); + assert.strictEqual(newSessionCount, 1); + } + }); +} diff --git a/test/js/node/test/parallel/test-tls-set-ciphers.js b/test/js/node/test/parallel/test-tls-set-ciphers.js new file mode 100644 index 0000000000..313c5e2389 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-set-ciphers.js @@ -0,0 +1,131 @@ +'use strict'; +const common = require('../common'); +if (!common.hasOpenSSL3) + common.skip('missing crypto, or OpenSSL version lower than 3'); + +const fixtures = require('../common/fixtures'); +const { inspect } = require('util'); + +// Test cipher: option for TLS. + +const { + assert, connect, keys +} = require(fixtures.path('tls-connect')); + + +function test(cciphers, sciphers, cipher, cerr, serr, options) { + assert(cipher || cerr || serr, 'test missing any expectations'); + const where = inspect(new Error()).split('\n')[2].replace(/[^(]*/, ''); + + const max_tls_ver = (ciphers, options) => { + if (options instanceof Object && Object.hasOwn(options, 'maxVersion')) + return options.maxVersion; + if ((typeof ciphers === 'string' || ciphers instanceof String) && ciphers.length > 0 && !ciphers.includes('TLS_')) + return 'TLSv1.2'; + + return 'TLSv1.3'; + }; + + connect({ + client: { + checkServerIdentity: (servername, cert) => { }, + ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, + ciphers: cciphers, + maxVersion: max_tls_ver(cciphers, options), + }, + server: { + cert: keys.agent6.cert, + key: keys.agent6.key, + ciphers: sciphers, + maxVersion: max_tls_ver(sciphers, options), + }, + }, common.mustCall((err, pair, cleanup) => { + function u(_) { return _ === undefined ? 'U' : _; } + console.log('test:', u(cciphers), u(sciphers), + 'expect', u(cipher), u(cerr), u(serr)); + console.log(' ', where); + if (!cipher) { + console.log('client', pair.client.err ? pair.client.err.code : undefined); + console.log('server', pair.server.err ? pair.server.err.code : undefined); + if (cerr) { + assert(pair.client.err); + assert.strictEqual(pair.client.err.code, cerr); + } + if (serr) { + assert(pair.server.err); + assert.strictEqual(pair.server.err.code, serr); + } + return cleanup(); + } + + const reply = 'So long and thanks for all the fish.'; + + assert.ifError(err); + assert.ifError(pair.server.err); + assert.ifError(pair.client.err); + assert(pair.server.conn); + assert(pair.client.conn); + assert.strictEqual(pair.client.conn.getCipher().name, cipher); + assert.strictEqual(pair.server.conn.getCipher().name, cipher); + + pair.server.conn.write(reply); + + pair.client.conn.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), reply); + return cleanup(); + })); + })); +} + +const U = undefined; + +// Have shared ciphers. +test(U, 'AES256-SHA', 'AES256-SHA'); +test('AES256-SHA', U, 'AES256-SHA'); + +test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); +test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); +test('TLS_AES_256_GCM_SHA384:!TLS_CHACHA20_POLY1305_SHA256', U, 'TLS_AES_256_GCM_SHA384'); + +// Do not have shared ciphers. +test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', 'ERR_SSL_NO_SHARED_CIPHER'); + +test('AES128-SHA', 'AES256-SHA', U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', + 'ERR_SSL_NO_SHARED_CIPHER'); +test('AES128-SHA:TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256:AES256-SHA', + U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', 'ERR_SSL_NO_SHARED_CIPHER'); + +// Cipher order ignored, TLS1.3 chosen before TLS1.2. +test('AES256-SHA:TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); +test(U, 'AES256-SHA:TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + +// Cipher order ignored, TLS1.3 before TLS1.2 and +// cipher suites are not disabled if TLS ciphers are set only +// TODO: maybe these tests should be reworked so maxVersion clamping +// is done explicitly and not implicitly in the test() function +test('AES256-SHA', U, 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); +test(U, 'AES256-SHA', 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); + +// TLS_AES_128_CCM_8_SHA256 & TLS_AES_128_CCM_SHA256 are not enabled by +// default, but work. +test('TLS_AES_128_CCM_8_SHA256', U, + U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', 'ERR_SSL_NO_SHARED_CIPHER'); + +test('TLS_AES_128_CCM_8_SHA256', 'TLS_AES_128_CCM_8_SHA256', + 'TLS_AES_128_CCM_8_SHA256'); + +// Invalid cipher values +test(9, 'AES256-SHA', U, 'ERR_INVALID_ARG_TYPE', U); +test('AES256-SHA', 9, U, U, 'ERR_INVALID_ARG_TYPE'); +test(':', 'AES256-SHA', U, 'ERR_INVALID_ARG_VALUE', U); +test('AES256-SHA', ':', U, U, 'ERR_INVALID_ARG_VALUE'); + +// Using '' is synonymous for "use default ciphers" +test('TLS_AES_256_GCM_SHA384', '', 'TLS_AES_256_GCM_SHA384'); +test('', 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + +// Using null should be treated the same as undefined. +test(null, 'AES256-SHA', 'AES256-SHA'); +test('AES256-SHA', null, 'AES256-SHA'); diff --git a/test/js/node/test/parallel/test-tls-startcom-wosign-whitelist.js b/test/js/node/test/parallel/test-tls-startcom-wosign-whitelist.js new file mode 100644 index 0000000000..56ffd73aac --- /dev/null +++ b/test/js/node/test/parallel/test-tls-startcom-wosign-whitelist.js @@ -0,0 +1,84 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +let finished = 0; + +function loadPEM(n) { + return fixtures.readKey(`${n}.pem`); +} + +const testCases = [ + { // agent8 is signed by fake-startcom-root with notBefore of + // Oct 20 23:59:59 2016 GMT. It passes StartCom/WoSign check. + serverOpts: { + key: loadPEM('agent8-key'), + cert: loadPEM('agent8-cert') + }, + clientOpts: { + ca: loadPEM('fake-startcom-root-cert'), + port: undefined, + rejectUnauthorized: true + }, + errorCode: 'CERT_REVOKED' + }, + { // agent9 is signed by fake-startcom-root with notBefore of + // Oct 21 00:00:01 2016 GMT. It fails StartCom/WoSign check. + serverOpts: { + key: loadPEM('agent9-key'), + cert: loadPEM('agent9-cert') + }, + clientOpts: { + ca: loadPEM('fake-startcom-root-cert'), + port: undefined, + rejectUnauthorized: true + }, + errorCode: 'CERT_REVOKED' + }, +]; + + +function runNextTest(server, tindex) { + server.close(function() { + finished++; + runTest(tindex + 1); + }); +} + + +function runTest(tindex) { + const tcase = testCases[tindex]; + + if (!tcase) return; + + const server = tls.createServer(tcase.serverOpts, function(s) { + s.resume(); + }).listen(0, function() { + tcase.clientOpts.port = this.address().port; + const client = tls.connect(tcase.clientOpts); + client.on('error', function(e) { + assert.strictEqual(e.code, tcase.errorCode); + runNextTest(server, tindex); + }); + + client.on('secureConnect', function() { + // agent8 can pass StartCom/WoSign check so that the secureConnect + // is established. + assert.strictEqual(tcase.errorCode, 'CERT_REVOKED'); + client.end(); + runNextTest(server, tindex); + }); + }); +} + + +runTest(0); + +process.on('exit', function() { + assert.strictEqual(finished, testCases.length); +}); diff --git a/test/js/node/test/parallel/test-tls-tlswrap-segfault-2.js b/test/js/node/test/parallel/test-tls-tlswrap-segfault-2.js new file mode 100644 index 0000000000..d4f3b12b99 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-tlswrap-segfault-2.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// This test ensures that Node.js doesn't incur a segfault while +// adding session or keylog listeners after destroy. +// https://github.com/nodejs/node/issues/38133 +// https://github.com/nodejs/node/issues/38135 + +const tls = require('tls'); +const tlsSocketKeyLog = tls.connect('cause-error'); +tlsSocketKeyLog.on('error', common.mustCall()); +tlsSocketKeyLog.on('close', common.mustCall(() => { + tlsSocketKeyLog.on('keylog', common.mustNotCall()); +})); + +const tlsSocketSession = tls.connect('cause-error-2'); +tlsSocketSession.on('error', common.mustCall()); +tlsSocketSession.on('close', common.mustCall(() => { + tlsSocketSession.on('session', common.mustNotCall()); +})); diff --git a/test/js/node/test/parallel/test-tls-tlswrap-segfault.js b/test/js/node/test/parallel/test-tls-tlswrap-segfault.js new file mode 100644 index 0000000000..a36016efa4 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-tlswrap-segfault.js @@ -0,0 +1,42 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); + +// This test ensures that Node.js doesn't incur a segfault while accessing +// TLSWrap fields after the parent handle was destroyed. +// https://github.com/nodejs/node/issues/5108 + +const assert = require('assert'); +const tls = require('tls'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = tls.createServer(options, function(s) { + s.end('hello'); +}).listen(0, function() { + const opts = { + port: this.address().port, + rejectUnauthorized: false + }; + const client = tls.connect(opts, function() { + putImmediate(client); + }); + client.resume(); +}); + +function putImmediate(client) { + setImmediate(function() { + if (client.ssl) { + const fd = client.ssl.fd; + assert(!!fd); + putImmediate(client); + } else { + server.close(); + } + }); +} diff --git a/test/js/node/test/parallel/test-tls-zero-clear-in.js b/test/js/node/test/parallel/test-tls-zero-clear-in.js new file mode 100644 index 0000000000..f24fb6f992 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-zero-clear-in.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const cert = fixtures.readKey('rsa_cert.crt'); +const key = fixtures.readKey('rsa_private.pem'); + +const server = tls.createServer({ + cert, + key +}, function(c) { + // Nop + setTimeout(function() { + c.end(); + server.close(); + }, 20); +}).listen(0, common.mustCall(function() { + const conn = tls.connect({ + cert: cert, + key: key, + rejectUnauthorized: false, + port: this.address().port + }, function() { + setTimeout(function() { + conn.destroy(); + }, 20); + }); + + // SSL_write() call's return value, when called 0 bytes, should not be + // treated as error. + conn.end(''); + + conn.on('error', common.mustNotCall()); +})); diff --git a/test/js/node/test/parallel/test-tty-stdin-end.js b/test/js/node/test/parallel/test-tty-stdin-end.js new file mode 100644 index 0000000000..c78f58446d --- /dev/null +++ b/test/js/node/test/parallel/test-tty-stdin-end.js @@ -0,0 +1,7 @@ +'use strict'; +require('../common'); + +// This test ensures that Node.js doesn't crash on `process.stdin.emit("end")`. +// https://github.com/nodejs/node/issues/1068 + +process.stdin.emit('end'); diff --git a/test/js/node/test/parallel/test-tz-version.js b/test/js/node/test/parallel/test-tz-version.js new file mode 100644 index 0000000000..6e4b14e1ac --- /dev/null +++ b/test/js/node/test/parallel/test-tz-version.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasIntl) { + common.skip('missing Intl'); +} + +// Refs: https://github.com/nodejs/node/blob/1af63a90ca3a59ca05b3a12ad7dbea04008db7d9/configure.py#L1694-L1711 +if (process.config.variables.icu_path !== 'deps/icu-small') { + // If Node.js is configured to use its built-in ICU, it uses a strict subset + // of ICU formed using `tools/icu/shrink-icu-src.py`, which is present in + // `deps/icu-small`. It is not the same as configuring the build with + // `./configure --with-intl=small-icu`. The latter only uses a subset of the + // locales, i.e., it uses the English locale, `root,en`, by default and other + // locales can also be specified using the `--with-icu-locales` option. + common.skip('not using the icu data file present in deps/icu-small/source/data/in/icudt##l.dat.bz2'); +} + +const fixtures = require('../common/fixtures'); + +// This test ensures the correctness of the automated timezone upgrade PRs. + +const { strictEqual } = require('assert'); +const { readFileSync } = require('fs'); + +const expectedVersion = readFileSync(fixtures.path('tz-version.txt'), 'utf8').trim(); +strictEqual(process.versions.tz, expectedVersion); diff --git a/test/js/node/test/parallel/test-unhandled-exception-with-worker-inuse.js b/test/js/node/test/parallel/test-unhandled-exception-with-worker-inuse.js new file mode 100644 index 0000000000..a3e823ca70 --- /dev/null +++ b/test/js/node/test/parallel/test-unhandled-exception-with-worker-inuse.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); + +// https://github.com/nodejs/node/issues/45421 +// +// Check that node will NOT call v8::Isolate::SetIdle() when exiting +// due to an unhandled exception, otherwise the assertion(enabled in +// debug build only) in the SetIdle(), which checks that the vm state +// is either EXTERNAL or IDLE will fail. +// +// The root cause of this issue is that before PerIsolateMessageListener() +// is invoked by v8, v8 preserves the JS vm state, although it should +// switch to EXTERNEL. https://bugs.chromium.org/p/v8/issues/detail?id=13464 +// +// Therefore, this commit can be considered as an workaround of the v8 bug, +// but we also find it not useful to call SetIdle() when terminating. + +if (process.argv[2] === 'child') { + const { Worker } = require('worker_threads'); + new Worker('', { eval: true }); + throw new Error('xxx'); +} else { + const assert = require('assert'); + const { spawnSync } = require('child_process'); + const result = spawnSync(process.execPath, [__filename, 'child']); + + const stderr = result.stderr.toString().trim(); + // Expect error message to be preserved + assert.match(stderr, /xxx/); + // Expect no crash + assert(!common.nodeProcessAborted(result.status, result.signal), stderr); +} diff --git a/test/js/node/test/parallel/test-url-canParse-whatwg.js b/test/js/node/test/parallel/test-url-canParse-whatwg.js new file mode 100644 index 0000000000..d5ffee7053 --- /dev/null +++ b/test/js/node/test/parallel/test-url-canParse-whatwg.js @@ -0,0 +1,19 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// One argument is required +assert.throws(() => { + URL.canParse(); +}, { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', +}); + +{ + // This test is to ensure that the v8 fast api works. + for (let i = 0; i < 1e5; i++) { + assert(URL.canParse('https://www.example.com/path/?query=param#hash')); + } +} diff --git a/test/js/node/test/parallel/test-url-domain-ascii-unicode.js b/test/js/node/test/parallel/test-url-domain-ascii-unicode.js new file mode 100644 index 0000000000..737294c241 --- /dev/null +++ b/test/js/node/test/parallel/test-url-domain-ascii-unicode.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIntl) + common.skip('missing Intl'); + +const strictEqual = require('assert').strictEqual; +const url = require('url'); + +const domainToASCII = url.domainToASCII; +const domainToUnicode = url.domainToUnicode; + +const domainWithASCII = [ + ['ıíd', 'xn--d-iga7r'], + ['يٴ', 'xn--mhb8f'], + ['www.ϧƽəʐ.com', 'www.xn--cja62apfr6c.com'], + ['новини.com', 'xn--b1amarcd.com'], + ['名がドメイン.com', 'xn--v8jxj3d1dzdz08w.com'], + ['افغانستا.icom.museum', 'xn--mgbaal8b0b9b2b.icom.museum'], + ['الجزائر.icom.fake', 'xn--lgbbat1ad8j.icom.fake'], + ['भारत.org', 'xn--h2brj9c.org'], +]; + +domainWithASCII.forEach((pair) => { + const domain = pair[0]; + const ascii = pair[1]; + const domainConvertedToASCII = domainToASCII(domain); + strictEqual(domainConvertedToASCII, ascii); + const asciiConvertedToUnicode = domainToUnicode(ascii); + strictEqual(asciiConvertedToUnicode, domain); +}); diff --git a/test/js/node/test/parallel/test-url-format-whatwg.js b/test/js/node/test/parallel/test-url-format-whatwg.js new file mode 100644 index 0000000000..bf9f8eaac6 --- /dev/null +++ b/test/js/node/test/parallel/test-url-format-whatwg.js @@ -0,0 +1,147 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIntl) + common.skip('missing Intl'); + +const assert = require('assert'); +const url = require('url'); + +const myURL = new URL('http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c'); + +assert.strictEqual( + url.format(myURL), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {}), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +{ + [true, 1, 'test', Infinity].forEach((value) => { + assert.throws( + () => url.format(myURL, value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + common.invalidArgTypeHelper(value) + } + ); + }); +} + +// Any falsy value other than undefined will be treated as false. +// Any truthy value will be treated as true. + +assert.strictEqual( + url.format(myURL, { auth: false }), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { auth: '' }), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { auth: 0 }), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { auth: 1 }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { auth: {} }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { fragment: false }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b' +); + +assert.strictEqual( + url.format(myURL, { fragment: '' }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b' +); + +assert.strictEqual( + url.format(myURL, { fragment: 0 }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b' +); + +assert.strictEqual( + url.format(myURL, { fragment: 1 }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { fragment: {} }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { search: false }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a#c' +); + +assert.strictEqual( + url.format(myURL, { search: '' }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a#c' +); + +assert.strictEqual( + url.format(myURL, { search: 0 }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a#c' +); + +assert.strictEqual( + url.format(myURL, { search: 1 }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { search: {} }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { unicode: true }), + 'http://user:pass@理容ナカムラ.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { unicode: 1 }), + 'http://user:pass@理容ナカムラ.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { unicode: {} }), + 'http://user:pass@理容ナカムラ.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { unicode: false }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, { unicode: 0 }), + 'http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(new URL('http://user:pass@xn--0zwm56d.com:8080/path'), { unicode: true }), + 'http://user:pass@测试.com:8080/path' +); + +assert.strictEqual( + url.format(new URL('tel:123')), + url.format(new URL('tel:123'), { unicode: true }) +); diff --git a/test/js/node/test/parallel/test-url-format.js b/test/js/node/test/parallel/test-url-format.js new file mode 100644 index 0000000000..883d060ac2 --- /dev/null +++ b/test/js/node/test/parallel/test-url-format.js @@ -0,0 +1,277 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const url = require('url'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +// Formatting tests to verify that it'll format slightly wonky content to a +// valid URL. +const formatTests = { + 'http://example.com?': { + href: 'http://example.com/?', + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + search: '?', + query: {}, + pathname: '/' + }, + 'http://example.com?foo=bar#frag': { + href: 'http://example.com/?foo=bar#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=bar', + query: 'foo=bar', + pathname: '/' + }, + 'http://example.com?foo=@bar#frag': { + href: 'http://example.com/?foo=@bar#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=@bar', + query: 'foo=@bar', + pathname: '/' + }, + 'http://example.com?foo=/bar/#frag': { + href: 'http://example.com/?foo=/bar/#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=/bar/', + query: 'foo=/bar/', + pathname: '/' + }, + 'http://example.com?foo=?bar/#frag': { + href: 'http://example.com/?foo=?bar/#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=?bar/', + query: 'foo=?bar/', + pathname: '/' + }, + 'http://example.com#frag=?bar/#frag': { + href: 'http://example.com/#frag=?bar/#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag=?bar/#frag', + pathname: '/' + }, + 'http://google.com" onload="alert(42)/': { + href: 'http://google.com/%22%20onload=%22alert(42)/', + protocol: 'http:', + host: 'google.com', + pathname: '/%22%20onload=%22alert(42)/' + }, + 'http://a.com/a/b/c?s#h': { + href: 'http://a.com/a/b/c?s#h', + protocol: 'http', + host: 'a.com', + pathname: 'a/b/c', + hash: 'h', + search: 's' + }, + 'xmpp:isaacschlueter@jabber.org': { + href: 'xmpp:isaacschlueter@jabber.org', + protocol: 'xmpp:', + host: 'jabber.org', + auth: 'isaacschlueter', + hostname: 'jabber.org' + }, + 'http://atpass:foo%40bar@127.0.0.1/': { + href: 'http://atpass:foo%40bar@127.0.0.1/', + auth: 'atpass:foo@bar', + hostname: '127.0.0.1', + protocol: 'http:', + pathname: '/' + }, + 'http://atslash%2F%40:%2F%40@foo/': { + href: 'http://atslash%2F%40:%2F%40@foo/', + auth: 'atslash/@:/@', + hostname: 'foo', + protocol: 'http:', + pathname: '/' + }, + 'svn+ssh://foo/bar': { + href: 'svn+ssh://foo/bar', + hostname: 'foo', + protocol: 'svn+ssh:', + pathname: '/bar', + slashes: true + }, + 'dash-test://foo/bar': { + href: 'dash-test://foo/bar', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar', + slashes: true + }, + 'dash-test:foo/bar': { + href: 'dash-test:foo/bar', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar' + }, + 'dot.test://foo/bar': { + href: 'dot.test://foo/bar', + hostname: 'foo', + protocol: 'dot.test:', + pathname: '/bar', + slashes: true + }, + 'dot.test:foo/bar': { + href: 'dot.test:foo/bar', + hostname: 'foo', + protocol: 'dot.test:', + pathname: '/bar' + }, + // IPv6 support + 'coap:u:p@[::1]:61616/.well-known/r?n=Temperature': { + href: 'coap:u:p@[::1]:61616/.well-known/r?n=Temperature', + protocol: 'coap:', + auth: 'u:p', + hostname: '::1', + port: '61616', + pathname: '/.well-known/r', + search: 'n=Temperature' + }, + 'coap:[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616/s/stopButton': { + href: 'coap:[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616/s/stopButton', + protocol: 'coap', + host: '[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616', + pathname: '/s/stopButton' + }, + 'http://[::]/': { + href: 'http://[::]/', + protocol: 'http:', + hostname: '[::]', + pathname: '/' + }, + + // Encode context-specific delimiters in path and query, but do not touch + // other non-delimiter chars like `%`. + // + + // `#`,`?` in path + '/path/to/%%23%3F+=&.txt?foo=theA1#bar': { + href: '/path/to/%%23%3F+=&.txt?foo=theA1#bar', + pathname: '/path/to/%#?+=&.txt', + query: { + foo: 'theA1' + }, + hash: '#bar' + }, + + // `#`,`?` in path + `#` in query + '/path/to/%%23%3F+=&.txt?foo=the%231#bar': { + href: '/path/to/%%23%3F+=&.txt?foo=the%231#bar', + pathname: '/path/to/%#?+=&.txt', + query: { + foo: 'the#1' + }, + hash: '#bar' + }, + + // `#` in path end + `#` in query + '/path/to/%%23?foo=the%231#bar': { + href: '/path/to/%%23?foo=the%231#bar', + pathname: '/path/to/%#', + query: { + foo: 'the#1' + }, + hash: '#bar' + }, + + // `?` and `#` in path and search + 'http://ex.com/foo%3F100%m%23r?abc=the%231?&foo=bar#frag': { + href: 'http://ex.com/foo%3F100%m%23r?abc=the%231?&foo=bar#frag', + protocol: 'http:', + hostname: 'ex.com', + hash: '#frag', + search: '?abc=the#1?&foo=bar', + pathname: '/foo?100%m#r', + }, + + // `?` and `#` in search only + 'http://ex.com/fooA100%mBr?abc=the%231?&foo=bar#frag': { + href: 'http://ex.com/fooA100%mBr?abc=the%231?&foo=bar#frag', + protocol: 'http:', + hostname: 'ex.com', + hash: '#frag', + search: '?abc=the#1?&foo=bar', + pathname: '/fooA100%mBr', + }, + + // Multiple `#` in search + 'http://example.com/?foo=bar%231%232%233&abc=%234%23%235#frag': { + href: 'http://example.com/?foo=bar%231%232%233&abc=%234%23%235#frag', + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=bar#1#2#3&abc=#4##5', + query: {}, + pathname: '/' + }, + + // More than 255 characters in hostname which exceeds the limit + [`http://${'a'.repeat(255)}.com/node`]: { + href: 'http:///node', + protocol: 'http:', + slashes: true, + host: '', + hostname: '', + pathname: '/node', + path: '/node' + }, + + // Greater than or equal to 63 characters after `.` in hostname + [`http://www.${'z'.repeat(63)}example.com/node`]: { + href: `http://www.${'z'.repeat(63)}example.com/node`, + protocol: 'http:', + slashes: true, + host: `www.${'z'.repeat(63)}example.com`, + hostname: `www.${'z'.repeat(63)}example.com`, + pathname: '/node', + path: '/node' + }, + + // https://github.com/nodejs/node/issues/3361 + 'file:///home/user': { + href: 'file:///home/user', + protocol: 'file', + pathname: '/home/user', + path: '/home/user' + }, + + // surrogate in auth + 'http://%F0%9F%98%80@www.example.com/': { + href: 'http://%F0%9F%98%80@www.example.com/', + protocol: 'http:', + auth: '\uD83D\uDE00', + hostname: 'www.example.com', + pathname: '/' + } +}; +for (const u in formatTests) { + const expect = formatTests[u].href; + delete formatTests[u].href; + const actual = url.format(u); + const actualObj = url.format(formatTests[u]); + assert.strictEqual(actual, expect, + `wonky format(${u}) == ${expect}\nactual:${actual}`); + assert.strictEqual(actualObj, expect, + `wonky format(${JSON.stringify(formatTests[u])}) == ${ + expect}\nactual: ${actualObj}`); +} diff --git a/test/js/node/test/parallel/test-url-is-url-internal.js b/test/js/node/test/parallel/test-url-is-url-internal.js new file mode 100644 index 0000000000..0c05975fd7 --- /dev/null +++ b/test/js/node/test/parallel/test-url-is-url-internal.js @@ -0,0 +1,24 @@ +/* + * NOTE (@DonIsaac) this file tests node internals, which Bun does not match. + * We aim for API compatability, but make no guarantees about internals. + */ + +// // Flags: --expose-internals +// 'use strict'; + +// require('../common'); + +// const { URL, parse } = require('node:url'); +// const assert = require('node:assert'); +// const { isURL } = require('internal/url'); +// const { test } = require('node:test'); + +// test('isURL', () => { +// assert.strictEqual(isURL(new URL('https://www.nodejs.org')), true); +// assert.strictEqual(isURL(parse('https://www.nodejs.org')), false); +// assert.strictEqual(isURL({ +// href: 'https://www.nodejs.org', +// protocol: 'https:', +// path: '/', +// }), false); +// }); diff --git a/test/js/node/test/parallel/test-url-parse-format.js b/test/js/node/test/parallel/test-url-parse-format.js new file mode 100644 index 0000000000..f8761514a3 --- /dev/null +++ b/test/js/node/test/parallel/test-url-parse-format.js @@ -0,0 +1,1077 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +const assert = require('assert'); +const inspect = require('util').inspect; + +const url = require('url'); + +// URLs to parse, and expected data +// { url : parsed } +const parseTests = { + '//some_path': { + href: '//some_path', + pathname: '//some_path', + path: '//some_path' + }, + + 'http:\\\\evil-phisher\\foo.html#h\\a\\s\\h': { + protocol: 'http:', + slashes: true, + host: 'evil-phisher', + hostname: 'evil-phisher', + pathname: '/foo.html', + path: '/foo.html', + hash: '#h%5Ca%5Cs%5Ch', + href: 'http://evil-phisher/foo.html#h%5Ca%5Cs%5Ch' + }, + + 'http:\\\\evil-phisher\\foo.html?json="\\"foo\\""#h\\a\\s\\h': { + protocol: 'http:', + slashes: true, + host: 'evil-phisher', + hostname: 'evil-phisher', + pathname: '/foo.html', + search: '?json=%22%5C%22foo%5C%22%22', + query: 'json=%22%5C%22foo%5C%22%22', + path: '/foo.html?json=%22%5C%22foo%5C%22%22', + hash: '#h%5Ca%5Cs%5Ch', + href: 'http://evil-phisher/foo.html?json=%22%5C%22foo%5C%22%22#h%5Ca%5Cs%5Ch' + }, + + 'http:\\\\evil-phisher\\foo.html#h\\a\\s\\h?blarg': { + protocol: 'http:', + slashes: true, + host: 'evil-phisher', + hostname: 'evil-phisher', + pathname: '/foo.html', + path: '/foo.html', + hash: '#h%5Ca%5Cs%5Ch?blarg', + href: 'http://evil-phisher/foo.html#h%5Ca%5Cs%5Ch?blarg' + }, + + + 'http:\\\\evil-phisher\\foo.html': { + protocol: 'http:', + slashes: true, + host: 'evil-phisher', + hostname: 'evil-phisher', + pathname: '/foo.html', + path: '/foo.html', + href: 'http://evil-phisher/foo.html' + }, + + 'HTTP://www.example.com/': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'HTTP://www.example.com': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://www.ExAmPlE.com/': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://user:pw@www.ExAmPlE.com/': { + href: 'http://user:pw@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'user:pw', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://USER:PW@www.ExAmPlE.com/': { + href: 'http://USER:PW@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'USER:PW', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://user@www.example.com/': { + href: 'http://user@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'user', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://user%3Apw@www.example.com/': { + href: 'http://user:pw@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'user:pw', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://x.com/path?that\'s#all, folks': { + href: 'http://x.com/path?that%27s#all,%20folks', + protocol: 'http:', + slashes: true, + host: 'x.com', + hostname: 'x.com', + search: '?that%27s', + query: 'that%27s', + pathname: '/path', + hash: '#all,%20folks', + path: '/path?that%27s' + }, + + 'HTTP://X.COM/Y': { + href: 'http://x.com/Y', + protocol: 'http:', + slashes: true, + host: 'x.com', + hostname: 'x.com', + pathname: '/Y', + path: '/Y' + }, + + // Whitespace in the front + ' http://www.example.com/': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + // + not an invalid host character + // per https://url.spec.whatwg.org/#host-parsing + 'http://x.y.com+a/b/c': { + href: 'http://x.y.com+a/b/c', + protocol: 'http:', + slashes: true, + host: 'x.y.com+a', + hostname: 'x.y.com+a', + pathname: '/b/c', + path: '/b/c' + }, + + // An unexpected invalid char in the hostname. + 'HtTp://x.y.cOm;a/b/c?d=e#f gi': { + href: 'http://x.y.com/;a/b/c?d=e#f%20g%3Ch%3Ei', + protocol: 'http:', + slashes: true, + host: 'x.y.com', + hostname: 'x.y.com', + pathname: ';a/b/c', + search: '?d=e', + query: 'd=e', + hash: '#f%20g%3Ch%3Ei', + path: ';a/b/c?d=e' + }, + + // Make sure that we don't accidentally lcast the path parts. + 'HtTp://x.y.cOm;A/b/c?d=e#f gi': { + href: 'http://x.y.com/;A/b/c?d=e#f%20g%3Ch%3Ei', + protocol: 'http:', + slashes: true, + host: 'x.y.com', + hostname: 'x.y.com', + pathname: ';A/b/c', + search: '?d=e', + query: 'd=e', + hash: '#f%20g%3Ch%3Ei', + path: ';A/b/c?d=e' + }, + + 'http://x...y...#p': { + href: 'http://x...y.../#p', + protocol: 'http:', + slashes: true, + host: 'x...y...', + hostname: 'x...y...', + hash: '#p', + pathname: '/', + path: '/' + }, + + 'http://x/p/"quoted"': { + href: 'http://x/p/%22quoted%22', + protocol: 'http:', + slashes: true, + host: 'x', + hostname: 'x', + pathname: '/p/%22quoted%22', + path: '/p/%22quoted%22' + }, + + ' Is a URL!': { + href: '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!', + pathname: '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!', + path: '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!' + }, + + 'http://www.narwhaljs.org/blog/categories?id=news': { + href: 'http://www.narwhaljs.org/blog/categories?id=news', + protocol: 'http:', + slashes: true, + host: 'www.narwhaljs.org', + hostname: 'www.narwhaljs.org', + search: '?id=news', + query: 'id=news', + pathname: '/blog/categories', + path: '/blog/categories?id=news' + }, + + 'http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=': { + href: 'http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=', + protocol: 'http:', + slashes: true, + host: 'mt0.google.com', + hostname: 'mt0.google.com', + pathname: '/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=', + path: '/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=' + }, + + 'http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=': { + href: 'http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api' + + '&x=2&y=2&z=3&s=', + protocol: 'http:', + slashes: true, + host: 'mt0.google.com', + hostname: 'mt0.google.com', + search: '???&hl=en&src=api&x=2&y=2&z=3&s=', + query: '??&hl=en&src=api&x=2&y=2&z=3&s=', + pathname: '/vt/lyrs=m@114', + path: '/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=' + }, + + 'http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=': { + href: 'http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=', + protocol: 'http:', + slashes: true, + host: 'mt0.google.com', + auth: 'user:pass', + hostname: 'mt0.google.com', + search: '???&hl=en&src=api&x=2&y=2&z=3&s=', + query: '??&hl=en&src=api&x=2&y=2&z=3&s=', + pathname: '/vt/lyrs=m@114', + path: '/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=' + }, + + 'file:///etc/passwd': { + href: 'file:///etc/passwd', + slashes: true, + protocol: 'file:', + pathname: '/etc/passwd', + hostname: '', + host: '', + path: '/etc/passwd' + }, + + 'file://localhost/etc/passwd': { + href: 'file://localhost/etc/passwd', + protocol: 'file:', + slashes: true, + pathname: '/etc/passwd', + hostname: 'localhost', + host: 'localhost', + path: '/etc/passwd' + }, + + 'file://foo/etc/passwd': { + href: 'file://foo/etc/passwd', + protocol: 'file:', + slashes: true, + pathname: '/etc/passwd', + hostname: 'foo', + host: 'foo', + path: '/etc/passwd' + }, + + 'file:///etc/node/': { + href: 'file:///etc/node/', + slashes: true, + protocol: 'file:', + pathname: '/etc/node/', + hostname: '', + host: '', + path: '/etc/node/' + }, + + 'file://localhost/etc/node/': { + href: 'file://localhost/etc/node/', + protocol: 'file:', + slashes: true, + pathname: '/etc/node/', + hostname: 'localhost', + host: 'localhost', + path: '/etc/node/' + }, + + 'file://foo/etc/node/': { + href: 'file://foo/etc/node/', + protocol: 'file:', + slashes: true, + pathname: '/etc/node/', + hostname: 'foo', + host: 'foo', + path: '/etc/node/' + }, + + 'http:/baz/../foo/bar': { + href: 'http:/baz/../foo/bar', + protocol: 'http:', + pathname: '/baz/../foo/bar', + path: '/baz/../foo/bar' + }, + + 'http://user:pass@example.com:8000/foo/bar?baz=quux#frag': { + href: 'http://user:pass@example.com:8000/foo/bar?baz=quux#frag', + protocol: 'http:', + slashes: true, + host: 'example.com:8000', + auth: 'user:pass', + port: '8000', + hostname: 'example.com', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + '//user:pass@example.com:8000/foo/bar?baz=quux#frag': { + href: '//user:pass@example.com:8000/foo/bar?baz=quux#frag', + slashes: true, + host: 'example.com:8000', + auth: 'user:pass', + port: '8000', + hostname: 'example.com', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + '/foo/bar?baz=quux#frag': { + href: '/foo/bar?baz=quux#frag', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + 'http:/foo/bar?baz=quux#frag': { + href: 'http:/foo/bar?baz=quux#frag', + protocol: 'http:', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + 'mailto:foo@bar.com?subject=hello': { + href: 'mailto:foo@bar.com?subject=hello', + protocol: 'mailto:', + host: 'bar.com', + auth: 'foo', + hostname: 'bar.com', + search: '?subject=hello', + query: 'subject=hello', + path: '?subject=hello' + }, + + 'javascript:alert(\'hello\');': { + href: 'javascript:alert(\'hello\');', + protocol: 'javascript:', + pathname: 'alert(\'hello\');', + path: 'alert(\'hello\');' + }, + + 'xmpp:isaacschlueter@jabber.org': { + href: 'xmpp:isaacschlueter@jabber.org', + protocol: 'xmpp:', + host: 'jabber.org', + auth: 'isaacschlueter', + hostname: 'jabber.org' + }, + + 'http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar': { + href: 'http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar', + protocol: 'http:', + slashes: true, + host: '127.0.0.1:8080', + auth: 'atpass:foo@bar', + hostname: '127.0.0.1', + port: '8080', + pathname: '/path', + search: '?search=foo', + query: 'search=foo', + hash: '#bar', + path: '/path?search=foo' + }, + + 'svn+ssh://foo/bar': { + href: 'svn+ssh://foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'svn+ssh:', + pathname: '/bar', + path: '/bar', + slashes: true + }, + + 'dash-test://foo/bar': { + href: 'dash-test://foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar', + path: '/bar', + slashes: true + }, + + 'dash-test:foo/bar': { + href: 'dash-test:foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar', + path: '/bar' + }, + + 'dot.test://foo/bar': { + href: 'dot.test://foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dot.test:', + pathname: '/bar', + path: '/bar', + slashes: true + }, + + 'dot.test:foo/bar': { + href: 'dot.test:foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dot.test:', + pathname: '/bar', + path: '/bar' + }, + + // IDNA tests + 'http://www.日本語.com/': { + href: 'http://www.xn--wgv71a119e.com/', + protocol: 'http:', + slashes: true, + host: 'www.xn--wgv71a119e.com', + hostname: 'www.xn--wgv71a119e.com', + pathname: '/', + path: '/' + }, + + 'http://example.Bücher.com/': { + href: 'http://example.xn--bcher-kva.com/', + protocol: 'http:', + slashes: true, + host: 'example.xn--bcher-kva.com', + hostname: 'example.xn--bcher-kva.com', + pathname: '/', + path: '/' + }, + + 'http://www.Äffchen.com/': { + href: 'http://www.xn--ffchen-9ta.com/', + protocol: 'http:', + slashes: true, + host: 'www.xn--ffchen-9ta.com', + hostname: 'www.xn--ffchen-9ta.com', + pathname: '/', + path: '/' + }, + + 'http://www.Äffchen.cOm;A/b/c?d=e#f gi': { + href: 'http://www.xn--ffchen-9ta.com/;A/b/c?d=e#f%20g%3Ch%3Ei', + protocol: 'http:', + slashes: true, + host: 'www.xn--ffchen-9ta.com', + hostname: 'www.xn--ffchen-9ta.com', + pathname: ';A/b/c', + search: '?d=e', + query: 'd=e', + hash: '#f%20g%3Ch%3Ei', + path: ';A/b/c?d=e' + }, + + 'http://SÉLIER.COM/': { + href: 'http://xn--slier-bsa.com/', + protocol: 'http:', + slashes: true, + host: 'xn--slier-bsa.com', + hostname: 'xn--slier-bsa.com', + pathname: '/', + path: '/' + }, + + 'http://ليهمابتكلموشعربي؟.ي؟/': { + href: 'http://xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f/', + protocol: 'http:', + slashes: true, + host: 'xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f', + hostname: 'xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f', + pathname: '/', + path: '/' + }, + + 'http://➡.ws/➡': { + href: 'http://xn--hgi.ws/➡', + protocol: 'http:', + slashes: true, + host: 'xn--hgi.ws', + hostname: 'xn--hgi.ws', + pathname: '/➡', + path: '/➡' + }, + + 'http://bucket_name.s3.amazonaws.com/image.jpg': { + protocol: 'http:', + slashes: true, + host: 'bucket_name.s3.amazonaws.com', + hostname: 'bucket_name.s3.amazonaws.com', + pathname: '/image.jpg', + href: 'http://bucket_name.s3.amazonaws.com/image.jpg', + path: '/image.jpg' + }, + + 'git+http://github.com/joyent/node.git': { + protocol: 'git+http:', + slashes: true, + host: 'github.com', + hostname: 'github.com', + pathname: '/joyent/node.git', + path: '/joyent/node.git', + href: 'git+http://github.com/joyent/node.git' + }, + + // If local1@domain1 is uses as a relative URL it may + // be parse into auth@hostname, but here there is no + // way to make it work in url.parse, I add the test to be explicit + 'local1@domain1': { + pathname: 'local1@domain1', + path: 'local1@domain1', + href: 'local1@domain1' + }, + + // While this may seem counter-intuitive, a browser will parse + // as a path. + 'www.example.com': { + href: 'www.example.com', + pathname: 'www.example.com', + path: 'www.example.com' + }, + + // ipv6 support + '[fe80::1]': { + href: '[fe80::1]', + pathname: '[fe80::1]', + path: '[fe80::1]' + }, + + 'coap://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]': { + protocol: 'coap:', + slashes: true, + host: '[fedc:ba98:7654:3210:fedc:ba98:7654:3210]', + hostname: 'fedc:ba98:7654:3210:fedc:ba98:7654:3210', + href: 'coap://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]/', + pathname: '/', + path: '/' + }, + + 'coap://[1080:0:0:0:8:800:200C:417A]:61616/': { + protocol: 'coap:', + slashes: true, + host: '[1080:0:0:0:8:800:200c:417a]:61616', + port: '61616', + hostname: '1080:0:0:0:8:800:200c:417a', + href: 'coap://[1080:0:0:0:8:800:200c:417a]:61616/', + pathname: '/', + path: '/' + }, + + 'http://user:password@[3ffe:2a00:100:7031::1]:8080': { + protocol: 'http:', + slashes: true, + auth: 'user:password', + host: '[3ffe:2a00:100:7031::1]:8080', + port: '8080', + hostname: '3ffe:2a00:100:7031::1', + href: 'http://user:password@[3ffe:2a00:100:7031::1]:8080/', + pathname: '/', + path: '/' + }, + + 'coap://u:p@[::192.9.5.5]:61616/.well-known/r?n=Temperature': { + protocol: 'coap:', + slashes: true, + auth: 'u:p', + host: '[::192.9.5.5]:61616', + port: '61616', + hostname: '::192.9.5.5', + href: 'coap://u:p@[::192.9.5.5]:61616/.well-known/r?n=Temperature', + search: '?n=Temperature', + query: 'n=Temperature', + pathname: '/.well-known/r', + path: '/.well-known/r?n=Temperature' + }, + + // empty port + 'http://example.com:': { + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/', + pathname: '/', + path: '/' + }, + + 'http://example.com:/a/b.html': { + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/a/b.html', + pathname: '/a/b.html', + path: '/a/b.html' + }, + + 'http://example.com:?a=b': { + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/?a=b', + search: '?a=b', + query: 'a=b', + pathname: '/', + path: '/?a=b' + }, + + 'http://example.com:#abc': { + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/#abc', + hash: '#abc', + pathname: '/', + path: '/' + }, + + 'http://[fe80::1]:/a/b?a=b#abc': { + protocol: 'http:', + slashes: true, + host: '[fe80::1]', + hostname: 'fe80::1', + href: 'http://[fe80::1]/a/b?a=b#abc', + search: '?a=b', + query: 'a=b', + hash: '#abc', + pathname: '/a/b', + path: '/a/b?a=b' + }, + + 'http://-lovemonsterz.tumblr.com/rss': { + protocol: 'http:', + slashes: true, + host: '-lovemonsterz.tumblr.com', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://-lovemonsterz.tumblr.com/rss', + pathname: '/rss', + path: '/rss', + }, + + 'http://-lovemonsterz.tumblr.com:80/rss': { + protocol: 'http:', + slashes: true, + port: '80', + host: '-lovemonsterz.tumblr.com:80', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://-lovemonsterz.tumblr.com:80/rss', + pathname: '/rss', + path: '/rss', + }, + + 'http://user:pass@-lovemonsterz.tumblr.com/rss': { + protocol: 'http:', + slashes: true, + auth: 'user:pass', + host: '-lovemonsterz.tumblr.com', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://user:pass@-lovemonsterz.tumblr.com/rss', + pathname: '/rss', + path: '/rss', + }, + + 'http://user:pass@-lovemonsterz.tumblr.com:80/rss': { + protocol: 'http:', + slashes: true, + auth: 'user:pass', + port: '80', + host: '-lovemonsterz.tumblr.com:80', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://user:pass@-lovemonsterz.tumblr.com:80/rss', + pathname: '/rss', + path: '/rss', + }, + + 'http://_jabber._tcp.google.com/test': { + protocol: 'http:', + slashes: true, + host: '_jabber._tcp.google.com', + hostname: '_jabber._tcp.google.com', + href: 'http://_jabber._tcp.google.com/test', + pathname: '/test', + path: '/test', + }, + + 'http://user:pass@_jabber._tcp.google.com/test': { + protocol: 'http:', + slashes: true, + auth: 'user:pass', + host: '_jabber._tcp.google.com', + hostname: '_jabber._tcp.google.com', + href: 'http://user:pass@_jabber._tcp.google.com/test', + pathname: '/test', + path: '/test', + }, + + 'http://_jabber._tcp.google.com:80/test': { + protocol: 'http:', + slashes: true, + port: '80', + host: '_jabber._tcp.google.com:80', + hostname: '_jabber._tcp.google.com', + href: 'http://_jabber._tcp.google.com:80/test', + pathname: '/test', + path: '/test', + }, + + 'http://user:pass@_jabber._tcp.google.com:80/test': { + protocol: 'http:', + slashes: true, + auth: 'user:pass', + port: '80', + host: '_jabber._tcp.google.com:80', + hostname: '_jabber._tcp.google.com', + href: 'http://user:pass@_jabber._tcp.google.com:80/test', + pathname: '/test', + path: '/test', + }, + + 'http://x:1/\' <>"`/{}|\\^~`/': { + protocol: 'http:', + slashes: true, + host: 'x:1', + port: '1', + hostname: 'x', + pathname: '/%27%20%3C%3E%22%60/%7B%7D%7C/%5E~%60/', + path: '/%27%20%3C%3E%22%60/%7B%7D%7C/%5E~%60/', + href: 'http://x:1/%27%20%3C%3E%22%60/%7B%7D%7C/%5E~%60/' + }, + + 'http://a@b@c/': { + protocol: 'http:', + slashes: true, + auth: 'a@b', + host: 'c', + hostname: 'c', + href: 'http://a%40b@c/', + path: '/', + pathname: '/' + }, + + 'http://a@b?@c': { + protocol: 'http:', + slashes: true, + auth: 'a', + host: 'b', + hostname: 'b', + href: 'http://a@b/?@c', + path: '/?@c', + pathname: '/', + search: '?@c', + query: '@c' + }, + + 'http://a.b/\tbc\ndr\ref g"hq\'j?mn\\op^q=r`99{st|uv}wz': { + protocol: 'http:', + slashes: true, + host: 'a.b', + port: null, + hostname: 'a.b', + hash: null, + pathname: '/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E', + path: '/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz', + search: '?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz', + query: 'mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz', + href: 'http://a.b/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz' + }, + + 'http://a\r" \t\n<\'b:b@c\r\nd/e?f': { + protocol: 'http:', + slashes: true, + auth: 'a" <\'b:b', + host: 'cd', + port: null, + hostname: 'cd', + hash: null, + search: '?f', + query: 'f', + pathname: '/e', + path: '/e?f', + href: 'http://a%22%20%3C\'b:b@cd/e?f' + }, + + // Git urls used by npm + 'git+ssh://git@github.com:npm/npm': { + protocol: 'git+ssh:', + slashes: true, + auth: 'git', + host: 'github.com', + port: null, + hostname: 'github.com', + hash: null, + search: null, + query: null, + pathname: '/:npm/npm', + path: '/:npm/npm', + href: 'git+ssh://git@github.com/:npm/npm' + }, + + 'https://*': { + protocol: 'https:', + slashes: true, + auth: null, + host: '*', + port: null, + hostname: '*', + hash: null, + search: null, + query: null, + pathname: '/', + path: '/', + href: 'https://*/' + }, + + // The following two URLs are the same, but they differ for a capital A. + // Verify that the protocol is checked in a case-insensitive manner. + 'javascript:alert(1);a=\x27@white-listed.com\x27': { + protocol: 'javascript:', + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: null, + pathname: "alert(1);a='@white-listed.com'", + path: "alert(1);a='@white-listed.com'", + href: "javascript:alert(1);a='@white-listed.com'" + }, + + 'javAscript:alert(1);a=\x27@white-listed.com\x27': { + protocol: 'javascript:', + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: null, + pathname: "alert(1);a='@white-listed.com'", + path: "alert(1);a='@white-listed.com'", + href: "javascript:alert(1);a='@white-listed.com'" + }, + + 'ws://www.example.com': { + protocol: 'ws:', + slashes: true, + hostname: 'www.example.com', + host: 'www.example.com', + pathname: '/', + path: '/', + href: 'ws://www.example.com/' + }, + + 'wss://www.example.com': { + protocol: 'wss:', + slashes: true, + hostname: 'www.example.com', + host: 'www.example.com', + pathname: '/', + path: '/', + href: 'wss://www.example.com/' + }, + + '//fhqwhgads@example.com/everybody-to-the-limit': { + protocol: null, + slashes: true, + auth: 'fhqwhgads', + host: 'example.com', + port: null, + hostname: 'example.com', + hash: null, + search: null, + query: null, + pathname: '/everybody-to-the-limit', + path: '/everybody-to-the-limit', + href: '//fhqwhgads@example.com/everybody-to-the-limit' + }, + + '//fhqwhgads@example.com/everybody#to-the-limit': { + protocol: null, + slashes: true, + auth: 'fhqwhgads', + host: 'example.com', + port: null, + hostname: 'example.com', + hash: '#to-the-limit', + search: null, + query: null, + pathname: '/everybody', + path: '/everybody', + href: '//fhqwhgads@example.com/everybody#to-the-limit' + }, + + '\bhttp://example.com/\b': { + protocol: 'http:', + slashes: true, + auth: null, + host: 'example.com', + port: null, + hostname: 'example.com', + hash: null, + search: null, + query: null, + pathname: '/', + path: '/', + href: 'http://example.com/' + }, + + 'https://evil.com$.example.com': { + protocol: 'https:', + slashes: true, + auth: null, + host: 'evil.com$.example.com', + port: null, + hostname: 'evil.com$.example.com', + hash: null, + search: null, + query: null, + pathname: '/', + path: '/', + href: 'https://evil.com$.example.com/' + }, + + // Validate the output of hostname with commas. + 'x://0.0,1.1/': { + protocol: 'x:', + slashes: true, + auth: null, + host: '0.0,1.1', + port: null, + hostname: '0.0,1.1', + hash: null, + search: null, + query: null, + pathname: '/', + path: '/', + href: 'x://0.0,1.1/' + } +}; + +for (const u in parseTests) { + let actual = url.parse(u); + const spaced = url.parse(` \t ${u}\n\t`); + let expected = Object.assign(new url.Url(), parseTests[u]); + + Object.keys(actual).forEach(function(i) { + if (expected[i] === undefined && actual[i] === null) { + expected[i] = null; + } + }); + + assert.deepStrictEqual( + actual, + expected, + `parsing ${u} and expected ${inspect(expected)} but got ${inspect(actual)}` + ); + assert.deepStrictEqual( + spaced, + expected, + `expected ${inspect(expected)}, got ${inspect(spaced)}` + ); + + expected = parseTests[u].href; + actual = url.format(parseTests[u]); + + assert.strictEqual(actual, expected, + `format(${u}) == ${u}\nactual:${actual}`); +} + +{ + const parsed = url.parse('http://nodejs.org/') + .resolveObject('jAvascript:alert(1);a=\x27@white-listed.com\x27'); + + const expected = Object.assign(new url.Url(), { + protocol: 'javascript:', + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: null, + pathname: "alert(1);a='@white-listed.com'", + path: "alert(1);a='@white-listed.com'", + href: "javascript:alert(1);a='@white-listed.com'" + }); + + assert.deepStrictEqual(parsed, expected); +} diff --git a/test/js/node/test/parallel/test-url-parse-invalid-input.js b/test/js/node/test/parallel/test-url-parse-invalid-input.js new file mode 100644 index 0000000000..a8ae4838af --- /dev/null +++ b/test/js/node/test/parallel/test-url-parse-invalid-input.js @@ -0,0 +1,107 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const url = require('url'); + +// https://github.com/joyent/node/issues/568 +[ + [undefined, 'undefined'], + [null, 'object'], + [true, 'boolean'], + [false, 'boolean'], + [0.0, 'number'], + [0, 'number'], + [[], 'object'], + [{}, 'object'], + [() => {}, 'function'], + [Symbol('foo'), 'symbol'], +].forEach(([val, type]) => { + assert.throws(() => { + url.parse(val); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "url" argument must be of type string.' + + common.invalidArgTypeHelper(val) + }); +}); + +assert.throws(() => { url.parse('http://%E0%A4%A@fail'); }, + (e) => { + // The error should be a URIError. + if (!(e instanceof URIError)) + return false; + + // The error should be from the JS engine and not from Node.js. + // JS engine errors do not have the `code` property. + return e.code === undefined; + }); + +assert.throws(() => { url.parse('http://[127.0.0.1\x00c8763]:8000/'); }, + { code: 'ERR_INVALID_URL', input: 'http://[127.0.0.1\x00c8763]:8000/' } +); + +if (common.hasIntl) { + // An array of Unicode code points whose Unicode NFKD contains a "bad + // character". + const badIDNA = (() => { + const BAD_CHARS = '#%/:?@[\\]^|'; + const out = []; + for (let i = 0x80; i < 0x110000; i++) { + const cp = String.fromCodePoint(i); + for (const badChar of BAD_CHARS) { + if (cp.normalize('NFKD').includes(badChar)) { + out.push(cp); + } + } + } + return out; + })(); + + // The generation logic above should at a minimum produce these two + // characters. + assert(badIDNA.includes('℀')); + assert(badIDNA.includes('@')); + + for (const badCodePoint of badIDNA) { + const badURL = `http://fail${badCodePoint}fail.com/`; + assert.throws(() => { url.parse(badURL); }, + (e) => e.code === 'ERR_INVALID_URL', + `parsing ${badURL}`); + } + + assert.throws(() => { url.parse('http://\u00AD/bad.com/'); }, + (e) => e.code === 'ERR_INVALID_URL', + 'parsing http://\u00AD/bad.com/'); +} + +{ + const badURLs = [ + 'https://evil.com:.example.com', + 'git+ssh://git@github.com:npm/npm', + ]; + badURLs.forEach((badURL) => { + common.spawnPromisified(process.execPath, ['-e', `url.parse(${JSON.stringify(badURL)})`]) + .then(common.mustCall(({ code, stdout, stderr }) => { + assert.strictEqual(code, 0); + assert.strictEqual(stdout, ''); + // NOTE: bun formats errors slightly differently from node, but we're + // printing the same deprecation message. + // assert.match(stderr, /\[DEP0170\] DeprecationWarning:/); + assert.match(stderr, /\DEP0170/); + assert.match(stderr, /\DeprecationWarning/); + })); + }); + + // Warning should only happen once per process. + common.expectWarning({ + DeprecationWarning: { + // eslint-disable-next-line @stylistic/js/max-len + DEP0169: '`url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.', + DEP0170: `The URL ${badURLs[0]} is invalid. Future versions of Node.js will throw an error.`, + }, + }); + badURLs.forEach((badURL) => { + url.parse(badURL); + }); +} diff --git a/test/js/node/test/parallel/test-url-parse-query.js b/test/js/node/test/parallel/test-url-parse-query.js new file mode 100644 index 0000000000..9cb21486c1 --- /dev/null +++ b/test/js/node/test/parallel/test-url-parse-query.js @@ -0,0 +1,101 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const url = require('url'); + +function createWithNoPrototype(properties = []) { + const noProto = { __proto__: null }; + properties.forEach((property) => { + noProto[property.key] = property.value; + }); + return noProto; +} + +function check(actual, expected) { + // NOTE: Node creates a new object with no prototype when parsing queries. + // Their query parsing logic is written in JS. We re-use URLSearchParams + // from WebCore, which is spec-compliant with newer standards. + // assert.notStrictEqual(Object.getPrototypeOf(actual), Object.prototype); + assert.deepStrictEqual(Object.keys(actual).sort(), + Object.keys(expected).sort()); + Object.keys(expected).forEach(function(key) { + assert.deepStrictEqual( + actual[key], + expected[key], + `actual[${key}] !== expected[${key}]: ${actual[key]} !== ${expected[key]}` + ); + }); +} + +const parseTestsWithQueryString = { + '/foo/bar?baz=quux#frag': { + href: '/foo/bar?baz=quux#frag', + hash: '#frag', + search: '?baz=quux', + query: createWithNoPrototype([{ key: 'baz', value: 'quux' }]), + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + 'http://example.com': { + href: 'http://example.com/', + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + query: createWithNoPrototype(), + search: null, + pathname: '/', + path: '/' + }, + '/example': { + protocol: null, + slashes: null, + auth: undefined, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: createWithNoPrototype(), + pathname: '/example', + path: '/example', + href: '/example' + }, + '/example?query=value': { + protocol: null, + slashes: null, + auth: undefined, + host: null, + port: null, + hostname: null, + hash: null, + search: '?query=value', + query: createWithNoPrototype([{ key: 'query', value: 'value' }]), + pathname: '/example', + path: '/example?query=value', + href: '/example?query=value' + } +}; +for (const u in parseTestsWithQueryString) { + const actual = url.parse(u, true); + const expected = Object.assign(new url.Url(), parseTestsWithQueryString[u]); + for (const i in actual) { + if (actual[i] === null && expected[i] === undefined) { + expected[i] = null; + } + } + + const properties = Object.keys(actual).sort(); + assert.deepStrictEqual(properties, Object.keys(expected).sort()); + properties.forEach((property) => { + if (property === 'query') { + check(actual[property], expected[property]); + } else { + assert.deepStrictEqual( + actual[property], + expected[property], + `${u}\n\nactual['${property}'] !== expected['${property}']: ${actual[property]} !== ${expected[property]}` + ); + } + }); +} diff --git a/test/js/node/test/parallel/test-url-relative.js b/test/js/node/test/parallel/test-url-relative.js new file mode 100644 index 0000000000..2751ec3b51 --- /dev/null +++ b/test/js/node/test/parallel/test-url-relative.js @@ -0,0 +1,443 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const inspect = require('util').inspect; +const url = require('url'); + +// When source is false +assert.strictEqual(url.resolveObject('', 'foo'), 'foo'); + +// [from, path, expected] +const relativeTests = [ + ['/foo/bar/baz', 'quux', '/foo/bar/quux'], + ['/foo/bar/baz', 'quux/asdf', '/foo/bar/quux/asdf'], + ['/foo/bar/baz', 'quux/baz', '/foo/bar/quux/baz'], + ['/foo/bar/baz', '../quux/baz', '/foo/quux/baz'], + ['/foo/bar/baz', '/bar', '/bar'], + ['/foo/bar/baz/', 'quux', '/foo/bar/baz/quux'], + ['/foo/bar/baz/', 'quux/baz', '/foo/bar/baz/quux/baz'], + ['/foo/bar/baz', '../../../../../../../../quux/baz', '/quux/baz'], + ['/foo/bar/baz', '../../../../../../../quux/baz', '/quux/baz'], + ['/foo', '.', '/'], + ['/foo', '..', '/'], + ['/foo/', '.', '/foo/'], + ['/foo/', '..', '/'], + ['/foo/bar', '.', '/foo/'], + ['/foo/bar', '..', '/'], + ['/foo/bar/', '.', '/foo/bar/'], + ['/foo/bar/', '..', '/foo/'], + ['foo/bar', '../../../baz', '../../baz'], + ['foo/bar/', '../../../baz', '../baz'], + ['http://example.com/b//c//d;p?q#blarg', 'https:#hash2', 'https:///#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'https:/p/a/t/h?s#hash2', + 'https://p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'https://u:p@h.com/p/a/t/h?s#hash2', + 'https://u:p@h.com/p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'https:/a/b/c/d', + 'https://a/b/c/d'], + ['http://example.com/b//c//d;p?q#blarg', + 'http:#hash2', + 'http://example.com/b//c//d;p?q#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'http:/p/a/t/h?s#hash2', + 'http://example.com/p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'http://u:p@h.com/p/a/t/h?s#hash2', + 'http://u:p@h.com/p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'http:/a/b/c/d', + 'http://example.com/a/b/c/d'], + ['/foo/bar/baz', '/../etc/passwd', '/etc/passwd'], + ['http://localhost', 'file:///Users/foo', 'file:///Users/foo'], + ['http://localhost', 'file://foo/Users', 'file://foo/Users'], + ['https://registry.npmjs.org', '@foo/bar', 'https://registry.npmjs.org/@foo/bar'], +]; +for (let i = 0; i < relativeTests.length; i++) { + const relativeTest = relativeTests[i]; + + const a = url.resolve(relativeTest[0], relativeTest[1]); + const e = relativeTest[2]; + assert.strictEqual(a, e, + `resolve(${relativeTest[0]}, ${relativeTest[1]})` + + ` == ${e}\n actual=${a}`); +} + +// +// Tests below taken from Chiron +// http://code.google.com/p/chironjs/source/browse/trunk/src/test/http/url.js +// +// Copyright (c) 2002-2008 Kris Kowal +// used with permission under MIT License +// +// Changes marked with @isaacs + +const bases = [ + 'http://a/b/c/d;p?q', + 'http://a/b/c/d;p?q=1/2', + 'http://a/b/c/d;p=1/2?q', + 'fred:///s//a/b/c', + 'http:///s//a/b/c', +]; + +// [to, from, result] +const relativeTests2 = [ + // http://lists.w3.org/Archives/Public/uri/2004Feb/0114.html + ['../c', 'foo:a/b', 'foo:c'], + ['foo:.', 'foo:a', 'foo:'], + ['/foo/../../../bar', 'zz:abc', 'zz:/bar'], + ['/foo/../bar', 'zz:abc', 'zz:/bar'], + // @isaacs Disagree. Not how web browsers resolve this. + ['foo/../../../bar', 'zz:abc', 'zz:bar'], + // ['foo/../../../bar', 'zz:abc', 'zz:../../bar'], // @isaacs Added + ['foo/../bar', 'zz:abc', 'zz:bar'], + ['zz:.', 'zz:abc', 'zz:'], + ['/.', bases[0], 'http://a/'], + ['/.foo', bases[0], 'http://a/.foo'], + ['.foo', bases[0], 'http://a/b/c/.foo'], + + // http://gbiv.com/protocols/uri/test/rel_examples1.html + // examples from RFC 2396 + ['g:h', bases[0], 'g:h'], + ['g', bases[0], 'http://a/b/c/g'], + ['./g', bases[0], 'http://a/b/c/g'], + ['g/', bases[0], 'http://a/b/c/g/'], + ['/g', bases[0], 'http://a/g'], + ['//g', bases[0], 'http://g/'], + // Changed with RFC 2396bis + // ('?y', bases[0], 'http://a/b/c/d;p?y'], + ['?y', bases[0], 'http://a/b/c/d;p?y'], + ['g?y', bases[0], 'http://a/b/c/g?y'], + // Changed with RFC 2396bis + // ('#s', bases[0], CURRENT_DOC_URI + '#s'], + ['#s', bases[0], 'http://a/b/c/d;p?q#s'], + ['g#s', bases[0], 'http://a/b/c/g#s'], + ['g?y#s', bases[0], 'http://a/b/c/g?y#s'], + [';x', bases[0], 'http://a/b/c/;x'], + ['g;x', bases[0], 'http://a/b/c/g;x'], + ['g;x?y#s', bases[0], 'http://a/b/c/g;x?y#s'], + // Changed with RFC 2396bis + // ('', bases[0], CURRENT_DOC_URI], + ['', bases[0], 'http://a/b/c/d;p?q'], + ['.', bases[0], 'http://a/b/c/'], + ['./', bases[0], 'http://a/b/c/'], + ['..', bases[0], 'http://a/b/'], + ['../', bases[0], 'http://a/b/'], + ['../g', bases[0], 'http://a/b/g'], + ['../..', bases[0], 'http://a/'], + ['../../', bases[0], 'http://a/'], + ['../../g', bases[0], 'http://a/g'], + ['../../../g', bases[0], ('http://a/../g', 'http://a/g')], + ['../../../../g', bases[0], ('http://a/../../g', 'http://a/g')], + // Changed with RFC 2396bis + // ('/./g', bases[0], 'http://a/./g'], + ['/./g', bases[0], 'http://a/g'], + // Changed with RFC 2396bis + // ('/../g', bases[0], 'http://a/../g'], + ['/../g', bases[0], 'http://a/g'], + ['g.', bases[0], 'http://a/b/c/g.'], + ['.g', bases[0], 'http://a/b/c/.g'], + ['g..', bases[0], 'http://a/b/c/g..'], + ['..g', bases[0], 'http://a/b/c/..g'], + ['./../g', bases[0], 'http://a/b/g'], + ['./g/.', bases[0], 'http://a/b/c/g/'], + ['g/./h', bases[0], 'http://a/b/c/g/h'], + ['g/../h', bases[0], 'http://a/b/c/h'], + ['g;x=1/./y', bases[0], 'http://a/b/c/g;x=1/y'], + ['g;x=1/../y', bases[0], 'http://a/b/c/y'], + ['g?y/./x', bases[0], 'http://a/b/c/g?y/./x'], + ['g?y/../x', bases[0], 'http://a/b/c/g?y/../x'], + ['g#s/./x', bases[0], 'http://a/b/c/g#s/./x'], + ['g#s/../x', bases[0], 'http://a/b/c/g#s/../x'], + ['http:g', bases[0], ('http:g', 'http://a/b/c/g')], + ['http:', bases[0], ('http:', bases[0])], + // Not sure where this one originated + ['/a/b/c/./../../g', bases[0], 'http://a/a/g'], + + // http://gbiv.com/protocols/uri/test/rel_examples2.html + // slashes in base URI's query args + ['g', bases[1], 'http://a/b/c/g'], + ['./g', bases[1], 'http://a/b/c/g'], + ['g/', bases[1], 'http://a/b/c/g/'], + ['/g', bases[1], 'http://a/g'], + ['//g', bases[1], 'http://g/'], + // Changed in RFC 2396bis + // ('?y', bases[1], 'http://a/b/c/?y'], + ['?y', bases[1], 'http://a/b/c/d;p?y'], + ['g?y', bases[1], 'http://a/b/c/g?y'], + ['g?y/./x', bases[1], 'http://a/b/c/g?y/./x'], + ['g?y/../x', bases[1], 'http://a/b/c/g?y/../x'], + ['g#s', bases[1], 'http://a/b/c/g#s'], + ['g#s/./x', bases[1], 'http://a/b/c/g#s/./x'], + ['g#s/../x', bases[1], 'http://a/b/c/g#s/../x'], + ['./', bases[1], 'http://a/b/c/'], + ['../', bases[1], 'http://a/b/'], + ['../g', bases[1], 'http://a/b/g'], + ['../../', bases[1], 'http://a/'], + ['../../g', bases[1], 'http://a/g'], + + // http://gbiv.com/protocols/uri/test/rel_examples3.html + // slashes in path params + // all of these changed in RFC 2396bis + ['g', bases[2], 'http://a/b/c/d;p=1/g'], + ['./g', bases[2], 'http://a/b/c/d;p=1/g'], + ['g/', bases[2], 'http://a/b/c/d;p=1/g/'], + ['g?y', bases[2], 'http://a/b/c/d;p=1/g?y'], + [';x', bases[2], 'http://a/b/c/d;p=1/;x'], + ['g;x', bases[2], 'http://a/b/c/d;p=1/g;x'], + ['g;x=1/./y', bases[2], 'http://a/b/c/d;p=1/g;x=1/y'], + ['g;x=1/../y', bases[2], 'http://a/b/c/d;p=1/y'], + ['./', bases[2], 'http://a/b/c/d;p=1/'], + ['../', bases[2], 'http://a/b/c/'], + ['../g', bases[2], 'http://a/b/c/g'], + ['../../', bases[2], 'http://a/b/'], + ['../../g', bases[2], 'http://a/b/g'], + + // http://gbiv.com/protocols/uri/test/rel_examples4.html + // double and triple slash, unknown scheme + ['g:h', bases[3], 'g:h'], + ['g', bases[3], 'fred:///s//a/b/g'], + ['./g', bases[3], 'fred:///s//a/b/g'], + ['g/', bases[3], 'fred:///s//a/b/g/'], + ['/g', bases[3], 'fred:///g'], // May change to fred:///s//a/g + ['//g', bases[3], 'fred://g'], // May change to fred:///s//g + ['//g/x', bases[3], 'fred://g/x'], // May change to fred:///s//g/x + ['///g', bases[3], 'fred:///g'], + ['./', bases[3], 'fred:///s//a/b/'], + ['../', bases[3], 'fred:///s//a/'], + ['../g', bases[3], 'fred:///s//a/g'], + + ['../../', bases[3], 'fred:///s//'], + ['../../g', bases[3], 'fred:///s//g'], + ['../../../g', bases[3], 'fred:///s/g'], + // May change to fred:///s//a/../../../g + ['../../../../g', bases[3], 'fred:///g'], + + // http://gbiv.com/protocols/uri/test/rel_examples5.html + // double and triple slash, well-known scheme + ['g:h', bases[4], 'g:h'], + ['g', bases[4], 'http:///s//a/b/g'], + ['./g', bases[4], 'http:///s//a/b/g'], + ['g/', bases[4], 'http:///s//a/b/g/'], + ['/g', bases[4], 'http:///g'], // May change to http:///s//a/g + ['//g', bases[4], 'http://g/'], // May change to http:///s//g + ['//g/x', bases[4], 'http://g/x'], // May change to http:///s//g/x + ['///g', bases[4], 'http:///g'], + ['./', bases[4], 'http:///s//a/b/'], + ['../', bases[4], 'http:///s//a/'], + ['../g', bases[4], 'http:///s//a/g'], + ['../../', bases[4], 'http:///s//'], + ['../../g', bases[4], 'http:///s//g'], + // May change to http:///s//a/../../g + ['../../../g', bases[4], 'http:///s/g'], + // May change to http:///s//a/../../../g + ['../../../../g', bases[4], 'http:///g'], + + // From Dan Connelly's tests in http://www.w3.org/2000/10/swap/uripath.py + ['bar:abc', 'foo:xyz', 'bar:abc'], + ['../abc', 'http://example/x/y/z', 'http://example/x/abc'], + ['http://example/x/abc', 'http://example2/x/y/z', 'http://example/x/abc'], + ['../r', 'http://ex/x/y/z', 'http://ex/x/r'], + ['q/r', 'http://ex/x/y', 'http://ex/x/q/r'], + ['q/r#s', 'http://ex/x/y', 'http://ex/x/q/r#s'], + ['q/r#s/t', 'http://ex/x/y', 'http://ex/x/q/r#s/t'], + ['ftp://ex/x/q/r', 'http://ex/x/y', 'ftp://ex/x/q/r'], + ['', 'http://ex/x/y', 'http://ex/x/y'], + ['', 'http://ex/x/y/', 'http://ex/x/y/'], + ['', 'http://ex/x/y/pdq', 'http://ex/x/y/pdq'], + ['z/', 'http://ex/x/y/', 'http://ex/x/y/z/'], + ['#Animal', + 'file:/swap/test/animal.rdf', + 'file:/swap/test/animal.rdf#Animal'], + ['../abc', 'file:/e/x/y/z', 'file:/e/x/abc'], + ['/example/x/abc', 'file:/example2/x/y/z', 'file:/example/x/abc'], + ['../r', 'file:/ex/x/y/z', 'file:/ex/x/r'], + ['/r', 'file:/ex/x/y/z', 'file:/r'], + ['q/r', 'file:/ex/x/y', 'file:/ex/x/q/r'], + ['q/r#s', 'file:/ex/x/y', 'file:/ex/x/q/r#s'], + ['q/r#', 'file:/ex/x/y', 'file:/ex/x/q/r#'], + ['q/r#s/t', 'file:/ex/x/y', 'file:/ex/x/q/r#s/t'], + ['ftp://ex/x/q/r', 'file:/ex/x/y', 'ftp://ex/x/q/r'], + ['', 'file:/ex/x/y', 'file:/ex/x/y'], + ['', 'file:/ex/x/y/', 'file:/ex/x/y/'], + ['', 'file:/ex/x/y/pdq', 'file:/ex/x/y/pdq'], + ['z/', 'file:/ex/x/y/', 'file:/ex/x/y/z/'], + ['file://meetings.example.com/cal#m1', + 'file:/devel/WWW/2000/10/swap/test/reluri-1.n3', + 'file://meetings.example.com/cal#m1'], + ['file://meetings.example.com/cal#m1', + 'file:/home/connolly/w3ccvs/WWW/2000/10/swap/test/reluri-1.n3', + 'file://meetings.example.com/cal#m1'], + ['./#blort', 'file:/some/dir/foo', 'file:/some/dir/#blort'], + ['./#', 'file:/some/dir/foo', 'file:/some/dir/#'], + // Ryan Lee + ['./', 'http://example/x/abc.efg', 'http://example/x/'], + + + // Graham Klyne's tests + // http://www.ninebynine.org/Software/HaskellUtils/Network/UriTest.xls + // 01-31 are from Connelly's cases + + // 32-49 + ['./q:r', 'http://ex/x/y', 'http://ex/x/q:r'], + ['./p=q:r', 'http://ex/x/y', 'http://ex/x/p=q:r'], + ['?pp/rr', 'http://ex/x/y?pp/qq', 'http://ex/x/y?pp/rr'], + ['y/z', 'http://ex/x/y?pp/qq', 'http://ex/x/y/z'], + ['local/qual@domain.org#frag', + 'mailto:local', + 'mailto:local/qual@domain.org#frag'], + ['more/qual2@domain2.org#frag', + 'mailto:local/qual1@domain1.org', + 'mailto:local/more/qual2@domain2.org#frag'], + ['y?q', 'http://ex/x/y?q', 'http://ex/x/y?q'], + ['/x/y?q', 'http://ex?p', 'http://ex/x/y?q'], + ['c/d', 'foo:a/b', 'foo:a/c/d'], + ['/c/d', 'foo:a/b', 'foo:/c/d'], + ['', 'foo:a/b?c#d', 'foo:a/b?c'], + ['b/c', 'foo:a', 'foo:b/c'], + ['../b/c', 'foo:/a/y/z', 'foo:/a/b/c'], + ['./b/c', 'foo:a', 'foo:b/c'], + ['/./b/c', 'foo:a', 'foo:/b/c'], + ['../../d', 'foo://a//b/c', 'foo://a/d'], + ['.', 'foo:a', 'foo:'], + ['..', 'foo:a', 'foo:'], + + // 50-57[cf. TimBL comments -- + // http://lists.w3.org/Archives/Public/uri/2003Feb/0028.html, + // http://lists.w3.org/Archives/Public/uri/2003Jan/0008.html) + ['abc', 'http://example/x/y%2Fz', 'http://example/x/abc'], + ['../../x%2Fabc', 'http://example/a/x/y/z', 'http://example/a/x%2Fabc'], + ['../x%2Fabc', 'http://example/a/x/y%2Fz', 'http://example/a/x%2Fabc'], + ['abc', 'http://example/x%2Fy/z', 'http://example/x%2Fy/abc'], + ['q%3Ar', 'http://ex/x/y', 'http://ex/x/q%3Ar'], + ['/x%2Fabc', 'http://example/x/y%2Fz', 'http://example/x%2Fabc'], + ['/x%2Fabc', 'http://example/x/y/z', 'http://example/x%2Fabc'], + ['/x%2Fabc', 'http://example/x/y%2Fz', 'http://example/x%2Fabc'], + + // 70-77 + ['local2@domain2', 'mailto:local1@domain1?query1', 'mailto:local2@domain2'], + ['local2@domain2?query2', + 'mailto:local1@domain1', + 'mailto:local2@domain2?query2'], + ['local2@domain2?query2', + 'mailto:local1@domain1?query1', + 'mailto:local2@domain2?query2'], + ['?query2', 'mailto:local@domain?query1', 'mailto:local@domain?query2'], + ['local@domain?query2', 'mailto:?query1', 'mailto:local@domain?query2'], + ['?query2', 'mailto:local@domain?query1', 'mailto:local@domain?query2'], + ['http://example/a/b?c/../d', 'foo:bar', 'http://example/a/b?c/../d'], + ['http://example/a/b#c/../d', 'foo:bar', 'http://example/a/b#c/../d'], + + // 82-88 + // @isaacs Disagree. Not how browsers do it. + // ['http:this', 'http://example.org/base/uri', 'http:this'], + // @isaacs Added + ['http:this', 'http://example.org/base/uri', 'http://example.org/base/this'], + ['http:this', 'http:base', 'http:this'], + ['.//g', 'f:/a', 'f://g'], + ['b/c//d/e', 'f://example.org/base/a', 'f://example.org/base/b/c//d/e'], + ['m2@example.ord/c2@example.org', + 'mid:m@example.ord/c@example.org', + 'mid:m@example.ord/m2@example.ord/c2@example.org'], + ['mini1.xml', + 'file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/', + 'file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/mini1.xml'], + ['../b/c', 'foo:a/y/z', 'foo:a/b/c'], + + // changing auth + ['http://diff:auth@www.example.com', + 'http://asdf:qwer@www.example.com', + 'http://diff:auth@www.example.com/'], + + // changing port + ['https://example.com:81/', + 'https://example.com:82/', + 'https://example.com:81/'], + + // https://github.com/nodejs/node/issues/1435 + ['https://another.host.com/', + 'https://user:password@example.org/', + 'https://another.host.com/'], + ['//another.host.com/', + 'https://user:password@example.org/', + 'https://another.host.com/'], + ['http://another.host.com/', + 'https://user:password@example.org/', + 'http://another.host.com/'], + ['mailto:another.host.com', + 'mailto:user@example.org', + 'mailto:another.host.com'], + ['https://example.com/foo', + 'https://user:password@example.com', + 'https://user:password@example.com/foo'], + + // No path at all + ['#hash1', '#hash2', '#hash1'], +]; +for (let i = 0; i < relativeTests2.length; i++) { + const relativeTest = relativeTests2[i]; + + const a = url.resolve(relativeTest[1], relativeTest[0]); + const e = url.format(relativeTest[2]); + assert.strictEqual(a, e, + `resolve(${relativeTest[0]}, ${relativeTest[1]})` + + ` == ${e}\n actual=${a}`); +} + +// If format and parse are inverse operations then +// resolveObject(parse(x), y) == parse(resolve(x, y)) + +// format: [from, path, expected] +for (let i = 0; i < relativeTests.length; i++) { + const relativeTest = relativeTests[i]; + + let actual = url.resolveObject(url.parse(relativeTest[0]), relativeTest[1]); + let expected = url.parse(relativeTest[2]); + + + assert.deepStrictEqual(actual, expected); + + expected = relativeTest[2]; + actual = url.format(actual); + + assert.strictEqual(actual, expected, + `format(${actual}) == ${expected}\n` + + `actual: ${actual}`); + +} + +// format: [to, from, result] +// the test: ['.//g', 'f:/a', 'f://g'] is a fundamental problem +// url.parse('f:/a') does not have a host +// url.resolve('f:/a', './/g') does not have a host because you have moved +// down to the g directory. i.e. f: //g, however when this url is parsed +// f:// will indicate that the host is g which is not the case. +// it is unclear to me how to keep this information from being lost +// it may be that a pathname of ////g should collapse to /g but this seems +// to be a lot of work for an edge case. Right now I remove the test +if (relativeTests2[181][0] === './/g' && + relativeTests2[181][1] === 'f:/a' && + relativeTests2[181][2] === 'f://g') { + relativeTests2.splice(181, 1); +} +for (let i = 0; i < relativeTests2.length; i++) { + const relativeTest = relativeTests2[i]; + + let actual = url.resolveObject(url.parse(relativeTest[1]), relativeTest[0]); + let expected = url.parse(relativeTest[2]); + + assert.deepStrictEqual( + actual, + expected, + `expected ${inspect(expected)} but got ${inspect(actual)}` + ); + + expected = url.format(relativeTest[2]); + actual = url.format(actual); + + assert.strictEqual(actual, expected, + `format(${relativeTest[1]}) == ${expected}\n` + + `actual: ${actual}`); +} diff --git a/test/js/node/test/parallel/test-url-revokeobjecturl.js b/test/js/node/test/parallel/test-url-revokeobjecturl.js new file mode 100644 index 0000000000..dae980c4d0 --- /dev/null +++ b/test/js/node/test/parallel/test-url-revokeobjecturl.js @@ -0,0 +1,14 @@ +'use strict'; + +require('../common'); + +// Test ensures that the function receives the url argument. + +const assert = require('node:assert'); + +assert.throws(() => { + URL.revokeObjectURL(); +}, { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', +}); diff --git a/test/js/node/test/parallel/test-url-urltooptions.js b/test/js/node/test/parallel/test-url-urltooptions.js new file mode 100644 index 0000000000..cc4838eeec --- /dev/null +++ b/test/js/node/test/parallel/test-url-urltooptions.js @@ -0,0 +1,37 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { urlToHttpOptions } = require('url'); + +// Test urlToHttpOptions +const urlObj = new URL('http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test'); +const opts = urlToHttpOptions(urlObj); +assert.strictEqual(opts instanceof URL, false); +assert.strictEqual(opts.protocol, 'http:'); +assert.strictEqual(opts.auth, 'user:pass'); +assert.strictEqual(opts.hostname, 'foo.bar.com'); +assert.strictEqual(opts.port, 21); +assert.strictEqual(opts.path, '/aaa/zzz?l=24'); +assert.strictEqual(opts.pathname, '/aaa/zzz'); +assert.strictEqual(opts.search, '?l=24'); +assert.strictEqual(opts.hash, '#test'); + +const { hostname } = urlToHttpOptions(new URL('http://[::1]:21')); +assert.strictEqual(hostname, '::1'); + +// If a WHATWG URL object is copied, it is possible that the resulting copy +// contains the Symbols that Node uses for brand checking, but not the data +// properties, which are getters. Verify that urlToHttpOptions() can handle +// such a case. +const copiedUrlObj = { ...urlObj }; +const copiedOpts = urlToHttpOptions(copiedUrlObj); +assert.strictEqual(copiedOpts instanceof URL, false); +assert.strictEqual(copiedOpts.protocol, undefined); +assert.strictEqual(copiedOpts.auth, undefined); +assert.strictEqual(copiedOpts.hostname, undefined); +assert.strictEqual(copiedOpts.port, NaN); +assert.strictEqual(copiedOpts.path, ''); +assert.strictEqual(copiedOpts.pathname, undefined); +assert.strictEqual(copiedOpts.search, undefined); +assert.strictEqual(copiedOpts.hash, undefined); +assert.strictEqual(copiedOpts.href, undefined); diff --git a/test/js/node/test/parallel/test-utf8-scripts.js b/test/js/node/test/parallel/test-utf8-scripts.js new file mode 100644 index 0000000000..4bf5b0cd5b --- /dev/null +++ b/test/js/node/test/parallel/test-utf8-scripts.js @@ -0,0 +1,30 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// üäö + +console.log('Σὲ γνωρίζω ἀπὸ τὴν κόψη'); + +assert.match('Hellö Wörld', /Hellö Wörld/); diff --git a/test/js/node/test/parallel/test-util-callbackify.js b/test/js/node/test/parallel/test-util-callbackify.js new file mode 100644 index 0000000000..7f7afb6407 --- /dev/null +++ b/test/js/node/test/parallel/test-util-callbackify.js @@ -0,0 +1,296 @@ +'use strict'; +const common = require('../common'); + +// This test checks that the semantics of `util.callbackify` are as described in +// the API docs + +const assert = require('assert'); +const { callbackify } = require('util'); +const { execFile } = require('child_process'); +const fixtures = require('../common/fixtures'); + +const values = [ + 'hello world', + null, + undefined, + false, + 0, + {}, + { key: 'value' }, + Symbol('I am a symbol'), + function ok() {}, + ['array', 'with', 4, 'values'], + new Error('boo'), +]; + +{ + // Test that the resolution value is passed as second argument to callback + for (const value of values) { + // Test and `async function` + async function asyncFn() { + return value; + } + + const cbAsyncFn = callbackify(asyncFn); + cbAsyncFn(common.mustSucceed((ret) => { + assert.strictEqual(ret, value); + })); + + // Test Promise factory + function promiseFn() { + return Promise.resolve(value); + } + + const cbPromiseFn = callbackify(promiseFn); + cbPromiseFn(common.mustSucceed((ret) => { + assert.strictEqual(ret, value); + })); + + // Test Thenable + function thenableFn() { + return { + then(onRes, onRej) { + onRes(value); + } + }; + } + + const cbThenableFn = callbackify(thenableFn); + cbThenableFn(common.mustSucceed((ret) => { + assert.strictEqual(ret, value); + })); + } +} + +{ + // Test that rejection reason is passed as first argument to callback + for (const value of values) { + // Test an `async function` + async function asyncFn() { + return Promise.reject(value); + } + + const cbAsyncFn = callbackify(asyncFn); + assert.strictEqual(cbAsyncFn.length, 1); + assert.strictEqual(cbAsyncFn.name, 'asyncFnCallbackified'); + cbAsyncFn(common.mustCall((err, ret) => { + assert.strictEqual(ret, undefined); + if (err instanceof Error) { + if ('reason' in err) { + assert(!value); + assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION'); + assert.strictEqual(err.reason, value); + } else { + assert.strictEqual(String(value).endsWith(err.message), true); + } + } else { + assert.strictEqual(err, value); + } + })); + + // Test a Promise factory + function promiseFn() { + return Promise.reject(value); + } + const obj = {}; + Object.defineProperty(promiseFn, 'name', { + value: obj, + writable: false, + enumerable: false, + configurable: true + }); + + const cbPromiseFn = callbackify(promiseFn); + assert.strictEqual(promiseFn.name, obj); + cbPromiseFn(common.mustCall((err, ret) => { + assert.strictEqual(ret, undefined); + if (err instanceof Error) { + if ('reason' in err) { + assert(!value); + assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION'); + assert.strictEqual(err.reason, value); + } else { + assert.strictEqual(String(value).endsWith(err.message), true); + } + } else { + assert.strictEqual(err, value); + } + })); + + // Test Thenable + function thenableFn() { + return { + then(onRes, onRej) { + onRej(value); + } + }; + } + + const cbThenableFn = callbackify(thenableFn); + cbThenableFn(common.mustCall((err, ret) => { + assert.strictEqual(ret, undefined); + if (err instanceof Error) { + if ('reason' in err) { + assert(!value); + assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION'); + assert.strictEqual(err.reason, value); + } else { + assert.strictEqual(String(value).endsWith(err.message), true); + } + } else { + assert.strictEqual(err, value); + } + })); + } +} + +{ + // Test that arguments passed to callbackified function are passed to original + for (const value of values) { + async function asyncFn(arg) { + assert.strictEqual(arg, value); + return arg; + } + + const cbAsyncFn = callbackify(asyncFn); + assert.strictEqual(cbAsyncFn.length, 2); + assert.notStrictEqual( + Object.getPrototypeOf(cbAsyncFn), + Object.getPrototypeOf(asyncFn) + ); + assert.strictEqual(Object.getPrototypeOf(cbAsyncFn), Function.prototype); + cbAsyncFn(value, common.mustSucceed((ret) => { + assert.strictEqual(ret, value); + })); + + function promiseFn(arg) { + assert.strictEqual(arg, value); + return Promise.resolve(arg); + } + const obj = {}; + Object.defineProperty(promiseFn, 'length', { + value: obj, + writable: false, + enumerable: false, + configurable: true + }); + + const cbPromiseFn = callbackify(promiseFn); + assert.strictEqual(promiseFn.length, obj); + cbPromiseFn(value, common.mustSucceed((ret) => { + assert.strictEqual(ret, value); + })); + } +} + +{ + // Test that `this` binding is the same for callbackified and original + for (const value of values) { + const iAmThis = { + fn(arg) { + assert.strictEqual(this, iAmThis); + return Promise.resolve(arg); + }, + }; + iAmThis.cbFn = callbackify(iAmThis.fn); + iAmThis.cbFn(value, common.mustSucceed(function(ret) { + assert.strictEqual(ret, value); + assert.strictEqual(this, iAmThis); + })); + + const iAmThat = { + async fn(arg) { + assert.strictEqual(this, iAmThat); + return arg; + }, + }; + iAmThat.cbFn = callbackify(iAmThat.fn); + iAmThat.cbFn(value, common.mustSucceed(function(ret) { + assert.strictEqual(ret, value); + assert.strictEqual(this, iAmThat); + })); + } +} + +{ + // Test that callback that throws emits an `uncaughtException` event + const fixture = fixtures.path('uncaught-exceptions', 'callbackify1.js'); + execFile( + process.execPath, + [fixture], + common.mustCall((err, stdout, stderr) => { + assert.strictEqual(err.code, 1); + assert.strictEqual(Object.getPrototypeOf(err).name, 'Error'); + assert.strictEqual(stdout, ''); + const errLines = stderr.trim().split(/[\r\n]+/); + const errLine = errLines.find((l) => /^error/.exec(l)); + assert.strictEqual(errLine, `error: ${fixture}`); + }) + ); +} + +{ + // Test that handled `uncaughtException` works and passes rejection reason + const fixture = fixtures.path('uncaught-exceptions', 'callbackify2.js'); + execFile( + process.execPath, + [fixture], + common.mustSucceed((stdout, stderr) => { + assert.strictEqual( + stdout.trim(), + `ifError got unwanted exception: ${fixture}`); + assert.strictEqual(stderr, ''); + }) + ); +} + +{ + // Verify that non-function inputs throw. + ['foo', null, undefined, false, 0, {}, Symbol(), []].forEach((value) => { + assert.throws(() => { + callbackify(value); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "original" argument must be of type function.' + + common.invalidArgTypeHelper(value) + }); + }); +} + +{ + async function asyncFn() { + return 42; + } + + const cb = callbackify(asyncFn); + const args = []; + + // Verify that the last argument to the callbackified function is a function. + ['foo', null, undefined, false, 0, {}, Symbol(), []].forEach((value) => { + args.push(value); + assert.throws(() => { + cb(...args); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + }); +} + +{ + // Test Promise factory + function promiseFn(value) { + return Promise.reject(value); + } + + const cbPromiseFn = callbackify(promiseFn); + + cbPromiseFn(null, (err) => { + assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION'); + assert.strictEqual(err.reason, null); + // skipped, bun doesn't hide callbackifyOnRejected from the stack trace + // const stack = err.stack.split(/[\r\n]+/); + // assert.match(stack[1], /at process\.processTicksAndRejections/); + }); +} diff --git a/test/js/node/test/parallel/test-util-deprecate-invalid-code.js b/test/js/node/test/parallel/test-util-deprecate-invalid-code.js new file mode 100644 index 0000000000..7e68c18817 --- /dev/null +++ b/test/js/node/test/parallel/test-util-deprecate-invalid-code.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); + +[1, true, false, null, {}].forEach((notString) => { + assert.throws(() => util.deprecate(() => {}, 'message', notString), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "code" argument must be of type string.' + + common.invalidArgTypeHelper(notString) + }); +}); diff --git a/test/js/node/test/parallel/test-util-deprecate.js b/test/js/node/test/parallel/test-util-deprecate.js new file mode 100644 index 0000000000..1b4a5e7662 --- /dev/null +++ b/test/js/node/test/parallel/test-util-deprecate.js @@ -0,0 +1,57 @@ +'use strict'; + +require('../common'); + +// Tests basic functionality of util.deprecate(). + +const assert = require('assert'); +const util = require('util'); + +const expectedWarnings = new Map(); + +// Emits deprecation only once if same function is called. +{ + const msg = 'fhqwhgads'; + const fn = util.deprecate(() => {}, msg); + expectedWarnings.set(msg, { code: undefined, count: 1 }); + fn(); + fn(); +} + +// Emits deprecation twice for different functions. +{ + const msg = 'sterrance'; + const fn1 = util.deprecate(() => {}, msg); + const fn2 = util.deprecate(() => {}, msg); + expectedWarnings.set(msg, { code: undefined, count: 2 }); + fn1(); + fn2(); +} + +// Emits deprecation only once if optional code is the same, even for different +// functions. +{ + const msg = 'cannonmouth'; + const code = 'deprecatesque'; + const fn1 = util.deprecate(() => {}, msg, code); + const fn2 = util.deprecate(() => {}, msg, code); + expectedWarnings.set(msg, { code, count: 1 }); + fn1(); + fn2(); + fn1(); + fn2(); +} + +process.on('warning', (warning) => { + assert.strictEqual(warning.name, 'DeprecationWarning'); + assert.ok(expectedWarnings.has(warning.message)); + const expected = expectedWarnings.get(warning.message); + assert.strictEqual(warning.code, expected.code); + expected.count = expected.count - 1; + if (expected.count === 0) + expectedWarnings.delete(warning.message); +}); + +process.on('exit', () => { + assert.deepStrictEqual(expectedWarnings, new Map()); +}); diff --git a/test/js/node/test/parallel/test-util-emit-experimental-warning.js b/test/js/node/test/parallel/test-util-emit-experimental-warning.js new file mode 100644 index 0000000000..6b6a45ec03 --- /dev/null +++ b/test/js/node/test/parallel/test-util-emit-experimental-warning.js @@ -0,0 +1,19 @@ +'use strict'; +// emitExperimentalWarning is a node internal not used by bun, so this test is skipped + +// // Flags: --expose-internals +// const common = require('../common'); +// const assert = require('assert'); +// const { emitExperimentalWarning } = require('internal/util'); + +// This test ensures that the emitExperimentalWarning in internal/util emits a +// warning when passed an unsupported feature and that it simply returns +// when passed the same feature multiple times. + +// process.on('warning', common.mustCall((warning) => { +// assert.match(warning.message, /is an experimental feature/); +// }, 2)); + +// emitExperimentalWarning('feature1'); +// emitExperimentalWarning('feature1'); // should not warn +// emitExperimentalWarning('feature2'); diff --git a/test/js/node/test/parallel/test-util-inherits.js b/test/js/node/test/parallel/test-util-inherits.js new file mode 100644 index 0000000000..2ff8a84446 --- /dev/null +++ b/test/js/node/test/parallel/test-util-inherits.js @@ -0,0 +1,105 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { inherits } = require('util'); + +// Super constructor +function A() { + this._a = 'a'; +} +A.prototype.a = function() { return this._a; }; + +// One level of inheritance +function B(value) { + A.call(this); + this._b = value; +} +inherits(B, A); +B.prototype.b = function() { return this._b; }; + +assert.deepStrictEqual( + Object.getOwnPropertyDescriptor(B, 'super_'), + { + value: A, + enumerable: false, + configurable: true, + writable: true + } +); + +const b = new B('b'); +assert.strictEqual(b.a(), 'a'); +assert.strictEqual(b.b(), 'b'); +assert.strictEqual(b.constructor, B); + +// Two levels of inheritance +function C() { + B.call(this, 'b'); + this._c = 'c'; +} +inherits(C, B); +C.prototype.c = function() { return this._c; }; +C.prototype.getValue = function() { return this.a() + this.b() + this.c(); }; + +assert.strictEqual(C.super_, B); + +const c = new C(); +assert.strictEqual(c.getValue(), 'abc'); +assert.strictEqual(c.constructor, C); + +// Inherits can be called after setting prototype properties +function D() { + C.call(this); + this._d = 'd'; +} + +D.prototype.d = function() { return this._d; }; +inherits(D, C); + +assert.strictEqual(D.super_, C); + +const d = new D(); +assert.strictEqual(d.c(), 'c'); +assert.strictEqual(d.d(), 'd'); +assert.strictEqual(d.constructor, D); + +// ES6 classes can inherit from a constructor function +class E { + constructor() { + D.call(this); + this._e = 'e'; + } + e() { return this._e; } +} +inherits(E, D); + +assert.strictEqual(E.super_, D); + +const e = new E(); +assert.strictEqual(e.getValue(), 'abc'); +assert.strictEqual(e.d(), 'd'); +assert.strictEqual(e.e(), 'e'); +assert.strictEqual(e.constructor, E); + +// Should throw with invalid arguments +assert.throws(() => { + inherits(A, {}); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', +}); + +assert.throws(() => { + inherits(A, null); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', +}); + +assert.throws(() => { + inherits(null, A); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', +}); diff --git a/test/js/node/test/parallel/test-util-inspect-getters-accessing-this.js b/test/js/node/test/parallel/test-util-inspect-getters-accessing-this.js new file mode 100644 index 0000000000..998cd82db8 --- /dev/null +++ b/test/js/node/test/parallel/test-util-inspect-getters-accessing-this.js @@ -0,0 +1,67 @@ +'use strict'; + +require('../common'); + +// This test ensures that util.inspect logs getters +// which access this. + +const assert = require('assert'); + +const { inspect } = require('util'); + +{ + class X { + constructor() { + this._y = 123; + } + + get y() { + return this._y; + } + } + + const result = inspect(new X(), { + getters: true, + showHidden: true + }); + + assert.strictEqual( + result, + 'X { _y: 123, [y]: [Getter: 123] }' + ); +} + +// Regression test for https://github.com/nodejs/node/issues/37054 +{ + class A { + constructor(B) { + this.B = B; + } + get b() { + return this.B; + } + } + + class B { + constructor() { + this.A = new A(this); + } + get a() { + return this.A; + } + } + + const result = inspect(new B(), { + depth: 1, + getters: true, + showHidden: true + }); + + assert.strictEqual( + result, + ' B {\n' + + ' A: A { B: [Circular *1], [b]: [Getter] [Circular *1] },\n' + + ' [a]: [Getter] A { B: [Circular *1], [b]: [Getter] [Circular *1] }\n' + + '}', + ); +} diff --git a/test/js/node/test/parallel/test-util-inspect-long-running.js b/test/js/node/test/parallel/test-util-inspect-long-running.js new file mode 100644 index 0000000000..167f72ba64 --- /dev/null +++ b/test/js/node/test/parallel/test-util-inspect-long-running.js @@ -0,0 +1,20 @@ +'use strict'; + +require('../common'); + +// Test that huge objects don't crash due to exceeding the maximum heap size. + +const util = require('util'); + +// Create a difficult to stringify object. Without the artificial limitation +// this would crash or throw an maximum string size error. +let last = {}; +const obj = last; + +for (let i = 0; i < 1000; i++) { + last.next = { circular: obj, last, obj: { a: 1, b: 2, c: true } }; + last = last.next; + obj[i] = last; +} + +util.inspect(obj, { depth: Infinity }); diff --git a/test/js/node/test/parallel/test-util-inspect-proxy.js b/test/js/node/test/parallel/test-util-inspect-proxy.js new file mode 100644 index 0000000000..054a544d62 --- /dev/null +++ b/test/js/node/test/parallel/test-util-inspect-proxy.js @@ -0,0 +1,183 @@ +// Flags: --expose-internals +'use strict'; + +// tests testing proxy internals are skipped, as getProxyDetails is not available in bun + +require('../common'); +const assert = require('assert'); +const util = require('util'); +// const { internalBinding } = require('internal/test/binding'); +// const processUtil = internalBinding('util'); +const opts = { showProxy: true }; + +let proxyObj; +let called = false; +const target = { + [util.inspect.custom](depth, { showProxy }) { + if (showProxy === false) { + called = true; + if (proxyObj !== this) { + throw new Error('Failed'); + } + } + return [1, 2, 3]; + } +}; +const handler = { + getPrototypeOf() { throw new Error('getPrototypeOf'); }, + setPrototypeOf() { throw new Error('setPrototypeOf'); }, + isExtensible() { throw new Error('isExtensible'); }, + preventExtensions() { throw new Error('preventExtensions'); }, + getOwnPropertyDescriptor() { throw new Error('getOwnPropertyDescriptor'); }, + defineProperty() { throw new Error('defineProperty'); }, + has() { throw new Error('has'); }, + get() { throw new Error('get'); }, + set() { throw new Error('set'); }, + deleteProperty() { throw new Error('deleteProperty'); }, + ownKeys() { throw new Error('ownKeys'); }, + apply() { throw new Error('apply'); }, + construct() { throw new Error('construct'); } +}; +proxyObj = new Proxy(target, handler); + +// Inspecting the proxy should not actually walk it's properties +util.inspect(proxyObj, opts); + +// Make sure inspecting object does not trigger any proxy traps. +util.format('%s', proxyObj); + +// getProxyDetails is an internal method, not intended for public use. +// This is here to test that the internals are working correctly. +// let details = processUtil.getProxyDetails(proxyObj, true); +// assert.strictEqual(target, details[0]); +// assert.strictEqual(handler, details[1]); + +// details = processUtil.getProxyDetails(proxyObj); +// assert.strictEqual(target, details[0]); +// assert.strictEqual(handler, details[1]); + +// details = processUtil.getProxyDetails(proxyObj, false); +// assert.strictEqual(target, details); + +// details = processUtil.getProxyDetails({}, true); +// assert.strictEqual(details, undefined); + +const r = Proxy.revocable({}, {}); +r.revoke(); + +// details = processUtil.getProxyDetails(r.proxy, true); +// assert.strictEqual(details[0], null); +// assert.strictEqual(details[1], null); + +// details = processUtil.getProxyDetails(r.proxy, false); +// assert.strictEqual(details, null); + +assert.strictEqual(util.inspect(r.proxy), ''); +assert.strictEqual( + util.inspect(r, { showProxy: true }), + '{ proxy: , revoke: [Function (anonymous)] }', +); + +assert.strictEqual(util.format('%s', r.proxy), ''); + +assert.strictEqual( + util.inspect(proxyObj, opts), + 'Proxy [\n' + + ' [ 1, 2, 3 ],\n' + + ' {\n' + + ' getPrototypeOf: [Function: getPrototypeOf],\n' + + ' setPrototypeOf: [Function: setPrototypeOf],\n' + + ' isExtensible: [Function: isExtensible],\n' + + ' preventExtensions: [Function: preventExtensions],\n' + + ' getOwnPropertyDescriptor: [Function: getOwnPropertyDescriptor],\n' + + ' defineProperty: [Function: defineProperty],\n' + + ' has: [Function: has],\n' + + ' get: [Function: get],\n' + + ' set: [Function: set],\n' + + ' deleteProperty: [Function: deleteProperty],\n' + + ' ownKeys: [Function: ownKeys],\n' + + ' apply: [Function: apply],\n' + + ' construct: [Function: construct]\n' + + ' }\n' + + ']' +); + +// // Using getProxyDetails with non-proxy returns undefined +// assert.strictEqual(processUtil.getProxyDetails({}), undefined); + +// Inspecting a proxy without the showProxy option set to true should not +// trigger any proxy handlers. +assert.strictEqual(util.inspect(proxyObj), '[ 1, 2, 3 ]'); +assert(called); + +// Yo dawg, I heard you liked Proxy so I put a Proxy +// inside your Proxy that proxies your Proxy's Proxy. +const proxy1 = new Proxy({}, {}); +const proxy2 = new Proxy(proxy1, {}); +const proxy3 = new Proxy(proxy2, proxy1); +const proxy4 = new Proxy(proxy1, proxy2); +const proxy5 = new Proxy(proxy3, proxy4); +const proxy6 = new Proxy(proxy5, proxy5); +const expected0 = '{}'; +const expected1 = 'Proxy [ {}, {} ]'; +const expected2 = 'Proxy [ Proxy [ {}, {} ], {} ]'; +const expected3 = 'Proxy [ Proxy [ Proxy [ {}, {} ], {} ], Proxy [ {}, {} ] ]'; +const expected4 = 'Proxy [ Proxy [ {}, {} ], Proxy [ Proxy [ {}, {} ], {} ] ]'; +const expected5 = 'Proxy [\n ' + + 'Proxy [ Proxy [ Proxy [Array], {} ], Proxy [ {}, {} ] ],\n' + + ' Proxy [ Proxy [ {}, {} ], Proxy [ Proxy [Array], {} ] ]' + + '\n]'; +const expected6 = 'Proxy [\n' + + ' Proxy [\n' + + ' Proxy [ Proxy [Array], Proxy [Array] ],\n' + + ' Proxy [ Proxy [Array], Proxy [Array] ]\n' + + ' ],\n' + + ' Proxy [\n' + + ' Proxy [ Proxy [Array], Proxy [Array] ],\n' + + ' Proxy [ Proxy [Array], Proxy [Array] ]\n' + + ' ]\n' + + ']'; +assert.strictEqual( + util.inspect(proxy1, { showProxy: 1, depth: null }), + expected1); +assert.strictEqual(util.inspect(proxy2, opts), expected2); +assert.strictEqual(util.inspect(proxy3, opts), expected3); +assert.strictEqual(util.inspect(proxy4, opts), expected4); +assert.strictEqual(util.inspect(proxy5, opts), expected5); +assert.strictEqual(util.inspect(proxy6, opts), expected6); +assert.strictEqual(util.inspect(proxy1), expected0); +assert.strictEqual(util.inspect(proxy2), expected0); +assert.strictEqual(util.inspect(proxy3), expected0); +assert.strictEqual(util.inspect(proxy4), expected0); +assert.strictEqual(util.inspect(proxy5), expected0); +assert.strictEqual(util.inspect(proxy6), expected0); + +// Just for fun, let's create a Proxy using Arrays. +const proxy7 = new Proxy([], []); +const expected7 = 'Proxy [ [], [] ]'; +assert.strictEqual(util.inspect(proxy7, opts), expected7); +assert.strictEqual(util.inspect(proxy7), '[]'); + +// Now we're just getting silly, right? +const proxy8 = new Proxy(Date, []); +const proxy9 = new Proxy(Date, String); +const expected8 = 'Proxy [ [Function: Date], [] ]'; +const expected9 = 'Proxy [ [Function: Date], [Function: String] ]'; +assert.strictEqual(util.inspect(proxy8, opts), expected8); +assert.strictEqual(util.inspect(proxy9, opts), expected9); +assert.strictEqual(util.inspect(proxy8), '[Function: Date]'); +assert.strictEqual(util.inspect(proxy9), '[Function: Date]'); + +const proxy10 = new Proxy(() => {}, {}); +const proxy11 = new Proxy(() => {}, { + get() { + return proxy11; + }, + apply() { + return proxy11; + } +}); +const expected10 = '[Function (anonymous)]'; +const expected11 = '[Function (anonymous)]'; +assert.strictEqual(util.inspect(proxy10), expected10); +assert.strictEqual(util.inspect(proxy11), expected11); diff --git a/test/js/node/test/parallel/test-util-primordial-monkeypatching.js b/test/js/node/test/parallel/test-util-primordial-monkeypatching.js new file mode 100644 index 0000000000..bf282a1212 --- /dev/null +++ b/test/js/node/test/parallel/test-util-primordial-monkeypatching.js @@ -0,0 +1,11 @@ +'use strict'; + +// Monkeypatch Object.keys() so that it throws an unexpected error. This tests +// that `util.inspect()` is unaffected by monkey-patching `Object`. + +require('../common'); +const assert = require('assert'); +const util = require('util'); + +Object.keys = () => { throw new Error('fhqwhgads'); }; +assert.strictEqual(util.inspect({}), '{}'); diff --git a/test/js/node/test/parallel/test-util-stripvtcontrolcharacters.js b/test/js/node/test/parallel/test-util-stripvtcontrolcharacters.js new file mode 100644 index 0000000000..a201c78976 --- /dev/null +++ b/test/js/node/test/parallel/test-util-stripvtcontrolcharacters.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); +const util = require('util'); +const assert = require('node:assert'); + +// Ref: https://github.com/chalk/ansi-regex/blob/main/test.js +const tests = [ + // [before, expected] + ['\u001B[0m\u001B[4m\u001B[42m\u001B[31mfoo\u001B[39m\u001B[49m\u001B[24mfoo\u001B[0m', 'foofoo'], // Basic ANSI + ['\u001B[0;33;49;3;9;4mbar\u001B[0m', 'bar'], // Advanced colors + ['foo\u001B[0gbar', 'foobar'], // Clear tabs + ['foo\u001B[Kbar', 'foobar'], // Clear line + ['foo\u001B[2Jbar', 'foobar'], // Clear screen +]; + +for (const ST of ['\u0007', '\u001B\u005C', '\u009C']) { + tests.push( + [`\u001B]8;;mailto:no-replay@mail.com${ST}mail\u001B]8;;${ST}`, 'mail'], + [`\u001B]8;k=v;https://example-a.com/?a_b=1&c=2#tit%20le${ST}click\u001B]8;;${ST}`, 'click'], + ); +} + +for (const [before, expected] of tests) { + assert.strictEqual(util.stripVTControlCharacters(before), expected); +} diff --git a/test/js/node/test/parallel/test-util-styletext.js b/test/js/node/test/parallel/test-util-styletext.js new file mode 100644 index 0000000000..6baa6a60ea --- /dev/null +++ b/test/js/node/test/parallel/test-util-styletext.js @@ -0,0 +1,43 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const util = require('util'); + +[ + undefined, + null, + false, + 5n, + 5, + Symbol(), + () => {}, + {}, +].forEach((invalidOption) => { + assert.throws(() => { + util.styleText(invalidOption, 'test'); + }, { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => { + util.styleText('red', invalidOption); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +assert.throws(() => { + util.styleText('invalid', 'text'); +}, { + code: 'ERR_INVALID_ARG_VALUE', +}); + +assert.strictEqual(util.styleText('red', 'test'), '\u001b[31mtest\u001b[39m'); + +assert.strictEqual(util.styleText(['bold', 'red'], 'test'), '\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m'); +assert.strictEqual(util.styleText(['bold', 'red'], 'test'), util.styleText('bold', util.styleText('red', 'test'))); + +assert.throws(() => { + util.styleText(['invalid'], 'text'); +}, { + code: 'ERR_INVALID_ARG_VALUE', +}); diff --git a/test/js/node/test/parallel/test-util-types.js b/test/js/node/test/parallel/test-util-types.js new file mode 100644 index 0000000000..755209036f --- /dev/null +++ b/test/js/node/test/parallel/test-util-types.js @@ -0,0 +1,296 @@ +'use strict'; +// JSStream is disabled because it is a node internal +// vm tests are skipped because node:vm Module is not yet implemented +// // Flags: --experimental-vm-modules --expose-internals +const common = require('../common'); +const assert = require('assert'); +const { types, inspect } = require('util'); +const vm = require('vm'); +// const { internalBinding } = require('internal/test/binding'); +// const { JSStream } = internalBinding('js_stream'); + +// const external = (new JSStream())._externalStream; + +for (const [ value, _method ] of [ + // [ external, 'isExternal' ], + [ new Date() ], + [ (function() { return arguments; })(), 'isArgumentsObject' ], + [ new Boolean(), 'isBooleanObject' ], + [ new Number(), 'isNumberObject' ], + [ new String(), 'isStringObject' ], + [ Object(Symbol()), 'isSymbolObject' ], + [ Object(BigInt(0)), 'isBigIntObject' ], + [ new Error(), 'isNativeError' ], + [ new RegExp() ], + [ async function() {}, 'isAsyncFunction' ], + [ function*() {}, 'isGeneratorFunction' ], + [ (function*() {})(), 'isGeneratorObject' ], + [ Promise.resolve() ], + [ new Map() ], + [ new Set() ], + [ (new Map())[Symbol.iterator](), 'isMapIterator' ], + [ (new Set())[Symbol.iterator](), 'isSetIterator' ], + [ new WeakMap() ], + [ new WeakSet() ], + [ new ArrayBuffer() ], + [ new Uint8Array() ], + [ new Uint8ClampedArray() ], + [ new Uint16Array() ], + [ new Uint32Array() ], + [ new Int8Array() ], + [ new Int16Array() ], + [ new Int32Array() ], + [ new Float32Array() ], + [ new Float64Array() ], + [ new BigInt64Array() ], + [ new BigUint64Array() ], + [ Object.defineProperty(new Uint8Array(), + Symbol.toStringTag, + { value: 'foo' }) ], + [ new DataView(new ArrayBuffer()) ], + [ new SharedArrayBuffer() ], + [ new Proxy({}, {}), 'isProxy' ], +]) { + const method = _method || `is${value.constructor.name}`; + assert(method in types, `Missing ${method} for ${inspect(value)}`); + assert(types[method](value), `Want ${inspect(value)} to match ${method}`); + + for (const key of Object.keys(types)) { + if ((types.isArrayBufferView(value) || + types.isAnyArrayBuffer(value)) && key.includes('Array') || + key === 'isBoxedPrimitive') { + continue; + } + + assert.strictEqual(types[key](value), + key === method, + `${inspect(value)}: ${key}, ` + + `${method}, ${types[key](value)}`); + } +} + +// Check boxed primitives. +[ + new Boolean(), + new Number(), + new String(), + Object(Symbol()), + Object(BigInt(0)), +].forEach((entry) => assert(types.isBoxedPrimitive(entry))); + +{ + assert(!types.isUint8Array({ [Symbol.toStringTag]: 'Uint8Array' })); + assert(types.isUint8Array(vm.runInNewContext('new Uint8Array'))); + + assert(!types.isUint8ClampedArray({ + [Symbol.toStringTag]: 'Uint8ClampedArray' + })); + assert(types.isUint8ClampedArray( + vm.runInNewContext('new Uint8ClampedArray') + )); + + assert(!types.isUint16Array({ [Symbol.toStringTag]: 'Uint16Array' })); + assert(types.isUint16Array(vm.runInNewContext('new Uint16Array'))); + + assert(!types.isUint32Array({ [Symbol.toStringTag]: 'Uint32Array' })); + assert(types.isUint32Array(vm.runInNewContext('new Uint32Array'))); + + assert(!types.isInt8Array({ [Symbol.toStringTag]: 'Int8Array' })); + assert(types.isInt8Array(vm.runInNewContext('new Int8Array'))); + + assert(!types.isInt16Array({ [Symbol.toStringTag]: 'Int16Array' })); + assert(types.isInt16Array(vm.runInNewContext('new Int16Array'))); + + assert(!types.isInt32Array({ [Symbol.toStringTag]: 'Int32Array' })); + assert(types.isInt32Array(vm.runInNewContext('new Int32Array'))); + + assert(!types.isFloat32Array({ [Symbol.toStringTag]: 'Float32Array' })); + assert(types.isFloat32Array(vm.runInNewContext('new Float32Array'))); + + assert(!types.isFloat64Array({ [Symbol.toStringTag]: 'Float64Array' })); + assert(types.isFloat64Array(vm.runInNewContext('new Float64Array'))); + + assert(!types.isBigInt64Array({ [Symbol.toStringTag]: 'BigInt64Array' })); + assert(types.isBigInt64Array(vm.runInNewContext('new BigInt64Array'))); + + assert(!types.isBigUint64Array({ [Symbol.toStringTag]: 'BigUint64Array' })); + assert(types.isBigUint64Array(vm.runInNewContext('new BigUint64Array'))); +} + +{ + const primitive = true; + const arrayBuffer = new ArrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const dataView = new DataView(arrayBuffer); + const uint8Array = new Uint8Array(arrayBuffer); + const uint8ClampedArray = new Uint8ClampedArray(arrayBuffer); + const uint16Array = new Uint16Array(arrayBuffer); + const uint32Array = new Uint32Array(arrayBuffer); + const int8Array = new Int8Array(arrayBuffer); + const int16Array = new Int16Array(arrayBuffer); + const int32Array = new Int32Array(arrayBuffer); + const float32Array = new Float32Array(arrayBuffer); + const float64Array = new Float64Array(arrayBuffer); + const bigInt64Array = new BigInt64Array(arrayBuffer); + const bigUint64Array = new BigUint64Array(arrayBuffer); + + const fakeBuffer = { __proto__: Buffer.prototype }; + const fakeDataView = { __proto__: DataView.prototype }; + const fakeUint8Array = { __proto__: Uint8Array.prototype }; + const fakeUint8ClampedArray = { __proto__: Uint8ClampedArray.prototype }; + const fakeUint16Array = { __proto__: Uint16Array.prototype }; + const fakeUint32Array = { __proto__: Uint32Array.prototype }; + const fakeInt8Array = { __proto__: Int8Array.prototype }; + const fakeInt16Array = { __proto__: Int16Array.prototype }; + const fakeInt32Array = { __proto__: Int32Array.prototype }; + const fakeFloat32Array = { __proto__: Float32Array.prototype }; + const fakeFloat64Array = { __proto__: Float64Array.prototype }; + const fakeBigInt64Array = { __proto__: BigInt64Array.prototype }; + const fakeBigUint64Array = { __proto__: BigUint64Array.prototype }; + + const stealthyDataView = + Object.setPrototypeOf(new DataView(arrayBuffer), Uint8Array.prototype); + const stealthyUint8Array = + Object.setPrototypeOf(new Uint8Array(arrayBuffer), ArrayBuffer.prototype); + const stealthyUint8ClampedArray = + Object.setPrototypeOf( + new Uint8ClampedArray(arrayBuffer), ArrayBuffer.prototype + ); + const stealthyUint16Array = + Object.setPrototypeOf(new Uint16Array(arrayBuffer), Uint16Array.prototype); + const stealthyUint32Array = + Object.setPrototypeOf(new Uint32Array(arrayBuffer), Uint32Array.prototype); + const stealthyInt8Array = + Object.setPrototypeOf(new Int8Array(arrayBuffer), Int8Array.prototype); + const stealthyInt16Array = + Object.setPrototypeOf(new Int16Array(arrayBuffer), Int16Array.prototype); + const stealthyInt32Array = + Object.setPrototypeOf(new Int32Array(arrayBuffer), Int32Array.prototype); + const stealthyFloat32Array = + Object.setPrototypeOf( + new Float32Array(arrayBuffer), Float32Array.prototype + ); + const stealthyFloat64Array = + Object.setPrototypeOf( + new Float64Array(arrayBuffer), Float64Array.prototype + ); + const stealthyBigInt64Array = + Object.setPrototypeOf( + new BigInt64Array(arrayBuffer), BigInt64Array.prototype + ); + const stealthyBigUint64Array = + Object.setPrototypeOf( + new BigUint64Array(arrayBuffer), BigUint64Array.prototype + ); + + const all = [ + primitive, arrayBuffer, buffer, fakeBuffer, + dataView, fakeDataView, stealthyDataView, + uint8Array, fakeUint8Array, stealthyUint8Array, + uint8ClampedArray, fakeUint8ClampedArray, stealthyUint8ClampedArray, + uint16Array, fakeUint16Array, stealthyUint16Array, + uint32Array, fakeUint32Array, stealthyUint32Array, + int8Array, fakeInt8Array, stealthyInt8Array, + int16Array, fakeInt16Array, stealthyInt16Array, + int32Array, fakeInt32Array, stealthyInt32Array, + float32Array, fakeFloat32Array, stealthyFloat32Array, + float64Array, fakeFloat64Array, stealthyFloat64Array, + bigInt64Array, fakeBigInt64Array, stealthyBigInt64Array, + bigUint64Array, fakeBigUint64Array, stealthyBigUint64Array, + ]; + + const expected = { + isArrayBufferView: [ + buffer, + dataView, stealthyDataView, + uint8Array, stealthyUint8Array, + uint8ClampedArray, stealthyUint8ClampedArray, + uint16Array, stealthyUint16Array, + uint32Array, stealthyUint32Array, + int8Array, stealthyInt8Array, + int16Array, stealthyInt16Array, + int32Array, stealthyInt32Array, + float32Array, stealthyFloat32Array, + float64Array, stealthyFloat64Array, + bigInt64Array, stealthyBigInt64Array, + bigUint64Array, stealthyBigUint64Array, + ], + isTypedArray: [ + buffer, + uint8Array, stealthyUint8Array, + uint8ClampedArray, stealthyUint8ClampedArray, + uint16Array, stealthyUint16Array, + uint32Array, stealthyUint32Array, + int8Array, stealthyInt8Array, + int16Array, stealthyInt16Array, + int32Array, stealthyInt32Array, + float32Array, stealthyFloat32Array, + float64Array, stealthyFloat64Array, + bigInt64Array, stealthyBigInt64Array, + bigUint64Array, stealthyBigUint64Array, + ], + isUint8Array: [ + buffer, uint8Array, stealthyUint8Array, + ], + isUint8ClampedArray: [ + uint8ClampedArray, stealthyUint8ClampedArray, + ], + isUint16Array: [ + uint16Array, stealthyUint16Array, + ], + isUint32Array: [ + uint32Array, stealthyUint32Array, + ], + isInt8Array: [ + int8Array, stealthyInt8Array, + ], + isInt16Array: [ + int16Array, stealthyInt16Array, + ], + isInt32Array: [ + int32Array, stealthyInt32Array, + ], + isFloat32Array: [ + float32Array, stealthyFloat32Array, + ], + isFloat64Array: [ + float64Array, stealthyFloat64Array, + ], + isBigInt64Array: [ + bigInt64Array, stealthyBigInt64Array, + ], + isBigUint64Array: [ + bigUint64Array, stealthyBigUint64Array, + ] + }; + + for (const testedFunc of Object.keys(expected)) { + const func = types[testedFunc]; + const yup = []; + for (const value of all) { + if (func(value)) { + yup.push(value); + } + } + console.log('Testing', testedFunc); + assert.deepStrictEqual(yup, expected[testedFunc]); + } +} + +// (skipped) +// (async () => { +// const m = new vm.SourceTextModule(''); +// await m.link(() => 0); +// await m.evaluate(); +// assert.ok(types.isModuleNamespaceObject(m.namespace)); +// })().then(common.mustCall()); + +{ + // eslint-disable-next-line node-core/crypto-check + if (common.hasCrypto) { + const crypto = require('crypto'); + assert.ok(!types.isKeyObject(crypto.createHash('sha1'))); + } + assert.ok(!types.isCryptoKey()); + assert.ok(!types.isKeyObject()); +} diff --git a/test/js/node/test/parallel/test-v8-deserialize-buffer.js b/test/js/node/test/parallel/test-v8-deserialize-buffer.js new file mode 100644 index 0000000000..f05631a72a --- /dev/null +++ b/test/js/node/test/parallel/test-v8-deserialize-buffer.js @@ -0,0 +1,7 @@ +'use strict'; + +const common = require('../common'); +const v8 = require('v8'); + +process.on('warning', common.mustNotCall()); +v8.deserialize(v8.serialize(Buffer.alloc(0))); diff --git a/test/js/node/test/parallel/test-v8-global-setter.js b/test/js/node/test/parallel/test-v8-global-setter.js new file mode 100644 index 0000000000..1cb0898e61 --- /dev/null +++ b/test/js/node/test/parallel/test-v8-global-setter.js @@ -0,0 +1,29 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test ensures v8 correctly sets a property on the global object if it +// has a setter interceptor in strict mode. +// https://github.com/nodejs/node-v0.x-archive/issues/6235 + +require('vm').runInNewContext('"use strict"; var v = 1; v = 2'); diff --git a/test/js/node/test/parallel/test-vm-access-process-env.js b/test/js/node/test/parallel/test-vm-access-process-env.js new file mode 100644 index 0000000000..c6b18ec902 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-access-process-env.js @@ -0,0 +1,33 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Tests that node does neither crash nor throw an error when accessing +// process.env when inside a VM context. +// See https://github.com/nodejs/node-v0.x-archive/issues/7511. + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const context = vm.createContext({ process }); +const result = vm.runInContext('process.env["PATH"]', context); +assert.notStrictEqual(undefined, result); diff --git a/test/js/node/test/parallel/test-vm-attributes-property-not-on-sandbox.js b/test/js/node/test/parallel/test-vm-attributes-property-not-on-sandbox.js new file mode 100644 index 0000000000..313dd71e47 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-attributes-property-not-on-sandbox.js @@ -0,0 +1,18 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// Assert that accessor descriptors are not flattened on the sandbox. +// Issue: https://github.com/nodejs/node/issues/2734 +const sandbox = {}; +vm.createContext(sandbox); +const code = `Object.defineProperty( + this, + 'foo', + { get: function() {return 17} } + ); + var desc = Object.getOwnPropertyDescriptor(this, 'foo');`; + +vm.runInContext(code, sandbox); +assert.strictEqual(typeof sandbox.desc.get, 'function'); diff --git a/test/js/node/test/parallel/test-vm-context-async-script.js b/test/js/node/test/parallel/test-vm-context-async-script.js new file mode 100644 index 0000000000..879315e37b --- /dev/null +++ b/test/js/node/test/parallel/test-vm-context-async-script.js @@ -0,0 +1,35 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const sandbox = { setTimeout }; + +const ctx = vm.createContext(sandbox); + +vm.runInContext('setTimeout(function() { x = 3; }, 0);', ctx); +setTimeout(common.mustCall(() => { + assert.strictEqual(sandbox.x, 3); + assert.strictEqual(ctx.x, 3); +}), 1); diff --git a/test/js/node/test/parallel/test-vm-context-property-forwarding.js b/test/js/node/test/parallel/test-vm-context-property-forwarding.js new file mode 100644 index 0000000000..53d38c1467 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-context-property-forwarding.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const sandbox = { x: 3 }; + +const ctx = vm.createContext(sandbox); + +assert.strictEqual(vm.runInContext('x;', ctx), 3); +vm.runInContext('y = 4;', ctx); +assert.strictEqual(sandbox.y, 4); +assert.strictEqual(ctx.y, 4); + +// Test `IndexedPropertyGetterCallback` and `IndexedPropertyDeleterCallback` +const x = { get 1() { return 5; } }; +const pd_expected = Object.getOwnPropertyDescriptor(x, 1); +const ctx2 = vm.createContext(x); +const pd_actual = Object.getOwnPropertyDescriptor(ctx2, 1); + +assert.deepStrictEqual(pd_actual, pd_expected); +assert.strictEqual(ctx2[1], 5); +delete ctx2[1]; +assert.strictEqual(ctx2[1], undefined); + +// https://github.com/nodejs/node/issues/33806 +{ + const ctx = vm.createContext(); + + Object.defineProperty(ctx, 'prop', { + get() { + return undefined; + }, + set(val) { + throw new Error('test error'); + }, + }); + + assert.throws(() => { + vm.runInContext('prop = 42', ctx); + }, { + message: 'test error', + }); +} diff --git a/test/js/node/test/parallel/test-vm-create-and-run-in-context.js b/test/js/node/test/parallel/test-vm-create-and-run-in-context.js new file mode 100644 index 0000000000..bd746cf2df --- /dev/null +++ b/test/js/node/test/parallel/test-vm-create-and-run-in-context.js @@ -0,0 +1,50 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Flags: --expose-gc +require('../common'); +const assert = require('assert'); + +const vm = require('vm'); + +// Run in a new empty context +let context = vm.createContext(); +let result = vm.runInContext('"passed";', context); +assert.strictEqual(result, 'passed'); + +// Create a new pre-populated context +context = vm.createContext({ 'foo': 'bar', 'thing': 'lala' }); +assert.strictEqual(context.foo, 'bar'); +assert.strictEqual(context.thing, 'lala'); + +// Test updating context +result = vm.runInContext('var foo = 3;', context); +assert.strictEqual(context.foo, 3); +assert.strictEqual(context.thing, 'lala'); + +// https://github.com/nodejs/node/issues/5768 +// Run in contextified sandbox without referencing the context +const sandbox = { x: 1 }; +vm.createContext(sandbox); +global.gc(); +vm.runInContext('x = 2', sandbox); +// Should not crash. diff --git a/test/js/node/test/parallel/test-vm-create-context-accessors.js b/test/js/node/test/parallel/test-vm-create-context-accessors.js new file mode 100644 index 0000000000..39fc5c4eec --- /dev/null +++ b/test/js/node/test/parallel/test-vm-create-context-accessors.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +let ctx = {}; + +Object.defineProperty(ctx, 'getter', { + get: function() { + return 'ok'; + } +}); + +let val; +Object.defineProperty(ctx, 'setter', { + set: function(_val) { + val = _val; + }, + get: function() { + return `ok=${val}`; + } +}); + +ctx = vm.createContext(ctx); + +const result = vm.runInContext('setter = "test";[getter,setter]', ctx); +assert.strictEqual(result[0], 'ok'); +assert.strictEqual(result[1], 'ok=test'); diff --git a/test/js/node/test/parallel/test-vm-create-context-arg.js b/test/js/node/test/parallel/test-vm-create-context-arg.js new file mode 100644 index 0000000000..c07079cb6c --- /dev/null +++ b/test/js/node/test/parallel/test-vm-create-context-arg.js @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +assert.throws(() => { + vm.createContext('string is not supported'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +// Should not throw. +vm.createContext({ a: 1 }); +vm.createContext([0, 1, 2, 3]); + +const sandbox = {}; +vm.createContext(sandbox); +vm.createContext(sandbox); diff --git a/test/js/node/test/parallel/test-vm-create-context-circular-reference.js b/test/js/node/test/parallel/test-vm-create-context-circular-reference.js new file mode 100644 index 0000000000..7ea22781bd --- /dev/null +++ b/test/js/node/test/parallel/test-vm-create-context-circular-reference.js @@ -0,0 +1,34 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +let sbx = {}; +sbx.window = sbx; + +sbx = vm.createContext(sbx); + +sbx.test = 123; + +assert.strictEqual(sbx.window.window.window.window.window.test, 123); diff --git a/test/js/node/test/parallel/test-vm-cross-context.js b/test/js/node/test/parallel/test-vm-cross-context.js new file mode 100644 index 0000000000..b7cf1309d3 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-cross-context.js @@ -0,0 +1,29 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +const vm = require('vm'); +const ctx = vm.createContext(global); + +// Should not throw. +vm.runInContext('!function() { var x = console.log; }()', ctx); diff --git a/test/js/node/test/parallel/test-vm-data-property-writable.js b/test/js/node/test/parallel/test-vm-data-property-writable.js new file mode 100644 index 0000000000..00937ae412 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-data-property-writable.js @@ -0,0 +1,28 @@ +'use strict'; +// Refs: https://github.com/nodejs/node/issues/10223 + +require('../common'); +const vm = require('vm'); +const assert = require('assert'); + +const context = vm.createContext({}); + +let code = ` + Object.defineProperty(this, 'foo', {value: 5}); + Object.getOwnPropertyDescriptor(this, 'foo'); +`; + +let desc = vm.runInContext(code, context); + +assert.strictEqual(desc.writable, false); + +// Check that interceptors work for symbols. +code = ` + const bar = Symbol('bar'); + Object.defineProperty(this, bar, {value: 6}); + Object.getOwnPropertyDescriptor(this, bar); +`; + +desc = vm.runInContext(code, context); + +assert.strictEqual(desc.value, 6); diff --git a/test/js/node/test/parallel/test-vm-deleting-property.js b/test/js/node/test/parallel/test-vm-deleting-property.js new file mode 100644 index 0000000000..994aa0aff9 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-deleting-property.js @@ -0,0 +1,15 @@ +'use strict'; +// Refs: https://github.com/nodejs/node/issues/6287 + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const context = vm.createContext(); +const res = vm.runInContext(` + this.x = 'prop'; + delete this.x; + Object.getOwnPropertyDescriptor(this, 'x'); +`, context); + +assert.strictEqual(res, undefined); diff --git a/test/js/node/test/parallel/test-vm-function-declaration.js b/test/js/node/test/parallel/test-vm-function-declaration.js new file mode 100644 index 0000000000..766e5ec78b --- /dev/null +++ b/test/js/node/test/parallel/test-vm-function-declaration.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const vm = require('vm'); +const o = vm.createContext({ console }); + +// Function declaration and expression should both be copied to the +// sandboxed context. +let code = 'let a = function() {};\n'; +code += 'function b(){}\n'; +code += 'var c = function() {};\n'; +code += 'var d = () => {};\n'; +code += 'let e = () => {};\n'; + +// Grab the global b function as the completion value, to ensure that +// we are getting the global function, and not some other thing +code += '(function(){return this})().b;\n'; + +const res = vm.runInContext(code, o, 'test'); +assert.strictEqual(typeof res, 'function'); +assert.strictEqual(res.name, 'b'); +assert.strictEqual(typeof o.a, 'undefined'); +assert.strictEqual(typeof o.b, 'function'); +assert.strictEqual(typeof o.c, 'function'); +assert.strictEqual(typeof o.d, 'function'); +assert.strictEqual(typeof o.e, 'undefined'); +assert.strictEqual(res, o.b); diff --git a/test/js/node/test/parallel/test-vm-function-redefinition.js b/test/js/node/test/parallel/test-vm-function-redefinition.js new file mode 100644 index 0000000000..fa1ddde389 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-function-redefinition.js @@ -0,0 +1,11 @@ +'use strict'; +// Refs: https://github.com/nodejs/node/issues/548 +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const context = vm.createContext(); + +vm.runInContext('function test() { return 0; }', context); +vm.runInContext('function test() { return 1; }', context); +const result = vm.runInContext('test()', context); +assert.strictEqual(result, 1); diff --git a/test/js/node/test/parallel/test-vm-getters.js b/test/js/node/test/parallel/test-vm-getters.js new file mode 100644 index 0000000000..af27eeee64 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-getters.js @@ -0,0 +1,24 @@ +'use strict'; +// Refs: https://github.com/nodejs/node/issues/2734 +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const sandbox = {}; + +Object.defineProperty(sandbox, 'prop', { + get() { + return 'foo'; + } +}); + +const descriptor = Object.getOwnPropertyDescriptor(sandbox, 'prop'); +const context = vm.createContext(sandbox); +const code = 'Object.getOwnPropertyDescriptor(this, "prop");'; +const result = vm.runInContext(code, context); + +// Ref: https://github.com/nodejs/node/issues/11803 + +assert.deepStrictEqual(Object.keys(result), Object.keys(descriptor)); +for (const prop of Object.keys(result)) { + assert.strictEqual(result[prop], descriptor[prop]); +} diff --git a/test/js/node/test/parallel/test-vm-global-assignment.js b/test/js/node/test/parallel/test-vm-global-assignment.js new file mode 100644 index 0000000000..3fb3470b4c --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-assignment.js @@ -0,0 +1,15 @@ +'use strict'; +// Regression test for https://github.com/nodejs/node/issues/10806 + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const ctx = vm.createContext({ open() { } }); +const window = vm.runInContext('this', ctx); +const other = 123; + +assert.notStrictEqual(window.open, other); +window.open = other; +assert.strictEqual(window.open, other); +window.open = other; +assert.strictEqual(window.open, other); diff --git a/test/js/node/test/parallel/test-vm-global-configurable-properties.js b/test/js/node/test/parallel/test-vm-global-configurable-properties.js new file mode 100644 index 0000000000..4428e747ea --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-configurable-properties.js @@ -0,0 +1,15 @@ +'use strict'; +// https://github.com/nodejs/node/issues/47799 + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const ctx = vm.createContext(); + +const window = vm.runInContext('this', ctx); + +Object.defineProperty(window, 'x', { value: '1', configurable: true }); +assert.strictEqual(window.x, '1'); +Object.defineProperty(window, 'x', { value: '2', configurable: true }); +assert.strictEqual(window.x, '2'); diff --git a/test/js/node/test/parallel/test-vm-global-define-property.js b/test/js/node/test/parallel/test-vm-global-define-property.js new file mode 100644 index 0000000000..0b9a4dfb88 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-define-property.js @@ -0,0 +1,47 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +const vm = require('vm'); + +const code = + 'Object.defineProperty(this, "f", {\n' + + ' get: function() { return x; },\n' + + ' set: function(k) { x = k; },\n' + + ' configurable: true,\n' + + ' enumerable: true\n' + + '});\n' + + 'g = f;\n' + + 'f;\n'; + +const x = {}; +const o = vm.createContext({ console, x }); + +const res = vm.runInContext(code, o, 'test'); + +assert(res); +assert.strictEqual(typeof res, 'object'); +assert.strictEqual(res, x); +assert.strictEqual(o.f, res); +assert.deepStrictEqual(Object.keys(o), ['console', 'x', 'f', 'g']); diff --git a/test/js/node/test/parallel/test-vm-global-get-own.js b/test/js/node/test/parallel/test-vm-global-get-own.js new file mode 100644 index 0000000000..246fcbf866 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-get-own.js @@ -0,0 +1,105 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// These assertions check that we can set new keys to the global context, +// get them back and also list them via getOwnProperty* or in. +// +// Related to: +// - https://github.com/nodejs/node/issues/45983 + +const global = vm.runInContext('this', vm.createContext()); + +function runAssertions(data, property, viaDefine, value1, value2, value3) { + // Define the property for the first time + setPropertyAndAssert(data, property, viaDefine, value1); + // Update the property + setPropertyAndAssert(data, property, viaDefine, value2); + // Delete the property + deletePropertyAndAssert(data, property); + // Re-define the property + setPropertyAndAssert(data, property, viaDefine, value3); + // Delete the property again + deletePropertyAndAssert(data, property); +} + +const fun1 = () => 1; +const fun2 = () => 2; +const fun3 = () => 3; + +function runAssertionsOnSandbox(builder) { + const sandboxContext = vm.createContext({ runAssertions, fun1, fun2, fun3 }); + vm.runInContext(builder('this'), sandboxContext); + vm.runInContext(builder('{}'), sandboxContext); +} + +// Assertions on: define property +runAssertions(global, 'toto', true, 1, 2, 3); +runAssertions(global, Symbol.for('toto'), true, 1, 2, 3); +runAssertions(global, 'tutu', true, fun1, fun2, fun3); +runAssertions(global, Symbol.for('tutu'), true, fun1, fun2, fun3); +runAssertions(global, 'tyty', true, fun1, 2, 3); +runAssertions(global, Symbol.for('tyty'), true, fun1, 2, 3); + +// Assertions on: direct assignment +runAssertions(global, 'titi', false, 1, 2, 3); +runAssertions(global, Symbol.for('titi'), false, 1, 2, 3); +runAssertions(global, 'tata', false, fun1, fun2, fun3); +runAssertions(global, Symbol.for('tata'), false, fun1, fun2, fun3); +runAssertions(global, 'tztz', false, fun1, 2, 3); +runAssertions(global, Symbol.for('tztz'), false, fun1, 2, 3); + +// Assertions on: define property from sandbox +runAssertionsOnSandbox( + (variable) => ` + runAssertions(${variable}, 'toto', true, 1, 2, 3); + runAssertions(${variable}, Symbol.for('toto'), true, 1, 2, 3); + runAssertions(${variable}, 'tutu', true, fun1, fun2, fun3); + runAssertions(${variable}, Symbol.for('tutu'), true, fun1, fun2, fun3); + runAssertions(${variable}, 'tyty', true, fun1, 2, 3); + runAssertions(${variable}, Symbol.for('tyty'), true, fun1, 2, 3);` +); + +// Assertions on: direct assignment from sandbox +runAssertionsOnSandbox( + (variable) => ` + runAssertions(${variable}, 'titi', false, 1, 2, 3); + runAssertions(${variable}, Symbol.for('titi'), false, 1, 2, 3); + runAssertions(${variable}, 'tata', false, fun1, fun2, fun3); + runAssertions(${variable}, Symbol.for('tata'), false, fun1, fun2, fun3); + runAssertions(${variable}, 'tztz', false, fun1, 2, 3); + runAssertions(${variable}, Symbol.for('tztz'), false, fun1, 2, 3);` +); + +// Helpers + +// Set the property on data and assert it worked +function setPropertyAndAssert(data, property, viaDefine, value) { + if (viaDefine) { + Object.defineProperty(data, property, { + enumerable: true, + writable: true, + value: value, + configurable: true, + }); + } else { + data[property] = value; + } + assert.strictEqual(data[property], value); + assert.ok(property in data); + if (typeof property === 'string') { + assert.ok(Object.getOwnPropertyNames(data).includes(property)); + } else { + assert.ok(Object.getOwnPropertySymbols(data).includes(property)); + } +} + +// Delete the property from data and assert it worked +function deletePropertyAndAssert(data, property) { + delete data[property]; + assert.strictEqual(data[property], undefined); + assert.ok(!(property in data)); + assert.ok(!Object.getOwnPropertyNames(data).includes(property)); + assert.ok(!Object.getOwnPropertySymbols(data).includes(property)); +} diff --git a/test/js/node/test/parallel/test-vm-global-non-writable-properties.js b/test/js/node/test/parallel/test-vm-global-non-writable-properties.js new file mode 100644 index 0000000000..771c9794bd --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-non-writable-properties.js @@ -0,0 +1,15 @@ +'use strict'; +// https://github.com/nodejs/node/issues/10223 + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const ctx = vm.createContext(); +vm.runInContext('Object.defineProperty(this, "x", { value: 42 })', ctx); +assert.strictEqual(vm.runInContext('x', ctx), 42); +vm.runInContext('x = 0', ctx); // Does not throw but x... +assert.strictEqual(vm.runInContext('x', ctx), 42); // ...should be unaltered. +assert.throws(() => vm.runInContext('"use strict"; x = 0', ctx), + /Attempted to assign to readonly property./); +assert.strictEqual(vm.runInContext('x', ctx), 42); diff --git a/test/js/node/test/parallel/test-vm-global-property-enumerator.js b/test/js/node/test/parallel/test-vm-global-property-enumerator.js new file mode 100644 index 0000000000..7b37c2af41 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-property-enumerator.js @@ -0,0 +1,49 @@ +'use strict'; +require('../common'); +const vm = require('vm'); +const assert = require('assert'); + +// Regression of https://github.com/nodejs/node/issues/53346 + +const cases = [ + { + get key() { + return 'value'; + }, + }, + { + // Intentionally single setter. + // eslint-disable-next-line accessor-pairs + set key(value) {}, + }, + {}, + { + key: 'value', + }, + (new class GetterObject { + get key() { + return 'value'; + } + }()), + (new class SetterObject { + // Intentionally single setter. + // eslint-disable-next-line accessor-pairs + set key(value) { + // noop + } + }()), + [], + [['key', 'value']], + { + __proto__: { + key: 'value', + }, + }, +]; + +for (const [idx, obj] of cases.entries()) { + const ctx = vm.createContext(obj); + const globalObj = vm.runInContext('this', ctx); + const keys = Object.keys(globalObj); + assert.deepStrictEqual(keys, Object.keys(obj), `Case ${idx} failed`); +} diff --git a/test/js/node/test/parallel/test-vm-global-property-interceptors.js b/test/js/node/test/parallel/test-vm-global-property-interceptors.js new file mode 100644 index 0000000000..c80ed04e3e --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-property-interceptors.js @@ -0,0 +1,129 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const dSymbol = Symbol('d'); +const sandbox = { + a: 'a', + dSymbol +}; + +Object.defineProperties(sandbox, { + b: { + value: 'b' + }, + c: { + value: 'c', + writable: true, + enumerable: true + }, + [dSymbol]: { + value: 'd' + }, + e: { + value: 'e', + configurable: true + }, + f: {} +}); + +const ctx = vm.createContext(sandbox); + +const result = vm.runInContext(` +const getDesc = (prop) => Object.getOwnPropertyDescriptor(this, prop); +const result = { + a: getDesc('a'), + b: getDesc('b'), + c: getDesc('c'), + d: getDesc(dSymbol), + e: getDesc('e'), + f: getDesc('f'), + g: getDesc('g') +}; +result; +`, ctx); + +// eslint-disable-next-line no-restricted-properties +assert.deepEqual(result, { + a: { value: 'a', writable: true, enumerable: true, configurable: true }, + b: { value: 'b', writable: false, enumerable: false, configurable: false }, + c: { value: 'c', writable: true, enumerable: true, configurable: false }, + d: { value: 'd', writable: false, enumerable: false, configurable: false }, + e: { value: 'e', writable: false, enumerable: false, configurable: true }, + f: { + value: undefined, + writable: false, + enumerable: false, + configurable: false + }, + g: undefined +}); + +// Define new properties +vm.runInContext(` +Object.defineProperty(this, 'h', {value: 'h'}); +Object.defineProperty(this, 'i', {}); +Object.defineProperty(this, 'j', { + get() { return 'j'; } +}); +let kValue = 0; +Object.defineProperty(this, 'k', { + get() { return kValue; }, + set(value) { kValue = value } +}); +`, ctx); + +assert.deepStrictEqual(Object.getOwnPropertyDescriptor(ctx, 'h'), { + value: 'h', + writable: false, + enumerable: false, + configurable: false +}); + +assert.deepStrictEqual(Object.getOwnPropertyDescriptor(ctx, 'i'), { + value: undefined, + writable: false, + enumerable: false, + configurable: false +}); + +const jDesc = Object.getOwnPropertyDescriptor(ctx, 'j'); +assert.strictEqual(typeof jDesc.get, 'function'); +assert.strictEqual(typeof jDesc.set, 'undefined'); +assert.strictEqual(jDesc.enumerable, false); +assert.strictEqual(jDesc.configurable, false); + +const kDesc = Object.getOwnPropertyDescriptor(ctx, 'k'); +assert.strictEqual(typeof kDesc.get, 'function'); +assert.strictEqual(typeof kDesc.set, 'function'); +assert.strictEqual(kDesc.enumerable, false); +assert.strictEqual(kDesc.configurable, false); + +assert.strictEqual(ctx.k, 0); +ctx.k = 1; +assert.strictEqual(ctx.k, 1); +assert.strictEqual(vm.runInContext('k;', ctx), 1); +vm.runInContext('k = 2;', ctx); +assert.strictEqual(ctx.k, 2); +assert.strictEqual(vm.runInContext('k;', ctx), 2); + +// Redefine properties on the global object +assert.strictEqual(typeof vm.runInContext('encodeURI;', ctx), 'function'); +assert.strictEqual(ctx.encodeURI, undefined); +vm.runInContext(` +Object.defineProperty(this, 'encodeURI', { value: 42 }); +`, ctx); +assert.strictEqual(vm.runInContext('encodeURI;', ctx), 42); +assert.strictEqual(ctx.encodeURI, 42); + +// Redefine properties on the sandbox +vm.runInContext(` +Object.defineProperty(this, 'e', { value: 'newE' }); +`, ctx); +assert.strictEqual(ctx.e, 'newE'); + +assert.throws(() => vm.runInContext(` +'use strict'; +Object.defineProperty(this, 'f', { value: 'newF' }); +`, ctx), /TypeError: .*readonly property.,*/); diff --git a/test/js/node/test/parallel/test-vm-global-property-prototype.js b/test/js/node/test/parallel/test-vm-global-property-prototype.js new file mode 100644 index 0000000000..fe8abc8be4 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-property-prototype.js @@ -0,0 +1,83 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const sandbox = { + onSelf: 'onSelf', +}; + +function onSelfGetter() { + return 'onSelfGetter'; +} + +Object.defineProperty(sandbox, 'onSelfGetter', { + get: onSelfGetter, +}); + +Object.defineProperty(sandbox, 1, { + value: 'onSelfIndexed', + writable: false, + enumerable: false, + configurable: true, +}); + +const ctx = vm.createContext(sandbox); + +const result = vm.runInContext(` +Object.prototype.onProto = 'onProto'; +Object.defineProperty(Object.prototype, 'onProtoGetter', { + get() { + return 'onProtoGetter'; + }, +}); +Object.defineProperty(Object.prototype, 2, { + value: 'onProtoIndexed', + writable: false, + enumerable: false, + configurable: true, +}); + +const resultHasOwn = { + onSelf: Object.hasOwn(this, 'onSelf'), + onSelfGetter: Object.hasOwn(this, 'onSelfGetter'), + onSelfIndexed: Object.hasOwn(this, 1), + onProto: Object.hasOwn(this, 'onProto'), + onProtoGetter: Object.hasOwn(this, 'onProtoGetter'), + onProtoIndexed: Object.hasOwn(this, 2), +}; + +const getDesc = (prop) => Object.getOwnPropertyDescriptor(this, prop); +const resultDesc = { + onSelf: getDesc('onSelf'), + onSelfGetter: getDesc('onSelfGetter'), + onSelfIndexed: getDesc(1), + onProto: getDesc('onProto'), + onProtoGetter: getDesc('onProtoGetter'), + onProtoIndexed: getDesc(2), +}; +({ + resultHasOwn, + resultDesc, +}); +`, ctx); + +// eslint-disable-next-line no-restricted-properties +assert.deepEqual(result, { + resultHasOwn: { + onSelf: true, + onSelfGetter: true, + onSelfIndexed: true, + onProto: false, + onProtoGetter: false, + onProtoIndexed: false, + }, + resultDesc: { + onSelf: { value: 'onSelf', writable: true, enumerable: true, configurable: true }, + onSelfGetter: { get: onSelfGetter, set: undefined, enumerable: false, configurable: false }, + onSelfIndexed: { value: 'onSelfIndexed', writable: false, enumerable: false, configurable: true }, + onProto: undefined, + onProtoGetter: undefined, + onProtoIndexed: undefined, + }, +}); diff --git a/test/js/node/test/parallel/test-vm-global-setter.js b/test/js/node/test/parallel/test-vm-global-setter.js new file mode 100644 index 0000000000..a8e390bc2a --- /dev/null +++ b/test/js/node/test/parallel/test-vm-global-setter.js @@ -0,0 +1,161 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const getSetSymbolReceivingFunction = Symbol('sym-1'); +const getSetSymbolReceivingNumber = Symbol('sym-2'); +const symbolReceivingNumber = Symbol('sym-3'); +const unknownSymbolReceivingNumber = Symbol('sym-4'); + +const window = createWindow(); + +const descriptor1 = Object.getOwnPropertyDescriptor( + window.globalProxy, + 'getSetPropReceivingFunction' +); +assert.strictEqual(typeof descriptor1.get, 'function'); +assert.strictEqual(typeof descriptor1.set, 'function'); +assert.strictEqual(descriptor1.configurable, true); + +const descriptor2 = Object.getOwnPropertyDescriptor( + window.globalProxy, + 'getSetPropReceivingNumber' +); +assert.strictEqual(typeof descriptor2.get, 'function'); +assert.strictEqual(typeof descriptor2.set, 'function'); +assert.strictEqual(descriptor2.configurable, true); + +const descriptor3 = Object.getOwnPropertyDescriptor( + window.globalProxy, + 'propReceivingNumber' +); +assert.strictEqual(descriptor3.value, 44); + +const descriptor4 = Object.getOwnPropertyDescriptor( + window.globalProxy, + 'unknownPropReceivingNumber' +); +assert.strictEqual(descriptor4, undefined); + +const descriptor5 = Object.getOwnPropertyDescriptor( + window.globalProxy, + getSetSymbolReceivingFunction +); +assert.strictEqual(typeof descriptor5.get, 'function'); +assert.strictEqual(typeof descriptor5.set, 'function'); +assert.strictEqual(descriptor5.configurable, true); + +const descriptor6 = Object.getOwnPropertyDescriptor( + window.globalProxy, + getSetSymbolReceivingNumber +); +assert.strictEqual(typeof descriptor6.get, 'function'); +assert.strictEqual(typeof descriptor6.set, 'function'); +assert.strictEqual(descriptor6.configurable, true); + +const descriptor7 = Object.getOwnPropertyDescriptor( + window.globalProxy, + symbolReceivingNumber +); +assert.strictEqual(descriptor7.value, 48); + +const descriptor8 = Object.getOwnPropertyDescriptor( + window.globalProxy, + unknownSymbolReceivingNumber +); +assert.strictEqual(descriptor8, undefined); + +const descriptor9 = Object.getOwnPropertyDescriptor( + window.globalProxy, + 'getSetPropThrowing' +); +assert.strictEqual(typeof descriptor9.get, 'function'); +assert.strictEqual(typeof descriptor9.set, 'function'); +assert.strictEqual(descriptor9.configurable, true); + +const descriptor10 = Object.getOwnPropertyDescriptor( + window.globalProxy, + 'nonWritableProp' +); +assert.strictEqual(descriptor10.value, 51); +assert.strictEqual(descriptor10.writable, false); + +// Regression test for GH-42962. This assignment should not throw. +window.globalProxy.getSetPropReceivingFunction = () => {}; +assert.strictEqual(window.globalProxy.getSetPropReceivingFunction, 42); + +window.globalProxy.getSetPropReceivingNumber = 143; +assert.strictEqual(window.globalProxy.getSetPropReceivingNumber, 43); + +window.globalProxy.propReceivingNumber = 144; +assert.strictEqual(window.globalProxy.propReceivingNumber, 144); + +window.globalProxy.unknownPropReceivingNumber = 145; +assert.strictEqual(window.globalProxy.unknownPropReceivingNumber, 145); + +window.globalProxy[getSetSymbolReceivingFunction] = () => {}; +assert.strictEqual(window.globalProxy[getSetSymbolReceivingFunction], 46); + +window.globalProxy[getSetSymbolReceivingNumber] = 147; +assert.strictEqual(window.globalProxy[getSetSymbolReceivingNumber], 47); + +window.globalProxy[symbolReceivingNumber] = 148; +assert.strictEqual(window.globalProxy[symbolReceivingNumber], 148); + +window.globalProxy[unknownSymbolReceivingNumber] = 149; +assert.strictEqual(window.globalProxy[unknownSymbolReceivingNumber], 149); + +assert.throws( + () => (window.globalProxy.getSetPropThrowing = 150), + new Error('setter called') +); +assert.strictEqual(window.globalProxy.getSetPropThrowing, 50); + +assert.throws( + () => (window.globalProxy.nonWritableProp = 151), + new TypeError('Attempted to assign to readonly property.') +); +assert.strictEqual(window.globalProxy.nonWritableProp, 51); + +function createWindow() { + const obj = {}; + vm.createContext(obj); + Object.defineProperty(obj, 'getSetPropReceivingFunction', { + get: common.mustCall(() => 42), + set: common.mustCall(), + configurable: true, + }); + Object.defineProperty(obj, 'getSetPropReceivingNumber', { + get: common.mustCall(() => 43), + set: common.mustCall(), + configurable: true, + }); + obj.propReceivingNumber = 44; + Object.defineProperty(obj, getSetSymbolReceivingFunction, { + get: common.mustCall(() => 46), + set: common.mustCall(), + configurable: true, + }); + Object.defineProperty(obj, getSetSymbolReceivingNumber, { + get: common.mustCall(() => 47), + set: common.mustCall(), + configurable: true, + }); + obj[symbolReceivingNumber] = 48; + Object.defineProperty(obj, 'getSetPropThrowing', { + get: common.mustCall(() => 50), + set: common.mustCall(() => { + throw new Error('setter called'); + }), + configurable: true, + }); + Object.defineProperty(obj, 'nonWritableProp', { + value: 51, + writable: false, + }); + + obj.globalProxy = vm.runInContext('this', obj); + + return obj; +} diff --git a/test/js/node/test/parallel/test-vm-harmony-symbols.js b/test/js/node/test/parallel/test-vm-harmony-symbols.js new file mode 100644 index 0000000000..5936025070 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-harmony-symbols.js @@ -0,0 +1,37 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// The sandbox should have its own Symbol constructor. +let sandbox = {}; +vm.runInNewContext('this.Symbol = Symbol', sandbox); +assert.strictEqual(typeof sandbox.Symbol, 'function'); +assert.notStrictEqual(sandbox.Symbol, Symbol); + +// Unless we copy the Symbol constructor explicitly, of course. +sandbox = { Symbol }; +vm.runInNewContext('this.Symbol = Symbol', sandbox); +assert.strictEqual(typeof sandbox.Symbol, 'function'); +assert.strictEqual(sandbox.Symbol, Symbol); diff --git a/test/js/node/test/parallel/test-vm-indexed-properties.js b/test/js/node/test/parallel/test-vm-indexed-properties.js new file mode 100644 index 0000000000..34ef8d020b --- /dev/null +++ b/test/js/node/test/parallel/test-vm-indexed-properties.js @@ -0,0 +1,17 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const code = `Object.defineProperty(this, 99, { + value: 20, + enumerable: true + });`; + + +const sandbox = {}; +const ctx = vm.createContext(sandbox); +vm.runInContext(code, ctx); + +assert.strictEqual(sandbox[99], 20); diff --git a/test/js/node/test/parallel/test-vm-inherited_properties.js b/test/js/node/test/parallel/test-vm-inherited_properties.js new file mode 100644 index 0000000000..92cd64a6df --- /dev/null +++ b/test/js/node/test/parallel/test-vm-inherited_properties.js @@ -0,0 +1,39 @@ +'use strict'; + +require('../common'); + +const vm = require('vm'); +const assert = require('assert'); + +let base = { + propBase: 1 +}; + +let sandbox = Object.create(base, { + propSandbox: { value: 3 } +}); + +const context = vm.createContext(sandbox); + +let result = vm.runInContext('Object.hasOwnProperty(this, "propBase");', + context); + +assert.strictEqual(result, false); + + +// Ref: https://github.com/nodejs/node/issues/5350 +base = { __proto__: null }; +base.x = 1; +base.y = 2; + +sandbox = { __proto__: base }; +sandbox.z = 3; + +assert.deepStrictEqual(Object.keys(sandbox), ['z']); + +const code = 'x = 0; z = 4;'; +result = vm.runInNewContext(code, sandbox); +assert.strictEqual(result, 4); + +// Check that y is not an own property. +assert.deepStrictEqual(Object.keys(sandbox), ['z', 'x']); diff --git a/test/js/node/test/parallel/test-vm-low-stack-space.js b/test/js/node/test/parallel/test-vm-low-stack-space.js new file mode 100644 index 0000000000..6a49a983d5 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-low-stack-space.js @@ -0,0 +1,26 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +function a() { + try { + return a(); + } catch { + // Throw an exception as near to the recursion-based RangeError as possible. + return vm.runInThisContext('() => 42')(); + } +} + +assert.strictEqual(a(), 42); + +function b() { + try { + return b(); + } catch { + // This writes a lot of noise to stderr, but it still works. + return vm.runInNewContext('() => 42')(); + } +} + +assert.strictEqual(b(), 42); diff --git a/test/js/node/test/parallel/test-vm-new-script-this-context.js b/test/js/node/test/parallel/test-vm-new-script-this-context.js new file mode 100644 index 0000000000..18f39f9086 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-new-script-this-context.js @@ -0,0 +1,68 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const Script = require('vm').Script; + +// Run a string +let script = new Script('\'passed\';'); +const result = script.runInThisContext(script); +assert.strictEqual(result, 'passed'); + +// Thrown error +script = new Script('throw new Error(\'test\');'); +assert.throws(() => { + script.runInThisContext(script); +}, /^Error: test$/); + +global.hello = 5; +script = new Script('hello = 2'); +script.runInThisContext(script); +assert.strictEqual(global.hello, 2); + + +// Pass values +global.code = 'foo = 1;' + + 'bar = 2;' + + 'if (typeof baz !== "undefined") throw new Error("test fail");'; +global.foo = 2; +global.obj = { foo: 0, baz: 3 }; +script = new Script(global.code); +script.runInThisContext(script); +assert.strictEqual(global.obj.foo, 0); +assert.strictEqual(global.bar, 2); +assert.strictEqual(global.foo, 1); + +// Call a function +global.f = function() { global.foo = 100; }; +script = new Script('f()'); +script.runInThisContext(script); +assert.strictEqual(global.foo, 100); + +common.allowGlobals( + global.hello, + global.code, + global.foo, + global.obj, + global.f +); diff --git a/test/js/node/test/parallel/test-vm-not-strict.js b/test/js/node/test/parallel/test-vm-not-strict.js new file mode 100644 index 0000000000..8a72158a8a --- /dev/null +++ b/test/js/node/test/parallel/test-vm-not-strict.js @@ -0,0 +1,37 @@ +/* eslint-disable strict, no-var, no-delete-var, no-undef, node-core/required-modules, node-core/require-common-first */ +// Importing common would break the execution. Indeed running `vm.runInThisContext` alters the global context +// when declaring new variables with `var`. The other rules (strict, no-var, no-delete-var) have been disabled +// in order to be able to test this specific not-strict case playing with `var` and `delete`. +// Related to bug report: https://github.com/nodejs/node/issues/43129 +var assert = require('assert'); +var vm = require('vm'); + +var data = []; +var a = 'direct'; +delete a; +data.push(a); + +var item2 = vm.runInThisContext(` +var unusedB = 1; +var data = []; +var b = "this"; +delete b; +data.push(b); +data[0] +`); +data.push(item2); + +vm.runInContext( + ` +var unusedC = 1; +var c = "new"; +delete c; +data.push(c); +`, + vm.createContext({ data: data }) +); + +assert.deepStrictEqual(data, ['direct', 'this', 'new']); + +assert.strictEqual(typeof unusedB, 'number'); // Declared within runInThisContext +assert.strictEqual(typeof unusedC, 'undefined'); // Declared within runInContext diff --git a/test/js/node/test/parallel/test-vm-ownkeys.js b/test/js/node/test/parallel/test-vm-ownkeys.js new file mode 100644 index 0000000000..47938a176b --- /dev/null +++ b/test/js/node/test/parallel/test-vm-ownkeys.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); +const vm = require('vm'); +const assert = require('assert'); + +const sym1 = Symbol('1'); +const sym2 = Symbol('2'); +const sandbox = { + a: true, + [sym1]: true, +}; +Object.defineProperty(sandbox, 'b', { value: true }); +Object.defineProperty(sandbox, sym2, { value: true }); + +const ctx = vm.createContext(sandbox); + +assert.deepStrictEqual(Reflect.ownKeys(sandbox), ['a', 'b', sym1, sym2]); +assert.deepStrictEqual(Object.getOwnPropertyNames(sandbox), ['a', 'b']); +assert.deepStrictEqual(Object.getOwnPropertySymbols(sandbox), [sym1, sym2]); + +const nativeKeys = vm.runInNewContext('Reflect.ownKeys(this);'); +const ownKeys = vm.runInContext('Reflect.ownKeys(this);', ctx); +const restKeys = ownKeys.filter((key) => !nativeKeys.includes(key)); +// This should not fail +assert.deepStrictEqual(Array.from(restKeys), ['a', 'b', sym1, sym2]); diff --git a/test/js/node/test/parallel/test-vm-ownpropertynames.js b/test/js/node/test/parallel/test-vm-ownpropertynames.js new file mode 100644 index 0000000000..f076195257 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-ownpropertynames.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); +const vm = require('vm'); +const assert = require('assert'); + +const sym1 = Symbol('1'); +const sym2 = Symbol('2'); +const sandbox = { + a: true, + [sym1]: true, +}; +Object.defineProperty(sandbox, 'b', { value: true }); +Object.defineProperty(sandbox, sym2, { value: true }); + +const ctx = vm.createContext(sandbox); + +assert.deepStrictEqual(Reflect.ownKeys(sandbox), ['a', 'b', sym1, sym2]); +assert.deepStrictEqual(Object.getOwnPropertyNames(sandbox), ['a', 'b']); +assert.deepStrictEqual(Object.getOwnPropertySymbols(sandbox), [sym1, sym2]); + +const nativeNames = vm.runInNewContext('Object.getOwnPropertyNames(this);'); +const ownNames = vm.runInContext('Object.getOwnPropertyNames(this);', ctx); +const restNames = ownNames.filter((name) => !nativeNames.includes(name)); +// This should not fail +assert.deepStrictEqual(Array.from(restNames), ['a', 'b']); diff --git a/test/js/node/test/parallel/test-vm-ownpropertysymbols.js b/test/js/node/test/parallel/test-vm-ownpropertysymbols.js new file mode 100644 index 0000000000..f943b86049 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-ownpropertysymbols.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); +const vm = require('vm'); +const assert = require('assert'); + +const sym1 = Symbol('1'); +const sym2 = Symbol('2'); +const sandbox = { + a: true, + [sym1]: true, +}; +Object.defineProperty(sandbox, 'b', { value: true }); +Object.defineProperty(sandbox, sym2, { value: true }); + +const ctx = vm.createContext(sandbox); + +assert.deepStrictEqual(Reflect.ownKeys(sandbox), ['a', 'b', sym1, sym2]); +assert.deepStrictEqual(Object.getOwnPropertyNames(sandbox), ['a', 'b']); +assert.deepStrictEqual(Object.getOwnPropertySymbols(sandbox), [sym1, sym2]); + +const nativeSym = vm.runInNewContext('Object.getOwnPropertySymbols(this);'); +const ownSym = vm.runInContext('Object.getOwnPropertySymbols(this);', ctx); +const restSym = ownSym.filter((sym) => !nativeSym.includes(sym)); +// This should not fail +assert.deepStrictEqual(Array.from(restSym), [sym1, sym2]); diff --git a/test/js/node/test/parallel/test-vm-parse-abort-on-uncaught-exception.js b/test/js/node/test/parallel/test-vm-parse-abort-on-uncaught-exception.js new file mode 100644 index 0000000000..64bbb4dd4f --- /dev/null +++ b/test/js/node/test/parallel/test-vm-parse-abort-on-uncaught-exception.js @@ -0,0 +1,18 @@ +// Flags: --abort-on-uncaught-exception +'use strict'; +require('../common'); +const vm = require('vm'); + +// Regression test for https://github.com/nodejs/node/issues/13258 + +try { + new vm.Script({ toString() { throw new Error('foo'); } }, {}); +} catch { + // Continue regardless of error. +} + +try { + new vm.Script('[', {}); +} catch { + // Continue regardless of error. +} diff --git a/test/js/node/test/parallel/test-vm-preserves-property.js b/test/js/node/test/parallel/test-vm-preserves-property.js new file mode 100644 index 0000000000..0846fd4f79 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-preserves-property.js @@ -0,0 +1,25 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const vm = require('vm'); + +const x = {}; +Object.defineProperty(x, 'prop', { + configurable: false, + enumerable: false, + writable: false, + value: 'val' +}); +const o = vm.createContext(x); + +const code = 'Object.getOwnPropertyDescriptor(this, "prop")'; +const res = vm.runInContext(code, o, 'test'); + +assert(res); +assert.strictEqual(typeof res, 'object'); +assert.strictEqual(res.value, 'val'); +assert.strictEqual(res.configurable, false); +assert.strictEqual(res.enumerable, false); +assert.strictEqual(res.writable, false); diff --git a/test/js/node/test/parallel/test-vm-proxies.js b/test/js/node/test/parallel/test-vm-proxies.js new file mode 100644 index 0000000000..405f730577 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-proxies.js @@ -0,0 +1,18 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// src/node_contextify.cc filters out the Proxy object from the parent +// context. Make sure that the new context has a Proxy object of its own. +let sandbox = {}; +vm.runInNewContext('this.Proxy = Proxy', sandbox); +assert.strictEqual(typeof sandbox.Proxy, 'function'); +assert.notStrictEqual(sandbox.Proxy, Proxy); + +// Unless we copy the Proxy object explicitly, of course. +sandbox = { Proxy }; +vm.runInNewContext('this.Proxy = Proxy', sandbox); +assert.strictEqual(typeof sandbox.Proxy, 'function'); +assert.strictEqual(sandbox.Proxy, Proxy); diff --git a/test/js/node/test/parallel/test-vm-proxy-failure-CP.js b/test/js/node/test/parallel/test-vm-proxy-failure-CP.js new file mode 100644 index 0000000000..93027576d8 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-proxy-failure-CP.js @@ -0,0 +1,15 @@ +'use strict'; +require('../common'); +const vm = require('vm'); + +// Check that we do not accidentally query attributes. +// Issue: https://github.com/nodejs/node/issues/11902 +const handler = { + getOwnPropertyDescriptor: (target, prop) => { + throw new Error('whoops'); + } +}; +const sandbox = new Proxy({ foo: 'bar' }, handler); +const context = vm.createContext(sandbox); + +vm.runInContext('', context); diff --git a/test/js/node/test/parallel/test-vm-script-throw-in-tostring.js b/test/js/node/test/parallel/test-vm-script-throw-in-tostring.js new file mode 100644 index 0000000000..20e7a75079 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-script-throw-in-tostring.js @@ -0,0 +1,14 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const vm = require('vm'); + +assert.throws(() => { + new vm.Script({ + toString() { + throw new Error(); + } + }); +}, Error); diff --git a/test/js/node/test/parallel/test-vm-set-property-proxy.js b/test/js/node/test/parallel/test-vm-set-property-proxy.js new file mode 100644 index 0000000000..2e3293bf62 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-set-property-proxy.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// Regression test for https://github.com/nodejs/node/issues/34606 + +const handler = { + getOwnPropertyDescriptor: common.mustCallAtLeast(() => { + return {}; + }) +}; + +const proxy = new Proxy({}, handler); +assert.throws(() => vm.runInNewContext('p = 6', proxy), + /getOwnPropertyDescriptor/); diff --git a/test/js/node/test/parallel/test-vm-set-proto-null-on-globalthis.js b/test/js/node/test/parallel/test-vm-set-proto-null-on-globalthis.js new file mode 100644 index 0000000000..869124fa86 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-set-proto-null-on-globalthis.js @@ -0,0 +1,13 @@ +'use strict'; +require('../common'); + +// Setting __proto__ on vm context's globalThis should not cause a crash +// Regression test for https://github.com/nodejs/node/issues/47798 + +const vm = require('vm'); +const context = vm.createContext(); + +const contextGlobalThis = vm.runInContext('this', context); + +// Should not crash. +contextGlobalThis.__proto__ = null; // eslint-disable-line no-proto diff --git a/test/js/node/test/parallel/test-vm-static-this.js b/test/js/node/test/parallel/test-vm-static-this.js new file mode 100644 index 0000000000..e9382d6c3b --- /dev/null +++ b/test/js/node/test/parallel/test-vm-static-this.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* eslint-disable strict */ +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// Run a string +const result = vm.runInThisContext('\'passed\';'); +assert.strictEqual(result, 'passed'); + +// thrown error +assert.throws(function() { + vm.runInThisContext('throw new Error(\'test\');'); +}, /test/); + +global.hello = 5; +vm.runInThisContext('hello = 2'); +assert.strictEqual(global.hello, 2); + + +// pass values +const code = 'foo = 1;' + + 'bar = 2;' + + 'if (typeof baz !== \'undefined\')' + + 'throw new Error(\'test fail\');'; +global.foo = 2; +global.obj = { foo: 0, baz: 3 }; +/* eslint-disable no-unused-vars */ +const baz = vm.runInThisContext(code); +/* eslint-enable no-unused-vars */ +assert.strictEqual(global.obj.foo, 0); +assert.strictEqual(global.bar, 2); +assert.strictEqual(global.foo, 1); + +// call a function +global.f = function() { global.foo = 100; }; +vm.runInThisContext('f()'); +assert.strictEqual(global.foo, 100); + +common.allowGlobals( + global.hello, + global.foo, + global.obj, + global.f +); diff --git a/test/js/node/test/parallel/test-vm-strict-mode.js b/test/js/node/test/parallel/test-vm-strict-mode.js new file mode 100644 index 0000000000..b1b233664d --- /dev/null +++ b/test/js/node/test/parallel/test-vm-strict-mode.js @@ -0,0 +1,14 @@ +'use strict'; +// https://github.com/nodejs/node/issues/12300 + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const ctx = vm.createContext({ x: 42 }); + +// This might look as if x has not been declared, but x is defined on the +// sandbox and the assignment should not throw. +vm.runInContext('"use strict"; x = 1', ctx); + +assert.strictEqual(ctx.x, 1); diff --git a/test/js/node/test/parallel/test-vm-symbols.js b/test/js/node/test/parallel/test-vm-symbols.js new file mode 100644 index 0000000000..e5a4e9e756 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-symbols.js @@ -0,0 +1,23 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +const vm = require('vm'); + +const symbol = Symbol(); + +function Document() { + this[symbol] = 'foo'; +} + +Document.prototype.getSymbolValue = function() { + return this[symbol]; +}; + +const context = new Document(); +vm.createContext(context); + +assert.strictEqual(context.getSymbolValue(), 'foo'); + +assert.strictEqual(vm.runInContext('this.getSymbolValue()', context), 'foo'); diff --git a/test/js/node/test/parallel/test-vm-syntax-error-message.js b/test/js/node/test/parallel/test-vm-syntax-error-message.js new file mode 100644 index 0000000000..c49ff6aeb1 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-syntax-error-message.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const child_process = require('child_process'); + +const p = child_process.spawn(process.execPath, [ + '-e', + 'vm = require("vm");' + + 'context = vm.createContext({});' + + 'try { vm.runInContext("throw new Error(\'boo\')", context); } ' + + 'catch (e) { console.log(e.message); }', +]); + +p.stderr.on('data', common.mustNotCall()); + +let output = ''; + +p.stdout.on('data', (data) => output += data); + +p.stdout.on('end', common.mustCall(() => { + assert.strictEqual(output.replace(/[\r\n]+/g, ''), 'boo'); +})); diff --git a/test/js/node/test/parallel/test-weakref.js b/test/js/node/test/parallel/test-weakref.js new file mode 100644 index 0000000000..ca7485aaa6 --- /dev/null +++ b/test/js/node/test/parallel/test-weakref.js @@ -0,0 +1,13 @@ +'use strict'; + +// Flags: --expose-gc + +require('../common'); +const assert = require('assert'); + +const w = new globalThis.WeakRef({}); + +setTimeout(() => { + globalThis.gc(); + assert.strictEqual(w.deref(), undefined); +}, 200); diff --git a/test/js/node/test/parallel/test-webcrypto-derivebits-cfrg.js b/test/js/node/test/parallel/test-webcrypto-derivebits-cfrg.js new file mode 100644 index 0000000000..4ab325efa4 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-derivebits-cfrg.js @@ -0,0 +1,230 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L142 + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kTests = [ + { + name: 'X25519', + size: 32, + pkcs8: '302e020100300506032b656e04220420c8838e76d057dfb7d8c95a69e138160ad' + + 'd6373fd71a4d276bb56e3a81b64ff61', + spki: '302a300506032b656e0321001cf2b1e6022ec537371ed7f53e54fa1154d83e98eb' + + '64ea51fae5b3307cfe9706', + result: '2768409dfab99ec23b8c89b93ff5880295f76176088f89e43dfebe7ea1950008' + }, + { + name: 'X448', + size: 56, + pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + + 'd8a42d8f17176272b89f2272d1819295c6525c0829671b052ef0727530f188e31' + + 'd0cc53bf26929e', + spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + + 'ca615edc53633efb52ea31e6e6a0a1dbacc6e76cbce6482d7e4ba3d55d9e802765' + + 'ce6f', + result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' + + '726e0f6dcf810eb594dca97b4882bd44c43ea7dc67f49a4e', + }, +]; + +async function prepareKeys() { + const keys = {}; + await Promise.all( + kTests.map(async ({ name, size, pkcs8, spki, result }) => { + const [ + privateKey, + publicKey, + ] = await Promise.all([ + subtle.importKey( + 'pkcs8', + Buffer.from(pkcs8, 'hex'), + { name }, + true, + ['deriveKey', 'deriveBits']), + subtle.importKey( + 'spki', + Buffer.from(spki, 'hex'), + { name }, + true, + []), + ]); + keys[name] = { + privateKey, + publicKey, + size, + result, + }; + })); + return keys; +} + +(async function() { + const keys = await prepareKeys(); + + await Promise.all( + Object.keys(keys).map(async (name) => { + const { size, result, privateKey, publicKey } = keys[name]; + + { + // Good parameters + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size); + + assert(bits instanceof ArrayBuffer); + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Case insensitivity + const bits = await subtle.deriveBits({ + name: name.toLowerCase(), + public: publicKey + }, privateKey, 8 * size); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Null length + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, null); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Default length + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Short Result + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size - 32); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + result.slice(0, -8)); + } + + { + // Too long result + await assert.rejects(subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size + 8), { + message: /derived bit length is too small/ + }); + } + + { + // Non-multiple of 8 + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size - 11); + + const expected = Buffer.from(result.slice(0, -2), 'hex'); + expected[size - 2] = expected[size - 2] & 0b11111000; + assert.deepStrictEqual( + Buffer.from(bits), + expected); + } + })); + + // Error tests + { + // Missing public property + await assert.rejects( + subtle.deriveBits( + { name: 'X448' }, + keys.X448.privateKey, + 8 * keys.X448.size), + { code: 'ERR_MISSING_OPTION' }); + } + + { + // The public property is not a CryptoKey + await assert.rejects( + subtle.deriveBits( + { + name: 'X448', + public: { message: 'Not a CryptoKey' } + }, + keys.X448.privateKey, + 8 * keys.X448.size), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // Mismatched types + await assert.rejects( + subtle.deriveBits( + { + name: 'X448', + public: keys.X25519.publicKey + }, + keys.X448.privateKey, + 8 * keys.X448.size), + { message: 'The public and private keys must be of the same type' }); + } + + { + // Base key is not a private key + await assert.rejects(subtle.deriveBits({ + name: 'X448', + public: keys.X448.publicKey + }, keys.X448.publicKey, null), { + name: 'InvalidAccessError' + }); + } + + { + // Base key is not a private key + await assert.rejects(subtle.deriveBits({ + name: 'X448', + public: keys.X448.privateKey + }, keys.X448.publicKey, null), { + name: 'InvalidAccessError' + }); + } + + { + // Public is a secret key + const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC', length: 256 }, + false, ['encrypt']); + + await assert.rejects(subtle.deriveBits({ + name: 'X448', + public: key + }, keys.X448.publicKey, null), { + name: 'InvalidAccessError' + }); + } +})().then(common.mustCall()); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-webcrypto-derivekey-cfrg.js b/test/js/node/test/parallel/test-webcrypto-derivekey-cfrg.js new file mode 100644 index 0000000000..7c4a315a37 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-derivekey-cfrg.js @@ -0,0 +1,193 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L143 + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kTests = [ + { + name: 'X25519', + size: 32, + pkcs8: '302e020100300506032b656e04220420c8838e76d057dfb7d8c95a69e138160ad' + + 'd6373fd71a4d276bb56e3a81b64ff61', + spki: '302a300506032b656e0321001cf2b1e6022ec537371ed7f53e54fa1154d83e98eb' + + '64ea51fae5b3307cfe9706', + result: '2768409dfab99ec23b8c89b93ff5880295f76176088f89e43dfebe7ea1950008' + }, + { + name: 'X448', + size: 56, + pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + + 'd8a42d8f17176272b89f2272d1819295c6525c0829671b052ef0727530f188e31' + + 'd0cc53bf26929e', + spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + + 'ca615edc53633efb52ea31e6e6a0a1dbacc6e76cbce6482d7e4ba3d55d9e802765' + + 'ce6f', + result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' + }, +]; + +async function prepareKeys() { + const keys = {}; + await Promise.all( + kTests.map(async ({ name, size, pkcs8, spki, result }) => { + const [ + privateKey, + publicKey, + ] = await Promise.all([ + subtle.importKey( + 'pkcs8', + Buffer.from(pkcs8, 'hex'), + { name }, + true, + ['deriveKey', 'deriveBits']), + subtle.importKey( + 'spki', + Buffer.from(spki, 'hex'), + { name }, + true, + []), + ]); + keys[name] = { + privateKey, + publicKey, + size, + result, + }; + })); + return keys; +} + +(async function() { + const keys = await prepareKeys(); + const otherArgs = [ + { name: 'HMAC', hash: 'SHA-256', length: 256 }, + true, + ['sign', 'verify']]; + + await Promise.all( + Object.keys(keys).map(async (name) => { + const { result, privateKey, publicKey } = keys[name]; + + { + // Good parameters + const key = await subtle.deriveKey({ + name, + public: publicKey + }, privateKey, ...otherArgs); + + const raw = await subtle.exportKey('raw', key); + + assert.strictEqual(Buffer.from(raw).toString('hex'), result); + } + + { + // Case insensitivity + const key = await subtle.deriveKey({ + name: name.toLowerCase(), + public: publicKey + }, privateKey, { + name: 'HmAc', + hash: 'SHA-256', + length: 256 + }, true, ['sign', 'verify']); + + const raw = await subtle.exportKey('raw', key); + + assert.strictEqual(Buffer.from(raw).toString('hex'), result); + } + })); + + // Error tests + { + // Missing public property + await assert.rejects( + subtle.deriveKey( + { name: 'X448' }, + keys.X448.privateKey, + ...otherArgs), + { code: 'ERR_MISSING_OPTION' }); + } + + { + // The public property is not a CryptoKey + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: { message: 'Not a CryptoKey' } + }, + keys.X448.privateKey, + ...otherArgs), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // Mismatched named curves + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: keys.X25519.publicKey + }, + keys.X448.privateKey, + ...otherArgs), + { message: 'The public and private keys must be of the same type' }); + } + + { + // Base key is not a private key + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: keys.X448.publicKey + }, + keys.X448.publicKey, + ...otherArgs), + { name: 'InvalidAccessError' }); + } + + { + // Public is not a public key + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: keys.X448.privateKey + }, + keys.X448.privateKey, + ...otherArgs), + { name: 'InvalidAccessError' }); + } + + { + // Public is a secret key + const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC', length: 256 }, + false, ['encrypt']); + + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: key + }, + keys.X448.publicKey, + ...otherArgs), + { name: 'InvalidAccessError' }); + } +})().then(common.mustCall()); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-webcrypto-derivekey.js b/test/js/node/test/parallel/test-webcrypto-derivekey.js new file mode 100644 index 0000000000..f42bf8f4be --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-derivekey.js @@ -0,0 +1,211 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; +const { KeyObject } = require('crypto'); + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test ECDH key derivation +{ + async function test(namedCurve) { + const [alice, bob] = await Promise.all([ + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveKey']), + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveKey']), + ]); + + const [secret1, secret2] = await Promise.all([ + subtle.deriveKey({ + name: 'ECDH', namedCurve, public: alice.publicKey + }, bob.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + subtle.deriveKey({ + name: 'ECDH', namedCurve, public: bob.publicKey + }, alice.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + ]); + + const [raw1, raw2] = await Promise.all([ + subtle.exportKey('raw', secret1), + subtle.exportKey('raw', secret2), + ]); + + assert.deepStrictEqual(raw1, raw2); + } + + test('P-521').then(common.mustCall()); +} + +// Test HKDF key derivation +{ + async function test(pass, info, salt, hash, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'HKDF', hash }, + false, ['deriveKey']); + + const secret = await subtle.deriveKey({ + name: 'HKDF', + hash, + salt: ec.encode(salt), + info: ec.encode(info) + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt']); + + const raw = await subtle.exportKey('raw', secret); + + assert.strictEqual(Buffer.from(raw).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 'my friend', 'SHA-256', + '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e32941ef21bdeb0fa87b6b6'], + ['hello', 'there', 'my friend', 'SHA-384', + 'e36cf2cf943d8f3a88adb80f478745c336ac811b1a86d03a7d10eb0b6b52295c'], + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test PBKDF2 key derivation +{ + async function test(pass, salt, iterations, hash, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'PBKDF2', hash }, + false, ['deriveKey']); + const secret = await subtle.deriveKey({ + name: 'PBKDF2', + hash, + salt: ec.encode(salt), + iterations, + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt']); + + const raw = await subtle.exportKey('raw', secret); + + assert.strictEqual(Buffer.from(raw).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 10, 'SHA-256', + 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7ce7678b4cb16fad880'], + ['hello', 'there', 5, 'SHA-384', + '201509b012c9cd2fbe7ea938f0c509b36ecb140f38bf9130e96923f55f46756d'], + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test default key lengths +{ + const vectors = [ + ['PBKDF2', 'deriveKey', 528], + ['HKDF', 'deriveKey', 528], + [{ name: 'HMAC', hash: 'SHA-1' }, 'sign', 512], + [{ name: 'HMAC', hash: 'SHA-256' }, 'sign', 512], + // Not long enough secret generated by ECDH + // [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], + // [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], + ]; + + (async () => { + const keyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-521' }, false, ['deriveKey']); + for (const [derivedKeyAlgorithm, usage, expected] of vectors) { + const derived = await subtle.deriveKey( + { name: 'ECDH', public: keyPair.publicKey }, + keyPair.privateKey, + derivedKeyAlgorithm, + false, + [usage]); + + if (derived.algorithm.name === 'HMAC') { + assert.strictEqual(derived.algorithm.length, expected); + } else { + // KDFs cannot be exportable and do not indicate their length + const secretKey = KeyObject.from(derived); + assert.strictEqual(secretKey.symmetricKeySize, expected / 8); + } + } + })().then(common.mustCall()); +} + +{ + const vectors = [ + [{ name: 'HMAC', hash: 'SHA-1' }, 'sign', 512], + [{ name: 'HMAC', hash: 'SHA-256' }, 'sign', 512], + [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], + [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], + ]; + + (async () => { + for (const [derivedKeyAlgorithm, usage, expected] of vectors) { + const derived = await subtle.deriveKey( + { name: 'PBKDF2', salt: new Uint8Array([]), hash: 'SHA-256', iterations: 20 }, + await subtle.importKey('raw', new Uint8Array([]), { name: 'PBKDF2' }, false, ['deriveKey']), + derivedKeyAlgorithm, + false, + [usage]); + + assert.strictEqual(derived.algorithm.length, expected); + } + })().then(common.mustCall()); +} + +// Test X25519 and X448 key derivation +if (!common.openSSLIsBoringSSL) { + async function test(name) { + const [alice, bob] = await Promise.all([ + subtle.generateKey({ name }, true, ['deriveKey']), + subtle.generateKey({ name }, true, ['deriveKey']), + ]); + + const [secret1, secret2] = await Promise.all([ + subtle.deriveKey({ + name, public: alice.publicKey + }, bob.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + subtle.deriveKey({ + name, public: bob.publicKey + }, alice.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + ]); + + const [raw1, raw2] = await Promise.all([ + subtle.exportKey('raw', secret1), + subtle.exportKey('raw', secret2), + ]); + + assert.deepStrictEqual(raw1, raw2); + } + + test('X25519').then(common.mustCall()); + test('X448').then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-webcrypto-digest.js b/test/js/node/test/parallel/test-webcrypto-digest.js new file mode 100644 index 0000000000..bfd01ecc12 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-digest.js @@ -0,0 +1,174 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { Buffer } = require('buffer'); +const { subtle } = globalThis.crypto; +const { createHash } = require('crypto'); + +const kTests = [ + ['SHA-1', 'sha1', 160], + ['SHA-256', 'sha256', 256], + ['SHA-384', 'sha384', 384], + ['SHA-512', 'sha512', 512], +]; + +// Empty hash just works, not checking result +subtle.digest('SHA-512', Buffer.alloc(0)) + .then(common.mustCall()); + +// TODO(@jasnell): Need to move this particular test to pummel +// // Careful, this is an expensive operation because of both the memory +// // allocation and the cost of performing the hash on such a large +// // input. +// subtle.digest('SHA-512', new ArrayBuffer(2 ** 31 - 1)) +// .then(common.mustCall()); + +// TODO(@jasnell): Change max to 2 ** 31 - 1 +// assert.rejects(subtle.digest('SHA-512', new ArrayBuffer(kMaxLength + 1)), { +// code: 'ERR_OUT_OF_RANGE' +// }); + +const kData = (new TextEncoder()).encode('hello'); +(async function() { + await Promise.all(kTests.map(async (test) => { + // Get the digest using the legacy crypto API + const checkValue = + createHash(test[1]).update(kData).digest().toString('hex'); + + // Get the digest using the SubtleCrypto API + const values = Promise.all([ + subtle.digest({ name: test[0] }, kData), + subtle.digest({ name: test[0], length: test[2] }, kData), + subtle.digest(test[0], kData), + subtle.digest(test[0], kData.buffer), + subtle.digest(test[0], new DataView(kData.buffer)), + subtle.digest(test[0], Buffer.from(kData)), + ]); + + // subtle.digest copies the input data, so changing it + // while we're waiting on the Promises should never + // cause the test to fail. + kData[0] = 0x1; + kData[2] = 0x2; + kData[4] = 0x3; + + // Compare that the legacy crypto API and SubtleCrypto API + // produce the same results + (await values).forEach((v) => { + assert(v instanceof ArrayBuffer); + assert.strictEqual(checkValue, Buffer.from(v).toString('hex')); + }); + })); +})().then(common.mustCall()); + +Promise.all([1, null, undefined].map((i) => + assert.rejects(subtle.digest(i, Buffer.alloc(0)), { + message: /Unrecognized algorithm name/, + name: 'NotSupportedError', + }) +)).then(common.mustCall()); + +assert.rejects(subtle.digest('', Buffer.alloc(0)), { + message: /Unrecognized algorithm name/, + name: 'NotSupportedError', +}).then(common.mustCall()); + +Promise.all([1, [], {}, null, undefined].map((i) => + assert.rejects(subtle.digest('SHA-256', i), { + code: 'ERR_INVALID_ARG_TYPE' + }) +)).then(common.mustCall()); + +const kSourceData = { + empty: '', + short: '156eea7cc14c56cb94db030a4a9d95ff', + medium: 'b6c8f9df648cd088b70f38e74197b18cb81e1e435' + + '0d50bccb8fb5a7379c87bb2e3d6ed5461ed1e9f36' + + 'f340a3962a446b815b794b4bd43a4403502077b22' + + '56cc807837f3aacd118eb4b9c2baeb897068625ab' + + 'aca193', + long: null +}; + +kSourceData.long = kSourceData.medium.repeat(1024); + +const kDigestedData = { + 'sha-1': { + empty: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', + short: 'c91318cdf2396a015e3f4e6a86a0ba65b8635944', + medium: 'e541060870eb16bf33b68e51f513526893986729', + long: '3098b50037ecd02ebd657653b2bfa01eee27a2ea' + }, + 'sha-256': { + empty: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + short: 'a2831186984792c7d32d59c89740687f19addc1b959e71a1cc538a3b7ed843f2', + medium: '535367877ef014d7fc717e5cb7843e59b61aee62c7029cec7ec6c12fd924e0e4', + long: '14cdea9dc75f5a6274d9fc1e64009912f1dcd306b48fe8e9cf122de671571781' + }, + 'sha-384': { + empty: '38b060a751ac96384cd9327eb1b1e36a21fdb71114b' + + 'e07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2' + + 'f14898b95b', + short: '6bf5ea6524d1cddc43f7cf3b56ee059227404a2f538' + + 'f022a3db7447a782c06c1ed05e8ab4f5edc17f37114' + + '40dfe97731', + medium: 'cbc2c588fe5b25f916da28b4e47a484ae6fc1fe490' + + '2dd5c9939a6bfd034ab3b48b39087436011f6a9987' + + '9d279540e977', + long: '49f4fdb3981968f97d57370f85345067cd5296a97dd1' + + 'a18e06911e756e9608492529870e1ad130998d57cbfb' + + 'b7c1d09e' + }, + 'sha-512': { + empty: 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5' + + '715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318' + + 'd2877eec2f63b931bd47417a81a538327af927da3e', + short: '375248be5ff34be74cab4ff1c3bc8dc68bd5f8dff40' + + '23e98f87b865cff2c724292df189443a64ff4134a65' + + 'cd4635b9d4f5dc0d3fb67528002a63acf26c9da575', + medium: 'b9109f839e8ea43c890f293ce11dc6e2798d1e2431' + + 'f1e4b919e3b20c4f36303ba39c916db306c45a3b65' + + '761ff5be85328eeaf42c3830f1d95e7a41165b7d2d36', + long: '4b02caf650276030ea5617e597c5d53fd9daa68b78bfe' + + '60b22aab8d36a4c2a3affdb71234f49276737c575ddf7' + + '4d14054cbd6fdb98fd0ddcbcb46f91ad76b6ee' + } +}; + +async function testDigest(size, name) { + const digest = await subtle.digest( + name, + Buffer.from(kSourceData[size], 'hex')); + + assert.strictEqual( + Buffer.from(digest).toString('hex'), + kDigestedData[name.toLowerCase()][size]); +} + +(async function() { + const variations = []; + Object.keys(kSourceData).forEach((size) => { + Object.keys(kDigestedData).forEach((alg) => { + const upCase = alg.toUpperCase(); + const downCase = alg.toLowerCase(); + const mixedCase = upCase.slice(0, 1) + downCase.slice(1); + + variations.push(testDigest(size, upCase)); + variations.push(testDigest(size, downCase)); + variations.push(testDigest(size, mixedCase)); + }); + }); + + await Promise.all(variations); +})().then(common.mustCall()); + +(async () => { + await assert.rejects(subtle.digest('RSA-OAEP', Buffer.alloc(1)), { + name: 'NotSupportedError', + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js new file mode 100644 index 0000000000..6a6f45bab0 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -0,0 +1,241 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { + // Using a copy of plaintext to prevent tampering of the original + plaintext = Buffer.from(plaintext); + + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + plaintext[0] = 255 - plaintext[0]; + + assert.strictEqual( + Buffer.from(output).toString('hex'), + Buffer.from(result).toString('hex')); + + // Converting the returned ArrayBuffer into a Buffer right away, + // so that the next line works + const check = Buffer.from(await subtle.decrypt(algorithm, key, output)); + check[0] = 255 - check[0]; + + assert.strictEqual( + Buffer.from(check).toString('hex'), + Buffer.from(plaintext).toString('hex')); +} + +async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['decrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /CryptoKey doesn't support encryption/ + }); +} + +async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + + return assert.rejects(subtle.decrypt(algorithm, key, output), { + message: /CryptoKey doesn't support decryption/ + }); +} + +async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { + assert.notStrictEqual(algorithm.name, alg); + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: alg }, + false, + ['encrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /CryptoKey doesn't match AlgorithmIdentifier/ + }); +} + +async function testDecrypt({ keyBuffer, algorithm, result }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + await subtle.decrypt(algorithm, key, result); +} + +// Test aes-cbc vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_cbc')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CTR')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /algorithm\.iv must contain exactly 16 bytes/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /algorithm\.iv must contain exactly 16 bytes/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +// Test aes-ctr vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_ctr')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + }); + + // TODO(@jasnell): These fail for different reasons. Need to + // make them consistent + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /.*/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /.*/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +// Test aes-gcm vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_gcm')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /is not a valid AES-GCM tag length/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /is not a valid AES-GCM tag length/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +{ + (async function() { + const secretKey = await subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'], + ); + + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + const aad = globalThis.crypto.getRandomValues(new Uint8Array(32)); + + const encrypted = await subtle.encrypt( + { + name: 'AES-GCM', + iv, + additionalData: aad, + tagLength: 128 + }, + secretKey, + globalThis.crypto.getRandomValues(new Uint8Array(32)) + ); + + await subtle.decrypt( + { + name: 'AES-GCM', + iv, + additionalData: aad, + tagLength: 128, + }, + secretKey, + new Uint8Array(encrypted), + ); + })().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt.js new file mode 100644 index 0000000000..cba193b8c7 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -0,0 +1,124 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test Encrypt/Decrypt RSA-OAEP +{ + const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); + + async function test() { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-384', + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt({ + name: 'RSA-OAEP', + label: ec.encode('a label') + }, publicKey, buf); + + const plaintext = await subtle.decrypt({ + name: 'RSA-OAEP', + label: ec.encode('a label') + }, privateKey, ciphertext); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Test Encrypt/Decrypt AES-CTR +{ + const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); + const counter = globalThis.crypto.getRandomValues(new Uint8Array(16)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-CTR', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt( + { name: 'AES-CTR', counter, length: 64 }, key, buf, + ); + + const plaintext = await subtle.decrypt( + { name: 'AES-CTR', counter, length: 64 }, key, ciphertext, + ); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Test Encrypt/Decrypt AES-CBC +{ + const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); + const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-CBC', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt( + { name: 'AES-CBC', iv }, key, buf, + ); + + const plaintext = await subtle.decrypt( + { name: 'AES-CBC', iv }, key, ciphertext, + ); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Test Encrypt/Decrypt AES-GCM +{ + const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-GCM', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt( + { name: 'AES-GCM', iv }, key, buf, + ); + + const plaintext = await subtle.decrypt( + { name: 'AES-GCM', iv }, key, ciphertext, + ); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-webcrypto-getRandomValues.js b/test/js/node/test/parallel/test-webcrypto-getRandomValues.js new file mode 100644 index 0000000000..f0fbe61a20 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-getRandomValues.js @@ -0,0 +1,11 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { getRandomValues } = globalThis.crypto; + +assert.throws(() => getRandomValues(new Uint8Array()), { code: 'ERR_INVALID_THIS' }); diff --git a/test/js/node/test/parallel/test-webcrypto-sign-verify.js b/test/js/node/test/parallel/test-webcrypto-sign-verify.js new file mode 100644 index 0000000000..e8899a7a0b --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-sign-verify.js @@ -0,0 +1,146 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test Sign/Verify RSASSA-PKCS1-v1_5 +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'RSASSA-PKCS1-v1_5' + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'RSASSA-PKCS1-v1_5' + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify RSA-PSS +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'RSA-PSS', + saltLength: 256, + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'RSA-PSS', + saltLength: 256, + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify ECDSA +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'ECDSA', + namedCurve: 'P-384', + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'ECDSA', + hash: 'SHA-384', + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'ECDSA', + hash: 'SHA-384', + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify HMAC +{ + async function test(data) { + const ec = new TextEncoder(); + + const key = await subtle.generateKey({ + name: 'HMAC', + length: 256, + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'HMAC', + }, key, ec.encode(data)); + + assert(await subtle.verify({ + name: 'HMAC', + }, key, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify Ed25519 +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'Ed25519', + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'Ed25519', + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'Ed25519', + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify Ed448 +if (!common.openSSLIsBoringSSL){ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'Ed448', + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'Ed448', + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'Ed448', + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-webcrypto-wrap-unwrap.js b/test/js/node/test/parallel/test-webcrypto-wrap-unwrap.js new file mode 100644 index 0000000000..dae7fe0068 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-wrap-unwrap.js @@ -0,0 +1,310 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L150 + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kWrappingData = { + 'RSA-OAEP': { + generate: { + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + wrap: { label: new Uint8Array(8) }, + pair: true + }, + 'AES-CTR': { + generate: { length: 128 }, + wrap: { counter: new Uint8Array(16), length: 64 }, + pair: false + }, + 'AES-CBC': { + generate: { length: 128 }, + wrap: { iv: new Uint8Array(16) }, + pair: false + }, + 'AES-GCM': { + generate: { length: 128 }, + wrap: { + iv: new Uint8Array(16), + additionalData: new Uint8Array(16), + tagLength: 64 + }, + pair: false + }, + 'AES-KW': { + generate: { length: 128 }, + wrap: { }, + pair: false + } +}; + +function generateWrappingKeys() { + return Promise.all(Object.keys(kWrappingData).map(async (name) => { + const keys = await subtle.generateKey( + { name, ...kWrappingData[name].generate }, + true, + ['wrapKey', 'unwrapKey']); + if (kWrappingData[name].pair) { + kWrappingData[name].wrappingKey = keys.publicKey; + kWrappingData[name].unwrappingKey = keys.privateKey; + } else { + kWrappingData[name].wrappingKey = keys; + kWrappingData[name].unwrappingKey = keys; + } + })); +} + +async function generateKeysToWrap() { + const parameters = [ + { + algorithm: { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'RSA-OAEP', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['decrypt'], + publicUsages: ['encrypt'], + pair: true, + }, + { + algorithm: { + name: 'ECDSA', + namedCurve: 'P-384' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'ECDH', + namedCurve: 'P-384' + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + { + algorithm: { + name: 'Ed25519', + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'Ed448', + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'X25519', + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + { + algorithm: { + name: 'X448', + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + { + algorithm: { + name: 'AES-CTR', + length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-CBC', + length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-GCM', length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-KW', + length: 128 + }, + usages: ['wrapKey', 'unwrapKey'], + pair: false, + }, + { + algorithm: { + name: 'HMAC', + length: 128, + hash: 'SHA-256' + }, + usages: ['sign', 'verify'], + pair: false, + }, + ]; + + const allkeys = await Promise.all(parameters.map(async (params) => { + const usages = 'usages' in params ? + params.usages : + params.publicUsages.concat(params.privateUsages); + + const keys = await subtle.generateKey(params.algorithm, true, usages); + + if (params.pair) { + return [ + { + algorithm: params.algorithm, + usages: params.publicUsages, + key: keys.publicKey, + }, + { + algorithm: params.algorithm, + usages: params.privateUsages, + key: keys.privateKey, + }, + ]; + } + + return [{ + algorithm: params.algorithm, + usages: params.usages, + key: keys, + }]; + })); + + return allkeys.flat(); +} + +function getFormats(key) { + switch (key.key.type) { + case 'secret': return ['raw', 'jwk']; + case 'public': return ['spki', 'jwk']; + case 'private': return ['pkcs8', 'jwk']; + } +} + +// If the wrapping algorithm is AES-KW, the exported key +// material length must be a multiple of 8. +// If the wrapping algorithm is RSA-OAEP, the exported key +// material maximum length is a factor of the modulusLength +// +// As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey +// we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes +// in length +async function wrappingIsPossible(name, exported) { + if ('byteLength' in exported) { + switch (name) { + case 'AES-KW': + return exported.byteLength % 8 === 0; + case 'RSA-OAEP': + return exported.byteLength <= 446; + } + } else if ('kty' in exported && name === 'RSA-OAEP') { + return JSON.stringify(exported).length <= 478; + } + return true; +} + +async function testWrap(wrappingKey, unwrappingKey, key, wrap, format) { + const exported = await subtle.exportKey(format, key.key); + if (!(await wrappingIsPossible(wrappingKey.algorithm.name, exported))) + return; + + const wrapped = + await subtle.wrapKey( + format, + key.key, + wrappingKey, + { name: wrappingKey.algorithm.name, ...wrap }); + const unwrapped = + await subtle.unwrapKey( + format, + wrapped, + unwrappingKey, + { name: wrappingKey.algorithm.name, ...wrap }, + key.algorithm, + true, + key.usages); + assert(unwrapped.extractable); + + const exportedAgain = await subtle.exportKey(format, unwrapped); + assert.deepStrictEqual(exported, exportedAgain); +} + +function testWrapping(name, keys) { + const variations = []; + + const { + wrappingKey, + unwrappingKey, + wrap + } = kWrappingData[name]; + + keys.forEach((key) => { + getFormats(key).forEach((format) => { + variations.push(testWrap(wrappingKey, unwrappingKey, key, wrap, format)); + }); + }); + + return variations; +} + +(async function() { + await generateWrappingKeys(); + const keys = await generateKeysToWrap(); + const variations = []; + Object.keys(kWrappingData).forEach((name) => { + variations.push(...testWrapping(name, keys)); + }); + await Promise.all(variations); +})().then(common.mustCall()); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-websocket.js b/test/js/node/test/parallel/test-websocket.js new file mode 100644 index 0000000000..c595ec12bf --- /dev/null +++ b/test/js/node/test/parallel/test-websocket.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.strictEqual(typeof WebSocket, 'function'); diff --git a/test/js/node/test/parallel/test-webstream-string-tag.js b/test/js/node/test/parallel/test-webstream-string-tag.js new file mode 100644 index 0000000000..980a204a9b --- /dev/null +++ b/test/js/node/test/parallel/test-webstream-string-tag.js @@ -0,0 +1,18 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); + +const classesToBeTested = [ WritableStream, WritableStreamDefaultWriter, WritableStreamDefaultController, + ReadableStream, ReadableStreamBYOBRequest, ReadableStreamDefaultReader, + ReadableStreamBYOBReader, ReadableStreamDefaultController, ReadableByteStreamController, + ByteLengthQueuingStrategy, CountQueuingStrategy, TransformStream, + TransformStreamDefaultController]; + + +classesToBeTested.forEach((cls) => { + assert.strictEqual(cls.prototype[Symbol.toStringTag], cls.name); + assert.deepStrictEqual(Object.getOwnPropertyDescriptor(cls.prototype, Symbol.toStringTag), + { configurable: true, enumerable: false, value: cls.name, writable: false }); +}); diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-api-basics.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-api-basics.js new file mode 100644 index 0000000000..71b573a8df --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-api-basics.js @@ -0,0 +1,61 @@ +'use strict'; + +// From: https://github.com/w3c/web-platform-tests/blob/master/encoding/api-basics.html +// This is the part that can be run without ICU + +require('../common'); + +const assert = require('assert'); + +function testDecodeSample(encoding, string, bytes) { + assert.strictEqual( + new TextDecoder(encoding).decode(new Uint8Array(bytes)), + string); + assert.strictEqual( + new TextDecoder(encoding).decode(new Uint8Array(bytes).buffer), + string); +} + +// `z` (ASCII U+007A), cent (Latin-1 U+00A2), CJK water (BMP U+6C34), +// G-Clef (non-BMP U+1D11E), PUA (BMP U+F8FF), PUA (non-BMP U+10FFFD) +// byte-swapped BOM (non-character U+FFFE) +const sample = 'z\xA2\u6C34\uD834\uDD1E\uF8FF\uDBFF\uDFFD\uFFFE'; + +{ + const encoding = 'utf-8'; + const string = sample; + const bytes = [ + 0x7A, 0xC2, 0xA2, 0xE6, 0xB0, 0xB4, + 0xF0, 0x9D, 0x84, 0x9E, 0xEF, 0xA3, + 0xBF, 0xF4, 0x8F, 0xBF, 0xBD, 0xEF, + 0xBF, 0xBE, + ]; + const encoded = new TextEncoder().encode(string); + assert.deepStrictEqual([].slice.call(encoded), bytes); + assert.strictEqual( + new TextDecoder(encoding).decode(new Uint8Array(bytes)), + string); + assert.strictEqual( + new TextDecoder(encoding).decode(new Uint8Array(bytes).buffer), + string); +} + +testDecodeSample( + 'utf-16le', + sample, + [ + 0x7A, 0x00, 0xA2, 0x00, 0x34, 0x6C, + 0x34, 0xD8, 0x1E, 0xDD, 0xFF, 0xF8, + 0xFF, 0xDB, 0xFD, 0xDF, 0xFE, 0xFF, + ] +); + +testDecodeSample( + 'utf-16', + sample, + [ + 0x7A, 0x00, 0xA2, 0x00, 0x34, 0x6C, + 0x34, 0xD8, 0x1E, 0xDD, 0xFF, 0xF8, + 0xFF, 0xDB, 0xFD, 0xDF, 0xFE, 0xFF, + ] +); diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-fatal-streaming.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-fatal-streaming.js new file mode 100644 index 0000000000..164088270c --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-fatal-streaming.js @@ -0,0 +1,61 @@ +'use strict'; + +// From: https://github.com/w3c/web-platform-tests/blob/d74324b53c/encoding/textdecoder-fatal-streaming.html +// With the twist that we specifically test for Node.js error codes + +const common = require('../common'); +const assert = require('assert'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +{ + [ + { encoding: 'utf-8', sequence: [0xC0] }, + { encoding: 'utf-16le', sequence: [0x00] }, + { encoding: 'utf-16be', sequence: [0x00] }, + ].forEach((testCase) => { + const data = new Uint8Array([testCase.sequence]); + assert.throws( + () => { + const decoder = new TextDecoder(testCase.encoding, { fatal: true }); + decoder.decode(data); + }, { + code: 'ERR_ENCODING_INVALID_ENCODED_DATA', + name: 'TypeError', + message: + `The encoded data was not valid for encoding ${testCase.encoding}` + } + ); + }); +} + +{ + const decoder = new TextDecoder('utf-16le', { fatal: true }); + const odd = new Uint8Array([0x00]); + const even = new Uint8Array([0x00, 0x00]); + + assert.throws( + () => { + decoder.decode(even, { stream: true }); + decoder.decode(odd); + }, { + code: 'ERR_ENCODING_INVALID_ENCODED_DATA', + name: 'TypeError', + message: + 'The encoded data was not valid for encoding utf-16le' + } + ); + + assert.throws( + () => { + decoder.decode(odd, { stream: true }); + decoder.decode(even); + }, { + code: 'ERR_ENCODING_INVALID_ENCODED_DATA', + name: 'TypeError', + message: + 'The encoded data was not valid for encoding utf-16le' + } + ); +} diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-fatal.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-fatal.js new file mode 100644 index 0000000000..8778fa018e --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-fatal.js @@ -0,0 +1,84 @@ +'use strict'; + +// From: https://github.com/w3c/web-platform-tests/blob/39a67e2fff/encoding/textdecoder-fatal.html +// With the twist that we specifically test for Node.js error codes + +const common = require('../common'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +const assert = require('assert'); + +const bad = [ + { encoding: 'utf-8', input: [0xFF], name: 'invalid code' }, + { encoding: 'utf-8', input: [0xC0], name: 'ends early' }, + { encoding: 'utf-8', input: [0xE0], name: 'ends early 2' }, + { encoding: 'utf-8', input: [0xC0, 0x00], name: 'invalid trail' }, + { encoding: 'utf-8', input: [0xC0, 0xC0], name: 'invalid trail 2' }, + { encoding: 'utf-8', input: [0xE0, 0x00], name: 'invalid trail 3' }, + { encoding: 'utf-8', input: [0xE0, 0xC0], name: 'invalid trail 4' }, + { encoding: 'utf-8', input: [0xE0, 0x80, 0x00], name: 'invalid trail 5' }, + { encoding: 'utf-8', input: [0xE0, 0x80, 0xC0], name: 'invalid trail 6' }, + { encoding: 'utf-8', input: [0xFC, 0x80, 0x80, 0x80, 0x80, 0x80], + name: '> 0x10FFFF' }, + { encoding: 'utf-8', input: [0xFE, 0x80, 0x80, 0x80, 0x80, 0x80], + name: 'obsolete lead byte' }, + // Overlong encodings + { encoding: 'utf-8', input: [0xC0, 0x80], name: 'overlong U+0000 - 2 bytes' }, + { encoding: 'utf-8', input: [0xE0, 0x80, 0x80], + name: 'overlong U+0000 - 3 bytes' }, + { encoding: 'utf-8', input: [0xF0, 0x80, 0x80, 0x80], + name: 'overlong U+0000 - 4 bytes' }, + { encoding: 'utf-8', input: [0xF8, 0x80, 0x80, 0x80, 0x80], + name: 'overlong U+0000 - 5 bytes' }, + { encoding: 'utf-8', input: [0xFC, 0x80, 0x80, 0x80, 0x80, 0x80], + name: 'overlong U+0000 - 6 bytes' }, + { encoding: 'utf-8', input: [0xC1, 0xBF], name: 'overlong U+007F - 2 bytes' }, + { encoding: 'utf-8', input: [0xE0, 0x81, 0xBF], + name: 'overlong U+007F - 3 bytes' }, + { encoding: 'utf-8', input: [0xF0, 0x80, 0x81, 0xBF], + name: 'overlong U+007F - 4 bytes' }, + { encoding: 'utf-8', input: [0xF8, 0x80, 0x80, 0x81, 0xBF], + name: 'overlong U+007F - 5 bytes' }, + { encoding: 'utf-8', input: [0xFC, 0x80, 0x80, 0x80, 0x81, 0xBF], + name: 'overlong U+007F - 6 bytes' }, + { encoding: 'utf-8', input: [0xE0, 0x9F, 0xBF], + name: 'overlong U+07FF - 3 bytes' }, + { encoding: 'utf-8', input: [0xF0, 0x80, 0x9F, 0xBF], + name: 'overlong U+07FF - 4 bytes' }, + { encoding: 'utf-8', input: [0xF8, 0x80, 0x80, 0x9F, 0xBF], + name: 'overlong U+07FF - 5 bytes' }, + { encoding: 'utf-8', input: [0xFC, 0x80, 0x80, 0x80, 0x9F, 0xBF], + name: 'overlong U+07FF - 6 bytes' }, + { encoding: 'utf-8', input: [0xF0, 0x8F, 0xBF, 0xBF], + name: 'overlong U+FFFF - 4 bytes' }, + { encoding: 'utf-8', input: [0xF8, 0x80, 0x8F, 0xBF, 0xBF], + name: 'overlong U+FFFF - 5 bytes' }, + { encoding: 'utf-8', input: [0xFC, 0x80, 0x80, 0x8F, 0xBF, 0xBF], + name: 'overlong U+FFFF - 6 bytes' }, + { encoding: 'utf-8', input: [0xF8, 0x84, 0x8F, 0xBF, 0xBF], + name: 'overlong U+10FFFF - 5 bytes' }, + { encoding: 'utf-8', input: [0xFC, 0x80, 0x84, 0x8F, 0xBF, 0xBF], + name: 'overlong U+10FFFF - 6 bytes' }, + // UTF-16 surrogates encoded as code points in UTF-8 + { encoding: 'utf-8', input: [0xED, 0xA0, 0x80], name: 'lead surrogate' }, + { encoding: 'utf-8', input: [0xED, 0xB0, 0x80], name: 'trail surrogate' }, + { encoding: 'utf-8', input: [0xED, 0xA0, 0x80, 0xED, 0xB0, 0x80], + name: 'surrogate pair' }, + { encoding: 'utf-16le', input: [0x00], name: 'truncated code unit' }, + // Mismatched UTF-16 surrogates are exercised in utf16-surrogates.html + // FIXME: Add legacy encoding cases +]; + +bad.forEach((t) => { + assert.throws( + () => { + new TextDecoder(t.encoding, { fatal: true }) + .decode(new Uint8Array(t.input)); + }, { + code: 'ERR_ENCODING_INVALID_ENCODED_DATA', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-ignorebom.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-ignorebom.js new file mode 100644 index 0000000000..94fc3318d1 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-ignorebom.js @@ -0,0 +1,30 @@ +'use strict'; + +// From: https://github.com/w3c/web-platform-tests/blob/7f567fa29c/encoding/textdecoder-ignorebom.html +// This is the part that can be run without ICU + +require('../common'); + +const assert = require('assert'); + +const cases = [ + { + encoding: 'utf-8', + bytes: [0xEF, 0xBB, 0xBF, 0x61, 0x62, 0x63] + }, + { + encoding: 'utf-16le', + bytes: [0xFF, 0xFE, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00] + }, +]; + +cases.forEach((testCase) => { + const BOM = '\uFEFF'; + let decoder = new TextDecoder(testCase.encoding, { ignoreBOM: true }); + const bytes = new Uint8Array(testCase.bytes); + assert.strictEqual(decoder.decode(bytes), `${BOM}abc`); + decoder = new TextDecoder(testCase.encoding, { ignoreBOM: false }); + assert.strictEqual(decoder.decode(bytes), 'abc'); + decoder = new TextDecoder(testCase.encoding); + assert.strictEqual(decoder.decode(bytes), 'abc'); +}); diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-invalid-arg.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-invalid-arg.js new file mode 100644 index 0000000000..5c8a9837f6 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-invalid-arg.js @@ -0,0 +1,19 @@ +'use strict'; + +// This tests that ERR_INVALID_ARG_TYPE are thrown when +// invalid arguments are passed to TextDecoder. + +require('../common'); +const assert = require('assert'); + +{ + const notArrayBufferViewExamples = [false, {}, 1, '', new Error()]; + notArrayBufferViewExamples.forEach((invalidInputType) => { + assert.throws(() => { + new TextDecoder(undefined, null).decode(invalidInputType); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + }); +} diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-streaming.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-streaming.js new file mode 100644 index 0000000000..5484929326 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-streaming.js @@ -0,0 +1,38 @@ +'use strict'; + +// From: https://github.com/w3c/web-platform-tests/blob/fa9436d12c/encoding/textdecoder-streaming.html +// This is the part that can be run without ICU + +require('../common'); + +const assert = require('assert'); + +const string = + '\x00123ABCabc\x80\xFF\u0100\u1000\uFFFD\uD800\uDC00\uDBFF\uDFFF'; +const octets = { + 'utf-8': [ + 0x00, 0x31, 0x32, 0x33, 0x41, 0x42, 0x43, 0x61, 0x62, 0x63, 0xc2, 0x80, + 0xc3, 0xbf, 0xc4, 0x80, 0xe1, 0x80, 0x80, 0xef, 0xbf, 0xbd, 0xf0, 0x90, + 0x80, 0x80, 0xf4, 0x8f, 0xbf, 0xbf], + 'utf-16le': [ + 0x00, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x41, 0x00, 0x42, 0x00, + 0x43, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, 0x80, 0x00, 0xFF, 0x00, + 0x00, 0x01, 0x00, 0x10, 0xFD, 0xFF, 0x00, 0xD8, 0x00, 0xDC, 0xFF, 0xDB, + 0xFF, 0xDF] +}; + +Object.keys(octets).forEach((encoding) => { + for (let len = 1; len <= 5; ++len) { + const encoded = octets[encoding]; + const decoder = new TextDecoder(encoding); + let out = ''; + for (let i = 0; i < encoded.length; i += len) { + const sub = []; + for (let j = i; j < encoded.length && j < i + len; ++j) + sub.push(encoded[j]); + out += decoder.decode(new Uint8Array(sub), { stream: true }); + } + out += decoder.decode(); + assert.strictEqual(out, string); + } +}); diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-utf16-surrogates.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-utf16-surrogates.js new file mode 100644 index 0000000000..a2a31af28c --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-utf16-surrogates.js @@ -0,0 +1,56 @@ +'use strict'; + +// From: https://github.com/w3c/web-platform-tests/blob/39a67e2fff/encoding/textdecoder-utf16-surrogates.html +// With the twist that we specifically test for Node.js error codes + +const common = require('../common'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +const assert = require('assert'); + +const bad = [ + { + encoding: 'utf-16le', + input: [0x00, 0xd8], + expected: '\uFFFD', + name: 'lone surrogate lead' + }, + { + encoding: 'utf-16le', + input: [0x00, 0xdc], + expected: '\uFFFD', + name: 'lone surrogate trail' + }, + { + encoding: 'utf-16le', + input: [0x00, 0xd8, 0x00, 0x00], + expected: '\uFFFD\u0000', + name: 'unmatched surrogate lead' + }, + { + encoding: 'utf-16le', + input: [0x00, 0xdc, 0x00, 0x00], + expected: '\uFFFD\u0000', + name: 'unmatched surrogate trail' + }, + { + encoding: 'utf-16le', + input: [0x00, 0xdc, 0x00, 0xd8], + expected: '\uFFFD\uFFFD', + name: 'swapped surrogate pair' + }, +]; + +for (const t of bad) { + assert.throws( + () => { + new TextDecoder(t.encoding, { fatal: true }) + .decode(new Uint8Array(t.input)); + }, { + code: 'ERR_ENCODING_INVALID_ENCODED_DATA', + name: 'TypeError' + } + ); +} diff --git a/test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-passive.js b/test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-passive.js new file mode 100644 index 0000000000..97984bd9af --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-passive.js @@ -0,0 +1,65 @@ +'use strict'; + +const common = require('../common'); + +// Manually converted from https://github.com/web-platform-tests/wpt/blob/master/dom/events/AddEventListenerOptions-passive.html +// in order to define the `document` ourselves + +const { + fail, + ok, + strictEqual +} = require('assert'); + +{ + const document = new EventTarget(); + let supportsPassive = false; + const query_options = { + get passive() { + supportsPassive = true; + return false; + }, + get dummy() { + fail('dummy value getter invoked'); + return false; + } + }; + + document.addEventListener('test_event', null, query_options); + ok(supportsPassive); + + supportsPassive = false; + document.removeEventListener('test_event', null, query_options); + strictEqual(supportsPassive, false); +} +{ + function testPassiveValue(optionsValue, expectedDefaultPrevented) { + const document = new EventTarget(); + let defaultPrevented; + function handler(e) { + if (e.defaultPrevented) { + fail('Event prematurely marked defaultPrevented'); + } + e.preventDefault(); + defaultPrevented = e.defaultPrevented; + } + document.addEventListener('test', handler, optionsValue); + // TODO the WHATWG test is more extensive here and tests dispatching on + // document.body, if we ever support getParent we should amend this + const ev = new Event('test', { bubbles: true, cancelable: true }); + const uncanceled = document.dispatchEvent(ev); + + strictEqual(defaultPrevented, expectedDefaultPrevented); + strictEqual(uncanceled, !expectedDefaultPrevented); + + document.removeEventListener('test', handler, optionsValue); + } + testPassiveValue(undefined, true); + testPassiveValue({}, true); + testPassiveValue({ passive: false }, true); + + common.skip('TODO: passive listeners is still broken'); + testPassiveValue({ passive: 1 }, false); + testPassiveValue({ passive: true }, false); + testPassiveValue({ passive: 0 }, true); +} diff --git a/test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-signal.js b/test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-signal.js new file mode 100644 index 0000000000..460d2ee3f2 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-signal.js @@ -0,0 +1,168 @@ +'use strict'; + +require('../common'); + +const { + strictEqual, + throws, +} = require('assert'); + +// Manually ported from: wpt@dom/events/AddEventListenerOptions-signal.any.js + +{ + // Passing an AbortSignal to addEventListener does not prevent + // removeEventListener + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + et.addEventListener('test', handler, { signal: controller.signal }); + et.dispatchEvent(new Event('test')); + strictEqual(count, 1, 'Adding a signal still adds a listener'); + et.dispatchEvent(new Event('test')); + strictEqual(count, 2, 'The listener was not added with the once flag'); + controller.abort(); + et.dispatchEvent(new Event('test')); + strictEqual(count, 2, 'Aborting on the controller removes the listener'); + // See: https://github.com/nodejs/node/pull/37696 , adding an event listener + // should always return undefined. + strictEqual( + et.addEventListener('test', handler, { signal: controller.signal }), + undefined); + et.dispatchEvent(new Event('test')); + strictEqual(count, 2, 'Passing an aborted signal never adds the handler'); +} + +{ + // Passing an AbortSignal to addEventListener works with the once flag + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + et.addEventListener('test', handler, { signal: controller.signal }); + et.removeEventListener('test', handler); + et.dispatchEvent(new Event('test')); + strictEqual(count, 0, 'The listener was still removed'); +} + +{ + // Removing a once listener works with a passed signal + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + const options = { signal: controller.signal, once: true }; + et.addEventListener('test', handler, options); + controller.abort(); + et.dispatchEvent(new Event('test')); + strictEqual(count, 0, 'The listener was still removed'); +} + +{ + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + const options = { signal: controller.signal, once: true }; + et.addEventListener('test', handler, options); + et.removeEventListener('test', handler); + et.dispatchEvent(new Event('test')); + strictEqual(count, 0, 'The listener was still removed'); +} + +{ + // Passing an AbortSignal to multiple listeners + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + const options = { signal: controller.signal, once: true }; + et.addEventListener('first', handler, options); + et.addEventListener('second', handler, options); + controller.abort(); + et.dispatchEvent(new Event('first')); + et.dispatchEvent(new Event('second')); + strictEqual(count, 0, 'The listener was still removed'); +} + +{ + // Passing an AbortSignal to addEventListener works with the capture flag + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + const options = { signal: controller.signal, capture: true }; + et.addEventListener('test', handler, options); + controller.abort(); + et.dispatchEvent(new Event('test')); + strictEqual(count, 0, 'The listener was still removed'); +} + +{ + // Aborting from a listener does not call future listeners + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + const options = { signal: controller.signal }; + et.addEventListener('test', () => { + controller.abort(); + }, options); + et.addEventListener('test', handler, options); + et.dispatchEvent(new Event('test')); + strictEqual(count, 0, 'The listener was still removed'); +} + +{ + // Adding then aborting a listener in another listener does not call it + let count = 0; + function handler() { + count++; + } + const et = new EventTarget(); + const controller = new AbortController(); + et.addEventListener('test', () => { + et.addEventListener('test', handler, { signal: controller.signal }); + controller.abort(); + }, { signal: controller.signal }); + et.dispatchEvent(new Event('test')); + strictEqual(count, 0, 'The listener was still removed'); +} + +{ + // Aborting from a nested listener should remove it + const et = new EventTarget(); + const ac = new AbortController(); + let count = 0; + et.addEventListener('foo', () => { + et.addEventListener('foo', () => { + count++; + if (count > 5) ac.abort(); + et.dispatchEvent(new Event('foo')); + }, { signal: ac.signal }); + et.dispatchEvent(new Event('foo')); + }, { once: true }); + et.dispatchEvent(new Event('foo')); +} +{ + const et = new EventTarget(); + [1, 1n, {}, [], null, true, 'hi', Symbol(), () => {}].forEach((signal) => { + throws(() => et.addEventListener('foo', () => {}, { signal }), { + name: 'TypeError', + }); + }); +} diff --git a/test/js/node/test/parallel/test-whatwg-events-customevent.js b/test/js/node/test/parallel/test-whatwg-events-customevent.js new file mode 100644 index 0000000000..e21ea1783f --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-events-customevent.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); + +const { strictEqual, throws, equal } = require('assert'); + +// Manually converted from https://github.com/web-platform-tests/wpt/blob/master/dom/events/CustomEvent.html +// in order to define the `document` ourselves + +{ + const type = 'foo'; + const target = new EventTarget(); + + target.addEventListener(type, common.mustCall((evt) => { + strictEqual(evt.type, type); + })); + + target.dispatchEvent(new Event(type)); +} + +{ + throws(() => { + new Event(); + }, TypeError); +} + +{ + const event = new Event('foo'); + equal(event.type, 'foo'); + equal(event.bubbles, false); + equal(event.cancelable, false); + equal(event.detail, null); +} diff --git a/test/js/node/test/parallel/test-whatwg-events-event-constructors.js b/test/js/node/test/parallel/test-whatwg-events-event-constructors.js new file mode 100644 index 0000000000..7880b10043 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-events-event-constructors.js @@ -0,0 +1,29 @@ +'use strict'; + +require('../common'); +const { test, assert_equals, assert_array_equals } = + require('../common/wpt').harness; + +// Source: https://github.com/web-platform-tests/wpt/blob/6cef1d2087d6a07d7cc6cee8cf207eec92e27c5f/dom/events/Event-constructors.any.js#L91-L112 +test(function() { + const called = []; + const ev = new Event('Xx', { + get cancelable() { + called.push('cancelable'); + return false; + }, + get bubbles() { + called.push('bubbles'); + return true; + }, + get sweet() { + called.push('sweet'); + return 'x'; + }, + }); + assert_array_equals(called, ['bubbles', 'cancelable']); + assert_equals(ev.type, 'Xx'); + assert_equals(ev.bubbles, true); + assert_equals(ev.cancelable, false); + assert_equals(ev.sweet, undefined); +}); diff --git a/test/js/node/test/parallel/test-whatwg-events-eventtarget-this-of-listener.js b/test/js/node/test/parallel/test-whatwg-events-eventtarget-this-of-listener.js new file mode 100644 index 0000000000..16ee14feab --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-events-eventtarget-this-of-listener.js @@ -0,0 +1,119 @@ +'use strict'; + +require('../common'); +const { test, assert_equals, assert_unreached } = + require('../common/wpt').harness; + +// Manually ported from: https://github.com/web-platform-tests/wpt/blob/6cef1d2087d6a07d7cc6cee8cf207eec92e27c5f/dom/events/EventTarget-this-of-listener.html + +// Mock document +const document = { + createElement: () => new EventTarget(), + createTextNode: () => new EventTarget(), + createDocumentFragment: () => new EventTarget(), + createComment: () => new EventTarget(), + createProcessingInstruction: () => new EventTarget(), +}; + +test(() => { + const nodes = [ + document.createElement('p'), + document.createTextNode('some text'), + document.createDocumentFragment(), + document.createComment('a comment'), + document.createProcessingInstruction('target', 'data'), + ]; + + let callCount = 0; + for (const node of nodes) { + node.addEventListener('someevent', function() { + ++callCount; + assert_equals(this, node); + }); + + node.dispatchEvent(new Event('someevent')); + } + + assert_equals(callCount, nodes.length); +}, 'the this value inside the event listener callback should be the node'); + +test(() => { + const nodes = [ + document.createElement('p'), + document.createTextNode('some text'), + document.createDocumentFragment(), + document.createComment('a comment'), + document.createProcessingInstruction('target', 'data'), + ]; + + let callCount = 0; + for (const node of nodes) { + const handler = {}; + + node.addEventListener('someevent', handler); + handler.handleEvent = function() { + ++callCount; + assert_equals(this, handler); + }; + + node.dispatchEvent(new Event('someevent')); + } + + assert_equals(callCount, nodes.length); +}, 'addEventListener should not require handleEvent to be defined on object listeners'); + +test(() => { + const nodes = [ + document.createElement('p'), + document.createTextNode('some text'), + document.createDocumentFragment(), + document.createComment('a comment'), + document.createProcessingInstruction('target', 'data'), + ]; + + let callCount = 0; + for (const node of nodes) { + function handler() { + ++callCount; + assert_equals(this, node); + } + + handler.handleEvent = () => { + assert_unreached('should not call the handleEvent method on a function'); + }; + + node.addEventListener('someevent', handler); + + node.dispatchEvent(new Event('someevent')); + } + + assert_equals(callCount, nodes.length); +}, 'handleEvent properties added to a function before addEventListener are not reached'); + +test(() => { + const nodes = [ + document.createElement('p'), + document.createTextNode('some text'), + document.createDocumentFragment(), + document.createComment('a comment'), + document.createProcessingInstruction('target', 'data'), + ]; + + let callCount = 0; + for (const node of nodes) { + function handler() { + ++callCount; + assert_equals(this, node); + } + + node.addEventListener('someevent', handler); + + handler.handleEvent = () => { + assert_unreached('should not call the handleEvent method on a function'); + }; + + node.dispatchEvent(new Event('someevent')); + } + + assert_equals(callCount, nodes.length); +}, 'handleEvent properties added to a function after addEventListener are not reached'); diff --git a/test/js/node/test/parallel/test-whatwg-readablestream.mjs b/test/js/node/test/parallel/test-whatwg-readablestream.mjs new file mode 100644 index 0000000000..57ebed6045 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-readablestream.mjs @@ -0,0 +1,70 @@ +import { mustCall } from '../common/index.mjs'; +import { ReadableStream } from 'stream/web'; +import assert from 'assert'; + +{ + // Test tee() with close in the nextTick after enqueue + async function read(stream) { + const chunks = []; + for await (const chunk of stream) + chunks.push(chunk); + return Buffer.concat(chunks).toString(); + } + + const [r1, r2] = new ReadableStream({ + start(controller) { + process.nextTick(() => { + controller.enqueue(new Uint8Array([102, 111, 111, 98, 97, 114])); + + process.nextTick(() => { + controller.close(); + }); + }); + } + }).tee(); + + (async () => { + const [dataReader1, dataReader2] = await Promise.all([ + read(r1), + read(r2), + ]); + + assert.strictEqual(dataReader1, dataReader2); + assert.strictEqual(dataReader1, 'foobar'); + assert.strictEqual(dataReader2, 'foobar'); + })().then(mustCall()); +} + +{ + // Test ReadableByteStream.tee() with close in the nextTick after enqueue + async function read(stream) { + const chunks = []; + for await (const chunk of stream) + chunks.push(chunk); + return Buffer.concat(chunks).toString(); + } + + const [r1, r2] = new ReadableStream({ + type: 'bytes', + start(controller) { + process.nextTick(() => { + controller.enqueue(new Uint8Array([102, 111, 111, 98, 97, 114])); + + process.nextTick(() => { + controller.close(); + }); + }); + } + }).tee(); + + (async () => { + const [dataReader1, dataReader2] = await Promise.all([ + read(r1), + read(r2), + ]); + + assert.strictEqual(dataReader1, dataReader2); + assert.strictEqual(dataReader1, 'foobar'); + assert.strictEqual(dataReader2, 'foobar'); + })().then(mustCall()); +} diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-deepequal.js b/test/js/node/test/parallel/test-whatwg-url-custom-deepequal.js new file mode 100644 index 0000000000..9150b1561b --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-deepequal.js @@ -0,0 +1,18 @@ +'use strict'; +// This tests that the internal flags in URL objects are consistent, as manifest +// through assert libraries. +// See https://github.com/nodejs/node/issues/24211 + +// Tests below are not from WPT. + +require('../common'); +const assert = require('assert'); + +assert.deepStrictEqual( + new URL('./foo', 'https://example.com/'), + new URL('https://example.com/foo') +); +assert.deepStrictEqual( + new URL('./foo', 'https://user:pass@example.com/'), + new URL('https://user:pass@example.com/foo') +); diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-domainto.js b/test/js/node/test/parallel/test-whatwg-url-custom-domainto.js new file mode 100644 index 0000000000..b7458d7a8e --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-domainto.js @@ -0,0 +1,57 @@ +'use strict'; + +// Tests below are not from WPT. + +const common = require('../common'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +const assert = require('assert'); +const { domainToASCII, domainToUnicode } = require('url'); + +const tests = require('../fixtures/url-idna'); +const fixtures = require('../common/fixtures'); +const wptToASCIITests = require( + fixtures.path('wpt', 'url', 'resources', 'toascii.json') +); + +{ + const expectedError = { code: 'ERR_MISSING_ARGS', name: 'TypeError' }; + assert.throws(() => domainToASCII(), expectedError); + assert.throws(() => domainToUnicode(), expectedError); + assert.strictEqual(domainToASCII(undefined), 'undefined'); + assert.strictEqual(domainToUnicode(undefined), 'undefined'); +} + +{ + for (const [i, { ascii, unicode }] of tests.entries()) { + assert.strictEqual(ascii, domainToASCII(unicode), + `domainToASCII(${i + 1})`); + assert.strictEqual(unicode, domainToUnicode(ascii), + `domainToUnicode(${i + 1})`); + assert.strictEqual(ascii, domainToASCII(domainToUnicode(ascii)), + `domainToASCII(domainToUnicode(${i + 1}))`); + assert.strictEqual(unicode, domainToUnicode(domainToASCII(unicode)), + `domainToUnicode(domainToASCII(${i + 1}))`); + } +} + +{ + for (const [i, test] of wptToASCIITests.entries()) { + if (typeof test === 'string') + continue; // skip comments + const { comment, input, output } = test; + let caseComment = `Case ${i + 1}`; + if (comment) + caseComment += ` (${comment})`; + if (output === null) { + assert.strictEqual(domainToASCII(input), '', caseComment); + assert.strictEqual(domainToUnicode(input), '', caseComment); + } else { + assert.strictEqual(domainToASCII(input), output, caseComment); + const roundtripped = domainToASCII(domainToUnicode(input)); + assert.strictEqual(roundtripped, output, caseComment); + } + } +} diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-href-side-effect.js b/test/js/node/test/parallel/test-whatwg-url-custom-href-side-effect.js new file mode 100644 index 0000000000..30967d9fea --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-href-side-effect.js @@ -0,0 +1,15 @@ +'use strict'; + +// Tests below are not from WPT. +require('../common'); +const assert = require('assert'); + +const ref = new URL('http://example.com/path'); +const url = new URL('http://example.com/path'); +assert.throws(() => { + url.href = ''; +}, { + name: 'TypeError' +}); + +assert.deepStrictEqual(url, ref); diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-inspect.js b/test/js/node/test/parallel/test-whatwg-url-custom-inspect.js new file mode 100644 index 0000000000..946c097eac --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-inspect.js @@ -0,0 +1,70 @@ +'use strict'; + +// Tests below are not from WPT. + +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const util = require('util'); +const assert = require('assert'); + +const url = new URL('https://username:password@host.name:8080/path/name/?que=ry#hash'); + +assert.strictEqual( + util.inspect(url), + `URL { + href: 'https://username:password@host.name:8080/path/name/?que=ry#hash', + origin: 'https://host.name:8080', + protocol: 'https:', + username: 'username', + password: 'password', + host: 'host.name:8080', + hostname: 'host.name', + port: '8080', + pathname: '/path/name/', + search: '?que=ry', + searchParams: URLSearchParams { 'que' => 'ry' }, + hash: '#hash' +}`); + +assert.strictEqual( + util.inspect(url, { showHidden: true }), + `URL { + href: 'https://username:password@host.name:8080/path/name/?que=ry#hash', + origin: 'https://host.name:8080', + protocol: 'https:', + username: 'username', + password: 'password', + host: 'host.name:8080', + hostname: 'host.name', + port: '8080', + pathname: '/path/name/', + search: '?que=ry', + searchParams: URLSearchParams { 'que' => 'ry' }, + hash: '#hash', + [Symbol(context)]: URLContext { + href: 'https://username:password@host.name:8080/path/name/?que=ry#hash', + protocol_end: 6, + username_end: 16, + host_start: 25, + host_end: 35, + pathname_start: 40, + search_start: 51, + hash_start: 58, + port: 8080, + scheme_type: 2, + [hasPort]: [Getter], + [hasSearch]: [Getter], + [hasHash]: [Getter] + } +}`); + +assert.strictEqual( + util.inspect({ a: url }, { depth: 0 }), + '{ a: URL {} }'); + +class MyURL extends URL {} +assert(util.inspect(new MyURL(url.href)).startsWith('MyURL {')); diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-parsing.js b/test/js/node/test/parallel/test-whatwg-url-custom-parsing.js new file mode 100644 index 0000000000..cdeda59eec --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-parsing.js @@ -0,0 +1,87 @@ +'use strict'; + +// Tests below are not from WPT. + +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const tests = require( + fixtures.path('wpt', 'url', 'resources', 'urltestdata.json') +); + +const originalFailures = tests.filter((test) => test.failure); + +const typeFailures = [ + { input: '' }, + { input: 'test' }, + { input: undefined }, + { input: 0 }, + { input: true }, + { input: false }, + { input: null }, + { input: new Date() }, + { input: new RegExp() }, + { input: 'test', base: null }, + { input: 'http://nodejs.org', base: null }, + { input: () => {} }, +]; + +// See https://github.com/w3c/web-platform-tests/pull/10955 +// > If `failure` is true, parsing `about:blank` against `base` +// > must give failure. This tests that the logic for converting +// > base URLs into strings properly fails the whole parsing +// > algorithm if the base URL cannot be parsed. +const aboutBlankFailures = originalFailures + .map((test) => ({ + input: 'about:blank', + base: test.input, + failure: true + })); + +const failureTests = originalFailures + .concat(typeFailures) + .concat(aboutBlankFailures); + +const expectedError = { code: 'ERR_INVALID_URL', name: 'TypeError' }; + +for (const test of failureTests) { + assert.throws( + () => new URL(test.input, test.base), + (error) => { + assert.throws(() => { throw error; }, expectedError); + assert.strictEqual(`${error}`, 'TypeError: Invalid URL'); + assert.strictEqual(error.message, 'Invalid URL'); + return true; + }); +} + +const additional_tests = + require(fixtures.path('url-tests-additional.js')); + +for (const test of additional_tests) { + const url = new URL(test.url); + if (test.href) assert.strictEqual(url.href, test.href); + if (test.origin) assert.strictEqual(url.origin, test.origin); + if (test.protocol) assert.strictEqual(url.protocol, test.protocol); + if (test.username) assert.strictEqual(url.username, test.username); + if (test.password) assert.strictEqual(url.password, test.password); + if (test.hostname) assert.strictEqual(url.hostname, test.hostname); + if (test.host) assert.strictEqual(url.host, test.host); + if (test.port !== undefined) assert.strictEqual(url.port, test.port); + if (test.pathname) assert.strictEqual(url.pathname, test.pathname); + if (test.search) assert.strictEqual(url.search, test.search); + if (test.hash) assert.strictEqual(url.hash, test.hash); +} + +assert.throws(() => { + new URL(); +}, { + name: 'TypeError', + code: 'ERR_MISSING_ARGS', +}); diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-searchparams-sort.js b/test/js/node/test/parallel/test-whatwg-url-custom-searchparams-sort.js new file mode 100644 index 0000000000..e0b0c5c1ed --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-searchparams-sort.js @@ -0,0 +1,51 @@ +'use strict'; + +// Tests below are not from WPT. + +require('../common'); +const { test, assert_array_equals } = require('../common/wpt').harness; + +// TODO(joyeecheung): upstream this to WPT, if possible - even +// just as a test for large inputs. Other implementations may +// have a similar cutoff anyway. + +// Test bottom-up iterative stable merge sort because we only use that +// algorithm to sort > 100 search params. +const tests = [{ input: '', output: [] }]; +const pairs = []; +for (let i = 10; i < 100; i++) { + pairs.push([`a${i}`, 'b']); + tests[0].output.push([`a${i}`, 'b']); +} +tests[0].input = pairs.sort(() => Math.random() > 0.5) + .map((pair) => pair.join('=')).join('&'); + +tests.push( + { + 'input': 'z=a&=b&c=d', + 'output': [['', 'b'], ['c', 'd'], ['z', 'a']] + } +); + +tests.forEach((val) => { + test(() => { + const params = new URLSearchParams(val.input); + let i = 0; + params.sort(); + for (const param of params) { + assert_array_equals(param, val.output[i]); + i++; + } + }, `Parse and sort: ${val.input}`); + + test(() => { + const url = new URL(`?${val.input}`, 'https://example/'); + url.searchParams.sort(); + const params = new URLSearchParams(url.search); + let i = 0; + for (const param of params) { + assert_array_equals(param, val.output[i]); + i++; + } + }, `URL parse and sort: ${val.input}`); +}); diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-searchparams.js b/test/js/node/test/parallel/test-whatwg-url-custom-searchparams.js new file mode 100644 index 0000000000..faec86e017 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-searchparams.js @@ -0,0 +1,147 @@ +'use strict'; + +// Tests below are not from WPT. + +require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD' + + '&a=%EF%BF%BD&a=%F0%9F%98%80&a=%EF%BF%BD%EF%BF%BD' + + '&a=%5Bobject+Object%5D'; +const values = ['a', 1, true, undefined, null, '\uD83D', '\uDE00', + '\uD83D\uDE00', '\uDE00\uD83D', {}]; +const normalizedValues = ['a', '1', 'true', 'undefined', 'null', '\uFFFD', + '\uFFFD', '\uD83D\uDE00', '\uFFFD\uFFFD', + '[object Object]']; + +const m = new URL('http://example.org'); +const ownSymbolsBeforeGetterAccess = Object.getOwnPropertySymbols(m); +const sp = m.searchParams; +assert.deepStrictEqual(Object.getOwnPropertySymbols(m), ownSymbolsBeforeGetterAccess); + +assert(sp); +assert.strictEqual(sp.toString(), ''); +assert.strictEqual(m.search, ''); + +assert(!sp.has('a')); +values.forEach((i) => sp.set('a', i)); +assert(sp.has('a')); +assert.strictEqual(sp.get('a'), '[object Object]'); +sp.delete('a'); +assert(!sp.has('a')); + +m.search = ''; +assert.strictEqual(sp.toString(), ''); + +values.forEach((i) => sp.append('a', i)); +assert(sp.has('a')); +assert.strictEqual(sp.getAll('a').length, values.length); +assert.strictEqual(sp.get('a'), 'a'); + +assert.strictEqual(sp.toString(), serialized); + +assert.strictEqual(m.search, `?${serialized}`); + +sp.delete('a'); +values.forEach((i) => sp.append('a', i)); +assert.strictEqual(m.href, `http://example.org/?${serialized}`); + +sp.delete('a'); +values.forEach((i) => sp.append('a', i)); +assert.strictEqual(m.toString(), `http://example.org/?${serialized}`); + +sp.delete('a'); +values.forEach((i) => sp.append('a', i)); +assert.strictEqual(m.toJSON(), `http://example.org/?${serialized}`); + +sp.delete('a'); +values.forEach((i) => sp.append('a', i)); +m.href = 'http://example.org'; +assert.strictEqual(m.href, 'http://example.org/'); +assert.strictEqual(sp.size, 0); + +sp.delete('a'); +values.forEach((i) => sp.append('a', i)); +m.search = ''; +assert.strictEqual(m.href, 'http://example.org/'); +assert.strictEqual(sp.size, 0); + +sp.delete('a'); +values.forEach((i) => sp.append('a', i)); +m.pathname = '/test'; +assert.strictEqual(m.href, `http://example.org/test?${serialized}`); +m.pathname = ''; + +sp.delete('a'); +values.forEach((i) => sp.append('a', i)); +m.hash = '#test'; +assert.strictEqual(m.href, `http://example.org/?${serialized}#test`); +m.hash = ''; + +assert.strictEqual(sp[Symbol.iterator], sp.entries); + +let key, val; +let n = 0; +for ([key, val] of sp) { + assert.strictEqual(key, 'a', n); + assert.strictEqual(val, normalizedValues[n], n); + n++; +} +n = 0; +for (key of sp.keys()) { + assert.strictEqual(key, 'a', n); + n++; +} +n = 0; +for (val of sp.values()) { + assert.strictEqual(val, normalizedValues[n], n); + n++; +} +n = 0; +sp.forEach(function(val, key, obj) { + assert.strictEqual(this, undefined, n); + assert.strictEqual(key, 'a', n); + assert.strictEqual(val, normalizedValues[n], n); + assert.strictEqual(obj, sp, n); + n++; +}); +sp.forEach(function() { + assert.strictEqual(this, m); +}, m); + +{ + const callbackErr = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }; + assert.throws(() => sp.forEach(), callbackErr); + assert.throws(() => sp.forEach(1), callbackErr); +} + +m.search = '?a=a&b=b'; +assert.strictEqual(sp.toString(), 'a=a&b=b'); + +const tests = require(fixtures.path('url-searchparams.js')); + +for (const [input, expected, parsed] of tests) { + if (input[0] !== '?') { + const sp = new URLSearchParams(input); + assert.strictEqual(String(sp), expected); + assert.deepStrictEqual(Array.from(sp), parsed); + + m.search = input; + assert.strictEqual(String(m.searchParams), expected); + assert.deepStrictEqual(Array.from(m.searchParams), parsed); + } + + { + const sp = new URLSearchParams(`?${input}`); + assert.strictEqual(String(sp), expected); + assert.deepStrictEqual(Array.from(sp), parsed); + + m.search = `?${input}`; + assert.strictEqual(String(m.searchParams), expected); + assert.deepStrictEqual(Array.from(m.searchParams), parsed); + } +} diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-setters.js b/test/js/node/test/parallel/test-whatwg-url-custom-setters.js new file mode 100644 index 0000000000..b98bf5d8d3 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-setters.js @@ -0,0 +1,60 @@ +'use strict'; + +// Tests below are not from WPT. + +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const assert = require('assert'); +const { test, assert_equals } = require('../common/wpt').harness; +const fixtures = require('../common/fixtures'); + +// TODO(joyeecheung): we should submit these to the upstream +const additionalTestCases = + require(fixtures.path('url-setter-tests-additional.js')); + +{ + for (const attributeToBeSet in additionalTestCases) { + if (attributeToBeSet === 'comment') { + continue; + } + const testCases = additionalTestCases[attributeToBeSet]; + for (const testCase of testCases) { + let name = `Setting <${testCase.href}>.${attributeToBeSet}` + + ` = "${testCase.new_value}"`; + if ('comment' in testCase) { + name += ` ${testCase.comment}`; + } + test(function() { + const url = new URL(testCase.href); + url[attributeToBeSet] = testCase.new_value; + for (const attribute in testCase.expected) { + assert_equals(url[attribute], testCase.expected[attribute]); + } + }, `URL: ${name}`); + } + } +} + +{ + const url = new URL('http://example.com/'); + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + const props = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(url)); + for (const [name, { set }] of Object.entries(props)) { + if (set) { + assert.throws(() => url[name] = obj, + /^Error: toString$/, + `url.${name} = { toString() { throw ... } }`); + assert.throws(() => url[name] = sym, + /^TypeError: Cannot convert a Symbol value to a string$/, + `url.${name} = ${String(sym)}`); + } + } +} diff --git a/test/js/node/test/parallel/test-whatwg-url-custom-tostringtag.js b/test/js/node/test/parallel/test-whatwg-url-custom-tostringtag.js new file mode 100644 index 0000000000..54e5850a8f --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-custom-tostringtag.js @@ -0,0 +1,32 @@ +'use strict'; + +// Tests below are not from WPT. + +require('../common'); +const assert = require('assert'); + +const toString = Object.prototype.toString; + +const url = new URL('http://example.org'); +const sp = url.searchParams; +const spIterator = sp.entries(); + +const test = [ + [url, 'URL'], + [sp, 'URLSearchParams'], + [spIterator, 'URLSearchParams Iterator'], + // Web IDL spec says we have to return 'URLPrototype', but it is too + // expensive to implement; therefore, use Chrome's behavior for now, until + // spec is changed. + [Object.getPrototypeOf(url), 'URL'], + [Object.getPrototypeOf(sp), 'URLSearchParams'], + [Object.getPrototypeOf(spIterator), 'URLSearchParams Iterator'], +]; + +test.forEach(([obj, expected]) => { + assert.strictEqual(obj[Symbol.toStringTag], expected, + `${obj[Symbol.toStringTag]} !== ${expected}`); + const str = toString.call(obj); + assert.strictEqual(str, `[object ${expected}]`, + `${str} !== [object ${expected}]`); +}); diff --git a/test/js/node/test/parallel/test-whatwg-url-override-hostname.js b/test/js/node/test/parallel/test-whatwg-url-override-hostname.js new file mode 100644 index 0000000000..61e3412c6b --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-override-hostname.js @@ -0,0 +1,20 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +{ + const url = new (class extends URL { get hostname() { return 'bar.com'; } })('http://foo.com/'); + assert.strictEqual(url.href, 'http://foo.com/'); + assert.strictEqual(url.toString(), 'http://foo.com/'); + assert.strictEqual(url.toJSON(), 'http://foo.com/'); + assert.strictEqual(url.hash, ''); + assert.strictEqual(url.host, 'foo.com'); + assert.strictEqual(url.hostname, 'bar.com'); + assert.strictEqual(url.origin, 'http://foo.com'); + assert.strictEqual(url.password, ''); + assert.strictEqual(url.protocol, 'http:'); + assert.strictEqual(url.username, ''); + assert.strictEqual(url.search, ''); + assert.strictEqual(url.searchParams.toString(), ''); +} diff --git a/test/js/node/test/parallel/test-whatwg-url-toascii.js b/test/js/node/test/parallel/test-whatwg-url-toascii.js new file mode 100644 index 0000000000..e5180bfb34 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-toascii.js @@ -0,0 +1,86 @@ +'use strict'; +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const fixtures = require('../common/fixtures'); +const { test, assert_equals, assert_throws } = require('../common/wpt').harness; + +const request = { + response: require( + fixtures.path('wpt', 'url', 'resources', 'toascii.json') + ) +}; + +// The following tests are copied from WPT. Modifications to them should be +// upstreamed first. +// Refs: https://github.com/w3c/web-platform-tests/blob/4839a0a804/url/toascii.window.js +// License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html + +/* eslint-disable */ +// async_test(t => { +// const request = new XMLHttpRequest() +// request.open("GET", "toascii.json") +// request.send() +// request.responseType = "json" +// request.onload = t.step_func_done(() => { + runTests(request.response) +// }) +// }, "Loading data…") + +function makeURL(type, input) { + input = "https://" + input + "/x" + if(type === "url") { + return new URL(input) + } else { + const url = document.createElement(type) + url.href = input + return url + } +} + +function runTests(tests) { + for(var i = 0, l = tests.length; i < l; i++) { + let hostTest = tests[i] + if (typeof hostTest === "string") { + continue // skip comments + } + const typeName = { "url": "URL", "a": "", "area": "" } + // ;["url", "a", "area"].forEach((type) => { + ;["url"].forEach((type) => { + test(() => { + if(hostTest.output !== null) { + const url = makeURL("url", hostTest.input) + assert_equals(url.host, hostTest.output) + assert_equals(url.hostname, hostTest.output) + assert_equals(url.pathname, "/x") + assert_equals(url.href, "https://" + hostTest.output + "/x") + } else { + if(type === "url") { + assert_throws(new TypeError, () => makeURL("url", hostTest.input)) + } else { + const url = makeURL(type, hostTest.input) + assert_equals(url.host, "") + assert_equals(url.hostname, "") + assert_equals(url.pathname, "") + assert_equals(url.href, "https://" + hostTest.input + "/x") + } + } + }, hostTest.input + " (using " + typeName[type] + ")") + ;["host", "hostname"].forEach((val) => { + test(() => { + const url = makeURL(type, "x") + url[val] = hostTest.input + if(hostTest.output !== null) { + assert_equals(url[val], hostTest.output) + } else { + assert_equals(url[val], "x") + } + }, hostTest.input + " (using " + typeName[type] + "." + val + ")") + }) + }) + } +} +/* eslint-enable */ diff --git a/test/js/node/test/parallel/test-windows-abort-exitcode.js b/test/js/node/test/parallel/test-windows-abort-exitcode.js new file mode 100644 index 0000000000..e2e6570c1c --- /dev/null +++ b/test/js/node/test/parallel/test-windows-abort-exitcode.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +if (!common.isWindows) + common.skip('test is windows specific'); + +const assert = require('assert'); +const spawn = require('child_process').spawn; + +// This test makes sure that an aborted node process +// exits with code 3 on Windows. +// Spawn a child, force an abort, and then check the +// exit code in the parent. + +if (process.argv[2] === 'child') { + process.abort(); +} else { + const child = spawn(process.execPath, [__filename, 'child']); + child.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 134); + assert.strictEqual(signal, null); + })); +} diff --git a/test/js/node/test/parallel/test-windows-failed-heap-allocation.js b/test/js/node/test/parallel/test-windows-failed-heap-allocation.js new file mode 100644 index 0000000000..b9183239a2 --- /dev/null +++ b/test/js/node/test/parallel/test-windows-failed-heap-allocation.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +// This test ensures that an out of memory error exits with code 134 on Windows + +if (!common.isWindows) return common.skip('Windows-only'); + +const assert = require('assert'); +const { exec } = require('child_process'); + +if (process.argv[2] === 'heapBomb') { + // Heap bomb, imitates a memory leak quickly + const fn = (nM) => [...Array(nM)].map((i) => fn(nM * 2)); + fn(2); +} + +// Run child in tmpdir to avoid report files in repo +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// --max-old-space-size=3 is the min 'old space' in V8, explodes fast +const cmd = `"${process.execPath}" --max-old-space-size=30 "${__filename}"`; +exec(`${cmd} heapBomb`, { cwd: tmpdir.path }, common.mustCall((err, stdout, stderr) => { + const msg = `Wrong exit code of ${err.code}! Expected 134 for abort`; + // Note: common.nodeProcessAborted() is not asserted here because it + // returns true on 134 as well as 0x80000003 (V8's base::OS::Abort) + assert.strictEqual(err.code, 134, msg); +})); diff --git a/test/js/node/test/parallel/test-worker-arraybuffer-zerofill.js b/test/js/node/test/parallel/test-worker-arraybuffer-zerofill.js new file mode 100644 index 0000000000..3dcf4c006e --- /dev/null +++ b/test/js/node/test/parallel/test-worker-arraybuffer-zerofill.js @@ -0,0 +1,33 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Make sure that allocating uninitialized ArrayBuffers in one thread does not +// affect the zero-initialization in other threads. + +const w = new Worker(` +const { parentPort } = require('worker_threads'); + +function post() { + const uint32array = new Uint32Array(64); + parentPort.postMessage(uint32array.reduce((a, b) => a + b)); +} + +setInterval(post, 0); +`, { eval: true }); + +function allocBuffers() { + Buffer.allocUnsafe(32 * 1024 * 1024); +} + +const interval = setInterval(allocBuffers, 0); + +let messages = 0; +w.on('message', (sum) => { + assert.strictEqual(sum, 0); + if (messages++ === 100) { + clearInterval(interval); + w.terminate(); + } +}); diff --git a/test/js/node/test/parallel/test-worker-cjs-workerdata.js b/test/js/node/test/parallel/test-worker-cjs-workerdata.js new file mode 100644 index 0000000000..949011fee8 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-cjs-workerdata.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +const workerData = 'Hello from main thread'; + +const worker = new Worker(fixtures.path('worker-data.cjs'), { + workerData +}); + +worker.on('message', common.mustCall((message) => { + assert.strictEqual(message, workerData); +})); diff --git a/test/js/node/test/parallel/test-worker-cleanexit-with-js.js b/test/js/node/test/parallel/test-worker-cleanexit-with-js.js new file mode 100644 index 0000000000..b4725e297a --- /dev/null +++ b/test/js/node/test/parallel/test-worker-cleanexit-with-js.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); + +// Harden the thread interactions on the exit path. +// Ensure workers are able to bail out safe at +// arbitrary execution points. By running a lot of +// JS code in a tight loop, the expectation +// is that those will be at various control flow points +// preferably in the JS land. + +const { Worker } = require('worker_threads'); +const code = 'setInterval(() => {' + + "require('v8').deserialize(require('v8').serialize({ foo: 'bar' }));" + + "require('vm').runInThisContext('x = \"foo\";');" + + "eval('const y = \"vm\";');}, 10);"; +for (let i = 0; i < 9; i++) { + new Worker(code, { eval: true }); +} +new Worker(code, { eval: true }).on('online', common.mustCall((msg) => { + process.exit(0); +})); diff --git a/test/js/node/test/parallel/test-worker-cleanexit-with-moduleload.js b/test/js/node/test/parallel/test-worker-cleanexit-with-moduleload.js new file mode 100644 index 0000000000..f2e8ad786f --- /dev/null +++ b/test/js/node/test/parallel/test-worker-cleanexit-with-moduleload.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); + +// Harden the thread interactions on the exit path. +// Ensure workers are able to bail out safe at +// arbitrary execution points. By using a number of +// internal modules as load candidates, the expectation +// is that those will be at various control flow points +// preferably in the C++ land. + +const { Worker } = require('worker_threads'); +const modules = [ 'fs', 'assert', 'async_hooks', 'buffer', 'child_process', + 'net', 'http', 'os', 'path', 'v8', 'vm', +]; +if (common.hasCrypto) { + modules.push('https'); +} + +for (let i = 0; i < 10; i++) { + new Worker(`const modules = [${modules.map((m) => `'${m}'`)}];` + + 'modules.forEach((module) => {' + + 'const m = require(module);' + + '});', { eval: true }); +} + +// Allow workers to go live. +setTimeout(() => { + process.exit(0); +}, 200); diff --git a/test/js/node/test/parallel/test-worker-esm-exit.js b/test/js/node/test/parallel/test-worker-esm-exit.js new file mode 100644 index 0000000000..8c38faf3b7 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-esm-exit.js @@ -0,0 +1,11 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +const w = new Worker(fixtures.path('es-modules/import-process-exit.mjs')); +w.on('error', common.mustNotCall()); +w.on('exit', + common.mustCall((code) => assert.strictEqual(code, 42)) +); diff --git a/test/js/node/test/parallel/test-worker-esmodule.js b/test/js/node/test/parallel/test-worker-esmodule.js new file mode 100644 index 0000000000..e7f9bd7aea --- /dev/null +++ b/test/js/node/test/parallel/test-worker-esmodule.js @@ -0,0 +1,10 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +const w = new Worker(fixtures.path('worker-script.mjs')); +w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); +})); diff --git a/test/js/node/test/parallel/test-worker-exit-event-error.js b/test/js/node/test/parallel/test-worker-exit-event-error.js new file mode 100644 index 0000000000..e2427c7dff --- /dev/null +++ b/test/js/node/test/parallel/test-worker-exit-event-error.js @@ -0,0 +1,8 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +process.on('uncaughtException', common.mustCall()); + +new Worker('', { eval: true }) + .on('exit', common.mustCall(() => { throw new Error('foo'); })); diff --git a/test/js/node/test/parallel/test-worker-exit-from-uncaught-exception.js b/test/js/node/test/parallel/test-worker-exit-from-uncaught-exception.js new file mode 100644 index 0000000000..b9f8aeee97 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-exit-from-uncaught-exception.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Check that `process.exit()` can be called inside a Worker from an uncaught +// exception handler. + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + w.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 42); + })); + return; +} + +process.on('uncaughtException', () => { + process.exit(42); +}); + +throw new Error(); diff --git a/test/js/node/test/parallel/test-worker-http2-generic-streams-terminate.js b/test/js/node/test/parallel/test-worker-http2-generic-streams-terminate.js new file mode 100644 index 0000000000..234697fb4d --- /dev/null +++ b/test/js/node/test/parallel/test-worker-http2-generic-streams-terminate.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); +const { Duplex } = require('stream'); +const { Worker, workerData } = require('worker_threads'); + +// Tests the interaction between terminating a Worker thread and running +// the native SetImmediate queue, which may attempt to perform multiple +// calls into JS even though one already terminates the Worker. + +if (!workerData) { + const counter = new Int32Array(new SharedArrayBuffer(4)); + const worker = new Worker(__filename, { workerData: { counter } }); + worker.on('exit', common.mustCall(() => { + assert.strictEqual(counter[0], 1); + })); +} else { + const { counter } = workerData; + + // Start two HTTP/2 connections. This will trigger write()s call from inside + // the SetImmediate queue. + for (let i = 0; i < 2; i++) { + http2.connect('http://localhost', { + createConnection() { + return new Duplex({ + write(chunk, enc, cb) { + Atomics.add(counter, 0, 1); + process.exit(); + }, + read() { } + }); + } + }); + } +} diff --git a/test/js/node/test/parallel/test-worker-invalid-workerdata.js b/test/js/node/test/parallel/test-worker-invalid-workerdata.js new file mode 100644 index 0000000000..08fd78bdc0 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-invalid-workerdata.js @@ -0,0 +1,14 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// This tests verifies that failing to serialize workerData does not keep +// the process alive. +// Refs: https://github.com/nodejs/node/issues/22736 + +assert.throws(() => { + new Worker('./worker.js', { + workerData: { fn: () => {} } + }); +}, /DataCloneError/); diff --git a/test/js/node/test/parallel/test-worker-load-file-with-extension-other-than-js.js b/test/js/node/test/parallel/test-worker-load-file-with-extension-other-than-js.js new file mode 100644 index 0000000000..5dca297576 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-load-file-with-extension-other-than-js.js @@ -0,0 +1,9 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +const { Worker } = require('worker_threads'); + +(common.mustCall(() => { + new Worker(fixtures.path('worker-script.ts')); +}))(); diff --git a/test/js/node/test/parallel/test-worker-message-channel-sharedarraybuffer.js b/test/js/node/test/parallel/test-worker-message-channel-sharedarraybuffer.js new file mode 100644 index 0000000000..220aa978b1 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-message-channel-sharedarraybuffer.js @@ -0,0 +1,28 @@ +// Flags: --expose-gc +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +{ + const sharedArrayBuffer = new SharedArrayBuffer(12); + const local = Buffer.from(sharedArrayBuffer); + + const w = new Worker(` + const { parentPort } = require('worker_threads'); + parentPort.on('message', ({ sharedArrayBuffer }) => { + const local = Buffer.from(sharedArrayBuffer); + local.write('world!', 6); + parentPort.postMessage('written!'); + }); + `, { eval: true }); + w.on('message', common.mustCall(() => { + assert.strictEqual(local.toString(), 'Hello world!'); + global.gc(); + w.terminate(); + })); + w.postMessage({ sharedArrayBuffer }); + // This would be a race condition if the memory regions were overlapping + local.write('Hello '); +} diff --git a/test/js/node/test/parallel/test-worker-message-port-infinite-message-loop.js b/test/js/node/test/parallel/test-worker-message-port-infinite-message-loop.js new file mode 100644 index 0000000000..0cd1cc0680 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-message-port-infinite-message-loop.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { MessageChannel } = require('worker_threads'); + +// Make sure that an infinite asynchronous .on('message')/postMessage loop +// does not lead to a stack overflow and does not starve the event loop. +// We schedule timeouts both from before the .on('message') handler and +// inside of it, which both should run. + +const { port1, port2 } = new MessageChannel(); +let count = 0; +port1.on('message', () => { + if (count === 0) { + setTimeout(common.mustCall(() => { + port1.close(); + }), 0); + } + + port2.postMessage(0); + assert(count++ < 10000, `hit ${count} loop iterations`); +}); + +port2.postMessage(0); + +// This is part of the test -- the event loop should be available and not stall +// out due to the recursive .postMessage() calls. +setTimeout(common.mustCall(), 0); diff --git a/test/js/node/test/parallel/test-worker-message-port-transfer-terminate.js b/test/js/node/test/parallel/test-worker-message-port-transfer-terminate.js new file mode 100644 index 0000000000..dddf91e3f3 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-message-port-transfer-terminate.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); +const { Worker, MessageChannel } = require('worker_threads'); + +// Check the interaction of calling .terminate() while transferring +// MessagePort objects; in particular, that it does not crash the process. + +for (let i = 0; i < 10; ++i) { + const w = new Worker( + "require('worker_threads').parentPort.on('message', () => {})", + { eval: true }); + setImmediate(() => { + const port = new MessageChannel().port1; + w.postMessage({ port }, [ port ]); + w.terminate(); + }); +} diff --git a/test/js/node/test/parallel/test-worker-message-port-wasm-module.js b/test/js/node/test/parallel/test-worker-message-port-wasm-module.js new file mode 100644 index 0000000000..b1aa522dc4 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-message-port-wasm-module.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const { Worker } = require('worker_threads'); +const wasmModule = new WebAssembly.Module(fixtures.readSync('simple.wasm')); + +const worker = new Worker(` +const { parentPort } = require('worker_threads'); +parentPort.once('message', ({ wasmModule }) => { + const instance = new WebAssembly.Instance(wasmModule); + parentPort.postMessage(instance.exports.add(10, 20)); +}); +`, { eval: true }); + +worker.once('message', common.mustCall((num) => assert.strictEqual(num, 30))); +worker.postMessage({ wasmModule }); diff --git a/test/js/node/test/parallel/test-worker-mjs-workerdata.js b/test/js/node/test/parallel/test-worker-mjs-workerdata.js new file mode 100644 index 0000000000..b0a65e2e80 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-mjs-workerdata.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +const workerData = 'Hello from main thread'; + +const worker = new Worker(fixtures.path('worker-data.mjs'), { + workerData +}); + +worker.on('message', common.mustCall((message) => { + assert.strictEqual(message, workerData); +})); diff --git a/test/js/node/test/parallel/test-worker-nested-on-process-exit.js b/test/js/node/test/parallel/test-worker-nested-on-process-exit.js new file mode 100644 index 0000000000..aa544fa328 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-nested-on-process-exit.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, workerData } = require('worker_threads'); + +// Test that 'exit' events for nested Workers are not received when a Worker +// terminates itself through process.exit(). + +if (workerData === null) { + const nestedWorkerExitCounter = new Int32Array(new SharedArrayBuffer(4)); + const w = new Worker(__filename, { workerData: nestedWorkerExitCounter }); + w.on('exit', common.mustCall(() => { + assert.strictEqual(nestedWorkerExitCounter[0], 0); + })); +} else { + const nestedWorker = new Worker('setInterval(() => {}, 100)', { eval: true }); + // The counter should never be increased here. + nestedWorker.on('exit', () => workerData[0]++); + nestedWorker.on('online', () => process.exit()); +} diff --git a/test/js/node/test/parallel/test-worker-no-sab.js b/test/js/node/test/parallel/test-worker-no-sab.js new file mode 100644 index 0000000000..e96c987484 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-no-sab.js @@ -0,0 +1,21 @@ +// Flags: --enable-sharedarraybuffer-per-context + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Regression test for https://github.com/nodejs/node/issues/39717. + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + + w.on('exit', common.mustCall((status) => { + assert.strictEqual(status, 2); + })); +} else { + process.exit(2); +} diff --git a/test/js/node/test/parallel/test-worker-non-fatal-uncaught-exception.js b/test/js/node/test/parallel/test-worker-non-fatal-uncaught-exception.js new file mode 100644 index 0000000000..01df55eec1 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-non-fatal-uncaught-exception.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Check that `process._fatalException()` returns a boolean when run inside a +// worker. + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + w.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + })); + return; +} + +process.once('uncaughtException', () => { + process.nextTick(() => { + assert.strictEqual(res, true); + }); +}); + +const res = process._fatalException(new Error()); diff --git a/test/js/node/test/parallel/test-worker-on-process-exit.js b/test/js/node/test/parallel/test-worker-on-process-exit.js new file mode 100644 index 0000000000..ec1c4affd1 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-on-process-exit.js @@ -0,0 +1,22 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const { Worker } = require('worker_threads'); + +// Test that 'exit' events for Workers are not received when the main thread +// terminates itself through process.exit(). + +if (process.argv[2] !== 'child') { + const { + stdout, stderr, status + } = spawnSync(process.execPath, [__filename, 'child'], { encoding: 'utf8' }); + assert.strictEqual(stderr, ''); + assert.strictEqual(stdout, ''); + assert.strictEqual(status, 0); +} else { + const nestedWorker = new Worker('setInterval(() => {}, 100)', { eval: true }); + // This console.log() should never fire. + nestedWorker.on('exit', () => console.log('exit event received')); + nestedWorker.on('online', () => process.exit()); +} diff --git a/test/js/node/test/parallel/test-worker-onmessage-not-a-function.js b/test/js/node/test/parallel/test-worker-onmessage-not-a-function.js new file mode 100644 index 0000000000..df07353075 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-onmessage-not-a-function.js @@ -0,0 +1,25 @@ +// When MessagePort.onmessage is set to a value that is not a function, the +// setter should call .unref() and .stop(), clearing a previous onmessage +// listener from holding the event loop open. This test confirms that +// functionality. + +'use strict'; +const common = require('../common'); +const { Worker, parentPort } = require('worker_threads'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + w.postMessage(2); +} else { + // .onmessage uses a setter. Set .onmessage to a function that ultimately + // should not be called. This will call .ref() and .start() which will keep + // the event loop open (and prevent this from exiting) if the subsequent + // assignment of a value to .onmessage doesn't call .unref() and .stop(). + parentPort.onmessage = common.mustNotCall(); + // Setting `onmessage` to a value that is not a function should clear the + // previous value and also should allow the event loop to exit. (In other + // words, this test should exit rather than run indefinitely.) + parentPort.onmessage = 'fhqwhgads'; +} diff --git a/test/js/node/test/parallel/test-worker-onmessage.js b/test/js/node/test/parallel/test-worker-onmessage.js new file mode 100644 index 0000000000..3ed10755ce --- /dev/null +++ b/test/js/node/test/parallel/test-worker-onmessage.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, parentPort } = require('worker_threads'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + const expectation = [ 4, undefined, null ]; + const actual = []; + w.on('message', common.mustCall((message) => { + actual.push(message); + if (actual.length === expectation.length) { + assert.deepStrictEqual(expectation, actual); + w.terminate(); + } + }, expectation.length)); + w.postMessage(2); +} else { + parentPort.onmessage = common.mustCall((message) => { + parentPort.postMessage(message.data * 2); + parentPort.postMessage(undefined); + parentPort.postMessage(null); + }); +} diff --git a/test/js/node/test/parallel/test-worker-parent-port-ref.js b/test/js/node/test/parallel/test-worker-parent-port-ref.js new file mode 100644 index 0000000000..c1e79b9faa --- /dev/null +++ b/test/js/node/test/parallel/test-worker-parent-port-ref.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { isMainThread, parentPort, Worker } = require('worker_threads'); + +// This test makes sure that we manipulate the references of +// `parentPort` correctly so that any worker threads will +// automatically exit when there are no any other references. +{ + if (isMainThread) { + const worker = new Worker(__filename); + + worker.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + }), 1); + + worker.on('online', common.mustCall()); + } else { + const messageCallback = () => {}; + parentPort.on('message', messageCallback); + // The thread won't exit if we don't make the 'message' listener off. + parentPort.off('message', messageCallback); + } +} diff --git a/test/js/node/test/parallel/test-worker-ref-onexit.js b/test/js/node/test/parallel/test-worker-ref-onexit.js new file mode 100644 index 0000000000..24c940f8c8 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-ref-onexit.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// Check that worker.unref() makes the 'exit' event not be emitted, if it is +// the only thing we would otherwise be waiting for. + +// Use `setInterval()` to make sure the worker is alive until the end of the +// event loop turn. +const w = new Worker('setInterval(() => {}, 100);', { eval: true }); +w.unref(); +w.on('exit', common.mustNotCall()); diff --git a/test/js/node/test/parallel/test-worker-ref.js b/test/js/node/test/parallel/test-worker-ref.js new file mode 100644 index 0000000000..645fc0fbad --- /dev/null +++ b/test/js/node/test/parallel/test-worker-ref.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// Test that calling worker.unref() leads to 'beforeExit' being emitted, and +// that we can resurrect the worker using worker.ref() from there. + +const w = new Worker(` +const { parentPort } = require('worker_threads'); +parentPort.once('message', (msg) => { + parentPort.postMessage(msg); +}); +`, { eval: true }); + +process.once('beforeExit', common.mustCall(() => { + console.log('beforeExit'); + w.ref(); + w.postMessage({ hello: 'world' }); +})); + +w.once('message', common.mustCall((msg) => { + console.log('message', msg); +})); + +w.on('exit', common.mustCall(() => { + console.log('exit'); +})); + +w.unref(); diff --git a/test/js/node/test/parallel/test-worker-relative-path-double-dot.js b/test/js/node/test/parallel/test-worker-relative-path-double-dot.js new file mode 100644 index 0000000000..86707c1590 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-relative-path-double-dot.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const path = require('path'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker_threads'); + +if (isMainThread) { + const cwdName = path.relative('../', '.'); + const relativePath = path.relative('.', __filename); + const w = new Worker(path.join('..', cwdName, relativePath)); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); + })); +} else { + parentPort.postMessage('Hello, world!'); +} diff --git a/test/js/node/test/parallel/test-worker-relative-path.js b/test/js/node/test/parallel/test-worker-relative-path.js new file mode 100644 index 0000000000..73dc5b3637 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-relative-path.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +const path = require('path'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker_threads'); + +if (isMainThread) { + const w = new Worker(`./${path.relative('.', __filename)}`); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); + })); +} else { + parentPort.postMessage('Hello, world!'); +} diff --git a/test/js/node/test/parallel/test-worker-safe-getters.js b/test/js/node/test/parallel/test-worker-safe-getters.js new file mode 100644 index 0000000000..69856659a5 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-safe-getters.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const { Worker, isMainThread } = require('worker_threads'); + +if (isMainThread) { + const w = new Worker(__filename, { + stdin: true, + stdout: true, + stderr: true + }); + + const { stdin, stdout, stderr } = w; + + w.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + + // `postMessage` should not throw after termination + // (this mimics the browser behavior). + w.postMessage('foobar'); + w.ref(); + w.unref(); + + // Sanity check. + assert.strictEqual(w.threadId, -1); + assert.strictEqual(w.stdin, stdin); + assert.strictEqual(w.stdout, stdout); + assert.strictEqual(w.stderr, stderr); + })); +} else { + process.exit(0); +} diff --git a/test/js/node/test/parallel/test-worker-sharedarraybuffer-from-worker-thread.js b/test/js/node/test/parallel/test-worker-sharedarraybuffer-from-worker-thread.js new file mode 100644 index 0000000000..ce8410f6dd --- /dev/null +++ b/test/js/node/test/parallel/test-worker-sharedarraybuffer-from-worker-thread.js @@ -0,0 +1,28 @@ +// Flags: --debug-arraybuffer-allocations +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Regression test for https://github.com/nodejs/node/issues/28777 +// Make sure that SharedArrayBuffers and transferred ArrayBuffers created in +// Worker threads are accessible after the creating thread ended. + +for (const ctor of ['ArrayBuffer', 'SharedArrayBuffer']) { + const w = new Worker(` + const { parentPort } = require('worker_threads'); + const arrayBuffer = new ${ctor}(4); + parentPort.postMessage( + arrayBuffer, + '${ctor}' === 'SharedArrayBuffer' ? [] : [arrayBuffer]); + `, { eval: true }); + + let arrayBuffer; + w.once('message', common.mustCall((message) => arrayBuffer = message)); + w.once('exit', common.mustCall(() => { + assert.strictEqual(arrayBuffer.constructor.name, ctor); + const uint8array = new Uint8Array(arrayBuffer); + uint8array[0] = 42; + assert.deepStrictEqual(uint8array, new Uint8Array([42, 0, 0, 0])); + })); +} diff --git a/test/js/node/test/parallel/test-worker-terminate-nested.js b/test/js/node/test/parallel/test-worker-terminate-nested.js new file mode 100644 index 0000000000..3924528cea --- /dev/null +++ b/test/js/node/test/parallel/test-worker-terminate-nested.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// Check that a Worker that's running another Worker can be terminated. + +const worker = new Worker(` +const { Worker, parentPort } = require('worker_threads'); +const worker = new Worker('setInterval(() => {}, 10);', { eval: true }); +worker.on('online', () => { + parentPort.postMessage({}); +}); +`, { eval: true }); + +worker.on('message', common.mustCall(() => worker.terminate())); diff --git a/test/js/node/test/parallel/test-worker-terminate-null-handler.js b/test/js/node/test/parallel/test-worker-terminate-null-handler.js new file mode 100644 index 0000000000..9db2e38b5c --- /dev/null +++ b/test/js/node/test/parallel/test-worker-terminate-null-handler.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Test that calling worker.terminate() if kHandler is null should return an +// empty promise that resolves to undefined, even when a callback is passed + +const worker = new Worker(` +const { parentPort } = require('worker_threads'); +parentPort.postMessage({ hello: 'world' }); +`, { eval: true }); + +process.once('beforeExit', common.mustCall(() => worker.ref())); + +worker.on('exit', common.mustCall(() => { + worker.terminate().then((res) => assert.strictEqual(res, undefined)); + worker.terminate(() => null).then( + (res) => assert.strictEqual(res, undefined) + ); +})); + +worker.unref(); diff --git a/test/js/node/test/parallel/test-worker-terminate-timers.js b/test/js/node/test/parallel/test-worker-terminate-timers.js new file mode 100644 index 0000000000..defaadf9fe --- /dev/null +++ b/test/js/node/test/parallel/test-worker-terminate-timers.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// Test that calling .terminate() during a timer callback works fine. + +for (const fn of ['setTimeout', 'setImmediate', 'setInterval']) { + const worker = new Worker(` + const { parentPort } = require('worker_threads'); + ${fn}(() => { + parentPort.postMessage({}); + while (true); + });`, { eval: true }); + + worker.on('message', common.mustCallAtLeast(() => { + worker.terminate(); + })); +} diff --git a/test/js/node/test/parallel/test-worker-unref-from-message-during-exit.js b/test/js/node/test/parallel/test-worker-unref-from-message-during-exit.js new file mode 100644 index 0000000000..3e696f369e --- /dev/null +++ b/test/js/node/test/parallel/test-worker-unref-from-message-during-exit.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// This used to crash because the `.unref()` was unexpected while the Worker +// was exiting. + +const w = new Worker(` +require('worker_threads').parentPort.postMessage({}); +`, { eval: true }); +w.on('message', common.mustCall(() => { + w.unref(); +})); + +// Wait a bit so that the 'message' event is emitted while the Worker exits. +Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); diff --git a/test/js/node/test/parallel/test-worker-workerdata-sharedarraybuffer.js b/test/js/node/test/parallel/test-worker-workerdata-sharedarraybuffer.js new file mode 100644 index 0000000000..4e3d508ac9 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-workerdata-sharedarraybuffer.js @@ -0,0 +1,32 @@ +// Flags: --expose-gc +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +{ + const sharedArrayBuffer = new SharedArrayBuffer(12); + const local = Buffer.from(sharedArrayBuffer); + + const w = new Worker(` + const { parentPort, workerData } = require('worker_threads'); + const local = Buffer.from(workerData.sharedArrayBuffer); + + parentPort.on('message', () => { + local.write('world!', 6); + parentPort.postMessage('written!'); + }); + `, { + eval: true, + workerData: { sharedArrayBuffer } + }); + w.on('message', common.mustCall(() => { + assert.strictEqual(local.toString(), 'Hello world!'); + global.gc(); + w.terminate(); + })); + w.postMessage({}); + // This would be a race condition if the memory regions were overlapping + local.write('Hello '); +} diff --git a/test/js/node/test/parallel/test-worker.js b/test/js/node/test/parallel/test-worker.js new file mode 100644 index 0000000000..9154aaa12b --- /dev/null +++ b/test/js/node/test/parallel/test-worker.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker_threads'); + +const kTestString = 'Hello, world!'; + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, kTestString); + })); +} else { + setImmediate(() => { + process.nextTick(() => { + parentPort.postMessage(kTestString); + }); + }); +} diff --git a/test/js/node/test/parallel/test-worker.mjs b/test/js/node/test/parallel/test-worker.mjs new file mode 100644 index 0000000000..4ee3f7dc96 --- /dev/null +++ b/test/js/node/test/parallel/test-worker.mjs @@ -0,0 +1,18 @@ +import { mustCall } from '../common/index.mjs'; +import assert from 'assert'; +import { Worker, isMainThread, parentPort } from 'worker_threads'; + +const kTestString = 'Hello, world!'; + +if (isMainThread) { + const w = new Worker(new URL(import.meta.url)); + w.on('message', mustCall((message) => { + assert.strictEqual(message, kTestString); + })); +} else { + setImmediate(() => { + process.nextTick(() => { + parentPort.postMessage(kTestString); + }); + }); +} diff --git a/test/js/node/test/parallel/test-zlib-brotli-16GB.js b/test/js/node/test/parallel/test-zlib-brotli-16GB.js new file mode 100644 index 0000000000..9b894320e9 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-brotli-16GB.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const { createBrotliDecompress } = require('node:zlib'); +const strictEqual = require('node:assert').strictEqual; +const { getDefaultHighWaterMark } = require('stream'); + +// This tiny HEX string is a 16GB file. +// This test verifies that the stream actually stops. +/* eslint-disable @stylistic/js/max-len */ +const content = 'cfffff7ff82700e2b14020f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c32200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bff3f'; + +const buf = Buffer.from(content, 'hex'); + +const decoder = createBrotliDecompress(); +decoder.end(buf); + +// We need to wait to verify that the libuv thread pool had time +// to process the data and the buffer is not empty. +setTimeout(common.mustCall(() => { + // There is only one chunk in the buffer + strictEqual(decoder._readableState.buffer.length, getDefaultHighWaterMark() / (16 * 1024)); +}), common.platformTimeout(500)); diff --git a/test/js/node/test/parallel/test-zlib-brotli-flush.js b/test/js/node/test/parallel/test-zlib-brotli-flush.js new file mode 100644 index 0000000000..fd730bfacd --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-brotli-flush.js @@ -0,0 +1,27 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fixtures = require('../common/fixtures'); + +const file = fixtures.readSync('person.jpg'); +const chunkSize = 16; +const deflater = new zlib.BrotliCompress(); + +const chunk = file.slice(0, chunkSize); +const expectedFull = Buffer.from('iweA/9j/4AAQSkZJRgABAQEASA==', 'base64'); +let actualFull; + +deflater.write(chunk, function() { + deflater.flush(function() { + const bufs = []; + let buf; + while ((buf = deflater.read()) !== null) + bufs.push(buf); + actualFull = Buffer.concat(bufs); + }); +}); + +process.once('exit', function() { + assert.deepStrictEqual(actualFull, expectedFull); +}); diff --git a/test/js/node/test/parallel/test-zlib-brotli-from-brotli.js b/test/js/node/test/parallel/test-zlib-brotli-from-brotli.js new file mode 100644 index 0000000000..7e6bfb419e --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-brotli-from-brotli.js @@ -0,0 +1,31 @@ +'use strict'; +// Test unzipping a file that was created with a non-node brotli lib, +// piped in as fast as possible. + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fixtures = require('../common/fixtures'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const decompress = new zlib.BrotliDecompress(); + +const fs = require('fs'); + +const fixture = fixtures.path('person.jpg.br'); +const unzippedFixture = fixtures.path('person.jpg'); +const outputFile = tmpdir.resolve('person.jpg'); +const expect = fs.readFileSync(unzippedFixture); +const inp = fs.createReadStream(fixture); +const out = fs.createWriteStream(outputFile); + +inp.pipe(decompress).pipe(out); +out.on('close', common.mustCall(() => { + const actual = fs.readFileSync(outputFile); + assert.strictEqual(actual.length, expect.length); + for (let i = 0, l = actual.length; i < l; i++) { + assert.strictEqual(actual[i], expect[i], `byte[${i}]`); + } +})); diff --git a/test/js/node/test/parallel/test-zlib-brotli-from-string.js b/test/js/node/test/parallel/test-zlib-brotli-from-string.js new file mode 100644 index 0000000000..30be44517a --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-brotli-from-string.js @@ -0,0 +1,38 @@ +'use strict'; +// Test compressing and uncompressing a string with brotli + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const inputString = 'ΩΩLorem ipsum dolor sit amet, consectetur adipiscing eli' + + 't. Morbi faucibus, purus at gravida dictum, libero arcu ' + + 'convallis lacus, in commodo libero metus eu nisi. Nullam' + + ' commodo, neque nec porta placerat, nisi est fermentum a' + + 'ugue, vitae gravida tellus sapien sit amet tellus. Aenea' + + 'n non diam orci. Proin quis elit turpis. Suspendisse non' + + ' diam ipsum. Suspendisse nec ullamcorper odio. Vestibulu' + + 'm arcu mi, sodales non suscipit id, ultrices ut massa. S' + + 'ed ac sem sit amet arcu malesuada fermentum. Nunc sed. '; +const compressedString = 'G/gBQBwHdky2aHV5KK9Snf05//1pPdmNw/7232fnIm1IB' + + 'K1AA8RsN8OB8Nb7Lpgk3UWWUlzQXZyHQeBBbXMTQXC1j7' + + 'wg3LJs9LqOGHRH2bj/a2iCTLLx8hBOyTqgoVuD1e+Qqdn' + + 'f1rkUNyrWq6LtOhWgxP3QUwdhKGdZm3rJWaDDBV7+pDk1' + + 'MIkrmjp4ma2xVi5MsgJScA3tP1I7mXeby6MELozrwoBQD' + + 'mVTnEAicZNj4lkGqntJe2qSnGyeMmcFgraK94vCg/4iLu' + + 'Tw5RhKhnVY++dZ6niUBmRqIutsjf5TzwF5iAg8a9UkjF5' + + '2eZ0tB2vo6v8SqVfNMkBmmhxr0NT9LkYF69aEjlYzj7IE' + + 'KmEUQf1HBogRYhFIt4ymRNEgHAIzOyNEsQM='; + +zlib.brotliCompress(inputString, common.mustCall((err, buffer) => { + assert(inputString.length > buffer.length); + + zlib.brotliDecompress(buffer, common.mustCall((err, buffer) => { + assert.strictEqual(buffer.toString(), inputString); + })); +})); + +const buffer = Buffer.from(compressedString, 'base64'); +zlib.brotliDecompress(buffer, common.mustCall((err, buffer) => { + assert.strictEqual(buffer.toString(), inputString); +})); diff --git a/test/js/node/test/parallel/test-zlib-brotli-kmaxlength-rangeerror.js b/test/js/node/test/parallel/test-zlib-brotli-kmaxlength-rangeerror.js new file mode 100644 index 0000000000..6a59ad34b0 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-brotli-kmaxlength-rangeerror.js @@ -0,0 +1,28 @@ +'use strict'; +require('../common'); + +// This test ensures that zlib throws a RangeError if the final buffer needs to +// be larger than kMaxLength and concatenation fails. +// https://github.com/nodejs/node/pull/1811 + +const assert = require('assert'); + +// Change kMaxLength for zlib to trigger the error without having to allocate +// large Buffers. +const buffer = require('buffer'); +const oldkMaxLength = buffer.kMaxLength; +buffer.kMaxLength = 64; +const zlib = require('zlib'); +buffer.kMaxLength = oldkMaxLength; + +const encoded = Buffer.from('G38A+CXCIrFAIAM=', 'base64'); + +// Async +zlib.brotliDecompress(encoded, function(err) { + assert.ok(err instanceof RangeError); +}); + +// Sync +assert.throws(function() { + zlib.brotliDecompressSync(encoded); +}, RangeError); diff --git a/test/js/node/test/parallel/test-zlib-brotli.js b/test/js/node/test/parallel/test-zlib-brotli.js new file mode 100644 index 0000000000..ef31db3dd6 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-brotli.js @@ -0,0 +1,94 @@ +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const zlib = require('zlib'); + +// Test some brotli-specific properties of the brotli streams that can not +// be easily covered through expanding zlib-only tests. + +const sampleBuffer = fixtures.readSync('/pss-vectors.json'); + +{ + // Test setting the quality parameter at stream creation: + const sizes = []; + for (let quality = zlib.constants.BROTLI_MIN_QUALITY; + quality <= zlib.constants.BROTLI_MAX_QUALITY; + quality++) { + const encoded = zlib.brotliCompressSync(sampleBuffer, { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: quality + } + }); + sizes.push(encoded.length); + } + + // Increasing quality should roughly correspond to decreasing compressed size: + for (let i = 0; i < sizes.length - 1; i++) { + assert(sizes[i + 1] <= sizes[i] * 1.05, sizes); // 5 % margin of error. + } + assert(sizes[0] > sizes[sizes.length - 1], sizes); +} + +{ + // Test that setting out-of-bounds option values or keys fails. + assert.throws(() => { + zlib.createBrotliCompress({ + params: { + 10000: 0 + } + }); + }, { + code: 'ERR_BROTLI_INVALID_PARAM', + name: 'RangeError', + message: '10000 is not a valid Brotli parameter' + }); + + // Test that accidentally using duplicate keys fails. + assert.throws(() => { + zlib.createBrotliCompress({ + params: { + '0': 0, + '00': 0 + } + }); + }, { + code: 'ERR_BROTLI_INVALID_PARAM', + name: 'RangeError', + message: '00 is not a valid Brotli parameter' + }); + + assert.throws(() => { + zlib.createBrotliCompress({ + params: { + // This is a boolean flag + [zlib.constants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING]: 42 + } + }); + }, { + code: 'ERR_ZLIB_INITIALIZATION_FAILED', + name: 'Error', + message: 'Initialization failed' + }); +} + +{ + // Test options.flush range + assert.throws(() => { + zlib.brotliCompressSync('', { flush: zlib.constants.Z_FINISH }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.flush" is out of range. It must be >= 0 ' + + 'and <= 3. Received 4', + }); + + assert.throws(() => { + zlib.brotliCompressSync('', { finishFlush: zlib.constants.Z_FINISH }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.finishFlush" is out of range. It must be ' + + '>= 0 and <= 3. Received 4', + }); +} diff --git a/test/js/node/test/parallel/test-zlib-close-after-error.js b/test/js/node/test/parallel/test-zlib-close-after-error.js new file mode 100644 index 0000000000..63d418be09 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-close-after-error.js @@ -0,0 +1,16 @@ +'use strict'; +// https://github.com/nodejs/node/issues/6034 + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const decompress = zlib.createGunzip(15); + +decompress.on('error', common.mustCall((err) => { + assert.strictEqual(decompress._closed, true); + decompress.close(); +})); + +assert.strictEqual(decompress._closed, false); +decompress.write('something invalid'); diff --git a/test/js/node/test/parallel/test-zlib-close-after-write.js b/test/js/node/test/parallel/test-zlib-close-after-write.js new file mode 100644 index 0000000000..211318dc5a --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-close-after-write.js @@ -0,0 +1,30 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const zlib = require('zlib'); + +zlib.gzip('hello', common.mustCall((err, out) => { + const unzip = zlib.createGunzip(); + unzip.write(out); + unzip.close(common.mustCall()); +})); diff --git a/test/js/node/test/parallel/test-zlib-close-in-ondata.js b/test/js/node/test/parallel/test-zlib-close-in-ondata.js new file mode 100644 index 0000000000..44d996311d --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-close-in-ondata.js @@ -0,0 +1,10 @@ +'use strict'; + +const common = require('../common'); +const zlib = require('zlib'); + +const ts = zlib.createGzip(); +const buf = Buffer.alloc(1024 * 1024 * 20); + +ts.on('data', common.mustCall(() => ts.close())); +ts.end(buf); diff --git a/test/js/node/test/parallel/test-zlib-const.js b/test/js/node/test/parallel/test-zlib-const.js new file mode 100644 index 0000000000..342c8c712a --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-const.js @@ -0,0 +1,38 @@ +/* eslint-disable strict */ +require('../common'); +const assert = require('assert'); + +const zlib = require('zlib'); + +assert.strictEqual(zlib.constants.Z_OK, 0, + [ + 'Expected Z_OK to be 0;', + `got ${zlib.constants.Z_OK}`, + ].join(' ')); +zlib.constants.Z_OK = 1; +assert.strictEqual(zlib.constants.Z_OK, 0, + [ + 'Z_OK should be immutable.', + `Expected to get 0, got ${zlib.constants.Z_OK}`, + ].join(' ')); + +assert.strictEqual(zlib.codes.Z_OK, 0, + `Expected Z_OK to be 0; got ${zlib.codes.Z_OK}`); +zlib.codes.Z_OK = 1; +assert.strictEqual(zlib.codes.Z_OK, 0, + [ + 'Z_OK should be immutable.', + `Expected to get 0, got ${zlib.codes.Z_OK}`, + ].join(' ')); +zlib.codes = { Z_OK: 1 }; +assert.strictEqual(zlib.codes.Z_OK, 0, + [ + 'Z_OK should be immutable.', + `Expected to get 0, got ${zlib.codes.Z_OK}`, + ].join(' ')); + +assert.ok(Object.isFrozen(zlib.codes), + [ + 'Expected zlib.codes to be frozen, but Object.isFrozen', + `returned ${Object.isFrozen(zlib.codes)}`, + ].join(' ')); diff --git a/test/js/node/test/parallel/test-zlib-convenience-methods.js b/test/js/node/test/parallel/test-zlib-convenience-methods.js new file mode 100644 index 0000000000..01ec7e211b --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-convenience-methods.js @@ -0,0 +1,133 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Test convenience methods with and without options supplied + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +// Must be a multiple of 4 characters in total to test all ArrayBufferView +// types. +const expectStr = 'blah'.repeat(8); +const expectBuf = Buffer.from(expectStr); + +const opts = { + level: 9, + chunkSize: 1024, +}; + +const optsInfo = { + info: true +}; + +for (const [type, expect] of [ + ['string', expectStr], + ['Buffer', expectBuf], + ...common.getBufferSources(expectBuf).map((obj) => + [obj[Symbol.toStringTag], obj] + ), +]) { + for (const method of [ + ['gzip', 'gunzip', 'Gzip', 'Gunzip'], + ['gzip', 'unzip', 'Gzip', 'Unzip'], + ['deflate', 'inflate', 'Deflate', 'Inflate'], + ['deflateRaw', 'inflateRaw', 'DeflateRaw', 'InflateRaw'], + ['brotliCompress', 'brotliDecompress', + 'BrotliCompress', 'BrotliDecompress'], + ]) { + zlib[method[0]](expect, opts, common.mustCall((err, result) => { + zlib[method[1]](result, opts, common.mustCall((err, result) => { + assert.strictEqual(result.toString(), expectStr, + `Should get original string after ${method[0]}/` + + `${method[1]} ${type} with options.`); + })); + })); + + zlib[method[0]](expect, common.mustCall((err, result) => { + zlib[method[1]](result, common.mustCall((err, result) => { + assert.strictEqual(result.toString(), expectStr, + `Should get original string after ${method[0]}/` + + `${method[1]} ${type} without options.`); + })); + })); + + zlib[method[0]](expect, optsInfo, common.mustCall((err, result) => { + assert.ok(result.engine instanceof zlib[method[2]], + `Should get engine ${method[2]} after ${method[0]} ` + + `${type} with info option.`); + + const compressed = result.buffer; + zlib[method[1]](compressed, optsInfo, common.mustCall((err, result) => { + assert.strictEqual(result.buffer.toString(), expectStr, + `Should get original string after ${method[0]}/` + + `${method[1]} ${type} with info option.`); + assert.ok(result.engine instanceof zlib[method[3]], + `Should get engine ${method[3]} after ${method[0]} ` + + `${type} with info option.`); + })); + })); + + { + const compressed = zlib[`${method[0]}Sync`](expect, opts); + const decompressed = zlib[`${method[1]}Sync`](compressed, opts); + assert.strictEqual(decompressed.toString(), expectStr, + `Should get original string after ${method[0]}Sync/` + + `${method[1]}Sync ${type} with options.`); + } + + + { + const compressed = zlib[`${method[0]}Sync`](expect); + const decompressed = zlib[`${method[1]}Sync`](compressed); + assert.strictEqual(decompressed.toString(), expectStr, + `Should get original string after ${method[0]}Sync/` + + `${method[1]}Sync ${type} without options.`); + } + + + { + const compressed = zlib[`${method[0]}Sync`](expect, optsInfo); + assert.ok(compressed.engine instanceof zlib[method[2]], + `Should get engine ${method[2]} after ${method[0]} ` + + `${type} with info option.`); + const decompressed = zlib[`${method[1]}Sync`](compressed.buffer, + optsInfo); + assert.strictEqual(decompressed.buffer.toString(), expectStr, + `Should get original string after ${method[0]}Sync/` + + `${method[1]}Sync ${type} without options.`); + assert.ok(decompressed.engine instanceof zlib[method[3]], + `Should get engine ${method[3]} after ${method[0]} ` + + `${type} with info option.`); + } + } +} + +assert.throws( + () => zlib.gzip('abc'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "callback" argument must be of type function. ' + + 'Received undefined' + } +); diff --git a/test/js/node/test/parallel/test-zlib-crc32.js b/test/js/node/test/parallel/test-zlib-crc32.js new file mode 100644 index 0000000000..fb8d4958ec --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-crc32.js @@ -0,0 +1,211 @@ +'use strict'; + +require('../common'); +const zlib = require('zlib'); +const assert = require('assert'); +const { Buffer } = require('buffer'); + +// The following test data comes from +// https://github.com/zlib-ng/zlib-ng/blob/5401b24/test/test_crc32.cc +// test_crc32.cc -- crc32 unit test +// Copyright (C) 2019-2021 IBM Corporation +// Authors: Rogerio Alves +// Matheus Castanho +// For conditions of distribution and use, see copyright notice in zlib.h +// +const tests = [ + [0x0, 0x0, 0, 0x0], + [0xffffffff, 0x0, 0, 0x0], + [0x0, 0x0, 255, 0x0], /* BZ 174799. */ + [0x0, 0x0, 256, 0x0], + [0x0, 0x0, 257, 0x0], + [0x0, 0x0, 32767, 0x0], + [0x0, 0x0, 32768, 0x0], + [0x0, 0x0, 32769, 0x0], + [0x0, '', 0, 0x0], + [0xffffffff, '', 0, 0xffffffff], + [0x0, 'abacus', 6, 0xc3d7115b], + [0x0, 'backlog', 7, 0x269205], + [0x0, 'campfire', 8, 0x22a515f8], + [0x0, 'delta', 5, 0x9643fed9], + [0x0, 'executable', 10, 0xd68eda01], + [0x0, 'file', 4, 0x8c9f3610], + [0x0, 'greatest', 8, 0xc1abd6cd], + [0x0, 'hello', 5, 0x3610a686], + [0x0, 'inverter', 8, 0xc9e962c9], + [0x0, 'jigsaw', 6, 0xce4e3f69], + [0x0, 'karate', 6, 0x890be0e2], + [0x0, 'landscape', 9, 0xc4e0330b], + [0x0, 'machine', 7, 0x1505df84], + [0x0, 'nanometer', 9, 0xd4e19f39], + [0x0, 'oblivion', 8, 0xdae9de77], + [0x0, 'panama', 6, 0x66b8979c], + [0x0, 'quest', 5, 0x4317f817], + [0x0, 'resource', 8, 0xbc91f416], + [0x0, 'secret', 6, 0x5ca2e8e5], + [0x0, 'test', 4, 0xd87f7e0c], + [0x0, 'ultimate', 8, 0x3fc79b0b], + [0x0, 'vector', 6, 0x1b6e485b], + [0x0, 'walrus', 6, 0xbe769b97], + [0x0, 'xeno', 4, 0xe7a06444], + [0x0, 'yelling', 7, 0xfe3944e5], + [0x0, 'zlib', 4, 0x73887d3a], + [0x0, '4BJD7PocN1VqX0jXVpWB', 20, 0xd487a5a1], + [0x0, 'F1rPWI7XvDs6nAIRx41l', 20, 0x61a0132e], + [0x0, 'ldhKlsVkPFOveXgkGtC2', 20, 0xdf02f76], + [0x0, '5KKnGOOrs8BvJ35iKTOS', 20, 0x579b2b0a], + [0x0, '0l1tw7GOcem06Ddu7yn4', 20, 0xf7d16e2d], + [0x0, 'MCr47CjPIn9R1IvE1Tm5', 20, 0x731788f5], + [0x0, 'UcixbzPKTIv0SvILHVdO', 20, 0x7112bb11], + [0x0, 'dGnAyAhRQDsWw0ESou24', 20, 0xf32a0dac], + [0x0, 'di0nvmY9UYMYDh0r45XT', 20, 0x625437bb], + [0x0, '2XKDwHfAhFsV0RhbqtvH', 20, 0x896930f9], + [0x0, 'ZhrANFIiIvRnqClIVyeD', 20, 0x8579a37], + [0x0, 'v7Q9ehzioTOVeDIZioT1', 20, 0x632aa8e0], + [0x0, 'Yod5hEeKcYqyhfXbhxj2', 20, 0xc829af29], + [0x0, 'GehSWY2ay4uUKhehXYb0', 20, 0x1b08b7e8], + [0x0, 'kwytJmq6UqpflV8Y8GoE', 20, 0x4e33b192], + [0x0, '70684206568419061514', 20, 0x59a179f0], + [0x0, '42015093765128581010', 20, 0xcd1013d7], + [0x0, '88214814356148806939', 20, 0xab927546], + [0x0, '43472694284527343838', 20, 0x11f3b20c], + [0x0, '49769333513942933689', 20, 0xd562d4ca], + [0x0, '54979784887993251199', 20, 0x233395f7], + [0x0, '58360544869206793220', 20, 0x2d167fd5], + [0x0, '27347953487840714234', 20, 0x8b5108ba], + [0x0, '07650690295365319082', 20, 0xc46b3cd8], + [0x0, '42655507906821911703', 20, 0xc10b2662], + [0x0, '29977409200786225655', 20, 0xc9a0f9d2], + [0x0, '85181542907229116674', 20, 0x9341357b], + [0x0, '87963594337989416799', 20, 0xf0424937], + [0x0, '21395988329504168551', 20, 0xd7c4c31f], + [0x0, '51991013580943379423', 20, 0xf11edcc4], + [0x0, '*]+@!);({_$;}[_},?{?;(_?,=-][@', 30, 0x40795df4], + [0x0, '_@:_).&(#.[:[{[:)$++-($_;@[)}+', 30, 0xdd61a631], + [0x0, '&[!,[$_==}+.]@!;*(+},[;:)$;)-@', 30, 0xca907a99], + [0x0, ']{.[.+?+[[=;[?}_#&;[=)__$$:+=_', 30, 0xf652deac], + [0x0, '-%.)=/[@].:.(:,()$;=%@-$?]{%+%', 30, 0xaf39a5a9], + [0x0, '+]#$(@&.=:,*];/.!]%/{:){:@(;)$', 30, 0x6bebb4cf], + // eslint-disable-next-line no-template-curly-in-string + [0x0, ')-._.:?[&:.=+}(*$/=!.${;(=$@!}', 30, 0x76430bac], + [0x0, ':(_*&%/[[}+,?#$&*+#[([*-/#;%(]', 30, 0x6c80c388], + [0x0, '{[#-;:$/{)(+[}#]/{&!%(@)%:@-$:', 30, 0xd54d977d], + [0x0, '_{$*,}(&,@.)):=!/%(&(,,-?$}}}!', 30, 0xe3966ad5], + [0x0, + 'e$98KNzqaV)Y:2X?]77].{gKRD4G5{mHZk,Z)SpU%L3FSgv!Wb8MLAFdi{+fp)c,@8m6v)yXg@]HBDFk?.4&}g5_udE*JHCiH=aL', + 100, 0xe7c71db9], + [0x0, + 'r*Fd}ef+5RJQ;+W=4jTR9)R*p!B;]Ed7tkrLi;88U7g@3v!5pk2X6D)vt,.@N8c]@yyEcKi[vwUu@.Ppm@C6%Mv*3Nw}Y,58_aH)', + 100, 0xeaa52777], + [0x0, + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&', + 100, 0xcd472048], + [0x7a30360d, 'abacus', 6, 0xf8655a84], + [0x6fd767ee, 'backlog', 7, 0x1ed834b1], + [0xefeb7589, 'campfire', 8, 0x686cfca], + [0x61cf7e6b, 'delta', 5, 0x1554e4b1], + [0xdc712e2, 'executable', 10, 0x761b4254], + [0xad23c7fd, 'file', 4, 0x7abdd09b], + [0x85cb2317, 'greatest', 8, 0x4ba91c6b], + [0x9eed31b0, 'inverter', 8, 0xd5e78ba5], + [0xb94f34ca, 'jigsaw', 6, 0x23649109], + [0xab058a2, 'karate', 6, 0xc5591f41], + [0x5bff2b7a, 'landscape', 9, 0xf10eb644], + [0x605c9a5f, 'machine', 7, 0xbaa0a636], + [0x51bdeea5, 'nanometer', 9, 0x6af89afb], + [0x85c21c79, 'oblivion', 8, 0xecae222b], + [0x97216f56, 'panama', 6, 0x47dffac4], + [0x18444af2, 'quest', 5, 0x70c2fe36], + [0xbe6ce359, 'resource', 8, 0x1471d925], + [0x843071f1, 'secret', 6, 0x50c9a0db], + [0xf2480c60, 'ultimate', 8, 0xf973daf8], + [0x2d2feb3d, 'vector', 6, 0x344ac03d], + [0x7490310a, 'walrus', 6, 0x6d1408ef], + [0x97d247d4, 'xeno', 4, 0xe62670b5], + [0x93cf7599, 'yelling', 7, 0x1b36da38], + [0x73c84278, 'zlib', 4, 0x6432d127], + [0x228a87d1, '4BJD7PocN1VqX0jXVpWB', 20, 0x997107d0], + [0xa7a048d0, 'F1rPWI7XvDs6nAIRx41l', 20, 0xdc567274], + [0x1f0ded40, 'ldhKlsVkPFOveXgkGtC2', 20, 0xdcc63870], + [0xa804a62f, '5KKnGOOrs8BvJ35iKTOS', 20, 0x6926cffd], + [0x508fae6a, '0l1tw7GOcem06Ddu7yn4', 20, 0xb52b38bc], + [0xe5adaf4f, 'MCr47CjPIn9R1IvE1Tm5', 20, 0xf83b8178], + [0x67136a40, 'UcixbzPKTIv0SvILHVdO', 20, 0xc5213070], + [0xb00c4a10, 'dGnAyAhRQDsWw0ESou24', 20, 0xbc7648b0], + [0x2e0c84b5, 'di0nvmY9UYMYDh0r45XT', 20, 0xd8123a72], + [0x81238d44, '2XKDwHfAhFsV0RhbqtvH', 20, 0xd5ac5620], + [0xf853aa92, 'ZhrANFIiIvRnqClIVyeD', 20, 0xceae099d], + [0x5a692325, 'v7Q9ehzioTOVeDIZioT1', 20, 0xb07d2b24], + [0x3275b9f, 'Yod5hEeKcYqyhfXbhxj2', 20, 0x24ce91df], + [0x38371feb, 'GehSWY2ay4uUKhehXYb0', 20, 0x707b3b30], + [0xafc8bf62, 'kwytJmq6UqpflV8Y8GoE', 20, 0x16abc6a9], + [0x9b07db73, '70684206568419061514', 20, 0xae1fb7b7], + [0xe75b214, '42015093765128581010', 20, 0xd4eecd2d], + [0x72d0fe6f, '88214814356148806939', 20, 0x4660ec7], + [0xf857a4b1, '43472694284527343838', 20, 0xfd8afdf7], + [0x54b8e14, '49769333513942933689', 20, 0xc6d1b5f2], + [0xd6aa5616, '54979784887993251199', 20, 0x32476461], + [0x11e63098, '58360544869206793220', 20, 0xd917cf1a], + [0xbe92385, '27347953487840714234', 20, 0x4ad14a12], + [0x49511de0, '07650690295365319082', 20, 0xe37b5c6c], + [0x3db13bc1, '42655507906821911703', 20, 0x7cc497f1], + [0xbb899bea, '29977409200786225655', 20, 0x99781bb2], + [0xf6cd9436, '85181542907229116674', 20, 0x132256a1], + [0x9109e6c3, '87963594337989416799', 20, 0xbfdb2c83], + [0x75770fc, '21395988329504168551', 20, 0x8d9d1e81], + [0x69b1d19b, '51991013580943379423', 20, 0x7b6d4404], + [0xc6132975, '*]+@!);({_$;}[_},?{?;(_?,=-][@', 30, 0x8619f010], + [0xd58cb00c, '_@:_).&(#.[:[{[:)$++-($_;@[)}+', 30, 0x15746ac3], + [0xb63b8caa, '&[!,[$_==}+.]@!;*(+},[;:)$;)-@', 30, 0xaccf812f], + [0x8a45a2b8, ']{.[.+?+[[=;[?}_#&;[=)__$$:+=_', 30, 0x78af45de], + [0xcbe95b78, '-%.)=/[@].:.(:,()$;=%@-$?]{%+%', 30, 0x25b06b59], + [0x4ef8a54b, '+]#$(@&.=:,*];/.!]%/{:){:@(;)$', 30, 0x4ba0d08f], + // eslint-disable-next-line no-template-curly-in-string + [0x76ad267a, ')-._.:?[&:.=+}(*$/=!.${;(=$@!}', 30, 0xe26b6aac], + [0x569e613c, ':(_*&%/[[}+,?#$&*+#[([*-/#;%(]', 30, 0x7e2b0a66], + [0x36aa61da, '{[#-;:$/{)(+[}#]/{&!%(@)%:@-$:', 30, 0xb3430dc7], + [0xf67222df, '_{$*,}(&,@.)):=!/%(&(,,-?$}}}!', 30, 0x626c17a], + [0x74b34fd3, + 'e$98KNzqaV)Y:2X?]77].{gKRD4G5{mHZk,Z)SpU%L3FSgv!Wb8MLAFdi{+fp)c,@8m6v)yXg@]HBDFk?.4&}g5_udE*JHCiH=aL', + 100, 0xccf98060], + [0x351fd770, + 'r*Fd}ef+5RJQ;+W=4jTR9)R*p!B;]Ed7tkrLi;88U7g@3v!5pk2X6D)vt,.@N8c]@yyEcKi[vwUu@.Ppm@C6%Mv*3Nw}Y,58_aH)', + 100, 0xd8b95312], + [0xc45aef77, + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&', + 100, 0xbb1c9912], + [0xc45aef77, + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&' + + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&' + + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&' + + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&' + + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&' + + 'h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&', + 600, 0x888AFA5B], +]; + +for (const [ crc, data, len, expected ] of tests) { + if (data === 0) { + continue; + } + const buf = Buffer.from(data, 'utf8'); + assert.strictEqual(buf.length, len); + assert.strictEqual(zlib.crc32(buf, crc), expected, + `crc32('${data}', ${crc}) in buffer is not ${expected}`); + assert.strictEqual(zlib.crc32(buf.toString(), crc), expected, + `crc32('${data}', ${crc}) in string is not ${expected}`); + if (crc === 0) { + assert.strictEqual(zlib.crc32(buf), expected, + `crc32('${data}') in buffer is not ${expected}`); + assert.strictEqual(zlib.crc32(buf.toString()), expected, + `crc32('${data}') in string is not ${expected}`); + } +} + +[undefined, null, true, 1, () => {}, {}].forEach((invalid) => { + assert.throws(() => { zlib.crc32(invalid); }, { code: 'ERR_INVALID_ARG_TYPE' }); +}); + +[null, true, () => {}, {}].forEach((invalid) => { + assert.throws(() => { zlib.crc32('test', invalid); }, { code: 'ERR_INVALID_ARG_TYPE' }); +}); diff --git a/test/js/node/test/parallel/test-zlib-create-raw.js b/test/js/node/test/parallel/test-zlib-create-raw.js new file mode 100644 index 0000000000..92e21545e4 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-create-raw.js @@ -0,0 +1,15 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +{ + const inflateRaw = zlib.createInflateRaw(); + assert(inflateRaw instanceof zlib.InflateRaw); +} + +{ + const deflateRaw = zlib.createDeflateRaw(); + assert(deflateRaw instanceof zlib.DeflateRaw); +} diff --git a/test/js/node/test/parallel/test-zlib-deflate-constructors.js b/test/js/node/test/parallel/test-zlib-deflate-constructors.js new file mode 100644 index 0000000000..bf4b6d3374 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-deflate-constructors.js @@ -0,0 +1,309 @@ +'use strict'; + +require('../common'); + +const zlib = require('zlib'); +const assert = require('assert'); + +// Work with and without `new` keyword +assert.ok(zlib.Deflate() instanceof zlib.Deflate); +assert.ok(new zlib.Deflate() instanceof zlib.Deflate); + +assert.ok(zlib.DeflateRaw() instanceof zlib.DeflateRaw); +assert.ok(new zlib.DeflateRaw() instanceof zlib.DeflateRaw); + +// Throws if `options.chunkSize` is invalid +assert.throws( + () => new zlib.Deflate({ chunkSize: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.chunkSize" property must be of type number. ' + + 'Received type string ("test")' + } +); + +assert.throws( + () => new zlib.Deflate({ chunkSize: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.chunkSize" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ chunkSize: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.chunkSize" is out of range. It must ' + + 'be >= 64. Received 0' + } +); + +// Confirm that maximum chunk size cannot be exceeded because it is `Infinity`. +assert.strictEqual(zlib.constants.Z_MAX_CHUNK, Infinity); + +// Throws if `options.windowBits` is invalid +assert.throws( + () => new zlib.Deflate({ windowBits: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.windowBits" property must be of type number. ' + + 'Received type string ("test")' + } +); + +assert.throws( + () => new zlib.Deflate({ windowBits: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ windowBits: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ windowBits: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be >= 8 and <= 15. Received 0' + } +); + +// Throws if `options.level` is invalid +assert.throws( + () => new zlib.Deflate({ level: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.level" property must be of type number. ' + + 'Received type string ("test")' + } +); + +assert.throws( + () => new zlib.Deflate({ level: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.level" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ level: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.level" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ level: -2 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.level" is out of range. It must ' + + 'be >= -1 and <= 9. Received -2' + } +); + +// Throws if `level` invalid in `Deflate.prototype.params()` +assert.throws( + () => new zlib.Deflate().params('test'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "level" argument must be of type number. ' + + 'Received type string ("test")' + } +); + +assert.throws( + () => new zlib.Deflate().params(-Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "level" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "level" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(-2), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "level" is out of range. It must ' + + 'be >= -1 and <= 9. Received -2' + } +); + +// Throws if options.memLevel is invalid +assert.throws( + () => new zlib.Deflate({ memLevel: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.memLevel" property must be of type number. ' + + 'Received type string ("test")' + } +); + +assert.throws( + () => new zlib.Deflate({ memLevel: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ memLevel: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ memLevel: -2 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be >= 1 and <= 9. Received -2' + } +); + +// Does not throw if opts.strategy is valid +new zlib.Deflate({ strategy: zlib.constants.Z_FILTERED }); +new zlib.Deflate({ strategy: zlib.constants.Z_HUFFMAN_ONLY }); +new zlib.Deflate({ strategy: zlib.constants.Z_RLE }); +new zlib.Deflate({ strategy: zlib.constants.Z_FIXED }); +new zlib.Deflate({ strategy: zlib.constants.Z_DEFAULT_STRATEGY }); + +// Throws if options.strategy is invalid +assert.throws( + () => new zlib.Deflate({ strategy: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.strategy" property must be of type number. ' + + 'Received type string ("test")' + } +); + +assert.throws( + () => new zlib.Deflate({ strategy: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.strategy" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ strategy: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.strategy" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ strategy: -2 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.strategy" is out of range. It must ' + + 'be >= 0 and <= 4. Received -2' + } +); + +// Throws TypeError if `strategy` is invalid in `Deflate.prototype.params()` +assert.throws( + () => new zlib.Deflate().params(0, 'test'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "strategy" argument must be of type number. ' + + 'Received type string ("test")' + } +); + +assert.throws( + () => new zlib.Deflate().params(0, -Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "strategy" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(0, Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "strategy" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(0, -2), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "strategy" is out of range. It must ' + + 'be >= 0 and <= 4. Received -2' + } +); + +// Throws if opts.dictionary is not a Buffer +assert.throws( + () => new zlib.Deflate({ dictionary: 'not a buffer' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } +); diff --git a/test/js/node/test/parallel/test-zlib-deflate-raw-inherits.js b/test/js/node/test/parallel/test-zlib-deflate-raw-inherits.js new file mode 100644 index 0000000000..34bf31058a --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-deflate-raw-inherits.js @@ -0,0 +1,27 @@ +'use strict'; + +require('../common'); +const { DeflateRaw } = require('zlib'); +const { Readable } = require('stream'); + +// Validates that zlib.DeflateRaw can be inherited +// with Object.setPrototypeOf + +function NotInitialized(options) { + DeflateRaw.call(this, options); + this.prop = true; +} +Object.setPrototypeOf(NotInitialized.prototype, DeflateRaw.prototype); +Object.setPrototypeOf(NotInitialized, DeflateRaw); + +const dest = new NotInitialized(); + +const read = new Readable({ + read() { + this.push(Buffer.from('a test string')); + this.push(null); + } +}); + +read.pipe(dest); +dest.resume(); diff --git a/test/js/node/test/parallel/test-zlib-destroy-pipe.js b/test/js/node/test/parallel/test-zlib-destroy-pipe.js new file mode 100644 index 0000000000..67821a21b6 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-destroy-pipe.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const zlib = require('zlib'); +const { Writable } = require('stream'); + +// Verify that the zlib transform does not error in case +// it is destroyed with data still in flight + +const ts = zlib.createGzip(); + +const ws = new Writable({ + write: common.mustCall((chunk, enc, cb) => { + setImmediate(cb); + ts.destroy(); + }) +}); + +const buf = Buffer.allocUnsafe(1024 * 1024 * 20); +ts.end(buf); +ts.pipe(ws); diff --git a/test/js/node/test/parallel/test-zlib-destroy.js b/test/js/node/test/parallel/test-zlib-destroy.js new file mode 100644 index 0000000000..775b7020b4 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-destroy.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const zlib = require('zlib'); + +// Verify that the zlib transform does clean up +// the handle when calling destroy. + +{ + const ts = zlib.createGzip(); + ts.destroy(); + assert.strictEqual(ts._handle, null); + + ts.on('close', common.mustCall(() => { + ts.close(common.mustCall()); + })); +} + +{ + // Ensure 'error' is only emitted once. + const decompress = zlib.createGunzip(15); + + decompress.on('error', common.mustCall((err) => { + decompress.close(); + })); + + decompress.write('something invalid'); + decompress.destroy(new Error('asd')); +} diff --git a/test/js/node/test/parallel/test-zlib-dictionary-fail.js b/test/js/node/test/parallel/test-zlib-dictionary-fail.js new file mode 100644 index 0000000000..9546954841 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-dictionary-fail.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +// String "test" encoded with dictionary "dict". +const input = Buffer.from([0x78, 0xBB, 0x04, 0x09, 0x01, 0xA5]); + +{ + const stream = zlib.createInflate(); + + stream.on('error', common.mustCall(function(err) { + assert.match(err.message, /Missing dictionary/); + })); + + stream.write(input); +} + +{ + const stream = zlib.createInflate({ dictionary: Buffer.from('fail') }); + + stream.on('error', common.mustCall(function(err) { + assert.match(err.message, /Bad dictionary/); + })); + + stream.write(input); +} + +{ + const stream = zlib.createInflateRaw({ dictionary: Buffer.from('fail') }); + + stream.on('error', common.mustCall(function(err) { + // It's not possible to separate invalid dict and invalid data when using + // the raw format + assert.match(err.message, /(invalid|Operation-Ending-Supplemental Code is 0x12)/); + })); + + stream.write(input); +} diff --git a/test/js/node/test/parallel/test-zlib-dictionary.js b/test/js/node/test/parallel/test-zlib-dictionary.js new file mode 100644 index 0000000000..47eaaa62d0 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-dictionary.js @@ -0,0 +1,175 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Test compression/decompression with dictionary + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const spdyDict = Buffer.from([ + 'optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-', + 'languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi', + 'f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser', + '-agent10010120020120220320420520630030130230330430530630740040140240340440', + '5406407408409410411412413414415416417500501502503504505accept-rangesageeta', + 'glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic', + 'ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran', + 'sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati', + 'oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo', + 'ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe', + 'pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic', + 'ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1', + '.1statusversionurl\0', +].join('')); + +const input = [ + 'HTTP/1.1 200 Ok', + 'Server: node.js', + 'Content-Length: 0', + '', +].join('\r\n'); + +function basicDictionaryTest(spdyDict) { + let output = ''; + const deflate = zlib.createDeflate({ dictionary: spdyDict }); + const inflate = zlib.createInflate({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.end(); +} + +function deflateResetDictionaryTest(spdyDict) { + let doneReset = false; + let output = ''; + const deflate = zlib.createDeflate({ dictionary: spdyDict }); + const inflate = zlib.createInflate({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + if (doneReset) + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.flush(function() { + deflate.reset(); + doneReset = true; + deflate.write(input); + deflate.end(); + }); +} + +function rawDictionaryTest(spdyDict) { + let output = ''; + const deflate = zlib.createDeflateRaw({ dictionary: spdyDict }); + const inflate = zlib.createInflateRaw({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.end(); +} + +function deflateRawResetDictionaryTest(spdyDict) { + let doneReset = false; + let output = ''; + const deflate = zlib.createDeflateRaw({ dictionary: spdyDict }); + const inflate = zlib.createInflateRaw({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + if (doneReset) + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.flush(function() { + deflate.reset(); + doneReset = true; + deflate.write(input); + deflate.end(); + }); +} + +for (const dict of [spdyDict, ...common.getBufferSources(spdyDict)]) { + basicDictionaryTest(dict); + deflateResetDictionaryTest(dict); + rawDictionaryTest(dict); + deflateRawResetDictionaryTest(dict); +} \ No newline at end of file diff --git a/test/js/node/test/parallel/test-zlib-empty-buffer.js b/test/js/node/test/parallel/test-zlib-empty-buffer.js new file mode 100644 index 0000000000..27fd1340fd --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-empty-buffer.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const zlib = require('zlib'); +const { inspect, promisify } = require('util'); +const assert = require('assert'); +const emptyBuffer = Buffer.alloc(0); + +(async function() { + for (const [ compress, decompress, method ] of [ + [ zlib.deflateRawSync, zlib.inflateRawSync, 'raw sync' ], + [ zlib.deflateSync, zlib.inflateSync, 'deflate sync' ], + [ zlib.gzipSync, zlib.gunzipSync, 'gzip sync' ], + [ zlib.brotliCompressSync, zlib.brotliDecompressSync, 'br sync' ], + [ promisify(zlib.deflateRaw), promisify(zlib.inflateRaw), 'raw' ], + [ promisify(zlib.deflate), promisify(zlib.inflate), 'deflate' ], + [ promisify(zlib.gzip), promisify(zlib.gunzip), 'gzip' ], + [ promisify(zlib.brotliCompress), promisify(zlib.brotliDecompress), 'br' ], + ]) { + const compressed = await compress(emptyBuffer); + const decompressed = await decompress(compressed); + assert.deepStrictEqual( + emptyBuffer, decompressed, + `Expected ${inspect(compressed)} to match ${inspect(decompressed)} ` + + `to match for ${method}`); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-zlib-failed-init.js b/test/js/node/test/parallel/test-zlib-failed-init.js new file mode 100644 index 0000000000..95f401a371 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-failed-init.js @@ -0,0 +1,46 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const zlib = require('zlib'); + +assert.throws( + () => zlib.createGzip({ chunkSize: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.chunkSize" is out of range. It must ' + + 'be >= 64. Received 0' + } +); + +assert.throws( + () => zlib.createGzip({ windowBits: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be >= 9 and <= 15. Received 0' + } +); + +assert.throws( + () => zlib.createGzip({ memLevel: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be >= 1 and <= 9. Received 0' + } +); + +{ + const stream = zlib.createGzip({ level: NaN }); + assert.strictEqual(stream._level, zlib.constants.Z_DEFAULT_COMPRESSION); +} + +{ + const stream = zlib.createGzip({ strategy: NaN }); + assert.strictEqual(stream._strategy, zlib.constants.Z_DEFAULT_STRATEGY); +} diff --git a/test/js/node/test/parallel/test-zlib-flush-drain-longblock.js b/test/js/node/test/parallel/test-zlib-flush-drain-longblock.js new file mode 100644 index 0000000000..e2f56ec762 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-flush-drain-longblock.js @@ -0,0 +1,27 @@ +'use strict'; + +// Regression test for https://github.com/nodejs/node/issues/14523. +// Checks that flushes interact properly with writableState.needDrain, +// even if no flush callback was passed. + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const zipper = zlib.createGzip({ highWaterMark: 16384 }); +const unzipper = zlib.createGunzip(); +zipper.pipe(unzipper); + +zipper.write('A'.repeat(17000)); +zipper.flush(); + +let received = 0; +unzipper.on('data', common.mustCallAtLeast((d) => { + received += d.length; +}, 2)); + +// Properly `.end()`ing the streams would interfere with checking that +// `.flush()` works. +process.on('exit', () => { + assert.strictEqual(received, 17000); +}); diff --git a/test/js/node/test/parallel/test-zlib-flush-drain.js b/test/js/node/test/parallel/test-zlib-flush-drain.js new file mode 100644 index 0000000000..6993d2c9fe --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-flush-drain.js @@ -0,0 +1,51 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const bigData = Buffer.alloc(10240, 'x'); + +const opts = { + level: 0, + highWaterMark: 16 +}; + +const deflater = zlib.createDeflate(opts); + +// Shim deflater.flush so we can count times executed +let flushCount = 0; +let drainCount = 0; + +const flush = deflater.flush; +deflater.flush = function(kind, callback) { + flushCount++; + flush.call(this, kind, callback); +}; + +deflater.write(bigData); + +const ws = deflater._writableState; +const beforeFlush = ws.needDrain; +let afterFlush = ws.needDrain; + +deflater.on('data', () => { +}); + +deflater.flush(function(err) { + afterFlush = ws.needDrain; +}); + +deflater.on('drain', function() { + drainCount++; +}); + +process.once('exit', function() { + assert.strictEqual( + beforeFlush, true); + assert.strictEqual( + afterFlush, false); + assert.strictEqual( + drainCount, 1); + assert.strictEqual( + flushCount, 1); +}); diff --git a/test/js/node/test/parallel/test-zlib-flush-flags.js b/test/js/node/test/parallel/test-zlib-flush-flags.js new file mode 100644 index 0000000000..f3392b7a41 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-flush-flags.js @@ -0,0 +1,48 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +zlib.createGzip({ flush: zlib.constants.Z_SYNC_FLUSH }); + +assert.throws( + () => zlib.createGzip({ flush: 'foobar' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.flush" property must be of type number. ' + + 'Received type string ("foobar")' + } +); + +assert.throws( + () => zlib.createGzip({ flush: 10000 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.flush" is out of range. It must ' + + 'be >= 0 and <= 5. Received 10000' + } +); + +zlib.createGzip({ finishFlush: zlib.constants.Z_SYNC_FLUSH }); + +assert.throws( + () => zlib.createGzip({ finishFlush: 'foobar' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.finishFlush" property must be of type number. ' + + 'Received type string ("foobar")' + } +); + +assert.throws( + () => zlib.createGzip({ finishFlush: 10000 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.finishFlush" is out of range. It must ' + + 'be >= 0 and <= 5. Received 10000' + } +); diff --git a/test/js/node/test/parallel/test-zlib-flush-write-sync-interleaved.js b/test/js/node/test/parallel/test-zlib-flush-write-sync-interleaved.js new file mode 100644 index 0000000000..f8387f4006 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-flush-write-sync-interleaved.js @@ -0,0 +1,57 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { createGzip, createGunzip, Z_PARTIAL_FLUSH } = require('zlib'); + +// Verify that .flush() behaves like .write() in terms of ordering, e.g. in +// a sequence like .write() + .flush() + .write() + .flush() each .flush() call +// only affects the data written before it. +// Refs: https://github.com/nodejs/node/issues/28478 + +const compress = createGzip(); +const decompress = createGunzip(); +decompress.setEncoding('utf8'); + +const events = []; +const compressedChunks = []; + +for (const chunk of ['abc', 'def', 'ghi']) { + compress.write(chunk, common.mustCall(() => events.push({ written: chunk }))); + compress.flush(Z_PARTIAL_FLUSH, common.mustCall(() => { + events.push('flushed'); + const chunk = compress.read(); + if (chunk !== null) + compressedChunks.push(chunk); + })); +} + +compress.end(common.mustCall(() => { + events.push('compress end'); + writeToDecompress(); +})); + +function writeToDecompress() { + // Write the compressed chunks to a decompressor, one by one, in order to + // verify that the flushes actually worked. + const chunk = compressedChunks.shift(); + if (chunk === undefined) return decompress.end(); + decompress.write(chunk, common.mustCall(() => { + events.push({ read: decompress.read() }); + writeToDecompress(); + })); +} + +process.on('exit', () => { + assert.deepStrictEqual(events, [ + { written: 'abc' }, + 'flushed', + { written: 'def' }, + 'flushed', + { written: 'ghi' }, + 'flushed', + 'compress end', + { read: 'abc' }, + { read: 'def' }, + { read: 'ghi' }, + ]); +}); diff --git a/test/js/node/test/parallel/test-zlib-flush.js b/test/js/node/test/parallel/test-zlib-flush.js new file mode 100644 index 0000000000..557775d509 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-flush.js @@ -0,0 +1,36 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fixtures = require('../common/fixtures'); + +const file = fixtures.readSync('person.jpg'); +const chunkSize = 16; +const opts = { level: 0 }; +const deflater = zlib.createDeflate(opts); + +const chunk = file.slice(0, chunkSize); +const expectedNone = Buffer.from([0x78, 0x01]); +const blkhdr = Buffer.from([0x00, 0x10, 0x00, 0xef, 0xff]); +const adler32 = Buffer.from([0x00, 0x00, 0x00, 0xff, 0xff]); +const expectedFull = Buffer.concat([blkhdr, chunk, adler32]); +let actualNone; +let actualFull; + +deflater.write(chunk, function() { + deflater.flush(zlib.constants.Z_NO_FLUSH, function() { + actualNone = deflater.read(); + deflater.flush(function() { + const bufs = []; + let buf; + while ((buf = deflater.read()) !== null) + bufs.push(buf); + actualFull = Buffer.concat(bufs); + }); + }); +}); + +process.once('exit', function() { + assert.deepStrictEqual(actualNone, expectedNone); + assert.deepStrictEqual(actualFull, expectedFull); +}); diff --git a/test/js/node/test/parallel/test-zlib-from-concatenated-gzip.js b/test/js/node/test/parallel/test-zlib-from-concatenated-gzip.js new file mode 100644 index 0000000000..1de36dacf9 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-from-concatenated-gzip.js @@ -0,0 +1,83 @@ +'use strict'; +// Test unzipping a gzip file that contains multiple concatenated "members" + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const abc = 'abc'; +const def = 'def'; + +const abcEncoded = zlib.gzipSync(abc); +const defEncoded = zlib.gzipSync(def); + +const data = Buffer.concat([ + abcEncoded, + defEncoded, +]); + +assert.strictEqual(zlib.gunzipSync(data).toString(), (abc + def)); + +zlib.gunzip(data, common.mustSucceed((result) => { + assert.strictEqual(result.toString(), (abc + def)); +})); + +zlib.unzip(data, common.mustSucceed((result) => { + assert.strictEqual(result.toString(), (abc + def)); +})); + +// Multi-member support does not apply to zlib inflate/deflate. +zlib.unzip(Buffer.concat([ + zlib.deflateSync('abc'), + zlib.deflateSync('def'), +]), common.mustSucceed((result) => { + assert.strictEqual(result.toString(), abc); +})); + +// Files that have the "right" magic bytes for starting a new gzip member +// in the middle of themselves, even if they are part of a single +// regularly compressed member +const pmmFileZlib = fixtures.path('pseudo-multimember-gzip.z'); +const pmmFileGz = fixtures.path('pseudo-multimember-gzip.gz'); + +const pmmExpected = zlib.inflateSync(fs.readFileSync(pmmFileZlib)); +const pmmResultBuffers = []; + +fs.createReadStream(pmmFileGz) + .pipe(zlib.createGunzip()) + .on('error', (err) => { + assert.ifError(err); + }) + .on('data', (data) => pmmResultBuffers.push(data)) + .on('finish', common.mustCall(() => { + // Result should match original random garbage + assert.deepStrictEqual(Buffer.concat(pmmResultBuffers), pmmExpected); + })); + +// Test that the next gzip member can wrap around the input buffer boundary +[0, 1, 2, 3, 4, defEncoded.length].forEach((offset) => { + const resultBuffers = []; + + const unzip = zlib.createGunzip() + .on('error', (err) => { + assert.ifError(err); + }) + .on('data', (data) => resultBuffers.push(data)) + .on('finish', common.mustCall(() => { + assert.strictEqual( + Buffer.concat(resultBuffers).toString(), + 'abcdef', + `result should match original input (offset = ${offset})` + ); + })); + + // First write: write "abc" + the first bytes of "def" + unzip.write(Buffer.concat([ + abcEncoded, defEncoded.slice(0, offset), + ])); + + // Write remaining bytes of "def" + unzip.end(defEncoded.slice(offset)); +}); diff --git a/test/js/node/test/parallel/test-zlib-from-gzip-with-trailing-garbage.js b/test/js/node/test/parallel/test-zlib-from-gzip-with-trailing-garbage.js new file mode 100644 index 0000000000..477a6c544f --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-from-gzip-with-trailing-garbage.js @@ -0,0 +1,66 @@ +'use strict'; +// Test unzipping a gzip file that has trailing garbage + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +// Should ignore trailing null-bytes +let data = Buffer.concat([ + zlib.gzipSync('abc'), + zlib.gzipSync('def'), + Buffer.alloc(10), +]); + +assert.strictEqual(zlib.gunzipSync(data).toString(), 'abcdef'); + +zlib.gunzip(data, common.mustSucceed((result) => { + assert.strictEqual( + result.toString(), + 'abcdef', + `result '${result.toString()}' should match original string` + ); +})); + +// If the trailing garbage happens to look like a gzip header, it should +// throw an error. +data = Buffer.concat([ + zlib.gzipSync('abc'), + zlib.gzipSync('def'), + Buffer.from([0x1f, 0x8b, 0xff, 0xff]), + Buffer.alloc(10), +]); + +assert.throws( + () => zlib.gunzipSync(data), + /^Error: unknown compression method$/ +); + +zlib.gunzip(data, common.mustCall((err, result) => { + common.expectsError({ + code: 'Z_DATA_ERROR', + name: 'Error', + message: 'unknown compression method' + })(err); + assert.strictEqual(result, undefined); +})); + +// In this case the trailing junk is too short to be a gzip segment +// So we ignore it and decompression succeeds. +data = Buffer.concat([ + zlib.gzipSync('abc'), + zlib.gzipSync('def'), + Buffer.from([0x1f, 0x8b, 0xff, 0xff]), +]); + +assert.throws( + () => zlib.gunzipSync(data), + /^Error: unknown compression method$/ +); + +zlib.gunzip(data, common.mustCall((err, result) => { + assert(err instanceof Error); + assert.strictEqual(err.code, 'Z_DATA_ERROR'); + assert.strictEqual(err.message, 'unknown compression method'); + assert.strictEqual(result, undefined); +})); diff --git a/test/js/node/test/parallel/test-zlib-from-gzip.js b/test/js/node/test/parallel/test-zlib-from-gzip.js new file mode 100644 index 0000000000..f8fbe16764 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-from-gzip.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Test unzipping a file that was created with a non-node gzip lib, +// piped in as fast as possible. + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fixtures = require('../common/fixtures'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const gunzip = zlib.createGunzip(); + +const fs = require('fs'); + +const fixture = fixtures.path('person.jpg.gz'); +const unzippedFixture = fixtures.path('person.jpg'); +const outputFile = tmpdir.resolve('person.jpg'); +const expect = fs.readFileSync(unzippedFixture); +const inp = fs.createReadStream(fixture); +const out = fs.createWriteStream(outputFile); + +inp.pipe(gunzip).pipe(out); +out.on('close', common.mustCall(() => { + const actual = fs.readFileSync(outputFile); + assert.strictEqual(actual.length, expect.length); + for (let i = 0, l = actual.length; i < l; i++) { + assert.strictEqual(actual[i], expect[i], `byte[${i}]`); + } +})); diff --git a/test/js/node/test/parallel/test-zlib-from-string.js b/test/js/node/test/parallel/test-zlib-from-string.js new file mode 100644 index 0000000000..92b6f86646 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-from-string.js @@ -0,0 +1,83 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Test compressing and uncompressing a string with zlib + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const inputString = 'ΩΩLorem ipsum dolor sit amet, consectetur adipiscing eli' + + 't. Morbi faucibus, purus at gravida dictum, libero arcu ' + + 'convallis lacus, in commodo libero metus eu nisi. Nullam' + + ' commodo, neque nec porta placerat, nisi est fermentum a' + + 'ugue, vitae gravida tellus sapien sit amet tellus. Aenea' + + 'n non diam orci. Proin quis elit turpis. Suspendisse non' + + ' diam ipsum. Suspendisse nec ullamcorper odio. Vestibulu' + + 'm arcu mi, sodales non suscipit id, ultrices ut massa. S' + + 'ed ac sem sit amet arcu malesuada fermentum. Nunc sed. '; +const expectedBase64Deflate = 'eJxdUUtOQzEMvMoc4OndgT0gJCT2buJWlpI4jePeqZfpmX' + + 'AKLRKbLOzx/HK73q6vOrhCunlF1qIDJhNUeW5I2ozT5OkD' + + 'lKWLJWkncJG5403HQXAkT3Jw29B9uIEmToMukglZ0vS6oc' + + 'iBh4JG8sV4oVLEUCitK2kxq1WzPnChHDzsaGKy491LofoA' + + 'bWh8do43oeuYhB5EPCjcLjzYJo48KrfQBvnJecNFJvHT1+' + + 'RSQsGoC7dn2t/xjhduTA1NWyQIZR0pbHwMDatnD+crPqKS' + + 'qGPHp1vnlsWM/07ubf7bheF7kqSj84Bm0R1fYTfaK8vqqq' + + 'fKBtNMhe3OZh6N95CTvMX5HJJi4xOVzCgUOIMSLH7wmeOH' + + 'aFE4RdpnGavKtrB5xzfO/Ll9'; +const expectedBase64Gzip = 'H4sIAAAAAAAAA11RS05DMQy8yhzg6d2BPSAkJPZu4laWkjiN4' + + '96pl+mZcAotEpss7PH8crverq86uEK6eUXWogMmE1R5bkjajN' + + 'Pk6QOUpYslaSdwkbnjTcdBcCRPcnDb0H24gSZOgy6SCVnS9Lq' + + 'hyIGHgkbyxXihUsRQKK0raTGrVbM+cKEcPOxoYrLj3Uuh+gBt' + + 'aHx2jjeh65iEHkQ8KNwuPNgmjjwqt9AG+cl5w0Um8dPX5FJCw' + + 'agLt2fa3/GOF25MDU1bJAhlHSlsfAwNq2cP5ys+opKoY8enW+' + + 'eWxYz/Tu5t/tuF4XuSpKPzgGbRHV9hN9ory+qqp8oG00yF7c5' + + 'mHo33kJO8xfkckmLjE5XMKBQ4gxIsfvCZ44doUThF2mcZq8q2' + + 'sHnHNzRtagj5AQAA'; + +zlib.deflate(inputString, common.mustCall((err, buffer) => { + zlib.inflate(buffer, common.mustCall((err, inflated) => { + assert.strictEqual(inflated.toString(), inputString); + })); +})); + +zlib.gzip(inputString, common.mustCall((err, buffer) => { + // Can't actually guarantee that we'll get exactly the same + // deflated bytes when we compress a string, since the header + // depends on stuff other than the input string itself. + // However, decrypting it should definitely yield the same + // result that we're expecting, and this should match what we get + // from inflating the known valid deflate data. + zlib.gunzip(buffer, common.mustCall((err, gunzipped) => { + assert.strictEqual(gunzipped.toString(), inputString); + })); +})); + +let buffer = Buffer.from(expectedBase64Deflate, 'base64'); +zlib.unzip(buffer, common.mustCall((err, buffer) => { + assert.strictEqual(buffer.toString(), inputString); +})); + +buffer = Buffer.from(expectedBase64Gzip, 'base64'); +zlib.unzip(buffer, common.mustCall((err, buffer) => { + assert.strictEqual(buffer.toString(), inputString); +})); diff --git a/test/js/node/test/parallel/test-zlib-invalid-arg-value-brotli-compress.js b/test/js/node/test/parallel/test-zlib-invalid-arg-value-brotli-compress.js new file mode 100644 index 0000000000..688acddd16 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-invalid-arg-value-brotli-compress.js @@ -0,0 +1,20 @@ +'use strict'; + +require('../common'); + +// This test ensures that the BrotliCompress function throws +// ERR_INVALID_ARG_TYPE when the values of the `params` key-value object are +// neither numbers nor booleans. + +const assert = require('assert'); +const { BrotliCompress, constants } = require('zlib'); + +const opts = { + params: { + [constants.BROTLI_PARAM_MODE]: 'lol' + } +}; + +assert.throws(() => BrotliCompress(opts), { + code: 'ERR_INVALID_ARG_TYPE' +}); diff --git a/test/js/node/test/parallel/test-zlib-invalid-input-memory.js b/test/js/node/test/parallel/test-zlib-invalid-input-memory.js new file mode 100644 index 0000000000..c4dbe4c081 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-invalid-input-memory.js @@ -0,0 +1,28 @@ +// Flags: --expose-gc +'use strict'; +const common = require('../common'); +const { onGC } = require('../common/gc'); +const assert = require('assert'); +const zlib = require('zlib'); + +// Checks that, if a zlib context fails with an error, it can still be GC'ed: +// Refs: https://github.com/nodejs/node/issues/22705 + +const ongc = common.mustCall(); + +{ + const input = Buffer.from('foobar'); + const strm = zlib.createInflate(); + strm.end(input); + strm.once('error', common.mustCall((err) => { + assert(err); + setImmediate(() => { + global.gc(); + // Keep the event loop alive for seeing the async_hooks destroy hook + // we use for GC tracking... + // TODO(addaleax): This should maybe not be necessary? + setImmediate(() => {}); + }); + })); + onGC(strm, { ongc }); +} \ No newline at end of file diff --git a/test/js/node/test/parallel/test-zlib-invalid-input.js b/test/js/node/test/parallel/test-zlib-invalid-input.js new file mode 100644 index 0000000000..7aa44dfe70 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-invalid-input.js @@ -0,0 +1,60 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Test uncompressing invalid input + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const nonStringInputs = [ + 1, + true, + { a: 1 }, + ['a'], +]; + +// zlib.Unzip classes need to get valid data, or else they'll throw. +const unzips = [ + zlib.Unzip(), + zlib.Gunzip(), + zlib.Inflate(), + zlib.InflateRaw(), + zlib.BrotliDecompress(), +]; + +nonStringInputs.forEach(common.mustCall((input) => { + assert.throws(() => { + zlib.gunzip(input); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); +}, nonStringInputs.length)); + +unzips.forEach(common.mustCall((uz, i) => { + uz.on('error', common.mustCall()); + uz.on('end', common.mustNotCall()); + + // This will trigger error event + uz.write('this is not valid compressed data.'); +}, unzips.length)); diff --git a/test/js/node/test/parallel/test-zlib-kmaxlength-rangeerror.js b/test/js/node/test/parallel/test-zlib-kmaxlength-rangeerror.js new file mode 100644 index 0000000000..9803630214 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-kmaxlength-rangeerror.js @@ -0,0 +1,28 @@ +'use strict'; +require('../common'); + +// This test ensures that zlib throws a RangeError if the final buffer needs to +// be larger than kMaxLength and concatenation fails. +// https://github.com/nodejs/node/pull/1811 + +const assert = require('assert'); + +// Change kMaxLength for zlib to trigger the error without having to allocate +// large Buffers. +const buffer = require('buffer'); +const oldkMaxLength = buffer.kMaxLength; +buffer.kMaxLength = 64; +const zlib = require('zlib'); +buffer.kMaxLength = oldkMaxLength; + +const encoded = Buffer.from('H4sIAAAAAAAAA0tMHFgAAIw2K/GAAAAA', 'base64'); + +// Async +zlib.gunzip(encoded, function(err) { + assert.ok(err instanceof RangeError); +}); + +// Sync +assert.throws(function() { + zlib.gunzipSync(encoded); +}, RangeError); diff --git a/test/js/node/test/parallel/test-zlib-maxOutputLength.js b/test/js/node/test/parallel/test-zlib-maxOutputLength.js new file mode 100644 index 0000000000..9af0b3736f --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-maxOutputLength.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const encoded = Buffer.from('G38A+CXCIrFAIAM=', 'base64'); + +// Async +zlib.brotliDecompress(encoded, { maxOutputLength: 64 }, common.expectsError({ + code: 'ERR_BUFFER_TOO_LARGE', + message: 'Cannot create a Buffer larger than 64 bytes' +})); + +// Sync +assert.throws(function() { + zlib.brotliDecompressSync(encoded, { maxOutputLength: 64 }); +}, RangeError); + +// Async +zlib.brotliDecompress(encoded, { maxOutputLength: 256 }, function(err) { + assert.strictEqual(err, null); +}); + +// Sync +zlib.brotliDecompressSync(encoded, { maxOutputLength: 256 }); diff --git a/test/js/node/test/parallel/test-zlib-no-stream.js b/test/js/node/test/parallel/test-zlib-no-stream.js new file mode 100644 index 0000000000..68da269ab8 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-no-stream.js @@ -0,0 +1,14 @@ +/* eslint-disable node-core/required-modules */ +/* eslint-disable node-core/require-common-first */ + +'use strict'; + +// We are not loading common because it will load the stream module, +// defeating the purpose of this test. + +const { gzipSync } = require('zlib'); + +// Avoid regressions such as https://github.com/nodejs/node/issues/36615 + +// This must not throw +gzipSync('fooobar'); diff --git a/test/js/node/test/parallel/test-zlib-not-string-or-buffer.js b/test/js/node/test/parallel/test-zlib-not-string-or-buffer.js new file mode 100644 index 0000000000..35e969737f --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-not-string-or-buffer.js @@ -0,0 +1,30 @@ +'use strict'; + +// Check the error condition testing for passing something other than a string +// or buffer. + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +[ + undefined, + null, + true, + false, + 0, + 1, + [1, 2, 3], + { foo: 'bar' }, +].forEach((input) => { + assert.throws( + () => zlib.deflateSync(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "buffer" argument must be of type string, ' + + 'Buffer, TypedArray, DataView, or ArrayBuffer.' + + common.invalidArgTypeHelper(input) + } + ); +}); diff --git a/test/js/node/test/parallel/test-zlib-object-write.js b/test/js/node/test/parallel/test-zlib-object-write.js new file mode 100644 index 0000000000..2be5edab89 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-object-write.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Gunzip } = require('zlib'); + +const gunzip = new Gunzip({ objectMode: true }); +gunzip.on('error', common.mustNotCall()); +assert.throws(() => { + gunzip.write({}); +}, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' +}); diff --git a/test/js/node/test/parallel/test-zlib-params.js b/test/js/node/test/parallel/test-zlib-params.js new file mode 100644 index 0000000000..18271fe022 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-params.js @@ -0,0 +1,40 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fixtures = require('../common/fixtures'); + +const file = fixtures.readSync('person.jpg'); +const chunkSize = 12 * 1024; +const opts = { level: 9, strategy: zlib.constants.Z_DEFAULT_STRATEGY }; +const deflater = zlib.createDeflate(opts); + +const chunk1 = file.slice(0, chunkSize); +const chunk2 = file.slice(chunkSize); +const blkhdr = Buffer.from([0x00, 0x5a, 0x82, 0xa5, 0x7d]); +const blkftr = Buffer.from('010000ffff7dac3072', 'hex'); +const expected = Buffer.concat([blkhdr, chunk2, blkftr]); +const bufs = []; + +function read() { + let buf; + while ((buf = deflater.read()) !== null) { + bufs.push(buf); + } +} + +deflater.write(chunk1, function() { + deflater.params(0, zlib.constants.Z_DEFAULT_STRATEGY, function() { + while (deflater.read()); + + deflater.on('readable', read); + + deflater.end(chunk2); + }); + while (deflater.read()); +}); + +process.once('exit', function() { + const actual = Buffer.concat(bufs); + assert.deepStrictEqual(actual, expected); +}); diff --git a/test/js/node/test/parallel/test-zlib-premature-end.js b/test/js/node/test/parallel/test-zlib-premature-end.js new file mode 100644 index 0000000000..17446c907d --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-premature-end.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +const zlib = require('zlib'); +const assert = require('assert'); + +const input = '0123456789'.repeat(4); + +for (const [ compress, decompressor ] of [ + [ zlib.deflateRawSync, zlib.createInflateRaw ], + [ zlib.deflateSync, zlib.createInflate ], + [ zlib.brotliCompressSync, zlib.createBrotliDecompress ], +]) { + const compressed = compress(input); + const trailingData = Buffer.from('not valid compressed data'); + + for (const variant of [ + (stream) => { stream.end(compressed); }, + (stream) => { stream.write(compressed); stream.write(trailingData); }, + (stream) => { stream.write(compressed); stream.end(trailingData); }, + (stream) => { stream.write(Buffer.concat([compressed, trailingData])); }, + (stream) => { stream.end(Buffer.concat([compressed, trailingData])); }, + ]) { + let output = ''; + const stream = decompressor(); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => output += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(output, input); + assert.strictEqual(stream.bytesWritten, compressed.length); + })); + variant(stream); + } +} diff --git a/test/js/node/test/parallel/test-zlib-random-byte-pipes.js b/test/js/node/test/parallel/test-zlib-random-byte-pipes.js new file mode 100644 index 0000000000..d8d039a6d6 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-random-byte-pipes.js @@ -0,0 +1,158 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const stream = require('stream'); +const zlib = require('zlib'); + +const Stream = stream.Stream; + +// Emit random bytes, and keep a shasum +class RandomReadStream extends Stream { + constructor(opt) { + super(); + + this.readable = true; + this._paused = false; + this._processing = false; + + this._hasher = crypto.createHash('sha1'); + opt = opt || {}; + + // base block size. + opt.block = opt.block || 256 * 1024; + + // Total number of bytes to emit + opt.total = opt.total || 256 * 1024 * 1024; + this._remaining = opt.total; + + // How variable to make the block sizes + opt.jitter = opt.jitter || 1024; + + this._opt = opt; + + this._process = this._process.bind(this); + + process.nextTick(this._process); + } + + pause() { + this._paused = true; + this.emit('pause'); + } + + resume() { + // console.error("rrs resume"); + this._paused = false; + this.emit('resume'); + this._process(); + } + + _process() { + if (this._processing) return; + if (this._paused) return; + + this._processing = true; + + if (!this._remaining) { + this._hash = this._hasher.digest('hex').toLowerCase().trim(); + this._processing = false; + + this.emit('end'); + return; + } + + // Figure out how many bytes to output + // if finished, then just emit end. + let block = this._opt.block; + const jitter = this._opt.jitter; + if (jitter) { + block += Math.ceil(Math.random() * jitter - (jitter / 2)); + } + block = Math.min(block, this._remaining); + const buf = Buffer.allocUnsafe(block); + for (let i = 0; i < block; i++) { + buf[i] = Math.random() * 256; + } + + this._hasher.update(buf); + + this._remaining -= block; + + this._processing = false; + + this.emit('data', buf); + process.nextTick(this._process); + } +} + +// A filter that just verifies a shasum +class HashStream extends Stream { + constructor() { + super(); + this.readable = this.writable = true; + this._hasher = crypto.createHash('sha1'); + } + + write(c) { + // Simulate the way that an fs.ReadStream returns false + // on *every* write, only to resume a moment later. + this._hasher.update(c); + process.nextTick(() => this.resume()); + return false; + } + + resume() { + this.emit('resume'); + process.nextTick(() => this.emit('drain')); + } + + end(c) { + if (c) { + this.write(c); + } + this._hash = this._hasher.digest('hex').toLowerCase().trim(); + this.emit('data', this._hash); + this.emit('end'); + } +} + +for (const [ createCompress, createDecompress ] of [ + [ zlib.createGzip, zlib.createGunzip ], + [ zlib.createBrotliCompress, zlib.createBrotliDecompress ], +]) { + const inp = new RandomReadStream({ total: 1024, block: 256, jitter: 16 }); + const out = new HashStream(); + const gzip = createCompress(); + const gunz = createDecompress(); + + inp.pipe(gzip).pipe(gunz).pipe(out); + + out.on('data', common.mustCall((c) => { + assert.strictEqual(c, inp._hash, `Hash '${c}' equals '${inp._hash}'.`); + })); +} diff --git a/test/js/node/test/parallel/test-zlib-reset-before-write.js b/test/js/node/test/parallel/test-zlib-reset-before-write.js new file mode 100644 index 0000000000..afa207f12c --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-reset-before-write.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +// Tests that zlib streams support .reset() and .params() +// before the first write. That is important to ensure that +// lazy init of zlib native library handles these cases. + +for (const fn of [ + (z, cb) => { + z.reset(); + cb(); + }, + (z, cb) => z.params(0, zlib.constants.Z_DEFAULT_STRATEGY, cb), +]) { + const deflate = zlib.createDeflate(); + const inflate = zlib.createInflate(); + + deflate.pipe(inflate); + + const output = []; + inflate + .on('error', (err) => { + assert.ifError(err); + }) + .on('data', (chunk) => output.push(chunk)) + .on('end', common.mustCall( + () => assert.strictEqual(Buffer.concat(output).toString(), 'abc'))); + + fn(deflate, () => { + fn(inflate, () => { + deflate.write('abc'); + deflate.end(); + }); + }); +} diff --git a/test/js/node/test/parallel/test-zlib-sync-no-event.js b/test/js/node/test/parallel/test-zlib-sync-no-event.js new file mode 100644 index 0000000000..e7f25c8476 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-sync-no-event.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const zlib = require('zlib'); +const assert = require('assert'); + +const message = 'Come on, Fhqwhgads.'; +const buffer = Buffer.from(message); + +const zipper = new zlib.Gzip(); +zipper.on('close', common.mustNotCall()); + +const zipped = zipper._processChunk(buffer, zlib.constants.Z_FINISH); + +const unzipper = new zlib.Gunzip(); +unzipper.on('close', common.mustNotCall()); + +const unzipped = unzipper._processChunk(zipped, zlib.constants.Z_FINISH); +assert.notStrictEqual(zipped.toString(), message); +assert.strictEqual(unzipped.toString(), message); diff --git a/test/js/node/test/parallel/test-zlib-truncated.js b/test/js/node/test/parallel/test-zlib-truncated.js new file mode 100644 index 0000000000..94bc0e21cb --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-truncated.js @@ -0,0 +1,64 @@ +'use strict'; +// Tests zlib streams with truncated compressed input + +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const inputString = 'ΩΩLorem ipsum dolor sit amet, consectetur adipiscing eli' + + 't. Morbi faucibus, purus at gravida dictum, libero arcu ' + + 'convallis lacus, in commodo libero metus eu nisi. Nullam' + + ' commodo, neque nec porta placerat, nisi est fermentum a' + + 'ugue, vitae gravida tellus sapien sit amet tellus. Aenea' + + 'n non diam orci. Proin quis elit turpis. Suspendisse non' + + ' diam ipsum. Suspendisse nec ullamcorper odio. Vestibulu' + + 'm arcu mi, sodales non suscipit id, ultrices ut massa. S' + + 'ed ac sem sit amet arcu malesuada fermentum. Nunc sed. '; + +const errMessage = /unexpected end of file/; + +[ + { comp: 'gzip', decomp: 'gunzip', decompSync: 'gunzipSync' }, + { comp: 'gzip', decomp: 'unzip', decompSync: 'unzipSync' }, + { comp: 'deflate', decomp: 'inflate', decompSync: 'inflateSync' }, + { comp: 'deflateRaw', decomp: 'inflateRaw', decompSync: 'inflateRawSync' }, +].forEach(function(methods) { + zlib[methods.comp](inputString, function(err, compressed) { + assert.ifError(err); + const truncated = compressed.slice(0, compressed.length / 2); + const toUTF8 = (buffer) => buffer.toString('utf-8'); + + // sync sanity + const decompressed = zlib[methods.decompSync](compressed); + assert.strictEqual(toUTF8(decompressed), inputString); + + // async sanity + zlib[methods.decomp](compressed, function(err, result) { + assert.ifError(err); + assert.strictEqual(toUTF8(result), inputString); + }); + + // Sync truncated input test + assert.throws(function() { + zlib[methods.decompSync](truncated); + }, errMessage); + + // Async truncated input test + zlib[methods.decomp](truncated, function(err, result) { + assert.match(err.message, errMessage); + }); + + const syncFlushOpt = { finishFlush: zlib.constants.Z_SYNC_FLUSH }; + + // Sync truncated input test, finishFlush = Z_SYNC_FLUSH + const result = toUTF8(zlib[methods.decompSync](truncated, syncFlushOpt)); + assert.strictEqual(result, inputString.slice(0, result.length)); + + // Async truncated input test, finishFlush = Z_SYNC_FLUSH + zlib[methods.decomp](truncated, syncFlushOpt, function(err, decompressed) { + assert.ifError(err); + const result = toUTF8(decompressed); + assert.strictEqual(result, inputString.slice(0, result.length)); + }); + }); +}); diff --git a/test/js/node/test/parallel/test-zlib-unzip-one-byte-chunks.js b/test/js/node/test/parallel/test-zlib-unzip-one-byte-chunks.js new file mode 100644 index 0000000000..51af5153a4 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-unzip-one-byte-chunks.js @@ -0,0 +1,30 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const data = Buffer.concat([ + zlib.gzipSync('abc'), + zlib.gzipSync('def'), +]); + +const resultBuffers = []; + +const unzip = zlib.createUnzip() + .on('error', (err) => { + assert.ifError(err); + }) + .on('data', (data) => resultBuffers.push(data)) + .on('finish', common.mustCall(() => { + const unzipped = Buffer.concat(resultBuffers).toString(); + assert.strictEqual(unzipped, 'abcdef', + `'${unzipped}' should match 'abcdef' after zipping ` + + 'and unzipping'); + })); + +for (let i = 0; i < data.length; i++) { + // Write each single byte individually. + unzip.write(Buffer.from([data[i]])); +} + +unzip.end(); diff --git a/test/js/node/test/parallel/test-zlib-write-after-close.js b/test/js/node/test/parallel/test-zlib-write-after-close.js new file mode 100644 index 0000000000..eb8ff43539 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-write-after-close.js @@ -0,0 +1,34 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const zlib = require('zlib'); + +zlib.gzip('hello', common.mustCall(function(err, out) { + const unzip = zlib.createGunzip(); + unzip.close(common.mustCall()); + unzip.write('asd', common.expectsError({ + code: 'ERR_STREAM_DESTROYED', + name: 'Error', + message: 'Cannot call write after a stream was destroyed' + })); +})); diff --git a/test/js/node/test/parallel/test-zlib-write-after-end.js b/test/js/node/test/parallel/test-zlib-write-after-end.js new file mode 100644 index 0000000000..2b31ff30dc --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-write-after-end.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const zlib = require('zlib'); + +// Regression test for https://github.com/nodejs/node/issues/30976 +// Writes to a stream should finish even after the readable side has been ended. + +const data = zlib.deflateRawSync('Welcome'); + +const inflate = zlib.createInflateRaw(); + +inflate.resume(); +inflate.write(data, common.mustCall()); +inflate.write(Buffer.from([0x00]), common.mustCall()); +inflate.write(Buffer.from([0x00]), common.mustCall()); +inflate.flush(common.mustCall()); diff --git a/test/js/node/test/parallel/test-zlib-write-after-flush.js b/test/js/node/test/parallel/test-zlib-write-after-flush.js new file mode 100644 index 0000000000..6edcae2e2f --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-write-after-flush.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +for (const [ createCompress, createDecompress ] of [ + [ zlib.createGzip, zlib.createGunzip ], + [ zlib.createBrotliCompress, zlib.createBrotliDecompress ], +]) { + const gzip = createCompress(); + const gunz = createDecompress(); + + gzip.pipe(gunz); + + let output = ''; + const input = 'A line of data\n'; + gunz.setEncoding('utf8'); + gunz.on('data', (c) => output += c); + gunz.on('end', common.mustCall(() => { + assert.strictEqual(output, input); + })); + + // Make sure that flush/write doesn't trigger an assert failure + gzip.flush(); + gzip.write(input); + gzip.end(); + gunz.read(0); +} diff --git a/test/js/node/test/parallel/test-zlib-zero-byte.js b/test/js/node/test/parallel/test-zlib-zero-byte.js new file mode 100644 index 0000000000..fc57960f1e --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-zero-byte.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +for (const Compressor of [ zlib.Gzip, zlib.BrotliCompress ]) { + const gz = Compressor(); + const emptyBuffer = Buffer.alloc(0); + let received = 0; + gz.on('data', function(c) { + received += c.length; + }); + + gz.on('end', common.mustCall(function() { + const expected = Compressor === zlib.Gzip ? 20 : 1; + assert.strictEqual(received, expected, + `${received}, ${expected}, ${Compressor.name}`); + })); + gz.on('finish', common.mustCall()); + gz.write(emptyBuffer); + gz.end(); +} diff --git a/test/js/node/test/parallel/test-zlib-zero-windowBits.js b/test/js/node/test/parallel/test-zlib-zero-windowBits.js new file mode 100644 index 0000000000..a27fd6734a --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-zero-windowBits.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + + +// windowBits is a special case in zlib. On the compression side, 0 is invalid. +// On the decompression side, it indicates that zlib should use the value from +// the header of the compressed stream. +{ + const inflate = zlib.createInflate({ windowBits: 0 }); + assert(inflate instanceof zlib.Inflate); +} + +{ + const gunzip = zlib.createGunzip({ windowBits: 0 }); + assert(gunzip instanceof zlib.Gunzip); +} + +{ + const unzip = zlib.createUnzip({ windowBits: 0 }); + assert(unzip instanceof zlib.Unzip); +} + +{ + assert.throws(() => zlib.createGzip({ windowBits: 0 }), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. ' + + 'It must be >= 9 and <= 15. Received 0' + }); +} diff --git a/test/js/node/test/parallel/test-zlib.js b/test/js/node/test/parallel/test-zlib.js new file mode 100644 index 0000000000..65050b85a0 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib.js @@ -0,0 +1,232 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const stream = require('stream'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +// Should not segfault. +assert.throws(() => zlib.gzipSync(Buffer.alloc(0), { windowBits: 8 }), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. ' + + 'It must be >= 9 and <= 15. Received 8', +}); + +let zlibPairs = [ + [zlib.Deflate, zlib.Inflate], + [zlib.Gzip, zlib.Gunzip], + [zlib.Deflate, zlib.Unzip], + [zlib.Gzip, zlib.Unzip], + [zlib.DeflateRaw, zlib.InflateRaw], + [zlib.BrotliCompress, zlib.BrotliDecompress], +]; + +// How fast to trickle through the slowstream +let trickle = [128, 1024, 1024 * 1024]; + +// Tunable options for zlib classes. + +// several different chunk sizes +let chunkSize = [128, 1024, 1024 * 16, 1024 * 1024]; + +// This is every possible value. +let level = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +let windowBits = [8, 9, 10, 11, 12, 13, 14, 15]; +let memLevel = [1, 2, 3, 4, 5, 6, 7, 8, 9]; +let strategy = [0, 1, 2, 3, 4]; + +// It's nice in theory to test every combination, but it +// takes WAY too long. Maybe a pummel test could do this? +if (!process.env.PUMMEL) { + trickle = [1024]; + chunkSize = [1024 * 16]; + level = [6]; + memLevel = [8]; + windowBits = [15]; + strategy = [0]; +} + +let testFiles = ['person.jpg', 'elipses.txt', 'empty.txt']; + +if (process.env.FAST) { + zlibPairs = [[zlib.Gzip, zlib.Unzip]]; + testFiles = ['person.jpg']; +} + +const tests = {}; +testFiles.forEach(common.mustCall((file) => { + tests[file] = fixtures.readSync(file); +}, testFiles.length)); + + +// Stream that saves everything +class BufferStream extends stream.Stream { + constructor() { + super(); + this.chunks = []; + this.length = 0; + this.writable = true; + this.readable = true; + } + + write(c) { + this.chunks.push(c); + this.length += c.length; + return true; + } + + end(c) { + if (c) this.write(c); + // flatten + const buf = Buffer.allocUnsafe(this.length); + let i = 0; + this.chunks.forEach((c) => { + c.copy(buf, i); + i += c.length; + }); + this.emit('data', buf); + this.emit('end'); + return true; + } +} + +class SlowStream extends stream.Stream { + constructor(trickle) { + super(); + this.trickle = trickle; + this.offset = 0; + this.readable = this.writable = true; + } + + write() { + throw new Error('not implemented, just call ss.end(chunk)'); + } + + pause() { + this.paused = true; + this.emit('pause'); + } + + resume() { + const emit = () => { + if (this.paused) return; + if (this.offset >= this.length) { + this.ended = true; + return this.emit('end'); + } + const end = Math.min(this.offset + this.trickle, this.length); + const c = this.chunk.slice(this.offset, end); + this.offset += c.length; + this.emit('data', c); + process.nextTick(emit); + }; + + if (this.ended) return; + this.emit('resume'); + if (!this.chunk) return; + this.paused = false; + emit(); + } + + end(chunk) { + // Walk over the chunk in blocks. + this.chunk = chunk; + this.length = chunk.length; + this.resume(); + return this.ended; + } +} + +// windowBits: 8 shouldn't throw +zlib.createDeflateRaw({ windowBits: 8 }); + +{ + const node = fs.createReadStream(fixtures.path('person.jpg')); + const raw = []; + const reinflated = []; + node.on('data', (chunk) => raw.push(chunk)); + + // Usually, the inflate windowBits parameter needs to be at least the + // value of the matching deflate’s windowBits. However, inflate raw with + // windowBits = 8 should be able to handle compressed data from a source + // that does not know about the silent 8-to-9 upgrade of windowBits + // that most versions of zlib/Node perform, and which *still* results in + // a valid 8-bit-window zlib stream. + node.pipe(zlib.createDeflateRaw({ windowBits: 9 })) + .pipe(zlib.createInflateRaw({ windowBits: 8 })) + .on('data', (chunk) => reinflated.push(chunk)) + .on('end', common.mustCall( + () => assert(Buffer.concat(raw).equals(Buffer.concat(reinflated))))) + .on('close', common.mustCall(1)); +} + +// For each of the files, make sure that compressing and +// decompressing results in the same data, for every combination +// of the options set above. + +const testKeys = Object.keys(tests); +testKeys.forEach(common.mustCall((file) => { + const test = tests[file]; + chunkSize.forEach(common.mustCall((chunkSize) => { + trickle.forEach(common.mustCall((trickle) => { + windowBits.forEach(common.mustCall((windowBits) => { + level.forEach(common.mustCall((level) => { + memLevel.forEach(common.mustCall((memLevel) => { + strategy.forEach(common.mustCall((strategy) => { + zlibPairs.forEach(common.mustCall((pair) => { + const Def = pair[0]; + const Inf = pair[1]; + const opts = { level, windowBits, memLevel, strategy }; + + const def = new Def(opts); + const inf = new Inf(opts); + const ss = new SlowStream(trickle); + const buf = new BufferStream(); + + // Verify that the same exact buffer comes out the other end. + buf.on('data', common.mustCall((c) => { + const msg = `${file} ${chunkSize} ${ + JSON.stringify(opts)} ${Def.name} -> ${Inf.name}`; + let i; + for (i = 0; i < Math.max(c.length, test.length); i++) { + if (c[i] !== test[i]) { + assert.fail(msg); + break; + } + } + })); + + // The magic happens here. + ss.pipe(def).pipe(inf).pipe(buf); + ss.end(test); + }, zlibPairs.length)); + }, strategy.length)); + }, memLevel.length)); + }, level.length)); + }, windowBits.length)); + }, trickle.length)); + }, chunkSize.length)); +}, testKeys.length)); diff --git a/test/js/node/test/parallel/timer-immediate.test.js b/test/js/node/test/parallel/timer-immediate.test.js deleted file mode 100644 index 1f74aa8bde..0000000000 --- a/test/js/node/test/parallel/timer-immediate.test.js +++ /dev/null @@ -1,19 +0,0 @@ -//#FILE: test-timer-immediate.js -//#SHA1: 00e4e451b5feda969bd3352a194ba0ee0e5bab85 -//----------------- -"use strict"; - -// Note: We're not using the 'common' module as it's not necessary for this test - -test("setImmediate should be called", done => { - // Recreate the global.process object - global.process = {}; - - // Use setImmediate and expect it to be called - setImmediate(() => { - expect(true).toBe(true); // This assertion is just to ensure the callback is called - done(); - }); -}); - -//<#END_FILE: test-timer-immediate.js diff --git a/test/js/node/test/parallel/timers-args.test.js b/test/js/node/test/parallel/timers-args.test.js deleted file mode 100644 index ca38fd5b37..0000000000 --- a/test/js/node/test/parallel/timers-args.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-timers-args.js -//#SHA1: 27f971d534c9bb3a1c14ae176ee8f34b0cdbed6f -//----------------- -"use strict"; - -function range(n) { - return "x" - .repeat(n + 1) - .split("") - .map(function (_, i) { - return i; - }); -} - -test("setTimeout with increasing number of arguments", done => { - function timeout(nargs) { - const args = range(nargs); - setTimeout(callback, 1, ...args); - - function callback(...receivedArgs) { - expect(receivedArgs).toEqual(args); - if (nargs < 128) { - timeout(nargs + 1); - } else { - done(); - } - } - } - - timeout(0); -}); - -test("setInterval with increasing number of arguments", done => { - function interval(nargs) { - const args = range(nargs); - const timer = setInterval(callback, 1, ...args); - - function callback(...receivedArgs) { - clearInterval(timer); - expect(receivedArgs).toEqual(args); - if (nargs < 128) { - interval(nargs + 1); - } else { - done(); - } - } - } - - interval(0); -}); - -//<#END_FILE: test-timers-args.js diff --git a/test/js/node/test/parallel/timers-clear-timeout-interval-equivalent.test.js b/test/js/node/test/parallel/timers-clear-timeout-interval-equivalent.test.js deleted file mode 100644 index 1fc88ba7ef..0000000000 --- a/test/js/node/test/parallel/timers-clear-timeout-interval-equivalent.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-timers-clear-timeout-interval-equivalent.js -//#SHA1: 2c98894fc8abe7d6e800533325274f39f9a16ff4 -//----------------- -"use strict"; - -// This test makes sure that timers created with setTimeout can be disarmed by -// clearInterval and that timers created with setInterval can be disarmed by -// clearTimeout. -// -// This behavior is documented in the HTML Living Standard: -// -// * Refs: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-setinterval - -test("Disarm interval with clearTimeout", () => { - const mockCallback = jest.fn(); - const interval = setInterval(mockCallback, 1); - clearTimeout(interval); - - return new Promise(resolve => { - setTimeout(() => { - expect(mockCallback).not.toHaveBeenCalled(); - resolve(); - }, 10); - }); -}); - -test("Disarm timeout with clearInterval", () => { - const mockCallback = jest.fn(); - const timeout = setTimeout(mockCallback, 1); - clearInterval(timeout); - - return new Promise(resolve => { - setTimeout(() => { - expect(mockCallback).not.toHaveBeenCalled(); - resolve(); - }, 10); - }); -}); - -//<#END_FILE: test-timers-clear-timeout-interval-equivalent.js diff --git a/test/js/node/test/parallel/timers-clearimmediate.test.js b/test/js/node/test/parallel/timers-clearimmediate.test.js deleted file mode 100644 index d29f715d38..0000000000 --- a/test/js/node/test/parallel/timers-clearimmediate.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-timers-clearImmediate.js -//#SHA1: 819914471d2e9d0a4629df9ef4b96e8e87ae7606 -//----------------- -"use strict"; - -const N = 3; - -function next() { - const fn = jest.fn(); - const immediate = setImmediate(fn); - clearImmediate(immediate); - expect(fn).not.toHaveBeenCalled(); -} - -test("clearImmediate cancels setImmediate", () => { - for (let i = 0; i < N; i++) { - next(); - } -}); - -//<#END_FILE: test-timers-clearImmediate.js diff --git a/test/js/node/test/parallel/timers-immediate-queue.test.js b/test/js/node/test/parallel/timers-immediate-queue.test.js deleted file mode 100644 index 92aefa2084..0000000000 --- a/test/js/node/test/parallel/timers-immediate-queue.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-timers-immediate-queue.js -//#SHA1: 04dace9bcd1a7634224efafb4fdd5a953bfc28f6 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// setImmediate should run clear its queued cbs once per event loop turn -// but immediates queued while processing the current queue should happen -// on the next turn of the event loop. - -// hit should be the exact same size of QUEUE, if we're letting things -// recursively add to the immediate QUEUE hit will be > QUEUE - -test("setImmediate queue processing", done => { - let ticked = false; - let hit = 0; - const QUEUE = 10; - - function run() { - if (hit === 0) { - setTimeout(() => { - ticked = true; - }, 1); - const now = Date.now(); - while (Date.now() - now < 2); - } - - if (ticked) return; - - hit += 1; - setImmediate(run); - } - - for (let i = 0; i < QUEUE; i++) setImmediate(run); - - // Use setImmediate to ensure all other immediates have run - setImmediate(() => { - expect(hit).toBe(QUEUE); - done(); - }); -}); - -//<#END_FILE: test-timers-immediate-queue.js diff --git a/test/js/node/test/parallel/timers-next-tick.test.js b/test/js/node/test/parallel/timers-next-tick.test.js deleted file mode 100644 index eced9d48cc..0000000000 --- a/test/js/node/test/parallel/timers-next-tick.test.js +++ /dev/null @@ -1,50 +0,0 @@ -//#FILE: test-timers-next-tick.js -//#SHA1: c6c6f667dac048dd0aed0a244e484ae1f0127d53 -//----------------- -"use strict"; - -// This test verifies that the next tick queue runs after each -// individual Timeout, as well as each individual Immediate. - -test("next tick queue runs after each Timeout and Immediate", async () => { - const timeoutSpy1 = jest.fn(); - const timeoutSpy2 = jest.fn(); - const immediateSpy1 = jest.fn(); - const immediateSpy2 = jest.fn(); - - setTimeout(timeoutSpy1, 1); - const t2 = setTimeout(jest.fn(), 1); - const t3 = setTimeout(jest.fn(), 1); - setTimeout(timeoutSpy2, 1); - - await new Promise(resolve => setTimeout(resolve, 5)); - - setImmediate(immediateSpy1); - const i2 = setImmediate(jest.fn()); - const i3 = setImmediate(jest.fn()); - setImmediate(immediateSpy2); - - await new Promise(resolve => setImmediate(resolve)); - - expect(timeoutSpy1).toHaveBeenCalledTimes(1); - expect(timeoutSpy2).toHaveBeenCalledTimes(1); - expect(immediateSpy1).toHaveBeenCalledTimes(1); - expect(immediateSpy2).toHaveBeenCalledTimes(1); - - // Confirm that clearing Timeouts from a next tick doesn't explode. - process.nextTick(() => { - clearTimeout(t2); - clearTimeout(t3); - }); - - // Confirm that clearing Immediates from a next tick doesn't explode. - process.nextTick(() => { - clearImmediate(i2); - clearImmediate(i3); - }); - - // Wait for next tick to complete - await new Promise(process.nextTick); -}); - -//<#END_FILE: test-timers-next-tick.js diff --git a/test/js/node/test/parallel/timers-now.test.js b/test/js/node/test/parallel/timers-now.test.js deleted file mode 100644 index 1a0e58d2eb..0000000000 --- a/test/js/node/test/parallel/timers-now.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-timers-now.js -//#SHA1: 6d35fc8681a7698d480f851c3c4ba5bfca7fb0b9 -//----------------- -"use strict"; -// Flags: --expose-internals - -test("getLibuvNow() return value fits in a SMI after start-up", () => { - // We can't use internal bindings in Jest, so we'll need to mock this behavior - // For the purpose of this test, we'll create a mock function that returns a small number - const mockGetLibuvNow = jest.fn(() => 1000); - - // Simulate the binding object - const binding = { - getLibuvNow: mockGetLibuvNow, - }; - - // Call the function and check the result - const result = binding.getLibuvNow(); - - // Check if the result is less than 0x3ffffff (67108863 in decimal) - expect(result).toBeLessThan(0x3ffffff); - - // Ensure the mock function was called - expect(mockGetLibuvNow).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-timers-now.js diff --git a/test/js/node/test/parallel/timers-ordering.test.js b/test/js/node/test/parallel/timers-ordering.test.js deleted file mode 100644 index bd3769d4fc..0000000000 --- a/test/js/node/test/parallel/timers-ordering.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-timers-ordering.js -//#SHA1: b2be662b4e86125b4dcd87ced4316d84acf57912 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const N = 30; - -test("Timer ordering and timing", async () => { - let last_i = 0; - let last_ts = 0; - - function f(i) { - return new Promise(resolve => { - if (i <= N) { - // check order - expect(i).toBe(last_i + 1); - last_i = i; - - // Check that this iteration is fired at least 1ms later than the previous - const now = Date.now(); - expect(now).toBeGreaterThanOrEqual(last_ts + 1); - last_ts = now; - - // Schedule next iteration - setTimeout(() => resolve(f(i + 1)), 1); - } else { - resolve(); - } - }); - } - - await f(1); -}, 10000); // Increased timeout to ensure all iterations complete - -//<#END_FILE: test-timers-ordering.js diff --git a/test/js/node/test/parallel/timers-promises.test.js b/test/js/node/test/parallel/timers-promises.test.js deleted file mode 100644 index 2bba7b563b..0000000000 --- a/test/js/node/test/parallel/timers-promises.test.js +++ /dev/null @@ -1,13 +0,0 @@ -//#FILE: test-timers-promises.js -//#SHA1: 78e3867e4cb1a41f11f7fa930e5a7319149c9452 -//----------------- -"use strict"; - -const timer = require("node:timers"); -const timerPromises = require("node:timers/promises"); - -test("(node:timers/promises) is equal to (node:timers).promises", () => { - expect(timerPromises).toEqual(timer.promises); -}); - -//<#END_FILE: test-timers-promises.js diff --git a/test/js/node/test/parallel/timers-setimmediate-infinite-loop.test.js b/test/js/node/test/parallel/timers-setimmediate-infinite-loop.test.js deleted file mode 100644 index d034693ae2..0000000000 --- a/test/js/node/test/parallel/timers-setimmediate-infinite-loop.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-timers-setimmediate-infinite-loop.js -//#SHA1: cd4dd01a6d06097758004eaa8d36d8712977d49d -//----------------- -"use strict"; - -// This test ensures that if an Immediate callback clears subsequent -// immediates we don't get stuck in an infinite loop. -// -// If the process does get stuck, it will be timed out by the test -// runner. -// -// Ref: https://github.com/nodejs/node/issues/9756 - -test("setImmediate clears subsequent immediates without infinite loop", done => { - const firstCallback = jest.fn(() => { - clearImmediate(i2); - clearImmediate(i3); - }); - - const secondCallback = jest.fn(); - const thirdCallback = jest.fn(); - - setImmediate(firstCallback); - - const i2 = setImmediate(secondCallback); - const i3 = setImmediate(thirdCallback); - - // Use a timeout to ensure all immediates have been processed - setTimeout(() => { - expect(firstCallback).toHaveBeenCalledTimes(1); - expect(secondCallback).not.toHaveBeenCalled(); - expect(thirdCallback).not.toHaveBeenCalled(); - done(); - }, 100); -}); - -//<#END_FILE: test-timers-setimmediate-infinite-loop.js diff --git a/test/js/node/test/parallel/timers-timeout-with-non-integer.test.js b/test/js/node/test/parallel/timers-timeout-with-non-integer.test.js deleted file mode 100644 index 2207d34ec6..0000000000 --- a/test/js/node/test/parallel/timers-timeout-with-non-integer.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-timers-timeout-with-non-integer.js -//#SHA1: 371503ab8a31c6749ef77eabac3054e5a43ab231 -//----------------- -"use strict"; - -/** - * This test is for https://github.com/nodejs/node/issues/24203 - */ -test("setTimeout with non-integer time", done => { - let count = 50; - const time = 1.00000000000001; - - const exec = jest.fn(() => { - if (--count === 0) { - expect(exec).toHaveBeenCalledTimes(50); - done(); - return; - } - setTimeout(exec, time); - }); - - exec(); -}, 10000); // Increased timeout to ensure all iterations complete - -//<#END_FILE: test-timers-timeout-with-non-integer.js diff --git a/test/js/node/test/parallel/timers-to-primitive.test.js b/test/js/node/test/parallel/timers-to-primitive.test.js deleted file mode 100644 index a47b84132c..0000000000 --- a/test/js/node/test/parallel/timers-to-primitive.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-timers-to-primitive.js -//#SHA1: 87671c76fb38458f9d4f68aa747b2d95076f6bb4 -//----------------- -"use strict"; - -test("Timeout and Interval primitives", () => { - const timeouts = [setTimeout(() => {}, 1), setInterval(() => {}, 1)]; - - timeouts.forEach(timeout => { - expect(Number.isNaN(+timeout)).toBe(false); - expect(+timeout).toBe(timeout[Symbol.toPrimitive]()); - expect(`${timeout}`).toBe(timeout[Symbol.toPrimitive]().toString()); - expect(Object.keys({ [timeout]: timeout })).toEqual([`${timeout}`]); - clearTimeout(+timeout); - }); -}); - -test("clearTimeout works with number id", () => { - const timeout = setTimeout(() => {}, 1); - const id = +timeout; - expect(() => clearTimeout(id)).not.toThrow(); -}); - -test("clearTimeout works with string id", () => { - const timeout = setTimeout(() => {}, 1); - const id = `${timeout}`; - expect(() => clearTimeout(id)).not.toThrow(); -}); - -//<#END_FILE: test-timers-to-primitive.js diff --git a/test/js/node/test/parallel/timers-unrefed-in-beforeexit.test.js b/test/js/node/test/parallel/timers-unrefed-in-beforeexit.test.js deleted file mode 100644 index 79967d0376..0000000000 --- a/test/js/node/test/parallel/timers-unrefed-in-beforeexit.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-timers-unrefed-in-beforeexit.js -//#SHA1: c873b545965c8b223c0bc38f2acdd4bc952427ea -//----------------- -"use strict"; - -test("unrefed timer in beforeExit should not prevent exit", () => { - const beforeExitHandler = jest.fn(() => { - setTimeout(jest.fn(), 1).unref(); - }); - - process.on("beforeExit", beforeExitHandler); - - // Simulate process exit - process.emit("beforeExit"); - - expect(beforeExitHandler).toHaveBeenCalledTimes(1); - - // Clean up the event listener - process.removeListener("beforeExit", beforeExitHandler); -}); - -//<#END_FILE: test-timers-unrefed-in-beforeexit.js diff --git a/test/js/node/test/parallel/timers-zero-timeout.test.js b/test/js/node/test/parallel/timers-zero-timeout.test.js deleted file mode 100644 index 3f16eaf6ae..0000000000 --- a/test/js/node/test/parallel/timers-zero-timeout.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-timers-zero-timeout.js -//#SHA1: f6d7cfab9ecf6f2f94001a4e153baf29dd3203b1 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// https://github.com/joyent/node/issues/2079 - zero timeout drops extra args -test("setTimeout with zero timeout and extra args", done => { - const f = jest.fn((a, b, c) => { - expect(a).toBe("foo"); - expect(b).toBe("bar"); - expect(c).toBe("baz"); - done(); - }); - - setTimeout(f, 0, "foo", "bar", "baz"); - setTimeout(() => {}, 0); -}); - -test("setInterval with zero timeout and extra args", done => { - let ncalled = 3; - - const f = jest.fn((a, b, c) => { - expect(a).toBe("foo"); - expect(b).toBe("bar"); - expect(c).toBe("baz"); - if (--ncalled === 0) { - clearTimeout(iv); - expect(f).toHaveBeenCalledTimes(3); - done(); - } - }); - - const iv = setInterval(f, 0, "foo", "bar", "baz"); -}); - -//<#END_FILE: test-timers-zero-timeout.js diff --git a/test/js/node/test/parallel/tls-add-context.test.js b/test/js/node/test/parallel/tls-add-context.test.js deleted file mode 100644 index ca3d03c578..0000000000 --- a/test/js/node/test/parallel/tls-add-context.test.js +++ /dev/null @@ -1,93 +0,0 @@ -//#FILE: test-tls-add-context.js -//#SHA1: 61f134fe8c8fb63a00278b2d70dfecf11efb5df9 -//----------------- -"use strict"; - -const crypto = require("crypto"); -const fixtures = require("../common/fixtures"); -const tls = require("tls"); - -// Skip test if crypto is not available -if (!crypto) { - test.skip("missing crypto", () => {}); -} else { - function loadPEM(n) { - return fixtures.readKey(`${n}.pem`); - } - - const serverOptions = { - key: loadPEM("agent2-key"), - cert: loadPEM("agent2-cert"), - ca: [loadPEM("ca2-cert")], - requestCert: true, - rejectUnauthorized: false, - }; - - let connections = 0; - - test("TLS add context", done => { - const server = tls.createServer(serverOptions, c => { - if (++connections === 3) { - server.close(); - } - if (c.servername === "unknowncontext") { - expect(c.authorized).toBe(false); - return; - } - expect(c.authorized).toBe(true); - }); - - const secureContext = { - key: loadPEM("agent1-key"), - cert: loadPEM("agent1-cert"), - ca: [loadPEM("ca1-cert")], - }; - server.addContext("context1", secureContext); - server.addContext("context2", tls.createSecureContext(secureContext)); - - const clientOptionsBase = { - key: loadPEM("agent1-key"), - cert: loadPEM("agent1-cert"), - ca: [loadPEM("ca1-cert")], - rejectUnauthorized: false, - }; - - server.listen(0, () => { - const client1 = tls.connect( - { - ...clientOptionsBase, - port: server.address().port, - servername: "context1", - }, - () => { - client1.end(); - }, - ); - - const client2 = tls.connect( - { - ...clientOptionsBase, - port: server.address().port, - servername: "context2", - }, - () => { - client2.end(); - }, - ); - - const client3 = tls.connect( - { - ...clientOptionsBase, - port: server.address().port, - servername: "unknowncontext", - }, - () => { - client3.end(); - done(); - }, - ); - }); - }); -} - -//<#END_FILE: test-tls-add-context.js diff --git a/test/js/node/test/parallel/tls-ca-concat.test.js b/test/js/node/test/parallel/tls-ca-concat.test.js deleted file mode 100644 index dcee3b9522..0000000000 --- a/test/js/node/test/parallel/tls-ca-concat.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-tls-ca-concat.js -//#SHA1: 23b19f45d7777ee95a3c8d8ba4a727b149ea7409 -//----------------- -"use strict"; - -const fixtures = require("../common/fixtures"); - -// Check ca option can contain concatenated certs by prepending an unrelated -// non-CA cert and showing that agent6's CA root is still found. - -const { connect, keys } = require(fixtures.path("tls-connect")); - -test("ca option can contain concatenated certs", async () => { - await new Promise((resolve, reject) => { - connect( - { - client: { - checkServerIdentity: (servername, cert) => {}, - ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, - }, - server: { - cert: keys.agent6.cert, - key: keys.agent6.key, - }, - }, - (err, pair, cleanup) => { - if (err) { - cleanup(); - reject(err); - } else { - cleanup(); - resolve(); - } - }, - ); - }); -}); - -//<#END_FILE: test-tls-ca-concat.js diff --git a/test/js/node/test/parallel/tls-cert-ext-encoding.test.js b/test/js/node/test/parallel/tls-cert-ext-encoding.test.js deleted file mode 100644 index ed33875b57..0000000000 --- a/test/js/node/test/parallel/tls-cert-ext-encoding.test.js +++ /dev/null @@ -1,90 +0,0 @@ -//#FILE: test-tls-cert-ext-encoding.js -//#SHA1: 235c886145290bd46f4f40110eace31e4cbcf55f -//----------------- -"use strict"; - -const tls = require("tls"); - -// NOTE: This certificate is hand-generated, hence it is not located in -// `test/fixtures/keys` to avoid confusion. -// -// The key property of this cert is that subjectAltName contains a string with -// a type `23` which cannot be encoded into string by `X509V3_EXT_print`. -const pem = ` ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAzrmfPz5M3wTq2/CwMeSQr/N+R1FCJ+O5n+SMleKvBqaK63eJ -kL4BnySMc+ZLKCt4UQSsPFIBK63QFq8n6/vjuTDMJiBTsvzytw8zJt1Zr2HA71N3 -VIPt6NdJ/w5lgddTYxR7XudJZJ5lk3PkG8ZgrhuenPYP80UJYVzAC2YZ9KYe3r2B -rVbut1j+8h0TwVcx2Zg5PorsC/EVxHwo4dCmIHceodikr3UVqHneRcrDBytdG6Mo -IqHhZJwBeii/EES9tpWwWbzYYh+38aGGLIF2h5UlVpr0bdBVVUg+uVX3y/Qmu2Qv -4CrAO2IPV6JER9Niwl3ktzNjOMAUQG6BCRSqRQIDAQABAoIBAAmB0+cOsG5ZRYvT -5+aDgnv1EMuq2wYGnRTTZ/vErxP5OM5XcwYrFtwAzEzQPIieZywisOEdTFx74+QH -LijWLsTnj5v5RKAorejpVArnhyZfsoXPKt/CKYDZ1ddbDCQKiRU3be0RafisqDM9 -0zHLz8pyDrtdPaKMfD/0Cgj8KxlrLTmfD4otPXds8fZpQe1hR1y12XKVp47l1siW -qFGTaUPDJpQ67xybR08x5DOqmyo4cNMOuReRWrc/qRbWint9U1882eOH09gVfpJZ -Gp6FZVPSgz10MZdLSPLhXqZkY4IxIvNltjBDqkmivd12CD+GVr0qUmTJHzTpk+kG -/CWuRQkCgYEA4EFf8SJHEl0fLDJnOQFyUPY3MalMuopUkQ5CBUe3QXjQhHXsRDfj -Ci/lyzShJkHPbMDHb/rx3lYZB0xNhwnMWKS1gCFVgOCOTZLfD0K1Anxc1hOSgVxI -y5FdO9VW7oQNlsMH/WuDHps0HhJW/00lcrmdyoUM1+fE/3yPQndhUmMCgYEA6/z6 -8Gq4PHHNql+gwunAH2cZKNdmcP4Co8MvXCZwIJsLenUuLIZQ/YBKZoM/y5X/cFAG -WFJJuUe6KFetPaDm6NgZgpOmawyUwd5czDjJ6wWgsRywiTISInfJlgWLBVMOuba7 -iBL9Xuy0hmcbj0ByoRW9l3gCiBX3yJw3I6wqXTcCgYBnjei22eRF15iIeTHvQfq+ -5iNwnEQhM7V/Uj0sYQR/iEGJmUaj7ca6somDf2cW2nblOlQeIpxD1jAyjYqTW/Pv -zwc9BqeMHqW3rqWwT1Z0smbQODOD5tB6qEKMWaSN+Y6o2qC65kWjAXpclI110PME -+i+iEDRxEsaGT8d7otLfDwKBgQCs+xBaQG/x5p2SAGzP0xYALstzc4jk1FzM+5rw -mkBgtiXQyqpg+sfNOkfPIvAVZEsMYax0+0SNKrWbMsGLRjFchmMUovQ+zccQ4NT2 -4b2op8Rlbxk8R9ahK1s5u7Bu47YMjZSjJwBQn4OobVX3SI994njJ2a9JX4j0pQWK -AX5AOwKBgAfOsr8HSHTcxSW4F9gegj+hXsRYbdA+eUkFhEGrYyRJgIlQrk/HbuZC -mKd/bQ5R/vwd1cxgV6A0APzpZtbwdhvP0RWji+WnPPovgGcfK0AHFstHnga67/uu -h2LHnKQZ1qWHn+BXWo5d7hBRwWVaK66g3GDN0blZpSz1kKcpy1Pl ------END RSA PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIICwjCCAaqgAwIBAgIDAQABMA0GCSqGSIb3DQEBDQUAMBUxEzARBgNVBAMWCmxv -Y2FsLmhvc3QwHhcNMTkxMjA1MDQyODMzWhcNNDQxMTI5MDQyODMzWjAVMRMwEQYD -VQQDFgpsb2NhbC5ob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -zrmfPz5M3wTq2/CwMeSQr/N+R1FCJ+O5n+SMleKvBqaK63eJkL4BnySMc+ZLKCt4 -UQSsPFIBK63QFq8n6/vjuTDMJiBTsvzytw8zJt1Zr2HA71N3VIPt6NdJ/w5lgddT -YxR7XudJZJ5lk3PkG8ZgrhuenPYP80UJYVzAC2YZ9KYe3r2BrVbut1j+8h0TwVcx -2Zg5PorsC/EVxHwo4dCmIHceodikr3UVqHneRcrDBytdG6MoIqHhZJwBeii/EES9 -tpWwWbzYYh+38aGGLIF2h5UlVpr0bdBVVUg+uVX3y/Qmu2Qv4CrAO2IPV6JER9Ni -wl3ktzNjOMAUQG6BCRSqRQIDAQABoxswGTAXBgNVHREEEDAOlwwqLmxvY2FsLmhv -c3QwDQYJKoZIhvcNAQENBQADggEBAH5ThRLDLwOGuhKsifyiq7k8gbx1FqRegO7H -SIiIYYB35v5Pk0ZPN8QBJwNQzJEjUMjCpHXNdBxknBXRaA8vkbnryMfJm37gPTwA -m6r0uEG78WgcEAe8bgf9iKtQGP/iydKXpSSpDgKoHbswIxD5qtzT+o6VNnkRTSfK -/OGwakluFSoJ/Q9rLpR8lKjA01BhetXMmHbETiY8LSkxOymMldXSzUTD1WdrVn8U -L3dobxT//R/0GraKXG02mf3gZNlb0MMTvW0pVwVy39YmcPEGh8L0hWh1rpAA/VXC -f79uOowv3lLTzQ9na5EThA0tp8d837hdYrrIHh5cfTqBDxG0Tu8= ------END CERTIFICATE----- -`; - -const options = { - key: pem, - cert: pem, -}; - -test("TLS certificate extension encoding", async () => { - const server = tls.createServer(options, socket => { - socket.end(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - const client = tls.connect( - { - port: server.address().port, - rejectUnauthorized: false, - }, - () => { - // This should not crash process: - client.getPeerCertificate(); - - server.close(); - client.end(); - resolve(); - }, - ); - }); - }); -}); - -//<#END_FILE: test-tls-cert-ext-encoding.js diff --git a/test/js/node/test/parallel/tls-client-abort.test.js b/test/js/node/test/parallel/tls-client-abort.test.js deleted file mode 100644 index 4679c1f0e4..0000000000 --- a/test/js/node/test/parallel/tls-client-abort.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-tls-client-abort.js -//#SHA1: e4f4d09f8de79ff5f4bdefcdaf1bbebb49f3cc16 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const tls = require("tls"); -const fs = require("fs"); -const path = require("path"); - -const hasCrypto = (() => { - try { - require("crypto"); - return true; - } catch { - return false; - } -})(); - -if (!hasCrypto) { - test.skip("missing crypto", () => {}); -} else { - test("TLS client abort", () => { - const cert = fs.readFileSync(path.join(__dirname, "..", "fixtures", "keys", "rsa_cert.crt")); - const key = fs.readFileSync(path.join(__dirname, "..", "fixtures", "keys", "rsa_private.pem")); - - const onConnect = jest.fn(); - const conn = tls.connect({ cert, key, port: 0 }, onConnect); - - conn.on("error", () => {}); // Expecting an error, but not testing its content - conn.destroy(); - - expect(onConnect).not.toHaveBeenCalled(); - }); -} - -//<#END_FILE: test-tls-client-abort.js diff --git a/test/js/node/test/parallel/tls-client-default-ciphers.test.js b/test/js/node/test/parallel/tls-client-default-ciphers.test.js deleted file mode 100644 index d337071777..0000000000 --- a/test/js/node/test/parallel/tls-client-default-ciphers.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-tls-client-default-ciphers.js -//#SHA1: 8a9af503503ffc8b8a7c27089fd7cf417e22ec16 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const tls = require("tls"); - -if (typeof Bun !== "undefined") { - test = it; -} - -if (!("crypto" in process.versions)) { - test.skip("missing crypto", () => {}); -} else { - test("tls.connect uses default ciphers", () => { - let ciphers = ""; - - class Done extends Error {} - - tls.createSecureContext = function (options) { - ciphers = options.ciphers; - throw new Done(); - }; - - expect(() => tls.connect()).toThrow(Done); - - expect(ciphers).toBe(tls.DEFAULT_CIPHERS); - }); -} - -//<#END_FILE: test-tls-client-default-ciphers.js diff --git a/test/js/node/test/parallel/tls-env-extra-ca-no-crypto.test.js b/test/js/node/test/parallel/tls-env-extra-ca-no-crypto.test.js deleted file mode 100644 index 5b48f8f775..0000000000 --- a/test/js/node/test/parallel/tls-env-extra-ca-no-crypto.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-tls-env-extra-ca-no-crypto.js -//#SHA1: da8421700b140b9bef4723eee10dbae7786423b6 -//----------------- -"use strict"; - -const { fork } = require("child_process"); -const path = require("path"); - -// This test ensures that trying to load extra certs won't throw even when -// there is no crypto support, i.e., built with "./configure --without-ssl". -if (process.argv[2] === "child") { - // exit -} else { - const NODE_EXTRA_CA_CERTS = path.join(__dirname, "..", "fixtures", "keys", "ca1-cert.pem"); - - test("Loading extra certs without crypto support", () => { - return new Promise(resolve => { - fork(__filename, ["child"], { env: { ...process.env, NODE_EXTRA_CA_CERTS } }).on("exit", status => { - // Client did not succeed in connecting - expect(status).toBe(0); - resolve(); - }); - }); - }); -} - -//<#END_FILE: test-tls-env-extra-ca-no-crypto.js diff --git a/test/js/node/test/parallel/tls-fast-writing.test.js b/test/js/node/test/parallel/tls-fast-writing.test.js deleted file mode 100644 index 7656d5eb15..0000000000 --- a/test/js/node/test/parallel/tls-fast-writing.test.js +++ /dev/null @@ -1,97 +0,0 @@ -//#FILE: test-tls-fast-writing.js -//#SHA1: 3a9ce4612ccf460fb5c0dfd4be6f8f5cad06c4a4 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const fixtures = require("../common/fixtures"); -const tls = require("tls"); - -const options = { - key: fixtures.readKey("rsa_private.pem"), - cert: fixtures.readKey("rsa_cert.crt"), - ca: [fixtures.readKey("rsa_ca.crt")], -}; - -let gotChunk = false; -let gotDrain = false; - -function onconnection(conn) { - conn.on("data", function (c) { - if (!gotChunk) { - gotChunk = true; - console.log("ok - got chunk"); - } - - // Just some basic sanity checks. - expect(c.length).toBeGreaterThan(0); - expect(Buffer.isBuffer(c)).toBe(true); - - if (gotDrain) { - process.exit(0); - } - }); -} - -test("TLS fast writing", done => { - if (!process.versions.openssl) { - console.log("1..0 # Skipped: missing crypto"); - return done(); - } - - const server = tls.createServer(options, onconnection); - - server.listen(0, function () { - const chunk = Buffer.alloc(1024, "x"); - const opt = { port: this.address().port, rejectUnauthorized: false }; - const conn = tls.connect(opt, function () { - conn.on("drain", ondrain); - write(); - }); - - function ondrain() { - if (!gotDrain) { - gotDrain = true; - console.log("ok - got drain"); - } - if (gotChunk) { - process.exit(0); - } - write(); - } - - function write() { - // This needs to return false eventually - while (false !== conn.write(chunk)); - } - }); - - // Clean up - process.on("exit", () => { - server.close(); - expect(gotChunk).toBe(true); - expect(gotDrain).toBe(true); - done(); - }); -}); - -//<#END_FILE: test-tls-fast-writing.js diff --git a/test/js/node/test/parallel/tls-inception.test.js b/test/js/node/test/parallel/tls-inception.test.js deleted file mode 100644 index 6aef54d4c3..0000000000 --- a/test/js/node/test/parallel/tls-inception.test.js +++ /dev/null @@ -1,98 +0,0 @@ -//#FILE: test-tls-inception.js -//#SHA1: 410893674973cae3603656c032a18e01a1cd759a -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const crypto = require("crypto"); -const fixtures = require("../common/fixtures"); -const tls = require("tls"); -const net = require("net"); - -if (!crypto.getCurves().includes("prime256v1")) { - test.skip("missing crypto support"); -} - -const options = { - key: fixtures.readKey("rsa_private.pem"), - cert: fixtures.readKey("rsa_cert.crt"), -}; - -const body = "A".repeat(40000); - -test("TLS inception", async () => { - // the "proxy" server - const a = tls.createServer(options, socket => { - const myOptions = { - host: "127.0.0.1", - port: b.address().port, - rejectUnauthorized: false, - }; - const dest = net.connect(myOptions); - dest.pipe(socket); - socket.pipe(dest); - - dest.on("end", () => { - socket.destroy(); - }); - }); - - // the "target" server - const b = tls.createServer(options, socket => { - socket.end(body); - }); - - await new Promise(resolve => { - a.listen(0, () => { - b.listen(0, resolve); - }); - }); - - const myOptions = { - host: "127.0.0.1", - port: a.address().port, - rejectUnauthorized: false, - }; - - return new Promise(resolve => { - const socket = tls.connect(myOptions); - const ssl = tls.connect({ - socket: socket, - rejectUnauthorized: false, - }); - ssl.setEncoding("utf8"); - let buf = ""; - ssl.on("data", data => { - buf += data; - }); - ssl.on("end", () => { - expect(buf).toBe(body); - ssl.end(); - a.close(); - b.close(); - resolve(); - }); - }); -}); - -//<#END_FILE: test-tls-inception.js diff --git a/test/js/node/test/parallel/tls-js-stream.test.js b/test/js/node/test/parallel/tls-js-stream.test.js deleted file mode 100644 index 811e6a23d3..0000000000 --- a/test/js/node/test/parallel/tls-js-stream.test.js +++ /dev/null @@ -1,78 +0,0 @@ -//#FILE: test-tls-js-stream.js -//#SHA1: 90a8d9d14bac997dbf3abb56da784bfcba8efee6 -//----------------- -"use strict"; - -const fixtures = require("../common/fixtures"); -const net = require("net"); -const stream = require("stream"); -const tls = require("tls"); - -if (!tls.createSecureContext) { - test.skip("missing crypto"); -} - -test("TLS over JavaScript stream", done => { - const server = tls.createServer( - { - key: fixtures.readKey("agent1-key.pem"), - cert: fixtures.readKey("agent1-cert.pem"), - }, - c => { - console.log("new client"); - c.resume(); - c.end("ohai"); - }, - ); - - server.listen(0, () => { - const raw = net.connect(server.address().port); - - let pending = false; - raw.on("readable", () => { - if (pending) p._read(); - }); - - raw.on("end", () => { - p.push(null); - }); - - const p = new stream.Duplex({ - read: function read() { - pending = false; - - const chunk = raw.read(); - if (chunk) { - console.log("read", chunk); - this.push(chunk); - } else { - pending = true; - } - }, - write: function write(data, enc, cb) { - console.log("write", data, enc); - raw.write(data, enc, cb); - }, - }); - - const socket = tls.connect( - { - socket: p, - rejectUnauthorized: false, - }, - () => { - console.log("client secure"); - socket.resume(); - socket.end("hello"); - }, - ); - - socket.once("close", () => { - console.log("client close"); - server.close(); - done(); - }); - }); -}); - -//<#END_FILE: test-tls-js-stream.js diff --git a/test/js/node/test/parallel/tls-net-socket-keepalive.test.js b/test/js/node/test/parallel/tls-net-socket-keepalive.test.js deleted file mode 100644 index a0378791cc..0000000000 --- a/test/js/node/test/parallel/tls-net-socket-keepalive.test.js +++ /dev/null @@ -1,80 +0,0 @@ -//#FILE: test-tls-net-socket-keepalive.js -//#SHA1: 9ae6965b63d37c0f9cb2ab7d068d4da2a68b8b1f -//----------------- -"use strict"; - -const fixtures = require("../common/fixtures"); -const tls = require("tls"); -const net = require("net"); - -// This test ensures that when tls sockets are created with `allowHalfOpen`, -// they won't hang. -const key = fixtures.readKey("agent1-key.pem"); -const cert = fixtures.readKey("agent1-cert.pem"); -const ca = fixtures.readKey("ca1-cert.pem"); -const options = { - key, - cert, - ca: [ca], -}; - -test("TLS sockets with allowHalfOpen do not hang", done => { - const server = tls.createServer(options, conn => { - conn.write("hello", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - conn.on("data", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - conn.on("end", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - conn.on("data", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - conn.on("close", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - conn.end(); - }); - - server.listen(0, () => { - const netSocket = new net.Socket({ - allowHalfOpen: true, - }); - - const socket = tls.connect({ - socket: netSocket, - rejectUnauthorized: false, - }); - - const { port, address } = server.address(); - - // Doing `net.Socket.connect()` after `tls.connect()` will make tls module - // wrap the socket in StreamWrap. - netSocket.connect({ - port, - address, - }); - - socket.on("secureConnect", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - socket.on("end", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - socket.on("data", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - }); - socket.on("close", () => { - expect(true).toBe(true); // Equivalent to common.mustCall() - server.close(); - done(); - }); - - socket.write("hello"); - socket.end(); - }); -}); - -//<#END_FILE: test-tls-net-socket-keepalive.js diff --git a/test/js/node/test/parallel/tls-peer-certificate-multi-keys.test.js b/test/js/node/test/parallel/tls-peer-certificate-multi-keys.test.js deleted file mode 100644 index d1cfe70e15..0000000000 --- a/test/js/node/test/parallel/tls-peer-certificate-multi-keys.test.js +++ /dev/null @@ -1,87 +0,0 @@ -//#FILE: test-tls-peer-certificate-multi-keys.js -//#SHA1: d30d685d74ebea73274e19cea1a19a2b8cea5120 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const tls = require("tls"); -const fixtures = require("../common/fixtures"); - -const options = { - key: fixtures.readKey("rsa_private.pem"), - cert: fixtures.readKey("rsa_cert.crt"), -}; - -let server; - -beforeAll(() => { - if (!process.versions.openssl) { - return test.skip("missing crypto"); - } -}); - -afterAll(() => { - if (server) { - server.close(); - } -}); - -test("TLS peer certificate with multiple keys", async () => { - server = tls.createServer(options, cleartext => { - cleartext.end("World"); - }); - - const serverSecureConnectionPromise = new Promise(resolve => { - server.once("secureConnection", socket => { - const cert = socket.getCertificate(); - // The server's local cert is the client's peer cert. - expect(cert.subject.OU).toEqual(["Test TLS Certificate", "Engineering"]); - resolve(); - }); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const clientConnectionPromise = new Promise((resolve, reject) => { - const socket = tls.connect( - { - port: server.address().port, - rejectUnauthorized: false, - }, - () => { - const peerCert = socket.getPeerCertificate(); - expect(peerCert.subject.OU).toEqual(["Test TLS Certificate", "Engineering"]); - socket.end("Hello"); - resolve(); - }, - ); - - socket.on("error", reject); - }); - - await Promise.all([serverSecureConnectionPromise, clientConnectionPromise]); -}); - -//<#END_FILE: test-tls-peer-certificate-multi-keys.js diff --git a/test/js/node/test/parallel/tls-startcom-wosign-whitelist.test.js b/test/js/node/test/parallel/tls-startcom-wosign-whitelist.test.js deleted file mode 100644 index fde4c90e29..0000000000 --- a/test/js/node/test/parallel/tls-startcom-wosign-whitelist.test.js +++ /dev/null @@ -1,82 +0,0 @@ -//#FILE: test-tls-startcom-wosign-whitelist.js -//#SHA1: 6742ecdeeaa94ca1efad850b021d5308b3077358 -//----------------- -"use strict"; - -const tls = require("tls"); -const fixtures = require("../common/fixtures"); - -function loadPEM(n) { - return fixtures.readKey(`${n}.pem`); -} - -const testCases = [ - { - // agent8 is signed by fake-startcom-root with notBefore of - // Oct 20 23:59:59 2016 GMT. It passes StartCom/WoSign check. - serverOpts: { - key: loadPEM("agent8-key"), - cert: loadPEM("agent8-cert"), - }, - clientOpts: { - ca: loadPEM("fake-startcom-root-cert"), - port: undefined, - rejectUnauthorized: true, - }, - errorCode: "CERT_REVOKED", - }, - { - // agent9 is signed by fake-startcom-root with notBefore of - // Oct 21 00:00:01 2016 GMT. It fails StartCom/WoSign check. - serverOpts: { - key: loadPEM("agent9-key"), - cert: loadPEM("agent9-cert"), - }, - clientOpts: { - ca: loadPEM("fake-startcom-root-cert"), - port: undefined, - rejectUnauthorized: true, - }, - errorCode: "CERT_REVOKED", - }, -]; - -describe("TLS StartCom/WoSign Whitelist", () => { - let finishedTests = 0; - - afterAll(() => { - expect(finishedTests).toBe(testCases.length); - }); - - testCases.forEach((tcase, index) => { - it(`should handle case ${index + 1} correctly`, async () => { - const server = tls.createServer(tcase.serverOpts, s => { - s.resume(); - }); - - await new Promise(resolve => { - server.listen(0, () => { - tcase.clientOpts.port = server.address().port; - const client = tls.connect(tcase.clientOpts); - - client.on("error", e => { - expect(e.code).toBe(tcase.errorCode); - server.close(resolve); - }); - - client.on("secureConnect", () => { - // agent8 can pass StartCom/WoSign check so that the secureConnect - // is established. - expect(tcase.errorCode).toBe("CERT_REVOKED"); - client.end(); - server.close(resolve); - }); - }); - }); - - finishedTests++; - }); - }); -}); - -//<#END_FILE: test-tls-startcom-wosign-whitelist.js diff --git a/test/js/node/test/parallel/tls-ticket-cluster.test.js b/test/js/node/test/parallel/tls-ticket-cluster.test.js deleted file mode 100644 index 2def9c478c..0000000000 --- a/test/js/node/test/parallel/tls-ticket-cluster.test.js +++ /dev/null @@ -1,153 +0,0 @@ -//#FILE: test-tls-ticket-cluster.js -//#SHA1: a65d425111990ee164b458fd373b9f5bb083f6df -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const tls = require("tls"); -const cluster = require("cluster"); -const fixtures = require("../common/fixtures"); - -const workerCount = 4; -const expectedReqCount = 16; - -if (!process.env.NODE_SKIP_NO_CRYPTO_TEST) { - test("skip if missing crypto", () => { - const hasCrypto = typeof process.versions.openssl === "string"; - if (!hasCrypto) { - console.log("1..0 # Skipped: missing crypto"); - } - expect(hasCrypto).toBe(true); - }); -} - -if (cluster.isPrimary) { - let listeningCount = 0; - let reusedCount = 0; - let reqCount = 0; - let lastSession = null; - let workerPort = null; - - function shoot() { - console.error("[primary] connecting", workerPort, "session?", !!lastSession); - const c = tls - .connect( - workerPort, - { - session: lastSession, - rejectUnauthorized: false, - }, - () => { - c.on("end", c.end); - }, - ) - .on("close", () => { - // Wait for close to shoot off another connection. We don't want to shoot - // until a new session is allocated, if one will be. The new session is - // not guaranteed on secureConnect (it depends on TLS1.2 vs TLS1.3), but - // it is guaranteed to happen before the connection is closed. - if (++reqCount === expectedReqCount) { - Object.keys(cluster.workers).forEach(function (id) { - cluster.workers[id].send("die"); - }); - } else { - shoot(); - } - }) - .once("session", session => { - expect(lastSession).toBeFalsy(); - lastSession = session; - }); - - c.resume(); // See close_notify comment in server - } - - function fork() { - const worker = cluster.fork(); - worker.on("message", ({ msg, port }) => { - console.error("[primary] got %j", msg); - if (msg === "reused") { - ++reusedCount; - } else if (msg === "listening" && ++listeningCount === workerCount) { - workerPort = port; - shoot(); - } - }); - - worker.on("exit", () => { - console.error("[primary] worker died"); - }); - } - - test("cluster primary", () => { - for (let i = 0; i < workerCount; i++) { - fork(); - } - - process.on("exit", () => { - expect(reqCount).toBe(expectedReqCount); - expect(reusedCount + 1).toBe(reqCount); - }); - }); -} else { - const key = fixtures.readKey("rsa_private.pem"); - const cert = fixtures.readKey("rsa_cert.crt"); - - const options = { key, cert }; - - const server = tls.createServer(options, c => { - console.error("[worker] connection reused?", c.isSessionReused()); - if (c.isSessionReused()) { - process.send({ msg: "reused" }); - } else { - process.send({ msg: "not-reused" }); - } - // Used to just .end(), but that means client gets close_notify before - // NewSessionTicket. Send data until that problem is solved. - c.end("x"); - }); - - server.listen(0, () => { - const { port } = server.address(); - process.send({ - msg: "listening", - port, - }); - }); - - process.on("message", function listener(msg) { - console.error("[worker] got %j", msg); - if (msg === "die") { - server.close(() => { - console.error("[worker] server close"); - process.exit(); - }); - } - }); - - process.on("exit", () => { - console.error("[worker] exit"); - }); -} - -//<#END_FILE: test-tls-ticket-cluster.js diff --git a/test/js/node/test/parallel/tls-transport-destroy-after-own-gc.test.js b/test/js/node/test/parallel/tls-transport-destroy-after-own-gc.test.js deleted file mode 100644 index 1f7c20dda3..0000000000 --- a/test/js/node/test/parallel/tls-transport-destroy-after-own-gc.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-tls-transport-destroy-after-own-gc.js -//#SHA1: e2ef35ad88444196d24664e93fb9efdad050c876 -//----------------- -// Flags: --expose-gc -"use strict"; - -// Regression test for https://github.com/nodejs/node/issues/17475 -// Unfortunately, this tests only "works" reliably when checked with valgrind or -// a similar tool. - -const { TLSSocket } = require("tls"); -const makeDuplexPair = require("../common/duplexpair"); - -test("TLSSocket destruction after garbage collection", done => { - if (!process.versions.openssl) { - done(); - return; - } - - let { clientSide } = makeDuplexPair(); - - let clientTLS = new TLSSocket(clientSide, { isServer: false }); - let clientTLSHandle = clientTLS._handle; // eslint-disable-line no-unused-vars - - setImmediate(() => { - clientTLS = null; - global.gc(); - clientTLSHandle = null; - global.gc(); - setImmediate(() => { - clientSide = null; - global.gc(); - // If we've reached this point without crashing, the test has passed - done(); - }); - }); -}); - -//<#END_FILE: test-tls-transport-destroy-after-own-gc.js diff --git a/test/js/node/test/parallel/tls-wrap-econnreset.test.js b/test/js/node/test/parallel/tls-wrap-econnreset.test.js deleted file mode 100644 index 3f11b13be2..0000000000 --- a/test/js/node/test/parallel/tls-wrap-econnreset.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-tls-wrap-econnreset.js -//#SHA1: 22f57b68ee3c5d271a9235972865773da523a34e -//----------------- -"use strict"; - -const net = require("net"); -const tls = require("tls"); - -// Skip the test if crypto is not available -if (!("crypto" in process.versions)) { - test.skip("missing crypto", () => {}); -} else { - test("TLS connection reset", async () => { - const server = net.createServer(c => { - c.end(); - }); - - await new Promise(resolve => { - server.listen(0, resolve); - }); - - const port = server.address().port; - - let errored = false; - const tlsConnection = tls.connect(port, "127.0.0.1"); - - await expect( - new Promise((_, reject) => { - tlsConnection.once("error", reject); - }), - ).rejects.toMatchObject({ - code: "ECONNRESET", - path: undefined, - host: "127.0.0.1", - port: port, - localAddress: undefined, - message: expect.any(String), - }); - - errored = true; - server.close(); - - await new Promise(resolve => { - tlsConnection.on("close", resolve); - }); - - expect(errored).toBe(true); - }); -} - -//<#END_FILE: test-tls-wrap-econnreset.js diff --git a/test/js/node/test/parallel/tls-zero-clear-in.test.js b/test/js/node/test/parallel/tls-zero-clear-in.test.js deleted file mode 100644 index 4b254bdf3c..0000000000 --- a/test/js/node/test/parallel/tls-zero-clear-in.test.js +++ /dev/null @@ -1,81 +0,0 @@ -//#FILE: test-tls-zero-clear-in.js -//#SHA1: 6014fd3aa5a294b4e8594a32f0eb8e7b3c206213 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const tls = require("tls"); -const { readKey } = require("../common/fixtures"); - -if (!process.versions.openssl) { - test.skip("missing crypto"); -} - -const cert = readKey("rsa_cert.crt"); -const key = readKey("rsa_private.pem"); - -test("SSL_write() call with 0 bytes should not be treated as error", done => { - const server = tls.createServer( - { - cert, - key, - }, - c => { - // Nop - setTimeout(() => { - c.end(); - server.close(); - }, 20); - }, - ); - - server.listen(0, () => { - const conn = tls.connect( - { - cert: cert, - key: key, - rejectUnauthorized: false, - port: server.address().port, - }, - () => { - setTimeout(() => { - conn.destroy(); - }, 20); - }, - ); - - // SSL_write() call's return value, when called 0 bytes, should not be - // treated as error. - conn.end(""); - - conn.on("error", err => { - done(new Error("Unexpected error event")); - }); - - setTimeout(() => { - done(); - }, 100); - }); -}); - -//<#END_FILE: test-tls-zero-clear-in.js diff --git a/test/js/node/test/parallel/tty-stdin-end.test.js b/test/js/node/test/parallel/tty-stdin-end.test.js deleted file mode 100644 index cb2f825045..0000000000 --- a/test/js/node/test/parallel/tty-stdin-end.test.js +++ /dev/null @@ -1,15 +0,0 @@ -//#FILE: test-tty-stdin-end.js -//#SHA1: 66243635acf852b7a108d06e289e5db2b2573bad -//----------------- -"use strict"; - -// This test ensures that Node.js doesn't crash on `process.stdin.emit("end")`. -// https://github.com/nodejs/node/issues/1068 - -test("process.stdin.emit('end') doesn't crash", () => { - expect(() => { - process.stdin.emit("end"); - }).not.toThrow(); -}); - -//<#END_FILE: test-tty-stdin-end.js diff --git a/test/js/node/test/parallel/tz-version.test.js b/test/js/node/test/parallel/tz-version.test.js deleted file mode 100644 index 04bac4dcdb..0000000000 --- a/test/js/node/test/parallel/tz-version.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-tz-version.js -//#SHA1: 7c06414dda474dff448a3998499dddffdf084126 -//----------------- -"use strict"; - -// Skip the test if Intl is not available -if (typeof Intl === "undefined") { - test.skip("missing Intl", () => {}); -} else { - // Refs: https://github.com/nodejs/node/blob/1af63a90ca3a59ca05b3a12ad7dbea04008db7d9/configure.py#L1694-L1711 - if (process.config.variables.icu_path !== "deps/icu-small") { - // If Node.js is configured to use its built-in ICU, it uses a strict subset - // of ICU formed using `tools/icu/shrink-icu-src.py`, which is present in - // `deps/icu-small`. It is not the same as configuring the build with - // `./configure --with-intl=small-icu`. The latter only uses a subset of the - // locales, i.e., it uses the English locale, `root,en`, by default and other - // locales can also be specified using the `--with-icu-locales` option. - test.skip("not using the icu data file present in deps/icu-small/source/data/in/icudt##l.dat.bz2", () => {}); - } else { - const { readFileSync } = require("fs"); - const path = require("path"); - - // This test ensures the correctness of the automated timezone upgrade PRs. - - test("timezone version matches expected version", () => { - const expectedVersion = readFileSync(path.join(__dirname, "fixtures", "tz-version.txt"), "utf8").trim(); - expect(process.versions.tz).toBe(expectedVersion); - }); - } -} - -//<#END_FILE: test-tz-version.js diff --git a/test/js/node/test/parallel/unhandled-exception-with-worker-inuse.test.js b/test/js/node/test/parallel/unhandled-exception-with-worker-inuse.test.js deleted file mode 100644 index 6acae8cf95..0000000000 --- a/test/js/node/test/parallel/unhandled-exception-with-worker-inuse.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-unhandled-exception-with-worker-inuse.js -//#SHA1: 4eb89fdb9d675dbef4f89ed61ab77225b9200b4a -//----------------- -"use strict"; - -// https://github.com/nodejs/node/issues/45421 -// -// Check that node will NOT call v8::Isolate::SetIdle() when exiting -// due to an unhandled exception, otherwise the assertion(enabled in -// debug build only) in the SetIdle(), which checks that the vm state -// is either EXTERNAL or IDLE will fail. -// -// The root cause of this issue is that before PerIsolateMessageListener() -// is invoked by v8, v8 preserves the JS vm state, although it should -// switch to EXTERNEL. https://bugs.chromium.org/p/v8/issues/detail?id=13464 -// -// Therefore, this commit can be considered as an workaround of the v8 bug, -// but we also find it not useful to call SetIdle() when terminating. - -if (process.argv[2] === "child") { - const { Worker } = require("worker_threads"); - new Worker("", { eval: true }); - throw new Error("xxx"); -} else { - const { spawnSync } = require("child_process"); - - test("unhandled exception with worker in use", () => { - const result = spawnSync(process.execPath, [__filename, "child"]); - - const stderr = result.stderr.toString().trim(); - // Expect error message to be preserved - expect(stderr).toMatch(/xxx/); - // Expect no crash - expect(result.status).not.toBe(null); - expect(result.signal).toBe(null); - }); -} - -//<#END_FILE: test-unhandled-exception-with-worker-inuse.js diff --git a/test/js/node/test/parallel/url-canparse-whatwg.test.js b/test/js/node/test/parallel/url-canparse-whatwg.test.js deleted file mode 100644 index a6c920c536..0000000000 --- a/test/js/node/test/parallel/url-canparse-whatwg.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-url-canParse-whatwg.js -//#SHA1: e1170e8b8d0057443bfb307c64dbd27b204ac2f0 -//----------------- -"use strict"; - -test("URL.canParse requires one argument", () => { - expect(() => { - URL.canParse(); - }).toThrow( - expect.objectContaining({ - code: "ERR_MISSING_ARGS", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -test("URL.canParse works with v8 fast api", () => { - // This test is to ensure that the v8 fast api works. - for (let i = 0; i < 1e5; i++) { - expect(URL.canParse("https://www.example.com/path/?query=param#hash")).toBe(true); - } -}); - -//<#END_FILE: test-url-canParse-whatwg.js diff --git a/test/js/node/test/parallel/url-domain-ascii-unicode.test.js b/test/js/node/test/parallel/url-domain-ascii-unicode.test.js deleted file mode 100644 index 0a716939a9..0000000000 --- a/test/js/node/test/parallel/url-domain-ascii-unicode.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-url-domain-ascii-unicode.js -//#SHA1: 717d40eef6d2d8f5adccf01fe09dc43f8b776e13 -//----------------- -"use strict"; - -const url = require("url"); - -const domainToASCII = url.domainToASCII; -const domainToUnicode = url.domainToUnicode; - -const domainWithASCII = [ - ["ıíd", "xn--d-iga7r"], - ["يٴ", "xn--mhb8f"], - ["www.ϧƽəʐ.com", "www.xn--cja62apfr6c.com"], - ["новини.com", "xn--b1amarcd.com"], - ["名がドメイン.com", "xn--v8jxj3d1dzdz08w.com"], - ["افغانستا.icom.museum", "xn--mgbaal8b0b9b2b.icom.museum"], - ["الجزائر.icom.fake", "xn--lgbbat1ad8j.icom.fake"], - ["भारत.org", "xn--h2brj9c.org"], -]; - -describe("URL domain ASCII and Unicode conversion", () => { - // Skip the entire test suite if Intl is not available - beforeAll(() => { - if (typeof Intl === "undefined") { - throw new Error("missing Intl"); - } - }); - - test.each(domainWithASCII)("converts %s <-> %s", (domain, ascii) => { - const domainConvertedToASCII = domainToASCII(domain); - expect(domainConvertedToASCII).toBe(ascii); - - const asciiConvertedToUnicode = domainToUnicode(ascii); - expect(asciiConvertedToUnicode).toBe(domain); - }); -}); - -//<#END_FILE: test-url-domain-ascii-unicode.js diff --git a/test/js/node/test/parallel/url-is-url.test.js b/test/js/node/test/parallel/url-is-url.test.js deleted file mode 100644 index de30ec10f0..0000000000 --- a/test/js/node/test/parallel/url-is-url.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-url-is-url.js -//#SHA1: 337a63661b82d89cf2592ee32d91780acb8e5925 -//----------------- -"use strict"; - -const { URL, parse } = require("url"); - -// Remove the internal module usage -// const { isURL } = require('internal/url'); - -// Implement a simple isURL function for testing purposes -function isURL(input) { - return input instanceof URL; -} - -test("isURL function", () => { - expect(isURL(new URL("https://www.nodejs.org"))).toBe(true); - expect(isURL(parse("https://www.nodejs.org"))).toBe(false); - expect( - isURL({ - href: "https://www.nodejs.org", - protocol: "https:", - path: "/", - }), - ).toBe(false); -}); - -//<#END_FILE: test-url-is-url.js diff --git a/test/js/node/test/parallel/url-revokeobjecturl.test.js b/test/js/node/test/parallel/url-revokeobjecturl.test.js deleted file mode 100644 index 190ed562f5..0000000000 --- a/test/js/node/test/parallel/url-revokeobjecturl.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-url-revokeobjecturl.js -//#SHA1: 573bfad806102976807ad71fef71b079005d8bfa -//----------------- -"use strict"; - -// Test ensures that the function receives the url argument. - -test("URL.revokeObjectURL() throws with missing argument", () => { - expect(() => { - URL.revokeObjectURL(); - }).toThrow( - expect.objectContaining({ - code: "ERR_MISSING_ARGS", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-url-revokeobjecturl.js diff --git a/test/js/node/test/parallel/url-urltooptions.test.js b/test/js/node/test/parallel/url-urltooptions.test.js deleted file mode 100644 index 3cb50f21b5..0000000000 --- a/test/js/node/test/parallel/url-urltooptions.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-url-urltooptions.js -//#SHA1: 0ba1cb976ec5888306f7c820a39afcb93b882d03 -//----------------- -"use strict"; - -const { URL } = require("url"); -const { urlToHttpOptions } = require("url"); - -describe("urlToHttpOptions", () => { - test("converts URL object to HTTP options", () => { - const urlObj = new URL("http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test"); - const opts = urlToHttpOptions(urlObj); - - expect(opts).not.toBeInstanceOf(URL); - expect(opts.protocol).toBe("http:"); - expect(opts.auth).toBe("user:pass"); - expect(opts.hostname).toBe("foo.bar.com"); - expect(opts.port).toBe(21); - expect(opts.path).toBe("/aaa/zzz?l=24"); - expect(opts.pathname).toBe("/aaa/zzz"); - expect(opts.search).toBe("?l=24"); - expect(opts.hash).toBe("#test"); - }); - - test("handles IPv6 hostname correctly", () => { - const { hostname } = urlToHttpOptions(new URL("http://[::1]:21")); - expect(hostname).toBe("::1"); - }); - - test("handles copied URL object with missing data properties", () => { - const urlObj = new URL("http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test"); - const copiedUrlObj = { ...urlObj }; - const copiedOpts = urlToHttpOptions(copiedUrlObj); - - expect(copiedOpts).not.toBeInstanceOf(URL); - expect(copiedOpts.protocol).toBeUndefined(); - expect(copiedOpts.auth).toBeUndefined(); - expect(copiedOpts.hostname).toBeUndefined(); - expect(copiedOpts.port).toBeNaN(); - expect(copiedOpts.path).toBe(""); - expect(copiedOpts.pathname).toBeUndefined(); - expect(copiedOpts.search).toBeUndefined(); - expect(copiedOpts.hash).toBeUndefined(); - expect(copiedOpts.href).toBeUndefined(); - }); -}); - -//<#END_FILE: test-url-urltooptions.js diff --git a/test/js/node/test/parallel/utf8-scripts.test.js b/test/js/node/test/parallel/utf8-scripts.test.js deleted file mode 100644 index e4c5a4bec4..0000000000 --- a/test/js/node/test/parallel/utf8-scripts.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-utf8-scripts.js -//#SHA1: 103e32f817ebd35d617fba00e947304390c431d5 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// üäö - -test("UTF-8 scripts", () => { - const consoleSpy = jest.spyOn(console, "log"); - - console.log("Σὲ γνωρίζω ἀπὸ τὴν κόψη"); - - expect(consoleSpy).toHaveBeenCalledWith("Σὲ γνωρίζω ἀπὸ τὴν κόψη"); - consoleSpy.mockRestore(); - - expect("Hellö Wörld").toMatch(/Hellö Wörld/); -}); - -//<#END_FILE: test-utf8-scripts.js diff --git a/test/js/node/test/parallel/util-deprecate.test.js b/test/js/node/test/parallel/util-deprecate.test.js deleted file mode 100644 index 319c02c3b0..0000000000 --- a/test/js/node/test/parallel/util-deprecate.test.js +++ /dev/null @@ -1,79 +0,0 @@ -//#FILE: test-util-deprecate.js -//#SHA1: 43c232bacd8dcc9f39194125c071db2ab14dfb51 -//----------------- -"use strict"; - -const util = require("util"); - -// Tests basic functionality of util.deprecate(). - -// Mock process.on for warnings -const mockWarningListener = jest.fn(); -const mockExitListener = jest.fn(); -process.on = jest.fn((event, listener) => { - if (event === "warning") mockWarningListener.mockImplementation(listener); - if (event === "exit") mockExitListener.mockImplementation(listener); -}); - -const expectedWarnings = new Map(); - -test("Emits deprecation only once if same function is called", () => { - const msg = "fhqwhgads"; - const fn = util.deprecate(() => {}, msg); - expectedWarnings.set(msg, { code: undefined, count: 1 }); - fn(); - fn(); -}); - -test("Emits deprecation twice for different functions", () => { - const msg = "sterrance"; - const fn1 = util.deprecate(() => {}, msg); - const fn2 = util.deprecate(() => {}, msg); - expectedWarnings.set(msg, { code: undefined, count: 2 }); - fn1(); - fn2(); -}); - -test("Emits deprecation only once if optional code is the same, even for different functions", () => { - const msg = "cannonmouth"; - const code = "deprecatesque"; - const fn1 = util.deprecate(() => {}, msg, code); - const fn2 = util.deprecate(() => {}, msg, code); - expectedWarnings.set(msg, { code, count: 1 }); - fn1(); - fn2(); - fn1(); - fn2(); -}); - -test("Handles warnings correctly", () => { - expectedWarnings.forEach((expected, message) => { - for (let i = 0; i < expected.count; i++) { - mockWarningListener({ - name: "DeprecationWarning", - message: message, - code: expected.code, - }); - } - }); - - expect(mockWarningListener).toHaveBeenCalledTimes( - Array.from(expectedWarnings.values()).reduce((acc, curr) => acc + curr.count, 0), - ); - - mockWarningListener.mock.calls.forEach(([warning]) => { - expect(warning.name).toBe("DeprecationWarning"); - expect(expectedWarnings.has(warning.message)).toBe(true); - const expected = expectedWarnings.get(warning.message); - expect(warning.code).toBe(expected.code); - expected.count--; - if (expected.count === 0) { - expectedWarnings.delete(warning.message); - } - }); -}); - -test("All warnings are processed", () => { - mockExitListener(); - expect(expectedWarnings.size).toBe(0); -}); diff --git a/test/js/node/test/parallel/util-inspect-getters-accessing-this.test.js b/test/js/node/test/parallel/util-inspect-getters-accessing-this.test.js deleted file mode 100644 index 78cec627d2..0000000000 --- a/test/js/node/test/parallel/util-inspect-getters-accessing-this.test.js +++ /dev/null @@ -1,64 +0,0 @@ -//#FILE: test-util-inspect-getters-accessing-this.js -//#SHA1: 92c41c06f838da46cbbfcd7f695a19784af3f581 -//----------------- -"use strict"; - -const { inspect } = require("util"); - -// This test ensures that util.inspect logs getters -// which access this. - -test("util.inspect logs getters accessing this", () => { - class X { - constructor() { - this._y = 123; - } - - get y() { - return this._y; - } - } - - const result = inspect(new X(), { - getters: true, - showHidden: true, - }); - - expect(result).toBe("X { _y: 123, [y]: [Getter: 123] }"); -}); - -// Regression test for https://github.com/nodejs/node/issues/37054 -test("util.inspect handles circular references in getters", () => { - class A { - constructor(B) { - this.B = B; - } - get b() { - return this.B; - } - } - - class B { - constructor() { - this.A = new A(this); - } - get a() { - return this.A; - } - } - - const result = inspect(new B(), { - depth: 1, - getters: true, - showHidden: true, - }); - - expect(result).toBe( - " B {\n" + - " A: A { B: [Circular *1], [b]: [Getter] [Circular *1] },\n" + - " [a]: [Getter] A { B: [Circular *1], [b]: [Getter] [Circular *1] }\n" + - "}", - ); -}); - -//<#END_FILE: test-util-inspect-getters-accessing-this.js diff --git a/test/js/node/test/parallel/util-inspect-long-running.test.js b/test/js/node/test/parallel/util-inspect-long-running.test.js deleted file mode 100644 index 7fba0e08b7..0000000000 --- a/test/js/node/test/parallel/util-inspect-long-running.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-util-inspect-long-running.js -//#SHA1: 2e4cbb5743a4dfcf869e84ce6d58795f96465aeb -//----------------- -"use strict"; - -// Test that huge objects don't crash due to exceeding the maximum heap size. - -const util = require("util"); - -test("util.inspect handles huge objects without crashing", () => { - // Create a difficult to stringify object. Without the artificial limitation - // this would crash or throw an maximum string size error. - let last = {}; - const obj = last; - - for (let i = 0; i < 1000; i++) { - last.next = { circular: obj, last, obj: { a: 1, b: 2, c: true } }; - last = last.next; - obj[i] = last; - } - - // This should not throw an error or crash - expect(() => { - util.inspect(obj, { depth: Infinity }); - }).not.toThrow(); -}); - -//<#END_FILE: test-util-inspect-long-running.js diff --git a/test/js/node/test/parallel/util-primordial-monkeypatching.test.js b/test/js/node/test/parallel/util-primordial-monkeypatching.test.js deleted file mode 100644 index 8f369c1d8f..0000000000 --- a/test/js/node/test/parallel/util-primordial-monkeypatching.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-util-primordial-monkeypatching.js -//#SHA1: 72e754f1abd435e598620901f26b087e0bf9d5a7 -//----------------- -"use strict"; - -// Monkeypatch Object.keys() so that it throws an unexpected error. This tests -// that `util.inspect()` is unaffected by monkey-patching `Object`. - -const util = require("util"); - -test("util.inspect() is unaffected by monkey-patching Object.keys()", () => { - const originalObjectKeys = Object.keys; - - // Monkey-patch Object.keys - Object.keys = () => { - throw new Error("fhqwhgads"); - }; - - try { - expect(util.inspect({})).toBe("{}"); - } finally { - // Restore original Object.keys to avoid affecting other tests - Object.keys = originalObjectKeys; - } -}); diff --git a/test/js/node/test/parallel/util-sigint-watchdog.test.js b/test/js/node/test/parallel/util-sigint-watchdog.test.js deleted file mode 100644 index 8f265aa240..0000000000 --- a/test/js/node/test/parallel/util-sigint-watchdog.test.js +++ /dev/null @@ -1,83 +0,0 @@ -//#FILE: test-util-sigint-watchdog.js -//#SHA1: 8731497954a21e75af21af0adde10619d98a703f -//----------------- -"use strict"; - -// Skip test on Windows platforms -if (process.platform === "win32") { - test.skip("platform not supported", () => {}); -} else { - const binding = { - startSigintWatchdog: jest.fn(), - stopSigintWatchdog: jest.fn(), - watchdogHasPendingSigint: jest.fn(), - }; - - // Mock the process.kill function - const originalKill = process.kill; - beforeAll(() => { - process.kill = jest.fn(); - }); - afterAll(() => { - process.kill = originalKill; - }); - - function waitForPendingSignal(cb) { - if (binding.watchdogHasPendingSigint()) cb(); - else setTimeout(waitForPendingSignal, 10, cb); - } - - test("with no signal observed", () => { - binding.startSigintWatchdog(); - binding.stopSigintWatchdog.mockReturnValue(false); - const hadPendingSignals = binding.stopSigintWatchdog(); - expect(hadPendingSignals).toBe(false); - }); - - test("with one call to the watchdog, one signal", done => { - binding.startSigintWatchdog(); - process.kill(process.pid, "SIGINT"); - binding.watchdogHasPendingSigint.mockReturnValue(true); - binding.stopSigintWatchdog.mockReturnValue(true); - - waitForPendingSignal(() => { - const hadPendingSignals = binding.stopSigintWatchdog(); - expect(hadPendingSignals).toBe(true); - done(); - }); - }); - - test("nested calls are okay", done => { - binding.startSigintWatchdog(); - binding.startSigintWatchdog(); - process.kill(process.pid, "SIGINT"); - binding.watchdogHasPendingSigint.mockReturnValue(true); - binding.stopSigintWatchdog.mockReturnValueOnce(true).mockReturnValueOnce(false); - - waitForPendingSignal(() => { - const hadPendingSignals1 = binding.stopSigintWatchdog(); - const hadPendingSignals2 = binding.stopSigintWatchdog(); - expect(hadPendingSignals1).toBe(true); - expect(hadPendingSignals2).toBe(false); - done(); - }); - }); - - test("signal comes in after first call to stop", done => { - binding.startSigintWatchdog(); - binding.startSigintWatchdog(); - binding.stopSigintWatchdog.mockReturnValueOnce(false).mockReturnValueOnce(true); - const hadPendingSignals1 = binding.stopSigintWatchdog(); - process.kill(process.pid, "SIGINT"); - binding.watchdogHasPendingSigint.mockReturnValue(true); - - waitForPendingSignal(() => { - const hadPendingSignals2 = binding.stopSigintWatchdog(); - expect(hadPendingSignals1).toBe(false); - expect(hadPendingSignals2).toBe(true); - done(); - }); - }); -} - -//<#END_FILE: test-util-sigint-watchdog.js diff --git a/test/js/node/test/parallel/uv-unmapped-exception.test.js b/test/js/node/test/parallel/uv-unmapped-exception.test.js deleted file mode 100644 index c8a5147fdc..0000000000 --- a/test/js/node/test/parallel/uv-unmapped-exception.test.js +++ /dev/null @@ -1,47 +0,0 @@ -//#FILE: test-uv-unmapped-exception.js -//#SHA1: 2878fb9a4523acb34f6283c980cde15b06fdc055 -//----------------- -"use strict"; - -// We can't use internal modules, so we'll need to implement our own UVException and UVExceptionWithHostPort -class UVException extends Error { - constructor({ errno, syscall }) { - super(`UNKNOWN: unknown error, ${syscall}`); - this.errno = errno; - this.syscall = syscall; - this.code = "UNKNOWN"; - } -} - -class UVExceptionWithHostPort extends Error { - constructor(errno, syscall, address, port) { - super(`${syscall} UNKNOWN: unknown error ${address}:${port}`); - this.code = "UNKNOWN"; - this.errno = errno; - this.syscall = syscall; - this.address = address; - this.port = port; - } -} - -test("UVException", () => { - const exception = new UVException({ errno: 100, syscall: "open" }); - - expect(exception.message).toBe("UNKNOWN: unknown error, open"); - expect(exception.errno).toBe(100); - expect(exception.syscall).toBe("open"); - expect(exception.code).toBe("UNKNOWN"); -}); - -test("UVExceptionWithHostPort", () => { - const exception = new UVExceptionWithHostPort(100, "listen", "127.0.0.1", 80); - - expect(exception.message).toBe("listen UNKNOWN: unknown error 127.0.0.1:80"); - expect(exception.code).toBe("UNKNOWN"); - expect(exception.errno).toBe(100); - expect(exception.syscall).toBe("listen"); - expect(exception.address).toBe("127.0.0.1"); - expect(exception.port).toBe(80); -}); - -//<#END_FILE: test-uv-unmapped-exception.js diff --git a/test/js/node/test/parallel/v8-collect-gc-profile-in-worker.test.js b/test/js/node/test/parallel/v8-collect-gc-profile-in-worker.test.js deleted file mode 100644 index ff43b62a54..0000000000 --- a/test/js/node/test/parallel/v8-collect-gc-profile-in-worker.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-v8-collect-gc-profile-in-worker.js -//#SHA1: 212956c00ed8e788f682c6335879f6844972a424 -//----------------- -// Flags: --expose-gc -"use strict"; - -const { Worker } = require("worker_threads"); - -// Replace testGCProfiler with a Jest-compatible mock -const testGCProfiler = jest.fn(); - -if (!process.env.isWorker) { - test("Worker thread creation", () => { - process.env.isWorker = 1; - const worker = new Worker(__filename); - expect(worker).toBeDefined(); - }); -} else { - test("GC profiler in worker thread", () => { - testGCProfiler(); - for (let i = 0; i < 100; i++) { - new Array(100); - } - - // Check if global.gc is available - if (typeof global.gc === "function") { - global.gc(); - } else { - console.warn("global.gc is not available. Make sure to run with --expose-gc flag."); - } - - expect(testGCProfiler).toHaveBeenCalledTimes(1); - }); -} - -//<#END_FILE: test-v8-collect-gc-profile-in-worker.js diff --git a/test/js/node/test/parallel/v8-deserialize-buffer.test.js b/test/js/node/test/parallel/v8-deserialize-buffer.test.js deleted file mode 100644 index d84675a6c0..0000000000 --- a/test/js/node/test/parallel/v8-deserialize-buffer.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-v8-deserialize-buffer.js -//#SHA1: ef80f8c41f9e9b893ea639f4558713addcf82b9a -//----------------- -"use strict"; - -const v8 = require("v8"); - -test("v8.deserialize should not emit warnings for Buffer.alloc(0)", () => { - const warningListener = jest.fn(); - process.on("warning", warningListener); - - v8.deserialize(v8.serialize(Buffer.alloc(0))); - - expect(warningListener).not.toHaveBeenCalled(); - - // Clean up the listener - process.removeListener("warning", warningListener); -}); - -//<#END_FILE: test-v8-deserialize-buffer.js diff --git a/test/js/node/test/parallel/v8-flag-pool-size-0.test.js b/test/js/node/test/parallel/v8-flag-pool-size-0.test.js deleted file mode 100644 index 0b32378981..0000000000 --- a/test/js/node/test/parallel/v8-flag-pool-size-0.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-v8-flag-pool-size-0.js -//#SHA1: 32e0b30d9305b4bf5509c6061176b2f87c47c66d -//----------------- -// Flags: --v8-pool-size=0 --expose-gc - -"use strict"; - -// This test doesn't require any specific assertions or expectations. -// It's primarily checking that the process doesn't crash or hang when -// running with the specified flags. - -test("V8 tasks scheduled by GC are handled on worker threads with --v8-pool-size=0", () => { - // This verifies that V8 tasks scheduled by GC are handled on worker threads if - // `--v8-pool-size=0` is given. The worker threads are managed by Node.js' - // `v8::Platform` implementation. - - // Trigger garbage collection - globalThis.gc(); - - // If we've reached this point without crashing or hanging, the test is successful - expect(true).toBe(true); -}); - -//<#END_FILE: test-v8-flag-pool-size-0.js diff --git a/test/js/node/test/parallel/v8-global-setter.test.js b/test/js/node/test/parallel/v8-global-setter.test.js deleted file mode 100644 index eb5fe888f2..0000000000 --- a/test/js/node/test/parallel/v8-global-setter.test.js +++ /dev/null @@ -1,37 +0,0 @@ -//#FILE: test-v8-global-setter.js -//#SHA1: 8711ed3c5a3491001fcf441ed56e14fcca67c8ef -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -// This test ensures v8 correctly sets a property on the global object if it -// has a setter interceptor in strict mode. -// https://github.com/nodejs/node-v0.x-archive/issues/6235 - -test("v8 global setter in strict mode", () => { - expect(() => { - require("vm").runInNewContext('"use strict"; var v = 1; v = 2'); - }).not.toThrow(); -}); - -//<#END_FILE: test-v8-global-setter.js diff --git a/test/js/node/test/parallel/v8-serialize-leak.test.js b/test/js/node/test/parallel/v8-serialize-leak.test.js deleted file mode 100644 index a8d0bb51e0..0000000000 --- a/test/js/node/test/parallel/v8-serialize-leak.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-v8-serialize-leak.js -//#SHA1: f1b10774a48610a130cac36679953f9af1ed15e1 -//----------------- -"use strict"; -// Flags: --expose-gc - -const v8 = require("v8"); - -// On IBMi, the rss memory always returns zero -if (process.platform === "os400") { - test.skip("On IBMi, the rss memory always returns zero"); -} - -test("v8.serialize should not leak memory", async () => { - const before = process.memoryUsage.rss(); - - for (let i = 0; i < 1000000; i++) { - v8.serialize(""); - } - - async function gcUntil(message, condition) { - for (let i = 0; i < 10; i++) { - global.gc(); - await new Promise(resolve => setTimeout(resolve, 100)); - if (condition()) { - return; - } - } - throw new Error(`${message} failed to be true in time`); - } - - await gcUntil("RSS should go down", () => { - const after = process.memoryUsage.rss(); - if (process.env.ASAN_OPTIONS) { - console.log(`ASan: before=${before} after=${after}`); - return after < before * 10; - } else if (process.config.variables.node_builtin_modules_path) { - console.log(`node_builtin_modules_path: before=${before} after=${after}`); - return after < before * 10; - } - console.log(`before=${before} after=${after}`); - return after < before * 10; - }); -}); - -//<#END_FILE: test-v8-serialize-leak.js diff --git a/test/js/node/test/parallel/vm-access-process-env.test.js b/test/js/node/test/parallel/vm-access-process-env.test.js deleted file mode 100644 index 4bac2e235c..0000000000 --- a/test/js/node/test/parallel/vm-access-process-env.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-vm-access-process-env.js -//#SHA1: ffc014abc0d92ea62b7c0510d69cad5e89d632af -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Tests that node does neither crash nor throw an error when accessing -// process.env when inside a VM context. -// See https://github.com/nodejs/node-v0.x-archive/issues/7511. - -const vm = require("vm"); - -test("VM context can access process.env", () => { - const context = vm.createContext({ process }); - const result = vm.runInContext('process.env["PATH"]', context); - expect(result).not.toBeUndefined(); -}); - -//<#END_FILE: test-vm-access-process-env.js diff --git a/test/js/node/test/parallel/vm-attributes-property-not-on-sandbox.test.js b/test/js/node/test/parallel/vm-attributes-property-not-on-sandbox.test.js deleted file mode 100644 index 3cfeab76a7..0000000000 --- a/test/js/node/test/parallel/vm-attributes-property-not-on-sandbox.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-vm-attributes-property-not-on-sandbox.js -//#SHA1: c864df0cb9b3ab90c8582ad86f50a8e94be92114 -//----------------- -"use strict"; -const vm = require("vm"); - -// Assert that accessor descriptors are not flattened on the sandbox. -// Issue: https://github.com/nodejs/node/issues/2734 -test("accessor descriptors are not flattened on the sandbox", () => { - const sandbox = {}; - vm.createContext(sandbox); - const code = `Object.defineProperty( - this, - 'foo', - { get: function() {return 17} } - ); - var desc = Object.getOwnPropertyDescriptor(this, 'foo');`; - - vm.runInContext(code, sandbox); - expect(typeof sandbox.desc.get).toBe("function"); -}); - -//<#END_FILE: test-vm-attributes-property-not-on-sandbox.js diff --git a/test/js/node/test/parallel/vm-context-async-script.test.js b/test/js/node/test/parallel/vm-context-async-script.test.js deleted file mode 100644 index 52d90dde39..0000000000 --- a/test/js/node/test/parallel/vm-context-async-script.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-vm-context-async-script.js -//#SHA1: 411e3a9c4ea8c062420ae8f112c62ed7c2a1a962 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const vm = require("vm"); - -test("vm.runInContext with async script", done => { - const sandbox = { setTimeout }; - const ctx = vm.createContext(sandbox); - - vm.runInContext("setTimeout(function() { x = 3; }, 0);", ctx); - - setTimeout(() => { - expect(sandbox.x).toBe(3); - expect(ctx.x).toBe(3); - done(); - }, 1); -}); - -//<#END_FILE: test-vm-context-async-script.js diff --git a/test/js/node/test/parallel/vm-context-property-forwarding.test.js b/test/js/node/test/parallel/vm-context-property-forwarding.test.js deleted file mode 100644 index ca3e22c38a..0000000000 --- a/test/js/node/test/parallel/vm-context-property-forwarding.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-vm-context-property-forwarding.js -//#SHA1: 611b4eee260bc77dfbaece761208e11a57fe226f -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const vm = require("vm"); - -test("vm context property forwarding", () => { - const sandbox = { x: 3 }; - const ctx = vm.createContext(sandbox); - - expect(vm.runInContext("x;", ctx)).toBe(3); - vm.runInContext("y = 4;", ctx); - expect(sandbox.y).toBe(4); - expect(ctx.y).toBe(4); -}); - -test("IndexedPropertyGetterCallback and IndexedPropertyDeleterCallback", () => { - const x = { - get 1() { - return 5; - }, - }; - const pd_expected = Object.getOwnPropertyDescriptor(x, 1); - const ctx2 = vm.createContext(x); - const pd_actual = Object.getOwnPropertyDescriptor(ctx2, 1); - - expect(pd_actual).toEqual(pd_expected); - expect(ctx2[1]).toBe(5); - delete ctx2[1]; - expect(ctx2[1]).toBeUndefined(); -}); - -// https://github.com/nodejs/node/issues/33806 -test("property setter error propagation", () => { - const ctx = vm.createContext(); - - Object.defineProperty(ctx, "prop", { - get() { - return undefined; - }, - set() { - throw new Error("test error"); - }, - }); - - expect(() => { - vm.runInContext("prop = 42", ctx); - }).toThrow( - expect.objectContaining({ - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-vm-context-property-forwarding.js diff --git a/test/js/node/test/parallel/vm-create-and-run-in-context.test.js b/test/js/node/test/parallel/vm-create-and-run-in-context.test.js deleted file mode 100644 index e6e450ef9f..0000000000 --- a/test/js/node/test/parallel/vm-create-and-run-in-context.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-vm-create-and-run-in-context.js -//#SHA1: 4583962cfdb3266d8b850442cec0b4cfa49bc6d2 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Flags: --expose-gc - -const vm = require("vm"); - -describe("vm.createContext and vm.runInContext", () => { - test("Run in a new empty context", () => { - const context = vm.createContext(); - const result = vm.runInContext('"passed";', context); - expect(result).toBe("passed"); - }); - - test("Create a new pre-populated context", () => { - const context = vm.createContext({ foo: "bar", thing: "lala" }); - expect(context.foo).toBe("bar"); - expect(context.thing).toBe("lala"); - }); - - test("Test updating context", () => { - const context = vm.createContext({ foo: "bar", thing: "lala" }); - vm.runInContext("var foo = 3;", context); - expect(context.foo).toBe(3); - expect(context.thing).toBe("lala"); - }); - - test("Run in contextified sandbox without referencing the context", () => { - // https://github.com/nodejs/node/issues/5768 - const sandbox = { x: 1 }; - vm.createContext(sandbox); - global.gc(); - vm.runInContext("x = 2", sandbox); - // Should not crash. - expect(sandbox.x).toBe(2); - }); -}); - -//<#END_FILE: test-vm-create-and-run-in-context.js diff --git a/test/js/node/test/parallel/vm-create-context-accessors.test.js b/test/js/node/test/parallel/vm-create-context-accessors.test.js deleted file mode 100644 index 6f55c5f5af..0000000000 --- a/test/js/node/test/parallel/vm-create-context-accessors.test.js +++ /dev/null @@ -1,54 +0,0 @@ -//#FILE: test-vm-create-context-accessors.js -//#SHA1: af7f3fb956333bd0669014a9e3c8f3f308efc68e -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const vm = require("vm"); - -test("vm.createContext with accessors", () => { - let ctx = {}; - - Object.defineProperty(ctx, "getter", { - get: function () { - return "ok"; - }, - }); - - let val; - Object.defineProperty(ctx, "setter", { - set: function (_val) { - val = _val; - }, - get: function () { - return `ok=${val}`; - }, - }); - - ctx = vm.createContext(ctx); - - const result = vm.runInContext('setter = "test";[getter,setter]', ctx); - expect(result[0]).toBe("ok"); - expect(result[1]).toBe("ok=test"); -}); - -//<#END_FILE: test-vm-create-context-accessors.js diff --git a/test/js/node/test/parallel/vm-create-context-circular-reference.test.js b/test/js/node/test/parallel/vm-create-context-circular-reference.test.js deleted file mode 100644 index a088f700bf..0000000000 --- a/test/js/node/test/parallel/vm-create-context-circular-reference.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-vm-create-context-circular-reference.js -//#SHA1: 30740bd812c7365e1f7b72a3d98c5ca91d8adbb5 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const vm = require("vm"); - -test("vm.createContext with circular reference", () => { - let sbx = {}; - sbx.window = sbx; - - sbx = vm.createContext(sbx); - - sbx.test = 123; - - expect(sbx.window.window.window.window.window.test).toBe(123); -}); - -//<#END_FILE: test-vm-create-context-circular-reference.js diff --git a/test/js/node/test/parallel/vm-cross-context.test.js b/test/js/node/test/parallel/vm-cross-context.test.js deleted file mode 100644 index 52c716d18a..0000000000 --- a/test/js/node/test/parallel/vm-cross-context.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-vm-cross-context.js -//#SHA1: c8f367f7edfa69a5b052feab2bb6c9326f16c9d3 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const vm = require("vm"); - -test("vm.runInContext should not throw when accessing console.log", () => { - const ctx = vm.createContext(global); - - // Should not throw. - expect(() => { - vm.runInContext("!function() { var x = console.log; }()", ctx); - }).not.toThrow(); -}); - -//<#END_FILE: test-vm-cross-context.js diff --git a/test/js/node/test/parallel/vm-data-property-writable.test.js b/test/js/node/test/parallel/vm-data-property-writable.test.js deleted file mode 100644 index e85965fb83..0000000000 --- a/test/js/node/test/parallel/vm-data-property-writable.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-vm-data-property-writable.js -//#SHA1: fc562132c9b5c9f17d55e08df7456dea7a1f41e3 -//----------------- -"use strict"; -// Refs: https://github.com/nodejs/node/issues/10223 - -const vm = require("vm"); - -test("vm data property writable", () => { - const context = vm.createContext({}); - - let code = ` - Object.defineProperty(this, 'foo', {value: 5}); - Object.getOwnPropertyDescriptor(this, 'foo'); - `; - - let desc = vm.runInContext(code, context); - - expect(desc.writable).toBe(false); - - // Check that interceptors work for symbols. - code = ` - const bar = Symbol('bar'); - Object.defineProperty(this, bar, {value: 6}); - Object.getOwnPropertyDescriptor(this, bar); - `; - - desc = vm.runInContext(code, context); - - expect(desc.value).toBe(6); -}); - -//<#END_FILE: test-vm-data-property-writable.js diff --git a/test/js/node/test/parallel/vm-deleting-property.test.js b/test/js/node/test/parallel/vm-deleting-property.test.js deleted file mode 100644 index ac94e9a72d..0000000000 --- a/test/js/node/test/parallel/vm-deleting-property.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-vm-deleting-property.js -//#SHA1: 20a3a3752a4d1b140cd8762c1d66c8dc734ed3fa -//----------------- -"use strict"; -// Refs: https://github.com/nodejs/node/issues/6287 - -const vm = require("vm"); - -test("deleting property in vm context", () => { - const context = vm.createContext(); - const res = vm.runInContext( - ` - this.x = 'prop'; - delete this.x; - Object.getOwnPropertyDescriptor(this, 'x'); - `, - context, - ); - - expect(res).toBeUndefined(); -}); - -//<#END_FILE: test-vm-deleting-property.js diff --git a/test/js/node/test/parallel/vm-function-declaration.test.js b/test/js/node/test/parallel/vm-function-declaration.test.js deleted file mode 100644 index 2137b50b48..0000000000 --- a/test/js/node/test/parallel/vm-function-declaration.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-vm-function-declaration.js -//#SHA1: 002bfe8201b2483628b46efd3d5e081fb17fc4b4 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const vm = require("vm"); - -test("Function declaration and expression in vm context", () => { - const o = vm.createContext({ console }); - - // Function declaration and expression should both be copied to the - // sandboxed context. - let code = "let a = function() {};\n"; - code += "function b(){}\n"; - code += "var c = function() {};\n"; - code += "var d = () => {};\n"; - code += "let e = () => {};\n"; - - // Grab the global b function as the completion value, to ensure that - // we are getting the global function, and not some other thing - code += "(function(){return this})().b;\n"; - - const res = vm.runInContext(code, o, "test"); - - expect(typeof res).toBe("function"); - expect(res.name).toBe("b"); - expect(typeof o.a).toBe("undefined"); - expect(typeof o.b).toBe("function"); - expect(typeof o.c).toBe("function"); - expect(typeof o.d).toBe("function"); - expect(typeof o.e).toBe("undefined"); - expect(res).toBe(o.b); -}); - -//<#END_FILE: test-vm-function-declaration.js diff --git a/test/js/node/test/parallel/vm-function-redefinition.test.js b/test/js/node/test/parallel/vm-function-redefinition.test.js deleted file mode 100644 index 6395cbd57e..0000000000 --- a/test/js/node/test/parallel/vm-function-redefinition.test.js +++ /dev/null @@ -1,17 +0,0 @@ -//#FILE: test-vm-function-redefinition.js -//#SHA1: afca30c05276f19448e4a2e01381c8a6fb13d544 -//----------------- -"use strict"; -// Refs: https://github.com/nodejs/node/issues/548 -const vm = require("vm"); - -test("function redefinition in vm context", () => { - const context = vm.createContext(); - - vm.runInContext("function test() { return 0; }", context); - vm.runInContext("function test() { return 1; }", context); - const result = vm.runInContext("test()", context); - expect(result).toBe(1); -}); - -//<#END_FILE: test-vm-function-redefinition.js diff --git a/test/js/node/test/parallel/vm-global-assignment.test.js b/test/js/node/test/parallel/vm-global-assignment.test.js deleted file mode 100644 index bacb3cf2d7..0000000000 --- a/test/js/node/test/parallel/vm-global-assignment.test.js +++ /dev/null @@ -1,36 +0,0 @@ -//#FILE: test-vm-global-assignment.js -//#SHA1: 54d8ce4e5d93c89a8573a3230065f4e244b198db -//----------------- -"use strict"; - -// Regression test for https://github.com/nodejs/node/issues/10806 - -const vm = require("vm"); - -describe("VM global assignment", () => { - let ctx; - let window; - const other = 123; - - beforeEach(() => { - ctx = vm.createContext({ open() {} }); - window = vm.runInContext("this", ctx); - }); - - test("window.open is not equal to other initially", () => { - expect(window.open).not.toBe(other); - }); - - test("window.open can be assigned", () => { - window.open = other; - expect(window.open).toBe(other); - }); - - test("window.open can be reassigned", () => { - window.open = other; - window.open = other; - expect(window.open).toBe(other); - }); -}); - -//<#END_FILE: test-vm-global-assignment.js diff --git a/test/js/node/test/parallel/vm-global-configurable-properties.test.js b/test/js/node/test/parallel/vm-global-configurable-properties.test.js deleted file mode 100644 index 860b260159..0000000000 --- a/test/js/node/test/parallel/vm-global-configurable-properties.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-vm-global-configurable-properties.js -//#SHA1: abaa38afe456cd1fcf98be472781d25e3918d1b5 -//----------------- -"use strict"; -// https://github.com/nodejs/node/issues/47799 - -const vm = require("vm"); - -test("VM global configurable properties", () => { - const ctx = vm.createContext(); - - const window = vm.runInContext("this", ctx); - - Object.defineProperty(window, "x", { value: "1", configurable: true }); - expect(window.x).toBe("1"); - - Object.defineProperty(window, "x", { value: "2", configurable: true }); - expect(window.x).toBe("2"); -}); - -//<#END_FILE: test-vm-global-configurable-properties.js diff --git a/test/js/node/test/parallel/vm-harmony-symbols.test.js b/test/js/node/test/parallel/vm-harmony-symbols.test.js deleted file mode 100644 index 1d8b86bdc4..0000000000 --- a/test/js/node/test/parallel/vm-harmony-symbols.test.js +++ /dev/null @@ -1,42 +0,0 @@ -//#FILE: test-vm-harmony-symbols.js -//#SHA1: 36768ab105e0bcc19dccae0fea22801068fdaedd -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const vm = require("vm"); - -test("The sandbox should have its own Symbol constructor", () => { - let sandbox = {}; - vm.runInNewContext("this.Symbol = Symbol", sandbox); - expect(typeof sandbox.Symbol).toBe("function"); - expect(sandbox.Symbol).not.toBe(Symbol); -}); - -test("Explicitly copying the Symbol constructor", () => { - let sandbox = { Symbol }; - vm.runInNewContext("this.Symbol = Symbol", sandbox); - expect(typeof sandbox.Symbol).toBe("function"); - expect(sandbox.Symbol).toBe(Symbol); -}); - -//<#END_FILE: test-vm-harmony-symbols.js diff --git a/test/js/node/test/parallel/vm-indexed-properties.test.js b/test/js/node/test/parallel/vm-indexed-properties.test.js deleted file mode 100644 index 6b9999fadf..0000000000 --- a/test/js/node/test/parallel/vm-indexed-properties.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-vm-indexed-properties.js -//#SHA1: 5938ca1da86f05ceda978b0f2d9640734d6c0ab6 -//----------------- -"use strict"; - -const vm = require("vm"); - -test("vm indexed properties", () => { - const code = `Object.defineProperty(this, 99, { - value: 20, - enumerable: true - });`; - - const sandbox = {}; - const ctx = vm.createContext(sandbox); - vm.runInContext(code, ctx); - - expect(sandbox[99]).toBe(20); -}); - -//<#END_FILE: test-vm-indexed-properties.js diff --git a/test/js/node/test/parallel/vm-low-stack-space.test.js b/test/js/node/test/parallel/vm-low-stack-space.test.js deleted file mode 100644 index e4ccd2e9b2..0000000000 --- a/test/js/node/test/parallel/vm-low-stack-space.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-vm-low-stack-space.js -//#SHA1: fffd6c9c17b9ff755e1dd126a42d6ea176282d00 -//----------------- -"use strict"; -const vm = require("vm"); - -test("vm.runInThisContext in low stack space", () => { - function a() { - try { - return a(); - } catch { - // Throw an exception as near to the recursion-based RangeError as possible. - return vm.runInThisContext("() => 42")(); - } - } - - expect(a()).toBe(42); -}); - -test("vm.runInNewContext in low stack space", () => { - function b() { - try { - return b(); - } catch { - // This writes a lot of noise to stderr, but it still works. - return vm.runInNewContext("() => 42")(); - } - } - - expect(b()).toBe(42); -}); - -//<#END_FILE: test-vm-low-stack-space.js diff --git a/test/js/node/test/parallel/vm-new-script-new-context.test.js b/test/js/node/test/parallel/vm-new-script-new-context.test.js deleted file mode 100644 index ea14f33bc6..0000000000 --- a/test/js/node/test/parallel/vm-new-script-new-context.test.js +++ /dev/null @@ -1,127 +0,0 @@ -//#FILE: test-vm-new-script-new-context.js -//#SHA1: 444a86a44203350903ab84a30de39e24a28732ec -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const { Script } = require("vm"); - -test("Script runInNewContext returns expected result", () => { - const script = new Script("'passed';"); - const result1 = script.runInNewContext(); - const result2 = script.runInNewContext(); - expect(result1).toBe("passed"); - expect(result2).toBe("passed"); -}); - -test("Script throws error when expected", () => { - const script = new Script("throw new Error('test');"); - expect(() => { - script.runInNewContext(); - }).toThrow( - expect.objectContaining({ - name: "Error", - message: expect.any(String), - }), - ); -}); - -test("Script throws ReferenceError for undefined variable", () => { - const script = new Script("foo.bar = 5;"); - expect(() => { - script.runInNewContext(); - }).toThrow( - expect.objectContaining({ - name: "ReferenceError", - message: expect.any(String), - }), - ); -}); - -test("Script does not affect global scope", () => { - global.hello = 5; - const script = new Script("hello = 2"); - script.runInNewContext(); - expect(global.hello).toBe(5); - - // Cleanup - delete global.hello; -}); - -test("Script runs in new context with provided object", () => { - global.code = "foo = 1;" + "bar = 2;" + "if (baz !== 3) throw new Error('test fail');"; - global.foo = 2; - global.obj = { foo: 0, baz: 3 }; - const script = new Script(global.code); - script.runInNewContext(global.obj); - expect(global.obj.foo).toBe(1); - expect(global.obj.bar).toBe(2); - expect(global.foo).toBe(2); - - // cleanup - delete global.code; - delete global.foo; - delete global.obj; -}); - -test("Script can modify global scope through provided function", () => { - const script = new Script("f()"); - function changeFoo() { - global.foo = 100; - } - script.runInNewContext({ f: changeFoo }); - expect(global.foo).toBe(100); - - // cleanup - delete global.foo; -}); - -test("Script modifies provided object and throws when object is not provided", () => { - const script = new Script("f.a = 2"); - const f = { a: 1 }; - script.runInNewContext({ f }); - expect(f.a).toBe(2); - - expect(() => { - script.runInNewContext(); - }).toThrow( - expect.objectContaining({ - name: "ReferenceError", - message: expect.any(String), - }), - ); -}); - -test("Script.runInNewContext throws when called with invalid this", () => { - const script = new Script(""); - expect(() => { - script.runInNewContext.call("'hello';"); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-vm-new-script-new-context.js diff --git a/test/js/node/test/parallel/vm-proxies.test.js b/test/js/node/test/parallel/vm-proxies.test.js deleted file mode 100644 index b713c7773a..0000000000 --- a/test/js/node/test/parallel/vm-proxies.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-vm-proxies.js -//#SHA1: 3119c41e6c3cc80d380c9467c2f922b2df3e5616 -//----------------- -"use strict"; - -const vm = require("vm"); - -test("Proxy object in new context", () => { - // src/node_contextify.cc filters out the Proxy object from the parent - // context. Make sure that the new context has a Proxy object of its own. - let sandbox = {}; - vm.runInNewContext("this.Proxy = Proxy", sandbox); - expect(typeof sandbox.Proxy).toBe("function"); - expect(sandbox.Proxy).not.toBe(Proxy); -}); - -test("Explicitly copied Proxy object in new context", () => { - // Unless we copy the Proxy object explicitly, of course. - const sandbox = { Proxy }; - vm.runInNewContext("this.Proxy = Proxy", sandbox); - expect(typeof sandbox.Proxy).toBe("function"); - expect(sandbox.Proxy).toBe(Proxy); -}); - -//<#END_FILE: test-vm-proxies.js diff --git a/test/js/node/test/parallel/vm-proxy-failure-cp.test.js b/test/js/node/test/parallel/vm-proxy-failure-cp.test.js deleted file mode 100644 index 05d2c1e7b7..0000000000 --- a/test/js/node/test/parallel/vm-proxy-failure-cp.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-vm-proxy-failure-CP.js -//#SHA1: d3eb5284a94f718a6ae1e07c0b30e01dad295ea9 -//----------------- -"use strict"; -const vm = require("vm"); - -// Check that we do not accidentally query attributes. -// Issue: https://github.com/nodejs/node/issues/11902 -test("vm does not accidentally query attributes", () => { - const handler = { - getOwnPropertyDescriptor: (target, prop) => { - throw new Error("whoops"); - }, - }; - const sandbox = new Proxy({ foo: "bar" }, handler); - const context = vm.createContext(sandbox); - - expect(() => { - vm.runInContext("", context); - }).not.toThrow(); -}); - -//<#END_FILE: test-vm-proxy-failure-CP.js diff --git a/test/js/node/test/parallel/vm-script-throw-in-tostring.test.js b/test/js/node/test/parallel/vm-script-throw-in-tostring.test.js deleted file mode 100644 index acd0eeb817..0000000000 --- a/test/js/node/test/parallel/vm-script-throw-in-tostring.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-vm-script-throw-in-tostring.js -//#SHA1: 16675c8942dbb81a032c117fa42c9611cda082e0 -//----------------- -"use strict"; - -const vm = require("vm"); - -test("vm.Script throws when toString throws", () => { - expect(() => { - new vm.Script({ - toString() { - throw new Error(); - }, - }); - }).toThrow( - expect.objectContaining({ - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-vm-script-throw-in-tostring.js diff --git a/test/js/node/test/parallel/vm-strict-mode.test.js b/test/js/node/test/parallel/vm-strict-mode.test.js deleted file mode 100644 index e2f06f8555..0000000000 --- a/test/js/node/test/parallel/vm-strict-mode.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-vm-strict-mode.js -//#SHA1: ab6c6c72920e9bf095b41b255872b9d0604301c7 -//----------------- -"use strict"; -// https://github.com/nodejs/node/issues/12300 - -const vm = require("vm"); - -test("vm strict mode assignment", () => { - const ctx = vm.createContext({ x: 42 }); - - // This might look as if x has not been declared, but x is defined on the - // sandbox and the assignment should not throw. - expect(() => { - vm.runInContext('"use strict"; x = 1', ctx); - }).not.toThrow(); - - expect(ctx.x).toBe(1); -}); - -//<#END_FILE: test-vm-strict-mode.js diff --git a/test/js/node/test/parallel/vm-syntax-error-message.test.js b/test/js/node/test/parallel/vm-syntax-error-message.test.js deleted file mode 100644 index 49be35047c..0000000000 --- a/test/js/node/test/parallel/vm-syntax-error-message.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-vm-syntax-error-message.js -//#SHA1: dbd3683e08ad5cf574d1824108446e9c425adf1b -//----------------- -"use strict"; - -const child_process = require("child_process"); - -test("vm syntax error message", done => { - const p = child_process.spawn(process.execPath, [ - "-e", - 'vm = require("vm");' + - "context = vm.createContext({});" + - "try { vm.runInContext(\"throw new Error('boo')\", context); } " + - "catch (e) { console.log(e.message); }", - ]); - - p.stderr.on("data", () => { - throw new Error("stderr should not receive any data"); - }); - - let output = ""; - - p.stdout.on("data", data => (output += data)); - - p.stdout.on("end", () => { - expect(output.replace(/[\r\n]+/g, "")).toBe("boo"); - done(); - }); -}); - -//<#END_FILE: test-vm-syntax-error-message.js diff --git a/test/js/node/test/parallel/webcrypto-encrypt-decrypt.test.js b/test/js/node/test/parallel/webcrypto-encrypt-decrypt.test.js deleted file mode 100644 index 560edf8eb6..0000000000 --- a/test/js/node/test/parallel/webcrypto-encrypt-decrypt.test.js +++ /dev/null @@ -1,110 +0,0 @@ -//#FILE: test-webcrypto-encrypt-decrypt.js -//#SHA1: 791cad35ebee437d2a982e6101d47daa2f775a4b -//----------------- -"use strict"; - -const { subtle } = globalThis.crypto; - -// This is only a partial test. The WebCrypto Web Platform Tests -// will provide much greater coverage. - -// Test Encrypt/Decrypt RSA-OAEP -test("Encrypt/Decrypt RSA-OAEP", async () => { - const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); - const ec = new TextEncoder(); - const { publicKey, privateKey } = await subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-384", - }, - true, - ["encrypt", "decrypt"], - ); - - const ciphertext = await subtle.encrypt( - { - name: "RSA-OAEP", - label: ec.encode("a label"), - }, - publicKey, - buf, - ); - - const plaintext = await subtle.decrypt( - { - name: "RSA-OAEP", - label: ec.encode("a label"), - }, - privateKey, - ciphertext, - ); - - expect(Buffer.from(plaintext).toString("hex")).toBe(Buffer.from(buf).toString("hex")); -}); - -// Test Encrypt/Decrypt AES-CTR -test("Encrypt/Decrypt AES-CTR", async () => { - const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); - const counter = globalThis.crypto.getRandomValues(new Uint8Array(16)); - - const key = await subtle.generateKey( - { - name: "AES-CTR", - length: 256, - }, - true, - ["encrypt", "decrypt"], - ); - - const ciphertext = await subtle.encrypt({ name: "AES-CTR", counter, length: 64 }, key, buf); - - const plaintext = await subtle.decrypt({ name: "AES-CTR", counter, length: 64 }, key, ciphertext); - - expect(Buffer.from(plaintext).toString("hex")).toBe(Buffer.from(buf).toString("hex")); -}); - -// Test Encrypt/Decrypt AES-CBC -test("Encrypt/Decrypt AES-CBC", async () => { - const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); - const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); - - const key = await subtle.generateKey( - { - name: "AES-CBC", - length: 256, - }, - true, - ["encrypt", "decrypt"], - ); - - const ciphertext = await subtle.encrypt({ name: "AES-CBC", iv }, key, buf); - - const plaintext = await subtle.decrypt({ name: "AES-CBC", iv }, key, ciphertext); - - expect(Buffer.from(plaintext).toString("hex")).toBe(Buffer.from(buf).toString("hex")); -}); - -// Test Encrypt/Decrypt AES-GCM -test("Encrypt/Decrypt AES-GCM", async () => { - const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); - const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); - - const key = await subtle.generateKey( - { - name: "AES-GCM", - length: 256, - }, - true, - ["encrypt", "decrypt"], - ); - - const ciphertext = await subtle.encrypt({ name: "AES-GCM", iv }, key, buf); - - const plaintext = await subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext); - - expect(Buffer.from(plaintext).toString("hex")).toBe(Buffer.from(buf).toString("hex")); -}); - -//<#END_FILE: test-webcrypto-encrypt-decrypt.js diff --git a/test/js/node/test/parallel/webcrypto-getrandomvalues.test.js b/test/js/node/test/parallel/webcrypto-getrandomvalues.test.js deleted file mode 100644 index 3ec9961927..0000000000 --- a/test/js/node/test/parallel/webcrypto-getrandomvalues.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-webcrypto-getRandomValues.js -//#SHA1: d5d696eb0e68968d2411efa24e6e4c9bd46a1678 -//----------------- -"use strict"; - -if (!globalThis.crypto) { - it("skips test when crypto is missing", () => { - console.log("missing crypto"); - return; - }); -} else { - describe("webcrypto getRandomValues", () => { - const { getRandomValues } = globalThis.crypto; - - it("throws ERR_INVALID_THIS when called without proper this", () => { - expect(() => getRandomValues(new Uint8Array())).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - message: expect.any(String), - }), - ); - }); - }); -} - -//<#END_FILE: test-webcrypto-getRandomValues.js diff --git a/test/js/node/test/parallel/webstream-string-tag.test.js b/test/js/node/test/parallel/webstream-string-tag.test.js deleted file mode 100644 index f8791dbe76..0000000000 --- a/test/js/node/test/parallel/webstream-string-tag.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-webstream-string-tag.js -//#SHA1: 53f13b84555ff37eeee23ca4552540d76b88c2ad -//----------------- -"use strict"; - -test("Web Stream classes have correct Symbol.toStringTag", () => { - const classesToBeTested = [ - WritableStream, - WritableStreamDefaultWriter, - WritableStreamDefaultController, - ReadableStream, - ReadableStreamBYOBRequest, - ReadableStreamDefaultReader, - ReadableStreamBYOBReader, - ReadableStreamDefaultController, - ReadableByteStreamController, - ByteLengthQueuingStrategy, - CountQueuingStrategy, - TransformStream, - TransformStreamDefaultController, - ]; - - classesToBeTested.forEach(cls => { - expect(cls.prototype[Symbol.toStringTag]).toBe(cls.name); - expect(Object.getOwnPropertyDescriptor(cls.prototype, Symbol.toStringTag)).toEqual({ - configurable: true, - enumerable: false, - value: cls.name, - writable: false, - }); - }); -}); - -//<#END_FILE: test-webstream-string-tag.js diff --git a/test/js/node/test/parallel/whatwg-encoding-custom-api-basics.test.js b/test/js/node/test/parallel/whatwg-encoding-custom-api-basics.test.js deleted file mode 100644 index b686f76961..0000000000 --- a/test/js/node/test/parallel/whatwg-encoding-custom-api-basics.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-whatwg-encoding-custom-api-basics.js -//#SHA1: 8181a892b0d1e5885b29b1165631875e587b7700 -//----------------- -"use strict"; - -// From: https://github.com/w3c/web-platform-tests/blob/master/encoding/api-basics.html -// This is the part that can be run without ICU - -function testDecodeSample(encoding, string, bytes) { - expect(new TextDecoder(encoding).decode(new Uint8Array(bytes))).toBe(string); - expect(new TextDecoder(encoding).decode(new Uint8Array(bytes).buffer)).toBe(string); -} - -// `z` (ASCII U+007A), cent (Latin-1 U+00A2), CJK water (BMP U+6C34), -// G-Clef (non-BMP U+1D11E), PUA (BMP U+F8FF), PUA (non-BMP U+10FFFD) -// byte-swapped BOM (non-character U+FFFE) -const sample = "z\xA2\u6C34\uD834\uDD1E\uF8FF\uDBFF\uDFFD\uFFFE"; - -test("utf-8 encoding and decoding", () => { - const encoding = "utf-8"; - const string = sample; - const bytes = [ - 0x7a, 0xc2, 0xa2, 0xe6, 0xb0, 0xb4, 0xf0, 0x9d, 0x84, 0x9e, 0xef, 0xa3, 0xbf, 0xf4, 0x8f, 0xbf, 0xbd, 0xef, 0xbf, - 0xbe, - ]; - const encoded = new TextEncoder().encode(string); - expect([...encoded]).toEqual(bytes); - expect(new TextDecoder(encoding).decode(new Uint8Array(bytes))).toBe(string); - expect(new TextDecoder(encoding).decode(new Uint8Array(bytes).buffer)).toBe(string); -}); - -test("utf-16le decoding", () => { - testDecodeSample( - "utf-16le", - sample, - [0x7a, 0x00, 0xa2, 0x00, 0x34, 0x6c, 0x34, 0xd8, 0x1e, 0xdd, 0xff, 0xf8, 0xff, 0xdb, 0xfd, 0xdf, 0xfe, 0xff], - ); -}); - -test("utf-16 decoding", () => { - testDecodeSample( - "utf-16", - sample, - [0x7a, 0x00, 0xa2, 0x00, 0x34, 0x6c, 0x34, 0xd8, 0x1e, 0xdd, 0xff, 0xf8, 0xff, 0xdb, 0xfd, 0xdf, 0xfe, 0xff], - ); -}); - -//<#END_FILE: test-whatwg-encoding-custom-api-basics.js diff --git a/test/js/node/test/parallel/whatwg-encoding-custom-fatal-streaming.test.js b/test/js/node/test/parallel/whatwg-encoding-custom-fatal-streaming.test.js deleted file mode 100644 index ce4cd6051f..0000000000 --- a/test/js/node/test/parallel/whatwg-encoding-custom-fatal-streaming.test.js +++ /dev/null @@ -1,61 +0,0 @@ -//#FILE: test-whatwg-encoding-custom-fatal-streaming.js -//#SHA1: 325f0a1e055a1756532d2818d4e563dfbddb928b -//----------------- -"use strict"; - -// From: https://github.com/w3c/web-platform-tests/blob/d74324b53c/encoding/textdecoder-fatal-streaming.html -// With the twist that we specifically test for Node.js error codes - -if (!globalThis.Intl) { - test.skip("missing Intl", () => {}); -} else { - test("TextDecoder with fatal option and invalid sequences", () => { - [ - { encoding: "utf-8", sequence: [0xc0] }, - { encoding: "utf-16le", sequence: [0x00] }, - { encoding: "utf-16be", sequence: [0x00] }, - ].forEach(testCase => { - const data = new Uint8Array(testCase.sequence); - expect(() => { - const decoder = new TextDecoder(testCase.encoding, { fatal: true }); - decoder.decode(data); - }).toThrow( - expect.objectContaining({ - code: "ERR_ENCODING_INVALID_ENCODED_DATA", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - }); - - test("TextDecoder with utf-16le and streaming", () => { - const decoder = new TextDecoder("utf-16le", { fatal: true }); - const odd = new Uint8Array([0x00]); - const even = new Uint8Array([0x00, 0x00]); - - expect(() => { - decoder.decode(even, { stream: true }); - decoder.decode(odd); - }).toThrow( - expect.objectContaining({ - code: "ERR_ENCODING_INVALID_ENCODED_DATA", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => { - decoder.decode(odd, { stream: true }); - decoder.decode(even); - }).toThrow( - expect.objectContaining({ - code: "ERR_ENCODING_INVALID_ENCODED_DATA", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -} - -//<#END_FILE: test-whatwg-encoding-custom-fatal-streaming.js diff --git a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-fatal.test.js b/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-fatal.test.js deleted file mode 100644 index 14aa0d31c3..0000000000 --- a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-fatal.test.js +++ /dev/null @@ -1,68 +0,0 @@ -//#FILE: test-whatwg-encoding-custom-textdecoder-fatal.js -//#SHA1: e2b44e43b78b053687ab4a8dc5de7557f6637643 -//----------------- -"use strict"; - -// From: https://github.com/w3c/web-platform-tests/blob/39a67e2fff/encoding/textdecoder-fatal.html -// With the twist that we specifically test for Node.js error codes - -if (!globalThis.Intl) { - test.skip("missing Intl"); -} - -const bad = [ - { encoding: "utf-8", input: [0xff], name: "invalid code" }, - { encoding: "utf-8", input: [0xc0], name: "ends early" }, - { encoding: "utf-8", input: [0xe0], name: "ends early 2" }, - { encoding: "utf-8", input: [0xc0, 0x00], name: "invalid trail" }, - { encoding: "utf-8", input: [0xc0, 0xc0], name: "invalid trail 2" }, - { encoding: "utf-8", input: [0xe0, 0x00], name: "invalid trail 3" }, - { encoding: "utf-8", input: [0xe0, 0xc0], name: "invalid trail 4" }, - { encoding: "utf-8", input: [0xe0, 0x80, 0x00], name: "invalid trail 5" }, - { encoding: "utf-8", input: [0xe0, 0x80, 0xc0], name: "invalid trail 6" }, - { encoding: "utf-8", input: [0xfc, 0x80, 0x80, 0x80, 0x80, 0x80], name: "> 0x10FFFF" }, - { encoding: "utf-8", input: [0xfe, 0x80, 0x80, 0x80, 0x80, 0x80], name: "obsolete lead byte" }, - // Overlong encodings - { encoding: "utf-8", input: [0xc0, 0x80], name: "overlong U+0000 - 2 bytes" }, - { encoding: "utf-8", input: [0xe0, 0x80, 0x80], name: "overlong U+0000 - 3 bytes" }, - { encoding: "utf-8", input: [0xf0, 0x80, 0x80, 0x80], name: "overlong U+0000 - 4 bytes" }, - { encoding: "utf-8", input: [0xf8, 0x80, 0x80, 0x80, 0x80], name: "overlong U+0000 - 5 bytes" }, - { encoding: "utf-8", input: [0xfc, 0x80, 0x80, 0x80, 0x80, 0x80], name: "overlong U+0000 - 6 bytes" }, - { encoding: "utf-8", input: [0xc1, 0xbf], name: "overlong U+007F - 2 bytes" }, - { encoding: "utf-8", input: [0xe0, 0x81, 0xbf], name: "overlong U+007F - 3 bytes" }, - { encoding: "utf-8", input: [0xf0, 0x80, 0x81, 0xbf], name: "overlong U+007F - 4 bytes" }, - { encoding: "utf-8", input: [0xf8, 0x80, 0x80, 0x81, 0xbf], name: "overlong U+007F - 5 bytes" }, - { encoding: "utf-8", input: [0xfc, 0x80, 0x80, 0x80, 0x81, 0xbf], name: "overlong U+007F - 6 bytes" }, - { encoding: "utf-8", input: [0xe0, 0x9f, 0xbf], name: "overlong U+07FF - 3 bytes" }, - { encoding: "utf-8", input: [0xf0, 0x80, 0x9f, 0xbf], name: "overlong U+07FF - 4 bytes" }, - { encoding: "utf-8", input: [0xf8, 0x80, 0x80, 0x9f, 0xbf], name: "overlong U+07FF - 5 bytes" }, - { encoding: "utf-8", input: [0xfc, 0x80, 0x80, 0x80, 0x9f, 0xbf], name: "overlong U+07FF - 6 bytes" }, - { encoding: "utf-8", input: [0xf0, 0x8f, 0xbf, 0xbf], name: "overlong U+FFFF - 4 bytes" }, - { encoding: "utf-8", input: [0xf8, 0x80, 0x8f, 0xbf, 0xbf], name: "overlong U+FFFF - 5 bytes" }, - { encoding: "utf-8", input: [0xfc, 0x80, 0x80, 0x8f, 0xbf, 0xbf], name: "overlong U+FFFF - 6 bytes" }, - { encoding: "utf-8", input: [0xf8, 0x84, 0x8f, 0xbf, 0xbf], name: "overlong U+10FFFF - 5 bytes" }, - { encoding: "utf-8", input: [0xfc, 0x80, 0x84, 0x8f, 0xbf, 0xbf], name: "overlong U+10FFFF - 6 bytes" }, - // UTF-16 surrogates encoded as code points in UTF-8 - { encoding: "utf-8", input: [0xed, 0xa0, 0x80], name: "lead surrogate" }, - { encoding: "utf-8", input: [0xed, 0xb0, 0x80], name: "trail surrogate" }, - { encoding: "utf-8", input: [0xed, 0xa0, 0x80, 0xed, 0xb0, 0x80], name: "surrogate pair" }, - { encoding: "utf-16le", input: [0x00], name: "truncated code unit" }, - // Mismatched UTF-16 surrogates are exercised in utf16-surrogates.html - // FIXME: Add legacy encoding cases -]; - -bad.forEach(t => { - test(`TextDecoder fatal error: ${t.name}`, () => { - expect(() => { - new TextDecoder(t.encoding, { fatal: true }).decode(new Uint8Array(t.input)); - }).toThrow( - expect.objectContaining({ - code: "ERR_ENCODING_INVALID_ENCODED_DATA", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-whatwg-encoding-custom-textdecoder-fatal.js diff --git a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-ignorebom.test.js b/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-ignorebom.test.js deleted file mode 100644 index 43a7a005ce..0000000000 --- a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-ignorebom.test.js +++ /dev/null @@ -1,35 +0,0 @@ -//#FILE: test-whatwg-encoding-custom-textdecoder-ignorebom.js -//#SHA1: 8a119026559b0341b524a97eaaf87e948a502dd6 -//----------------- -"use strict"; - -// From: https://github.com/w3c/web-platform-tests/blob/7f567fa29c/encoding/textdecoder-ignorebom.html -// This is the part that can be run without ICU - -const cases = [ - { - encoding: "utf-8", - bytes: [0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63], - }, - { - encoding: "utf-16le", - bytes: [0xff, 0xfe, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00], - }, -]; - -cases.forEach(testCase => { - test(`TextDecoder with ${testCase.encoding} encoding`, () => { - const BOM = "\uFEFF"; - let decoder = new TextDecoder(testCase.encoding, { ignoreBOM: true }); - const bytes = new Uint8Array(testCase.bytes); - expect(decoder.decode(bytes)).toBe(`${BOM}abc`); - - decoder = new TextDecoder(testCase.encoding, { ignoreBOM: false }); - expect(decoder.decode(bytes)).toBe("abc"); - - decoder = new TextDecoder(testCase.encoding); - expect(decoder.decode(bytes)).toBe("abc"); - }); -}); - -//<#END_FILE: test-whatwg-encoding-custom-textdecoder-ignorebom.js diff --git a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-invalid-arg.test.js b/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-invalid-arg.test.js deleted file mode 100644 index 6389f03fbd..0000000000 --- a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-invalid-arg.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-whatwg-encoding-custom-textdecoder-invalid-arg.js -//#SHA1: eaf5b5a330366828645f6a0be0dbd859cf9f1bda -//----------------- -"use strict"; - -// This tests that ERR_INVALID_ARG_TYPE are thrown when -// invalid arguments are passed to TextDecoder. - -test("TextDecoder throws ERR_INVALID_ARG_TYPE for invalid input types", () => { - const notArrayBufferViewExamples = [false, {}, 1, "", new Error()]; - notArrayBufferViewExamples.forEach(invalidInputType => { - expect(() => { - new TextDecoder(undefined, null).decode(invalidInputType); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-whatwg-encoding-custom-textdecoder-invalid-arg.js diff --git a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-streaming.test.js b/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-streaming.test.js deleted file mode 100644 index af2ab322ed..0000000000 --- a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-streaming.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-whatwg-encoding-custom-textdecoder-streaming.js -//#SHA1: d98fc14ce0348f22b3bae160b4e1e4882ce75749 -//----------------- -"use strict"; - -// From: https://github.com/w3c/web-platform-tests/blob/fa9436d12c/encoding/textdecoder-streaming.html -// This is the part that can be run without ICU - -const string = "\x00123ABCabc\x80\xFF\u0100\u1000\uFFFD\uD800\uDC00\uDBFF\uDFFF"; -const octets = { - "utf-8": [ - 0x00, 0x31, 0x32, 0x33, 0x41, 0x42, 0x43, 0x61, 0x62, 0x63, 0xc2, 0x80, 0xc3, 0xbf, 0xc4, 0x80, 0xe1, 0x80, 0x80, - 0xef, 0xbf, 0xbd, 0xf0, 0x90, 0x80, 0x80, 0xf4, 0x8f, 0xbf, 0xbf, - ], - "utf-16le": [ - 0x00, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, - 0x00, 0x80, 0x00, 0xff, 0x00, 0x00, 0x01, 0x00, 0x10, 0xfd, 0xff, 0x00, 0xd8, 0x00, 0xdc, 0xff, 0xdb, 0xff, 0xdf, - ], -}; - -Object.keys(octets).forEach(encoding => { - describe(`TextDecoder streaming for ${encoding}`, () => { - for (let len = 1; len <= 5; ++len) { - it(`should correctly decode with chunk size ${len}`, () => { - const encoded = octets[encoding]; - const decoder = new TextDecoder(encoding); - let out = ""; - for (let i = 0; i < encoded.length; i += len) { - const sub = []; - for (let j = i; j < encoded.length && j < i + len; ++j) sub.push(encoded[j]); - out += decoder.decode(new Uint8Array(sub), { stream: true }); - } - out += decoder.decode(); - expect(out).toBe(string); - }); - } - }); -}); - -//<#END_FILE: test-whatwg-encoding-custom-textdecoder-streaming.js diff --git a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-utf16-surrogates.test.js b/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-utf16-surrogates.test.js deleted file mode 100644 index 22c5f5f5da..0000000000 --- a/test/js/node/test/parallel/whatwg-encoding-custom-textdecoder-utf16-surrogates.test.js +++ /dev/null @@ -1,60 +0,0 @@ -//#FILE: test-whatwg-encoding-custom-textdecoder-utf16-surrogates.js -//#SHA1: 0b74fafedb0961a831dff991c01aba65b15efb80 -//----------------- -"use strict"; - -// From: https://github.com/w3c/web-platform-tests/blob/39a67e2fff/encoding/textdecoder-utf16-surrogates.html -// With the twist that we specifically test for Node.js error codes - -if (!globalThis.Intl) { - test.skip("missing Intl"); -} - -const bad = [ - { - encoding: "utf-16le", - input: [0x00, 0xd8], - expected: "\uFFFD", - name: "lone surrogate lead", - }, - { - encoding: "utf-16le", - input: [0x00, 0xdc], - expected: "\uFFFD", - name: "lone surrogate trail", - }, - { - encoding: "utf-16le", - input: [0x00, 0xd8, 0x00, 0x00], - expected: "\uFFFD\u0000", - name: "unmatched surrogate lead", - }, - { - encoding: "utf-16le", - input: [0x00, 0xdc, 0x00, 0x00], - expected: "\uFFFD\u0000", - name: "unmatched surrogate trail", - }, - { - encoding: "utf-16le", - input: [0x00, 0xdc, 0x00, 0xd8], - expected: "\uFFFD\uFFFD", - name: "swapped surrogate pair", - }, -]; - -bad.forEach(t => { - test(`TextDecoder with fatal option throws for ${t.name}`, () => { - expect(() => { - new TextDecoder(t.encoding, { fatal: true }).decode(new Uint8Array(t.input)); - }).toThrow( - expect.objectContaining({ - code: "ERR_ENCODING_INVALID_ENCODED_DATA", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-whatwg-encoding-custom-textdecoder-utf16-surrogates.js diff --git a/test/js/node/test/parallel/whatwg-events-add-event-listener-options-passive.test.js b/test/js/node/test/parallel/whatwg-events-add-event-listener-options-passive.test.js deleted file mode 100644 index a918709441..0000000000 --- a/test/js/node/test/parallel/whatwg-events-add-event-listener-options-passive.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-whatwg-events-add-event-listener-options-passive.js -//#SHA1: e3c00da24b307d0e8466611bee33f70db48ad39c -//----------------- -"use strict"; - -// Manually converted from https://github.com/web-platform-tests/wpt/blob/master/dom/events/AddEventListenerOptions-passive.html -// in order to define the `document` ourselves - -test("AddEventListener options passive", () => { - const document = new EventTarget(); - let supportsPassive = false; - const query_options = { - get passive() { - supportsPassive = true; - return false; - }, - get dummy() { - throw new Error("dummy value getter invoked"); - }, - }; - - document.addEventListener("test_event", null, query_options); - expect(supportsPassive).toBe(true); - - supportsPassive = false; - document.removeEventListener("test_event", null, query_options); - expect(supportsPassive).toBe(false); -}); - -test("testPassiveValue", () => { - function testPassiveValue(optionsValue, expectedDefaultPrevented) { - const document = new EventTarget(); - let defaultPrevented; - function handler(e) { - if (e.defaultPrevented) { - throw new Error("Event prematurely marked defaultPrevented"); - } - e.preventDefault(); - defaultPrevented = e.defaultPrevented; - } - document.addEventListener("test", handler, optionsValue); - // TODO the WHATWG test is more extensive here and tests dispatching on - // document.body, if we ever support getParent we should amend this - const ev = new Event("test", { bubbles: true, cancelable: true }); - const uncanceled = document.dispatchEvent(ev); - - expect(defaultPrevented).toBe(expectedDefaultPrevented); - expect(uncanceled).toBe(!expectedDefaultPrevented); - - document.removeEventListener("test", handler, optionsValue); - } - testPassiveValue(undefined, true); - testPassiveValue({}, true); - testPassiveValue({ passive: false }, true); - - // TODO: passive listeners is still broken - // testPassiveValue({ passive: 1 }, false); - // testPassiveValue({ passive: true }, false); - // testPassiveValue({ passive: 0 }, true); -}); - -//<#END_FILE: test-whatwg-events-add-event-listener-options-passive.js diff --git a/test/js/node/test/parallel/whatwg-events-add-event-listener-options-signal.test.js b/test/js/node/test/parallel/whatwg-events-add-event-listener-options-signal.test.js deleted file mode 100644 index db5c7ef849..0000000000 --- a/test/js/node/test/parallel/whatwg-events-add-event-listener-options-signal.test.js +++ /dev/null @@ -1,176 +0,0 @@ -//#FILE: test-whatwg-events-add-event-listener-options-signal.js -//#SHA1: 2282c25dbc2f2c8bec3b2b97e0a68f3073c75c91 -//----------------- -"use strict"; - -// Manually ported from: wpt@dom/events/AddEventListenerOptions-signal.any.js - -test("Passing an AbortSignal to addEventListener does not prevent removeEventListener", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - et.addEventListener("test", handler, { signal: controller.signal }); - et.dispatchEvent(new Event("test")); - expect(count).toBe(1); - et.dispatchEvent(new Event("test")); - expect(count).toBe(2); - controller.abort(); - et.dispatchEvent(new Event("test")); - expect(count).toBe(2); - // See: https://github.com/nodejs/node/pull/37696 , adding an event listener - // should always return undefined. - expect(et.addEventListener("test", handler, { signal: controller.signal })).toBeUndefined(); - et.dispatchEvent(new Event("test")); - expect(count).toBe(2); -}); - -test("Passing an AbortSignal to addEventListener works with the once flag", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - et.addEventListener("test", handler, { signal: controller.signal }); - et.removeEventListener("test", handler); - et.dispatchEvent(new Event("test")); - expect(count).toBe(0); -}); - -test("Removing a once listener works with a passed signal", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - const options = { signal: controller.signal, once: true }; - et.addEventListener("test", handler, options); - controller.abort(); - et.dispatchEvent(new Event("test")); - expect(count).toBe(0); -}); - -test("Removing a once listener with options works", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - const options = { signal: controller.signal, once: true }; - et.addEventListener("test", handler, options); - et.removeEventListener("test", handler); - et.dispatchEvent(new Event("test")); - expect(count).toBe(0); -}); - -test("Passing an AbortSignal to multiple listeners", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - const options = { signal: controller.signal, once: true }; - et.addEventListener("first", handler, options); - et.addEventListener("second", handler, options); - controller.abort(); - et.dispatchEvent(new Event("first")); - et.dispatchEvent(new Event("second")); - expect(count).toBe(0); -}); - -test("Passing an AbortSignal to addEventListener works with the capture flag", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - const options = { signal: controller.signal, capture: true }; - et.addEventListener("test", handler, options); - controller.abort(); - et.dispatchEvent(new Event("test")); - expect(count).toBe(0); -}); - -test("Aborting from a listener does not call future listeners", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - const options = { signal: controller.signal }; - et.addEventListener( - "test", - () => { - controller.abort(); - }, - options, - ); - et.addEventListener("test", handler, options); - et.dispatchEvent(new Event("test")); - expect(count).toBe(0); -}); - -test("Adding then aborting a listener in another listener does not call it", () => { - let count = 0; - function handler() { - count++; - } - const et = new EventTarget(); - const controller = new AbortController(); - et.addEventListener( - "test", - () => { - et.addEventListener("test", handler, { signal: controller.signal }); - controller.abort(); - }, - { signal: controller.signal }, - ); - et.dispatchEvent(new Event("test")); - expect(count).toBe(0); -}); - -test("Aborting from a nested listener should remove it", () => { - const et = new EventTarget(); - const ac = new AbortController(); - let count = 0; - et.addEventListener( - "foo", - () => { - et.addEventListener( - "foo", - () => { - count++; - if (count > 5) ac.abort(); - et.dispatchEvent(new Event("foo")); - }, - { signal: ac.signal }, - ); - et.dispatchEvent(new Event("foo")); - }, - { once: true }, - ); - et.dispatchEvent(new Event("foo")); - expect(count).toBe(6); -}); - -test("Invalid signal values throw TypeError", () => { - const et = new EventTarget(); - [1, 1n, {}, [], null, true, "hi", Symbol(), () => {}].forEach(signal => { - expect(() => et.addEventListener("foo", () => {}, { signal })).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-whatwg-events-add-event-listener-options-signal.js diff --git a/test/js/node/test/parallel/whatwg-events-event-constructors.test.js b/test/js/node/test/parallel/whatwg-events-event-constructors.test.js deleted file mode 100644 index 8f08b26d1b..0000000000 --- a/test/js/node/test/parallel/whatwg-events-event-constructors.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-whatwg-events-event-constructors.js -//#SHA1: cf82cf4c0bfbf8bd7cdc9e9328587c2a1266cad8 -//----------------- -"use strict"; - -// Source: https://github.com/web-platform-tests/wpt/blob/6cef1d2087d6a07d7cc6cee8cf207eec92e27c5f/dom/events/Event-constructors.any.js#L91-L112 -test("Event constructor with getter options", () => { - const called = []; - const ev = new Event("Xx", { - get cancelable() { - called.push("cancelable"); - return false; - }, - get bubbles() { - called.push("bubbles"); - return true; - }, - get sweet() { - called.push("sweet"); - return "x"; - }, - }); - - expect(called).toEqual(["bubbles", "cancelable"]); - expect(ev.type).toBe("Xx"); - expect(ev.bubbles).toBe(true); - expect(ev.cancelable).toBe(false); - expect(ev.sweet).toBeUndefined(); -}); - -//<#END_FILE: test-whatwg-events-event-constructors.js diff --git a/test/js/node/test/parallel/whatwg-events-eventtarget-this-of-listener.test.js b/test/js/node/test/parallel/whatwg-events-eventtarget-this-of-listener.test.js deleted file mode 100644 index 9de2361e15..0000000000 --- a/test/js/node/test/parallel/whatwg-events-eventtarget-this-of-listener.test.js +++ /dev/null @@ -1,120 +0,0 @@ -//#FILE: test-whatwg-events-eventtarget-this-of-listener.js -//#SHA1: 8325e99e2f04d0fbf14abd12f002da81e4a6c338 -//----------------- -"use strict"; - -// Manually ported from: https://github.com/web-platform-tests/wpt/blob/6cef1d2087d6a07d7cc6cee8cf207eec92e27c5f/dom/events/EventTarget-this-of-listener.html - -// Mock document -const document = { - createElement: () => new EventTarget(), - createTextNode: () => new EventTarget(), - createDocumentFragment: () => new EventTarget(), - createComment: () => new EventTarget(), - createProcessingInstruction: () => new EventTarget(), -}; - -test("the this value inside the event listener callback should be the node", () => { - const nodes = [ - document.createElement("p"), - document.createTextNode("some text"), - document.createDocumentFragment(), - document.createComment("a comment"), - document.createProcessingInstruction("target", "data"), - ]; - - let callCount = 0; - for (const node of nodes) { - node.addEventListener("someevent", function () { - ++callCount; - expect(this).toBe(node); - }); - - node.dispatchEvent(new Event("someevent")); - } - - expect(callCount).toBe(nodes.length); -}); - -test("addEventListener should not require handleEvent to be defined on object listeners", () => { - const nodes = [ - document.createElement("p"), - document.createTextNode("some text"), - document.createDocumentFragment(), - document.createComment("a comment"), - document.createProcessingInstruction("target", "data"), - ]; - - let callCount = 0; - for (const node of nodes) { - const handler = {}; - - node.addEventListener("someevent", handler); - handler.handleEvent = function () { - ++callCount; - expect(this).toBe(handler); - }; - - node.dispatchEvent(new Event("someevent")); - } - - expect(callCount).toBe(nodes.length); -}); - -test("handleEvent properties added to a function before addEventListener are not reached", () => { - const nodes = [ - document.createElement("p"), - document.createTextNode("some text"), - document.createDocumentFragment(), - document.createComment("a comment"), - document.createProcessingInstruction("target", "data"), - ]; - - let callCount = 0; - for (const node of nodes) { - function handler() { - ++callCount; - expect(this).toBe(node); - } - - handler.handleEvent = () => { - throw new Error("should not call the handleEvent method on a function"); - }; - - node.addEventListener("someevent", handler); - - node.dispatchEvent(new Event("someevent")); - } - - expect(callCount).toBe(nodes.length); -}); - -test("handleEvent properties added to a function after addEventListener are not reached", () => { - const nodes = [ - document.createElement("p"), - document.createTextNode("some text"), - document.createDocumentFragment(), - document.createComment("a comment"), - document.createProcessingInstruction("target", "data"), - ]; - - let callCount = 0; - for (const node of nodes) { - function handler() { - ++callCount; - expect(this).toBe(node); - } - - node.addEventListener("someevent", handler); - - handler.handleEvent = () => { - throw new Error("should not call the handleEvent method on a function"); - }; - - node.dispatchEvent(new Event("someevent")); - } - - expect(callCount).toBe(nodes.length); -}); - -//<#END_FILE: test-whatwg-events-eventtarget-this-of-listener.js diff --git a/test/js/node/test/parallel/whatwg-url-canparse.test.js b/test/js/node/test/parallel/whatwg-url-canparse.test.js deleted file mode 100644 index e8f7099fbe..0000000000 --- a/test/js/node/test/parallel/whatwg-url-canparse.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-whatwg-url-canparse.js -//#SHA1: 23d2ea01c951bea491747284dca1abaa17596ff1 -//----------------- -"use strict"; - -const { URL } = require("url"); - -// Note: We're not using internal bindings as per your instructions -// Instead, we'll mock the canParse function - -// Mock the canParse function -const canParse = jest.fn((url, base) => { - try { - new URL(url, base); - return true; - } catch { - return false; - } -}); - -describe("URL.canParse", () => { - test("should not throw when called without a base string", () => { - expect(() => URL.canParse("https://example.org")).not.toThrow(); - expect(URL.canParse("https://example.org")).toBe(true); - expect(canParse("https://example.org")).toBe(true); - }); - - test("should correctly parse URL with base", () => { - // This for-loop is used to test V8 Fast API optimizations - for (let i = 0; i < 100000; i++) { - // This example is used because only parsing the first parameter - // results in an invalid URL. They have to be used together to - // produce truthy value. - expect(URL.canParse("/", "http://n")).toBe(true); - } - }); -}); - -//<#END_FILE: test-whatwg-url-canparse.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-deepequal.test.js b/test/js/node/test/parallel/whatwg-url-custom-deepequal.test.js deleted file mode 100644 index f3bc0a35f7..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-deepequal.test.js +++ /dev/null @@ -1,17 +0,0 @@ -//#FILE: test-whatwg-url-custom-deepequal.js -//#SHA1: 57a28f56bb87a00fe2433fabebd4a85cb2da39d0 -//----------------- -"use strict"; -// This tests that the internal flags in URL objects are consistent, as manifest -// through assert libraries. -// See https://github.com/nodejs/node/issues/24211 - -// Tests below are not from WPT. - -test("URL objects are deeply equal", () => { - expect(new URL("./foo", "https://example.com/")).toEqual(new URL("https://example.com/foo")); - - expect(new URL("./foo", "https://user:pass@example.com/")).toEqual(new URL("https://user:pass@example.com/foo")); -}); - -//<#END_FILE: test-whatwg-url-custom-deepequal.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-href-side-effect.test.js b/test/js/node/test/parallel/whatwg-url-custom-href-side-effect.test.js deleted file mode 100644 index e535f59ad2..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-href-side-effect.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-whatwg-url-custom-href-side-effect.js -//#SHA1: c2abb976ed209d25f38bb1ff1e7d8c2110ee51d4 -//----------------- -"use strict"; - -// Tests below are not from WPT. - -test("URL href assignment side effect", () => { - const ref = new URL("http://example.com/path"); - const url = new URL("http://example.com/path"); - - expect(() => { - url.href = ""; - }).toThrow( - expect.objectContaining({ - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(url).toEqual(ref); -}); - -//<#END_FILE: test-whatwg-url-custom-href-side-effect.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-searchparams-entries.test.js b/test/js/node/test/parallel/whatwg-url-custom-searchparams-entries.test.js deleted file mode 100644 index 8d7bd72648..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-searchparams-entries.test.js +++ /dev/null @@ -1,62 +0,0 @@ -//#FILE: test-whatwg-url-custom-searchparams-entries.js -//#SHA1: 4ba98b18a2f44b46ac4e6e0ee5179e97083100be -//----------------- -"use strict"; - -// Tests below are not from WPT. -test("URLSearchParams entries", () => { - const params = new URLSearchParams("a=b&c=d"); - const entries = params.entries(); - - expect(typeof entries[Symbol.iterator]).toBe("function"); - expect(entries[Symbol.iterator]()).toBe(entries); - - expect(entries.next()).toEqual({ - value: ["a", "b"], - done: false, - }); - - expect(entries.next()).toEqual({ - value: ["c", "d"], - done: false, - }); - - expect(entries.next()).toEqual({ - value: undefined, - done: true, - }); - - expect(entries.next()).toEqual({ - value: undefined, - done: true, - }); -}); - -test("entries.next() throws with invalid this", () => { - const params = new URLSearchParams("a=b&c=d"); - const entries = params.entries(); - - expect(() => { - entries.next.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -test("params.entries() throws with invalid this", () => { - expect(() => { - URLSearchParams.prototype.entries.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-whatwg-url-custom-searchparams-entries.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-searchparams-foreach.test.js b/test/js/node/test/parallel/whatwg-url-custom-searchparams-foreach.test.js deleted file mode 100644 index 233a679a00..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-searchparams-foreach.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-whatwg-url-custom-searchparams-foreach.js -//#SHA1: affe74306c7fdeb688aadc771c4d7d5b769fc236 -//----------------- -"use strict"; - -// Tests below are not from WPT. - -test('URLSearchParams.forEach called with invalid "this"', () => { - const params = new URLSearchParams(); - expect(() => { - params.forEach.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-whatwg-url-custom-searchparams-foreach.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-searchparams-keys.test.js b/test/js/node/test/parallel/whatwg-url-custom-searchparams-keys.test.js deleted file mode 100644 index 0b4a13ea30..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-searchparams-keys.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-whatwg-url-custom-searchparams-keys.js -//#SHA1: 06abe929cfe842fcdd80b44cee8a0092358e5fdf -//----------------- -"use strict"; - -// Tests below are not from WPT. - -describe("URLSearchParams keys", () => { - let params; - let keys; - - beforeEach(() => { - params = new URLSearchParams("a=b&c=d"); - keys = params.keys(); - }); - - test("keys iterator is a function and returns self", () => { - expect(typeof keys[Symbol.iterator]).toBe("function"); - expect(keys[Symbol.iterator]()).toBe(keys); - }); - - test("keys iterator returns correct values", () => { - expect(keys.next()).toEqual({ - value: "a", - done: false, - }); - expect(keys.next()).toEqual({ - value: "c", - done: false, - }); - expect(keys.next()).toEqual({ - value: undefined, - done: true, - }); - expect(keys.next()).toEqual({ - value: undefined, - done: true, - }); - }); - - test("keys.next() throws with invalid this", () => { - expect(() => { - keys.next.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); - }); - - test("params.keys() throws with invalid this", () => { - expect(() => { - params.keys.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); - }); -}); - -//<#END_FILE: test-whatwg-url-custom-searchparams-keys.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-searchparams-sort.test.js b/test/js/node/test/parallel/whatwg-url-custom-searchparams-sort.test.js deleted file mode 100644 index 2bbaa7c6a6..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-searchparams-sort.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-whatwg-url-custom-searchparams-sort.js -//#SHA1: 9a97c952cb19488375ead5a62f11fb579fbee211 -//----------------- -"use strict"; - -// Tests below are not from WPT. - -// TODO(joyeecheung): upstream this to WPT, if possible - even -// just as a test for large inputs. Other implementations may -// have a similar cutoff anyway. - -// Test bottom-up iterative stable merge sort because we only use that -// algorithm to sort > 100 search params. -const tests = [{ input: "", output: [] }]; -const pairs = []; -for (let i = 10; i < 100; i++) { - pairs.push([`a${i}`, "b"]); - tests[0].output.push([`a${i}`, "b"]); -} -tests[0].input = pairs - .sort(() => Math.random() > 0.5) - .map(pair => pair.join("=")) - .join("&"); - -tests.push({ - input: "z=a&=b&c=d", - output: [ - ["", "b"], - ["c", "d"], - ["z", "a"], - ], -}); - -tests.forEach(val => { - test(`Parse and sort: ${val.input}`, () => { - const params = new URLSearchParams(val.input); - let i = 0; - params.sort(); - for (const param of params) { - expect(param).toEqual(val.output[i]); - i++; - } - }); - - test(`URL parse and sort: ${val.input}`, () => { - const url = new URL(`?${val.input}`, "https://example/"); - url.searchParams.sort(); - const params = new URLSearchParams(url.search); - let i = 0; - for (const param of params) { - expect(param).toEqual(val.output[i]); - i++; - } - }); -}); - -//<#END_FILE: test-whatwg-url-custom-searchparams-sort.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-searchparams-stringifier.test.js b/test/js/node/test/parallel/whatwg-url-custom-searchparams-stringifier.test.js deleted file mode 100644 index 94028430ba..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-searchparams-stringifier.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-whatwg-url-custom-searchparams-stringifier.js -//#SHA1: 588663b1cad21e26a4b8e25c0659d204a5d96542 -//----------------- -"use strict"; - -// Tests below are not from WPT. - -test("URLSearchParams toString called with invalid this", () => { - const params = new URLSearchParams(); - expect(() => { - params.toString.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -// The URLSearchParams stringifier mutates the base URL using -// different percent-encoding rules than the URL itself. -test("URLSearchParams stringifier mutates base URL with different percent-encoding", () => { - const myUrl = new URL("https://example.org?foo=~bar"); - expect(myUrl.search).toBe("?foo=~bar"); - myUrl.searchParams.sort(); - expect(myUrl.search).toBe("?foo=%7Ebar"); -}); - -//<#END_FILE: test-whatwg-url-custom-searchparams-stringifier.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-searchparams-values.test.js b/test/js/node/test/parallel/whatwg-url-custom-searchparams-values.test.js deleted file mode 100644 index 5b7d88801c..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-searchparams-values.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-whatwg-url-custom-searchparams-values.js -//#SHA1: 7df0ccf30363d589199bb3f71c68e5559e9e4f59 -//----------------- -"use strict"; - -// Tests below are not from WPT. - -test("URLSearchParams values() method", () => { - const params = new URLSearchParams("a=b&c=d"); - const values = params.values(); - - expect(typeof values[Symbol.iterator]).toBe("function"); - expect(values[Symbol.iterator]()).toBe(values); - expect(values.next()).toEqual({ - value: "b", - done: false, - }); - expect(values.next()).toEqual({ - value: "d", - done: false, - }); - expect(values.next()).toEqual({ - value: undefined, - done: true, - }); - expect(values.next()).toEqual({ - value: undefined, - done: true, - }); - - expect(() => { - values.next.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => { - params.values.call(undefined); - }).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_THIS", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-whatwg-url-custom-searchparams-values.js diff --git a/test/js/node/test/parallel/whatwg-url-custom-searchparams.test.js b/test/js/node/test/parallel/whatwg-url-custom-searchparams.test.js deleted file mode 100644 index 7de337086e..0000000000 --- a/test/js/node/test/parallel/whatwg-url-custom-searchparams.test.js +++ /dev/null @@ -1,191 +0,0 @@ -//#FILE: test-whatwg-url-custom-searchparams.js -//#SHA1: 8308ed9fc341a1caaadcf01653bd49b96cebf599 -//----------------- -"use strict"; - -// Tests below are not from WPT. - -const assert = require("assert"); -const fixtures = require("../common/fixtures"); - -const serialized = - "a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD" + - "&a=%EF%BF%BD&a=%F0%9F%98%80&a=%EF%BF%BD%EF%BF%BD" + - "&a=%5Bobject+Object%5D"; -const values = ["a", 1, true, undefined, null, "\uD83D", "\uDE00", "\uD83D\uDE00", "\uDE00\uD83D", {}]; -const normalizedValues = [ - "a", - "1", - "true", - "undefined", - "null", - "\uFFFD", - "\uFFFD", - "\uD83D\uDE00", - "\uFFFD\uFFFD", - "[object Object]", -]; - -describe("WHATWG URL Custom SearchParams", () => { - let m, sp; - - beforeEach(() => { - m = new URL("http://example.org"); - sp = m.searchParams; - }); - - it("should not modify own symbols when accessing searchParams", () => { - const ownSymbolsBeforeGetterAccess = Object.getOwnPropertySymbols(m); - expect(sp).toBeDefined(); - expect(Object.getOwnPropertySymbols(m)).toEqual(ownSymbolsBeforeGetterAccess); - }); - - it("should initialize with empty search params", () => { - expect(sp.toString()).toBe(""); - expect(m.search).toBe(""); - }); - - it("should handle setting and deleting search params", () => { - expect(sp.has("a")).toBe(false); - values.forEach(i => sp.set("a", i)); - expect(sp.has("a")).toBe(true); - expect(sp.get("a")).toBe("[object Object]"); - sp.delete("a"); - expect(sp.has("a")).toBe(false); - }); - - it("should handle appending search params", () => { - m.search = ""; - expect(sp.toString()).toBe(""); - - values.forEach(i => sp.append("a", i)); - expect(sp.has("a")).toBe(true); - expect(sp.getAll("a").length).toBe(values.length); - expect(sp.get("a")).toBe("a"); - - expect(sp.toString()).toBe(serialized); - expect(m.search).toBe(`?${serialized}`); - }); - - it("should update URL components when modifying search params", () => { - sp.delete("a"); - values.forEach(i => sp.append("a", i)); - expect(m.href).toBe(`http://example.org/?${serialized}`); - expect(m.toString()).toBe(`http://example.org/?${serialized}`); - expect(m.toJSON()).toBe(`http://example.org/?${serialized}`); - }); - - it("should clear search params when setting href or search", () => { - sp.delete("a"); - values.forEach(i => sp.append("a", i)); - m.href = "http://example.org"; - expect(m.href).toBe("http://example.org/"); - expect(sp.size).toBe(0); - - values.forEach(i => sp.append("a", i)); - m.search = ""; - expect(m.href).toBe("http://example.org/"); - expect(sp.size).toBe(0); - }); - - it("should update URL components when modifying pathname or hash", () => { - sp.delete("a"); - values.forEach(i => sp.append("a", i)); - m.pathname = "/test"; - expect(m.href).toBe(`http://example.org/test?${serialized}`); - m.pathname = ""; - - sp.delete("a"); - values.forEach(i => sp.append("a", i)); - m.hash = "#test"; - expect(m.href).toBe(`http://example.org/?${serialized}#test`); - m.hash = ""; - }); - - it("should have correct iteration behavior", () => { - expect(sp[Symbol.iterator]).toBe(sp.entries); - - sp.delete("a"); - values.forEach(i => sp.append("a", i)); - - let n = 0; - for (const [key, val] of sp) { - expect(key).toBe("a"); - expect(val).toBe(normalizedValues[n]); - n++; - } - - n = 0; - for (const key of sp.keys()) { - expect(key).toBe("a"); - n++; - } - - n = 0; - for (const val of sp.values()) { - expect(val).toBe(normalizedValues[n]); - n++; - } - - n = 0; - sp.forEach(function (val, key, obj) { - expect(this).toBeUndefined(); - expect(key).toBe("a"); - expect(val).toBe(normalizedValues[n]); - expect(obj).toBe(sp); - n++; - }); - - sp.forEach(function () { - expect(this).toBe(m); - }, m); - }); - - it("should throw for invalid forEach arguments", () => { - expect(() => sp.forEach()).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - expect(() => sp.forEach(1)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); - - it("should handle setting search directly", () => { - m.search = "?a=a&b=b"; - expect(sp.toString()).toBe("a=a&b=b"); - }); - - it("should pass URL search params tests", () => { - const tests = require(fixtures.path("url-searchparams.js")); - - for (const [input, expected, parsed] of tests) { - if (input[0] !== "?") { - const sp = new URLSearchParams(input); - expect(String(sp)).toBe(expected); - expect(Array.from(sp)).toEqual(parsed); - - m.search = input; - expect(String(m.searchParams)).toBe(expected); - expect(Array.from(m.searchParams)).toEqual(parsed); - } - - { - const sp = new URLSearchParams(`?${input}`); - expect(String(sp)).toBe(expected); - expect(Array.from(sp)).toEqual(parsed); - - m.search = `?${input}`; - expect(String(m.searchParams)).toBe(expected); - expect(Array.from(m.searchParams)).toEqual(parsed); - } - } - }); -}); - -//<#END_FILE: test-whatwg-url-custom-searchparams.js diff --git a/test/js/node/test/parallel/whatwg-url-override-hostname.test.js b/test/js/node/test/parallel/whatwg-url-override-hostname.test.js deleted file mode 100644 index 876de04206..0000000000 --- a/test/js/node/test/parallel/whatwg-url-override-hostname.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-whatwg-url-override-hostname.js -//#SHA1: 22f2c5e784a47cc59c7f0b3229a68f4bbcfeff3e -//----------------- -"use strict"; - -test("URL with overridden hostname getter", () => { - const url = new (class extends URL { - get hostname() { - return "bar.com"; - } - })("http://foo.com/"); - - expect(url.href).toBe("http://foo.com/"); - expect(url.toString()).toBe("http://foo.com/"); - expect(url.toJSON()).toBe("http://foo.com/"); - expect(url.hash).toBe(""); - expect(url.host).toBe("foo.com"); - expect(url.hostname).toBe("bar.com"); - expect(url.origin).toBe("http://foo.com"); - expect(url.password).toBe(""); - expect(url.protocol).toBe("http:"); - expect(url.username).toBe(""); - expect(url.search).toBe(""); - expect(url.searchParams.toString()).toBe(""); -}); - -//<#END_FILE: test-whatwg-url-override-hostname.js diff --git a/test/js/node/test/parallel/worker-arraybuffer-zerofill.test.js b/test/js/node/test/parallel/worker-arraybuffer-zerofill.test.js deleted file mode 100644 index 7f9380841c..0000000000 --- a/test/js/node/test/parallel/worker-arraybuffer-zerofill.test.js +++ /dev/null @@ -1,43 +0,0 @@ -//#FILE: test-worker-arraybuffer-zerofill.js -//#SHA1: 3fd8cc412e658cb491c61d2323ff3a45af4d6dd1 -//----------------- -"use strict"; - -const { Worker } = require("worker_threads"); - -// Make sure that allocating uninitialized ArrayBuffers in one thread does not -// affect the zero-initialization in other threads. - -test("zero-initialization in other threads", done => { - const w = new Worker( - ` - const { parentPort } = require('worker_threads'); - - function post() { - const uint32array = new Uint32Array(64); - parentPort.postMessage(uint32array.reduce((a, b) => a + b)); - } - - setInterval(post, 0); - `, - { eval: true }, - ); - - function allocBuffers() { - Buffer.allocUnsafe(32 * 1024 * 1024); - } - - const interval = setInterval(allocBuffers, 0); - - let messages = 0; - w.on("message", sum => { - expect(sum).toBe(0); - if (messages++ === 100) { - clearInterval(interval); - w.terminate(); - done(); - } - }); -}); - -//<#END_FILE: test-worker-arraybuffer-zerofill.js diff --git a/test/js/node/test/parallel/worker-cjs-workerdata.test.js b/test/js/node/test/parallel/worker-cjs-workerdata.test.js deleted file mode 100644 index 0c0158346e..0000000000 --- a/test/js/node/test/parallel/worker-cjs-workerdata.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-worker-cjs-workerdata.js -//#SHA1: 8e5d70084de66c757d227df54612de48d1048ad3 -//----------------- -"use strict"; -const fixtures = require("../common/fixtures"); -const { Worker } = require("worker_threads"); - -const workerData = "Hello from main thread"; - -test("Worker with CJS module and workerData", done => { - const worker = new Worker(fixtures.path("worker-data.cjs"), { - workerData, - }); - - worker.on("message", message => { - expect(message).toBe(workerData); - done(); - }); -}); - -//<#END_FILE: test-worker-cjs-workerdata.js diff --git a/test/js/node/test/parallel/worker-cleanexit-with-js.test.js b/test/js/node/test/parallel/worker-cleanexit-with-js.test.js deleted file mode 100644 index 9e9acb2f4b..0000000000 --- a/test/js/node/test/parallel/worker-cleanexit-with-js.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-worker-cleanexit-with-js.js -//#SHA1: f47fd2ea6e4a0adf79207268baf613eb071e493a -//----------------- -"use strict"; - -// Harden the thread interactions on the exit path. -// Ensure workers are able to bail out safe at -// arbitrary execution points. By running a lot of -// JS code in a tight loop, the expectation -// is that those will be at various control flow points -// preferably in the JS land. - -const { Worker } = require("worker_threads"); - -test("Workers can bail out safely at arbitrary execution points", done => { - const code = - "setInterval(() => {" + - "require('v8').deserialize(require('v8').serialize({ foo: 'bar' }));" + - "require('vm').runInThisContext('x = \"foo\";');" + - "eval('const y = \"vm\";');}, 10);"; - - for (let i = 0; i < 9; i++) { - new Worker(code, { eval: true }); - } - - const lastWorker = new Worker(code, { eval: true }); - lastWorker.on("online", () => { - expect(true).toBe(true); // Ensure the worker came online - process.exit(0); - done(); - }); -}); - -//<#END_FILE: test-worker-cleanexit-with-js.js diff --git a/test/js/node/test/parallel/worker-cleanexit-with-moduleload.test.js b/test/js/node/test/parallel/worker-cleanexit-with-moduleload.test.js deleted file mode 100644 index ffad2cb95e..0000000000 --- a/test/js/node/test/parallel/worker-cleanexit-with-moduleload.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-worker-cleanexit-with-moduleload.js -//#SHA1: cdaed88b3a0ebbc07619e10f35ea0c62e5134b63 -//----------------- -"use strict"; - -const { Worker } = require("worker_threads"); - -// Harden the thread interactions on the exit path. -// Ensure workers are able to bail out safe at -// arbitrary execution points. By using a number of -// internal modules as load candidates, the expectation -// is that those will be at various control flow points -// preferably in the C++ land. - -const modules = ["fs", "assert", "async_hooks", "buffer", "child_process", "net", "http", "os", "path", "v8", "vm"]; - -if (process.versions.openssl) { - modules.push("https"); -} - -test("Workers can exit cleanly while loading modules", done => { - for (let i = 0; i < 10; i++) { - new Worker( - `const modules = [${modules.map(m => `'${m}'`)}];` + - "modules.forEach((module) => {" + - "const m = require(module);" + - "});", - { eval: true }, - ); - } - - // Allow workers to go live. - setTimeout(() => { - done(); - }, 200); -}, 300); // Set timeout to 300ms to allow for the 200ms delay - -//<#END_FILE: test-worker-cleanexit-with-moduleload.js diff --git a/test/js/node/test/parallel/worker-esmodule.test.js b/test/js/node/test/parallel/worker-esmodule.test.js deleted file mode 100644 index e08d58041a..0000000000 --- a/test/js/node/test/parallel/worker-esmodule.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-worker-esmodule.js -//#SHA1: a40c6a55aa2fe45203bec4808e0d53efed2fa4e4 -//----------------- -"use strict"; - -const fixtures = require("../common/fixtures"); -const { Worker } = require("worker_threads"); - -test("Worker can load ES module", () => { - const w = new Worker(fixtures.path("worker-script.mjs")); - - return new Promise(resolve => { - w.on("message", message => { - expect(message).toBe("Hello, world!"); - resolve(); - }); - }); -}); - -//<#END_FILE: test-worker-esmodule.js diff --git a/test/js/node/test/parallel/worker-invalid-workerdata.test.js b/test/js/node/test/parallel/worker-invalid-workerdata.test.js deleted file mode 100644 index 1776a6d26c..0000000000 --- a/test/js/node/test/parallel/worker-invalid-workerdata.test.js +++ /dev/null @@ -1,25 +0,0 @@ -//#FILE: test-worker-invalid-workerdata.js -//#SHA1: 2e1989d95e34d3603c30290e012335261767ae90 -//----------------- -"use strict"; - -const { Worker } = require("worker_threads"); - -// This tests verifies that failing to serialize workerData does not keep -// the process alive. -// Refs: https://github.com/nodejs/node/issues/22736 - -test("Worker creation with unserializable workerData throws DataCloneError", () => { - expect(() => { - new Worker("./worker.js", { - workerData: { fn: () => {} }, - }); - }).toThrow( - expect.objectContaining({ - name: "DataCloneError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-worker-invalid-workerdata.js diff --git a/test/js/node/test/parallel/worker-memory.test.js b/test/js/node/test/parallel/worker-memory.test.js deleted file mode 100644 index d18eb5bccd..0000000000 --- a/test/js/node/test/parallel/worker-memory.test.js +++ /dev/null @@ -1,63 +0,0 @@ -//#FILE: test-worker-memory.js -//#SHA1: e425b7d8f32d04fae4a6e0c697a78aeae4de2f60 -//----------------- -"use strict"; - -const util = require("util"); -const { Worker } = require("worker_threads"); -const os = require("os"); - -if (process.platform === "os400") { - test.skip("On IBMi, the rss memory always returns zero"); -} - -let numWorkers = +process.env.JOBS || os.availableParallelism(); -if (numWorkers > 20) { - // Cap the number of workers at 20 (as an even divisor of 60 used as - // the total number of workers started) otherwise the test fails on - // machines with high core counts. - numWorkers = 20; -} - -// Verify that a Worker's memory isn't kept in memory after the thread finishes. - -function run(n, done) { - console.log(`run() called with n=${n} (numWorkers=${numWorkers})`); - if (n <= 0) return done(); - const worker = new Worker("require('worker_threads').parentPort.postMessage(2 + 2)", { eval: true }); - worker.on("message", value => { - expect(value).toBe(4); - }); - worker.on("exit", () => { - run(n - 1, done); - }); -} - -test("Worker memory is not kept after thread finishes", async () => { - const startStats = process.memoryUsage(); - let finished = 0; - - const runPromises = Array(numWorkers) - .fill() - .map( - () => - new Promise(resolve => { - run(60 / numWorkers, () => { - console.log(`done() called (finished=${finished})`); - if (++finished === numWorkers) { - const finishStats = process.memoryUsage(); - // A typical value for this ratio would be ~1.15. - // 5 as a upper limit is generous, but the main point is that we - // don't have the memory of 50 Isolates/Node.js environments just lying - // around somewhere. - expect(finishStats.rss / startStats.rss).toBeLessThan(5); - } - resolve(); - }); - }), - ); - - await Promise.all(runPromises); -}, 30000); // Increase timeout to 30 seconds - -//<#END_FILE: test-worker-memory.js diff --git a/test/js/node/test/parallel/worker-message-channel-sharedarraybuffer.test.js b/test/js/node/test/parallel/worker-message-channel-sharedarraybuffer.test.js deleted file mode 100644 index 088cb1530c..0000000000 --- a/test/js/node/test/parallel/worker-message-channel-sharedarraybuffer.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-worker-message-channel-sharedarraybuffer.js -//#SHA1: 567096de16f1ea1b7a50e53cbdbd47b531af0e2a -//----------------- -// Flags: --expose-gc -"use strict"; - -const { Worker } = require("worker_threads"); - -test("SharedArrayBuffer can be shared between main thread and worker", async () => { - const sharedArrayBuffer = new SharedArrayBuffer(12); - const local = Buffer.from(sharedArrayBuffer); - - const w = new Worker( - ` - const { parentPort } = require('worker_threads'); - parentPort.on('message', ({ sharedArrayBuffer }) => { - const local = Buffer.from(sharedArrayBuffer); - local.write('world!', 6); - parentPort.postMessage('written!'); - }); - `, - { eval: true }, - ); - - const messagePromise = new Promise(resolve => { - w.on("message", resolve); - }); - - w.postMessage({ sharedArrayBuffer }); - // This would be a race condition if the memory regions were overlapping - local.write("Hello "); - - await messagePromise; - - expect(local.toString()).toBe("Hello world!"); - - global.gc(); - await w.terminate(); -}); - -//<#END_FILE: test-worker-message-channel-sharedarraybuffer.js diff --git a/test/js/node/test/parallel/worker-message-port-transfer-terminate.test.js b/test/js/node/test/parallel/worker-message-port-transfer-terminate.test.js deleted file mode 100644 index c00d6d2251..0000000000 --- a/test/js/node/test/parallel/worker-message-port-transfer-terminate.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-worker-message-port-transfer-terminate.js -//#SHA1: ca776ac07835f8e6306ba671531b8e5a73e25f27 -//----------------- -"use strict"; - -const { Worker, MessageChannel } = require("worker_threads"); - -// Check the interaction of calling .terminate() while transferring -// MessagePort objects; in particular, that it does not crash the process. - -test("Worker termination while transferring MessagePort objects", done => { - let completedIterations = 0; - - for (let i = 0; i < 10; ++i) { - const w = new Worker("require('worker_threads').parentPort.on('message', () => {})", { eval: true }); - - setImmediate(() => { - const port = new MessageChannel().port1; - w.postMessage({ port }, [port]); - w.terminate().then(() => { - completedIterations++; - if (completedIterations === 10) { - done(); - } - }); - }); - } -}); - -//<#END_FILE: test-worker-message-port-transfer-terminate.js diff --git a/test/js/node/test/parallel/worker-mjs-workerdata.test.js b/test/js/node/test/parallel/worker-mjs-workerdata.test.js deleted file mode 100644 index d3bfe6d442..0000000000 --- a/test/js/node/test/parallel/worker-mjs-workerdata.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-worker-mjs-workerdata.js -//#SHA1: c4df37cd769b399dd8245d29bed7f385f1adb472 -//----------------- -"use strict"; - -const fixtures = require("../common/fixtures"); -const { Worker } = require("worker_threads"); - -const workerData = "Hello from main thread"; - -test("Worker with workerData in MJS", done => { - const worker = new Worker(fixtures.path("worker-data.mjs"), { - workerData, - }); - - worker.on("message", message => { - expect(message).toBe(workerData); - done(); - }); -}); - -//<#END_FILE: test-worker-mjs-workerdata.js diff --git a/test/js/node/test/parallel/worker-nested-on-process-exit.test.js b/test/js/node/test/parallel/worker-nested-on-process-exit.test.js deleted file mode 100644 index d198efaf1f..0000000000 --- a/test/js/node/test/parallel/worker-nested-on-process-exit.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-worker-nested-on-process-exit.js -//#SHA1: a76202f79766e7ca3f39552cc00b4f31e8f38a56 -//----------------- -"use strict"; -const { Worker, workerData } = require("worker_threads"); - -// Test that 'exit' events for nested Workers are not received when a Worker -// terminates itself through process.exit(). - -if (workerData === null) { - test("nested worker exit events", () => { - const nestedWorkerExitCounter = new Int32Array(new SharedArrayBuffer(4)); - const w = new Worker(__filename, { workerData: nestedWorkerExitCounter }); - - return new Promise(resolve => { - w.on("exit", () => { - expect(nestedWorkerExitCounter[0]).toBe(0); - resolve(); - }); - }); - }); -} else { - const nestedWorker = new Worker("setInterval(() => {}, 100)", { eval: true }); - // The counter should never be increased here. - nestedWorker.on("exit", () => workerData[0]++); - nestedWorker.on("online", () => process.exit()); -} - -//<#END_FILE: test-worker-nested-on-process-exit.js diff --git a/test/js/node/test/parallel/worker-nested-uncaught.test.js b/test/js/node/test/parallel/worker-nested-uncaught.test.js deleted file mode 100644 index 050f33a797..0000000000 --- a/test/js/node/test/parallel/worker-nested-uncaught.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-worker-nested-uncaught.js -//#SHA1: 948558ccf744615abbd0df028a83b36d36ad7aff -//----------------- -"use strict"; -const { Worker } = require("worker_threads"); - -// Regression test for https://github.com/nodejs/node/issues/34309 - -test("nested worker uncaught error", done => { - const w = new Worker( - `const { Worker } = require('worker_threads'); - new Worker("throw new Error('uncaught')", { eval:true })`, - { eval: true }, - ); - - w.on("error", error => { - expect(error).toEqual( - expect.objectContaining({ - name: "Error", - message: expect.any(String), - }), - ); - done(); - }); -}); - -//<#END_FILE: test-worker-nested-uncaught.js diff --git a/test/js/node/test/parallel/worker-ref-onexit.test.js b/test/js/node/test/parallel/worker-ref-onexit.test.js deleted file mode 100644 index fb360433d4..0000000000 --- a/test/js/node/test/parallel/worker-ref-onexit.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-worker-ref-onexit.js -//#SHA1: b34eb5ff18bd889eb47cd37e696410f428b0b11b -//----------------- -"use strict"; -const { Worker } = require("worker_threads"); - -// Check that worker.unref() makes the 'exit' event not be emitted, if it is -// the only thing we would otherwise be waiting for. - -test("worker.unref() prevents exit event emission", done => { - // Use `setInterval()` to make sure the worker is alive until the end of the - // event loop turn. - const w = new Worker("setInterval(() => {}, 100);", { eval: true }); - w.unref(); - - const exitHandler = jest.fn(); - w.on("exit", exitHandler); - - // We need to use a timeout here to allow the event loop to complete - // and ensure the exit event is not called - setTimeout(() => { - expect(exitHandler).not.toHaveBeenCalled(); - done(); - }, 500); -}); - -//<#END_FILE: test-worker-ref-onexit.js diff --git a/test/js/node/test/parallel/worker-relative-path-double-dot.test.js b/test/js/node/test/parallel/worker-relative-path-double-dot.test.js deleted file mode 100644 index daed4389b1..0000000000 --- a/test/js/node/test/parallel/worker-relative-path-double-dot.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-worker-relative-path-double-dot.js -//#SHA1: 2b605c02bfb9cfc2d42533a6a184dfdbd5a6e5b7 -//----------------- -"use strict"; -const path = require("path"); -const { Worker, isMainThread, parentPort } = require("worker_threads"); - -if (isMainThread) { - test("Worker with relative path using double dot", done => { - const cwdName = path.relative("../", "."); - const relativePath = path.relative(".", __filename); - const w = new Worker(path.join("..", cwdName, relativePath)); - - w.on("message", message => { - expect(message).toBe("Hello, world!"); - done(); - }); - }); -} else { - parentPort.postMessage("Hello, world!"); -} - -//<#END_FILE: test-worker-relative-path-double-dot.js diff --git a/test/js/node/test/parallel/worker-relative-path.test.js b/test/js/node/test/parallel/worker-relative-path.test.js deleted file mode 100644 index a6971694c1..0000000000 --- a/test/js/node/test/parallel/worker-relative-path.test.js +++ /dev/null @@ -1,20 +0,0 @@ -//#FILE: test-worker-relative-path.js -//#SHA1: 2d69899e96eee7500da11857679bf9f2ec18c259 -//----------------- -"use strict"; -const path = require("path"); -const { Worker, isMainThread, parentPort } = require("worker_threads"); - -if (isMainThread) { - test("Worker can be started with a relative path", done => { - const w = new Worker(`./${path.relative(".", __filename)}`); - w.on("message", message => { - expect(message).toBe("Hello, world!"); - done(); - }); - }); -} else { - parentPort.postMessage("Hello, world!"); -} - -//<#END_FILE: test-worker-relative-path.js diff --git a/test/js/node/test/parallel/worker-sharedarraybuffer-from-worker-thread.test.js b/test/js/node/test/parallel/worker-sharedarraybuffer-from-worker-thread.test.js deleted file mode 100644 index 0a4c29b25e..0000000000 --- a/test/js/node/test/parallel/worker-sharedarraybuffer-from-worker-thread.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-worker-sharedarraybuffer-from-worker-thread.js -//#SHA1: a33c9c03494dc79f7dd926382125f034d8ae4bbc -//----------------- -// Flags: --debug-arraybuffer-allocations -"use strict"; - -// Regression test for https://github.com/nodejs/node/issues/28777 -// Make sure that SharedArrayBuffers and transferred ArrayBuffers created in -// Worker threads are accessible after the creating thread ended. - -const { Worker } = require("worker_threads"); - -["ArrayBuffer", "SharedArrayBuffer"].forEach(ctor => { - test(`${ctor} from worker thread`, async () => { - const w = new Worker( - ` - const { parentPort } = require('worker_threads'); - const arrayBuffer = new ${ctor}(4); - parentPort.postMessage( - arrayBuffer, - '${ctor}' === 'SharedArrayBuffer' ? [] : [arrayBuffer]); - `, - { eval: true }, - ); - - let arrayBuffer; - await new Promise(resolve => { - w.once("message", message => { - arrayBuffer = message; - resolve(); - }); - }); - - await new Promise(resolve => { - w.once("exit", () => { - expect(arrayBuffer.constructor.name).toBe(ctor); - const uint8array = new Uint8Array(arrayBuffer); - uint8array[0] = 42; - expect(uint8array).toEqual(new Uint8Array([42, 0, 0, 0])); - resolve(); - }); - }); - }); -}); - -//<#END_FILE: test-worker-sharedarraybuffer-from-worker-thread.js diff --git a/test/js/node/test/parallel/worker-terminate-http2-respond-with-file.test.js b/test/js/node/test/parallel/worker-terminate-http2-respond-with-file.test.js deleted file mode 100644 index c317604d79..0000000000 --- a/test/js/node/test/parallel/worker-terminate-http2-respond-with-file.test.js +++ /dev/null @@ -1,51 +0,0 @@ -//#FILE: test-worker-terminate-http2-respond-with-file.js -//#SHA1: e8ce958ee3283a8ec8c83acc27cc3a02824ebfb7 -//----------------- -"use strict"; - -const http2 = require("http2"); -const makeDuplexPair = require("../common/duplexpair"); -const { Worker, isMainThread } = require("worker_threads"); - -// This is a variant of test-http2-generic-streams-sendfile for checking -// that Workers can be terminated during a .respondWithFile() operation. - -if (isMainThread) { - test("Worker can be terminated during respondWithFile operation", () => { - const worker = new Worker(__filename); - expect(worker).toBeDefined(); - }); -} else { - test("HTTP/2 server responds with file", done => { - const server = http2.createServer(); - server.on("stream", (stream, headers) => { - stream.respondWithFile(process.execPath); // Use a large-ish file. - }); - - const { clientSide, serverSide } = makeDuplexPair(); - server.emit("connection", serverSide); - - const client = http2.connect("http://localhost:80", { - createConnection: () => clientSide, - }); - - const req = client.request(); - - req.on("response", headers => { - expect(headers[":status"]).toBe(200); - }); - - req.on("data", () => { - process.exit(); - done(); - }); - - req.on("end", () => { - done.fail("Request should not end"); - }); - - req.end(); - }); -} - -//<#END_FILE: test-worker-terminate-http2-respond-with-file.js diff --git a/test/js/node/test/parallel/worker-unref-from-message-during-exit.test.js b/test/js/node/test/parallel/worker-unref-from-message-during-exit.test.js deleted file mode 100644 index 338155b933..0000000000 --- a/test/js/node/test/parallel/worker-unref-from-message-during-exit.test.js +++ /dev/null @@ -1,31 +0,0 @@ -//#FILE: test-worker-unref-from-message-during-exit.js -//#SHA1: 8555ad4d197dfc269beffcf6b2a3940df8a41659 -//----------------- -"use strict"; -const { Worker } = require("worker_threads"); - -// This used to crash because the `.unref()` was unexpected while the Worker -// was exiting. - -test("Worker unref from message during exit", async () => { - const w = new Worker( - ` -require('worker_threads').parentPort.postMessage({}); -`, - { eval: true }, - ); - - const messagePromise = new Promise(resolve => { - w.on("message", () => { - w.unref(); - resolve(); - }); - }); - - // Wait a bit so that the 'message' event is emitted while the Worker exits. - await Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); - - await expect(messagePromise).resolves.toBeUndefined(); -}); - -//<#END_FILE: test-worker-unref-from-message-during-exit.js diff --git a/test/js/node/test/parallel/worker-workerdata-sharedarraybuffer.test.js b/test/js/node/test/parallel/worker-workerdata-sharedarraybuffer.test.js deleted file mode 100644 index 4ff64a3328..0000000000 --- a/test/js/node/test/parallel/worker-workerdata-sharedarraybuffer.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-worker-workerdata-sharedarraybuffer.js -//#SHA1: 2737167494699dbfe6986e4b879ecdeafd31a8a8 -//----------------- -// Flags: --expose-gc -"use strict"; - -const { Worker } = require("worker_threads"); - -test("SharedArrayBuffer can be passed via workerData and modified", async () => { - const sharedArrayBuffer = new SharedArrayBuffer(12); - const local = Buffer.from(sharedArrayBuffer); - - const w = new Worker( - ` - const { parentPort, workerData } = require('worker_threads'); - const local = Buffer.from(workerData.sharedArrayBuffer); - - parentPort.on('message', () => { - local.write('world!', 6); - parentPort.postMessage('written!'); - }); - `, - { - eval: true, - workerData: { sharedArrayBuffer }, - }, - ); - - const messagePromise = new Promise(resolve => { - w.on("message", resolve); - }); - - w.postMessage({}); - // This would be a race condition if the memory regions were overlapping - local.write("Hello "); - - await messagePromise; - - expect(local.toString()).toBe("Hello world!"); - - global.gc(); - await w.terminate(); -}); - -//<#END_FILE: test-worker-workerdata-sharedarraybuffer.js diff --git a/test/js/node/test/parallel/worker.test.js b/test/js/node/test/parallel/worker.test.js deleted file mode 100644 index 7328a65dec..0000000000 --- a/test/js/node/test/parallel/worker.test.js +++ /dev/null @@ -1,26 +0,0 @@ -//#FILE: test-worker.js -//#SHA1: 830c4e2ce132228fe7d49fd760271deed934db23 -//----------------- -"use strict"; - -const { Worker, isMainThread, parentPort } = require("worker_threads"); - -const kTestString = "Hello, world!"; - -if (isMainThread) { - test("Worker thread communication", done => { - const w = new Worker(__filename); - w.on("message", message => { - expect(message).toBe(kTestString); - done(); - }); - }); -} else { - setImmediate(() => { - process.nextTick(() => { - parentPort.postMessage(kTestString); - }); - }); -} - -//<#END_FILE: test-worker.js diff --git a/test/js/node/test/parallel/wrap-js-stream-read-stop.test.js b/test/js/node/test/parallel/wrap-js-stream-read-stop.test.js deleted file mode 100644 index 6efe3f5df1..0000000000 --- a/test/js/node/test/parallel/wrap-js-stream-read-stop.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-wrap-js-stream-read-stop.js -//#SHA1: 53e905da53ef7a130579672b04ddd6e65b7cb8d5 -//----------------- -"use strict"; - -const Stream = require("stream"); - -class FakeStream extends Stream { - constructor() { - super(); - this._paused = false; - } - - pause() { - this._paused = true; - } - - resume() { - this._paused = false; - } - - isPaused() { - return this._paused; - } -} - -class WrapStream { - constructor(stream) { - this.stream = stream; - this.stream.resume(); - } - - readStop() { - this.stream.pause(); - return 0; - } -} - -describe("WrapStream", () => { - let fakeStreamObj; - let wrappedStream; - - beforeEach(() => { - fakeStreamObj = new FakeStream(); - wrappedStream = new WrapStream(fakeStreamObj); - }); - - test("Resume by wrapped stream upon construction", () => { - expect(fakeStreamObj.isPaused()).toBe(false); - }); - - test("Pause and resume fakeStreamObj", () => { - fakeStreamObj.pause(); - expect(fakeStreamObj.isPaused()).toBe(true); - - fakeStreamObj.resume(); - expect(fakeStreamObj.isPaused()).toBe(false); - }); - - test("readStop method", () => { - expect(wrappedStream.readStop()).toBe(0); - expect(fakeStreamObj.isPaused()).toBe(true); - }); -}); - -//<#END_FILE: test-wrap-js-stream-read-stop.js diff --git a/test/js/node/test/parallel/zlib-brotli-16gb.test.js b/test/js/node/test/parallel/zlib-brotli-16gb.test.js deleted file mode 100644 index edefbaafe9..0000000000 --- a/test/js/node/test/parallel/zlib-brotli-16gb.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-zlib-brotli-16GB.js -//#SHA1: 0c5e79f3713fdd0597dda7e531484a0ef3170578 -//----------------- -"use strict"; - -const { createBrotliDecompress } = require("node:zlib"); -const { getDefaultHighWaterMark } = require("stream"); - -// This tiny HEX string is a 16GB file. -// This test verifies that the stream actually stops. -const content = - "cfffff7ff82700e2b14020f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c32200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdf" + - "fe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8de" + - "fff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074e" + - "ffff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500ba" + - "f7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200d" + - "dfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180" + - "eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b0004" + - "0f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800" + - "a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c0" + - "0d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c16" + - "00e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0" + - "b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f011087" + - "0500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c" + - "30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4" + - "610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e" + - "2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300" + - "715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe098" + - "0382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04" + - "401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f0" + - "2200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f" + - "0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19" + - "f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff0" + - "4f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff" + - "82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3f" + - "fc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1" + - "ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bff3f"; - -test("the test", async () => { - const buf = Buffer.from(content, "hex"); - - const decoder = createBrotliDecompress(); - decoder.end(buf); - - const { promise, resolve, reject } = Promise.withResolvers(); - - setTimeout(() => { - try { - expect(decoder._readableState.buffer.length).toBe(getDefaultHighWaterMark() / (16 * 1024)); - resolve(); - } catch (e) { - reject(e); - } - }, 500); - await promise; -}); - -//#FILE: test-zlib-brotli-16GB.js -//----------------- diff --git a/test/js/node/test/parallel/zlib-brotli-flush.test.js b/test/js/node/test/parallel/zlib-brotli-flush.test.js deleted file mode 100644 index 77723ad78f..0000000000 --- a/test/js/node/test/parallel/zlib-brotli-flush.test.js +++ /dev/null @@ -1,33 +0,0 @@ -//#FILE: test-zlib-brotli-flush.js -//#SHA1: b0a953be98db6dd674668bfd6cffa3e283144ad1 -//----------------- -"use strict"; -const zlib = require("zlib"); -const fs = require("fs"); -const path = require("path"); - -const fixturesPath = path.join(__dirname, "..", "fixtures"); -const file = fs.readFileSync(path.join(fixturesPath, "person.jpg")); -const chunkSize = 16; - -test("BrotliCompress flush should produce expected output", done => { - const deflater = new zlib.BrotliCompress(); - const chunk = file.slice(0, chunkSize); - const expectedFull = Buffer.from("iweA/9j/4AAQSkZJRgABAQEASA==", "base64"); - let actualFull; - - deflater.write(chunk, () => { - deflater.flush(() => { - const bufs = []; - let buf; - while ((buf = deflater.read()) !== null) { - bufs.push(buf); - } - actualFull = Buffer.concat(bufs); - expect(actualFull).toEqual(expectedFull); - done(); - }); - }); -}); - -//<#END_FILE: test-zlib-brotli-flush.js diff --git a/test/js/node/test/parallel/zlib-brotli-from-string.test.js b/test/js/node/test/parallel/zlib-brotli-from-string.test.js deleted file mode 100644 index fa861d967b..0000000000 --- a/test/js/node/test/parallel/zlib-brotli-from-string.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-zlib-brotli-from-string.js -//#SHA1: bb4656c195e75f9d49e2bad9e7b1130f571fa68b -//----------------- -"use strict"; -// Test compressing and uncompressing a string with brotli - -const zlib = require("zlib"); - -const inputString = - "ΩΩLorem ipsum dolor sit amet, consectetur adipiscing eli" + - "t. Morbi faucibus, purus at gravida dictum, libero arcu " + - "convallis lacus, in commodo libero metus eu nisi. Nullam" + - " commodo, neque nec porta placerat, nisi est fermentum a" + - "ugue, vitae gravida tellus sapien sit amet tellus. Aenea" + - "n non diam orci. Proin quis elit turpis. Suspendisse non" + - " diam ipsum. Suspendisse nec ullamcorper odio. Vestibulu" + - "m arcu mi, sodales non suscipit id, ultrices ut massa. S" + - "ed ac sem sit amet arcu malesuada fermentum. Nunc sed. "; -const compressedString = - "G/gBQBwHdky2aHV5KK9Snf05//1pPdmNw/7232fnIm1IB" + - "K1AA8RsN8OB8Nb7Lpgk3UWWUlzQXZyHQeBBbXMTQXC1j7" + - "wg3LJs9LqOGHRH2bj/a2iCTLLx8hBOyTqgoVuD1e+Qqdn" + - "f1rkUNyrWq6LtOhWgxP3QUwdhKGdZm3rJWaDDBV7+pDk1" + - "MIkrmjp4ma2xVi5MsgJScA3tP1I7mXeby6MELozrwoBQD" + - "mVTnEAicZNj4lkGqntJe2qSnGyeMmcFgraK94vCg/4iLu" + - "Tw5RhKhnVY++dZ6niUBmRqIutsjf5TzwF5iAg8a9UkjF5" + - "2eZ0tB2vo6v8SqVfNMkBmmhxr0NT9LkYF69aEjlYzj7IE" + - "KmEUQf1HBogRYhFIt4ymRNEgHAIzOyNEsQM="; - -test("brotli compress and decompress string", async () => { - const compressCallback = jest.fn(); - const decompressCallback = jest.fn(); - - await new Promise(resolve => { - zlib.brotliCompress(inputString, (err, buffer) => { - compressCallback(); - expect(inputString.length).toBeGreaterThan(buffer.length); - - zlib.brotliDecompress(buffer, (err, buffer) => { - decompressCallback(); - expect(buffer.toString()).toBe(inputString); - resolve(); - }); - }); - }); - - expect(compressCallback).toHaveBeenCalledTimes(1); - expect(decompressCallback).toHaveBeenCalledTimes(1); -}); - -test("brotli decompress base64 string", async () => { - const decompressCallback = jest.fn(); - - const buffer = Buffer.from(compressedString, "base64"); - await new Promise(resolve => { - zlib.brotliDecompress(buffer, (err, buffer) => { - decompressCallback(); - expect(buffer.toString()).toBe(inputString); - resolve(); - }); - }); - - expect(decompressCallback).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-zlib-brotli-from-string.js diff --git a/test/js/node/test/parallel/zlib-brotli-kmaxlength-rangeerror.test.js b/test/js/node/test/parallel/zlib-brotli-kmaxlength-rangeerror.test.js deleted file mode 100644 index c3cbb9d19b..0000000000 --- a/test/js/node/test/parallel/zlib-brotli-kmaxlength-rangeerror.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-zlib-brotli-kmaxlength-rangeerror.js -//#SHA1: f0d3ad25e8a844b31b7e14ab68a84182fd5f52b7 -//----------------- -"use strict"; - -const util = require("util"); - -// Change kMaxLength for zlib to trigger the error without having to allocate large Buffers. -const buffer = require("buffer"); -const oldkMaxLength = buffer.kMaxLength; -buffer.kMaxLength = 64; -const zlib = require("zlib"); -buffer.kMaxLength = oldkMaxLength; - -// Create a large input buffer -const encoded = Buffer.from("G38A+CXCIrFAIAM=", "base64"); - -test("brotliDecompress throws RangeError for large output (async)", async () => { - await expect(async () => util.promisify(zlib.brotliDecompress)(encoded)).toThrowWithCodeAsync( - RangeError, - "ERR_BUFFER_TOO_LARGE", - ); -}, 1000); - -test("brotliDecompressSync throws RangeError for large output", () => { - expect(() => zlib.brotliDecompressSync(encoded)).toThrowWithCode(RangeError, "ERR_BUFFER_TOO_LARGE"); -}); - -//<#END_FILE: test-zlib-brotli-kmaxlength-rangeerror.js diff --git a/test/js/node/test/parallel/zlib-brotli.test.js b/test/js/node/test/parallel/zlib-brotli.test.js deleted file mode 100644 index d61df3eae3..0000000000 --- a/test/js/node/test/parallel/zlib-brotli.test.js +++ /dev/null @@ -1,101 +0,0 @@ -//#FILE: test-zlib-brotli.js -//#SHA1: 53d893f351dd67279a8561e244183e38864c0c92 -//----------------- -"use strict"; - -const fixtures = require("../common/fixtures"); -const zlib = require("zlib"); - -// Test some brotli-specific properties of the brotli streams that can not -// be easily covered through expanding zlib-only tests. - -const sampleBuffer = fixtures.readSync("/pss-vectors.json"); - -test("Quality parameter at stream creation", () => { - const sizes = []; - for (let quality = zlib.constants.BROTLI_MIN_QUALITY; quality <= zlib.constants.BROTLI_MAX_QUALITY; quality++) { - const encoded = zlib.brotliCompressSync(sampleBuffer, { - params: { - [zlib.constants.BROTLI_PARAM_QUALITY]: quality, - }, - }); - sizes.push(encoded.length); - } - - // Increasing quality should roughly correspond to decreasing compressed size: - for (let i = 0; i < sizes.length - 1; i++) { - expect(sizes[i + 1]).toBeLessThanOrEqual(sizes[i] * 1.05); // 5 % margin of error. - } - expect(sizes[0]).toBeGreaterThan(sizes[sizes.length - 1]); -}); - -test("Setting out-of-bounds option values or keys", () => { - expect(() => { - zlib.createBrotliCompress({ - params: { - 10000: 0, - }, - }); - }).toThrow( - expect.objectContaining({ - code: "ERR_BROTLI_INVALID_PARAM", - name: "RangeError", - message: expect.any(String), - }), - ); - - // Test that accidentally using duplicate keys fails. - expect(() => { - zlib.createBrotliCompress({ - params: { - 0: 0, - "00": 0, - }, - }); - }).toThrow( - expect.objectContaining({ - code: "ERR_BROTLI_INVALID_PARAM", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => { - zlib.createBrotliCompress({ - params: { - // This is a boolean flag - [zlib.constants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING]: 42, - }, - }); - }).toThrow( - expect.objectContaining({ - code: "ERR_ZLIB_INITIALIZATION_FAILED", - name: "Error", - message: expect.any(String), - }), - ); -}); - -test("Options.flush range", () => { - expect(() => { - zlib.brotliCompressSync("", { flush: zlib.constants.Z_FINISH }); - }).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => { - zlib.brotliCompressSync("", { finishFlush: zlib.constants.Z_FINISH }); - }).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-zlib-brotli.js diff --git a/test/js/node/test/parallel/zlib-close-after-error.test.js b/test/js/node/test/parallel/zlib-close-after-error.test.js deleted file mode 100644 index f336f5a950..0000000000 --- a/test/js/node/test/parallel/zlib-close-after-error.test.js +++ /dev/null @@ -1,22 +0,0 @@ -//#FILE: test-zlib-close-after-error.js -//#SHA1: 1f561376d8af1a6b21f9f9abf6813a20cde33be6 -//----------------- -"use strict"; -// https://github.com/nodejs/node/issues/6034 - -const zlib = require("zlib"); - -test("zlib close after error", done => { - const decompress = zlib.createGunzip(15); - - decompress.on("error", err => { - expect(decompress._closed).toBe(true); - decompress.close(); - done(); - }); - - expect(decompress._closed).toBe(false); - decompress.write("something invalid"); -}); - -//<#END_FILE: test-zlib-close-after-error.js diff --git a/test/js/node/test/parallel/zlib-close-after-write.test.js b/test/js/node/test/parallel/zlib-close-after-write.test.js deleted file mode 100644 index 05a029b37f..0000000000 --- a/test/js/node/test/parallel/zlib-close-after-write.test.js +++ /dev/null @@ -1,48 +0,0 @@ -//#FILE: test-zlib-close-after-write.js -//#SHA1: 7fad593914e2a23d73598e4366e685b9aa91cc24 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const zlib = require("zlib"); - -test("zlib close after write", async () => { - const gzipPromise = new Promise((resolve, reject) => { - zlib.gzip("hello", (err, out) => { - if (err) reject(err); - else resolve(out); - }); - }); - - const out = await gzipPromise; - - const unzip = zlib.createGunzip(); - unzip.write(out); - - const closePromise = new Promise(resolve => { - unzip.close(resolve); - }); - - await closePromise; -}); - -//<#END_FILE: test-zlib-close-after-write.js diff --git a/test/js/node/test/parallel/zlib-close-in-ondata.test.js b/test/js/node/test/parallel/zlib-close-in-ondata.test.js deleted file mode 100644 index e4449d57d4..0000000000 --- a/test/js/node/test/parallel/zlib-close-in-ondata.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-zlib-close-in-ondata.js -//#SHA1: 8218c0461dd0733882aaf37688e3b32b164e3535 -//----------------- -"use strict"; - -const zlib = require("zlib"); - -test("zlib stream closes in ondata event", done => { - const ts = zlib.createGzip(); - const buf = Buffer.alloc(1024 * 1024 * 20); - - ts.on( - "data", - jest.fn(() => { - ts.close(); - done(); - }), - ); - - ts.end(buf); -}); - -//<#END_FILE: test-zlib-close-in-ondata.js diff --git a/test/js/node/test/parallel/zlib-const.test.js b/test/js/node/test/parallel/zlib-const.test.js deleted file mode 100644 index a81535e446..0000000000 --- a/test/js/node/test/parallel/zlib-const.test.js +++ /dev/null @@ -1,23 +0,0 @@ -//#FILE: test-zlib-const.js -//#SHA1: d85ad9e395d5781dbe5bf05e5514104bd9503be8 -//----------------- -const zlib = require("zlib"); - -test("zlib constants and codes are immutable", () => { - // Test Z_OK constant - expect(zlib.constants.Z_OK).toBe(0); - zlib.constants.Z_OK = 1; - expect(zlib.constants.Z_OK).toBe(0); - - // Test Z_OK code - expect(zlib.codes.Z_OK).toBe(0); - zlib.codes.Z_OK = 1; - expect(zlib.codes.Z_OK).toBe(0); - zlib.codes = { Z_OK: 1 }; - expect(zlib.codes.Z_OK).toBe(0); - - // Test if zlib.codes is frozen - expect(Object.isFrozen(zlib.codes)).toBe(true); -}); - -//<#END_FILE: test-zlib-const.js diff --git a/test/js/node/test/parallel/zlib-convenience-methods.test.js b/test/js/node/test/parallel/zlib-convenience-methods.test.js deleted file mode 100644 index f1c853be23..0000000000 --- a/test/js/node/test/parallel/zlib-convenience-methods.test.js +++ /dev/null @@ -1,96 +0,0 @@ -//#FILE: test-zlib-convenience-methods.js -//#SHA1: e215a52650eaa95999dbb7d77f5b03376cdc673b -//----------------- -"use strict"; - -const zlib = require("zlib"); -const util = require("util"); -const { getBufferSources } = require("../common"); - -// Must be a multiple of 4 characters in total to test all ArrayBufferView -// types. -const expectStr = "blah".repeat(8); -const expectBuf = Buffer.from(expectStr); - -const opts = { - level: 9, - chunkSize: 1024, -}; - -const optsInfo = { - info: true, -}; - -const methodPairs = [ - ["gzip", "gunzip", "Gzip", "Gunzip"], - ["gzip", "unzip", "Gzip", "Unzip"], - ["deflate", "inflate", "Deflate", "Inflate"], - ["deflateRaw", "inflateRaw", "DeflateRaw", "InflateRaw"], - ["brotliCompress", "brotliDecompress", "BrotliCompress", "BrotliDecompress"], -]; - -const testCases = [ - ["string", expectStr], - ["Buffer", expectBuf], - ...getBufferSources(expectBuf).map(obj => [obj[Symbol.toStringTag], obj]), -]; - -describe("zlib convenience methods", () => { - describe.each(testCases)("%s input", (type, expected) => { - for (const [compress, decompress, CompressClass, DecompressClass] of methodPairs) { - describe(`${compress}/${decompress}`, async () => { - test("Async with options", async () => { - const c = await util.promisify(zlib[compress])(expected, opts); - const d = await util.promisify(zlib[decompress])(c, opts); - expect(d.toString()).toEqual(expectStr); - }); - - test("Async without options", async () => { - const c = await util.promisify(zlib[compress])(expected); - const d = await util.promisify(zlib[decompress])(c); - expect(d.toString()).toEqual(expectStr); - }); - - test("Async with info option", async () => { - const c = await util.promisify(zlib[compress])(expected, optsInfo); - expect(c.engine).toBeInstanceOf(zlib[CompressClass]); - const d = await util.promisify(zlib[decompress])(c.buffer, optsInfo); - expect(d.engine).toBeInstanceOf(zlib[DecompressClass]); - expect(d.buffer.toString()).toEqual(expectStr); - }); - - test("Sync with options", () => { - const c = zlib[compress + "Sync"](expected, opts); - const d = zlib[decompress + "Sync"](c, opts); - expect(d.toString()).toEqual(expectStr); - }); - - test("Sync without options", async () => { - const c = zlib[compress + "Sync"](expected); - const d = zlib[decompress + "Sync"](c); - expect(d.toString()).toEqual(expectStr); - }); - - test("Sync with info option", () => { - const c = zlib[compress + "Sync"](expected, optsInfo); - expect(c.engine).toBeInstanceOf(zlib[CompressClass]); - const d = zlib[decompress + "Sync"](c.buffer, optsInfo); - expect(d.engine).toBeInstanceOf(zlib[DecompressClass]); - expect(d.buffer.toString()).toEqual(expectStr); - }); - }); - } - }); - - test("throws error when callback is not provided", () => { - expect(() => zlib.gzip("abc")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.stringContaining('The "callback" argument must be of type function'), - }), - ); - }); -}); - -//<#END_FILE: test-zlib-convenience-methods.js diff --git a/test/js/node/test/parallel/zlib-crc32.test.js b/test/js/node/test/parallel/zlib-crc32.test.js deleted file mode 100644 index 5c1e51b63a..0000000000 --- a/test/js/node/test/parallel/zlib-crc32.test.js +++ /dev/null @@ -1,249 +0,0 @@ -//#FILE: test-zlib-crc32.js -//#SHA1: a46ac98c5c568eec892b62f9da1effc368081b07 -//----------------- -"use strict"; - -const zlib = require("zlib"); -const { Buffer } = require("buffer"); - -// The following test data comes from -// https://github.com/zlib-ng/zlib-ng/blob/5401b24/test/test_crc32.cc -// test_crc32.cc -- crc32 unit test -// Copyright (C) 2019-2021 IBM Corporation -// Authors: Rogerio Alves -// Matheus Castanho -// For conditions of distribution and use, see copyright notice in zlib.h -// -const tests = [ - [0x0, 0x0, 0, 0x0], - [0xffffffff, 0x0, 0, 0x0], - [0x0, 0x0, 255, 0x0] /* BZ 174799. */, - [0x0, 0x0, 256, 0x0], - [0x0, 0x0, 257, 0x0], - [0x0, 0x0, 32767, 0x0], - [0x0, 0x0, 32768, 0x0], - [0x0, 0x0, 32769, 0x0], - [0x0, "", 0, 0x0], - [0xffffffff, "", 0, 0xffffffff], - [0x0, "abacus", 6, 0xc3d7115b], - [0x0, "backlog", 7, 0x269205], - [0x0, "campfire", 8, 0x22a515f8], - [0x0, "delta", 5, 0x9643fed9], - [0x0, "executable", 10, 0xd68eda01], - [0x0, "file", 4, 0x8c9f3610], - [0x0, "greatest", 8, 0xc1abd6cd], - [0x0, "hello", 5, 0x3610a686], - [0x0, "inverter", 8, 0xc9e962c9], - [0x0, "jigsaw", 6, 0xce4e3f69], - [0x0, "karate", 6, 0x890be0e2], - [0x0, "landscape", 9, 0xc4e0330b], - [0x0, "machine", 7, 0x1505df84], - [0x0, "nanometer", 9, 0xd4e19f39], - [0x0, "oblivion", 8, 0xdae9de77], - [0x0, "panama", 6, 0x66b8979c], - [0x0, "quest", 5, 0x4317f817], - [0x0, "resource", 8, 0xbc91f416], - [0x0, "secret", 6, 0x5ca2e8e5], - [0x0, "test", 4, 0xd87f7e0c], - [0x0, "ultimate", 8, 0x3fc79b0b], - [0x0, "vector", 6, 0x1b6e485b], - [0x0, "walrus", 6, 0xbe769b97], - [0x0, "xeno", 4, 0xe7a06444], - [0x0, "yelling", 7, 0xfe3944e5], - [0x0, "zlib", 4, 0x73887d3a], - [0x0, "4BJD7PocN1VqX0jXVpWB", 20, 0xd487a5a1], - [0x0, "F1rPWI7XvDs6nAIRx41l", 20, 0x61a0132e], - [0x0, "ldhKlsVkPFOveXgkGtC2", 20, 0xdf02f76], - [0x0, "5KKnGOOrs8BvJ35iKTOS", 20, 0x579b2b0a], - [0x0, "0l1tw7GOcem06Ddu7yn4", 20, 0xf7d16e2d], - [0x0, "MCr47CjPIn9R1IvE1Tm5", 20, 0x731788f5], - [0x0, "UcixbzPKTIv0SvILHVdO", 20, 0x7112bb11], - [0x0, "dGnAyAhRQDsWw0ESou24", 20, 0xf32a0dac], - [0x0, "di0nvmY9UYMYDh0r45XT", 20, 0x625437bb], - [0x0, "2XKDwHfAhFsV0RhbqtvH", 20, 0x896930f9], - [0x0, "ZhrANFIiIvRnqClIVyeD", 20, 0x8579a37], - [0x0, "v7Q9ehzioTOVeDIZioT1", 20, 0x632aa8e0], - [0x0, "Yod5hEeKcYqyhfXbhxj2", 20, 0xc829af29], - [0x0, "GehSWY2ay4uUKhehXYb0", 20, 0x1b08b7e8], - [0x0, "kwytJmq6UqpflV8Y8GoE", 20, 0x4e33b192], - [0x0, "70684206568419061514", 20, 0x59a179f0], - [0x0, "42015093765128581010", 20, 0xcd1013d7], - [0x0, "88214814356148806939", 20, 0xab927546], - [0x0, "43472694284527343838", 20, 0x11f3b20c], - [0x0, "49769333513942933689", 20, 0xd562d4ca], - [0x0, "54979784887993251199", 20, 0x233395f7], - [0x0, "58360544869206793220", 20, 0x2d167fd5], - [0x0, "27347953487840714234", 20, 0x8b5108ba], - [0x0, "07650690295365319082", 20, 0xc46b3cd8], - [0x0, "42655507906821911703", 20, 0xc10b2662], - [0x0, "29977409200786225655", 20, 0xc9a0f9d2], - [0x0, "85181542907229116674", 20, 0x9341357b], - [0x0, "87963594337989416799", 20, 0xf0424937], - [0x0, "21395988329504168551", 20, 0xd7c4c31f], - [0x0, "51991013580943379423", 20, 0xf11edcc4], - [0x0, "*]+@!);({_$;}[_},?{?;(_?,=-][@", 30, 0x40795df4], - [0x0, "_@:_).&(#.[:[{[:)$++-($_;@[)}+", 30, 0xdd61a631], - [0x0, "&[!,[$_==}+.]@!;*(+},[;:)$;)-@", 30, 0xca907a99], - [0x0, "]{.[.+?+[[=;[?}_#&;[=)__$$:+=_", 30, 0xf652deac], - [0x0, "-%.)=/[@].:.(:,()$;=%@-$?]{%+%", 30, 0xaf39a5a9], - [0x0, "+]#$(@&.=:,*];/.!]%/{:){:@(;)$", 30, 0x6bebb4cf], - // eslint-disable-next-line no-template-curly-in-string - [0x0, ")-._.:?[&:.=+}(*$/=!.${;(=$@!}", 30, 0x76430bac], - [0x0, ":(_*&%/[[}+,?#$&*+#[([*-/#;%(]", 30, 0x6c80c388], - [0x0, "{[#-;:$/{)(+[}#]/{&!%(@)%:@-$:", 30, 0xd54d977d], - [0x0, "_{$*,}(&,@.)):=!/%(&(,,-?$}}}!", 30, 0xe3966ad5], - [ - 0x0, - "e$98KNzqaV)Y:2X?]77].{gKRD4G5{mHZk,Z)SpU%L3FSgv!Wb8MLAFdi{+fp)c,@8m6v)yXg@]HBDFk?.4&}g5_udE*JHCiH=aL", - 100, - 0xe7c71db9, - ], - [ - 0x0, - "r*Fd}ef+5RJQ;+W=4jTR9)R*p!B;]Ed7tkrLi;88U7g@3v!5pk2X6D)vt,.@N8c]@yyEcKi[vwUu@.Ppm@C6%Mv*3Nw}Y,58_aH)", - 100, - 0xeaa52777, - ], - [ - 0x0, - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&", - 100, - 0xcd472048, - ], - [0x7a30360d, "abacus", 6, 0xf8655a84], - [0x6fd767ee, "backlog", 7, 0x1ed834b1], - [0xefeb7589, "campfire", 8, 0x686cfca], - [0x61cf7e6b, "delta", 5, 0x1554e4b1], - [0xdc712e2, "executable", 10, 0x761b4254], - [0xad23c7fd, "file", 4, 0x7abdd09b], - [0x85cb2317, "greatest", 8, 0x4ba91c6b], - [0x9eed31b0, "inverter", 8, 0xd5e78ba5], - [0xb94f34ca, "jigsaw", 6, 0x23649109], - [0xab058a2, "karate", 6, 0xc5591f41], - [0x5bff2b7a, "landscape", 9, 0xf10eb644], - [0x605c9a5f, "machine", 7, 0xbaa0a636], - [0x51bdeea5, "nanometer", 9, 0x6af89afb], - [0x85c21c79, "oblivion", 8, 0xecae222b], - [0x97216f56, "panama", 6, 0x47dffac4], - [0x18444af2, "quest", 5, 0x70c2fe36], - [0xbe6ce359, "resource", 8, 0x1471d925], - [0x843071f1, "secret", 6, 0x50c9a0db], - [0xf2480c60, "ultimate", 8, 0xf973daf8], - [0x2d2feb3d, "vector", 6, 0x344ac03d], - [0x7490310a, "walrus", 6, 0x6d1408ef], - [0x97d247d4, "xeno", 4, 0xe62670b5], - [0x93cf7599, "yelling", 7, 0x1b36da38], - [0x73c84278, "zlib", 4, 0x6432d127], - [0x228a87d1, "4BJD7PocN1VqX0jXVpWB", 20, 0x997107d0], - [0xa7a048d0, "F1rPWI7XvDs6nAIRx41l", 20, 0xdc567274], - [0x1f0ded40, "ldhKlsVkPFOveXgkGtC2", 20, 0xdcc63870], - [0xa804a62f, "5KKnGOOrs8BvJ35iKTOS", 20, 0x6926cffd], - [0x508fae6a, "0l1tw7GOcem06Ddu7yn4", 20, 0xb52b38bc], - [0xe5adaf4f, "MCr47CjPIn9R1IvE1Tm5", 20, 0xf83b8178], - [0x67136a40, "UcixbzPKTIv0SvILHVdO", 20, 0xc5213070], - [0xb00c4a10, "dGnAyAhRQDsWw0ESou24", 20, 0xbc7648b0], - [0x2e0c84b5, "di0nvmY9UYMYDh0r45XT", 20, 0xd8123a72], - [0x81238d44, "2XKDwHfAhFsV0RhbqtvH", 20, 0xd5ac5620], - [0xf853aa92, "ZhrANFIiIvRnqClIVyeD", 20, 0xceae099d], - [0x5a692325, "v7Q9ehzioTOVeDIZioT1", 20, 0xb07d2b24], - [0x3275b9f, "Yod5hEeKcYqyhfXbhxj2", 20, 0x24ce91df], - [0x38371feb, "GehSWY2ay4uUKhehXYb0", 20, 0x707b3b30], - [0xafc8bf62, "kwytJmq6UqpflV8Y8GoE", 20, 0x16abc6a9], - [0x9b07db73, "70684206568419061514", 20, 0xae1fb7b7], - [0xe75b214, "42015093765128581010", 20, 0xd4eecd2d], - [0x72d0fe6f, "88214814356148806939", 20, 0x4660ec7], - [0xf857a4b1, "43472694284527343838", 20, 0xfd8afdf7], - [0x54b8e14, "49769333513942933689", 20, 0xc6d1b5f2], - [0xd6aa5616, "54979784887993251199", 20, 0x32476461], - [0x11e63098, "58360544869206793220", 20, 0xd917cf1a], - [0xbe92385, "27347953487840714234", 20, 0x4ad14a12], - [0x49511de0, "07650690295365319082", 20, 0xe37b5c6c], - [0x3db13bc1, "42655507906821911703", 20, 0x7cc497f1], - [0xbb899bea, "29977409200786225655", 20, 0x99781bb2], - [0xf6cd9436, "85181542907229116674", 20, 0x132256a1], - [0x9109e6c3, "87963594337989416799", 20, 0xbfdb2c83], - [0x75770fc, "21395988329504168551", 20, 0x8d9d1e81], - [0x69b1d19b, "51991013580943379423", 20, 0x7b6d4404], - [0xc6132975, "*]+@!);({_$;}[_},?{?;(_?,=-][@", 30, 0x8619f010], - [0xd58cb00c, "_@:_).&(#.[:[{[:)$++-($_;@[)}+", 30, 0x15746ac3], - [0xb63b8caa, "&[!,[$_==}+.]@!;*(+},[;:)$;)-@", 30, 0xaccf812f], - [0x8a45a2b8, "]{.[.+?+[[=;[?}_#&;[=)__$$:+=_", 30, 0x78af45de], - [0xcbe95b78, "-%.)=/[@].:.(:,()$;=%@-$?]{%+%", 30, 0x25b06b59], - [0x4ef8a54b, "+]#$(@&.=:,*];/.!]%/{:){:@(;)$", 30, 0x4ba0d08f], - // eslint-disable-next-line no-template-curly-in-string - [0x76ad267a, ")-._.:?[&:.=+}(*$/=!.${;(=$@!}", 30, 0xe26b6aac], - [0x569e613c, ":(_*&%/[[}+,?#$&*+#[([*-/#;%(]", 30, 0x7e2b0a66], - [0x36aa61da, "{[#-;:$/{)(+[}#]/{&!%(@)%:@-$:", 30, 0xb3430dc7], - [0xf67222df, "_{$*,}(&,@.)):=!/%(&(,,-?$}}}!", 30, 0x626c17a], - [ - 0x74b34fd3, - "e$98KNzqaV)Y:2X?]77].{gKRD4G5{mHZk,Z)SpU%L3FSgv!Wb8MLAFdi{+fp)c,@8m6v)yXg@]HBDFk?.4&}g5_udE*JHCiH=aL", - 100, - 0xccf98060, - ], - [ - 0x351fd770, - "r*Fd}ef+5RJQ;+W=4jTR9)R*p!B;]Ed7tkrLi;88U7g@3v!5pk2X6D)vt,.@N8c]@yyEcKi[vwUu@.Ppm@C6%Mv*3Nw}Y,58_aH)", - 100, - 0xd8b95312, - ], - [ - 0xc45aef77, - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&", - 100, - 0xbb1c9912, - ], - [ - 0xc45aef77, - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&" + - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&" + - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&" + - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&" + - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&" + - "h{bcmdC+a;t+Cf{6Y_dFq-{X4Yu&7uNfVDh?q&_u.UWJU],-GiH7ADzb7-V.Q%4=+v!$L9W+T=bP]$_:]Vyg}A.ygD.r;h-D]m%&", - 600, - 0x888afa5b, - ], -]; - -describe("zlib.crc32", () => { - test("crc32 test case", () => { - for (const [crc, data, len, expected] of tests) { - if (data === 0) { - return; - } - const buf = Buffer.from(data, "utf8"); - expect(buf.length).toBe(len); - expect(zlib.crc32(buf, crc)).toBe(expected); - expect(zlib.crc32(buf.toString(), crc)).toBe(expected); - if (crc === 0) { - expect(zlib.crc32(buf)).toBe(expected); - expect(zlib.crc32(buf.toString())).toBe(expected); - } - } - }); - - test("invalid input types", () => { - [undefined, null, true, 1, () => {}, {}].forEach(invalid => { - expect(() => zlib.crc32(invalid)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - }); - }); - - test("invalid crc types", () => { - [null, true, () => {}, {}].forEach(invalid => { - expect(() => zlib.crc32("test", invalid)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - }); - }); -}); - -//<#END_FILE: test-zlib-crc32.js diff --git a/test/js/node/test/parallel/zlib-create-raw.test.js b/test/js/node/test/parallel/zlib-create-raw.test.js deleted file mode 100644 index 4bd60d6d8e..0000000000 --- a/test/js/node/test/parallel/zlib-create-raw.test.js +++ /dev/null @@ -1,18 +0,0 @@ -//#FILE: test-zlib-create-raw.js -//#SHA1: 187539d5696ec6b7c567dfba0d1528c4b65d1e0a -//----------------- -"use strict"; - -const zlib = require("zlib"); - -test("zlib.createInflateRaw returns an instance of InflateRaw", () => { - const inflateRaw = zlib.createInflateRaw(); - expect(inflateRaw).toBeInstanceOf(zlib.InflateRaw); -}); - -test("zlib.createDeflateRaw returns an instance of DeflateRaw", () => { - const deflateRaw = zlib.createDeflateRaw(); - expect(deflateRaw).toBeInstanceOf(zlib.DeflateRaw); -}); - -//<#END_FILE: test-zlib-create-raw.js diff --git a/test/js/node/test/parallel/zlib-deflate-constructors.test.js b/test/js/node/test/parallel/zlib-deflate-constructors.test.js deleted file mode 100644 index d9a0411de2..0000000000 --- a/test/js/node/test/parallel/zlib-deflate-constructors.test.js +++ /dev/null @@ -1,281 +0,0 @@ -//#FILE: test-zlib-deflate-constructors.js -//#SHA1: fdacb219f8fcceeeb4800473b37fca16fcf4c241 -//----------------- -"use strict"; - -const zlib = require("zlib"); - -// Work with and without `new` keyword -test("Deflate constructor works with and without new keyword", () => { - expect(zlib.Deflate()).toBeInstanceOf(zlib.Deflate); - expect(new zlib.Deflate()).toBeInstanceOf(zlib.Deflate); -}); - -test("DeflateRaw constructor works with and without new keyword", () => { - expect(zlib.DeflateRaw()).toBeInstanceOf(zlib.DeflateRaw); - expect(new zlib.DeflateRaw()).toBeInstanceOf(zlib.DeflateRaw); -}); - -// Throws if `options.chunkSize` is invalid -test("Deflate constructor throws for invalid chunkSize", () => { - expect(() => new zlib.Deflate({ chunkSize: "test" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ chunkSize: -Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ chunkSize: 0 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -// Confirm that maximum chunk size cannot be exceeded because it is `Infinity`. -test("Z_MAX_CHUNK is Infinity", () => { - expect(zlib.constants.Z_MAX_CHUNK).toBe(Infinity); -}); - -// Throws if `options.windowBits` is invalid -test("Deflate constructor throws for invalid windowBits", () => { - expect(() => new zlib.Deflate({ windowBits: "test" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ windowBits: -Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ windowBits: Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ windowBits: 0 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -// Throws if `options.level` is invalid -test("Deflate constructor throws for invalid level", () => { - expect(() => new zlib.Deflate({ level: "test" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ level: -Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ level: Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ level: -2 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -// Throws if `level` invalid in `Deflate.prototype.params()` -test("Deflate.prototype.params throws for invalid level", () => { - expect(() => new zlib.Deflate().params("test")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate().params(-Infinity)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate().params(Infinity)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate().params(-2)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -// Throws if options.memLevel is invalid -test("Deflate constructor throws for invalid memLevel", () => { - expect(() => new zlib.Deflate({ memLevel: "test" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ memLevel: -Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ memLevel: Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ memLevel: -2 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -// Does not throw if opts.strategy is valid -test("Deflate constructor does not throw for valid strategy", () => { - expect(() => new zlib.Deflate({ strategy: zlib.constants.Z_FILTERED })).not.toThrow(); - expect(() => new zlib.Deflate({ strategy: zlib.constants.Z_HUFFMAN_ONLY })).not.toThrow(); - expect(() => new zlib.Deflate({ strategy: zlib.constants.Z_RLE })).not.toThrow(); - expect(() => new zlib.Deflate({ strategy: zlib.constants.Z_FIXED })).not.toThrow(); - expect(() => new zlib.Deflate({ strategy: zlib.constants.Z_DEFAULT_STRATEGY })).not.toThrow(); -}); - -// Throws if options.strategy is invalid -test("Deflate constructor throws for invalid strategy", () => { - expect(() => new zlib.Deflate({ strategy: "test" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ strategy: -Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ strategy: Infinity })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate({ strategy: -2 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -// Throws TypeError if `strategy` is invalid in `Deflate.prototype.params()` -test("Deflate.prototype.params throws for invalid strategy", () => { - expect(() => new zlib.Deflate().params(0, "test")).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate().params(0, -Infinity)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate().params(0, Infinity)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); - - expect(() => new zlib.Deflate().params(0, -2)).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -// Throws if opts.dictionary is not a Buffer -test("Deflate constructor throws if dictionary is not a Buffer", () => { - expect(() => new zlib.Deflate({ dictionary: "not a buffer" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-zlib-deflate-constructors.js diff --git a/test/js/node/test/parallel/zlib-deflate-raw-inherits.test.js b/test/js/node/test/parallel/zlib-deflate-raw-inherits.test.js deleted file mode 100644 index 1eb9243059..0000000000 --- a/test/js/node/test/parallel/zlib-deflate-raw-inherits.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-zlib-deflate-raw-inherits.js -//#SHA1: 9e1873864f4af27abf3a8a36a87edd2d036805d8 -//----------------- -"use strict"; - -const { DeflateRaw } = require("zlib"); -const { Readable } = require("stream"); - -// Validates that zlib.DeflateRaw can be inherited -// with Object.setPrototypeOf - -test("DeflateRaw can be inherited with Object.setPrototypeOf", done => { - function NotInitialized(options) { - DeflateRaw.call(this, options); - this.prop = true; - } - Object.setPrototypeOf(NotInitialized.prototype, DeflateRaw.prototype); - Object.setPrototypeOf(NotInitialized, DeflateRaw); - - const dest = new NotInitialized(); - - const read = new Readable({ - read() { - this.push(Buffer.from("a test string")); - this.push(null); - }, - }); - - read.pipe(dest); - dest.resume(); - - // We need to add an event listener to ensure the test completes - dest.on("finish", () => { - expect(dest.prop).toBe(true); - expect(dest instanceof DeflateRaw).toBe(true); - done(); - }); -}); - -//<#END_FILE: test-zlib-deflate-raw-inherits.js diff --git a/test/js/node/test/parallel/zlib-destroy-pipe.test.js b/test/js/node/test/parallel/zlib-destroy-pipe.test.js deleted file mode 100644 index 90866882de..0000000000 --- a/test/js/node/test/parallel/zlib-destroy-pipe.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-zlib-destroy-pipe.js -//#SHA1: 55e5ddd18c87bc58f331f82caa482cd49f5de168 -//----------------- -"use strict"; - -const zlib = require("zlib"); -const { Writable } = require("stream"); - -test("verify that the zlib transform does not error in case it is destroyed with data still in flight", () => { - const ts = zlib.createGzip(); - - const ws = new Writable({ - write(chunk, enc, cb) { - setImmediate(cb); - ts.destroy(); - }, - }); - - const buf = Buffer.allocUnsafe(1024 * 1024 * 20); - ts.end(buf); - ts.pipe(ws); -}); - -//<#END_FILE: test-zlib-destroy-pipe.js diff --git a/test/js/node/test/parallel/zlib-destroy.test.js b/test/js/node/test/parallel/zlib-destroy.test.js deleted file mode 100644 index 0947ce2827..0000000000 --- a/test/js/node/test/parallel/zlib-destroy.test.js +++ /dev/null @@ -1,40 +0,0 @@ -//#FILE: test-zlib-destroy.js -//#SHA1: b28cfad7c9e73659c624238b74dcc38146c94203 -//----------------- -"use strict"; - -const zlib = require("zlib"); - -// Verify that the zlib transform does clean up -// the handle when calling destroy. - -test("zlib transform cleans up handle on destroy", done => { - const ts = zlib.createGzip(); - ts.destroy(); - expect(ts._handle).toBeNull(); - - ts.on("close", () => { - ts.close(() => { - done(); - }); - }); -}); - -test("error is only emitted once", done => { - const decompress = zlib.createGunzip(15); - - let errorCount = 0; - decompress.on("error", err => { - errorCount++; - decompress.close(); - - // Ensure this callback is only called once - expect(errorCount).toBe(1); - done(); - }); - - decompress.write("something invalid"); - decompress.destroy(new Error("asd")); -}); - -//<#END_FILE: test-zlib-destroy.js diff --git a/test/js/node/test/parallel/zlib-dictionary-fail.test.js b/test/js/node/test/parallel/zlib-dictionary-fail.test.js deleted file mode 100644 index 4085281359..0000000000 --- a/test/js/node/test/parallel/zlib-dictionary-fail.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-zlib-dictionary-fail.js -//#SHA1: e9c6d383f9b0a202067a125c016f1ef3cd5be558 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const zlib = require("zlib"); - -// String "test" encoded with dictionary "dict". -const input = Buffer.from([0x78, 0xbb, 0x04, 0x09, 0x01, 0xa5]); - -test("zlib.createInflate without dictionary", async () => { - const stream = zlib.createInflate(); - - await expect( - new Promise((resolve, reject) => { - stream.on("error", reject); - stream.write(input); - }), - ).rejects.toThrow( - expect.objectContaining({ - message: expect.stringMatching(/Missing dictionary/), - }), - ); -}); - -test("zlib.createInflate with wrong dictionary", async () => { - const stream = zlib.createInflate({ dictionary: Buffer.from("fail") }); - - await expect( - new Promise((resolve, reject) => { - stream.on("error", reject); - stream.write(input); - }), - ).rejects.toThrow( - expect.objectContaining({ - message: expect.stringMatching(/Bad dictionary/), - }), - ); -}); - -test("zlib.createInflateRaw with wrong dictionary", async () => { - const stream = zlib.createInflateRaw({ dictionary: Buffer.from("fail") }); - - await expect( - new Promise((resolve, reject) => { - stream.on("error", reject); - stream.write(input); - }), - ).rejects.toThrow( - expect.objectContaining({ - message: expect.stringMatching(/(invalid|Operation-Ending-Supplemental Code is 0x12)/), - }), - ); -}); - -//<#END_FILE: test-zlib-dictionary-fail.js diff --git a/test/js/node/test/parallel/zlib-dictionary.test.js b/test/js/node/test/parallel/zlib-dictionary.test.js deleted file mode 100644 index e7b9fbc82a..0000000000 --- a/test/js/node/test/parallel/zlib-dictionary.test.js +++ /dev/null @@ -1,176 +0,0 @@ -//#FILE: test-zlib-dictionary.js -//#SHA1: b5fc5a33125dfeaa9965b0564a8196b056b95918 -//----------------- -"use strict"; - -const zlib = require("zlib"); - -const spdyDict = Buffer.from( - [ - "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-", - "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi", - "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser", - "-agent10010120020120220320420520630030130230330430530630740040140240340440", - "5406407408409410411412413414415416417500501502503504505accept-rangesageeta", - "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic", - "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran", - "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati", - "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo", - "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe", - "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic", - "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1", - ".1statusversionurl\0", - ].join(""), -); - -const input = ["HTTP/1.1 200 Ok", "Server: node.js", "Content-Length: 0", ""].join("\r\n"); - -function basicDictionaryTest(spdyDict) { - return new Promise(resolve => { - let output = ""; - const deflate = zlib.createDeflate({ dictionary: spdyDict }); - const inflate = zlib.createInflate({ dictionary: spdyDict }); - inflate.setEncoding("utf-8"); - - deflate.on("data", chunk => { - inflate.write(chunk); - }); - - inflate.on("data", chunk => { - output += chunk; - }); - - deflate.on("end", () => { - inflate.end(); - }); - - inflate.on("end", () => { - expect(output).toBe(input); - resolve(); - }); - - deflate.write(input); - deflate.end(); - }); -} - -function deflateResetDictionaryTest(spdyDict) { - return new Promise(resolve => { - let doneReset = false; - let output = ""; - const deflate = zlib.createDeflate({ dictionary: spdyDict }); - const inflate = zlib.createInflate({ dictionary: spdyDict }); - inflate.setEncoding("utf-8"); - - deflate.on("data", chunk => { - if (doneReset) inflate.write(chunk); - }); - - inflate.on("data", chunk => { - output += chunk; - }); - - deflate.on("end", () => { - inflate.end(); - }); - - inflate.on("end", () => { - expect(output).toBe(input); - resolve(); - }); - - deflate.write(input); - deflate.flush(() => { - deflate.reset(); - doneReset = true; - deflate.write(input); - deflate.end(); - }); - }); -} - -function rawDictionaryTest(spdyDict) { - return new Promise(resolve => { - let output = ""; - const deflate = zlib.createDeflateRaw({ dictionary: spdyDict }); - const inflate = zlib.createInflateRaw({ dictionary: spdyDict }); - inflate.setEncoding("utf-8"); - - deflate.on("data", chunk => { - inflate.write(chunk); - }); - - inflate.on("data", chunk => { - output += chunk; - }); - - deflate.on("end", () => { - inflate.end(); - }); - - inflate.on("end", () => { - expect(output).toBe(input); - resolve(); - }); - - deflate.write(input); - deflate.end(); - }); -} - -function deflateRawResetDictionaryTest(spdyDict) { - return new Promise(resolve => { - let doneReset = false; - let output = ""; - const deflate = zlib.createDeflateRaw({ dictionary: spdyDict }); - const inflate = zlib.createInflateRaw({ dictionary: spdyDict }); - inflate.setEncoding("utf-8"); - - deflate.on("data", chunk => { - if (doneReset) inflate.write(chunk); - }); - - inflate.on("data", chunk => { - output += chunk; - }); - - deflate.on("end", () => { - inflate.end(); - }); - - inflate.on("end", () => { - expect(output).toBe(input); - resolve(); - }); - - deflate.write(input); - deflate.flush(() => { - deflate.reset(); - doneReset = true; - deflate.write(input); - deflate.end(); - }); - }); -} - -const dictionaries = [spdyDict, Buffer.from(spdyDict), new Uint8Array(spdyDict)]; - -describe("zlib dictionary tests", () => { - test.each(dictionaries)("basic dictionary test", async dict => { - await basicDictionaryTest(dict); - }); - - test.each(dictionaries)("deflate reset dictionary test", async dict => { - await deflateResetDictionaryTest(dict); - }); - - test.each(dictionaries)("raw dictionary test", async dict => { - await rawDictionaryTest(dict); - }); - - test.each(dictionaries)("deflate raw reset dictionary test", async dict => { - await deflateRawResetDictionaryTest(dict); - }); -}); - -//<#END_FILE: test-zlib-dictionary.js diff --git a/test/js/node/test/parallel/zlib-empty-buffer.test.js b/test/js/node/test/parallel/zlib-empty-buffer.test.js deleted file mode 100644 index 553415206f..0000000000 --- a/test/js/node/test/parallel/zlib-empty-buffer.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-zlib-empty-buffer.js -//#SHA1: 7a2e8687dcd6e4bd815aa22c2bbff08251d9d91a -//----------------- -"use strict"; -const zlib = require("zlib"); -const { inspect, promisify } = require("util"); -const emptyBuffer = Buffer.alloc(0); - -describe("zlib empty buffer", () => { - const testCases = [ - [zlib.deflateRawSync, zlib.inflateRawSync, "raw sync"], - [zlib.deflateSync, zlib.inflateSync, "deflate sync"], - [zlib.gzipSync, zlib.gunzipSync, "gzip sync"], - [zlib.brotliCompressSync, zlib.brotliDecompressSync, "br sync"], - [promisify(zlib.deflateRaw), promisify(zlib.inflateRaw), "raw"], - [promisify(zlib.deflate), promisify(zlib.inflate), "deflate"], - [promisify(zlib.gzip), promisify(zlib.gunzip), "gzip"], - [promisify(zlib.brotliCompress), promisify(zlib.brotliDecompress), "br"], - ]; - - test.each(testCases)("compresses and decompresses empty buffer using %s", async (compress, decompress, method) => { - const compressed = await compress(emptyBuffer); - const decompressed = await decompress(compressed); - expect(decompressed).toStrictEqual(emptyBuffer); - }); -}); - -//<#END_FILE: test-zlib-empty-buffer.js diff --git a/test/js/node/test/parallel/zlib-failed-init.test.js b/test/js/node/test/parallel/zlib-failed-init.test.js deleted file mode 100644 index 1f47ce229d..0000000000 --- a/test/js/node/test/parallel/zlib-failed-init.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-zlib-failed-init.js -//#SHA1: a1c770e81c677ebefa0e5969e3441b28e63ab75a -//----------------- -"use strict"; - -const zlib = require("zlib"); - -test("zlib createGzip with invalid chunkSize", () => { - expect(() => zlib.createGzip({ chunkSize: 0 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - }), - ); -}); - -test("zlib createGzip with invalid windowBits", () => { - expect(() => zlib.createGzip({ windowBits: 0 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - }), - ); -}); - -test("zlib createGzip with invalid memLevel", () => { - expect(() => zlib.createGzip({ memLevel: 0 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - }), - ); -}); - -test("zlib createGzip with NaN level", () => { - const stream = zlib.createGzip({ level: NaN }); - expect(stream._level).toBe(zlib.constants.Z_DEFAULT_COMPRESSION); -}); - -test("zlib createGzip with NaN strategy", () => { - const stream = zlib.createGzip({ strategy: NaN }); - expect(stream._strategy).toBe(zlib.constants.Z_DEFAULT_STRATEGY); -}); - -//<#END_FILE: test-zlib-failed-init.js diff --git a/test/js/node/test/parallel/zlib-flush-drain-longblock.test.js b/test/js/node/test/parallel/zlib-flush-drain-longblock.test.js deleted file mode 100644 index 21f8a7d720..0000000000 --- a/test/js/node/test/parallel/zlib-flush-drain-longblock.test.js +++ /dev/null @@ -1,34 +0,0 @@ -//#FILE: test-zlib-flush-drain-longblock.js -//#SHA1: 95927f13fbb59e0a8a2a32c14d0443fc110bab6e -//----------------- -"use strict"; - -const zlib = require("zlib"); - -test("zlib flush interacts properly with writableState.needDrain", done => { - const zipper = zlib.createGzip({ highWaterMark: 16384 }); - const unzipper = zlib.createGunzip(); - zipper.pipe(unzipper); - - zipper.write("A".repeat(17000)); - zipper.flush(); - - let received = 0; - let dataCallCount = 0; - - unzipper.on("data", d => { - received += d.length; - dataCallCount++; - }); - - unzipper.on("end", () => { - expect(received).toBe(17000); - expect(dataCallCount).toBeGreaterThanOrEqual(2); - done(); - }); - - // Properly end the streams to ensure all data is processed - zipper.end(); -}); - -//<#END_FILE: test-zlib-flush-drain-longblock.js diff --git a/test/js/node/test/parallel/zlib-flush-drain.test.js b/test/js/node/test/parallel/zlib-flush-drain.test.js deleted file mode 100644 index b4407e7cbd..0000000000 --- a/test/js/node/test/parallel/zlib-flush-drain.test.js +++ /dev/null @@ -1,53 +0,0 @@ -//#FILE: test-zlib-flush-drain.js -//#SHA1: 2f83bee63a56543c9824833e4fa7d8f5b33a373e -//----------------- -"use strict"; -const zlib = require("zlib"); - -const bigData = Buffer.alloc(10240, "x"); - -const opts = { - level: 0, - highWaterMark: 16, -}; - -let flushCount = 0; -let drainCount = 0; -let beforeFlush, afterFlush; - -test("zlib flush and drain behavior", done => { - const deflater = zlib.createDeflate(opts); - - // Shim deflater.flush so we can count times executed - const flush = deflater.flush; - deflater.flush = function (kind, callback) { - flushCount++; - flush.call(this, kind, callback); - }; - - deflater.write(bigData); - - const ws = deflater._writableState; - beforeFlush = ws.needDrain; - - deflater.on("data", () => {}); - - deflater.flush(function (err) { - afterFlush = ws.needDrain; - }); - - deflater.on("drain", function () { - drainCount++; - }); - - // Use setTimeout to ensure all asynchronous operations have completed - setTimeout(() => { - expect(beforeFlush).toBe(true); - expect(afterFlush).toBe(false); - expect(drainCount).toBe(1); - expect(flushCount).toBe(1); - done(); - }, 100); -}); - -//<#END_FILE: test-zlib-flush-drain.js diff --git a/test/js/node/test/parallel/zlib-flush-flags.test.js b/test/js/node/test/parallel/zlib-flush-flags.test.js deleted file mode 100644 index 306fe03791..0000000000 --- a/test/js/node/test/parallel/zlib-flush-flags.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-zlib-flush-flags.js -//#SHA1: 9fb4201163deb1a6dc1f74c5c7a2723ec1a9d342 -//----------------- -"use strict"; - -const zlib = require("zlib"); - -test("createGzip with valid flush option", () => { - expect(() => zlib.createGzip({ flush: zlib.constants.Z_SYNC_FLUSH })).not.toThrow(); -}); - -test("createGzip with invalid flush option type", () => { - expect(() => zlib.createGzip({ flush: "foobar" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -test("createGzip with out of range flush option", () => { - expect(() => zlib.createGzip({ flush: 10000 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -test("createGzip with valid finishFlush option", () => { - expect(() => zlib.createGzip({ finishFlush: zlib.constants.Z_SYNC_FLUSH })).not.toThrow(); -}); - -test("createGzip with invalid finishFlush option type", () => { - expect(() => zlib.createGzip({ finishFlush: "foobar" })).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: expect.any(String), - }), - ); -}); - -test("createGzip with out of range finishFlush option", () => { - expect(() => zlib.createGzip({ finishFlush: 10000 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-zlib-flush-flags.js diff --git a/test/js/node/test/parallel/zlib-flush-write-sync-interleaved.test.js b/test/js/node/test/parallel/zlib-flush-write-sync-interleaved.test.js deleted file mode 100644 index 1661372b3d..0000000000 --- a/test/js/node/test/parallel/zlib-flush-write-sync-interleaved.test.js +++ /dev/null @@ -1,66 +0,0 @@ -//#FILE: test-zlib-flush-write-sync-interleaved.js -//#SHA1: 35bfe36486f112686943448a115a586035455ba7 -//----------------- -"use strict"; -const { createGzip, createGunzip, Z_PARTIAL_FLUSH } = require("zlib"); - -// Verify that .flush() behaves like .write() in terms of ordering, e.g. in -// a sequence like .write() + .flush() + .write() + .flush() each .flush() call -// only affects the data written before it. -// Refs: https://github.com/nodejs/node/issues/28478 - -test("zlib flush and write ordering", done => { - const compress = createGzip(); - const decompress = createGunzip(); - decompress.setEncoding("utf8"); - - const events = []; - const compressedChunks = []; - - for (const chunk of ["abc", "def", "ghi"]) { - compress.write(chunk, () => events.push({ written: chunk })); - compress.flush(Z_PARTIAL_FLUSH, () => { - events.push("flushed"); - const chunk = compress.read(); - if (chunk !== null) compressedChunks.push(chunk); - }); - } - - compress.end(() => { - events.push("compress end"); - writeToDecompress(); - }); - - function writeToDecompress() { - // Write the compressed chunks to a decompressor, one by one, in order to - // verify that the flushes actually worked. - const chunk = compressedChunks.shift(); - if (chunk === undefined) { - decompress.end(); - checkResults(); - return; - } - decompress.write(chunk, () => { - events.push({ read: decompress.read() }); - writeToDecompress(); - }); - } - - function checkResults() { - expect(events).toEqual([ - { written: "abc" }, - "flushed", - { written: "def" }, - "flushed", - { written: "ghi" }, - "flushed", - "compress end", - { read: "abc" }, - { read: "def" }, - { read: "ghi" }, - ]); - done(); - } -}); - -//<#END_FILE: test-zlib-flush-write-sync-interleaved.js diff --git a/test/js/node/test/parallel/zlib-flush.test.js b/test/js/node/test/parallel/zlib-flush.test.js deleted file mode 100644 index cfda6ffb6d..0000000000 --- a/test/js/node/test/parallel/zlib-flush.test.js +++ /dev/null @@ -1,38 +0,0 @@ -//#FILE: test-zlib-flush.js -//#SHA1: 61f325893c63c826c2a498bc52ef3401f9a5a542 -//----------------- -"use strict"; - -const zlib = require("node:zlib"); - -test("zlib flush", async () => { - const opts = { level: 0 }; - const deflater = zlib.createDeflate(opts); - const chunk = Buffer.from("/9j/4AAQSkZJRgABAQEASA==", "base64"); - const expectedNone = Buffer.from([0x78, 0x01]); - const blkhdr = Buffer.from([0x00, 0x10, 0x00, 0xef, 0xff]); - const adler32 = Buffer.from([0x00, 0x00, 0x00, 0xff, 0xff]); - const expectedFull = Buffer.concat([blkhdr, chunk, adler32]); - let actualNone; - let actualFull; - - await new Promise(resolve => { - deflater.write(chunk, function () { - deflater.flush(zlib.constants.Z_NO_FLUSH, function () { - actualNone = deflater.read(); - deflater.flush(function () { - const bufs = []; - let buf; - while ((buf = deflater.read()) !== null) bufs.push(buf); - actualFull = Buffer.concat(bufs); - resolve(); - }); - }); - }); - }); - - expect(actualNone).toEqual(expectedNone); - expect(actualFull).toEqual(expectedFull); -}); - -//<#END_FILE: test-zlib-flush.js diff --git a/test/js/node/test/parallel/zlib-from-concatenated-gzip.test.js b/test/js/node/test/parallel/zlib-from-concatenated-gzip.test.js deleted file mode 100644 index 1961f9e215..0000000000 --- a/test/js/node/test/parallel/zlib-from-concatenated-gzip.test.js +++ /dev/null @@ -1,99 +0,0 @@ -//#FILE: test-zlib-from-concatenated-gzip.js -//#SHA1: cf8f45097fb201dc583ee154b34585b6a0a0dc34 -//----------------- -"use strict"; -// Test unzipping a gzip file that contains multiple concatenated "members" - -const zlib = require("zlib"); -const fs = require("fs"); -const path = require("path"); - -const abc = "abc"; -const def = "def"; - -const abcEncoded = zlib.gzipSync(abc); -const defEncoded = zlib.gzipSync(def); - -const data = Buffer.concat([abcEncoded, defEncoded]); - -test("gunzipSync concatenated gzip members", () => { - expect(zlib.gunzipSync(data).toString()).toBe(abc + def); -}); - -test("gunzip concatenated gzip members", async () => { - const result = await new Promise((resolve, reject) => { - zlib.gunzip(data, (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }); - expect(result.toString()).toBe(abc + def); -}); - -test("unzip concatenated gzip members", async () => { - const result = await new Promise((resolve, reject) => { - zlib.unzip(data, (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }); - expect(result.toString()).toBe(abc + def); -}); - -test("unzip concatenated deflate members", async () => { - const result = await new Promise((resolve, reject) => { - zlib.unzip(Buffer.concat([zlib.deflateSync("abc"), zlib.deflateSync("def")]), (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }); - expect(result.toString()).toBe(abc); -}); - -test("pseudo-multimember gzip file", async () => { - const pmmFileZlib = path.join(__dirname, "../fixtures/pseudo-multimember-gzip.z"); - const pmmFileGz = path.join(__dirname, "../fixtures/pseudo-multimember-gzip.gz"); - - const pmmExpected = zlib.inflateSync(fs.readFileSync(pmmFileZlib)); - const pmmResultBuffers = []; - - await new Promise((resolve, reject) => { - fs.createReadStream(pmmFileGz) - .pipe(zlib.createGunzip()) - .on("error", reject) - .on("data", data => pmmResultBuffers.push(data)) - .on("finish", resolve); - }); - - // Result should match original random garbage - expect(Buffer.concat(pmmResultBuffers)).toEqual(pmmExpected); -}); - -test("gzip member wrapping around input buffer boundary", async () => { - const offsets = [0, 1, 2, 3, 4, defEncoded.length]; - - for (const offset of offsets) { - const resultBuffers = []; - - await new Promise((resolve, reject) => { - const unzip = zlib - .createGunzip() - .on("error", reject) - .on("data", data => resultBuffers.push(data)) - .on("finish", resolve); - - // First write: write "abc" + the first bytes of "def" - unzip.write(Buffer.concat([abcEncoded, defEncoded.slice(0, offset)])); - - // Write remaining bytes of "def" - unzip.end(defEncoded.slice(offset)); - }); - - expect(Buffer.concat(resultBuffers).toString()).toBe( - "abcdef", - `result should match original input (offset = ${offset})`, - ); - } -}); - -//<#END_FILE: test-zlib-from-concatenated-gzip.js diff --git a/test/js/node/test/parallel/zlib-from-gzip-with-trailing-garbage.test.js b/test/js/node/test/parallel/zlib-from-gzip-with-trailing-garbage.test.js deleted file mode 100644 index 4b2371eabd..0000000000 --- a/test/js/node/test/parallel/zlib-from-gzip-with-trailing-garbage.test.js +++ /dev/null @@ -1,76 +0,0 @@ -//#FILE: test-zlib-from-gzip-with-trailing-garbage.js -//#SHA1: 8c3ebbede1912a48995aaada3c3e470d48bc01f3 -//----------------- -"use strict"; -const zlib = require("zlib"); - -describe("Unzipping a gzip file with trailing garbage", () => { - test("Should ignore trailing null-bytes", () => { - const data = Buffer.concat([zlib.gzipSync("abc"), zlib.gzipSync("def"), Buffer.alloc(10)]); - - expect(zlib.gunzipSync(data).toString()).toBe("abcdef"); - }); - - test("Should ignore trailing null-bytes (async)", async () => { - const data = Buffer.concat([zlib.gzipSync("abc"), zlib.gzipSync("def"), Buffer.alloc(10)]); - - const result = await new Promise((resolve, reject) => { - zlib.gunzip(data, (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }); - - expect(result.toString()).toBe("abcdef"); - }); - - test("Should throw error if trailing garbage looks like a gzip header", () => { - const data = Buffer.concat([ - zlib.gzipSync("abc"), - zlib.gzipSync("def"), - Buffer.from([0x1f, 0x8b, 0xff, 0xff]), - Buffer.alloc(10), - ]); - - expect(() => zlib.gunzipSync(data)).toThrow("unknown compression method"); - }); - - test("Should throw error if trailing garbage looks like a gzip header (async)", async () => { - const data = Buffer.concat([ - zlib.gzipSync("abc"), - zlib.gzipSync("def"), - Buffer.from([0x1f, 0x8b, 0xff, 0xff]), - Buffer.alloc(10), - ]); - - await expect( - new Promise((resolve, reject) => { - zlib.gunzip(data, (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }), - ).rejects.toThrow("unknown compression method"); - }); - - test("Should throw error if trailing junk is too short to be a gzip segment", () => { - const data = Buffer.concat([zlib.gzipSync("abc"), zlib.gzipSync("def"), Buffer.from([0x1f, 0x8b, 0xff, 0xff])]); - - expect(() => zlib.gunzipSync(data)).toThrow("unknown compression method"); - }); - - test("Should throw error if trailing junk is too short to be a gzip segment (async)", async () => { - const data = Buffer.concat([zlib.gzipSync("abc"), zlib.gzipSync("def"), Buffer.from([0x1f, 0x8b, 0xff, 0xff])]); - - await expect( - new Promise((resolve, reject) => { - zlib.gunzip(data, (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }), - ).rejects.toThrow("unknown compression method"); - }); -}); - -//<#END_FILE: test-zlib-from-gzip-with-trailing-garbage.js diff --git a/test/js/node/test/parallel/zlib-from-gzip.test.js b/test/js/node/test/parallel/zlib-from-gzip.test.js deleted file mode 100644 index c1406008f2..0000000000 --- a/test/js/node/test/parallel/zlib-from-gzip.test.js +++ /dev/null @@ -1,41 +0,0 @@ -//#FILE: test-zlib-from-gzip.js -//#SHA1: 44570a611316087d64497151e9ed570aca9060d5 -//----------------- -"use strict"; - -import { tmpdirSync } from "harness"; - -const assert = require("assert"); -const path = require("path"); -const zlib = require("zlib"); -const fixtures = require("../common/fixtures"); -const fs = require("fs"); - -test("test unzipping a file that was created with a non-node gzip lib, piped in as fast as possible", async () => { - const x = tmpdirSync(); - const gunzip = zlib.createGunzip(); - const fixture = fixtures.path("person.jpg.gz"); - const unzippedFixture = fixtures.path("person.jpg"); - const outputFile = path.resolve(x, "person.jpg"); - const expected = fs.readFileSync(unzippedFixture); - const inp = fs.createReadStream(fixture); - const out = fs.createWriteStream(outputFile); - - inp.pipe(gunzip).pipe(out); - - const { promise, resolve, reject } = Promise.withResolvers(); - - out.on("close", () => { - try { - const actual = fs.readFileSync(outputFile); - expect(actual.length).toBe(expected.length); - expect(actual).toEqual(expected); - resolve(); - } catch (e) { - reject(e); - } - }); - await promise; -}); - -//<#END_FILE: test-zlib-from-gzip.js diff --git a/test/js/node/test/parallel/zlib-from-string.test.js b/test/js/node/test/parallel/zlib-from-string.test.js deleted file mode 100644 index a9b8900d7f..0000000000 --- a/test/js/node/test/parallel/zlib-from-string.test.js +++ /dev/null @@ -1,121 +0,0 @@ -//#FILE: test-zlib-from-string.js -//#SHA1: 0514669607bbf01e20f41875fa716660ebfcf28b -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Test compressing and uncompressing a string with zlib - -const zlib = require("zlib"); - -const inputString = - "ΩΩLorem ipsum dolor sit amet, consectetur adipiscing eli" + - "t. Morbi faucibus, purus at gravida dictum, libero arcu " + - "convallis lacus, in commodo libero metus eu nisi. Nullam" + - " commodo, neque nec porta placerat, nisi est fermentum a" + - "ugue, vitae gravida tellus sapien sit amet tellus. Aenea" + - "n non diam orci. Proin quis elit turpis. Suspendisse non" + - " diam ipsum. Suspendisse nec ullamcorper odio. Vestibulu" + - "m arcu mi, sodales non suscipit id, ultrices ut massa. S" + - "ed ac sem sit amet arcu malesuada fermentum. Nunc sed. "; -const expectedBase64Deflate = - "eJxdUUtOQzEMvMoc4OndgT0gJCT2buJWlpI4jePeqZfpmX" + - "AKLRKbLOzx/HK73q6vOrhCunlF1qIDJhNUeW5I2ozT5OkD" + - "lKWLJWkncJG5403HQXAkT3Jw29B9uIEmToMukglZ0vS6oc" + - "iBh4JG8sV4oVLEUCitK2kxq1WzPnChHDzsaGKy491LofoA" + - "bWh8do43oeuYhB5EPCjcLjzYJo48KrfQBvnJecNFJvHT1+" + - "RSQsGoC7dn2t/xjhduTA1NWyQIZR0pbHwMDatnD+crPqKS" + - "qGPHp1vnlsWM/07ubf7bheF7kqSj84Bm0R1fYTfaK8vqqq" + - "fKBtNMhe3OZh6N95CTvMX5HJJi4xOVzCgUOIMSLH7wmeOH" + - "aFE4RdpnGavKtrB5xzfO/Ll9"; -const expectedBase64Gzip = - "H4sIAAAAAAAAA11RS05DMQy8yhzg6d2BPSAkJPZu4laWkjiN4" + - "96pl+mZcAotEpss7PH8crverq86uEK6eUXWogMmE1R5bkjajN" + - "Pk6QOUpYslaSdwkbnjTcdBcCRPcnDb0H24gSZOgy6SCVnS9Lq" + - "hyIGHgkbyxXihUsRQKK0raTGrVbM+cKEcPOxoYrLj3Uuh+gBt" + - "aHx2jjeh65iEHkQ8KNwuPNgmjjwqt9AG+cl5w0Um8dPX5FJCw" + - "agLt2fa3/GOF25MDU1bJAhlHSlsfAwNq2cP5ys+opKoY8enW+" + - "eWxYz/Tu5t/tuF4XuSpKPzgGbRHV9hN9ory+qqp8oG00yF7c5" + - "mHo33kJO8xfkckmLjE5XMKBQ4gxIsfvCZ44doUThF2mcZq8q2" + - "sHnHNzRtagj5AQAA"; - -test("deflate and inflate", async () => { - const buffer = await new Promise((resolve, reject) => { - zlib.deflate(inputString, (err, buffer) => { - if (err) reject(err); - else resolve(buffer); - }); - }); - - const inflated = await new Promise((resolve, reject) => { - zlib.inflate(buffer, (err, inflated) => { - if (err) reject(err); - else resolve(inflated); - }); - }); - - expect(inflated.toString()).toBe(inputString); -}); - -test("gzip and gunzip", async () => { - const buffer = await new Promise((resolve, reject) => { - zlib.gzip(inputString, (err, buffer) => { - if (err) reject(err); - else resolve(buffer); - }); - }); - - const gunzipped = await new Promise((resolve, reject) => { - zlib.gunzip(buffer, (err, gunzipped) => { - if (err) reject(err); - else resolve(gunzipped); - }); - }); - - expect(gunzipped.toString()).toBe(inputString); -}); - -test("unzip base64 deflate", async () => { - const buffer = Buffer.from(expectedBase64Deflate, "base64"); - const unzipped = await new Promise((resolve, reject) => { - zlib.unzip(buffer, (err, unzipped) => { - if (err) reject(err); - else resolve(unzipped); - }); - }); - - expect(unzipped.toString()).toBe(inputString); -}); - -test("unzip base64 gzip", async () => { - const buffer = Buffer.from(expectedBase64Gzip, "base64"); - const unzipped = await new Promise((resolve, reject) => { - zlib.unzip(buffer, (err, unzipped) => { - if (err) reject(err); - else resolve(unzipped); - }); - }); - - expect(unzipped.toString()).toBe(inputString); -}); - -//<#END_FILE: test-zlib-from-string.js diff --git a/test/js/node/test/parallel/zlib-invalid-arg-value-brotli-compress.test.js b/test/js/node/test/parallel/zlib-invalid-arg-value-brotli-compress.test.js deleted file mode 100644 index 4fbb56f1a0..0000000000 --- a/test/js/node/test/parallel/zlib-invalid-arg-value-brotli-compress.test.js +++ /dev/null @@ -1,27 +0,0 @@ -//#FILE: test-zlib-invalid-arg-value-brotli-compress.js -//#SHA1: fab88f892e21351603b5ae7227837d01149bdae6 -//----------------- -"use strict"; - -// This test ensures that the BrotliCompress function throws -// ERR_INVALID_ARG_TYPE when the values of the `params` key-value object are -// neither numbers nor booleans. - -const { BrotliCompress, constants } = require("zlib"); - -test("BrotliCompress throws ERR_INVALID_ARG_TYPE for invalid params value", () => { - const opts = { - params: { - [constants.BROTLI_PARAM_MODE]: "lol", - }, - }; - - expect(() => BrotliCompress(opts)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-zlib-invalid-arg-value-brotli-compress.js diff --git a/test/js/node/test/parallel/zlib-invalid-input-memory.test.js b/test/js/node/test/parallel/zlib-invalid-input-memory.test.js deleted file mode 100644 index 7193d0a531..0000000000 --- a/test/js/node/test/parallel/zlib-invalid-input-memory.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-zlib-invalid-input-memory.js -//#SHA1: 2607db89f2850bfbe75959ca2cd56e647b9eac78 -//----------------- -"use strict"; -const zlib = require("zlib"); -const onGC = require("../common/ongc"); -const common = require("../common"); - -const ongc = common.mustCall(); - -test.todo("zlib context with error can be garbage collected", () => { - const input = Buffer.from("foobar"); - const strm = zlib.createInflate(); - - strm.end(input); - - strm.once("error", err => { - expect(err).toBeTruthy(); - - setImmediate(() => { - global.gc(); - // Keep the event loop alive for seeing the async_hooks destroy hook we use for GC tracking... - // TODO(addaleax): This should maybe not be necessary? - setImmediate(() => {}); - }); - }); - onGC(strm, { ongc }); -}); - -//<#END_FILE: test-zlib-invalid-input-memory.js diff --git a/test/js/node/test/parallel/zlib-invalid-input.test.js b/test/js/node/test/parallel/zlib-invalid-input.test.js deleted file mode 100644 index aeb28f609d..0000000000 --- a/test/js/node/test/parallel/zlib-invalid-input.test.js +++ /dev/null @@ -1,70 +0,0 @@ -//#FILE: test-zlib-invalid-input.js -//#SHA1: b76d7dde545ea75c25000ee36864841cf73951e0 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -// Test uncompressing invalid input - -const zlib = require("zlib"); - -const nonStringInputs = [1, true, { a: 1 }, ["a"]]; - -// zlib.Unzip classes need to get valid data, or else they'll throw. -const unzips = [zlib.Unzip(), zlib.Gunzip(), zlib.Inflate(), zlib.InflateRaw(), zlib.BrotliDecompress()]; - -test("zlib.gunzip throws TypeError for non-string inputs", () => { - nonStringInputs.forEach(input => { - expect(() => { - zlib.gunzip(input); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_TYPE", - message: expect.any(String), - }), - ); - }); -}); - -test("unzip classes emit error for invalid compressed data", done => { - let completedCount = 0; - - unzips.forEach((uz, i) => { - uz.on("error", err => { - expect(err).toBeTruthy(); - completedCount++; - if (completedCount === unzips.length) { - done(); - } - }); - - uz.on("end", () => { - throw new Error("end event should not be emitted"); - }); - - // This will trigger error event - uz.write("this is not valid compressed data."); - }); -}); - -//<#END_FILE: test-zlib-invalid-input.js diff --git a/test/js/node/test/parallel/zlib-kmaxlength-rangeerror.test.js b/test/js/node/test/parallel/zlib-kmaxlength-rangeerror.test.js deleted file mode 100644 index 9d61c6ba81..0000000000 --- a/test/js/node/test/parallel/zlib-kmaxlength-rangeerror.test.js +++ /dev/null @@ -1,30 +0,0 @@ -//#FILE: test-zlib-kmaxlength-rangeerror.js -//#SHA1: b6507dfb859656a2d5194153ddf45de79e0ef0ce -//----------------- -"use strict"; - -const util = require("util"); - -// Change kMaxLength for zlib to trigger the error without having to allocate large Buffers. -const buffer = require("buffer"); -const oldkMaxLength = buffer.kMaxLength; -buffer.kMaxLength = 64; -const zlib = require("zlib"); -buffer.kMaxLength = oldkMaxLength; - -const encoded = Buffer.from("H4sIAAAAAAAAA0tMHFgAAIw2K/GAAAAA", "base64"); - -describe("ensure that zlib throws a RangeError if the final buffer needs to be larger than kMaxLength and concatenation fails", () => { - test("sync", () => { - expect(() => zlib.gunzipSync(encoded)).toThrowWithCode(RangeError, "ERR_BUFFER_TOO_LARGE"); - }); - - test("async", async () => { - await expect(async () => util.promisify(zlib.gunzip)(encoded)).toThrowWithCodeAsync( - RangeError, - "ERR_BUFFER_TOO_LARGE", - ); - }); -}); - -//<#END_FILE: test-zlib-kmaxlength-rangeerror.js diff --git a/test/js/node/test/parallel/zlib-maxoutputlength.test.js b/test/js/node/test/parallel/zlib-maxoutputlength.test.js deleted file mode 100644 index 2223043725..0000000000 --- a/test/js/node/test/parallel/zlib-maxoutputlength.test.js +++ /dev/null @@ -1,45 +0,0 @@ -//#FILE: test-zlib-maxOutputLength.js -//#SHA1: 807eb08c901d7476140bcb35b519518450bef3e6 -//----------------- -"use strict"; -const zlib = require("zlib"); -const { promisify } = require("util"); - -const brotliDecompressAsync = promisify(zlib.brotliDecompress); - -const encoded = Buffer.from("G38A+CXCIrFAIAM=", "base64"); - -describe("zlib.brotliDecompress with maxOutputLength", () => { - test("Async: should throw ERR_BUFFER_TOO_LARGE when maxOutputLength is exceeded", async () => { - await expect(brotliDecompressAsync(encoded, { maxOutputLength: 64 })).rejects.toThrow( - expect.objectContaining({ - code: "ERR_BUFFER_TOO_LARGE", - message: expect.stringContaining("Cannot create a Buffer larger than 64 bytes"), - }), - ); - }); - - test("Sync: should throw ERR_BUFFER_TOO_LARGE when maxOutputLength is exceeded", () => { - expect(() => { - zlib.brotliDecompressSync(encoded, { maxOutputLength: 64 }); - }).toThrow( - expect.objectContaining({ - name: "RangeError", - code: "ERR_BUFFER_TOO_LARGE", - message: expect.stringContaining("Cannot create a Buffer larger than 64 bytes"), - }), - ); - }); - - test("Async: should not throw error when maxOutputLength is sufficient", async () => { - await brotliDecompressAsync(encoded, { maxOutputLength: 256 }); - }); - - test("Sync: should not throw error when maxOutputLength is sufficient", () => { - expect(() => { - zlib.brotliDecompressSync(encoded, { maxOutputLength: 256 }); - }).not.toThrow(); - }); -}); - -//<#END_FILE: test-zlib-maxOutputLength.js diff --git a/test/js/node/test/parallel/zlib-no-stream.test.js b/test/js/node/test/parallel/zlib-no-stream.test.js deleted file mode 100644 index 871f4686a2..0000000000 --- a/test/js/node/test/parallel/zlib-no-stream.test.js +++ /dev/null @@ -1,21 +0,0 @@ -//#FILE: test-zlib-no-stream.js -//#SHA1: 5755924e9363a20243c326747623e8e266f81625 -//----------------- -/* eslint-disable node-core/required-modules */ -/* eslint-disable node-core/require-common-first */ - -"use strict"; - -// We are not loading common because it will load the stream module, -// defeating the purpose of this test. - -const { gzipSync } = require("zlib"); - -// Avoid regressions such as https://github.com/nodejs/node/issues/36615 - -test("gzipSync should not throw", () => { - // This must not throw - expect(() => gzipSync("fooobar")).not.toThrow(); -}); - -//<#END_FILE: test-zlib-no-stream.js diff --git a/test/js/node/test/parallel/zlib-not-string-or-buffer.test.js b/test/js/node/test/parallel/zlib-not-string-or-buffer.test.js deleted file mode 100644 index 626430ec72..0000000000 --- a/test/js/node/test/parallel/zlib-not-string-or-buffer.test.js +++ /dev/null @@ -1,24 +0,0 @@ -//#FILE: test-zlib-not-string-or-buffer.js -//#SHA1: d07db97d9393df2ab9453800ae80f2921d93b6e2 -//----------------- -"use strict"; - -// Check the error condition testing for passing something other than a string -// or buffer. - -const zlib = require("zlib"); - -test("zlib.deflateSync throws for invalid input types", () => { - const invalidInputs = [undefined, null, true, false, 0, 1, [1, 2, 3], { foo: "bar" }]; - - invalidInputs.forEach(input => { - expect(() => zlib.deflateSync(input)).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - }), - ); - }); -}); - -//<#END_FILE: test-zlib-not-string-or-buffer.js diff --git a/test/js/node/test/parallel/zlib-object-write.test.js b/test/js/node/test/parallel/zlib-object-write.test.js deleted file mode 100644 index 63934f60b2..0000000000 --- a/test/js/node/test/parallel/zlib-object-write.test.js +++ /dev/null @@ -1,28 +0,0 @@ -//#FILE: test-zlib-object-write.js -//#SHA1: 8866194c1a944026a655101d66189f364b414ad7 -//----------------- -"use strict"; - -const { Gunzip } = require("zlib"); - -test("Gunzip in object mode throws on non-buffer write", () => { - const gunzip = new Gunzip({ objectMode: true }); - - // We use jest.fn() to create a mock function that we expect not to be called - const errorHandler = jest.fn(); - gunzip.on("error", errorHandler); - - expect(() => { - gunzip.write({}); - }).toThrow( - expect.objectContaining({ - name: "TypeError", - code: "ERR_INVALID_ARG_TYPE", - }), - ); - - // Verify that the error handler was not called - expect(errorHandler).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-zlib-object-write.js diff --git a/test/js/node/test/parallel/zlib-params.test.js b/test/js/node/test/parallel/zlib-params.test.js deleted file mode 100644 index 2c8063d78a..0000000000 --- a/test/js/node/test/parallel/zlib-params.test.js +++ /dev/null @@ -1,49 +0,0 @@ -//#FILE: test-zlib-params.js -//#SHA1: d7d1b0c78ae9b4b5df5a1057c18fc7f2ef735526 -//----------------- -"use strict"; -const zlib = require("zlib"); -const fs = require("fs"); -const path = require("path"); - -const fixturesPath = path.join(__dirname, "..", "fixtures"); -const file = fs.readFileSync(path.join(fixturesPath, "person.jpg")); -const chunkSize = 12 * 1024; -const opts = { level: 9, strategy: zlib.constants.Z_DEFAULT_STRATEGY }; - -test("zlib params change mid-stream", done => { - const deflater = zlib.createDeflate(opts); - - const chunk1 = file.slice(0, chunkSize); - const chunk2 = file.slice(chunkSize); - const blkhdr = Buffer.from([0x00, 0x5a, 0x82, 0xa5, 0x7d]); - const blkftr = Buffer.from("010000ffff7dac3072", "hex"); - const expected = Buffer.concat([blkhdr, chunk2, blkftr]); - const bufs = []; - - function read() { - let buf; - while ((buf = deflater.read()) !== null) { - bufs.push(buf); - } - } - - deflater.write(chunk1, () => { - deflater.params(0, zlib.constants.Z_DEFAULT_STRATEGY, () => { - while (deflater.read()); - - deflater.on("readable", read); - - deflater.end(chunk2); - }); - while (deflater.read()); - }); - - deflater.on("end", () => { - const actual = Buffer.concat(bufs); - expect(actual).toEqual(expected); - done(); - }); -}); - -//<#END_FILE: test-zlib-params.js diff --git a/test/js/node/test/parallel/zlib-premature-end.test.js b/test/js/node/test/parallel/zlib-premature-end.test.js deleted file mode 100644 index cb20184ac0..0000000000 --- a/test/js/node/test/parallel/zlib-premature-end.test.js +++ /dev/null @@ -1,59 +0,0 @@ -//#FILE: test-zlib-premature-end.js -//#SHA1: f44e08e4886032467eb9cf64412c7eda2b575a16 -//----------------- -"use strict"; -const zlib = require("zlib"); - -const input = "0123456789".repeat(4); - -const testCases = [ - [zlib.deflateRawSync, zlib.createInflateRaw], - [zlib.deflateSync, zlib.createInflate], - [zlib.brotliCompressSync, zlib.createBrotliDecompress], -]; - -const variants = [ - (stream, compressed, trailingData) => { - stream.end(compressed); - }, - (stream, compressed, trailingData) => { - stream.write(compressed); - stream.write(trailingData); - }, - (stream, compressed, trailingData) => { - stream.write(compressed); - stream.end(trailingData); - }, - (stream, compressed, trailingData) => { - stream.write(Buffer.concat([compressed, trailingData])); - }, - (stream, compressed, trailingData) => { - stream.end(Buffer.concat([compressed, trailingData])); - }, -]; - -describe("zlib premature end tests", () => { - testCases.forEach(([compress, decompressor]) => { - describe(`${compress.name} and ${decompressor.name}`, () => { - const compressed = compress(input); - const trailingData = Buffer.from("not valid compressed data"); - - variants.forEach((variant, index) => { - test(`variant ${index + 1}`, done => { - let output = ""; - const stream = decompressor(); - stream.setEncoding("utf8"); - stream.on("data", chunk => (output += chunk)); - stream.on("end", () => { - expect(output).toBe(input); - expect(stream.bytesWritten).toBe(compressed.length); - done(); - }); - variant(stream, compressed, trailingData); - }); - }); - }); - }); -}); - -//<#END_FILE: test-zlib-premature-end.js diff --git a/test/js/node/test/parallel/zlib-random-byte-pipes.test.js b/test/js/node/test/parallel/zlib-random-byte-pipes.test.js deleted file mode 100644 index fb37cf53f9..0000000000 --- a/test/js/node/test/parallel/zlib-random-byte-pipes.test.js +++ /dev/null @@ -1,166 +0,0 @@ -//#FILE: test-zlib-random-byte-pipes.js -//#SHA1: ef7e7d3683660b911f9e24a6f40947a47be3dbba -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const crypto = require("crypto"); -const stream = require("stream"); -const zlib = require("zlib"); - -const Stream = stream.Stream; - -// Emit random bytes, and keep a shasum -class RandomReadStream extends Stream { - constructor(opt) { - super(); - - this.readable = true; - this._paused = false; - this._processing = false; - - this._hasher = crypto.createHash("sha1"); - opt = opt || {}; - - // base block size. - opt.block = opt.block || 256 * 1024; - - // Total number of bytes to emit - opt.total = opt.total || 256 * 1024 * 1024; - this._remaining = opt.total; - - // How variable to make the block sizes - opt.jitter = opt.jitter || 1024; - - this._opt = opt; - - this._process = this._process.bind(this); - - process.nextTick(this._process); - } - - pause() { - this._paused = true; - this.emit("pause"); - } - - resume() { - // console.error("rrs resume"); - this._paused = false; - this.emit("resume"); - this._process(); - } - - _process() { - if (this._processing) return; - if (this._paused) return; - - this._processing = true; - - if (!this._remaining) { - this._hash = this._hasher.digest("hex").toLowerCase().trim(); - this._processing = false; - - this.emit("end"); - return; - } - - // Figure out how many bytes to output - // if finished, then just emit end. - let block = this._opt.block; - const jitter = this._opt.jitter; - if (jitter) { - block += Math.ceil(Math.random() * jitter - jitter / 2); - } - block = Math.min(block, this._remaining); - const buf = Buffer.allocUnsafe(block); - for (let i = 0; i < block; i++) { - buf[i] = Math.random() * 256; - } - - this._hasher.update(buf); - - this._remaining -= block; - - this._processing = false; - - this.emit("data", buf); - process.nextTick(this._process); - } -} - -// A filter that just verifies a shasum -class HashStream extends Stream { - constructor() { - super(); - this.readable = this.writable = true; - this._hasher = crypto.createHash("sha1"); - } - - write(c) { - // Simulate the way that an fs.ReadStream returns false - // on *every* write, only to resume a moment later. - this._hasher.update(c); - process.nextTick(() => this.resume()); - return false; - } - - resume() { - this.emit("resume"); - process.nextTick(() => this.emit("drain")); - } - - end(c) { - if (c) { - this.write(c); - } - this._hash = this._hasher.digest("hex").toLowerCase().trim(); - this.emit("data", this._hash); - this.emit("end"); - } -} - -test("zlib random byte pipes", async () => { - const compressionMethods = [ - [zlib.createGzip, zlib.createGunzip], - [zlib.createBrotliCompress, zlib.createBrotliDecompress], - ]; - - for (const [createCompress, createDecompress] of compressionMethods) { - const inp = new RandomReadStream({ total: 1024, block: 256, jitter: 16 }); - const out = new HashStream(); - const gzip = createCompress(); - const gunz = createDecompress(); - - inp.pipe(gzip).pipe(gunz).pipe(out); - - await new Promise(resolve => { - out.on("data", c => { - expect(c).toBe(inp._hash); - resolve(); - }); - }); - } -}); - -//<#END_FILE: test-zlib-random-byte-pipes.js diff --git a/test/js/node/test/parallel/zlib-reset-before-write.test.js b/test/js/node/test/parallel/zlib-reset-before-write.test.js deleted file mode 100644 index fd997b8a51..0000000000 --- a/test/js/node/test/parallel/zlib-reset-before-write.test.js +++ /dev/null @@ -1,46 +0,0 @@ -//#FILE: test-zlib-reset-before-write.js -//#SHA1: 44561d35a5b7e4fc363d7dbde7ec6891af1f338a -//----------------- -"use strict"; -const zlib = require("zlib"); - -// Tests that zlib streams support .reset() and .params() -// before the first write. That is important to ensure that -// lazy init of zlib native library handles these cases. - -const testCases = [ - (z, cb) => { - z.reset(); - cb(); - }, - (z, cb) => z.params(0, zlib.constants.Z_DEFAULT_STRATEGY, cb), -]; - -testCases.forEach((fn, index) => { - test(`zlib stream supports ${index === 0 ? ".reset()" : ".params()"} before first write`, done => { - const deflate = zlib.createDeflate(); - const inflate = zlib.createInflate(); - - deflate.pipe(inflate); - - const output = []; - inflate - .on("error", err => { - expect(err).toBeFalsy(); - }) - .on("data", chunk => output.push(chunk)) - .on("end", () => { - expect(Buffer.concat(output).toString()).toBe("abc"); - done(); - }); - - fn(deflate, () => { - fn(inflate, () => { - deflate.write("abc"); - deflate.end(); - }); - }); - }); -}); - -//<#END_FILE: test-zlib-reset-before-write.js diff --git a/test/js/node/test/parallel/zlib-sync-no-event.test.js b/test/js/node/test/parallel/zlib-sync-no-event.test.js deleted file mode 100644 index 9e821acfe9..0000000000 --- a/test/js/node/test/parallel/zlib-sync-no-event.test.js +++ /dev/null @@ -1,39 +0,0 @@ -//#FILE: test-zlib-sync-no-event.js -//#SHA1: 382796f607eb25a85aa067e0dbc3d5103d321def -//----------------- -"use strict"; -const zlib = require("zlib"); - -const message = "Come on, Fhqwhgads."; -const buffer = Buffer.from(message); - -test("zlib sync compression and decompression without events", () => { - const zipper = new zlib.Gzip(); - const closeSpy = jest.fn(); - zipper.on("close", closeSpy); - - const zipped = zipper._processChunk(buffer, zlib.constants.Z_FINISH); - - const unzipper = new zlib.Gunzip(); - const unzipperCloseSpy = jest.fn(); - unzipper.on("close", unzipperCloseSpy); - - const unzipped = unzipper._processChunk(zipped, zlib.constants.Z_FINISH); - - expect(zipped).toEqual( - // prettier-ignore - Buffer.from([ 31, 139, 8, 0, 0, 0, 0, 0, 0, osbyte(), 115, 206, 207, 77, 85, 200, 207, 211, 81, 112, 203, 40, 44, 207, 72, 79, 76, 41, 214, 3, 0, 160, 120, 128, 220, 19, 0, 0, 0 ]), - ); - expect(unzipped.toString()).toEqual(message); - - expect(closeSpy).not.toHaveBeenCalled(); - expect(unzipperCloseSpy).not.toHaveBeenCalled(); -}); - -//<#END_FILE: test-zlib-sync-no-event.js - -function osbyte() { - if (process.platform === "darwin") return 19; - if (process.platform === "linux") return 3; - if (process.platform === "win32") return 10; -} diff --git a/test/js/node/test/parallel/zlib-truncated.test.js b/test/js/node/test/parallel/zlib-truncated.test.js deleted file mode 100644 index 703ce49c04..0000000000 --- a/test/js/node/test/parallel/zlib-truncated.test.js +++ /dev/null @@ -1,99 +0,0 @@ -//#FILE: test-zlib-truncated.js -//#SHA1: 79f9bcf3c52b3d0736ebe457652d579a856d1f7b -//----------------- -"use strict"; -// Tests zlib streams with truncated compressed input - -const zlib = require("zlib"); - -const inputString = - "ΩΩLorem ipsum dolor sit amet, consectetur adipiscing eli" + - "t. Morbi faucibus, purus at gravida dictum, libero arcu " + - "convallis lacus, in commodo libero metus eu nisi. Nullam" + - " commodo, neque nec porta placerat, nisi est fermentum a" + - "ugue, vitae gravida tellus sapien sit amet tellus. Aenea" + - "n non diam orci. Proin quis elit turpis. Suspendisse non" + - " diam ipsum. Suspendisse nec ullamcorper odio. Vestibulu" + - "m arcu mi, sodales non suscipit id, ultrices ut massa. S" + - "ed ac sem sit amet arcu malesuada fermentum. Nunc sed. "; - -const errMessage = /unexpected end of file/; - -const methods = [ - { comp: "gzip", decomp: "gunzip", decompSync: "gunzipSync" }, - { comp: "gzip", decomp: "unzip", decompSync: "unzipSync" }, - { comp: "deflate", decomp: "inflate", decompSync: "inflateSync" }, - { comp: "deflateRaw", decomp: "inflateRaw", decompSync: "inflateRawSync" }, -]; - -methods.forEach(({ comp, decomp, decompSync }) => { - test(`Test ${comp} and ${decomp}`, async () => { - const compressed = await new Promise((resolve, reject) => { - zlib[comp](inputString, (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }); - - const truncated = compressed.slice(0, compressed.length / 2); - const toUTF8 = buffer => buffer.toString("utf-8"); - - // sync sanity - const decompressed = zlib[decompSync](compressed); - expect(toUTF8(decompressed)).toBe(inputString); - - // async sanity - await new Promise((resolve, reject) => { - zlib[decomp](compressed, (err, result) => { - if (err) reject(err); - else { - expect(toUTF8(result)).toBe(inputString); - resolve(); - } - }); - }); - - // Sync truncated input test - expect(() => { - zlib[decompSync](truncated); - }).toThrow( - expect.objectContaining({ - message: expect.stringMatching(errMessage), - }), - ); - - // Async truncated input test - await expect( - new Promise((resolve, reject) => { - zlib[decomp](truncated, err => { - if (err) reject(err); - else resolve(); - }); - }), - ).rejects.toThrow( - expect.objectContaining({ - message: expect.stringMatching(errMessage), - }), - ); - - const syncFlushOpt = { finishFlush: zlib.constants.Z_SYNC_FLUSH }; - - // Sync truncated input test, finishFlush = Z_SYNC_FLUSH - const result = toUTF8(zlib[decompSync](truncated, syncFlushOpt)); - expect(result).toBe(inputString.slice(0, result.length)); - - // Async truncated input test, finishFlush = Z_SYNC_FLUSH - await new Promise((resolve, reject) => { - zlib[decomp](truncated, syncFlushOpt, (err, decompressed) => { - if (err) reject(err); - else { - const result = toUTF8(decompressed); - expect(result).toBe(inputString.slice(0, result.length)); - resolve(); - } - }); - }); - }); -}); - -//<#END_FILE: test-zlib-truncated.js diff --git a/test/js/node/test/parallel/zlib-unzip-one-byte-chunks.test.js b/test/js/node/test/parallel/zlib-unzip-one-byte-chunks.test.js deleted file mode 100644 index f4fa2a0795..0000000000 --- a/test/js/node/test/parallel/zlib-unzip-one-byte-chunks.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-zlib-unzip-one-byte-chunks.js -//#SHA1: 3c242140501ae0e8e9277c68696c231a04070018 -//----------------- -"use strict"; -const zlib = require("zlib"); - -test("zlib unzip one byte chunks", done => { - const data = Buffer.concat([zlib.gzipSync("abc"), zlib.gzipSync("def")]); - - const resultBuffers = []; - - const unzip = zlib - .createUnzip() - .on("error", err => { - expect(err).toBeFalsy(); - }) - .on("data", data => resultBuffers.push(data)) - .on("finish", () => { - const unzipped = Buffer.concat(resultBuffers).toString(); - expect(unzipped).toBe("abcdef"); - done(); - }); - - for (let i = 0; i < data.length; i++) { - // Write each single byte individually. - unzip.write(Buffer.from([data[i]])); - } - - unzip.end(); -}); - -//<#END_FILE: test-zlib-unzip-one-byte-chunks.js diff --git a/test/js/node/test/parallel/zlib-write-after-close.test.js b/test/js/node/test/parallel/zlib-write-after-close.test.js deleted file mode 100644 index a778b7731c..0000000000 --- a/test/js/node/test/parallel/zlib-write-after-close.test.js +++ /dev/null @@ -1,52 +0,0 @@ -//#FILE: test-zlib-write-after-close.js -//#SHA1: 5e09b22ace4e546969c9d31f686446adda15fbd2 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; - -const zlib = require("node:zlib"); - -test("zlib should not allow writing after close", async () => { - const closeCallback = jest.fn(); - - await new Promise(resolve => { - zlib.gzip("hello", function () { - const unzip = zlib.createGunzip(); - unzip.close(closeCallback); - unzip.write("asd", function (err) { - expect(err).toEqual( - expect.objectContaining({ - code: "ERR_STREAM_DESTROYED", - name: "Error", - message: "Cannot call write after a stream was destroyed", - }), - ); - resolve(); - }); - }); - }); - - expect(closeCallback).toHaveBeenCalledTimes(1); -}); - -//<#END_FILE: test-zlib-write-after-close.js diff --git a/test/js/node/test/parallel/zlib-write-after-end.test.js b/test/js/node/test/parallel/zlib-write-after-end.test.js deleted file mode 100644 index c4e0586e65..0000000000 --- a/test/js/node/test/parallel/zlib-write-after-end.test.js +++ /dev/null @@ -1,29 +0,0 @@ -//#FILE: test-zlib-write-after-end.js -//#SHA1: 0d11ed6c9992b52c81a45bdb3d6fe0db4ab2681a -//----------------- -"use strict"; -const zlib = require("zlib"); - -// Regression test for https://github.com/nodejs/node/issues/30976 -// Writes to a stream should finish even after the readable side has been ended. - -test("zlib write after end", done => { - const data = zlib.deflateRawSync("Welcome"); - - const inflate = zlib.createInflateRaw(); - - inflate.resume(); - - const writeCallback = jest.fn(); - - inflate.write(data, writeCallback); - inflate.write(Buffer.from([0x00]), writeCallback); - inflate.write(Buffer.from([0x00]), writeCallback); - - inflate.flush(() => { - expect(writeCallback).toHaveBeenCalledTimes(3); - done(); - }); -}); - -//<#END_FILE: test-zlib-write-after-end.js diff --git a/test/js/node/test/parallel/zlib-write-after-flush.test.js b/test/js/node/test/parallel/zlib-write-after-flush.test.js deleted file mode 100644 index 3bdcfb8d4f..0000000000 --- a/test/js/node/test/parallel/zlib-write-after-flush.test.js +++ /dev/null @@ -1,57 +0,0 @@ -//#FILE: test-zlib-write-after-flush.js -//#SHA1: 2cd2c91ba7a105560cdf6831ece0e95174aac860 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const zlib = require("zlib"); - -const compressionMethods = [ - [zlib.createGzip, zlib.createGunzip], - [zlib.createBrotliCompress, zlib.createBrotliDecompress], -]; - -compressionMethods.forEach(([createCompress, createDecompress]) => { - test(`${createCompress.name} and ${createDecompress.name}`, done => { - const gzip = createCompress(); - const gunz = createDecompress(); - - gzip.pipe(gunz); - - let output = ""; - const input = "A line of data\n"; - gunz.setEncoding("utf8"); - gunz.on("data", c => (output += c)); - gunz.on("end", () => { - expect(output).toBe(input); - done(); - }); - - // Make sure that flush/write doesn't trigger an assert failure - gzip.flush(); - gzip.write(input); - gzip.end(); - gunz.read(0); - }); -}); - -//<#END_FILE: test-zlib-write-after-flush.js diff --git a/test/js/node/test/parallel/zlib-zero-byte.test.js b/test/js/node/test/parallel/zlib-zero-byte.test.js deleted file mode 100644 index 762ee875f2..0000000000 --- a/test/js/node/test/parallel/zlib-zero-byte.test.js +++ /dev/null @@ -1,56 +0,0 @@ -//#FILE: test-zlib-zero-byte.js -//#SHA1: 54539c28619fb98230547ba0929ddad146f15bc5 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const zlib = require("zlib"); - -for (const Compressor of [zlib.Gzip, zlib.BrotliCompress]) { - test(`${Compressor.name} compresses zero-byte input`, async () => { - const gz = Compressor(); - const emptyBuffer = Buffer.alloc(0); - let received = 0; - - gz.on("data", c => { - received += c.length; - }); - - const endPromise = new Promise(resolve => { - gz.on("end", resolve); - }); - - const finishPromise = new Promise(resolve => { - gz.on("finish", resolve); - }); - - gz.write(emptyBuffer); - gz.end(); - - await Promise.all([endPromise, finishPromise]); - - const expected = Compressor === zlib.Gzip ? 20 : 1; - expect(received).toBe(expected); - }); -} - -//<#END_FILE: test-zlib-zero-byte.js diff --git a/test/js/node/test/parallel/zlib-zero-windowbits.test.js b/test/js/node/test/parallel/zlib-zero-windowbits.test.js deleted file mode 100644 index 32f4225a30..0000000000 --- a/test/js/node/test/parallel/zlib-zero-windowbits.test.js +++ /dev/null @@ -1,32 +0,0 @@ -//#FILE: test-zlib-zero-windowBits.js -//#SHA1: 3f1f031d2f5ab37f2ea2a963d6de0e2ececa9d33 -//----------------- -"use strict"; - -const zlib = require("zlib"); - -// windowBits is a special case in zlib. On the compression side, 0 is invalid. -// On the decompression side, it indicates that zlib should use the value from -// the header of the compressed stream. -test("windowBits 0 for decompression", () => { - const inflate = zlib.createInflate({ windowBits: 0 }); - expect(inflate).toBeInstanceOf(zlib.Inflate); - - const gunzip = zlib.createGunzip({ windowBits: 0 }); - expect(gunzip).toBeInstanceOf(zlib.Gunzip); - - const unzip = zlib.createUnzip({ windowBits: 0 }); - expect(unzip).toBeInstanceOf(zlib.Unzip); -}); - -test("windowBits 0 for compression throws error", () => { - expect(() => zlib.createGzip({ windowBits: 0 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - message: expect.any(String), - }), - ); -}); - -//<#END_FILE: test-zlib-zero-windowBits.js diff --git a/test/js/node/test/parallel/zlib.test.js b/test/js/node/test/parallel/zlib.test.js deleted file mode 100644 index d6b27408a1..0000000000 --- a/test/js/node/test/parallel/zlib.test.js +++ /dev/null @@ -1,233 +0,0 @@ -//#FILE: test-zlib.js -//#SHA1: 0e67da3898d627175ffca51fdbd1042571d0c405 -//----------------- -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -"use strict"; -const zlib = require("zlib"); -const stream = require("stream"); -const fs = require("fs"); -const path = require("path"); - -const fixturesPath = path.join(__dirname, "..", "fixtures"); - -// Should not segfault. -test("gzipSync with invalid windowBits", () => { - expect(() => zlib.gzipSync(Buffer.alloc(0), { windowBits: 8 })).toThrow( - expect.objectContaining({ - code: "ERR_OUT_OF_RANGE", - name: "RangeError", - }), - ); -}); - -let zlibPairs = [ - [zlib.Deflate, zlib.Inflate], - [zlib.Gzip, zlib.Gunzip], - [zlib.Deflate, zlib.Unzip], - [zlib.Gzip, zlib.Unzip], - [zlib.DeflateRaw, zlib.InflateRaw], - [zlib.BrotliCompress, zlib.BrotliDecompress], -]; - -// How fast to trickle through the slowstream -let trickle = [128, 1024, 1024 * 1024]; - -// Tunable options for zlib classes. - -// several different chunk sizes -let chunkSize = [128, 1024, 1024 * 16, 1024 * 1024]; - -// This is every possible value. -let level = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; -let windowBits = [8, 9, 10, 11, 12, 13, 14, 15]; -let memLevel = [1, 2, 3, 4, 5, 6, 7, 8, 9]; -let strategy = [0, 1, 2, 3, 4]; - -// It's nice in theory to test every combination, but it -// takes WAY too long. Maybe a pummel test could do this? -if (!process.env.PUMMEL) { - trickle = [1024]; - chunkSize = [1024 * 16]; - level = [6]; - memLevel = [8]; - windowBits = [15]; - strategy = [0]; -} - -let testFiles = ["person.jpg", "elipses.txt", "empty.txt"]; - -if (process.env.FAST) { - zlibPairs = [[zlib.Gzip, zlib.Unzip]]; - testFiles = ["person.jpg"]; -} - -const tests = {}; -testFiles.forEach(file => { - tests[file] = fs.readFileSync(path.join(fixturesPath, file)); -}); - -// Stream that saves everything -class BufferStream extends stream.Stream { - constructor() { - super(); - this.chunks = []; - this.length = 0; - this.writable = true; - this.readable = true; - } - - write(c) { - this.chunks.push(c); - this.length += c.length; - return true; - } - - end(c) { - if (c) this.write(c); - // flatten - const buf = Buffer.allocUnsafe(this.length); - let i = 0; - this.chunks.forEach(c => { - c.copy(buf, i); - i += c.length; - }); - this.emit("data", buf); - this.emit("end"); - return true; - } -} - -class SlowStream extends stream.Stream { - constructor(trickle) { - super(); - this.trickle = trickle; - this.offset = 0; - this.readable = this.writable = true; - } - - write() { - throw new Error("not implemented, just call ss.end(chunk)"); - } - - pause() { - this.paused = true; - this.emit("pause"); - } - - resume() { - const emit = () => { - if (this.paused) return; - if (this.offset >= this.length) { - this.ended = true; - return this.emit("end"); - } - const end = Math.min(this.offset + this.trickle, this.length); - const c = this.chunk.slice(this.offset, end); - this.offset += c.length; - this.emit("data", c); - process.nextTick(emit); - }; - - if (this.ended) return; - this.emit("resume"); - if (!this.chunk) return; - this.paused = false; - emit(); - } - - end(chunk) { - // Walk over the chunk in blocks. - this.chunk = chunk; - this.length = chunk.length; - this.resume(); - return this.ended; - } -} - -test("createDeflateRaw with windowBits 8", () => { - expect(() => zlib.createDeflateRaw({ windowBits: 8 })).not.toThrow(); -}); - -test("inflate raw with windowBits 8", async () => { - const node = fs.createReadStream(path.join(fixturesPath, "person.jpg")); - const raw = []; - const reinflated = []; - - await new Promise((resolve, reject) => { - node.on("data", chunk => raw.push(chunk)); - - node - .pipe(zlib.createDeflateRaw({ windowBits: 9 })) - .pipe(zlib.createInflateRaw({ windowBits: 8 })) - .on("data", chunk => reinflated.push(chunk)) - .on("end", () => { - expect(Buffer.concat(raw)).toEqual(Buffer.concat(reinflated)); - resolve(); - }) - .on("error", reject); - }); -}); - -// For each of the files, make sure that compressing and -// decompressing results in the same data, for every combination -// of the options set above. - -const testKeys = Object.keys(tests); -testKeys.forEach(file => { - const test = tests[file]; - chunkSize.forEach(chunkSize => { - trickle.forEach(trickle => { - windowBits.forEach(windowBits => { - level.forEach(level => { - memLevel.forEach(memLevel => { - strategy.forEach(strategy => { - zlibPairs.forEach(pair => { - const [Def, Inf] = pair; - const opts = { level, windowBits, memLevel, strategy }; - - it(`${file} ${chunkSize} ${JSON.stringify(opts)} ${Def.name} -> ${Inf.name}`, done => { - const def = new Def(opts); - const inf = new Inf(opts); - const ss = new SlowStream(trickle); - const buf = new BufferStream(); - - // Verify that the same exact buffer comes out the other end. - buf.on("data", c => { - expect(c).toEqual(test); - done(); - }); - - // The magic happens here. - ss.pipe(def).pipe(inf).pipe(buf); - ss.end(test); - }); - }); - }); - }); - }); - }); - }); - }); -}); - -//<#END_FILE: test-zlib.js diff --git a/test/js/node/test/sequential/test-stream2-fs.js b/test/js/node/test/sequential/test-stream2-fs.js new file mode 100644 index 0000000000..3d06abc921 --- /dev/null +++ b/test/js/node/test/sequential/test-stream2-fs.js @@ -0,0 +1,70 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); + +const fs = require('fs'); +const FSReadable = fs.ReadStream; + +const path = require('path'); +const file = path.resolve(fixtures.path('x1024.txt')); + +const size = fs.statSync(file).size; + +const expectLengths = [1024]; + +const Stream = require('stream'); + +class TestWriter extends Stream { + constructor() { + super(); + this.buffer = []; + this.length = 0; + } + + write(c) { + this.buffer.push(c.toString()); + this.length += c.length; + return true; + } + + end(c) { + if (c) this.buffer.push(c.toString()); + this.emit('results', this.buffer); + } +} + +const r = new FSReadable(file); +const w = new TestWriter(); + +w.on('results', function(res) { + console.error(res, w.length); + assert.strictEqual(w.length, size); + assert.deepStrictEqual(res.map(function(c) { + return c.length; + }), expectLengths); + console.log('ok'); +}); + +r.pipe(w); diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts index 0852df396e..a4ce788966 100644 --- a/test/js/node/tls/node-tls-connect.test.ts +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -241,7 +241,7 @@ for (const { name, connect } of tests) { expect(cert.ca).toBeFalse(); expect(cert.bits).toBe(2048); expect(cert.modulus).toBe( - "BEEE8773AF7C8861EC11351188B9B1798734FB0729B674369BE3285A29FE5DACBFAB700D09D7904CF1027D89298BD68BE0EF1DF94363012B0DEB97F632CB76894BCC216535337B9DB6125EF68996DD35B4BEA07E86C41DA071907A86651E84F8C72141F889CC0F770554791E9F07BBE47C375D2D77B44DBE2AB0ED442BC1F49ABE4F8904977E3DFD61CD501D8EFF819FF1792AEDFFACA7D281FD1DB8C5D972D22F68FA7103CA11AC9AAED1CDD12C33C0B8B47964B37338953D2415EDCE8B83D52E2076CA960385CC3A5CA75A75951AAFDB2AD3DB98A6FDD4BAA32F575FEA7B11F671A9EAA95D7D9FAF958AC609F3C48DEC5BDDCF1BC1542031ED9D4B281D7DD1", + "beee8773af7c8861ec11351188b9b1798734fb0729b674369be3285a29fe5dacbfab700d09d7904cf1027d89298bd68be0ef1df94363012b0deb97f632cb76894bcc216535337b9db6125ef68996dd35b4bea07e86c41da071907a86651e84f8c72141f889cc0f770554791e9f07bbe47c375d2d77b44dbe2ab0ed442bc1f49abe4f8904977e3dfd61cd501d8eff819ff1792aedffaca7d281fd1db8c5d972d22f68fa7103ca11ac9aaed1cdd12c33c0b8b47964b37338953d2415edce8b83d52e2076ca960385cc3a5ca75a75951aafdb2ad3db98a6fdd4baa32f575fea7b11f671a9eaa95d7d9faf958ac609f3c48dec5bddcf1bc1542031ed9d4b281d7dd1", ); expect(cert.exponent).toBe("0x10001"); expect(cert.pubkey).toBeInstanceOf(Buffer); @@ -254,7 +254,7 @@ for (const { name, connect } of tests) { expect(cert.fingerprint512).toBe( "2D:31:CB:D2:A0:CA:E5:D4:B5:59:11:48:4B:BC:65:11:4F:AB:02:24:59:D8:73:43:2F:9A:31:92:BC:AF:26:66:CD:DB:8B:03:74:0C:C1:84:AF:54:2D:7C:FD:EF:07:6E:85:66:98:6B:82:4F:A5:72:97:A2:19:8C:7B:57:D6:15", ); - expect(cert.serialNumber).toBe("1DA7A7B8D71402ED2D8C3646A5CEDF2B8117EFC8"); + expect(cert.serialNumber).toBe("1da7a7b8d71402ed2d8c3646a5cedf2b8117efc8"); expect(cert.raw).toBeInstanceOf(Buffer); } finally { socket.end(); diff --git a/test/js/node/tls/node-tls-server.test.ts b/test/js/node/tls/node-tls-server.test.ts index 2cefec9c40..acf35ce584 100644 --- a/test/js/node/tls/node-tls-server.test.ts +++ b/test/js/node/tls/node-tls-server.test.ts @@ -319,7 +319,7 @@ describe("tls.createServer", () => { expect(cert.ca).toBeFalse(); expect(cert.bits).toBe(2048); expect(cert.modulus).toBe( - "BEEE8773AF7C8861EC11351188B9B1798734FB0729B674369BE3285A29FE5DACBFAB700D09D7904CF1027D89298BD68BE0EF1DF94363012B0DEB97F632CB76894BCC216535337B9DB6125EF68996DD35B4BEA07E86C41DA071907A86651E84F8C72141F889CC0F770554791E9F07BBE47C375D2D77B44DBE2AB0ED442BC1F49ABE4F8904977E3DFD61CD501D8EFF819FF1792AEDFFACA7D281FD1DB8C5D972D22F68FA7103CA11AC9AAED1CDD12C33C0B8B47964B37338953D2415EDCE8B83D52E2076CA960385CC3A5CA75A75951AAFDB2AD3DB98A6FDD4BAA32F575FEA7B11F671A9EAA95D7D9FAF958AC609F3C48DEC5BDDCF1BC1542031ED9D4B281D7DD1", + "beee8773af7c8861ec11351188b9b1798734fb0729b674369be3285a29fe5dacbfab700d09d7904cf1027d89298bd68be0ef1df94363012b0deb97f632cb76894bcc216535337b9db6125ef68996dd35b4bea07e86c41da071907a86651e84f8c72141f889cc0f770554791e9f07bbe47c375d2d77b44dbe2ab0ed442bc1f49abe4f8904977e3dfd61cd501d8eff819ff1792aedffaca7d281fd1db8c5d972d22f68fa7103ca11ac9aaed1cdd12c33c0b8b47964b37338953d2415edce8b83d52e2076ca960385cc3a5ca75a75951aafdb2ad3db98a6fdd4baa32f575fea7b11f671a9eaa95d7d9faf958ac609f3c48dec5bddcf1bc1542031ed9d4b281d7dd1", ); expect(cert.exponent).toBe("0x10001"); expect(cert.pubkey).toBeInstanceOf(Buffer); @@ -333,7 +333,7 @@ describe("tls.createServer", () => { expect(cert.fingerprint512).toBe( "2D:31:CB:D2:A0:CA:E5:D4:B5:59:11:48:4B:BC:65:11:4F:AB:02:24:59:D8:73:43:2F:9A:31:92:BC:AF:26:66:CD:DB:8B:03:74:0C:C1:84:AF:54:2D:7C:FD:EF:07:6E:85:66:98:6B:82:4F:A5:72:97:A2:19:8C:7B:57:D6:15", ); - expect(cert.serialNumber).toBe("1DA7A7B8D71402ED2D8C3646A5CEDF2B8117EFC8"); + expect(cert.serialNumber).toBe("1da7a7b8d71402ed2d8c3646a5cedf2b8117efc8"); expect(cert.raw).toBeInstanceOf(Buffer); client?.end(); diff --git a/test/js/node/url/url-parse-ipv6.test.ts b/test/js/node/url/url-parse-ipv6.test.ts new file mode 100644 index 0000000000..08de7bce99 --- /dev/null +++ b/test/js/node/url/url-parse-ipv6.test.ts @@ -0,0 +1,190 @@ +// prettier-ignore +import url from "node:url"; +import { describe, beforeAll, it, expect } from "bun:test"; + +// url.parse is deprecated. +process.emitWarning = () => {}; + +describe("Invalid IPv6 addresses", () => { + it.each([ + "https://[::1", + "https://[:::1]", + "https://[\n::1]", + "http://[::banana]", + ])("Invalid hostnames - parsing '%s' fails", input => { + expect(() => url.parse(input)).toThrowError(TypeError); + }); + + it.each([ + "https://[::1]::", + "https://[::1]:foo" + ])("Invalid ports - parsing '%s' fails", input => { + expect(() => url.parse(input)).toThrowError(TypeError); + }); +}); // + +describe("Valid spot checks", () => { + it.each([ + // ports + ["http://[::1]:", { host: "[::1]", hostname: "::1", port: null, path: "/", href: "http://[::1]/" }], // trailing colons are ignored + ["http://[::1]:1", { host: "[::1]", hostname: "::1", port: "1", path: "/", href: "http://[::1]/" }], + + // unicast + ["http://[::0]", { host: "[::0]", path: "/" }], + ["http://[::f]", { host: "[::f]", path: "/" }], + ["http://[::F]", { host: "[::F]", path: "/" }], + // these are technically invalid unicast addresses but url.parse allows them + ["http://[::7]", { host: "[::7]", path: "/" }], + // ["http://[::z]", { host: "[::7]", path: "/" }], + // ["http://[::😩]", { host: "[::😩]", path: "/" }], + + // full form-ish + ["https://[::1:2:3:4:5]", { host: "[::1:2:3:4:5]", path: "/" }], + ["[0:0:0:1:2:3:4:5]", { host: "[0:0:0:1:2:3:4:5]", path: "/" }], + ])("Parsing '%s' succeeds", (input, expected) => { + expect(url.parse(input)).toMatchObject(expect.objectContaining(expected)); + }); +}); // + +// checks on all properties +describe.each([ + [ + "[::1]", // w/o a protocol, it's treated as a path + { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: null, + pathname: "[::1]", + path: "[::1]", + href: "[::1]", + }, + ], + [ + "https://[::1]", + { + protocol: "https:", + slashes: true, + auth: null, + host: "[::1]", + port: null, + hostname: "::1", + hash: null, + search: null, + query: null, + pathname: "/", + path: "/", + href: "https://[::1]/", + }, + ], + [ + "http://user@[::1]:3000/foo/bar#baz?a=hi&b=1&c=%20", + { + protocol: "http:", + slashes: true, + auth: "user", + host: "[::1]:3000", + port: "3000", + hostname: "::1", + hash: "#baz?a=hi&b=1&c=%20", + search: null, + query: null, + pathname: "/foo/bar", + path: "/foo/bar", + href: "http://user@[::1]:3000/foo/bar#baz?a=hi&b=1&c=%20", + }, + ], + [ + "http://user@[::1]:80/foo/bar?a=hi&b=1&c=%20", + { + protocol: "http:", + slashes: true, + auth: "user", + host: "[::1]:80", + port: "80", + hostname: "::1", + hash: null, + search: "?a=hi&b=1&c=%20", + query: "a=hi&b=1&c=%20", + pathname: "/foo/bar", + path: "/foo/bar?a=hi&b=1&c=%20", + href: "http://user@[::1]:80/foo/bar?a=hi&b=1&c=%20", + }, + ], + /* + [ + // 7 bytes instead of 8 + "http://[0:0:1:2:3:4:5]/foo?bar#bar", + { + protocol: "http:", + slashes: true, + auth: null, + host: "[0:0:1:2:3:4:5]", + port: null, + hostname: "0:0:1:2:3:4:5", + hash: "#bar", + search: "?bar", + query: "bar", + pathname: "/foo", + path: "/foo?bar", + href: "http://[0:0:1:2:3:4:5]/foo?bar#bar", + }, + ], + */ + [ + "file://[::1]", + { + protocol: "file:", + slashes: true, + auth: null, + host: "[::1]", + port: null, + hostname: "::1", + hash: null, + search: null, + query: null, + pathname: "/", + path: "/", + href: "file://[::1]/", + }, + ], +])("Valid", (input, expected) => { + describe(`url.parse("${input}")`, () => { + let parsed: url.UrlWithStringQuery; + + beforeAll(() => { + parsed = url.parse(input); + }); + + it("parses to the expected object", () => { + expect(parsed).toMatchObject(expected); + }); + + it("is a Url, not a URL", () => { + expect(parsed).not.toBeInstanceOf(url.URL); + expect(parsed).not.toBeInstanceOf(globalThis.URL); + }); + }); // + + describe(`url.parse("${input}", true)`, () => { + let parsed: url.UrlWithParsedQuery; + + beforeAll(() => { + parsed = url.parse(input, true); + }); + + it("parses to the expected object", () => { + const { query, ...rest } = expected; + expect(parsed).toMatchObject(expect.objectContaining(rest)); + }); + + it("parses the query", () => { + expect(parsed.query).not.toBeInstanceOf(String); + }); + }); // +}); // diff --git a/test/js/node/util/bun-inspect.test.ts b/test/js/node/util/bun-inspect.test.ts index 57151a7b5b..65de3b0ed4 100644 --- a/test/js/node/util/bun-inspect.test.ts +++ b/test/js/node/util/bun-inspect.test.ts @@ -3,13 +3,13 @@ import stripAnsi from "strip-ansi"; describe("Bun.inspect", () => { it("reports error instead of [native code]", () => { - expect( + expect(() => Bun.inspect({ [Symbol.for("nodejs.util.inspect.custom")]() { throw new Error("custom inspect"); }, }), - ).toBe("[custom formatter threw an exception]"); + ).toThrow("custom inspect"); }); it("supports colors: false", () => { @@ -47,18 +47,46 @@ describe("Bun.inspect", () => { expect(() => Bun.inspect({}, { depth: -1 })).toThrow(); expect(() => Bun.inspect({}, { depth: -13210 })).toThrow(); }); - it("depth = Infinity works", () => { - function createRecursiveObject(n: number): any { - if (n === 0) return { hi: true }; - return { a: createRecursiveObject(n - 1) }; + for (let base of [new Error("hi"), { a: "hi" }]) { + it(`depth = Infinity works for ${base.constructor.name}`, () => { + function createRecursiveObject(n: number): any { + if (n === 0) { + return { a: base }; + } + return { a: createRecursiveObject(n - 1) }; + } + + const obj = createRecursiveObject(512); + expect(Bun.inspect(obj, { depth: Infinity })).toContain("hi"); + // this gets converted to u16, which if just truncating, will turn into 0 + expect(Bun.inspect(obj, { depth: 0x0fff0000 })).toContain("hi"); + }); + } + + it("stack overflow is thrown when it should be for objects", () => { + var object = { a: { b: { c: { d: 1 } } } }; + for (let i = 0; i < 16 * 1024; i++) { + object = { a: object }; } - const obj = createRecursiveObject(1000); - - expect(Bun.inspect(obj, { depth: Infinity })).toContain("hi"); - // this gets converted to u16, which if just truncating, will turn into 0 - expect(Bun.inspect(obj, { depth: 0x0fff0000 })).toContain("hi"); + expect(() => Bun.inspect(object, { depth: Infinity })).toThrowErrorMatchingInlineSnapshot( + `"Maximum call stack size exceeded."`, + ); }); + + it("stack overflow is thrown when it should be for Error", () => { + var object = { a: { b: { c: { d: 1 } } } }; + for (let i = 0; i < 16 * 1024; i++) { + const err = new Error("hello"); + err.object = object; + object = err; + } + + expect(() => Bun.inspect(object, { depth: Infinity })).toThrowErrorMatchingInlineSnapshot( + `"Maximum call stack size exceeded."`, + ); + }); + it("depth = 0", () => { expect(Bun.inspect({ a: { b: { c: { d: 1 } } } }, { depth: 0 })).toEqual("{\n a: [Object ...],\n}"); }); diff --git a/test/js/node/util/custom-inspect.test.js b/test/js/node/util/custom-inspect.test.js index 040f0abb8d..bc8763b214 100644 --- a/test/js/node/util/custom-inspect.test.js +++ b/test/js/node/util/custom-inspect.test.js @@ -155,11 +155,6 @@ for (const [name, inspect] of process.versions.bun }, }; - if (Bun.inspect === inspect) { - // make sure this doesnt crash - expect(inspect(obj)).toBeString(); - } else { - expect(() => inspect(obj)).toThrow(); - } + expect(() => inspect(obj)).toThrow(); }); } diff --git a/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js b/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js index 0c4bc55a79..5dffd034fa 100644 --- a/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js +++ b/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js @@ -656,14 +656,9 @@ test("no assertion failures 2", () => { // Prevent non-enumerable error properties from being printed. { - // TODO(bun): Make originalLine and originalColumn non-enumerable let err = new Error(); err.message = "foobar"; - let out = util - .inspect(err) - .replace(/\{\s*originalLine: .+\s*originalColumn: .+\s*\}/, "") - .trim() - .split("\n"); + let out = util.inspect(err).trim().split("\n"); assert.strictEqual(out[0], "Error: foobar"); assert(out.at(-1).startsWith(" at "), 'Expected "' + out.at(-1) + '" to start with " at "'); // Reset the error, the stack is otherwise not recreated. @@ -671,21 +666,13 @@ test("no assertion failures 2", () => { err.message = "foobar"; err.name = "Unique"; Object.defineProperty(err, "stack", { value: err.stack, enumerable: true }); - out = util - .inspect(err) - .replace(/\{\s*originalLine: .+\s*originalColumn: .+\s*\}/, "") - .trim() - .split("\n"); + out = util.inspect(err).trim().split("\n"); assert.strictEqual(out[0], "Unique: foobar"); assert(out.at(-1).startsWith(" at "), 'Expected "' + out.at(-1) + '" to start with " at "'); err.name = "Baz"; - out = util - .inspect(err) - .replace(/\n\s*originalLine: .+\s*originalColumn: .+/, "") - .trim() - .split("\n"); + out = util.inspect(err).trim().split("\n"); assert.strictEqual(out[0], "Unique: foobar"); - assert.strictEqual(out.at(-2), " name: 'Baz',"); + assert.strictEqual(out.at(-2), " name: 'Baz'"); assert.strictEqual(out.at(-1), "}"); } diff --git a/test/js/node/util/test-util-types.test.js b/test/js/node/util/test-util-types.test.js index 031d18ca20..084da3bfac 100644 --- a/test/js/node/util/test-util-types.test.js +++ b/test/js/node/util/test-util-types.test.js @@ -47,6 +47,7 @@ for (const [value, _method] of [ [new DataView(new ArrayBuffer())], [new SharedArrayBuffer()], [new Proxy({}, {}), "isProxy"], + [new EventTarget()], ]) { const method = _method || `is${value.constructor.name}`; test(method, () => { diff --git a/test/js/node/util/util.test.js b/test/js/node/util/util.test.js index dd55d12b1a..3f02c2b183 100644 --- a/test/js/node/util/util.test.js +++ b/test/js/node/util/util.test.js @@ -341,7 +341,13 @@ describe("util", () => { }); it("styleText", () => { - [undefined, null, false, 5n, 5, Symbol(), () => {}, {}, []].forEach(invalidOption => { + it("multiplecolors", () => { + expect(util.styleText(["bold", "red"], "test")).toBe("\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m"); + expect(util.styleText("bold"), "test").toBe("\u001b[1mtest\u001b[22m"); + expect(util.styleText("red", "test")).toBe("\u001b[31mtest\u001b[39m"); + }); + + [undefined, null, false, 5n, 5, Symbol(), () => {}, {}].forEach(invalidOption => { assert.throws( () => { util.styleText(invalidOption, "test"); diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index 69dcf9307f..814aee3ab3 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -1,6 +1,6 @@ import { nativeFrameForTesting } from "bun:internal-for-testing"; -import { afterEach, expect, test } from "bun:test"; import { noInline } from "bun:jsc"; +import { afterEach, expect, mock, test } from "bun:test"; const origPrepareStackTrace = Error.prepareStackTrace; afterEach(() => { Error.prepareStackTrace = origPrepareStackTrace; @@ -697,3 +697,30 @@ test("Error.prepareStackTrace propagates exceptions", () => { ]), ).toThrow("hi"); }); + +test("CallFrame.p.getScriptNameOrSourceURL inside eval", () => { + let prevPrepareStackTrace = Error.prepareStackTrace; + const prepare = mock((e, s) => { + expect(s[0].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); + expect(s[1].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); + expect(s[2].getScriptNameOrSourceURL()).toBe("[native code]"); + expect(s[3].getScriptNameOrSourceURL()).toBe(import.meta.path); + expect(s[4].getScriptNameOrSourceURL()).toBe(import.meta.path); + }); + Error.prepareStackTrace = prepare; + let evalScript = `(function() { + throw new Error("bad error!"); + })() //# sourceURL=https://zombo.com/welcome-to-zombo.js`; + + try { + function insideAFunction() { + eval(evalScript); + } + insideAFunction(); + } catch (e) { + e.stack; + } + Error.prepareStackTrace = prevPrepareStackTrace; + + expect(prepare).toHaveBeenCalledTimes(1); +}); diff --git a/test/js/node/vm/happy-dom-vm-16277.test.ts b/test/js/node/vm/happy-dom-vm-16277.test.ts new file mode 100644 index 0000000000..022a11e21b --- /dev/null +++ b/test/js/node/vm/happy-dom-vm-16277.test.ts @@ -0,0 +1,27 @@ +import { test, expect } from "bun:test"; +import { Window } from "happy-dom"; +test("reproduction", async (): Promise => { + expect.assertions(1); + for (let i: number = 0; i < 2; ++i) { + // TODO: have a reproduction of this that doesn't depend on a 10 MB file. + const response: Response = new Response(` + + + + + +`); + const window: Window = new Window({ url: "http://youtube.com" }); + const localStorage = window.localStorage; + global.window = window; + global.document = window.document; + localStorage.clear(); + document.body.innerHTML = await response.text(); + } + + // This test passes by simply not crashing. + expect().pass(); +}); diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index f0a66ec2e9..38a626895e 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -453,3 +453,74 @@ resp.text().then((a) => { delete URL.prototype.ok; } }); + +test("can get sourceURL from eval inside node:vm", () => { + try { + runInNewContext( + ` +throw new Error("hello"); +//# sourceURL=hellohello.js +`, + {}, + ); + } catch (e: any) { + var err: Error = e; + } + + expect(err!.stack!.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "")).toMatchInlineSnapshot(` +"Error: hello + at hellohello.js:2:16 + at runInNewContext (unknown) + at (:459:5)" +`); +}); + +test("can get sourceURL inside node:vm", () => { + const err = runInNewContext( + ` + +function hello() { + return Bun.inspect(new Error("hello")); +} + +hello(); + +//# sourceURL=hellohello.js +`, + { Bun }, + ); + + expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "")).toMatchInlineSnapshot(` +"4 | return Bun.inspect(new Error("hello")); + ^ +error: hello + at hello (hellohello.js:4:24) + at hellohello.js:7:6 + at (:479:15) +" +`); +}); + +test("eval sourceURL is correct", () => { + const err = eval( + ` + +function hello() { + return Bun.inspect(new Error("hello")); +} + +hello(); + +//# sourceURL=hellohello.js +`, + ); + expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "")).toMatchInlineSnapshot(` +"4 | return Bun.inspect(new Error("hello")); + ^ +error: hello + at hello (hellohello.js:4:24) + at eval (hellohello.js:7:6) + at (:505:15) +" +`); +}); diff --git a/test/js/node/watch/fs.watch.test.ts b/test/js/node/watch/fs.watch.test.ts index b758af71f0..d599ef3c0a 100644 --- a/test/js/node/watch/fs.watch.test.ts +++ b/test/js/node/watch/fs.watch.test.ts @@ -1,4 +1,4 @@ -import { pathToFileURL } from "bun"; +import { file, pathToFileURL } from "bun"; import { bunRun, bunRunAsScript, isWindows, tempDirWithFiles } from "harness"; import fs, { FSWatcher } from "node:fs"; import path from "path"; @@ -447,7 +447,8 @@ describe("fs.watch", () => { watcher.close(); expect.unreachable(); } catch (err: any) { - expect(err.message).toBe("Permission denied"); + expect(err.message).toBe(`EACCES: permission denied, open '${filepath}'`); + expect(err.path).toBe(filepath); expect(err.code).toBe("EACCES"); expect(err.syscall).toBe("open"); } @@ -463,7 +464,8 @@ describe("fs.watch", () => { watcher.close(); expect.unreachable(); } catch (err: any) { - expect(err.message).toBe("Permission denied"); + expect(err.message).toBe(`EACCES: permission denied, open '${filepath}'`); + expect(err.path).toBe(filepath); expect(err.code).toBe("EACCES"); expect(err.syscall).toBe("open"); } diff --git a/test/js/node/worker_threads/worker_thread_check.ts b/test/js/node/worker_threads/worker_thread_check.ts index 48ae9de7a1..004786cad6 100644 --- a/test/js/node/worker_threads/worker_thread_check.ts +++ b/test/js/node/worker_threads/worker_thread_check.ts @@ -28,6 +28,7 @@ if (isMainThread) { action, port: server.port, }, + env: process.env, }); worker.ref(); const { promise, resolve } = Promise.withResolvers(); diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 6ed8af8ace..38857a83fd 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -151,12 +151,12 @@ test("receiveMessageOnPort works across threads", async () => { let sharedBufferView = new Int32Array(sharedBuffer); let msg = { sharedBuffer }; worker.postMessage(msg); - expect(Atomics.wait(sharedBufferView, 0, 0)).toBe("ok"); + expect(await Atomics.waitAsync(sharedBufferView, 0, 0).value).toBe("ok"); const message = receiveMessageOnPort(port1); expect(message).toBeDefined(); expect(message!.message).toBe("done!"); await worker.terminate(); -}); +}, 9999999); test("receiveMessageOnPort works as FIFO", () => { const { port1, port2 } = new MessageChannel(); @@ -188,7 +188,7 @@ test("receiveMessageOnPort works as FIFO", () => { receiveMessageOnPort(value); }).toThrow(); } -}); +}, 9999999); test("you can override globalThis.postMessage", async () => { const worker = new Worker(new URL("./worker-override-postMessage.js", import.meta.url).href); diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 8c0089c760..ce681484fe 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -1,10 +1,11 @@ import { postgres, sql } from "bun:sql"; -import { expect, test } from "bun:test"; +import { expect, test, mock } from "bun:test"; import { $ } from "bun"; import { bunExe, isCI, withoutAggressiveGC } from "harness"; import path from "path"; -if (!isCI) { +const hasPsql = Bun.which("psql"); +if (!isCI && hasPsql) { require("./bootstrap.js"); // macOS location: /opt/homebrew/var/postgresql@14/pg_hba.conf @@ -13,18 +14,20 @@ if (!isCI) { // local all postgres trust // local all bun_sql_test_scram scram-sha-256 // local all bun_sql_test trust - // + // local all bun_sql_test_md5 md5 + // # IPv4 local connections: // host all ${USERNAME} 127.0.0.1/32 trust // host all postgres 127.0.0.1/32 trust // host all bun_sql_test_scram 127.0.0.1/32 scram-sha-256 // host all bun_sql_test 127.0.0.1/32 trust + // host all bun_sql_test_md5 127.0.0.1/32 md5 // # IPv6 local connections: // host all ${USERNAME} ::1/128 trust // host all postgres ::1/128 trust // host all bun_sql_test ::1/128 trust // host all bun_sql_test_scram ::1/128 scram-sha-256 - // + // host all bun_sql_test_md5 ::1/128 md5 // # Allow replication connections from localhost, by a user with the // # replication privilege. // local replication all trust @@ -33,9 +36,6 @@ if (!isCI) { // --- Expected pg_hba.conf --- process.env.DATABASE_URL = "postgres://bun_sql_test@localhost:5432/bun_sql_test"; - const delay = ms => Bun.sleep(ms); - const rel = x => new URL(x, import.meta.url); - const login = { username: "bun_sql_test", }; @@ -54,8 +54,8 @@ if (!isCI) { db: "bun_sql_test", username: login.username, password: login.password, - idle_timeout: 1, - connect_timeout: 1, + idle_timeout: 0, + connect_timeout: 0, max: 1, }; @@ -67,6 +67,153 @@ if (!isCI) { expect(result).toBe(1); }); + test("Connection timeout works", async () => { + const onclose = mock(); + const onconnect = mock(); + await using sql = postgres({ + ...options, + hostname: "unreachable_host", + connection_timeout: 1, + onconnect, + onclose, + }); + let error: any; + try { + await sql`select pg_sleep(2)`; + } catch (e) { + error = e; + } + expect(error.code).toBe(`ERR_POSTGRES_CONNECTION_TIMEOUT`); + expect(error.message).toContain("Connection timeout after 1ms"); + expect(onconnect).not.toHaveBeenCalled(); + expect(onclose).toHaveBeenCalledTimes(1); + }); + + test("Idle timeout works at start", async () => { + const onclose = mock(); + const onconnect = mock(); + await using sql = postgres({ + ...options, + idle_timeout: 1, + onconnect, + onclose, + }); + let error: any; + try { + await sql`select pg_sleep(2)`; + } catch (e) { + error = e; + } + expect(error.code).toBe(`ERR_POSTGRES_IDLE_TIMEOUT`); + expect(onconnect).toHaveBeenCalled(); + expect(onclose).toHaveBeenCalledTimes(1); + }); + + test("Idle timeout is reset when a query is run", async () => { + const onClosePromise = Promise.withResolvers(); + const onclose = mock(err => { + onClosePromise.resolve(err); + }); + const onconnect = mock(); + await using sql = postgres({ + ...options, + idle_timeout: 100, + onconnect, + onclose, + }); + expect(await sql`select 123 as x`).toEqual([{ x: 123 }]); + expect(onconnect).toHaveBeenCalledTimes(1); + expect(onclose).not.toHaveBeenCalled(); + const err = await onClosePromise.promise; + expect(err.code).toBe(`ERR_POSTGRES_IDLE_TIMEOUT`); + }); + + test("Max lifetime works", async () => { + const onClosePromise = Promise.withResolvers(); + const onclose = mock(err => { + onClosePromise.resolve(err); + }); + const onconnect = mock(); + const sql = postgres({ + ...options, + max_lifetime: 64, + onconnect, + onclose, + }); + let error: any; + expect(await sql`select 1 as x`).toEqual([{ x: 1 }]); + expect(onconnect).toHaveBeenCalledTimes(1); + try { + while (true) { + for (let i = 0; i < 100; i++) { + await sql`select pg_sleep(1)`; + } + } + } catch (e) { + error = e; + } + + expect(onclose).toHaveBeenCalledTimes(1); + + expect(error.code).toBe(`ERR_POSTGRES_LIFETIME_TIMEOUT`); + }); + + // Last one wins. + test("Handles duplicate string column names", async () => { + const result = await sql`select 1 as x, 2 as x, 3 as x`; + expect(result).toEqual([{ x: 3 }]); + }); + + test("Handles numeric column names", async () => { + // deliberately out of order + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 0 as "0"`; + expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, "0": 0 }]); + + expect(Object.keys(result[0])).toEqual(["0", "1", "2", "3"]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + // Last one wins. + test("Handles duplicate numeric column names", async () => { + const result = await sql`select 1 as "1", 2 as "1", 3 as "1"`; + expect(result).toEqual([{ "1": 3 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as x`; + expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, x: 4 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names with duplicates", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x`; + expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 2 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + + // Named columns are inserted first, but they appear from JS as last. + expect(Object.keys(result[0])).toEqual(["1", "2", "3", "x"]); + }); + + test("Handles mixed column names with duplicates at the end", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x, 3 as x, 4 as "y"`; + expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 3, y: 4 }]); + + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names with duplicates at the start", async () => { + const result = await sql`select 1 as "1", 2 as "1", 3 as "2", 4 as "3", 1 as x, 2 as x, 3 as x`; + expect(result).toEqual([{ "1": 2, "2": 3, "3": 4, x: 3 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + test("Uses default database without slash", async () => { const sql = postgres("postgres://localhost"); expect(sql.options.username).toBe(sql.options.database); @@ -145,10 +292,9 @@ if (!isCI) { expect(x).toEqual({ a: "hello", b: 42 }); }); - // It's treating as a string. - test.todo("implicit jsonb", async () => { + test("implicit jsonb", async () => { const x = (await sql`select ${{ a: "hello", b: 42 }}::jsonb as x`)[0].x; - expect([x.a, x.b].join(",")).toBe("hello,42"); + expect(x).toEqual({ a: "hello", b: 42 }); }); test("bulk insert nested sql()", async () => { @@ -428,9 +574,11 @@ if (!isCI) { test("Null sets to null", async () => expect((await sql`select ${null} as x`)[0].x).toBeNull()); // Add code property. - test.todo("Throw syntax error", async () => { - const code = await sql`wat 1`.catch(x => x); - console.log({ code }); + test("Throw syntax error", async () => { + const err = await sql`wat 1`.catch(x => x); + expect(err.code).toBe("ERR_POSTGRES_SYNTAX_ERROR"); + expect(err.errno).toBe(42601); + expect(err).toBeInstanceOf(SyntaxError); }); // t('Connect using uri', async() => @@ -502,13 +650,26 @@ if (!isCI) { // return [1, (await sql`select 1 as x`)[0].x] // }) - // t('Login without password', async() => { - // return [true, (await postgres({ ...options, ...login })`select true as x`)[0].x] - // }) + test("Login without password", async () => { + await using sql = postgres({ ...options, ...login }); + expect((await sql`select true as x`)[0].x).toBe(true); + }); - // t('Login using MD5', async() => { - // return [true, (await postgres({ ...options, ...login_md5 })`select true as x`)[0].x] - // }) + test("Login using MD5", async () => { + await using sql = postgres({ ...options, ...login_md5 }); + expect(await sql`select true as x`).toEqual([{ x: true }]); + }); + + test("Login with bad credentials propagates error from server", async () => { + const sql = postgres({ ...options, ...login_md5, username: "bad_user", password: "bad_password" }); + let err; + try { + await sql`select true as x`; + } catch (e) { + err = e; + } + expect(err.code).toBe("ERR_POSTGRES_SERVER_ERROR"); + }); test("Login using scram-sha-256", async () => { await using sql = postgres({ ...options, ...login_scram }); @@ -1159,9 +1320,10 @@ if (!isCI) { // ] // }) - // t('dynamic column name', async() => { - // return ['!not_valid', Object.keys((await sql`select 1 as ${ sql('!not_valid') }`)[0])[0]] - // }) + test.todo("dynamic column name", async () => { + const result = await sql`select 1 as ${"\\!not_valid"}`; + expect(Object.keys(result[0])[0]).toBe("!not_valid"); + }); // t('dynamic select as', async() => { // return ['2', (await sql`select ${ sql({ a: 1, b: 2 }) }`)[0].b] @@ -1178,12 +1340,12 @@ if (!isCI) { // return ['the answer', (await sql`insert into test ${ sql(x) } returning *`)[0].b, await sql`drop table test`] // }) - // t('dynamic insert pluck', async() => { - // await sql`create table test (a int, b text)` - // const x = { a: 42, b: 'the answer' } - - // return [null, (await sql`insert into test ${ sql(x, 'a') } returning *`)[0].b, await sql`drop table test`] - // }) + // test.todo("dynamic insert pluck", async () => { + // await sql`create table test (a int, b text)`; + // const x = { a: 42, b: "the answer" }; + // const [{ b }] = await sql`insert into test ${sql(x, "a")} returning *`; + // expect(b).toBe("the answer"); + // }); // t('dynamic in with empty array', async() => { // await sql`create table test (a int)` diff --git a/test/js/sql/tls-sql.test.ts b/test/js/sql/tls-sql.test.ts index 2bc99bd3ad..78bd4d0daa 100644 --- a/test/js/sql/tls-sql.test.ts +++ b/test/js/sql/tls-sql.test.ts @@ -4,20 +4,22 @@ import { sql as SQL } from "bun"; const TLS_POSTGRES_DATABASE_URL = getSecret("TLS_POSTGRES_DATABASE_URL"); -test("tls (explicit)", async () => { - const sql = new SQL({ - url: TLS_POSTGRES_DATABASE_URL!, - tls: true, - adapter: "postgresql", +if (TLS_POSTGRES_DATABASE_URL) { + test("tls (explicit)", async () => { + const sql = new SQL({ + url: TLS_POSTGRES_DATABASE_URL!, + tls: true, + adapter: "postgresql", + }); + + const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`; + expect(one).toBe(1); + expect(two).toBe("2"); }); - const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`; - expect(one).toBe(1); - expect(two).toBe("2"); -}); - -test("tls (implicit)", async () => { - const [{ one, two }] = await SQL`SELECT 1 as one, '2' as two`; - expect(one).toBe(1); - expect(two).toBe("2"); -}); + test("tls (implicit)", async () => { + const [{ one, two }] = await SQL`SELECT 1 as one, '2' as two`; + expect(one).toBe(1); + expect(two).toBe("2"); + }); +} diff --git a/test/js/third_party/@duckdb/node-api/duckdb.test.ts b/test/js/third_party/@duckdb/node-api/duckdb.test.ts new file mode 100644 index 0000000000..4fb3c1e012 --- /dev/null +++ b/test/js/third_party/@duckdb/node-api/duckdb.test.ts @@ -0,0 +1,1038 @@ +// Copyright 2018-2023 Stichting DuckDB Foundation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Copied from https://github.com/duckdb/duckdb-node-neo/blob/3f85023c6b42d6b288a2e0f92dd7c7b40cf2a63c/api/test/api.test.ts, +// with minor modifications to work as a Bun test + +import { libcFamily } from "harness"; +if (libcFamily == "musl") { + // @duckdb/node-bindings does not distribute musl binaries, so we skip this test on musl to avoid CI noise + process.exit(0); +} + +import { describe, test } from "bun:test"; +import assert from "node:assert"; +// Must be CJS require so that the above code can exit before we attempt to import DuckDB +const { + DateParts, + DuckDBAnyType, + DuckDBArrayType, + DuckDBArrayVector, + DuckDBBigIntType, + DuckDBBigIntVector, + DuckDBBitType, + DuckDBBitVector, + DuckDBBlobType, + DuckDBBlobValue, + DuckDBBlobVector, + DuckDBBooleanType, + DuckDBBooleanVector, + DuckDBConnection, + DuckDBDataChunk, + DuckDBDateType, + DuckDBDateValue, + DuckDBDateVector, + DuckDBDecimal16Vector, + DuckDBDecimal2Vector, + DuckDBDecimal4Vector, + DuckDBDecimal8Vector, + DuckDBDecimalType, + DuckDBDecimalValue, + DuckDBDoubleType, + DuckDBDoubleVector, + DuckDBEnum1Vector, + DuckDBEnum2Vector, + DuckDBEnum4Vector, + DuckDBEnumType, + DuckDBFloatType, + DuckDBFloatVector, + DuckDBHugeIntType, + DuckDBHugeIntVector, + DuckDBInstance, + DuckDBIntegerType, + DuckDBIntegerVector, + DuckDBIntervalType, + DuckDBIntervalVector, + DuckDBListType, + DuckDBListVector, + DuckDBMapType, + DuckDBMapVector, + DuckDBPendingResultState, + DuckDBResult, + DuckDBSQLNullType, + DuckDBSmallIntType, + DuckDBSmallIntVector, + DuckDBStructType, + DuckDBStructVector, + DuckDBTimeTZType, + DuckDBTimeTZValue, + DuckDBTimeTZVector, + DuckDBTimeType, + DuckDBTimeValue, + DuckDBTimeVector, + DuckDBTimestampMillisecondsType, + DuckDBTimestampMillisecondsValue, + DuckDBTimestampMillisecondsVector, + DuckDBTimestampNanosecondsType, + DuckDBTimestampNanosecondsValue, + DuckDBTimestampNanosecondsVector, + DuckDBTimestampSecondsType, + DuckDBTimestampSecondsValue, + DuckDBTimestampSecondsVector, + DuckDBTimestampTZType, + DuckDBTimestampTZValue, + DuckDBTimestampTZVector, + DuckDBTimestampType, + DuckDBTimestampValue, + DuckDBTimestampVector, + DuckDBTinyIntType, + DuckDBTinyIntVector, + DuckDBType, + DuckDBTypeId, + DuckDBUBigIntType, + DuckDBUBigIntVector, + DuckDBUHugeIntType, + DuckDBUHugeIntVector, + DuckDBUIntegerType, + DuckDBUIntegerVector, + DuckDBUSmallIntType, + DuckDBUSmallIntVector, + DuckDBUTinyIntType, + DuckDBUTinyIntVector, + DuckDBUUIDType, + DuckDBUUIDValue, + DuckDBUUIDVector, + DuckDBUnionType, + DuckDBUnionVector, + DuckDBValue, + DuckDBVarCharType, + DuckDBVarCharVector, + DuckDBVarIntType, + DuckDBVarIntVector, + DuckDBVector, + ResultReturnType, + StatementType, + TimeParts, + TimeTZParts, + TimestampParts, + arrayValue, + bitValue, + configurationOptionDescriptions, + dateValue, + decimalValue, + intervalValue, + listValue, + mapValue, + structValue, + timeTZValue, + timeValue, + timestampTZValue, + timestampValue, + unionValue, + version, +} = require("@duckdb/node-api"); + +const BI_10_8 = 100000000n; +const BI_10_10 = 10000000000n; +const BI_18_9s = BI_10_8 * BI_10_10 - 1n; +const BI_38_9s = BI_10_8 * BI_10_10 * BI_10_10 * BI_10_10 - 1n; + +async function sleep(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +async function withConnection(fn: (connection: DuckDBConnection) => Promise) { + const instance = await DuckDBInstance.create(); + const connection = await instance.connect(); + await fn(connection); +} + +interface ExpectedColumn { + readonly name: string; + readonly type: DuckDBType; +} + +function assertColumns(result: DuckDBResult, expectedColumns: readonly ExpectedColumn[]) { + assert.strictEqual(result.columnCount, expectedColumns.length, "column count"); + for (let i = 0; i < expectedColumns.length; i++) { + const { name, type } = expectedColumns[i]; + assert.strictEqual(result.columnName(i), name, "column name"); + assert.strictEqual(result.columnTypeId(i), type.typeId, `column type id (column: ${name})`); + assert.deepStrictEqual(result.columnType(i), type, `column type (column: ${name})`); + } +} + +function isVectorType>( + vector: DuckDBVector | null, + vectorType: new (...args: any[]) => TVector, +): vector is TVector { + return vector instanceof vectorType; +} + +function getColumnVector>( + chunk: DuckDBDataChunk, + columnIndex: number, + vectorType: new (...args: any[]) => TVector, +): TVector { + const columnVector = chunk.getColumnVector(columnIndex); + if (!isVectorType(columnVector, vectorType)) { + assert.fail(`expected column ${columnIndex} to be a ${vectorType}`); + } + return columnVector; +} + +function assertVectorValues( + vector: DuckDBVector | null | undefined, + values: readonly TValue[], + vectorName: string, +) { + if (!vector) { + assert.fail(`${vectorName} unexpectedly null or undefined`); + } + assert.strictEqual( + vector.itemCount, + values.length, + `expected vector ${vectorName} item count to be ${values.length} but found ${vector.itemCount}`, + ); + for (let i = 0; i < values.length; i++) { + const actual: TValue | null = vector.getItem(i); + const expected = values[i]; + assert.deepStrictEqual( + actual, + expected, + `expected vector ${vectorName}[${i}] to be ${expected} but found ${actual}`, + ); + } +} + +function assertValues>( + chunk: DuckDBDataChunk, + columnIndex: number, + vectorType: new (...args: any[]) => TVector, + values: readonly (TValue | null)[], +) { + const vector = getColumnVector(chunk, columnIndex, vectorType); + assertVectorValues(vector, values, `${columnIndex}`); +} + +function bigints(start: bigint, end: bigint) { + return Array.from({ length: Number(end - start) + 1 }).map((_, i) => start + BigInt(i)); +} + +describe("api", () => { + test("should expose version", () => { + const ver = version(); + assert.ok(ver.startsWith("v"), `version starts with 'v'`); + }); + test("should expose configuration option descriptions", () => { + const descriptions = configurationOptionDescriptions(); + assert.ok(descriptions["memory_limit"], `descriptions has 'memory_limit'`); + }); + test("ReturnResultType enum", () => { + assert.equal(ResultReturnType.INVALID, 0); + assert.equal(ResultReturnType.CHANGED_ROWS, 1); + assert.equal(ResultReturnType.NOTHING, 2); + assert.equal(ResultReturnType.QUERY_RESULT, 3); + + assert.equal(ResultReturnType[ResultReturnType.INVALID], "INVALID"); + assert.equal(ResultReturnType[ResultReturnType.CHANGED_ROWS], "CHANGED_ROWS"); + assert.equal(ResultReturnType[ResultReturnType.NOTHING], "NOTHING"); + assert.equal(ResultReturnType[ResultReturnType.QUERY_RESULT], "QUERY_RESULT"); + }); + test("StatementType enum", () => { + assert.equal(StatementType.INVALID, 0); + assert.equal(StatementType.SELECT, 1); + assert.equal(StatementType.INSERT, 2); + assert.equal(StatementType.UPDATE, 3); + assert.equal(StatementType.EXPLAIN, 4); + assert.equal(StatementType.DELETE, 5); + assert.equal(StatementType.PREPARE, 6); + assert.equal(StatementType.CREATE, 7); + assert.equal(StatementType.EXECUTE, 8); + assert.equal(StatementType.ALTER, 9); + assert.equal(StatementType.TRANSACTION, 10); + assert.equal(StatementType.COPY, 11); + assert.equal(StatementType.ANALYZE, 12); + assert.equal(StatementType.VARIABLE_SET, 13); + assert.equal(StatementType.CREATE_FUNC, 14); + assert.equal(StatementType.DROP, 15); + assert.equal(StatementType.EXPORT, 16); + assert.equal(StatementType.PRAGMA, 17); + assert.equal(StatementType.VACUUM, 18); + assert.equal(StatementType.CALL, 19); + assert.equal(StatementType.SET, 20); + assert.equal(StatementType.LOAD, 21); + assert.equal(StatementType.RELATION, 22); + assert.equal(StatementType.EXTENSION, 23); + assert.equal(StatementType.LOGICAL_PLAN, 24); + assert.equal(StatementType.ATTACH, 25); + assert.equal(StatementType.DETACH, 26); + assert.equal(StatementType.MULTI, 27); + + assert.equal(StatementType[StatementType.INVALID], "INVALID"); + assert.equal(StatementType[StatementType.SELECT], "SELECT"); + assert.equal(StatementType[StatementType.INSERT], "INSERT"); + assert.equal(StatementType[StatementType.UPDATE], "UPDATE"); + assert.equal(StatementType[StatementType.EXPLAIN], "EXPLAIN"); + assert.equal(StatementType[StatementType.DELETE], "DELETE"); + assert.equal(StatementType[StatementType.PREPARE], "PREPARE"); + assert.equal(StatementType[StatementType.CREATE], "CREATE"); + assert.equal(StatementType[StatementType.EXECUTE], "EXECUTE"); + assert.equal(StatementType[StatementType.ALTER], "ALTER"); + assert.equal(StatementType[StatementType.TRANSACTION], "TRANSACTION"); + assert.equal(StatementType[StatementType.COPY], "COPY"); + assert.equal(StatementType[StatementType.ANALYZE], "ANALYZE"); + assert.equal(StatementType[StatementType.VARIABLE_SET], "VARIABLE_SET"); + assert.equal(StatementType[StatementType.CREATE_FUNC], "CREATE_FUNC"); + assert.equal(StatementType[StatementType.DROP], "DROP"); + assert.equal(StatementType[StatementType.EXPORT], "EXPORT"); + assert.equal(StatementType[StatementType.PRAGMA], "PRAGMA"); + assert.equal(StatementType[StatementType.VACUUM], "VACUUM"); + assert.equal(StatementType[StatementType.CALL], "CALL"); + assert.equal(StatementType[StatementType.SET], "SET"); + assert.equal(StatementType[StatementType.LOAD], "LOAD"); + assert.equal(StatementType[StatementType.RELATION], "RELATION"); + assert.equal(StatementType[StatementType.EXTENSION], "EXTENSION"); + assert.equal(StatementType[StatementType.LOGICAL_PLAN], "LOGICAL_PLAN"); + assert.equal(StatementType[StatementType.ATTACH], "ATTACH"); + assert.equal(StatementType[StatementType.DETACH], "DETACH"); + assert.equal(StatementType[StatementType.MULTI], "MULTI"); + }); + test("DuckDBType toString", () => { + assert.equal(DuckDBBooleanType.instance.toString(), "BOOLEAN"); + assert.equal(DuckDBTinyIntType.instance.toString(), "TINYINT"); + assert.equal(DuckDBSmallIntType.instance.toString(), "SMALLINT"); + assert.equal(DuckDBIntegerType.instance.toString(), "INTEGER"); + assert.equal(DuckDBBigIntType.instance.toString(), "BIGINT"); + assert.equal(DuckDBUTinyIntType.instance.toString(), "UTINYINT"); + assert.equal(DuckDBUSmallIntType.instance.toString(), "USMALLINT"); + assert.equal(DuckDBUIntegerType.instance.toString(), "UINTEGER"); + assert.equal(DuckDBUBigIntType.instance.toString(), "UBIGINT"); + assert.equal(DuckDBFloatType.instance.toString(), "FLOAT"); + assert.equal(DuckDBDoubleType.instance.toString(), "DOUBLE"); + assert.equal(DuckDBTimestampType.instance.toString(), "TIMESTAMP"); + assert.equal(DuckDBDateType.instance.toString(), "DATE"); + assert.equal(DuckDBTimeType.instance.toString(), "TIME"); + assert.equal(DuckDBIntervalType.instance.toString(), "INTERVAL"); + assert.equal(DuckDBHugeIntType.instance.toString(), "HUGEINT"); + assert.equal(DuckDBUHugeIntType.instance.toString(), "UHUGEINT"); + assert.equal(DuckDBVarCharType.instance.toString(), "VARCHAR"); + assert.equal(DuckDBBlobType.instance.toString(), "BLOB"); + assert.equal(new DuckDBDecimalType(17, 5).toString(), "DECIMAL(17,5)"); + assert.equal(DuckDBTimestampSecondsType.instance.toString(), "TIMESTAMP_S"); + assert.equal(DuckDBTimestampMillisecondsType.instance.toString(), "TIMESTAMP_MS"); + assert.equal(DuckDBTimestampNanosecondsType.instance.toString(), "TIMESTAMP_NS"); + assert.equal( + new DuckDBEnumType(["fly", "swim", "walk"], DuckDBTypeId.UTINYINT).toString(), + `ENUM('fly', 'swim', 'walk')`, + ); + assert.equal(new DuckDBListType(DuckDBIntegerType.instance).toString(), "INTEGER[]"); + assert.equal( + new DuckDBStructType(["id", "ts"], [DuckDBVarCharType.instance, DuckDBTimestampType.instance]).toString(), + 'STRUCT("id" VARCHAR, "ts" TIMESTAMP)', + ); + assert.equal( + new DuckDBMapType(DuckDBIntegerType.instance, DuckDBVarCharType.instance).toString(), + "MAP(INTEGER, VARCHAR)", + ); + assert.equal(new DuckDBArrayType(DuckDBIntegerType.instance, 3).toString(), "INTEGER[3]"); + assert.equal(DuckDBUUIDType.instance.toString(), "UUID"); + assert.equal( + new DuckDBUnionType(["str", "num"], [DuckDBVarCharType.instance, DuckDBIntegerType.instance]).toString(), + 'UNION("str" VARCHAR, "num" INTEGER)', + ); + assert.equal(DuckDBBitType.instance.toString(), "BIT"); + assert.equal(DuckDBTimeTZType.instance.toString(), "TIME WITH TIME ZONE"); + assert.equal(DuckDBTimestampTZType.instance.toString(), "TIMESTAMP WITH TIME ZONE"); + assert.equal(DuckDBAnyType.instance.toString(), "ANY"); + assert.equal(DuckDBVarIntType.instance.toString(), "VARINT"); + assert.equal(DuckDBSQLNullType.instance.toString(), "SQLNULL"); + }); + test("should support creating, connecting, running a basic query, and reading results", async () => { + const instance = await DuckDBInstance.create(); + const connection = await instance.connect(); + const result = await connection.run("select 42 as num"); + assertColumns(result, [{ name: "num", type: DuckDBIntegerType.instance }]); + const chunk = await result.fetchChunk(); + assert.strictEqual(chunk.columnCount, 1); + assert.strictEqual(chunk.rowCount, 1); + assertValues(chunk, 0, DuckDBIntegerVector, [42]); + }); + test("should support running prepared statements", async () => { + await withConnection(async connection => { + const prepared = await connection.prepare("select $num as a, $str as b, $bool as c, $null as d"); + assert.strictEqual(prepared.parameterCount, 4); + assert.strictEqual(prepared.parameterName(1), "num"); + assert.strictEqual(prepared.parameterName(2), "str"); + assert.strictEqual(prepared.parameterName(3), "bool"); + assert.strictEqual(prepared.parameterName(4), "null"); + prepared.bindInteger(1, 10); + prepared.bindVarchar(2, "abc"); + prepared.bindBoolean(3, true); + prepared.bindNull(4); + const result = await prepared.run(); + assertColumns(result, [ + { name: "a", type: DuckDBIntegerType.instance }, + { name: "b", type: DuckDBVarCharType.instance }, + { name: "c", type: DuckDBBooleanType.instance }, + { name: "d", type: DuckDBIntegerType.instance }, + ]); + const chunk = await result.fetchChunk(); + assert.strictEqual(chunk.columnCount, 4); + assert.strictEqual(chunk.rowCount, 1); + assertValues(chunk, 0, DuckDBIntegerVector, [10]); + assertValues(chunk, 1, DuckDBVarCharVector, ["abc"]); + assertValues(chunk, 2, DuckDBBooleanVector, [true]); + assertValues(chunk, 3, DuckDBIntegerVector, [null]); + }); + }); + test("should support starting prepared statements and running them incrementally", async () => { + await withConnection(async connection => { + const prepared = await connection.prepare("select int from test_all_types()"); + const pending = prepared.start(); + let taskCount = 0; + while (pending.runTask() !== DuckDBPendingResultState.RESULT_READY) { + taskCount++; + if (taskCount > 100) { + // arbitrary upper bound on the number of tasks expected for this simple query + assert.fail("Unexpectedly large number of tasks"); + } + await sleep(1); + } + // console.debug('task count: ', taskCount); + const result = await pending.getResult(); + assertColumns(result, [{ name: "int", type: DuckDBIntegerType.instance }]); + const chunk = await result.fetchChunk(); + assert.strictEqual(chunk.columnCount, 1); + assert.strictEqual(chunk.rowCount, 3); + assertValues(chunk, 0, DuckDBIntegerVector, [DuckDBIntegerType.Min, DuckDBIntegerType.Max, null]); + }); + }); + test("should support streaming results from prepared statements", async () => { + await withConnection(async connection => { + const prepared = await connection.prepare("from range(10000)"); + const pending = prepared.start(); + const result = await pending.getResult(); + assertColumns(result, [{ name: "range", type: DuckDBBigIntType.instance }]); + const chunks: DuckDBDataChunk[] = []; + let currentChunk: DuckDBDataChunk | null = null; + currentChunk = await result.fetchChunk(); + while (currentChunk.rowCount > 0) { + chunks.push(currentChunk); + currentChunk = await result.fetchChunk(); + } + currentChunk = null; + assert.strictEqual(chunks.length, 5); // ceil(10000 / 2048) = 5 + assertValues(chunks[0], 0, DuckDBBigIntVector, bigints(0n, 2048n - 1n)); + assertValues(chunks[1], 0, DuckDBBigIntVector, bigints(2048n, 2048n * 2n - 1n)); + assertValues(chunks[2], 0, DuckDBBigIntVector, bigints(2048n * 2n, 2048n * 3n - 1n)); + assertValues(chunks[3], 0, DuckDBBigIntVector, bigints(2048n * 3n, 2048n * 4n - 1n)); + assertValues(chunks[4], 0, DuckDBBigIntVector, bigints(2048n * 4n, 9999n)); + }); + }); + test("should support all data types", async () => { + await withConnection(async connection => { + const result = await connection.run("from test_all_types(use_large_enum=true)"); + const smallEnumValues = ["DUCK_DUCK_ENUM", "GOOSE"]; + const mediumEnumValues = Array.from({ length: 300 }).map((_, i) => `enum_${i}`); + const largeEnumValues = Array.from({ length: 70000 }).map((_, i) => `enum_${i}`); + assertColumns(result, [ + { name: "bool", type: DuckDBBooleanType.instance }, + { name: "tinyint", type: DuckDBTinyIntType.instance }, + { name: "smallint", type: DuckDBSmallIntType.instance }, + { name: "int", type: DuckDBIntegerType.instance }, + { name: "bigint", type: DuckDBBigIntType.instance }, + { name: "hugeint", type: DuckDBHugeIntType.instance }, + { name: "uhugeint", type: DuckDBUHugeIntType.instance }, + { name: "utinyint", type: DuckDBUTinyIntType.instance }, + { name: "usmallint", type: DuckDBUSmallIntType.instance }, + { name: "uint", type: DuckDBUIntegerType.instance }, + { name: "ubigint", type: DuckDBUBigIntType.instance }, + { name: "varint", type: DuckDBVarIntType.instance }, + { name: "date", type: DuckDBDateType.instance }, + { name: "time", type: DuckDBTimeType.instance }, + { name: "timestamp", type: DuckDBTimestampType.instance }, + { name: "timestamp_s", type: DuckDBTimestampSecondsType.instance }, + { name: "timestamp_ms", type: DuckDBTimestampMillisecondsType.instance }, + { name: "timestamp_ns", type: DuckDBTimestampNanosecondsType.instance }, + { name: "time_tz", type: DuckDBTimeTZType.instance }, + { name: "timestamp_tz", type: DuckDBTimestampTZType.instance }, + { name: "float", type: DuckDBFloatType.instance }, + { name: "double", type: DuckDBDoubleType.instance }, + { name: "dec_4_1", type: new DuckDBDecimalType(4, 1) }, + { name: "dec_9_4", type: new DuckDBDecimalType(9, 4) }, + { name: "dec_18_6", type: new DuckDBDecimalType(18, 6) }, + { name: "dec38_10", type: new DuckDBDecimalType(38, 10) }, + { name: "uuid", type: DuckDBUUIDType.instance }, + { name: "interval", type: DuckDBIntervalType.instance }, + { name: "varchar", type: DuckDBVarCharType.instance }, + { name: "blob", type: DuckDBBlobType.instance }, + { name: "bit", type: DuckDBBitType.instance }, + { name: "small_enum", type: new DuckDBEnumType(smallEnumValues, DuckDBTypeId.UTINYINT) }, + { name: "medium_enum", type: new DuckDBEnumType(mediumEnumValues, DuckDBTypeId.USMALLINT) }, + { name: "large_enum", type: new DuckDBEnumType(largeEnumValues, DuckDBTypeId.UINTEGER) }, + { name: "int_array", type: new DuckDBListType(DuckDBIntegerType.instance) }, + { name: "double_array", type: new DuckDBListType(DuckDBDoubleType.instance) }, + { name: "date_array", type: new DuckDBListType(DuckDBDateType.instance) }, + { name: "timestamp_array", type: new DuckDBListType(DuckDBTimestampType.instance) }, + { name: "timestamptz_array", type: new DuckDBListType(DuckDBTimestampTZType.instance) }, + { name: "varchar_array", type: new DuckDBListType(DuckDBVarCharType.instance) }, + { name: "nested_int_array", type: new DuckDBListType(new DuckDBListType(DuckDBIntegerType.instance)) }, + { + name: "struct", + type: new DuckDBStructType(["a", "b"], [DuckDBIntegerType.instance, DuckDBVarCharType.instance]), + }, + { + name: "struct_of_arrays", + type: new DuckDBStructType( + ["a", "b"], + [new DuckDBListType(DuckDBIntegerType.instance), new DuckDBListType(DuckDBVarCharType.instance)], + ), + }, + { + name: "array_of_structs", + type: new DuckDBListType( + new DuckDBStructType(["a", "b"], [DuckDBIntegerType.instance, DuckDBVarCharType.instance]), + ), + }, + { name: "map", type: new DuckDBMapType(DuckDBVarCharType.instance, DuckDBVarCharType.instance) }, + { + name: "union", + type: new DuckDBUnionType(["name", "age"], [DuckDBVarCharType.instance, DuckDBSmallIntType.instance]), + }, + { name: "fixed_int_array", type: new DuckDBArrayType(DuckDBIntegerType.instance, 3) }, + { name: "fixed_varchar_array", type: new DuckDBArrayType(DuckDBVarCharType.instance, 3) }, + { + name: "fixed_nested_int_array", + type: new DuckDBArrayType(new DuckDBArrayType(DuckDBIntegerType.instance, 3), 3), + }, + { + name: "fixed_nested_varchar_array", + type: new DuckDBArrayType(new DuckDBArrayType(DuckDBVarCharType.instance, 3), 3), + }, + { + name: "fixed_struct_array", + type: new DuckDBArrayType( + new DuckDBStructType(["a", "b"], [DuckDBIntegerType.instance, DuckDBVarCharType.instance]), + 3, + ), + }, + { + name: "struct_of_fixed_array", + type: new DuckDBStructType( + ["a", "b"], + [new DuckDBArrayType(DuckDBIntegerType.instance, 3), new DuckDBArrayType(DuckDBVarCharType.instance, 3)], + ), + }, + { + name: "fixed_array_of_int_list", + type: new DuckDBArrayType(new DuckDBListType(DuckDBIntegerType.instance), 3), + }, + { + name: "list_of_fixed_int_array", + type: new DuckDBListType(new DuckDBArrayType(DuckDBIntegerType.instance, 3)), + }, + ]); + + const chunk = await result.fetchChunk(); + assert.strictEqual(chunk.columnCount, 54); + assert.strictEqual(chunk.rowCount, 3); + + assertValues(chunk, 0, DuckDBBooleanVector, [false, true, null]); + assertValues(chunk, 1, DuckDBTinyIntVector, [DuckDBTinyIntType.Min, DuckDBTinyIntType.Max, null]); + assertValues(chunk, 2, DuckDBSmallIntVector, [DuckDBSmallIntType.Min, DuckDBSmallIntType.Max, null]); + assertValues(chunk, 3, DuckDBIntegerVector, [DuckDBIntegerType.Min, DuckDBIntegerType.Max, null]); + assertValues(chunk, 4, DuckDBBigIntVector, [DuckDBBigIntType.Min, DuckDBBigIntType.Max, null]); + assertValues(chunk, 5, DuckDBHugeIntVector, [DuckDBHugeIntType.Min, DuckDBHugeIntType.Max, null]); + assertValues(chunk, 6, DuckDBUHugeIntVector, [DuckDBUHugeIntType.Min, DuckDBUHugeIntType.Max, null]); + assertValues(chunk, 7, DuckDBUTinyIntVector, [DuckDBUTinyIntType.Min, DuckDBUTinyIntType.Max, null]); + assertValues(chunk, 8, DuckDBUSmallIntVector, [DuckDBUSmallIntType.Min, DuckDBUSmallIntType.Max, null]); + assertValues(chunk, 9, DuckDBUIntegerVector, [DuckDBUIntegerType.Min, DuckDBUIntegerType.Max, null]); + assertValues(chunk, 10, DuckDBUBigIntVector, [DuckDBUBigIntType.Min, DuckDBUBigIntType.Max, null]); + assertValues(chunk, 11, DuckDBVarIntVector, [DuckDBVarIntType.Min, DuckDBVarIntType.Max, null]); + assertValues(chunk, 12, DuckDBDateVector, [DuckDBDateValue.Min, DuckDBDateValue.Max, null]); + assertValues(chunk, 13, DuckDBTimeVector, [DuckDBTimeValue.Min, DuckDBTimeValue.Max, null]); + assertValues(chunk, 14, DuckDBTimestampVector, [DuckDBTimestampValue.Min, DuckDBTimestampValue.Max, null]); + assertValues(chunk, 15, DuckDBTimestampSecondsVector, [ + DuckDBTimestampSecondsValue.Min, + DuckDBTimestampSecondsValue.Max, + null, + ]); + assertValues(chunk, 16, DuckDBTimestampMillisecondsVector, [ + DuckDBTimestampMillisecondsValue.Min, + DuckDBTimestampMillisecondsValue.Max, + null, + ]); + assertValues(chunk, 17, DuckDBTimestampNanosecondsVector, [ + DuckDBTimestampNanosecondsValue.Min, + DuckDBTimestampNanosecondsValue.Max, + null, + ]); + assertValues(chunk, 18, DuckDBTimeTZVector, [DuckDBTimeTZValue.Min, DuckDBTimeTZValue.Max, null]); + assertValues(chunk, 19, DuckDBTimestampTZVector, [DuckDBTimestampTZValue.Min, DuckDBTimestampTZValue.Max, null]); + assertValues(chunk, 20, DuckDBFloatVector, [DuckDBFloatType.Min, DuckDBFloatType.Max, null]); + assertValues(chunk, 21, DuckDBDoubleVector, [DuckDBDoubleType.Min, DuckDBDoubleType.Max, null]); + assertValues(chunk, 22, DuckDBDecimal2Vector, [decimalValue(-9999n, 4, 1), decimalValue(9999n, 4, 1), null]); + assertValues(chunk, 23, DuckDBDecimal4Vector, [ + decimalValue(-999999999n, 9, 4), + decimalValue(999999999n, 9, 4), + null, + ]); + assertValues(chunk, 24, DuckDBDecimal8Vector, [ + decimalValue(-BI_18_9s, 18, 6), + decimalValue(BI_18_9s, 18, 6), + null, + ]); + assertValues(chunk, 25, DuckDBDecimal16Vector, [ + decimalValue(-BI_38_9s, 38, 10), + decimalValue(BI_38_9s, 38, 10), + null, + ]); + assertValues(chunk, 26, DuckDBUUIDVector, [DuckDBUUIDValue.Min, DuckDBUUIDValue.Max, null]); + assertValues(chunk, 27, DuckDBIntervalVector, [ + intervalValue(0, 0, 0n), + intervalValue(999, 999, 999999999n), + null, + ]); + assertValues(chunk, 28, DuckDBVarCharVector, ["🦆🦆🦆🦆🦆🦆", "goo\0se", null]); + assertValues(chunk, 29, DuckDBBlobVector, [ + DuckDBBlobValue.fromString("thisisalongblob\x00withnullbytes"), + DuckDBBlobValue.fromString("\x00\x00\x00a"), + null, + ]); + assertValues(chunk, 30, DuckDBBitVector, [bitValue("0010001001011100010101011010111"), bitValue("10101"), null]); + assertValues(chunk, 31, DuckDBEnum1Vector, [ + smallEnumValues[0], + smallEnumValues[smallEnumValues.length - 1], + null, + ]); + assertValues(chunk, 32, DuckDBEnum2Vector, [ + mediumEnumValues[0], + mediumEnumValues[mediumEnumValues.length - 1], + null, + ]); + assertValues(chunk, 33, DuckDBEnum4Vector, [ + largeEnumValues[0], + largeEnumValues[largeEnumValues.length - 1], + null, + ]); + // int_array + assertValues(chunk, 34, DuckDBListVector, [listValue([]), listValue([42, 999, null, null, -42]), null]); + // double_array + assertValues(chunk, 35, DuckDBListVector, [ + listValue([]), + listValue([42.0, NaN, Infinity, -Infinity, null, -42.0]), + null, + ]); + // date_array + assertValues(chunk, 36, DuckDBListVector, [ + listValue([]), + listValue([dateValue(0), DuckDBDateValue.PosInf, DuckDBDateValue.NegInf, null, dateValue(19124)]), + null, + ]); + // timestamp_array + assertValues(chunk, 37, DuckDBListVector, [ + listValue([]), + listValue([ + DuckDBTimestampValue.Epoch, + DuckDBTimestampValue.PosInf, + DuckDBTimestampValue.NegInf, + null, + // 1652372625 is 2022-05-12 16:23:45 + timestampValue(1652372625n * 1000n * 1000n), + ]), + null, + ]); + // timestamptz_array + assertValues(chunk, 38, DuckDBListVector, [ + listValue([]), + listValue([ + DuckDBTimestampTZValue.Epoch, + DuckDBTimestampTZValue.PosInf, + DuckDBTimestampTZValue.NegInf, + null, + // 1652397825 = 1652372625 + 25200, 25200 = 7 * 60 * 60 = 7 hours in seconds + // This 7 hour difference is hard coded into test_all_types (value is 2022-05-12 16:23:45-07) + timestampTZValue(1652397825n * 1000n * 1000n), + ]), + null, + ]); + // varchar_array + assertValues(chunk, 39, DuckDBListVector, [ + listValue([]), + // Note that the string 'goose' in varchar_array does NOT have an embedded null character. + listValue(["🦆🦆🦆🦆🦆🦆", "goose", null, ""]), + null, + ]); + // nested_int_array + assertValues(chunk, 40, DuckDBListVector, [ + listValue([]), + listValue([ + listValue([]), + listValue([42, 999, null, null, -42]), + null, + listValue([]), + listValue([42, 999, null, null, -42]), + ]), + null, + ]); + assertValues(chunk, 41, DuckDBStructVector, [ + structValue({ "a": null, "b": null }), + structValue({ "a": 42, "b": "🦆🦆🦆🦆🦆🦆" }), + null, + ]); + // struct_of_arrays + assertValues(chunk, 42, DuckDBStructVector, [ + structValue({ "a": null, "b": null }), + structValue({ + "a": listValue([42, 999, null, null, -42]), + "b": listValue(["🦆🦆🦆🦆🦆🦆", "goose", null, ""]), + }), + null, + ]); + // array_of_structs + assertValues(chunk, 43, DuckDBListVector, [ + listValue([]), + listValue([structValue({ "a": null, "b": null }), structValue({ "a": 42, "b": "🦆🦆🦆🦆🦆🦆" }), null]), + null, + ]); + assertValues(chunk, 44, DuckDBMapVector, [ + mapValue([]), + mapValue([ + { key: "key1", value: "🦆🦆🦆🦆🦆🦆" }, + { key: "key2", value: "goose" }, + ]), + null, + ]); + assertValues(chunk, 45, DuckDBUnionVector, [ + unionValue("name", "Frank"), + unionValue("age", 5), + null, + ]); + // fixed_int_array + assertValues(chunk, 46, DuckDBArrayVector, [arrayValue([null, 2, 3]), arrayValue([4, 5, 6]), null]); + // fixed_varchar_array + assertValues(chunk, 47, DuckDBArrayVector, [arrayValue(["a", null, "c"]), arrayValue(["d", "e", "f"]), null]); + // fixed_nested_int_array + assertValues(chunk, 48, DuckDBArrayVector, [ + arrayValue([arrayValue([null, 2, 3]), null, arrayValue([null, 2, 3])]), + arrayValue([arrayValue([4, 5, 6]), arrayValue([null, 2, 3]), arrayValue([4, 5, 6])]), + null, + ]); + // fixed_nested_varchar_array + assertValues(chunk, 49, DuckDBArrayVector, [ + arrayValue([arrayValue(["a", null, "c"]), null, arrayValue(["a", null, "c"])]), + arrayValue([arrayValue(["d", "e", "f"]), arrayValue(["a", null, "c"]), arrayValue(["d", "e", "f"])]), + null, + ]); + // fixed_struct_array + assertValues(chunk, 50, DuckDBArrayVector, [ + arrayValue([ + structValue({ "a": null, "b": null }), + structValue({ "a": 42, "b": "🦆🦆🦆🦆🦆🦆" }), + structValue({ "a": null, "b": null }), + ]), + arrayValue([ + structValue({ "a": 42, "b": "🦆🦆🦆🦆🦆🦆" }), + structValue({ "a": null, "b": null }), + structValue({ "a": 42, "b": "🦆🦆🦆🦆🦆🦆" }), + ]), + null, + ]); + // struct_of_fixed_array + assertValues(chunk, 51, DuckDBStructVector, [ + structValue({ + "a": arrayValue([null, 2, 3]), + "b": arrayValue(["a", null, "c"]), + }), + structValue({ + "a": arrayValue([4, 5, 6]), + "b": arrayValue(["d", "e", "f"]), + }), + null, + ]); + // fixed_array_of_int_list + assertValues(chunk, 52, DuckDBArrayVector, [ + arrayValue([listValue([]), listValue([42, 999, null, null, -42]), listValue([])]), + arrayValue([listValue([42, 999, null, null, -42]), listValue([]), listValue([42, 999, null, null, -42])]), + null, + ]); + // list_of_fixed_int_array + assertValues(chunk, 53, DuckDBListVector, [ + listValue([arrayValue([null, 2, 3]), arrayValue([4, 5, 6]), arrayValue([null, 2, 3])]), + listValue([arrayValue([4, 5, 6]), arrayValue([null, 2, 3]), arrayValue([4, 5, 6])]), + null, + ]); + }); + }); + test("values toString", () => { + // array + assert.equal(arrayValue([]).toString(), "[]"); + assert.equal(arrayValue([1, 2, 3]).toString(), "[1, 2, 3]"); + assert.equal(arrayValue(["a", "b", "c"]).toString(), `['a', 'b', 'c']`); + + // bit + assert.equal(bitValue("").toString(), ""); + assert.equal(bitValue("10101").toString(), "10101"); + assert.equal(bitValue("0010001001011100010101011010111").toString(), "0010001001011100010101011010111"); + + // blob + assert.equal(DuckDBBlobValue.fromString("").toString(), ""); + assert.equal( + DuckDBBlobValue.fromString("thisisalongblob\x00withnullbytes").toString(), + "thisisalongblob\\x00withnullbytes", + ); + assert.equal(DuckDBBlobValue.fromString("\x00\x00\x00a").toString(), "\\x00\\x00\\x00a"); + + // date + assert.equal(DuckDBDateValue.Epoch.toString(), "1970-01-01"); + assert.equal(DuckDBDateValue.Max.toString(), "5881580-07-10"); + assert.equal(DuckDBDateValue.Min.toString(), "5877642-06-25 (BC)"); + + // decimal + assert.equal(decimalValue(0n, 4, 1).toString(), "0.0"); + assert.equal(decimalValue(9876n, 4, 1).toString(), "987.6"); + assert.equal(decimalValue(-9876n, 4, 1).toString(), "-987.6"); + + assert.equal(decimalValue(0n, 9, 4).toString(), "0.0000"); + assert.equal(decimalValue(987654321n, 9, 4).toString(), "98765.4321"); + assert.equal(decimalValue(-987654321n, 9, 4).toString(), "-98765.4321"); + + assert.equal(decimalValue(0n, 18, 6).toString(), "0.000000"); + assert.equal(decimalValue(987654321098765432n, 18, 6).toString(), "987654321098.765432"); + assert.equal(decimalValue(-987654321098765432n, 18, 6).toString(), "-987654321098.765432"); + + assert.equal(decimalValue(0n, 38, 10).toString(), "0.0000000000"); + assert.equal( + decimalValue(98765432109876543210987654321098765432n, 38, 10).toString(), + "9876543210987654321098765432.1098765432", + ); + assert.equal( + decimalValue(-98765432109876543210987654321098765432n, 38, 10).toString(), + "-9876543210987654321098765432.1098765432", + ); + + // interval + assert.equal(intervalValue(0, 0, 0n).toString(), "00:00:00"); + + assert.equal(intervalValue(1, 0, 0n).toString(), "1 month"); + assert.equal(intervalValue(-1, 0, 0n).toString(), "-1 month"); + assert.equal(intervalValue(2, 0, 0n).toString(), "2 months"); + assert.equal(intervalValue(-2, 0, 0n).toString(), "-2 months"); + assert.equal(intervalValue(12, 0, 0n).toString(), "1 year"); + assert.equal(intervalValue(-12, 0, 0n).toString(), "-1 year"); + assert.equal(intervalValue(24, 0, 0n).toString(), "2 years"); + assert.equal(intervalValue(-24, 0, 0n).toString(), "-2 years"); + assert.equal(intervalValue(25, 0, 0n).toString(), "2 years 1 month"); + assert.equal(intervalValue(-25, 0, 0n).toString(), "-2 years -1 month"); + + assert.equal(intervalValue(0, 1, 0n).toString(), "1 day"); + assert.equal(intervalValue(0, -1, 0n).toString(), "-1 day"); + assert.equal(intervalValue(0, 2, 0n).toString(), "2 days"); + assert.equal(intervalValue(0, -2, 0n).toString(), "-2 days"); + assert.equal(intervalValue(0, 30, 0n).toString(), "30 days"); + assert.equal(intervalValue(0, 365, 0n).toString(), "365 days"); + + assert.equal(intervalValue(0, 0, 1n).toString(), "00:00:00.000001"); + assert.equal(intervalValue(0, 0, -1n).toString(), "-00:00:00.000001"); + assert.equal(intervalValue(0, 0, 987654n).toString(), "00:00:00.987654"); + assert.equal(intervalValue(0, 0, -987654n).toString(), "-00:00:00.987654"); + assert.equal(intervalValue(0, 0, 1000000n).toString(), "00:00:01"); + assert.equal(intervalValue(0, 0, -1000000n).toString(), "-00:00:01"); + assert.equal(intervalValue(0, 0, 59n * 1000000n).toString(), "00:00:59"); + assert.equal(intervalValue(0, 0, -59n * 1000000n).toString(), "-00:00:59"); + assert.equal(intervalValue(0, 0, 60n * 1000000n).toString(), "00:01:00"); + assert.equal(intervalValue(0, 0, -60n * 1000000n).toString(), "-00:01:00"); + assert.equal(intervalValue(0, 0, 59n * 60n * 1000000n).toString(), "00:59:00"); + assert.equal(intervalValue(0, 0, -59n * 60n * 1000000n).toString(), "-00:59:00"); + assert.equal(intervalValue(0, 0, 60n * 60n * 1000000n).toString(), "01:00:00"); + assert.equal(intervalValue(0, 0, -60n * 60n * 1000000n).toString(), "-01:00:00"); + assert.equal(intervalValue(0, 0, 24n * 60n * 60n * 1000000n).toString(), "24:00:00"); + assert.equal(intervalValue(0, 0, -24n * 60n * 60n * 1000000n).toString(), "-24:00:00"); + assert.equal(intervalValue(0, 0, 2147483647n * 60n * 60n * 1000000n).toString(), "2147483647:00:00"); + assert.equal(intervalValue(0, 0, -2147483647n * 60n * 60n * 1000000n).toString(), "-2147483647:00:00"); + assert.equal(intervalValue(0, 0, 2147483647n * 60n * 60n * 1000000n + 1n).toString(), "2147483647:00:00.000001"); + assert.equal( + intervalValue(0, 0, -(2147483647n * 60n * 60n * 1000000n + 1n)).toString(), + "-2147483647:00:00.000001", + ); + + assert.equal( + intervalValue(2 * 12 + 3, 5, (7n * 60n * 60n + 11n * 60n + 13n) * 1000000n + 17n).toString(), + "2 years 3 months 5 days 07:11:13.000017", + ); + assert.equal( + intervalValue(-(2 * 12 + 3), -5, -((7n * 60n * 60n + 11n * 60n + 13n) * 1000000n + 17n)).toString(), + "-2 years -3 months -5 days -07:11:13.000017", + ); + + // list + assert.equal(listValue([]).toString(), "[]"); + assert.equal(listValue([1, 2, 3]).toString(), "[1, 2, 3]"); + assert.equal(listValue(["a", "b", "c"]).toString(), `['a', 'b', 'c']`); + + // map + assert.equal(mapValue([]).toString(), "{}"); + assert.equal( + mapValue([ + { key: 1, value: "a" }, + { key: 2, value: "b" }, + ]).toString(), + `{1: 'a', 2: 'b'}`, + ); + + // struct + assert.equal(structValue({}).toString(), "{}"); + assert.equal(structValue({ a: 1, b: 2 }).toString(), `{'a': 1, 'b': 2}`); + + // timestamp milliseconds + assert.equal(DuckDBTimestampMillisecondsValue.Epoch.toString(), "1970-01-01 00:00:00"); + assert.equal(DuckDBTimestampMillisecondsValue.Max.toString(), "294247-01-10 04:00:54.775"); + assert.equal(DuckDBTimestampMillisecondsValue.Min.toString(), "290309-12-22 (BC) 00:00:00"); + + // timestamp nanoseconds + assert.equal(DuckDBTimestampNanosecondsValue.Epoch.toString(), "1970-01-01 00:00:00"); + assert.equal(DuckDBTimestampNanosecondsValue.Max.toString(), "2262-04-11 23:47:16.854775806"); + assert.equal(DuckDBTimestampNanosecondsValue.Min.toString(), "1677-09-22 00:00:00"); + + // timestamp seconds + assert.equal(DuckDBTimestampSecondsValue.Epoch.toString(), "1970-01-01 00:00:00"); + assert.equal(DuckDBTimestampSecondsValue.Max.toString(), "294247-01-10 04:00:54"); + assert.equal(DuckDBTimestampSecondsValue.Min.toString(), "290309-12-22 (BC) 00:00:00"); + + // timestamp tz + assert.equal(DuckDBTimestampTZValue.Epoch.toString(), "1970-01-01 00:00:00"); + // assert.equal(DuckDBTimestampTZValue.Max.toString(), '294247-01-09 20:00:54.775806-08'); // in PST + assert.equal(DuckDBTimestampTZValue.Max.toString(), "294247-01-10 04:00:54.775806"); // TODO TZ + // assert.equal(DuckDBTimestampTZValue.Min.toString(), '290309-12-21 (BC) 16:00:00-08'); // in PST + assert.equal(DuckDBTimestampTZValue.Min.toString(), "290309-12-22 (BC) 00:00:00"); // TODO TZ + assert.equal(DuckDBTimestampTZValue.PosInf.toString(), "infinity"); + assert.equal(DuckDBTimestampTZValue.NegInf.toString(), "-infinity"); + + // timestamp + assert.equal(DuckDBTimestampValue.Epoch.toString(), "1970-01-01 00:00:00"); + assert.equal(DuckDBTimestampValue.Max.toString(), "294247-01-10 04:00:54.775806"); + assert.equal(DuckDBTimestampValue.Min.toString(), "290309-12-22 (BC) 00:00:00"); + assert.equal(DuckDBTimestampValue.PosInf.toString(), "infinity"); + assert.equal(DuckDBTimestampValue.NegInf.toString(), "-infinity"); + + // time tz + assert.equal(timeTZValue(0n, 0).toString(), "00:00:00"); + // assert.equal(DuckDBTimeTZValue.Max.toString(), '24:00:00-15:59:59'); + assert.equal(DuckDBTimeTZValue.Max.toString(), "24:00:00"); // TODO TZ + // assert.equal(DuckDBTimeTZValue.Max.toString(), '00:00:00+15:59:59'); + assert.equal(DuckDBTimeTZValue.Min.toString(), "00:00:00"); // TODO TZ + + // time + assert.equal(DuckDBTimeValue.Max.toString(), "24:00:00"); + assert.equal(DuckDBTimeValue.Min.toString(), "00:00:00"); + assert.equal(timeValue((12n * 60n * 60n + 34n * 60n + 56n) * 1000000n + 987654n).toString(), "12:34:56.987654"); + + // union + assert.equal(unionValue("a", 42).toString(), "42"); + assert.equal(unionValue("b", "duck").toString(), "duck"); + + // uuid + assert.equal(DuckDBUUIDValue.Min.toString(), "00000000-0000-0000-0000-000000000000"); + assert.equal(DuckDBUUIDValue.Max.toString(), "ffffffff-ffff-ffff-ffff-ffffffffffff"); + }); + test("date isFinite", () => { + assert(DuckDBDateValue.Epoch.isFinite); + assert(DuckDBDateValue.Max.isFinite); + assert(DuckDBDateValue.Min.isFinite); + assert(!DuckDBDateValue.PosInf.isFinite); + assert(!DuckDBDateValue.NegInf.isFinite); + }); + test("value conversion", () => { + const dateParts: DateParts = { year: 2024, month: 6, day: 3 }; + const timeParts: TimeParts = { hour: 12, min: 34, sec: 56, micros: 789123 }; + const timeTZParts: TimeTZParts = { time: timeParts, offset: DuckDBTimeTZValue.MinOffset }; + const timestampParts: TimestampParts = { date: dateParts, time: timeParts }; + + assert.deepEqual(DuckDBDateValue.fromParts(dateParts).toParts(), dateParts); + assert.deepEqual(DuckDBTimeValue.fromParts(timeParts).toParts(), timeParts); + assert.deepEqual(DuckDBTimeTZValue.fromParts(timeTZParts).toParts(), timeTZParts); + assert.deepEqual(DuckDBTimestampValue.fromParts(timestampParts).toParts(), timestampParts); + assert.deepEqual(DuckDBTimestampTZValue.fromParts(timestampParts).toParts(), timestampParts); + + assert.deepEqual(DuckDBDecimalValue.fromDouble(3.14159, 6, 5), decimalValue(314159n, 6, 5)); + assert.deepEqual(decimalValue(314159n, 6, 5).toDouble(), 3.14159); + }); + test("result inspection conveniences", async () => { + await withConnection(async connection => { + const result = await connection.run("select i::int as a, i::int + 10 as b from range(3) t(i)"); + assert.deepEqual(result.columnNames(), ["a", "b"]); + assert.deepEqual(result.columnTypes(), [DuckDBIntegerType.instance, DuckDBIntegerType.instance]); + const chunks = await result.fetchAllChunks(); + const chunkColumns = chunks.map(chunk => chunk.getColumns()); + assert.deepEqual(chunkColumns, [ + [ + [0, 1, 2], + [10, 11, 12], + ], + ]); + const chunkRows = chunks.map(chunk => chunk.getRows()); + assert.deepEqual(chunkRows, [ + [ + [0, 10], + [1, 11], + [2, 12], + ], + ]); + }); + }); + test("result reader", async () => { + await withConnection(async connection => { + const reader = await connection.runAndReadAll("select i::int as a, i::int + 10000 as b from range(5000) t(i)"); + assert.deepEqual(reader.columnNames(), ["a", "b"]); + assert.deepEqual(reader.columnTypes(), [DuckDBIntegerType.instance, DuckDBIntegerType.instance]); + const columns = reader.getColumns(); + assert.equal(columns.length, 2); + assert.equal(columns[0][0], 0); + assert.equal(columns[0][4999], 4999); + assert.equal(columns[1][0], 10000); + assert.equal(columns[1][4999], 14999); + const rows = reader.getRows(); + assert.equal(rows.length, 5000); + assert.deepEqual(rows[0], [0, 10000]); + assert.deepEqual(rows[4999], [4999, 14999]); + }); + }); + test("default duckdb_api without explicit options", async () => { + const instance = await DuckDBInstance.create(); + const connection = await instance.connect(); + const result = await connection.run(`select current_setting('duckdb_api') as duckdb_api`); + assertColumns(result, [{ name: "duckdb_api", type: DuckDBVarCharType.instance }]); + const chunk = await result.fetchChunk(); + assert.strictEqual(chunk.columnCount, 1); + assert.strictEqual(chunk.rowCount, 1); + assertValues(chunk, 0, DuckDBVarCharVector, ["node-neo-api"]); + }); + test("default duckdb_api with explicit options", async () => { + const instance = await DuckDBInstance.create(undefined, {}); + const connection = await instance.connect(); + const result = await connection.run(`select current_setting('duckdb_api') as duckdb_api`); + assertColumns(result, [{ name: "duckdb_api", type: DuckDBVarCharType.instance }]); + const chunk = await result.fetchChunk(); + assert.strictEqual(chunk.columnCount, 1); + assert.strictEqual(chunk.rowCount, 1); + assertValues(chunk, 0, DuckDBVarCharVector, ["node-neo-api"]); + }); + test("overriding duckdb_api", async () => { + const instance = await DuckDBInstance.create(undefined, { "duckdb_api": "custom-duckdb-api" }); + const connection = await instance.connect(); + const result = await connection.run(`select current_setting('duckdb_api') as duckdb_api`); + assertColumns(result, [{ name: "duckdb_api", type: DuckDBVarCharType.instance }]); + const chunk = await result.fetchChunk(); + assert.strictEqual(chunk.columnCount, 1); + assert.strictEqual(chunk.rowCount, 1); + assertValues(chunk, 0, DuckDBVarCharVector, ["custom-duckdb-api"]); + }); +}); diff --git a/test/js/third_party/@electric-sql/pglite/pglite.test.ts b/test/js/third_party/@electric-sql/pglite/pglite.test.ts new file mode 100644 index 0000000000..45423d5a74 --- /dev/null +++ b/test/js/third_party/@electric-sql/pglite/pglite.test.ts @@ -0,0 +1,19 @@ +import { PGlite } from "@electric-sql/pglite"; + +describe("pglite", () => { + it("can initialize successfully", async () => { + const db = new PGlite(); + expect(await db.query("SELECT version()")).toEqual({ + rows: [ + { + version: + // since pglite is wasm, there is only one binary for all platforms. it always thinks it + // is x86_64-pc-linux-gnu. + "PostgreSQL 16.4 on x86_64-pc-linux-gnu, compiled by emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.72 (437140d149d9c977ffc8b09dbaf9b0f5a02db190), 32-bit", + }, + ], + fields: [{ name: "version", dataTypeID: 25 }], + affectedRows: 0, + }); + }); +}); diff --git a/test/js/third_party/@napi-rs/canvas/.gitignore b/test/js/third_party/@napi-rs/canvas/.gitignore new file mode 100644 index 0000000000..9b1ee42e84 --- /dev/null +++ b/test/js/third_party/@napi-rs/canvas/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/test/js/third_party/@napi-rs/canvas/expected.png b/test/js/third_party/@napi-rs/canvas/expected.png new file mode 100644 index 0000000000..3a98dd5ee0 Binary files /dev/null and b/test/js/third_party/@napi-rs/canvas/expected.png differ diff --git a/test/js/third_party/@napi-rs/canvas/icon-small.png b/test/js/third_party/@napi-rs/canvas/icon-small.png new file mode 100644 index 0000000000..69bc385e8c Binary files /dev/null and b/test/js/third_party/@napi-rs/canvas/icon-small.png differ diff --git a/test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts b/test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts new file mode 100644 index 0000000000..b03b58df0e --- /dev/null +++ b/test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts @@ -0,0 +1,25 @@ +// Create an image, then print it as binary to stdout +import { createCanvas, loadImage } from "@napi-rs/canvas"; +import { Jimp } from "jimp"; +import { join } from "path"; + +describe("@napi-rs/canvas", () => { + it("produces correct output", async () => { + const canvas = createCanvas(200, 200); + const ctx = canvas.getContext("2d"); + + ctx.lineWidth = 10; + ctx.strokeStyle = "red"; + ctx.fillStyle = "blue"; + + ctx.fillRect(0, 0, 200, 200); + ctx.strokeRect(50, 50, 100, 100); + + const image = await loadImage(join(__dirname, "icon-small.png")); + ctx.drawImage(image, 0, 0); + + const expected = await Jimp.read(join(__dirname, "expected.png")); + const actual = await Jimp.read(await canvas.encode("png")); + expect(Array.from(actual.bitmap.data)).toEqual(Array.from(expected.bitmap.data)); + }); +}); diff --git a/test/js/third_party/pnpm/pnpm.test.ts b/test/js/third_party/pnpm/pnpm.test.ts index 9094de5eaf..c6e8e6ff03 100644 --- a/test/js/third_party/pnpm/pnpm.test.ts +++ b/test/js/third_party/pnpm/pnpm.test.ts @@ -9,7 +9,7 @@ it("successfully traverses pnpm-generated install directory", async () => { // let { exited } = Bun.spawn({ - cmd: [bunExe(), "create", "vite", "my-vite-app", "--template", "solid-ts"], + cmd: [bunExe(), "create", "vite@5", "my-vite-app", "--template", "solid-ts"], cwd: package_dir, stdio: ["ignore", "inherit", "inherit"], env: bunEnv, diff --git a/test/js/third_party/svelte/bun-loader-svelte.ts b/test/js/third_party/svelte/bun-loader-svelte.ts index c30a7bd11e..e90562b38f 100644 --- a/test/js/third_party/svelte/bun-loader-svelte.ts +++ b/test/js/third_party/svelte/bun-loader-svelte.ts @@ -11,7 +11,8 @@ await plugin({ readFileSync(path.substring(0, path.includes("?") ? path.indexOf("?") : path.length), "utf-8"), { filename: path, - generate: "ssr", + generate: "server", + dev: false, }, ).js.code, loader: "js", diff --git a/test/js/third_party/svelte/svelte.test.ts b/test/js/third_party/svelte/svelte.test.ts index 67167ecbe0..05a56e4c42 100644 --- a/test/js/third_party/svelte/svelte.test.ts +++ b/test/js/third_party/svelte/svelte.test.ts @@ -1,12 +1,13 @@ import { describe, expect, it } from "bun:test"; +import { render as svelteRender } from "svelte/server"; import "./bun-loader-svelte"; describe("require", () => { it("SSRs `

Hello world!

` with Svelte", () => { const { default: App } = require("./hello.svelte"); - const { html } = App.render(); + const { body } = svelteRender(App); - expect(html).toBe("

Hello world!

"); + expect(body).toBe("

Hello world!

"); }); it("works if you require it 1,000 times", () => { @@ -14,7 +15,7 @@ describe("require", () => { Bun.unsafe.gcAggressionLevel(0); for (let i = 0; i < 1000; i++) { const { default: App } = require("./hello.svelte?r" + i); - expect(App.render).toBeFunction(); + expect(App).toBeFunction(); } Bun.gc(true); Bun.unsafe.gcAggressionLevel(prev); @@ -27,7 +28,7 @@ describe("dynamic import", () => { Bun.unsafe.gcAggressionLevel(0); for (let i = 0; i < 1000; i++) { const { default: App } = await import("./hello.svelte?i" + i); - expect(App.render).toBeFunction(); + expect(App).toBeFunction(); } Bun.gc(true); Bun.unsafe.gcAggressionLevel(prev); @@ -35,8 +36,7 @@ describe("dynamic import", () => { it("SSRs `

Hello world!

` with Svelte", async () => { const { default: App }: any = await import("./hello.svelte"); - const { html } = App.render(); - - expect(html).toBe("

Hello world!

"); + const { body } = svelteRender(App); + expect(body).toBe("

Hello world!

"); }); }); diff --git a/test/js/web/broadcastchannel/broadcast-channel.test.ts b/test/js/web/broadcastchannel/broadcast-channel.test.ts index 918bbd36b3..cd17faf5a0 100644 --- a/test/js/web/broadcastchannel/broadcast-channel.test.ts +++ b/test/js/web/broadcastchannel/broadcast-channel.test.ts @@ -178,20 +178,38 @@ test("Closing a channel in onmessage prevents already queued tasks from firing o c1.postMessage("done"); }); -test("broadcast channel used with workers", done => { - var bc = new BroadcastChannel("hello test"); - var count = 0; - var workersCount = 100; - bc.onmessage = (e: MessageEvent) => { - expect(e).toBeInstanceOf(MessageEvent); - expect(e.target).toBe(bc); - expect(e.data).toBe("hello from worker"); - if (++count == workersCount) { - bc.close(); - done(); +test("broadcast channel used with workers", async () => { + const batchSize = 1; + for (let total = 0; total < 100; total++) { + let bc = new BroadcastChannel("hello test"); + + let promises: Promise[] = []; + let resolveFns = []; + + for (var i = 0; i < batchSize; i++) { + const { promise, resolve } = Promise.withResolvers(); + promises.push(promise); + resolveFns.push(resolve); } - }; - for (var i = 0; i < workersCount; i++) { - new Worker(new URL("./broadcast-channel-worker.ts", import.meta.url).href); + bc.onmessage = (e: MessageEvent) => { + expect(e).toBeInstanceOf(MessageEvent); + expect(e.target).toBe(bc); + expect(e.data).toBe("hello from worker"); + const resolve = resolveFns.shift(); + if (resolve) { + resolve(); + } else { + console.warn("resolve fn not found"); + } + console.count("resolve fn called"); + }; + + for (let i = 0; i < batchSize; i++) { + new Worker(new URL("./broadcast-channel-worker.ts", import.meta.url).href); + } + + await Promise.all(promises); + bc.close(); + console.count("Batch complete"); } -}); +}, 99999); diff --git a/test/js/web/console/console-group.fixture.js b/test/js/web/console/console-group.fixture.js index 34d103720d..7b36024a3c 100644 --- a/test/js/web/console/console-group.fixture.js +++ b/test/js/web/console/console-group.fixture.js @@ -41,11 +41,22 @@ console.groupEnd(); console.groupEnd(); // Extra console.groupEnd(); // Extra +class NamedError extends Error { + constructor(message) { + super(message); + this.name = "NamedError"; + } +} + // Group with different log types console.group("Different logs"); console.log("Regular log"); console.info("Info log"); console.warn("Warning log"); +console.warn(new Error("console.warn an error")); +console.error(new Error("console.error an error")); +console.error(new NamedError("console.error a named error")); +console.warn(new NamedError("console.warn a named error")); console.error("Error log"); console.debug("Debug log"); console.groupEnd(); diff --git a/test/js/web/console/console-log.test.ts b/test/js/web/console/console-log.test.ts index 64e94a2069..17cb8994cd 100644 --- a/test/js/web/console/console-log.test.ts +++ b/test/js/web/console/console-log.test.ts @@ -58,12 +58,93 @@ it("long arrays get cutoff", () => { }); it("console.group", async () => { + const filepath = join(import.meta.dir, "console-group.fixture.js").replaceAll("\\", "/"); const proc = Bun.spawnSync({ - cmd: [bunExe(), join(import.meta.dir, "console-group.fixture.js")], - env: bunEnv, + cmd: [bunExe(), filepath], + env: { ...bunEnv, "BUN_JSC_showPrivateScriptsInStackTraces": "0" }, stdio: ["inherit", "pipe", "pipe"], }); expect(proc.exitCode).toBe(0); - expect(proc.stderr.toString("utf8").replaceAll("\r\n", "\n").trim()).toMatchSnapshot("console-group-error"); - expect(proc.stdout.toString("utf8").replaceAll("\r\n", "\n").trim()).toMatchSnapshot("console-group-output"); + let stdout = proc.stdout + .toString("utf8") + .replaceAll("\r\n", "\n") + .replaceAll("\\", "/") + .trim() + .replaceAll(filepath, ""); + let stderr = proc.stderr + .toString("utf8") + .replaceAll("\r\n", "\n") + .replaceAll("\\", "/") + .trim() + .replaceAll(filepath, ""); + expect(stdout).toMatchInlineSnapshot(` +"Basic group + Inside basic group +Outer group + Inside outer group + Inner group + Inside inner group + Back to outer group +Level 1 + Level 2 + Level 3 + Deep inside +undefined +Empty nested +Test extra end + Inside +Different logs + Regular log + Info log + Debug log +Complex types + { + a: 1, + b: 2, + } + [ 1, 2, 3 ] +null + undefined + 0 + false + + Inside falsy groups +🎉 Unicode! + Inside unicode group + Tab\tNewline +Quote"Backslash + Special chars" +`); + expect(stderr).toMatchInlineSnapshot(` +"Warning log + warn: console.warn an error + at :56:14 + + 52 | console.group("Different logs"); +53 | console.log("Regular log"); +54 | console.info("Info log"); +55 | console.warn("Warning log"); +56 | console.warn(new Error("console.warn an error")); +57 | console.error(new Error("console.error an error")); + ^ +error: console.error an error + at :57:15 + + 41 | console.groupEnd(); // Extra +42 | console.groupEnd(); // Extra +43 | +44 | class NamedError extends Error { +45 | constructor(message) { +46 | super(message); + ^ +NamedError: console.error a named error + at new NamedError (:46:5) + at :58:15 + + NamedError: console.warn a named error + at new NamedError (:46:5) + at :59:14 + + Error log" +`); }); diff --git a/test/js/web/fetch/fetch-gzip.test.ts b/test/js/web/fetch/fetch-gzip.test.ts index 83028ae12b..8f162b004a 100644 --- a/test/js/web/fetch/fetch-gzip.test.ts +++ b/test/js/web/fetch/fetch-gzip.test.ts @@ -210,8 +210,7 @@ it("fetch() with a gzip response works (multiple chunks, TCP server)", async don await write("\r\n"); socket.flush(); - }, - drain(socket) {}, + } }, }); await 1; diff --git a/test/js/web/fetch/fetch-leak-test-fixture-3.js b/test/js/web/fetch/fetch-leak-test-fixture-3.js index 3e75027ebf..33796c1667 100644 --- a/test/js/web/fetch/fetch-leak-test-fixture-3.js +++ b/test/js/web/fetch/fetch-leak-test-fixture-3.js @@ -1,8 +1,15 @@ -const payload = Buffer.alloc(64 * 64 * 1024, "X"); +// Reduce memory pressure by not cloning the buffer each Response. +const payload = new Blob([Buffer.alloc(64 * 64 * 1024, "X")]); + const server = Bun.serve({ port: 0, + idleTimeout: 0, async fetch(req) { return new Response(payload); }, }); -console.log(server.url.href); +if (process.send) { + process.send(server.url.href); +} else { + console.log(server.url.href); +} diff --git a/test/js/web/fetch/fetch-leak-test-fixture-4.js b/test/js/web/fetch/fetch-leak-test-fixture-4.js index 73c8ccd5d6..c25971c462 100644 --- a/test/js/web/fetch/fetch-leak-test-fixture-4.js +++ b/test/js/web/fetch/fetch-leak-test-fixture-4.js @@ -23,8 +23,16 @@ try { Bun.gc(true); await Bun.sleep(10); const stats = getHeapStats(); - expect(stats.Response || 0).toBeLessThanOrEqual(threshold); - expect(stats.Promise || 0).toBeLessThanOrEqual(threshold); + let { Response, Promise } = stats; + Response ||= 0; + Promise ||= 0; + console.log({ + rss: ((process.memoryUsage.rss() / 1024 / 1024) | 0) + " MB", + Response, + Promise, + }); + expect(Response).toBeLessThanOrEqual(threshold); + expect(Promise).toBeLessThanOrEqual(threshold); } } process.exit(0); diff --git a/test/js/web/fetch/fetch-leak-test-fixture-5.js b/test/js/web/fetch/fetch-leak-test-fixture-5.js index 88d0b7c1df..afcdb6e31b 100644 --- a/test/js/web/fetch/fetch-leak-test-fixture-5.js +++ b/test/js/web/fetch/fetch-leak-test-fixture-5.js @@ -66,6 +66,20 @@ function getBody() { case "urlsearchparams": body = getURLSearchParams(); break; + case "iterator": + body = async function* iter() { + yield (cachedBody ??= getString()); + }; + break; + case "stream": + body = new ReadableStream({ + async pull(c) { + await Bun.sleep(10); + c.enqueue((cachedBody ??= getBuffer())); + c.close(); + }, + }); + break; default: throw new Error(`Invalid type: ${type}`); } @@ -85,7 +99,8 @@ try { { Bun.gc(true); - await Bun.sleep(10); + await Bun.sleep(100); + Bun.gc(true); const stats = getHeapStats(); expect(stats.Response || 0).toBeLessThanOrEqual(threshold); expect(stats.Promise || 0).toBeLessThanOrEqual(threshold); diff --git a/test/js/web/fetch/fetch-leak.test.ts b/test/js/web/fetch/fetch-leak.test.ts index 04b68de276..b3b288b3ab 100644 --- a/test/js/web/fetch/fetch-leak.test.ts +++ b/test/js/web/fetch/fetch-leak.test.ts @@ -10,7 +10,7 @@ describe("fetch doesn't leak", () => { var count = 0; using server = Bun.serve({ port: 0, - + idleTimeout: 0, fetch(req) { count++; return new Response(body); @@ -20,7 +20,7 @@ describe("fetch doesn't leak", () => { const proc = Bun.spawn({ env: { ...bunEnv, - SERVER: `http://${server.hostname}:${server.port}`, + SERVER: server.url.href, COUNT: "200", }, stderr: "inherit", @@ -49,6 +49,7 @@ describe("fetch doesn't leak", () => { const serveOptions = { port: 0, + idleTimeout: 0, fetch(req) { return new Response(body, { headers }); }, @@ -62,8 +63,8 @@ describe("fetch doesn't leak", () => { const env = { ...bunEnv, - SERVER: `${tls ? "https" : "http"}://${server.hostname}:${server.port}`, - BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString("10"), + SERVER: server.url.href, + BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString(10), NAME: name, }; @@ -99,12 +100,13 @@ describe("fetch doesn't leak", () => { } }); -describe.each(["FormData", "Blob", "Buffer", "String", "URLSearchParams"])("Sending %s", type => { +describe.each(["FormData", "Blob", "Buffer", "String", "URLSearchParams", "stream", "iterator"])("Sending %s", type => { test( "does not leak", async () => { using server = Bun.serve({ port: 0, + idleTimeout: 0, fetch(req) { return new Response(); }, @@ -151,7 +153,7 @@ test("do not leak", async () => { let url; let isDone = false; - server.listen(0, function attack() { + server.listen(0, "127.0.0.1", function attack() { if (isDone) { return; } @@ -165,7 +167,7 @@ test("do not leak", async () => { let prev = Infinity; let count = 0; - const interval = setInterval(() => { + var interval = setInterval(() => { isDone = true; gc(); const next = process.memoryUsage().heapUsed; diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 21a72ede53..b3414f453b 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -1209,12 +1209,15 @@ describe("fetch() with streaming", () => { expect(buffer.toString("utf8")).toBe("unreachable"); } catch (err) { if (compression === "br") { - expect((err as Error).name).toBe("BrotliDecompressionError"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("BrotliDecompressionError"); } else if (compression === "deflate-libdeflate") { // Since the compressed data is different, the error ends up different. - expect((err as Error).name).toBe("ShortRead"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ShortRead"); } else { - expect((err as Error).name).toBe("ZlibError"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ZlibError"); } } } @@ -1306,7 +1309,8 @@ describe("fetch() with streaming", () => { gcTick(false); expect(buffer.toString("utf8")).toBe("unreachable"); } catch (err) { - expect((err as Error).name).toBe("ConnectionClosed"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ConnectionClosed"); } } }); diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index 37818cd03c..6e8fc443ef 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -1,12 +1,14 @@ import { AnyFunction, serve, ServeOptions, Server, sleep, TCPSocketListener } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; import { chmodSync, readFileSync, rmSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, gc, isWindows, tls, tmpdirSync, withoutAggressiveGC } from "harness"; +import { bunEnv, bunExe, gc, isBroken, isWindows, tls, tmpdirSync, withoutAggressiveGC } from "harness"; import { mkfifo } from "mkfifo"; import net from "net"; import { join } from "path"; import { gzipSync } from "zlib"; - +import { Readable } from "stream"; +import { once } from "events"; +import type { AddressInfo } from "net"; const tmp_dir = tmpdirSync(); const fixture = readFileSync(join(import.meta.dir, "fetch.js.txt"), "utf8").replaceAll("\r\n", "\n"); @@ -15,6 +17,7 @@ const fetchFixture4 = join(import.meta.dir, "fetch-leak-test-fixture-4.js"); let server: Server; function startServer({ fetch, ...options }: ServeOptions) { server = serve({ + idleTimeout: 0, ...options, fetch, port: 0, @@ -893,7 +896,7 @@ describe("Bun.file", () => { forEachMethod(m => () => { const file = Bun.file(path); - expect(async () => await file[m]()).toThrow("Permission denied"); + expect(async () => await file[m]()).toThrow("permission denied"); }); afterAll(() => { @@ -906,7 +909,7 @@ describe("Bun.file", () => { forEachMethod(m => async () => { const file = Bun.file(path); - expect(async () => await file[m]()).toThrow("No such file or directory"); + expect(async () => await file[m]()).toThrow("no such file or directory"); }); }); }); @@ -1312,9 +1315,16 @@ describe("Response", () => { method: "POST", body: await Bun.file(import.meta.dir + "/fixtures/file.txt").arrayBuffer(), }); - var input = await response.arrayBuffer(); + const input = await response.bytes(); var output = await Bun.file(import.meta.dir + "/fixtures/file.txt").stream(); - expect(new Uint8Array(input)).toEqual((await output.getReader().read()).value); + let chunks: Uint8Array[] = []; + const reader = output.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + expect(input).toEqual(Buffer.concat(chunks)); }); }); @@ -2008,7 +2018,7 @@ describe("http/1.1 response body length", () => { expect(response.arrayBuffer()).resolves.toHaveLength(0); }); - it("should ignore body on HEAD", async () => { + it.todoIf(isBroken)("should ignore body on HEAD", async () => { const response = await fetch(`http://${getHost()}/text`, { method: "HEAD" }); expect(response.status).toBe(200); expect(response.arrayBuffer()).resolves.toHaveLength(0); @@ -2016,35 +2026,31 @@ describe("http/1.1 response body length", () => { }); describe("fetch Response life cycle", () => { it("should not keep Response alive if not consumed", async () => { - const serverProcess = Bun.spawn({ + let deferred = Promise.withResolvers(); + + await using serverProcess = Bun.spawn({ cmd: [bunExe(), "--smol", fetchFixture3], stderr: "inherit", - stdout: "pipe", - stdin: "ignore", + stdout: "inherit", + stdin: "inherit", env: bunEnv, + ipc(message) { + deferred.resolve(message); + }, }); - async function getServerUrl() { - const reader = serverProcess.stdout.getReader(); - const { done, value } = await reader.read(); - return new TextDecoder().decode(value); - } - const serverUrl = await getServerUrl(); - const clientProcess = Bun.spawn({ + const serverUrl = await deferred.promise; + await using clientProcess = Bun.spawn({ cmd: [bunExe(), "--smol", fetchFixture4, serverUrl], stderr: "inherit", - stdout: "pipe", - stdin: "ignore", + stdout: "inherit", + stdin: "inherit", env: bunEnv, }); - try { - expect(await clientProcess.exited).toBe(0); - } finally { - serverProcess.kill(); - } + expect(await clientProcess.exited).toBe(0); }); it("should allow to get promise result after response is GC'd", async () => { - const server = Bun.serve({ + using server = Bun.serve({ port: 0, async fetch(request: Request) { return new Response( @@ -2074,3 +2080,249 @@ describe("fetch Response life cycle", () => { } }); }); + +describe("fetch should allow duplex", () => { + it("should allow duplex streaming", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response(req.body); + }, + }); + const intervalStream = new ReadableStream({ + start(c) { + let count = 0; + const timer = setInterval(() => { + c.enqueue("Hello\n"); + if (count === 5) { + clearInterval(timer); + c.close(); + } + count++; + }, 20); + }, + }).pipeThrough(new TextEncoderStream()); + + const resp = await fetch(server.url, { + method: "POST", + body: intervalStream, + duplex: "half", + }); + + const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader(); + var result = ""; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + result += value; + } + expect(result).toBe("Hello\n".repeat(6)); + }); + + it("should allow duplex extending Readable (sync)", async () => { + class HelloWorldStream extends Readable { + constructor(options) { + super(options); + this.chunks = ["Hello", " ", "World!"]; + this.index = 0; + } + + _read(size) { + if (this.index < this.chunks.length) { + this.push(this.chunks[this.index]); + this.index++; + } else { + this.push(null); + } + } + } + + using server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response(req.body); + }, + }); + const response = await fetch(server.url, { + body: new HelloWorldStream(), + method: "POST", + duplex: "half", + }); + + expect(await response.text()).toBe("Hello World!"); + }); + it("should allow duplex extending Readable (async)", async () => { + class HelloWorldStream extends Readable { + constructor(options) { + super(options); + this.chunks = ["Hello", " ", "World!"]; + this.index = 0; + } + + _read(size) { + setTimeout(() => { + if (this.index < this.chunks.length) { + this.push(this.chunks[this.index]); + this.index++; + } else { + this.push(null); + } + }, 20); + } + } + + using server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response(req.body); + }, + }); + const response = await fetch(server.url, { + body: new HelloWorldStream(), + method: "POST", + duplex: "half", + }); + + expect(await response.text()).toBe("Hello World!"); + }); + + it("should allow duplex using async iterator (async)", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response(req.body); + }, + }); + const response = await fetch(server.url, { + body: async function* iter() { + yield "Hello"; + await Bun.sleep(20); + yield " "; + await Bun.sleep(20); + yield "World!"; + }, + method: "POST", + duplex: "half", + }); + + expect(await response.text()).toBe("Hello World!"); + }); + + it("should fail in redirects .follow when using duplex", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + if (req.url.indexOf("/redirect") === -1) { + return Response.redirect("/"); + } + return new Response(req.body); + }, + }); + + expect(async () => { + const response = await fetch(server.url, { + body: async function* iter() { + yield "Hello"; + await Bun.sleep(20); + yield " "; + await Bun.sleep(20); + yield "World!"; + }, + method: "POST", + duplex: "half", + }); + + await response.text(); + }).toThrow(); + }); + + it("should work in redirects .manual when using duplex", async () => { + using server = Bun.serve({ + port: 0, + idleTimeout: 0, + async fetch(req) { + if (req.url.indexOf("/redirect") === -1) { + return Response.redirect("/"); + } + return new Response(req.body); + }, + }); + + expect(async () => { + const response = await fetch(server.url, { + body: async function* iter() { + yield "Hello"; + await Bun.sleep(20); + yield " "; + await Bun.sleep(20); + yield "World!"; + }, + method: "POST", + duplex: "half", + redirect: "manual", + }); + + await response.text(); + }).not.toThrow(); + }); +}); + +it("should allow to follow redirect if connection is closed, abort should work even if the socket was closed before the redirect", async () => { + for (const type of ["normal", "delay"]) { + await using server = net.createServer(socket => { + let body = ""; + socket.on("data", data => { + body += data.toString("utf8"); + + const headerEndIndex = body.indexOf("\r\n\r\n"); + if (headerEndIndex !== -1) { + // headers received + const headers = body.split("\r\n\r\n")[0]; + const path = headers.split("\r\n")[0].split(" ")[1]; + if (path === "/redirect") { + socket.end( + "HTTP/1.1 308 Permanent Redirect\r\nCache-Control: public, max-age=0, must-revalidate\r\nContent-Type: text/plain\r\nLocation: /\r\nConnection: close\r\n\r\n", + ); + } else { + if (type === "delay") { + setTimeout(() => { + if (!socket.destroyed) + socket.end( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: close\r\n\r\nHello Bun", + ); + }, 200); + } else { + socket.end( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: close\r\n\r\nHello Bun", + ); + } + } + } + }); + }); + await once(server.listen(0), "listening"); + + try { + let { address, port } = server.address() as AddressInfo; + if (address === "::") { + address = "[::]"; + } + const response = await fetch(`http://${address}:${port}/redirect`, { + signal: AbortSignal.timeout(150), + }); + if (type === "delay") { + console.error(response, type); + expect.unreachable(); + } else { + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello Bun"); + } + } catch (err) { + if (type === "delay") { + expect((err as Error).name).toBe("TimeoutError"); + } else { + expect.unreachable(); + } + } + } +}); diff --git a/test/js/web/fetch/fetch.unix.test.ts b/test/js/web/fetch/fetch.unix.test.ts index 3722a2c372..c62ec91ed1 100644 --- a/test/js/web/fetch/fetch.unix.test.ts +++ b/test/js/web/fetch/fetch.unix.test.ts @@ -48,7 +48,7 @@ it("throws an error when the directory is not found", () => { return new Response("hello"); }, }), - ).toThrow("No such file or directory"); + ).toThrow("no such file or directory"); }); if (process.platform === "linux") { diff --git a/test/js/web/html/html-rewriter-doctype.test.ts b/test/js/web/html/html-rewriter-doctype.test.ts new file mode 100644 index 0000000000..632a6fccb3 --- /dev/null +++ b/test/js/web/html/html-rewriter-doctype.test.ts @@ -0,0 +1,24 @@ +import { expect, test, describe } from "bun:test"; + +describe("HTMLRewriter DOCTYPE handler", () => { + test("remove and removed property work on DOCTYPE", () => { + const html = "Hello"; + let sawDoctype = false; + let wasRemoved = false; + + const rewriter = new HTMLRewriter().onDocument({ + doctype(doctype) { + sawDoctype = true; + doctype.remove(); + wasRemoved = doctype.removed; + }, + }); + + const result = rewriter.transform(html); + + expect(sawDoctype).toBe(true); + expect(wasRemoved).toBe(true); + expect(result).not.toContain(""); + }); +}); diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index 1caa6eb7ae..b8636ccf9f 100644 --- a/test/js/web/streams/streams.test.js +++ b/test/js/web/streams/streams.test.js @@ -7,7 +7,7 @@ import { readableStreamToText, } from "bun"; import { describe, expect, it, test } from "bun:test"; -import { tmpdirSync, isWindows, isMacOS } from "harness"; +import { tmpdirSync, isWindows, isMacOS, bunEnv } from "harness"; import { mkfifo } from "mkfifo"; import { createReadStream, realpathSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -445,6 +445,7 @@ it.todoIf(isWindows || isMacOS)("Bun.file() read text from pipe", async () => { stdout: "pipe", stdin: null, env: { + ...bunEnv, FIFO_TEST: large, }, }); diff --git a/test/js/web/timers/performance-entries.test.ts b/test/js/web/timers/performance-entries.test.ts new file mode 100644 index 0000000000..bfc297cc1a --- /dev/null +++ b/test/js/web/timers/performance-entries.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test"; +import { estimateShallowMemoryUsageOf } from "bun:jsc"; + +test("memory usage of Performance", () => { + const initial = estimateShallowMemoryUsageOf(performance); + for (let i = 0; i < 1024; i++) { + performance.mark(`mark-${i}`); + } + const final = estimateShallowMemoryUsageOf(performance); + + for (let i = 1; i < 1024; i++) { + performance.measure(`measure-${i}`, `mark-${i}`, `mark-${i - 1}`); + } + const final2 = estimateShallowMemoryUsageOf(performance); + expect(final2).toBeGreaterThan(final); + expect(final).toBeGreaterThan(initial); +}); diff --git a/test/js/web/timers/setTimeout.test.js b/test/js/web/timers/setTimeout.test.js index 2e4b3174ea..af7a143284 100644 --- a/test/js/web/timers/setTimeout.test.js +++ b/test/js/web/timers/setTimeout.test.js @@ -267,7 +267,7 @@ it("setTimeout if refreshed before run, should reschedule to run later", done => let start = Date.now(); let timer = setTimeout(() => { let end = Date.now(); - expect(end - start).toBeGreaterThan(149); + expect(end - start).toBeGreaterThanOrEqual(149); done(); }, 100); diff --git a/test/js/web/websocket/autobahn.test.ts b/test/js/web/websocket/autobahn.test.ts index 931db2794f..edc472883d 100644 --- a/test/js/web/websocket/autobahn.test.ts +++ b/test/js/web/websocket/autobahn.test.ts @@ -1,16 +1,20 @@ import { which } from "bun"; import { afterAll, describe, expect, it } from "bun:test"; import child_process from "child_process"; -import { tempDirWithFiles } from "harness"; - +import { tempDirWithFiles, isLinux } from "harness"; const dockerCLI = which("docker") as string; function isDockerEnabled(): boolean { if (!dockerCLI) { return false; } + // TODO: investigate why its not starting on Linux arm64 + if (isLinux && process.arch === "arm64") { + return false; + } + try { - const info = child_process.execSync(`${dockerCLI} info`, { stdio: "ignore" }); + const info = child_process.execSync(`${dockerCLI} info`, { stdio: ["ignore", "pipe", "inherit"] }); return info.toString().indexOf("Server Version:") !== -1; } catch { return false; @@ -19,7 +23,7 @@ function isDockerEnabled(): boolean { if (isDockerEnabled()) { describe("autobahn", async () => { - const url = "ws://localhost:9001"; + const url = "ws://localhost:9002"; const agent = encodeURIComponent("bun/1.0.0"); let docker: child_process.ChildProcessWithoutNullStreams | null = null; const { promise, resolve } = Promise.withResolvers(); @@ -29,7 +33,7 @@ if (isDockerEnabled()) { // ], const CWD = tempDirWithFiles("autobahn", { "fuzzingserver.json": `{ - "url": "ws://127.0.0.1:9001", + "url": "ws://127.0.0.1:9002", "outdir": "./", "cases": ["*"], "exclude-agent-cases": {} @@ -48,7 +52,7 @@ if (isDockerEnabled()) { "-v", `${CWD}:/reports`, "-p", - "9001:9001", + "9002:9002", "--name", "fuzzingserver", "crossbario/autobahn-testsuite", diff --git a/test/js/web/websocket/websocket-client.test.ts b/test/js/web/websocket/websocket-client.test.ts index 3e260f6eba..d771350fad 100644 --- a/test/js/web/websocket/websocket-client.test.ts +++ b/test/js/web/websocket/websocket-client.test.ts @@ -3,6 +3,15 @@ import { spawn } from "bun"; import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { bunEnv, bunExe, nodeExe } from "harness"; import * as path from "node:path"; +function test( + label: string, + fn: (ws: WebSocket, done: (err?: unknown) => void) => void, + timeout?: number, + isOnly = false, +) { + return makeTest(label, fn, timeout, isOnly); +} +test.only = (label, fn, timeout) => makeTest(label, fn, timeout, true); const strings = [ { @@ -236,8 +245,13 @@ describe("WebSocket", () => { }); }); -function test(label: string, fn: (ws: WebSocket, done: (err?: unknown) => void) => void, timeout?: number) { - it( +function makeTest( + label: string, + fn: (ws: WebSocket, done: (err?: unknown) => void) => void, + timeout?: number, + isOnly = false, +) { + return (isOnly ? it.only : it)( label, testDone => { let isDone = false; diff --git a/test/js/web/websocket/websocket.test.js b/test/js/web/websocket/websocket.test.js index e1736b1199..7a84a615e3 100644 --- a/test/js/web/websocket/websocket.test.js +++ b/test/js/web/websocket/websocket.test.js @@ -500,34 +500,8 @@ describe("WebSocket", () => { }); it("instances should be finalized when GC'd", async () => { - const { expect } = require("bun:test"); - - using server = Bun.serve({ - port: 0, - fetch(req, server) { - return server.upgrade(req); - }, - websocket: { - open() {}, - data() {}, - message() {}, - drain() {}, - }, - }); - - function openAndCloseWS() { - const { promise, resolve } = Promise.withResolvers(); - const sock = new WebSocket(server.url.href.replace("http", "ws")); - sock.addEventListener("open", _ => { - sock.addEventListener("close", () => { - resolve(); - }); - sock.close(); - }); - - return promise; - } - + let current_websocket_count = 0; + let initial_websocket_count = 0; function getWebSocketCount() { Bun.gc(true); const objectTypeCounts = require("bun:jsc").heapStats().objectTypeCounts || { @@ -535,25 +509,53 @@ describe("WebSocket", () => { }; return objectTypeCounts.WebSocket || 0; } - let current_websocket_count = 0; - let initial_websocket_count = 0; - for (let i = 0; i < 1000; i++) { - await openAndCloseWS(); - if (i % 100 === 0) { - current_websocket_count = getWebSocketCount(); - // if we have more than 1 batch of websockets open, we have a problem - expect(current_websocket_count).toBeLessThanOrEqual(100); - if (initial_websocket_count === 0) { - initial_websocket_count = current_websocket_count; + async function run() { + using server = Bun.serve({ + port: 0, + fetch(req, server) { + return server.upgrade(req); + }, + websocket: { + open() {}, + data() {}, + message() {}, + drain() {}, + }, + }); + + function onOpen(sock, resolve) { + sock.addEventListener("close", resolve, { once: true }); + sock.close(); + } + + function openAndCloseWS() { + const { promise, resolve } = Promise.withResolvers(); + const sock = new WebSocket(server.url.href.replace("http", "ws")); + sock.addEventListener("open", onOpen.bind(undefined, sock, resolve), { + once: true, + }); + + return promise; + } + + for (let i = 0; i < 1000; i++) { + await openAndCloseWS(); + if (i % 100 === 0) { + if (initial_websocket_count === 0) { + initial_websocket_count = getWebSocketCount(); + } } } } + await run(); + // wait next tick to run the last time - await Bun.sleep(1); + await Bun.sleep(100); current_websocket_count = getWebSocketCount(); + console.log({ current_websocket_count, initial_websocket_count }); // expect that current and initial websocket be close to the same (normaly 1 or 2 difference) - expect(Math.abs(current_websocket_count - initial_websocket_count)).toBeLessThanOrEqual(5); + expect(Math.abs(current_websocket_count - initial_websocket_count)).toBeLessThanOrEqual(50); }); it("should be able to send big messages", async () => { diff --git a/test/js/workerd/html-rewriter.test.js b/test/js/workerd/html-rewriter.test.js index 2bdd093be4..4e58b43ac5 100644 --- a/test/js/workerd/html-rewriter.test.js +++ b/test/js/workerd/html-rewriter.test.js @@ -15,6 +15,50 @@ var setTimeoutAsync = (fn, delay) => { }; describe("HTMLRewriter", () => { + it("error handling", () => { + expect(() => new HTMLRewriter().transform(Symbol("ok"))).toThrow(); + }); + + it("error inside element handler", () => { + expect(() => + new HTMLRewriter() + .on("div", { + element(element) { + throw new Error("test"); + }, + }) + .transform(new Response("
hello
")), + ).toThrow("test"); + }); + + it("error inside element handler (string)", () => { + expect(() => + new HTMLRewriter() + .on("div", { + element(element) { + throw new Error("test"); + }, + }) + .transform("
hello
"), + ).toThrow("test"); + }); + + it("async error inside element handler", async () => { + try { + await new HTMLRewriter() + .on("div", { + async element(element) { + await Bun.sleep(0); + throw new Error("test"); + }, + }) + .transform(new Response("
hello
")); + expect.unreachable(); + } catch (e) { + expect(e.message).toBe("test"); + } + }); + it("HTMLRewriter: async replacement", async () => { await gcTick(); const res = new HTMLRewriter() diff --git a/test/napi/napi-app/binding.gyp b/test/napi/napi-app/binding.gyp index aebdebb7ef..39b61162a2 100644 --- a/test/napi/napi-app/binding.gyp +++ b/test/napi/napi-app/binding.gyp @@ -10,7 +10,7 @@ "AdditionalOptions": ["/std:c++20"], }, }, - "sources": ["main.cpp"], + "sources": ["main.cpp", "wrap_tests.cpp"], "include_dirs": [" - -#include +#include "napi_with_version.h" +#include "utils.h" +#include "wrap_tests.h" #include #include +#include #include #include #include @@ -30,23 +31,6 @@ napi_value fail_fmt(napi_env env, const char *fmt, ...) { return fail(env, buf); } -napi_value ok(napi_env env) { - napi_value result; - napi_get_undefined(env, &result); - return result; -} - -static void run_gc(const Napi::CallbackInfo &info) { - info[0].As().Call(0, nullptr); -} - -// calls napi_typeof and asserts it returns napi_ok -static napi_valuetype get_typeof(napi_env env, napi_value value) { - napi_valuetype result; - assert(napi_typeof(env, value, &result) == napi_ok); - return result; -} - napi_value test_issue_7685(const Napi::CallbackInfo &info) { Napi::Env env(info.Env()); Napi::HandleScope scope(env); @@ -595,33 +579,6 @@ napi_value was_finalize_called(const Napi::CallbackInfo &info) { return ret; } -static const char *napi_valuetype_to_string(napi_valuetype type) { - switch (type) { - case napi_undefined: - return "undefined"; - case napi_null: - return "null"; - case napi_boolean: - return "boolean"; - case napi_number: - return "number"; - case napi_string: - return "string"; - case napi_symbol: - return "symbol"; - case napi_object: - return "object"; - case napi_function: - return "function"; - case napi_external: - return "external"; - case napi_bigint: - return "bigint"; - default: - return "unknown"; - } -} - // calls a function (the sole argument) which must throw. catches and returns // the thrown error napi_value call_and_get_exception(const Napi::CallbackInfo &info) { @@ -1013,6 +970,167 @@ static napi_value try_add_tag(const Napi::CallbackInfo &info) { return Napi::Boolean::New(env, status == napi_ok); } +static napi_value bigint_to_i64(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + // start at 1 is intentional, since argument 0 is the callback to run GC + // passed to every function + // perform test on all arguments + for (size_t i = 1; i < info.Length(); i++) { + napi_value bigint = info[i]; + + napi_valuetype type; + NODE_API_CALL(env, napi_typeof(env, bigint, &type)); + + int64_t result = 0; + bool lossless = false; + + if (type != napi_bigint) { + printf("napi_get_value_bigint_int64 return for non-bigint: %d\n", + napi_get_value_bigint_int64(env, bigint, &result, &lossless)); + } else { + NODE_API_CALL( + env, napi_get_value_bigint_int64(env, bigint, &result, &lossless)); + printf("napi_get_value_bigint_int64 result: %" PRId64 "\n", result); + printf("lossless: %s\n", lossless ? "true" : "false"); + } + } + + return ok(env); +} + +static napi_value bigint_to_u64(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + // start at 1 is intentional, since argument 0 is the callback to run GC + // passed to every function + // perform test on all arguments + for (size_t i = 1; i < info.Length(); i++) { + napi_value bigint = info[i]; + + napi_valuetype type; + NODE_API_CALL(env, napi_typeof(env, bigint, &type)); + + uint64_t result; + bool lossless; + + if (type != napi_bigint) { + printf("napi_get_value_bigint_uint64 return for non-bigint: %d\n", + napi_get_value_bigint_uint64(env, bigint, &result, &lossless)); + } else { + NODE_API_CALL( + env, napi_get_value_bigint_uint64(env, bigint, &result, &lossless)); + printf("napi_get_value_bigint_uint64 result: %" PRIu64 "\n", result); + printf("lossless: %s\n", lossless ? "true" : "false"); + } + } + + return ok(env); +} + +static napi_value bigint_to_64_null(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value bigint; + NODE_API_CALL(env, napi_create_bigint_int64(env, 5, &bigint)); + + int64_t result_signed; + uint64_t result_unsigned; + bool lossless; + + printf("status (int64, null result) = %d\n", + napi_get_value_bigint_int64(env, bigint, nullptr, &lossless)); + printf("status (int64, null lossless) = %d\n", + napi_get_value_bigint_int64(env, bigint, &result_signed, nullptr)); + printf("status (uint64, null result) = %d\n", + napi_get_value_bigint_uint64(env, bigint, nullptr, &lossless)); + printf("status (uint64, null lossless) = %d\n", + napi_get_value_bigint_uint64(env, bigint, &result_unsigned, nullptr)); + + return ok(env); +} + +static napi_value create_weird_bigints(const Napi::CallbackInfo &info) { + // create bigints by passing weird parameters to napi_create_bigint_words + napi_env env = info.Env(); + + std::array bigints; + std::array words{{123, 0, 0, 0}}; + + NODE_API_CALL(env, napi_create_bigint_int64(env, 0, &bigints[0])); + NODE_API_CALL(env, napi_create_bigint_uint64(env, 0, &bigints[1])); + // sign is not 0 or 1 (should be interpreted as negative) + NODE_API_CALL(env, + napi_create_bigint_words(env, 2, 1, words.data(), &bigints[2])); + // leading zeroes in word representation + NODE_API_CALL(env, + napi_create_bigint_words(env, 0, 4, words.data(), &bigints[3])); + // zero + NODE_API_CALL(env, + napi_create_bigint_words(env, 1, 0, words.data(), &bigints[4])); + + napi_value array; + NODE_API_CALL(env, + napi_create_array_with_length(env, bigints.size(), &array)); + for (size_t i = 0; i < bigints.size(); i++) { + NODE_API_CALL(env, napi_set_element(env, array, (uint32_t)i, bigints[i])); + } + return array; +} + +// Call Node-API functions in ways that result in different error handling +// (erroneous call, valid call, or valid call while an exception is pending) and +// log information from napi_get_last_error_info +static napi_value test_extended_error_messages(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + const napi_extended_error_info *error; + + // this function is implemented in C++ + // error because the result pointer is null + printf("erroneous napi_create_double returned code %d\n", + napi_create_double(env, 1.0, nullptr)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("erroneous napi_create_double info: code = %d, message = %s\n", + error->error_code, error->error_message); + + // this function should succeed and the success should overwrite the error + // from the last call + napi_value js_number; + printf("successful napi_create_double returned code %d\n", + napi_create_double(env, 5.0, &js_number)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("successful napi_create_double info: code = %d, message = %s\n", + error->error_code, + error->error_message ? error->error_message : "(null)"); + + // this function is implemented in zig + // error because the value is not an array + unsigned int len; + printf("erroneous napi_get_array_length returned code %d\n", + napi_get_array_length(env, js_number, &len)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("erroneous napi_get_array_length info: code = %d, message = %s\n", + error->error_code, error->error_message); + + // throw an exception + NODE_API_CALL(env, napi_throw_type_error(env, nullptr, "oops!")); + // nothing is wrong with this call by itself, but it should return + // napi_pending_exception without doing anything because an exception is + // pending + napi_value coerced_string; + printf("napi_coerce_to_string with pending exception returned code %d\n", + napi_coerce_to_string(env, js_number, &coerced_string)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf( + "napi_coerce_to_string with pending exception info: code = %d, message = " + "%s\n", + error->error_code, error->error_message); + + // clear the exception + napi_value exception; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); + + return ok(env); +} + Napi::Value RunCallback(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); // this function is invoked without the GC callback @@ -1079,6 +1197,15 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { exports.Set("add_tag", Napi::Function::New(env, add_tag)); exports.Set("try_add_tag", Napi::Function::New(env, try_add_tag)); exports.Set("check_tag", Napi::Function::New(env, check_tag)); + exports.Set("bigint_to_i64", Napi::Function::New(env, bigint_to_i64)); + exports.Set("bigint_to_u64", Napi::Function::New(env, bigint_to_u64)); + exports.Set("bigint_to_64_null", Napi::Function::New(env, bigint_to_64_null)); + exports.Set("create_weird_bigints", + Napi::Function::New(env, create_weird_bigints)); + exports.Set("test_extended_error_messages", + Napi::Function::New(env, test_extended_error_messages)); + + napitests::register_wrap_tests(env, exports); return exports; } diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js index 6cb1280cf2..8516abb5b6 100644 --- a/test/napi/napi-app/module.js +++ b/test/napi/napi-app/module.js @@ -1,4 +1,30 @@ const nativeTests = require("./build/Release/napitests.node"); +const secondAddon = require("./build/Release/second_addon.node"); + +function assert(ok) { + if (!ok) { + throw new Error("assertion failed"); + } +} + +async function gcUntil(fn) { + const MAX = 100; + for (let i = 0; i < MAX; i++) { + await new Promise(resolve => { + setTimeout(resolve, 1); + }); + if (typeof Bun == "object") { + Bun.gc(true); + } else { + // if this fails, you need to pass --expose-gc to node + global.gc(); + } + if (fn()) { + return; + } + } + throw new Error(`Condition was not met after ${MAX} GC attempts`); +} nativeTests.test_napi_class_constructor_handle_scope = () => { const NapiClass = nativeTests.get_class_with_constructor(); @@ -270,4 +296,202 @@ nativeTests.test_type_tag = () => { console.log("o2 matches o2:", nativeTests.check_tag(o2, 3, 4)); }; +nativeTests.test_napi_wrap = () => { + const values = [ + {}, + {}, // should be able to be wrapped differently than the distinct empty object above + 5, + new Number(5), + "abc", + new String("abc"), + null, + Symbol("abc"), + Symbol.for("abc"), + new (nativeTests.get_class_with_constructor())(), + new Proxy( + {}, + Object.fromEntries( + [ + "apply", + "construct", + "defineProperty", + "deleteProperty", + "get", + "getOwnPropertyDescriptor", + "getPrototypeOf", + "has", + "isExtensible", + "ownKeys", + "preventExtensions", + "set", + "setPrototypeOf", + ].map(name => [ + name, + () => { + throw new Error("oops"); + }, + ]), + ), + ), + ]; + const wrapSuccess = Array(values.length).fill(false); + for (const [i, v] of values.entries()) { + wrapSuccess[i] = nativeTests.try_wrap(v, i + 1); + console.log(`${typeof v} did wrap: `, wrapSuccess[i]); + } + + for (const [i, v] of values.entries()) { + if (wrapSuccess[i]) { + if (nativeTests.try_unwrap(v) !== i + 1) { + throw new Error("could not unwrap same value"); + } + } else { + if (nativeTests.try_unwrap(v) !== undefined) { + throw new Error("value unwraps without being successfully wrapped"); + } + } + } +}; + +nativeTests.test_napi_wrap_proxy = () => { + const target = {}; + const proxy = new Proxy(target, {}); + assert(nativeTests.try_wrap(target, 5)); + assert(nativeTests.try_wrap(proxy, 6)); + console.log(nativeTests.try_unwrap(target), nativeTests.try_unwrap(proxy)); +}; + +nativeTests.test_napi_wrap_cross_addon = () => { + const wrapped = {}; + console.log("wrap succeeds:", nativeTests.try_wrap(wrapped, 42)); + console.log("unwrapped from other addon", secondAddon.try_unwrap(wrapped)); +}; + +nativeTests.test_napi_wrap_prototype = () => { + class Foo {} + console.log("wrap prototype succeeds:", nativeTests.try_wrap(Foo.prototype, 42)); + // wrapping should not look at prototype chain + console.log("unwrap instance:", nativeTests.try_unwrap(new Foo())); +}; + +nativeTests.test_napi_remove_wrap = () => { + const targets = [{}, new (nativeTests.get_class_with_constructor())()]; + for (const t of targets) { + const target = {}; + // fails + assert(nativeTests.try_remove_wrap(target) === undefined); + // wrap it + assert(nativeTests.try_wrap(target, 5)); + // remove yields the wrapped value + assert(nativeTests.try_remove_wrap(target) === 5); + // neither remove nor unwrap work anymore + assert(nativeTests.try_unwrap(target) === undefined); + assert(nativeTests.try_remove_wrap(target) === undefined); + // can re-wrap + assert(nativeTests.try_wrap(target, 6)); + assert(nativeTests.try_unwrap(target) === 6); + } +}; + +// parameters to create_wrap are: object, ask_for_ref, strong +const createWrapWithoutRef = o => nativeTests.create_wrap(o, false, false); +const createWrapWithWeakRef = o => nativeTests.create_wrap(o, true, false); +const createWrapWithStrongRef = o => nativeTests.create_wrap(o, true, true); + +nativeTests.test_wrap_lifetime_without_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithoutRef(object) === object); + assert(nativeTests.get_wrap_data(object) === 42); + object = undefined; + await gcUntil(() => nativeTests.was_wrap_finalize_called()); +}; + +nativeTests.test_wrap_lifetime_with_weak_ref = async () => { + // this looks the same as test_wrap_lifetime_without_ref because it is -- these cases should behave the same + let object = { foo: "bar" }; + assert(createWrapWithWeakRef(object) === object); + assert(nativeTests.get_wrap_data(object) === 42); + object = undefined; + await gcUntil(() => nativeTests.was_wrap_finalize_called()); +}; + +nativeTests.test_wrap_lifetime_with_strong_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithStrongRef(object) === object); + assert(nativeTests.get_wrap_data(object) === 42); + + object = undefined; + // still referenced by native module so this should fail + try { + await gcUntil(() => nativeTests.was_wrap_finalize_called()); + throw new Error("object was garbage collected while still referenced by native code"); + } catch (e) { + if (!e.toString().includes("Condition was not met")) { + throw e; + } + } + + // can still get the value using the ref + assert(nativeTests.get_wrap_data_from_ref() === 42); + + // now we free it + nativeTests.unref_wrapped_value(); + await gcUntil(() => nativeTests.was_wrap_finalize_called()); +}; + +nativeTests.test_remove_wrap_lifetime_with_weak_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithWeakRef(object) === object); + + assert(nativeTests.get_wrap_data(object) === 42); + + nativeTests.remove_wrap(object); + assert(nativeTests.get_wrap_data(object) === undefined); + assert(nativeTests.get_wrap_data_from_ref() === undefined); + assert(nativeTests.get_object_from_ref() === object); + + object = undefined; + + // ref will stop working once the object is collected + await gcUntil(() => nativeTests.get_object_from_ref() === undefined); + + // finalizer shouldn't have been called + assert(nativeTests.was_wrap_finalize_called() === false); +}; + +nativeTests.test_remove_wrap_lifetime_with_strong_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithStrongRef(object) === object); + + assert(nativeTests.get_wrap_data(object) === 42); + + nativeTests.remove_wrap(object); + assert(nativeTests.get_wrap_data(object) === undefined); + assert(nativeTests.get_wrap_data_from_ref() === undefined); + assert(nativeTests.get_object_from_ref() === object); + + object = undefined; + + // finalizer should not be called and object should not be freed + try { + await gcUntil(() => nativeTests.was_wrap_finalize_called() || nativeTests.get_object_from_ref() === undefined); + throw new Error("finalizer ran"); + } catch (e) { + if (!e.toString().includes("Condition was not met")) { + throw e; + } + } + + // native code can still get the object + assert(JSON.stringify(nativeTests.get_object_from_ref()) === `{"foo":"bar"}`); + + // now it gets deleted + nativeTests.unref_wrapped_value(); + await gcUntil(() => nativeTests.get_object_from_ref() === undefined); +}; + +nativeTests.test_create_bigint_words = () => { + console.log(nativeTests.create_weird_bigints()); +}; + module.exports = nativeTests; diff --git a/test/napi/napi-app/napi_with_version.h b/test/napi/napi-app/napi_with_version.h new file mode 100644 index 0000000000..f852184087 --- /dev/null +++ b/test/napi/napi-app/napi_with_version.h @@ -0,0 +1,8 @@ +#pragma once +#define NAPI_EXPERIMENTAL +#include +#include + +// TODO(@190n): remove this when CI has Node 22.6 +typedef struct napi_env__ *napi_env; +typedef napi_env node_api_basic_env; diff --git a/test/napi/napi-app/second_addon.c b/test/napi/napi-app/second_addon.c new file mode 100644 index 0000000000..85232861dd --- /dev/null +++ b/test/napi/napi-app/second_addon.c @@ -0,0 +1,53 @@ +#include +#include +#include + +#define NODE_API_CALL(env, call) \ + do { \ + napi_status status = (call); \ + if (status != napi_ok) { \ + const napi_extended_error_info *error_info = NULL; \ + napi_get_last_error_info((env), &error_info); \ + const char *err_message = error_info->error_message; \ + bool is_pending; \ + napi_is_exception_pending((env), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + const char *message = \ + (err_message == NULL) ? "empty error message" : err_message; \ + napi_throw_error((env), NULL, message); \ + } \ + return NULL; \ + } \ + } while (0) + +static napi_value try_unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + if (argc != 1) { + napi_throw_error(env, NULL, "Wrong number of arguments to try_unwrap"); + return NULL; + } + + double *pointer; + if (napi_unwrap(env, argv[0], (void **)(&pointer)) != napi_ok) { + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; + } else { + napi_value number; + NODE_API_CALL(env, napi_create_double(env, *pointer, &number)); + return number; + } +} + +/* napi_value */ NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) { + napi_value try_unwrap_function; + NODE_API_CALL(env, + napi_create_function(env, "try_unwrap", NAPI_AUTO_LENGTH, + try_unwrap, NULL, &try_unwrap_function)); + NODE_API_CALL(env, napi_set_named_property(env, exports, "try_unwrap", + try_unwrap_function)); + return exports; +} diff --git a/test/napi/napi-app/utils.h b/test/napi/napi-app/utils.h new file mode 100644 index 0000000000..92e158e6b7 --- /dev/null +++ b/test/napi/napi-app/utils.h @@ -0,0 +1,89 @@ +#pragma once +#include "napi_with_version.h" +#include + +// e.g NODE_API_CALL(env, napi_create_int32(env, 5, &my_napi_integer)) +#define NODE_API_CALL(env, call) NODE_API_CALL_CUSTOM_RETURN(env, NULL, call) + +// Version of NODE_API_CALL for functions not returning napi_value +#define NODE_API_CALL_CUSTOM_RETURN(env, value_to_return_if_threw, call) \ + NODE_API_ASSERT_CUSTOM_RETURN(env, value_to_return_if_threw, \ + (call) == napi_ok) + +// Throw an error in the given napi_env and return if expr is false +#define NODE_API_ASSERT(env, expr) \ + NODE_API_ASSERT_CUSTOM_RETURN(env, NULL, expr) + +#ifdef _MSC_VER +#define CURRENT_FUNCTION_NAME __FUNCSIG__ +#else +#define CURRENT_FUNCTION_NAME __PRETTY_FUNCTION__ +#endif + +// Version of NODE_API_ASSERT for functions not returning napi_value +#define NODE_API_ASSERT_CUSTOM_RETURN(ENV, VALUE_TO_RETURN_IF_THREW, EXPR) \ + do { \ + if (!(EXPR)) { \ + bool is_pending; \ + napi_is_exception_pending((ENV), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + char buf[4096] = {0}; \ + snprintf(buf, sizeof(buf) - 1, "%s (%s:%d): Assertion failed: %s", \ + CURRENT_FUNCTION_NAME, __FILE__, __LINE__, #EXPR); \ + napi_throw_error((ENV), NULL, buf); \ + } \ + return (VALUE_TO_RETURN_IF_THREW); \ + } \ + } while (0) + +#define REGISTER_FUNCTION(ENV, EXPORTS, FUNCTION) \ + EXPORTS.Set(#FUNCTION, Napi::Function::New(ENV, FUNCTION)) + +static inline napi_value ok(napi_env env) { + napi_value result; + napi_get_undefined(env, &result); + return result; +} + +// For functions that take a garbage collection callback as the first argument +// (functions not called directly by module.js), use this to trigger GC +static inline void run_gc(const Napi::CallbackInfo &info) { + info[0].As().Call(0, nullptr); +} + +// calls napi_typeof and asserts it returns napi_ok +static inline napi_valuetype get_typeof(napi_env env, napi_value value) { + napi_valuetype result; + // return an invalid napi_valuetype if the call to napi_typeof fails + NODE_API_CALL_CUSTOM_RETURN(env, static_cast(INT_MAX), + napi_typeof(env, value, &result)); + return result; +} + +static inline const char *napi_valuetype_to_string(napi_valuetype type) { + switch (type) { + case napi_undefined: + return "undefined"; + case napi_null: + return "null"; + case napi_boolean: + return "boolean"; + case napi_number: + return "number"; + case napi_string: + return "string"; + case napi_symbol: + return "symbol"; + case napi_object: + return "object"; + case napi_function: + return "function"; + case napi_external: + return "external"; + case napi_bigint: + return "bigint"; + default: + return "unknown"; + } +} diff --git a/test/napi/napi-app/wrap_tests.cpp b/test/napi/napi-app/wrap_tests.cpp new file mode 100644 index 0000000000..5365a29e89 --- /dev/null +++ b/test/napi/napi-app/wrap_tests.cpp @@ -0,0 +1,232 @@ +#include "wrap_tests.h" + +#include "utils.h" +#include + +namespace napitests { + +static napi_ref ref_to_wrapped_object = nullptr; +static bool wrap_finalize_called = false; + +// static void delete_the_ref(napi_env env, void *_data, void *_hint) { +// printf("delete_the_ref\n"); +// // not using NODE_API_ASSERT as this runs in a finalizer where allocating +// an +// // error might cause a harder-to-debug crash +// assert(ref_to_wrapped_object); +// napi_delete_reference(env, ref_to_wrapped_object); +// ref_to_wrapped_object = nullptr; +// } + +static void finalize_for_create_wrap(napi_env env, void *opaque_data, + void *opaque_hint) { + int *data = reinterpret_cast(opaque_data); + int *hint = reinterpret_cast(opaque_hint); + printf("finalize_for_create_wrap, data = %d, hint = %d\n", *data, *hint); + delete data; + delete hint; + // TODO: this needs https://github.com/oven-sh/bun/pulls/14501 to work + // if (ref_to_wrapped_object) { + // node_api_post_finalizer(env, delete_the_ref, nullptr, nullptr); + // } + wrap_finalize_called = true; +} + +// create_wrap(js_object: object, ask_for_ref: boolean, strong: boolean): object +static napi_value create_wrap(const Napi::CallbackInfo &info) { + wrap_finalize_called = false; + napi_env env = info.Env(); + napi_value js_object = info[0]; + + napi_value js_ask_for_ref = info[1]; + bool ask_for_ref; + NODE_API_CALL(env, napi_get_value_bool(env, js_ask_for_ref, &ask_for_ref)); + napi_value js_strong = info[2]; + bool strong; + NODE_API_CALL(env, napi_get_value_bool(env, js_strong, &strong)); + + // wrap it + int *wrap_data = new int(42); + int *wrap_hint = new int(123); + + NODE_API_CALL(env, napi_wrap(env, js_object, wrap_data, + finalize_for_create_wrap, wrap_hint, + ask_for_ref ? &ref_to_wrapped_object : nullptr)); + if (ask_for_ref && strong) { + uint32_t new_refcount; + NODE_API_CALL( + env, napi_reference_ref(env, ref_to_wrapped_object, &new_refcount)); + NODE_API_ASSERT(env, new_refcount == 1); + } + + if (!ask_for_ref) { + ref_to_wrapped_object = nullptr; + } + + return js_object; +} + +// get_wrap_data(js_object: object): number +static napi_value get_wrap_data(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value js_object = info[0]; + + void *wrapped_data; + napi_status status = napi_unwrap(env, js_object, &wrapped_data); + if (status != napi_ok) { + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; + } + + napi_value js_number; + NODE_API_CALL(env, + napi_create_int32(env, *reinterpret_cast(wrapped_data), + &js_number)); + return js_number; +} + +// get_object_from_ref(): object +static napi_value get_object_from_ref(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value wrapped_object; + NODE_API_CALL(env, napi_get_reference_value(env, ref_to_wrapped_object, + &wrapped_object)); + + if (!wrapped_object) { + NODE_API_CALL(env, napi_get_undefined(env, &wrapped_object)); + } + return wrapped_object; +} + +// get_wrap_data_from_ref(): number|undefined +static napi_value get_wrap_data_from_ref(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value wrapped_object; + NODE_API_CALL(env, napi_get_reference_value(env, ref_to_wrapped_object, + &wrapped_object)); + + void *wrapped_data; + napi_status status = napi_unwrap(env, wrapped_object, &wrapped_data); + if (status == napi_ok) { + napi_value js_number; + NODE_API_CALL(env, + napi_create_int32(env, *reinterpret_cast(wrapped_data), + &js_number)); + return js_number; + } else if (status == napi_invalid_arg) { + // no longer wrapped + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; + } else { + NODE_API_ASSERT(env, false && "this should not be reached"); + return nullptr; + } +} + +// remove_wrap_data(js_object: object): undefined +static napi_value remove_wrap(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value js_object = info[0]; + + void *wrap_data; + NODE_API_CALL(env, napi_remove_wrap(env, js_object, &wrap_data)); + + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; +} + +// unref_wrapped_value(): undefined +static napi_value unref_wrapped_value(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + uint32_t new_refcount; + NODE_API_CALL( + env, napi_reference_unref(env, ref_to_wrapped_object, &new_refcount)); + // should never have been set higher than 1 + NODE_API_ASSERT(env, new_refcount == 0); + + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; +} + +static napi_value was_wrap_finalize_called(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + return Napi::Boolean::New(env, wrap_finalize_called); +} + +// try_wrap(value: any, num: number): bool +// wraps value in a C++ object corresponding to the number num +// true if success +static napi_value try_wrap(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value value = info[0]; + napi_value js_num = info[1]; + double c_num; + NODE_API_CALL(env, napi_get_value_double(env, js_num, &c_num)); + + napi_status status = napi_wrap( + env, value, reinterpret_cast(new double{c_num}), + [](napi_env env, void *data, void *hint) { + (void)env; + (void)hint; + delete reinterpret_cast(data); + }, + nullptr, nullptr); + + napi_value js_result; + assert(napi_get_boolean(env, status == napi_ok, &js_result) == napi_ok); + return js_result; +} + +// try_unwrap(any): number|undefined +static napi_value try_unwrap(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value value = info[0]; + + double *wrapped; + napi_status status = + napi_unwrap(env, value, reinterpret_cast(&wrapped)); + napi_value result; + if (status == napi_ok) { + NODE_API_CALL(env, napi_create_double(env, *wrapped, &result)); + } else { + NODE_API_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +static napi_value try_remove_wrap(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value value = info[0]; + + double *wrapped; + napi_status status = + napi_remove_wrap(env, value, reinterpret_cast(&wrapped)); + napi_value result; + if (status == napi_ok) { + NODE_API_CALL(env, napi_create_double(env, *wrapped, &result)); + } else { + NODE_API_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +void register_wrap_tests(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, create_wrap); + REGISTER_FUNCTION(env, exports, get_wrap_data); + REGISTER_FUNCTION(env, exports, get_object_from_ref); + REGISTER_FUNCTION(env, exports, get_wrap_data_from_ref); + REGISTER_FUNCTION(env, exports, remove_wrap); + REGISTER_FUNCTION(env, exports, unref_wrapped_value); + REGISTER_FUNCTION(env, exports, was_wrap_finalize_called); + REGISTER_FUNCTION(env, exports, try_wrap); + REGISTER_FUNCTION(env, exports, try_unwrap); + REGISTER_FUNCTION(env, exports, try_remove_wrap); +} + +} // namespace napitests diff --git a/test/napi/napi-app/wrap_tests.h b/test/napi/napi-app/wrap_tests.h new file mode 100644 index 0000000000..a70a44240e --- /dev/null +++ b/test/napi/napi-app/wrap_tests.h @@ -0,0 +1,11 @@ +#pragma once + +// Helper functions used by JS to test napi_wrap + +#include "napi_with_version.h" + +namespace napitests { + +void register_wrap_tests(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index 25144c80f7..1030c4eb86 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -115,6 +115,7 @@ describe("napi", () => { outdir: dir, target, format, + throw: true, }); expect(build.logs).toBeEmpty(); @@ -319,6 +320,70 @@ describe("napi", () => { checkSameOutput("test_type_tag", []); }); }); + + describe("napi_wrap", () => { + it("accepts the right kinds of values", () => { + checkSameOutput("test_napi_wrap", []); + }); + + it("is shared between addons", () => { + checkSameOutput("test_napi_wrap_cross_addon", []); + }); + + it("does not follow prototypes", () => { + checkSameOutput("test_napi_wrap_prototype", []); + }); + + it("does not consider proxies", () => { + checkSameOutput("test_napi_wrap_proxy", []); + }); + + it("can remove a wrap", () => { + checkSameOutput("test_napi_remove_wrap", []); + }); + + it("has the right lifetime", () => { + checkSameOutput("test_wrap_lifetime_without_ref", []); + checkSameOutput("test_wrap_lifetime_with_weak_ref", []); + checkSameOutput("test_wrap_lifetime_with_strong_ref", []); + checkSameOutput("test_remove_wrap_lifetime_with_weak_ref", []); + checkSameOutput("test_remove_wrap_lifetime_with_strong_ref", []); + }); + }); + + describe("bigint conversion to int64/uint64", () => { + it("works", () => { + const tests = [-1n, 0n, 1n]; + for (const power of [63, 64, 65]) { + for (const sign of [-1, 1]) { + const boundary = BigInt(sign) * 2n ** BigInt(power); + tests.push(boundary, boundary - 1n, boundary + 1n); + } + } + + const testsString = "[" + tests.map(bigint => bigint.toString() + "n").join(",") + "]"; + checkSameOutput("bigint_to_i64", testsString); + checkSameOutput("bigint_to_u64", testsString); + }); + it("returns the right error code", () => { + const badTypes = '[null, undefined, 5, "123", "abc"]'; + checkSameOutput("bigint_to_i64", badTypes); + checkSameOutput("bigint_to_u64", badTypes); + checkSameOutput("bigint_to_64_null", []); + }); + }); + + describe("create_bigint_words", () => { + it("works", () => { + checkSameOutput("test_create_bigint_words", []); + }); + }); + + describe("napi_get_last_error_info", () => { + it("returns information from the most recent call", () => { + checkSameOutput("test_extended_error_messages", []); + }); + }); }); function checkSameOutput(test: string, args: any[] | string) { diff --git a/test/package-json-lint.test.ts b/test/package-json-lint.test.ts index fb3d9a55c3..aa253f17e3 100644 --- a/test/package-json-lint.test.ts +++ b/test/package-json-lint.test.ts @@ -22,20 +22,25 @@ describe("package.json dependencies must be exact versions", async () => { optionalDependencies = {}, } = await Bun.file(join(dir, "./package.json")).json(); + // Hyphen is necessary to accept prerelease versions like "1.1.3-alpha.7" + // This regex still forbids semver ranges like "1.0.0 - 1.2.0", as those must have spaces + // around the hyphen. + const okRegex = /^([a-zA-Z0-9\.\-])+$/; + for (const [name, dep] of Object.entries(dependencies)) { - expect(dep).toMatch(/^([a-zA-Z0-9\.])+$/); + expect(dep, `dependency ${name} specifies non-exact version "${dep}"`).toMatch(okRegex); } for (const [name, dep] of Object.entries(devDependencies)) { - expect(dep).toMatch(/^([a-zA-Z0-9\.])+$/); + expect(dep, `dev dependency ${name} specifies non-exact version "${dep}"`).toMatch(okRegex); } for (const [name, dep] of Object.entries(peerDependencies)) { - expect(dep).toMatch(/^([a-zA-Z0-9\.])+$/); + expect(dep, `peer dependency ${name} specifies non-exact version "${dep}"`).toMatch(okRegex); } for (const [name, dep] of Object.entries(optionalDependencies)) { - expect(dep).toMatch(/^([a-zA-Z0-9\.])+$/); + expect(dep, `optional dependency ${name} specifies non-exact version "${dep}"`).toMatch(okRegex); } }); } diff --git a/test/package.json b/test/package.json index 03a8717ce3..edc248dadf 100644 --- a/test/package.json +++ b/test/package.json @@ -4,33 +4,42 @@ "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "@types/supertest": "2.0.12", - "@types/utf-8-validate": "5.0.0" + "@types/utf-8-validate": "5.0.0", + "@types/ws": "8.5.10", + "@types/puppeteer": "7.0.4" }, "dependencies": { "@azure/service-bus": "7.9.4", + "@duckdb/node-api": "1.1.3-alpha.7", + "@electric-sql/pglite": "0.2.15", "@grpc/grpc-js": "1.12.0", "@grpc/proto-loader": "0.7.10", - "@napi-rs/canvas": "0.1.47", + "@napi-rs/canvas": "0.1.65", "@prisma/client": "5.8.0", "@remix-run/react": "2.10.3", "@remix-run/serve": "2.10.3", "@resvg/resvg-js": "2.4.1", "@swc/core": "1.3.38", - "@types/ws": "8.5.10", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.1.0", "aws-cdk-lib": "2.148.0", "axios": "1.6.8", "body-parser": "1.20.2", "comlink": "4.4.1", + "devalue": "5.1.1", "es-module-lexer": "1.3.0", "esbuild": "0.18.6", "express": "4.18.2", "fast-glob": "3.3.1", "filenamify": "6.0.0", + "happy-dom": "16.5.3", "http2-wrapper": "2.2.1", "https-proxy-agent": "7.0.5", "iconv-lite": "0.6.3", "isbot": "5.1.13", "jest-extended": "4.0.0", + "jimp": "1.6.0", + "jsdom": "25.0.1", "jsonwebtoken": "9.0.2", "jws": "4.0.0", "lodash": "4.17.21", @@ -59,9 +68,10 @@ "string-width": "7.0.0", "stripe": "15.4.0", "supertest": "6.3.3", - "svelte": "3.55.1", + "svelte": "5.4.0", "typescript": "5.0.2", "undici": "5.20.0", + "v8-heapsnapshot": "1.3.1", "verdaccio": "6.0.0", "vitest": "0.32.2", "webpack": "5.88.0", diff --git a/test/preload.ts b/test/preload.ts index 811af099b9..35322932e1 100644 --- a/test/preload.ts +++ b/test/preload.ts @@ -4,6 +4,7 @@ import * as harness from "./harness"; // so process.env = {} causes them to be out of sync and we assume Bun.env is for (let key in process.env) { if (key === "TZ") continue; + if (key in harness.bunEnv) continue; delete process.env[key]; } @@ -12,7 +13,6 @@ for (let key in harness.bunEnv) { if (harness.bunEnv[key] === undefined) { continue; } - process.env[key] = harness.bunEnv[key] + ""; } diff --git a/test/regression/issue/08093.test.ts b/test/regression/issue/08093.test.ts index 280d0ec4df..4d32dab6b7 100644 --- a/test/regression/issue/08093.test.ts +++ b/test/regression/issue/08093.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env } from "harness"; +import { bunExe, bunEnv as env, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/regression/issue/09555.test.ts b/test/regression/issue/09555.test.ts index ef2fc27b58..52868bae84 100644 --- a/test/regression/issue/09555.test.ts +++ b/test/regression/issue/09555.test.ts @@ -28,8 +28,8 @@ describe("#09555", () => { let total = 0; const res = await fetch(server.url.href); - const stream = Readable.fromWeb(res.body); - let chunks = []; + const stream = Readable.fromWeb(res.body!); + let chunks: any[] = []; for await (const chunk of stream) { total += chunk.length; chunks.push(chunk); diff --git a/test/regression/issue/09559.test.ts b/test/regression/issue/09559.test.ts index 3399ec3022..f462e6845e 100644 --- a/test/regression/issue/09559.test.ts +++ b/test/regression/issue/09559.test.ts @@ -12,7 +12,6 @@ test("bun build --target bun should support non-ascii source", async () => { console.log(JSON.stringify({\u{6211}})); `, }; - const filenames = Object.keys(files); const source = tempDirWithFiles("source", files); $.throws(true); diff --git a/test/regression/issue/10132.test.ts b/test/regression/issue/10132.test.ts index 77a3018a1d..f4bf2790a1 100644 --- a/test/regression/issue/10132.test.ts +++ b/test/regression/issue/10132.test.ts @@ -50,14 +50,14 @@ echo My name is bun-hello2 } }); -test("issue #10132, bun run sets cwd", async () => { +test("bun run sets cwd for script, matching npm", async () => { $.cwd(dir); const currentPwd = (await $`${bunExe()} run get-pwd`.text()).trim(); expect(currentPwd).toBe(dir); const currentPwd2 = join(currentPwd, "subdir", "one"); $.cwd(currentPwd2); - expect((await $`${bunExe()} run get-pwd`.text()).trim()).toBe(currentPwd2); + expect((await $`${bunExe()} run get-pwd`.text()).trim()).toBe(dir); $.cwd(process.cwd()); }); diff --git a/test/regression/issue/11806.test.ts b/test/regression/issue/11806.test.ts new file mode 100644 index 0000000000..6a31b5144a --- /dev/null +++ b/test/regression/issue/11806.test.ts @@ -0,0 +1,38 @@ +import { test, expect } from "bun:test"; +import { bunExe, tempDirWithFiles } from "harness"; + +test("11806", () => { + const dir = tempDirWithFiles("11806", { + "package.json": JSON.stringify({ + "name": "project", + "workspaces": ["apps/*"], + }), + "apps": { + "api": { + "package.json": JSON.stringify({ + "name": "api", + "jest": { + "testRegex": ".*\\.spec\\.ts$", + }, + "devDependencies": { + "typescript": "^5.7.3", + }, + }), + }, + }, + }); + + const result1 = Bun.spawnSync({ + cmd: [bunExe(), "install"], + stdio: ["inherit", "inherit", "inherit"], + cwd: dir + "/apps/api", + }); + expect(result1.exitCode).toBe(0); + + const result2 = Bun.spawnSync({ + cmd: [bunExe(), "add", "--dev", "typescript"], + stdio: ["inherit", "inherit", "inherit"], + cwd: dir + "/apps/api", + }); + expect(result2.exitCode).toBe(0); +}); diff --git a/test/regression/issue/14976/14976.test.ts b/test/regression/issue/14976/14976.test.ts index 37e7c72df0..804eb7cc20 100644 --- a/test/regression/issue/14976/14976.test.ts +++ b/test/regression/issue/14976/14976.test.ts @@ -39,6 +39,7 @@ test("bun build --target=bun outputs only ascii", async () => { const build_result = await Bun.build({ entrypoints: [import.meta.dirname + "/import_target.ts"], target: "bun", + throw: true, }); expect(build_result.success).toBe(true); expect(build_result.outputs.length).toBe(1); diff --git a/test/regression/issue/16007.test.ts b/test/regression/issue/16007.test.ts new file mode 100644 index 0000000000..fa09d4fedc --- /dev/null +++ b/test/regression/issue/16007.test.ts @@ -0,0 +1,12 @@ +import { it, expect } from "bun:test"; + +it("Set is propperly formatted in Bun.inspect()", () => { + const set = new Set(["foo", "bar"]); + const formatted = Bun.inspect({ set }); + expect(formatted).toBe(`{ + set: Set(2) { + "foo", + "bar", + }, +}`); +}); diff --git a/test/regression/issue/16312.test.ts b/test/regression/issue/16312.test.ts new file mode 100644 index 0000000000..15b9019c0d --- /dev/null +++ b/test/regression/issue/16312.test.ts @@ -0,0 +1,13 @@ +import { test, afterEach, expect } from "bun:test"; +import { cleanup } from "@testing-library/react"; +import * as matchers from "@testing-library/jest-dom/matchers"; + +expect.extend(matchers); +afterEach(() => { + cleanup(); +}); + +test("expect extended", () => { + // @ts-ignore + expect(expect.toBeInTheDocument).not.toBe(undefined); +}); diff --git a/test/regression/issue/ctrl-c.test.ts b/test/regression/issue/ctrl-c.test.ts index 6c336a2444..232e9f17d2 100644 --- a/test/regression/issue/ctrl-c.test.ts +++ b/test/regression/issue/ctrl-c.test.ts @@ -1,5 +1,50 @@ -import { expect, test } from "bun:test"; +import { expect, it, test } from "bun:test"; import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; +import { join } from "path"; + +test.skipIf(isWindows)("verify that we can call sigint 4096 times", () => { + const dir = tempDirWithFiles("ctrlc", { + "index.js": /*js*/ ` + let count = 0; + process.exitCode = 1; + + const handler = () => { + count++; + console.count("SIGINT"); + if (count === 1024 * 4) { + process.off("SIGINT", handler); + process.exitCode = 0; + clearTimeout(timer); + } + }; + process.on("SIGINT", handler); + var timer = setTimeout(() => {}, 999999); + var remaining = 1024 * 4; + + function go() { + for (var i = 0, end = Math.min(1024, remaining); i < end; i++) { + process.kill(process.pid, "SIGINT"); + } + remaining -= i; + + if (remaining > 0) { + setTimeout(go, 10); + } + } + go(); + `, + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), join(dir, "index.js")], + cwd: dir, + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + }); + expect(result.exitCode).toBe(0); + expect(result.signalCode).toBeUndefined(); +}); test.skipIf(isWindows)("verify that we forward SIGINT from parent to child in bun run", () => { const dir = tempDirWithFiles("ctrlc", { @@ -16,12 +61,12 @@ test.skipIf(isWindows)("verify that we forward SIGINT from parent to child in bu { "name": "ctrlc", "scripts": { - "start": "${bunExe()} index.js" + "start": " ${bunExe()} index.js" } } `, }); - + console.log(dir); const result = Bun.spawnSync({ cmd: [bunExe(), "start"], cwd: dir, @@ -32,3 +77,76 @@ test.skipIf(isWindows)("verify that we forward SIGINT from parent to child in bu expect(result.exitCode).toBe(null); expect(result.signalCode).toBe("SIGKILL"); }); + +for (const mode of [ + ["vite"], + ["dev"], + ...(isWindows ? [] : [["./node_modules/.bin/vite"]]), + ["--bun", "vite"], + ["--bun", "dev"], + ...(isWindows ? [] : [["--bun", "./node_modules/.bin/vite"]]), +]) { + it("kills on SIGINT in: 'bun " + mode.join(" ") + "'", async () => { + const dir = tempDirWithFiles("ctrlc", { + "package.json": JSON.stringify({ + name: "ctrlc", + scripts: { + "dev": "vite", + }, + devDependencies: { + "vite": "^6.0.1", + }, + }), + }); + expect( + Bun.spawnSync({ + cmd: [bunExe(), "install"], + cwd: dir, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }).exitCode, + ).toBe(0); + const proc = Bun.spawn({ + cmd: [bunExe(), ...mode], + cwd: dir, + stdin: "inherit", + stdout: "pipe", + stderr: "inherit", + env: { ...bunEnv, PORT: "9876" }, + }); + + // wait for vite to start + const reader = proc.stdout.getReader(); + await reader.read(); // wait for first bit of stdout + reader.releaseLock(); + + expect(proc.killed).toBe(false); + + // send sigint + process.kill(proc.pid, "SIGINT"); + + // wait for exit or 200ms + await Promise.race([proc.exited, Bun.sleep(200)]); + + // wait to allow a moment to be killed + await Bun.sleep(100); // wait for kill + expect({ + killed: proc.killed, + exitCode: proc.exitCode, + signalCode: proc.signalCode, + }).toEqual( + isWindows + ? { + killed: true, + exitCode: 1, + signalCode: null, + } + : { + killed: true, + exitCode: null, + signalCode: "SIGINT", + }, + ); + }); +} diff --git a/test/snippets/react-context-value-func.tsx b/test/snippets/react-context-value-func.tsx index 800ad428d7..c693717466 100644 --- a/test/snippets/react-context-value-func.tsx +++ b/test/snippets/react-context-value-func.tsx @@ -10,7 +10,7 @@ const ContextProvider = ({ children }) => { return {children(foo)}; }; -const ContextValue = ({}) => ( +const ContextValue = () => ( {foo => { if (foo) { diff --git a/test/tsconfig.json b/test/tsconfig.json index cc96fd4e47..c43516cf9a 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,24 +1,7 @@ { - "include": [".", "../packages/bun-types/index.d.ts", "./testing-internals.d.ts"], + "extends": "../tsconfig.base.json", "compilerOptions": { - "lib": ["ESNext"], - "module": "ESNext", - "target": "ESNext", - "moduleResolution": "bundler", - "moduleDetection": "force", - "allowImportingTsExtensions": true, - "experimentalDecorators": true, - "noEmit": true, - "composite": true, - "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "preserve", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - "resolveJsonModule": true, - "noImplicitThis": false, + // Path remapping "baseUrl": ".", "paths": { "harness": ["harness.ts"], @@ -29,8 +12,23 @@ "foo/bar": ["js/bun/resolve/baz.js"], "@faasjs/*": ["js/bun/resolve/*.js", "js/bun/resolve/*/src/index.js"], "@faasjs/larger/*": ["js/bun/resolve/*/larger-index.js"] - } + }, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, - - "exclude": ["bundler/fixtures", "snapshots", "js/deno"] + "include": [ + // + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.cts", + "../src/js/internal-for-testing.ts" + ], + "exclude": [ + "fixtures", + "__snapshots__", // bun snapshots (toMatchSnapshot) + "./snapshots", + "./js/deno", + "./node.js" // entire node.js upstream repository + ] } diff --git a/test/v8/v8.test.ts b/test/v8/v8.test.ts index ad7b2b1a3d..d2a3a468ba 100644 --- a/test/v8/v8.test.ts +++ b/test/v8/v8.test.ts @@ -1,6 +1,6 @@ import { spawn, spawnSync } from "bun"; import { beforeAll, describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, tmpdirSync, isWindows } from "harness"; +import { bunEnv, bunExe, tmpdirSync, isWindows, isMusl, isBroken, nodeExe } from "harness"; import assert from "node:assert"; import fs from "node:fs/promises"; import { join, basename } from "path"; @@ -38,7 +38,7 @@ const directories = { }; async function install(srcDir: string, tmpDir: string, runtime: Runtime): Promise { - await fs.cp(srcDir, tmpDir, { recursive: true }); + await fs.cp(srcDir, tmpDir, { recursive: true, force: true }); const install = spawn({ cmd: [bunExe(), "install", "--ignore-scripts"], cwd: tmpDir, @@ -47,9 +47,9 @@ async function install(srcDir: string, tmpDir: string, runtime: Runtime): Promis stdout: "inherit", stderr: "inherit", }); - await install.exited; - if (install.exitCode != 0) { - throw new Error("build failed"); + const exitCode = await install.exited; + if (exitCode !== 0) { + throw new Error(`install failed: ${exitCode}`); } } @@ -63,20 +63,24 @@ async function build( cmd: runtime == Runtime.bun ? [bunExe(), "x", "--bun", "node-gyp", "rebuild", buildMode == BuildMode.debug ? "--debug" : "--release"] - : ["npx", "node-gyp", "rebuild", "--release"], // for node.js we don't bother with debug mode + : [bunExe(), "x", "node-gyp", "rebuild", "--release"], // for node.js we don't bother with debug mode cwd: tmpDir, env: bunEnv, stdin: "inherit", stdout: "pipe", stderr: "pipe", }); - await build.exited; - const out = await new Response(build.stdout).text(); - const err = await new Response(build.stderr).text(); - if (build.exitCode != 0) { + const [exitCode, out, err] = await Promise.all([ + build.exited, + new Response(build.stdout).text(), + new Response(build.stderr).text(), + ]); + if (exitCode !== 0) { console.error(err); - throw new Error("build failed"); + console.log(out); + throw new Error(`build failed: ${exitCode}`); } + return { out, err, @@ -84,171 +88,185 @@ async function build( }; } -beforeAll(async () => { - // set up clean directories for our 4 builds - directories.bunRelease = tmpdirSync(); - directories.bunDebug = tmpdirSync(); - directories.node = tmpdirSync(); - directories.badModules = tmpdirSync(); +describe.todoIf(isBroken && isMusl)("node:v8", () => { + beforeAll(async () => { + // set up clean directories for our 4 builds + directories.bunRelease = tmpdirSync(); + directories.bunDebug = tmpdirSync(); + directories.node = tmpdirSync(); + directories.badModules = tmpdirSync(); - await install(srcDir, directories.bunRelease, Runtime.bun); - await install(srcDir, directories.bunDebug, Runtime.bun); - await install(srcDir, directories.node, Runtime.node); - await install(join(__dirname, "bad-modules"), directories.badModules, Runtime.node); + await install(srcDir, directories.bunRelease, Runtime.bun); + await install(srcDir, directories.bunDebug, Runtime.bun); + await install(srcDir, directories.node, Runtime.node); + await install(join(__dirname, "bad-modules"), directories.badModules, Runtime.node); - const results = await Promise.all([ - build(srcDir, directories.bunRelease, Runtime.bun, BuildMode.release), - build(srcDir, directories.bunDebug, Runtime.bun, BuildMode.debug), - build(srcDir, directories.node, Runtime.node, BuildMode.release), - build(join(__dirname, "bad-modules"), directories.badModules, Runtime.node, BuildMode.release), - ]); - for (const r of results) { - console.log(r.description, "stdout:"); - console.log(r.out); - console.log(r.description, "stderr:"); - console.log(r.err); - } -}); + const results = await Promise.all([ + build(srcDir, directories.bunRelease, Runtime.bun, BuildMode.release), + build(srcDir, directories.bunDebug, Runtime.bun, BuildMode.debug), + build(srcDir, directories.node, Runtime.node, BuildMode.release), + build(join(__dirname, "bad-modules"), directories.badModules, Runtime.node, BuildMode.release), + ]); + for (const r of results) { + console.log(r.description, "stdout:"); + console.log(r.out); + console.log(r.description, "stderr:"); + console.log(r.err); + } + }); -describe("module lifecycle", () => { - it("can call a basic native function", () => { - checkSameOutput("test_v8_native_call", []); + describe("module lifecycle", () => { + it("can call a basic native function", async () => { + await checkSameOutput("test_v8_native_call", []); + }); }); -}); -describe("primitives", () => { - it("can create and distinguish between null, undefined, true, and false", () => { - checkSameOutput("test_v8_primitives", []); + describe("primitives", () => { + it("can create and distinguish between null, undefined, true, and false", async () => { + await checkSameOutput("test_v8_primitives", []); + }); }); -}); -describe("Number", () => { - it("can create small integer", () => { - checkSameOutput("test_v8_number_int", []); + describe("Number", () => { + it("can create small integer", async () => { + await checkSameOutput("test_v8_number_int", []); + }); + // non-i32 v8::Number is not implemented yet + it("can create large integer", async () => { + await checkSameOutput("test_v8_number_large_int", []); + }); + it("can create fraction", async () => { + await checkSameOutput("test_v8_number_fraction", []); + }); }); - // non-i32 v8::Number is not implemented yet - it("can create large integer", () => { - checkSameOutput("test_v8_number_large_int", []); - }); - it("can create fraction", () => { - checkSameOutput("test_v8_number_fraction", []); - }); -}); -describe("String", () => { - it("can create and read back strings with only ASCII characters", () => { - checkSameOutput("test_v8_string_ascii", []); + describe("String", () => { + it("can create and read back strings with only ASCII characters", async () => { + await checkSameOutput("test_v8_string_ascii", []); + }); + // non-ASCII strings are not implemented yet + it("can create and read back strings with UTF-8 characters", async () => { + await checkSameOutput("test_v8_string_utf8", []); + }); + it("handles replacement correctly in strings with invalid UTF-8 sequences", async () => { + await checkSameOutput("test_v8_string_invalid_utf8", []); + }); + it("can create strings from null-terminated Latin-1 data", async () => { + await checkSameOutput("test_v8_string_latin1", []); + }); + describe("WriteUtf8", () => { + it("truncates the string correctly", async () => { + await checkSameOutput("test_v8_string_write_utf8", []); + }); + }); }); - // non-ASCII strings are not implemented yet - it("can create and read back strings with UTF-8 characters", () => { - checkSameOutput("test_v8_string_utf8", []); + + describe("External", () => { + it("can create an external and read back the correct value", async () => { + await checkSameOutput("test_v8_external", []); + }); }); - it("handles replacement correctly in strings with invalid UTF-8 sequences", () => { - checkSameOutput("test_v8_string_invalid_utf8", []); + + describe("Object", () => { + it("can create an object and set properties", async () => { + await checkSameOutput("test_v8_object", []); + }); }); - it("can create strings from null-terminated Latin-1 data", () => { - checkSameOutput("test_v8_string_latin1", []); + describe("Array", () => { + // v8::Array::New is broken as it still tries to reinterpret locals as JSValues + it.skip("can create an array from a C array of Locals", async () => { + await checkSameOutput("test_v8_array_new", []); + }); }); - describe("WriteUtf8", () => { - it("truncates the string correctly", () => { - checkSameOutput("test_v8_string_write_utf8", []); + + describe("ObjectTemplate", () => { + it("creates objects with internal fields", async () => { + await checkSameOutput("test_v8_object_template", []); + }); + }); + + describe("FunctionTemplate", () => { + it("keeps the data parameter alive", async () => { + await checkSameOutput("test_v8_function_template", []); + }); + }); + + describe("Function", () => { + it("correctly receives all its arguments from JS", async () => { + await checkSameOutput("print_values_from_js", [5.0, true, null, false, "async meow", {}]); + await checkSameOutput("print_native_function", []); + }); + + it("correctly receives the this value from JS", async () => { + await checkSameOutput("call_function_with_weird_this_values", []); + }); + }); + + describe("error handling", () => { + it("throws an error for modules built using the wrong ABI version", () => { + expect(() => require(join(directories.badModules, "build/Release/mismatched_abi_version.node"))).toThrow( + "The module 'mismatched_abi_version' was compiled against a different Node.js ABI version using NODE_MODULE_VERSION 42.", + ); + }); + + it("throws an error for modules with no entrypoint", () => { + expect(() => require(join(directories.badModules, "build/Release/no_entrypoint.node"))).toThrow( + "The module 'no_entrypoint' has no declared entry point.", + ); + }); + }); + + describe("Global", () => { + it("can create, modify, and read the value from global handles", async () => { + await checkSameOutput("test_v8_global", []); + }); + }); + + describe("HandleScope", () => { + it("can hold a lot of locals", async () => { + await checkSameOutput("test_many_v8_locals", []); + }); + it("keeps GC objects alive", async () => { + await checkSameOutput("test_handle_scope_gc", []); + }, 10000); + }); + + describe("EscapableHandleScope", () => { + it("keeps handles alive in the outer scope", async () => { + await checkSameOutput("test_v8_escapable_handle_scope", []); + }); + }); + + describe("uv_os_getpid", () => { + it.skipIf(isWindows)("returns the same result as getpid on POSIX", async () => { + await checkSameOutput("test_uv_os_getpid", []); + }); + }); + + describe("uv_os_getppid", () => { + it.skipIf(isWindows)("returns the same result as getppid on POSIX", async () => { + await checkSameOutput("test_uv_os_getppid", []); }); }); }); -describe("External", () => { - it("can create an external and read back the correct value", () => { - checkSameOutput("test_v8_external", []); - }); -}); - -describe("Object", () => { - it("can create an object and set properties", () => { - checkSameOutput("test_v8_object", []); - }); -}); -describe("Array", () => { - // v8::Array::New is broken as it still tries to reinterpret locals as JSValues - it.skip("can create an array from a C array of Locals", () => { - checkSameOutput("test_v8_array_new", []); - }); -}); - -describe("ObjectTemplate", () => { - it("creates objects with internal fields", () => { - checkSameOutput("test_v8_object_template", []); - }); -}); - -describe("FunctionTemplate", () => { - it("keeps the data parameter alive", () => { - checkSameOutput("test_v8_function_template", []); - }); -}); - -describe("Function", () => { - it("correctly receives all its arguments from JS", () => { - checkSameOutput("print_values_from_js", [5.0, true, null, false, "meow", {}]); - checkSameOutput("print_native_function", []); - }); - - it("correctly receives the this value from JS", () => { - checkSameOutput("call_function_with_weird_this_values", []); - }); -}); - -describe("error handling", () => { - it("throws an error for modules built using the wrong ABI version", () => { - expect(() => require(join(directories.badModules, "build/Release/mismatched_abi_version.node"))).toThrow( - "The module 'mismatched_abi_version' was compiled against a different Node.js ABI version using NODE_MODULE_VERSION 42.", - ); - }); - - it("throws an error for modules with no entrypoint", () => { - expect(() => require(join(directories.badModules, "build/Release/no_entrypoint.node"))).toThrow( - "The module 'no_entrypoint' has no declared entry point.", - ); - }); -}); - -describe("Global", () => { - it("can create, modify, and read the value from global handles", () => { - checkSameOutput("test_v8_global", []); - }); -}); - -describe("HandleScope", () => { - it("can hold a lot of locals", () => { - checkSameOutput("test_many_v8_locals", []); - }); - it("keeps GC objects alive", () => { - checkSameOutput("test_handle_scope_gc", []); - }, 10000); -}); - -describe("EscapableHandleScope", () => { - it("keeps handles alive in the outer scope", () => { - checkSameOutput("test_v8_escapable_handle_scope", []); - }); -}); - -describe("uv_os_getpid", () => { - it.skipIf(isWindows)("returns the same result as getpid on POSIX", () => { - checkSameOutput("test_uv_os_getpid", []); - }); -}); - -describe("uv_os_getppid", () => { - it.skipIf(isWindows)("returns the same result as getppid on POSIX", () => { - checkSameOutput("test_uv_os_getppid", []); - }); -}); - -function checkSameOutput(testName: string, args: any[], thisValue?: any) { - const nodeResult = runOn(Runtime.node, BuildMode.release, testName, args, thisValue).trim(); - let bunReleaseResult = runOn(Runtime.bun, BuildMode.release, testName, args, thisValue); - let bunDebugResult = runOn(Runtime.bun, BuildMode.debug, testName, args, thisValue); - +async function checkSameOutput(testName: string, args: any[], thisValue?: any) { + const [nodeResultResolution, bunReleaseResultResolution, bunDebugResultResolution] = await Promise.allSettled([ + runOn(Runtime.node, BuildMode.release, testName, args, thisValue), + runOn(Runtime.bun, BuildMode.release, testName, args, thisValue), + runOn(Runtime.bun, BuildMode.debug, testName, args, thisValue), + ]); + const errors = [nodeResultResolution, bunReleaseResultResolution, bunDebugResultResolution] + .filter(r => r.status === "rejected") + .map(r => r.reason); + if (errors.length > 0) { + throw new AggregateError(errors); + } + let [nodeResult, bunReleaseResult, bunDebugResult] = [ + nodeResultResolution, + bunReleaseResultResolution, + bunDebugResultResolution, + ].map(r => (r as any).value); // remove all debug logs bunReleaseResult = bunReleaseResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); bunDebugResult = bunDebugResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); @@ -260,7 +278,7 @@ function checkSameOutput(testName: string, args: any[], thisValue?: any) { return nodeResult; } -function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: any[], thisValue?: any) { +async function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: any[], thisValue?: any) { if (runtime == Runtime.node) { assert(buildMode == BuildMode.release); } @@ -270,7 +288,7 @@ function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: : buildMode == BuildMode.debug ? directories.bunDebug : directories.bunRelease; - const exe = runtime == Runtime.node ? "node" : bunExe(); + const exe = runtime == Runtime.node ? (nodeExe() ?? "node") : bunExe(); const cmd = [ exe, @@ -284,16 +302,21 @@ function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: cmd.push("debug"); } - const exec = spawnSync({ + const proc = spawn({ cmd, cwd: baseDir, env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], }); - const errs = exec.stderr.toString(); + const [exitCode, out, err] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); const crashMsg = `test ${testName} crashed under ${Runtime[runtime]} in ${BuildMode[buildMode]} mode`; - if (errs !== "") { - throw new Error(`${crashMsg}: ${errs}`); + if (exitCode !== 0) { + throw new Error(`${crashMsg}: ${err}\n${out}`.trim()); } - expect(exec.success, crashMsg).toBeTrue(); - return exec.stdout.toString(); + expect(exitCode, crashMsg).toBe(0); + return out.trim(); } diff --git a/tsconfig.base.json b/tsconfig.base.json index d186c359de..a28d20e3fa 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,19 +1,38 @@ { "compilerOptions": { - "lib": ["ESNext"], - "module": "esnext", - "target": "esnext", - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "noEmit": true, - "strict": true, - "noImplicitAny": false, - "allowJs": true, - "downlevelIteration": true, - "esModuleInterop": true, - "skipLibCheck": true, - "jsx": "react-jsx", + "composite": true, - "typeRoots": ["./packages"] + // Enable latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "resolveJsonModule": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + // TODO: enable this + // "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + + // Stricter type-checking + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "noImplicitAny": false, + "noImplicitThis": false, + + // Enable decorators + "experimentalDecorators": true, + "emitDecoratorMetadata": true } } diff --git a/tsconfig.json b/tsconfig.json index e1e4627658..c3ec51e2a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,18 @@ { - "extends": "./tsconfig.base.json", + "files": [], + "include": [], "compilerOptions": { + "noEmit": true, + "skipLibCheck": true, "experimentalDecorators": true, - "emitDecoratorMetadata": true, - // "skipLibCheck": true, - "allowJs": true + "emitDecoratorMetadata": true }, - "include": [".", "packages/bun-types/index.d.ts"], - "exclude": [ - "src/test", - "src/js/out", - // "src/js/builtins", - "packages", - "bench", - "examples/*/*", - "build", - ".zig-cache", - "test", - "vendor", - "bun-webkit", - "src/api/demo", - "node_modules" - ], - "files": ["src/js/builtins.d.ts"] + "references": [ + // + { "path": "./src" }, + { "path": "./src/bake" }, + { "path": "./src/js" }, + { "path": "./test" }, + { "path": "./packages/bun-types" } + ] }